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
+79 -1
View File
@@ -71,7 +71,7 @@ final class Schema
* ⚠️ 它**不再**作为"要不要跑升级"的判据 —— 见 fingerprint() 的说明。
* 它仍然要手工维护,因为它给人看(排查时一眼知道代码到哪一版了)。
*/
public const VERSION = '15';
public const VERSION = '16';
/**
* 「源码指纹」:拿**本文件自身的内容**算一个哈希,作为"要不要跑升级"的判据。
@@ -604,6 +604,19 @@ CREATE TABLE IF NOT EXISTS `task_comments` (
CONSTRAINT `fk_tc_task` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_tc_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='任务评论(支持 @人)'
SQL,
<<<'SQL'
CREATE TABLE IF NOT EXISTS `regions` (
/* ⚠️ VARCHAR(12) 不是 6:东莞、中山这类「市直管镇」的城市,数据集里第四级
(镇 / 街道)是 9 位代码 —— 写 6 会让插入在第 2170 行被截断裂掉,
后面一千多条全丢(实测只剩 19 个省)。 */
`code` VARCHAR(12) NOT NULL COMMENT '行政区划代码:省 2 位 / 市 4 位 / 区县 6 位(市直管镇的镇 9 位)',
`name` VARCHAR(60) NOT NULL COMMENT '标准名称,如「北京市」「青岛市」「市南区」',
`level` TINYINT NOT NULL COMMENT '1=省级 2=地级市 3=区县(含市直管镇)',
`parent_code` VARCHAR(12) DEFAULT NULL COMMENT '上级代码;省级为 NULL',
PRIMARY KEY (`code`),
KEY `idx_region_parent` (`parent_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='全国行政区划(省/市/区县三级):地址结构化与导入时对齐用'
SQL,
];
}
@@ -807,6 +820,36 @@ SQL,
// 就要为页面上的每个人回读一次存储,几十个人 = 几十次外网往返。
self::addColumn($db, 'persons', 'avatar_color',
"ADD COLUMN `avatar_color` CHAR(7) DEFAULT NULL COMMENT '头像主色(上传时算一次;页面按它派生底色/字色)' AFTER `avatar`");
// ---- 地址结构化(版本 16)----
// 用户 2026-10-04 第 4 条:地址 = 国家 / 省 / 市 / 区 / 详细地址五级,
// 而且「如果没有非常详细的地址,就只记到区」—— 所以四级都可空,
// 只有 `address`(详细地址)是原来那一个自由文本列,语义不变。
// ⚠️ 存的是**标准名**不是代码:通讯录导进来的地址是自由文本,
// 按代码存就要求导入时 100% 匹配得上,匹配不上的整条地址就丢了;
// 存标准名则"对得上就规范化、对不上就照抄",永远不会更差。
self::addColumn($db, 'persons', 'addr_country',
"ADD COLUMN `addr_country` VARCHAR(60) DEFAULT NULL COMMENT '国家' AFTER `address`");
self::addColumn($db, 'persons', 'addr_province',
"ADD COLUMN `addr_province` VARCHAR(60) DEFAULT NULL COMMENT '省 / 自治区 / 直辖市 / 特别行政区(标准名,对应 regions.level=1)' AFTER `addr_country`");
self::addColumn($db, 'persons', 'addr_city',
"ADD COLUMN `addr_city` VARCHAR(60) DEFAULT NULL COMMENT '地级市(标准名,对应 regions.level=2)' AFTER `addr_province`");
self::addColumn($db, 'persons', 'addr_district',
"ADD COLUMN `addr_district` VARCHAR(60) DEFAULT NULL COMMENT '区 / 县(标准名,对应 regions.level=3)' AFTER `addr_city`");
// ⚠️ CREATE TABLE IF NOT EXISTS 不会改已存在的列宽 —— 老库(v16 之前建的
// `code VARCHAR(6)`)必须显式 MODIFY,否则东莞那种 9 位镇级代码插不进去。
$db->exec("ALTER TABLE `regions` MODIFY COLUMN `code`
VARCHAR(12) NOT NULL COMMENT '行政区划代码:省 2 位 / 市 4 位 / 区县 6 位(市直管镇的镇 9 位)'");
$db->exec("ALTER TABLE `regions` MODIFY COLUMN `parent_code`
VARCHAR(12) DEFAULT NULL COMMENT '上级代码;省级为 NULL'");
// 全国行政区划灌库(只是数据步骤 → 容错跑:种子文件丢了也不该挡住后面的升级)
self::tryStep('灌入全国行政区划(省/市/区县)', static function () use ($db) {
\App\Region::seed($db);
});
// 老地址(原来那一整串)拆成结构化:只补空字段,绝不覆盖用户已经改过的
self::tryStep('老地址拆成国家/省/市/区', static function () use ($db) {
self::backfillAddress($db);
});
// 老头像(v11 之前传的)补算一次:回读已存的图算主色。
// 纯数据步骤 → 容错跑:某张图读不回来(OSS 权限、对象丢了)只影响那一个人,
// 他会自动落到「按姓名取色」的兜底上,不该把后面的升级步骤挡住。
@@ -1386,6 +1429,41 @@ SQL);
}
}
/**
* 老地址(原来 `address` 里那一整串自由文本)拆成 国家/省/市/区。
*
* ⚠️ 只补**空着**的四个字段,绝不覆盖:用户如果已经手工选过省市区,
* 一次升级把他选的改成"从文本里猜的",比不猜更糟(猜错就悄悄错了)。
* ⚠️ 拆不出来的(比如只写了「老家」)就**原样留在 address 里** ——
* 详细地址那一栏本来就允许是任意文本,不拆等于没变,不会丢东西。
*/
private static function backfillAddress(\PDO $db): void
{
$rows = $db->query(
"SELECT id, address FROM persons
WHERE address IS NOT NULL AND address <> ''
AND addr_province IS NULL AND addr_city IS NULL AND addr_district IS NULL
ORDER BY id"
)->fetchAll();
if (!$rows) {
return;
}
$up = $db->prepare(
'UPDATE persons SET addr_country = ?, addr_province = ?, addr_city = ?,
addr_district = ?, address = ? WHERE id = ?'
);
foreach ($rows as $r) {
$p = \App\Region::parse((string)$r['address']);
if ($p['province'] === null && $p['city'] === null && $p['district'] === null) {
continue; // 什么都认不出来 → 整串留在详细地址里,不动它
}
$up->execute([
$p['country'], $p['province'], $p['city'],
$p['district'], $p['detail'] ?? '', (int)$r['id'],
]);
}
}
/** 列不存在才加 */
private static function addColumn(\PDO $db, string $table, string $column, string $alter): void
{