主题色改为完整色板:画布 342x200,加明度条,删除右侧预设圆点

用户:「还是不对呀,需要一个完整的色板,右侧那些默认颜色就不要了。」

两个改动:
1. 删掉右侧那排 12 个预设圆点($presets 变量、CSS、JS 同步逻辑一并删)。
   色板本身能取任意颜色,再摆一排样例是重复,还挤占横向空间。
2. 色板做成「完整」的:
   面板 248×246 → 372×357
   画布 226×150 → 342×200(太小拖着不好定位颜色)
   新增明度条,底色随当前色相实时刷新 ——
   否则永远是灰阶,用户看不出拖到那里会得到什么颜色
   面板加标题「选择颜色」,按钮文案「确定」→「完成」

顺带简化光标定位:
原来 cursor 挂在 .cp 里,要靠两个 getBoundingClientRect 相减算偏移;
现在改挂到 .cp-sv-wrap(与 canvas 同级),坐标直接相对 canvas 算,
少一层偏移、少一个坑。

验证(浏览器实测):
  .color-preset 残留 0 个、.color-presets 容器 0 个
  面板 372×357、画布 342×200,完整在视口内
  色相 200° → 明度条背景 linear-gradient(#000, rgb(0,170,255)),确实跟着变
  画布 80%/20% 处取色 → #2996cc,光标落在画布范围内
  明度拖到 50% → #1a5e80(同色相压暗,符合预期)
  手输 #2E7D5B → 隐藏字段 #2e7d5b,点完成关闭
  4 个路由 200
This commit is contained in:
2026-11-04 18:57:18 +08:00
parent ad1b0eedf4
commit 338a35cacd
4 changed files with 95 additions and 84 deletions
+39 -35
View File
@@ -710,18 +710,18 @@
} }
/* ---------- 主题色选择器(后台软件编辑) ---------- /* ---------- 主题色选择器(后台软件编辑) ----------
* 用户 2026-11-04:「还是不够丰富,让用户自己选:点击之后弹出一个颜色选择器。」 * 用户 2026-11-04:「还是不对呀,需要一个完整的色板,右侧那些默认颜色就不要了。」
* *
* 原来是原生 <input type="color">:只有个小方块,用户不知道能点, * 现在的形态是一块**完整色板**(不是小浮窗):
* 而且各浏览器系统面板样式不一。现在自带一个面板: * 340×200 的色相/饱和度画布 + 色相条 + 明度条 + 十六进制输入。
* 色相/饱和度渐变板(Canvas)+ 明度滑块 + 十六进制输入。 * 右侧那排预设圆点已删除 —— 既能任意取色,再摆一排样例就是重复。
* 预设圆点保留为快捷色,但真正的能力是**任意取色**。
* *
* 交互约定: * 交互约定:
* - 点色块 → 面板出现(定位在其下方/上方,视空间而定) * - 点色块 → 色板出现(下方空间不够自动翻到上方)
* - 在渐变板上拖动 → 实时更新;滑条调明度 * - 画布内拖动 = 同时调饱和度与明度
* - 十六进制框可手输,输入合法立即同步 * - 色相条单独调 H;明度条单独调 V
* - 点「确定」或点面板外 → 关闭,值已写入隐藏字段 * - 十六进制框可手输,合法立即同步
* - 点「完成」/ 点色板外 / Esc → 关闭(值早已写入隐藏字段)
*/ */
function bindColorPicker() { function bindColorPicker() {
var rows = document.querySelectorAll('[data-colorpicker]'); var rows = document.querySelectorAll('[data-colorpicker]');
@@ -730,7 +730,6 @@
(function (row) { (function (row) {
var trigger = row.querySelector('[data-color-trigger]'); var trigger = row.querySelector('[data-color-trigger]');
var valueEl = row.querySelector('[data-color-value]'); var valueEl = row.querySelector('[data-color-value]');
var presets = row.querySelectorAll('.color-preset');
var reset = row.querySelector('.color-reset'); var reset = row.querySelector('.color-reset');
var panel = row.querySelector('[data-color-panel]'); var panel = row.querySelector('[data-color-panel]');
if (!trigger || !valueEl || !panel) { return; } if (!trigger || !valueEl || !panel) { return; }
@@ -738,6 +737,7 @@
var canvas = panel.querySelector('[data-cp-sv]'); var canvas = panel.querySelector('[data-cp-sv]');
var cursor = panel.querySelector('[data-cp-cursor]'); var cursor = panel.querySelector('[data-cp-cursor]');
var hueEl = panel.querySelector('[data-cp-hue]'); var hueEl = panel.querySelector('[data-cp-hue]');
var valEl = panel.querySelector('[data-cp-val]'); // 明度条(可能不存在,做兼容)
var prevEl = panel.querySelector('[data-cp-prev]'); var prevEl = panel.querySelector('[data-cp-prev]');
var hexEl = panel.querySelector('[data-cp-hex]'); var hexEl = panel.querySelector('[data-cp-hex]');
var okBtn = panel.querySelector('[data-cp-ok]'); var okBtn = panel.querySelector('[data-cp-ok]');
@@ -801,17 +801,16 @@
} }
function moveCursor() { function moveCursor() {
/* ⚠️ 两个坑,都踩过: /* cursor 现在是 .cp-sv-wrap 的子元素(和 canvas 同级),
* ① cursor 是 position:absolute 相对 .cp 定位,canvas 在 .cp 的 * 所以坐标可以直接相对 canvas 自己算 —— 不用再管 .cp 的 padding:
* 内边距里侧 —— 要补 offsetLeft/offsetTop,否则整体偏移; * left/top 相对 wrap,wrap 和 canvas 左/上边重合。
* ② canvas 的**显示尺寸**受 CSS width:100% 影响,不等于其内部 * 但仍要注意:canvas 显示尺寸(CSS width:100%)≠ 内部像素宽高,
* 像素宽高(220×150)。按内部尺寸算会越拉越偏。 * 必须用 getBoundingClientRect() 的宽高。 */
* 必须用 getBoundingClientRect() 的宽高。 */ var br = canvas.getBoundingClientRect();
var rect = canvas.getBoundingClientRect(); var w = br.width || canvas.width;
var w = rect.width || canvas.width; var h = br.height || canvas.height;
var h = rect.height || canvas.height; cursor.style.left = (hsv.s * w) + 'px';
cursor.style.left = (canvas.offsetLeft + hsv.s * w) + 'px'; cursor.style.top = ((1 - hsv.v) * h) + 'px';
cursor.style.top = (canvas.offsetTop + (1 - hsv.v) * h) + 'px';
cursor.style.background = curHex(); cursor.style.background = curHex();
} }
@@ -824,16 +823,19 @@
if (prevEl) { prevEl.style.background = hex; } if (prevEl) { prevEl.style.background = hex; }
if (!skipHex && hexEl) { hexEl.value = hex.toUpperCase(); } if (!skipHex && hexEl) { hexEl.value = hex.toUpperCase(); }
hueEl.value = Math.round(hsv.h); hueEl.value = Math.round(hsv.h);
if (valEl) { valEl.value = Math.round(hsv.v * 100); }
drawSV(); drawSV();
drawValTrack();
moveCursor(); moveCursor();
syncPresets(hex);
} }
function syncPresets(v) { /* 明度条的底色要跟着当前色相变 —— 否则永远是灰阶,
for (var k = 0; k < presets.length; k++) { * 用户看不清"拖到这里会得到什么颜色"。 */
var same = (presets[k].getAttribute('data-color') || '').toLowerCase() === (v || '').toLowerCase(); function drawValTrack() {
if (same) { presets[k].classList.add('on'); } else { presets[k].classList.remove('on'); } if (!valEl) { return; }
} var pure = hsvToRgb(hsv.h, hsv.s, 1);
var top = rgbToHex(pure[0], pure[1], pure[2]);
valEl.style.background = 'linear-gradient(to right, #000, ' + top + ')';
} }
function setHex(hex, skipHex) { function setHex(hex, skipHex) {
@@ -893,6 +895,14 @@
syncAll(); syncAll();
}); });
// ---- 明度滑条 ----
if (valEl) {
valEl.addEventListener('input', function () {
hsv.v = (parseFloat(this.value) || 0) / 100;
syncAll();
});
}
// ---- 十六进制手输 ---- // ---- 十六进制手输 ----
hexEl.addEventListener('input', function () { hexEl.addEventListener('input', function () {
var v = this.value.trim(); var v = this.value.trim();
@@ -903,21 +913,15 @@
okBtn.addEventListener('click', function () { closePanel(); }); okBtn.addEventListener('click', function () { closePanel(); });
// ---- 预设 / 用默认 ---- // ---- 用默认(清空 = 保存后回落到按 id 取的默认色)----
for (var j = 0; j < presets.length; j++) {
presets[j].addEventListener('click', function () {
setHex(this.getAttribute('data-color') || '');
});
}
if (reset) { if (reset) {
reset.addEventListener('click', function () { reset.addEventListener('click', function () {
// 清空 = 保存后回落到按 id 取的默认色
valueEl.value = ''; valueEl.value = '';
valueEl.setAttribute('name', 'color'); valueEl.setAttribute('name', 'color');
syncPresets('');
trigger.style.setProperty('--cur', '#cccccc'); trigger.style.setProperty('--cur', '#cccccc');
var hexTxt = trigger.querySelector('.color-trigger-hex'); var hexTxt = trigger.querySelector('.color-trigger-hex');
if (hexTxt) { hexTxt.textContent = '跟随默认'; } if (hexTxt) { hexTxt.textContent = '跟随默认'; }
closePanel();
}); });
} }
+35 -24
View File
@@ -387,9 +387,9 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
border-radius:9px; background:var(--card); cursor:pointer; flex:none; border-radius:9px; background:var(--card); cursor:pointer; flex:none;
} }
/* ---------- 自绘取色面板 ---------- /* ---------- 自绘色板 ----------
* 用户 2026-11-04:「点击之后弹出一个颜色选择器,让用户自己选。」 * 用户 2026-11-04:「需要一个完整的色板,右侧那些默认颜色就不要了。」
* 点击色块 → 面板出现在其下方;下方空间不够时翻到上方(JS 加 .cp-up)。 */ * ⇒ 面板做大(宽 372),色板画布 340×200;右侧预设圆点已删除。 */
.color-trigger{ .color-trigger{
display:inline-flex; align-items:center; gap:9px; display:inline-flex; align-items:center; gap:9px;
padding:5px 12px 5px 6px; height:34px; padding:5px 12px 5px 6px; height:34px;
@@ -405,52 +405,63 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
box-shadow:inset 0 0 0 1px rgba(0,0,0,.10); box-shadow:inset 0 0 0 1px rgba(0,0,0,.10);
} }
.color-trigger-hex{font-weight:600; letter-spacing:.2px} .color-trigger-hex{font-weight:600; letter-spacing:.2px}
.color-trigger-caret{font-size:10px; color:var(--muted); margin-left:1px}
.cp{ .cp{
position:absolute; top:calc(100% + 8px); left:0; z-index:80; position:absolute; top:calc(100% + 8px); left:0; z-index:80;
width:248px; padding:10px; width:372px; padding:14px;
background:var(--card); border:1px solid var(--line-strong); background:var(--card); border:1px solid var(--line-strong);
border-radius:12px; box-shadow:0 10px 30px rgba(16,24,40,.16); border-radius:14px; box-shadow:0 12px 36px rgba(16,24,40,.20);
user-select:none; user-select:none; box-sizing:border-box;
} }
.cp.cp-up{top:auto; bottom:calc(100% + 8px)} .cp.cp-up{top:auto; bottom:calc(100% + 8px)}
.cp-hd{
font-size:12.5px; font-weight:700; color:var(--muted);
letter-spacing:.3px; margin:0 0 10px;
}
/* 画布容器:相对定位,光标挂在这里面,坐标计算就不用管面板 padding */
.cp-sv-wrap{position:relative; border-radius:9px; overflow:hidden; line-height:0}
.cp-sv{ .cp-sv{
display:block; width:100%; height:150px; border-radius:8px; display:block; width:100%; height:200px; border-radius:9px;
cursor:crosshair; cursor:crosshair;
} }
.cp-cursor{ .cp-cursor{
position:absolute; width:14px; height:14px; margin:-7px 0 0 -7px; position:absolute; width:15px; height:15px; margin:-7.5px 0 0 -7.5px;
border:2px solid #fff; border-radius:50%; pointer-events:none; border:2px solid #fff; border-radius:50%; pointer-events:none;
box-shadow:0 0 0 1px rgba(0,0,0,.35); box-shadow:0 0 0 1px rgba(0,0,0,.4), 0 1px 3px rgba(0,0,0,.3);
/* 位置由 JS 写入 left/top,相对面板内边距(10px)计算,这里不加偏移 */ }
/* 两条滑条:色相 + 明度 */
.cp-slider{margin:11px 0 0}
.cp-slider input[type=range]{
-webkit-appearance:none; appearance:none; width:100%; height:14px; border-radius:7px;
outline:none; cursor:pointer; display:block;
} }
.cp-hue{margin:9px 0 8px}
.cp-hue input[type=range]{ .cp-hue input[type=range]{
-webkit-appearance:none; appearance:none; width:100%; height:12px; border-radius:6px;
background:linear-gradient(to right, background:linear-gradient(to right,
#f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%); #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{ .cp-val input[type=range]{background:linear-gradient(to right, #000, #fff)}
-webkit-appearance:none; width:16px; height:16px; border-radius:50%; .cp-slider input[type=range]::-webkit-slider-thumb{
background:#fff; border:2px solid #fff; box-shadow:0 0 0 1px rgba(0,0,0,.35); cursor:pointer; -webkit-appearance:none; width:18px; height:18px; border-radius:50%;
background:#fff; border:2px solid #fff;
box-shadow:0 0 0 1px rgba(0,0,0,.35), 0 1px 4px rgba(0,0,0,.25); cursor:pointer;
} }
.cp-hue input[type=range]::-moz-range-thumb{ .cp-slider input[type=range]::-moz-range-thumb{
width:14px; height:14px; border-radius:50%; background:#fff; 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; 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-foot{display:flex; align-items:center; gap:8px; margin-top:13px}
.cp-preview{ .cp-preview{
width:26px; height:26px; border-radius:7px; flex:none; width:30px; height:30px; border-radius:8px; flex:none;
box-shadow:inset 0 0 0 1px rgba(0,0,0,.12); box-shadow:inset 0 0 0 1px rgba(0,0,0,.12);
} }
.cp-hex{ .cp-hex{
flex:1; min-width:0; height:28px; padding:0 8px; flex:1; min-width:0; height:30px; padding:0 9px;
border:1px solid var(--line-strong); border-radius:7px; border:1px solid var(--line-strong); border-radius:8px;
font-family:ui-monospace,Menlo,Consolas,monospace; font-size:12.5px; font-family:ui-monospace,Menlo,Consolas,monospace; font-size:13px;
text-transform:uppercase; background:var(--card); color:var(--fg); text-transform:uppercase; background:var(--card); color:var(--fg);
} }
.cp-ok{flex:none; padding:5px 11px !important; font-size:12.5px !important} .cp-ok{flex:none; padding:6px 14px !important; font-size:13px !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;
+20 -24
View File
@@ -538,44 +538,40 @@ function fb_admin_taxonomy_page($pdo, $editSw, $editCat) {
// 主题色:前台按软件换色,用短链接进不同软件一眼就能区分 // 主题色:前台按软件换色,用短链接进不同软件一眼就能区分
$curColor = $esw ? fb_color_norm((string)$esw['color']) : ''; $curColor = $esw ? fb_color_norm((string)$esw['color']) : '';
$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(),不要再在这里写死一份数组。 /* 颜色选择:**点色块弹出一整块色板**。
* 以前这里硬编码了另外 8 个色值,和 util.php 的调色板完全不同步 —— * 用户 2026-11-04:「还是不对呀,需要一个完整的色板,右侧那些默认颜色就不要了。」
* 结果后台只能选那 8 个,用户在后台挑不到新颜色。 */ * ⇒ ① 右侧那排预设圆点删除,界面更干净,能力靠色板本身(任意取色)覆盖;
$presets = fb_color_palette(); * ② 色板要"完整":足够大的色相/饱和度画布 + 色相条 + 明度条 +
/* 颜色选择:**点色块弹出取色面板**(色相/饱和度渐变 + 明度条 + 十六进制), * 十六进制输入。太小会让人觉得是"玩具",拖着不好定位颜色。
* 而不是只给一排预设圆点。 * ⚠️ 不再需要 $presets:色板本身就能取任意颜色,
* 用户 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" data-colorpicker>'; echo ' <div class="color-row" data-colorpicker>';
echo ' <button type="button" class="color-trigger" data-color-trigger' echo ' <button type="button" class="color-trigger" data-color-trigger'
. ' style="--cur:' . e($sugColor) . '" title="点击选择颜色">' . ' style="--cur:' . e($sugColor) . '" title="点击选择颜色">'
. ' <span class="color-trigger-chip"></span>' . ' <span class="color-trigger-chip"></span>'
. ' <span class="color-trigger-hex">' . e(strtoupper($sugColor)) . '</span>' . ' <span class="color-trigger-hex">' . e(strtoupper($sugColor)) . '</span>'
. ' <span class="color-trigger-caret">▾</span>'
. ' </button>'; . ' </button>';
echo ' <input type="hidden" name="color" value="' . e($sugColor) . '" data-color-value>'; echo ' <input type="hidden" name="color" value="' . e($sugColor) . '" data-color-value>';
// 预设作为快捷色(点了就直接生效),保留但不占主位
echo ' <div class="color-presets" aria-label="常用颜色">';
foreach ($presets as $pc) {
$on = strcasecmp($pc, $sugColor) === 0 ? ' on' : '';
echo '<button type="button" class="color-preset' . $on . '" data-color="' . e($pc)
. '" style="background:' . e($pc) . '" title="' . e($pc) . '"></button>';
}
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 ' <div class="cp" data-color-panel hidden>';
echo ' <canvas class="cp-sv" width="220" height="150" data-cp-sv></canvas>'; echo ' <div class="cp-hd">选择颜色</div>';
echo ' <div class="cp-cursor" data-cp-cursor></div>'; // 画布做大:340×200,拖起来才好定位
echo ' <div class="cp-hue"><input type="range" min="0" max="360" value="0" data-cp-hue aria-label="色相"></div>'; echo ' <div class="cp-sv-wrap">';
echo ' <canvas class="cp-sv" width="340" height="200" data-cp-sv></canvas>';
echo ' <div class="cp-cursor" data-cp-cursor></div>';
echo ' </div>';
// 色相条
echo ' <div class="cp-slider cp-hue"><input type="range" min="0" max="360" value="0" data-cp-hue aria-label="色相"></div>';
// 明度条:背景由 JS 按当前色相刷新
echo ' <div class="cp-slider cp-val"><input type="range" min="0" max="100" value="100" data-cp-val aria-label="明度"></div>';
echo ' <div class="cp-foot">'; echo ' <div class="cp-foot">';
echo ' <span class="cp-preview" data-cp-prev></span>'; 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 ' <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 ' <button type="button" class="btn sm cp-ok" data-cp-ok>完成</button>';
echo ' </div>'; echo ' </div>';
echo ' </div>'; echo ' </div>';
echo ' </div>'; echo ' </div>';
+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', 41); } if (!defined('FB_ASSET_V')) { define('FB_ASSET_V', 43); }
} }
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------