网页端下拉框改成自绘浮层(照 FeedLite 的做法)(#85)

用户 2026-11-05:「网页版的下拉框也太丑了。你参考一下那个反馈系统里面的
下拉框到底是怎么做的?按它的做法做,它也有主题颜色,我们这个也有主题颜色,
就按照一样的做法去做。」

看了 FeedLite(~/code/FeedLite)的实现,关键结论:
  **原生 <select> 收起时能改样式,但展开的选项列表由浏览器绘制,CSS 完全改不动**
  (只能改 option 底色,各平台还不统一)⇒ 想好看只有自绘这条路。

照它的做法移植:

① CSS(assets/css/app.css)
   .sel-native  原生 select 视觉隐藏(**不能 display:none**,那样不参与提交)
   .sel         外层 inline-flex
   .sel-btn     触发按钮 —— 外观与普通输入框完全一致(40 高、同圆角同边框)
   .sel-caret   自绘箭头(CSS mask + 内联 SVG),**打开时转 180° 并变主题色**
   .sel-panel   浮层,position:fixed 挂 body
   .sel-item    列表项:34 高、圆角 7
   .sel-item.on 选中项:深一档主题底色 + 主题色文字 + 右侧对勾

② 主题色三档浅色(照 FeedLite 的比例)
   helpers.php 新增 fb_color_tint()(把 hex 往白色混)。
   layout.php 在 <body> 的 style 里输出:
     --brand-tint-1(混 91% 白)→ hover
     --brand-tint-2(混 80% 白)→ 选中项
     --brand-tint-3(混 72% 白)→ 选中项 hover
   ⚠️ 在 PHP 里算好,**不用 CSS 的 color-mix()** —— FeedLite 实测过:
      `color-mix(in srgb, var(--brand) 9%, #fff)` 在 Chrome 里解析成透明。

③ JS(assets/js/app.js 新增 fbBindSelects())
   遍历所有 select,在旁边生成按钮 + 浮层,原生 select 保留参与提交。
   · 派发原生 change → 兼容 onchange="this.form.submit()" 的内联写法
   · 浮层 fixed 定位(面板/浮层里大量 overflow:hidden,放旁边会被裁)
   · 下方空间不足且上方更宽裕时**向上翻**
   · 键盘:↑↓ / Home / End / Enter / Esc
   · 浮层 minWidth 跟随触发按钮 → "和输入框一样宽"
   · data-sel-enhanced 幂等标记

④ AJAX 路径也要增强
   task_panel.paint() 与 entry_popup.loadPanel() 里,紧跟 fbBootAntd() 之后
   调一次 window.fbBindSelects(document) —— 否则动态插入的 select 是裸的。

⚠️ app.js 整个是 IIFE 包裹的(结尾 `})();`),新函数必须插在闭包内部;
   直接追加到结尾会跑到全局作用域。

实测(Playwright + 站点真实 app.js/css):
  增强:data-sel-enhanced=1、原生 select 已隐藏、按钮文字「工作相关」✅
  尺寸:按钮 648 x 40,行宽 648 → 占满整行 ✅
  浮层:宽 648(与按钮同宽)✅ 6 项、选中项有对勾 ✅
  打开态:底色 rgb(250,242,238)=tint-1、文字 rgb(168,74,40)=brand、
          箭头 rotate 180° ✅
  选「个人事务」→ select.value=3、按钮文字同步、浮层关闭 ✅
This commit is contained in:
2026-11-05 20:23:16 +08:00
parent 47a0027ef7
commit 69c47c3c43
6 changed files with 414 additions and 1 deletions
+130
View File
@@ -7339,3 +7339,133 @@ label.is-req,
text-decoration: none;
}
.pv-fallback-dl:hover { background: #eee; }
/* ══════════════════════════════════════════════════════════════════════
#85 自绘下拉框(替换原生 select 的展开列表)
══════════════════════════════════════════════════════════════════════
🔴 用户 2026-11-05:「网页版的下拉框也太丑了。你参考一下那个反馈系统
(FeedLite)里面的下拉框到底是怎么做的?按它的做法做,它也有主题颜色,
我们这个也有主题颜色,就按照一样的做法去做。」
**为什么必须自绘**:原生 `<select>` 收起时能改样式,
但**展开的选项列表由浏览器/系统绘制,CSS 完全改不动**(只能改 option 底色),
各平台样式还不统一。所以想"好看"只有一条路:自己画。
**但绝不重写 HTML**:原生 select 保留在 DOM 里参与表单提交,
JS 在它旁边生成一个按钮 + 浮层。这样:
· 全站所有 select 自动获得新样式,不用逐个改模板;
· `onchange="this.form.submit()"` 这类内联逻辑照样工作(我们派发原生 change);
· 无 JS 时原生 select 仍可用(只是朴素),不会白屏。
⚠️ 浮层用 `position: fixed` 挂在 body 上 —— 面板/浮层里有很多
`overflow: hidden`,放在 select 旁边会被裁掉(FeedLite 踩过)。
⚠️ 三档浅色由 layout.php 里 PHP 算好(--brand-tint-1/2/3),
不用 `color-mix()`:那个配 var() 在 Chrome 里会解析成透明。
══════════════════════════════════════════════════════════════════════ */
/* 原生 select:视觉隐藏但仍参与提交与事件(**不能 display:none**,那样不参与提交) */
.sel-native {
position: absolute !important;
width: 1px !important; height: 1px !important;
margin: 0 !important; padding: 0 !important;
border: 0 !important; opacity: 0 !important;
pointer-events: none !important;
clip: rect(0 0 0 0) !important;
}
/* 外层:inline-flex,跟输入框一样参与行内布局 */
.sel { position: relative; display: inline-flex; vertical-align: middle; max-width: 100%; min-width: 0; }
/* 触发按钮:外观与普通输入框完全一致(同高同圆角同边框) */
.sel-btn {
display: inline-flex; align-items: center; gap: 10px;
height: 40px; padding: 0 12px;
border: 1px solid var(--line); border-radius: var(--radius-s);
background: #fff; color: var(--ink);
font-family: inherit; font-size: 14px; line-height: 1;
cursor: pointer; text-align: left;
box-shadow: 0 1px 2px rgba(16,24,40,.04);
transition: border-color .12s, box-shadow .12s, background .12s;
max-width: 100%; min-width: 0;
}
.sel-btn:hover { border-color: var(--brand-2); }
.sel-btn:focus-visible { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-tint-1, rgba(168,74,40,.10)); }
.sel-txt { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 未选值(「未分类」「不循环」这类占位)→ 次要色,弱化存在感 */
.sel-btn.is-placeholder .sel-txt { color: var(--muted); }
/* 自绘箭头:默认灰;**打开时转 180° 并变主题色** */
.sel-caret {
width: 11px; height: 7px; flex: none;
background: currentColor; opacity: .55;
-webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='7' viewBox='0 0 11 7'%3E%3Cpath d='M1 1.2 5.5 5.8 10 1.2' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat;
mask: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='7' viewBox='0 0 11 7'%3E%3Cpath d='M1 1.2 5.5 5.8 10 1.2' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat;
transition: transform .16s, background .12s, opacity .12s;
}
.sel-btn.is-open .sel-caret { transform: rotate(180deg); background: var(--brand); opacity: 1; }
/* 打开态整体染主题色:浅主题底 + 主题边框 */
.sel-btn.is-open {
border-color: var(--brand);
background: var(--brand-tint-1, #FAF2EE);
color: var(--brand);
box-shadow: 0 0 0 3px var(--brand-tint-1, rgba(168,74,40,.10));
}
.sel-btn.is-open .sel-txt { color: var(--brand); font-weight: 600; }
/* 浮层:fixed,不被任何 overflow 容器裁切 */
.sel-panel {
position: fixed; z-index: 9998;
max-height: min(320px, calc(100vh - 32px));
overflow-y: auto; overscroll-behavior: contain;
padding: 5px;
background: #fff;
border: 1px solid var(--line);
border-radius: 11px;
box-shadow: 0 10px 30px rgba(16,24,40,.16), 0 2px 6px rgba(16,24,40,.06);
animation: selIn .12s ease-out;
}
@keyframes selIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.sel-panel.is-up { animation-name: selInUp; }
@keyframes selInUp { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
/* 列表项 */
.sel-item {
display: flex; align-items: center; gap: 8px; width: 100%;
padding: 0 10px; height: 34px;
border: 0; border-radius: 7px;
background: transparent; color: var(--ink);
font-family: inherit; font-size: 14px; line-height: 1;
cursor: pointer; text-align: left; white-space: nowrap;
transition: background .1s, color .1s;
-webkit-appearance: none; appearance: none;
}
.sel-item-txt { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* hover / 键盘高亮:主题色最浅一档 */
.sel-item:hover, .sel-item.is-active {
background: var(--brand-tint-1, #FAF2EE);
color: var(--brand);
}
/* 选中项:深一档 + 主题色文字 + 右侧对勾,一眼看出当前选的是哪个 */
.sel-item.on {
background: var(--brand-tint-2, #F3E3DC);
color: var(--brand);
font-weight: 600;
}
.sel-item.on:hover, .sel-item.on.is-active { background: var(--brand-tint-3, #EDD5CB); color: var(--brand); }
.sel-item-ck {
width: 13px; height: 10px; flex: none;
background: currentColor;
-webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='13' height='10' viewBox='0 0 13 10'%3E%3Cpath d='M1.5 5.2 4.8 8.4 11.5 1.6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat;
mask: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='13' height='10' viewBox='0 0 13 10'%3E%3Cpath d='M1.5 5.2 4.8 8.4 11.5 1.6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat;
}
/* 面板里的字段行:下拉要占满整行(与输入框同宽) */
.tk-attr-row > .sel { width: 100%; }
.tk-attr-row > .sel > .sel-btn { width: 100%; }
/* 窄屏:至少占满可用宽度,别挤成一小坨 */
@media (max-width: 640px) {
.sel { flex: 1 1 auto; min-width: 0; }
.sel-btn { width: 100%; }
}