主题色改为自绘取色面板(任意取色);图标上传加即时预览与状态提示

一、颜色不够丰富 → 点击弹取色面板
  用户:「还是不行,颜色还是不够丰富。让用户选择吧:点击之后弹出一个颜色选择器。」
  原来只有一个原生 <input type=color> 小方块,用户不知道能点,
  而且系统面板各浏览器样式不一、只能取纯色。
  改成自绘面板:
    色相/饱和度渐变板(Canvas)+ 明度滑块 + 十六进制输入 + 确定
    12 个预设圆点降级为快捷方式,真正的能力是任意取色
  交互:点色块弹出(下方空间不够自动翻上方)、板内拖动实时更新、
        十六进制可手输、点确定或面板外关闭、Esc 关闭

  ⚠️ 光标定位踩了两个坑,都写进注释了:
    ① 不能用 canvas.offsetLeft(相对 offsetParent,受父级 padding 影响,
       实测算出 236px 超出画布宽度 226px);要用两个 getBoundingClientRect 相减
    ② canvas 显示尺寸受 CSS width:100% 影响,≠ 内部像素 220×150,
       必须用 rect.width 而不是 canvas.width

二、图标上传没反馈
  用户:「上传的图标没有显示,也没有上传成功、失败之类的提示信息。」
  后端逻辑完整(格式/大小/尺寸/存储模式都校验了),问题在**前端零反馈**:
  裸 <input type=file>,选完文件什么都不发生,而提示条在页面顶部容易被错过。
  加了 bindIconUpload():
    ① 选中文件立刻 FileReader 本地预览,不等提交
    ② 显示文件名 + 尺寸 + 大小,让用户确认选对了
    ③ 选之前就拦掉明显不合格的(格式/超 2MB/小于 32×32/空文件)
    ④ 非正方形给出警告(显示时会被裁切)
  ⚠️ 前端校验只是体验优化,绝不能当安全边界 ——
     真正的校验仍在 PHP 的 fb_icon_store_upload() 里。

验证(用独立测试页 + 浏览器实测,不登录后台、不动数据):
  面板初始隐藏 → 点击后 248×246 出现
  Canvas 四角像素分别为灰白/蓝/黑 → 渐变确实画出来了
  板上取色 → 隐藏字段 #2178d9 同步到按钮文字/十六进制框/预览色
  点预设 → #2B822E;手输 → #a020f0;色相滑条 120° → #20f020
  光标中心 (238,54) 落在画布区域 (11..237, 11..161) 内
  4 个路由 200
This commit is contained in:
2026-11-04 18:52:45 +08:00
parent a00e2221c4
commit ad1b0eedf4
4 changed files with 394 additions and 27 deletions
+290 -20
View File
@@ -709,49 +709,318 @@
} }
} }
/* ---------- 主题色选择器(后台软件编辑) ---------- */ /* ---------- 主题色选择器(后台软件编辑) ----------
* 用户 2026-11-04:「还是不够丰富,让用户自己选:点击之后弹出一个颜色选择器。」
*
* 原来是原生 <input type="color">:只有个小方块,用户不知道能点,
* 而且各浏览器系统面板样式不一。现在自带一个面板:
* 色相/饱和度渐变板(Canvas)+ 明度滑块 + 十六进制输入。
* 预设圆点保留为快捷色,但真正的能力是**任意取色**。
*
* 交互约定:
* - 点色块 → 面板出现(定位在其下方/上方,视空间而定)
* - 在渐变板上拖动 → 实时更新;滑条调明度
* - 十六进制框可手输,输入合法立即同步
* - 点「确定」或点面板外 → 关闭,值已写入隐藏字段
*/
function bindColorPicker() { function bindColorPicker() {
var rows = document.querySelectorAll('.color-row'); var rows = document.querySelectorAll('[data-colorpicker]');
for (var i = 0; i < rows.length; i++) { for (var i = 0; i < rows.length; i++) {
(function (row) { (function (row) {
var input = row.querySelector('input[type=color]'); var trigger = row.querySelector('[data-color-trigger]');
var valueEl = row.querySelector('[data-color-value]');
var presets = row.querySelectorAll('.color-preset'); var presets = row.querySelectorAll('.color-preset');
var reset = row.querySelector('.color-reset'); var reset = row.querySelector('.color-reset');
if (!input) { return; } var panel = row.querySelector('[data-color-panel]');
if (!trigger || !valueEl || !panel) { return; }
function syncOn(v) { var canvas = panel.querySelector('[data-cp-sv]');
var cursor = panel.querySelector('[data-cp-cursor]');
var hueEl = panel.querySelector('[data-cp-hue]');
var prevEl = panel.querySelector('[data-cp-prev]');
var hexEl = panel.querySelector('[data-cp-hex]');
var okBtn = panel.querySelector('[data-cp-ok]');
var hsv = { h: 0, s: 0, v: 0 };
function hexToRgb(h) {
h = (h || '').replace('#', '');
if (h.length === 3) { h = h[0]+h[0]+h[1]+h[1]+h[2]+h[2]; }
return [parseInt(h.substr(0,2),16) || 0, parseInt(h.substr(2,2),16) || 0, parseInt(h.substr(4,2),16) || 0];
}
function rgbToHex(r, g, b) {
function p(n) { n = Math.max(0, Math.min(255, Math.round(n))); return (n < 16 ? '0' : '') + n.toString(16); }
return '#' + p(r) + p(g) + p(b);
}
function rgbToHsv(r, g, b) {
r /= 255; g /= 255; b /= 255;
var mx = Math.max(r, g, b), mn = Math.min(r, g, b), d = mx - mn, h = 0;
if (d !== 0) {
if (mx === r) { h = ((g - b) / d) % 6; }
else if (mx === g) { h = (b - r) / d + 2; }
else { h = (r - g) / d + 4; }
h *= 60; if (h < 0) { h += 360; }
}
return { h: h, s: mx === 0 ? 0 : d / mx, v: mx };
}
function hsvToRgb(h, s, v) {
var c = v * s, x = c * (1 - Math.abs(((h / 60) % 2) - 1)), m = v - c, r = 0, g = 0, b = 0;
if (h < 60) { r = c; g = x; }
else if (h < 120){ r = x; g = c; }
else if (h < 180){ g = c; b = x; }
else if (h < 240){ g = x; b = c; }
else if (h < 300){ r = x; b = c; }
else { r = c; b = x; }
return [(r + m) * 255, (g + m) * 255, (b + m) * 255];
}
function curHex() {
var c = hsvToRgb(hsv.h, hsv.s, hsv.v);
return rgbToHex(c[0], c[1], c[2]);
}
/* 画色相/饱和度板:底色是当前色相的纯色,叠一层
* 左→右白到透明的横向渐变(饱和度)+ 上→下透明的纵向渐变(明度)。
* 这是取色器通用画法,比逐像素画快得多。 */
function drawSV() {
var ctx = canvas.getContext('2d');
var w = canvas.width, h = canvas.height;
var base = hsvToRgb(hsv.h, 1, 1);
ctx.fillStyle = rgbToHex(base[0], base[1], base[2]);
ctx.fillRect(0, 0, w, h);
var gx = ctx.createLinearGradient(0, 0, w, 0);
gx.addColorStop(0, 'rgba(255,255,255,1)');
gx.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = gx; ctx.fillRect(0, 0, w, h);
var gy = ctx.createLinearGradient(0, 0, 0, h);
gy.addColorStop(0, 'rgba(0,0,0,0)');
gy.addColorStop(1, 'rgba(0,0,0,1)');
ctx.fillStyle = gy; ctx.fillRect(0, 0, w, h);
}
function moveCursor() {
/* ⚠️ 两个坑,都踩过:
* ① cursor 是 position:absolute 相对 .cp 定位,canvas 在 .cp 的
* 内边距里侧 —— 要补 offsetLeft/offsetTop,否则整体偏移;
* ② canvas 的**显示尺寸**受 CSS width:100% 影响,不等于其内部
* 像素宽高(220×150)。按内部尺寸算会越拉越偏。
* 必须用 getBoundingClientRect() 的宽高。 */
var rect = canvas.getBoundingClientRect();
var w = rect.width || canvas.width;
var h = rect.height || canvas.height;
cursor.style.left = (canvas.offsetLeft + hsv.s * w) + 'px';
cursor.style.top = (canvas.offsetTop + (1 - hsv.v) * h) + 'px';
cursor.style.background = curHex();
}
function syncAll(skipHex) {
var hex = curHex();
valueEl.value = hex;
trigger.style.setProperty('--cur', hex);
var hexTxt = trigger.querySelector('.color-trigger-hex');
if (hexTxt) { hexTxt.textContent = hex.toUpperCase(); }
if (prevEl) { prevEl.style.background = hex; }
if (!skipHex && hexEl) { hexEl.value = hex.toUpperCase(); }
hueEl.value = Math.round(hsv.h);
drawSV();
moveCursor();
syncPresets(hex);
}
function syncPresets(v) {
for (var k = 0; k < presets.length; k++) { for (var k = 0; k < presets.length; k++) {
var same = (presets[k].getAttribute('data-color') || '').toLowerCase() === (v || '').toLowerCase(); var same = (presets[k].getAttribute('data-color') || '').toLowerCase() === (v || '').toLowerCase();
if (same) { presets[k].classList.add('on'); } else { presets[k].classList.remove('on'); } if (same) { presets[k].classList.add('on'); } else { presets[k].classList.remove('on'); }
} }
} }
function setHex(hex, skipHex) {
var c = hexToRgb(hex);
hsv = rgbToHsv(c[0], c[1], c[2]);
syncAll(skipHex);
}
// ---- 打开 / 关闭 ----
function openPanel() {
panel.hidden = false;
// 面板可能超出视口底部,翻到上方
var r = trigger.getBoundingClientRect();
panel.classList.remove('cp-up');
var need = panel.offsetHeight + 8;
if (r.bottom + need > window.innerHeight && r.top > need) {
panel.classList.add('cp-up');
}
drawSV(); moveCursor();
}
function closePanel() { panel.hidden = true; }
trigger.addEventListener('click', function (e) {
e.stopPropagation();
if (panel.hidden) { openPanel(); } else { closePanel(); }
});
document.addEventListener('click', function (e) {
if (!panel.hidden && !panel.contains(e.target) && e.target !== trigger && !trigger.contains(e.target)) {
closePanel();
}
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && !panel.hidden) { closePanel(); }
});
// ---- 渐变板拖动 ----
var dragging = false;
function pickSV(ev) {
var rect = canvas.getBoundingClientRect();
var x = (ev.touches ? ev.touches[0].clientX : ev.clientX) - rect.left;
var y = (ev.touches ? ev.touches[0].clientY : ev.clientY) - rect.top;
hsv.s = Math.max(0, Math.min(1, x / rect.width));
hsv.v = Math.max(0, Math.min(1, 1 - y / rect.height));
syncAll();
}
canvas.addEventListener('mousedown', function (e) { dragging = true; pickSV(e); e.preventDefault(); });
canvas.addEventListener('touchstart', function (e) { dragging = true; pickSV(e); }, { passive: true });
document.addEventListener('mousemove', function (e) { if (dragging) { pickSV(e); } });
document.addEventListener('touchmove', function (e) { if (dragging) { pickSV(e); } }, { passive: true });
document.addEventListener('mouseup', function () { dragging = false; });
document.addEventListener('touchend', function () { dragging = false; });
// ---- 色相滑条 ----
hueEl.addEventListener('input', function () {
hsv.h = parseFloat(this.value) || 0;
syncAll();
});
// ---- 十六进制手输 ----
hexEl.addEventListener('input', function () {
var v = this.value.trim();
if (/^#?[0-9a-fA-F]{6}$/.test(v)) {
setHex(v[0] === '#' ? v : '#' + v, true);
}
});
okBtn.addEventListener('click', function () { closePanel(); });
// ---- 预设 / 用默认 ----
for (var j = 0; j < presets.length; j++) { for (var j = 0; j < presets.length; j++) {
presets[j].addEventListener('click', function () { presets[j].addEventListener('click', function () {
var c = this.getAttribute('data-color') || ''; setHex(this.getAttribute('data-color') || '');
input.value = c;
syncOn(c);
}); });
} }
input.addEventListener('input', function () { syncOn(this.value); });
if (reset) { if (reset) {
reset.addEventListener('click', function () { reset.addEventListener('click', function () {
// 清空 color 字段 = 保存后回落到按 id 取的默认色 // 清空 = 保存后回落到按 id 取的默认色
input.value = '#0057b8'; valueEl.value = '';
syncOn(''); valueEl.setAttribute('name', 'color');
var hidden = document.createElement('input'); syncPresets('');
hidden.type = 'hidden'; trigger.style.setProperty('--cur', '#cccccc');
hidden.name = 'color'; var hexTxt = trigger.querySelector('.color-trigger-hex');
hidden.value = ''; if (hexTxt) { hexTxt.textContent = '跟随默认'; }
row.appendChild(hidden);
input.removeAttribute('name'); // 提交时以隐藏字段为准
}); });
} }
// 初始化
if (valueEl.value) { setHex(valueEl.value, false); } else { syncAll(); }
})(rows[i]); })(rows[i]);
} }
} }
/* ---------- 图标上传:选中即预览 + 即时校验 ----------
* 用户 2026-11-04:「上传的那个图标没有显示,也没有上传成功、失败之类的提示信息。」
*
* 原来就是一个裸 <input type="file">,选完文件什么都没发生 ——
* 用户不知道选没选中、图片对不对、会不会传不上去。表单整页提交后
* 提示条又在页面顶部,很容易错过。
*
* 这里做三件事(都在前端,最终仍以后端校验为准):
* ① 选中文件立刻用 FileReader 本地预览,不等提交
* ② 显示文件名 + 尺寸 + 大小,让用户确认选对了
* ③ 选之前就拦掉明显不合格的(格式/大小/太小),省一次白提交
* ⚠️ 前端校验只是体验优化,**绝不能**当作安全边界 ——
* 真正的类型/尺寸/大小校验仍在 php 的 fb_icon_store_upload() 里。
*/
function bindIconUpload() {
var inputs = document.querySelectorAll('input[type=file][name=icon_file]');
for (var i = 0; i < inputs.length; i++) {
(function (input) {
var wrap = input.closest('.icon-up');
if (!wrap) { return; }
var prev = wrap.querySelector('.icon-prev');
// 状态条:放在文件输入框下面,比页面顶部的提示显眼得多
var tip = document.createElement('span');
tip.className = 'icon-up-tip';
tip.setAttribute('role', 'status');
input.parentNode.insertBefore(tip, input.nextSibling);
function setTip(msg, kind) {
tip.textContent = msg || '';
tip.className = 'icon-up-tip' + (kind ? ' is-' + kind : '');
}
input.addEventListener('change', function () {
var f = this.files && this.files[0];
if (!f) { setTip(''); return; }
var KB = Math.round(f.size / 1024);
var sizeTxt = KB >= 1024 ? (KB / 1024).toFixed(1) + ' MB' : KB + ' KB';
// ① 格式
var okType = /^image\/(png|jpeg|webp)$/.test(f.type);
if (!okType) {
setTip('✕ 只能选 PNG / JPG / WEBP 图片,当前是「' + (f.type || '未知类型') + '」', 'err');
this.value = '';
return;
}
// ② 大小(后端限 2MB)
if (f.size > 2 * 1024 * 1024) {
setTip('✕ 图片 ' + sizeTxt + ',超过 2 MB 上限,请压缩后再传', 'err');
this.value = '';
return;
}
if (f.size <= 0) {
setTip('✕ 这是个空文件', 'err');
this.value = '';
return;
}
// ③ 本地预览 + 尺寸检查
var fr = new FileReader();
fr.onload = function (ev) {
var img = new Image();
img.onload = function () {
if (img.width < 32 || img.height < 32) {
setTip('✕ 图片只有 ' + img.width + '×' + img.height + ',至少 32×32', 'err');
input.value = '';
return;
}
if (prev) {
prev.innerHTML = '';
var el = document.createElement('img');
el.src = ev.target.result;
el.alt = '';
el.width = 48; el.height = 48;
prev.appendChild(el);
}
var warn = (img.width !== img.height) ? ' ⚠ 不是正方形,显示时会被裁切' : '';
setTip('✓ 已选择:' + f.name + '(' + img.width + '×' + img.height + ',' + sizeTxt + ')'
+ warn + ' 保存后生效', 'ok');
};
img.onerror = function () {
setTip('✕ 这张图读不出来,可能已损坏', 'err');
input.value = '';
};
img.src = ev.target.result;
};
fr.onerror = function () { setTip('✕ 读取文件失败', 'err'); };
fr.readAsDataURL(f);
});
})(inputs[i]);
}
}
/* ---------- 图标快选(后台软件编辑) ---------- */ /* ---------- 图标快选(后台软件编辑) ---------- */
function bindIconPicker() { function bindIconPicker() {
var boxes = document.querySelectorAll('.icon-presets'); var boxes = document.querySelectorAll('.icon-presets');
@@ -867,6 +1136,7 @@
bindWontfixPanel(); bindWontfixPanel();
bindColorPicker(); bindColorPicker();
bindIconPicker(); bindIconPicker();
bindIconUpload();
bindSoftwarePanel(); bindSoftwarePanel();
bindMailTest(); bindMailTest();
bindInlineValidate(); // ★ 必须在 bindNoDoubleSubmit 之前 bindInlineValidate(); // ★ 必须在 bindNoDoubleSubmit 之前
+74 -1
View File
@@ -363,6 +363,14 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
} }
.icon-up-main{flex:1; min-width:220px; display:flex; flex-direction:column} .icon-up-main{flex:1; min-width:220px; display:flex; flex-direction:column}
.icon-up-main input[type=file]{font-size:13.5px; max-width:100%} .icon-up-main input[type=file]{font-size:13.5px; max-width:100%}
/* 上传状态提示:选中文件后立刻告诉用户「选到了什么、合不合格」。
* 用户 2026-11-04:「上传的图标没有显示,也没有成功/失败提示。」 */
.icon-up-tip{
display:block; margin-top:7px; font-size:12.5px; line-height:1.6;
color:var(--muted); word-break:break-all;
}
.icon-up-tip.is-ok{color:var(--ok, #16794a)}
.icon-up-tip.is-err{color:var(--danger, #c0392b); font-weight:600}
/* chip 里也不用白底反色了 —— 图标本来就没有底色 */ /* chip 里也不用白底反色了 —— 图标本来就没有底色 */
.chip.on .sw-badge-nm{color:#fff} .chip.on .sw-badge-nm{color:#fff}
.chip .sw-badge-ic{background:none !important; color:inherit !important; box-shadow:none} .chip .sw-badge-ic{background:none !important; color:inherit !important; box-shadow:none}
@@ -373,11 +381,76 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
flex:none; margin-right:2px; vertical-align:middle; flex:none; margin-right:2px; vertical-align:middle;
box-shadow:0 0 0 1px rgba(0,0,0,.12); box-shadow:0 0 0 1px rgba(0,0,0,.12);
} }
.color-row{display:flex; align-items:center; gap:10px; flex-wrap:wrap} .color-row{display:flex; align-items:center; gap:10px; flex-wrap:wrap; position:relative}
.color-input{ .color-input{
width:46px; height:34px; padding:2px; border:1px solid var(--line-strong); width:46px; height:34px; padding:2px; border:1px solid var(--line-strong);
border-radius:9px; background:var(--card); cursor:pointer; flex:none; border-radius:9px; background:var(--card); cursor:pointer; flex:none;
} }
/* ---------- 自绘取色面板 ----------
* 用户 2026-11-04:「点击之后弹出一个颜色选择器,让用户自己选。」
* 点击色块 → 面板出现在其下方;下方空间不够时翻到上方(JS 加 .cp-up)。 */
.color-trigger{
display:inline-flex; align-items:center; gap:9px;
padding:5px 12px 5px 6px; height:34px;
border:1px solid var(--line-strong); border-radius:9px;
background:var(--card); cursor:pointer; flex:none;
font-size:13px; font-family:ui-monospace,Menlo,Consolas,monospace;
color:var(--fg);
}
.color-trigger:hover{border-color:var(--brand)}
.color-trigger-chip{
width:22px; height:22px; border-radius:6px; flex:none;
background:var(--cur, #0057B8);
box-shadow:inset 0 0 0 1px rgba(0,0,0,.10);
}
.color-trigger-hex{font-weight:600; letter-spacing:.2px}
.cp{
position:absolute; top:calc(100% + 8px); left:0; z-index:80;
width:248px; padding:10px;
background:var(--card); border:1px solid var(--line-strong);
border-radius:12px; box-shadow:0 10px 30px rgba(16,24,40,.16);
user-select:none;
}
.cp.cp-up{top:auto; bottom:calc(100% + 8px)}
.cp-sv{
display:block; width:100%; height:150px; border-radius:8px;
cursor:crosshair;
}
.cp-cursor{
position:absolute; width:14px; height:14px; margin:-7px 0 0 -7px;
border:2px solid #fff; border-radius:50%; pointer-events:none;
box-shadow:0 0 0 1px rgba(0,0,0,.35);
/* 位置由 JS 写入 left/top,相对面板内边距(10px)计算,这里不加偏移 */
}
.cp-hue{margin:9px 0 8px}
.cp-hue input[type=range]{
-webkit-appearance:none; appearance:none; width:100%; height:12px; border-radius:6px;
background:linear-gradient(to right,
#f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
outline:none; cursor:pointer;
}
.cp-hue input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none; width:16px; height:16px; border-radius:50%;
background:#fff; border:2px solid #fff; box-shadow:0 0 0 1px rgba(0,0,0,.35); cursor:pointer;
}
.cp-hue input[type=range]::-moz-range-thumb{
width:14px; height:14px; border-radius:50%; background:#fff;
border:2px solid #fff; box-shadow:0 0 0 1px rgba(0,0,0,.35); cursor:pointer;
}
.cp-foot{display:flex; align-items:center; gap:7px}
.cp-preview{
width:26px; height:26px; border-radius:7px; flex:none;
box-shadow:inset 0 0 0 1px rgba(0,0,0,.12);
}
.cp-hex{
flex:1; min-width:0; height:28px; padding:0 8px;
border:1px solid var(--line-strong); border-radius:7px;
font-family:ui-monospace,Menlo,Consolas,monospace; font-size:12.5px;
text-transform:uppercase; background:var(--card); color:var(--fg);
}
.cp-ok{flex:none; padding:5px 11px !important; font-size:12.5px !important}
.color-presets{display:flex; gap:6px; flex-wrap:wrap} .color-presets{display:flex; gap:6px; flex-wrap:wrap}
.color-preset{ .color-preset{
width:26px; height:26px; border-radius:50%; border:2px solid transparent; width:26px; height:26px; border-radius:50%; border:2px solid transparent;
+29 -5
View File
@@ -540,13 +540,25 @@ function fb_admin_taxonomy_page($pdo, $editSw, $editCat) {
$sugColor = $curColor !== '' ? $curColor : fb_color_default_for($esw ? (int)$esw['id'] : 0); $sugColor = $curColor !== '' ? $curColor : fb_color_default_for($esw ? (int)$esw['id'] : 0);
/* 🔴 必须用 fb_color_palette(),不要再在这里写死一份数组。 /* 🔴 必须用 fb_color_palette(),不要再在这里写死一份数组。
* 以前这里硬编码了另外 8 个色值,和 util.php 的调色板完全不同步 —— * 以前这里硬编码了另外 8 个色值,和 util.php 的调色板完全不同步 ——
* 结果后台只能选那 8 个,用户在后台挑不到新颜色, * 结果后台只能选那 8 个,用户在后台挑不到新颜色。 */
* 而他从没配置过的软件却会被分到「调色板里另一批」颜色。 */
$presets = fb_color_palette(); $presets = fb_color_palette();
/* 颜色选择:**点色块弹出取色面板**(色相/饱和度渐变 + 明度条 + 十六进制),
* 而不是只给一排预设圆点。
* 用户 2026-11-04:「还是不够丰富,让用户自己选:点击之后弹出一个颜色选择器。」
* ⇒ 预设只是快捷方式,真正要给的是**任意取色**能力。
* 原生 <input type="color"> 只有一个小方块,用户不知道能点,
* 而且它的系统面板不在我们控制内(各浏览器样式不一)。
* 这里自带一个面板:Canvas 渐变 + 拖动,谁都看得懂怎么用。 */
echo ' <div class="field"><span>主题色<i class="hint">前台进这个软件时整体配色跟着变,用来区分不同软件</i></span>'; echo ' <div class="field"><span>主题色<i class="hint">前台进这个软件时整体配色跟着变,用来区分不同软件</i></span>';
echo ' <div class="color-row">'; echo ' <div class="color-row" data-colorpicker>';
echo ' <input type="color" name="color" value="' . e($sugColor) . '" class="color-input">'; echo ' <button type="button" class="color-trigger" data-color-trigger'
echo ' <div class="color-presets">'; . ' style="--cur:' . e($sugColor) . '" title="点击选择颜色">'
. ' <span class="color-trigger-chip"></span>'
. ' <span class="color-trigger-hex">' . e(strtoupper($sugColor)) . '</span>'
. ' </button>';
echo ' <input type="hidden" name="color" value="' . e($sugColor) . '" data-color-value>';
// 预设作为快捷色(点了就直接生效),保留但不占主位
echo ' <div class="color-presets" aria-label="常用颜色">';
foreach ($presets as $pc) { foreach ($presets as $pc) {
$on = strcasecmp($pc, $sugColor) === 0 ? ' on' : ''; $on = strcasecmp($pc, $sugColor) === 0 ? ' on' : '';
echo '<button type="button" class="color-preset' . $on . '" data-color="' . e($pc) echo '<button type="button" class="color-preset' . $on . '" data-color="' . e($pc)
@@ -554,6 +566,18 @@ function fb_admin_taxonomy_page($pdo, $editSw, $editCat) {
} }
echo ' </div>'; echo ' </div>';
echo ' <button type="button" class="btn sm color-reset">用默认</button>'; echo ' <button type="button" class="btn sm color-reset">用默认</button>';
/* 取色面板:默认隐藏,点色块才出现。位置由 JS 定位到触发按钮下方。 */
echo ' <div class="cp" data-color-panel hidden>';
echo ' <canvas class="cp-sv" width="220" height="150" data-cp-sv></canvas>';
echo ' <div class="cp-cursor" data-cp-cursor></div>';
echo ' <div class="cp-hue"><input type="range" min="0" max="360" value="0" data-cp-hue aria-label="色相"></div>';
echo ' <div class="cp-foot">';
echo ' <span class="cp-preview" data-cp-prev></span>';
echo ' <input type="text" class="cp-hex" maxlength="7" value="' . e(strtoupper($sugColor)) . '" data-cp-hex aria-label="十六进制颜色值">';
echo ' <button type="button" class="btn sm cp-ok" data-cp-ok>确定</button>';
echo ' </div>';
echo ' </div>';
echo ' </div>'; echo ' </div>';
echo ' </div>'; echo ' </div>';
if ($esw) { if ($esw) {
+1 -1
View File
@@ -8,7 +8,7 @@ if (!defined('FB_ROOT')) {
define('FB_ROOT', dirname(__DIR__)); define('FB_ROOT', dirname(__DIR__));
// 静态资源版本号:改 assets/ 下的 css/js 后把这个数字 +1, // 静态资源版本号:改 assets/ 下的 css/js 后把这个数字 +1,
// 让浏览器与 CDN 缓存失效。全站只此一处,别再往页面里写死数字。 // 让浏览器与 CDN 缓存失效。全站只此一处,别再往页面里写死数字。
if (!defined('FB_ASSET_V')) { define('FB_ASSET_V', 39); } if (!defined('FB_ASSET_V')) { define('FB_ASSET_V', 41); }
} }
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------