v24 第 18 批:选人弹框改「暂存 + 确定」两段式(用户第 18 条)
用户原话:「在弹出框里输入一个人的首字母…筛选出这个人,点击这个人的头像或者是名字,
把这个人的信息**暂存到这个选择框的上半部分**。这时候他可以继续在输入框里面输入另外
一个人的拼音首字母,等所有人都选完之后,**点击确定按钮**完成多选然后再返回原来的界面儿,
把他选中的多个人都放入到之前的页面儿里。」
改动:
- templates/_person_picker.php:多选弹框加**暂存区**(搜索框之下的"上半部分")
与底部「取消 / 确定」;这两块**只在 `$ppMode='multi'` 时渲染**。
- assets/js/person_picker.js:
· 点候选人(**click**,不是 change —— 用户点的是"头像或名字")→ 进暂存区、
弹窗**不关**、输入框清空并重新聚焦("继续输入另一个人的首字母");
· 再点同一个人 = 从暂存区撤掉;暂存区里每个 chip 也带 ×;
· 「确定」= 关弹窗(勾选本来就在 input 上,等于已生效)+ 重画外面 chips;
· 「取消」= 按打开时的快照还原;
· **没点确定就关**(× / Esc / 点遮罩)→ 问一句"要保留吗",**绝不静默丢**;
· 单选保持旧行为(点一个即选中并关闭)。
- assets/css/app.css:.pp-stage / .pp-actions 等(含 `[hidden]` 规则 —— 本项目老坑)。
🔴 两个设计取舍(都写在代码注释里):
① **暂存状态就用 `input.checked` 表达,不另存一份数组**。字段的提交语义只由
`input.checked` 决定(表单直接提交它们),另存一份就必然要"确定时写回 input"——
漏做就是"我选了、保存后没人"(第 11 批栽过同类)。所以确定 = 关弹窗。
② **单选完全不动**:它的字段名(person_id / owner_id)在后端是**一个值**,
多选会直接改坏数据。模板里包在 `if ($ppIsMulti)` 里,e2e 断言钉住"单选不许有"。
🔴 真机探针(新套件 _fb_b18_picker.py)抓到两个 jsdom 测不出的真 bug:
① **单选点"已选中的那个人"→ 浏览器不触发 change**(值没变)→ 弹窗一直开着
→ 用户以为"点了没反应"。补一刀 `if (!stagedMode && box.checked) closeModal(true)`,
并在 jsdom 里加了同口径回归断言(runSingle())。
② 人数显示按 id 当 class 查(`pp-stage-n`)→ 取到 null → 永远显示 0(chip 却正常)。
验证:
- e2e 937/0(+17 条本批断言,含单选回归)· 升级 149/0 · 城市解析 32/32 ·
桌面几何 428/0 · 移动端 688/0 · 交互 92/0 · 窄屏溢出 24/0 ·
**选人弹框真机 10/10**(新套件,含"暂存区几何上在搜索框之下、清单之上")·
题图真机 4/4。
- 反向验证 3/3:退回"点一个关一次" / 去掉关闭时的询问 / 给单选也渲染暂存区
—— 三种坏法全部被断言抓住。
This commit is contained in:
@@ -3378,3 +3378,41 @@ body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb,
|
||||
@media (max-width: 620px) {
|
||||
.loc-detail { flex: 1 1 100%; }
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
v24 第 18 批:选人弹框改「暂存 + 确定」两段式(用户第 18 条)
|
||||
上半部分是暂存区(点了头像/名字但还没确定的人),下面是候选清单,
|
||||
最下面「取消 / 确定」。⚠️ 只有多选(.pp-mode=multi)才渲染这两块。
|
||||
============================================================ */
|
||||
|
||||
/* ---- 暂存区:在搜索框**下面**、候选清单**上面** ----
|
||||
⚠️ 用户说的是"选择框的上半部分"。搜索框在最顶(要一直能打字搜下一个人),
|
||||
所以暂存区放在搜索框之下、清单之上 —— 视觉上仍然属于"上半部分",
|
||||
而且不会因为暂存区变高把搜索框挤走(搜索是要反复用的)。 */
|
||||
.pp-stage {
|
||||
border: 1px solid var(--line); border-radius: 8px;
|
||||
background: #FDFBF7; padding: 8px 10px; margin-bottom: 8px;
|
||||
max-height: 148px; overflow-y: auto;
|
||||
}
|
||||
.pp-stage[hidden] { display: none; }
|
||||
.pp-stage-label {
|
||||
display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px;
|
||||
}
|
||||
.pp-stage-label b { color: var(--brand); font-variant-numeric: tabular-nums; }
|
||||
.pp-stage-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
|
||||
/* ---- 底部按钮:取消在左、确定在右(主操作靠右,与全站一致)---- */
|
||||
.pp-actions {
|
||||
display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px;
|
||||
padding-top: 10px; border-top: 1px solid var(--line-2);
|
||||
}
|
||||
|
||||
/* ---- 暂存区里的 chip 比外面的紧凑一点(弹窗里空间有限)---- */
|
||||
.pp-stage-chips .pp-chip { font-size: 12.5px; padding: 2px 5px 2px 3px; }
|
||||
.pp-stage-chips .pp-chip img { width: 20px; height: 20px; }
|
||||
|
||||
/* ---- 窄屏:弹窗几乎占满,暂存区别太高(把清单挤没了就没法选了)---- */
|
||||
@media (max-width: 480px) {
|
||||
.pp-modal-box { max-height: 86vh; }
|
||||
.pp-stage { max-height: 104px; }
|
||||
}
|
||||
|
||||
+251
-16
@@ -1,13 +1,34 @@
|
||||
/**
|
||||
* 全局人员选择器(v24 用户第 11 条)
|
||||
* 全局人员选择器(v24 用户第 11 条;第 18 批改成"暂存 + 确定"两段式)
|
||||
*
|
||||
* 一个页面里可能有多个 .pp(观影同行人 / 旅程同行人 / 资源归属 / 共享给谁 …),
|
||||
* 所以这里按容器逐个初始化,状态全挂在容器自己身上,互不影响。
|
||||
*
|
||||
* 三件事:
|
||||
* 四件事:
|
||||
* ① 汉字 → 拼音首字母(Intl 拼音排序 + 23 个锚字二分,零依赖)
|
||||
* ② 搜索:名字 / 显示名 / 首字母 三者任一命中就留下
|
||||
* ③ chips 与弹窗里的勾选状态双向同步
|
||||
* ③ 多选:点候选人 → 进**暂存区**(弹窗不关)→ 点「确定」才写回原页面
|
||||
* ④ chips 与弹窗里的勾选状态双向同步
|
||||
*
|
||||
* ────────────────────────────────────────────────────────────────
|
||||
* v24 第 18 批:多选从"点一个关一次"改成"暂存 + 确定"。
|
||||
*
|
||||
* 用户原话:「输入一个人的首字母…筛选出这个人,点击这个人的头像或者名字,
|
||||
* 把这个人的信息**暂存到选择框的上半部分**…继续输入另一个人的首字母,
|
||||
* 等所有人都选完之后,**点击确定按钮**完成多选然后再返回原来的界面。」
|
||||
*
|
||||
* 🔴 关键的取舍:**"暂存"状态挂在 DOM 的 input.checked 上,而不是另存一份数组**。
|
||||
* 理由:字段的**提交语义**始终只由 `input.checked` 决定(表单直接提交它们),
|
||||
* 另存一份数组就必然要"确定时把数组写回 input"这一步 —— 那一步一旦漏做/做错,
|
||||
* 用户看到的是"我明明选了、保存后没人"(这个项目的第 11 批就栽过同类问题:
|
||||
* 视觉状态与实际提交状态各存一份,必然飘)。
|
||||
* 现在:暂存就用 checked 表达,确定 = 关弹窗(外加一次 syncChips),
|
||||
* 取消 = 按打开时的快照还原 checked。**没有第三份状态。**
|
||||
*
|
||||
* 🔴 **提示"没点确定就关掉"**:用户原话里有个隐含前提 —— 不确定就不该生效。
|
||||
* 但选人组件常嵌在**别的表单**里,如果关闭时静默丢弃,用户会以为选上了
|
||||
* (比"多一步确认"糟得多)。所以:关掉时若暂存区与打开时不同,弹一句
|
||||
* 确认框问"要保留吗"。这样两种意图都能实现,且**永远不会静默丢数据**。
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
@@ -72,6 +93,28 @@
|
||||
var emptyTip = root.querySelector('.pp-empty');
|
||||
if (!modal || !list || !chips) { return; }
|
||||
|
||||
// ---- 第 18 批新增的暂存区 / 按钮(只有 multi 才渲染,所以可能为 null)----
|
||||
var stage = root.querySelector('.pp-stage');
|
||||
// 🔴 按 **class** 找(`.pp-stage-label b`),不要按 id 找。
|
||||
// 第一版写的是 `root.querySelector('.pp-stage-n')` —— 但那个名字在模板里是
|
||||
// 个 **id**(`<b id="pp1-stage-n">`),DOM 里并没有 `pp-stage-n` 这个 class
|
||||
// → 取到 null → 人数永远显示 0(chip 却正常,所以看着"只是数字不对")。
|
||||
// 按 class 找还顺便去掉了"必须知道组件序号"这个隐含假设。
|
||||
var stageN = root.querySelector('.pp-stage-label b');
|
||||
var stageChips = root.querySelector('.pp-stage-chips');
|
||||
var confirmBtn = root.querySelector('.pp-confirm');
|
||||
var cancelBtn = root.querySelector('.pp-cancel');
|
||||
var stagedMode = !!(isMulti && stage && stageChips);
|
||||
|
||||
/**
|
||||
* 打开弹窗那一刻的选中快照 —— 用于「取消」/「关了但没确定」时还原。
|
||||
*
|
||||
* ⚠️ 存的是**当下的 DOM 状态**,不是服务端传来的初始值:
|
||||
* 用户可能先关一次、留在页面上的 chips 已经变了(例如先在弹窗外删了一个 chip),
|
||||
* 再打开时"原状"应该是那个新状态,而不是最初渲染的状态。
|
||||
*/
|
||||
var snapshot = [];
|
||||
|
||||
// 预计算每个人的首字母串,挂在 data-initials 上(列表再长也只是算一次)
|
||||
// ⚠️⚠️ **每个字段单独算,用空格分隔,绝不拼成一串**。
|
||||
// "姓名 + 称呼" 拼起来会造出**跨字段的巧合命中**:
|
||||
@@ -147,6 +190,90 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把"当前勾选"渲染进弹窗**上半部分的暂存区**(v24 第 18 批 用户原话:
|
||||
* 「把这个人的信息暂存到这个选择框的上半部分」)。
|
||||
*
|
||||
* ⚠️ 暂存区的内容**完全由 input.checked 推出来**,不另存一份"暂存数组"
|
||||
* (见文件头部的说明:两份状态必然飘)。
|
||||
* ⚠️ 每个 chip 带一个 ×:用户在弹窗里就能改主意(不用先确定再回页面删)。
|
||||
*/
|
||||
function syncStage() {
|
||||
if (!stagedMode) { return; }
|
||||
stageChips.innerHTML = '';
|
||||
var n = 0;
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.pp-item input:checked'), function (box) {
|
||||
if (box.value === '0') { return; }
|
||||
var item = box.closest('.pp-item');
|
||||
if (!item) { return; }
|
||||
n++;
|
||||
var nameEl = item.querySelector('.pp-name');
|
||||
var av = item.querySelector('.pp-av');
|
||||
var chip = document.createElement('span');
|
||||
chip.className = 'pp-chip';
|
||||
chip.setAttribute('data-id', box.value);
|
||||
if (av) {
|
||||
var im = document.createElement('img');
|
||||
im.src = av.getAttribute('src');
|
||||
im.alt = '';
|
||||
chip.appendChild(im);
|
||||
}
|
||||
var nm = document.createElement('span');
|
||||
nm.className = 'pp-name';
|
||||
nm.textContent = nameEl ? nameEl.textContent : box.value;
|
||||
chip.appendChild(nm);
|
||||
var x = document.createElement('button');
|
||||
x.type = 'button'; x.className = 'pp-x'; x.textContent = '×';
|
||||
x.title = '移除'; x.setAttribute('data-id', box.value);
|
||||
chip.appendChild(x);
|
||||
stageChips.appendChild(chip);
|
||||
});
|
||||
if (stageN) { stageN.textContent = String(n); }
|
||||
// 一个人都没暂存时不占地方(留一条空横条反而让人以为"这里坏了")
|
||||
stage.hidden = n === 0;
|
||||
// 确定按钮上带个人数,省得用户回头看"我到底选了几个"
|
||||
if (confirmBtn) {
|
||||
confirmBtn.textContent = n > 0 ? ('确定(' + n + ')') : '确定';
|
||||
}
|
||||
}
|
||||
|
||||
/** 内部的统一刷新:chips(外面)+ 暂存区(里面)一起重画 */
|
||||
function refresh() {
|
||||
syncChips();
|
||||
syncStage();
|
||||
}
|
||||
|
||||
/** 记下"打开弹窗那一刻"的勾选,供取消/未确定关闭时还原 */
|
||||
function takeSnapshot() {
|
||||
snapshot = [];
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.pp-item input:checked'), function (box) {
|
||||
snapshot.push(String(box.value));
|
||||
});
|
||||
}
|
||||
|
||||
/** 还原到快照(取消 / 关掉但没确定) */
|
||||
function restoreSnapshot() {
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.pp-item input'), function (box) {
|
||||
box.checked = snapshot.indexOf(String(box.value)) >= 0;
|
||||
});
|
||||
refresh();
|
||||
}
|
||||
|
||||
/** 现在与打开时相比,暂存内容是否已经变了 */
|
||||
function stagedChanged() {
|
||||
var now = [];
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.pp-item input:checked'), function (box) {
|
||||
now.push(String(box.value));
|
||||
});
|
||||
if (now.length !== snapshot.length) { return true; }
|
||||
now.sort();
|
||||
var sn = snapshot.slice().sort();
|
||||
for (var i = 0; i < now.length; i++) {
|
||||
if (now[i] !== sn[i]) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
var q = (search.value || '').trim().toLowerCase();
|
||||
var shown = 0;
|
||||
@@ -226,18 +353,84 @@
|
||||
function openModal() {
|
||||
modal.hidden = false;
|
||||
buildList();
|
||||
// ⚠️ 快照必须在 **buildList 之后**取:清单是打开时才建的,
|
||||
// 建之前 list 里一个 input 都没有 —— 先取快照会永远拿到空数组,
|
||||
// 于是"取消还原"会把所有人的勾都清掉(比不还原还糟)。
|
||||
takeSnapshot();
|
||||
search.value = '';
|
||||
applyFilter();
|
||||
syncChips();
|
||||
refresh();
|
||||
search.focus();
|
||||
}
|
||||
function closeModal() { modal.hidden = true; }
|
||||
|
||||
/**
|
||||
* 关弹窗。
|
||||
*
|
||||
* @param {boolean} force true = 无条件关(点「确定」用);
|
||||
* false/省略 = **暂存有改动时先问一句**。
|
||||
*
|
||||
* 🔴 为什么"没确定就关"不能静默丢弃(第 18 批的核心取舍):
|
||||
* 选人组件常被嵌在**别的表单**里(观影、旅程、任务…都这样)。
|
||||
* 用户点了三个人、直接点 × —— 如果此时把勾全部丢掉,
|
||||
* 接着一提交表单,结果是"我明明选了,保存后没有"。
|
||||
* 但反过来也不能强行"关=确定":用户原话明确要"点确定才算数"。
|
||||
* 折中:有改动就**问一句**,两种意图都能实现,且永远不会静默丢。
|
||||
*/
|
||||
function closeModal(force) {
|
||||
if (force !== true && stagedChanged() && !modal.hidden) {
|
||||
// ⚠️ 用 confirm 而不是自定义弹窗:这里要的是"必须回答"的打断,
|
||||
// 而且它同步返回、不引入新的 DOM 状态(选人组件已经在弹窗里了,
|
||||
// 再叠一层自绘弹窗会变成两层遮罩,Esc/焦点都难处理)。
|
||||
if (window.confirm('还有没点「确定」的选择,要保留吗?\n\n确定 = 保留(等同于点确定)\n取消 = 放弃这次改动,回到打开前的样子')) {
|
||||
refresh(); // 保留:勾选原样留着,外面 chips 也刷新
|
||||
modal.hidden = true;
|
||||
return;
|
||||
}
|
||||
restoreSnapshot();
|
||||
}
|
||||
modal.hidden = true;
|
||||
}
|
||||
|
||||
if (openBtn) { openBtn.addEventListener('click', openModal); }
|
||||
if (closeBtn) { closeBtn.addEventListener('click', closeModal); }
|
||||
modal.addEventListener('click', function (e) { if (e.target === modal) { closeModal(); } });
|
||||
if (closeBtn) { closeBtn.addEventListener('click', function () { closeModal(false); }); }
|
||||
modal.addEventListener('click', function (e) {
|
||||
if (e.target === modal) { closeModal(false); }
|
||||
});
|
||||
search.addEventListener('input', applyFilter);
|
||||
|
||||
// ---- 第 18 批:确定 / 取消 ----
|
||||
if (confirmBtn) {
|
||||
confirmBtn.addEventListener('click', function () {
|
||||
// 确定 = 关掉(勾选本来就都在 input 上,等于已生效)+ 刷新外面 chips
|
||||
refresh();
|
||||
closeModal(true);
|
||||
});
|
||||
}
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', function () {
|
||||
restoreSnapshot();
|
||||
closeModal(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂存区的 × :在弹窗里直接撤掉某个人。
|
||||
* ⚠️ 它必须挂在 stageChips 上(不是整个 modal):modal 上已有的 click 是
|
||||
* "点遮罩关闭",事件冒泡上来会被误当成点到了遮罩(target 是 chip 不是 modal,
|
||||
* 所以其实不会触发 —— 但显式分开更清楚,将来改 modal 那段也不会误伤这里)。
|
||||
*/
|
||||
if (stageChips) {
|
||||
stageChips.addEventListener('click', function (e) {
|
||||
var x = e.target.closest ? e.target.closest('.pp-x') : null;
|
||||
if (!x) { return; }
|
||||
var gid = x.getAttribute('data-id');
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.pp-item input'), function (box) {
|
||||
if (box.value === gid) { box.checked = false; }
|
||||
});
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
|
||||
chips.addEventListener('click', function (e) {
|
||||
var x = e.target.closest ? e.target.closest('.pp-x') : null;
|
||||
if (!x) { return; }
|
||||
@@ -245,27 +438,69 @@
|
||||
Array.prototype.forEach.call(list.querySelectorAll('.pp-item input'), function (box) {
|
||||
if (box.value === gid) { box.checked = false; }
|
||||
});
|
||||
syncChips();
|
||||
refresh();
|
||||
});
|
||||
|
||||
/* 多选:勾一个 = 出 chip + 关弹窗(再添加就再点一次「+ 添加人」);
|
||||
单选:点一个 = 换掉当前这个人 + 关弹窗 */
|
||||
/* 点候选人:**多选不再关弹窗**(第 18 批 用户原话:"继续输入另一个人的首字母…
|
||||
等所有人都选完之后,点击确定按钮"),只把它放进上半部分的暂存区;
|
||||
单选保持旧行为(点一个 = 选中 + 立即关闭)—— 单选没有"确定"这一步。
|
||||
⚠️ 监听用 **click** 而不是 change:label 包着 input 时 change 会正常冒泡,
|
||||
但点 **名字/头像**(label 里的文字与图片)在部分浏览器里只触发 label 的
|
||||
默认行为、change 的时序也不一致;click 更直接。用户原话点的就是
|
||||
"点击这个人的头像或者是名字",所以按 click 实现最贴合。 */
|
||||
list.addEventListener('click', function (e) {
|
||||
var item = e.target.closest ? e.target.closest('.pp-item') : null;
|
||||
if (!item) { return; }
|
||||
var box = item.querySelector('input');
|
||||
if (!box) { return; }
|
||||
|
||||
if (!stagedMode) {
|
||||
/* 🔴 单选:点**已经选中的那个人**时,浏览器**不会触发 change**(值没变化),
|
||||
于是下面的 change 处理器不跑、弹窗就一直开着 —— 用户看到的是
|
||||
"点了这个人,怎么没反应"(真机探针抓到的,不是假想)。
|
||||
这里补一刀:点已选中项 = 直接确认并关闭。
|
||||
这也符合单选的直觉(我就是要选他,他已经选中 → 关掉就对了)。 */
|
||||
if (box.checked) { closeModal(true); }
|
||||
return;
|
||||
}
|
||||
|
||||
// 「不选」那一项只在单选下存在,这里多选场景不会遇到;保险起见跳过 value=0
|
||||
if (box.value === '0') { return; }
|
||||
e.preventDefault(); // 别让 label 的默认行为再去 toggle 一次
|
||||
box.checked = !box.checked; // 点一下 = 暂存;再点一下 = 撤掉
|
||||
// ⚠️ 输入框**清空并重新聚焦**:用户原话"这时候他可以继续在输入框里面输入
|
||||
// 另外一个人的拼音首字母" —— 不清空的话上一轮的过滤条件还在,
|
||||
// 下一个人的候选会被挡住(看起来像"搜不到人")。
|
||||
search.value = '';
|
||||
applyFilter();
|
||||
refresh();
|
||||
search.focus();
|
||||
});
|
||||
|
||||
/* 单选:勾一个 = 换掉当前这个人 + 关弹窗(多选已由上面的 click 处理) */
|
||||
list.addEventListener('change', function (e) {
|
||||
if (!e.target || e.target.tagName !== 'INPUT') { return; }
|
||||
syncChips();
|
||||
if (e.target.checked) { closeModal(); }
|
||||
refresh();
|
||||
if (e.target.checked) { closeModal(true); }
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && !modal.hidden) { closeModal(); }
|
||||
if (e.key === 'Escape' && !modal.hidden) { closeModal(false); }
|
||||
// 多选弹窗里按回车 = 点确定(用户填完最后一个字母顺手回车很自然)
|
||||
if (e.key === 'Enter' && !modal.hidden && stagedMode
|
||||
&& e.target === search) {
|
||||
e.preventDefault();
|
||||
refresh();
|
||||
closeModal(true);
|
||||
}
|
||||
});
|
||||
|
||||
syncChips();
|
||||
// 把"重画 chips"暴露出去:外部程序化改了勾选(ppSetSelected)之后要能刷新展示
|
||||
refresh();
|
||||
// 把"重画 chips + 暂存区"暴露出去:外部程序化改了勾选(ppSetSelected)之后要能刷新展示
|
||||
// ⚠️ 这里的作用域里变量名是 `root`(不是 box)—— 写成 box 会抛
|
||||
// "box is not defined",而且是在**初始化末尾**抛,前面都正常,
|
||||
// 只有后面的 ppSetSelected 才失效(e2e 抓不到,真浏览器才报)。
|
||||
root.__ppSync = syncChips;
|
||||
root.__ppSync = refresh;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,6 +22,24 @@
|
||||
*
|
||||
* ⚠️ 同一页出现多个选择器时 $ppId 必须不同 —— 片段自己按出现顺序编号,
|
||||
* 调用方不用管(但同一页重复 include 同一份变量也不会撞)。
|
||||
*
|
||||
* ────────────────────────────────────────────────────────────────
|
||||
* v24 第 18 批(用户第 18 条):**多选改成"暂存 + 确定"两段式**。
|
||||
*
|
||||
* 用户原话:「在弹出框里输入一个人的首字母…筛选出这个人,点击这个人的头像或者是名字,
|
||||
* 把这个人的信息**暂存到这个选择框的上半部分**。这时候他可以继续在输入框里面输入
|
||||
* 另外一个人的拼音首字母,等所有人都选完之后,**点击确定按钮**完成多选然后再返回
|
||||
* 原来的界面儿,把他选中的多个人都放入到之前的页面儿里。」
|
||||
*
|
||||
* 与旧行为的差别(这是本批的核心):
|
||||
* · 旧:勾一个 = 立刻出 chip **并关掉弹窗** → 选 5 个人要开合 5 次;
|
||||
* · 新:勾一个 = 进弹窗**上半部分的暂存区**、弹窗**不关**、输入框清空继续搜下一个;
|
||||
* 全部选完点「确定」才关闭并把结果写回原页面。
|
||||
* · 关掉弹窗(× / Esc / 点遮罩)**不会**丢掉改动 —— 见下面"取消即还原"的说明。
|
||||
*
|
||||
* ⚠️ 单选($ppMode = 'single')**不改**:单选语义就是"只能有一个",
|
||||
* 再加一个确定按钮等于让用户白点一次。它的字段名(person_id / owner_id…)
|
||||
* 在后端是一个值,多选会直接改坏数据。所以暂存区与确定按钮**只在 multi 下渲染**。
|
||||
*/
|
||||
|
||||
$ppMode = $ppMode ?? 'multi';
|
||||
@@ -81,6 +99,17 @@ $ppHasSel = count(array_filter($ppSelected)) > 0;
|
||||
<input class="input pp-search" id="<?= e($ppId) ?>-search" type="text"
|
||||
placeholder="输入名字或拼音首字母,如 yyq 找「杨永全」" autocomplete="off">
|
||||
</div>
|
||||
<?php /* ---- 暂存区(**只有多选**才有;v24 第 18 批)----
|
||||
用户原话:「把选出来的人暂存到**选择框的上半部分**…等所有人都选完之后,
|
||||
点击确定按钮完成多选」。所以这里就是"已经点了头像/名字、还没点确定"
|
||||
的那批人。它由 JS 渲染(点一个 append 一个),服务端不预置内容 ——
|
||||
服务端渲染的 chips(弹窗外面那个)才是"已经确定过"的人。 */ ?>
|
||||
<?php if ($ppIsMulti): ?>
|
||||
<div class="pp-stage" id="<?= e($ppId) ?>-stage" hidden>
|
||||
<span class="pp-stage-label">已选 <b id="<?= e($ppId) ?>-stage-n">0</b> 人</span>
|
||||
<div class="pp-stage-chips" id="<?= e($ppId) ?>-stage-chips"></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
/* ⚠️ v24 第 11 批 #7 续:候选人清单**首屏不建 DOM**。
|
||||
线上真实数据下,每页 1~3 个选人组件 × 1184 人 = 几千个 <label>+<input>,
|
||||
@@ -134,6 +163,16 @@ $ppHasSel = count(array_filter($ppSelected)) > 0;
|
||||
<?php unset($ppRow); ?>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<?php /* ---- 底部按钮(v24 第 18 批)----
|
||||
⚠️ multi 才有「确定」;single 保持旧行为(点一个即选、立即关闭),
|
||||
因为单选语义就是"只能有一个",多一步确定是白让人点。 */ ?>
|
||||
<?php if ($ppIsMulti): ?>
|
||||
<div class="pp-actions">
|
||||
<button type="button" class="btn btn-ghost pp-cancel" id="<?= e($ppId) ?>-cancel">取消</button>
|
||||
<button type="button" class="btn pp-confirm" id="<?= e($ppId) ?>-confirm">确定</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<noscript><style>.pp-open { display: none; } .pp-modal { position: static; display: block; background: none; padding: 0; } .pp-modal-box { box-shadow: none; border: 1px solid var(--line); max-height: none; }</style></noscript>
|
||||
|
||||
Reference in New Issue
Block a user