登录居中 · 个人 slogan · 三种生日 · 称呼视角化 · 附件统一走代理

按九点反馈一次性改造:

1. 登录页「图标 + 用手机号登录你的家书」包进 .auth-head 并真正居中
   (以前只加了个 class 名,没有样式,看着像做了其实没做)。

2. slogan 改成两段式:系统级 slogan 作兜底,每个人可在自己的资料里写一个,
   填了就显示自己的、留空就回落到系统那句。settings 里「家庭名称」字段去掉
   (只在安装时归档,不参与展示)。

3. 三个生日:身份证生日(从证件号截取,永不改动)/真实阳历/农历。
   同一人填了其一即自动推出另一个(靠 birthday_source 指明填的是哪一侧,
   不依赖 JS)。三者落在同一天时合并成一条提醒,否则分别提醒。
   农历表不是照抄流传的那份 lunarInfo(它在 5 个年份上是错的)——
   用五个独立实现按 7.3 万天多数表决重新推导,再要求 100% 重现。

4. 称呼改成「视角属性」:nicknames(family_id, viewer_id, target_id, nickname)。
   「小明」不是某个人的属性——同一个人被不同人叫法不同,所以是「谁看」的属性。
   旧的 persons.nickname 由 Nickname::migrateLegacy() 搬到新表,只搬一次。

5. 点自己的头像/名字直接进「我的资料」,不必再绕
   「名册 → 找到自己 → 编辑资料」三步。

6. 附件(头像与其它上传)一律走 media.php 代理,不再直连 uploads/ 或云端地址:
   可读性由登录态决定而不是「知道文件名」,Content-Type/缓存/下载头由服务端统一给。
   uploads/ 整个目录 deny(含历史遗留备份文件),并有真实探针验证。

7. OSS 设置页加「连通性测试」(本地 / 云端两个按钮),逐步给出结论与耗时。

8. 生日输入改用「原生日期框 + 24 小时制时:分:秒 三个下拉」:
   datetime-local 的格式由浏览器 UI 语言决定(中文环境渲染成「下午 03:30」,
   没有秒、还容易把上下午填反)。点输入框任意一处都能弹出选择器(showPicker)。

9. 结构版本号收敛成 Schema::VERSION 一处,安装时即盖章,避免首次请求白跑升级。

验证(全部为「先注入 bug 证伪、再跑通过」):
  php -l 42 个文件;e2e 149 条;老库升级专测 37 条;反向验证 23 个注入全被抓;
  桌面几何 108 条(768/1280/1920/2560);移动端真机几何 212 条(360/375/390/414);
  交互检查 49 条(生日整框可点、登录居中)。
This commit is contained in:
2026-09-30 09:44:55 +08:00
parent 1669ab48d0
commit 7e9b4520c0
26 changed files with 2561 additions and 204 deletions
+63
View File
@@ -0,0 +1,63 @@
<?php
/**
* 「日期 + 24 小时制时:分:秒」组合输入控件(从 TripLog 的 _datetime.php 移植)。
*
* ⚠️ 为什么不用 <input type="datetime-local">:
* 它的显示格式由**浏览器 UI 语言 / 系统时间格式**决定,<html lang> 与元素 lang
* 都压不过它。中文环境实测会渲染成「下午 03:30」,用户很容易把上下午填反;
* 而且它只有时:分,**没有秒**。拆成「原生日期选择器 + 三个下拉」后,
* 24 小时制与"精确到秒"都由控件本身保证,浏览器改不掉。
*
* 日期部分仍是原生 `<input type="date">`(app.js 里的 showPicker 脚本对它同样生效,
* 点输入框任意位置都能弹出日历 —— 这正是"整个输入框点击都可以弹出"的落点)。
*
* 用法(include 前 set 好这些变量;模板里只读它们):
* $dtName 字段基名,如 'birthday_solar' → 产出 *_date / _hour / _minute / _second
* $dtValue 可空;'Y-m-d H:i:s' / 'Y-m-d H:i' / 'Y-m-d'。服务端用 read_dt_input($dtName) 取值
* $dtRequired 日期是否必填(默认 false)
* $dtId 可选;日期控件的 id(供 <label for>)
* $dtSeconds 是否要"秒"这一档(默认 true;生日要精确到时分秒)
*/
$dtName = (string)($dtName ?? 'time');
$dtValue = $dtValue ?? null;
$dtRequired = (bool)($dtRequired ?? false);
$dtId = (string)($dtId ?? '');
$dtSeconds = (bool)($dtSeconds ?? true);
$dtTs = (!empty($dtValue) && ($t = strtotime(trim((string)$dtValue)))) ? $t : false;
$dtDate = $dtTs ? date('Y-m-d', $dtTs) : '';
$dtHour = $dtTs ? (int)date('G', $dtTs) : 0; // G = 0..23,天然 24 小时制
$dtMin = $dtTs ? (int)date('i', $dtTs) : 0;
$dtSec = $dtTs ? (int)date('s', $dtTs) : 0;
?>
<div class="dt-input">
<input type="date" class="dt-date" name="<?= e($dtName) ?>_date"
<?= $dtId !== '' ? 'id="' . e($dtId) . '"' : '' ?>
value="<?= e($dtDate) ?>"<?= $dtRequired ? ' required' : '' ?>
aria-label="日期">
<span class="dt-time">
<select class="dt-hour" name="<?= e($dtName) ?>_hour" aria-label="时(24 小时制)">
<?php for ($h = 0; $h < 24; $h++): $hv = sprintf('%02d', $h); ?>
<option value="<?= $hv ?>"<?= $h === $dtHour ? ' selected' : '' ?>><?= $hv ?></option>
<?php endfor; ?>
</select>
<span class="dt-colon" aria-hidden="true">:</span>
<select class="dt-minute" name="<?= e($dtName) ?>_minute" aria-label="分">
<?php for ($m = 0; $m < 60; $m++): $mv = sprintf('%02d', $m); ?>
<option value="<?= $mv ?>"<?= $m === $dtMin ? ' selected' : '' ?>><?= $mv ?></option>
<?php endfor; ?>
</select>
<?php if ($dtSeconds): ?>
<span class="dt-colon" aria-hidden="true">:</span>
<select class="dt-second" name="<?= e($dtName) ?>_second" aria-label="秒">
<?php for ($s = 0; $s < 60; $s++): $sv = sprintf('%02d', $s); ?>
<option value="<?= $sv ?>"<?= $s === $dtSec ? ' selected' : '' ?>><?= $sv ?></option>
<?php endfor; ?>
</select>
<?php endif; ?>
</span>
</div>
<?php /* 用完即清,避免残留变量影响下一次 include */ unset(
$dtName, $dtValue, $dtRequired, $dtId, $dtSeconds,
$dtTs, $dtDate, $dtHour, $dtMin, $dtSec
); ?>
+69 -14
View File
@@ -4,27 +4,56 @@
*
* 同一份数据,爸爸登进来看到的是「我的父亲/我的儿子」,
* 爱人登进来看到的是同一批人,称呼却变成「公公/婆婆」。
* 称呼全部由 Kinship 现算,库里不存任何一次性称谓。
* 称呼全部由 Kinship 现算、由 Nickname 决定"我叫他什么",库里不存任何一次性称谓。
*
* 标题下面那一行小字的取法(这处被明确纠正过):
* 先看**我自己**有没有写 slogan → 没写才回落到家庭统一的那句。
* 所以同一页不同人登进来看到的是不同的话,而不是全家共用一个口号。
* 家庭名称本身不再展示 —— 标题已经是「某某某的家书」,再挂一个家庭名只会打架。
*/
$kinCount = count($kin);
$hasRel = $relCount > 0;
// 显示名一律走「我怎么称呼他」:同一个孩子,爸爸看到"儿子"、妈妈看到"小宝"。
// ⚠️ 用**已经取好的那张表**(target_id => 我叫他什么)做本地查表,
// 不要再对每个人调一次 Nickname::display() —— 那是每人一条 SQL。
// 没有称呼的人回落到姓名,所以永远不会出现空白名字。
$shownOf = function (array $row) use ($myNicknames): string {
$nk = (string)($myNicknames[(int)($row['id'] ?? 0)] ?? '');
return $nk !== '' ? $nk : (string)($row['name'] ?? '');
};
// 有亲属关系 → 只显示家人;还没建 → 退回显示全名册,别让首页空着
$list = $hasRel ? $kin : [];
if (!$list) {
foreach (\App\Person::all((int)\App\Auth::familyId()) as $p) {
$list[(int)$p['id']] = $p + ['label' => ''];
foreach (\App\Person::all((int)\App\Auth::familyId(), '', 'member', (int)$meId) as $p) {
$p['label'] = '';
$list[(int)$p['id']] = $p;
}
}
// 标题下的一行小字:我的一句话 → 家庭统一那句 → 兜底
$motto = trim((string)$mySlogan);
$mottoIsMine = $motto !== '';
if (!$mottoIsMine) {
$motto = trim((string)($family['motto'] ?? ''));
}
if ($motto === '') {
$motto = '一本用岁月写成的家谱';
}
// 生日事件上用简称,卡片才不会一行塞不下;长名字在详情页里出
$shortKind = ['id' => '身份证生日', 'solar' => '阳历生日', 'lunar' => '农历生日'];
?>
<section class="hero">
<img class="hero-logo" src="<?= e(asset('/assets/icons/icon-192.png')) ?>" alt="">
<div>
<div class="hero-main">
<h1><?= e(\App\Auth::homeTitle()) ?></h1>
<p class="motto">
<?= e($motto) ?>
<?php if ($mottoIsMine): ?>
<span class="motto-from">我的一句话</span>
<?php endif; ?>
<?php if ($me): ?>
<?= e($family['name']) ?> · 这里的称呼都是按你的视角算的
<?php else: ?>
<?= e($family['motto'] ?: '一本用岁月写成的家谱') ?>
<a class="motto-edit" href="<?= e(url('people/me')) ?>"><?= $mottoIsMine ? '改' : '写一句我自己的' ?></a>
<?php endif; ?>
</p>
</div>
@@ -40,22 +69,41 @@ if (!$list) {
<h2 class="section-title">接下来的日子 <span class="hint">两个月内</span></h2>
<div class="bdays">
<?php foreach ($birthdays as $b): ?>
<div class="bday">
<?php
// 同一个人可能出两条(农历与阳历不在同一天);落在同一天时后端已经并成一条,
// kinds 里会有多项 —— 这里照着列出来,用户才知道"今天过的是哪个生日"。
$bkinds = (array)($b['kinds'] ?? []);
$bwords = [];
foreach ($bkinds as $k) {
$bwords[] = $shortKind[$k] ?? (string)$k;
}
$bshown = (string)($b['shown'] ?? $b['name'] ?? '');
?>
<a class="bday" href="<?= e(url('people/show', ['id' => (int)$b['id']])) ?>">
<img src="<?= e(avatar_url($b['avatar'])) ?>" alt="">
<div>
<b><?= e($b['name']) ?></b>
<b><?= e($bshown) ?></b>
<?php if (!empty($b['label']) && $b['label'] !== '我'): ?>
<span class="badge"><?= e($b['label']) ?></span>
<?php endif; ?>
<div class="d">
<?php if ($b['days_left'] === 0): ?>
<?php if ((int)$b['days_left'] === 0): ?>
就是今天,生日快乐
<?php else: ?>
还有 <?= (int)$b['days_left'] ?> 天过生日
<?php endif; ?>
</div>
<div class="bday-meta">
<span class="bday-date"><?= e((string)$b['date']) ?></span>
<?php foreach ($bwords as $w): ?>
<span class="bd-kind"><?= e($w) ?></span>
<?php endforeach; ?>
<?php if (!empty($b['lunar_label'])): ?>
<span class="muted small">农历<?= e((string)$b['lunar_label']) ?></span>
<?php endif; ?>
</div>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
</section>
@@ -97,11 +145,18 @@ if (!$list) {
<?php else: ?>
<div class="people-grid">
<?php foreach ($list as $p): ?>
<?php
// ⚠️ 这里必须用「我给他起的称呼」,不能直接用姓名:
// 同一个「杨父」,儿媳妇看到的是"公公大人"、儿子看到的是"爸"。
$pshow = $shownOf($p);
$pIsMe = (int)$p['id'] === (int)$meId;
?>
<a class="person" href="<?= e(url('people/show', ['id' => (int)$p['id']])) ?>">
<div class="ph"><img src="<?= e(avatar_url($p['avatar'] ?? '')) ?>" alt=""></div>
<div class="pb">
<div class="pname">
<?= e($p['name']) ?>
<?= e($pshow) ?>
<?php if ($pIsMe): ?><span class="badge badge-me">我</span><?php endif; ?>
<?php if (!empty($p['is_placeholder'])): ?><span class="badge">待确认</span><?php endif; ?>
</div>
<div class="pmeta">
@@ -117,7 +172,7 @@ if (!$list) {
} elseif (!empty($p['account_id'])) {
echo '<span class="badge badge-open">已开通</span>';
}
$age = age_from($p['birthday'] ?? '');
$age = age_from(\App\Birthday::solarOf($p));
if ($age !== null) { echo ' · ' . (int)$age . ' 岁'; }
?>
</div>
@@ -126,4 +181,4 @@ if (!$list) {
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
</section>
+3 -1
View File
@@ -15,11 +15,13 @@
<label for="family_name">家庭名称</label>
<input class="input" id="family_name" name="family_name"
value="<?= e(old('family_name', '我们家')) ?>" placeholder="比如:我们家" required>
<span class="tip">只存档用 —— 页面上显示的是「某某某的家书」,不会再展示这个名称。</span>
</div>
<div class="field full">
<label for="motto">一句话</label>
<label for="motto">系统 slogan</label>
<input class="input" id="motto" name="motto" value="<?= e(old('motto')) ?>"
placeholder="写在首页的一行小字,可留空">
<span class="tip">每个人都可以写自己的一句;只有没写的人才会看到这句。</span>
</div>
<div class="field">
<label for="name">你的姓名</label>
+6 -2
View File
@@ -41,11 +41,15 @@ $isLoggedIn = \App\Auth::check();
<?php endforeach; ?>
</nav>
<div class="topbar-right">
<span class="who">
<?php /* 点自己的头像/名字 = 直接编辑自己的资料。
⚠️ 以前这里是个不可点的 <span>:想改自己的信息得绕
「名册 → 找到自己 → 编辑资料」三步,而最该一眼可达的
恰恰是「改我自己」这件事。 */ ?>
<a class="who" href="<?= e(url('people/me')) ?>" title="改我自己的资料">
<img src="<?= e(avatar_url($_SESSION['avatar'] ?? null)) ?>" alt="">
<span class="who-name"><?= e(\App\Auth::name()) ?></span>
<?php if (\App\Auth::isGuest()): ?><span class="badge">访客</span><?php endif; ?>
</span>
</a>
<a class="link-quiet who-pwd" href="<?= e(url('auth/password')) ?>">改密码</a>
<a class="link-quiet" href="<?= e(url('auth/logout', ['_csrf' => csrf_token()])) ?>">退出</a>
</div>
+5 -3
View File
@@ -1,8 +1,10 @@
<div class="auth-wrap">
<div class="auth-card">
<img class="logo" src="<?= e(asset('/assets/icons/icon-192.png')) ?>" alt="">
<h1><?= e($familyName) ?></h1>
<p class="lead">用手机号登录你的家书。</p>
<div class="auth-head">
<img class="logo" src="<?= e(asset('/assets/icons/icon-192.png')) ?>" alt="">
<h1><?= e($familyName) ?></h1>
<p class="lead">用手机号登录你的家书</p>
</div>
<?php if (!empty($error)): ?>
<div class="flash"><div class="flash-item err"><?= e($error) ?></div></div>
+95 -22
View File
@@ -1,6 +1,29 @@
<?php
/**
* 成员表单(新增 / 编辑)。
*
* 三处与早期版本不同的地方:
* · 「小名」没了 —— 称呼是**视角属性**(妈妈叫"明明"、姐姐叫"小弟"),
* 在「成员详情」页里各自设置自己怎么叫,不写在这个人的档案里。
* · 生日变成**三个**:身份证生日(由证件推导、只读)、阳历生日、农历生日。
* 阳历与农历是同一个人的同一天,所以用「以哪个为准」明确这次填的是哪一个,
* 服务端据此反推另一个 —— 不靠 JS,避免"JS 没跑到就静默存错"。
* · 生日精确到**时分秒**:原生 datetime-local 只有时分、且中文环境会显示成
* 「下午 03:30」,所以用 _datetime.php 的「日期 + 24 小时制时:分:秒」组合控件。
*/
$isEdit = !empty($person['id']);
$avatarUrl = avatar_url($person['avatar'] ?? null);
$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;
}
$idBd = trim((string)($person['birthday_id_card'] ?? ''));
?>
<div class="page-head">
@@ -24,12 +47,6 @@ $avatarUrl = avatar_url($person['avatar'] ?? null);
<?php if (!empty($errors['name'])): ?><span class="err"><?= e($errors['name']) ?></span><?php endif; ?>
</div>
<div class="field">
<label for="nickname">小名</label>
<input class="input" id="nickname" name="nickname"
value="<?= e(old('nickname', $person['nickname'] ?? '')) ?>" placeholder="家里怎么叫他/她">
</div>
<div class="field">
<label for="gender">性别</label>
<select class="select" id="gender" name="gender">
@@ -40,14 +57,6 @@ $avatarUrl = avatar_url($person['avatar'] ?? null);
</select>
</div>
<div class="field">
<label for="birthday">生日</label>
<input class="input" id="birthday" name="birthday" type="date"
value="<?= e(old('birthday', $person['birthday'] ?? '')) ?>">
<span class="tip">填了之后,首页会提醒还有几天过生日。</span>
<?php if (!empty($errors['birthday'])): ?><span class="err"><?= e($errors['birthday']) ?></span><?php endif; ?>
</div>
<div class="field">
<label for="birth_rank">家中排行</label>
<input class="input" id="birth_rank" name="birth_rank" type="number" min="1" max="99"
@@ -56,6 +65,76 @@ $avatarUrl = avatar_url($person['avatar'] ?? null);
<?php if (!empty($errors['birth_rank'])): ?><span class="err"><?= e($errors['birth_rank']) ?></span><?php endif; ?>
</div>
<div class="field">
<label for="sort_order">排序号</label>
<input class="input" id="sort_order" name="sort_order" type="number"
value="<?= e(old('sort_order', $person['sort_order'] ?? 1000)) ?>">
<span class="tip">小的排前面。</span>
</div>
<!-- ==================== 三个生日 ==================== -->
<div class="field full">
<label>生日</label>
<p class="small muted" style="margin:0 0 12px">
一个人有三个生日:<b>身份证上的</b>(由证件推导,系统永远不会改它)、
<b>真实阳历生日</b>、<b>农历生日</b>。填了身份证号会自动读出第一个;
阳历与农历只要填一个,另一个自动算出来。三个不在同一天时,首页会分别提醒。
</p>
<div class="field" style="margin-bottom:12px">
<label for="birthday_id_card_display">身份证生日(只读)</label>
<span class="bd-readonly" id="birthday_id_card_display"><?= $idBd !== '' ? e($idBd) : '填了身份证号后自动读出' ?></span>
</div>
<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>
<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>
<div class="field">
<label for="phone">手机号</label>
<input class="input" id="phone" name="phone" inputmode="numeric"
@@ -77,7 +156,8 @@ $avatarUrl = avatar_url($person['avatar'] ?? null);
value="<?= e(old('id_card', $idCardValue ?? '')) ?>" placeholder="可留空">
<span class="tip">
加密存储,页面上一律只显示前 6 后 4 位。
家庭场景里真正需要它的场合很少 —— 能不填就别填。
<b>填了它,上面的「身份证生日」会自动读出来</b>(当前:<span id="idCardBirthday"><?= $idBd !== '' ? e($idBd) : '' ?></span>),
这个日期系统永远不会改动。家庭场景里真正需要它的场合很少 —— 能不填就别填。
</span>
<?php if (!empty($errors['id_card'])): ?><span class="err"><?= e($errors['id_card']) ?></span><?php endif; ?>
</div>
@@ -104,13 +184,6 @@ $avatarUrl = avatar_url($person['avatar'] ?? null);
style="width:88px;height:88px;border-radius:12px;object-fit:cover;border:1px solid var(--line)">
</div>
<div class="field">
<label for="sort_order">排序号</label>
<input class="input" id="sort_order" name="sort_order" type="number"
value="<?= e(old('sort_order', $person['sort_order'] ?? 1000)) ?>">
<span class="tip">小的排前面。</span>
</div>
<div class="field full">
<label class="check-item">
<input type="checkbox" name="is_placeholder" value="1"
+16 -3
View File
@@ -10,7 +10,7 @@
<form class="search" method="get" action="index.php">
<input type="hidden" name="r" value="people">
<input class="input" name="q" value="<?= e($q) ?>" placeholder="按姓名、小名或邮箱搜索">
<input class="input" name="q" value="<?= e($q) ?>" placeholder="按姓名、称呼或邮箱搜索">
<button class="btn btn-ghost btn-sm" type="submit">搜索</button>
<?php if ($q !== ''): ?>
<a class="link-quiet" href="<?= e(url('people')) ?>">清除</a>
@@ -25,11 +25,21 @@
<?php else: ?>
<div class="people-grid">
<?php foreach ($people as $p): ?>
<?php
// 显示名走「我怎么称呼他」:同一个孩子,爸爸看到"儿子"、妈妈看到"小宝"。
// ⚠️ 不要再读 $p['nickname'] —— 那是已废弃的旧列(全家共用一个小名)。
$pshow = (string)($p['shown'] ?? '');
if ($pshow === '') {
$pshow = (string)$p['name'];
}
$pNick = (string)($p['my_nickname'] ?? '');
?>
<a class="person" href="<?= e(url('people/show', ['id' => (int)$p['id']])) ?>">
<div class="ph"><img src="<?= e(avatar_url($p['avatar'])) ?>" alt=""></div>
<div class="pb">
<div class="pname">
<?= e($p['name']) ?>
<?= e($pshow) ?>
<?php if ((int)$p['id'] === (int)$meId): ?><span class="badge badge-me">我</span><?php endif; ?>
<?php if (!empty($p['is_placeholder'])): ?><span class="badge">待确认</span><?php endif; ?>
</div>
<div class="pmeta">
@@ -45,8 +55,11 @@
} elseif (!empty($p['account_id'])) {
echo '<span class="badge badge-open">已开通</span>';
}
// 我叫他别的名字时,把本名再用小字附上,免得自己都认不出这人是谁
if ($pNick !== '' && $pNick !== (string)$p['name']) {
echo ' · <span class="muted">' . e((string)$p['name']) . '</span>';
}
?>
<?php if (!empty($p['nickname'])): ?> · <?= e($p['nickname']) ?><?php endif; ?>
</div>
</div>
</a>
+1 -1
View File
@@ -8,7 +8,7 @@
<div class="role-cards">
<div class="role-card">
<div class="rc-name">管理员</div>
<p class="rc-desc">一个家庭通常只有一位。改家庭名称、附件存哪儿、谁能登录,都在他手里。</p>
<p class="rc-desc">一个家庭通常只有一位。系统级的 slogan、附件存哪儿、谁能登录,都在他手里。</p>
</div>
<div class="role-card">
<div class="rc-name">家庭成员</div>
+189
View File
@@ -0,0 +1,189 @@
<?php
/**
* 「我的资料」—— 每个人自己的页面。
*
* 从顶栏的头像/名字一点就到,不用再绕「名册 → 找到自己 → 编辑资料」三步。
*
* 这里只放**属于我自己的**东西:
* · 我的姓名、性别、头像、联系方式、备注
* · 我的三个生日(身份证生日只读,由证件推导)
* · 我的 slogan(没填就用家庭统一的那句)
* · **我怎么称呼别人** —— 这是视角属性,只有我自己能定,改了也只影响我自己看到的
* 排行、排序号、虚拟人物标记、以及会牵动身份证生日的身份证号,
* 都属于"管理员编名册",不在这里(这也是为了不让一次自助提交悄悄改掉这些)。
*/
$p = $person;
$isSelf = true;
$avatarUrl = avatar_url($p['avatar'] ?? null);
$bdSource = old('birthday_source', 'solar');
$idBd = trim((string)($p['birthday_id_card'] ?? ''));
// 我已设过的称呼:target_id => nickname
$myNicks = [];
foreach ($nicknames as $n) {
$myNicks[(int)$n['target_id']] = (string)$n['nickname'];
}
?>
<div class="page-head">
<h1>我的资料</h1>
<span class="sub">你自己看到的样子 · 别人看到你的名字不变</span>
<div class="actions">
<a class="btn btn-ghost btn-sm" href="<?= e(url('auth/password')) ?>">改密码</a>
<a class="btn btn-ghost btn-sm" href="<?= e(url('people/show', ['id' => (int)$p['id']])) ?>">看我的详情页</a>
</div>
</div>
<form method="post" enctype="multipart/form-data" action="<?= e(url('people/me_save')) ?>">
<?= csrf_field() ?>
<div class="card card-pad">
<div class="form-grid">
<div class="field">
<label for="name">姓名 *</label>
<input class="input" id="name" name="name" value="<?= e(old('name', $p['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', $p['gender'] ?? ''); ?>
<option value="" <?= $g === '' ? 'selected' : '' ?>>不填</option>
<option value="m" <?= $g === 'm' ? 'selected' : '' ?>>男</option>
<option value="f" <?= $g === 'f' ? 'selected' : '' ?>>女</option>
</select>
</div>
<div class="field full">
<label for="slogan">我的一句话</label>
<input class="input" id="slogan" name="slogan" maxlength="200"
value="<?= e(old('slogan', $p['slogan'] ?? '')) ?>"
placeholder="留空就用家庭统一的那句:<?= e($familyMotto !== '' ? $familyMotto : '(还没设)') ?>">
<span class="tip">显示在你自己的首页标题下面。</span>
<?php if (!empty($errors['slogan'])): ?><span class="err"><?= e($errors['slogan']) ?></span><?php endif; ?>
</div>
<!-- ==================== 我的三个生日 ==================== -->
<div class="field full">
<label>我的生日</label>
<p class="small muted" style="margin:0 0 12px">
身份证上的那个由证件推导,<b>系统永远不会改它</b>;
下面两个是"实际过的阳历生日"和"长辈记的农历生日"。
改其中任意一个,另一个会自动跟着变。
</p>
<div class="field" style="margin-bottom:12px">
<label>身份证生日(只读)</label>
<span class="bd-readonly"><?= $idBd !== '' ? e($idBd) : '名册里还没填身份证号' ?></span>
</div>
<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 = old('birthday_solar', \App\Birthday::pretty(\App\Birthday::solarOf($p)));
$dtId = 'birthday_solar_date';
$dtRequired = false;
require __DIR__ . '/../_datetime.php';
?>
<?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 = old('birthday_lunar', (string)($p['birthday_lunar'] ?? ''));
$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', $p['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>
<div class="field">
<label for="email">邮箱</label>
<input class="input" id="email" name="email" type="email"
value="<?= e(old('email', $p['email'] ?? '')) ?>">
<?php if (!empty($errors['email'])): ?><span class="err"><?= e($errors['email']) ?></span><?php endif; ?>
</div>
<div class="field">
<label for="address">住址</label>
<input class="input" id="address" name="address"
value="<?= e(old('address', $p['address'] ?? '')) ?>">
</div>
<div class="field full">
<label for="note">备注</label>
<textarea class="input" id="note" name="note"><?= e(old('note', $p['note'] ?? '')) ?></textarea>
</div>
<div class="field">
<label for="avatar">换头像</label>
<input class="input" id="avatar" name="avatar" type="file" accept="image/*">
<span class="tip">会自动裁成正方形,上限 5MB。</span>
</div>
<div class="field">
<label>当前头像</label>
<img id="avatar-preview" src="<?= e($avatarUrl) ?>" alt=""
style="width:88px;height:88px;border-radius:12px;object-fit:cover;border:1px solid var(--line)">
</div>
</div>
<div class="btn-row">
<button class="btn" type="submit">保存我的资料</button>
</div>
</div>
</form>
<!-- ==================== 我怎么称呼别人 ==================== -->
<div class="card card-pad" style="margin-top:20px">
<h2 class="section-title">
我怎么称呼家人
<span class="hint">只影响你自己看到的叫法</span>
</h2>
<p class="small muted" style="margin:0 0 16px">
同一个「小明」,妈妈叫"明明"、姐姐叫"小弟" —— 称呼是**每个人各自定的**,
不是这个人自带的属性。所以这里改的只影响你自己的页面,别人看到的还是他们自己的叫法。
留空 = 恢复成用姓名显示。
</p>
<?php if (!$options): ?>
<p class="small muted" style="margin:0">名册里还没有别人。</p>
<?php else: ?>
<div class="nick-list">
<?php foreach ($options as $o): $oid = (int)$o['id']; ?>
<form method="post" action="<?= e(url('nickname/save')) ?>" class="nick-row">
<?= csrf_field() ?>
<input type="hidden" name="target_id" value="<?= $oid ?>">
<input type="hidden" name="back" value="people/me">
<span class="nick-who">
<img src="<?= e(avatar_url($o['avatar'] ?? '')) ?>" alt="">
<span><?= e((string)$o['name']) ?></span>
</span>
<input class="input" name="nickname" maxlength="50"
value="<?= e($myNicks[$oid] ?? '') ?>" placeholder="我叫他/她…">
<button class="btn btn-ghost btn-sm" type="submit">记下</button>
</form>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
+108 -24
View File
@@ -5,7 +5,7 @@
* 页面上每个人名旁边都带着「我该怎么称呼他」,同一个页面
* 爸爸看到「我的儿子」,爱人看到「我的丈夫」,全靠 Kinship 现算。
*/
$age = age_from($person['birthday'] ?? '');
$age = age_from(\App\Birthday::solarOf($person));
$admin = \App\Auth::isAdmin();
$canW = \App\Auth::canWrite();
$pid = (int)$person['id'];
@@ -13,28 +13,47 @@ $p = $person;
$isSelf = (int)$p['id'] === (int)\App\Auth::personId();
$role = (string)($p['account_role'] ?? '');
$nameOf = function (int $id) use ($people): string {
return (string)($people[$id]['name'] ?? '(已移除)');
// ⚠️ 显示名一律走「我怎么称呼他」,不要直接 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 = ['id' => '身份证生日', 'solar' => '阳历生日', 'lunar' => '农历生日'];
?>
<div class="page-head">
<h1><?= e($p['name']) ?></h1>
<h1><?= e($pShown) ?></h1>
<span class="sub">
<?php if ($labelToMe !== '我'): ?>
<span class="rel-label"><?= e($labelToMe) ?></span>
<?php else: ?>
<?php if ($isSelf): ?>
这是你自己
<?php else: ?>
<span class="rel-label"><?= e($labelToMe) ?></span>
<?php if ($isMyNick): ?>
· 本名 <?= e((string)$p['name']) ?>
<?php endif; ?>
<?php endif; ?>
<?php if (!empty($p['nickname'])): ?> · 小名 <?= e($p['nickname']) ?><?php endif; ?>
<?php if (!empty($p['is_placeholder'])): ?><span class="badge">虚拟人物</span><?php endif; ?>
</span>
<div class="actions">
<?php if ($isSelf): ?>
<a class="btn btn-sm" href="<?= e(url('people/me')) ?>">改我自己的资料</a>
<?php endif; ?>
<?php if ($canW): ?>
<a class="btn btn-ghost btn-sm" href="<?= e(url('people/edit', ['id' => $pid])) ?>">编辑资料</a>
<?php endif; ?>
@@ -49,10 +68,15 @@ $theirSibs = \App\Kinship::siblingIds($graph, $pid);
</div>
</div>
<?php if ($isSelf && !empty($p['slogan'])): ?>
<p class="motto-line">“<?= e((string)$p['slogan']) ?>”</p>
<?php endif; ?>
<div class="detail">
<aside class="detail-photo">
<img src="<?= e(avatar_url($p['avatar'])) ?>" alt="">
<div class="name"><?= e($p['name']) ?></div>
<div class="name"><?= e($pShown) ?></div>
<?php if ($isMyNick): ?><div class="sub">本名 <?= e((string)$p['name']) ?></div><?php endif; ?>
<div class="sub">
<?php if ($role === 'admin' && $p['account_status'] === 'active'): ?>
家庭管理员
@@ -69,10 +93,7 @@ $theirSibs = \App\Kinship::siblingIds($graph, $pid);
<div>
<div class="card card-pad">
<dl class="kv">
<div class="kv-row"><dt>姓名</dt><dd><?= e($p['name']) ?></dd></div>
<?php if (!empty($p['nickname'])): ?>
<div class="kv-row"><dt>小名</dt><dd><?= e($p['nickname']) ?></dd></div>
<?php endif; ?>
<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>
@@ -80,20 +101,61 @@ $theirSibs = \App\Kinship::siblingIds($graph, $pid);
<?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 (!empty($p['birthday'])): ?>
<?= e($p['birthday']) ?>
<?php if ($age !== null): ?> · <?= (int)$age ?> 岁<?php endif; ?>
<?php $d = days_until_birthday($p['birthday']); ?>
<?php if ($d === 0): ?> · <span style="color:var(--brand)">今天生日</span>
<?php elseif ($d !== null): ?> · 还有 <?= (int)$d ?> 天<?php endif; ?>
<?php else: ?>
<?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>
@@ -118,10 +180,32 @@ $theirSibs = \App\Kinship::siblingIds($graph, $pid);
</dl>
</div>
<!-- ============ 我叫他/她什么(视角属性,只有我自己能改) ============ -->
<?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($p['name']) ?> 的亲属
<?= e($pShown) ?> 的亲属
<span class="hint">关系是双向的,填一次两边都生效</span>
</h2>
@@ -211,7 +295,7 @@ $theirSibs = \App\Kinship::siblingIds($graph, $pid);
<option value="">— 选择一个人 —</option>
<?php foreach ($options as $o): ?>
<option value="<?= (int)$o['id'] ?>">
<?= e($o['name']) ?><?= !empty($o['nickname']) ? '(' . e($o['nickname']) . ')' : '' ?>
<?= e((string)$o['name']) ?><?= !empty($o['my_nickname']) ? '(' . e((string)$o['my_nickname']) . ')' : '' ?>
</option>
<?php endforeach; ?>
</select>
@@ -260,7 +344,7 @@ $theirSibs = \App\Kinship::siblingIds($graph, $pid);
<!-- ============ 以这个人看出去的家谱 ============ -->
<?php if (!empty($theirTree['nodes']) && count($theirTree['nodes']) > 1): ?>
<div class="card card-pad" style="margin-top:16px">
<h2 class="section-title">从 <?= e($p['name']) ?> 的角度看 <span class="hint">上下各两代</span></h2>
<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">
@@ -268,7 +352,7 @@ $theirSibs = \App\Kinship::siblingIds($graph, $pid);
<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($n['name']) ?></span>
<span class="mn-name"><?= e($nameOf((int)$id)) ?></span>
<span class="mn-label"><?= e($n['label']) ?></span>
</a>
<?php endforeach; ?>
+101 -17
View File
@@ -1,15 +1,29 @@
<?php
/**
* 设置(仅管理员)。
*
* 与早期版本的两处不同,都是被明确要求改的:
* ① **「家庭名称」撤了**。首页标题已经是「某某某的家书」,一个家庭级名字
* 既没人看、又容易和它打架;后端 settings/update 也不再接收这个字段,
* 免得"某次提交把名字清空"。库里那一列留着不动(历史数据无害)。
* ② **slogan 变成"系统级默认"**。真正的 slogan 是每个人自己写的那句
* (在「我的资料」里),这里是**没人写自己的时候拿出来的兜底**。
* 另加:阿里云 OSS 的**连通性测试**——逐步摊开结论,
* 让"连不上"有具体原因,而不是一句"写入失败"。
*/
?>
<form method="post" action="<?= e(url('settings/update')) ?>" class="card card-pad">
<?= csrf_field() ?>
<h2 class="section-title">家庭</h2>
<h2 class="section-title">家庭 <span class="hint">首页上那一行小字</span></h2>
<div class="form-grid">
<div class="field full">
<label for="name">家庭名称</label>
<input class="input" id="name" name="name" value="<?= e($family['name']) ?>" required>
</div>
<div class="field full">
<label for="motto">一句话</label>
<label for="motto">系统 slogan</label>
<input class="input" id="motto" name="motto" value="<?= e($family['motto'] ?? '') ?>"
placeholder="展示在首页的一行小字">
placeholder="例如:一本用岁月写成的家谱">
<span class="tip">
每个人都可以在「我的资料」里写**自己的一句**,有的话就显示他自己的;
<b>没写的人才用这里的这句</b>。所以这是一句"兜底",不是全家的统一口号。
</span>
</div>
</div>
<div class="btn-row">
@@ -28,6 +42,9 @@
<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">
@@ -39,14 +56,14 @@
</label>
<label class="radio-item">
<input type="radio" name="driver" value="oss" <?= $storageDriver === 'oss' ? 'checked' : '' ?>>
<span><b>阿里云 OSS</b><br><span class="muted small">文件独立存放,换服务器不丢;也可以挂 CDN 加速。</span></span>
<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">
@@ -122,13 +139,76 @@
</div>
</form>
<form method="post" action="<?= e(url('settings/test_storage')) ?>" style="margin-top:12px">
<?= csrf_field() ?>
<button class="btn btn-ghost" type="submit">测试存储是否可用</button>
<span class="small muted" style="margin-left:10px">
会真的写一个临时文件、读回来、再删掉 —— 比只连一下可靠。
</span>
</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">
@@ -145,6 +225,10 @@
<?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>
@@ -165,4 +249,4 @@
<p class="small muted" style="margin:10px 0 0">
访客账号在「<a href="<?= e(url('accounts/invite')) ?>">邀请访客</a>」里开通。
</p>
</div>
</div>