Files
FamilyBook/templates/books/index.php
T
laoyang 7138314dc7 v24 第 15 批:活动 / 旅程两级语义(术语 · 派生时间城市 · 表单三项 · 题图可点)
用户原话四条,逐条落地:

1. 「旅程模块,按钮是记一次活动,而不是记一趟旅程。旅程是活动里面的概念。」
   → 顶层一律改称**活动**:按钮「记一次活动」、页标题「活动」、`N 次` / `N 段旅程`;
     活动详情三组改叫「计划的旅程 / 走过的旅程 / 没成行的」;
     节点表单「这段旅程叫什么」「阶段」;flash 文案 17 处同步。
     (导航条仍叫「旅程」——用户就是这么称呼这个模块,列表页加副标题说清两级关系。)

2. 「活动不需要开始和结束时间,是根据活动中的旅程,最早的旅程开始时间,和最晚的
     旅程结束时间,计算出来的。目的地也不需要…去的城市,也是根据旅程,得到的。」
   → 活动**不再存**这三样,全部**读时算出来**:
     · Trip::derived()      最早开始 / 最晚结束 / 城市去重
     · Trip::cities()       按最早出现时间排(先后顺序本身是信息,不是字母序)
     · Trip::attachDerived() 列表页批量补(避免 N+1)
     · Trip::activitySpan()  没有旅程时说「还没有旅程」,不用「日期待补」
     create/update 不再写 events 的起止与地点(老值保留不动,页面不读)。
     列表排序/搜索、首页「最近去了」改走子查询。

3. 「题图那里,没有办法上传图片,点击哪里都没反应。」——**真 bug**
   「点图 = 开文件框」那句 JS 只写在 people/form.php 的内页脚本里(头像专用);
   第 14 批加的旅程题图与图书封面照抄了同一套 DOM 却没有对应的 JS,
   于是图块有 cursor:pointer 与 hover 高亮(看着能点),点下去毫无反应。
   → 做成声明式通用绑定:图上写 data-pick-input="#fileInputId",
     app.js 统一接管(一个页面多个也各绑各的),并删掉旧的那份专用实现。
     三处(头像 / 题图 / 图书封面)全挂上。

4. 「添加活动的页面,顺序是这样的:添加标题,上传并裁剪题图,然后一段简介,就可以了。」
   → 主区就这三项,顺序照原话;时间/城市不做输入项(只留一句说明它们从哪来,
     不说清用户会满页找"怎么没地方填时间")。
     备注 / 公开 / 同行的人收进「更多设置」折叠区——功能一个没丢。

顺带修掉两处会炸的实现问题:
· ORDER BY 引用聚合别名(first_at)在 MariaDB/MySQL 报 1247,且只在运行时炸、
  php -l 看不出 → 改成 ORDER BY MIN(starts_at),否则整个旅程模块 Fatal。
· e2e 里"详情页显示了派生时间"原本写成整页查找,下面每段旅程自己也渲染日期,
  于是把派生值塞成 null 也照样绿(借光假绿)→ 判据收进页头那一段。

验证:e2e 845/0(+15 条本批断言)· 升级 134/0 · 桌面几何 428/0 · 移动几何 688/0 ·
交互 92/0 · 题图真机点击 4/0 · 读书窄屏 15/0 · 家谱 34/0 + 10/0 + 反向 3/3 ·
观影按钮 60/0 + 反向 3/3 · 本批反向验证 4/4(题图绑定抽掉→红、派生值不挂→红、
城市退回读 location→红、时间字段加回表单→红)· 部署静态断言新增两组(含"活动不许
再写起止时间/目的地"与"ORDER BY 不许引用聚合别名"的反退化防线)。
部署后服务器自检 19/19,五个关键文件 md5 与线上逐一核对一致。
2026-10-05 10:58:17 +08:00

165 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
/**
* 书架(v24 第 14 批 用户第 5 条)。
*
* 一眼能看出三件事:**有多少书 / 现在在读哪几本 / 哪些读完了**。
* 所以状态筛选条在最上面,卡片上标着状态徽标与评分。
*
* @param array $rows 书(已带 note_count)
* @param array $cats 用过的分类(筛选用)
* @param array $counts 各状态的数量
* @param array $people 名册(筛"谁的书")
* @param string $status 当前状态筛选
* @param string $cat 当前分类筛选
* @param string $q 关键词
* @param int $owner 当前"谁的书"筛选
*/
$canWrite = \App\Auth::canWrite();
$counts = $counts ?? [];
$status = (string)($status ?? '');
$cat = (string)($cat ?? '');
$q = (string)($q ?? '');
$owner = (int)($owner ?? 0);
$people = $people ?? [];
$stList = ['' => '全部'] + \App\Book::STATUS;
?>
<div class="page-head">
<h1>读书</h1>
<span class="sub">
共 <?= (int)($counts['_all'] ?? 0) ?> 本<?= (int)($counts['reading'] ?? 0) > 0
? ' · ' . (int)$counts['reading'] . ' 本在读' : '' ?>
</span>
<div class="actions">
<?php if ($canWrite): ?>
<a class="btn" href="<?= e(url('books/new')) ?>">添加图书</a>
<?php else: ?>
<span class="link-quiet">只读账号</span>
<?php endif; ?>
<?php $bkSetCurrent = 'list'; require __DIR__ . '/_settings_menu.php'; ?>
</div>
</div>
<?php /* 状态筛选条:一段一个筛选,都在同一行(窄屏自动换行)。
这四个状态就是用户说的"每个任务都有状态"在读书这边的对应物。 */ ?>
<div class="book-filter">
<?php foreach ($stList as $k => $v): ?>
<a class="bf-btn<?= $status === (string)$k ? ' is-on' : '' ?>"
href="<?= e(url('books', array_filter([
'status' => (string)$k, 'cat' => $cat, 'q' => $q,
'owner' => $owner > 0 ? (string)$owner : '',
], static fn($x) => (string)$x !== ''))) ?>">
<?= e($v) ?><?= $k !== '' ? ' <span class="bf-n">' . (int)($counts[$k] ?? 0) . '</span>' : '' ?>
</a>
<?php endforeach; ?>
</div>
<form class="search" method="get" action="index.php">
<input type="hidden" name="r" value="books">
<?php if ($status !== ''): ?><input type="hidden" name="status" value="<?= e($status) ?>"><?php endif; ?>
<input class="input" type="text" name="q" value="<?= e($q) ?>" placeholder="搜书名 / 作者 / 出版社 / 标签 / ISBN">
<?php if ($cats): ?>
<select class="select" name="cat">
<option value="">全部分类</option>
<?php foreach ($cats as $c): ?>
<option value="<?= e((string)$c['name']) ?>" <?= $cat === (string)$c['name'] ? 'selected' : '' ?>>
<?= e((string)$c['name']) ?>(<?= (int)$c['n'] ?>)
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
<?php if ($people): ?>
<select class="select" name="owner">
<option value="0">所有人的书</option>
<?php foreach ($people as $p): ?>
<option value="<?= (int)$p['id'] ?>" <?= $owner === (int)$p['id'] ? 'selected' : '' ?>>
<?= e(person_name($p)) ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
<button class="btn" type="submit">筛选</button>
<?php if ($q !== '' || $cat !== '' || $owner > 0 || $status !== ''): ?>
<a class="search-clear" href="<?= e(url('books')) ?>">清除</a>
<?php endif; ?>
</form>
<?php if (!$rows): ?>
<div class="card card-pad empty">
<b><?= ($q !== '' || $cat !== '' || $owner > 0 || $status !== '') ? '没有匹配的书' : '书架还是空的' ?></b>
<?php if ($q !== '' || $cat !== '' || $owner > 0 || $status !== ''): ?>
换个条件试试,或清除筛选看全部。
<?php else: ?>
<?php if ($canWrite): ?>
点右上角「添加图书」—— 可以上传 EPUB / TXT 让它自己读出书名作者与正文,
也可以只手填一条"想读的书"。
<?php else: ?>
家人还没有往书架上放书。
<?php endif; ?>
<?php endif; ?>
</div>
<?php else: ?>
<div class="book-grid">
<?php foreach ($rows as $b): ?>
<?php
$bid = (int)$b['id'];
$st = (string)($b['status'] ?? 'wish');
$rate = $b['rating'] === null ? null : (int)$b['rating'];
?>
<div class="book-card">
<a class="book-cover" href="<?= e(url('books/show', ['id' => $bid])) ?>">
<?php if (!empty($b['cover_key'])): ?>
<img src="<?= e(media_url((string)$b['cover_key'])) ?>" alt="">
<?php else: ?>
<span class="book-cover-empty">📖</span>
<?php endif; ?>
</a>
<div class="book-body">
<a class="book-title" href="<?= e(url('books/show', ['id' => $bid])) ?>">
<?= e((string)$b['title']) ?>
</a>
<?php if (!empty($b['author'])): ?>
<div class="book-author"><?= e((string)$b['author']) ?></div>
<?php endif; ?>
<div class="book-meta">
<span class="kb-st is-<?= e($st === 'reading' ? 'doing' : ($st === 'done' ? 'done' : 'todo')) ?> is-static">
<?= e(\App\Book::statusWord($st)) ?>
</span>
<?php if ($rate !== null): ?>
<span class="book-rate" title="评分"><?= $rate ?>/10</span>
<?php endif; ?>
<?php if ((int)($b['note_count'] ?? 0) > 0): ?>
<span class="book-note-n" title="笔记">✎ <?= (int)$b['note_count'] ?></span>
<?php endif; ?>
<?php if ((int)($b['chapter_count'] ?? 0) > 0): ?>
<span class="small muted"><?= (int)$b['chapter_count'] ?> 章</span>
<?php endif; ?>
</div>
<?php if ($st === 'reading' && (int)($b['chapter_count'] ?? 0) > 0 && (int)($b['progress'] ?? 0) > 0): ?>
<?php $pct = min(100, (int)round((int)$b['progress'] * 100 / max(1, (int)$b['chapter_count']))); ?>
<div class="bk-bar"><i style="width: <?= $pct ?>%"></i></div>
<span class="small muted">读到第 <?= (int)$b['progress'] ?> 章</span>
<?php endif; ?>
<div class="book-foot">
<?php if (!empty($b['file_key'])): ?>
<a class="btn btn-sm" href="<?= e(url('books/read', ['id' => $bid])) ?>">读</a>
<?php endif; ?>
<?php if ($canWrite): ?>
<a class="btn btn-ghost btn-sm" href="<?= e(url('books/edit', ['id' => $bid])) ?>">改</a>
<?php if ($st !== 'done'): ?>
<form method="post" action="<?= e(url('books/status')) ?>" class="res-inline">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= $bid ?>">
<input type="hidden" name="status" value="done">
<?php /* 从列表点的 → 做完还回列表(不然会莫名跳进详情页) */ ?>
<input type="hidden" name="back" value="list">
<button class="btn btn-ghost btn-sm" type="submit">读完</button>
</form>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>