Files
FamilyBook/templates/people/index.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

163 lines
6.8 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
/**
* 家庭名册 —— 按「关系分组」分栏显示。
*
* 两个刻意的设计:
* ① **分栏顺序由看的人自己定**($groups 已经是"按我的习惯排好"的顺序,
* 见 App\PersonGroup::orderFor)—— 所以我看到的「家人」在第一栏,
* 妈妈可以把「亲戚」挪到第一栏,两个人相安无事。
* ② 分类是全家共用的一套,所以**改一个人的分组是全家的信息**,
* 这里只有能写的人(管理员 / 家庭成员)才看得到那一行「调整分组」。
*
* ⚠️ 卡片结构是 div > a + details,**不能写成 a > form**:
* 表单套在链接里是非法 HTML,浏览器会把 DOM 拆开,表单里那个「移动」按钮
* 可能连提交机会都没有(或者把整张卡片点成跳转)。
*/
$canMove = \App\Auth::canWrite();
$total = 0;
foreach ($grouped as $list) {
$total += count($list);
}
/**
* 画一栏人。用闭包是因为"分好组的各栏"和"未分类"两处要画同一套卡片,
* 复制一遍迟早会改漏一处。
*/
$renderGrid = function (array $list) use ($groups, $meId, $canMove): void {
if (!$list) {
return;
}
?>
<div class="people-grid">
<?php foreach ($list as $p): ?>
<?php
// 显示名走「我怎么称呼他」:同一个孩子,爸爸看到"儿子"、妈妈看到"小宝"。
// ⚠️ 不要再读 $p['nickname'] —— 那是已废弃的旧列(全家共用一个小名)。
$pshow = (string)($p['shown'] ?? '');
if ($pshow === '') {
$pshow = (string)$p['name'];
}
$pNick = (string)($p['my_nickname'] ?? '');
$pid = (int)$p['id'];
?>
<div class="person">
<a class="person-main" href="<?= e(url('people/show', ['id' => $pid])) ?>">
<div class="ph"><img src="<?= e(avatar_url($p['avatar'])) ?>" alt=""></div>
<div class="pb">
<div class="pname">
<?= e($pshow) ?>
<?php if ($pid === (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">
<?php if (!empty($p['label']) && $p['label'] !== '我'): ?>
<span class="rel-label"><?= e($p['label']) ?></span>
<?php endif; ?>
<?php
$r = $p['account_role'] ?? '';
if ($r === 'admin' && ($p['account_status'] ?? '') === 'active') {
echo '<span class="badge badge-admin">管理员</span>';
} elseif ($r === 'guest') {
echo '<span class="badge">访客</span>';
} 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>';
}
?>
</div>
</div>
</a>
<?php if ($canMove): ?>
<details class="person-more">
<summary>调整分组</summary>
<form method="post" action="<?= e(url('people/group')) ?>" class="person-move">
<?= csrf_field() ?>
<input type="hidden" name="person_id" value="<?= $pid ?>">
<label class="sr-only" for="pg<?= $pid ?>">分组</label>
<select class="select" id="pg<?= $pid ?>" name="group_id">
<option value="">未分类</option>
<?php foreach ($groups as $g): $gid = (int)$g['id']; ?>
<option value="<?= $gid ?>"
<?= (int)($p['group_id'] ?? 0) === $gid ? 'selected' : '' ?>>
<?= e((string)$g['name']) ?>
</option>
<?php endforeach; ?>
</select>
<button class="btn btn-ghost btn-sm" type="submit">移动</button>
</form>
</details>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php
};
?>
<div class="page-head">
<h1>家庭名册</h1>
<span class="sub">共 <?= $total ?> 人<?= $groups && $q === '' ? ',分 ' . count($groups) . ' 个关系分组' : '' ?></span>
<div class="actions">
<?php if (\App\Auth::isAdmin()): ?>
<a class="btn btn-sm" href="<?= e(url('people/new')) ?>">添加成员</a>
<?php endif; ?>
</div>
</div>
<form class="search" method="get" action="index.php">
<input type="hidden" name="r" value="people">
<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>
<?php endif; ?>
</form>
<?php if (!$people): ?>
<div class="empty">
<b><?= $q !== '' ? '没有匹配的人' : '名册还是空的' ?></b>
<?= $q !== '' ? '换个词试试。' : '把家里人加进来,之后观影、旅行的同行人就可以直接从名册里选。' ?>
</div>
<?php elseif ($q !== ''): ?>
<?php /* 搜索时按"命中的人"平铺 —— 分栏会把一次搜索拆成一堆只有一两个人的碎块,反而更难看 */ ?>
<p class="small muted" style="margin:0 0 12px">
找到 <?= count($people) ?> 个匹配「<?= e($q) ?>」的人(搜索时不分组)。
</p>
<?php $renderGrid($people); ?>
<?php else: ?>
<?php foreach ($groups as $g): ?>
<?php
$gid = (int)$g['id'];
$list = $grouped[$gid] ?? [];
// 空分组不画 —— 七个预置分类里家里可能只用到三个,
// 把空栏也摊出来会让名册看上去破破烂烂的
if (!$list) {
continue;
}
?>
<section class="group-block">
<h2 class="group-head">
<span class="group-title"><?= e((string)$g['name']) ?></span>
<span class="group-n"><?= count($list) ?> 人</span>
</h2>
<?php $renderGrid($list); ?>
</section>
<?php endforeach; ?>
<?php if (!empty($grouped[0])): ?>
<section class="group-block">
<h2 class="group-head">
<span class="group-title">未分类</span>
<span class="group-n"><?= count($grouped[0]) ?> 人</span>
</h2>
<p class="small muted" style="margin:-4px 0 12px">
这些人还没归到任何分组里。管理员可以在卡片下面直接选一个分组(或去「编辑成员」里改)。
</p>
<?php $renderGrid($grouped[0]); ?>
</section>
<?php endif; ?>
<?php endif; ?>