完善节点

This commit is contained in:
wjluoxiao
2026-11-10 07:09:25 +08:00
parent 9ebd3afe94
commit cd392e7268
67 changed files with 572 additions and 482 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

+1 -1
View File
@@ -32,7 +32,7 @@ app.registerExtension({
const el = wDur.inputEl || wDur.element;
if (el) {
el.readOnly = true;
el.style.backgroundColor = "#1a1a1a";
el.style.backgroundColor = "var(--xb-bg)";
el.style.color = "#00E676";
el.style.textAlign = "center";
el.style.fontWeight = "bold";
+3 -3
View File
@@ -31,7 +31,7 @@ app.registerExtension({
const el = wDur.inputEl || wDur.element;
if (el) {
el.readOnly = true;
el.style.cssText = "background-color:#1a1a1a;color:#00E676;text-align:center;font-weight:bold;font-size:15px;min-width:120px;white-space:nowrap;overflow:hidden;";
el.style.cssText = "background-color:var(--xb-bg);color:#00E676;text-align:center;font-weight:bold;font-size:15px;min-width:120px;white-space:nowrap;overflow:hidden;";
}
}, 100);
}
@@ -56,14 +56,14 @@ app.registerExtension({
}
const ctr = document.createElement("div");
Object.assign(ctr.style, { display:"flex", flexDirection:"column", gap:"6px", width:"100%", padding:"6px", boxSizing:"border-box", background:"#161616", borderRadius:"6px", color:"#ccc", fontFamily:"sans-serif", marginTop:"4px" });
Object.assign(ctr.style, { display:"flex", flexDirection:"column", gap:"6px", width:"100%", padding:"6px", boxSizing:"border-box", background:"#161616", borderRadius:"6px", color:"var(--xb-text)", fontFamily:"sans-serif", marginTop:"4px" });
const audioEl = document.createElement("audio");
audioEl.controls = true; audioEl.style.cssText = "width:100%;height:34px;outline:none;";
ctr.appendChild(audioEl);
const canvas = document.createElement("canvas");
canvas.style.cssText = "width:100%;height:40px;border-radius:4px;background:#111;cursor:pointer;display:block;";
canvas.style.cssText = "width:100%;height:40px;border-radius:4px;background:var(--xb-inset);cursor:pointer;display:block;";
const dpr = window.devicePixelRatio || 1;
canvas.width = 1200 * dpr; canvas.height = 80 * dpr;
ctr.appendChild(canvas);
+4 -4
View File
@@ -30,7 +30,7 @@ app.registerExtension({
// Nodes 2.0:显示型字段走 options.read_only(Vue 部件没有 element 可改)
if (wTot && wTot.options) wTot.options.read_only = true;
if (wTot) setTimeout(() => { const el = wTot.inputEl || wTot.element; if (el) { el.readOnly = true; el.style.cssText = "background-color:#1a1a1a;color:#00E676;text-align:center;font-weight:bold;font-size:14px;min-width:120px;white-space:nowrap;overflow:hidden;"; } }, 100);
if (wTot) setTimeout(() => { const el = wTot.inputEl || wTot.element; if (el) { el.readOnly = true; el.style.cssText = "background-color:var(--xb-bg);color:#00E676;text-align:center;font-weight:bold;font-size:14px;min-width:120px;white-space:nowrap;overflow:hidden;"; } }, 100);
const audioEl1 = document.createElement("audio"); audioEl1.controls = true; audioEl1.style.cssText = "width:100%;height:30px;outline:none;";
const audioEl2 = document.createElement("audio"); audioEl2.controls = true; audioEl2.style.cssText = "width:100%;height:30px;outline:none;";
@@ -39,7 +39,7 @@ app.registerExtension({
let drags = { 1: null, 2: null }; // 存储双轨的拖动状态
const ctr = document.createElement("div");
Object.assign(ctr.style, { display:"flex", flexDirection:"column", gap:"4px", width:"100%", padding:"6px", boxSizing:"border-box", background:"#161616", borderRadius:"6px", color:"#ccc", fontFamily:"sans-serif", marginTop:"4px" });
Object.assign(ctr.style, { display:"flex", flexDirection:"column", gap:"4px", width:"100%", padding:"6px", boxSizing:"border-box", background:"#161616", borderRadius:"6px", color:"var(--xb-text)", fontFamily:"sans-serif", marginTop:"4px" });
const mkUpload = (w, label) => {
const fi = document.createElement("input"); fi.type = "file"; fi.accept = "audio/*,video/*"; fi.style.display = "none";
@@ -59,12 +59,12 @@ app.registerExtension({
const dpr = window.devicePixelRatio || 1;
const label1 = document.createElement("div"); label1.textContent = "🎵 音频1"; label1.style.cssText = "font-size:11px;color:#4FC3F7;font-weight:bold;";
ctr.appendChild(label1); ctr.appendChild(audioEl1);
const canvas1 = document.createElement("canvas"); canvas1.style.cssText = "width:100%;height:36px;border-radius:3px;background:#111;cursor:pointer;display:block;";
const canvas1 = document.createElement("canvas"); canvas1.style.cssText = "width:100%;height:36px;border-radius:3px;background:var(--xb-inset);cursor:pointer;display:block;";
canvas1.width = 1200 * dpr; canvas1.height = 72 * dpr; ctr.appendChild(canvas1);
const label2 = document.createElement("div"); label2.textContent = "🎵 音频2"; label2.style.cssText = "font-size:11px;color:#F44336;font-weight:bold;";
ctr.appendChild(label2); ctr.appendChild(audioEl2);
const canvas2 = document.createElement("canvas"); canvas2.style.cssText = "width:100%;height:36px;border-radius:3px;background:#111;cursor:pointer;display:block;";
const canvas2 = document.createElement("canvas"); canvas2.style.cssText = "width:100%;height:36px;border-radius:3px;background:var(--xb-inset);cursor:pointer;display:block;";
canvas2.width = 1200 * dpr; canvas2.height = 72 * dpr; ctr.appendChild(canvas2);
const domWidget = node.addDOMWidget("xb3_ui", "custom", ctr);
+1 -1
View File
@@ -31,7 +31,7 @@ app.registerExtension({
[w.mutes1_data, w.mutes2_data].forEach(wd => { hideWidget(node, wd); });
if (w.total_display) {
if (w.total_display.options) w.total_display.options.read_only = true; // Nodes 2.0
setTimeout(() => { const el = w.total_display.inputEl || w.total_display.element; if (el) { el.readOnly = true; el.style.cssText = "background-color:#1a1a1a;color:#00E676;text-align:center;font-weight:bold;font-size:14px;border:none;"; } }, 200);
setTimeout(() => { const el = w.total_display.inputEl || w.total_display.element; if (el) { el.readOnly = true; el.style.cssText = "background-color:var(--xb-bg);color:#00E676;text-align:center;font-weight:bold;font-size:14px;border:none;"; } }, 200);
}
// 轨道状态机
+1 -1
View File
@@ -270,7 +270,7 @@ export function ensureBanner(node, name, text, minH = 30) {
el.dataset.xbBanner = "1";
el.textContent = text;
el.style.cssText =
"padding:6px 8px;border-radius:6px;background:#2b3138;color:#cfd6dd;" +
"padding:6px 8px;border-radius:6px;background:var(--xb-bg-raise);color:var(--xb-text);" +
"font-size:12px;line-height:1.4;text-align:center;user-select:none;";
w = node.addDOMWidget(name, name, el, { serialize: false, hideOnZoom: false, getMinHeight: () => minH });
sizeDomWidget(node, w, minH);
+1 -1
View File
@@ -73,7 +73,7 @@ app.registerExtension({
[w.mutes1_data, w.mutes2_data].forEach(wd => { hideWidget(node, wd); });
if (w.total_display) {
if (w.total_display.options) w.total_display.options.read_only = true; // Nodes 2.0
setTimeout(() => { const el = w.total_display.inputEl || w.total_display.element; if (el) { el.readOnly = true; el.style.cssText = "background-color:#1a1a1a;color:#00E676;text-align:center;font-weight:bold;font-size:14px;border:none;"; } }, 200);
setTimeout(() => { const el = w.total_display.inputEl || w.total_display.element; if (el) { el.readOnly = true; el.style.cssText = "background-color:var(--xb-bg);color:#00E676;text-align:center;font-weight:bold;font-size:14px;border:none;"; } }, 200);
}
// ── 轨道状态机(来自 xb_audio_slicer_v3.js)──
+3 -3
View File
@@ -73,7 +73,7 @@ app.registerExtension({
const el = wDur.inputEl || wDur.element;
if (el) {
el.readOnly = true;
el.style.cssText = "background-color:#1a1a1a;color:#00E676;text-align:center;font-weight:bold;font-size:15px;min-width:120px;white-space:nowrap;overflow:hidden;";
el.style.cssText = "background-color:var(--xb-bg);color:#00E676;text-align:center;font-weight:bold;font-size:15px;min-width:120px;white-space:nowrap;overflow:hidden;";
}
}, 100);
}
@@ -100,14 +100,14 @@ app.registerExtension({
// ── 波形可视化 UI(来自 xb_audio_slicer_v1.js)──
const ctr = document.createElement("div");
Object.assign(ctr.style, { display: "flex", flexDirection: "column", gap: "6px", width: "100%", padding: "6px", boxSizing: "border-box", background: "#161616", borderRadius: "6px", color: "#ccc", fontFamily: "sans-serif", marginTop: "4px" });
Object.assign(ctr.style, { display: "flex", flexDirection: "column", gap: "6px", width: "100%", padding: "6px", boxSizing: "border-box", background: "#161616", borderRadius: "6px", color: "var(--xb-text)", fontFamily: "sans-serif", marginTop: "4px" });
const audioEl = document.createElement("audio");
audioEl.controls = true; audioEl.style.cssText = "width:100%;height:34px;outline:none;";
ctr.appendChild(audioEl);
const canvas = document.createElement("canvas");
canvas.style.cssText = "width:100%;height:40px;border-radius:4px;background:#111;cursor:pointer;display:block;";
canvas.style.cssText = "width:100%;height:40px;border-radius:4px;background:var(--xb-inset);cursor:pointer;display:block;";
const dpr = window.devicePixelRatio || 1;
canvas.width = 1200 * dpr; canvas.height = 80 * dpr;
ctr.appendChild(canvas);
+20 -20
View File
@@ -130,12 +130,12 @@ function mkBtn(text, tip, onClick, css) {
b.textContent = text;
if (tip) b.title = tip;
b.style.cssText = "font-size:11px;line-height:1.2;height:22px;padding:0 8px;border-radius:5px;"
+ "border:1px solid #555;background:#2b2b2b;color:#ddd;cursor:pointer;white-space:nowrap;font-family:inherit;"
+ "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);cursor:pointer;white-space:nowrap;font-family:inherit;"
+ (css || "");
b.addEventListener("pointerdown", (e) => e.stopPropagation());
b.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); onClick?.(e); });
b.addEventListener("mouseenter", () => { b.style.background = "#3a3a3a"; });
b.addEventListener("mouseleave", () => { b.style.background = "#2b2b2b"; });
b.addEventListener("mouseenter", () => { b.style.background = "var(--xb-border)"; });
b.addEventListener("mouseleave", () => { b.style.background = "var(--xb-bg-raise)"; });
return b;
}
@@ -250,14 +250,14 @@ function openPicker(node, kind) {
+ "display:flex;overflow:auto;";
const panel = document.createElement("div");
panel.style.cssText = "width:660px;max-width:94vw;max-height:86vh;margin:auto;flex:0 0 auto;display:flex;flex-direction:column;"
+ "background:#1c1c1e;border:1px solid #333;border-radius:8px;color:#ddd;font:13px/1.55 system-ui,sans-serif;"
+ "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;color:var(--xb-text);font:13px/1.55 system-ui,sans-serif;"
+ "box-shadow:0 20px 40px rgba(0,0,0,.6);overflow:hidden;";
const head = document.createElement("div");
head.style.cssText = "padding:12px 16px;background:#232326;border-bottom:1px solid #3a3a3a;display:flex;align-items:center;gap:10px;";
head.style.cssText = "padding:12px 16px;background:var(--xb-bg-soft);border-bottom:1px solid var(--xb-border);display:flex;align-items:center;gap:10px;";
const hTitle = document.createElement("div");
hTitle.textContent = isGroups ? "⚙️ 选择要控制的分组" : "⚙️ 选择要控制的节点";
hTitle.style.cssText = "flex:1 1 auto;font-size:15px;font-weight:700;color:#eee;";
hTitle.style.cssText = "flex:1 1 auto;font-size:15px;font-weight:700;color:var(--xb-text);";
const hClose = mkBtn("✕", "关闭", () => close());
head.append(hTitle, hClose);
@@ -266,10 +266,10 @@ function openPicker(node, kind) {
const search = document.createElement("input");
search.type = "text";
search.placeholder = isGroups ? "搜索分组…" : "搜索节点名 / 类型…";
search.style.cssText = "flex:1 1 220px;min-width:160px;height:26px;background:#1e1e1e;color:#ddd;"
+ "border:1px solid #444;border-radius:5px;padding:0 8px;font-size:12px;font-family:inherit;";
search.style.cssText = "flex:1 1 220px;min-width:160px;height:26px;background:var(--xb-bg);color:var(--xb-text);"
+ "border:1px solid var(--xb-border);border-radius:5px;padding:0 8px;font-size:12px;font-family:inherit;";
const countEl = document.createElement("span");
countEl.style.cssText = "color:#8a8a8a;font-size:12px;font-variant-numeric:tabular-nums;";
countEl.style.cssText = "color:var(--xb-text-dim);font-size:12px;font-variant-numeric:tabular-nums;";
const hintEl = document.createElement("span");
hintEl.textContent = "⠿ 拖拽可调整顺序";
hintEl.style.cssText = "color:#6f7883;font-size:11px;";
@@ -279,7 +279,7 @@ function openPicker(node, kind) {
list.style.cssText = "flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px 12px;display:flex;flex-direction:column;gap:3px;";
const foot = document.createElement("div");
foot.style.cssText = "padding:10px 16px;border-top:1px solid #3a3a3a;display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;background:#18181a;";
foot.style.cssText = "padding:10px 16px;border-top:1px solid var(--xb-border);display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;background:var(--xb-bg-soft);";
const close = () => { try { overlay.remove(); } catch (_) { /* ignore */ } };
@@ -319,7 +319,7 @@ function openPicker(node, kind) {
ghost.setAttribute("data-xb-ghost", "1");
ghost.style.cssText += `;position:fixed;left:${srcRect.left}px;top:${srcRect.top}px;width:${srcRect.width}px;`
+ "box-sizing:border-box;z-index:10360;pointer-events:none;margin:0;"
+ "background:#33343a;border:1px solid #f59e0b;border-radius:6px;"
+ "background:var(--xb-bg-raise);border:1px solid #f59e0b;border-radius:6px;"
+ "box-shadow:0 14px 30px rgba(0,0,0,.65),0 0 0 1px rgba(245,158,11,.25);"
+ "transform:scale(1.02);opacity:.98;";
document.body.append(ghost);
@@ -348,7 +348,7 @@ function openPicker(node, kind) {
panel.style.height = "";
src.style.opacity = "1";
src.style.borderStyle = "solid";
src.style.borderColor = "#333";
src.style.borderColor = "var(--xb-border)";
};
const onMove = (e) => {
if (!dragKey) return;
@@ -411,7 +411,7 @@ function openPicker(node, kind) {
shown++;
const row = document.createElement("label");
row.style.cssText = "display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;cursor:pointer;"
+ "background:#242424;border:1px solid #333;";
+ "background:var(--xb-bg-raise);border:1px solid var(--xb-border);";
row.dataset.xbRow = it.key;
const grip = document.createElement("span");
grip.textContent = "⠿";
@@ -444,7 +444,7 @@ function openPicker(node, kind) {
if (!shown) {
const empty = document.createElement("div");
empty.textContent = isGroups ? "当前画布上没有分组" : "没有匹配的节点";
empty.style.cssText = "padding:16px;color:#8a8a8a;text-align:center;";
empty.style.cssText = "padding:16px;color:var(--xb-text-dim);text-align:center;";
list.append(empty);
}
syncCount();
@@ -481,7 +481,7 @@ function openPicker(node, kind) {
};
const cancel = mkBtn("取消", "放弃修改", close);
const ok = mkBtn("确定", "保存选择", confirm, "border-color:#3f86d0;background:#22456b;color:#eaf4ff;font-weight:600;");
const ok = mkBtn("确定", "保存选择", confirm, "border-color:#3f86d0;background:#22456b;color:var(--xb-on-accent);font-weight:600;");
foot.append(
mkBtn("全选", "选中当前筛选结果里的全部条目", () => selectAll(true)),
mkBtn("清空", "取消全部选择", () => selectAll(false)),
@@ -502,7 +502,7 @@ function buildPanel(node, kind) {
const isGroups = kind === "groups";
const container = document.createElement("div");
container.style.cssText = "width:100%;height:100%;box-sizing:border-box;display:flex;flex-direction:column;"
+ `gap:4px;padding:${PANEL_PAD / 2}px;overflow:visible;background:#2b2b2b;border-radius:6px;`;
+ `gap:4px;padding:${PANEL_PAD / 2}px;overflow:visible;background:var(--xb-bg-raise);border-radius:6px;`;
// 节点表面只留「开关列表」——选择 / 禁用方式 / 切换模式 全部收进右键菜单
const list = document.createElement("div");
@@ -615,7 +615,7 @@ function toggleRow(node, index) {
function buildSwitch(enabled, disabled) {
const box = document.createElement("span");
box.style.cssText = "flex:0 0 auto;position:relative;width:34px;height:18px;border-radius:9px;"
+ `background:${disabled ? "#3a3a3a" : enabled ? "#3f86d0" : "#4a4a4a"};`
+ `background:${disabled ? "var(--xb-border)" : enabled ? "#3f86d0" : "var(--xb-border)"};`
+ "transition:background .15s;";
const knob = document.createElement("span");
knob.style.cssText = "position:absolute;top:2px;width:14px;height:14px;border-radius:7px;background:#fff;"
@@ -676,7 +676,7 @@ function refreshRows(node) {
? "还没有选择分组 —— "
: "还没有选择节点 —— ")
+ "右击本节点 →「" + (node.type === TYPE_GROUPS ? "选择分组" : "选择节点") + "…」勾选。";
tip.style.cssText = "padding:10px;color:#8a8a8a;font-size:11px;line-height:1.6;";
tip.style.cssText = "padding:10px;color:var(--xb-text-dim);font-size:11px;line-height:1.6;";
list.append(tip);
applyPanelHeight(node, true);
return;
@@ -689,7 +689,7 @@ function refreshRows(node) {
const el = document.createElement("div");
el.style.cssText = "display:flex;align-items:center;gap:8px;padding:3px 6px;border-radius:5px;"
+ `background:${enabled ? "#22456b" : "#1f1f1f"};`
+ `border:1px solid ${enabled ? "#3f86d0" : "#3a3a3a"};`
+ `border:1px solid ${enabled ? "#3f86d0" : "var(--xb-border)"};`
+ "cursor:pointer;";
const name = document.createElement("span");
@@ -707,7 +707,7 @@ function refreshRows(node) {
if (!ok) {
// 永远保持一个开启:拒绝全关 → 闪一下提示
el.style.borderColor = "#e05555";
setTimeout(() => { try { el.style.borderColor = enabled ? "#3f86d0" : "#3a3a3a"; } catch (_) { /* ignore */ } }, 450);
setTimeout(() => { try { el.style.borderColor = enabled ? "#3f86d0" : "var(--xb-border)"; } catch (_) { /* ignore */ } }, 450);
}
});
el.addEventListener("mouseenter", () => { el.style.filter = "brightness(1.18)"; });
+60 -60
View File
@@ -10,7 +10,7 @@
* · 普通图片文件夹(无清单):按文件名多宫格 + 搜索 + 排序 + 加载更多;
* · 单选(一个端口一张图)→ 回填节点内部字段 pose_ref / outfit_ref。
*
* 视觉沿用本包弹窗风格(#1c1c1e / 880px / 圆角 8px)。
* 视觉沿用本包弹窗风格(var(--xb-bg) / 880px / 圆角 8px)。
*/
import { api } from "../../scripts/api.js";
@@ -164,25 +164,25 @@ function shell(title) {
const mask = el("div", "position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,.78);"
+ "display:flex;overflow:auto;box-sizing:border-box;");
const card = el("div", "margin:auto;flex:0 0 auto;display:flex;flex-direction:column;"
+ "background:#1c1c1e;border:1px solid #333;border-radius:8px;color:#ddd;"
+ "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;color:var(--xb-text);"
+ "font-family:system-ui,sans-serif;line-height:1.65;box-shadow:0 20px 40px rgba(0,0,0,.6);overflow:hidden;");
const head = el("div", "padding:14px 18px 12px;background:#232326;border-bottom:1px solid #3a3a3a;"
const head = el("div", "padding:14px 18px 12px;background:var(--xb-bg-soft);border-bottom:1px solid var(--xb-border);"
+ "display:flex;align-items:center;gap:12px;flex-wrap:wrap;");
head.append(el("span", "flex:1 1 320px;min-width:200px;font-size:1.2em;font-weight:700;color:#eee;", title));
head.append(el("span", "flex:1 1 320px;min-width:200px;font-size:1.2em;font-weight:700;color:var(--xb-text);", title));
// 顶部常驻条(放「禁用」勾选框等,切换视图时不会被清空)
const top = el("div", "padding:8px 18px;background:#202024;border-bottom:1px solid #333;"
const top = el("div", "padding:8px 18px;background:var(--xb-bg);border-bottom:1px solid var(--xb-border);"
+ "display:flex;align-items:center;gap:12px;flex-wrap:wrap;");
top.hidden = true;
const body = el("div", "padding:14px 18px 16px;display:flex;flex-direction:column;gap:10px;overflow:auto;min-height:0;");
const foot = el("div", "padding:10px 18px;border-top:1px solid #3a3a3a;background:#18181a;"
const foot = el("div", "padding:10px 18px;border-top:1px solid var(--xb-border);background:var(--xb-bg-soft);"
+ "display:flex;align-items:center;gap:10px;flex-wrap:wrap;");
// 顶部工具条:字号 A−/A+ | 界面 −/%/+ | ✕
const tools = el("span", "display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-weight:normal;");
const fontVal = el("span", "min-width:26px;text-align:center;color:#e8a87c;font-variant-numeric:tabular-nums;", String(fontGet()));
const zoomVal = el("span", "min-width:46px;text-align:center;color:#ffd98a;font-variant-numeric:tabular-nums;",
const fontVal = el("span", "min-width:26px;text-align:center;color:var(--xb-orange);font-variant-numeric:tabular-nums;", String(fontGet()));
const zoomVal = el("span", "min-width:46px;text-align:center;color:var(--xb-warn);font-variant-numeric:tabular-nums;",
Math.round(zoomGet() * 100) + "%");
const refreshVals = () => {
fontVal.textContent = String(fontGet());
@@ -193,30 +193,30 @@ function shell(title) {
applyScale(card);
try { card.dispatchEvent(new Event("xb-scale")); } catch (_) {}
};
const fontMinus = mkBtn("A−", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;height:1.9em;padding:0 .55em;", () => {
const fontMinus = mkBtn("A−", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);height:1.9em;padding:0 .55em;", () => {
fontSet(fontGet() - 1);
scaleNow();
refreshVals();
});
const fontPlus = mkBtn("A+", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;height:1.9em;padding:0 .55em;", () => {
const fontPlus = mkBtn("A+", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);height:1.9em;padding:0 .55em;", () => {
fontSet(fontGet() + 1);
scaleNow();
refreshVals();
});
const zoomMinus = mkBtn("−", "border:1px solid #8a6d3b;background:#5a4a2a;color:#ffd98a;height:1.9em;padding:0 .6em;", () => {
const zoomMinus = mkBtn("−", "border:1px solid #8a6d3b;background:var(--xb-tint-warn);color:var(--xb-warn);height:1.9em;padding:0 .6em;", () => {
zoomSet(zoomGet() - ZOOM_STEP);
scaleNow();
refreshVals();
});
const zoomPlus = mkBtn("+", "border:1px solid #8a6d3b;background:#5a4a2a;color:#ffd98a;height:1.9em;padding:0 .6em;", () => {
const zoomPlus = mkBtn("+", "border:1px solid #8a6d3b;background:var(--xb-tint-warn);color:var(--xb-warn);height:1.9em;padding:0 .6em;", () => {
zoomSet(zoomGet() + ZOOM_STEP);
scaleNow();
refreshVals();
});
tools.append(
el("span", "color:#8a8a8a;font-size:.9em;", "字号"), fontMinus, fontVal, fontPlus,
el("span", "color:var(--xb-text-dim);font-size:.9em;", "字号"), fontMinus, fontVal, fontPlus,
el("span", "color:#5a5a5a;", "|"),
el("span", "color:#8a8a8a;font-size:.9em;", "界面"), zoomMinus, zoomVal, zoomPlus,
el("span", "color:var(--xb-text-dim);font-size:.9em;", "界面"), zoomMinus, zoomVal, zoomPlus,
);
head.append(tools);
@@ -235,7 +235,7 @@ function shell(title) {
scaleNow();
refreshVals();
};
tools.append(mkBtn("✕", "border:1px solid #555;background:#2b2b2b;color:#ddd;height:1.9em;padding:0 .7em;", close));
tools.append(mkBtn("✕", "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);height:1.9em;padding:0 .7em;", close));
card.append(head, top, body, foot);
mask.append(card);
@@ -290,23 +290,23 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
disableBox.type = "checkbox";
disableBox.checked = disabled;
disableBox.id = `xb-ref-disable-${kind}`;
const disableText = el("span", "color:#ddd;cursor:pointer;user-select:none;", `🚫 禁用此参考图(不做任何输出)`);
const disableText = el("span", "color:var(--xb-text);cursor:pointer;user-select:none;", `🚫 禁用此参考图(不做任何输出)`);
const disableWrap = el("label", "display:flex;align-items:center;gap:8px;cursor:pointer;padding:2px 0;");
disableWrap.append(disableBox, disableText);
const HINT_OFF = "勾选后本参考图不做任何输出(等同 mute):接在它下游的节点会被直接跳过,不执行也不报错。";
const HINT_ON = `已禁用:${kindName}图不做任何输出,下游节点会被跳过。取消勾选即恢复。`;
const disableHint = el("span", "color:#8a8a8a;font-size:.88em;", HINT_OFF);
const disableHint = el("span", "color:var(--xb-text-dim);font-size:.88em;", HINT_OFF);
ui.top.append(disableWrap, disableHint);
ui.top.hidden = false;
disableBox.addEventListener("change", () => {
disabled = disableBox.checked;
disableText.style.color = disabled ? "#ffb74d" : "#ddd";
disableHint.style.color = disabled ? "#d9a441" : "#8a8a8a";
disableText.style.color = disabled ? "#ffb74d" : "var(--xb-text)";
disableHint.style.color = disabled ? "#d9a441" : "var(--xb-text-dim)";
disableHint.textContent = disabled ? HINT_ON : HINT_OFF;
emit(null, false); // 立即写回节点(不关窗,方便接着选图)
});
disableHint.textContent = disabled ? HINT_ON : HINT_OFF;
disableText.style.color = disabled ? "#ffb74d" : "#ddd";
disableText.style.color = disabled ? "#ffb74d" : "var(--xb-text)";
// ── ① 绑定界面(未绑定 / 未确认时强制先绑定) ──────────────────
async function showBinding(settings, message = "") {
@@ -314,22 +314,22 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
ui.foot.replaceChildren();
const cfg = (settings && settings[kind]) || {};
ui.body.append(el("div", "color:#ffb74d;line-height:1.75;white-space:pre-wrap;",
ui.body.append(el("div", "color:var(--xb-warn);line-height:1.75;white-space:pre-wrap;",
`⚠️ 首次使用需手动绑定${kind === "pose" ? "姿态" : "穿搭"}参考图库目录。\n`
+ "· 整套图库:选择包含 data / img / thumbs 的目录"
+ "(目录里有 data/*.json 素材清单即可自动识别)\n"
+ "· 普通文件夹:任意装图片的目录即可(按文件名多宫格显示)\n"
+ "· 本节点不内置、不分发任何图库素材;请自行确认素材的合法来源与使用授权。"));
const pathInput = el("input", "flex:1 1 320px;min-width:220px;height:2em;background:#1e1e1e;color:#ddd;"
+ "border:1px solid #444;border-radius:4px;padding:0 8px;font-size:1em;box-sizing:border-box;");
const pathInput = el("input", "flex:1 1 320px;min-width:220px;height:2em;background:var(--xb-bg);color:var(--xb-text);"
+ "border:1px solid var(--xb-border);border-radius:4px;padding:0 8px;font-size:1em;box-sizing:border-box;");
pathInput.type = "text";
pathInput.value = cfg.directory || "";
pathInput.placeholder = "填写 ComfyUI 所在电脑上的绝对路径";
const status = el("div", "color:#e07070;min-height:1.4em;white-space:pre-wrap;", message);
const browse = mkBtn("📂 浏览本机目录", "border:1px solid #555;background:#2b2b2b;color:#ddd;height:2.2em;padding:0 12px;", async () => {
const browse = mkBtn("📂 浏览本机目录", "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);height:2.2em;padding:0 12px;", async () => {
try {
const resp = await api.fetchApi("/xb_toolbox/choose_folder", { method: "POST" });
const data = await resp.json();
@@ -357,7 +357,7 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
agreeRow.append(agree, el("span", "",
"我已确认合法获得该图库的使用权,并自行承担素材使用与合规责任(本插件只读取本机文件,不上传、不分发)。"));
const save = mkBtn("✅ 检查并绑定", "background:#2d5a88;color:#fff;border:none;height:2.2em;padding:0 18px;font-weight:600;", async () => {
const save = mkBtn("✅ 检查并绑定", "background:var(--xb-btn-primary);color:#fff;border:none;height:2.2em;padding:0 18px;font-weight:600;", async () => {
save.disabled = true;
status.style.color = "#bbb";
status.textContent = "正在检查目录…";
@@ -381,12 +381,12 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
});
ui.body.append(row, agreeRow, status);
ui.foot.append(el("span", "flex:1 1 auto;color:#8a8a8a;", "绑定信息只保存在本机 ComfyUI 用户目录,不写进工作流。"), save);
ui.foot.append(el("span", "flex:1 1 auto;color:var(--xb-text-dim);", "绑定信息只保存在本机 ComfyUI 用户目录,不写进工作流。"), save);
}
// ── ② 图库界面 ────────────────────────────────────────────────
async function showGallery(settings, message = "") {
ui.body.replaceChildren(el("div", "color:#bbb;", "正在读取图库…"));
ui.body.replaceChildren(el("div", "color:var(--xb-text-dim);", "正在读取图库…"));
ui.foot.replaceChildren();
let cat;
@@ -406,19 +406,19 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
// ── 筛选栏 ──
const bar = el("div", "display:flex;gap:8px;align-items:center;flex-wrap:wrap;");
const search = el("input", "flex:1 1 240px;min-width:160px;height:2em;background:#1e1e1e;color:#ddd;"
+ "border:1px solid #444;border-radius:4px;padding:0 8px;font-size:1em;box-sizing:border-box;");
const search = el("input", "flex:1 1 240px;min-width:160px;height:2em;background:var(--xb-bg);color:var(--xb-text);"
+ "border:1px solid var(--xb-border);border-radius:4px;padding:0 8px;font-size:1em;box-sizing:border-box;");
search.type = "search";
search.placeholder = kind === "pose" ? "搜索姿势、编号、体系、分类、角度…" : "搜索款式、颜色、关键词、编号…";
bar.append(search);
const makeSelect = (caption, values, allLabel, onChange) => {
const select = el("select", "height:2em;background:#1e1e1e;color:#ddd;border:1px solid #444;"
const select = el("select", "height:2em;background:var(--xb-bg);color:var(--xb-text);border:1px solid var(--xb-border);"
+ "border-radius:4px;padding:0 6px;font-size:1em;");
select.add(new Option(allLabel, ""));
for (const value of values) select.add(new Option(value, value));
select.addEventListener("change", () => onChange(select.value));
const wrap = el("label", "display:flex;gap:6px;align-items:center;color:#aaa;white-space:nowrap;");
const wrap = el("label", "display:flex;gap:6px;align-items:center;color:var(--xb-text-dim);white-space:nowrap;");
wrap.append(el("span", "", caption), select);
return { wrap, select };
};
@@ -451,7 +451,7 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
if ((facets.tag || []).length) bar.append(tagSel.wrap);
} else {
bar.append(catSel.wrap);
const sortSel = el("select", "height:2em;background:#1e1e1e;color:#ddd;border:1px solid #444;"
const sortSel = el("select", "height:2em;background:var(--xb-bg);color:var(--xb-text);border:1px solid var(--xb-border);"
+ "border-radius:4px;padding:0 6px;font-size:1em;");
sortSel.add(new Option("文件名 ↑", "name"));
sortSel.add(new Option("文件名 ↓", "name-desc"));
@@ -459,7 +459,7 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
bar.append(sortSel);
}
bar.append(mkBtn("重置筛选", "border:1px solid #555;background:transparent;color:#ccc;height:2em;padding:0 10px;", () => {
bar.append(mkBtn("重置筛选", "border:1px solid var(--xb-border);background:transparent;color:var(--xb-text);height:2em;padding:0 10px;", () => {
Object.assign(state, { query: "", system: "", category: "", angle: "", tag: "", collection: "all" });
search.value = "";
sysSel.select.value = "";
@@ -474,7 +474,7 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
const tabButtons = [];
if (isLibrary) {
for (const [key, text] of [["all", "全部"], ["favorites", "♡ 收藏"], ["recent", "最近使用"]]) {
const btn = mkBtn(text, "border:1px solid #555;background:#242424;color:#ccc;height:2em;padding:0 10px;", () => {
const btn = mkBtn(text, "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);height:2em;padding:0 10px;", () => {
state.collection = key;
render();
});
@@ -483,7 +483,7 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
tabs.append(btn);
}
if (canGroup) {
tabs.append(mkBtn("切换分组 / 逐张", "border:1px solid #555;background:transparent;color:#bbb;height:2em;padding:0 10px;", () => {
tabs.append(mkBtn("切换分组 / 逐张", "border:1px solid var(--xb-border);background:transparent;color:var(--xb-text-dim);height:2em;padding:0 10px;", () => {
state.grouped = !state.grouped;
render();
}));
@@ -492,11 +492,11 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
// 不能分组的库(没有姿势编号)不显示「切换分组」按钮,state.grouped 一并置 false
if (!canGroup) state.grouped = false;
const summary = el("div", "color:#9fc3e8;font-size:.95em;");
const summary = el("div", "color:var(--xb-info);font-size:.95em;");
const notice = el("div", "color:#e07070;min-height:1.2em;");
notice.textContent = message || "";
const grid = el("div", "display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;align-content:start;");
const more = mkBtn("加载更多", "border:1px dashed #555;background:transparent;color:#bbb;height:2.2em;grid-column:1/-1;", () => {
const more = mkBtn("加载更多", "border:1px dashed var(--xb-border);background:transparent;color:var(--xb-text-dim);height:2.2em;grid-column:1/-1;", () => {
state.shown += PAGE;
render(false);
});
@@ -514,10 +514,10 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
// ── 卡片 ──
function makeCard(item, groupItems) {
const card = el("div", "position:relative;border:2px solid " + (selected?.id === item.id ? "#5b9bd5" : "#333")
+ ";border-radius:6px;overflow:hidden;background:#222;display:flex;flex-direction:column;");
const card = el("div", "position:relative;border:2px solid " + (selected?.id === item.id ? "#5b9bd5" : "var(--xb-border)")
+ ";border-radius:6px;overflow:hidden;background:var(--xb-inset);display:flex;flex-direction:column;");
// 缩略图用 contain:姿势/服装都要看全,绝不裁切;背景压深色让留白不刺眼
const img = el("img", "width:100%;height:9.6em;object-fit:contain;display:block;background:#101012;cursor:zoom-in;");
const img = el("img", "width:100%;height:9.6em;object-fit:contain;display:block;background:var(--xb-bg);cursor:zoom-in;");
img.loading = "lazy";
img.decoding = "async";
img.alt = item.name;
@@ -537,9 +537,9 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
}
const meta = el("div", "padding:6px 8px;font-size:.86em;line-height:1.5;overflow:hidden;");
meta.append(el("div", "color:#8fd3ff;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;", item.name));
meta.append(el("div", "color:var(--xb-info);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;", item.name));
const sub = [item.system, item.category, item.angle].filter(Boolean).join(" · ");
if (sub) meta.append(el("div", "color:#8a8a8a;font-size:.92em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;", sub));
if (sub) meta.append(el("div", "color:var(--xb-text-dim);font-size:.92em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;", sub));
const actions = el("div", "display:flex;gap:5px;padding:0 8px 7px;");
const pick = mkBtn(selected?.id === item.id ? "✓ 已选" : "选择",
@@ -549,7 +549,7 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
render(false);
});
const fav = mkBtn(favorites().includes(item.id) ? "♥" : "♡",
"flex:0 0 auto;border:1px solid #555;background:transparent;color:#e89;height:1.9em;font-size:.8em;padding:0 8px;", () => {
"flex:0 0 auto;border:1px solid var(--xb-border);background:transparent;color:#e89;height:1.9em;font-size:.8em;padding:0 8px;", () => {
const list = favorites();
writeLocal(`favorites.${kind}`, list.includes(item.id)
? list.filter((id) => id !== item.id)
@@ -567,9 +567,9 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
// 视角条只用单字(正/斜/侧/俯/仰)+ 悬停全名:卡片变窄或字号放大时也绝不换行挤压
const angleBtn = mkBtn(angle.slice(0, 1), "flex:1 1 0;min-width:0;font-size:.78em;line-height:1.5;height:1.9em;padding:0;"
+ "white-space:nowrap;overflow:hidden;"
+ "border:1px solid " + (isCurrent ? "#5b9bd5" : "#444") + ";"
+ "background:" + (target ? (isCurrent ? "#2d5a88" : "#2a2a2a") : "transparent") + ";"
+ "color:" + (target ? "#ddd" : "#555") + ";", () => {
+ "border:1px solid " + (isCurrent ? "#5b9bd5" : "var(--xb-border)") + ";"
+ "background:" + (target ? (isCurrent ? "#2d5a88" : "var(--xb-bg-raise)") : "transparent") + ";"
+ "color:" + (target ? "var(--xb-text)" : "var(--xb-border)") + ";", () => {
if (target) preview(target, groupItems);
});
angleBtn.title = target ? `${angle} · 点击打开大图` : `${angle} 暂无素材`;
@@ -583,7 +583,7 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
// ── 大图预览 ──
function preview(item, groupItems) {
const view = shell(`预览 · ${item.name}`);
const stage = el("div", "display:flex;justify-content:center;align-items:center;background:#111;"
const stage = el("div", "display:flex;justify-content:center;align-items:center;background:var(--xb-inset);"
+ "border-radius:6px;overflow:hidden;padding:4px;box-sizing:border-box;");
// ⚠️ 只给 max-width / max-height + object-fit:contain:浏览器会按原图比例缩放,
// 永远显示完整画面、不会被拉伸变形;两处上限都用**布局像素**(视口高度 ÷ 界面缩放),
@@ -614,7 +614,7 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
[value.system, value.category, value.angle].filter(Boolean).join(" · "),
`ID:${value.id}`,
].filter(Boolean).join("\n");
const info = el("div", "color:#bbb;white-space:pre-wrap;font-size:.95em;line-height:1.6;");
const info = el("div", "color:var(--xb-text-dim);white-space:pre-wrap;font-size:.95em;line-height:1.6;");
info.textContent = previewLines(item);
let currentItem = item;
@@ -624,8 +624,8 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
if (!isLibrary || !Array.isArray(groupItems) || groupItems.length < 2) return;
for (const angle of ANGLE_ORDER) {
const target = groupItems.find((value) => value.angle === angle);
const btn = mkBtn(angle, "border:1px solid " + (target && target.id === currentItem.id ? "#5b9bd5" : "#444")
+ ";background:" + (target ? "#2a2a2a" : "transparent") + ";color:" + (target ? "#ddd" : "#555")
const btn = mkBtn(angle, "border:1px solid " + (target && target.id === currentItem.id ? "#5b9bd5" : "var(--xb-border)")
+ ";background:" + (target ? "var(--xb-bg-raise)" : "transparent") + ";color:" + (target ? "var(--xb-text)" : "var(--xb-border)")
+ ";height:1.9em;padding:0 10px;white-space:nowrap;", () => {
if (!target) return;
currentItem = target;
@@ -640,13 +640,13 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
paintAngles();
view.body.append(stage, info, angles);
const use = mkBtn("使用此图", "background:#2d5a88;color:#fff;border:none;height:2.2em;padding:0 18px;font-weight:600;", () => {
const use = mkBtn("使用此图", "background:var(--xb-btn-primary);color:#fff;border:none;height:2.2em;padding:0 18px;font-weight:600;", () => {
selected = currentItem;
currentPick = currentItem;
view.close();
render(false);
});
view.foot.append(el("span", "flex:1 1 auto;color:#8a8a8a;", "Esc 关闭"), use);
view.foot.append(el("span", "flex:1 1 auto;color:var(--xb-text-dim);", "Esc 关闭"), use);
}
// ── 渲染 ──
@@ -686,9 +686,9 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
for (const btn of tabButtons) {
const active = btn.dataset.collection === state.collection;
btn.style.background = active ? "#2d5a88" : "#242424";
btn.style.color = active ? "#fff" : "#ccc";
btn.style.borderColor = active ? "#5b9bd5" : "#555";
btn.style.background = active ? "#2d5a88" : "var(--xb-bg-raise)";
btn.style.color = active ? "#fff" : "var(--xb-text)";
btn.style.borderColor = active ? "#5b9bd5" : "var(--xb-border)";
}
paintFooter();
}
@@ -697,24 +697,24 @@ export async function openRefPicker({ kind, current = "", onPick } = {}) {
function paintFooter() {
ui.foot.replaceChildren();
if (selected) {
const thumb = el("img", "width:46px;height:46px;object-fit:contain;border-radius:4px;border:1px solid #444;background:#101012;flex:0 0 auto;");
const thumb = el("img", "width:46px;height:46px;object-fit:contain;border-radius:4px;border:1px solid var(--xb-border);background:var(--xb-bg);flex:0 0 auto;");
thumb.src = imageUrl(kind, selected, true);
const label = (selected.group ? `[${selected.group}] ` : "") + selected.name;
const text = el("span", "flex:1 1 auto;color:#ddd;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;", label);
const text = el("span", "flex:1 1 auto;color:var(--xb-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;", label);
text.title = label;
ui.foot.append(thumb, text);
} else {
ui.foot.append(el("span", "flex:1 1 auto;color:#8a8a8a;", "点卡片上的「选择」或大图里的「使用此图」"));
ui.foot.append(el("span", "flex:1 1 auto;color:var(--xb-text-dim);", "点卡片上的「选择」或大图里的「使用此图」"));
}
const use = mkBtn(disabled ? "✅ 保存(当前已禁用)" : (selected ? "✅ 使用此图" : "请先选一张"),
"background:#2d5a88;color:#fff;border:none;height:2.2em;padding:0 18px;font-weight:600;", () => {
"background:var(--xb-btn-primary);color:#fff;border:none;height:2.2em;padding:0 18px;font-weight:600;", () => {
if (!selected) return;
const list = recent();
writeLocal(`recent.${kind}`, [selected.id, ...list.filter((id) => id !== selected.id)].slice(0, 80));
emit(selected, true);
});
use.disabled = !selected;
const rebind = mkBtn("🔧 更换图库目录", "border:1px solid #555;background:transparent;color:#bbb;height:2.2em;padding:0 10px;", () => {
const rebind = mkBtn("🔧 更换图库目录", "border:1px solid var(--xb-border);background:transparent;color:var(--xb-text-dim);height:2.2em;padding:0 10px;", () => {
req("/settings")
.then((data) => showBinding(data.settings))
.catch((err) => { notice.textContent = err?.message || String(err); });
+109 -109
View File
@@ -1453,15 +1453,15 @@ function el(tag, css, text) {
return e;
}
const BOX_CSS = "background:#1d1d1d;color:#ccc;border:1px solid #444;border-radius:4px;padding:5px 8px;font-size:12px;box-sizing:border-box;font-family:inherit;";
const BOX_CSS = "background:var(--xb-bg);color:var(--xb-text);border:1px solid var(--xb-border);border-radius:4px;padding:5px 8px;font-size:12px;box-sizing:border-box;font-family:inherit;";
function makeSectionTitle(text) {
return el("div", "font-size:13px;font-weight:600;color:#9fc3e8;margin:14px 0 8px;padding-bottom:5px;border-bottom:1px solid #333;", text);
return el("div", "font-size:13px;font-weight:600;color:var(--xb-info);margin:14px 0 8px;padding-bottom:5px;border-bottom:1px solid var(--xb-border);", text);
}
function field(labelText, control) {
const g = el("div", "display:flex;align-items:center;gap:12px;margin-bottom:10px;");
const lab = el("label", "flex:0 0 160px;font-size:13px;color:#999;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;", labelText);
const lab = el("label", "flex:0 0 160px;font-size:13px;color:var(--xb-text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;", labelText);
control.style.flex = "1 1 0";
control.style.minWidth = "0";
g.append(lab, control);
@@ -1488,7 +1488,7 @@ function radioRow(options, value, onChange) {
for (const o of options) {
const val = (typeof o === "object" && o !== null && o.value !== undefined) ? String(o.value) : String(o);
const label = (typeof o === "object" && o !== null && o.label) ? String(o.label) : String(o);
const lab = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:#999;cursor:pointer;user-select:none;");
const lab = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:var(--xb-text-dim);cursor:pointer;user-select:none;");
const c = el("input", "width:18px;height:18px;accent-color:#f59e0b;cursor:pointer;");
c.type = "checkbox";
c.checked = String(value) === val;
@@ -1515,7 +1515,7 @@ function textareaControl(value, onChange, css) {
}
function checkboxControl(value, labelText, onChange, tip) {
const w = el("label", "display:flex;align-items:center;gap:8px;cursor:pointer;font-size:12px;color:#ddd;user-select:none;");
const w = el("label", "display:flex;align-items:center;gap:8px;cursor:pointer;font-size:12px;color:var(--xb-text);user-select:none;");
const c = el("input", "width:16px;height:16px;accent-color:#f59e0b;cursor:pointer;");
c.type = "checkbox";
c.checked = !!value;
@@ -1577,10 +1577,10 @@ function zoomSync(root) {
}
function buildZoomRow(root) {
const row = el("span", "display:inline-flex;align-items:center;gap:4px;user-select:none;");
const minus = smallBtn("A−", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;width:22px;height:22px;padding:0;line-height:1;", "缩小编辑字号(Ctrl/⌘+滚轮 也可)");
const val = el("span", "min-width:26px;text-align:center;font-size:12px;color:#e8a87c;font-variant-numeric:tabular-nums;", String(zoomDisplay()));
const minus = smallBtn("A−", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);width:22px;height:22px;padding:0;line-height:1;", "缩小编辑字号(Ctrl/⌘+滚轮 也可)");
const val = el("span", "min-width:26px;text-align:center;font-size:12px;color:var(--xb-orange);font-variant-numeric:tabular-nums;", String(zoomDisplay()));
val.classList.add("ipp-zoom-val");
const plus = smallBtn("A+", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;width:22px;height:22px;padding:0;line-height:1;", "放大编辑字号(Ctrl/⌘+滚轮 也可)");
const plus = smallBtn("A+", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);width:22px;height:22px;padding:0;line-height:1;", "放大编辑字号(Ctrl/⌘+滚轮 也可)");
const step = (d) => {
zoomSet((zoomGet() == null ? 12 : zoomGet()) + d);
zoomApplyContainer(root);
@@ -1624,10 +1624,10 @@ window.addEventListener("resize", () => {
function buildUIRow(target) {
const row = el("span", "display:inline-flex;align-items:center;gap:4px;user-select:none;");
row.append(el("span", "font-size:11px;color:#888;white-space:nowrap;", "界面"));
const minus = smallBtn("−", "border:1px solid #8a6d3b;background:#5a4a2a;color:#ffd98a;width:22px;height:22px;padding:0;line-height:1;font-size:13px;", "缩小整个界面(Ctrl/⌘+滚轮 也可)");
const val = el("span", "min-width:42px;text-align:center;font-size:12px;color:#ffd98a;font-variant-numeric:tabular-nums;", Math.round(uiGet() * 100) + "%");
const minus = smallBtn("−", "border:1px solid #8a6d3b;background:var(--xb-tint-warn);color:var(--xb-warn);width:22px;height:22px;padding:0;line-height:1;font-size:13px;", "缩小整个界面(Ctrl/⌘+滚轮 也可)");
const val = el("span", "min-width:42px;text-align:center;font-size:12px;color:var(--xb-warn);font-variant-numeric:tabular-nums;", Math.round(uiGet() * 100) + "%");
val.classList.add("ipp-ui-val");
const plus = smallBtn("+", "border:1px solid #8a6d3b;background:#5a4a2a;color:#ffd98a;width:22px;height:22px;padding:0;line-height:1;font-size:13px;", "放大整个界面(Ctrl/⌘+滚轮 也可)");
const plus = smallBtn("+", "border:1px solid #8a6d3b;background:var(--xb-tint-warn);color:var(--xb-warn);width:22px;height:22px;padding:0;line-height:1;font-size:13px;", "放大整个界面(Ctrl/⌘+滚轮 也可)");
const step = (d) => {
uiSet(uiGet() + d * UI_STEP);
uiApply(target);
@@ -1889,12 +1889,12 @@ function openPanel(ctx, panelId) {
const title = `${cat.label} 元素`;
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.78);z-index:9999;display:flex;align-items:center;justify-content:center;overflow:auto;");
const dialog = el("section", "background:#1c1c1e;border:1px solid #333;border-radius:8px;width:880px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 20px 40px rgba(0,0,0,.6);flex:0 0 auto;color:#ddd;font-size:13px;");
const dialog = el("section", "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:880px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 20px 40px rgba(0,0,0,.6);flex:0 0 auto;color:var(--xb-text);font-size:13px;");
const header = el("div", "padding:16px 20px 12px;border-bottom:1px solid #444;");
const titleEl = el("div", "font-size:18px;font-weight:700;color:#eee;", title);
const header = el("div", "padding:16px 20px 12px;border-bottom:1px solid var(--xb-border);");
const titleEl = el("div", "font-size:18px;font-weight:700;color:var(--xb-text);", title);
header.append(titleEl);
header.append(el("div", "font-size:12px;color:#999;margin-top:5px;", "点选项行即加入提示词(变蓝),再点一下移除(变灰);点【添加详细描述】会在新窗口里给该词条写补充描述(悬停行可看)。配置随工作流保存,改完重新执行节点即生效。"));
header.append(el("div", "font-size:12px;color:var(--xb-text-dim);margin-top:5px;", "点选项行即加入提示词(变蓝),再点一下移除(变灰);点【添加详细描述】会在新窗口里给该词条写补充描述(悬停行可看)。配置随工作流保存,改完重新执行节点即生效。"));
dialog.append(header);
// ⚠️ 顶部不留内边距:滚动容器的 padding 区不参与裁剪,滚上去的行会从这条缝里
@@ -1903,12 +1903,12 @@ function openPanel(ctx, panelId) {
const panelBox = el("div", "padding:0 20px 14px;overflow-y:auto;flex:1;min-height:280px;");
// ── 吸顶预览框(不受滚动条约束,始终在画面内) ──
const sticky = el("div", "position:sticky;top:0;z-index:6;background:#1c1c1e;padding:16px 0 10px;margin:0 0 6px;border-bottom:1px solid #333;");
const sticky = el("div", "position:sticky;top:0;z-index:6;background:var(--xb-bg);padding:16px 0 10px;margin:0 0 6px;border-bottom:1px solid var(--xb-border);");
const head = el("div", "display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap;");
head.append(el("span", "font-size:12px;font-weight:600;color:#9fc3e8;", "📝 提示词预览(与节点提示词框实时同步,可直接编辑)"));
head.append(el("span", "font-size:12px;font-weight:600;color:var(--xb-info);", "📝 提示词预览(与节点提示词框实时同步,可直接编辑)"));
const headCount = el("span", "font-size:11px;color:#888;", "");
head.append(headCount);
const presetChip = el("div", "font-size:11px;color:#f0d79a;background:#2a2416;border:1px dashed #8a6d3b;border-radius:4px;padding:4px 8px;margin-bottom:6px;white-space:pre-wrap;line-height:1.5;display:none;");
const presetChip = el("div", "font-size:11px;color:var(--xb-warn);background:var(--xb-tint-warn);border:1px dashed #8a6d3b;border-radius:4px;padding:4px 8px;margin-bottom:6px;white-space:pre-wrap;line-height:1.5;display:none;");
const preview = el("textarea", BOX_CSS + "width:100%;min-height:74px;max-height:210px;resize:vertical;line-height:1.65;font-size:12px;");
preview.spellcheck = false;
preview.placeholder = "点左侧 + 添加元素,或直接在这里手写提示词…";
@@ -1946,10 +1946,10 @@ function openPanel(ctx, panelId) {
// ── 底部 ──
const error = el("div", "color:#e55;font-size:12px;margin:0 20px;min-height:16px;");
const footer = el("div", "padding:12px 20px;border-top:1px solid #444;display:flex;justify-content:flex-end;gap:10px;background:#18181a;border-bottom-left-radius:8px;border-bottom-right-radius:8px;");
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:8px 20px;font-size:14px;cursor:pointer;font-family:inherit;", "取消");
const saveBtn = el("button", "background:#2d5a88;color:#fff;border:none;border-radius:4px;padding:8px 20px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;", "💾 保存");
const autoRow = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:#999;cursor:pointer;user-select:none;");
const footer = el("div", "padding:12px 20px;border-top:1px solid var(--xb-border);display:flex;justify-content:flex-end;gap:10px;background:var(--xb-bg-soft);border-bottom-left-radius:8px;border-bottom-right-radius:8px;");
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:8px 20px;font-size:14px;cursor:pointer;font-family:inherit;", "取消");
const saveBtn = el("button", "background:var(--xb-btn-primary);color:#fff;border:none;border-radius:4px;padding:8px 20px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;", "💾 保存");
const autoRow = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:var(--xb-text-dim);cursor:pointer;user-select:none;");
const autoCb = el("input", "width:18px;height:18px;accent-color:#f59e0b;cursor:pointer;");
autoCb.type = "checkbox";
autoCb.checked = settings.auto_save !== false;
@@ -2034,13 +2034,13 @@ const CAT_UI = {}; // 会话级:cat -> { collapsed, onlySel, keyword }(不
* ============================================================ */
function openConfirm(title, message, onOk, okText) {
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:10200;display:flex;align-items:center;justify-content:center;");
const box = el("section", "background:#1c1c1e;border:1px solid #333;border-radius:8px;width:460px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:#ddd;font-size:13px;");
const box = el("section", "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:460px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:var(--xb-text);font-size:13px;");
box.dataset.ippModal = "confirm";
box.append(el("div", "font-size:15px;font-weight:700;color:#eee;margin-bottom:8px;", title || "确认"));
box.append(el("div", "font-size:13px;color:#bbb;line-height:1.8;white-space:pre-wrap;margin-bottom:14px;", message || ""));
box.append(el("div", "font-size:15px;font-weight:700;color:var(--xb-text);margin-bottom:8px;", title || "确认"));
box.append(el("div", "font-size:13px;color:var(--xb-text-dim);line-height:1.8;white-space:pre-wrap;margin-bottom:14px;", message || ""));
const row = el("div", "display:flex;gap:10px;justify-content:flex-end;");
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const okBtn = el("button", "background:#8a3a2a;border:1px solid #c2604a;color:#ffe7df;border-radius:4px;padding:7px 18px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", okText || "确定");
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const okBtn = el("button", "background:var(--xb-tint-error);border:1px solid #c2604a;color:#ffe7df;border-radius:4px;padding:7px 18px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", okText || "确定");
const close = () => { try { overlay.remove(); } catch (_) {} };
cancelBtn.addEventListener("click", (e) => { e.stopPropagation(); close(); });
okBtn.addEventListener("click", (e) => { e.stopPropagation(); close(); try { onOk && onOk(); } catch (_) {} });
@@ -2058,29 +2058,29 @@ function openConfirm(title, message, onOk, okText) {
* ============================================================ */
function openRelPicker(node, nameText, lang, current, onPick) {
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.78);z-index:10200;display:flex;align-items:center;justify-content:center;");
const box = el("section", "background:#1c1c1e;border:1px solid #333;border-radius:8px;width:780px;max-width:94vw;max-height:92vh;overflow:auto;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:#ddd;font-size:13px;");
const box = el("section", "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:780px;max-width:94vw;max-height:92vh;overflow:auto;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:var(--xb-text);font-size:13px;");
box.dataset.ippModal = "rel";
box.append(el("div", "font-size:15px;font-weight:700;color:#eee;margin-bottom:4px;", "🎒 道具与主体的关系"));
box.append(el("div", "font-size:12px;color:#8a97a5;margin-bottom:12px;line-height:1.7;",
box.append(el("div", "font-size:15px;font-weight:700;color:var(--xb-text);margin-bottom:4px;", "🎒 道具与主体的关系"));
box.append(el("div", "font-size:12px;color:var(--xb-text-dim);margin-bottom:12px;line-height:1.7;",
`道具:${nameText}\n选一个关系词,拼进提示词时它会加在道具前面(例:手拿折扇 / a folding fan, held in the hand)。`));
const grid = el("div", "display:grid;grid-template-columns:repeat(6,1fr);gap:6px;margin-bottom:14px;");
const close = () => { try { overlay.remove(); } catch (_) {} };
for (const [id, zh, en] of RELATIONS) {
const on = id === current;
const b = el("button", `display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;min-height:46px;border-radius:6px;cursor:pointer;font-family:inherit;padding:4px 2px;box-sizing:border-box;border:1px solid ${on ? "#7fb3e0" : "#4a4a4a"};background:${on ? "#2d5a88" : "#262626"};color:${on ? "#eaf4ff" : "#ddd"};`);
const b = el("button", `display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;min-height:46px;border-radius:6px;cursor:pointer;font-family:inherit;padding:4px 2px;box-sizing:border-box;border:1px solid ${on ? "#7fb3e0" : "var(--xb-border)"};background:${on ? "#2d5a88" : "var(--xb-bg-raise)"};color:${on ? "#eaf4ff" : "var(--xb-text)"};`);
b.dataset.rel = id;
b.title = `${zh} / ${en}`;
b.append(el("span", "font-size:13px;line-height:1.2;", zh));
b.append(el("span", "font-size:9px;line-height:1.2;color:#9fb6cc;text-align:center;word-break:break-word;", en));
b.addEventListener("mouseenter", () => { if (!on) b.style.background = "#333"; });
b.addEventListener("mouseleave", () => { if (!on) b.style.background = "#262626"; });
b.addEventListener("mouseenter", () => { if (!on) b.style.background = "var(--xb-border)"; });
b.addEventListener("mouseleave", () => { if (!on) b.style.background = "var(--xb-bg-raise)"; });
b.addEventListener("click", (e) => { e.stopPropagation(); close(); try { onPick && onPick(id); } catch (_) {} });
grid.append(b);
}
box.append(grid);
const row = el("div", "display:flex;gap:10px;justify-content:flex-end;");
const noneBtn = el("button", "background:#2a4a6a;border:1px solid #5b9bd5;color:#cfe3f7;border-radius:4px;padding:7px 16px;font-size:13px;cursor:pointer;font-family:inherit;", "不加关系(只加道具词)");
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const noneBtn = el("button", "background:var(--xb-tint-info);border:1px solid #5b9bd5;color:var(--xb-info);border-radius:4px;padding:7px 16px;font-size:13px;cursor:pointer;font-family:inherit;", "不加关系(只加道具词)");
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
noneBtn.addEventListener("click", (e) => { e.stopPropagation(); close(); try { onPick && onPick(""); } catch (_) {} });
cancelBtn.addEventListener("click", (e) => { e.stopPropagation(); close(); });
overlay.addEventListener("mousedown", (e) => { if (e.target === overlay) close(); });
@@ -2098,10 +2098,10 @@ function openRelPicker(node, nameText, lang, current, onPick) {
* ============================================================ */
function openDescEditor(node, entry, nameText, value, onSave) {
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:10100;display:flex;align-items:center;justify-content:center;");
const box = el("section", "background:#1c1c1e;border:1px solid #333;border-radius:8px;width:600px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:#ddd;font-size:13px;");
const box = el("section", "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:600px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:var(--xb-text);font-size:13px;");
box.dataset.ippModal = "desc"; // 稳定锤点(便于定位/自动化测试)
box.append(el("div", "font-size:15px;font-weight:700;color:#eee;margin-bottom:4px;", "📝 添加详细描述"));
box.append(el("div", "font-size:12px;color:#8a97a5;margin-bottom:10px;line-height:1.7;white-space:pre-wrap;",
box.append(el("div", "font-size:15px;font-weight:700;color:var(--xb-text);margin-bottom:4px;", "📝 添加详细描述"));
box.append(el("div", "font-size:12px;color:var(--xb-text-dim);margin-bottom:10px;line-height:1.7;white-space:pre-wrap;",
`词条:${nameText}\n`
+ "写在这里的内容会以「词条(描述)」的形式拼进提示词(例:一位女性(银灰色短发)),"
+ "所以只写要补的细节即可 —— 不用重复词条本身,也别写整句话。"));
@@ -2113,9 +2113,9 @@ function openDescEditor(node, entry, nameText, value, onSave) {
const tip = el("div", "font-size:11px;color:#777;margin-top:6px;", "Esc = 取消 · Ctrl/⌘ + Enter = 保存");
box.append(tip);
const row = el("div", "display:flex;justify-content:flex-end;gap:10px;margin-top:14px;");
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const clrBtn = el("button", "background:transparent;border:1px solid #844;color:#ffb0b0;border-radius:4px;padding:7px 14px;font-size:13px;cursor:pointer;font-family:inherit;margin-right:auto;", "🗑 清除描述");
const okBtn = el("button", "background:#2d5a88;color:#fff;border:none;border-radius:4px;padding:7px 20px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", "✅ 保存描述");
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const clrBtn = el("button", "background:transparent;border:1px solid #844;color:var(--xb-error);border-radius:4px;padding:7px 14px;font-size:13px;cursor:pointer;font-family:inherit;margin-right:auto;", "🗑 清除描述");
const okBtn = el("button", "background:var(--xb-btn-primary);color:#fff;border:none;border-radius:4px;padding:7px 20px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", "✅ 保存描述");
row.append(clrBtn, cancelBtn, okBtn);
box.append(row);
overlay.append(box);
@@ -2139,11 +2139,11 @@ function openDescEditor(node, entry, nameText, value, onSave) {
/** 自建槽位的「中/英名称」编辑窗(同样是一个小弹窗) */
function openSlotNameEditor(zhValue, enValue, onSave) {
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:10100;display:flex;align-items:center;justify-content:center;");
const box = el("section", "background:#1c1c1e;border:1px solid #333;border-radius:8px;width:520px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:#ddd;font-size:13px;");
const box = el("section", "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:520px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:var(--xb-text);font-size:13px;");
box.dataset.ippModal = "slotname"; // 稳定锤点
box.append(el("div", "font-size:15px;font-weight:700;color:#eee;margin-bottom:10px;", "✎ 自建槽位名称(中 / 英)"));
box.append(el("div", "font-size:15px;font-weight:700;color:var(--xb-text);margin-bottom:10px;", "✎ 自建槽位名称(中 / 英)"));
const mk = (label, val, ph) => {
box.append(el("div", "font-size:11px;color:#8a97a5;margin:6px 0 3px;", label));
box.append(el("div", "font-size:11px;color:var(--xb-text-dim);margin:6px 0 3px;", label));
const i = el("input", BOX_CSS + "width:100%;font-size:13px;padding:6px 8px;");
i.type = "text";
i.value = val || "";
@@ -2154,8 +2154,8 @@ function openSlotNameEditor(zhValue, enValue, onSave) {
const zh = mk("中文词条(中文 [ZH] 输出时用)", zhValue, "例:银灰色短发");
const en = mk("English word(英文 [EN] 输出时用)", enValue, "e.g. silver-gray short hair");
const row = el("div", "display:flex;justify-content:flex-end;gap:10px;margin-top:14px;");
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const okBtn = el("button", "background:#2d5a88;color:#fff;border:none;border-radius:4px;padding:7px 20px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", "✅ 保存名称");
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const okBtn = el("button", "background:var(--xb-btn-primary);color:#fff;border:none;border-radius:4px;padding:7px 20px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", "✅ 保存名称");
row.append(cancelBtn, okBtn);
box.append(row);
overlay.append(box);
@@ -2190,12 +2190,12 @@ function renderCategoryPanel(box, api, catId) {
catRow.dataset.ippCatRow = "1";
for (const c of CATEGORIES) {
const on = c.id === catId;
const b = el("button", `box-sizing:border-box;height:30px;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid ${on ? "#7fb3e0" : "#5b9bd5"};background:${on ? "#2d5a88" : "#3a3a3a"};color:${on ? "#eaf4ff" : "#eee"};font-size:13px;cursor:pointer;font-family:inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;`, c.label);
const b = el("button", `box-sizing:border-box;height:30px;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid ${on ? "#7fb3e0" : "#5b9bd5"};background:${on ? "#2d5a88" : "var(--xb-border)"};color:${on ? "#eaf4ff" : "var(--xb-text)"};font-size:13px;cursor:pointer;font-family:inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;`, c.label);
b.dataset.cat = c.id;
if (on) b.dataset.catActive = "1";
b.title = `切到「${c.label}」面板 —— ${c.tip}`;
b.addEventListener("mouseenter", () => { if (!on) b.style.background = "#4a4a4a"; });
b.addEventListener("mouseleave", () => { if (!on) b.style.background = "#3a3a3a"; });
b.addEventListener("mouseenter", () => { if (!on) b.style.background = "var(--xb-border)"; });
b.addEventListener("mouseleave", () => { if (!on) b.style.background = "var(--xb-border)"; });
b.addEventListener("click", (e) => {
e.stopPropagation();
if (!on && api.switchCategory) api.switchCategory(c.id);
@@ -2205,7 +2205,7 @@ function renderCategoryPanel(box, api, catId) {
box.append(catRow);
const bar = el("div", "display:flex;gap:8px;flex-wrap:wrap;margin-bottom:6px;");
bar.append(
smallBtn("🧹 清空提示词", "border:1px solid #a44;background:#7a2a2a;color:#ffd0d0;padding:6px 12px;", "清空正文(同时清掉所有元素的「已选」状态)", () => {
smallBtn("🧹 清空提示词", "border:1px solid #a44;background:var(--xb-tint-error);color:var(--xb-error);padding:6px 12px;", "清空正文(同时清掉所有元素的「已选」状态)", () => {
openConfirm("🧹 清空提示词", "确定清空提示词正文?\n(所有元素的「已选」状态也会一起清掉)", () => {
s.elements.selected = {};
s.elements.rel = {};
@@ -2215,17 +2215,17 @@ function renderCategoryPanel(box, api, catId) {
notify("已清空提示词", "success");
});
}),
smallBtn("🔧 按选择重建", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;padding:6px 12px;", "按「已选元素 + 固定分类顺序(风格→视角→主体→姿态→装扮→道具→光影→背景)+ 当前语言」重排正文(手工改乱了就用它)", () => {
smallBtn("🔧 按选择重建", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);padding:6px 12px;", "按「已选元素 + 固定分类顺序(风格→视角→主体→姿态→装扮→道具→光影→背景)+ 当前语言」重排正文(手工改乱了就用它)", () => {
const t = rebuildBody(s, index, lang());
bodySet(t);
fire();
notify(t ? "已按已选元素重建正文" : "当前没有已选元素(重建后正文为空)", t ? "success" : "warning");
}),
smallBtn("📋 复制成句", "border:1px solid #555;background:#2a2a2a;color:#ccc;padding:6px 12px;", "复制最终成句(选中有预设的档位时含预设句)", () => {
smallBtn("📋 复制成句", "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);padding:6px 12px;", "复制最终成句(选中有预设的档位时含预设句)", () => {
copyText(finalPrompt(surf.mode, surf.presetText, surf.lang, bodyGet(), surf.io));
}),
smallBtn("⬆️ 导出词表", "border:1px solid #6b9b5b;background:#3a5a2a;color:#d7f0c8;padding:6px 12px;", "把「自建槽位 + 已选 + 描述」导出成 JSON 备份", () => exportJson(s)),
smallBtn("⬇️ 导入词表", "border:1px solid #6b9b5b;background:#3a5a2a;color:#d7f0c8;padding:6px 12px;", "从 JSON 恢复「自建槽位 + 已选 + 描述」(合并,不覆盖内置词表)", () => importJson((obj) => {
smallBtn("⬆️ 导出词表", "border:1px solid #6b9b5b;background:var(--xb-tint-ok);color:var(--xb-ok);padding:6px 12px;", "把「自建槽位 + 已选 + 描述」导出成 JSON 备份", () => exportJson(s)),
smallBtn("⬇️ 导入词表", "border:1px solid #6b9b5b;background:var(--xb-tint-ok);color:var(--xb-ok);padding:6px 12px;", "从 JSON 恢复「自建槽位 + 已选 + 描述」(合并,不覆盖内置词表)", () => importJson((obj) => {
if (!obj || typeof obj !== "object") return;
for (const k of ["custom", "noted", "selected"]) {
if (obj[k] && typeof obj[k] === "object") s.elements[k] = Object.assign({}, s.elements[k], obj[k]);
@@ -2257,7 +2257,7 @@ function renderCategoryPanel(box, api, catId) {
// 436 条词条只在第一次渲染时建 DOM;标题栏仍可点一下收起(纯会话状态)
if (!CAT_UI[cat.id]) CAT_UI[cat.id] = { collapsed: false, onlySel: false, keyword: "", sub: "" };
const ui = CAT_UI[cat.id];
const wrap = el("div", "border:1px solid #333;border-radius:6px;margin-bottom:8px;background:#191919;overflow:hidden;");
const wrap = el("div", "border:1px solid var(--xb-border);border-radius:6px;margin-bottom:8px;background:var(--xb-inset);overflow:hidden;");
const selOf = () => (s.elements.selected[cat.id] || []);
const selArr = () => (s.elements.selected[cat.id] || (s.elements.selected[cat.id] = []));
const rebuild = () => { wrap.innerHTML = ""; build(); };
@@ -2332,14 +2332,14 @@ function renderCategoryPanel(box, api, catId) {
const cur = subOf(cat, ui.sub).id;
for (const c of chipRefs) {
const on = c.dataset.sub === cur;
c.style.background = on ? "#2d5a88" : "#2a2a2a";
c.style.borderColor = on ? "#7fb3e0" : "#555";
c.style.background = on ? "#2d5a88" : "var(--xb-bg-raise)";
c.style.borderColor = on ? "#7fb3e0" : "var(--xb-border)";
c.style.color = on ? "#eaf4ff" : "#bbb";
c.style.fontWeight = on ? "600" : "400";
}
};
for (const sub of (cat.subs || [])) {
const b = el("button", "border:1px solid #555;border-radius:14px;padding:4px 14px;font-size:12px;cursor:pointer;font-family:inherit;white-space:nowrap;", `${sub.label}(${(sub.keys || []).length})`);
const b = el("button", "border:1px solid var(--xb-border);border-radius:14px;padding:4px 14px;font-size:12px;cursor:pointer;font-family:inherit;white-space:nowrap;", `${sub.label}(${(sub.keys || []).length})`);
b.dataset.sub = sub.id;
b.title = `切到「${sub.label}」子类(${(sub.keys || []).length} 条)`;
b.addEventListener("mousedown", (e) => e.stopPropagation());
@@ -2389,7 +2389,7 @@ function renderCategoryPanel(box, api, catId) {
fire();
notify(n ? `已加入 ${n} 条` : "这些词条都已在提示词里", n ? "success" : "warning");
};
tb.append(smallBtn("+ 全部加入", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;", "把本分类当前可见的词条全部追加进提示词", () => {
tb.append(smallBtn("+ 全部加入", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);", "把本分类当前可见的词条全部追加进提示词", () => {
const list = visibleEntries();
if (!list.length) { notify("没有可加入的词条", "warning"); return; }
if (list.length > 60) {
@@ -2398,7 +2398,7 @@ function renderCategoryPanel(box, api, catId) {
}
addAllNow(list);
}));
tb.append(smallBtn("🗑 全部移除", "border:1px solid #a44;background:#5a2a2a;color:#ffd0d0;", "把本分类已选词条全部从提示词移除", () => {
tb.append(smallBtn("🗑 全部移除", "border:1px solid #a44;background:var(--xb-tint-error);color:var(--xb-error);", "把本分类已选词条全部从提示词移除", () => {
const keys = selOf().slice();
if (!keys.length) { notify("本分类还没有已选词条", "warning"); return; }
removeKeys(keys);
@@ -2428,7 +2428,7 @@ function renderCategoryPanel(box, api, catId) {
const selected = selOf().includes(entry.key);
const descKey = `${cat.id}|${entry.key}`;
const desc = s.elements.noted[descKey] || "";
const row = el("div", `display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;margin-bottom:3px;box-sizing:border-box;cursor:pointer;user-select:none;background:${selected ? "#22456b" : "#242424"};border:1px solid ${selected ? "#3f86d0" : "#3a3a3a"};`);
const row = el("div", `display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;margin-bottom:3px;box-sizing:border-box;cursor:pointer;user-select:none;background:${selected ? "#22456b" : "var(--xb-bg-raise)"};border:1px solid ${selected ? "#3f86d0" : "var(--xb-border)"};`);
// 悬停整行 → 显示完整词条、当前状态与详细描述(原生 tooltip)
row.title = `${entry.zh || "(空)"} / ${entry.en || "(空)"}`
+ (entry.custom ? "\n(自建槽位)" : "")
@@ -2447,14 +2447,14 @@ function renderCategoryPanel(box, api, catId) {
renderRows();
fire();
});
if (entry.custom) row.append(el("span", "font-size:10px;color:#f0d79a;border:1px solid #8a6d3b;border-radius:3px;padding:0 3px;flex:0 0 auto;", "自建"));
if (entry.custom) row.append(el("span", "font-size:10px;color:var(--xb-warn);border:1px solid #8a6d3b;border-radius:3px;padding:0 3px;flex:0 0 auto;", "自建"));
const rel = relPair(s, cat.id, entry.key);
if (rel) row.append(el("span", "font-size:10px;color:#9fe0c0;border:1px solid #3f7a5f;border-radius:3px;padding:0 3px;flex:0 0 auto;", rel.zh));
const nm = el("span", `flex:1 1 auto;min-width:0;font-size:12px;line-height:1.4;color:${selected ? "#eaf4ff" : "#c8c8c8"};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;`,
`${selected ? "✔ " : ""}${entry.zh || "(空)"} / ${entry.en || "(空)"}`);
row.append(nm);
if (cat.id === "props") {
row.append(smallBtn(rel ? `🔗 ${rel.zh}` : "🔗 关系", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;padding:4px 10px;flex:0 0 auto;", "选择/更换这个道具与主体的关系词(先移旧片段再加新片段)", () => {
row.append(smallBtn(rel ? `🔗 ${rel.zh}` : "🔗 关系", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);padding:4px 10px;flex:0 0 auto;", "选择/更换这个道具与主体的关系词(先移旧片段再加新片段)", () => {
openRelPicker(api.node, `${entry.zh || entry.key} / ${entry.en || ""}`, lang(),
(relPair(s, cat.id, entry.key) || {}).id || "",
(relId) => {
@@ -2466,7 +2466,7 @@ function renderCategoryPanel(box, api, catId) {
}));
}
const descBtn = smallBtn(desc ? "📝 详细描述" : "添加详细描述",
`border:1px solid ${desc ? "#8a6d3b" : "#555"};background:${desc ? "#3a2f16" : "#2a2a2a"};color:${desc ? "#ffd98a" : "#9fc3e8"};padding:4px 10px;flex:0 0 auto;`,
`border:1px solid ${desc ? "#8a6d3b" : "var(--xb-border)"};background:${desc ? "#3a2f16" : "var(--xb-bg-raise)"};color:${desc ? "#ffd98a" : "#9fc3e8"};padding:4px 10px;flex:0 0 auto;`,
desc ? "编辑该词条的详细描述(新窗口)" : "为该词条添加详细描述(新窗口)",
() => openDescEditor(api.node, entry, `${entry.zh || entry.en || entry.key}`, desc, (text) => {
const v = String(text || "").trim();
@@ -2505,7 +2505,7 @@ function renderCategoryPanel(box, api, catId) {
const selected = selOf().includes(it.key);
const descKey = `${cat.id}|${it.key}`;
const desc = s.elements.noted[descKey] || "";
const row = el("div", `display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;margin-bottom:3px;box-sizing:border-box;cursor:pointer;user-select:none;background:${selected ? "#22456b" : "#242424"};border:1px solid ${selected ? "#3f86d0" : "#3a3a3a"};`);
const row = el("div", `display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;margin-bottom:3px;box-sizing:border-box;cursor:pointer;user-select:none;background:${selected ? "#22456b" : "var(--xb-bg-raise)"};border:1px solid ${selected ? "#3f86d0" : "var(--xb-border)"};`);
row.title = `${it.zh || "(未填中文)"} / ${it.en || "(未填英文)"}\n(自建槽位)`
+ `\n${selected ? "已加入提示词 —— 再点一下移除" : "点一下加入提示词"}`
+ "\n\n详细描述:" + (desc || "(还没有,点【添加详细描述】写一条)");
@@ -2515,10 +2515,10 @@ function renderCategoryPanel(box, api, catId) {
rebuild();
fire();
});
row.append(el("span", "font-size:10px;color:#f0d79a;border:1px solid #8a6d3b;border-radius:3px;padding:0 3px;flex:0 0 auto;", "自建"));
row.append(el("span", "font-size:10px;color:var(--xb-warn);border:1px solid #8a6d3b;border-radius:3px;padding:0 3px;flex:0 0 auto;", "自建"));
row.append(el("span", `flex:1 1 auto;min-width:0;font-size:12px;color:${selected ? "#eaf4ff" : "#c8c8c8"};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;`,
`${selected ? "✔ " : ""}${it.zh || "(未填中文)"} / ${it.en || "(未填英文)"}`));
row.append(smallBtn("✎ 编辑名称", "border:1px solid #555;background:#2a2a2a;color:#9fc3e8;padding:4px 10px;flex:0 0 auto;", "新窗口编辑该槽位的中/英词条", () => {
row.append(smallBtn("✎ 编辑名称", "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-info);padding:4px 10px;flex:0 0 auto;", "新窗口编辑该槽位的中/英词条", () => {
openSlotNameEditor(it.zh, it.en, (z, e2) => {
it.zh = z; it.en = e2;
if (entry) { entry.zh = z; entry.en = e2; }
@@ -2528,7 +2528,7 @@ function renderCategoryPanel(box, api, catId) {
});
}));
row.append(smallBtn(desc ? "📝 详细描述" : "添加详细描述",
`border:1px solid ${desc ? "#8a6d3b" : "#555"};background:${desc ? "#3a2f16" : "#2a2a2a"};color:${desc ? "#ffd98a" : "#9fc3e8"};padding:4px 10px;flex:0 0 auto;`,
`border:1px solid ${desc ? "#8a6d3b" : "var(--xb-border)"};background:${desc ? "#3a2f16" : "var(--xb-bg-raise)"};color:${desc ? "#ffd98a" : "#9fc3e8"};padding:4px 10px;flex:0 0 auto;`,
desc ? "编辑该自建词条的详细描述(新窗口)" : "为该自建词条添加详细描述(新窗口)",
() => openDescEditor(api.node, entry || { key: it.key, zh: it.zh, en: it.en }, `${it.zh || it.en || it.key}`, desc, (text) => {
const v = String(text || "").trim();
@@ -2543,7 +2543,7 @@ function renderCategoryPanel(box, api, catId) {
api.refreshPreview(null);
fire();
})));
row.append(smallBtn("🗑", "border:1px solid #a44;background:#3a2020;color:#ffb0b0;width:24px;height:24px;padding:0;line-height:1;flex:0 0 auto;", "删除这个自建槽位", () => {
row.append(smallBtn("🗑", "border:1px solid #a44;background:var(--xb-tint-error);color:var(--xb-error);width:24px;height:24px;padding:0;line-height:1;flex:0 0 auto;", "删除这个自建槽位", () => {
openConfirm("删除自建槽位", `删除自建槽位「${it.zh || it.en || it.key}」?`, () => {
removeKeys([it.key]);
delete s.elements.noted[`${cat.id}|${it.key}`];
@@ -2561,7 +2561,7 @@ function renderCategoryPanel(box, api, catId) {
renderCustom();
const cusBar = el("div", "display:flex;gap:8px;flex-wrap:wrap;margin-top:4px;");
cusBar.append(smallBtn("+ 新增槽位", "border:1px dashed #5b9bd5;background:#2a3a4a;color:#9fc3e8;padding:6px 12px;", "新增一个自定义词条槽位(中/英各填一个)", () => {
cusBar.append(smallBtn("+ 新增槽位", "border:1px dashed #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);padding:6px 12px;", "新增一个自定义词条槽位(中/英各填一个)", () => {
const arr = s.elements.custom[cat.id] || (s.elements.custom[cat.id] = []);
arr.push({ key: `c${Date.now().toString(36)}${Math.floor(Math.random() * 46656).toString(36)}`, zh: "", en: "" });
index = buildIndex(s.elements);
@@ -2569,7 +2569,7 @@ function renderCategoryPanel(box, api, catId) {
rebuild();
fire();
}));
cusBar.append(smallBtn("🗑 删除槽位", "border:1px solid #a44;background:#5a2a2a;color:#ffd0d0;padding:6px 12px;", "删除本分类最后一个自建槽位(要删指定的,用该行右侧的 🗑)", () => {
cusBar.append(smallBtn("🗑 删除槽位", "border:1px solid #a44;background:var(--xb-tint-error);color:var(--xb-error);padding:6px 12px;", "删除本分类最后一个自建槽位(要删指定的,用该行右侧的 🗑)", () => {
const arr = s.elements.custom[cat.id] || [];
if (!arr.length) { notify("本分类还没有自建槽位", "warning"); return; }
const last = arr[arr.length - 1];
@@ -2592,10 +2592,10 @@ function renderCategoryPanel(box, api, catId) {
/** 分类外壳:标题栏总是建,内容懒建(展开才建) */
function build() {
wrap.innerHTML = "";
const headRow = el("div", "display:flex;align-items:center;gap:8px;padding:8px 10px;background:#20242a;cursor:pointer;flex-wrap:wrap;");
const arrow = el("span", "font-size:11px;color:#9fc3e8;width:12px;flex:0 0 12px;", ui.collapsed ? "▶" : "▼");
const cnt = el("span", "font-size:11px;color:#8a97a5;", countText());
headRow.append(arrow, el("span", "font-size:13px;font-weight:600;color:#cfe3f7;", cat.label), cnt,
const headRow = el("div", "display:flex;align-items:center;gap:8px;padding:8px 10px;background:var(--xb-bg-soft);cursor:pointer;flex-wrap:wrap;");
const arrow = el("span", "font-size:11px;color:var(--xb-info);width:12px;flex:0 0 12px;", ui.collapsed ? "▶" : "▼");
const cnt = el("span", "font-size:11px;color:var(--xb-text-dim);", countText());
headRow.append(arrow, el("span", "font-size:13px;font-weight:600;color:var(--xb-info);", cat.label), cnt,
el("span", "font-size:11px;color:#6f7c8a;flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;", cat.tip || ""));
const body = el("div", "padding:8px 10px;");
body.style.display = ui.collapsed ? "none" : "block";
@@ -2810,11 +2810,11 @@ function setupNode(node) {
const btnRow = el("div", "display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:30px;gap:6px;flex:0 0 auto;");
for (const b of PANEL_BUTTONS) {
const cat = catOf(b.panel);
const btn = el("button", "width:100%;height:30px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid #5b9bd5;background:#3a3a3a;color:#eee;font-size:13px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:inherit;", b.label);
const btn = el("button", "width:100%;height:30px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid #5b9bd5;background:var(--xb-border);color:var(--xb-text);font-size:13px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:inherit;", b.label);
btn.title = `打开「${cat.label}」元素面板 —— ${cat.tip}\\n(里面:点选项行加入/移除提示词,【添加详细描述】写补充描述)`;
btn.addEventListener("mousedown", (e) => e.stopPropagation());
btn.addEventListener("mouseenter", () => { btn.style.background = "#4a4a4a"; });
btn.addEventListener("mouseleave", () => { btn.style.background = "#3a3a3a"; });
btn.addEventListener("mouseenter", () => { btn.style.background = "var(--xb-border)"; });
btn.addEventListener("mouseleave", () => { btn.style.background = "var(--xb-border)"; });
btn.addEventListener("click", (e) => {
e.stopPropagation();
const settings = loadSettings(node);
@@ -2871,17 +2871,17 @@ function setupNode(node) {
btn.title = ref && ref.path
? `已选:${ref.name || ref.path}\n${ref.path}\n点击可重新选择`
: `尚未选择${kind === "pose" ? "姿态" : "穿搭"}参考图,点击打开图库${kind === "pose" ? "(输出端口:姿态图)" : "(输出端口:穿搭图)"}`;
btn.style.color = ref && ref.path ? "#d8ffd8" : "#eee";
btn.style.color = ref && ref.path ? "#d8ffd8" : "var(--xb-text)";
};
if (isGalleryNode(node)) {
const refRow = el("div", "display:grid;grid-template-columns:repeat(2,1fr);grid-auto-rows:30px;gap:6px;flex:0 0 auto;");
for (const kind of ["pose", "costume"]) {
const btn = el("button", "width:100%;height:30px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;"
+ "border-radius:6px;border:2px solid #c58a3a;background:#3a3a3a;color:#eee;font-size:13px;cursor:pointer;"
+ "border-radius:6px;border:2px solid #c58a3a;background:var(--xb-border);color:var(--xb-text);font-size:13px;cursor:pointer;"
+ "white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:inherit;", REF_LABELS[kind] + "选择");
btn.addEventListener("mousedown", (e) => e.stopPropagation());
btn.addEventListener("mouseenter", () => { btn.style.background = "#4a4a4a"; });
btn.addEventListener("mouseleave", () => { btn.style.background = "#3a3a3a"; });
btn.addEventListener("mouseenter", () => { btn.style.background = "var(--xb-border)"; });
btn.addEventListener("mouseleave", () => { btn.style.background = "var(--xb-border)"; });
btn.addEventListener("click", (e) => {
e.stopPropagation();
const widgetRef = findWidget(node, REF_FIELDS[kind]);
@@ -2915,7 +2915,7 @@ function setupNode(node) {
// ③ 提示词框(flex:1 1 auto 吃掉剩余高度;原来的「生成详情」状态行已按要求删除)
// 右上角三个小按钮:添加预设 / 编辑预设 / 复制文本(都作用于这一个提示词文本框)
const promptLab = el("div", "font-size:12px;color:#bbb;flex:1 1 auto;min-width:0;",
const promptLab = el("div", "font-size:12px;color:var(--xb-text-dim);flex:1 1 auto;min-width:0;",
"📝 节点提示词(与面板预览框双向同步)");
const promptBtns = el("div", "display:flex;gap:6px;align-items:center;flex:0 0 auto;");
promptBtns.append(
@@ -2972,11 +2972,11 @@ function setupNode(node) {
b.textContent = label;
b.title = tip;
b.style.cssText = "font-size:0.92em;line-height:1.35;height:1.9em;padding:0 0.75em;border-radius:4px;"
+ "border:1px solid #555;background:#2b2b2b;color:#ddd;cursor:pointer;white-space:nowrap;";
+ "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);cursor:pointer;white-space:nowrap;";
b.addEventListener("pointerdown", (e) => e.stopPropagation());
b.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); onClick?.(); });
b.addEventListener("mouseenter", () => { b.style.background = "#3a3a3a"; });
b.addEventListener("mouseleave", () => { b.style.background = "#2b2b2b"; });
b.addEventListener("mouseenter", () => { b.style.background = "var(--xb-border)"; });
b.addEventListener("mouseleave", () => { b.style.background = "var(--xb-bg-raise)"; });
return b;
}
@@ -3040,22 +3040,22 @@ function setupNode(node) {
card.style.cssText = "width:" + (opts.width || MODAL_DEFAULT_WIDTH) + "px;max-width:96vw;max-height:90vh;"
+ "margin:auto;flex:0 0 auto;"
+ "display:flex;flex-direction:column;"
+ "background:#1c1c1e;border:1px solid #333;border-radius:8px;color:#ddd;"
+ "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;color:var(--xb-text);"
+ "font:15px/1.65 system-ui,sans-serif;box-shadow:0 20px 40px rgba(0,0,0,.6);overflow:hidden;";
const head = document.createElement("div");
head.style.cssText = "padding:14px 18px 12px;background:#232326;border-bottom:1px solid #3a3a3a;"
head.style.cssText = "padding:14px 18px 12px;background:var(--xb-bg-soft);border-bottom:1px solid var(--xb-border);"
+ "display:flex;align-items:center;gap:12px;flex-wrap:wrap;";
const titleEl = document.createElement("span");
titleEl.textContent = title;
titleEl.style.cssText = "flex:1 1 300px;min-width:220px;font-size:1.2em;font-weight:700;color:#eee;";
titleEl.style.cssText = "flex:1 1 300px;min-width:220px;font-size:1.2em;font-weight:700;color:var(--xb-text);";
head.append(titleEl);
const tools = document.createElement("span");
tools.style.cssText = "display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-weight:normal;";
// 字号
const fontVal = document.createElement("span");
fontVal.style.cssText = "min-width:26px;text-align:center;color:#e8a87c;font-variant-numeric:tabular-nums;";
fontVal.style.cssText = "min-width:26px;text-align:center;color:var(--xb-orange);font-variant-numeric:tabular-nums;";
fontVal.textContent = String(modalFontGet());
const fontMinus = mkTinyBtn("A−", "缩小弹窗字号(Ctrl/⌘+滚轮 也可)", () => {
modalFontSet(modalFontGet() - 1); fontVal.textContent = String(modalFontGet()); applyModalScale(card);
@@ -3065,7 +3065,7 @@ function setupNode(node) {
});
// 界面缩放
const zoomVal = document.createElement("span");
zoomVal.style.cssText = "min-width:44px;text-align:center;color:#ffd98a;font-variant-numeric:tabular-nums;";
zoomVal.style.cssText = "min-width:44px;text-align:center;color:var(--xb-warn);font-variant-numeric:tabular-nums;";
zoomVal.textContent = Math.round(modalZoomGet() * 100) + "%";
const zoomMinus = mkTinyBtn("−", "缩小整个弹窗界面", () => {
modalZoomSet(modalZoomGet() - 0.1); zoomVal.textContent = Math.round(modalZoomGet() * 100) + "%"; applyModalScale(card);
@@ -3075,9 +3075,9 @@ function setupNode(node) {
});
const closeBtn = mkTinyBtn("✕", "关闭", () => close());
tools.append(
el("span", "color:#8a8a8a;", "字号"), fontMinus, fontVal, fontPlus,
el("span", "color:var(--xb-text-dim);", "字号"), fontMinus, fontVal, fontPlus,
el("span", "color:#5a5a5a;", "|"),
el("span", "color:#8a8a8a;", "界面"), zoomMinus, zoomVal, zoomPlus,
el("span", "color:var(--xb-text-dim);", "界面"), zoomMinus, zoomVal, zoomPlus,
closeBtn,
);
head.append(tools);
@@ -3154,7 +3154,7 @@ function setupNode(node) {
const { bodyEl, close } = openModal("📥 添加预设(点一项 → 加进下面的提示词文本框)");
if (!modes.length) {
const tip = document.createElement("div");
tip.style.cssText = "color:#ffb74d;";
tip.style.cssText = "color:var(--xb-warn);";
tip.textContent = `没有找到预设文件:xb_skill/${PRESET_NODE_NAME}/${data?.lang || "ZH"} 目录里没有 .txt。`
+ "可以先在「编辑预设」里把当前提示词存成一个新预设。";
bodyEl.append(tip);
@@ -3171,23 +3171,23 @@ function setupNode(node) {
const row = document.createElement("button");
row.type = "button";
row.style.cssText = "text-align:left;padding:7px 10px;border-radius:5px;border:1px solid #3d3d3d;"
+ "background:#1f1f1f;color:#ddd;cursor:pointer;font-size:1em;";
+ "background:var(--xb-inset);color:var(--xb-text);cursor:pointer;font-size:1em;";
const nameSpan = document.createElement("div");
nameSpan.textContent = (backupOnly.includes(label) ? "📦 " : "") + label;
nameSpan.style.cssText = "font-weight:bold;color:#8fd3ff;font-size:1.05em;";
nameSpan.style.cssText = "font-weight:bold;color:var(--xb-info);font-size:1.05em;";
nameSpan.title = backupOnly.includes(label) ? "来自备份目录(output/生图提示词预设)" : "来自预设目录(xb_skill)";
const prev = document.createElement("div");
prev.textContent = txt.length > 60 ? txt.slice(0, 60) + "…" : (txt || "(空文件)");
prev.style.cssText = "color:#9a9a9a;font-size:0.9em;margin-top:3px;line-height:1.5;";
row.append(nameSpan, prev);
row.addEventListener("mouseenter", () => { row.style.background = "#2c3a44"; row.style.borderColor = "#4a7a99"; });
row.addEventListener("mouseenter", () => { row.style.background = "var(--xb-bg-raise)"; row.style.borderColor = "#4a7a99"; });
row.addEventListener("mouseleave", () => { row.style.background = "#1f1f1f"; row.style.borderColor = "#3d3d3d"; });
row.addEventListener("click", () => { applyPresetLabel(label, txt); close(); });
list.append(row);
}
bodyEl.append(list);
const bdir = document.createElement("div");
bdir.style.cssText = "color:#8a8a8a;font-size:0.88em;margin-top:6px;word-break:break-all;flex:0 0 auto;";
bdir.style.cssText = "color:var(--xb-text-dim);font-size:0.88em;margin-top:6px;word-break:break-all;flex:0 0 auto;";
bdir.textContent = `📦 备份目录(也会被读取):${data?.backup_dir || "output/生图提示词预设"}`;
const foot = document.createElement("div");
foot.style.cssText = "display:flex;justify-content:flex-end;gap:6px;margin-top:4px;flex:0 0 auto;flex-wrap:wrap;";
@@ -3206,18 +3206,18 @@ function setupNode(node) {
const rowSel = document.createElement("div");
rowSel.style.cssText = "display:flex;gap:6px;align-items:center;flex:1 1 300px;min-width:240px;flex-wrap:nowrap;";
const sel = document.createElement("select");
sel.style.cssText = "flex:1 1 260px;min-width:200px;height:2em;background:#1e1e1e;color:#ddd;"
+ "border:1px solid #444;border-radius:4px;padding:0 6px;font-size:1em;";
rowSel.append(el("span", "color:#aaa;white-space:nowrap;", "已有预设(选中即载入)"), sel);
sel.style.cssText = "flex:1 1 260px;min-width:200px;height:2em;background:var(--xb-bg);color:var(--xb-text);"
+ "border:1px solid var(--xb-border);border-radius:4px;padding:0 6px;font-size:1em;";
rowSel.append(el("span", "color:var(--xb-text-dim);white-space:nowrap;", "已有预设(选中即载入)"), sel);
const rowName = document.createElement("div");
rowName.style.cssText = "display:flex;gap:6px;align-items:center;flex:1 1 260px;min-width:220px;flex-wrap:nowrap;";
rowName.append(el("span", "color:#aaa;white-space:nowrap;", "预设名称"));
rowName.append(el("span", "color:var(--xb-text-dim);white-space:nowrap;", "预设名称"));
const nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.placeholder = "例如:电影感特写(同名 = 覆盖,新名 = 新建)";
nameInput.style.cssText = "flex:1;min-width:140px;height:2em;background:#1e1e1e;color:#ddd;"
+ "border:1px solid #444;border-radius:4px;padding:0 8px;font-size:1em;";
nameInput.style.cssText = "flex:1;min-width:140px;height:2em;background:var(--xb-bg);color:var(--xb-text);"
+ "border:1px solid var(--xb-border);border-radius:4px;padding:0 8px;font-size:1em;";
nameInput.value = lastPresetLabel || "";
rowName.append(nameInput);
@@ -3253,8 +3253,8 @@ function setupNode(node) {
// ── 第 2 行:大文本编辑框 ──
const boxHead = document.createElement("div");
boxHead.style.cssText = "display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:2px;";
const boxLabel = el("span", "color:#9fc3e8;font-weight:bold;", "预设内容(这里是最终会加到提示词最顶端/被保存的正文)");
const charCount = el("span", "color:#8a8a8a;font-variant-numeric:tabular-nums;", "0 字");
const boxLabel = el("span", "color:var(--xb-info);font-weight:bold;", "预设内容(这里是最终会加到提示词最顶端/被保存的正文)");
const charCount = el("span", "color:var(--xb-text-dim);font-variant-numeric:tabular-nums;", "0 字");
const boxHeadRight = document.createElement("div");
boxHeadRight.style.cssText = "display:flex;gap:8px;align-items:center;flex:0 0 auto;";
boxHeadRight.append(charCount, fromPromptBtn);
@@ -3265,7 +3265,7 @@ function setupNode(node) {
box.dataset.autofit = "1";
box.placeholder = "在这里编写预设内容…(在左上方选择已有预设会自动载入到这里,或点「取当前提示词」)";
box.style.cssText = "width:100%;flex:1 1 auto;min-height:300px;resize:vertical;box-sizing:border-box;"
+ "background:#1d1d1d;color:#ddd;border:1px solid #444;border-radius:5px;padding:10px 12px;"
+ "background:var(--xb-bg);color:var(--xb-text);border:1px solid var(--xb-border);border-radius:5px;padding:10px 12px;"
+ "line-height:1.7;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;"
+ "font-family:inherit;font-size:" + modalFontGet() + "px;";
const fitBox = () => {
@@ -3280,11 +3280,11 @@ function setupNode(node) {
// ── 说明 / 状态 ──
const hint = document.createElement("div");
hint.style.cssText = "color:#8a8a8a;font-size:0.88em;word-break:break-all;";
hint.style.cssText = "color:var(--xb-text-dim);font-size:0.88em;word-break:break-all;";
hint.textContent = `保存位置:xb_skill/${PRESET_NODE_NAME}/${data?.lang || "ZH"}/  备份:${data?.backup_dir || "output/生图提示词预设"}`;
const status = document.createElement("div");
status.style.cssText = "min-height:18px;color:#aaa;";
status.style.cssText = "min-height:18px;color:var(--xb-text-dim);";
// ── 底部按钮 ──
const foot = document.createElement("div");
+142 -142
View File
@@ -1303,15 +1303,15 @@ function el(tag, css, text) {
return e;
}
const BOX_CSS = "background:#1d1d1d;color:#ccc;border:1px solid #444;border-radius:4px;padding:5px 8px;font-size:12px;box-sizing:border-box;font-family:inherit;";
const BOX_CSS = "background:var(--xb-bg);color:var(--xb-text);border:1px solid var(--xb-border);border-radius:4px;padding:5px 8px;font-size:12px;box-sizing:border-box;font-family:inherit;";
function makeSectionTitle(text) {
return el("div", "font-size:13px;font-weight:600;color:#9fc3e8;margin:14px 0 8px;padding-bottom:5px;border-bottom:1px solid #333;", text);
return el("div", "font-size:13px;font-weight:600;color:var(--xb-info);margin:14px 0 8px;padding-bottom:5px;border-bottom:1px solid var(--xb-border);", text);
}
function field(labelText, control) {
const g = el("div", "display:flex;align-items:center;gap:12px;margin-bottom:10px;");
const lab = el("label", "flex:0 0 160px;font-size:13px;color:#999;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;", labelText);
const lab = el("label", "flex:0 0 160px;font-size:13px;color:var(--xb-text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;", labelText);
control.style.flex = "1 1 0";
control.style.minWidth = "0";
g.append(lab, control);
@@ -1338,7 +1338,7 @@ function radioRow(options, value, onChange) {
for (const o of options) {
const val = (typeof o === "object" && o !== null && o.value !== undefined) ? String(o.value) : String(o);
const label = (typeof o === "object" && o !== null && o.label) ? String(o.label) : String(o);
const lab = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:#999;cursor:pointer;user-select:none;");
const lab = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:var(--xb-text-dim);cursor:pointer;user-select:none;");
const c = el("input", "width:18px;height:18px;accent-color:#f59e0b;cursor:pointer;");
c.type = "checkbox";
c.checked = String(value) === val;
@@ -1365,7 +1365,7 @@ function textareaControl(value, onChange, css) {
}
function checkboxControl(value, labelText, onChange, tip) {
const w = el("label", "display:flex;align-items:center;gap:8px;cursor:pointer;font-size:12px;color:#ddd;user-select:none;");
const w = el("label", "display:flex;align-items:center;gap:8px;cursor:pointer;font-size:12px;color:var(--xb-text);user-select:none;");
const c = el("input", "width:16px;height:16px;accent-color:#f59e0b;cursor:pointer;");
c.type = "checkbox";
c.checked = !!value;
@@ -1427,10 +1427,10 @@ function zoomSync(root) {
}
function buildZoomRow(root) {
const row = el("span", "display:inline-flex;align-items:center;gap:4px;user-select:none;");
const minus = smallBtn("A−", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;width:22px;height:22px;padding:0;line-height:1;", "缩小编辑字号(Ctrl/⌘+滚轮 也可)");
const val = el("span", "min-width:26px;text-align:center;font-size:12px;color:#e8a87c;font-variant-numeric:tabular-nums;", String(zoomDisplay()));
const minus = smallBtn("A−", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);width:22px;height:22px;padding:0;line-height:1;", "缩小编辑字号(Ctrl/⌘+滚轮 也可)");
const val = el("span", "min-width:26px;text-align:center;font-size:12px;color:var(--xb-orange);font-variant-numeric:tabular-nums;", String(zoomDisplay()));
val.classList.add("ipp-zoom-val");
const plus = smallBtn("A+", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;width:22px;height:22px;padding:0;line-height:1;", "放大编辑字号(Ctrl/⌘+滚轮 也可)");
const plus = smallBtn("A+", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);width:22px;height:22px;padding:0;line-height:1;", "放大编辑字号(Ctrl/⌘+滚轮 也可)");
const step = (d) => {
zoomSet((zoomGet() == null ? 12 : zoomGet()) + d);
zoomApplyContainer(root);
@@ -1474,10 +1474,10 @@ window.addEventListener("resize", () => {
function buildUIRow(target) {
const row = el("span", "display:inline-flex;align-items:center;gap:4px;user-select:none;");
row.append(el("span", "font-size:11px;color:#888;white-space:nowrap;", "界面"));
const minus = smallBtn("−", "border:1px solid #8a6d3b;background:#5a4a2a;color:#ffd98a;width:22px;height:22px;padding:0;line-height:1;font-size:13px;", "缩小整个界面(Ctrl/⌘+滚轮 也可)");
const val = el("span", "min-width:42px;text-align:center;font-size:12px;color:#ffd98a;font-variant-numeric:tabular-nums;", Math.round(uiGet() * 100) + "%");
const minus = smallBtn("−", "border:1px solid #8a6d3b;background:var(--xb-tint-warn);color:var(--xb-warn);width:22px;height:22px;padding:0;line-height:1;font-size:13px;", "缩小整个界面(Ctrl/⌘+滚轮 也可)");
const val = el("span", "min-width:42px;text-align:center;font-size:12px;color:var(--xb-warn);font-variant-numeric:tabular-nums;", Math.round(uiGet() * 100) + "%");
val.classList.add("ipp-ui-val");
const plus = smallBtn("+", "border:1px solid #8a6d3b;background:#5a4a2a;color:#ffd98a;width:22px;height:22px;padding:0;line-height:1;font-size:13px;", "放大整个界面(Ctrl/⌘+滚轮 也可)");
const plus = smallBtn("+", "border:1px solid #8a6d3b;background:var(--xb-tint-warn);color:var(--xb-warn);width:22px;height:22px;padding:0;line-height:1;font-size:13px;", "放大整个界面(Ctrl/⌘+滚轮 也可)");
const step = (d) => {
uiSet(uiGet() + d * UI_STEP);
uiApply(target);
@@ -1741,12 +1741,12 @@ function openPanel(ctx, panelId) {
const title = `${cat.label} 元素`;
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.78);z-index:9999;display:flex;align-items:center;justify-content:center;overflow:auto;");
const dialog = el("section", "background:#1c1c1e;border:1px solid #333;border-radius:8px;width:880px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 20px 40px rgba(0,0,0,.6);flex:0 0 auto;color:#ddd;font-size:13px;");
const dialog = el("section", "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:880px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 20px 40px rgba(0,0,0,.6);flex:0 0 auto;color:var(--xb-text);font-size:13px;");
const header = el("div", "padding:16px 20px 12px;border-bottom:1px solid #444;");
const titleEl = el("div", "font-size:18px;font-weight:700;color:#eee;", title);
const header = el("div", "padding:16px 20px 12px;border-bottom:1px solid var(--xb-border);");
const titleEl = el("div", "font-size:18px;font-weight:700;color:var(--xb-text);", title);
header.append(titleEl);
header.append(el("div", "font-size:12px;color:#999;margin-top:5px;", "点选项行即加入提示词(变蓝),再点一下移除(变灰);点【添加详细描述】会在新窗口里给该词条写补充描述(悬停行可看)。配置随工作流保存,改完重新执行节点即生效。"));
header.append(el("div", "font-size:12px;color:var(--xb-text-dim);margin-top:5px;", "点选项行即加入提示词(变蓝),再点一下移除(变灰);点【添加详细描述】会在新窗口里给该词条写补充描述(悬停行可看)。配置随工作流保存,改完重新执行节点即生效。"));
dialog.append(header);
// ⚠️ 顶部不留内边距:滚动容器的 padding 区不参与裁剪,滚上去的行会从这条缝里
@@ -1754,12 +1754,12 @@ function openPanel(ctx, panelId) {
const panelBox = el("div", "padding:0 20px 14px;overflow-y:auto;flex:1;min-height:280px;");
// ── 吸顶预览框(不受滚动条约束,始终在画面内) ──
const sticky = el("div", "position:sticky;top:0;z-index:6;background:#1c1c1e;padding:16px 0 10px;margin:0 0 6px;border-bottom:1px solid #333;");
const sticky = el("div", "position:sticky;top:0;z-index:6;background:var(--xb-bg);padding:16px 0 10px;margin:0 0 6px;border-bottom:1px solid var(--xb-border);");
const head = el("div", "display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap;");
head.append(el("span", "font-size:12px;font-weight:600;color:#9fc3e8;", "📝 提示词预览(与节点提示词框实时同步,可直接编辑)"));
head.append(el("span", "font-size:12px;font-weight:600;color:var(--xb-info);", "📝 提示词预览(与节点提示词框实时同步,可直接编辑)"));
const headCount = el("span", "font-size:11px;color:#888;", "");
head.append(headCount);
const presetChip = el("div", "font-size:11px;color:#f0d79a;background:#2a2416;border:1px dashed #8a6d3b;border-radius:4px;padding:4px 8px;margin-bottom:6px;white-space:pre-wrap;line-height:1.5;display:none;");
const presetChip = el("div", "font-size:11px;color:var(--xb-warn);background:var(--xb-tint-warn);border:1px dashed #8a6d3b;border-radius:4px;padding:4px 8px;margin-bottom:6px;white-space:pre-wrap;line-height:1.5;display:none;");
const preview = el("textarea", BOX_CSS + "width:100%;min-height:74px;max-height:210px;resize:vertical;line-height:1.65;font-size:12px;");
preview.spellcheck = false;
preview.placeholder = "点左侧 + 添加元素,或直接在这里手写提示词…";
@@ -1800,10 +1800,10 @@ function openPanel(ctx, panelId) {
// ── 底部 ──
const error = el("div", "color:#e55;font-size:12px;margin:0 20px;min-height:16px;");
const footer = el("div", "padding:12px 20px;border-top:1px solid #444;display:flex;justify-content:flex-end;gap:10px;background:#18181a;border-bottom-left-radius:8px;border-bottom-right-radius:8px;");
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:8px 20px;font-size:14px;cursor:pointer;font-family:inherit;", "取消");
const saveBtn = el("button", "background:#2d5a88;color:#fff;border:none;border-radius:4px;padding:8px 20px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;", "💾 保存");
const autoRow = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:#999;cursor:pointer;user-select:none;");
const footer = el("div", "padding:12px 20px;border-top:1px solid var(--xb-border);display:flex;justify-content:flex-end;gap:10px;background:var(--xb-bg-soft);border-bottom-left-radius:8px;border-bottom-right-radius:8px;");
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:8px 20px;font-size:14px;cursor:pointer;font-family:inherit;", "取消");
const saveBtn = el("button", "background:var(--xb-btn-primary);color:#fff;border:none;border-radius:4px;padding:8px 20px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;", "💾 保存");
const autoRow = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:var(--xb-text-dim);cursor:pointer;user-select:none;");
const autoCb = el("input", "width:18px;height:18px;accent-color:#f59e0b;cursor:pointer;");
autoCb.type = "checkbox";
autoCb.checked = settings.auto_save !== false;
@@ -1888,13 +1888,13 @@ const CAT_UI = {}; // 会话级:cat -> { collapsed, onlySel, keyword }(不
* ============================================================ */
function openConfirm(title, message, onOk, okText) {
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:10200;display:flex;align-items:center;justify-content:center;");
const box = el("section", "background:#1c1c1e;border:1px solid #333;border-radius:8px;width:460px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:#ddd;font-size:13px;");
const box = el("section", "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:460px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:var(--xb-text);font-size:13px;");
box.dataset.ippModal = "confirm";
box.append(el("div", "font-size:15px;font-weight:700;color:#eee;margin-bottom:8px;", title || "确认"));
box.append(el("div", "font-size:13px;color:#bbb;line-height:1.8;white-space:pre-wrap;margin-bottom:14px;", message || ""));
box.append(el("div", "font-size:15px;font-weight:700;color:var(--xb-text);margin-bottom:8px;", title || "确认"));
box.append(el("div", "font-size:13px;color:var(--xb-text-dim);line-height:1.8;white-space:pre-wrap;margin-bottom:14px;", message || ""));
const row = el("div", "display:flex;gap:10px;justify-content:flex-end;");
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const okBtn = el("button", "background:#8a3a2a;border:1px solid #c2604a;color:#ffe7df;border-radius:4px;padding:7px 18px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", okText || "确定");
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const okBtn = el("button", "background:var(--xb-tint-error);border:1px solid #c2604a;color:#ffe7df;border-radius:4px;padding:7px 18px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", okText || "确定");
const close = () => { try { overlay.remove(); } catch (_) {} };
cancelBtn.addEventListener("click", (e) => { e.stopPropagation(); close(); });
okBtn.addEventListener("click", (e) => { e.stopPropagation(); close(); try { onOk && onOk(); } catch (_) {} });
@@ -1912,29 +1912,29 @@ function openConfirm(title, message, onOk, okText) {
* ============================================================ */
function openRelPicker(node, nameText, lang, current, onPick) {
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.78);z-index:10200;display:flex;align-items:center;justify-content:center;");
const box = el("section", "background:#1c1c1e;border:1px solid #333;border-radius:8px;width:780px;max-width:94vw;max-height:92vh;overflow:auto;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:#ddd;font-size:13px;");
const box = el("section", "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:780px;max-width:94vw;max-height:92vh;overflow:auto;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:var(--xb-text);font-size:13px;");
box.dataset.ippModal = "rel";
box.append(el("div", "font-size:15px;font-weight:700;color:#eee;margin-bottom:4px;", "🎒 道具与主体的关系"));
box.append(el("div", "font-size:12px;color:#8a97a5;margin-bottom:12px;line-height:1.7;",
box.append(el("div", "font-size:15px;font-weight:700;color:var(--xb-text);margin-bottom:4px;", "🎒 道具与主体的关系"));
box.append(el("div", "font-size:12px;color:var(--xb-text-dim);margin-bottom:12px;line-height:1.7;",
`道具:${nameText}\n选一个关系词,拼进提示词时它会加在道具前面(例:手拿折扇 / a folding fan, held in the hand)。`));
const grid = el("div", "display:grid;grid-template-columns:repeat(6,1fr);gap:6px;margin-bottom:14px;");
const close = () => { try { overlay.remove(); } catch (_) {} };
for (const [id, zh, en] of RELATIONS) {
const on = id === current;
const b = el("button", `display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;min-height:46px;border-radius:6px;cursor:pointer;font-family:inherit;padding:4px 2px;box-sizing:border-box;border:1px solid ${on ? "#7fb3e0" : "#4a4a4a"};background:${on ? "#2d5a88" : "#262626"};color:${on ? "#eaf4ff" : "#ddd"};`);
const b = el("button", `display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;min-height:46px;border-radius:6px;cursor:pointer;font-family:inherit;padding:4px 2px;box-sizing:border-box;border:1px solid ${on ? "#7fb3e0" : "var(--xb-border)"};background:${on ? "#2d5a88" : "var(--xb-bg-raise)"};color:${on ? "#eaf4ff" : "var(--xb-text)"};`);
b.dataset.rel = id;
b.title = `${zh} / ${en}`;
b.append(el("span", "font-size:13px;line-height:1.2;", zh));
b.append(el("span", "font-size:9px;line-height:1.2;color:#9fb6cc;text-align:center;word-break:break-word;", en));
b.addEventListener("mouseenter", () => { if (!on) b.style.background = "#333"; });
b.addEventListener("mouseleave", () => { if (!on) b.style.background = "#262626"; });
b.addEventListener("mouseenter", () => { if (!on) b.style.background = "var(--xb-border)"; });
b.addEventListener("mouseleave", () => { if (!on) b.style.background = "var(--xb-bg-raise)"; });
b.addEventListener("click", (e) => { e.stopPropagation(); close(); try { onPick && onPick(id); } catch (_) {} });
grid.append(b);
}
box.append(grid);
const row = el("div", "display:flex;gap:10px;justify-content:flex-end;");
const noneBtn = el("button", "background:#2a4a6a;border:1px solid #5b9bd5;color:#cfe3f7;border-radius:4px;padding:7px 16px;font-size:13px;cursor:pointer;font-family:inherit;", "不加关系(只加道具词)");
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const noneBtn = el("button", "background:var(--xb-tint-info);border:1px solid #5b9bd5;color:var(--xb-info);border-radius:4px;padding:7px 16px;font-size:13px;cursor:pointer;font-family:inherit;", "不加关系(只加道具词)");
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
noneBtn.addEventListener("click", (e) => { e.stopPropagation(); close(); try { onPick && onPick(""); } catch (_) {} });
cancelBtn.addEventListener("click", (e) => { e.stopPropagation(); close(); });
overlay.addEventListener("mousedown", (e) => { if (e.target === overlay) close(); });
@@ -1952,10 +1952,10 @@ function openRelPicker(node, nameText, lang, current, onPick) {
* ============================================================ */
function openDescEditor(node, entry, nameText, value, onSave) {
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:10100;display:flex;align-items:center;justify-content:center;");
const box = el("section", "background:#1c1c1e;border:1px solid #333;border-radius:8px;width:600px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:#ddd;font-size:13px;");
const box = el("section", "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:600px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:var(--xb-text);font-size:13px;");
box.dataset.ippModal = "desc"; // 稳定锤点(便于定位/自动化测试)
box.append(el("div", "font-size:15px;font-weight:700;color:#eee;margin-bottom:4px;", "📝 添加详细描述"));
box.append(el("div", "font-size:12px;color:#8a97a5;margin-bottom:10px;line-height:1.7;white-space:pre-wrap;",
box.append(el("div", "font-size:15px;font-weight:700;color:var(--xb-text);margin-bottom:4px;", "📝 添加详细描述"));
box.append(el("div", "font-size:12px;color:var(--xb-text-dim);margin-bottom:10px;line-height:1.7;white-space:pre-wrap;",
`词条:${nameText}\n`
+ "写在这里的内容会以「词条(描述)」的形式拼进提示词(例:一位女性(银灰色短发)),"
+ "所以只写要补的细节即可 —— 不用重复词条本身,也别写整句话。"));
@@ -1967,9 +1967,9 @@ function openDescEditor(node, entry, nameText, value, onSave) {
const tip = el("div", "font-size:11px;color:#777;margin-top:6px;", "Esc = 取消 · Ctrl/⌘ + Enter = 保存");
box.append(tip);
const row = el("div", "display:flex;justify-content:flex-end;gap:10px;margin-top:14px;");
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const clrBtn = el("button", "background:transparent;border:1px solid #844;color:#ffb0b0;border-radius:4px;padding:7px 14px;font-size:13px;cursor:pointer;font-family:inherit;margin-right:auto;", "🗑 清除描述");
const okBtn = el("button", "background:#2d5a88;color:#fff;border:none;border-radius:4px;padding:7px 20px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", "✅ 保存描述");
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const clrBtn = el("button", "background:transparent;border:1px solid #844;color:var(--xb-error);border-radius:4px;padding:7px 14px;font-size:13px;cursor:pointer;font-family:inherit;margin-right:auto;", "🗑 清除描述");
const okBtn = el("button", "background:var(--xb-btn-primary);color:#fff;border:none;border-radius:4px;padding:7px 20px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", "✅ 保存描述");
row.append(clrBtn, cancelBtn, okBtn);
box.append(row);
overlay.append(box);
@@ -1993,11 +1993,11 @@ function openDescEditor(node, entry, nameText, value, onSave) {
/** 自建槽位的「中/英名称」编辑窗(同样是一个小弹窗) */
function openSlotNameEditor(zhValue, enValue, onSave) {
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:10100;display:flex;align-items:center;justify-content:center;");
const box = el("section", "background:#1c1c1e;border:1px solid #333;border-radius:8px;width:520px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:#ddd;font-size:13px;");
const box = el("section", "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:520px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:var(--xb-text);font-size:13px;");
box.dataset.ippModal = "slotname"; // 稳定锤点
box.append(el("div", "font-size:15px;font-weight:700;color:#eee;margin-bottom:10px;", "✎ 自建槽位名称(中 / 英)"));
box.append(el("div", "font-size:15px;font-weight:700;color:var(--xb-text);margin-bottom:10px;", "✎ 自建槽位名称(中 / 英)"));
const mk = (label, val, ph) => {
box.append(el("div", "font-size:11px;color:#8a97a5;margin:6px 0 3px;", label));
box.append(el("div", "font-size:11px;color:var(--xb-text-dim);margin:6px 0 3px;", label));
const i = el("input", BOX_CSS + "width:100%;font-size:13px;padding:6px 8px;");
i.type = "text";
i.value = val || "";
@@ -2008,8 +2008,8 @@ function openSlotNameEditor(zhValue, enValue, onSave) {
const zh = mk("中文词条(中文 [ZH] 输出时用)", zhValue, "例:银灰色短发");
const en = mk("English word(英文 [EN] 输出时用)", enValue, "e.g. silver-gray short hair");
const row = el("div", "display:flex;justify-content:flex-end;gap:10px;margin-top:14px;");
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const okBtn = el("button", "background:#2d5a88;color:#fff;border:none;border-radius:4px;padding:7px 20px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", "✅ 保存名称");
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const okBtn = el("button", "background:var(--xb-btn-primary);color:#fff;border:none;border-radius:4px;padding:7px 20px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", "✅ 保存名称");
row.append(cancelBtn, okBtn);
box.append(row);
overlay.append(box);
@@ -2044,12 +2044,12 @@ function renderCategoryPanel(box, api, catId) {
catRow.dataset.ippCatRow = "1";
for (const c of CATEGORIES) {
const on = c.id === catId;
const b = el("button", `box-sizing:border-box;height:30px;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid ${on ? "#7fb3e0" : "#5b9bd5"};background:${on ? "#2d5a88" : "#3a3a3a"};color:${on ? "#eaf4ff" : "#eee"};font-size:13px;cursor:pointer;font-family:inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;`, c.label);
const b = el("button", `box-sizing:border-box;height:30px;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid ${on ? "#7fb3e0" : "#5b9bd5"};background:${on ? "#2d5a88" : "var(--xb-border)"};color:${on ? "#eaf4ff" : "var(--xb-text)"};font-size:13px;cursor:pointer;font-family:inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;`, c.label);
b.dataset.cat = c.id;
if (on) b.dataset.catActive = "1";
b.title = `切到「${c.label}」面板 —— ${c.tip}`;
b.addEventListener("mouseenter", () => { if (!on) b.style.background = "#4a4a4a"; });
b.addEventListener("mouseleave", () => { if (!on) b.style.background = "#3a3a3a"; });
b.addEventListener("mouseenter", () => { if (!on) b.style.background = "var(--xb-border)"; });
b.addEventListener("mouseleave", () => { if (!on) b.style.background = "var(--xb-border)"; });
b.addEventListener("click", (e) => {
e.stopPropagation();
if (!on && api.switchCategory) api.switchCategory(c.id);
@@ -2059,7 +2059,7 @@ function renderCategoryPanel(box, api, catId) {
box.append(catRow);
const bar = el("div", "display:flex;gap:8px;flex-wrap:wrap;margin-bottom:6px;");
bar.append(
smallBtn("🧹 清空提示词", "border:1px solid #a44;background:#7a2a2a;color:#ffd0d0;padding:6px 12px;", "清空正文(同时清掉所有元素的「已选」状态)", () => {
smallBtn("🧹 清空提示词", "border:1px solid #a44;background:var(--xb-tint-error);color:var(--xb-error);padding:6px 12px;", "清空正文(同时清掉所有元素的「已选」状态)", () => {
openConfirm("🧹 清空提示词", "确定清空提示词正文?\n(所有元素的「已选」状态也会一起清掉)", () => {
s.elements.selected = {};
s.elements.rel = {};
@@ -2069,17 +2069,17 @@ function renderCategoryPanel(box, api, catId) {
notify("已清空提示词", "success");
});
}),
smallBtn("🔧 按选择重建", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;padding:6px 12px;", "按「已选元素 + 固定分类顺序(风格→视角→主体→姿态→装扮→道具→光影→背景)+ 当前语言」重排正文(手工改乱了就用它)", () => {
smallBtn("🔧 按选择重建", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);padding:6px 12px;", "按「已选元素 + 固定分类顺序(风格→视角→主体→姿态→装扮→道具→光影→背景)+ 当前语言」重排正文(手工改乱了就用它)", () => {
const t = rebuildBody(s, index, lang());
bodySet(t);
fire();
notify(t ? "已按已选元素重建正文" : "当前没有已选元素(重建后正文为空)", t ? "success" : "warning");
}),
smallBtn("📋 复制成句", "border:1px solid #555;background:#2a2a2a;color:#ccc;padding:6px 12px;", "复制最终成句(三视图模式下含预设句)", () => {
smallBtn("📋 复制成句", "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);padding:6px 12px;", "复制最终成句(三视图模式下含预设句)", () => {
copyText(finalPrompt(surf.mode, surf.presetText, surf.lang, bodyGet(), surf.io));
}),
smallBtn("⬆️ 导出词表", "border:1px solid #6b9b5b;background:#3a5a2a;color:#d7f0c8;padding:6px 12px;", "把「自建槽位 + 已选 + 描述」导出成 JSON 备份", () => exportJson(s)),
smallBtn("⬇️ 导入词表", "border:1px solid #6b9b5b;background:#3a5a2a;color:#d7f0c8;padding:6px 12px;", "从 JSON 恢复「自建槽位 + 已选 + 描述」(合并,不覆盖内置词表)", () => importJson((obj) => {
smallBtn("⬆️ 导出词表", "border:1px solid #6b9b5b;background:var(--xb-tint-ok);color:var(--xb-ok);padding:6px 12px;", "把「自建槽位 + 已选 + 描述」导出成 JSON 备份", () => exportJson(s)),
smallBtn("⬇️ 导入词表", "border:1px solid #6b9b5b;background:var(--xb-tint-ok);color:var(--xb-ok);padding:6px 12px;", "从 JSON 恢复「自建槽位 + 已选 + 描述」(合并,不覆盖内置词表)", () => importJson((obj) => {
if (!obj || typeof obj !== "object") return;
for (const k of ["custom", "noted", "selected"]) {
if (obj[k] && typeof obj[k] === "object") s.elements[k] = Object.assign({}, s.elements[k], obj[k]);
@@ -2111,7 +2111,7 @@ function renderCategoryPanel(box, api, catId) {
// 436 条词条只在第一次渲染时建 DOM;标题栏仍可点一下收起(纯会话状态)
if (!CAT_UI[cat.id]) CAT_UI[cat.id] = { collapsed: false, onlySel: false, keyword: "", sub: "" };
const ui = CAT_UI[cat.id];
const wrap = el("div", "border:1px solid #333;border-radius:6px;margin-bottom:8px;background:#191919;overflow:hidden;");
const wrap = el("div", "border:1px solid var(--xb-border);border-radius:6px;margin-bottom:8px;background:var(--xb-inset);overflow:hidden;");
const selOf = () => (s.elements.selected[cat.id] || []);
const selArr = () => (s.elements.selected[cat.id] || (s.elements.selected[cat.id] = []));
const rebuild = () => { wrap.innerHTML = ""; build(); };
@@ -2186,14 +2186,14 @@ function renderCategoryPanel(box, api, catId) {
const cur = subOf(cat, ui.sub).id;
for (const c of chipRefs) {
const on = c.dataset.sub === cur;
c.style.background = on ? "#2d5a88" : "#2a2a2a";
c.style.borderColor = on ? "#7fb3e0" : "#555";
c.style.background = on ? "#2d5a88" : "var(--xb-bg-raise)";
c.style.borderColor = on ? "#7fb3e0" : "var(--xb-border)";
c.style.color = on ? "#eaf4ff" : "#bbb";
c.style.fontWeight = on ? "600" : "400";
}
};
for (const sub of (cat.subs || [])) {
const b = el("button", "border:1px solid #555;border-radius:14px;padding:4px 14px;font-size:12px;cursor:pointer;font-family:inherit;white-space:nowrap;", `${sub.label}(${(sub.keys || []).length})`);
const b = el("button", "border:1px solid var(--xb-border);border-radius:14px;padding:4px 14px;font-size:12px;cursor:pointer;font-family:inherit;white-space:nowrap;", `${sub.label}(${(sub.keys || []).length})`);
b.dataset.sub = sub.id;
b.title = `切到「${sub.label}」子类(${(sub.keys || []).length} 条)`;
b.addEventListener("mousedown", (e) => e.stopPropagation());
@@ -2243,7 +2243,7 @@ function renderCategoryPanel(box, api, catId) {
fire();
notify(n ? `已加入 ${n} 条` : "这些词条都已在提示词里", n ? "success" : "warning");
};
tb.append(smallBtn("+ 全部加入", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;", "把本分类当前可见的词条全部追加进提示词", () => {
tb.append(smallBtn("+ 全部加入", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);", "把本分类当前可见的词条全部追加进提示词", () => {
const list = visibleEntries();
if (!list.length) { notify("没有可加入的词条", "warning"); return; }
if (list.length > 60) {
@@ -2252,7 +2252,7 @@ function renderCategoryPanel(box, api, catId) {
}
addAllNow(list);
}));
tb.append(smallBtn("🗑 全部移除", "border:1px solid #a44;background:#5a2a2a;color:#ffd0d0;", "把本分类已选词条全部从提示词移除", () => {
tb.append(smallBtn("🗑 全部移除", "border:1px solid #a44;background:var(--xb-tint-error);color:var(--xb-error);", "把本分类已选词条全部从提示词移除", () => {
const keys = selOf().slice();
if (!keys.length) { notify("本分类还没有已选词条", "warning"); return; }
removeKeys(keys);
@@ -2282,7 +2282,7 @@ function renderCategoryPanel(box, api, catId) {
const selected = selOf().includes(entry.key);
const descKey = `${cat.id}|${entry.key}`;
const desc = s.elements.noted[descKey] || "";
const row = el("div", `display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;margin-bottom:3px;box-sizing:border-box;cursor:pointer;user-select:none;background:${selected ? "#22456b" : "#242424"};border:1px solid ${selected ? "#3f86d0" : "#3a3a3a"};`);
const row = el("div", `display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;margin-bottom:3px;box-sizing:border-box;cursor:pointer;user-select:none;background:${selected ? "#22456b" : "var(--xb-bg-raise)"};border:1px solid ${selected ? "#3f86d0" : "var(--xb-border)"};`);
// 悬停整行 → 显示完整词条、当前状态与详细描述(原生 tooltip)
row.title = `${entry.zh || "(空)"} / ${entry.en || "(空)"}`
+ (entry.custom ? "\n(自建槽位)" : "")
@@ -2301,14 +2301,14 @@ function renderCategoryPanel(box, api, catId) {
renderRows();
fire();
});
if (entry.custom) row.append(el("span", "font-size:10px;color:#f0d79a;border:1px solid #8a6d3b;border-radius:3px;padding:0 3px;flex:0 0 auto;", "自建"));
if (entry.custom) row.append(el("span", "font-size:10px;color:var(--xb-warn);border:1px solid #8a6d3b;border-radius:3px;padding:0 3px;flex:0 0 auto;", "自建"));
const rel = relPair(s, cat.id, entry.key);
if (rel) row.append(el("span", "font-size:10px;color:#9fe0c0;border:1px solid #3f7a5f;border-radius:3px;padding:0 3px;flex:0 0 auto;", rel.zh));
const nm = el("span", `flex:1 1 auto;min-width:0;font-size:12px;line-height:1.4;color:${selected ? "#eaf4ff" : "#c8c8c8"};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;`,
`${selected ? "✔ " : ""}${entry.zh || "(空)"} / ${entry.en || "(空)"}`);
row.append(nm);
if (cat.id === "props") {
row.append(smallBtn(rel ? `🔗 ${rel.zh}` : "🔗 关系", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;padding:4px 10px;flex:0 0 auto;", "选择/更换这个道具与主体的关系词(先移旧片段再加新片段)", () => {
row.append(smallBtn(rel ? `🔗 ${rel.zh}` : "🔗 关系", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);padding:4px 10px;flex:0 0 auto;", "选择/更换这个道具与主体的关系词(先移旧片段再加新片段)", () => {
openRelPicker(api.node, `${entry.zh || entry.key} / ${entry.en || ""}`, lang(),
(relPair(s, cat.id, entry.key) || {}).id || "",
(relId) => {
@@ -2320,7 +2320,7 @@ function renderCategoryPanel(box, api, catId) {
}));
}
const descBtn = smallBtn(desc ? "📝 详细描述" : "添加详细描述",
`border:1px solid ${desc ? "#8a6d3b" : "#555"};background:${desc ? "#3a2f16" : "#2a2a2a"};color:${desc ? "#ffd98a" : "#9fc3e8"};padding:4px 10px;flex:0 0 auto;`,
`border:1px solid ${desc ? "#8a6d3b" : "var(--xb-border)"};background:${desc ? "#3a2f16" : "var(--xb-bg-raise)"};color:${desc ? "#ffd98a" : "#9fc3e8"};padding:4px 10px;flex:0 0 auto;`,
desc ? "编辑该词条的详细描述(新窗口)" : "为该词条添加详细描述(新窗口)",
() => openDescEditor(api.node, entry, `${entry.zh || entry.en || entry.key}`, desc, (text) => {
const v = String(text || "").trim();
@@ -2359,7 +2359,7 @@ function renderCategoryPanel(box, api, catId) {
const selected = selOf().includes(it.key);
const descKey = `${cat.id}|${it.key}`;
const desc = s.elements.noted[descKey] || "";
const row = el("div", `display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;margin-bottom:3px;box-sizing:border-box;cursor:pointer;user-select:none;background:${selected ? "#22456b" : "#242424"};border:1px solid ${selected ? "#3f86d0" : "#3a3a3a"};`);
const row = el("div", `display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;margin-bottom:3px;box-sizing:border-box;cursor:pointer;user-select:none;background:${selected ? "#22456b" : "var(--xb-bg-raise)"};border:1px solid ${selected ? "#3f86d0" : "var(--xb-border)"};`);
row.title = `${it.zh || "(未填中文)"} / ${it.en || "(未填英文)"}\n(自建槽位)`
+ `\n${selected ? "已加入提示词 —— 再点一下移除" : "点一下加入提示词"}`
+ "\n\n详细描述:" + (desc || "(还没有,点【添加详细描述】写一条)");
@@ -2369,10 +2369,10 @@ function renderCategoryPanel(box, api, catId) {
rebuild();
fire();
});
row.append(el("span", "font-size:10px;color:#f0d79a;border:1px solid #8a6d3b;border-radius:3px;padding:0 3px;flex:0 0 auto;", "自建"));
row.append(el("span", "font-size:10px;color:var(--xb-warn);border:1px solid #8a6d3b;border-radius:3px;padding:0 3px;flex:0 0 auto;", "自建"));
row.append(el("span", `flex:1 1 auto;min-width:0;font-size:12px;color:${selected ? "#eaf4ff" : "#c8c8c8"};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;`,
`${selected ? "✔ " : ""}${it.zh || "(未填中文)"} / ${it.en || "(未填英文)"}`));
row.append(smallBtn("✎ 编辑名称", "border:1px solid #555;background:#2a2a2a;color:#9fc3e8;padding:4px 10px;flex:0 0 auto;", "新窗口编辑该槽位的中/英词条", () => {
row.append(smallBtn("✎ 编辑名称", "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-info);padding:4px 10px;flex:0 0 auto;", "新窗口编辑该槽位的中/英词条", () => {
openSlotNameEditor(it.zh, it.en, (z, e2) => {
it.zh = z; it.en = e2;
if (entry) { entry.zh = z; entry.en = e2; }
@@ -2382,7 +2382,7 @@ function renderCategoryPanel(box, api, catId) {
});
}));
row.append(smallBtn(desc ? "📝 详细描述" : "添加详细描述",
`border:1px solid ${desc ? "#8a6d3b" : "#555"};background:${desc ? "#3a2f16" : "#2a2a2a"};color:${desc ? "#ffd98a" : "#9fc3e8"};padding:4px 10px;flex:0 0 auto;`,
`border:1px solid ${desc ? "#8a6d3b" : "var(--xb-border)"};background:${desc ? "#3a2f16" : "var(--xb-bg-raise)"};color:${desc ? "#ffd98a" : "#9fc3e8"};padding:4px 10px;flex:0 0 auto;`,
desc ? "编辑该自建词条的详细描述(新窗口)" : "为该自建词条添加详细描述(新窗口)",
() => openDescEditor(api.node, entry || { key: it.key, zh: it.zh, en: it.en }, `${it.zh || it.en || it.key}`, desc, (text) => {
const v = String(text || "").trim();
@@ -2397,7 +2397,7 @@ function renderCategoryPanel(box, api, catId) {
api.refreshPreview(null);
fire();
})));
row.append(smallBtn("🗑", "border:1px solid #a44;background:#3a2020;color:#ffb0b0;width:24px;height:24px;padding:0;line-height:1;flex:0 0 auto;", "删除这个自建槽位", () => {
row.append(smallBtn("🗑", "border:1px solid #a44;background:var(--xb-tint-error);color:var(--xb-error);width:24px;height:24px;padding:0;line-height:1;flex:0 0 auto;", "删除这个自建槽位", () => {
openConfirm("删除自建槽位", `删除自建槽位「${it.zh || it.en || it.key}」?`, () => {
removeKeys([it.key]);
delete s.elements.noted[`${cat.id}|${it.key}`];
@@ -2415,7 +2415,7 @@ function renderCategoryPanel(box, api, catId) {
renderCustom();
const cusBar = el("div", "display:flex;gap:8px;flex-wrap:wrap;margin-top:4px;");
cusBar.append(smallBtn("+ 新增槽位", "border:1px dashed #5b9bd5;background:#2a3a4a;color:#9fc3e8;padding:6px 12px;", "新增一个自定义词条槽位(中/英各填一个)", () => {
cusBar.append(smallBtn("+ 新增槽位", "border:1px dashed #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);padding:6px 12px;", "新增一个自定义词条槽位(中/英各填一个)", () => {
const arr = s.elements.custom[cat.id] || (s.elements.custom[cat.id] = []);
arr.push({ key: `c${Date.now().toString(36)}${Math.floor(Math.random() * 46656).toString(36)}`, zh: "", en: "" });
index = buildIndex(s.elements);
@@ -2423,7 +2423,7 @@ function renderCategoryPanel(box, api, catId) {
rebuild();
fire();
}));
cusBar.append(smallBtn("🗑 删除槽位", "border:1px solid #a44;background:#5a2a2a;color:#ffd0d0;padding:6px 12px;", "删除本分类最后一个自建槽位(要删指定的,用该行右侧的 🗑)", () => {
cusBar.append(smallBtn("🗑 删除槽位", "border:1px solid #a44;background:var(--xb-tint-error);color:var(--xb-error);padding:6px 12px;", "删除本分类最后一个自建槽位(要删指定的,用该行右侧的 🗑)", () => {
const arr = s.elements.custom[cat.id] || [];
if (!arr.length) { notify("本分类还没有自建槽位", "warning"); return; }
const last = arr[arr.length - 1];
@@ -2446,10 +2446,10 @@ function renderCategoryPanel(box, api, catId) {
/** 分类外壳:标题栏总是建,内容懒建(展开才建) */
function build() {
wrap.innerHTML = "";
const headRow = el("div", "display:flex;align-items:center;gap:8px;padding:8px 10px;background:#20242a;cursor:pointer;flex-wrap:wrap;");
const arrow = el("span", "font-size:11px;color:#9fc3e8;width:12px;flex:0 0 12px;", ui.collapsed ? "▶" : "▼");
const cnt = el("span", "font-size:11px;color:#8a97a5;", countText());
headRow.append(arrow, el("span", "font-size:13px;font-weight:600;color:#cfe3f7;", cat.label), cnt,
const headRow = el("div", "display:flex;align-items:center;gap:8px;padding:8px 10px;background:var(--xb-bg-soft);cursor:pointer;flex-wrap:wrap;");
const arrow = el("span", "font-size:11px;color:var(--xb-info);width:12px;flex:0 0 12px;", ui.collapsed ? "▶" : "▼");
const cnt = el("span", "font-size:11px;color:var(--xb-text-dim);", countText());
headRow.append(arrow, el("span", "font-size:13px;font-weight:600;color:var(--xb-info);", cat.label), cnt,
el("span", "font-size:11px;color:#6f7c8a;flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;", cat.tip || ""));
const body = el("div", "padding:8px 10px;");
body.style.display = ui.collapsed ? "none" : "block";
@@ -2673,11 +2673,11 @@ function setupNode(node) {
const btnRow = el("div", "display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:30px;gap:6px;flex:0 0 auto;");
for (const b of PANEL_BUTTONS) {
const cat = catOf(b.panel);
const btn = el("button", "width:100%;height:30px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid #5b9bd5;background:#3a3a3a;color:#eee;font-size:13px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:inherit;", b.label);
const btn = el("button", "width:100%;height:30px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid #5b9bd5;background:var(--xb-border);color:var(--xb-text);font-size:13px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:inherit;", b.label);
btn.title = `打开「${cat.label}」元素面板 —— ${cat.tip}\\n(里面:点选项行加入/移除提示词,【添加详细描述】写补充描述)`;
btn.addEventListener("mousedown", (e) => e.stopPropagation());
btn.addEventListener("mouseenter", () => { btn.style.background = "#4a4a4a"; });
btn.addEventListener("mouseleave", () => { btn.style.background = "#3a3a3a"; });
btn.addEventListener("mouseenter", () => { btn.style.background = "var(--xb-border)"; });
btn.addEventListener("mouseleave", () => { btn.style.background = "var(--xb-border)"; });
btn.addEventListener("click", (e) => {
e.stopPropagation();
const settings = loadSettings(node);
@@ -2776,9 +2776,9 @@ function setupNode(node) {
box.style.margin = "0 8px";
box.style.boxSizing = "border-box";
box.style.borderRadius = "4px";
box.style.border = "1px solid #444";
box.style.background = "#1d1d1d";
box.style.color = "#ccc";
box.style.border = "1px solid var(--xb-border)";
box.style.background = "var(--xb-bg)";
box.style.color = "var(--xb-text)";
box.style.padding = "5px 8px";
box.style.fontSize = "12px";
box.style.lineHeight = "1.6";
@@ -3463,7 +3463,7 @@ function xbrTextControl(value, placeholder, onChange) {
/** 导演台同款「种子 + 生成后控制(随机/固定/增加/减少)」一行 */
function xbrSeedRow(run, onChange) {
const g = el("div", "display:flex;align-items:center;gap:10px;margin-bottom:10px;");
g.append(el("label", "flex:0 0 160px;font-size:13px;color:#999;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;", "种子"));
g.append(el("label", "flex:0 0 160px;font-size:13px;color:var(--xb-text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;", "种子"));
const i = xbrNumberControl(run.seed, { min: 0, max: Number.MAX_SAFE_INTEGER, step: 1 }, (v) => { run.seed = Math.round(v); onChange?.(); });
i.style.flex = "1 1 0";
const ctrl = smallBtn("", "flex:1 1 0;min-width:0;background:#b45309;border:1px solid #d97706;color:#fff;text-align:center;", "生成后控制:点击在 随机 / 固定 / 增加 / 减少 间切换", null);
@@ -3486,24 +3486,24 @@ function xbrSeedRow(run, onChange) {
let xbrModal = null;
function xbrDialog({ label, title, subtitle, width = 820, autoSaveRef = null, onCommit = null, onImmediate = null, onChange = null, onClose = null }) {
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.78);z-index:10300;display:flex;align-items:center;justify-content:center;overflow:auto;");
const dialog = el("section", `background:#1c1c1e;border:1px solid #333;border-radius:8px;width:${width}px;max-width:94vw;` +
"max-height:90vh;display:flex;flex-direction:column;box-shadow:0 20px 40px rgba(0,0,0,.6);flex:0 0 auto;color:#ddd;font-size:13px;");
const dialog = el("section", `background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:${width}px;max-width:94vw;` +
"max-height:90vh;display:flex;flex-direction:column;box-shadow:0 20px 40px rgba(0,0,0,.6);flex:0 0 auto;color:var(--xb-text);font-size:13px;");
dialog.dataset.xbrProModal = label;
dialog.setAttribute("role", "dialog");
dialog.setAttribute("aria-modal", "true");
dialog.setAttribute("aria-label", label);
const header = el("div", "padding:18px 20px;border-bottom:1px solid #444;");
header.append(el("div", "font-size:17px;font-weight:700;color:#eee;", title));
if (subtitle) header.append(el("div", "font-size:12px;color:#999;margin-top:5px;line-height:1.6;", subtitle));
const header = el("div", "padding:18px 20px;border-bottom:1px solid var(--xb-border);");
header.append(el("div", "font-size:17px;font-weight:700;color:var(--xb-text);", title));
if (subtitle) header.append(el("div", "font-size:12px;color:var(--xb-text-dim);margin-top:5px;line-height:1.6;", subtitle));
const body = el("div", "padding:16px 20px;overflow-y:auto;flex:1;min-height:200px;");
const error = el("div", "color:#e55;font-size:12px;margin:0 20px;min-height:14px;");
const footer = el("div", "padding:12px 20px;border-top:1px solid #444;display:flex;justify-content:flex-end;gap:10px;align-items:center;background:#18181a;border-bottom-left-radius:8px;border-bottom-right-radius:8px;");
const footer = el("div", "padding:12px 20px;border-top:1px solid var(--xb-border);display:flex;justify-content:flex-end;gap:10px;align-items:center;background:var(--xb-bg-soft);border-bottom-left-radius:8px;border-bottom-right-radius:8px;");
const left = el("div", "display:flex;align-items:center;gap:16px;margin-right:auto;flex-wrap:wrap;");
let autoCb = null;
if (autoSaveRef) {
const autoRow = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:#999;cursor:pointer;user-select:none;");
const autoRow = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:var(--xb-text-dim);cursor:pointer;user-select:none;");
autoCb = el("input", "width:18px;height:18px;accent-color:#f59e0b;cursor:pointer;");
autoCb.type = "checkbox";
autoCb.checked = autoSaveRef.value !== false;
@@ -3518,8 +3518,8 @@ function xbrDialog({ label, title, subtitle, width = 820, autoSaveRef = null, on
uiRow.title = "调整整个弹窗界面大小(不缩放网页/画布;Ctrl/⌘+滚轮 也可)";
left.append(zoomRow, uiRow);
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:8px 20px;font-size:14px;cursor:pointer;font-family:inherit;", "取消");
const saveBtn = onCommit ? el("button", "background:#2d5a88;color:#fff;border:none;border-radius:4px;padding:8px 20px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;", "💾 保存") : null;
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:8px 20px;font-size:14px;cursor:pointer;font-family:inherit;", "取消");
const saveBtn = onCommit ? el("button", "background:var(--xb-btn-primary);color:#fff;border:none;border-radius:4px;padding:8px 20px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;", "💾 保存") : null;
footer.append(left, cancelBtn);
if (saveBtn) footer.append(saveBtn);
dialog.append(header, body, error, footer);
@@ -3766,15 +3766,15 @@ function xbrCopyPromptText(node) {
function xbrPresetDialog(title) {
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.78);z-index:10300;display:flex;overflow:auto;");
const dialog = el("section", "width:880px;max-width:96vw;max-height:90vh;margin:auto;flex:0 0 auto;"
+ "display:flex;flex-direction:column;background:#1c1c1e;border:1px solid #333;border-radius:8px;color:#ddd;"
+ "display:flex;flex-direction:column;background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;color:var(--xb-text);"
+ "font:15px/1.65 system-ui,sans-serif;box-shadow:0 20px 40px rgba(0,0,0,.6);overflow:hidden;");
const header = el("div", "padding:14px 18px 12px;background:#232326;border-bottom:1px solid #3a3a3a;"
const header = el("div", "padding:14px 18px 12px;background:var(--xb-bg-soft);border-bottom:1px solid var(--xb-border);"
+ "display:flex;align-items:center;gap:12px;flex-wrap:wrap;");
header.append(el("span", "flex:1 1 300px;min-width:220px;font-size:1.2em;font-weight:700;color:#eee;", title));
header.append(el("span", "flex:1 1 300px;min-width:220px;font-size:1.2em;font-weight:700;color:var(--xb-text);", title));
const tools = el("span", "display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-weight:normal;");
tools.append(el("span", "color:#8a8a8a;", "字号"), buildZoomRow(dialog),
tools.append(el("span", "color:var(--xb-text-dim);", "字号"), buildZoomRow(dialog),
el("span", "color:#5a5a5a;", "|"), buildUIRow(dialog));
const closeBtn = smallBtn("✕", "border:1px solid #555;background:#2b2b2b;color:#ddd;padding:3px 10px;", "关闭");
const closeBtn = smallBtn("✕", "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);padding:3px 10px;", "关闭");
tools.append(closeBtn);
header.append(tools);
const body = el("div", "padding:14px 18px 16px;display:flex;flex-direction:column;gap:10px;overflow:hidden;flex:1 1 auto;min-height:0;");
@@ -3802,7 +3802,7 @@ async function xbrShowPresetPicker(node, onPick) {
const backupOnly = Array.isArray(data?.backup_only) ? data.backup_only : [];
const { body, close } = xbrPresetDialog("📥 添加预设(点一项 → 加进节点提示词)");
if (!labels.length) {
body.append(el("div", "color:#ffb74d;",
body.append(el("div", "color:var(--xb-warn);",
`没有找到预设文件:xb_skill/${XBR_PRESET_LIB_NODE}/${data?.lang || "ZH"} 目录里没有 .txt。`
+ "可以先在「✏️ 编辑预设」里把当前提示词存成一个新预设。"));
}
@@ -3810,14 +3810,14 @@ async function xbrShowPresetPicker(node, onPick) {
for (const label of labels) {
const txt = xbrLibText(label, lang);
const row = el("button", "text-align:left;padding:7px 10px;border-radius:5px;border:1px solid #3d3d3d;"
+ "background:#1f1f1f;color:#ddd;cursor:pointer;font-size:1em;font-family:inherit;");
+ "background:var(--xb-inset);color:var(--xb-text);cursor:pointer;font-size:1em;font-family:inherit;");
row.type = "button";
const name = el("div", "font-weight:bold;color:#8fd3ff;font-size:1.05em;", (backupOnly.includes(label) ? "📦 " : "") + label);
const name = el("div", "font-weight:bold;color:var(--xb-info);font-size:1.05em;", (backupOnly.includes(label) ? "📦 " : "") + label);
name.title = backupOnly.includes(label) ? "来自备份目录(output/生图提示词预设)" : "来自预设目录(xb_skill)";
const prev = el("div", "color:#9a9a9a;font-size:0.9em;margin-top:3px;line-height:1.5;",
txt.length > 60 ? txt.slice(0, 60) + "…" : (txt || "(空文件)"));
row.append(name, prev);
row.addEventListener("mouseenter", () => { row.style.background = "#2c3a44"; row.style.borderColor = "#4a7a99"; });
row.addEventListener("mouseenter", () => { row.style.background = "var(--xb-bg-raise)"; row.style.borderColor = "#4a7a99"; });
row.addEventListener("mouseleave", () => { row.style.background = "#1f1f1f"; row.style.borderColor = "#3d3d3d"; });
row.addEventListener("click", () => {
if (typeof onPick === "function") onPick(label, txt);
@@ -3827,13 +3827,13 @@ async function xbrShowPresetPicker(node, onPick) {
list.append(row);
}
body.append(list);
body.append(el("div", "color:#8a8a8a;font-size:0.88em;word-break:break-all;flex:0 0 auto;",
body.append(el("div", "color:var(--xb-text-dim);font-size:0.88em;word-break:break-all;flex:0 0 auto;",
`📦 备份目录(也会被读取):${data?.backup_dir || "output/生图提示词预设"}`));
const foot = el("div", "display:flex;justify-content:flex-end;gap:6px;flex:0 0 auto;flex-wrap:wrap;");
foot.append(
smallBtn("刷新列表", "border:1px solid #555;background:#2b2b2b;color:#ddd;padding:5px 12px;font-size:12px;", "重新扫描预设目录与备份目录",
smallBtn("刷新列表", "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);padding:5px 12px;font-size:12px;", "重新扫描预设目录与备份目录",
async () => { close(); await xbrFetchPresetLib(true); xbrShowPresetPicker(node); }),
smallBtn("关闭", "border:1px solid #555;background:#2b2b2b;color:#ddd;padding:5px 12px;font-size:12px;", "关闭窗口", close),
smallBtn("关闭", "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);padding:5px 12px;font-size:12px;", "关闭窗口", close),
);
body.append(foot);
}
@@ -3869,13 +3869,13 @@ async function xbrShowPresetEditor(node, opts = {}) {
const { body, close } = xbrPresetDialog(cfg.title);
const rowSel = el("div", "display:flex;gap:6px;align-items:center;flex:1 1 300px;min-width:240px;flex-wrap:nowrap;");
const sel = el("select", "flex:1 1 260px;min-width:200px;height:2em;background:#1e1e1e;color:#ddd;"
+ "border:1px solid #444;border-radius:4px;padding:0 6px;font-size:1em;font-family:inherit;");
rowSel.append(el("span", "color:#aaa;white-space:nowrap;", "已有预设(选中即载入)"), sel);
const sel = el("select", "flex:1 1 260px;min-width:200px;height:2em;background:var(--xb-bg);color:var(--xb-text);"
+ "border:1px solid var(--xb-border);border-radius:4px;padding:0 6px;font-size:1em;font-family:inherit;");
rowSel.append(el("span", "color:var(--xb-text-dim);white-space:nowrap;", "已有预设(选中即载入)"), sel);
const rowName = el("div", "display:flex;gap:6px;align-items:center;flex:1 1 260px;min-width:220px;flex-wrap:nowrap;");
rowName.append(el("span", "color:#aaa;white-space:nowrap;", "预设名称"));
const nameInput = el("input", "flex:1;min-width:140px;height:2em;background:#1e1e1e;color:#ddd;"
+ "border:1px solid #444;border-radius:4px;padding:0 8px;font-size:1em;font-family:inherit;");
rowName.append(el("span", "color:var(--xb-text-dim);white-space:nowrap;", "预设名称"));
const nameInput = el("input", "flex:1;min-width:140px;height:2em;background:var(--xb-bg);color:var(--xb-text);"
+ "border:1px solid var(--xb-border);border-radius:4px;padding:0 8px;font-size:1em;font-family:inherit;");
nameInput.type = "text";
nameInput.placeholder = "例如:电影感特写(同名 = 覆盖,新名 = 新建)";
rowName.append(nameInput);
@@ -3884,10 +3884,10 @@ async function xbrShowPresetEditor(node, opts = {}) {
const boxHead = el("div", "display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:2px;flex:0 0 auto;");
const boxHeadRight = el("div", "display:flex;gap:8px;align-items:center;flex:0 0 auto;");
const charCount = el("span", "color:#8a8a8a;font-variant-numeric:tabular-nums;", "0 字");
const charCount = el("span", "color:var(--xb-text-dim);font-variant-numeric:tabular-nums;", "0 字");
boxHeadRight.append(charCount);
if (cfg.fromPrompt) {
boxHeadRight.append(smallBtn("取当前提示词", "border:1px solid #555;background:#2b2b2b;color:#ddd;padding:4px 10px;font-size:12px;",
boxHeadRight.append(smallBtn("取当前提示词", "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);padding:4px 10px;font-size:12px;",
"把节点提示词框的内容复制到编辑框", () => {
box.value = xbrPromptText(node);
status.style.color = "#aaa";
@@ -3895,10 +3895,10 @@ async function xbrShowPresetEditor(node, opts = {}) {
fitBox();
}));
}
boxHead.append(el("span", "color:#9fc3e8;font-weight:bold;", cfg.contentLabel), boxHeadRight);
boxHead.append(el("span", "color:var(--xb-info);font-weight:bold;", cfg.contentLabel), boxHeadRight);
const box = el("textarea", "width:100%;flex:1 1 auto;min-height:300px;resize:vertical;box-sizing:border-box;"
+ "background:#1d1d1d;color:#ddd;border:1px solid #444;border-radius:5px;padding:10px 12px;"
+ "background:var(--xb-bg);color:var(--xb-text);border:1px solid var(--xb-border);border-radius:5px;padding:10px 12px;"
+ "line-height:1.7;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font-family:inherit;font-size:15px;");
box.spellcheck = false;
box.placeholder = cfg.placeholder;
@@ -3910,12 +3910,12 @@ async function xbrShowPresetEditor(node, opts = {}) {
box.addEventListener("input", fitBox);
fitBox();
const hint = el("div", "color:#8a8a8a;font-size:0.88em;word-break:break-all;flex:0 0 auto;",
const hint = el("div", "color:var(--xb-text-dim);font-size:0.88em;word-break:break-all;flex:0 0 auto;",
`保存位置:xb_skill/${cfg.nodeName}/${data?.lang || "ZH"}/  备份:${data?.backup_dir || "output/生图提示词预设"}`);
const status = el("div", "min-height:18px;color:#aaa;flex:0 0 auto;");
const status = el("div", "min-height:18px;color:var(--xb-text-dim);flex:0 0 auto;");
const foot = el("div", "display:flex;justify-content:flex-end;gap:6px;flex:0 0 auto;flex-wrap:wrap;");
const btnCss = "border:1px solid #555;background:#2b2b2b;color:#ddd;padding:5px 14px;font-size:12px;";
const btnCss = "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);padding:5px 14px;font-size:12px;";
const fillSelect = (d) => {
const cur = sel.value;
@@ -3950,7 +3950,7 @@ async function xbrShowPresetEditor(node, opts = {}) {
try { nameInput.focus(); } catch (_) {}
});
const saveBtn = smallBtn("保存", btnCss + "border-color:#3f86d0;background:#22456b;color:#eaf4ff;",
const saveBtn = smallBtn("保存", btnCss + "border-color:#3f86d0;background:#22456b;color:var(--xb-on-accent);",
"把编辑框内容保存成预设文件(同名覆盖 / 新名新建)", async () => {
const label = String(nameInput.value || "").trim();
if (!label) { status.style.color = "#ff8a80"; status.textContent = "请先填写预设名称"; nameInput.focus(); return; }
@@ -4096,14 +4096,14 @@ async function xbrShowSkillEditor(node, presetName, onChanged) {
const { body, close } = xbrPresetDialog("✏️ 编辑 skill(技能文档整段作为 LLM 的系统提示词)");
const rowSel = el("div", "display:flex;gap:6px;align-items:center;flex:1 1 300px;min-width:240px;flex-wrap:nowrap;");
const sel = el("select", "flex:1 1 260px;min-width:200px;height:2em;background:#1e1e1e;color:#ddd;"
+ "border:1px solid #444;border-radius:4px;padding:0 6px;font-size:1em;font-family:inherit;");
rowSel.append(el("span", "color:#aaa;white-space:nowrap;", "已有 skill(选中即载入)"), sel);
const sel = el("select", "flex:1 1 260px;min-width:200px;height:2em;background:var(--xb-bg);color:var(--xb-text);"
+ "border:1px solid var(--xb-border);border-radius:4px;padding:0 6px;font-size:1em;font-family:inherit;");
rowSel.append(el("span", "color:var(--xb-text-dim);white-space:nowrap;", "已有 skill(选中即载入)"), sel);
const rowName = el("div", "display:flex;gap:6px;align-items:center;flex:1 1 260px;min-width:220px;flex-wrap:nowrap;");
rowName.append(el("span", "color:#aaa;white-space:nowrap;", "文件名"));
const nameInput = el("input", "flex:1;min-width:140px;height:2em;background:#1e1e1e;color:#ddd;"
+ "border:1px solid #444;border-radius:4px;padding:0 8px;font-size:1em;font-family:inherit;");
rowName.append(el("span", "color:var(--xb-text-dim);white-space:nowrap;", "文件名"));
const nameInput = el("input", "flex:1;min-width:140px;height:2em;background:var(--xb-bg);color:var(--xb-text);"
+ "border:1px solid var(--xb-border);border-radius:4px;padding:0 8px;font-size:1em;font-family:inherit;");
nameInput.type = "text";
nameInput.placeholder = "例如:my_skill.txt(同名 = 覆盖,新名 = 新建)";
rowName.append(nameInput);
@@ -4111,11 +4111,11 @@ async function xbrShowSkillEditor(node, presetName, onChanged) {
firstRow.append(rowSel, rowName);
const boxHead = el("div", "display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:2px;flex:0 0 auto;");
const charCount = el("span", "color:#8a8a8a;font-variant-numeric:tabular-nums;", "0 字");
boxHead.append(el("span", "color:#9fc3e8;font-weight:bold;", "skill 全文(整段作为系统提示词)"), charCount);
const charCount = el("span", "color:var(--xb-text-dim);font-variant-numeric:tabular-nums;", "0 字");
boxHead.append(el("span", "color:var(--xb-info);font-weight:bold;", "skill 全文(整段作为系统提示词)"), charCount);
const box = el("textarea", "width:100%;flex:1 1 auto;min-height:300px;resize:vertical;box-sizing:border-box;"
+ "background:#1d1d1d;color:#ddd;border:1px solid #444;border-radius:5px;padding:10px 12px;"
+ "background:var(--xb-bg);color:var(--xb-text);border:1px solid var(--xb-border);border-radius:5px;padding:10px 12px;"
+ "line-height:1.7;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font-family:inherit;font-size:15px;");
box.spellcheck = false;
box.placeholder = "在这里编写技能文档(角色设定 + 输出契约)…";
@@ -4127,11 +4127,11 @@ async function xbrShowSkillEditor(node, presetName, onChanged) {
box.addEventListener("input", fitBox);
fitBox();
const hint = el("div", "color:#8a8a8a;font-size:0.88em;word-break:break-all;flex:0 0 auto;",
const hint = el("div", "color:var(--xb-text-dim);font-size:0.88em;word-break:break-all;flex:0 0 auto;",
"保存位置:xb_skill/skills/ (同一批文件也被「🖼️ Qwen2.1提示词预设」等节点共用)");
const status = el("div", "min-height:18px;color:#aaa;flex:0 0 auto;");
const status = el("div", "min-height:18px;color:var(--xb-text-dim);flex:0 0 auto;");
const foot = el("div", "display:flex;justify-content:flex-end;gap:6px;flex:0 0 auto;flex-wrap:wrap;");
const btnCss = "border:1px solid #555;background:#2b2b2b;color:#ddd;padding:5px 14px;font-size:12px;";
const btnCss = "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);padding:5px 14px;font-size:12px;";
const fillSelect = () => {
const cur = sel.value;
@@ -4166,7 +4166,7 @@ async function xbrShowSkillEditor(node, presetName, onChanged) {
try { nameInput.focus(); } catch (_) {}
});
const saveBtn = smallBtn("保存", btnCss + "border-color:#3f86d0;background:#22456b;color:#eaf4ff;",
const saveBtn = smallBtn("保存", btnCss + "border-color:#3f86d0;background:#22456b;color:var(--xb-on-accent);",
"保存技能文件(同名覆盖 / 新名新建)", async () => {
const name = String(nameInput.value || "").trim();
if (!name) { status.style.color = "#ff8a80"; status.textContent = "请先填写文件名"; nameInput.focus(); return; }
@@ -4207,7 +4207,7 @@ async function xbrShowSkillEditor(node, presetName, onChanged) {
/* ── 弹窗内容:✨ 提示词增强预设 ─────────────────────────── */
function xbrRenderPreset(body, node, draft, ctx) {
const btnCss = "border:1px solid #555;background:#2b2b2b;color:#ddd;padding:4px 12px;font-size:12px;";
const btnCss = "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);padding:4px 12px;font-size:12px;";
// ── 设定词:文本框 + 预设词工具(添加 / 编辑 / 复制)──────────────────────
body.append(makeSectionTitle("设定词"));
@@ -4315,7 +4315,7 @@ function xbrRenderPreset(body, node, draft, ctx) {
if (presetOpts.length) {
body.append(field("增强预设", selectControl(presetOpts, draft.preset, (v) => { draft.preset = v; })));
} else {
body.append(el("div", "font-size:11px;color:#ffb74d;", "没有可用的增强预设(检查 LLM 预设模块是否正常加载)"));
body.append(el("div", "font-size:11px;color:var(--xb-warn);", "没有可用的增强预设(检查 LLM 预设模块是否正常加载)"));
}
body.append(el("div", "font-size:11px;color:#888;line-height:1.6;margin:2px 0 8px;",
`该预设内容即「🧩 提示词设定」输出的正文,同时作为 LLM 的系统提示词;只列与输出语言 ${draft.lang} 相符的预设。`));
@@ -4350,7 +4350,7 @@ function xbrRenderPreset(body, node, draft, ctx) {
if (taskOpts.length) {
body.append(field("反推预设", selectControl(taskOpts, draft.task_preset, (v) => { draft.task_preset = v; })));
} else {
body.append(el("div", "font-size:11px;color:#ffb74d;", "没有可用的反推预设"));
body.append(el("div", "font-size:11px;color:var(--xb-warn);", "没有可用的反推预设"));
}
body.append(el("div", "font-size:11px;color:#888;line-height:1.6;margin:2px 0 8px;",
"带 * 的预设里 * 是必填占位符,由节点上的提示词框或外接「📝 提示词」填入。"));
@@ -4465,7 +4465,7 @@ function xbrRenderHelp(body) {
" · 节点表面参数很少?故意的:选项都收在弹窗里,表面只留最常动的几个",
" · 改完参数要重新执行节点才生效",
];
body.append(el("div", "font-size:12px;color:#bbb;line-height:1.9;white-space:pre-wrap;font-family:ui-monospace,Consolas,monospace;", lines.join("\n")));
body.append(el("div", "font-size:12px;color:var(--xb-text-dim);line-height:1.9;white-space:pre-wrap;font-family:ui-monospace,Consolas,monospace;", lines.join("\n")));
}
const XBR_PANEL_RENDER = { llm: xbrRenderLlm, preset: xbrRenderPreset, help: xbrRenderHelp };
@@ -4571,12 +4571,12 @@ async function xbrOpenModal(node, panelId) {
function xbrBuildButtonRow(node) {
const grid = el("div", "display:grid;grid-template-columns:repeat(" + Math.max(1, XBR_PANEL_BUTTONS.length) + ",1fr);grid-auto-rows:30px;gap:6px;flex:0 0 auto;");
for (const b of XBR_PANEL_BUTTONS) {
const btn = el("button", "width:100%;height:30px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid #5b9bd5;background:#3a3a3a;color:#eee;font-size:13px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:inherit;", b.label);
const btn = el("button", "width:100%;height:30px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid #5b9bd5;background:var(--xb-border);color:var(--xb-text);font-size:13px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:inherit;", b.label);
btn.title = `打开「${b.title}」弹窗`;
btn.dataset.xbrProBtn = b.id;
btn.addEventListener("mousedown", (e) => e.stopPropagation());
btn.addEventListener("mouseenter", () => { btn.style.background = "#4a4a4a"; });
btn.addEventListener("mouseleave", () => { btn.style.background = "#3a3a3a"; });
btn.addEventListener("mouseenter", () => { btn.style.background = "var(--xb-border)"; });
btn.addEventListener("mouseleave", () => { btn.style.background = "var(--xb-border)"; });
btn.addEventListener("click", (e) => { e.stopPropagation(); xbrOpenModal(node, b.id); });
grid.append(btn);
}
@@ -4622,13 +4622,13 @@ function xbrInfoLines(node, promptText) {
/** 提示词框标题行(左标签 + 右「复制文本」)+ 参数设定显示;并挂上端口锁定/执行回写/摘要刷新 */
function xbrBuildPromptHeader(node, promptBox) {
const head = el("div", "display:flex;align-items:center;gap:8px;flex:0 0 auto;");
const lab = el("div", "font-size:12px;color:#bbb;", "📝 节点提示词(与面板预览框双向同步)");
const lab = el("div", "font-size:12px;color:var(--xb-text-dim);", "📝 节点提示词(与面板预览框双向同步)");
// 与「🖼️ 生图提示词预设」的提示词框右上角按钮完全同一套尺寸 / 配色
const copy = smallBtn("复制文本", "margin-left:auto;font-size:0.92em;line-height:1.35;height:1.9em;padding:0 0.75em;border-radius:4px;border:1px solid #555;background:#2b2b2b;color:#ddd;white-space:nowrap;",
const copy = smallBtn("复制文本", "margin-left:auto;font-size:0.92em;line-height:1.35;height:1.9em;padding:0 0.75em;border-radius:4px;border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);white-space:nowrap;",
"复制提示词框内容", () => { copyText(promptBox.value || ""); });
head.append(lab, copy);
const infoBox = el("div", "flex:0 0 auto;height:150px;overflow:auto;background:#1c1c1e;border:1px solid #333;border-radius:6px;padding:6px 9px;font-size:11px;line-height:1.65;color:#b9c6d2;white-space:pre-wrap;box-sizing:border-box;font-family:ui-monospace,Consolas,monospace;");
const infoBox = el("div", "flex:0 0 auto;height:150px;overflow:auto;background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:6px;padding:6px 9px;font-size:11px;line-height:1.65;color:#b9c6d2;white-space:pre-wrap;box-sizing:border-box;font-family:ui-monospace,Consolas,monospace;");
infoBox.dataset.captureWheel = "true";
infoBox.title = "当前配置摘要(只读)";
@@ -4642,8 +4642,8 @@ function xbrBuildPromptHeader(node, promptBox) {
const inp = (node.inputs || []).find((i) => i.name === "text");
const linked = !!inp && inp.link != null;
promptBox.readOnly = linked;
promptBox.style.background = linked ? "#141416" : "#1d1d1d";
promptBox.style.color = linked ? "#8a8a8a" : "#ccc";
promptBox.style.background = linked ? "#141416" : "var(--xb-bg)";
promptBox.style.color = linked ? "var(--xb-text-dim)" : "var(--xb-text)";
promptBox.title = linked ? "已由「📝 提示词」端口控制,断开连线后恢复编辑" : "";
lab.textContent = linked ? "📝 提示词已锁定(由「📝 提示词」端口控制)" : "📝 节点提示词(与面板预览框双向同步)";
lab.style.color = linked ? "#d9a441" : "#bbb";
+2 -2
View File
@@ -51,10 +51,10 @@ function setupDynamicOutputs(node) {
const dw = res.widget;
if (dw.inputEl) {
dw.inputEl.readOnly = true;
dw.inputEl.style.backgroundColor = "#2a2a2a";
dw.inputEl.style.backgroundColor = "var(--xb-bg-raise)";
dw.inputEl.style.color = "#bbbbbb";
dw.inputEl.style.fontSize = "12px";
dw.inputEl.style.border = "1px solid #444";
dw.inputEl.style.border = "1px solid var(--xb-border)";
}
// Nodes 2.0:显示型字段走 options.read_only(Vue 部件没有 element 可改)
try { if (dw.options) dw.options.read_only = true; } catch (_) {}
+26 -26
View File
@@ -7,7 +7,7 @@
* ② 手写提示词编辑框(右上角「📋 复制」按钮;「📝 提示词」端口接线后自动锁定)
* ③ 参数设定显示(只读摘要 4 行:LLM后端/语言/模型 | 增强预设 | 反推预设 | 温度等采样参数)
*
* 弹窗(导演台同款):居中 overlay + `#1c1c1e` 面板 + header(标题/副标题)+ 内容 + 底部「取消 / 💾 保存」
* 弹窗(导演台同款):居中 overlay + `var(--xb-bg)` 面板 + header(标题/副标题)+ 内容 + 底部「取消 / 💾 保存」
* · 🤖 大语言模型配置 = 复刻导演台「大语言模型配置」面板(去掉「开关」「故事扩写设置」两组)
* · ✨ 提示词增强预设 = 增强预设 / 反推预设 / 追加设定
* · 📖 使用说明
@@ -89,11 +89,11 @@ function ensureStyle() {
const st = el("style", "");
st.id = "xbr-style";
st.textContent = [
".xbr-input{box-sizing:border-box;background:var(--comfy-input-bg,#2a2a2a);color:var(--fg-color,#ddd);" +
"border:1px solid var(--border-color,#444);border-radius:4px;padding:6px 10px;font-size:13px;outline:none;font-family:inherit;}",
".xbr-input:hover{background:var(--comfy-input-bg-hover,#3a3a3a);}",
".xbr-input{box-sizing:border-box;background:var(--comfy-input-bg,var(--xb-bg-raise));color:var(--fg-color,var(--xb-text));" +
"border:1px solid var(--border-color,var(--xb-border));border-radius:4px;padding:6px 10px;font-size:13px;outline:none;font-family:inherit;}",
".xbr-input:hover{background:var(--comfy-input-bg-hover,var(--xb-border));}",
".xbr-input:focus{border-color:#5b9bd5;}",
".xbr-input option{background:var(--comfy-menu-bg,#2a2a2a);}",
".xbr-input option{background:var(--comfy-menu-bg,var(--xb-bg-raise));}",
".xbr-modal input[type='checkbox']{width:18px;height:18px;accent-color:#f59e0b;cursor:pointer;}",
".xbr-modal textarea{line-height:1.5;}",
].join("\n");
@@ -174,7 +174,7 @@ function numberControl(value, opts, onChange) {
return i;
}
function checkboxControl(value, labelText, onChange) {
const w = el("label", "display:flex;align-items:center;gap:10px;cursor:pointer;font-size:13px;color:var(--fg-color,#ddd);margin-bottom:8px;");
const w = el("label", "display:flex;align-items:center;gap:10px;cursor:pointer;font-size:13px;color:var(--fg-color,var(--xb-text));margin-bottom:8px;");
const c = el("input", "width:18px;height:18px;accent-color:#f59e0b;cursor:pointer;");
c.type = "checkbox";
c.checked = !!value;
@@ -216,8 +216,8 @@ function textareaControl(value, placeholder, rows, onChange) {
return t;
}
function smallBtn(text, css, title) {
const b = el("button", "border:1px solid var(--border-color,#555);background:var(--comfy-button-bg,#333);" +
"color:var(--fg-color,#eee);border-radius:4px;padding:5px 12px;font-size:12px;cursor:pointer;white-space:nowrap;" + (css || ""), text);
const b = el("button", "border:1px solid var(--border-color,var(--xb-border));background:var(--comfy-button-bg,var(--xb-border));" +
"color:var(--fg-color,var(--xb-text));border-radius:4px;padding:5px 12px;font-size:12px;cursor:pointer;white-space:nowrap;" + (css || ""), text);
b.type = "button";
if (title) b.title = title;
return b;
@@ -301,12 +301,12 @@ function buildUIRow(target) {
const row = el("span", "display:inline-flex;align-items:center;gap:4px;user-select:none;");
row.append(el("span", "font-size:11px;color:#888;white-space:nowrap;", "界面"));
const mk = (t, d) => {
const b = smallBtn(t, "width:22px;height:22px;padding:0;line-height:1;font-size:13px;border:1px solid #8a6d3b;background:#5a4a2a;color:#ffd98a;", d > 0 ? "放大整个界面(不缩放网页/画布)" : "缩小整个界面(不缩放网页/画布)");
const b = smallBtn(t, "width:22px;height:22px;padding:0;line-height:1;font-size:13px;border:1px solid #8a6d3b;background:var(--xb-tint-warn);color:var(--xb-warn);", d > 0 ? "放大整个界面(不缩放网页/画布)" : "缩小整个界面(不缩放网页/画布)");
b.addEventListener("mousedown", (e) => e.stopPropagation());
b.addEventListener("click", (e) => { e.stopPropagation(); uiZoomStep(d); uiZoomApply(target); uiZoomSyncLabels(target); });
return b;
};
const val = el("span", "min-width:40px;text-align:center;font-size:12px;color:#ffd98a;font-variant-numeric:tabular-nums;", Math.round(uiZoomGet() * 100) + "%");
const val = el("span", "min-width:40px;text-align:center;font-size:12px;color:var(--xb-warn);font-variant-numeric:tabular-nums;", Math.round(uiZoomGet() * 100) + "%");
val.classList.add("xbr-ui-zoom-val");
row.append(mk("−", -1), val, mk("+", 1));
return row;
@@ -479,15 +479,15 @@ function buildDialog({ label, title, subtitle, width = 820, footerExtraFactory =
ensureStyle();
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,0.78);z-index:9999;display:flex;" +
"align-items:center;justify-content:center;overflow:auto;");
const dialog = el("section", `background:#1c1c1e;border:1px solid #333;border-radius:8px;width:${width}px;max-width:94vw;` +
const dialog = el("section", `background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:${width}px;max-width:94vw;` +
"max-height:90vh;display:flex;flex-direction:column;box-shadow:0 20px 40px rgba(0,0,0,0.6);flex:0 0 auto;");
dialog.className = "xbr-modal";
dialog.setAttribute("role", "dialog");
dialog.setAttribute("aria-modal", "true");
dialog.setAttribute("aria-label", label);
const header = el("div", "padding:18px 20px;border-bottom:1px solid var(--border-color,#444);");
header.append(el("div", "font-size:17px;font-weight:700;color:var(--fg-color,#eee);", title));
const header = el("div", "padding:18px 20px;border-bottom:1px solid var(--border-color,var(--xb-border));");
header.append(el("div", "font-size:17px;font-weight:700;color:var(--fg-color,var(--xb-text));", title));
if (subtitle) header.append(el("div", "font-size:12px;color:var(--descrip-text,#999);margin-top:5px;line-height:1.6;", subtitle));
dialog.append(header);
@@ -495,8 +495,8 @@ function buildDialog({ label, title, subtitle, width = 820, footerExtraFactory =
dialog.append(body);
const error = el("div", "color:#e55;font-size:12px;margin:0 20px;min-height:14px;");
const footer = el("div", "padding:14px 20px;border-top:1px solid var(--border-color,#444);display:flex;" +
"justify-content:flex-end;gap:10px;align-items:center;background:#18181a;border-bottom-left-radius:8px;border-bottom-right-radius:8px;");
const footer = el("div", "padding:14px 20px;border-top:1px solid var(--border-color,var(--xb-border));display:flex;" +
"justify-content:flex-end;gap:10px;align-items:center;background:var(--xb-bg-soft);border-bottom-left-radius:8px;border-bottom-right-radius:8px;");
if (footerExtraFactory) {
// 导演台同款:底部左侧放「自动保存 + 界面缩放」,与右侧「取消/保存」同行
const left = el("div", "display:flex;align-items:center;gap:16px;margin-right:auto;flex-wrap:wrap;");
@@ -507,7 +507,7 @@ function buildDialog({ label, title, subtitle, width = 820, footerExtraFactory =
footer.append(cancelBtn);
let saveBtn = null;
if (onSave) {
saveBtn = smallBtn(saveLabel, "padding:8px 20px;font-size:14px;font-weight:600;background:#2d5a88;border:none;color:#fff;");
saveBtn = smallBtn(saveLabel, "padding:8px 20px;font-size:14px;font-weight:600;background:var(--xb-btn-primary);border:none;color:#fff;");
footer.append(saveBtn);
}
dialog.append(error, footer);
@@ -713,7 +713,7 @@ function renderHelpPanel(body) {
" · 界面缩放作用于整个弹窗(不缩放网页/画布),Ctrl/⌘ + 滚轮 也可调,记忆在浏览器本地",
" · 「取消」丢弃未保存改动;配置随工作流保存;在线 API 的 Key 存在 ComfyUI user 目录,不进工作流",
];
body.append(el("div", "font-size:12px;color:#bbb;line-height:1.9;white-space:pre-wrap;font-family:ui-monospace,Consolas,monospace;", lines.join("\n")));
body.append(el("div", "font-size:12px;color:var(--xb-text-dim);line-height:1.9;white-space:pre-wrap;font-family:ui-monospace,Consolas,monospace;", lines.join("\n")));
}
const PANEL_RENDER = { llm: renderLlmPanel, preset: renderPresetPanel, help: renderHelpPanel };
@@ -884,7 +884,7 @@ function setupNode(node) {
const b = el("button", [
"width:100%", `height:${BTN_H}px`, "box-sizing:border-box",
"display:flex", "align-items:center", "justify-content:center",
"border-radius:6px", "border:2px solid #5b9bd5", "background:#3a3a3a", "color:#eee",
"border-radius:6px", "border:2px solid #5b9bd5", "background:var(--xb-border)", "color:var(--xb-text)",
"font-size:14px", "cursor:pointer", "white-space:nowrap", "overflow:hidden", "text-overflow:ellipsis",
"padding:0 6px", "font-family:inherit",
].join(";"));
@@ -893,8 +893,8 @@ function setupNode(node) {
b.title = `打开「${p.title}」弹窗`;
b.dataset.xbrPanelBtn = p.id;
b.addEventListener("mousedown", (e) => e.stopPropagation());
b.addEventListener("mouseenter", () => { b.style.background = "#4a4a4a"; });
b.addEventListener("mouseleave", () => { b.style.background = "#3a3a3a"; });
b.addEventListener("mouseenter", () => { b.style.background = "var(--xb-border)"; });
b.addEventListener("mouseleave", () => { b.style.background = "var(--xb-border)"; });
b.addEventListener("click", () => openPanelModal(node, p.id));
btnGrid.append(b);
}
@@ -919,7 +919,7 @@ function setupNode(node) {
promptWrap.append(promptHead);
const promptBox = el("textarea", "flex:1 1 auto;min-height:60px;width:100%;box-sizing:border-box;resize:none;line-height:1.5;" +
"font-family:inherit;background:#1c1c1e;color:var(--fg-color,#ddd);border:1px solid var(--border-color,#444);" +
"font-family:inherit;background:var(--xb-bg);color:var(--fg-color,var(--xb-text));border:1px solid var(--border-color,var(--xb-border));" +
"border-radius:6px;padding:8px 10px;font-size:13px;outline:none;");
promptBox.value = String(readWidgetValue(wCustom) ?? "");
promptBox.placeholder = "在这里写提示词 / 或带 * 预设的占位符内容(如 BBox 检测的目标名称)";
@@ -931,7 +931,7 @@ function setupNode(node) {
promptTimer = setTimeout(() => { setWidgetValue(wCustom, promptBox.value); node.setDirtyCanvas?.(true, true); }, 250);
});
promptBox.addEventListener("focus", () => { if (!promptBox.readOnly) promptBox.style.borderColor = "#5b9bd5"; });
promptBox.addEventListener("blur", () => { promptBox.style.borderColor = "var(--border-color,#444)"; });
promptBox.addEventListener("blur", () => { promptBox.style.borderColor = "var(--border-color,var(--xb-border))"; });
promptWrap.append(promptBox);
container.append(promptWrap);
@@ -941,9 +941,9 @@ function setupNode(node) {
try {
const linked = !!textInput() && textInput().link != null;
promptBox.readOnly = linked;
promptBox.style.background = linked ? "#141416" : "#1c1c1e";
promptBox.style.color = linked ? "#8a8a8a" : "var(--fg-color,#ddd)";
promptBox.style.borderColor = linked ? "#5a5a5a" : "var(--border-color,#444)";
promptBox.style.background = linked ? "#141416" : "var(--xb-bg)";
promptBox.style.color = linked ? "var(--xb-text-dim)" : "var(--fg-color,var(--xb-text))";
promptBox.style.borderColor = linked ? "#5a5a5a" : "var(--border-color,var(--xb-border))";
promptBox.title = linked ? "已由「📝 提示词」端口控制,断开连线后恢复编辑" : "";
lockHint.textContent = linked
? "🔒 手写提示词已锁定(由「📝 提示词」端口控制)"
@@ -954,7 +954,7 @@ function setupNode(node) {
node.__xbrUpdateLock = updateLock;
// ⑦ 参数设定显示(只读摘要)
const infoBox = el("div", `flex:0 0 auto;height:${MIN_INFO_H}px;overflow:auto;background:#1c1c1e;border:1px solid #333;` +
const infoBox = el("div", `flex:0 0 auto;height:${MIN_INFO_H}px;overflow:auto;background:var(--xb-bg);border:1px solid var(--xb-border);` +
"border-radius:6px;padding:6px 9px;font-size:11px;line-height:1.65;color:#b9c6d2;white-space:pre-wrap;box-sizing:border-box;font-family:ui-monospace,Consolas,monospace;");
infoBox.dataset.captureWheel = "true";
container.append(infoBox);
+101 -101
View File
@@ -1400,15 +1400,15 @@ function el(tag, css, text) {
return e;
}
const BOX_CSS = "background:#1d1d1d;color:#ccc;border:1px solid #444;border-radius:4px;padding:5px 8px;font-size:12px;box-sizing:border-box;font-family:inherit;";
const BOX_CSS = "background:var(--xb-bg);color:var(--xb-text);border:1px solid var(--xb-border);border-radius:4px;padding:5px 8px;font-size:12px;box-sizing:border-box;font-family:inherit;";
function makeSectionTitle(text) {
return el("div", "font-size:13px;font-weight:600;color:#9fc3e8;margin:14px 0 8px;padding-bottom:5px;border-bottom:1px solid #333;", text);
return el("div", "font-size:13px;font-weight:600;color:var(--xb-info);margin:14px 0 8px;padding-bottom:5px;border-bottom:1px solid var(--xb-border);", text);
}
function field(labelText, control) {
const g = el("div", "display:flex;align-items:center;gap:12px;margin-bottom:10px;");
const lab = el("label", "flex:0 0 160px;font-size:13px;color:#999;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;", labelText);
const lab = el("label", "flex:0 0 160px;font-size:13px;color:var(--xb-text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;", labelText);
control.style.flex = "1 1 0";
control.style.minWidth = "0";
g.append(lab, control);
@@ -1435,7 +1435,7 @@ function radioRow(options, value, onChange) {
for (const o of options) {
const val = (typeof o === "object" && o !== null && o.value !== undefined) ? String(o.value) : String(o);
const label = (typeof o === "object" && o !== null && o.label) ? String(o.label) : String(o);
const lab = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:#999;cursor:pointer;user-select:none;");
const lab = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:var(--xb-text-dim);cursor:pointer;user-select:none;");
const c = el("input", "width:18px;height:18px;accent-color:#f59e0b;cursor:pointer;");
c.type = "checkbox";
c.checked = String(value) === val;
@@ -1462,7 +1462,7 @@ function textareaControl(value, onChange, css) {
}
function checkboxControl(value, labelText, onChange, tip) {
const w = el("label", "display:flex;align-items:center;gap:8px;cursor:pointer;font-size:12px;color:#ddd;user-select:none;");
const w = el("label", "display:flex;align-items:center;gap:8px;cursor:pointer;font-size:12px;color:var(--xb-text);user-select:none;");
const c = el("input", "width:16px;height:16px;accent-color:#f59e0b;cursor:pointer;");
c.type = "checkbox";
c.checked = !!value;
@@ -1524,10 +1524,10 @@ function zoomSync(root) {
}
function buildZoomRow(root) {
const row = el("span", "display:inline-flex;align-items:center;gap:4px;user-select:none;");
const minus = smallBtn("A−", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;width:22px;height:22px;padding:0;line-height:1;", "缩小编辑字号(Ctrl/⌘+滚轮 也可)");
const val = el("span", "min-width:26px;text-align:center;font-size:12px;color:#e8a87c;font-variant-numeric:tabular-nums;", String(zoomDisplay()));
const minus = smallBtn("A−", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);width:22px;height:22px;padding:0;line-height:1;", "缩小编辑字号(Ctrl/⌘+滚轮 也可)");
const val = el("span", "min-width:26px;text-align:center;font-size:12px;color:var(--xb-orange);font-variant-numeric:tabular-nums;", String(zoomDisplay()));
val.classList.add("ipp-zoom-val");
const plus = smallBtn("A+", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;width:22px;height:22px;padding:0;line-height:1;", "放大编辑字号(Ctrl/⌘+滚轮 也可)");
const plus = smallBtn("A+", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);width:22px;height:22px;padding:0;line-height:1;", "放大编辑字号(Ctrl/⌘+滚轮 也可)");
const step = (d) => {
zoomSet((zoomGet() == null ? 12 : zoomGet()) + d);
zoomApplyContainer(root);
@@ -1571,10 +1571,10 @@ window.addEventListener("resize", () => {
function buildUIRow(target) {
const row = el("span", "display:inline-flex;align-items:center;gap:4px;user-select:none;");
row.append(el("span", "font-size:11px;color:#888;white-space:nowrap;", "界面"));
const minus = smallBtn("−", "border:1px solid #8a6d3b;background:#5a4a2a;color:#ffd98a;width:22px;height:22px;padding:0;line-height:1;font-size:13px;", "缩小整个界面(Ctrl/⌘+滚轮 也可)");
const val = el("span", "min-width:42px;text-align:center;font-size:12px;color:#ffd98a;font-variant-numeric:tabular-nums;", Math.round(uiGet() * 100) + "%");
const minus = smallBtn("−", "border:1px solid #8a6d3b;background:var(--xb-tint-warn);color:var(--xb-warn);width:22px;height:22px;padding:0;line-height:1;font-size:13px;", "缩小整个界面(Ctrl/⌘+滚轮 也可)");
const val = el("span", "min-width:42px;text-align:center;font-size:12px;color:var(--xb-warn);font-variant-numeric:tabular-nums;", Math.round(uiGet() * 100) + "%");
val.classList.add("ipp-ui-val");
const plus = smallBtn("+", "border:1px solid #8a6d3b;background:#5a4a2a;color:#ffd98a;width:22px;height:22px;padding:0;line-height:1;font-size:13px;", "放大整个界面(Ctrl/⌘+滚轮 也可)");
const plus = smallBtn("+", "border:1px solid #8a6d3b;background:var(--xb-tint-warn);color:var(--xb-warn);width:22px;height:22px;padding:0;line-height:1;font-size:13px;", "放大整个界面(Ctrl/⌘+滚轮 也可)");
const step = (d) => {
uiSet(uiGet() + d * UI_STEP);
uiApply(target);
@@ -1838,12 +1838,12 @@ function openPanel(ctx, panelId) {
const title = `${cat.label} 元素`;
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.78);z-index:9999;display:flex;align-items:center;justify-content:center;overflow:auto;");
const dialog = el("section", "background:#1c1c1e;border:1px solid #333;border-radius:8px;width:880px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 20px 40px rgba(0,0,0,.6);flex:0 0 auto;color:#ddd;font-size:13px;");
const dialog = el("section", "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:880px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 20px 40px rgba(0,0,0,.6);flex:0 0 auto;color:var(--xb-text);font-size:13px;");
const header = el("div", "padding:16px 20px 12px;border-bottom:1px solid #444;");
const titleEl = el("div", "font-size:18px;font-weight:700;color:#eee;", title);
const header = el("div", "padding:16px 20px 12px;border-bottom:1px solid var(--xb-border);");
const titleEl = el("div", "font-size:18px;font-weight:700;color:var(--xb-text);", title);
header.append(titleEl);
header.append(el("div", "font-size:12px;color:#999;margin-top:5px;", "点选项行即加入提示词(变蓝),再点一下移除(变灰);点【添加详细描述】会在新窗口里给该词条写补充描述(悬停行可看)。配置随工作流保存,改完重新执行节点即生效。"));
header.append(el("div", "font-size:12px;color:var(--xb-text-dim);margin-top:5px;", "点选项行即加入提示词(变蓝),再点一下移除(变灰);点【添加详细描述】会在新窗口里给该词条写补充描述(悬停行可看)。配置随工作流保存,改完重新执行节点即生效。"));
dialog.append(header);
// ⚠️ 顶部不留内边距:滚动容器的 padding 区不参与裁剪,滚上去的行会从这条缝里
@@ -1851,12 +1851,12 @@ function openPanel(ctx, panelId) {
const panelBox = el("div", "padding:0 20px 14px;overflow-y:auto;flex:1;min-height:280px;");
// ── 吸顶预览框(不受滚动条约束,始终在画面内) ──
const sticky = el("div", "position:sticky;top:0;z-index:6;background:#1c1c1e;padding:16px 0 10px;margin:0 0 6px;border-bottom:1px solid #333;");
const sticky = el("div", "position:sticky;top:0;z-index:6;background:var(--xb-bg);padding:16px 0 10px;margin:0 0 6px;border-bottom:1px solid var(--xb-border);");
const head = el("div", "display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap;");
head.append(el("span", "font-size:12px;font-weight:600;color:#9fc3e8;", "📝 提示词预览(与节点提示词框实时同步,可直接编辑)"));
head.append(el("span", "font-size:12px;font-weight:600;color:var(--xb-info);", "📝 提示词预览(与节点提示词框实时同步,可直接编辑)"));
const headCount = el("span", "font-size:11px;color:#888;", "");
head.append(headCount);
const presetChip = el("div", "font-size:11px;color:#f0d79a;background:#2a2416;border:1px dashed #8a6d3b;border-radius:4px;padding:4px 8px;margin-bottom:6px;white-space:pre-wrap;line-height:1.5;display:none;");
const presetChip = el("div", "font-size:11px;color:var(--xb-warn);background:var(--xb-tint-warn);border:1px dashed #8a6d3b;border-radius:4px;padding:4px 8px;margin-bottom:6px;white-space:pre-wrap;line-height:1.5;display:none;");
const preview = el("textarea", BOX_CSS + "width:100%;min-height:74px;max-height:210px;resize:vertical;line-height:1.65;font-size:12px;");
preview.spellcheck = false;
preview.placeholder = "点左侧 + 添加元素,或直接在这里手写提示词…";
@@ -1897,10 +1897,10 @@ function openPanel(ctx, panelId) {
// ── 底部 ──
const error = el("div", "color:#e55;font-size:12px;margin:0 20px;min-height:16px;");
const footer = el("div", "padding:12px 20px;border-top:1px solid #444;display:flex;justify-content:flex-end;gap:10px;background:#18181a;border-bottom-left-radius:8px;border-bottom-right-radius:8px;");
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:8px 20px;font-size:14px;cursor:pointer;font-family:inherit;", "取消");
const saveBtn = el("button", "background:#2d5a88;color:#fff;border:none;border-radius:4px;padding:8px 20px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;", "💾 保存");
const autoRow = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:#999;cursor:pointer;user-select:none;");
const footer = el("div", "padding:12px 20px;border-top:1px solid var(--xb-border);display:flex;justify-content:flex-end;gap:10px;background:var(--xb-bg-soft);border-bottom-left-radius:8px;border-bottom-right-radius:8px;");
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:8px 20px;font-size:14px;cursor:pointer;font-family:inherit;", "取消");
const saveBtn = el("button", "background:var(--xb-btn-primary);color:#fff;border:none;border-radius:4px;padding:8px 20px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;", "💾 保存");
const autoRow = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:var(--xb-text-dim);cursor:pointer;user-select:none;");
const autoCb = el("input", "width:18px;height:18px;accent-color:#f59e0b;cursor:pointer;");
autoCb.type = "checkbox";
autoCb.checked = settings.auto_save !== false;
@@ -1985,13 +1985,13 @@ const CAT_UI = {}; // 会话级:cat -> { collapsed, onlySel, keyword }(不
* ============================================================ */
function openConfirm(title, message, onOk, okText) {
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:10200;display:flex;align-items:center;justify-content:center;");
const box = el("section", "background:#1c1c1e;border:1px solid #333;border-radius:8px;width:460px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:#ddd;font-size:13px;");
const box = el("section", "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:460px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:var(--xb-text);font-size:13px;");
box.dataset.ippModal = "confirm";
box.append(el("div", "font-size:15px;font-weight:700;color:#eee;margin-bottom:8px;", title || "确认"));
box.append(el("div", "font-size:13px;color:#bbb;line-height:1.8;white-space:pre-wrap;margin-bottom:14px;", message || ""));
box.append(el("div", "font-size:15px;font-weight:700;color:var(--xb-text);margin-bottom:8px;", title || "确认"));
box.append(el("div", "font-size:13px;color:var(--xb-text-dim);line-height:1.8;white-space:pre-wrap;margin-bottom:14px;", message || ""));
const row = el("div", "display:flex;gap:10px;justify-content:flex-end;");
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const okBtn = el("button", "background:#8a3a2a;border:1px solid #c2604a;color:#ffe7df;border-radius:4px;padding:7px 18px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", okText || "确定");
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const okBtn = el("button", "background:var(--xb-tint-error);border:1px solid #c2604a;color:#ffe7df;border-radius:4px;padding:7px 18px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", okText || "确定");
const close = () => { try { overlay.remove(); } catch (_) {} };
cancelBtn.addEventListener("click", (e) => { e.stopPropagation(); close(); });
okBtn.addEventListener("click", (e) => { e.stopPropagation(); close(); try { onOk && onOk(); } catch (_) {} });
@@ -2009,29 +2009,29 @@ function openConfirm(title, message, onOk, okText) {
* ============================================================ */
function openRelPicker(node, nameText, lang, current, onPick) {
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.78);z-index:10200;display:flex;align-items:center;justify-content:center;");
const box = el("section", "background:#1c1c1e;border:1px solid #333;border-radius:8px;width:780px;max-width:94vw;max-height:92vh;overflow:auto;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:#ddd;font-size:13px;");
const box = el("section", "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:780px;max-width:94vw;max-height:92vh;overflow:auto;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:var(--xb-text);font-size:13px;");
box.dataset.ippModal = "rel";
box.append(el("div", "font-size:15px;font-weight:700;color:#eee;margin-bottom:4px;", "🎒 道具与主体的关系"));
box.append(el("div", "font-size:12px;color:#8a97a5;margin-bottom:12px;line-height:1.7;",
box.append(el("div", "font-size:15px;font-weight:700;color:var(--xb-text);margin-bottom:4px;", "🎒 道具与主体的关系"));
box.append(el("div", "font-size:12px;color:var(--xb-text-dim);margin-bottom:12px;line-height:1.7;",
`道具:${nameText}\n选一个关系词,拼进提示词时它会加在道具前面(例:手拿折扇 / a folding fan, held in the hand)。`));
const grid = el("div", "display:grid;grid-template-columns:repeat(6,1fr);gap:6px;margin-bottom:14px;");
const close = () => { try { overlay.remove(); } catch (_) {} };
for (const [id, zh, en] of RELATIONS) {
const on = id === current;
const b = el("button", `display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;min-height:46px;border-radius:6px;cursor:pointer;font-family:inherit;padding:4px 2px;box-sizing:border-box;border:1px solid ${on ? "#7fb3e0" : "#4a4a4a"};background:${on ? "#2d5a88" : "#262626"};color:${on ? "#eaf4ff" : "#ddd"};`);
const b = el("button", `display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;min-height:46px;border-radius:6px;cursor:pointer;font-family:inherit;padding:4px 2px;box-sizing:border-box;border:1px solid ${on ? "#7fb3e0" : "var(--xb-border)"};background:${on ? "#2d5a88" : "var(--xb-bg-raise)"};color:${on ? "#eaf4ff" : "var(--xb-text)"};`);
b.dataset.rel = id;
b.title = `${zh} / ${en}`;
b.append(el("span", "font-size:13px;line-height:1.2;", zh));
b.append(el("span", "font-size:9px;line-height:1.2;color:#9fb6cc;text-align:center;word-break:break-word;", en));
b.addEventListener("mouseenter", () => { if (!on) b.style.background = "#333"; });
b.addEventListener("mouseleave", () => { if (!on) b.style.background = "#262626"; });
b.addEventListener("mouseenter", () => { if (!on) b.style.background = "var(--xb-border)"; });
b.addEventListener("mouseleave", () => { if (!on) b.style.background = "var(--xb-bg-raise)"; });
b.addEventListener("click", (e) => { e.stopPropagation(); close(); try { onPick && onPick(id); } catch (_) {} });
grid.append(b);
}
box.append(grid);
const row = el("div", "display:flex;gap:10px;justify-content:flex-end;");
const noneBtn = el("button", "background:#2a4a6a;border:1px solid #5b9bd5;color:#cfe3f7;border-radius:4px;padding:7px 16px;font-size:13px;cursor:pointer;font-family:inherit;", "不加关系(只加道具词)");
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const noneBtn = el("button", "background:var(--xb-tint-info);border:1px solid #5b9bd5;color:var(--xb-info);border-radius:4px;padding:7px 16px;font-size:13px;cursor:pointer;font-family:inherit;", "不加关系(只加道具词)");
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
noneBtn.addEventListener("click", (e) => { e.stopPropagation(); close(); try { onPick && onPick(""); } catch (_) {} });
cancelBtn.addEventListener("click", (e) => { e.stopPropagation(); close(); });
overlay.addEventListener("mousedown", (e) => { if (e.target === overlay) close(); });
@@ -2049,10 +2049,10 @@ function openRelPicker(node, nameText, lang, current, onPick) {
* ============================================================ */
function openDescEditor(node, entry, nameText, value, onSave) {
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:10100;display:flex;align-items:center;justify-content:center;");
const box = el("section", "background:#1c1c1e;border:1px solid #333;border-radius:8px;width:600px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:#ddd;font-size:13px;");
const box = el("section", "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:600px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:var(--xb-text);font-size:13px;");
box.dataset.ippModal = "desc"; // 稳定锤点(便于定位/自动化测试)
box.append(el("div", "font-size:15px;font-weight:700;color:#eee;margin-bottom:4px;", "📝 添加详细描述"));
box.append(el("div", "font-size:12px;color:#8a97a5;margin-bottom:10px;line-height:1.7;white-space:pre-wrap;",
box.append(el("div", "font-size:15px;font-weight:700;color:var(--xb-text);margin-bottom:4px;", "📝 添加详细描述"));
box.append(el("div", "font-size:12px;color:var(--xb-text-dim);margin-bottom:10px;line-height:1.7;white-space:pre-wrap;",
`词条:${nameText}\n`
+ "写在这里的内容会以「词条(描述)」的形式拼进提示词(例:一位女性(银灰色短发)),"
+ "所以只写要补的细节即可 —— 不用重复词条本身,也别写整句话。"));
@@ -2064,9 +2064,9 @@ function openDescEditor(node, entry, nameText, value, onSave) {
const tip = el("div", "font-size:11px;color:#777;margin-top:6px;", "Esc = 取消 · Ctrl/⌘ + Enter = 保存");
box.append(tip);
const row = el("div", "display:flex;justify-content:flex-end;gap:10px;margin-top:14px;");
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const clrBtn = el("button", "background:transparent;border:1px solid #844;color:#ffb0b0;border-radius:4px;padding:7px 14px;font-size:13px;cursor:pointer;font-family:inherit;margin-right:auto;", "🗑 清除描述");
const okBtn = el("button", "background:#2d5a88;color:#fff;border:none;border-radius:4px;padding:7px 20px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", "✅ 保存描述");
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const clrBtn = el("button", "background:transparent;border:1px solid #844;color:var(--xb-error);border-radius:4px;padding:7px 14px;font-size:13px;cursor:pointer;font-family:inherit;margin-right:auto;", "🗑 清除描述");
const okBtn = el("button", "background:var(--xb-btn-primary);color:#fff;border:none;border-radius:4px;padding:7px 20px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", "✅ 保存描述");
row.append(clrBtn, cancelBtn, okBtn);
box.append(row);
overlay.append(box);
@@ -2090,11 +2090,11 @@ function openDescEditor(node, entry, nameText, value, onSave) {
/** 自建槽位的「中/英名称」编辑窗(同样是一个小弹窗) */
function openSlotNameEditor(zhValue, enValue, onSave) {
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:10100;display:flex;align-items:center;justify-content:center;");
const box = el("section", "background:#1c1c1e;border:1px solid #333;border-radius:8px;width:520px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:#ddd;font-size:13px;");
const box = el("section", "background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:520px;max-width:92vw;box-shadow:0 20px 40px rgba(0,0,0,.6);padding:18px;box-sizing:border-box;color:var(--xb-text);font-size:13px;");
box.dataset.ippModal = "slotname"; // 稳定锤点
box.append(el("div", "font-size:15px;font-weight:700;color:#eee;margin-bottom:10px;", "✎ 自建槽位名称(中 / 英)"));
box.append(el("div", "font-size:15px;font-weight:700;color:var(--xb-text);margin-bottom:10px;", "✎ 自建槽位名称(中 / 英)"));
const mk = (label, val, ph) => {
box.append(el("div", "font-size:11px;color:#8a97a5;margin:6px 0 3px;", label));
box.append(el("div", "font-size:11px;color:var(--xb-text-dim);margin:6px 0 3px;", label));
const i = el("input", BOX_CSS + "width:100%;font-size:13px;padding:6px 8px;");
i.type = "text";
i.value = val || "";
@@ -2105,8 +2105,8 @@ function openSlotNameEditor(zhValue, enValue, onSave) {
const zh = mk("中文词条(中文 [ZH] 输出时用)", zhValue, "例:银灰色短发");
const en = mk("English word(英文 [EN] 输出时用)", enValue, "e.g. silver-gray short hair");
const row = el("div", "display:flex;justify-content:flex-end;gap:10px;margin-top:14px;");
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const okBtn = el("button", "background:#2d5a88;color:#fff;border:none;border-radius:4px;padding:7px 20px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", "✅ 保存名称");
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;font-family:inherit;", "取消");
const okBtn = el("button", "background:var(--xb-btn-primary);color:#fff;border:none;border-radius:4px;padding:7px 20px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;", "✅ 保存名称");
row.append(cancelBtn, okBtn);
box.append(row);
overlay.append(box);
@@ -2141,12 +2141,12 @@ function renderCategoryPanel(box, api, catId) {
catRow.dataset.ippCatRow = "1";
for (const c of CATEGORIES) {
const on = c.id === catId;
const b = el("button", `box-sizing:border-box;height:30px;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid ${on ? "#7fb3e0" : "#5b9bd5"};background:${on ? "#2d5a88" : "#3a3a3a"};color:${on ? "#eaf4ff" : "#eee"};font-size:13px;cursor:pointer;font-family:inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;`, c.label);
const b = el("button", `box-sizing:border-box;height:30px;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid ${on ? "#7fb3e0" : "#5b9bd5"};background:${on ? "#2d5a88" : "var(--xb-border)"};color:${on ? "#eaf4ff" : "var(--xb-text)"};font-size:13px;cursor:pointer;font-family:inherit;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;`, c.label);
b.dataset.cat = c.id;
if (on) b.dataset.catActive = "1";
b.title = `切到「${c.label}」面板 —— ${c.tip}`;
b.addEventListener("mouseenter", () => { if (!on) b.style.background = "#4a4a4a"; });
b.addEventListener("mouseleave", () => { if (!on) b.style.background = "#3a3a3a"; });
b.addEventListener("mouseenter", () => { if (!on) b.style.background = "var(--xb-border)"; });
b.addEventListener("mouseleave", () => { if (!on) b.style.background = "var(--xb-border)"; });
b.addEventListener("click", (e) => {
e.stopPropagation();
if (!on && api.switchCategory) api.switchCategory(c.id);
@@ -2156,7 +2156,7 @@ function renderCategoryPanel(box, api, catId) {
box.append(catRow);
const bar = el("div", "display:flex;gap:8px;flex-wrap:wrap;margin-bottom:6px;");
bar.append(
smallBtn("🧹 清空提示词", "border:1px solid #a44;background:#7a2a2a;color:#ffd0d0;padding:6px 12px;", "清空正文(同时清掉所有元素的「已选」状态)", () => {
smallBtn("🧹 清空提示词", "border:1px solid #a44;background:var(--xb-tint-error);color:var(--xb-error);padding:6px 12px;", "清空正文(同时清掉所有元素的「已选」状态)", () => {
openConfirm("🧹 清空提示词", "确定清空提示词正文?\n(所有元素的「已选」状态也会一起清掉)", () => {
s.elements.selected = {};
s.elements.rel = {};
@@ -2166,17 +2166,17 @@ function renderCategoryPanel(box, api, catId) {
notify("已清空提示词", "success");
});
}),
smallBtn("🔧 按选择重建", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;padding:6px 12px;", "按「已选元素 + 固定分类顺序(风格→视角→主体→姿态→装扮→道具→光影→背景)+ 当前语言」重排正文(手工改乱了就用它)", () => {
smallBtn("🔧 按选择重建", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);padding:6px 12px;", "按「已选元素 + 固定分类顺序(风格→视角→主体→姿态→装扮→道具→光影→背景)+ 当前语言」重排正文(手工改乱了就用它)", () => {
const t = rebuildBody(s, index, lang());
bodySet(t);
fire();
notify(t ? "已按已选元素重建正文" : "当前没有已选元素(重建后正文为空)", t ? "success" : "warning");
}),
smallBtn("📋 复制成句", "border:1px solid #555;background:#2a2a2a;color:#ccc;padding:6px 12px;", "复制最终成句(三视图模式下含预设句)", () => {
smallBtn("📋 复制成句", "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);padding:6px 12px;", "复制最终成句(三视图模式下含预设句)", () => {
copyText(finalPrompt(surf.mode, surf.presetText, surf.lang, bodyGet(), surf.io));
}),
smallBtn("⬆️ 导出词表", "border:1px solid #6b9b5b;background:#3a5a2a;color:#d7f0c8;padding:6px 12px;", "把「自建槽位 + 已选 + 描述」导出成 JSON 备份", () => exportJson(s)),
smallBtn("⬇️ 导入词表", "border:1px solid #6b9b5b;background:#3a5a2a;color:#d7f0c8;padding:6px 12px;", "从 JSON 恢复「自建槽位 + 已选 + 描述」(合并,不覆盖内置词表)", () => importJson((obj) => {
smallBtn("⬆️ 导出词表", "border:1px solid #6b9b5b;background:var(--xb-tint-ok);color:var(--xb-ok);padding:6px 12px;", "把「自建槽位 + 已选 + 描述」导出成 JSON 备份", () => exportJson(s)),
smallBtn("⬇️ 导入词表", "border:1px solid #6b9b5b;background:var(--xb-tint-ok);color:var(--xb-ok);padding:6px 12px;", "从 JSON 恢复「自建槽位 + 已选 + 描述」(合并,不覆盖内置词表)", () => importJson((obj) => {
if (!obj || typeof obj !== "object") return;
for (const k of ["custom", "noted", "selected"]) {
if (obj[k] && typeof obj[k] === "object") s.elements[k] = Object.assign({}, s.elements[k], obj[k]);
@@ -2208,7 +2208,7 @@ function renderCategoryPanel(box, api, catId) {
// 436 条词条只在第一次渲染时建 DOM;标题栏仍可点一下收起(纯会话状态)
if (!CAT_UI[cat.id]) CAT_UI[cat.id] = { collapsed: false, onlySel: false, keyword: "", sub: "" };
const ui = CAT_UI[cat.id];
const wrap = el("div", "border:1px solid #333;border-radius:6px;margin-bottom:8px;background:#191919;overflow:hidden;");
const wrap = el("div", "border:1px solid var(--xb-border);border-radius:6px;margin-bottom:8px;background:var(--xb-inset);overflow:hidden;");
const selOf = () => (s.elements.selected[cat.id] || []);
const selArr = () => (s.elements.selected[cat.id] || (s.elements.selected[cat.id] = []));
const rebuild = () => { wrap.innerHTML = ""; build(); };
@@ -2283,14 +2283,14 @@ function renderCategoryPanel(box, api, catId) {
const cur = subOf(cat, ui.sub).id;
for (const c of chipRefs) {
const on = c.dataset.sub === cur;
c.style.background = on ? "#2d5a88" : "#2a2a2a";
c.style.borderColor = on ? "#7fb3e0" : "#555";
c.style.background = on ? "#2d5a88" : "var(--xb-bg-raise)";
c.style.borderColor = on ? "#7fb3e0" : "var(--xb-border)";
c.style.color = on ? "#eaf4ff" : "#bbb";
c.style.fontWeight = on ? "600" : "400";
}
};
for (const sub of (cat.subs || [])) {
const b = el("button", "border:1px solid #555;border-radius:14px;padding:4px 14px;font-size:12px;cursor:pointer;font-family:inherit;white-space:nowrap;", `${sub.label}(${(sub.keys || []).length})`);
const b = el("button", "border:1px solid var(--xb-border);border-radius:14px;padding:4px 14px;font-size:12px;cursor:pointer;font-family:inherit;white-space:nowrap;", `${sub.label}(${(sub.keys || []).length})`);
b.dataset.sub = sub.id;
b.title = `切到「${sub.label}」子类(${(sub.keys || []).length} 条)`;
b.addEventListener("mousedown", (e) => e.stopPropagation());
@@ -2340,7 +2340,7 @@ function renderCategoryPanel(box, api, catId) {
fire();
notify(n ? `已加入 ${n} 条` : "这些词条都已在提示词里", n ? "success" : "warning");
};
tb.append(smallBtn("+ 全部加入", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;", "把本分类当前可见的词条全部追加进提示词", () => {
tb.append(smallBtn("+ 全部加入", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);", "把本分类当前可见的词条全部追加进提示词", () => {
const list = visibleEntries();
if (!list.length) { notify("没有可加入的词条", "warning"); return; }
if (list.length > 60) {
@@ -2349,7 +2349,7 @@ function renderCategoryPanel(box, api, catId) {
}
addAllNow(list);
}));
tb.append(smallBtn("🗑 全部移除", "border:1px solid #a44;background:#5a2a2a;color:#ffd0d0;", "把本分类已选词条全部从提示词移除", () => {
tb.append(smallBtn("🗑 全部移除", "border:1px solid #a44;background:var(--xb-tint-error);color:var(--xb-error);", "把本分类已选词条全部从提示词移除", () => {
const keys = selOf().slice();
if (!keys.length) { notify("本分类还没有已选词条", "warning"); return; }
removeKeys(keys);
@@ -2379,7 +2379,7 @@ function renderCategoryPanel(box, api, catId) {
const selected = selOf().includes(entry.key);
const descKey = `${cat.id}|${entry.key}`;
const desc = s.elements.noted[descKey] || "";
const row = el("div", `display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;margin-bottom:3px;box-sizing:border-box;cursor:pointer;user-select:none;background:${selected ? "#22456b" : "#242424"};border:1px solid ${selected ? "#3f86d0" : "#3a3a3a"};`);
const row = el("div", `display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;margin-bottom:3px;box-sizing:border-box;cursor:pointer;user-select:none;background:${selected ? "#22456b" : "var(--xb-bg-raise)"};border:1px solid ${selected ? "#3f86d0" : "var(--xb-border)"};`);
// 悬停整行 → 显示完整词条、当前状态与详细描述(原生 tooltip)
row.title = `${entry.zh || "(空)"} / ${entry.en || "(空)"}`
+ (entry.custom ? "\n(自建槽位)" : "")
@@ -2398,14 +2398,14 @@ function renderCategoryPanel(box, api, catId) {
renderRows();
fire();
});
if (entry.custom) row.append(el("span", "font-size:10px;color:#f0d79a;border:1px solid #8a6d3b;border-radius:3px;padding:0 3px;flex:0 0 auto;", "自建"));
if (entry.custom) row.append(el("span", "font-size:10px;color:var(--xb-warn);border:1px solid #8a6d3b;border-radius:3px;padding:0 3px;flex:0 0 auto;", "自建"));
const rel = relPair(s, cat.id, entry.key);
if (rel) row.append(el("span", "font-size:10px;color:#9fe0c0;border:1px solid #3f7a5f;border-radius:3px;padding:0 3px;flex:0 0 auto;", rel.zh));
const nm = el("span", `flex:1 1 auto;min-width:0;font-size:12px;line-height:1.4;color:${selected ? "#eaf4ff" : "#c8c8c8"};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;`,
`${selected ? "✔ " : ""}${entry.zh || "(空)"} / ${entry.en || "(空)"}`);
row.append(nm);
if (cat.id === "props") {
row.append(smallBtn(rel ? `🔗 ${rel.zh}` : "🔗 关系", "border:1px solid #5b9bd5;background:#2a4a6a;color:#cfe3f7;padding:4px 10px;flex:0 0 auto;", "选择/更换这个道具与主体的关系词(先移旧片段再加新片段)", () => {
row.append(smallBtn(rel ? `🔗 ${rel.zh}` : "🔗 关系", "border:1px solid #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);padding:4px 10px;flex:0 0 auto;", "选择/更换这个道具与主体的关系词(先移旧片段再加新片段)", () => {
openRelPicker(api.node, `${entry.zh || entry.key} / ${entry.en || ""}`, lang(),
(relPair(s, cat.id, entry.key) || {}).id || "",
(relId) => {
@@ -2417,7 +2417,7 @@ function renderCategoryPanel(box, api, catId) {
}));
}
const descBtn = smallBtn(desc ? "📝 详细描述" : "添加详细描述",
`border:1px solid ${desc ? "#8a6d3b" : "#555"};background:${desc ? "#3a2f16" : "#2a2a2a"};color:${desc ? "#ffd98a" : "#9fc3e8"};padding:4px 10px;flex:0 0 auto;`,
`border:1px solid ${desc ? "#8a6d3b" : "var(--xb-border)"};background:${desc ? "#3a2f16" : "var(--xb-bg-raise)"};color:${desc ? "#ffd98a" : "#9fc3e8"};padding:4px 10px;flex:0 0 auto;`,
desc ? "编辑该词条的详细描述(新窗口)" : "为该词条添加详细描述(新窗口)",
() => openDescEditor(api.node, entry, `${entry.zh || entry.en || entry.key}`, desc, (text) => {
const v = String(text || "").trim();
@@ -2456,7 +2456,7 @@ function renderCategoryPanel(box, api, catId) {
const selected = selOf().includes(it.key);
const descKey = `${cat.id}|${it.key}`;
const desc = s.elements.noted[descKey] || "";
const row = el("div", `display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;margin-bottom:3px;box-sizing:border-box;cursor:pointer;user-select:none;background:${selected ? "#22456b" : "#242424"};border:1px solid ${selected ? "#3f86d0" : "#3a3a3a"};`);
const row = el("div", `display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;margin-bottom:3px;box-sizing:border-box;cursor:pointer;user-select:none;background:${selected ? "#22456b" : "var(--xb-bg-raise)"};border:1px solid ${selected ? "#3f86d0" : "var(--xb-border)"};`);
row.title = `${it.zh || "(未填中文)"} / ${it.en || "(未填英文)"}\n(自建槽位)`
+ `\n${selected ? "已加入提示词 —— 再点一下移除" : "点一下加入提示词"}`
+ "\n\n详细描述:" + (desc || "(还没有,点【添加详细描述】写一条)");
@@ -2466,10 +2466,10 @@ function renderCategoryPanel(box, api, catId) {
rebuild();
fire();
});
row.append(el("span", "font-size:10px;color:#f0d79a;border:1px solid #8a6d3b;border-radius:3px;padding:0 3px;flex:0 0 auto;", "自建"));
row.append(el("span", "font-size:10px;color:var(--xb-warn);border:1px solid #8a6d3b;border-radius:3px;padding:0 3px;flex:0 0 auto;", "自建"));
row.append(el("span", `flex:1 1 auto;min-width:0;font-size:12px;color:${selected ? "#eaf4ff" : "#c8c8c8"};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;`,
`${selected ? "✔ " : ""}${it.zh || "(未填中文)"} / ${it.en || "(未填英文)"}`));
row.append(smallBtn("✎ 编辑名称", "border:1px solid #555;background:#2a2a2a;color:#9fc3e8;padding:4px 10px;flex:0 0 auto;", "新窗口编辑该槽位的中/英词条", () => {
row.append(smallBtn("✎ 编辑名称", "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-info);padding:4px 10px;flex:0 0 auto;", "新窗口编辑该槽位的中/英词条", () => {
openSlotNameEditor(it.zh, it.en, (z, e2) => {
it.zh = z; it.en = e2;
if (entry) { entry.zh = z; entry.en = e2; }
@@ -2479,7 +2479,7 @@ function renderCategoryPanel(box, api, catId) {
});
}));
row.append(smallBtn(desc ? "📝 详细描述" : "添加详细描述",
`border:1px solid ${desc ? "#8a6d3b" : "#555"};background:${desc ? "#3a2f16" : "#2a2a2a"};color:${desc ? "#ffd98a" : "#9fc3e8"};padding:4px 10px;flex:0 0 auto;`,
`border:1px solid ${desc ? "#8a6d3b" : "var(--xb-border)"};background:${desc ? "#3a2f16" : "var(--xb-bg-raise)"};color:${desc ? "#ffd98a" : "#9fc3e8"};padding:4px 10px;flex:0 0 auto;`,
desc ? "编辑该自建词条的详细描述(新窗口)" : "为该自建词条添加详细描述(新窗口)",
() => openDescEditor(api.node, entry || { key: it.key, zh: it.zh, en: it.en }, `${it.zh || it.en || it.key}`, desc, (text) => {
const v = String(text || "").trim();
@@ -2494,7 +2494,7 @@ function renderCategoryPanel(box, api, catId) {
api.refreshPreview(null);
fire();
})));
row.append(smallBtn("🗑", "border:1px solid #a44;background:#3a2020;color:#ffb0b0;width:24px;height:24px;padding:0;line-height:1;flex:0 0 auto;", "删除这个自建槽位", () => {
row.append(smallBtn("🗑", "border:1px solid #a44;background:var(--xb-tint-error);color:var(--xb-error);width:24px;height:24px;padding:0;line-height:1;flex:0 0 auto;", "删除这个自建槽位", () => {
openConfirm("删除自建槽位", `删除自建槽位「${it.zh || it.en || it.key}」?`, () => {
removeKeys([it.key]);
delete s.elements.noted[`${cat.id}|${it.key}`];
@@ -2512,7 +2512,7 @@ function renderCategoryPanel(box, api, catId) {
renderCustom();
const cusBar = el("div", "display:flex;gap:8px;flex-wrap:wrap;margin-top:4px;");
cusBar.append(smallBtn("+ 新增槽位", "border:1px dashed #5b9bd5;background:#2a3a4a;color:#9fc3e8;padding:6px 12px;", "新增一个自定义词条槽位(中/英各填一个)", () => {
cusBar.append(smallBtn("+ 新增槽位", "border:1px dashed #5b9bd5;background:var(--xb-tint-info);color:var(--xb-info);padding:6px 12px;", "新增一个自定义词条槽位(中/英各填一个)", () => {
const arr = s.elements.custom[cat.id] || (s.elements.custom[cat.id] = []);
arr.push({ key: `c${Date.now().toString(36)}${Math.floor(Math.random() * 46656).toString(36)}`, zh: "", en: "" });
index = buildIndex(s.elements);
@@ -2520,7 +2520,7 @@ function renderCategoryPanel(box, api, catId) {
rebuild();
fire();
}));
cusBar.append(smallBtn("🗑 删除槽位", "border:1px solid #a44;background:#5a2a2a;color:#ffd0d0;padding:6px 12px;", "删除本分类最后一个自建槽位(要删指定的,用该行右侧的 🗑)", () => {
cusBar.append(smallBtn("🗑 删除槽位", "border:1px solid #a44;background:var(--xb-tint-error);color:var(--xb-error);padding:6px 12px;", "删除本分类最后一个自建槽位(要删指定的,用该行右侧的 🗑)", () => {
const arr = s.elements.custom[cat.id] || [];
if (!arr.length) { notify("本分类还没有自建槽位", "warning"); return; }
const last = arr[arr.length - 1];
@@ -2543,10 +2543,10 @@ function renderCategoryPanel(box, api, catId) {
/** 分类外壳:标题栏总是建,内容懒建(展开才建) */
function build() {
wrap.innerHTML = "";
const headRow = el("div", "display:flex;align-items:center;gap:8px;padding:8px 10px;background:#20242a;cursor:pointer;flex-wrap:wrap;");
const arrow = el("span", "font-size:11px;color:#9fc3e8;width:12px;flex:0 0 12px;", ui.collapsed ? "▶" : "▼");
const cnt = el("span", "font-size:11px;color:#8a97a5;", countText());
headRow.append(arrow, el("span", "font-size:13px;font-weight:600;color:#cfe3f7;", cat.label), cnt,
const headRow = el("div", "display:flex;align-items:center;gap:8px;padding:8px 10px;background:var(--xb-bg-soft);cursor:pointer;flex-wrap:wrap;");
const arrow = el("span", "font-size:11px;color:var(--xb-info);width:12px;flex:0 0 12px;", ui.collapsed ? "▶" : "▼");
const cnt = el("span", "font-size:11px;color:var(--xb-text-dim);", countText());
headRow.append(arrow, el("span", "font-size:13px;font-weight:600;color:var(--xb-info);", cat.label), cnt,
el("span", "font-size:11px;color:#6f7c8a;flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;", cat.tip || ""));
const body = el("div", "padding:8px 10px;");
body.style.display = ui.collapsed ? "none" : "block";
@@ -2811,11 +2811,11 @@ function setupNode(node) {
const btnRow = el("div", "display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:30px;gap:6px;flex:0 0 auto;");
for (const b of PANEL_BUTTONS) {
const cat = catOf(b.panel);
const btn = el("button", "width:100%;height:30px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid #5b9bd5;background:#3a3a3a;color:#eee;font-size:13px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:inherit;", b.label);
const btn = el("button", "width:100%;height:30px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid #5b9bd5;background:var(--xb-border);color:var(--xb-text);font-size:13px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:inherit;", b.label);
btn.title = `打开「${cat.label}」元素面板 —— ${cat.tip}\\n(里面:点选项行加入/移除提示词,【添加详细描述】写补充描述)`;
btn.addEventListener("mousedown", (e) => e.stopPropagation());
btn.addEventListener("mouseenter", () => { btn.style.background = "#4a4a4a"; });
btn.addEventListener("mouseleave", () => { btn.style.background = "#3a3a3a"; });
btn.addEventListener("mouseenter", () => { btn.style.background = "var(--xb-border)"; });
btn.addEventListener("mouseleave", () => { btn.style.background = "var(--xb-border)"; });
btn.addEventListener("click", (e) => {
e.stopPropagation();
const settings = loadSettings(node);
@@ -2914,9 +2914,9 @@ function setupNode(node) {
box.style.margin = "0 8px";
box.style.boxSizing = "border-box";
box.style.borderRadius = "4px";
box.style.border = "1px solid #444";
box.style.background = "#1d1d1d";
box.style.color = "#ccc";
box.style.border = "1px solid var(--xb-border)";
box.style.background = "var(--xb-bg)";
box.style.color = "var(--xb-text)";
box.style.padding = "5px 8px";
box.style.fontSize = "12px";
box.style.lineHeight = "1.6";
@@ -3453,7 +3453,7 @@ function xbrTextControl(value, placeholder, onChange) {
/** 导演台同款「种子 + 生成后控制(随机/固定/增加/减少)」一行 */
function xbrSeedRow(run, onChange) {
const g = el("div", "display:flex;align-items:center;gap:10px;margin-bottom:10px;");
g.append(el("label", "flex:0 0 160px;font-size:13px;color:#999;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;", "种子"));
g.append(el("label", "flex:0 0 160px;font-size:13px;color:var(--xb-text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;", "种子"));
const i = xbrNumberControl(run.seed, { min: 0, max: Number.MAX_SAFE_INTEGER, step: 1 }, (v) => { run.seed = Math.round(v); onChange?.(); });
i.style.flex = "1 1 0";
const ctrl = smallBtn("", "flex:1 1 0;min-width:0;background:#b45309;border:1px solid #d97706;color:#fff;text-align:center;", "生成后控制:点击在 随机 / 固定 / 增加 / 减少 间切换", null);
@@ -3476,24 +3476,24 @@ function xbrSeedRow(run, onChange) {
let xbrModal = null;
function xbrDialog({ label, title, subtitle, width = 820, autoSaveRef = null, onCommit = null, onImmediate = null, onChange = null, onClose = null }) {
const overlay = el("div", "position:fixed;inset:0;background:rgba(0,0,0,.78);z-index:10300;display:flex;align-items:center;justify-content:center;overflow:auto;");
const dialog = el("section", `background:#1c1c1e;border:1px solid #333;border-radius:8px;width:${width}px;max-width:94vw;` +
"max-height:90vh;display:flex;flex-direction:column;box-shadow:0 20px 40px rgba(0,0,0,.6);flex:0 0 auto;color:#ddd;font-size:13px;");
const dialog = el("section", `background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:8px;width:${width}px;max-width:94vw;` +
"max-height:90vh;display:flex;flex-direction:column;box-shadow:0 20px 40px rgba(0,0,0,.6);flex:0 0 auto;color:var(--xb-text);font-size:13px;");
dialog.dataset.xbrProModal = label;
dialog.setAttribute("role", "dialog");
dialog.setAttribute("aria-modal", "true");
dialog.setAttribute("aria-label", label);
const header = el("div", "padding:18px 20px;border-bottom:1px solid #444;");
header.append(el("div", "font-size:17px;font-weight:700;color:#eee;", title));
if (subtitle) header.append(el("div", "font-size:12px;color:#999;margin-top:5px;line-height:1.6;", subtitle));
const header = el("div", "padding:18px 20px;border-bottom:1px solid var(--xb-border);");
header.append(el("div", "font-size:17px;font-weight:700;color:var(--xb-text);", title));
if (subtitle) header.append(el("div", "font-size:12px;color:var(--xb-text-dim);margin-top:5px;line-height:1.6;", subtitle));
const body = el("div", "padding:16px 20px;overflow-y:auto;flex:1;min-height:200px;");
const error = el("div", "color:#e55;font-size:12px;margin:0 20px;min-height:14px;");
const footer = el("div", "padding:12px 20px;border-top:1px solid #444;display:flex;justify-content:flex-end;gap:10px;align-items:center;background:#18181a;border-bottom-left-radius:8px;border-bottom-right-radius:8px;");
const footer = el("div", "padding:12px 20px;border-top:1px solid var(--xb-border);display:flex;justify-content:flex-end;gap:10px;align-items:center;background:var(--xb-bg-soft);border-bottom-left-radius:8px;border-bottom-right-radius:8px;");
const left = el("div", "display:flex;align-items:center;gap:16px;margin-right:auto;flex-wrap:wrap;");
let autoCb = null;
if (autoSaveRef) {
const autoRow = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:#999;cursor:pointer;user-select:none;");
const autoRow = el("label", "display:flex;align-items:center;gap:6px;font-size:13px;color:var(--xb-text-dim);cursor:pointer;user-select:none;");
autoCb = el("input", "width:18px;height:18px;accent-color:#f59e0b;cursor:pointer;");
autoCb.type = "checkbox";
autoCb.checked = autoSaveRef.value !== false;
@@ -3508,8 +3508,8 @@ function xbrDialog({ label, title, subtitle, width = 820, autoSaveRef = null, on
uiRow.title = "调整整个弹窗界面大小(不缩放网页/画布;Ctrl/⌘+滚轮 也可)";
left.append(zoomRow, uiRow);
const cancelBtn = el("button", "background:transparent;border:1px solid #555;color:#fff;border-radius:4px;padding:8px 20px;font-size:14px;cursor:pointer;font-family:inherit;", "取消");
const saveBtn = onCommit ? el("button", "background:#2d5a88;color:#fff;border:none;border-radius:4px;padding:8px 20px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;", "💾 保存") : null;
const cancelBtn = el("button", "background:transparent;border:1px solid var(--xb-border);color:#fff;border-radius:4px;padding:8px 20px;font-size:14px;cursor:pointer;font-family:inherit;", "取消");
const saveBtn = onCommit ? el("button", "background:var(--xb-btn-primary);color:#fff;border:none;border-radius:4px;padding:8px 20px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;", "💾 保存") : null;
footer.append(left, cancelBtn);
if (saveBtn) footer.append(saveBtn);
dialog.append(header, body, error, footer);
@@ -3668,7 +3668,7 @@ function xbrRenderPreset(body, node, draft, ctx) {
body.append(taP);
const taRow = el("div", "display:flex;align-items:center;gap:10px;margin:6px 0 4px;");
if (modeDef) {
taRow.append(smallBtn("♻️ 恢复默认", "border:1px solid #555;background:#2a2a2a;color:#ccc;padding:4px 10px;",
taRow.append(smallBtn("♻️ 恢复默认", "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);padding:4px 10px;",
"把设定词恢复为该模式 × 该模版 × 该语言的默认文本",
() => { draft.presetText = modeDef; draft.presetTouched = false; ctx.rerender(); }));
}
@@ -3705,7 +3705,7 @@ function xbrRenderPreset(body, node, draft, ctx) {
"不用:SKILL 完全不生效(即使下面选过文件也不会送进模型)。"));
}
const foxRow = el("div", "display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:2px 0 8px;");
foxRow.append(smallBtn("📂 打开 SKILL 文件夹", "border:1px solid #555;background:#2a2a2a;color:#ccc;padding:4px 10px;",
foxRow.append(smallBtn("📂 打开 SKILL 文件夹", "border:1px solid var(--xb-border);background:var(--xb-bg-raise);color:var(--xb-text);padding:4px 10px;",
"在资源管理器里打开 support_llama/skills,方便新增 / 编辑技能文件",
async () => {
try {
@@ -3867,7 +3867,7 @@ function xbrRenderHelp(body) {
" · 弹窗底部:☑ 自动保存(默认开)、字号 A− / A+、界面 − / %",
" · 所有配置随工作流保存;「取消」丢弃未保存改动",
];
body.append(el("div", "font-size:12px;color:#bbb;line-height:1.9;white-space:pre-wrap;font-family:ui-monospace,Consolas,monospace;", lines.join("\n")));
body.append(el("div", "font-size:12px;color:var(--xb-text-dim);line-height:1.9;white-space:pre-wrap;font-family:ui-monospace,Consolas,monospace;", lines.join("\n")));
}
const XBR_PANEL_RENDER = { llm: xbrRenderLlm, preset: xbrRenderPreset, help: xbrRenderHelp };
@@ -3986,12 +3986,12 @@ async function xbrOpenModal(node, panelId) {
function xbrBuildButtonRow(node) {
const grid = el("div", "display:grid;grid-template-columns:repeat(" + Math.max(1, XBR_PANEL_BUTTONS.length) + ",1fr);grid-auto-rows:30px;gap:6px;flex:0 0 auto;");
for (const b of XBR_PANEL_BUTTONS) {
const btn = el("button", "width:100%;height:30px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid #5b9bd5;background:#3a3a3a;color:#eee;font-size:13px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:inherit;", b.label);
const btn = el("button", "width:100%;height:30px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;border-radius:6px;border:2px solid #5b9bd5;background:var(--xb-border);color:var(--xb-text);font-size:13px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:inherit;", b.label);
btn.title = `打开「${b.title}」弹窗`;
btn.dataset.xbrProBtn = b.id;
btn.addEventListener("mousedown", (e) => e.stopPropagation());
btn.addEventListener("mouseenter", () => { btn.style.background = "#4a4a4a"; });
btn.addEventListener("mouseleave", () => { btn.style.background = "#3a3a3a"; });
btn.addEventListener("mouseenter", () => { btn.style.background = "var(--xb-border)"; });
btn.addEventListener("mouseleave", () => { btn.style.background = "var(--xb-border)"; });
btn.addEventListener("click", (e) => { e.stopPropagation(); xbrOpenModal(node, b.id); });
grid.append(btn);
}
@@ -4027,11 +4027,11 @@ function xbrInfoLines(node, promptText) {
/** 提示词框标题行(左标签 + 右复制)+ 参数设定显示;并挂上端口锁定/执行回写/摘要刷新 */
function xbrBuildPromptHeader(node, promptBox) {
const head = el("div", "display:flex;align-items:center;gap:8px;flex:0 0 auto;");
const lab = el("div", "font-size:12px;color:#bbb;", "📝 节点提示词(与面板预览框双向同步)");
const lab = el("div", "font-size:12px;color:var(--xb-text-dim);", "📝 节点提示词(与面板预览框双向同步)");
const copy = smallBtn("📋 复制", "margin-left:auto;", "复制提示词框内容", () => { copyText(promptBox.value || ""); });
head.append(lab, copy);
const infoBox = el("div", "flex:0 0 auto;height:150px;overflow:auto;background:#1c1c1e;border:1px solid #333;border-radius:6px;padding:6px 9px;font-size:11px;line-height:1.65;color:#b9c6d2;white-space:pre-wrap;box-sizing:border-box;font-family:ui-monospace,Consolas,monospace;");
const infoBox = el("div", "flex:0 0 auto;height:150px;overflow:auto;background:var(--xb-bg);border:1px solid var(--xb-border);border-radius:6px;padding:6px 9px;font-size:11px;line-height:1.65;color:#b9c6d2;white-space:pre-wrap;box-sizing:border-box;font-family:ui-monospace,Consolas,monospace;");
infoBox.dataset.captureWheel = "true";
infoBox.title = "当前配置摘要(只读)";
@@ -4045,8 +4045,8 @@ function xbrBuildPromptHeader(node, promptBox) {
const inp = (node.inputs || []).find((i) => i.name === "text");
const linked = !!inp && inp.link != null;
promptBox.readOnly = linked;
promptBox.style.background = linked ? "#141416" : "#1d1d1d";
promptBox.style.color = linked ? "#8a8a8a" : "#ccc";
promptBox.style.background = linked ? "#141416" : "var(--xb-bg)";
promptBox.style.color = linked ? "var(--xb-text-dim)" : "var(--xb-text)";
promptBox.title = linked ? "已由「📝 提示词」端口控制,断开连线后恢复编辑" : "";
lab.textContent = linked ? "📝 提示词已锁定(由「📝 提示词」端口控制)" : "📝 节点提示词(与面板预览框双向同步)";
lab.style.color = linked ? "#d9a441" : "#bbb";
+90
View File
@@ -0,0 +1,90 @@
/**
* XB_ToolBox — 主题变量
* ============================================================
* 插件的 DOM widget(提示词面板、选择器、弹窗等)里的大量内联样式原本写死深色,
* 在 ComfyUI 浅色主题下会变成"黑块"。这里统一注入一组 CSS 变量,
* 各 widget 改用 var(--xb-*) 后即可自动跟随明暗主题。
*
* ComfyUI 通过调色板设置切换主题(不同版本可能改 <html> / <body> 的 class),
* 所以这里监听 class 变化实时更新,无需刷新页面。
*/
import { app } from "../../scripts/app.js";
const PALETTE = {
// 名称: [深色值, 浅色值]
"--xb-bg": ["#1c1c1e", "#ffffff"],
"--xb-bg-soft": ["#232326", "#f2f3f5"],
"--xb-bg-raise": ["#2b2b2b", "#e9eaee"],
"--xb-border": ["#3a3a3a", "#c9ced6"],
"--xb-text": ["#dddddd", "#23262a"],
"--xb-text-dim": ["#8a97a5", "#5c626b"],
"--xb-accent-bg": ["#2d5a88", "#2f6fbf"],
"--xb-info": ["#cfe3f7", "#1b5e9c"],
"--xb-ok": ["#d7f0c8", "#1f7a3a"],
"--xb-warn": ["#ffd98a", "#8a5f10"],
"--xb-error": ["#ffd0d0", "#a32626"],
"--xb-orange": ["#e8a87c", "#8a4a1f"],
"--xb-on-accent": ["#eaf4ff", "#ffffff"],
"--xb-tint-info": ["#2a4a6a", "#e3edf9"],
"--xb-tint-warn": ["#5a4a2a", "#fdf1d6"],
"--xb-tint-ok": ["#3a5a2a", "#e7f5e9"],
"--xb-tint-error": ["#5a2a2a", "#fdeaea"],
"--xb-btn-primary": ["#2d5a88", "#1f6fd0"],
"--xb-inset": ["#191919", "#f5f6f8"],
"--xb-float": ["#2a2a2e", "#ffffff"],
"--xb-btn-info": ["#2a4a6a", "#1f6fd0"],
"--xb-btn-warn": ["#b45309", "#c2410c"],
"--xb-btn-ok": ["#2d5a3a", "#1f7a3a"],
"--xb-btn-error": ["#a33a3a", "#b3261e"],
"--xb-accent-bg-hover": ["#22456b", "#1a5cb0"],
"--xb-accent-text": ["#eaf4ff", "#ffffff"],
};
function prefersLight() {
const html = document.documentElement;
const body = document.body;
if (html?.classList?.contains("light-theme")) return true;
if (html?.classList?.contains("dark-theme")) return false;
if (body?.classList?.contains("light-theme")) return true;
if (body?.classList?.contains("dark-theme")) return false;
try {
const palette = app?.ui?.settings?.getSettingValue?.("Comfy.ColorPalette");
if (palette === "light") return true;
if (palette === "dark") return false;
} catch (_) { /* ignore */ }
return false;
}
function applyTheme() {
const light = prefersLight();
const root = document.documentElement;
for (const [name, [dark, bright]] of Object.entries(PALETTE)) {
root.style.setProperty(name, light ? bright : dark);
}
root.dataset.xbTheme = light ? "light" : "dark";
}
let installed = false;
function install() {
if (installed) return;
installed = true;
applyTheme();
const observer = new MutationObserver(applyTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class", "data-theme"],
});
if (document.body) {
observer.observe(document.body, { attributes: true, attributeFilter: ["class"] });
}
}
app.registerExtension({
name: "XB_ToolBox.Theme",
async setup() {
install();
},
});
+3 -3
View File
@@ -31,7 +31,7 @@ app.registerExtension({
const el = wDur.inputEl || wDur.element;
if (el) {
el.readOnly = true;
el.style.cssText = "background-color:#1a1a1a;color:#00E676;text-align:center;font-weight:bold;font-size:15px;min-width:120px;white-space:nowrap;overflow:hidden;";
el.style.cssText = "background-color:var(--xb-bg);color:#00E676;text-align:center;font-weight:bold;font-size:15px;min-width:120px;white-space:nowrap;overflow:hidden;";
}
}, 100);
}
@@ -56,14 +56,14 @@ app.registerExtension({
}
const ctr = document.createElement("div");
Object.assign(ctr.style, { display:"flex", flexDirection:"column", gap:"6px", width:"100%", padding:"6px", boxSizing:"border-box", background:"#161616", borderRadius:"6px", color:"#ccc", fontFamily:"sans-serif", marginTop:"4px" });
Object.assign(ctr.style, { display:"flex", flexDirection:"column", gap:"6px", width:"100%", padding:"6px", boxSizing:"border-box", background:"#161616", borderRadius:"6px", color:"var(--xb-text)", fontFamily:"sans-serif", marginTop:"4px" });
const audioEl = document.createElement("audio");
audioEl.controls = true; audioEl.style.cssText = "width:100%;height:34px;outline:none;";
ctr.appendChild(audioEl);
const canvas = document.createElement("canvas");
canvas.style.cssText = "width:100%;height:40px;border-radius:4px;background:#111;cursor:pointer;display:block;";
canvas.style.cssText = "width:100%;height:40px;border-radius:4px;background:var(--xb-inset);cursor:pointer;display:block;";
const dpr = window.devicePixelRatio || 1;
canvas.width = 1200 * dpr; canvas.height = 80 * dpr;
ctr.appendChild(canvas);
+5 -5
View File
@@ -164,7 +164,7 @@ function makeButton(node, w, item) {
const button = document.createElement("button");
button.textContent = label;
if (tip) button.title = tip;
button.style.cssText = "padding:7px 6px;border:1px solid #555;border-radius:6px;background:#2a2a2e;color:#eee;cursor:pointer;font-size:12px;line-height:1.25;text-align:left;";
button.style.cssText = "padding:7px 6px;border:1px solid var(--xb-border);border-radius:6px;background:#2a2a2e;color:var(--xb-text);cursor:pointer;font-size:12px;line-height:1.25;text-align:left;";
button.addEventListener("mouseenter", () => { button.style.background = "#3a3a40"; });
button.addEventListener("mouseleave", () => { button.style.background = "#2a2a2e"; });
button.addEventListener("click", () => {
@@ -202,10 +202,10 @@ function addCustomTagRow(panel, node, w) {
const input = document.createElement("input");
input.type = "text";
input.placeholder = "例如 Rap / 高潮 / 吟唱";
input.style.cssText = "flex:1;padding:7px 8px;border:1px solid #555;border-radius:6px;background:#2a2a2e;color:#eee;font-size:12px;";
input.style.cssText = "flex:1;padding:7px 8px;border:1px solid var(--xb-border);border-radius:6px;background:#2a2a2e;color:var(--xb-text);font-size:12px;";
const button = document.createElement("button");
button.textContent = "插入标签";
button.style.cssText = "padding:7px 12px;border:1px solid #555;border-radius:6px;background:#2a2a2e;color:#eee;cursor:pointer;font-size:12px;";
button.style.cssText = "padding:7px 12px;border:1px solid var(--xb-border);border-radius:6px;background:#2a2a2e;color:var(--xb-text);cursor:pointer;font-size:12px;";
const commit = () => {
const raw = input.value.trim().replace(/^\[|\]$/g, "").trim();
if (!raw) return;
@@ -228,13 +228,13 @@ function showTagPicker(node, w) {
overlay.addEventListener("mousedown", (event) => { if (event.target === overlay) closeTagPicker(); });
const panel = document.createElement("div");
panel.style.cssText = "background:#1b1b1f;border:1px solid #444;border-radius:10px;padding:14px;max-width:700px;width:92%;max-height:86vh;overflow:auto;box-shadow:0 10px 40px rgba(0,0,0,.6);";
panel.style.cssText = "background:#1b1b1f;border:1px solid var(--xb-border);border-radius:10px;padding:14px;max-width:700px;width:92%;max-height:86vh;overflow:auto;box-shadow:0 10px 40px rgba(0,0,0,.6);";
const title = document.createElement("div");
title.textContent = "添加元素";
title.title = "点击标签插入到光标处。段落标签会独占一行并自动换行;"
+ "行级角色标注直接插在光标处,接着写歌词即可。";
title.style.cssText = "color:#eee;font-size:14px;font-weight:600;margin-bottom:8px;cursor:help;";
title.style.cssText = "color:var(--xb-text);font-size:14px;font-weight:600;margin-bottom:8px;cursor:help;";
panel.appendChild(title);
addGroup(panel, "① 官方段落词表(8 个)", GROUP_TIPS.OFFICIAL,