fix(theme-builder): 图层换不了图、背景关不掉、草稿清不掉
三个问题都是交互设计造成的,核心逻辑其实没坏(实测换图是生效的): 1. 「每层都只有这张图、换不了」 - 加层时 l.asset 默认绑 S.assets[0],连加三层全是同一张; - 换图的下拉只在「选中那一层」的编辑器里,用户根本看不出每层能各自换。 改为:新层不自动绑图(素材池只剩一张时才绑),每行带缩略图 + 「图片」下拉直接换。 2. 「背景图一直保留、关不掉」 - 「跟随全局」状态下背景下拉是 disabled,用户在箱主页看到背景还在又点不动。 改为:一律可点,一改就自动把这一页切成独立配置,并把全局当前值复制过来当起点 (改背景不会顺带把图案和图层弄丢;图层重新发 id,避免两个作用域共用一批 id)。 3. 「上次的数据关闭后还在」 - 之前只能靠隐藏参数 ?fresh=1 绕过 localStorage 草稿,UI 上没有出口。 顶栏加「新建」:清空工程 + 删草稿,二次确认。 顺带: - 「加一层」不再因 inherit 而 disabled(点了自动切独立),改为独立后若全局层数已达 10 会明确提示; - 跟随全局时不给拖拽框 —— 那时 cfgOf 返回的是 global 对象,一拖就改到全局去了; - 编辑器里删掉重复的图片下拉,只留尺寸。 回归:?dump=layers 全绿 —— 加层后 asset 为 [null,null]、行内下拉 2 个、 换第 2 层后 [null,blue] 且 CSS 只出现 blue;box 页 inherit 时 addDisabled/bgDisabled 均为 false; 在 box 加层后 inherit=false、本页 3 层而 **全局仍是 2 层**(没被污染); 新建后 assets 0、层 0、草稿删除、scope 回 global。 另外 ?dump=fx 13 项、?dump=validate 空、生成的 CSS 过 TestThemeCSSComplete + TestDecoSlotCount。
This commit is contained in:
@@ -51,6 +51,14 @@ http://127.0.0.1:8777/dev/theme-builder/
|
||||
默认全部「跟随全局」;勾掉就变成独立配置,导出时才会产出覆盖块。
|
||||
没配的页面自动沿用全局,不会出现「某页装饰消失」。
|
||||
|
||||
以前「跟随全局」时背景下拉和「加一层」都是 disabled —— 用户在箱主页看到背景还在、
|
||||
又点不动,这就是「背景图关不掉」的来源。现在:
|
||||
|
||||
- 「加一层」「改背景」永远可点,**一改就自动把这一页切成独立配置**,并把全局当前值复制过来当起点
|
||||
(改背景不会顺带把图案和图层弄丢;图层重新发 id,避免两个作用域共用一批 id,拖拽/高亮会串);
|
||||
- 这种状态下图层列表不展示全局的层、也不给拖拽框 —— 否则一拖就改到全局去了;
|
||||
- 想主动切换用「跟随全局」复选框,或用列表里的「改为独立配置」按钮。
|
||||
|
||||
(首页 `/` 与 `/explore` 不列出来 —— 全站公开页不注入 `ThemeSlug`,不加载主题 css,配了也没效果。)
|
||||
|
||||
**装饰图层** —— 每个作用域最多 10 层,三种类型:
|
||||
@@ -61,6 +69,11 @@ http://127.0.0.1:8777/dev/theme-builder/
|
||||
|
||||
锚点用 9 宫格 + 像素偏移,生成 `calc(0% + 40px) calc(100% - 36px)` 这种两值形式。
|
||||
|
||||
**每一行都能直接换图**:行里带缩略图 + 「图片」下拉,一眼看到这层用的是哪张。
|
||||
之前换图的下拉只出现在「选中那一层」的编辑器里,加上新层又默认绑第一张素材,
|
||||
连加三层就全是同一张图,看起来像「每层只能显示这张、换不了」。
|
||||
现在新加的层**不会**自动绑第一张(只有素材池只剩一张时才绑),行里显示「(选图片)」等你选。
|
||||
|
||||
**↑ / ↓ 调层级**。顺序就是层级:`background-image` 的第一层画在最上面,
|
||||
所以「谁挡住谁」是靠顺序定的(shiroko 里自行车被人挡住就是这么来的)。
|
||||
|
||||
@@ -98,6 +111,8 @@ http://127.0.0.1:8777/dev/theme-builder/
|
||||
|
||||
调主题是连续几十次的活,所以配置必须留得住:
|
||||
|
||||
- **新建**:顶栏按钮,清空当前工程并删掉浏览器草稿(有二次确认)。
|
||||
之前只有 `?fresh=1` 这个隐藏参数 —— 用户看到「上次的数据」一直在,就会以为关不掉。
|
||||
- **自动草稿**:每次改动后 700ms 存进 `localStorage`,下次打开自动恢复。
|
||||
图片多到顶破 5MB 配额时会提示改用 json(只提示一次)。
|
||||
- **保存工程 / 打开工程**:`theme-<slug>.json`,图片以 base64 内嵌,一个文件就能带走;
|
||||
@@ -139,6 +154,7 @@ INSTALL-<slug>.txt
|
||||
|
||||
```
|
||||
?demo=1 载入示例素材(canvas 画的背景 + 剪影人物)
|
||||
?rows=2 加 2 层并依次绑到不同素材(截图多图层效果用)
|
||||
?scope=admin 直接落在某个作用域
|
||||
?fresh=1 忽略 localStorage 草稿,从空白开始
|
||||
?dump=css 把最终 CSS 写进 <pre id="dump">
|
||||
@@ -147,6 +163,7 @@ INSTALL-<slug>.txt
|
||||
?dump=install 把安装说明写进 <pre id="dump">
|
||||
?dump=fx 跑处理算法自检(13 个断言,输出 JSON)
|
||||
?dump=fxpipe 跑弹窗链路冒烟(打开→改参→预览→落盘→无改动不重编码)
|
||||
?dump=layers 图层/作用域回归(加层默认不绑图、行内换图、自动切独立、新建清空)
|
||||
?dump=fxkey 端到端去白底:载入白底图,统计透明像素比例
|
||||
?loadurl=/static/images/x.webp&fx=x
|
||||
直接把项目里的图当素材载入(不必先下载再上传),fx= 打开处理面板
|
||||
|
||||
+277
-49
@@ -70,17 +70,28 @@
|
||||
|
||||
.layers{display:flex;flex-direction:column;gap:5px}
|
||||
.ly{
|
||||
display:flex;align-items:center;gap:7px;padding:6px 8px;border-radius:6px;cursor:pointer;
|
||||
display:flex;flex-direction:column;gap:5px;padding:6px 8px;border-radius:6px;cursor:pointer;
|
||||
background:var(--ui-panel2);border:1px solid transparent;
|
||||
}
|
||||
.ly:hover{border-color:var(--ui-line)}
|
||||
.ly.on{border-color:var(--ui-accent);background:#16303f}
|
||||
.ly .top{display:flex;align-items:center;gap:7px}
|
||||
.ly .dot{width:9px;height:9px;border-radius:3px;flex:0 0 auto;background:#4a5563}
|
||||
/* 缩略图:一眼看出「这一层用的是哪张图」。没有它的话,
|
||||
三层都绑着同一张图时,用户只能靠层名去猜。 */
|
||||
.ly .th{width:22px;height:22px;border-radius:4px;object-fit:contain;flex:0 0 auto;
|
||||
background:#0b0f14;border:1px solid var(--ui-line)}
|
||||
.ly .th.no{display:flex;align-items:center;justify-content:center;font-size:11px;
|
||||
color:#6f7c8d;border-style:dashed}
|
||||
.ly .nm{flex:1;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.ly .k{font-size:10px;color:var(--ui-text2)}
|
||||
.ly .x{opacity:.5;padding:0 4px}
|
||||
.ly .k{font-size:10px;color:var(--ui-text2);flex:0 0 auto}
|
||||
.ly .pick{display:flex;align-items:center;gap:5px}
|
||||
.ly .pick select{flex:1;min-width:0;font-size:11px;padding:2px 5px;height:24px}
|
||||
.ly .pick .tag{font-size:10px;color:var(--ui-text2);flex:0 0 auto}
|
||||
.ly .x{opacity:.5;padding:0 4px;flex:0 0 auto}
|
||||
.ly .x:hover{opacity:1;color:var(--ui-bad)}
|
||||
.ly .ord{opacity:.55;padding:0 5px;font-size:11px;line-height:1;border-color:transparent;background:none}
|
||||
.ly .ord{opacity:.55;padding:0 5px;font-size:11px;line-height:1;border-color:transparent;
|
||||
background:none;flex:0 0 auto}
|
||||
.ly .ord:hover:not(:disabled){opacity:1;background:#232c37;border-color:var(--ui-line)}
|
||||
.ly .ord:disabled{opacity:.18;cursor:default}
|
||||
|
||||
@@ -178,6 +189,7 @@
|
||||
<span class="brand">TamaBox 主题生成器</span>
|
||||
<label class="slug">slug <input id="slug" type="text" value="my-theme" spellcheck="false"></label>
|
||||
<button id="btn-demo">载入示例</button>
|
||||
<button id="btn-new" title="清空当前工程与浏览器草稿,从空白开始">新建</button>
|
||||
<button id="btn-save">保存工程</button>
|
||||
<button id="btn-open">打开工程</button>
|
||||
<button id="btn-reload">重载画板</button>
|
||||
@@ -288,15 +300,8 @@ const SCOPES = [
|
||||
const SLOT = 10; // 装饰层槽位数,必须与 internal/theme.DecoSlots 一致
|
||||
const VIEWPORTS = [1512, 1280, 1024, 820];
|
||||
|
||||
const S = {
|
||||
slug: 'my-theme',
|
||||
scope: 'global',
|
||||
viewport: 1512,
|
||||
mode: 'light',
|
||||
assets: [],
|
||||
seq: 0,
|
||||
curLayer: null,
|
||||
scopes: {},
|
||||
/* 默认配色/窄屏抽出来,「新建」时能原样还原;S 里存的是副本,不会被重置污染 */
|
||||
const D = {
|
||||
palette: {
|
||||
bg:'#eef3f9', surface:'rgba(255,255,255,0.86)', navbar:'rgba(255,255,255,0.52)',
|
||||
text:'#222d3b', primary:'#4a7fc1', border:'#d6e0ec',
|
||||
@@ -311,6 +316,20 @@ const S = {
|
||||
},
|
||||
};
|
||||
|
||||
const S = {
|
||||
slug: 'my-theme',
|
||||
scope: 'global',
|
||||
viewport: 1512,
|
||||
mode: 'light',
|
||||
assets: [],
|
||||
seq: 0,
|
||||
curLayer: null,
|
||||
scopes: {},
|
||||
palette: Object.assign({}, D.palette),
|
||||
narrow: Object.assign({}, D.narrow),
|
||||
dark: Object.assign({}, D.dark),
|
||||
};
|
||||
|
||||
const PALETTE_KEYS = [
|
||||
['bg','页面底色'], ['surface','卡片'], ['navbar','顶栏'],
|
||||
['text','正文'], ['primary','主色'], ['border','描边'],
|
||||
@@ -633,6 +652,9 @@ function syncOverlay(){
|
||||
doc.documentElement.appendChild(ov);
|
||||
}
|
||||
ov.innerHTML = '';
|
||||
// 跟随全局时不给拖拽框:这时 cfgOf 返回的是 global 的图层,
|
||||
// 一拖就改到全局去了 —— 用户以为在调这一页,实际全站都变了。
|
||||
if (S.scope !== 'global' && S.scopes[S.scope].inherit) return;
|
||||
|
||||
cfgOf(S.scope).layers.forEach(l => {
|
||||
if (l.kind !== 'image') return;
|
||||
@@ -726,14 +748,18 @@ function renderScopes(){
|
||||
}
|
||||
|
||||
function renderBgEditor(){
|
||||
const isGlobal = S.scope === 'global';
|
||||
const inherit = !isGlobal && S.scopes[S.scope].inherit;
|
||||
// 展示用 cfg:跟随全局时就是 global 那套(下拉里要能看到当前实际生效的值)
|
||||
const cfg = cfgOf(S.scope);
|
||||
const own = S.scope === 'global' || !S.scopes[S.scope].inherit;
|
||||
$('inherit').checked = S.scope !== 'global' && S.scopes[S.scope].inherit;
|
||||
$('inherit').disabled = S.scope === 'global';
|
||||
$('inherit').checked = inherit;
|
||||
$('inherit').disabled = isGlobal;
|
||||
|
||||
const el = $('bg-editor');
|
||||
el.innerHTML = '';
|
||||
|
||||
// 之前 inherit 时整个下拉是 disabled,用户在箱主页/问题页看背景还在、又点不动,
|
||||
// 这就是「背景图一直保留、关不掉」的来源。现在一律可点:一改就自动变独立配置。
|
||||
const mkPick = (label, key) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row';
|
||||
@@ -744,8 +770,12 @@ function renderBgEditor(){
|
||||
sel.innerHTML = '<option value="">(无)</option>' +
|
||||
S.assets.map(a => `<option value="${a.id}">${esc(a.name)}</option>`).join('');
|
||||
sel.value = cfg[key] || '';
|
||||
sel.disabled = !own;
|
||||
sel.onchange = () => { cfg[key] = sel.value || null; renderAll(); };
|
||||
sel.onchange = () => {
|
||||
ensureOwn(true);
|
||||
const c = S.scopes[S.scope];
|
||||
c[key] = sel.value || null;
|
||||
renderAll();
|
||||
};
|
||||
row.appendChild(sel);
|
||||
el.appendChild(row);
|
||||
};
|
||||
@@ -759,15 +789,26 @@ function renderBgEditor(){
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'text';
|
||||
inp.value = cfg.patternSize || '320px 320px';
|
||||
inp.disabled = !own;
|
||||
inp.style.width = '110px';
|
||||
inp.onchange = () => { cfg.patternSize = inp.value.trim() || '320px 320px'; renderAll(); };
|
||||
inp.onchange = () => {
|
||||
ensureOwn(true);
|
||||
S.scopes[S.scope].patternSize = inp.value.trim() || '320px 320px';
|
||||
renderAll();
|
||||
};
|
||||
row.append(s, inp);
|
||||
el.appendChild(row);
|
||||
|
||||
const tip = document.createElement('p');
|
||||
tip.className = 'hint';
|
||||
tip.textContent = '背景图按 cover 铺满整页;平铺图案叠在背景图之上、装饰图层之下。';
|
||||
if (inherit){
|
||||
// 说清楚「你看到的背景是从哪来的」,不然用户会以为关掉的是本页的
|
||||
const src = cfg.bg ? assetById(cfg.bg) : null;
|
||||
tip.textContent = `当前全部继承自「全局」:背景 ${src ? src.name : '无'}`
|
||||
+ `,图案 ${cfg.pattern && assetById(cfg.pattern) ? assetById(cfg.pattern).name : '无'}。`
|
||||
+ '在这里改任意一项会自动变成本页独立配置。';
|
||||
} else {
|
||||
tip.textContent = '背景图按 cover 铺满整页;平铺图案叠在背景图之上、装饰图层之下。';
|
||||
}
|
||||
el.appendChild(tip);
|
||||
}
|
||||
|
||||
@@ -787,54 +828,119 @@ function moveLayer(cfg, idx, delta){
|
||||
renderAll();
|
||||
}
|
||||
|
||||
/* 在任何「改动本页配置」之前调用:跟随全局的话先切成独立配置。
|
||||
否则改动会写到 global 对象上 —— 用户以为只改了这一页,实际全站都变了。
|
||||
copy=true 时把全局的当前值复制过来当起点(改背景不会顺带把图案弄丢);
|
||||
图层要重新发 id,否则两个作用域共用同一批 id,拖拽/高亮会串。 */
|
||||
function ensureOwn(copy){
|
||||
if (S.scope === 'global') return false;
|
||||
const c = S.scopes[S.scope];
|
||||
if (!c.inherit) return false;
|
||||
c.inherit = false;
|
||||
if (copy){
|
||||
const g = S.scopes.global;
|
||||
c.bg = g.bg;
|
||||
c.pattern = g.pattern;
|
||||
c.patternSize = g.patternSize;
|
||||
c.layers = g.layers.map(l => Object.assign({}, l, { id: uid('L') }));
|
||||
}
|
||||
S.curLayer = null;
|
||||
const label = (SCOPES.find(x => x.k === S.scope) || {}).label || S.scope;
|
||||
log(`「${label}」已改为独立配置(原来跟随全局)`
|
||||
+ (copy && S.scopes.global.layers.length ? `,已复制全局那 ${S.scopes.global.layers.length} 层作起点` : '')
|
||||
+ ',这里改的不影响别的页面', 'w');
|
||||
return true;
|
||||
}
|
||||
|
||||
function assetOptions(){
|
||||
return '<option value="">(选图片)</option>' +
|
||||
S.assets.map(a => `<option value="${a.id}">${esc(a.name)}</option>`).join('');
|
||||
}
|
||||
|
||||
function renderLayers(){
|
||||
const cfg = cfgOf(S.scope);
|
||||
const own = S.scope === 'global' || !S.scopes[S.scope].inherit;
|
||||
const cfg = cfgOf(S.scope);
|
||||
const wrap = $('layers');
|
||||
wrap.innerHTML = '';
|
||||
$('add-layer').disabled = !own || cfg.layers.length >= SLOT;
|
||||
// 「加一层」始终可用:跟随全局时点了会自动切成独立配置。
|
||||
// 之前这里 inherit 就 disabled,用户在箱主页点不动,只会以为工具坏了。
|
||||
$('add-layer').disabled = cfg.layers.length >= SLOT;
|
||||
|
||||
if (!own){
|
||||
const p = document.createElement('p');
|
||||
p.className = 'hint';
|
||||
p.textContent = '这一页跟随全局。勾掉上方「跟随全局」即可单独配置。';
|
||||
p.textContent = `这一页跟随全局,用的是全局那 ${cfg.layers.length} 层(不在这里改)。`;
|
||||
wrap.appendChild(p);
|
||||
const b = document.createElement('button');
|
||||
b.textContent = '改为独立配置';
|
||||
b.style.cssText = 'width:100%;margin-top:2px';
|
||||
b.onclick = () => { ensureOwn(); renderAll(); };
|
||||
wrap.appendChild(b);
|
||||
$('layer-editor').innerHTML = '';
|
||||
S.curLayer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cfg.layers.length){
|
||||
const p = document.createElement('p');
|
||||
p.className = 'hint';
|
||||
p.textContent = '还没有图层。点「+ 加一层」,可选图片 / 光斑 / 线性光。图片层选中后可以直接在右侧画板上拖动。';
|
||||
p.textContent = '还没有图层。点「+ 加一层」,可选图片 / 光斑 / 线性光。图片层在画板上可以直接拖。';
|
||||
wrap.appendChild(p);
|
||||
}
|
||||
|
||||
cfg.layers.forEach((l, i) => {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'ly' + (l.id === S.curLayer ? ' on' : '');
|
||||
const a = l.kind === 'image' ? assetById(l.asset) : null;
|
||||
const color = l.kind === 'image' ? '#5b9bd5' : l.kind === 'radial' ? '#d8a657' : '#9b7fd0';
|
||||
// 顺序就是层级:background-image 的第一层画在最上面。所以要能调,
|
||||
// 否则「谁挡住谁」配反了只能删了重加,位置尺寸全要重来。
|
||||
d.innerHTML = `<i class="dot" style="background:${color}"></i>
|
||||
<span class="nm">${esc(layerName(l))}</span>
|
||||
<span class="k">${i === 0 ? '最上层' : '第 ' + (i+1) + ' 层'}</span>
|
||||
<button class="ord" data-up="${l.id}" title="上移一层(更靠前)" ${i === 0 ? 'disabled' : ''}>↑</button>
|
||||
<button class="ord" data-down="${l.id}" title="下移一层(更靠后)" ${i === cfg.layers.length - 1 ? 'disabled' : ''}>↓</button>
|
||||
<button class="x" data-del="${l.id}" title="删除">×</button>`;
|
||||
const thumb = l.kind !== 'image'
|
||||
? `<span class="th no" style="color:${color}">~</span>`
|
||||
: (a ? `<img class="th" src="${a.dataURL}" alt="">` : '<span class="th no">?</span>');
|
||||
|
||||
d.innerHTML = `<div class="top">
|
||||
<i class="dot" style="background:${color}"></i>${thumb}
|
||||
<span class="nm">${esc(layerName(l))}</span>
|
||||
<span class="k">${i === 0 ? '最上层' : '第 ' + (i + 1) + ' 层'}</span>
|
||||
<button class="ord" data-up title="上移一层(更靠前)" ${i === 0 ? 'disabled' : ''}>↑</button>
|
||||
<button class="ord" data-down title="下移一层(更靠后)" ${i === cfg.layers.length - 1 ? 'disabled' : ''}>↓</button>
|
||||
<button class="x" data-del title="删除">×</button>
|
||||
</div>`;
|
||||
|
||||
// 图片层:换图的下拉直接放在行里,不必先选中再到底下的编辑器里找。
|
||||
// 之前只有「选中那一层」的编辑器里有下拉,于是看起来像「每层都只能用同一张图」。
|
||||
if (l.kind === 'image'){
|
||||
const pick = document.createElement('div');
|
||||
pick.className = 'pick';
|
||||
const tag = document.createElement('span');
|
||||
tag.className = 'tag';
|
||||
tag.textContent = '图片';
|
||||
const sel = document.createElement('select');
|
||||
sel.innerHTML = assetOptions();
|
||||
sel.value = l.asset || '';
|
||||
sel.onclick = e => e.stopPropagation();
|
||||
sel.onchange = () => {
|
||||
l.asset = sel.value || null;
|
||||
// 只有一张图时,切到「(选图片)」等于这层没东西,直接说清楚
|
||||
if (!l.asset) log('这一层暂未选图片,画布上不会显示', 'w');
|
||||
renderAll();
|
||||
};
|
||||
pick.append(tag, sel);
|
||||
d.appendChild(pick);
|
||||
}
|
||||
|
||||
d.querySelector('[data-up]').onclick = e => { e.stopPropagation(); moveLayer(cfg, i, -1); };
|
||||
d.querySelector('[data-down]').onclick = e => { e.stopPropagation(); moveLayer(cfg, i, 1); };
|
||||
d.onclick = e => {
|
||||
if (e.target.dataset.del || e.target.dataset.up || e.target.dataset.down) return;
|
||||
S.curLayer = l.id;
|
||||
renderLayers();
|
||||
highlightGuide(l.id);
|
||||
};
|
||||
d.querySelector('[data-del]').onclick = e => {
|
||||
e.stopPropagation();
|
||||
cfg.layers = cfg.layers.filter(x => x.id !== l.id);
|
||||
if (S.curLayer === l.id) S.curLayer = null;
|
||||
renderAll();
|
||||
};
|
||||
d.onclick = () => {
|
||||
S.curLayer = l.id;
|
||||
renderLayers();
|
||||
highlightGuide(l.id);
|
||||
};
|
||||
wrap.appendChild(d);
|
||||
});
|
||||
|
||||
@@ -874,12 +980,7 @@ function renderLayerEditor(l){
|
||||
};
|
||||
|
||||
if (l.kind === 'image'){
|
||||
const sel = document.createElement('select');
|
||||
sel.innerHTML = '<option value="">(选图片)</option>' +
|
||||
S.assets.map(a => `<option value="${a.id}">${esc(a.name)}</option>`).join('');
|
||||
sel.value = l.asset || '';
|
||||
sel.onchange = () => { l.asset = sel.value || null; renderAll(); };
|
||||
row('图片', sel);
|
||||
// 换图的下拉在图层行里,这里只放尺寸,避免同一件事有两个入口
|
||||
row('高度 px', numInput(l.h, 20, 4000, v => { l.h = v; renderAll(); }));
|
||||
const wIn = numInput(l.w, 0, 4000, v => { l.w = v; renderAll(); });
|
||||
wIn.title = '0 表示按图片原始比例自动算宽度';
|
||||
@@ -1173,6 +1274,28 @@ function saveDraft(){
|
||||
}
|
||||
}
|
||||
}
|
||||
/** 清空到空白状态,并删掉浏览器里的草稿。
|
||||
之前只有 ?fresh=1 这个隐藏参数:用户看到「上次的数据」一直在,也只能以为关不掉。 */
|
||||
function resetAll(){
|
||||
if (!confirm('清空当前工程?\n未导出的图层、配色、素材都会被删掉。建议先点「保存工程」。')) return;
|
||||
try { localStorage.removeItem(LS_KEY); } catch (_) {}
|
||||
S.slug = 'my-theme';
|
||||
S.scope = 'global';
|
||||
S.mode = 'light';
|
||||
S.curLayer = null;
|
||||
S.assets = [];
|
||||
S.palette = Object.assign({}, D.palette);
|
||||
S.narrow = Object.assign({}, D.narrow);
|
||||
S.dark = Object.assign({}, D.dark);
|
||||
S.scopes = {};
|
||||
SCOPES.forEach(sc => { S.scopes[sc.k] = blankScope(sc.k); });
|
||||
$('slug').value = S.slug;
|
||||
renderViewportBar();
|
||||
renderAll();
|
||||
loadPreview();
|
||||
log('已清空(浏览器草稿一并删掉),从空白开始', 'ok');
|
||||
}
|
||||
|
||||
function scheduleSave(){
|
||||
clearTimeout(saveTimer);
|
||||
saveTimer = setTimeout(saveDraft, 700);
|
||||
@@ -1949,15 +2072,31 @@ function boot(){
|
||||
$('slug').addEventListener('input', () => { S.slug = $('slug').value.trim() || 'my-theme'; applyTheme(); });
|
||||
$('btn-reload').onclick = () => { loadPreview(); log('重载画板'); };
|
||||
$('btn-demo').onclick = () => makeDemo();
|
||||
$('btn-new').onclick = resetAll;
|
||||
$('btn-export').onclick = () => doExport();
|
||||
$('add-layer').onclick = () => {
|
||||
const cfg = cfgOf(S.scope);
|
||||
if (cfg.layers.length >= SLOT) return;
|
||||
const l = newLayer(S.assets.length ? 'image' : 'radial');
|
||||
if (l.kind === 'image' && S.assets.length) l.asset = S.assets[0].id;
|
||||
// 跟随全局时先切成独立配置(并把全局那几层复制过来当起点),
|
||||
// 否则改动会写到 global 上,别页面也会跟着变。
|
||||
ensureOwn(true);
|
||||
const cfg = S.scopes[S.scope];
|
||||
if (cfg.layers.length >= SLOT){
|
||||
renderAll();
|
||||
log(`装饰层最多 ${SLOT} 个槽位`, 'w');
|
||||
return;
|
||||
}
|
||||
const kind = S.assets.length ? 'image' : 'radial';
|
||||
const l = newLayer(kind);
|
||||
// 多个素材时**不**自动绑图:连加三层会全是同一张,看起来就像「每层只能显示这张、换不了」。
|
||||
// 只有一个素材时才绑它(没得选,省一步)。
|
||||
if (kind === 'image' && S.assets.length === 1) l.asset = S.assets[0].id;
|
||||
cfg.layers.push(l);
|
||||
S.curLayer = l.id;
|
||||
renderAll();
|
||||
const sel = $('layers').querySelector('.ly.on select');
|
||||
if (sel) sel.focus();
|
||||
if (kind === 'image' && !l.asset){
|
||||
log('新图层还没选图片 —— 在这一层的「图片」下拉里挑一张', 'w');
|
||||
}
|
||||
};
|
||||
$('btn-save').onclick = () => {
|
||||
const errs = validate();
|
||||
@@ -2008,6 +2147,17 @@ function boot(){
|
||||
window.addEventListener('resize', fitStage);
|
||||
|
||||
if (params.get('demo')) makeDemo();
|
||||
// ?rows=N 加 N 层并依次绑到不同素材(截图 / 人工核对多图层效果用)
|
||||
const rows = parseInt(params.get('rows') || '0', 10);
|
||||
for (let i = 0; i < rows; i++){
|
||||
$('add-layer').click();
|
||||
const c0 = S.scopes[S.scope];
|
||||
const l0 = c0.layers[c0.layers.length - 1];
|
||||
if (l0 && l0.kind === 'image' && S.assets.length){
|
||||
l0.asset = S.assets[i % S.assets.length].id;
|
||||
}
|
||||
renderAll();
|
||||
}
|
||||
// ?loadurl=<站点路径> 直接把项目里的图当素材载入(不必先下载再上传)
|
||||
const loadURL = params.get('loadurl');
|
||||
if (loadURL){
|
||||
@@ -2039,6 +2189,84 @@ function boot(){
|
||||
pre.textContent = installTxt(S.slug);
|
||||
} else if (dump === 'fx'){
|
||||
pre.textContent = JSON.stringify(fxSelfTest(), null, 1);
|
||||
} else if (dump === 'layers'){
|
||||
// 回归用户反馈:加层全是同一张图 / 换不了;背景关不掉;跟随全局时点不动
|
||||
(async () => {
|
||||
const r = {};
|
||||
try {
|
||||
const mk = (id, w, h, color) => {
|
||||
const cv = document.createElement('canvas');
|
||||
cv.width = w; cv.height = h;
|
||||
const g = cv.getContext('2d');
|
||||
g.fillStyle = color; g.fillRect(0, 0, w, h);
|
||||
const conv = canvasAsset(cv, 0.9);
|
||||
return { id, name: id + '.webp', orig: { dataURL: conv.dataURL, w, h },
|
||||
fx: fxDefault(), ...conv };
|
||||
};
|
||||
S.assets = [mk('red', 100, 100, '#ff0000'), mk('blue', 200, 200, '#0000ff')];
|
||||
|
||||
$('add-layer').click();
|
||||
$('add-layer').click();
|
||||
const cfg = S.scopes.global;
|
||||
r.afterAdd = cfg.layers.map(l => l.asset); // 期望 [null, null]
|
||||
r.rowSelects = $('layers').querySelectorAll('select').length; // 期望 2
|
||||
r.addDisabled = $('add-layer').disabled;
|
||||
|
||||
// 行内换图:把第 2 层改成 blue
|
||||
const sels = [...$('layers').querySelectorAll('select')];
|
||||
sels[1].value = 'blue';
|
||||
sels[1].dispatchEvent(new Event('change'));
|
||||
r.afterChange = cfg.layers.map(l => l.asset); // 期望 [null, 'blue']
|
||||
r.cssUrls = [...buildCSS(false).matchAll(/themes\/[a-z0-9-]+\/([a-z0-9-]+)\.webp/g)]
|
||||
.map(m => m[1]);
|
||||
|
||||
// 切到 box(默认跟随全局)
|
||||
S.scope = 'box'; S.curLayer = null; renderAll();
|
||||
r.box = {
|
||||
inherit: S.scopes.box.inherit,
|
||||
addDisabled: $('add-layer').disabled,
|
||||
bgDisabled: $('bg-editor').querySelector('select').disabled,
|
||||
layerRows: $('layers').querySelectorAll('.ly').length,
|
||||
overlayGuides: doc ? doc.querySelectorAll('#__tb_ov > div').length : -1,
|
||||
};
|
||||
|
||||
// 在 box 点「加一层」→ 自动变独立 + 复制全局那两层,全局不能被改
|
||||
$('add-layer').click();
|
||||
r.boxAfterAdd = {
|
||||
inherit: S.scopes.box.inherit,
|
||||
layers: S.scopes.box.layers.length, // 2 复制 + 1 = 3
|
||||
globalLayers: S.scopes.global.layers.length, // 必须还是 2
|
||||
};
|
||||
|
||||
// 背景:inherit 页原来点不动,现在能改,且改完自动独立
|
||||
const bsel = $('bg-editor').querySelector('select');
|
||||
bsel.value = 'red';
|
||||
bsel.dispatchEvent(new Event('change'));
|
||||
r.boxBg = {
|
||||
boxBg: S.scopes.box.bg,
|
||||
globalBg: S.scopes.global.bg,
|
||||
inherit: S.scopes.box.inherit,
|
||||
cssHasBg: /--tb-page-image: url/.test(buildCSS(true)),
|
||||
};
|
||||
|
||||
// 新建:清空 + 删草稿
|
||||
const oc = window.confirm;
|
||||
window.confirm = () => true;
|
||||
resetAll();
|
||||
window.confirm = oc;
|
||||
r.afterReset = {
|
||||
assets: S.assets.length,
|
||||
globalLayers: S.scopes.global.layers.length,
|
||||
boxInherit: S.scopes.box.inherit,
|
||||
scope: S.scope,
|
||||
draft: localStorage.getItem(LS_KEY) === null ? '已删' : '还在',
|
||||
slug: $('slug').value,
|
||||
};
|
||||
} catch (e){
|
||||
r.error = e.message + ' | ' + (e.stack || '').split('\n')[1];
|
||||
}
|
||||
pre.textContent = JSON.stringify(r, null, 1);
|
||||
})();
|
||||
} else if (dump === 'fxkey'){
|
||||
// 端到端:把「白底立绘」载入 → 去白底 → 统计透明像素比例。
|
||||
// 与手写 floodfill 抠出来的那张(46.6% 透明)对比,差值应很小。
|
||||
|
||||
Reference in New Issue
Block a user