下拉框改为自绘列表;修复新建软件缺排序字段
用户两个反馈: ① 「排序那个怎么删了?排序还得留着呀」 ② 「所有下拉框拉下来之后全是灰色,不好看,重新设计」 一、排序字段(是误解,但确实有缺陷) 排序字段没有被删,一直在 if ($esw) 分支里渲染。 用户看到"没有",是因为他打开的是「添加软件」—— 新建时 $esw 为空,整块不渲染,所以确实没有这一项。 这是真实缺陷:新建软件时无法指定排序。 修法: - 把排序字段移出 if ($esw),新建/编辑都显示 - 新建时默认值 = 当前最大 sort_order + 1(排到最后) - 后端 sw_add 改为优先用表单值(用 isset 判断, 用户明确填 0 是合法值——排最前面,不能被当成"没填") 与「下载地址」不同:排序是真实会被用到的字段 (前台软件列表、顶栏三横线菜单都按它排),不能删。 二、下拉框重设计 为什么必须自绘:原生 select 的展开列表由浏览器绘制, CSS 只能改 option 的底色,改不了圆角/行高/hover 高亮, 各平台样式还不一样(Windows 是灰方框)。用户说的"全是灰色"就是这个。 但不重写 HTML:全站 16 个 select,逐个改容易漏, 也会破坏 onchange 提交这类内联逻辑。 做法:原生 select 保留(视觉隐藏,仍参与提交与 change 事件), JS 在旁生成按钮 + 浮层列表。 - 浮层用 position:fixed 挂 body,避免被 overflow:hidden 裁(踩过) - 位置自动避让视口,下方不够翻上方 - 支持键盘:上下键 Home End Enter Esc - 选中项带对勾 - 无 JS 时原生 select 仍可用 配色踩坑:选中项最初用 var(--brand-soft),而该变量会被软件主题色覆盖 —— 同步日历主题色 #ff3f44 时 --brand-soft 成了 #ffe8e9(粉红), 下拉里一片粉很难看。下拉是工具性控件,不该跟软件换色, 改用中性色:#eef1f5 选中 / #f1f3f6 hover,四个软件下完全一致。 验证(真实页面实测): - 2 个 select 均被增强,原生已隐藏 - 面板白底 rgb(255,255,255)、圆角 11px、双层柔和阴影、7 个列表项 - 选中项 rgb(238,241,245)、hover rgb(241,243,246)、带对勾 - 四个软件(#0057b8/#ff3f44/#a556f7/#3571fb)下选中项底色完全相同, 不再随主题色变粉 - 键盘下方向键三次 → 高亮「处理中」,底色正确 - 选择后 selectedIndex/value 同步、面板自动关闭、change 事件已触发 - 浮层 fixed 定位、完整可见、不被裁切 - 排序字段 3 处(软件/分类/状态)齐全;5 个路由 200
This commit is contained in:
+216
@@ -1109,6 +1109,221 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 自定义下拉:替换原生 <select> 的展开列表 ----------
|
||||
* 用户 2026-11-04:「所有下拉框拉下来之后怎么全是灰色的?不好看,重新设计。」
|
||||
*
|
||||
* 🔴 为什么必须自绘:原生 <select> 的展开列表是**浏览器渲染的原生控件**,
|
||||
* CSS 只能改 <option> 的 background/color,改不了圆角、行高、hover 高亮、
|
||||
* 分组间距 —— 而且各平台样式完全不同(macOS 是圆角浮层、Windows 是灰方框)。
|
||||
* 想让"展开后"好看,只能自己画一个列表。
|
||||
*
|
||||
* 设计取舍:
|
||||
* - **保留原生 select 作为数据源**:不重写 HTML(全站 16 个 select,
|
||||
* 逐个改容易漏、也容易破坏 onchange 内联逻辑)。
|
||||
* 把原生 select 视觉隐藏,在旁边放一个按钮 + 浮层列表。
|
||||
* - 原生 select 仍参与表单提交(值同步),onchange 也照样触发
|
||||
* —— 调用 this.form.submit() 的那些筛选下拉因此不用改动。
|
||||
* - 无 JS 时原生 select 正常可用(只是样式朴素),不会白屏。
|
||||
* - 浮层用 position:fixed 定位,避免被 overflow:hidden 容器裁掉(踩过)。
|
||||
*/
|
||||
function bindSelect() {
|
||||
var sels = document.querySelectorAll('select');
|
||||
for (var i = 0; i < sels.length; i++) {
|
||||
(function (sel) {
|
||||
// 已经增强过、或明确排除的跳过
|
||||
if (sel.getAttribute('data-enhanced') === '1') { return; }
|
||||
if (sel.hasAttribute('data-no-enhance')) { return; }
|
||||
// 隐藏域 / 多选 / 尺寸>1 的列表框不动
|
||||
if (sel.multiple || (parseInt(sel.getAttribute('size'), 10) || 0) > 1) { return; }
|
||||
|
||||
sel.setAttribute('data-enhanced', '1');
|
||||
|
||||
// 原生 select:保留在 DOM 里(提交用),但视觉隐藏
|
||||
sel.classList.add('sel-native');
|
||||
|
||||
// 包一层,方便定位
|
||||
var wrap = document.createElement('span');
|
||||
wrap.className = 'sel';
|
||||
sel.parentNode.insertBefore(wrap, sel);
|
||||
wrap.appendChild(sel);
|
||||
|
||||
// 触发按钮
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'sel-btn';
|
||||
btn.setAttribute('aria-haspopup', 'listbox');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
wrap.appendChild(btn);
|
||||
|
||||
// 浮层列表(挂到 body,避开一切 overflow 裁剪)
|
||||
var panel = document.createElement('div');
|
||||
panel.className = 'sel-panel';
|
||||
panel.hidden = true;
|
||||
panel.setAttribute('role', 'listbox');
|
||||
document.body.appendChild(panel);
|
||||
|
||||
var active = -1; // 键盘高亮项
|
||||
|
||||
function curText() {
|
||||
var o = sel.options[sel.selectedIndex];
|
||||
return o ? o.textContent : '';
|
||||
}
|
||||
function syncBtn() {
|
||||
btn.innerHTML = '<span class="sel-txt"></span><span class="sel-caret" aria-hidden="true"></span>';
|
||||
btn.querySelector('.sel-txt').textContent = curText();
|
||||
btn.disabled = sel.disabled;
|
||||
btn.classList.toggle('is-placeholder', sel.selectedIndex <= 0 && sel.options.length > 0
|
||||
&& (sel.options[0].value === ''));
|
||||
}
|
||||
|
||||
function buildList() {
|
||||
panel.innerHTML = '';
|
||||
for (var k = 0; k < sel.options.length; k++) {
|
||||
(function (idx) {
|
||||
var o = sel.options[idx];
|
||||
if (o.hidden) { return; }
|
||||
var item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = 'sel-item';
|
||||
item.setAttribute('role', 'option');
|
||||
var on = idx === sel.selectedIndex;
|
||||
if (on) { item.classList.add('on'); item.setAttribute('aria-selected', 'true'); }
|
||||
var label = document.createElement('span');
|
||||
label.className = 'sel-item-txt';
|
||||
label.textContent = o.textContent;
|
||||
item.appendChild(label);
|
||||
if (on) {
|
||||
var ck = document.createElement('span');
|
||||
ck.className = 'sel-item-ck';
|
||||
ck.setAttribute('aria-hidden', 'true');
|
||||
item.appendChild(ck);
|
||||
}
|
||||
item.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
pick(idx);
|
||||
});
|
||||
panel.appendChild(item);
|
||||
})(k);
|
||||
}
|
||||
}
|
||||
|
||||
function pick(idx) {
|
||||
if (idx < 0 || idx >= sel.options.length) { return; }
|
||||
var changed = sel.selectedIndex !== idx;
|
||||
sel.selectedIndex = idx;
|
||||
syncBtn();
|
||||
close();
|
||||
// 触发原生 change,兼容那些 onchange="this.form.submit()" 的下拉
|
||||
if (changed) {
|
||||
var ev = document.createEvent('HTMLEvents');
|
||||
ev.initEvent('change', true, false);
|
||||
sel.dispatchEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (btn.disabled) { return; }
|
||||
buildList();
|
||||
panel.hidden = false;
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
btn.classList.add('is-open');
|
||||
position();
|
||||
// 打开后键盘可直接上下选
|
||||
active = sel.selectedIndex;
|
||||
highlight(active);
|
||||
document.addEventListener('mousedown', outside, true);
|
||||
document.addEventListener('keydown', onKey, true);
|
||||
window.addEventListener('scroll', position, true);
|
||||
window.addEventListener('resize', position);
|
||||
}
|
||||
function close() {
|
||||
if (panel.hidden) { return; }
|
||||
panel.hidden = true;
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
btn.classList.remove('is-open');
|
||||
document.removeEventListener('mousedown', outside, true);
|
||||
document.removeEventListener('keydown', onKey, true);
|
||||
window.removeEventListener('scroll', position, true);
|
||||
window.removeEventListener('resize', position);
|
||||
}
|
||||
function outside(e) {
|
||||
if (!panel.contains(e.target) && !btn.contains(e.target)) { close(); }
|
||||
}
|
||||
|
||||
/* 定位:fixed,挂在 body 上,不服从任何祖先的 overflow。
|
||||
* 优先向下展开;下方空间不足且上方更宽裕时向上翻。 */
|
||||
function position() {
|
||||
if (panel.hidden) { return; }
|
||||
var r = btn.getBoundingClientRect();
|
||||
var vw = window.innerWidth, vh = window.innerHeight;
|
||||
var pw = Math.max(panel.offsetWidth, r.width);
|
||||
var ph = panel.offsetHeight;
|
||||
var margin = 8;
|
||||
|
||||
var left = r.left;
|
||||
if (left + pw + margin > vw) { left = Math.max(margin, vw - pw - margin); }
|
||||
|
||||
var below = vh - r.bottom - margin;
|
||||
var above = r.top - margin;
|
||||
var openUp = (ph > below) && (above > below);
|
||||
var top = openUp ? (r.top - ph - 4) : (r.bottom + 4);
|
||||
if (top < margin) { top = margin; }
|
||||
if (top + ph > vh - margin) { top = Math.max(margin, vh - ph - margin); }
|
||||
|
||||
panel.style.left = Math.round(left) + 'px';
|
||||
panel.style.top = Math.round(top) + 'px';
|
||||
panel.style.minWidth = Math.round(r.width) + 'px';
|
||||
panel.classList.toggle('is-up', openUp);
|
||||
}
|
||||
|
||||
function highlight(idx) {
|
||||
var items = panel.querySelectorAll('.sel-item');
|
||||
for (var k = 0; k < items.length; k++) {
|
||||
items[k].classList.toggle('is-active', k === idx);
|
||||
}
|
||||
if (idx >= 0 && items[idx]) {
|
||||
var it = items[idx];
|
||||
var t = it.offsetTop, b = t + it.offsetHeight;
|
||||
if (t < panel.scrollTop) { panel.scrollTop = t; }
|
||||
else if (b > panel.scrollTop + panel.clientHeight) {
|
||||
panel.scrollTop = b - panel.clientHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onKey(e) {
|
||||
var items = panel.querySelectorAll('.sel-item');
|
||||
if (e.key === 'Escape') { e.preventDefault(); close(); btn.focus(); return; }
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
active = Math.min(items.length - 1, active + 1); highlight(active);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
active = Math.max(0, active - 1); highlight(active);
|
||||
} else if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (active >= 0) { pick(active); }
|
||||
} else if (e.key === 'Home') {
|
||||
e.preventDefault(); active = 0; highlight(active);
|
||||
} else if (e.key === 'End') {
|
||||
e.preventDefault(); active = items.length - 1; highlight(active);
|
||||
}
|
||||
}
|
||||
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
if (panel.hidden) { open(); } else { close(); }
|
||||
});
|
||||
|
||||
// 原生 select 若在别处被程序改值,按钮文字要跟着变
|
||||
sel.addEventListener('change', syncBtn);
|
||||
|
||||
syncBtn();
|
||||
})(sels[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 顶栏软件下拉菜单(右上角三横线) ----------
|
||||
* 极简:点了就展开一个小浮窗,点外面 / Esc 就关。
|
||||
* 不做遮罩、不锁背景滚动 —— 那套是侧边栏的做法,下拉菜单不需要。 */
|
||||
@@ -1196,6 +1411,7 @@
|
||||
bindColorPicker();
|
||||
bindIconPicker();
|
||||
bindIconUpload();
|
||||
bindSelect();
|
||||
bindSoftwarePanel();
|
||||
bindMailTest();
|
||||
bindInlineValidate(); // ★ 必须在 bindNoDoubleSubmit 之前
|
||||
|
||||
Reference in New Issue
Block a user