Files
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

221 lines
9.8 KiB
PHP
Raw Permalink 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 $b 书
* @param array $chapters 章节目录(没有正文)
* @param array $notes 笔记
*/
$canWrite = \App\Auth::canWrite();
$meId = (int)\App\Auth::personId();
$st = (string)($b['status'] ?? 'wish');
$chapters = $chapters ?? [];
$notes = $notes ?? [];
$hasFile = !empty($b['file_key']);
$readable = $hasFile && ((int)($b['chapter_count'] ?? 0) > 0 || \App\BookParser::inlinable($b['file_format'] ?? ''));
?>
<div class="page-head">
<h1><?= e((string)$b['title']) ?></h1>
<span class="sub">
<?php if (!empty($b['author'])): ?><?= e((string)$b['author']) ?> · <?php endif; ?>
<?= e(\App\Book::statusWord($st)) ?>
</span>
<div class="actions">
<?php if ($readable): ?>
<a class="btn" href="<?= e(url('books/read', ['id' => (int)$b['id']])) ?>">
<?= (int)($b['progress'] ?? 0) > 0 ? '继续读' : '开始读' ?>
</a>
<?php endif; ?>
<?php if ($canWrite): ?>
<a class="btn btn-ghost" href="<?= e(url('books/edit', ['id' => (int)$b['id']])) ?>">编辑</a>
<form method="post" action="<?= e(url('books/destroy')) ?>" class="res-inline"
onsubmit="return confirm('把《<?= e((string)$b['title']) ?>》从书架移走吗?文件与笔记会一起删掉,不可恢复。')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$b['id'] ?>">
<button class="btn btn-danger" type="submit">移出书架</button>
</form>
<?php endif; ?>
<a class="btn btn-ghost" href="<?= e(url('books')) ?>">返回书架</a>
<?php $bkSetCurrent = 'show'; require __DIR__ . '/_settings_menu.php'; ?>
</div>
</div>
<div class="card card-pad book-hero">
<div class="book-hero-cover">
<?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; ?>
</div>
<div class="book-hero-body">
<?php if (!empty($b['subtitle'])): ?>
<p class="book-sub"><?= e((string)$b['subtitle']) ?></p>
<?php endif; ?>
<?php /* 状态的一排快捷:想读 → 在读 → 读完(与任务卡片上那个徽标同款思路,
都是"点一下就变",不用进编辑页)。 */ ?>
<div class="book-states">
<?php foreach (\App\Book::STATUS as $sk => $sv): ?>
<?php if ($canWrite): ?>
<form method="post" action="<?= e(url('books/status')) ?>" class="res-inline">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$b['id'] ?>">
<input type="hidden" name="status" value="<?= e($sk) ?>">
<button class="kb-st is-<?= e($sk === 'reading' ? 'doing' : ($sk === 'done' ? 'done' : 'todo')) ?><?= $st === $sk ? ' is-on' : '' ?>"
type="submit" title="标成「<?= e($sv) ?>」"><?= e($sv) ?></button>
</form>
<?php else: ?>
<span class="kb-st is-<?= e($sk === 'reading' ? 'doing' : ($sk === 'done' ? 'done' : 'todo')) ?><?= $st === $sk ? ' is-on' : '' ?> is-static"><?= e($sv) ?></span>
<?php endif; ?>
<?php endforeach; ?>
</div>
<dl class="kv">
<?php if (!empty($b['author'])): ?>
<div class="kv-row"><dt>作者</dt><dd><?= e((string)$b['author']) ?></dd></div>
<?php endif; ?>
<?php if (!empty($b['translator'])): ?>
<div class="kv-row"><dt>译者</dt><dd><?= e((string)$b['translator']) ?></dd></div>
<?php endif; ?>
<?php if (!empty($b['publisher'])): ?>
<div class="kv-row"><dt>出版社</dt><dd><?= e((string)$b['publisher']) ?></dd></div>
<?php endif; ?>
<?php if (!empty($b['published'])): ?>
<div class="kv-row"><dt>出版时间</dt><dd><?= e((string)$b['published']) ?></dd></div>
<?php endif; ?>
<?php if (!empty($b['category'])): ?>
<div class="kv-row"><dt>分类</dt><dd><?= e((string)$b['category']) ?></dd></div>
<?php endif; ?>
<?php if (!empty($b['tags'])): ?>
<div class="kv-row"><dt>标签</dt><dd><?= e((string)$b['tags']) ?></dd></div>
<?php endif; ?>
<?php if ((int)($b['pages'] ?? 0) > 0): ?>
<div class="kv-row"><dt>页数</dt><dd><?= (int)$b['pages'] ?> 页</dd></div>
<?php endif; ?>
<?php if ((int)($b['words'] ?? 0) > 0): ?>
<div class="kv-row"><dt>字数</dt><dd><?= e(\App\Book::wordsWord($b['words'])) ?></dd></div>
<?php endif; ?>
<?php if ($b['rating'] !== null): ?>
<div class="kv-row"><dt>评分</dt><dd><?= (int)$b['rating'] ?> / 10</dd></div>
<?php endif; ?>
<?php if (!empty($b['started_at'])): ?>
<div class="kv-row"><dt>开始读</dt><dd><?= e((string)$b['started_at']) ?></dd></div>
<?php endif; ?>
<?php if (!empty($b['finished_at'])): ?>
<div class="kv-row"><dt>读完于</dt><dd><?= e((string)$b['finished_at']) ?></dd></div>
<?php endif; ?>
<?php if (!empty($b['file_key'])): ?>
<div class="kv-row">
<dt>文件</dt>
<dd>
<?= e(\App\BookParser::formatWord((string)($b['file_format'] ?? ''))) ?>
<?php if ((int)($b['file_size'] ?? 0) > 0): ?>
· <?= e(\App\Book::sizeWord($b['file_size'])) ?>
<?php endif; ?>
· <a class="link-quiet" href="<?= e(media_url((string)$b['file_key'], ['dl' => 1])) ?>">下载</a>
</dd>
</div>
<?php endif; ?>
<?php if (!empty($b['owner_name'])): ?>
<div class="kv-row"><dt>在读的人</dt><dd><?= e((string)$b['owner_name']) ?></dd></div>
<?php endif; ?>
</dl>
<?php if (!empty($b['summary'])): ?>
<div class="book-sum">
<h2 class="cinema-h2">内容简介</h2>
<p class="info-text"><?= nl2br(e((string)$b['summary'])) ?></p>
</div>
<?php endif; ?>
<?php if ($chapters): ?>
<div class="book-toc-box">
<h2 class="cinema-h2">目录(<?= count($chapters) ?> 章)</h2>
<ol class="book-toc">
<?php foreach ($chapters as $c): ?>
<li<?= (int)$c['seq'] === (int)($b['progress'] ?? 0) ? ' class="is-cur"' : '' ?>>
<a href="<?= e(url('books/read', ['id' => (int)$b['id'], 'seq' => (int)$c['seq']])) ?>">
<?= e((string)$c['title']) ?>
</a>
<?php if ((int)$c['words'] > 0): ?>
<span class="small muted"><?= (int)$c['words'] ?> 字</span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ol>
</div>
<?php endif; ?>
</div>
</div>
<?php /* ==================== 笔记 ==================== */ ?>
<div class="card card-pad">
<h2 class="cinema-h2">
笔记与划线(<?= count($notes) ?>)
<span class="hint">读的时候留下的字;也可以从微信读书等 App 导入</span>
</h2>
<?php if ($canWrite): ?>
<form method="post" action="<?= e(url('books/note_add')) ?>" class="book-note-form">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$b['id'] ?>">
<div class="bn-row">
<select class="select" name="kind" aria-label="笔记类型">
<?php foreach (\App\Book::NOTE_KINDS as $kk => $kv): ?>
<option value="<?= e($kk) ?>"><?= e($kv) ?></option>
<?php endforeach; ?>
</select>
<input class="input" name="chapter" placeholder="在第几章(可不填)">
</div>
<textarea class="input" name="content" rows="3" required
placeholder="记一句想法,或把原文抄下来…"></textarea>
<div class="btn-row">
<button class="btn btn-sm" type="submit">记下来</button>
<a class="btn btn-ghost btn-sm" href="<?= e(url('books/notes_import')) ?>">批量导入</a>
</div>
</form>
<?php endif; ?>
<?php if (!$notes): ?>
<p class="empty-hint">还没有笔记。读到哪记到哪,以后翻起来才有意思。</p>
<?php else: ?>
<ul class="book-notes">
<?php foreach ($notes as $n): ?>
<?php
$kind = (string)($n['kind'] ?? 'note');
$mine = (int)($n['person_id'] ?? 0) === $meId;
?>
<li class="book-note is-<?= e($kind) ?>">
<div class="bn-head">
<span class="bn-kind"><?= e(\App\Book::kindWord($kind)) ?></span>
<?php if (!empty($n['chapter'])): ?>
<span class="bn-ch"><?= e((string)$n['chapter']) ?></span>
<?php endif; ?>
<?php if ((string)($n['source'] ?? 'manual') !== 'manual'): ?>
<span class="bn-src">来自 <?= e(\App\Book::sourceWord((string)$n['source'])) ?></span>
<?php endif; ?>
<?php if (!empty($n['person_name'])): ?>
<span class="bn-who"><?= e((string)($n['person_name'])) ?></span>
<?php endif; ?>
<span class="bn-time small muted"><?= e(substr((string)$n['created_at'], 0, 16)) ?></span>
<?php if ($canWrite && ($mine || (int)($n['person_id'] ?? 0) === 0)): ?>
<form method="post" action="<?= e(url('books/note_del')) ?>" class="res-inline bn-del">
<?= csrf_field() ?>
<input type="hidden" name="note_id" value="<?= (int)$n['id'] ?>">
<button class="btn btn-ghost btn-sm" type="submit"
onclick="return confirm('删掉这条笔记?')">删</button>
</form>
<?php endif; ?>
</div>
<div class="bn-body"><?= nl2br(e((string)$n['content'])) ?></div>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>