v24 第 9 批:名册九条(分组按钮/加入名册/生日月日/地址四级/千人性能/拖动修复/删除人员/返回名册/Semi DatePicker)

- #131 名册卡片加「分组」按钮(details+radio,显示当前分组,选中即改)
- #132 修复「从已有人里加入名册」无反应
- #133 生日只显示月和日(阳历 MM-DD / 农历中文名),PersonQuery::birthdaysOf 取代 birthdayOf
- #134 地址结构化为 国家/省/市/区+详细:regions 表(3429 行全国区划)+ Region.php +
  ?r=regions 级联接口 + 表单 .addr-grid + 老地址回填(Schema v16)
- #135 千人名册性能:.person content-visibility:auto + 名册页 ob_gzhandler
  (实测 服务端 1.15s/3.8MB → 0.17s/87KB;客户端导航→load 1.35s → 0.37s)
- #136 拖动排序改 Pointer Events(Chrome 下 HTML5 DnD 在 DOM 变动时断)
- #137 个人详情页底部「删除人员」
- #138 编辑资料页加「返回名册」
- #139 所有日期选择换 Semi Design DatePicker(渐进增强:原生四字段保 POST 契约,
  懒加载 vendor semi UMD,值回写原字段;工作经历 type=month 一并升级;宣纸主题对齐)

验证:e2e 785/0、batch9 37/0、upgrade 126/0、picker 28/0、mobile 688/0、interact 97/0、
batch_e 91/0、import 21/0、douban 22/0、drag 5/0、semi-integrate 28/0、semi-probe 17/0、
geom 5/5;部署后线上自检 16/16(含 4 个 semi 资源 200 + regions 路由)。
This commit is contained in:
2026-10-04 08:56:40 +08:00
parent 864e281265
commit 52cc1ecccb
19 changed files with 1629 additions and 154 deletions
+40 -1
View File
@@ -738,7 +738,8 @@ switch ($r) {
// 解密与生日格式化统一走 PersonQuery(见该类的类注释:
// 名册 / 挑人 / 高级搜索必须**同一套判据**,否则同一句搜索在不同页面结果不同)。
$row['phone_plain'] = PersonQuery::phonePlain($row);
[$row['birthday_show'], $row['birthday_kind']] = PersonQuery::birthdayOf($row);
// 用户 2026-10-04 第 3 条:阴历和阳历**都**列出来,且只显示月和日
$row['birthdays'] = PersonQuery::birthdaysOf($row);
// 首字母供前端"输入即筛选"用(filter.js 对 data-initials 做子序列匹配)
$row['initials'] = Pinyin::initials((string)$row['name'] . ' ' . (string)($row['my_nickname'] ?? ''));
}
@@ -751,6 +752,15 @@ switch ($r) {
$grouped[(int)($row['group_id'] ?? 0)][] = $row;
}
// v24 第 9 批 #5:千人名册的 HTML 有 3~4 MB。本机 php -S 与部分虚拟机
// 都不会替 PHP 输出开 gzip,原样吐 3.8 MB 又慢又费流量;
// 这里在渲染前挂 ob_gzhandler(自动按 Accept-Encoding 协商,
// 不支持 gzip 的客户端拿到的还是原文)。压 3.8 MB 约 60~90ms,
// 换来 10 倍体积差;media.php / dav 的二进制输出不走这条路。
if (($_SERVER['HTTP_ACCEPT_ENCODING'] ?? '') !== ''
&& stripos((string)$_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false) {
ob_start('ob_gzhandler');
}
View::render('people/index', [
'title' => '家庭名册',
'people' => $rows,
@@ -1339,6 +1349,12 @@ switch ($r) {
'id_card' => trim((string)($_POST['id_card'] ?? '')),
'email' => trim((string)($_POST['email'] ?? '')),
'address' => trim((string)($_POST['address'] ?? '')),
// 用户 2026-10-04 第 4 条:地址是**结构化五级**(国家/省/市/区/详细),
// 不再是一整串。四个下拉提交的是标准名而不是代码(见 Person::addrOrNull)。
'addr_country' => trim((string)($_POST['addr_country'] ?? '')),
'addr_province' => trim((string)($_POST['addr_province'] ?? '')),
'addr_city' => trim((string)($_POST['addr_city'] ?? '')),
'addr_district' => trim((string)($_POST['addr_district'] ?? '')),
'org' => trim((string)($_POST['org'] ?? '')),
'title' => trim((string)($_POST['title'] ?? '')),
'note' => trim((string)($_POST['note'] ?? '')),
@@ -1500,6 +1516,12 @@ switch ($r) {
'id_card' => $canEditAll ? trim((string)($_POST['id_card'] ?? '')) : Crypto::decrypt((string)$p['id_card_encrypted']),
'email' => trim((string)($_POST['email'] ?? '')),
'address' => trim((string)($_POST['address'] ?? '')),
// 用户 2026-10-04 第 4 条:地址是**结构化五级**(国家/省/市/区/详细),
// 不再是一整串。四个下拉提交的是标准名而不是代码(见 Person::addrOrNull)。
'addr_country' => trim((string)($_POST['addr_country'] ?? '')),
'addr_province' => trim((string)($_POST['addr_province'] ?? '')),
'addr_city' => trim((string)($_POST['addr_city'] ?? '')),
'addr_district' => trim((string)($_POST['addr_district'] ?? '')),
'org' => trim((string)($_POST['org'] ?? '')),
'title' => trim((string)($_POST['title'] ?? '')),
'note' => trim((string)($_POST['note'] ?? '')),
@@ -1682,6 +1704,23 @@ switch ($r) {
redirect('people');
break;
/**
* 行政区划级联数据(用户 2026-10-04 第 4 条:地址要「省 / 市 / 区」三级联动)。
*
* 只吐一个 JSON 列表给前端填下拉 —— 3429 条一次性塞进页面会让表单页重一百多 KB,
* 而用户一次只会用到其中两个下拉,按级取就够了。
* ⚠️ 必须登录才能取(虽然不是什么秘密,但没必要给匿名访客开一个能枚举全国的接口)。
*/
case 'regions':
Auth::requireLogin();
$parent = (string)($_GET['parent'] ?? '');
header('Content-Type: application/json; charset=utf-8');
echo json_encode(
['ok' => true, 'items' => \App\Region::childrenOf($parent)],
JSON_UNESCAPED_UNICODE
);
exit;
// ==================== 账号 ====================
case 'accounts/create':
Auth::requireAdmin();