Files
FamilyBook/templates/people/form.php
T
laoyang 7138314dc7 v24 第 15 批:活动 / 旅程两级语义(术语 · 派生时间城市 · 表单三项 · 题图可点)
用户原话四条,逐条落地:

1. 「旅程模块,按钮是记一次活动,而不是记一趟旅程。旅程是活动里面的概念。」
   → 顶层一律改称**活动**:按钮「记一次活动」、页标题「活动」、`N 次` / `N 段旅程`;
     活动详情三组改叫「计划的旅程 / 走过的旅程 / 没成行的」;
     节点表单「这段旅程叫什么」「阶段」;flash 文案 17 处同步。
     (导航条仍叫「旅程」——用户就是这么称呼这个模块,列表页加副标题说清两级关系。)

2. 「活动不需要开始和结束时间,是根据活动中的旅程,最早的旅程开始时间,和最晚的
     旅程结束时间,计算出来的。目的地也不需要…去的城市,也是根据旅程,得到的。」
   → 活动**不再存**这三样,全部**读时算出来**:
     · Trip::derived()      最早开始 / 最晚结束 / 城市去重
     · Trip::cities()       按最早出现时间排(先后顺序本身是信息,不是字母序)
     · Trip::attachDerived() 列表页批量补(避免 N+1)
     · Trip::activitySpan()  没有旅程时说「还没有旅程」,不用「日期待补」
     create/update 不再写 events 的起止与地点(老值保留不动,页面不读)。
     列表排序/搜索、首页「最近去了」改走子查询。

3. 「题图那里,没有办法上传图片,点击哪里都没反应。」——**真 bug**
   「点图 = 开文件框」那句 JS 只写在 people/form.php 的内页脚本里(头像专用);
   第 14 批加的旅程题图与图书封面照抄了同一套 DOM 却没有对应的 JS,
   于是图块有 cursor:pointer 与 hover 高亮(看着能点),点下去毫无反应。
   → 做成声明式通用绑定:图上写 data-pick-input="#fileInputId",
     app.js 统一接管(一个页面多个也各绑各的),并删掉旧的那份专用实现。
     三处(头像 / 题图 / 图书封面)全挂上。

4. 「添加活动的页面,顺序是这样的:添加标题,上传并裁剪题图,然后一段简介,就可以了。」
   → 主区就这三项,顺序照原话;时间/城市不做输入项(只留一句说明它们从哪来,
     不说清用户会满页找"怎么没地方填时间")。
     备注 / 公开 / 同行的人收进「更多设置」折叠区——功能一个没丢。

顺带修掉两处会炸的实现问题:
· ORDER BY 引用聚合别名(first_at)在 MariaDB/MySQL 报 1247,且只在运行时炸、
  php -l 看不出 → 改成 ORDER BY MIN(starts_at),否则整个旅程模块 Fatal。
· e2e 里"详情页显示了派生时间"原本写成整页查找,下面每段旅程自己也渲染日期,
  于是把派生值塞成 null 也照样绿(借光假绿)→ 判据收进页头那一段。

验证:e2e 845/0(+15 条本批断言)· 升级 134/0 · 桌面几何 428/0 · 移动几何 688/0 ·
交互 92/0 · 题图真机点击 4/0 · 读书窄屏 15/0 · 家谱 34/0 + 10/0 + 反向 3/3 ·
观影按钮 60/0 + 反向 3/3 · 本批反向验证 4/4(题图绑定抽掉→红、派生值不挂→红、
城市退回读 location→红、时间字段加回表单→红)· 部署静态断言新增两组(含"活动不许
再写起止时间/目的地"与"ORDER BY 不许引用聚合别名"的反退化防线)。
部署后服务器自检 19/19,五个关键文件 md5 与线上逐一核对一致。
2026-10-05 10:58:17 +08:00

495 lines
27 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
/**
* 成员表单(新增 / 编辑)。
*
* 三处与早期版本不同的地方:
* · 「小名」没了 —— 称呼是**视角属性**(妈妈叫"明明"、姐姐叫"小弟"),
* 在「成员详情」页里各自设置自己怎么叫,不写在这个人的档案里。
* · 生日有两种记法:阳历生日与农历生日(证件上那天照常存库,但按用户要求不再显示)。
* 阳历与农历是同一个人的同一天,所以用「以哪个为准」明确这次填的是哪一个,
* 服务端据此反推另一个 —— 不靠 JS,避免"JS 没跑到就静默存错"。
* · 生日精确到**时分秒**:原生 datetime-local 只有时分、且中文环境会显示成
* 「下午 03:30」,所以用 _datetime.php 的「日期 + 24 小时制时:分:秒」组合控件。
*/
$isEdit = !empty($person['id']);
$avatarUrl = avatar_url($person['avatar'] ?? null);
// 这张表单有**两个入口**(名册/详情页的「编辑资料」、个人设置里的「编辑我的资料」),
// 但只有一套字段。改自己、且不是能写的人(访客)时,只给属于他自己的那些 ——
// 见 index.php 的 people/update(那条分支调 Person::updateSelf)。
$canEditAll = $canEditAll ?? \App\Auth::canWrite();
$isSelf = $isSelf ?? false;
$bdSource = old('birthday_source', 'solar');
// 库里这列叫 birthday、表单叫 birthday_solar,一律走 solarOf() 取,别直接下标
$solVal = old('birthday_solar', \App\Birthday::pretty(\App\Birthday::solarOf($person ?? [])));
$lunVal = old('birthday_lunar', '');
if ($lunVal === '') {
// 农历控件的值 = 库里存的农历年月日 + 时间
$lv = (string)($person['birthday_lunar'] ?? '');
$lunVal = $lv;
}
// 补充联系方式(v6 起):新增时是空数组;编辑时是库里解密后的行;
// 校验失败重画时是用户刚提交的那批(连同填错的也要原样带回)。
$contacts = $contacts ?? [];
foreach (['phone', 'email', 'address', 'url'] as $ctKind) {
if (!isset($contacts[$ctKind]) || !is_array($contacts[$ctKind])) {
$contacts[$ctKind] = [];
}
}
$ctEmpty = ['label' => '', 'value' => ''];
$ctTitles = [
'phone' => '补充电话',
'email' => '补充邮箱',
'address' => '其他地址',
'url' => '网址',
];
$ctHints = [
'phone' => '主手机号在「手机号」一栏(开通登录用);这里放工作号、老人机、家里座机等',
'email' => '主邮箱在「邮箱」一栏;这里放其他邮箱',
'address' => '住址在「住址」一栏;这里放单位地址、老家地址等',
'url' => '主页、博客、店铺链接……',
];
?>
<div class="page-head">
<h1><?= $isEdit ? ($isSelf ? '我的资料' : '编辑成员') : '添加成员' ?></h1>
<span class="sub"><?= $isEdit ? e($person['name']) : '填姓名就行,其他都可以以后再补' ?></span>
<?php /* 用户 2026-10-04 第 8 条:编辑资料的页面也要能**回到名册**。
编辑时是从名册点进来的,改完最常做的下一件事就是回名册找下一个人 ——
没有这个按钮就只能靠浏览器后退(而提交保存之后后退会退到旧页面上)。 */ ?>
<div class="actions">
<a class="btn btn-ghost btn-sm" href="<?= e(url('people')) ?>">返回名册</a>
</div>
</div>
<form method="post" enctype="multipart/form-data"
action="<?= e(url($isEdit ? 'people/update' : 'people/create')) ?>">
<?= csrf_field() ?>
<?php if ($isEdit): ?>
<input type="hidden" name="id" value="<?= (int)$person['id'] ?>">
<?php endif; ?>
<div class="card card-pad">
<div class="form-grid">
<?php
// 裁剪比例不是拍脑袋定的,而是由「这个位置显示成什么形状」决定(见 App\ImageSlot)。
// 前端拿这些 data-* 去生成裁剪框,服务端拿同一个槽位去最终裁切 —— 一处定义、两处使用。
$avSlot = \App\ImageSlot::get('avatar');
?>
<?php /* 用户 2026-10-03 第 6 条:**头像放在表单最前面**,并且**去掉「选择文件」按钮**
—— 直接点那张(默认)头像就是选文件,选完弹裁剪框。
好处:① 省掉一整行"选择文件 + 当前头像"的重复控件;
② 一眼就知道"这里能换头像"(按钮式的 file 控件反而看不出当前长什么样)。 */ ?>
<div class="field full avatar-field">
<label>头像</label>
<div class="avatar-picker">
<img id="avatar-preview" src="<?= e($avatarUrl) ?>" alt=""
title="点击更换头像"
data-pick-input="#avatar"
data-has-avatar="<?= trim((string)($person['avatar'] ?? '')) !== '' ? '1' : '0' ?>">
<?php /* 真正吃文件的 input:视觉上藏起来,由上面的图片代它触发
(app.js 的通用绑定认 data-pick-input,不依赖 for/id 配对)。 */ ?>
<input class="sr-only" id="avatar" name="avatar" type="file" accept="image/*"
data-crop-ratio="<?= e((string)$avSlot['ratio']) ?>"
data-crop-w="<?= (int)$avSlot['w'] ?>"
data-crop-h="<?= (int)$avSlot['h'] ?>"
data-crop-label="<?= e((string)$avSlot['label']) ?>"
data-crop-hint="<?= e((string)$avSlot['hint']) ?>"
data-crop-preview="#avatar-preview">
<?php /* 用户挑的裁剪框(归一化的,相对原图 0..1);不裁就留空 = 居中裁切 */ ?>
<input type="hidden" name="avatar_crop" value="">
<span class="avatar-hint small muted">
点一下换头像;选完会弹裁剪框。<br>
头像是 1:1 正方形,多大都能传,保存时自动缩成 <?= (int)$avSlot['w'] ?>×<?= (int)$avSlot['h'] ?>。
</span>
</div>
</div>
<div class="field">
<label for="name">姓名 *</label>
<input class="input" id="name" name="name" value="<?= e(old('name', $person['name'] ?? '')) ?>" required>
<?php if (!empty($errors['name'])): ?><span class="err"><?= e($errors['name']) ?></span><?php endif; ?>
</div>
<div class="field">
<label for="gender">性别</label>
<select class="select" id="gender" name="gender">
<?php $g = old('gender', $person['gender'] ?? ''); ?>
<option value="" <?= $g === '' ? 'selected' : '' ?>>不填</option>
<option value="m" <?= $g === 'm' ? 'selected' : '' ?>>男</option>
<option value="f" <?= $g === 'f' ? 'selected' : '' ?>>女</option>
</select>
</div>
<?php if ($canEditAll): ?>
<div class="field">
<label for="birth_rank">家中排行</label>
<input class="input" id="birth_rank" name="birth_rank" type="number" min="1" max="99"
value="<?= e(old('birth_rank', $person['birth_rank'] ?? '')) ?>" placeholder="老大填 1">
<span class="tip">用来认兄弟姐妹的长幼:长子、次女、三弟…</span>
<?php if (!empty($errors['birth_rank'])): ?><span class="err"><?= e($errors['birth_rank']) ?></span><?php endif; ?>
</div>
<?php endif; ?>
<?php /* ⚠️ 这里**没有**「排序号」(已彻底取消,用户原话:「排序号,不要让用户填写」),
名册里的先后靠**拖动卡片**调。
⚠️「所属分组」**只在新增时出现**($isEdit 为假):
分组是"我和他的关系",写在我自己的名册条目上 ——
· 新增时这个人还不存在,别人名册里不可能有他,这里选了只影响我自己,
正是用户要的「增加名册时可选择所属分组」;
· 编辑时**不能**在这里给:我把他归成"同事"不该把妈妈那边的归类也改掉 ——
改分组走名册里拖动,或那张卡片的「调整分组」。 */ ?>
<?php if (!$isEdit): ?>
<div class="field">
<label for="group_id">所属分组</label>
<select class="select" id="group_id" name="group_id">
<option value="0">先不分类</option>
<?php foreach (\App\PersonGroup::orderFor((int)\App\Auth::familyId(), (int)\App\Auth::personId()) as $g): ?>
<option value="<?= (int)$g['id'] ?>"
<?= (int)old('group_id', 0) === (int)$g['id'] ? 'selected' : '' ?>>
<?= e((string)$g['name']) ?><?= (int)$g['owner_id'] === 0 ? '(公共)' : '' ?>
</option>
<?php endforeach; ?>
</select>
<span class="tip">加进来之后也能改:在名册里把他拖到那一栏就行。</span>
</div>
<?php endif; ?>
<!-- 生日块已挪到表单最底部:大多数情况下生日由身份证号读出、三个都对,
不需要动(用户原话:生日什么的应该放下面)。 -->
<?php /* 用户 2026-10-03 第 3 条:「编辑资料中,**这个人的**一句话,这个地方去掉,
每个人的一句话,**只能他自己修改**。如果他不修改,就是默认空的。」
→ 所以这一栏**只在改自己时出现**($isSelf):
别人编辑他时看不到、也改不了(那正是用户要"去掉"的场景);
而它确实得有个入口让他自己填,否则"只能他自己修改"就没有落点。
⚠️ 服务端同理:people/update 只在 $isSelf 时读 slogan,
否则"管理员编辑一次就把本人的那句话清空"。 */ ?>
<?php if ($isSelf): ?>
<div class="field full">
<label for="slogan">我的这一句话</label>
<input class="input" id="slogan" name="slogan" maxlength="200"
value="<?= e(old('slogan', $person['slogan'] ?? '')) ?>"
placeholder="可留空,如:慢慢来,比较快">
<span class="tip">只有你自己能改这句;显示在自己首页标题下面。留空就不显示。</span>
<?php if (!empty($errors['slogan'])): ?><span class="err"><?= e($errors['slogan']) ?></span><?php endif; ?>
</div>
<?php endif; ?>
<?php if ($canEditAll): ?>
<div class="field">
<label for="phone">手机号</label>
<input class="input" id="phone" name="phone" inputmode="numeric"
value="<?= e(old('phone', $phoneValue ?? '')) ?>" placeholder="可留空">
<span class="tip">开通登录后用它登录;加密存储,列表里只显示部分。其他号码放下面的「补充电话」。</span>
<?php if (!empty($errors['phone'])): ?><span class="err"><?= e($errors['phone']) ?></span><?php endif; ?>
</div>
<?php endif; ?>
<div class="field">
<label for="email">邮箱</label>
<input class="input" id="email" name="email" type="email"
value="<?= e(old('email', $person['email'] ?? '')) ?>" placeholder="可留空">
<?php if (!empty($errors['email'])): ?><span class="err"><?= e($errors['email']) ?></span><?php endif; ?>
</div>
<?php /* 用户 2026-10-03 第 4 条:单位/职务/职称**可以有多段**(记录工作变化)。
所以这里是一组"经历行",而不是三个单值输入框。
⚠️ 表单字段名是平行数组(job_org[] / job_title[] …),
服务端 read_jobs_input() + PersonJob::replace() 整组替换。 */ ?>
<div class="field full">
<label>工作经历</label>
<p class="small muted" style="margin:0 0 10px">
一个人可以有多段工作 —— 换过单位、提过职称都记下来。
<b>不填「结束年月」= 现在还在那儿</b>(列表里会标「现在」)。
</p>
<?php
// 回填优先级:老表单回显(old) → 库里已有的经历 → 完全空(新增时给一行空的)
$jobRows = $jobRows ?? [];
if (!$jobRows) {
$jobRows = [[]]; // 新增时给一行空白行,省得还要点"加一段"
}
?>
<div class="job-list" id="jobList">
<?php foreach ($jobRows as $jr): ?>
<div class="job-row">
<div class="job-cell">
<label class="job-lb">单位</label>
<input class="input" name="job_org[]" value="<?= e((string)($jr['org'] ?? '')) ?>" placeholder="如:中国海洋大学">
</div>
<div class="job-cell">
<label class="job-lb">职务</label>
<input class="input" name="job_title[]" value="<?= e((string)($jr['title'] ?? '')) ?>" placeholder="如:副院长">
</div>
<div class="job-cell">
<label class="job-lb">职称</label>
<input class="input" name="job_rank[]" value="<?= e((string)($jr['rank_name'] ?? '')) ?>" placeholder="如:副教授">
</div>
<div class="job-cell job-cell-sm">
<label class="job-lb">起</label>
<input class="input" type="month" name="job_start[]" value="<?= e((string)($jr['start_ym'] ?? '')) ?>">
</div>
<div class="job-cell job-cell-sm">
<label class="job-lb">止</label>
<input class="input" type="month" name="job_end[]" value="<?= e((string)($jr['end_ym'] ?? '')) ?>">
</div>
<button type="button" class="btn btn-ghost btn-sm job-del" title="删掉这一段">×</button>
</div>
<?php endforeach; ?>
</div>
<div class="btn-row" style="margin-top:8px">
<button type="button" class="btn btn-ghost btn-sm" id="jobAdd">+ 再加一段</button>
</div>
</div>
<?php if ($canEditAll): ?>
<div class="field full">
<label for="id_card">身份证号</label>
<input class="input mono" id="id_card" name="id_card"
value="<?= e(old('id_card', $idCardValue ?? '')) ?>" placeholder="可留空">
<span class="tip">
加密存储,页面上一律只显示前 6 后 4 位。
家庭场景里真正需要它的场合很少 —— 能不填就别填。
</span>
<?php if (!empty($errors['id_card'])): ?><span class="err"><?= e($errors['id_card']) ?></span><?php endif; ?>
</div>
<?php endif; ?>
<?php /* 用户 2026-10-04 第 4 条:地址拆成**国家 / 省 / 市 / 区 / 详细地址**五级。
前四级是级联下拉(数据在 regions 表里,全国 3429 条),
最后一级 `address` 就是原来的自由文本 —— 没那么详细时**只填到区**即可,
四个下拉里没选的那些存 NULL,不会留下一堆空串。
⚠️ 没有 JS 也能用:省/市/区三个下拉在服务端就把**当前这一条链**的选项渲染好了,
JS 只负责"换了省就把市和区跟着换"(见 assets/js/app.js 的 addr 段)。 */ ?>
<div class="field full">
<label for="addr_province">住址</label>
<div class="addr-grid" data-addr
data-src="<?= e(url('regions')) ?>">
<span class="addr-cell">
<span class="addr-k">国家</span>
<input class="input" id="addr_country" name="addr_country"
value="<?= e(old('addr_country', $person['addr_country'] ?? '')) ?>"
placeholder="中国" autocomplete="country-name">
</span>
<span class="addr-cell">
<span class="addr-k">省 / 自治区</span>
<select class="select" id="addr_province" name="addr_province" data-addr-lvl="1">
<option value="">(不填)</option>
<?php foreach (\App\Region::provinces() as $rg): ?>
<option value="<?= e($rg['name']) ?>" data-code="<?= e($rg['code']) ?>"
<?= old('addr_province', $person['addr_province'] ?? '') === $rg['name'] ? 'selected' : '' ?>>
<?= e($rg['name']) ?></option>
<?php endforeach; ?>
</select>
</span>
<span class="addr-cell">
<span class="addr-k">市</span>
<?php $cityCode = \App\Region::codeOfName(old('addr_province', $person['addr_province'] ?? ''), 1); ?>
<select class="select" id="addr_city" name="addr_city" data-addr-lvl="2"
data-code="<?= e($cityCode) ?>">
<option value="">(不填)</option>
<?php foreach (\App\Region::childrenOf($cityCode) as $rg): ?>
<option value="<?= e($rg['name']) ?>" data-code="<?= e($rg['code']) ?>"
<?= old('addr_city', $person['addr_city'] ?? '') === $rg['name'] ? 'selected' : '' ?>>
<?= e($rg['name']) ?></option>
<?php endforeach; ?>
</select>
</span>
<span class="addr-cell">
<span class="addr-k">区 / 县</span>
<?php $distCode = \App\Region::codeOfName(old('addr_city', $person['addr_city'] ?? ''), 2); ?>
<select class="select" id="addr_district" name="addr_district" data-addr-lvl="3"
data-code="<?= e($distCode) ?>">
<option value="">(不填)</option>
<?php foreach (\App\Region::childrenOf($distCode) as $rg): ?>
<option value="<?= e($rg['name']) ?>"
<?= old('addr_district', $person['addr_district'] ?? '') === $rg['name'] ? 'selected' : '' ?>>
<?= e($rg['name']) ?></option>
<?php endforeach; ?>
</select>
</span>
<span class="addr-cell addr-cell-wide">
<span class="addr-k">详细地址</span>
<input class="input" id="address" name="address"
value="<?= e(old('address', $person['address'] ?? '')) ?>"
placeholder="街道门牌、小区楼号……没有就留空" autocomplete="street-address">
</span>
</div>
<span class="tip">
没有详细地址时只填到「区 / 县」就行 —— 四级里没选的那些不会显示。
</span>
</div>
<?php /* 用户 2026-10-03 第 5 条:备注分两种 ——
① 这一栏是**这个人自己的**自我介绍/格言(他本人也能在个人设置里改,
别人打开他的资料能看到);
② 「我对他的私人备注」在下面单独一块(只有写的人自己看得到)。 */ ?>
<div class="field full">
<label for="note">自我介绍(他自己写的)</label>
<textarea class="input" id="note" name="note" placeholder="爱吃辣、对芒果过敏、怕坐过山车……"><?= e(old('note', $person['note'] ?? '')) ?></textarea>
<span class="tip">这是<b>他本人</b>的自我介绍,别人看得到。改的是同一份 —— 他也能在个人设置里改。</span>
</div>
<?php /* 用户第 5 条:**我对他的私人备注** —— 只有我自己看得到(存在 person_notes 里)。
只在"编辑别人"时出现(自己看自己不需要"我对我的备注")。
⚠️ 它是独立的出口(自己的路由 + 自己的表),不与上面那份混在一起:
「他愿意公开说的」与「我对他的私下记录」是两件事、两种可见性。 */ ?>
<?php if ($isEdit && !$isSelf): ?>
<div class="field full">
<label for="myNote">我对 <?= e((string)$person['name']) ?> 的备注(只有我自己看得到)</label>
<textarea class="input" id="myNote" name="my_note" rows="2"
placeholder="怎么认识的、要注意什么、上次见面聊了什么……"><?= e((string)($myNote ?? '')) ?></textarea>
<span class="tip">
<b>只有你自己看得到</b>,别人(包括他本人)都看不到 ——
这里写的是"我和他之间的事",不是他的公开资料。
</span>
</div>
<?php endif; ?>
<!-- ==================== 补充联系方式(可多条,对齐 vCard) ==================== -->
<?php foreach (['phone', 'email', 'address', 'url'] as $ctKind): ?>
<div class="field full ct-block" data-ct-kind="<?= $ctKind ?>">
<label><?= $ctTitles[$ctKind] ?></label>
<p class="small muted" style="margin:0 0 8px"><?= $ctHints[$ctKind] ?></p>
<?php foreach (array_merge($contacts[$ctKind], [$ctEmpty]) as $ctRow): ?>
<div class="ct-row" data-ct-row>
<input class="input ct-label" name="ct_<?= $ctKind ?>_label[]" maxlength="20"
value="<?= e((string)($ctRow['label'] ?? '')) ?>" placeholder="标注(如:工作)">
<input class="input" name="ct_<?= $ctKind ?>_value[]"
value="<?= e((string)($ctRow['value'] ?? '')) ?>"
placeholder="<?= $ctKind === 'phone' ? '号码' : ($ctKind === 'url' ? 'https://…' : '内容') ?>">
</div>
<?php endforeach; ?>
<button type="button" class="btn btn-ghost btn-sm ct-add">再加一个</button>
<?php if (!empty($errors['contacts'])): ?><span class="err"><?= e($errors['contacts']) ?></span><?php endif; ?>
</div>
<?php endforeach; ?>
<!-- ==================== 三个生日(放在最后:多数情况不用动) ==================== -->
<div class="field full">
<label>生日</label>
<p class="small muted" style="margin:0 0 12px">
一个人的生日分两种记法:<b>真实阳历生日</b>与<b>农历生日</b>。
阳历与农历只要填一个,另一个自动算出来。两个不在同一天时,首页会分别提醒。
</p>
<div class="bd-source">
<label for="bdSource">我这次填的是</label>
<select class="select" id="bdSource" name="birthday_source" style="width:auto">
<option value="solar" <?= $bdSource === 'solar' ? 'selected' : '' ?>>阳历(公历)</option>
<option value="lunar" <?= $bdSource === 'lunar' ? 'selected' : '' ?>>农历(阴历)</option>
</select>
<span class="small muted">选中的那一边为准,另一边自动算</span>
</div>
<div class="bd-pane" id="bdSolarWrap">
<p class="bd-pane-k">阳历生日</p>
<?php
$dtName = 'birthday_solar';
$dtValue = $solVal;
$dtId = 'birthday_solar_date';
$dtRequired = false;
require __DIR__ . '/../_datetime.php';
?>
<span class="tip">精确到时分秒。不填时分秒就按 00:00:00 记。</span>
<?php if (!empty($errors['birthday_solar'])): ?><span class="err"><?= e($errors['birthday_solar']) ?></span><?php endif; ?>
</div>
<div class="bd-pane" id="bdLunarWrap">
<p class="bd-pane-k">农历生日</p>
<?php
$dtName = 'birthday_lunar';
$dtValue = $lunVal;
$dtId = 'birthday_lunar_date';
$dtRequired = false;
require __DIR__ . '/../_datetime.php';
?>
<label class="check-item" style="margin-top:8px">
<input type="checkbox" name="birthday_lunar_leap" value="1"
<?= old('birthday_lunar_leap', $person['birthday_lunar_leap'] ?? 0) ? 'checked' : '' ?>>
<span>这个月是<b>闰月</b>(如「闰六月」)</span>
</label>
<?php if (!empty($errors['birthday_lunar'])): ?><span class="err"><?= e($errors['birthday_lunar']) ?></span><?php endif; ?>
</div>
</div>
<?php if ($canEditAll): ?>
<div class="field full">
<label class="check-item">
<input type="checkbox" name="is_placeholder" value="1"
<?= old('is_placeholder', $person['is_placeholder'] ?? 0) ? 'checked' : '' ?>>
<span>
<b>这是「虚拟人物」</b><br>
<span class="muted small">
还没弄清真实身份时先占个位,比如起名「我的父亲」「外婆」。
他能参与亲属关系,等弄清真名再改过来即可;家谱上会标「待确认」。
</span>
</span>
</label>
</div>
<?php endif; ?>
</div>
<div class="btn-row">
<button class="btn" type="submit"><?= $isEdit ? '保存' : '加入名册' ?></button>
<?php
// 取消:改自己的话回「个人设置」更顺手(那是他来的地方),
// 改别人 / 新增则回他的详情页 / 名册。
$cancelUrl = $isEdit
? ($isSelf ? url('settings/me') : url('people/show', ['id' => (int)$person['id']]))
: url('people');
?>
<a class="btn btn-ghost" href="<?= e($cancelUrl) ?>">取消</a>
<?php if ($isSelf): ?>
<a class="btn btn-ghost" href="<?= e(url('people/show', ['id' => (int)$person['id']])) ?>">看我的详情页</a>
<?php endif; ?>
</div>
</div>
</form>
<script>
(function () {
/* ---- 头像:点图即选文件(用户 2026-10-03 第 6 条)----
⚠️ 2026-10-05 起这段**已搬到 app.js 的通用绑定**(模板上写
`data-pick-input="#avatar"` 即可)。搬走的原因:只写在"头像"这一页,
别的页面照抄同一套 DOM 却没有 JS —— 题图/封面点了没反应。
这里不再保留副本(同一件事两处实现,改一处漏一处正是那次事故的成因)。 */
/* ---- 工作经历:+/× 增删一段(用户第 4 条)----
⚠️ 模板里的行是**服务端渲染**的(首屏没有 JS 也能填、也能提交),
这里只加"再加一段 / 删掉这段"两个交互 —— 渐进增强,不是二选一。 */
var list = document.getElementById('jobList');
var addBtn = document.getElementById('jobAdd');
function bindDel(row) {
var del = row.querySelector('.job-del');
if (!del) { return; }
del.addEventListener('click', function () {
var all = list.querySelectorAll('.job-row');
if (all.length <= 1) {
// 只剩一行时"删"= 清空这一行(而不是消失),否则页面会没有输入框可填
row.querySelectorAll('input').forEach(function (i) { i.value = ''; });
return;
}
row.remove();
});
}
if (list) {
Array.prototype.forEach.call(list.querySelectorAll('.job-row'), bindDel);
if (addBtn) {
addBtn.addEventListener('click', function () {
var first = list.querySelector('.job-row');
if (!first) { return; }
var row = first.cloneNode(true);
row.querySelectorAll('input').forEach(function (i) { i.value = ''; });
list.appendChild(row);
bindDel(row);
var f = row.querySelector('input');
if (f) { f.focus(); }
});
}
}
})();
</script>