Files
FamilyBook/templates/resources/index.php
T
laoyang 71a7ec35a0 v14: 观影/家谱/日程/旅程/资源/任务六模块统一,首页改 dashboard
用户 14 条需求一次交付(1-13 功能 + 14 工作方式)。

观影(1-4)
- 返回文案全模块统一为「返回观影」(不再有「返回列表」「返回观影记录」三种叫法)
- 观影人一律用彩色人物标签 person_chip:底色由头像主色算出来(上传时存库),
  瀑布流卡片也按人显示;五处观影模板全部改用它
- 人员显示统一走 person_name()(shown → 我给他起的称呼 → 姓名)
- 影院管理页与年度报告页的上下两块用 .card + .card 统一给间距(原来紧贴成一整块)

家谱(5)
- 树根从「我」换成**辈分最高的祖先**:库画的是"树根的祖先链+后代+树根的兄弟姐妹",
  根=我 时伯父/堂兄/嫂子/侄子一个都不画(12 人夹具只画出 8 人)
- focusMap 按**代数**圈定三代(不再按步数),normalizeLinks 把子女对称挂到父母双方
  —— 修掉"母亲登录看不到孩子"
- 新增「看全部 / 回到我」两个按钮,放在画布外(画布整块被库接管了手势)

统一事件原语(6-9)
- 旅程:TripLog 全量迁入(3 趟旅程 / 21 段行程 / 72 张照片 / 5 个人)。
  复用 events(kind='trip') + trip_details + trip_nodes(计划/执行双轨保留)
- 日程:每人可建日历本(自定义色)+ 共享只读/可写;观影与旅行是每人默认两本,
  内容是**从各模块实时派生**的(不复制,所以改了观影日期日历立刻跟着变);
  没有具体时分 = 全天;月视图参照群晖 Calendar
- 资源:挂在某人名下的附属信息(标题+正文+多附件),可共享给多人并设只读/读写,
  默认折叠、可搜标题与正文
- 任务:看板三列 + 两级子任务 + 评论@人 + 共享 + 循环(完成时按原截止日推下一期)
- 统一附件表:四个模块的附件共用一张,下载地址不落库、一律由 media.php 现算

模块化(10-11)
- App\Module 是模块的唯一来源(加一行 = 导航条/主题色/个人设置三处自动出现)
- 个人设置里可勾选要显示哪些模块、拖动排序(关掉的模块**不会**被兜底补回来)
- 每个模块一套主题色(家庭级),只覆盖 --brand 三兄弟,不碰纸墨底色

其他(12-13)
- 新增成员表单可选所属分组(编辑表单没有:分组是"我和他的关系",只归我自己)
- 首页改 dashboard:接下来的日子 / 未来七天 / 手头在做 / 往年今日。
  家人卡片从首页撤走(看人去名册)

验证(全部通过)
- e2e 542 条(新增 4 模块端到端 60+ 条)
- 桌面几何 368 / 移动端几何 520 / 家谱 12 / 交互 97 / 老库升级 123
- SQL 列名静态校验 0 处可疑(对着真实表结构)
- 反向验证 18 条新注入全部抓住

顺带修掉的既有问题
- 老库升级回填 SQL 按实际存在的列动态拼(缺列不再 1054 炸断升级)
- 全新安装时 Schema::ensure 跑在"人还没建"之前 → 补建日历本三处齐备
- 窄屏导航横向溢出(模块从 4 个加到 8 个后顶栏撑破视口)
2026-10-02 09:28:07 +08:00

231 lines
11 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
/**
* 资源(v12):挂在某人名下的附属信息(证件、照片、账号…)。
*
* 两条交互上的讲究:
* ① **默认折叠**:一屏几十条正文会很长,列表只露标题行(原生 <details>,
* 没 JS 也能开合),点开才看正文与附件。
* ② **归属于谁**:每条都挂在一个具体的人名下(不是"家庭共享"),
* 所以列表上一定看得到"这是谁的",新建时必须选归属人。
*
* @param array $rows 我能看到的资源(带 perm / owner_nick)
* @param array $people 名册(新建时选归属人、共享给别人)
* @param array $attachs resource_id => 附件列表
* @param array $shares resource_id => 共享列表
*/
$isEditing = !empty($editing);
$canWrite = \App\Auth::canWrite();
?>
<div class="page-head">
<h1>资源</h1>
<span class="sub">共 <?= count($rows) ?> 条</span>
<div class="actions">
<?php if ($canWrite): ?>
<button class="btn" type="button" id="resAddBtn"><?= $isEditing ? '收起' : '新建资源' ?></button>
<?php else: ?>
<span class="link-quiet">只读账号</span>
<?php endif; ?>
</div>
</div>
<form class="search" method="get" action="index.php">
<input type="hidden" name="r" value="resources">
<input class="input" type="text" name="keyword" value="<?= e($keyword) ?>" placeholder="搜标题或正文">
<select class="select" name="owner">
<option value="0">所有人</option>
<?php foreach ($people as $p): ?>
<option value="<?= (int)$p['id'] ?>"<?= (int)$ownerId === (int)$p['id'] ? ' selected' : '' ?>>
<?= e(person_name($p)) ?>
</option>
<?php endforeach; ?>
</select>
<button class="btn" type="submit">搜索</button>
<?php if ($keyword !== '' || $ownerId > 0): ?>
<a class="search-clear" href="<?= e(url('resources')) ?>">清除</a>
<?php endif; ?>
</form>
<?php if ($canWrite): ?>
<div class="card card-pad" id="resAddCard" <?= $isEditing ? '' : 'hidden' ?>>
<h2 class="cinema-h2"><?= $isEditing ? '编辑资源' : '新建资源' ?></h2>
<form method="post" enctype="multipart/form-data"
action="<?= e(url($isEditing ? 'resources/update' : 'resources/create')) ?>">
<?= csrf_field() ?>
<?php if ($isEditing): ?>
<input type="hidden" name="id" value="<?= (int)$editing['id'] ?>">
<?php endif; ?>
<div class="form-grid">
<div class="field">
<label for="rs-owner">归属于谁 *</label>
<select class="select" id="rs-owner" name="owner_id" required>
<?php foreach ($people as $p): ?>
<option value="<?= (int)$p['id'] ?>"
<?= (int)($editing['owner_id'] ?? (int)\App\Auth::personId()) === (int)$p['id'] ? ' selected' : '' ?>>
<?= e(person_name($p)) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="field">
<label for="rs-title">标题 *</label>
<input class="input" id="rs-title" name="title" required
value="<?= e($editing['title'] ?? '') ?>" placeholder="如:身份证 / 学位证">
</div>
<div class="field full">
<label for="rs-content">正文</label>
<textarea class="input" id="rs-content" name="content" rows="3"
placeholder="编号、有效期、放在哪里…"><?= e($editing['content'] ?? '') ?></textarea>
</div>
<div class="field full">
<label for="rs-file">附件(可多选)</label>
<input class="input" type="file" id="rs-file" name="attach[]" multiple>
</div>
</div>
<div class="btn-row">
<button class="btn" type="submit"><?= $isEditing ? '保存' : '新建' ?></button>
<?php if ($isEditing): ?>
<a class="btn btn-ghost" href="<?= e(url('resources')) ?>">取消</a>
<?php endif; ?>
</div>
</form>
</div>
<?php endif; ?>
<div class="card card-pad">
<h2 class="cinema-h2">资源列表</h2>
<?php if (!$rows): ?>
<p class="empty-hint">
<?= ($keyword !== '' || $ownerId > 0) ? '没有匹配的资源,换个条件试试。'
: '还没有资源。证件、照片、账号这些"挂在某人名下的东西"都可以记在这里。' ?>
</p>
<?php else: ?>
<div class="res-list">
<?php foreach ($rows as $r): ?>
<?php $rid = (int)$r['id']; $as = $attachs[$rid] ?? []; $sh = $shares[$rid] ?? []; ?>
<details class="res-item">
<summary class="res-head">
<span class="res-title"><?= e((string)$r['title']) ?></span>
<?php /* 归属人标签:查询里"人"的字段没加前缀(owner_name / avatar / avatar_color),
只有昵称带 owner_ 前缀 —— 在这里拼成 person_chip 认的形状。 */ ?>
<?= person_chip([
'id' => (int)$r['owner_id'], 'name' => (string)$r['owner_name'],
'my_nickname' => (string)($r['owner_nick'] ?? ''),
'avatar' => $r['avatar'] ?? null, 'avatar_color' => $r['avatar_color'] ?? null,
]) ?>
<span class="node-badge"><?= e(\App\Resource::permWord((string)$r['perm'])) ?></span>
<?php if ($as): ?><span class="res-n"><?= count($as) ?> 个附件</span><?php endif; ?>
</summary>
<div class="res-body">
<?php if (!empty($r['content'])): ?>
<p class="info-text"><?= nl2br(e((string)$r['content'])) ?></p>
<?php else: ?>
<p class="empty-hint">(没有正文)</p>
<?php endif; ?>
<?php if ($as): ?>
<div class="attach-row">
<?php foreach ($as as $a): ?>
<span class="attach-item">
<?php if (\App\Attachment::previewable($a)): ?>
<a href="<?= e(media_url((string)$a['key'])) ?>" target="_blank" rel="noopener">
<?= (int)$a['is_image'] === 1 ? '🖼' : '📄' ?> <?= e((string)($a['name'] ?: '附件')) ?>
</a>
<?php else: ?>
<?= (int)$a['is_image'] === 1 ? '🖼' : '📄' ?> <?= e((string)($a['name'] ?: '附件')) ?>
<?php endif; ?>
<span class="res-size"><?= e(\App\Attachment::sizeWord($a['size'] ?? 0)) ?></span>
<a class="link-quiet" href="<?= e(media_url((string)$a['key'], ['dl' => 1])) ?>">下载</a>
<?php if ($canWrite && \App\Resource::canWrite($r, (int)\App\Auth::personId())): ?>
<form method="post" action="<?= e(url('resources/attach_del')) ?>" class="res-inline">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$a['id'] ?>">
<button class="btn btn-ghost btn-sm" type="submit"
onclick="return confirm('删掉这个附件?')">删除</button>
</form>
<?php endif; ?>
</span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($sh): ?>
<div class="res-shares">
<span class="res-label">共享给</span>
<?php foreach ($sh as $s): ?>
<?= person_chip($s) ?>
<span class="node-badge"><?= ($s['perm'] ?? '') === 'write' ? '可读写' : '只读' ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($canWrite && \App\Resource::canWrite($r, (int)\App\Auth::personId())): ?>
<div class="res-ops">
<?php /* 补传附件 / 共享设置 / 编辑 / 删除 —— 每个都是独立的小表单,
不套在别的表单里(<form> 嵌套会让浏览器把外层切开)。 */ ?>
<form class="inline-form" method="post" enctype="multipart/form-data"
action="<?= e(url('resources/attach_add')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $rid ?>">
<input type="file" name="attach[]" multiple required>
<button class="btn btn-ghost btn-sm" type="submit">补传附件</button>
</form>
<details class="res-share-add">
<summary class="link-quiet">共享设置</summary>
<form class="inline-form" method="post" action="<?= e(url('resources/share')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $rid ?>">
<select class="select" name="person_id">
<?php foreach ($people as $p): ?>
<?php if ((int)$p['id'] === (int)$r['owner_id']) { continue; } ?>
<option value="<?= (int)$p['id'] ?>"><?= e(person_name($p)) ?></option>
<?php endforeach; ?>
</select>
<select class="select" name="perm">
<option value="read">只读</option>
<option value="write">可读写</option>
</select>
<button class="btn btn-sm" type="submit">共享</button>
</form>
<?php foreach ($sh as $s): ?>
<form class="inline-form" method="post" action="<?= e(url('resources/unshare')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $rid ?>">
<input type="hidden" name="person_id" value="<?= (int)$s['person_id'] ?>">
<span class="cal-share-name"><?= e(person_name($s)) ?></span>
<button class="btn btn-ghost btn-sm" type="submit">取消共享</button>
</form>
<?php endforeach; ?>
</details>
<a class="btn btn-ghost btn-sm" href="<?= e(url('resources', ['edit' => $rid])) ?>">编辑</a>
<form method="post" action="<?= e(url('resources/destroy')) ?>"
onsubmit="return confirm('删除「<?= e((string)$r['title']) ?>」?附件会一起删掉。')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $rid ?>">
<button class="btn btn-danger btn-sm" type="submit">删除</button>
</form>
</div>
<?php endif; ?>
</div>
</details>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php if ($canWrite): ?>
<script>
(function () {
var btn = document.getElementById('resAddBtn');
var card = document.getElementById('resAddCard');
if (!btn || !card) return;
btn.addEventListener('click', function () {
card.hidden = !card.hidden;
btn.textContent = card.hidden ? '新建资源' : '收起';
if (!card.hidden) { card.scrollIntoView({ block: 'nearest' }); }
});
})();
</script>
<?php endif; ?>