Files
FamilyBook/templates/settings/_profile.php
T
laoyang 1c460a0e83 图片可裁剪 · 名册按关系分组 · 个人设置独立 · 顶栏下拉菜单
① 上传图片一律可裁剪,比例由图位决定
   新增 app/ImageSlot.php 统一配置(头像 1:1/512×512;题图 16:9 只留注释,
   将来启用只加一行)。前端只挑区域、写回归一化矩形 "x,y,w,h",真正裁剪在服务端
   Upload::fitRegion():夹回图内 → 在框内居中收缩到目标比例(不外扩)→ 钉住 sx+sw ≤ w。
   比例解析单独写 parseRatioAttr():parseFloat('16:9') 得 16,会把题图裁成细长条且不报错。

② 名册按关系分组,顺序每人可自定
   分类全家共用(person_groups,另一个人可以自己加分类);「自己的」只体现在顺序上
   (user_settings.group_order,每人一份)。persons.group_id 故意不加外键(老库只能
   ADD COLUMN),完整性由 PersonGroup 保证:删分类把人置回未分类,人不会从名册消失。

③ 设置分两级:系统设置 / 个人设置
   个人设置拆成 templates/settings/me.php + _profile.php + _nicknames.php,
   取代 templates/people/me.php(旧路由与旧表单仍兼容)。

④ 右上角「姓名/头像 + 下拉菜单」
   原来是两个裸链接,窄屏会被 CSS 藏掉导致改密码/退出不可达;改成 <details> 零 JS 菜单
   (app.js 只补「点外面 / Esc」)。菜单项:个人设置 / 系统设置(管理员)/ 修改密码 / 退出登录。

Schema 版本 3 → 4;安装与老库升级共用 PersonGroup::backfillMissing() 一条回填路径。

验证:php -l 全量;e2e 202/0(新增「关系分组」与「图片裁剪」两节);老库升级 49/0;
桌面几何 108/0;移动端几何 192/0(360/375/390/414);交互 76/0;
反向验证 31/31(逐条把修复改回旧写法,指定的目标断言必须变红);
部署后本地↔线上 72 个文件 sha256 逐字节一致。
2026-09-30 11:06:22 +08:00

154 lines
7.1 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
/**
* 「我的资料」表单(个人设置的第一块)。
*
* 只放**属于我自己的**东西:
* · 我的姓名、性别、头像、联系方式、备注
* · 我的三个生日(身份证生日只读,由证件推导)
* · 我的 slogan(没填就用家庭统一的那句)
* 排行、排序号、虚拟人物标记、以及会牵动身份证生日的身份证号,
* 都属于"管理员编名册",不在这里(这也是为了不让一次自助提交悄悄改掉这些)。
*
* 作为独立片段是因为「个人设置」页还要接上称呼、分组顺序、分类管理三块,
* 全塞一个文件里会变成没人愿意读的六百行。
*/
$p = $person;
$avatarUrl = avatar_url($p['avatar'] ?? null);
$bdSource = old('birthday_source', 'solar');
$idBd = trim((string)($p['birthday_id_card'] ?? ''));
// 裁剪比例由「这个位置显示成什么形状」决定,前后端共用同一份配置(见 App\ImageSlot)
$avSlot = \App\ImageSlot::get('avatar');
?>
<form method="post" enctype="multipart/form-data" action="<?= e(url('settings/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/*"
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,相对原图)。不裁/没 JS 就留空 = 居中裁切 */ ?>
<input type="hidden" name="avatar_crop" value="">
<span class="tip">
选好图片后会弹出裁剪框,把要留的部分框住。<b>比例按它在页面上的形状定</b>
(头像是 1:1 正方形)。上限 5MB。
</span>
<?php if (!empty($errors['avatar'])): ?><span class="err"><?= e($errors['avatar']) ?></span><?php endif; ?>
</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>