网页端下拉框改成自绘浮层(照 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:
@@ -426,6 +426,40 @@ function days_until_birthday(?string $birthday): ?int
|
|||||||
* @param array $extra 额外要显示的行 [[标签, 值], …]
|
* @param array $extra 额外要显示的行 [[标签, 值], …]
|
||||||
* @return string 可直接放进 data-fb-entry 的 JSON 字符串(已转义)
|
* @return string 可直接放进 data-fb-entry 的 JSON 字符串(已转义)
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 把 hex 主题色往白色方向混,得到一档浅色。
|
||||||
|
*
|
||||||
|
* 🔴 #85(用户 2026-11-05):「参考一下那个反馈系统里面的下拉框到底是怎么做的…
|
||||||
|
* 它也有主题颜色,我们这个也有主题颜色,就按照一样的做法去做。」
|
||||||
|
*
|
||||||
|
* FeedLite 那边用三档浅色区分下拉项的三态:
|
||||||
|
* tint-1(混 91% 白)→ hover
|
||||||
|
* tint-2(混 80% 白)→ 选中项(比 hover 深一档,一眼分开)
|
||||||
|
* tint-3(混 72% 白)→ 选中项 hover
|
||||||
|
*
|
||||||
|
* ⚠️ 在 PHP 里算好再输出,**不用 CSS 的 `color-mix()`** ——
|
||||||
|
* FeedLite 实测过:`color-mix(in srgb, var(--brand) 9%, #fff)`
|
||||||
|
* 在 Chrome 里解析成透明(规则匹配上了但不生效)。
|
||||||
|
* ⚠️ `$ratio` 是"往白里混多少",不是"保留多少":
|
||||||
|
* 0.91 = 91% 白 + 9% 原色(很浅),0.0 = 原色本身。
|
||||||
|
*/
|
||||||
|
function fb_color_tint(string $hex, float $ratio): string
|
||||||
|
{
|
||||||
|
$hex = trim($hex);
|
||||||
|
if (preg_match('/^#?([0-9a-fA-F]{6})$/', $hex, $m) !== 1) {
|
||||||
|
return ''; // 不是 6 位 hex(比如 rgba()/色名)就放弃,调用方退回默认
|
||||||
|
}
|
||||||
|
$h = $m[1];
|
||||||
|
$r = (int)hexdec(substr($h, 0, 2));
|
||||||
|
$g = (int)hexdec(substr($h, 2, 2));
|
||||||
|
$b = (int)hexdec(substr($h, 4, 2));
|
||||||
|
$ratio = max(0.0, min(1.0, $ratio));
|
||||||
|
return sprintf('#%02x%02x%02x',
|
||||||
|
(int)round($r + (255 - $r) * $ratio),
|
||||||
|
(int)round($g + (255 - $g) * $ratio),
|
||||||
|
(int)round($b + (255 - $b) * $ratio));
|
||||||
|
}
|
||||||
|
|
||||||
function fb_entry_json(array $ev, string $detail = '', string $edit = '', array $extra = []): string
|
function fb_entry_json(array $ev, string $detail = '', string $edit = '', array $extra = []): string
|
||||||
{
|
{
|
||||||
$date = (string)($ev['date'] ?? '');
|
$date = (string)($ev['date'] ?? '');
|
||||||
|
|||||||
@@ -7339,3 +7339,133 @@ label.is-req,
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.pv-fallback-dl:hover { background: #eee; }
|
.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%; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -2848,4 +2848,236 @@ window.fbHomeTaskDrop = function (row, wasStatus) {
|
|||||||
bindNodeActs();
|
bindNodeActs();
|
||||||
}
|
}
|
||||||
window.fbBindNodeActs = bindNodeActs;
|
window.fbBindNodeActs = bindNodeActs;
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════════════════
|
||||||
|
#85 自绘下拉框(把原生 select 的展开列表换成自绘浮层)
|
||||||
|
══════════════════════════════════════════════════════════════════════
|
||||||
|
🔴 用户 2026-11-05:「网页版的下拉框也太丑了。你参考一下那个反馈系统
|
||||||
|
(FeedLite)里面的下拉框到底是怎么做的?按它的做法做。」
|
||||||
|
|
||||||
|
为什么必须自绘:原生 `<select>` 收起时能改样式,但**展开列表由浏览器绘制,
|
||||||
|
CSS 完全改不动**(只能改 option 底色),各平台样式还不统一。
|
||||||
|
|
||||||
|
但**不重写 HTML**:原生 select 留在 DOM 里参与表单提交,我们只在旁边
|
||||||
|
生成一个按钮 + 浮层。好处:
|
||||||
|
· 全站所有 select 自动获得新样式,不用逐个改模板;
|
||||||
|
· `onchange="this.form.submit()"` 这类内联逻辑照样工作(我们派发原生 change);
|
||||||
|
· 无 JS 时原生 select 仍可用(只是朴素),不会白屏。
|
||||||
|
|
||||||
|
⚠️ 浮层 `position: fixed` 挂 body —— 面板/浮层里大量 `overflow:hidden`,
|
||||||
|
放 select 旁边会被裁掉(FeedLite 踩过这个坑)。
|
||||||
|
⚠️ AJAX 换进来的内容(任务面板)要**再次调用** `fbBindSelects()`,
|
||||||
|
否则新 select 没有增强。已在 task_panel.paint / entry_popup.loadPanel 里接上。
|
||||||
|
══════════════════════════════════════════════════════════════════════ */
|
||||||
|
function fbBindSelects(root) {
|
||||||
|
var scope = root || document;
|
||||||
|
var sels = scope.querySelectorAll('select');
|
||||||
|
for (var i = 0; i < sels.length; i++) {
|
||||||
|
(function (sel) {
|
||||||
|
/* 幂等:已经增强过的跳过(AJAX 重画时会重复调用) */
|
||||||
|
if (sel.getAttribute('data-sel-enhanced') === '1') { return; }
|
||||||
|
/* 明确排除的(比如需要在原位置显示多行列表的) */
|
||||||
|
if (sel.hasAttribute('data-no-enhance')) { return; }
|
||||||
|
/* 多选 / 列表框不动 —— 那些本来就有自己的多行展示方式 */
|
||||||
|
if (sel.multiple || (parseInt(sel.getAttribute('size'), 10) || 0) > 1) { return; }
|
||||||
|
/* 隐藏域也不要(有些地方用它传值) */
|
||||||
|
if (sel.style.display === 'none' || sel.hidden) { return; }
|
||||||
|
|
||||||
|
sel.setAttribute('data-sel-enhanced', '1');
|
||||||
|
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);
|
||||||
|
|
||||||
|
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');
|
||||||
|
/* 先量宽度再定位:panel 刚显示时 offsetWidth 才有值 */
|
||||||
|
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 top = it.offsetTop, bot = top + it.offsetHeight;
|
||||||
|
if (top < panel.scrollTop) { panel.scrollTop = top; }
|
||||||
|
else if (bot > panel.scrollTop + panel.clientHeight) {
|
||||||
|
panel.scrollTop = bot - 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 对外暴露:AJAX 换进来的内容要再调一次 */
|
||||||
|
window.fbBindSelects = fbBindSelects;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* #85:首屏自动增强所有 select */
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', function () { fbBindSelects(document); });
|
||||||
|
} else {
|
||||||
|
fbBindSelects(document);
|
||||||
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -242,6 +242,10 @@
|
|||||||
让它先跑完;mountAntd* 有 data-antd-done 幂等标记,重复调用无害。 */
|
让它先跑完;mountAntd* 有 data-antd-done 幂等标记,重复调用无害。 */
|
||||||
if (typeof window.fbBootAntd === 'function') {
|
if (typeof window.fbBootAntd === 'function') {
|
||||||
try { window.fbBootAntd(); } catch (e) { /* 挂不上就留在原生下拉,不阻断面板 */ }
|
try { window.fbBootAntd(); } catch (e) { /* 挂不上就留在原生下拉,不阻断面板 */ }
|
||||||
|
/* #85:AJAX 换进来的 select 也要增强成自绘下拉 */
|
||||||
|
if (typeof window.fbBindSelects === 'function') {
|
||||||
|
try { window.fbBindSelects(document); } catch (e2) { /* 忽略 */ }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* 🔴 #78(用户 2026-11-05):「选人的时候不能按列表,因为人太多了。
|
/* 🔴 #78(用户 2026-11-05):「选人的时候不能按列表,因为人太多了。
|
||||||
你参考一下观影模式里面选人的方式。」
|
你参考一下观影模式里面选人的方式。」
|
||||||
|
|||||||
@@ -91,6 +91,10 @@
|
|||||||
⚠️ 必须放在 `innerHTML` 之后 —— 要挂的节点得先真的在 DOM 里。 */
|
⚠️ 必须放在 `innerHTML` 之后 —— 要挂的节点得先真的在 DOM 里。 */
|
||||||
if (typeof window.fbBootAntd === 'function') {
|
if (typeof window.fbBootAntd === 'function') {
|
||||||
try { window.fbBootAntd(); } catch (err) { /* 挂不上就留在原生下拉,不阻断面板 */ }
|
try { window.fbBootAntd(); } catch (err) { /* 挂不上就留在原生下拉,不阻断面板 */ }
|
||||||
|
/* #85:AJAX 换进来的 select 也要增强成自绘下拉 */
|
||||||
|
if (typeof window.fbBindSelects === 'function') {
|
||||||
|
try { window.fbBindSelects(document); } catch (e2) { /* 忽略 */ }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* #78:重画后选人组件是新节点 ⇒ 要重新接管(自带 data-pp-init 幂等标记)。
|
/* #78:重画后选人组件是新节点 ⇒ 要重新接管(自带 data-pp-init 幂等标记)。
|
||||||
不补这一步的话,做过任何动作之后「更换负责人 / + 添加人」就点不动了。 */
|
不补这一步的话,做过任何动作之后「更换负责人 / + 添加人」就点不动了。 */
|
||||||
|
|||||||
+10
-1
@@ -134,9 +134,18 @@ if ($_needAddr): ?>
|
|||||||
下面所有用到品牌色的地方(按钮、链接、页头高亮…)自动跟着变。
|
下面所有用到品牌色的地方(按钮、链接、页头高亮…)自动跟着变。
|
||||||
⚠️ 只覆盖这三个变量,**不碰** --paper / --ink 这些纸墨底色 ——
|
⚠️ 只覆盖这三个变量,**不碰** --paper / --ink 这些纸墨底色 ——
|
||||||
模块色是用来"分区"的,把整页背景也换了就不是家书了。 */ ?>
|
模块色是用来"分区"的,把整页背景也换了就不是家书了。 */ ?>
|
||||||
|
<?php
|
||||||
|
/* #85:下拉框浮层要用的三档浅色(与 FeedLite 同一套比例)。
|
||||||
|
在这里一次算好,CSS 直接用 var(--brand-tint-1) 之类。
|
||||||
|
⚠️ 算不出来(主题色不是 6 位 hex)就留空,CSS 里有 fallback。 */
|
||||||
|
$fbTint1 = fb_color_tint((string)$themeVars['brand'], 0.91);
|
||||||
|
$fbTint2 = fb_color_tint((string)$themeVars['brand'], 0.80);
|
||||||
|
$fbTint3 = fb_color_tint((string)$themeVars['brand'], 0.72);
|
||||||
|
?>
|
||||||
<body class="mod-<?= e($moduleKey) ?>"
|
<body class="mod-<?= e($moduleKey) ?>"
|
||||||
data-media-base="media.php?k="
|
data-media-base="media.php?k="
|
||||||
style="--brand: <?= e($themeVars['brand']) ?>; --brand-2: <?= e($themeVars['brand2']) ?>; --brand-ink: <?= e($themeVars['ink']) ?>;">
|
style="--brand: <?= e($themeVars['brand']) ?>; --brand-2: <?= e($themeVars['brand2']) ?>; --brand-ink: <?= e($themeVars['ink']) ?>;
|
||||||
|
--brand-tint-1: <?= e($fbTint1) ?>; --brand-tint-2: <?= e($fbTint2) ?>; --brand-tint-3: <?= e($fbTint3) ?>;">
|
||||||
|
|
||||||
<?php if ($isLoggedIn): ?>
|
<?php if ($isLoggedIn): ?>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
|
|||||||
Reference in New Issue
Block a user