dnfs2: 移除样式节点 persist 全局保持机制,样式仅节点作用域生效

This commit is contained in:
qing committed 2026-08-10 11:52:15 +00:00
1 parent 75a45313c4
commit 77bc367d03
4 files changed
+16 -61

No files matched your search

-4
View File
@@ -274,10 +274,6 @@ static json::value parse_sp(const std::string& content) {
static void validate_sp(const json::object& sp) {
std::string t = std::string(sp.at("_sp").as_string());
// persist(style/theme 的生命周期标记)可选,必须是布尔值
if (sp.contains("persist") && !sp.at("persist").is_bool())
throw DnfsExp("persist 必须是布尔值", 400);
if (t == "js") {
if (!sp.contains("code") || !sp.at("code").is_string())
throw DnfsExp("前端代码不能为空", 400);
+6 -10
View File
@@ -18,8 +18,7 @@
1. 登录后打开「树状笔记」,进入自己的目标节点(样式节点发布在**谁拥有**的节点下,权限见下)
2. 在「节点操作」表单中,将**节点类型**选为 `主题变量` 或 `界面样式`
3. 在「节点内容」中填写变量或 CSS(格式见后文)
4. 可选:勾选「离开本节点后仍保持生效」(`persist`)
5. 提交——内容经 AI 审核通过后即时生效
4. 提交——内容经 AI 审核通过后即时生效
### 发布权限
@@ -30,13 +29,10 @@
### 生命周期
| 模式 | 生效范围 | 移除时机 |
|---|---|---|
| 默认(节点作用域) | 进入该节点所在层时生效 | 切换/返回节点时自动还原 |
| `persist`(勾选保持) | 一旦访问即全局保持,跨节点生效 | 用户面板点「还原界面样式」,或停用对应节点 |
样式节点为**节点作用域**:进入该节点所在层时注入生效,切换/返回节点时自动移除,界面还原。
- 每个样式节点卡片上有「停用样式/启用样式」与「审计源码」按钮,可逐个控制
- 用户面板的「**还原界面样式**」一键清除全部注入样式(含 persist),本会话内 persist 样式不再自动注入(刷新页面恢复)
- 停用仅对当前浏览者本次生效,不影响其他访客
---
@@ -229,10 +225,10 @@ a:hover { text-shadow: 0 0 8px var(--accent); }
样式节点经 AI 审核,审核未通过会提示「内容审核不通过」;通过后刷新页面生效。非节点拥有者在他人节点下发布会被 403 拒绝。
**界面被改乱了怎么办?**
左上角用户面板 →「还原界面样式」,立即恢复默认。或在对应卡片上点「停用样式」。
离开该节点所在层即自动还原;或在对应卡片上点「停用样式」。
**想让全站访客都看到主题?**
发布时勾选「离开本节点后仍保持生效」(persist),并建议放在高层级节点中提高被访问到的概率。
**样式只在特定层生效吗?**
是。样式仅在访问到该节点所在层时生效,离开即还原;想让更多访客看到,可把节点放在高层级。
**样式节点之间冲突?**
多个样式节点同时生效时按 DOM 注入顺序叠加,同特异性后者优先;可用卡片上的启停按钮逐个排查。
-1
View File
@@ -38,7 +38,6 @@
</select>
<input type="file" id="sp-file" style="display:none">
<span id="sp-file-tip"></span>
<label id="sp-persist-wrap" style="display:none"><input type="checkbox" id="sp-persist"> 离开本节点后仍保持生效</label>
</div>
<label>节点内容</label><textarea id="input-note-content"></textarea>
<div class="form-actions">
+10 -46
View File
@@ -481,15 +481,9 @@ function renderContent(container, item) {
var sp_effects_disabled = (function() {
try { return localStorage.getItem('sp_effects_disabled') === '1'; } catch (e) { return false; }
})();
/* 清除当前页面已运行的全部特效沙箱(进入新节点 / 关闭特效时调用) */
/* 清除当前页面已运行的全部特效沙箱与节点作用域注入样式(进入新节点 / 关闭特效时调用)。
* persist 注入样式(data-persist="1")跨节点保留,由用户面板"还原界面样式"统一清除。 */
/* 清除当前页面已运行的全部特效沙箱与注入样式(进入新节点 / 关闭特效时调用) */
function clearAllEffects() {
document.querySelectorAll('.sp-effect-frame').forEach(function(f) { f.remove(); });
document.querySelectorAll('.sp-injected-style:not([data-persist="1"])').forEach(function(s) { s.remove(); });
}
/* 还原界面样式:清除 style/theme 节点的全部注入样式(含 persist),不清特效沙箱 */
function clearAllStyles() {
document.querySelectorAll('.sp-injected-style').forEach(function(s) { s.remove(); });
}
function renderSpJs(container, sp, appendNote) {
@@ -594,14 +588,11 @@ function renderSpFile(container, sp, appendNote) {
}
/* ===== 样式类节点(style/theme):CSS 注入 <style> 控制整个界面 =====
* 生命周期:
* - 节点作用域(默认):.sp-injected-style 无 data-persist 标记,随 load_notes 的
* clearAllEffects 移除,离开节点界面自动还原;
* - persist:true:data-persist="1" 标记,跨节点保留,由用户面板"还原界面样式"统一清除。
* 安全:节点拥有者或管理员可发布(后端强制)且经 AI 送审;CSS 无法执行脚本,风险面仅为界面外观,
* 可随时由用户一键还原。theme 是 style 的克制子集——仅允许覆盖 CSS 变量(:root 块),
* 生命周期:节点作用域——进入该节点所在层时注入生效,切换/返回节点时随
* clearAllEffects 移除,界面自动还原;卡片上可逐个停用/启用。
* 安全:节点拥有者或管理员可发布(后端强制)且经 AI 送审;CSS 无法执行脚本,风险面仅为界面外观。
* theme 是 style 的克制子集——仅允许覆盖 CSS 变量(:root 块),
* 变量名/值合法性由后端 validate_sp 强制,前端这里只做兜底过滤。 */
var sp_style_reset = false; /* 用户手动还原后,本会话不再自动注入 persist 样式(仍可在卡片手动启用) */
function themeVarsToCss(vars) {
var lines = [':root {'];
@@ -633,7 +624,6 @@ function renderSpCss(container, sp, appendNote, item) {
var kindLabel = is_theme ? '主题' : '样式';
var cssText = is_theme ? themeVarsToCss(sp.vars || {}) : String(sp.css || '');
var displayText = is_theme ? themeVarsToText(sp.vars || {}) : String(sp.css || '');
var persist = (sp.persist === true);
var noteId = (item && item.id != null) ? String(item.id) : null;
var bar = document.createElement('div');
@@ -668,13 +658,12 @@ function renderSpCss(container, sp, appendNote, item) {
}
styleEl = document.createElement('style');
styleEl.className = 'sp-injected-style';
if (persist) styleEl.setAttribute('data-persist', '1');
if (noteId) styleEl.setAttribute('data-note-id', noteId);
styleEl.textContent = cssText;
document.head.appendChild(styleEl);
toggle_btn.textContent = '停用' + kindLabel;
out.style.display = 'block';
out.textContent = persist ? kindLabel + '已生效(全局保持)' : kindLabel + '已生效(本层)';
out.textContent = kindLabel + '已生效(本层)';
}
function stopCss() {
if (styleEl) { styleEl.remove(); styleEl = null; }
@@ -691,8 +680,8 @@ function renderSpCss(container, sp, appendNote, item) {
container.appendChild(pre);
container.appendChild(out);
appendNote();
/* 渲染即生效;persist 样式在用户手动还原后(本会话)不再自动注入 */
if (!(persist && sp_style_reset)) applyCss();
/* 渲染即生效 */
applyCss();
}
function formatBytes(n) {
@@ -739,8 +728,6 @@ var sp_panel = document.getElementById('sp-panel');
var sp_type = document.getElementById('sp-type');
var sp_file = document.getElementById('sp-file');
var sp_file_tip = document.getElementById('sp-file-tip');
var sp_persist_wrap = document.getElementById('sp-persist-wrap');
var sp_persist = document.getElementById('sp-persist');
var sp_edit_original = null; /* 编辑文件节点时的原信封字符串:未选新文件则原样提交 */
/* 搜索框:当前层卡片标题/内容关键词过滤 */
var searchtext = document.getElementById('searchtext');
@@ -773,10 +760,6 @@ sp_type.addEventListener('change', function() {
var is_file = (t === 'file');
sp_file.style.display = is_file ? '' : 'none';
input_note_content.style.display = is_file ? 'none' : '';
/* persist 选项仅样式类节点(style/theme)可见,切走时复位勾选 */
var is_style = (t === 'style' || t === 'theme');
sp_persist_wrap.style.display = is_style ? '' : 'none';
if (!is_style) sp_persist.checked = false;
sp_file_tip.textContent = '';
sp_file_tip.className = is_file ? 'tip-yellow' : '';
if (!is_file) {
@@ -832,16 +815,12 @@ async function buildSubmitContent() {
if (t === 'style') {
var css = input_note_content.value;
if (!css.trim()) { alert('请输入 CSS 样式'); return null; }
var style_env = { _sp: 'style', css: css };
if (sp_persist.checked) style_env.persist = true;
return JSON.stringify(style_env);
return JSON.stringify({ _sp: 'style', css: css });
}
if (t === 'theme') {
var vars = parseThemeVars(input_note_content.value);
if (!vars) return null;
var theme_env = { _sp: 'theme', vars: vars };
if (sp_persist.checked) theme_env.persist = true;
return JSON.stringify(theme_env);
return JSON.stringify({ _sp: 'theme', vars: vars });
}
/* file:优先使用新选择的文件;编辑模式下未选新文件则保留原信封 */
if (sp_file.files.length === 0) {
@@ -921,12 +900,10 @@ function startEdit(item) {
sp_type.value = 'style';
sp_type.dispatchEvent(new Event('change'));
input_note_content.value = sp.css || '';
sp_persist.checked = (sp.persist === true);
} else if (sp && sp._sp === 'theme') {
sp_type.value = 'theme';
sp_type.dispatchEvent(new Event('change'));
input_note_content.value = themeVarsToText(sp.vars || {});
sp_persist.checked = (sp.persist === true);
} else if (sp) {
sp_type.value = sp._sp;
sp_type.dispatchEvent(new Event('change'));
@@ -950,7 +927,6 @@ function cancelEdit() {
sp_edit_original = null;
sp_file.value = '';
sp_file_tip.textContent = '';
sp_persist.checked = false;
if (sp_type.value !== 'text') {
sp_type.value = 'text';
sp_type.dispatchEvent(new Event('change'));
@@ -1269,18 +1245,6 @@ function renderUserPanel() {
});
userpanel.appendChild(effect_btn);
/* 还原界面样式:清除 style/theme 节点的全部注入样式(含 persist),并重渲染当前层——
* persist 卡片不再自动注入(sp_style_reset 会话级抑制),需要时可在卡片上手动启用 */
let style_reset_btn = document.createElement("div");
style_reset_btn.className = "tag";
style_reset_btn.textContent = "还原界面样式";
style_reset_btn.addEventListener("click", () => {
sp_style_reset = true;
clearAllStyles();
renderCards(current_children);
});
userpanel.appendChild(style_reset_btn);
/* 管理员专属入口:运维机器人 / 进程管理器 */
if (current_user.is_admin) {
let ops_btn = document.createElement("div");