Files
FamilyBook/templates/_person_picker.php
T
laoyang 8f80f3e835 v24 第 2 轮:19 条清单收尾(附件预览/CalDAV/任务分类看板/全局选人/家谱/纪念日/财务)
用户 2026-10-02 提的 19 条需求,本轮完成剩下的 8 条 + CalDAV 服务端:

- ③ 附件:图片/视频给缩略图,PDF/Word/Excel 给**同尺寸**图标;可预览的在本页
     弹层里看(不再新开标签页),渲染不了的点了直接下载;预览层右上角恒有下载按钮。
- ⑦b CalDAV:把「我的日历本」按 RFC 4791 只读子集分享出去(OPTIONS/PROPFIND/
     REPORT/GET,写方法一律 403)。用**专用口令**(库里只存 sha256),不是登录口令。
- ⑨⑩ 任务:分类与看板都由用户自建;已完成的任务从看板消失,改在「已完成」页
     按分类查看/搜索;设置入口在「新建任务」旁的 ☰。
- ⑪ 全局人员选择器:一个片段铺到观影/旅程/资源/日历共享/任务共享/财务共享;
     修掉了老组件「名字和拼音都搜不动」的 bug —— .att-item 自己声明了 display:flex,
     压过了浏览器 [hidden]{display:none},过滤根本没生效。
- ⑫ 家谱:头像改方形不再裁切;姻亲不在图上的卡片给「⇄ 姻亲」入口,
     以 TA 为中心重画 + 「返回我的家谱」。
- ⑬ 纪念日:阴历/阳历都能设且按对应历法重复;名册里每个人的生日默认可见
     (实时派生,不复制一份);全部作为全天日程进日历。
- ⑮ 财务:账本可共享,双方都能记账、都能看全部账目,资产−负债=净资产;
     金额一律以「分」存,不用浮点。

验证:端到端 724 项通过 / 0 失败;几何 368、交互 97、移动端 516、家谱 24、
升级 123 全绿;SQL 列名静态校验 0 处可疑;部署 11 条线上探针全过。
2026-10-03 08:18:05 +08:00

94 lines
5.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 全局人员选择器(v24 用户第 11 条)
*
* 「所有要查找人的地方」统一走这一个片段:chips + 弹窗 + 拼音首字母联想。
* 之前只有观影表单有这套(.att-*),旅程是平铺的 checkbox 网格、共享/归属是
* <select> 下拉 —— 名册一多就变成"在一长串里瞪眼找",而且三种地方三种手感。
*
* 用法(在模板里先准备好变量再 include):
* $ppName 表单字段名。多选传 'attendees[]';单选传 'owner_id' 等
* $ppPeople 候选的人(Person::roster() 的行,至少要有 id / name)
* $ppSelected 已选的 id 数组(单选传 0 或 1 个)
* $ppMode 'multi'(默认)| 'single'
* $ppOpenBtn 打开按钮文案(默认 multi=「+ 添加人」/ single=「选择」)
* $ppTitle 弹窗标题(默认「选择人」)
* $ppAllowEmpty 单选模式下是否给一个「不选」选项(筛选表单用;值为 0)
* $ppEmptyText 单选且未选时的占位(默认「未选择」)
* $ppTip 可选,渲染在按钮下面的一句话
*
* ⚠️ 无 JS 时也能用:选项就是弹窗列表里的原生 checkbox / radio,
* noscript 样式把弹窗摊平成一个普通列表,提交语义与有 JS 时完全一致。
*
* ⚠️ 同一页出现多个选择器时 $ppId 必须不同 —— 片段自己按出现顺序编号,
* 调用方不用管(但同一页重复 include 同一份变量也不会撞)。
*/
$ppMode = $ppMode ?? 'multi';
$ppPeople = $ppPeople ?? [];
$ppSelected = array_map('intval', (array)($ppSelected ?? []));
$ppOpenBtn = $ppOpenBtn ?? ($ppMode === 'single' ? '选择' : '+ 添加人');
$ppTitle = $ppTitle ?? '选择';
$ppEmptyText = $ppEmptyText ?? '未选择';
$ppAllowEmpty = !empty($ppAllowEmpty);
// 同一页多个实例要各有一套 id,否则 document.getElementById 全抓到第一个
static $ppSeq = 0;
$ppSeq++;
$ppId = 'pp' . $ppSeq;
$ppIsMulti = $ppMode === 'single' ? false : true;
$ppInputType = $ppIsMulti ? 'checkbox' : 'radio';
// ⚠️ 「已选」要按**非零**判断:筛选表单常用 0 表示「全部」,[0] 是个非空数组,
// 直接 !$ppSelected 会误判成"选了人",于是「不选」那一项反而不勾。
$ppHasSel = count(array_filter($ppSelected)) > 0;
?>
<div class="pp" id="<?= e($ppId) ?>" data-pp="<?= e($ppId) ?>" data-pp-mode="<?= e($ppIsMulti ? 'multi' : 'single') ?>">
<div class="pp-chips" id="<?= e($ppId) ?>-chips">
<?php /* ⚠️ 循环变量一律用 $ppRow,绝不叫 $p:宿主模板里 $p 常常是别的东西
(高级搜索页的 $p 是"查询条件数组"),被这个片段一覆盖,
页面后半段的 $p['xxx'] 就全成了 Warning。_datetime.php 踩过同款。 */ ?>
<?php foreach ($ppPeople as $ppRow): ?>
<?php if (in_array((int)$ppRow['id'], $ppSelected, true)): ?>
<span class="pp-chip" data-id="<?= (int)$ppRow['id'] ?>"><img src="<?= e(avatar_url($ppRow['avatar'] ?? null)) ?>" alt=""><span class="pp-name"><?= e(person_name($ppRow)) ?></span><?php if ($ppIsMulti): ?><button type="button" class="pp-x" data-id="<?= (int)$ppRow['id'] ?>" title="移除">×</button><?php endif; ?></span>
<?php endif; ?>
<?php endforeach; ?>
<?php if (!$ppIsMulti && !$ppHasSel): ?>
<span class="pp-chip pp-chip-empty"><?= e($ppEmptyText) ?></span>
<?php endif; ?>
</div>
<button type="button" class="btn btn-ghost btn-sm pp-open" id="<?= e($ppId) ?>-open"><?= e($ppOpenBtn) ?></button>
<?php if (!empty($ppTip)): ?><span class="tip"><?= e($ppTip) ?></span><?php endif; ?>
<div class="pp-modal" id="<?= e($ppId) ?>-modal" hidden>
<div class="pp-modal-box">
<div class="pp-modal-head">
<b><?= e($ppTitle) ?></b>
<button type="button" class="pp-close" id="<?= e($ppId) ?>-close" title="关闭">×</button>
</div>
<input class="input pp-search" id="<?= e($ppId) ?>-search" type="text"
placeholder="输入名字或拼音首字母,如 yyq 找「杨永全」" autocomplete="off">
<div class="pp-list" id="<?= e($ppId) ?>-list">
<?php if (!$ppIsMulti && $ppAllowEmpty): ?>
<label class="pp-item pp-item-none" data-name="不选" data-shown="不选">
<input type="radio" name="<?= e($ppName) ?>" value="0" <?= $ppHasSel ? '' : 'checked' ?>>
<span class="pp-name">不选</span>
</label>
<?php endif; ?>
<?php foreach ($ppPeople as $ppRow): ?>
<label class="pp-item"
data-name="<?= e((string)$ppRow['name']) ?>"
data-shown="<?= e(person_name($ppRow)) ?>">
<input type="<?= e($ppInputType) ?>" name="<?= e($ppName) ?>" value="<?= (int)$ppRow['id'] ?>"
<?= in_array((int)$ppRow['id'], $ppSelected, true) ? 'checked' : '' ?>>
<img class="pp-av" src="<?= e(avatar_url($ppRow['avatar'] ?? null)) ?>" alt="" loading="lazy">
<span class="pp-name"><?= e(person_name($ppRow)) ?></span>
<span class="pp-initials" hidden></span>
</label>
<?php endforeach; ?>
<?php unset($ppRow); ?>
<div class="pp-empty" hidden>没有匹配的人。</div>
</div>
</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>
</div>