1. 独立页(登录/安装/错误)套上 HTML 骨架 —— 修「首页为什么没有样式」: 新增 templates/layout-bare.php,View::render 对独立页也走完整 <head> + CSS。 2. 以个人为中心:Kinship::label() 现算称呼、Auth::homeTitle() 出「X的家书」; 同一批人在不同登录者眼里自动变成公公婆婆 / 岳父岳母(不存一次性称谓)。 3. 附件存储可配置:Storage 门面(local / 阿里云 OSS),库里只存相对键, 云端失败自动回退本地;设置页可选驱动并填 OSS 参数(照 MovieLog 的方案)。 4. 三种角色:admin / member / guest,访客只读、写操作一律 403; accounts.role 枚举扩容,老库靠 Schema::ensure() 自动升级。 5. 家谱:只存相邻两代的边(parent / spouse),其余称呼全部推导 —— 兄弟姐妹不存边(共同父母 + birth_rank 排行推长子/次女、哥哥/妹妹), 认不清的人用虚拟人物 is_placeholder 占位,关系天然双向。 顺带修掉本地 e2e 挖出的 8 个线上级 bug(性别写死、长幼方向反、关系方向静默写反、 birth_rank 不从表单取回、隐式可空参数致响应头失效、无文件字段被误判为上传失败、 layout 变量撞车覆盖 $me、世代 0 显示成「下 0 代」)。 验证:kinship 单测 37/0 · HTTP e2e 81/0 · 老库升级专测 14/0 · 反向验证 11/11(把每个修复改回旧写法,对应断言都会变红)。
168 lines
7.8 KiB
PHP
168 lines
7.8 KiB
PHP
<form method="post" action="<?= e(url('settings/update')) ?>" class="card card-pad">
|
|
<?= csrf_field() ?>
|
|
<h2 class="section-title">家庭</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>
|
|
<input class="input" id="motto" name="motto" value="<?= e($family['motto'] ?? '') ?>"
|
|
placeholder="展示在首页的一行小字">
|
|
</div>
|
|
</div>
|
|
<div class="btn-row">
|
|
<button class="btn" type="submit">保存</button>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- ==================== 附件存储 ==================== -->
|
|
<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">
|
|
数据库里只存「相对路径」,所以随时可以在本地与云端之间切换 ——
|
|
已上传的文件会按「本地优先」的顺序去找,切换后老文件照样能打开,出问题也能随时切回来。
|
|
</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">文件独立存放,换服务器不丢;也可以挂 CDN 加速。</span></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($storageDriver === 'oss' && !$storageOk): ?>
|
|
<div class="flash"><div class="flash-item err">
|
|
云端配置还不完整,系统现在**临时用本地存储**在跑 —— 填全下面的信息再保存。
|
|
</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>
|
|
|
|
<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: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><?= (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>
|