Files

719 lines
35 KiB
PHP
Raw Permalink 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
/**
* 成员详情 —— 同时是「以我为中心」的呈现页与亲属关系的录入入口。
*
* 页面上每个人名旁边都带着「我该怎么称呼他」,同一个页面
* 爸爸看到「我的儿子」,爱人看到「我的丈夫」,全靠 Kinship 现算。
*/
$age = age_from(\App\Birthday::solarOf($person));
$admin = \App\Auth::isAdmin();
$canW = \App\Auth::canWrite();
$pid = (int)$person['id'];
$p = $person;
$isSelf = (int)$p['id'] === (int)\App\Auth::personId();
$role = (string)($p['account_role'] ?? '');
// ⚠️ 显示名一律走「我怎么称呼他」,不要直接 echo 姓名:
// 同一个「小明」,妈妈看到的是"明明"、姐姐看到的是"小弟"。
// 这里一次性把「我设过的称呼」取出来做本地查表,避免每个节点一次 SQL。
$nickMap = $isSelf ? [] : \App\Nickname::mapFor((int)$meId);
$nameOf = function (int $id) use ($people, $pid, $nickMap): string {
$real = (string)($people[$id]['name'] ?? '(已移除)');
if ($id === $pid) {
return $real; // 我自己看到自己,永远用本名
}
$nk = (string)($nickMap[$id] ?? '');
return $nk !== '' ? $nk : $real;
};
// 当前这个人在我眼里叫什么(页面上所有"给他看的人名"都用它)
$pShown = $isSelf ? (string)$p['name'] : (string)($shown !== '' ? $shown : $p['name']);
$isMyNick = !$isSelf && $pShown !== (string)$p['name'];
// 当前这个人的父母/配偶/子女(用于在录入表单里显示已填的内容)
$theirParents = \App\Kinship::parents($graph, $pid);
$theirSpouses = \App\Kinship::spouseIds($graph, $pid);
$theirKids = \App\Kinship::childIds($graph, $pid);
$theirSibs = \App\Kinship::siblingIds($graph, $pid);
$shortKind = \App\Birthday::KINDS;
?>
<div class="page-head">
<h1><?= e($pShown) ?></h1>
<span class="sub">
<?php if (!$isSelf && (string)$labelToMe !== ''): ?>
<?php /* v24:没关系就不显示标签(Kinship::label() 推不出关系时返回空串) */ ?>
<span class="rel-label"><?= e($labelToMe) ?></span>
<?php if ($isMyNick): ?>
· 本名 <?= e((string)$p['name']) ?>
<?php endif; ?>
<?php endif; ?>
<?php if (!empty($p['is_placeholder'])): ?><span class="badge">虚拟人物</span><?php endif; ?>
</span>
<div class="actions">
<?php if ($isSelf || $canW): ?>
<a class="btn btn-ghost btn-sm" href="<?= e(url('people/edit', ['id' => $pid])) ?>">
<?= $isSelf ? '编辑我的资料' : '编辑资料' ?>
</a>
<?php endif; ?>
<?php /* 用户 2026-10-03 第 2 条:右上角那个「移除」换成**返回**(回名册列表)。
理由:详情页最常做的事是"看完回去",而"移除"是低频且危险的破坏性操作,
不该占着右上角最顺手的位置。
「删除」已移到页面最下方、放在「开通登录账号」下面(见文件末尾)。 */ ?>
<a class="btn btn-ghost btn-sm" href="<?= e(url('people')) ?>">返回</a>
</div>
</div>
<?php if ($isSelf && !empty($p['slogan'])): ?>
<p class="motto-line">“<?= e((string)$p['slogan']) ?>”</p>
<?php endif; ?>
<?php /* 用户 2026-10-04 第 11 批 #3 原话:「名册详情页面,需要修改。现在左侧是一个头像,
下面都是空白的,太浪费空间了。应该这样,左上角是头像,头像右侧开始排放姓名什么的基本信息,
然后就是其他信息开始往下顺序排放。」
⚠️ 旧布局是 `.detail { grid-template-columns: 260px 1fr }` 两栏:
左栏(头像卡)只有 ~330px 高,右栏好几张卡片 → **头像下面空出一大片**。
现在改成:
· 顶部一张「名片卡」:头像(左上)+ 右侧姓名/称呼/性别/账号身份等基本信息;
· 名片卡**下方**,其余各块全宽依次往下排。
这样无论信息多少都不会留白。 */ ?>
<div class="pcard">
<div class="pcard-avatar">
<img src="<?= e(avatar_url($p['avatar'])) ?>" alt="">
</div>
<div class="pcard-main">
<div class="pcard-name">
<?= e($pShown) ?>
<?php if ($isMyNick): ?><span class="pcard-real">本名 <?= e((string)$p['name']) ?></span><?php endif; ?>
<?php if (!empty($p['is_placeholder'])): ?><span class="badge">虚拟人物</span><?php endif; ?>
</div>
<div class="pcard-meta">
<?php if (!$isSelf && (string)$labelToMe !== ''): ?>
<span class="rel-label"><?= e($labelToMe) ?></span>
<?php endif; ?>
<span class="pcard-chip">
<?php if ($role === 'admin' && $p['account_status'] === 'active'): ?>
<b>家庭管理员</b>
<?php elseif ($role === 'guest'): ?>
访客(只读)
<?php elseif (!empty($p['account_id'])): ?>
已开通登录<?= $p['account_status'] === 'disabled' ? '(已停用)' : '' ?>
<?php else: ?>
还没有开通登录
<?php endif; ?>
</span>
<span class="pcard-chip"><?= e(\App\Person::genderWord((string)($p['gender'] ?? '')) ?: '性别未填') ?></span>
<?php if (!empty($p['birth_rank'])): ?>
<span class="pcard-chip">第 <?= (int)$p['birth_rank'] ?> 个孩子</span>
<?php endif; ?>
<?php if ($age !== null): ?>
<span class="pcard-chip"><?= (int)$age ?> 岁</span>
<?php endif; ?>
</div>
</div>
</div>
<div class="detail-stack">
<div>
<div class="card card-pad">
<dl class="kv">
<div class="kv-row"><dt>姓名</dt><dd><?= e((string)$p['name']) ?></dd></div>
<div class="kv-row">
<dt>性别</dt>
<dd><?= e(\App\Person::genderWord((string)($p['gender'] ?? '')) ?: '未填写') ?></dd>
</div>
<?php if (!empty($p['birth_rank'])): ?>
<div class="kv-row"><dt>家中排行</dt><dd>第 <?= (int)$p['birth_rank'] ?> 个孩子</dd></div>
<?php endif; ?>
<!-- ============ 三个生日 ============ -->
<div class="kv-row">
<dt>生日</dt>
<dd>
<?php if (!$birthdays): ?>
<span class="muted">未填写</span>
<?php else: ?>
<ul class="bd-list">
<?php foreach ($birthdays as $b): ?>
<li>
<span class="bd-kind bd-kind-<?= e((string)$b['kind']) ?>">
<?= e($shortKind[$b['kind']] ?? (string)$b['kind']) ?>
</span>
<span class="bd-val"><?= e((string)$b['text']) ?></span>
<?php if ((string)$b['note'] !== ''): ?>
<span class="muted small">(<?= e((string)$b['note']) ?>)</span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php if ($age !== null): ?>
<div class="muted small" style="margin-top:4px">今年 <?= (int)$age ?> 岁</div>
<?php endif; ?>
<?php endif; ?>
</dd>
</div>
<?php if ($birthdayNext): ?>
<div class="kv-row">
<dt>接下来</dt>
<dd>
<?php foreach ($birthdayNext as $n): ?>
<?php
$words = [];
foreach ((array)$n['kinds'] as $k) {
$words[] = $shortKind[$k] ?? (string)$k;
}
?>
<div>
<?= e((string)$n['date']) ?> ·
<?php if ((int)$n['days'] === 0): ?>
<b style="color:var(--brand)">今天</b>
<?php else: ?>
还有 <b><?= (int)$n['days'] ?></b> 天
<?php endif; ?>
<span class="muted small">
(<?= e(implode('、', $words)) ?><?= (string)$n['lunar_label'] !== '' ? ',农历' . e((string)$n['lunar_label']) : '' ?>)
</span>
</div>
<?php endforeach; ?>
</dd>
</div>
<?php endif; ?>
<div class="kv-row">
<dt>手机号</dt>
<dd class="mono"><?= e(\App\Crypto::maskPhone((string)($p['phone_encrypted'] ?? ''))) ?></dd>
</div>
<?php foreach (($contacts['phone'] ?? []) as $r): ?>
<div class="kv-row">
<dt>电话<?= (string)$r['label'] !== '' ? '(' . e((string)$r['label']) . ')' : '' ?></dt>
<dd class="mono"><?= e(\App\Crypto::maskPhone((string)$r['value'])) ?></dd>
</div>
<?php endforeach; ?>
<div class="kv-row">
<dt>身份证号</dt>
<dd class="mono"><?= e(\App\Crypto::maskIdCard((string)($p['id_card_encrypted'] ?? ''))) ?></dd>
</div>
<?php if (!empty($p['email'])): ?>
<div class="kv-row"><dt>邮箱</dt><dd><?= e($p['email']) ?></dd></div>
<?php endif; ?>
<?php foreach (($contacts['email'] ?? []) as $r): ?>
<div class="kv-row">
<dt>邮箱<?= (string)$r['label'] !== '' ? '(' . e((string)$r['label']) . ')' : '' ?></dt>
<dd><?= e((string)$r['value']) ?></dd>
</div>
<?php endforeach; ?>
<?php /* 用户 2026-10-03 第 4 条:工作单位/职务/职称**可以有多段** —— 所以这里列出
完整的"工作经历"(当前那段标「现在」),而不是只显示一个"当前单位"。
⚠️ 老数据(只有 persons.org/title、没有经历行)也要照常显示:
那样的情况下退回显示原来那一行(见下面的 $jobs 为空分支)。 */ ?>
<?php if ($jobs): ?>
<div class="kv-row">
<dt>工作</dt>
<dd>
<ul class="job-history">
<?php foreach ($jobs as $j): ?>
<?php $isNow = ($j['end_ym'] === null || $j['end_ym'] === ''); ?>
<li class="job-history-item">
<?php if ($isNow): ?><span class="node-badge">现在</span><?php endif; ?>
<span class="job-org"><?= e((string)($j['org'] ?? '')) ?></span>
<?php $jt = \App\PersonJob::joinTitle($j['title'] ?? '', $j['rank_name'] ?? ''); ?>
<?php if ($jt !== null): ?><span class="job-title"><?= e($jt) ?></span><?php endif; ?>
<?php if (($j['start_ym'] ?? '') !== '' || ($j['end_ym'] ?? '') !== ''): ?>
<span class="small muted job-span">
<?= e((string)($j['start_ym'] ?? '')) ?> ~ <?= $isNow ? '现在' : e((string)($j['end_ym'] ?? '')) ?>
</span>
<?php endif; ?>
<?php if (($j['note'] ?? '') !== ''): ?>
<span class="small muted">(<?= e((string)$j['note']) ?>)</span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</dd>
</div>
<?php elseif (!empty($p['org']) || !empty($p['title'])): ?>
<div class="kv-row">
<dt>工作</dt>
<dd><?= e(trim((string)$p['org'] . (($p['org'] ?? '') !== '' && ($p['title'] ?? '') !== '' ? ' · ' : '') . (string)$p['title'])) ?></dd>
</div>
<?php endif; ?>
<?php /* 用户 2026-10-04 第 4 条:地址是**结构化的**五级。
显示时把「国家 + 省 + 市 + 区」拼成一行前缀、详细地址接在后面;
一级都没填就退化成原来那一整串(老数据/只填了详细地址的情况)。
⚠️ 没有详细地址时**只显示到区**(不再跟着一个空的「·」)。 */ ?>
<?php
$addrHead = implode(' ', array_filter([
(string)($p['addr_country'] ?? ''),
(string)($p['addr_province'] ?? ''),
(string)($p['addr_city'] ?? ''),
(string)($p['addr_district'] ?? ''),
], static fn($s) => trim($s) !== ''));
$addrTail = trim((string)($p['address'] ?? ''));
$addrFull = $addrHead !== '' ? $addrHead . ($addrTail !== '' ? ' ' . $addrTail : '') : $addrTail;
?>
<?php if ($addrFull !== ''): ?>
<div class="kv-row"><dt>住址</dt><dd><?= e($addrFull) ?></dd></div>
<?php endif; ?>
<?php foreach (($contacts['address'] ?? []) as $r): ?>
<div class="kv-row">
<dt>地址<?= (string)$r['label'] !== '' ? '(' . e((string)$r['label']) . ')' : '' ?></dt>
<dd><?= e((string)$r['value']) ?></dd>
</div>
<?php endforeach; ?>
<?php foreach (($contacts['url'] ?? []) as $r): ?>
<div class="kv-row">
<dt>网址<?= (string)$r['label'] !== '' ? '(' . e((string)$r['label']) . ')' : '' ?></dt>
<dd class="mono"><?= e((string)$r['value']) ?></dd>
</div>
<?php endforeach; ?>
<?php /* 用户 2026-10-03 第 5 条:备注分两种 ——
① 他本人写的「自我介绍」(谁都能看,就是他愿意公开说的那句);
② 我对他的私人备注(**只有我自己看得到**,见下面单独一块)。 */ ?>
<?php if (!empty($p['note'])): ?>
<div class="kv-row"><dt>自我介绍</dt><dd><?= nl2br(e($p['note'])) ?></dd></div>
<?php endif; ?>
<div class="kv-row">
<dt>最近登录</dt>
<dd><?= !empty($p['last_login_at']) ? e($p['last_login_at']) : '从未登录' ?></dd>
</div>
</dl>
</div>
<?php /* ============ 我对他的私人备注(用户 2026-10-03 第 5 条)============
只有**写的人自己**看得到(存在 person_notes,按 viewer 区分)。
⚠️ 不是 person.note —— 那是"他愿意公开说的自我介绍"。
这一块属于"我和他之间的事"(怎么认识的、要注意什么),
所以它只出现在我的视角里,而且别人(包括他本人)看不到。 */ ?>
<?php if (!$isSelf): ?>
<div class="card card-pad my-note-card" style="margin-top:16px">
<h2 class="section-title">
我对 <?= e($pShown) ?> 的备注
<span class="hint">只有我自己看得到</span>
</h2>
<form method="post" action="<?= e(url('people/note')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $pid ?>">
<label class="sr-only" for="pn-note">我对他的备注</label>
<textarea class="input" id="pn-note" name="note" rows="2"
placeholder="怎么认识的、要注意什么、上次见面聊了什么……"><?= e((string)$myNote) ?></textarea>
<div class="btn-row" style="margin-top:8px">
<button class="btn btn-sm" type="submit">保存备注</button>
<span class="small muted">别人看不到这一栏(包括他本人)</span>
</div>
</form>
</div>
<?php endif; ?>
<!-- ============ 我叫他/她什么(视角属性,只有我自己能改) ============ -->
<?php if (!$isSelf): ?>
<div class="card card-pad" style="margin-top:16px">
<h2 class="section-title">
我怎么称呼 <?= e((string)$p['name']) ?>
<span class="hint">只影响我自己看到的</span>
</h2>
<form method="post" action="<?= e(url('nickname/save')) ?>" class="nick-inline">
<?= csrf_field() ?>
<input type="hidden" name="target_id" value="<?= $pid ?>">
<input type="hidden" name="back" value="people/show">
<input class="input" name="nickname" maxlength="50"
value="<?= e($myNickname) ?>" placeholder="留空 = 恢复成用姓名显示">
<button class="btn btn-sm" type="submit">记下</button>
</form>
<p class="small muted" style="margin:10px 0 0">
同一个「小明」,妈妈叫"明明"、姐姐叫"小弟" —— 称呼是<b>每个人各自定的</b>,
不是 TA 自带的属性。所以这里改的只影响你自己的页面,别人看到的还是他们自己的叫法。
</p>
</div>
<?php endif; ?>
<!-- ============ 亲属关系 ============ -->
<div class="card card-pad" style="margin-top:16px">
<h2 class="section-title">
<?= e($pShown) ?> 的亲属
<span class="hint">关系是双向的,填一次两边都生效</span>
</h2>
<?php
// 已有关系一览(从「当前这个人」的视角)
$blocks = [
['父母', $theirParents['father'] ? [[$theirParents['father'], '父亲']] : [],
$theirParents['mother'] ? [[$theirParents['mother'], '母亲']] : []],
];
?>
<div class="rel-summary">
<div class="rel-sum-item">
<span class="rel-sum-k">父亲</span>
<span class="rel-sum-v">
<?php if ($theirParents['father']): ?>
<a href="<?= e(url('people/show', ['id' => $theirParents['father']])) ?>"><?= e($nameOf($theirParents['father'])) ?></a>
<?php else: ?><span class="muted">未填写</span><?php endif; ?>
</span>
</div>
<div class="rel-sum-item">
<span class="rel-sum-k">母亲</span>
<span class="rel-sum-v">
<?php if ($theirParents['mother']): ?>
<a href="<?= e(url('people/show', ['id' => $theirParents['mother']])) ?>"><?= e($nameOf($theirParents['mother'])) ?></a>
<?php else: ?><span class="muted">未填写</span><?php endif; ?>
</span>
</div>
<div class="rel-sum-item">
<span class="rel-sum-k">配偶</span>
<span class="rel-sum-v">
<?php if ($theirSpouses): ?>
<?php foreach ($theirSpouses as $s): ?>
<a href="<?= e(url('people/show', ['id' => $s])) ?>"><?= e($nameOf($s)) ?></a>
<?php endforeach; ?>
<?php else: ?><span class="muted">未填写</span><?php endif; ?>
</span>
</div>
<div class="rel-sum-item">
<span class="rel-sum-k">子女</span>
<span class="rel-sum-v">
<?php if ($theirKids): ?>
<?php foreach ($theirKids as $c): ?>
<a href="<?= e(url('people/show', ['id' => $c])) ?>"><?= e($nameOf($c)) ?></a>
<?php $rk = $people[$c]['birth_rank'] ?? null; ?>
<?php if ($rk): ?><span class="muted small">(第<?= (int)$rk ?>)</span><?php endif; ?>
<?php endforeach; ?>
<?php else: ?><span class="muted">未填写</span><?php endif; ?>
</span>
</div>
<div class="rel-sum-item">
<span class="rel-sum-k">兄弟姐妹</span>
<span class="rel-sum-v">
<?php if ($theirSibs): ?>
<?php foreach ($theirSibs as $s): ?>
<a href="<?= e(url('people/show', ['id' => $s])) ?>"><?= e($nameOf($s)) ?></a>
<?php endforeach; ?>
<span class="muted small">(由共同父母自动认出)</span>
<?php else: ?><span class="muted">暂无</span><?php endif; ?>
</span>
</div>
</div>
<?php if ($canW): ?>
<div class="rel-add">
<h3 class="rel-add-title">添加/修改关系</h3>
<form method="post" action="<?= e(url('kin/relate')) ?>" class="rel-form">
<?= csrf_field() ?>
<input type="hidden" name="from_id" value="<?= $pid ?>">
<div class="field">
<label>关系</label>
<select class="select" name="kind" id="relKind">
<option value="parent">是我的父母</option>
<option value="child">是我的子女</option>
<option value="spouse">是我的配偶</option>
</select>
</div>
<div class="field" id="relRoleWrap">
<label id="relRoleLabel">是哪一位</label>
<select class="select" name="role">
<option value="father">父亲</option>
<option value="mother">母亲</option>
</select>
</div>
<div class="field">
<?php /* 用户 2026-10-03 第 8 条:「添加关系那里,选择一个人,应该也是和观影人一样,
点击弹窗,用拼音首字母选择,否则人多的时候,这个选择起来太麻烦了。」
—— 原来是个 <select>,名册一长就得在几百个选项里瞪眼找。
换成全局选人组件(与观影人同一套:弹窗 + 头像 + 首字母联想)。
⚠️ 这里**不能**写 `required`:组件在无 JS 时退化成一组原生 radio,
「必选一个 radio」HTML 没有这个约束(radio 组的 required 也不可靠)。
改成提交时用 JS 拦一下,把"没选人"挡在客户端;
万一真漏过去,服务端 kin/relate 也会判「选的人不对」并给人话提示。 */ ?>
<label>对方</label>
<?php
$ppName = 'to_id';
$ppPeople = $options;
$ppSelected = []; // 每次进页面都不预选:这是"新记一条关系",不是改旧的
$ppMode = 'single';
$ppOpenBtn = '选择对方';
$ppTitle = '对方是谁';
$ppEmptyText = '还没有选人';
$ppAllowEmpty = false;
require __DIR__ . '/../_person_picker.php';
?>
</div>
<div class="field" style="justify-content:flex-end">
<button class="btn btn-sm" type="submit">记下这条关系</button>
</div>
</form>
<p class="small muted" style="margin:12px 0 0">
找不到人?先到「添加成员」把人建出来 —— 不确定真实姓名时,
起个「我的父亲」这样的名字也可以,之后再改。
</p>
</div>
<?php if ($theirRels): ?>
<details class="rel-list">
<summary>已有关系明细(<?= count($theirRels) ?> 条)</summary>
<ul>
<?php foreach ($theirRels as $r): ?>
<li>
<?php if ($r['kind'] === 'parent'): ?>
<?= e($nameOf((int)$r['from_id'])) ?>
(<?= $r['from_role'] === 'mother' ? '母亲' : '父亲' ?>)
→
<?= e($nameOf((int)$r['to_id'])) ?>
<?php else: ?>
<?= e($nameOf((int)$r['from_id'])) ?> 与 <?= e($nameOf((int)$r['to_id'])) ?> 是配偶
<?php endif; ?>
<form method="post" action="<?= e(url('kin/unrelate')) ?>" style="display:inline">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
<input type="hidden" name="person_id" value="<?= $pid ?>">
<button class="btn btn-danger btn-sm" type="submit">解除</button>
</form>
</li>
<?php endforeach; ?>
</ul>
</details>
<?php endif; ?>
<?php else: ?>
<p class="small muted" style="margin:12px 0 0">你的身份是访客,只能查看亲属关系,不能修改。</p>
<?php endif; ?>
</div>
<!-- ============ 以这个人看出去的家谱 ============ -->
<?php if (!empty($theirTree['nodes']) && count($theirTree['nodes']) > 1): ?>
<div class="card card-pad" style="margin-top:16px">
<h2 class="section-title">从 <?= e($pShown) ?> 的角度看 <span class="hint">上下各两代</span></h2>
<div class="mini-tree">
<?php foreach ($theirTree['generations'] as $level => $ids): ?>
<div class="mini-gen">
<?php foreach ($ids as $id): $n = $theirTree['nodes'][$id]; ?>
<a class="mini-node <?= (int)$id === $pid ? 'is-me' : '' ?>"
href="<?= e(url('people/show', ['id' => (int)$id])) ?>">
<img src="<?= e(avatar_url($n['avatar'])) ?>" alt="">
<span class="mn-name"><?= e($nameOf((int)$id)) ?></span>
<span class="mn-label"><?= e($n['label']) ?></span>
</a>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- ============ 名册归属 / 分享名片 ============ -->
<?php if (!$isSelf): ?>
<div class="card card-pad" style="margin-top:16px">
<h2 class="section-title">名册 <span class="hint">名册是每人一份的</span></h2>
<p class="small muted" style="margin:0 0 14px">
<?php if (!empty($inMyRoster)): ?>
「<?= e($pShown) ?>」在你的名册里。归到哪一组、排第几,只影响你自己看到的名册 ——
家里其他人各有一份,互不打搅。
<?php else: ?>
「<?= e($pShown) ?>」还不在你的名册里。你仍然能在家谱与首页看到他,
但名册只列<b>你自己加的人</b>和<b>别人分享给你的名片</b>。
<?php endif; ?>
</p>
<div class="btn-row" style="margin-top:0;flex-wrap:wrap;align-items:center">
<?php if (empty($inMyRoster) && $canW): ?>
<form method="post" action="<?= e(url('people/pick')) ?>" style="display:inline">
<?= csrf_field() ?>
<input type="hidden" name="ids[]" value="<?= $pid ?>">
<button class="btn btn-sm" type="submit">加进我的名册</button>
</form>
<?php elseif (!empty($inMyRoster) && $canW): ?>
<form method="post" action="<?= e(url('people/forget')) ?>" style="display:inline">
<?= csrf_field() ?>
<input type="hidden" name="person_id" value="<?= $pid ?>">
<button class="btn btn-ghost btn-sm" type="submit">从我的名册里移走</button>
</form>
<?php endif; ?>
<?php if ($canW && !empty($options)): ?>
<?php /* 需求原话:「我可以把一个人的名片,分享给一个用户,
则这个名片,自动出现在这个用户的名册中」。 */ ?>
<details class="share-wrap">
<summary class="btn btn-ghost btn-sm">分享他的名片给…</summary>
<form method="post" action="<?= e(url('people/share')) ?>" class="inline-form">
<?= csrf_field() ?>
<input type="hidden" name="person_id" value="<?= $pid ?>">
<input type="hidden" name="back" value="<?= $pid ?>">
<?php /* 与「添加关系」同一套:既然这一页已经在用弹窗选人了,
就别让分享还留一个上百项的下拉(同页两种手感更别扭)。 */ ?>
<div class="share-to">
<?php
$ppName = 'to_id';
$ppPeople = $options;
$ppSelected = [];
$ppMode = 'single';
$ppOpenBtn = '选择分享给谁';
$ppTitle = '把他的名片分享给谁';
$ppEmptyText = '还没有选人';
require __DIR__ . '/../_person_picker.php';
?>
</div>
<button class="btn btn-sm" type="submit">分享</button>
</form>
</details>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php if ($admin): ?>
<div class="card card-pad" style="margin-top:16px">
<h2 class="section-title">登录账号</h2>
<?php if (empty($p['account_id'])): ?>
<p class="small muted" style="margin:0 0 14px">
开通后,<?= e($p['name']) ?> 就能用手机号和密码登录岁月家了。
</p>
<form method="post" action="<?= e(url('accounts/create')) ?>" class="form-grid">
<?= csrf_field() ?>
<input type="hidden" name="person_id" value="<?= $pid ?>">
<div class="field">
<label>登录密码</label>
<input class="input" name="password" type="password" placeholder="至少 8 位,含字母和数字" required>
</div>
<div class="field">
<label>角色</label>
<select class="select" name="role">
<option value="member">家庭成员(可创建与编辑)</option>
<option value="admin">管理员(可改系统设置)</option>
<option value="guest">访客(只能查看)</option>
</select>
</div>
<div class="field full">
<button class="btn btn-sm" type="submit">开通账号</button>
</div>
</form>
<?php else: ?>
<p class="small muted" style="margin:0 0 14px">
账号已开通,登录名是 <?= e($p['name']) ?> 的手机号。
当前角色:<b><?= e(\App\Account::roleWord($role)) ?></b>
<?= $role === 'guest' ? '—— 只能查看,不能新增或修改任何内容。' : '' ?>
</p>
<div class="btn-row" style="margin-top:0;flex-wrap:wrap">
<form method="post" action="<?= e(url('accounts/role')) ?>" style="display:inline">
<?= csrf_field() ?>
<input type="hidden" name="person_id" value="<?= $pid ?>">
<select class="select" name="role" style="width:auto;display:inline-block">
<option value="member" <?= $role === 'member' ? 'selected' : '' ?>>家庭成员</option>
<option value="admin" <?= $role === 'admin' ? 'selected' : '' ?>>管理员</option>
<option value="guest" <?= $role === 'guest' ? 'selected' : '' ?>>访客</option>
</select>
<button class="btn btn-ghost btn-sm" type="submit">改角色</button>
<?php if ($isSelf): ?>
<?php /* 自己也能改自己的权限(哪怕给自己降级)—— 系统的家可能就一个人用,
难道他还不能调自己的权限?唯一拦的是"降完一个管理员都不剩"。 */ ?>
<span class="small muted">可以改自己的。只有「把最后一位管理员降级」会被拦下。</span>
<?php endif; ?>
</form>
<?php if (!$isSelf): ?>
<form method="post" action="<?= e(url('accounts/status')) ?>" style="display:inline">
<?= csrf_field() ?>
<input type="hidden" name="person_id" value="<?= $pid ?>">
<input type="hidden" name="status" value="<?= $p['account_status'] === 'active' ? 'disabled' : 'active' ?>">
<button class="btn btn-ghost btn-sm" type="submit">
<?= $p['account_status'] === 'active' ? '停用账号' : '启用账号' ?>
</button>
</form>
<?php endif; ?>
</div>
<?php endif; ?>
<?php /* ==================== 危险操作:删除 ====================
用户 2026-10-05 第 12 批 #1 原话:「名册详情页面,删除按钮,就放在
开通账号按钮下面,紧挨着,长度大小,和开通账号一样,就是颜色改成红色。」
→ 上一版把它放在**页面最下方的独立区块**(danger-zone),用户要的是
**紧挨着「开通账号」**。所以搬进这张「登录账号」卡片里、放在同一列,
尺寸与「开通账号」完全一致(同 .btn 家族 + 100% 宽),只把颜色换成红。
破坏性操作仍保留两步确认(第一次点露出「确认删除/取消」)。 */ ?>
<?php if ($admin): ?>
<div class="del-inline">
<p class="del-inline-tip">
删除会把他从名册中移除,<b>并且</b>删掉登录账号与全部亲属关系连线,<b>不可恢复</b>。
若只想让他从<b>你的</b>名册里消失,用名册列表的「⋯ → 从我的名册移走」。
</p>
<button type="button" class="btn btn-danger btn-block-lg" id="delBigBtn">
删除 <?= e((string)$p['name']) ?>
</button>
<form method="post" action="<?= e(url('people/destroy')) ?>" id="delBigForm" hidden>
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $pid ?>">
</form>
<div class="del-big-confirm" id="delBigConfirm" hidden>
<span>确定删除?此操作无法撤销。</span>
<button class="btn btn-danger" type="button" id="delBigYes">确认删除</button>
<button class="btn btn-ghost" type="button" id="delBigNo">取消</button>
</div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div><?php /* /detail-stack —— 名片卡下方的全宽堆叠到此结束 */ ?>
<script>
/* 大删除按钮的两步确认(用户第 11 批 #2):第一次点露出"确认删除/取消",
第二次点才真的提交。比原生 confirm 可控,也与页面其它确认长得一致。 */
(function () {
var btn = document.getElementById('delBigBtn');
var box = document.getElementById('delBigConfirm');
var form = document.getElementById('delBigForm');
if (!btn || !box || !form) { return; }
btn.addEventListener('click', function () {
box.hidden = false;
btn.hidden = true;
});
var no = document.getElementById('delBigNo');
if (no) {
no.addEventListener('click', function () {
box.hidden = true;
btn.hidden = false;
});
}
var yes = document.getElementById('delBigYes');
if (yes) {
yes.addEventListener('click', function () {
yes.disabled = true;
yes.textContent = '正在删除…';
form.submit();
});
}
})();
// 关系表单的「是哪一位」下拉有**两种含义**:
// kind=parent → 对方是父亲/母亲(填的是对方)
// kind=child → 我是孩子的父亲/母亲(填的是自己)
// 所以标签必须跟着换,否则用户会以为在填对方的身份,把关系记反。
(function () {
var kind = document.getElementById('relKind');
var wrap = document.getElementById('relRoleWrap');
var lab = document.getElementById('relRoleLabel');
if (!kind || !wrap || !lab) return;
function sync() {
if (kind.value === 'spouse') {
wrap.style.display = 'none';
return;
}
wrap.style.display = '';
lab.textContent = kind.value === 'child' ? '我是孩子的' : '对方是我的';
}
kind.addEventListener('change', sync);
sync();
})();
// 「添加关系」的「对方」换成了弹窗选人组件(第 8 条),它没有原生 required ——
// 这里补一道客户端拦截,别让用户点了「记下这条关系」却什么都没发生(服务端会拒,
// 但来回一趟还看不到错在哪)。选人组件里勾中的那个 radio 才是真正的值。
(function () {
var form = document.querySelector('.rel-form');
if (!form) return;
form.addEventListener('submit', function (ev) {
var picked = form.querySelector('.pp-list input[name="to_id"]:checked');
if (!picked || picked.value === '0') {
ev.preventDefault();
alert('还没选「对方」——点上面的按钮,在弹出的列表里选一个人。');
}
});
})();
</script>