Files
FamilyBook/templates/people/form.php
T
laoyang 52cc1ecccb 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 路由)。
2026-10-04 08:56:40 +08:00

500 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-has-avatar="<?= trim((string)($person['avatar'] ?? '')) !== '' ? '1' : '0' ?>">
<?php /* 真正吃文件的 input:视觉上藏起来,由上面的图片代它触发(label/JS 都行,
这里用 JS 点它,兼容性最好,也不依赖 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 条)----
真 input[type=file] 视觉上藏起来了(.sr-only),点图代它触发。
⚠️ 不要用 <label for> 包住图片来做这件事:一旦图被包进 label,
.avatar-picker 的 hover/光标语义会变,而且有的浏览器在 label 里
点 <img> 不会激活 file 控件(要额外 CSS 才能稳)。直接点 input 最省事。 */
var prev = document.getElementById('avatar-preview');
var file = document.getElementById('avatar');
if (prev && file) {
prev.addEventListener('click', function () { file.click(); });
prev.style.cursor = 'pointer';
}
/* ---- 工作经历:+/× 增删一段(用户第 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>