book-index-ui
v0.50.0
Published
React components for browsing and editing ancient book index metadata
Downloads
11,011
Maintainers
Readme
book-index-ui
book-index-manager 的 React 组件库 + 存储客户端,发布为 npm 包 book-index-ui。
配合 book-index-manager 的 Python CLI 一起使用。
npm install book-index-ui包结构
两个独立入口:
import { ... } from 'book-index-ui' // React 组件 + 类型 + 数据层
import { ... } from 'book-index-ui/storage' // 仅数据层(无 React,体积小,适合 Node/Worker)
import 'book-index-ui/styles' // CSS(用到组件时引入)主要 export
React 组件
| 组件 | 用途 |
|---|---|
| IndexBrowser | 完整索引浏览器:搜索框 + 分类 tab + 推荐 + 最近浏览 |
| IndexView | 单条目详情视图(基于 ID 拉数据 + 渲染) |
| IndexDetail | 旧版详情纯渲染(2026-09 重构前,仍导出以兼容) |
| BookDetailLayout | 详情页外壳:Tab(基本信息/整理本/全文/反馈)+ 路由到下面四个 Page;kaiyuanguji-web 用它 |
| WorkPage / BookPage / CollectionPage / EntityPage | 现行详情页(2026-09 版式重构):作品 / 版本 / 丛编 / 人物 |
| primitives(PageFrame、SectionHead、DataTable、Chip …) | 四个 Page 共用的版式原语与 DETAIL_CSS |
| deriveEra / deriveDating / buildVersionTable … | core/detail-model.ts 的数据派生层(刊刻年代、角色归一、版本表) |
| WorkCatalog | 作品目录 |
| IndexEditor | 详情编辑器(写入需 storage 实现 saveItem) |
| HomePage | 首页:推荐丛编 + 经典作品(kaiyuanguji-web 用) |
| TextReader | 统一阅读器(overview#307):整理本与全文合一,版本下拉含全部版本,取数走 core/text-api(只认新结构),见下「统一阅读器」 |
| ReaderShell / ReaderToc / ImagePanel / ReaderMdText | 阅读器(整理本与全文共用),见下「阅读器」 |
| CollectionCatalog | 丛编目录(按册/卷分组) |
| EmendatedBySection | "校勘自" 引用列表 |
| VersionLineageView / VersionLineageGraph | 版本传承图(dagre + xyflow) |
| FeedbackButton / FeedbackList / FeedbackForm | 反馈组件 |
| LocaleProvider / LocaleToggle | 繁简切换 |
| useI18n / getT | 统一 i18n 入口:界面文字 t(key)、数据文字 convert,见下「界面文字与繁简」 |
| useT / useConvert | 旧 hook(整本字典 / 繁简转换),新代码用 useI18n |
| findTraditionalChars | 简体模式残留繁体字检测(vitest 与网站 e2e 共用) |
统一阅读器(TextReader,overview#307)
<TextReader
id={workOrBookId} transport={storage}
versionKey={key} chapter={chapterKey} // 受控(可不传,自己管)
onLocationChange={(loc, cause) => router.push(urlFor(loc))} // loc: { key, chapter, isDefault };cause: 'auto' | 'chapter' | 'version'
onNavigate={id => router.push(entryUrl(id))}
/>- 数据模型(规格 overview
项目进展/古籍索引网站/设计/阅读文本.md):items/<id>/manifest.json列全部版本(versions[0]是default),<key>/index.json是章目录(chapters[{ n, file:'001', title, has_json }]),章正文<key>/001.txt,整理本的章另有001.json。IndexStorage新增可选getTextManifest / getTextIndex / getChapter,BundleStorage已实现。 - 只认新结构(用户 09-30 定):条目没有
manifest.json(含旧的collated_edition/、full_text/…目录)就显示「暂无文本」,不从旧目录合成。旧的CollatedEdition/BookFullText暂留给还没迁移的页面,迁移在正式站验完后清掉。 - 界面:工具条一个「版本」下拉按 manifest 顺序列全部版本(含整理本),只有一份时只在书名后标来源名;正文末尾显示所选版本的出处与授权;章有 json 用结构化渲染(条目、作品链接、跨章搜索),否则按 md;切版本尽量停在同一章号,对不上回第一章。
- 组件不改 URL:宿主在
onLocationChange里改地址(新 URL:/read/<id>[/<key>][/<章>],主版本不写 key)。versionKey/chapter传了就受控;传了无效值时组件回落并以auto通知。 - 工具条另有上一章/下一章(窄屏隐去,翻卷交给正文底部的翻页卡片)、书名链接回条目页;窄屏(右栏藏起来)时正文末尾有「报告错字」入口。
阅读器(components/Reader/,2026-09 N5a)
整理本与全文共用一个外壳 ReaderShell:
- 布局:书影在正文左边,目录在最左。宽屏(≥1100px)目录是可收起的侧栏(默认展开);更窄时是抽屉(默认收起,选卷后自动合上,Esc 关闭,打开时 Tab 圈在抽屉内);窄屏(≤719px)书影区不显示,只留正文。
- 工具条(吸顶)只用文字与图标:目录、繁|简、A− A+、「阅读设置」键(overview#463)。其余偏好收进右侧「阅读设置」侧栏(≥860px 右侧可折叠栏;窄屏是底栏「设置」打开的底部抽屉),分三组:文字(字体、字号、繁简)、标注(专名线,对读时加标点)、版面(自然段、标出作品链接、书影、对读留白,竖排预留
allowVertical)。宿主可用settingsAnnotate/settingsLayout往侧栏塞自己的控件。字体选项只有宋体(默认,沿用--bim-font-reading)/楷体/系统默认(无衬线),全是系统已装字体,不加载网络字体;楷体栈Kaiti SC → Kaiti TC → STKaiti → KaiTi → AR PL UKai CN → Noto Serif CJK SC → serif。 - 正文:
--bim-font-reading(宋体,默认接--bim-font-serif,宿主覆盖后者即可)18px、行高 2.05(手机端同样),line-break: strict避头尾;界面黑体。 - 无障碍:第一个可聚焦元素是「跳到正文」;目录用游走 tabindex,几百卷也只有当前卷进 Tab 序列(↑↓ Home End 移动)。
- 偏好(字号、字体、自然段、专名线…)存
localStorage['bim-reader-prefs'],首帧用默认值,SSR 安全。 - 宿主若有吸顶导航,设
--bim-reader-top(如60px)。
书影接口:TextReader 的 resolveImages(chapterKey) 返回当卷的 ReaderPageImage[](可异步)——每页一张图,URL 由宿主给,可带 width/height 与逐字框 boxes(暂定格式 bim-charbox-v0:原图像素坐标 x/y/w/h,可选 char/textOffset/confidence)。其他格式的框设 boxFormat 并用 renderImageOverlay 自己画,这是与 CV 交付格式对齐前的扩展点。没有影像时书影区默认收起(imagePanel="auto"),点工具条「书影」可看占位。
版本下拉框(overview#235,同一 owner 有多份全文时用):ReaderShell 的四个 props——
| prop | 说明 |
|---|---|
| versions | ReaderVersion[](key / label / sourceName / license / sourceUrl / primary)。两份以上时工具条出「版本」下拉框,只有一份不出;有值时正文末尾显示所选版本的出处与授权 |
| currentVersionKey | 当前版本 key;不给(undefined)时组件自管,给了但无效则选 primary,再没有就第一份 |
| onVersionChange | 下拉框切换回调 (key) => void;组件不改 URL,由宿主换 toc 与正文(换上新目录后自动选中第一卷) |
| versionSource | 是否显示正文末尾的「出处 · 授权」,默认 true;宿主自己画出处时设 false |
配套两个导出(book-index-ui 根入口与 components/Reader):readerVersionsFromFullText(entries) 把 book-text 的 WorkFullTextEntry[] 换成 ReaderVersion[](顺序不变,清单已按「哪份最好」排好);pickReaderVersion(versions, key?) 给出当前选中的一份(key 有效用它,否则 primary,再否则第一份),宿主用它决定该载入哪份 toc / 正文。
resolveImages 要用 useCallback(或模块级函数)包一层:组件按 [resolveImages, 当前卷] 重新取书影,每次渲染都传一个新函数会反复请求。
正文实体标注(EntityText,overview#389)
给一段正文和实体区间:人名、地名、官职等画直线专名线,书名画波浪线(书名号样式);已收录的实体是 /item/<id> 链接,悬停或聚焦弹出摘要卡(取法同详情页 transport.getItem),Esc 收起。
import { EntityText, adaptEntityJson } from 'book-index-ui';
const spans = adaptEntityJson(entityJson); // entity.json → EntitySpan[];格式定稿后只改这一层
<EntityText text={paragraph} entities={spans} offsets="plain" plainBase={0}
transport={storage} onNavigate={(id, e) => { e.preventDefault(); router.push(`/item/${id}`); }} />entity.json 的 span 是纯字偏移(不计标点空白),配 offsets="plain";正文只是整卷一段时用 plainBase 给出本段起点。book-index://Work/<id> 等 URI 由 bookIndexUriToId 转站内 id。一页铺很多段时设 injectStyles={false},在页面上放一次 <style>{ENTITY_TEXT_CSS}</style>。
数据层(book-index-ui/storage)
| 类 | 用途 |
|---|---|
| BookIndexManager | Facade:统一 getItem/saveItem/deleteItem/generateId/... |
| BookIndexStorage | 本地文件系统实现(Node / Electron) |
| GithubStorage | GitHub 只读 + jsdelivr CDN fallback(浏览器/CI 用) |
| BundleStorage | 同域预打包数据读取(kaiyuanguji-web 生产模式) |
| LocalStorage | BookIndexStorage 的薄包装(兼容历史 API) |
| IndexStorage | 接口(自实现自定义后端时实现它) |
数据层还导出 encodeId / decodeId / smartDecode / extractType / shardOf / scoreEntry / rankByRelevance / cleanName 等纯函数工具。
界面文字与繁简(overview#337)
两类文字,两条路:
| 类别 | 例 | 怎么写 |
|---|---|---|
| 界面固定文字 | 标题、按钮、aria-label、单位、「展開其餘 {n} 冊」 | const { t } = useI18n(); t('detail.moreN', { n }) |
| 数据文字 | 书名、作者、提要、分类名、版本名、sections.json 里的分区名 | const { convert } = useI18n(); convert(data.title) |
- 字典按区域分文件:
src/i18n/messages/<区域>.ts,defineMessages(繁体栏, 简体栏, 英文栏?),繁简两栏键必须一一对应(tsc 检查);早期的整块字典locales/zh-Hant.ts/zh-Hans.ts仍可用。 - 键是带类型的点路径(
MessageKey),写错 tsc 报错;当前语言缺键回落到繁体。 - 没包
LocaleProvider时按繁体。网站务必在最外层包LocaleProvider(或网站的BimLocaleProvider),否则组件一律显示繁体。 - 组件外(Next.js
generateMetadata等服务端):getT(locale)('…'),数据文字用同一个转换函数(LocaleProvider的converter或内置 opencc)。 - 加英文:
Locale加'en',各字典填英文栏(可部分,缺的回落繁体);数据文字的英文另议。 - 检测:
tests/unit/zh-hans-leak.test.tsx在简体模式下渲染各页,用findTraditionalChars(常用繁体字表 + 专名白名单)断言正文无繁体残留;tests/unit/i18n-dictionaries.test.ts检查字典繁简同形、简体栏无繁体。
类型
IndexEntry / IndexDetailData / WorkDetailData / BookDetailData / CollectionDetailData / EntityDetailData / IndexType / IndexStatus / LineageGraph / CollatedEditionIndex / ResourceCatalog 等。
最小用法
1. 用 GithubStorage 直接渲染索引(浏览器)
import { IndexBrowser, GithubStorage, LocaleProvider } from 'book-index-ui';
import 'book-index-ui/styles';
const transport = new GithubStorage({
org: 'open-guji',
repos: { draft: 'book-index-draft', official: 'book-index' },
});
export default function App() {
return (
<LocaleProvider>
<IndexBrowser
transport={transport}
onEntryClick={entry => console.log('clicked', entry.id)}
/>
</LocaleProvider>
);
}2. 仅用数据层(Node 环境,例如 build script)
import { BookIndexManager } from 'book-index-ui/storage';
const mgr = new BookIndexManager('/workspace');
const item = await mgr.getItem('1evgowbkc2qyo');
console.log(item?.title);3. 自定义 storage 后端(VS Code 扩展、Electron 等)
import type { IndexStorage, IndexEntry, PageResult, LoadOptions } from 'book-index-ui';
class MyStorage implements IndexStorage {
async loadEntries(type, options): Promise<PageResult<IndexEntry>> { ... }
async getItem(id): Promise<Record<string, unknown> | null> { ... }
// ... 实现 IndexStorage 全部必选方法
}参考 kaiyuanguji-web/nextjs/src/lib/local-api-storage.ts(浏览器 → Next.js API 路由)和 guji-platform/src/storage/VscodeStorage.ts(VS Code 文件系统)。
4. 宿主接入要点(手机 / 无障碍,2026-09-28)
- 搜索结果是真链接:
IndexBrowser的结果卡片是<a href>,href 由BidUrlProvider的buildUrl生成(默认/book-index?id=…),站点路由不同时务必包一层 Provider。普通左键点击仍调onEntryClick走客户端路由;Ctrl / Cmd / Shift / 中键交给浏览器(新标签、复制链接)。不传onEntryClick则按 href 整页跳转。 - 反馈浮钮
FeedbackButton:- 层级取
--bim-feedback-fab-z(默认 40),低于抽屉与弹层;也可传zIndexprop。 - 挂载且未隐藏时在
<html>上写--bim-feedback-fab-offset(浮钮占掉的底部高度)。正文容器写padding-bottom: var(--bim-feedback-fab-offset, 0px),右下角内容就不会被盖住。 hiddenprop:移动端菜单 / 抽屉打开时传true,浮钮与留白一起撤掉。
- 层级取
数据约定
book-index-ui 读写的数据格式与 Python 端 book-index-manager CLI 完全一致。详见 根 README 的"存储结构"和"Manager API 对照表"。
关键约束:文件名清洗(cleanName)的 CJK 范围 TS/Python 必须一致——见 tests/unit/cleanName.test.ts 的跨语言 fixture。
开发
npm install
npm run dev # vite dev server(开发组件)
npm run test # vitest 单元测试
npm run test:e2e # Playwright E2E
npm run build:lib # 输出 dist/ 给 npm 发布许可
Apache 2.0
schema-v2 派生字段(overview#458)
详情页(BookDetailLayout 及其下各页)直接读 build 产物的 _ 字段,没有中间的归一/兼容层:
| 记录 | 页面读的字段 |
|---|---|
| Work | _books(版本卡)、_edition_count、_classifications(优先 zongmu)、_related(含 direction)、_collections;著录文字仍读源档的 indexed_by |
| Book | _collections(所属丛编与册次);上级作品的版本取自 Work 的 _books |
| Collection | _members(前 20 项)+_member_count;上级丛编读源档的 contained_in |
| Entity | _works |
卡片里 {id, h:1} 的枢纽引用没有 title,名称取自 _hubs.json:IndexStorage.getHubs?() 提供(BundleStorage 默认读 ${basePath}/_hubs.json,可用 hubsPath 改相对路径);不提供时枢纽引用无名,页面回退显示 ID。BookDetailLayout 会给传入的 transport 套一层 withHubTitles(只补枢纽卡的 title,不改字段形状)。夹具取自 book-index schema-v2 的 build/contract-sample/(tests/unit/fixtures/contract-sample/)。
tests/unit/DerivedGolden.test.tsx 用 tests/unit/fixtures/derived-golden/(真实 build 产物里挑的 47 个样本)逐字节比对整页渲染,改页面后输出有意变化时才重新生成(见该测试文件头部)。
