Files
FamilyBook/templates/settings.php
T
laoyang 8d79857f1a v54:首页任务标题截断(真根因=两列网格被撑破)/ 名册分组→标签且候选读词表 / 旅程地图视图 / 旅程设置按钮 / 显示方式默认列表或地图
用户 2026-10-16 傍晚五条。

① 首页「近期任务」标题太长会挤成两行
   用户原话:「首页**近期任务**那个地方要注意截断:如果任务标题太长,
   会把这个任务**挤成两行**。」
   🔴🔴 **真根因是两层,第一版只修了表面**:
     · 表面:`.dash-what` 只有 `flex:1 1 auto; min-width:0`,**缺** nowrap/hidden/ellipsis
       ⇒ 收缩后文字在框内换行;
     · 真身:`.home-duo` 是**两列 grid**,grid 子项默认 `min-width: auto`
       ("我最小也得装下我的内容")⇒ 一条长标题把那一列顶到 **1763px**
       (父 `home-duo` 才 1032px)⇒ **整个两列布局被撑破**、页面横向溢出到 **2089px**
       (视口 1440)。⇒ 行宽 1721px、`scrollWidth === clientWidth`,
       **`text-overflow: ellipsis` 看起来"没生效"** —— 其实容器自己先爆了。
   修法:`.home-duo > * { min-width: 0; }`,且 **整条 flex 链**都补
     (`.hd-pane` → `.hd-pane-head` → `.hd-pane-body` → `.dash-list/.tk-list` → `.tk-row`)
     —— 链上任何一层漏了,收缩就断在那一层、下面的 nowrap 全白写。
   配套:任务标题加 `title=` 悬停看全。
   ⚠️ 这是本项目**第二次**踩 flex/grid 的 `min-width:auto`(v53 的 `.mv-grid` 是同类)。
      **以后"文字截断不生效"先量容器有没有被撑破**。

② 名册「分组」→「标签」;打标签的候选要**从设置里读**
   用户原话:「现在名册里面给人打标签的时候,**这个标签数据是从哪读出来的**呀?
   在这个名册的设置页面,有很多分组,现在还叫分组。但是打标签的时候,
   **却不是从这些分组里读出来的**。……应该叫「名册标签」,是不是?
   **应该从这里面把所有标签都读出来**吧?……以后就没有分组的概念了,
   **全部都是标签的概念**。」
   🔴 **用户观察得完全正确,这是两套数据源**:
     · 设置页显示的词表来自 `person_groups`(**分组表**);
     · 打标签的候选来自 `PersonTag::cloud()`(**你已经打过的词**,现算的);
     ⇒ 两边根本不是一个数据源 ⇒ "设置里列着一堆、打标签时却只有几个"。
   修法:**`PersonTag::choices()` = 设置页词表 ∪ 我用过的词**(并集,词表在前)。
     · 词表里的词**还没用过**也能一点就打上(这正是词表的意义);
     · 用过的词补在后面(历史不丢);
     · 名册列表 / 新增 / 编辑 / 挑人 / 导入 **五处同一个函数**。
   界面口径全面改"标签"(**表名仍是 `person_groups`**:改名要背着 `PersonGroup` 类、
   `person_roster.group_id`、以及**另一个会话正在写的 `app/ApiHandler.php`**
   (它引用了 `PersonGroup::orderFor`)一起动,风险远大于收益):
     · `_person_groups.php` 标题「名册标签」、按钮「加一个我自己的标签」;
     · 删除文案改成"只是从候选里去掉这个词,已打在别人身上的不受影响"
       (**语义真的变了**:标签是各自身上的字符串,不是引用);
     · `people/_settings_menu.php`、`people/settings.php`、`settings/me.php`、
       `settings.php`(「公共分组」→「公共标签」)全部同步。
   顺带按"全部都是标签"改另两条入口:
     · `people/pick.php`「加进哪一组」下拉 → **打标签**;
     · `people/import.php` 同上;`ContactImporter::run()` 加 `$tags` 参数。
   🔴 新增 `PersonTag::addOne()`(**追加**一个标签)—— 这两处**绝不能用 `setFor()`**
      (它是"先 DELETE 再 INSERT"的整组替换)⇒ 会把人**原有**的标签清光
      (症状是"用一次导入,别人原有的标签全没了",极难联想)。
   ⚠️ `group_id` 在 pick/import 的**服务端仍收着**(老书签/脚本兼容),只是界面不再展示。

③ 旅程模块加**地图视图**(读 TripLog 移植)
   用户原话:「你去读一下 TripLog 的代码,在旅程这个模块,**可以在地图上显示旅程数据**。
   1. 先读一下它之前那个代码是怎么实现的,**在我们的旅程模块也实现一下**。」
   读 TripLog(`templates/activities/index.php`)得到的要点:
     · 用**高德 AMap**(合规白名单内,绝不是境外瓦片);
     · `position` 必须是 **[lng,lat] 数组**(传 `{lng,lat}` 裸对象会让 AMap 抛错、
       整个绘制循环当场中断 —— 它注释里写着"批 11 的光秃秃地图就是它");
     · `setFitView()` 后**上限 11 级**(只有一趟时会被放大到街道级,那是导航视角);
     · `.amap-marker` 整层 `pointer-events:none`(否则包装层挡住别家卡片的点击);
     · **容器必须有固定高度**(0 高度 → 空白地图)。
     它那套"同城环形扇开"是**几百行几何**,本仓数据量没压出这个问题 ⇒ **有意不做**。
   本仓数据更规整:`trip_nodes` 早有 `from_lng/from_lat/to_lng/to_lat`(v17 批)
   + `from_city/to_city` ⇒ 新增 `Trip::attachSpots()`:
     ① 有坐标取**重心**(不是端点 —— 端点会让"青岛→伊春"偏到某一头);
     ② 没坐标退到**城市重心**(`Geo::lookup` 离线城市表,**不发网络请求**:
        列表页几十趟若逐条打高德逆地理,一次开页就是几十个外部请求);
     ③ 都没有就不上图(列表里照常有,模板会说清"有几趟没上图")。
     🔴 城市那里**必须也取重心**:`cities()` 的顺序是**按时间**(出发地在最前),
        取"第一个城市"会让**所有旅程都叠在家上**、地图毫无信息量。
   布局:两个视图用**标签页**(`?view=map` 是**链接**不是纯 JS 按钮 ——
     书签/前进后退/无 JS 都正确;也**有意不**把地图藏在 `display:none` 里,
     高德在 0 尺寸容器里会渲染空白)。
   高德 SDK 加载器重构:原来 `fbLoadAddr()` 顺带拉 `addr_suggest.js`,
   而地图只用 SDK 自带的 `AMap.Map/Marker` ⇒ 抽出 `window.fbAmapSdk()`(只拉 SDK)、
   新增 `fb-amap` meta(不带 plugin),`fb-addr` 那份共用同一加载器。

④ 旅程列表页「新建旅程」**后面**加「设置」按钮
   用户原话:「2. 在旅程的「新建旅程」后面也加一个「设置」按钮。」
   ⇒ 补 `_settings_menu.php`(设置恒排最右),菜单**第一条**加「显示方式」。

⑤ 显示方式:默认列表还是地图(用户自配)
   用户原话:「3. 里面的设置让用户自己配置:**到底默认显示旅程列表,还是用地图显示**。」
   ⇒ 新增 `trips/display`(设置页 + `?set=` 快速设置)与 `trips/display_save`,
     存 **`user_settings.trip_default_view`**(每人一份,**不是**全家设置)。
   ⚠️ 访客也能改(只影响自己看到的默认,不是数据变更)。

验证(新增 2 套件 + 更新 e2e/deploy 断言):
 · _fb_v54_ui.py(**33 条**真机断言)
 · _b54_falsify.py(**11 注入 / 11 全抓**,基线 33/0)
 · e2e **1518/0**;familybook_deploy.sh 加「第54批形态」段(**24 条**)
 · 部署 **0 FAIL**;线上 **20 个文件 md5 逐字节一致**;
   线上实测:词表 24 条、`choices()` 24 条、**`cloud` 15 条全部 ⊆ `choices`**、
   4 趟旅程全部可上图(样例落在济南·青岛重心);诊断页已删复验 404

⚠️ 本轮踩坑(已写进 pitfalls.md):
 · 🔴🔴 **"文字截断不生效"要先量容器有没有被撑破**(第一嫌疑是 flex/grid 子项
   `min-width:auto`,不是 `text-overflow` 写错);
 · **反向断言("不该再出现 X")必须用 `_code_has`(剥注释)** ——
   需求原话正当留在注释里,裸 `grep -F` 会命中自己的说明(本轮部署假红一次);
 · **冗余防线会让反向验证打不红**(两处 `min-width:0` 只去掉一条 ⇒ 探针照旧全绿,
   注入必须一起去掉);
 · **界面多加一个字符就可能打歪既有断言**(我在词表管理页给标签名加了 `#`,
   把 e2e 的 `my_group_order()` 读成 `#家人`)。
2026-10-17 00:15:06 +08:00

354 lines
17 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
/**
* 设置(仅管理员)。
*
* 与早期版本的两处不同,都是被明确要求改的:
* ① **「家庭名称」撤了**。首页标题已经是「某某某的家书」,一个家庭级名字
* 既没人看、又容易和它打架;后端 settings/update 也不再接收这个字段,
* 免得"某次提交把名字清空"。库里那一列留着不动(历史数据无害)。
* ② **slogan 变成"系统级默认"**。真正的 slogan 是每个人自己写的那句
* (在「我的资料」里),这里是**没人写自己的时候拿出来的兜底**。
* 另加:阿里云 OSS 的**连通性测试**——逐步摊开结论,
* 让"连不上"有具体原因,而不是一句"写入失败"。
*/
?>
<form method="post" action="<?= e(url('settings/update')) ?>" class="card card-pad">
<?= csrf_field() ?>
<h2 class="section-title">家庭 <span class="hint">首页上那一行小字</span></h2>
<div class="form-grid">
<div class="field full">
<label for="motto">系统 slogan</label>
<input class="input" id="motto" name="motto" value="<?= e($family['motto'] ?? '') ?>"
placeholder="例如:一本用岁月写成的家谱">
<span class="tip">
每个人都可以在「我的资料」里写**自己的一句**,有的话就显示他自己的;
<b>没写的人才用这里的这句</b>。所以这是一句"兜底",不是全家的统一口号。
</span>
</div>
<?php /* ⚠️ 天气的坐标输入框**从这里撤了**(用户 2026-10-03 第 1 条)。
原话是「让用户在个人设置中填写自己的城市」——
地点是**每个人自己**的事(一家人可能分处两地),而且应该是"填城市名",
不该让用户去查经纬度。现在在「个人设置 → 天气 · 我所在的城市」里。 */ ?>
</div>
<div class="btn-row">
<button class="btn" type="submit">保存</button>
</div>
</form>
<?php if (($upgradeWarn ?? '') !== ''): ?>
<div class="flash" style="margin-top:16px">
<div class="flash-item err">
<b>上次结构升级有步骤失败了</b>(其余步骤已经跑完,站点可用):<br>
<?= e((string)$upgradeWarn) ?><br>
<span class="muted small">修好之后可以清掉这条提示。</span>
</div>
</div>
<?php endif; ?>
<!-- ==================== 公共标签 ==================== -->
<?php
/* 🔴 v54(用户 2026-10-16 第 2 条):这块从「公共分组」改口径为「公共标签」。
用户原话:「现在已经没有分组的概念了,**全部都是标签的概念**。」
⇒ 这里维护的是**公共标签词表**:名册里给成员打标签时,这些词会出现在候选里
(见 `App\PersonTag::choices()`)。表名仍叫 person_groups(见 _person_groups.php
的说明:改名要背着 PersonGroup 类与其它会话正在改的 App-API 一起动,不划算),
但界面上一律说"标签"。 */
$pubGroups = $pubGroups ?? [];
$pubCounts = $pubCounts ?? [];
$pubTotal = count($pubGroups);
?>
<div class="card card-pad" style="margin-top:20px" id="public-groups">
<h2 class="section-title">
公共标签
<span class="hint">全家人共用的一套</span>
</h2>
<p class="small muted" style="margin:0 0 16px">
这里维护的是<b>公共标签</b> —— 建一次,全家每个人给名册成员打标签时都能拿来用。
<br>
每个人还可以在「名册 → 设置 → 名册标签」里<b>再加只给自己看的标签</b>,
那部分不在这儿、也不会影响别人。挪顺序用 ▲ ▼(▲▼ 调的是"全家的默认顺序",
各人仍可在名册设置里改成自己习惯的顺序)。
<br>
删掉一个标签<b>只是从候选里去掉这个词</b>:已经打在别人身上的标签不受影响
(标签是各人身上记着的词,不是引用)。
</p>
<?php if ($pubGroups): ?>
<ol class="order-list">
<?php foreach ($pubGroups as $i => $g): $gid = (int)$g['id']; ?>
<li class="order-row">
<span class="order-no"><?= $i + 1 ?></span>
<span class="order-name"><?= e((string)$g['name']) ?></span>
<span class="order-count"><?= (int)($pubCounts[$gid] ?? 0) ?> 人</span>
<form method="post" action="<?= e(url('settings/groups/move')) ?>" class="order-move">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $gid ?>">
<input type="hidden" name="scope" value="public">
<button class="btn btn-ghost btn-sm" type="submit" name="dir" value="up"
title="上移一个" <?= $i === 0 ? 'disabled' : '' ?>>▲</button>
<button class="btn btn-ghost btn-sm" type="submit" name="dir" value="down"
title="下移一个" <?= $i === $pubTotal - 1 ? 'disabled' : '' ?>>▼</button>
</form>
</li>
<?php endforeach; ?>
</ol>
<?php endif; ?>
<form method="post" action="<?= e(url('settings/groups/add')) ?>" class="inline-form" style="margin-top:16px">
<?= csrf_field() ?>
<input type="hidden" name="scope" value="public">
<label class="sr-only" for="newPubGroup">新标签名</label>
<input class="input" id="newPubGroup" name="name" maxlength="20" placeholder="例如:邻居、亲家、老同事">
<button class="btn btn-sm" type="submit">加一个公共标签</button>
</form>
<?php if ($pubGroups): ?>
<ul class="group-manage" style="margin-top:14px">
<?php foreach ($pubGroups as $g): $gid = (int)$g['id']; ?>
<li class="group-manage-row">
<form method="post" action="<?= e(url('settings/groups/rename')) ?>" class="group-rename">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $gid ?>">
<input type="hidden" name="scope" value="public">
<label class="sr-only" for="pgname<?= $gid ?>">标签名</label>
<input class="input" id="pgname<?= $gid ?>" name="name" maxlength="20"
value="<?= e((string)$g['name']) ?>">
<button class="btn btn-ghost btn-sm" type="submit">改名</button>
</form>
<span class="group-count muted small"><?= (int)($pubCounts[$gid] ?? 0) ?> 人</span>
<details class="del-wrap">
<summary class="btn btn-ghost btn-sm">删除</summary>
<form method="post" action="<?= e(url('settings/groups/delete')) ?>" class="del-confirm">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $gid ?>">
<input type="hidden" name="scope" value="public">
<span class="small muted">这组的人会变成「未分类」,确定?</span>
<button class="btn btn-ghost btn-sm danger" type="submit">确认删除</button>
</form>
</details>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<!-- ==================== 附件存储 ==================== -->
<form method="post" action="<?= e(url('settings/storage')) ?>" class="card card-pad" style="margin-top:20px">
<?= csrf_field() ?>
<h2 class="section-title">
附件存储
<span class="hint">头像等文件存哪儿</span>
</h2>
<p class="small muted" style="margin:0 0 16px">
数据库里只存「相对路径」,所以随时可以在本地与云端之间切换 ——
已上传的文件会按「本地优先」的顺序去找,切换后老文件照样能打开,出问题也能随时切回来。
<br>
<b>不管选哪个,页面上的图片与附件都统一走 <code>media.php</code> 代理</b>:
桶不必开公共读、<code>uploads/</code> 也可以封死,能不能看由登录态决定。
</p>
<div class="field" style="margin-bottom:16px">
<label>存在哪里</label>
<div class="radio-row">
<label class="radio-item">
<input type="radio" name="driver" value="local" <?= $storageDriver === 'local' ? 'checked' : '' ?>>
<span><b>服务器本地</b><br><span class="muted small">最简单,文件和服务在一起。适合家里自己用。</span></span>
</label>
<label class="radio-item">
<input type="radio" name="driver" value="oss" <?= $storageDriver === 'oss' ? 'checked' : '' ?>>
<span><b>阿里云 OSS</b><br><span class="muted small">文件独立存放,换服务器不丢。</span></span>
</label>
</div>
</div>
<?php if ($storageDriver === 'oss' && !$storageOk): ?>
<div class="flash"><div class="flash-item err">
云端配置还不完整,系统现在<b>临时用本地存储</b>在跑 —— 填全下面的信息再保存。
</div></div>
<?php elseif ($storageDriver === 'oss'): ?>
<div class="flash"><div class="flash-item">
当前生效:阿里云 OSS<?= $storageEffective === 'local' ? '(配置不完整,实际走的本地)' : '' ?>
</div></div>
<?php endif; ?>
<details class="oss-box" <?= $storageDriver === 'oss' ? 'open' : '' ?>>
<summary>阿里云 OSS 参数</summary>
<div class="form-grid" style="margin-top:14px">
<div class="field">
<label>凭据方式</label>
<select class="select" name="oss[auth_mode]">
<option value="ak" <?= $storageOss['auth_mode'] === 'ak' ? 'selected' : '' ?>>AccessKey(手动填)</option>
<option value="ecs_role" <?= $storageOss['auth_mode'] === 'ecs_role' ? 'selected' : '' ?>>服务器绑定角色(更安全)</option>
</select>
<span class="tip">若这台服务器是阿里云 ECS 且绑了 RAM 角色,优先用角色 —— 服务器上不落长期密钥。</span>
</div>
<div class="field">
<label>Bucket</label>
<input class="input" name="oss[bucket]" value="<?= e($storageOss['bucket']) ?>" placeholder="例如 my-family-bucket">
</div>
<div class="field">
<label>Endpoint</label>
<input class="input" name="oss[endpoint]" value="<?= e($storageOss['endpoint']) ?>"
placeholder="oss-cn-beijing.aliyuncs.com">
<span class="tip">不要带 https://,且要和桶的地域一致。</span>
</div>
<div class="field">
<label>桶内前缀</label>
<input class="input" name="oss[prefix]" value="<?= e($storageOss['prefix']) ?>" placeholder="可留空,如 familybook">
<span class="tip">一个桶放多个站点时用它隔开。</span>
</div>
<div class="field">
<label>AccessKey ID</label>
<input class="input" name="oss[access_key_id]" value="<?= e($storageOss['access_key_id']) ?>"
autocomplete="off" placeholder="用角色方式时可留空">
</div>
<div class="field">
<label>AccessKey Secret</label>
<input class="input" name="oss[access_key_secret]" type="password"
value="<?= $storageOss['access_key_secret'] !== '' ? '••••••••••••' : '' ?>"
autocomplete="new-password" placeholder="留空表示不改动已保存的">
</div>
<div class="field">
<label>RAM 角色名</label>
<input class="input" name="oss[ram_role_name]" value="<?= e($storageOss['ram_role_name']) ?>"
placeholder="留空 = 自动取实例绑定的角色">
</div>
<div class="field">
<label>自定义域名 / CDN</label>
<input class="input" name="oss[custom_domain]" value="<?= e($storageOss['custom_domain']) ?>"
placeholder="可留空">
<span class="tip">填了会替代 bucket.endpoint 生成访问地址;该域名需已绑定到这个桶。</span>
</div>
<div class="field full">
<label class="check-item">
<input type="checkbox" name="oss[internal_endpoint]" value="1"
<?= $storageOss['internal_endpoint'] === '1' ? 'checked' : '' ?>>
<span>服务端读取优先走内网 endpoint(服务器与桶在同地域时更快,还免流量费)</span>
</label>
<label class="check-item">
<input type="checkbox" name="oss[https]" value="1"
<?= $storageOss['https'] === '1' ? 'checked' : '' ?>>
<span>用 HTTPS 访问对象</span>
</label>
</div>
</div>
</details>
<div class="btn-row">
<button class="btn" type="submit">保存存储设置</button>
</div>
</form>
<!-- ==================== 两种测试 ==================== -->
<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">
「当前存储」测的是<b>现在生效的那一个</b>(可能是本地),只回答"能不能存东西";
「OSS 连通性」测的是<b>下面填的那套 OSS 参数本身</b> —— 即使现在跑在本地也能测,
这样就可以<b>先把 OSS 测通、再切过去</b>,而不是"先切、发现不行、再切回来"。
</p>
<div class="test-actions">
<form method="post" action="<?= e(url('settings/test_storage')) ?>">
<?= csrf_field() ?>
<button class="btn btn-ghost" type="submit">测试当前存储</button>
<span class="small muted">写一个临时文件 → 读回来 → 删掉</span>
</form>
<form method="post" action="<?= e(url('settings/test_oss')) ?>">
<?= csrf_field() ?>
<button class="btn btn-ghost" type="submit">测试 OSS 连通性</button>
<span class="small muted">逐步:配置 → 凭据 → 地域 → 公网 → 内网 → 读写</span>
</form>
</div>
<?php if (!empty($ossDiag['steps'])): ?>
<?php
$dSteps = (array)$ossDiag['steps'];
$dOk = 0;
foreach ($dSteps as $s) {
if (!empty($s['ok'])) {
$dOk++;
}
}
$dAll = $dOk === count($dSteps);
?>
<div class="oss-diag <?= $dAll ? 'is-ok' : 'is-bad' ?>">
<div class="oss-diag-head">
<b><?= $dAll ? '全部通过' : '有问题' ?></b>
<span>共 <?= count($dSteps) ?> 步,通过 <?= $dOk ?> 步</span>
<?php if (!empty($ossDiag['at'])): ?>
<span class="muted small">· <?= e(date('Y-m-d H:i:s', (int)$ossDiag['at'])) ?></span>
<?php endif; ?>
</div>
<table class="oss-diag-table">
<thead>
<tr><th>步骤</th><th>结论</th><th>耗时</th><th>详情</th></tr>
</thead>
<tbody>
<?php foreach ($dSteps as $s): ?>
<tr class="<?= !empty($s['ok']) ? 'row-ok' : 'row-bad' ?>">
<td><?= !empty($s['ok']) ? '✓' : '✕' ?> <?= e((string)($s['name'] ?? '')) ?></td>
<td><?= !empty($s['ok']) ? '通过' : '失败' ?></td>
<td class="mono"><?= (int)($s['ms'] ?? 0) ?> ms</td>
<td><?= nl2br(e((string)($s['detail'] ?? ''))) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="small muted" style="margin:12px 0 0">
探针对象用后即删,不会在桶里留垃圾;跑完这一遍之后再切驱动最稳妥。
</p>
</div>
<?php else: ?>
<p class="small muted" style="margin:14px 0 0">
还没测过。建议<b>先在下面填好 OSS 参数并保存,再点「测试 OSS 连通性」</b>,
通过之后再回到上面把「存在哪里」切到阿里云 OSS。
</p>
<?php endif; ?>
</div>
<!-- ==================== 运行环境 ==================== -->
<div class="card card-pad" style="margin-top:20px">
<h2 class="section-title">运行环境 <span class="hint">排错时看这一块</span></h2>
<dl class="kv">
<div class="kv-row"><dt>PHP</dt><dd><?= e(PHP_VERSION) ?></dd></div>
<div class="kv-row"><dt>数据库</dt><dd><?= e($dbVersion) ?></dd></div>
<div class="kv-row"><dt>加密密钥</dt><dd><?= $keyOk ? '已配置' : '<span style="color:var(--red)">未配置</span>' ?></dd></div>
<div class="kv-row">
<dt>附件存储</dt>
<dd><?= $storageEffective === 'oss' ? '阿里云 OSS' : '服务器本地' ?>
<?php if ($storageDriver !== $storageEffective): ?>
<span class="muted small">(设置的是 OSS,但配置不全,实际用本地)</span>
<?php endif; ?>
</dd>
</div>
<div class="kv-row">
<dt>附件出口</dt>
<dd>统一走 media.php 代理 <span class="muted small">(本地与云端同一形态,需登录才能读)</span></dd>
</div>
<div class="kv-row">
<dt>亲属连线</dt>
<dd><?= (int)$relCount ?> 条</dd>
</div>
<div class="kv-row">
<dt>账号</dt>
<dd>
管理员 <?= (int)$roleCounts['admin'] ?> ·
家庭成员 <?= (int)$roleCounts['member'] ?> ·
访客 <?= (int)$roleCounts['guest'] ?>
</dd>
</div>
</dl>
<p class="small muted" style="margin:14px 0 0">
手机号与身份证号都是加密后入库的。这个密钥一旦换掉,已有数据就再也解不开 ——
请把它和数据库分开、各备份一份。
</p>
<p class="small muted" style="margin:10px 0 0">
访客账号在「<a href="<?= e(url('accounts/invite')) ?>">邀请访客</a>」里开通。
</p>
</div>