Files
FamilyBook/templates/books/weread.php
T

335 lines
15 KiB
PHP
Raw Normal View History

<?php
/**
* 微信读书导入(v24 第 24 批 用户第 5 条;**第 25 批 用户第 8 条加了同步与统计**)。
*
* 用户第 8 条原话:「微信读书导入,需要能够做到定期完成,自动同步。
* 现在可以导入了,不过封面图看不到,另外也缺少一些对微信读书的
* 统计功能,需要加强。」
*
* ⇒ 这一页现在四块:
* ① 授权(填 API Key)
* ② **同步**(自动开关 + 立即同步 + 上次/下次时间)—— 第 8 条新增
* ③ 拉取预览(勾选要导的书;**每本带封面图**)—— 封面是第 8 条修的
* ④ **统计**(阅读时长 / 天数 / 偏好分类 / 读得最多的书)—— 第 8 条新增
* ⑤ 另一条路:手动粘贴
*
* @param array|null $preview 「拉取预览」的结果(每本书 + 前几条笔记 + 封面)
* @param int $nbCount 预览时一共发现多少本有笔记的书
* @param array $sync 本次自动同步的结果(ran/books/covers/notes/error)
* @param array $stats 微信读书的阅读统计(WeRead::readStats)
*/
$preview = $preview ?? null;
$nbCount = (int)($nbCount ?? 0);
$me = (int)\App\Auth::personId();
$hasKey = \App\WeRead::hasKey($me);
$masked = \App\WeRead::maskedKey($me);
$canWrite = \App\Auth::canWrite();
$sync = (array)($sync ?? []);
$stats = (array)($stats ?? []);
$autoOn = \App\WeRead::autoSyncOn($me);
/** 秒 → 「X 小时 Y 分钟」(官方所有时长字段都是秒,这里统一转人话) */
$durWord = static function (int $sec): string {
if ($sec <= 0) {
return '—';
}
$h = intdiv($sec, 3600);
$m = intdiv($sec % 3600, 60);
if ($h > 0) {
return $h . ' 小时' . ($m > 0 ? ' ' . $m . ' 分钟' : '');
}
if ($m > 0) {
return $m . ' 分钟';
}
return $sec . ' 秒';
};
?>
<div class="page-head">
<h1>微信读书导入</h1>
<span class="sub">填一次 API Key,之后就能直接把你微信读书里的划线与想法导进来</span>
<div class="actions">
<?php $bkSetCurrent = 'import'; require __DIR__ . '/_settings_menu.php'; ?>
</div>
</div>
<?php /* ==================== ① 授权(填 API Key)==================== */ ?>
<div class="card card-pad">
<h2 class="cinema-h2">
授权
<?php if ($hasKey): ?>
<span class="node-badge">已授权</span>
<?php else: ?>
<span class="node-badge">还没授权</span>
<?php endif; ?>
</h2>
<p class="small muted" style="margin:0 0 12px">
去 <a href="https://weread.qq.com/r/weread-skills" target="_blank" rel="noopener">微信读书开放平台</a>
扫码登录,页面上点「获取 API Key」,会得到一串 <code class="mono">wrk-</code> 开头的字符串,
复制到下面就行。
</p>
<p class="tip" style="background:#FFF6E5;border-color:#E8C98A;padding:8px 12px;border-radius:8px">
<b>这串 Key 等于你的账号凭据</b> —— 我们会<b>加密存放</b>,页面上也只显示掩码;
不想再用时,把它清空保存即可(或去开放平台重置那串 Key)。
</p>
<?php if ($canWrite): ?>
<form method="post" action="<?= e(url('books/weread')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="act" value="key_save">
<div class="field">
<label>微信读书 API Key</label>
<input class="input mono" name="api_key" autocomplete="off"
placeholder="<?= $hasKey ? e($masked) . '(已保存,留空并保存 = 清除)' : 'wrk-xxxxxxxxxxxx' ?>">
<span class="tip">
<?php if ($hasKey): ?>
当前已保存:<b class="mono"><?= e($masked) ?></b>。要换就把新的整串贴进来(会覆盖)。
<?php else: ?>
形如 <code class="mono">wrk-NVbX…dgAA</code>,只在你自己的账号里可见。
<?php endif; ?>
</span>
</div>
<div class="btn-row" style="margin-top:0">
<button class="btn" type="submit">保存</button>
</div>
</form>
<?php else: ?>
<p class="empty-hint">只读账号不能配置导入。</p>
<?php endif; ?>
</div>
<?php /* ==================== ② 同步(v24 第 25 批 用户第 8 条)====================
用户原话:「需要能够做到定期完成,自动同步。」
⚠️ 这里必须**把机制说清楚**,否则用户会去找"定时任务在哪配":
· 家书没有挂系统 cron(见 WeRead::maybeSync 的注释:家用系统可能几周没人访问,
挂 cron 是在没人用的时候白烧请求);
· 同步发生在**打开这一页的时候**(超过 6 小时才真的联网,否则只读时间戳)。
所以这一块直接把「上次 / 下次」两个时间摆出来 —— 他能对上"我刚打开,它就该是最新的"。 */ ?>
<?php if ($canWrite && $hasKey): ?>
<div class="card card-pad">
<h2 class="cinema-h2">
自动同步
<?php if ($autoOn): ?>
<span class="node-badge" style="background:#EAF4EA;color:#3F7D5A;border-color:#C6DFC6">已开启</span>
<?php else: ?>
<span class="node-badge">已关闭</span>
<?php endif; ?>
</h2>
<p class="small muted" style="margin:0 0 10px">
开启后,<b>每次打开这一页都会顺手同步一次</b>(间隔约 6 小时,不会每次都去打接口)。
没有挂服务器定时任务 —— 你不在的时候同步不同步没人看得出来,白烧请求没有意义。
</p>
<div class="wr-sync">
<span class="wr-sync-item">上次同步:<b><?= e((string)(\App\WeRead::lastSync($me) ?: '还没同步过')) ?></b></span>
<span class="wr-sync-item">下次自动同步:<b><?= $autoOn ? e((string)\App\WeRead::nextSync($me)) : '(已关闭)' ?></b></span>
</div>
<?php if (!empty($sync['ran'])): ?>
<p class="tip" style="margin:10px 0 0">
<?php if (!empty($sync['error'])): ?>
刚才那次自动同步没成功:<?= e((string)$sync['error']) ?>
(不影响页面,下次打开再试)
<?php else: ?>
刚才顺手同步了一次:新增 <?= (int)$sync['books'] ?> 本书、
<?= (int)$sync['covers'] ?> 张封面、<?= (int)$sync['notes'] ?> 条笔记。
<?php endif; ?>
</p>
<?php endif; ?>
<div class="btn-row" style="margin-top:12px">
<form method="post" action="<?= e(url('books/weread')) ?>" class="res-inline">
<?= csrf_field() ?>
<input type="hidden" name="act" value="sync_now">
<button class="btn btn-sm" type="submit">立即同步</button>
</form>
<form method="post" action="<?= e(url('books/weread')) ?>" class="res-inline">
<?= csrf_field() ?>
<input type="hidden" name="act" value="auto_toggle">
<button class="btn btn-ghost btn-sm" type="submit">
<?= $autoOn ? '关闭自动同步' : '开启自动同步' ?>
</button>
</form>
</div>
</div>
<?php endif; ?>
<?php /* ==================== ③ 阅读统计(用户第 8 条)====================
官方 `/readdata/detail` 给的东西比本项目原来那一套丰富得多,这里先摆最常看的四样:
总时长 / 有效阅读天数 / 偏好分类 / 读得最多的书。 */ ?>
<?php if ($canWrite && $hasKey && !empty($stats['ok'])): ?>
<?php $sd = (array)($stats['data'] ?? []); ?>
<div class="card card-pad">
<h2 class="cinema-h2">
我的阅读统计
<span class="hint">来自微信读书官方接口(累计口径)</span>
</h2>
<div class="wr-stats">
<div class="wr-stat">
<b><?= e($durWord((int)($sd['seconds'] ?? 0))) ?></b>
<span>累计阅读时长</span>
</div>
<div class="wr-stat">
<b><?= (int)($sd['days'] ?? 0) ?> 天</b>
<span>有效阅读天数</span>
</div>
<?php if ((int)($sd['avgSec'] ?? 0) > 0): ?>
<div class="wr-stat">
<b><?= e($durWord((int)$sd['avgSec'])) ?></b>
<span>日均(按自然日)</span>
</div>
<?php endif; ?>
<?php foreach ((array)($sd['stat'] ?? []) as $st): ?>
<?php if (trim((string)($st['name'] ?? '')) === '') { continue; } ?>
<div class="wr-stat">
<b><?= e((string)$st['count']) ?></b>
<span><?= e((string)$st['name']) ?></span>
</div>
<?php endforeach; ?>
</div>
<?php if (!empty($sd['topBooks'])): ?>
<h3 class="wr-sub">读得最多的书</h3>
<ul class="wr-top">
<?php foreach (array_slice((array)$sd['topBooks'], 0, 6) as $tb): ?>
<li class="wr-top-item">
<?php if ((string)($tb['cover'] ?? '') !== ''): ?>
<img class="wr-top-cv" src="<?= e((string)$tb['cover']) ?>" alt="" loading="lazy">
<?php else: ?>
<span class="wr-top-cv wr-top-ph">📖</span>
<?php endif; ?>
<span class="wr-top-b">
<span class="wr-top-t"><?= e((string)($tb['title'] ?? '')) ?></span>
<?php if ((string)($tb['author'] ?? '') !== ''): ?>
<span class="small muted"><?= e((string)$tb['author']) ?></span>
<?php endif; ?>
</span>
<span class="wr-top-time"><?= e($durWord((int)($tb['seconds'] ?? 0))) ?></span>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php if (!empty($sd['categories'])): ?>
<h3 class="wr-sub">
偏好分类
<?php if ((string)($sd['preferWord'] ?? '') !== ''): ?>
<span class="small muted"><?= e((string)$sd['preferWord']) ?></span>
<?php endif; ?>
</h3>
<div class="wr-cats">
<?php foreach (array_slice((array)$sd['categories'], 0, 8) as $cc): ?>
<?php $pct = (int)($cc['val'] ?? 0); ?>
<div class="wr-cat">
<span class="wr-cat-n"><?= e((string)($cc['name'] ?? '')) ?></span>
<span class="wr-cat-bar"><i style="width: <?= max(4, min(100, $pct)) ?>%"></i></span>
<span class="wr-cat-v small muted">
<?= (int)($cc['books'] ?? 0) ?> 本 · <?= e($durWord((int)($cc['seconds'] ?? 0))) ?>
</span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($sd['authors'])): ?>
<h3 class="wr-sub">偏好作者</h3>
<p class="small muted" style="margin:0">
<?php $aw = []; foreach ((array)$sd['authors'] as $aa) {
$aw[] = e((string)$aa['name']) . '(' . (int)$aa['books'] . ' 本'
. ((string)$aa['timeWord'] !== '' ? ' · ' . e((string)$aa['timeWord']) : '') . ')';
} echo implode(' · ', array_slice($aw, 0, 6)); ?>
</p>
<?php endif; ?>
</div>
<?php endif; ?>
<?php /* ==================== ④ 拉取预览(先看再导)====================
⚠️ 必须有这一步:直接"一键全导"会把别人(或自己多年前)的书一次性灌进书架,
想撤回只能一本本删。先给个清单、让用户自己勾,是最省事也最不容易后悔的做法。 */ ?>
<?php if ($canWrite && $hasKey): ?>
<div class="card card-pad">
<h2 class="cinema-h2">从微信读书取数据</h2>
<form method="post" action="<?= e(url('books/weread')) ?>" class="inline-form">
<?= csrf_field() ?>
<input type="hidden" name="act" value="fetch">
<div class="btn-row" style="margin-top:0">
<button class="btn btn-sm" type="submit">拉取我的笔记清单</button>
</div>
<p class="tip" style="margin-top:6px">
会去微信读书取一次数据(你有笔记的书 + 每本书的封面/划线/想法),可能要十几秒。
</p>
</form>
</div>
<?php endif; ?>
<?php if ($preview !== null): ?>
<div class="card card-pad">
<h2 class="cinema-h2">
发现 <?= (int)$nbCount ?> 本有笔记的书
<span class="hint">勾选要导入的,然后点下面的「导入选中」</span>
</h2>
<?php if (!$preview): ?>
<p class="empty-hint">微信读书里还没有带笔记的书。</p>
<?php else: ?>
<form method="post" action="<?= e(url('books/weread')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="act" value="import">
<div class="wr-list">
<?php foreach ($preview as $p): ?>
<label class="wr-book">
<input type="checkbox" name="pick[]" value="<?= e((string)$p['bookId']) ?>"
<?= (int)$p['count'] > 0 ? 'checked' : '' ?>>
<?php /* 用户第 8 条:「封面图看不到」—— 预览里也把封面放出来,
选书时能一眼认出是哪一本(原来只有一行书名)。 */ ?>
<?php if ((string)($p['cover'] ?? '') !== ''): ?>
<img class="wr-cv" src="<?= e((string)$p['cover']) ?>" alt="" loading="lazy">
<?php else: ?>
<span class="wr-cv wr-cv-ph">📖</span>
<?php endif; ?>
<span class="wr-main">
<span class="wr-title"><?= e((string)$p['title']) ?></span>
<?php if ((string)$p['author'] !== ''): ?>
<span class="small muted"><?= e((string)$p['author']) ?></span>
<?php endif; ?>
<span class="node-badge"><?= (int)$p['count'] ?> 条</span>
<?php if ((int)$p['local'] > 0): ?>
<span class="node-badge">书架里已有</span>
<?php else: ?>
<span class="node-badge">会新建</span>
<?php endif; ?>
</span>
<?php if (!empty($p['items'])): ?>
<span class="wr-sample">
<?php foreach ($p['items'] as $it): ?>
<span class="wr-q">
<?php if ((string)($it['quote'] ?? '') !== ''): ?>
<i><?= e(mb_substr((string)$it['quote'], 0, 60, 'UTF-8')) ?></i>
<?php endif; ?>
<?php if ((string)($it['content'] ?? '') !== ''): ?>
<em><?= e(mb_substr((string)$it['content'], 0, 60, 'UTF-8')) ?></em>
<?php endif; ?>
</span>
<?php endforeach; ?>
</span>
<?php endif; ?>
</label>
<?php endforeach; ?>
</div>
<div class="btn-row">
<button class="btn" type="submit">导入选中</button>
</div>
</form>
<?php endif; ?>
</div>
<?php endif; ?>
<?php /* ==================== ⑤ 另一条路:手动粘贴 ====================
⚠️ 两种方式都留着,不是重复:
· API Key 走"全自动",但要求用户愿意把 Key 交出来;
· 粘贴走"零授权",App 里「导出笔记」复制一段文字即可。 */ ?>
<div class="card card-pad">
<h2 class="cinema-h2">或者:手动粘贴笔记</h2>
<p class="small muted" style="margin:0 0 10px">
不想配 Key 也行 —— 在微信读书 App 里打开一本书 → 右上角「…」→ 导出笔记,
把复制到的文字粘到那里,效果一样。
</p>
<div class="btn-row" style="margin-top:0">
<a class="btn btn-ghost btn-sm" href="<?= e(url('books/notes_import')) ?>">去粘贴导入</a>
</div>
</div>