文件还在测试中

This commit is contained in:
2026-08-08 15:53:53 +08:00
parent 5f1e7adb64
commit 8101177cb5
241 changed files with 18074 additions and 22 deletions
+65
View File
@@ -0,0 +1,65 @@
/* 后台交互 */
(function () {
var mt = document.querySelector('.menu-toggle');
if (mt) mt.addEventListener('click', function () {
var s = document.querySelector('.admin-side'); if (s) s.classList.toggle('open');
});
// 分系统(CRM/PSI)侧栏抽屉
var mt2 = document.querySelector('.subsys .menu-toggle');
if (mt2) mt2.addEventListener('click', function () {
document.body.classList.toggle('nav-open');
});
function val(k) { var el = document.getElementById('f_' + k); return el ? el.value : ''; }
function updatePreview() {
var pb = document.getElementById('themePreview'); if (!pb) return;
pb.style.background = 'linear-gradient(135deg,' + val('primary') + ',' + val('secondary') + ')';
pb.style.color = '#fff';
pb.querySelector('.pv-title').textContent = '降温服 · 风格预览';
pb.querySelector('.pv-sub').textContent = '主色 ' + val('primary') + ' / 辅色 ' + val('secondary');
}
var presetSel = document.getElementById('preset');
var presets = window.__presets__ || null;
if (presetSel && presets) {
presetSel.addEventListener('change', function () {
var p = presets[this.value]; if (!p) return;
['primary', 'primary_600', 'secondary', 'accent', 'bg', 'surface', 'text', 'muted', 'border', 'nav_bg']
.forEach(function (k) {
var el = document.getElementById('f_' + k);
if (el && p.vars[k] != null) el.value = p.vars[k];
});
updatePreview();
});
}
['primary', 'secondary', 'accent', 'bg', 'surface', 'text', 'muted', 'border', 'nav_bg']
.forEach(function (k) {
var el = document.getElementById('f_' + k);
if (el) el.addEventListener('input', updatePreview);
});
updatePreview();
// 确认删除
document.querySelectorAll('form.del-form').forEach(function (f) {
f.addEventListener('submit', function (e) {
if (!confirm('确定要删除吗?此操作不可恢复。')) e.preventDefault();
});
});
})();
// 通用删除/操作确认:用 data-confirm 属性替代内联 onclick/onsubmit 确认,
// 以配合严格 CSPscript-src 已移除 'unsafe-inline')。
document.addEventListener('submit', function (e) {
var el = e.target;
if (el && el.hasAttribute('data-confirm') && !window.confirm(el.getAttribute('data-confirm'))) {
e.preventDefault();
}
});
document.addEventListener('click', function (e) {
var el = e.target && e.target.closest ? e.target.closest('[data-confirm]') : null;
if (el && !window.confirm(el.getAttribute('data-confirm'))) {
e.preventDefault();
e.stopPropagation();
}
});
+88
View File
@@ -0,0 +1,88 @@
/* 固定版面富文本编辑器(依赖 contenteditable + execCommand,无外部依赖)
* - 工具栏:加粗/斜体/标题/列表/引用/链接/图片
* - 图片:从素材库选择或上传(复用 admin/media 接口)
* - 保存前将富文本 HTML 同步到隐藏 textarea(name=content)
*/
(function () {
'use strict';
var ed = document.getElementById('fxEditor');
var ta = document.getElementById('fxContent');
var form = document.getElementById('fxForm');
if (!ed || !ta || !form) return;
function sync() { ta.value = ed.innerHTML; }
ed.addEventListener('input', sync);
form.addEventListener('submit', function () { sync(); });
// 工具栏命令
document.querySelectorAll('.fx-toolbar button[data-cmd]').forEach(function (b) {
b.addEventListener('mousedown', function (e) { e.preventDefault(); }); // 防止编辑器失焦
b.addEventListener('click', function () {
var cmd = b.getAttribute('data-cmd');
var val = b.getAttribute('data-val') || null;
ed.focus();
document.execCommand(cmd, false, val);
sync();
});
});
// 链接
var linkBtn = document.getElementById('fxLink');
if (linkBtn) linkBtn.addEventListener('click', function () {
var url = prompt('链接地址(如 /contact 或 https://...');
if (url) { ed.focus(); document.execCommand('createLink', false, url); sync(); }
});
// 图片弹层
var modal = document.getElementById('fxModal');
var imgBtn = document.getElementById('fxImg');
var closeBtn = document.getElementById('fxModalClose');
if (imgBtn) imgBtn.addEventListener('click', openModal);
if (closeBtn) closeBtn.addEventListener('click', function () { modal.hidden = true; });
if (modal) modal.addEventListener('click', function (e) { if (e.target === modal) modal.hidden = true; });
function insertImg(url) {
ed.focus();
document.execCommand('insertHTML', false,
'<img src="' + url + '" alt="" style="max-width:100%;border-radius:8px;display:block;margin:8px 0">');
sync();
modal.hidden = true;
}
function openModal() {
modal.hidden = false;
var lib = document.getElementById('fxLib');
lib.innerHTML = '<span style="font-size:12px;color:#94a3b8">加载中…</span>';
fetch(window.__PB_MEDIA__)
.then(function (r) { return r.json(); })
.then(function (j) {
lib.innerHTML = '';
var items = (j && j.items) || [];
if (!items.length) { lib.innerHTML = '<span style="font-size:12px;color:#94a3b8">暂无素材,先上传</span>'; return; }
items.forEach(function (it) {
var img = document.createElement('img');
img.src = it.url; img.title = it.name;
img.addEventListener('click', function () { insertImg(it.url); });
lib.appendChild(img);
});
})
.catch(function () { lib.innerHTML = '<span style="font-size:12px;color:#dc2626">加载失败</span>'; });
}
// 上传图片
var up = document.getElementById('fxUp');
if (up) up.addEventListener('change', function () {
var file = this.files && this.files[0];
if (!file) return;
var fd = new FormData();
fd.append('file', file);
fetch(window.__PB_UPLOAD__, { method: 'POST', body: fd, headers: { 'X-CSRF-TOKEN': window.__PB_CSRF__ } })
.then(function (r) { return r.json(); })
.then(function (j) {
if (j && j.ok) { insertImg(j.url); openModal(); }
else alert((j && j.msg) || '上传失败');
})
.catch(function () { alert('上传失败,请重试'); });
this.value = '';
});
})();
+55
View File
@@ -0,0 +1,55 @@
/* 酷冰甲降温服 · 前台交互 */
(function () {
'use strict';
// 明暗切换
var root = document.documentElement;
var toggle = document.getElementById('themeToggle');
if (toggle) {
toggle.addEventListener('click', function () {
var cur = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
root.setAttribute('data-theme', cur);
try { localStorage.setItem('site_theme', cur); } catch (e) {}
});
}
// 滚动时 header 阴影
var header = document.getElementById('siteHeader');
function onScroll() {
if (header) header.classList.toggle('scrolled', window.scrollY > 10);
}
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
// 移动端菜单
var burger = document.getElementById('navBurger');
var links = document.getElementById('navLinks');
if (burger && links) {
burger.addEventListener('click', function () { links.classList.toggle('open'); });
links.addEventListener('click', function (e) {
if (e.target.tagName === 'A') links.classList.remove('open');
});
}
// 滚动揭示
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
if (en.isIntersecting) { en.target.classList.add('in'); io.unobserve(en.target); }
});
}, { threshold: 0.12 });
document.querySelectorAll('.reveal').forEach(function (el, i) {
el.style.transitionDelay = (i % 6) * 60 + 'ms';
io.observe(el);
});
// 磁性按钮
document.querySelectorAll('.magnetic').forEach(function (el) {
el.addEventListener('mousemove', function (e) {
var r = el.getBoundingClientRect();
var x = e.clientX - r.left - r.width / 2;
var y = e.clientY - r.top - r.height / 2;
el.style.transform = 'translate(' + x * 0.18 + 'px,' + y * 0.28 + 'px)';
});
el.addEventListener('mouseleave', function () { el.style.transform = ''; });
});
})();
+495
View File
@@ -0,0 +1,495 @@
/* 可视化页面编辑器(可复用):三栏布局(图标栏 | 编辑画布 | 标签页右栏)
* 右栏:预览(只读实时模拟)/ 属性 / 素材(可删除)
* 支持元素类型:text 文字, image 图片, button 链接按钮, buy 产品购买, price 产品价格, specs 产品规格
* 对齐:左/中/右 + 竖(文字=竖排 writing-mode,图片=垂直居中)
* 依赖全局(由后台 partial 注入):
* __PB_INIT__ 初始布局数组
* __PB_MODULE__ page|product|news|case|category
* __PB_MEDIA__ 素材列表接口
* __PB_UPLOAD__ 上传接口
* __PB_DELETE__ 删除接口前缀(拼接文件名)
* __PB_CSRF__ CSRF 令牌
*/
(function () {
'use strict';
var STAGE_W = 720;
var stage = document.getElementById('pbStage');
if (!stage) return;
var MODULE = (window.__PB_MODULE__ || 'page');
var elements = Array.isArray(window.__PB_INIT__) ? window.__PB_INIT__.slice() : [];
var selectedId = null;
var zCounter = elements.reduce(function (m, e) { return Math.max(m, e.z || 0); }, 0);
function uid() { return 'el_' + Date.now().toString(36) + Math.floor(Math.random() * 1e4).toString(36); }
function nextZ() { return ++zCounter; }
function findEl(id) { for (var i = 0; i < elements.length; i++) if (elements[i].id === id) return elements[i]; return null; }
function nodeOf(id) { return stage.querySelector('[data-id="' + id + '"]'); }
function ptOf(ev) { return ev.touches && ev.touches[0] ? ev.touches[0] : ev; }
function on(id, evt, fn) { var el = document.getElementById(id); if (el) el.addEventListener(evt, fn); }
/* ---------- 对齐样式(文字:text-align + 竖排;图片:flex 水平 + 垂直居中) ---------- */
function styleAlign(el, node) {
node.style.textAlign = '';
node.style.writingMode = '';
node.style.display = '';
node.style.flexDirection = '';
node.style.alignItems = '';
node.style.justifyContent = '';
if (el.type === 'image') {
node.style.display = 'flex';
node.style.flexDirection = 'column';
node.style.alignItems = (el.align === 'center') ? 'center' : (el.align === 'right') ? 'flex-end' : 'flex-start';
node.style.justifyContent = el.v ? 'center' : 'flex-start';
} else if (el.type === 'text') {
node.style.textAlign = el.align || 'left';
if (el.v) node.style.writingMode = 'vertical-rl';
}
}
/* ---------- 通用指针拖拽(鼠标 + 触摸) ---------- */
function onPointer(ev, handlers) {
ev.preventDefault();
var move = function (e) { if (handlers.move) handlers.move(ptOf(e)); };
var up = function () {
document.removeEventListener('mousemove', move);
document.removeEventListener('mouseup', up);
document.removeEventListener('touchmove', move);
document.removeEventListener('touchend', up);
if (handlers.end) handlers.end();
};
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', up);
document.addEventListener('touchmove', move, { passive: false });
document.addEventListener('touchend', up);
if (handlers.start) handlers.start(ptOf(ev));
}
/* ---------- 构建元素内部内容 ---------- */
function buildInner(el) {
if (el.type === 'image') {
var im = document.createElement('img');
im.className = 'pb-img'; im.src = el.src || ''; im.alt = ''; im.draggable = false;
im.addEventListener('load', updateStageSize);
return im;
}
if (el.type === 'button' || el.type === 'buy') {
var a = document.createElement('a');
a.className = 'pb-static pb-btn-prev' + (el.type === 'buy' ? ' pb-buy' : '');
a.textContent = el.type === 'buy' ? '立即购买' : (el.text || '按钮');
if (el.type === 'button') {
if (el.bg) a.style.background = el.bg;
if (el.tc) a.style.color = el.tc;
a.style.fontSize = (el.size === 'lg' ? '18px' : el.size === 'sm' ? '13px' : '15px');
}
return a;
}
if (el.type === 'price') {
var d = document.createElement('div');
d.className = 'pb-static pb-price-prev';
d.textContent = '¥ — 起/套';
if (el.color) d.style.color = el.color;
return d;
}
if (el.type === 'specs') {
var t = document.createElement('div');
t.className = 'pb-static pb-specs-prev';
t.innerHTML = '<div class="sp-row"><span>参数 1</span><span>值 1</span></div><div class="sp-row"><span>参数 2</span><span>值 2</span></div>';
return t;
}
var tx = document.createElement('div');
tx.className = 'pb-text';
tx.textContent = el.text != null ? el.text : '';
tx.style.fontSize = (el.fontSize || 18) + 'px';
tx.style.color = el.color || '#0f172a';
tx.style.fontWeight = el.bold ? '700' : '400';
tx.style.textAlign = el.align || 'left';
tx.style.lineHeight = '1.4';
return tx;
}
/* ---------- 渲染单个元素 ---------- */
function renderEl(el) {
var wrap = document.createElement('div');
wrap.className = 'pb-el';
wrap.setAttribute('data-id', el.id);
wrap.style.left = (el.x || 0) + 'px';
wrap.style.top = (el.y || 0) + 'px';
wrap.style.width = (el.w || 200) + 'px';
wrap.style.zIndex = el.z || 1;
wrap.appendChild(buildInner(el));
styleAlign(el, wrap);
var h = document.createElement('span');
h.className = 'pb-handle';
h.addEventListener('mousedown', function (e) { startResize(e, el); });
h.addEventListener('touchstart', function (e) { startResize(e, el); }, { passive: false });
wrap.appendChild(h);
wrap.addEventListener('mousedown', function (e) { startDrag(e, el); });
wrap.addEventListener('touchstart', function (e) { startDrag(e, el); }, { passive: false });
wrap.addEventListener('click', function (e) { e.stopPropagation(); selectEl(el.id); });
if (el.type === 'text') {
wrap.addEventListener('dblclick', function (e) { e.stopPropagation(); startEdit(el, wrap.firstChild); });
}
return wrap;
}
function renderAll() {
stage.innerHTML = '';
elements.forEach(function (el) { stage.appendChild(renderEl(el)); });
if (selectedId && nodeOf(selectedId)) nodeOf(selectedId).classList.add('sel');
updateStageSize();
renderPreview();
}
/* 画布高度自适应:容纳所有元素底部 */
function updateStageSize() {
var maxB = 480;
elements.forEach(function (el) {
var n = nodeOf(el.id);
if (!n) return;
var b = (el.y || 0) + n.offsetHeight;
if (b > maxB) maxB = b;
});
stage.style.minHeight = maxB + 'px';
}
function applyEl(el) {
var n = nodeOf(el.id);
if (!n) return;
n.style.width = (el.w || 200) + 'px';
styleAlign(el, n);
var inner = n.firstChild;
if (el.type === 'image') {
inner.src = el.src || '';
} else if (el.type === 'text') {
inner.textContent = el.text != null ? el.text : '';
inner.style.fontSize = (el.fontSize || 18) + 'px';
inner.style.color = el.color || '#0f172a';
inner.style.fontWeight = el.bold ? '700' : '400';
inner.style.textAlign = el.align || 'left';
inner.style.writingMode = el.v ? 'vertical-rl' : '';
} else if (el.type === 'button') {
inner.textContent = el.text || '按钮';
if (el.bg) inner.style.background = el.bg;
if (el.tc) inner.style.color = el.tc;
inner.style.fontSize = (el.size === 'lg' ? '18px' : el.size === 'sm' ? '13px' : '15px');
} else if (el.type === 'price') {
if (el.color) inner.style.color = el.color;
}
updateStageSize();
renderPreview();
}
/* ---------- 拖拽移动 ---------- */
function startDrag(e, el) {
if (e.target && e.target.classList.contains('pb-handle')) return;
e.stopPropagation();
selectEl(el.id);
var p = ptOf(e);
var sx = p.clientX, sy = p.clientY, ox = el.x || 0, oy = el.y || 0;
onPointer(e, {
move: function (p2) {
el.x = Math.max(0, ox + (p2.clientX - sx));
el.y = Math.max(0, oy + (p2.clientY - sy));
var n = nodeOf(el.id);
if (n) { n.style.left = el.x + 'px'; n.style.top = el.y + 'px'; }
}
});
}
/* ---------- 缩放(右下角) ---------- */
function startResize(e, el) {
e.stopPropagation(); e.preventDefault();
selectEl(el.id);
var p = ptOf(e);
var sx = p.clientX, sw = el.w || 200;
onPointer(e, {
move: function (p2) {
el.w = Math.max(40, sw + (p2.clientX - sx));
var n = nodeOf(el.id);
if (n) n.style.width = el.w + 'px';
}
});
}
/* ---------- 双击编辑文字 ---------- */
function startEdit(el, node) {
node.setAttribute('contenteditable', 'true');
node.focus();
try { document.getSelection().selectAllChildren(node); } catch (x) {}
function done() {
node.removeAttribute('contenteditable');
el.text = node.textContent;
node.removeEventListener('blur', done);
if (selectedId === el.id) { syncProps(); renderPreview(); }
}
node.addEventListener('blur', done);
}
/* ---------- 选中 / 属性面板 ---------- */
function selectEl(id) {
selectedId = id;
Array.prototype.forEach.call(stage.querySelectorAll('.pb-el'), function (n) {
n.classList.toggle('sel', n.getAttribute('data-id') === id);
});
renderProps(findEl(id));
if (id) switchTab('props');
}
function alignGroup(el, types) {
var html = '<label>对齐</label><div class="pb-align">';
html += '<button type="button" class="pb-ab" data-a="left">左</button>';
html += '<button type="button" class="pb-ab" data-a="center">中</button>';
html += '<button type="button" class="pb-ab" data-a="right">右</button>';
html += '<button type="button" class="pb-ab" data-a="v">竖</button>';
html += '</div>';
return html;
}
function wireAlign(el) {
document.querySelectorAll('#pbPropsBody .pb-ab').forEach(function (b) {
var a = b.getAttribute('data-a');
var on = (a === 'v') ? !!el.v : ((el.align || 'left') === a);
b.classList.toggle('active', on);
b.onclick = function () {
if (a === 'v') el.v = !el.v; else el.align = a;
wireAlign(el); applyEl(el); renderPreview();
};
});
}
function renderProps(el) {
var body = document.getElementById('pbPropsBody');
if (!el) { body.className = 'pb-empty'; body.textContent = '在画布中选择一个元素以编辑属性'; return; }
body.className = '';
if (el.type === 'text') {
body.innerHTML =
'<label>文字内容</label><textarea id="pp_text" rows="3"></textarea>' +
'<label>字号 (px)</label><input type="number" id="pp_size" min="10" max="160">' +
'<label>颜色</label><input type="color" id="pp_color">' +
'<label style="display:flex;align-items:center;gap:6px"><input type="checkbox" id="pp_bold"> 加粗</label>' +
alignGroup(el) +
'<button class="pb-del" id="pp_del" type="button">删除该元素</button>';
document.getElementById('pp_text').value = el.text != null ? el.text : '';
document.getElementById('pp_size').value = el.fontSize || 18;
document.getElementById('pp_color').value = el.color || '#0f172a';
document.getElementById('pp_bold').checked = !!el.bold;
bind('pp_text', 'input', function (v) { el.text = v; applyEl(el); });
bind('pp_size', 'input', function (v) { el.fontSize = parseInt(v, 10) || 18; applyEl(el); });
bind('pp_color', 'input', function (v) { el.color = v; applyEl(el); });
bind('pp_bold', 'change', function (v) { el.bold = !!v; applyEl(el); });
wireAlign(el);
} else if (el.type === 'image') {
body.innerHTML =
'<label>图片宽度 (px)</label><input type="number" id="pp_w" min="40" max="3000">' +
alignGroup(el) +
'<label>替换图片(点击素材)</label><div class="pb-lib" id="pp_lib"></div>' +
'<button class="pb-del" id="pp_del" type="button">删除该元素</button>';
document.getElementById('pp_w').value = el.w || 200;
bind('pp_w', 'input', function (v) { el.w = Math.max(40, parseInt(v, 10) || 200); applyEl(el); });
fillLib(document.getElementById('pp_lib'), function (url) { el.src = url; applyEl(el); });
wireAlign(el);
} else if (el.type === 'button') {
body.innerHTML =
'<label>按钮文字</label><input type="text" id="pp_text">' +
'<label>链接地址</label><input type="text" id="pp_href" placeholder="如 /contact 或 https://...">' +
'<label>背景色</label><input type="color" id="pp_bg">' +
'<label>文字颜色</label><input type="color" id="pp_tc">' +
'<label>尺寸</label><select id="pp_size"><option value="sm">小</option><option value="md">中</option><option value="lg">大</option></select>' +
'<button class="pb-del" id="pp_del" type="button">删除该元素</button>';
document.getElementById('pp_text').value = el.text || '按钮';
document.getElementById('pp_href').value = el.href || '';
document.getElementById('pp_bg').value = el.bg || '#0ea5e9';
document.getElementById('pp_tc').value = el.tc || '#ffffff';
document.getElementById('pp_size').value = el.size || 'md';
bind('pp_text', 'input', function (v) { el.text = v; applyEl(el); });
bind('pp_href', 'input', function (v) { el.href = v; });
bind('pp_bg', 'input', function (v) { el.bg = v; applyEl(el); });
bind('pp_tc', 'input', function (v) { el.tc = v; applyEl(el); });
bind('pp_size', 'change', function (v) { el.size = v; applyEl(el); });
} else if (el.type === 'price') {
body.innerHTML =
'<p class="pb-empty" style="margin-bottom:10px">自动显示本产品「价格」,无需填写。</p>' +
'<label>价格文字颜色</label><input type="color" id="pp_color">' +
'<button class="pb-del" id="pp_del" type="button">删除该元素</button>';
document.getElementById('pp_color').value = el.color || '#0f172a';
bind('pp_color', 'input', function (v) { el.color = v; applyEl(el); });
} else if (el.type === 'buy') {
body.innerHTML = '<p class="pb-empty">自动链接到本产品的「立即购买」下单页。</p><button class="pb-del" id="pp_del" type="button">删除该元素</button>';
} else if (el.type === 'specs') {
body.innerHTML = '<p class="pb-empty">自动显示本产品的「规格参数」。</p><button class="pb-del" id="pp_del" type="button">删除该元素</button>';
}
var del = document.getElementById('pp_del');
if (del) del.addEventListener('click', function () { removeEl(el.id); });
}
function bind(id, evt, fn) {
var el = document.getElementById(id);
if (!el) return;
el.addEventListener(evt, function () { fn(el.value !== undefined ? el.value : el.checked); });
}
function syncProps() { var el = findEl(selectedId); if (el) renderProps(el); }
function removeEl(id) {
elements = elements.filter(function (e) { return e.id !== id; });
if (selectedId === id) selectedId = null;
renderAll();
switchTab('preview');
if (!selectedId) renderProps(null);
}
/* ---------- 标签页切换 ---------- */
function switchTab(name) {
document.querySelectorAll('.pb-tab').forEach(function (t) {
t.classList.toggle('active', t.getAttribute('data-tab') === name);
});
var panes = { preview: 'panePreview', props: 'paneProps', material: 'paneMaterial' };
Object.keys(panes).forEach(function (k) {
document.getElementById(panes[k]).classList.toggle('hidden', k !== name);
});
if (name === 'preview') fitPreview();
}
document.querySelectorAll('.pb-tab').forEach(function (t) {
t.addEventListener('click', function () { switchTab(t.getAttribute('data-tab')); });
});
/* ---------- 实时预览(克隆只读 + 自适应缩放) ---------- */
function renderPreview() {
var pv = document.getElementById('pbPreview');
if (!pv) return;
pv.innerHTML = stage.innerHTML;
pv.querySelectorAll('.pb-handle').forEach(function (h) { h.remove(); });
pv.querySelectorAll('.pb-el').forEach(function (n) { n.classList.remove('sel'); n.style.outline = ''; });
pv.querySelectorAll('[contenteditable]').forEach(function (n) { n.removeAttribute('contenteditable'); });
fitPreview();
}
function fitPreview() {
var wrap = document.getElementById('pbPreviewWrap');
var pv = document.getElementById('pbPreview');
if (!wrap || !pv) return;
var avail = wrap.clientWidth || STAGE_W;
var scale = Math.min(1, avail / STAGE_W);
var m = 420;
pv.querySelectorAll('.pb-el').forEach(function (n) {
var y = parseInt(n.style.top, 10) || 0;
var b = y + n.offsetHeight;
if (b > m) m = b;
});
pv.style.transform = 'scale(' + scale + ')';
pv.style.transformOrigin = 'top left';
pv.style.height = m + 'px';
wrap.style.height = (m * scale) + 'px';
}
/* ---------- 素材库(含删除) ---------- */
function fillLib(container, onPick) {
if (!container) return;
container.innerHTML = '<span style="font-size:12px;color:#94a3b8">加载中…</span>';
fetch(window.__PB_MEDIA__)
.then(function (r) { return r.json(); })
.then(function (j) {
container.innerHTML = '';
var items = (j && j.items) || [];
if (!items.length) { container.innerHTML = '<span style="font-size:12px;color:#94a3b8">暂无素材,先上传</span>'; return; }
items.forEach(function (it) {
var cell = document.createElement('div');
cell.className = 'pb-lib-item';
var img = document.createElement('img');
img.src = it.url; img.title = it.name;
img.addEventListener('click', function () { onPick(it.url); });
var del = document.createElement('button');
del.className = 'pb-lib-del'; del.type = 'button'; del.textContent = '×'; del.title = '删除素材';
del.addEventListener('click', function (e) { e.stopPropagation(); deleteMedia(it.name); });
cell.appendChild(img); cell.appendChild(del);
container.appendChild(cell);
});
})
.catch(function () { container.innerHTML = '<span style="font-size:12px;color:#dc2626">加载失败</span>'; });
}
function deleteMedia(name) {
if (!confirm('确定删除素材「' + name + '」?此操作不可撤销。')) return;
fetch(window.__PB_DELETE__ + encodeURIComponent(name), {
method: 'POST',
headers: { 'X-CSRF-TOKEN': window.__PB_CSRF__ }
})
.then(function (r) { return r.json(); })
.then(function (j) {
if (j && j.ok) {
loadLib();
var pp = document.getElementById('pp_lib');
if (pp) fillLib(pp, function (url) { var el = findEl(selectedId); if (el) { el.src = url; applyEl(el); } });
} else { alert((j && j.msg) || '删除失败'); }
})
.catch(function () { alert('删除失败,请重试'); });
}
function loadLib() { fillLib(document.getElementById('pbLib'), function (url) { addImage(url); }); }
/* ---------- 添加元素 ---------- */
function addText() {
var el = { id: uid(), type: 'text', x: 40, y: 40, w: 320, text: '双击编辑文字', fontSize: 20, color: '#0f172a', bold: false, align: 'left', v: false, z: nextZ() };
elements.push(el); renderAll(); selectEl(el.id);
}
function addImage(url) {
var el = { id: uid(), type: 'image', x: 40, y: 40, w: 280, src: url, align: 'left', v: false, z: nextZ() };
elements.push(el); renderAll(); selectEl(el.id);
}
function addButton() {
var el = { id: uid(), type: 'button', x: 40, y: 40, w: 200, text: '按钮', href: '#', bg: '#0ea5e9', tc: '#ffffff', size: 'md', z: nextZ() };
elements.push(el); renderAll(); selectEl(el.id);
}
function addBuy() {
var el = { id: uid(), type: 'buy', x: 40, y: 40, w: 200, z: nextZ() };
elements.push(el); renderAll(); selectEl(el.id);
}
function addPrice() {
var el = { id: uid(), type: 'price', x: 40, y: 40, w: 240, color: '#0f172a', z: nextZ() };
elements.push(el); renderAll(); selectEl(el.id);
}
function addSpecs() {
var el = { id: uid(), type: 'specs', x: 40, y: 40, w: 360, z: nextZ() };
elements.push(el); renderAll(); selectEl(el.id);
}
/* 图标栏动作 */
document.querySelectorAll('.pb-rail-btn').forEach(function (b) {
b.addEventListener('click', function () {
var act = b.getAttribute('data-act');
if (act === 'material') { switchTab('material'); loadLib(); }
else if (act === 'image') { switchTab('material'); }
else if (act === 'text') { addText(); }
else if (act === 'button') { addButton(); }
else if (act === 'buy') { addBuy(); }
else if (act === 'price') { addPrice(); }
else if (act === 'specs') { addSpecs(); }
});
});
stage.addEventListener('click', function () { selectEl(null); });
/* ---------- 上传素材 ---------- */
on('pbFile', 'change', function () {
var file = this.files && this.files[0];
if (!file) return;
var fd = new FormData();
fd.append('file', file);
fetch(window.__PB_UPLOAD__, { method: 'POST', body: fd, headers: { 'X-CSRF-TOKEN': window.__PB_CSRF__ } })
.then(function (r) { return r.json(); })
.then(function (j) { if (j && j.ok) { addImage(j.url); loadLib(); } else alert((j && j.msg) || '上传失败'); })
.catch(function () { alert('上传失败,请重试'); });
this.value = '';
});
/* ---------- 保存:序列化到隐藏字段 ---------- */
on('pbForm', 'submit', function () {
var f = document.getElementById('pbLayout');
if (f) f.value = JSON.stringify(elements);
});
/* ---------- 启动 ---------- */
renderAll();
loadLib();
window.addEventListener('resize', fitPreview);
})();