v0.1.0 界面框架:抽屉骨架 + 13 个模块页面

对应服务端 FamilyBook(家书)的鸿蒙客户端首版。本轮只做**界面框架**,
数据全部是演示数据 —— 服务端尚无客户端接口,所有增删改只做入口与样式,
不落任何数据。

## 骨架
左侧抽屉 + 右侧内容。抽屉两套形态由窗口短边决定(≥600vp 为宽屏):
宽屏常驻侧栏、窄屏滑出层。判据取「短边」而非「宽度」,因为短边在旋转时
不变,比例接近 1:1 的设备横竖屏布局天然一致。

## 模块(与 Web 端 Module::LIST 一一对应,key 即路由名)
首页、家谱、名册、观影、旅程、日程、资源、任务、纪念日、财务、读书、
导航、笔记;另有通知消息、个人设置两个附加页。

## 与 Web 端保持一致的几处
· 模块清单唯一来源在 model/Modules.ets,加模块只改那一处;
· 模块主题色三档由主色按 HSL 压明度算出,预设色板与 Web 端逐值一致;
· 术语沿用 Web 端固定下来的口径(旅程 ⊃ 行程;观影的「影院」不单独占
  抽屉位,从模块内设置进);
· 页面结构照 Web 端的信息层次(首页信息面板 / 财务概览在账目之前 /
  纪念日「即将到来」在顶 / 任务按状态而非按看板)。

## 编译验证
hvigorw assembleHap --mode module -p product=default
→ BUILD SUCCESSFUL,0 error / 0 warning(含一次删 build 后的干净重建)

过程中修掉的编译期问题:
· Circle().fill() 是 API 26 才有的重载(compatibleSdk 是 24)
  → 换「方块 + 圆角」画圆,零版本风险;
· setWindowBackgroundColor 对 phone/tablet 不可用 → 去掉(页面已铺满同色);
· window.getLastWindow() 只有异步版 → 由 Ability 侧同步取窗存 AppStorage;
· 颜色类 @Param 声明成 string 导致传 Resource 报错 → 统一改 ResourceColor;
· 页面在 pages/<模块>/ 下,公共件 import 相对路径要多退一级。

(首轮编译 95 error / 18 warn,逐条定位到 5 类根因后归零。)
This commit is contained in:
2026-10-11 22:07:48 +08:00
commit 41e3aa3d95
80 changed files with 6985 additions and 0 deletions
+421
View File
@@ -0,0 +1,421 @@
/**
* 家书 FamilyBook · 演示数据(鸿蒙端)
*
* ⚠️ 本文件是**临时占位**:接口还没做,页面要能看出长什么样,所以这里造了一批
* 结构完整的假数据。接接口时删掉本文件即可 —— 页面只依赖 `model/Types.ets`
* 里的类型,不依赖这些常量本身。
*
* ⚠️ 数据刻意照 Web 端的真实形态造(首页 12 个模块卡、观影有海报位、
* 旅程分「计划/实际」两阶段的行程段、任务按看板分组带逾期标记…),
* 这样「页面该长什么样」的判断才站得住,而不是凭空拍。
*
* ⚠️ 全文件统一 `Fb` 前缀命名,避免与其他文件顶层名冲突(arkts-no-decl-merging)。
*/
import { FbAccount, FbAnniversaryItem, FbBookItem, FbCalBook, FbCalEvent, FbGroup,
FbKinChip, FbKinRelation, FbLedgerBook, FbLedgerEntry, FbModuleCard, FbMovieItem,
FbNavLink, FbNoteItem, FbNotificationItem, FbResourceItem, FbRosterItem, FbSession,
FbTaskItem, FbTripItem, FbTripNode, FbWeather, FbWeekStat } from './Types';
/** 假数据里的「我」 */
export const FB_DEMO_ME: FbAccount = {
id: 1,
name: '杨永全',
kinship: '我',
avatarColor: '#A84A28',
role: 'admin',
isGuest: false,
canWrite: true
};
/** 假会话 */
export const FB_DEMO_SESSION: FbSession = {
loggedIn: true,
familyId: 1,
familyTitle: '杨家',
slogan: '记录家庭的全部生活,一本用岁月写成的家谱',
me: FB_DEMO_ME
};
/** 造一个头像 chip 的快捷函数(少写重复字面量) */
function fbChip(id: number, shown: string, kinship: string, color: string): FbKinChip {
return { id: id, shown: shown, kinship: kinship, avatarColor: color, isAdmin: false };
}
/** 首页「我的家人」 */
export const FB_DEMO_KIN: FbKinChip[] = [
fbChip(2, '爱人', '配偶', '#9A5C7A'),
fbChip(3, '儿子', '儿子', '#4A7C82'),
fbChip(4, '女儿', '女儿', '#6B5B95'),
fbChip(5, '爸爸', '父亲', '#8A6B4B'),
fbChip(6, '妈妈', '母亲', '#B0763A'),
fbChip(7, '弟弟', '弟弟', '#3F7D5A')
];
/** 首页天气 */
export const FB_DEMO_WEATHER: FbWeather = {
place: '青岛市 · 崂山区',
temp: 21,
text: '多云',
wind: '东南风 3 级'
};
/** 首页近七天统计 */
export const FB_DEMO_WEEK_STAT: FbWeekStat = {
events: 11,
todos: 4,
overdue: 1
};
/** 首页日子标记 */
export const FB_DEMO_MARKS: string[] = ['寒露', '重阳节', '儿子生日'];
/** 首页模块卡片(顺序与 Web 端 Module::DEFAULT_NAV 一致) */
export const FB_DEMO_MODULE_CARDS: FbModuleCard[] = [
{ key: 'people', name: '名册', big: '12 人', lines: ['1089 人在这个家里'], color: '#4A7C82' },
{ key: 'movies', name: '观影', big: '86 部', lines: ['最近看了《漫长的季节》'], color: '#7A4E8C' },
{ key: 'trips', name: '旅程', big: '9 次', lines: ['最近去了青岛·崂山'], color: '#3F7D5A' },
{ key: 'calendar', name: '日程', big: '11 条', lines: ['未来七天'], color: '#B0763A' },
{ key: 'resources', name: '资源', big: '37 项', lines: ['证件 / 资料 / 附件'], color: '#5566A8' },
{ key: 'tasks', name: '任务', big: '3 件在做', lines: ['4 件待办 · 1 件逾期'], color: '#A8574F' },
{ key: 'days', name: '纪念日', big: '6 天后', lines: ['结婚周年'], color: '#B4633C' },
{ key: 'ledger', name: '财务', big: '¥86.4 万', lines: ['3 本账(净资产)'], color: '#5E7A3C' },
{ key: 'books', name: '读书', big: '2 本在读', lines: ['31 本读完 · 48 条笔记'], color: '#6B5B95' },
{ key: 'nav', name: '导航', big: '24 个', lines: ['我的常用站点'], color: '#3E7A9E' },
{ key: 'notes', name: '笔记', big: '63 条', lines: ['最近:阳台改造清单'], color: '#7A6A4F' }
];
// ============================================================
// 名册
// ============================================================
export const FB_DEMO_ROSTER: FbRosterItem[] = [
{ id: 1, name: '杨永全', kinship: '我', group: '本家', tags: ['管理员'], avatarColor: '#A84A28', roster: 'member', birthday: '03-12', isAdmin: true },
{ id: 2, name: '李慧', kinship: '配偶', group: '本家', tags: [], avatarColor: '#9A5C7A', roster: 'member', birthday: '07-08', isAdmin: false },
{ id: 3, name: '杨小满', kinship: '儿子', group: '本家', tags: ['学生'], avatarColor: '#4A7C82', roster: 'member', birthday: '10-18', isAdmin: false },
{ id: 4, name: '杨小禾', kinship: '女儿', group: '本家', tags: ['学生'], avatarColor: '#6B5B95', roster: 'member', birthday: '01-04', isAdmin: false },
{ id: 5, name: '杨建国', kinship: '父亲', group: '长辈', tags: ['退休'], avatarColor: '#8A6B4B', roster: 'member', birthday: '05-21', isAdmin: false },
{ id: 6, name: '王秀兰', kinship: '母亲', group: '长辈', tags: [], avatarColor: '#B0763A', roster: 'member', birthday: '11-30', isAdmin: false },
{ id: 7, name: '杨永强', kinship: '弟弟', group: '本家', tags: [], avatarColor: '#3F7D5A', roster: 'member', birthday: '08-15', isAdmin: false },
{ id: 8, name: '李国栋', kinship: '岳父', group: '姻亲', tags: [], avatarColor: '#5566A8', roster: 'member', birthday: '02-26', isAdmin: false },
{ id: 9, name: '陈静', kinship: '岳母', group: '姻亲', tags: [], avatarColor: '#A8574F', roster: 'member', birthday: '09-09', isAdmin: false },
{ id: 10, name: '张伟', kinship: '朋友', group: '亲友', tags: ['同事'], avatarColor: '#5E7A3C', roster: 'friend', birthday: '06-14', isAdmin: false },
{ id: 11, name: '刘敏', kinship: '朋友', group: '亲友', tags: ['同学'], avatarColor: '#7A6A4F', roster: 'friend', birthday: '12-02', isAdmin: false },
{ id: 12, name: '赵鹏', kinship: '朋友', group: '亲友', tags: ['邻居'], avatarColor: '#3D6B8E', roster: 'friend', birthday: '04-17', isAdmin: false }
];
/** 家谱连线(父母 / 配偶 / 子女) */
export const FB_DEMO_KIN_RELATIONS: FbKinRelation[] = [
{ id: 1, fromId: 1, fromName: '杨永全', toId: 5, toName: '杨建国', kind: 'parent', label: '父亲' },
{ id: 2, fromId: 1, fromName: '杨永全', toId: 6, toName: '王秀兰', kind: 'parent', label: '母亲' },
{ id: 3, fromId: 1, fromName: '杨永全', toId: 2, toName: '李慧', kind: 'spouse', label: '配偶' },
{ id: 4, fromId: 1, fromName: '杨永全', toId: 3, toName: '杨小满', kind: 'child', label: '儿子' },
{ id: 5, fromId: 1, fromName: '杨永全', toId: 4, toName: '杨小禾', kind: 'child', label: '女儿' },
{ id: 6, fromId: 1, fromName: '杨永全', toId: 7, toName: '杨永强', kind: 'child', label: '弟弟' },
{ id: 7, fromId: 2, fromName: '李慧', toId: 8, toName: '李国栋', kind: 'parent', label: '岳父' },
{ id: 8, fromId: 2, fromName: '李慧', toId: 9, toName: '陈静', kind: 'parent', label: '岳母' }
];
// ============================================================
// 观影
// ============================================================
export const FB_DEMO_MOVIES: FbMovieItem[] = [
{ id: 1, title: '漫长的季节', watchedAt: '2026-10-05 19:30', cinemaName: '家里', cinemaColor: '#8A6B4B', rating: 9.2, reviewCount: 3, viewers: [fbChip(1, '我', '我', '#A84A28'), fbChip(2, '爱人', '配偶', '#9A5C7A')], hasPoster: true },
{ id: 2, title: '流浪地球 3', watchedAt: '2026-09-28 14:00', cinemaName: '万达影城·李沧店', cinemaColor: '#5566A8', rating: 8.6, reviewCount: 2, viewers: [fbChip(1, '我', '我', '#A84A28'), fbChip(3, '儿子', '儿子', '#4A7C82'), fbChip(4, '女儿', '女儿', '#6B5B95')], hasPoster: true },
{ id: 3, title: '飞屋环游记', watchedAt: '2026-09-20 20:00', cinemaName: '家里', cinemaColor: '#8A6B4B', rating: 9.0, reviewCount: 1, viewers: [fbChip(4, '女儿', '女儿', '#6B5B95')], hasPoster: false },
{ id: 4, title: '奥本海默', watchedAt: '2026-09-12 19:10', cinemaName: 'CGV·市南店', cinemaColor: '#A8574F', rating: 8.9, reviewCount: 4, viewers: [fbChip(1, '我', '我', '#A84A28'), fbChip(2, '爱人', '配偶', '#9A5C7A')], hasPoster: true },
{ id: 5, title: '海蒂和爷爷', watchedAt: '2026-08-30 15:30', cinemaName: '家里', cinemaColor: '#8A6B4B', rating: 8.4, reviewCount: 2, viewers: [fbChip(4, '女儿', '女儿', '#6B5B95'), fbChip(3, '儿子', '儿子', '#4A7C82')], hasPoster: false },
{ id: 6, title: '宇宙探索编辑部', watchedAt: '2026-08-16 21:00', cinemaName: '家里', cinemaColor: '#8A6B4B', rating: 8.1, reviewCount: 1, viewers: [fbChip(1, '我', '我', '#A84A28')], hasPoster: true }
];
/** 观影影院(movies/cinemas) */
export const FB_DEMO_CINEMAS: string[] = ['家里', '万达影城·李沧店', 'CGV·市南店', '横店影城', '猫眼线上'];
// ============================================================
// 旅程
// ============================================================
export const FB_DEMO_TRIPS: FbTripItem[] = [
{ id: 1, title: '青岛·崂山 两日', sub: '青岛 · 2 天', dateRange: '2026-10-02 ~ 10-03', nodeCount: 8, doneCount: 8 },
{ id: 2, title: '烟台海边周末', sub: '烟台 · 2 天', dateRange: '2026-08-15 ~ 08-16', nodeCount: 6, doneCount: 6 },
{ id: 3, title: '国庆西安行', sub: '西安 · 4 天', dateRange: '2025-10-01 ~ 10-04', nodeCount: 12, doneCount: 12 },
{ id: 4, title: '寒假哈尔滨', sub: '哈尔滨 · 5 天', dateRange: '2026-01-20 ~ 01-24', nodeCount: 0, doneCount: 0 }
];
/** 一段旅程的行程(trips/show 时间轴;stage 分「计划 / 实际」) */
export const FB_DEMO_TRIP_NODES: FbTripNode[] = [
{ id: 1, title: '出发', when: '10-02 07:30', place: '家 → 青岛北站', stage: 'actual', status: 'done', mode: '自驾', expenseCent: 0 },
{ id: 2, title: '抵达民宿', when: '10-02 10:20', place: '崂山区·沙子口民宿', stage: 'actual', status: 'done', mode: '自驾', expenseCent: 68000 },
{ id: 3, title: '海边散步 + 午餐', when: '10-02 12:00', place: '仰口海水浴场', stage: 'actual', status: 'done', mode: '步行', expenseCent: 21800 },
{ id: 4, title: '太清宫', when: '10-02 15:00', place: '崂山太清景区', stage: 'actual', status: 'done', mode: '自驾', expenseCent: 27000 },
{ id: 5, title: '登山(巨峰)', when: '10-03 08:00', place: '崂山巨峰景区', stage: 'plan', status: 'done', mode: '自驾', expenseCent: 34000 },
{ id: 6, title: '返程', when: '10-03 17:00', place: '青岛 → 家', stage: 'plan', status: 'done', mode: '自驾', expenseCent: 0 }
];
// ============================================================
// 日程
// ============================================================
export const FB_DEMO_CAL_BOOKS: FbCalBook[] = [
{ id: 1, name: '我的日程', color: '#A84A28', canWrite: true, visible: true, shared: false, derived: false },
{ id: 2, name: '家庭日程', color: '#B0763A', canWrite: true, visible: true, shared: true, derived: false },
{ id: 3, name: '观影', color: '#7A4E8C', canWrite: true, visible: true, shared: false, derived: true },
{ id: 4, name: '旅行', color: '#3F7D5A', canWrite: true, visible: true, shared: false, derived: true },
{ id: 5, name: '孩子的课表', color: '#4A7C82', canWrite: false, visible: false, shared: true, derived: false },
{ id: 6, name: '节假日', color: '#C0533F', canWrite: false, visible: true, shared: false, derived: true }
];
export const FB_DEMO_EVENTS_TODAY: FbCalEvent[] = [
{ id: 1, title: '儿子数学兴趣班', date: '2026-10-11', time: '09:00', calName: '孩子的课表', color: '#4A7C82', allDay: false, place: '小区活动中心' },
{ id: 2, title: '家庭大扫除', date: '2026-10-11', time: '10:30', calName: '家庭日程', color: '#B0763A', allDay: false, place: '家' },
{ id: 3, title: '晚上看电影', date: '2026-10-11', time: '19:30', calName: '观影', color: '#7A4E8C', allDay: false, place: '家里' },
{ id: 4, title: '妈妈体检预约', date: '2026-10-11', time: '', calName: '家庭日程', color: '#B0763A', allDay: true, place: '市立医院' }
];
export const FB_DEMO_EVENTS_SOON: FbCalEvent[] = [
{ id: 11, title: '女儿钢琴课', date: '10-12', time: '16:00', calName: '孩子的课表', color: '#4A7C82', allDay: false, place: '琴行' },
{ id: 12, title: '结婚周年', date: '10-17', time: '', calName: '家庭日程', color: '#B0763A', allDay: true, place: '' },
{ id: 13, title: '周末露营', date: '10-18', time: '08:00', calName: '家庭日程', color: '#B0763A', allDay: false, place: '仰口' },
{ id: 14, title: '家长会', date: '10-20', time: '18:30', calName: '孩子的课表', color: '#4A7C82', allDay: false, place: '学校' }
];
// ============================================================
// 任务
// ============================================================
export const FB_DEMO_TASKS: FbTaskItem[] = [
{ id: 1, title: '交物业费', status: 'doing', board: '家务', boardColor: '#A8574F', dueText: '10-13 前', overdue: false, assignee: '我', childDone: 0, childTotal: 0 },
{ id: 2, title: '预约明年体检', status: 'doing', board: '家人健康', boardColor: '#5E7A3C', dueText: '10-20 前', overdue: false, assignee: '爱人', childDone: 1, childTotal: 3 },
{ id: 3, title: '整理去年旅行照片', status: 'doing', board: '家务', boardColor: '#A8574F', dueText: '', overdue: false, assignee: '我', childDone: 0, childTotal: 0 },
{ id: 4, title: '换季衣物收纳', status: 'todo', board: '家务', boardColor: '#A8574F', dueText: '10-09 前', overdue: true, assignee: '爱人', childDone: 0, childTotal: 0 },
{ id: 5, title: '给儿子买秋季校服', status: 'todo', board: '孩子', boardColor: '#4A7C82', dueText: '10-15 前', overdue: false, assignee: '我', childDone: 0, childTotal: 0 },
{ id: 6, title: '车保养', status: 'todo', board: '车与房', boardColor: '#5566A8', dueText: '', overdue: false, assignee: '', childDone: 0, childTotal: 0 },
{ id: 7, title: '读完《置身事内》', status: 'done', board: '家务', boardColor: '#A8574F', dueText: '', overdue: false, assignee: '我', childDone: 0, childTotal: 0 }
];
/** 任务看板(tasks/settings) */
export const FB_DEMO_TASK_BOARDS: string[] = ['家务', '家人健康', '孩子', '车与房'];
// ============================================================
// 纪念日
// ============================================================
export const FB_DEMO_ANNIVERSARIES: FbAnniversaryItem[] = [
{ id: 1, title: '结婚周年', date: '10-17', daysLeft: 6, kind: 'anniversary', people: [fbChip(1, '我', '我', '#A84A28'), fbChip(2, '爱人', '配偶', '#9A5C7A')], yearly: true },
{ id: 2, title: '儿子生日', date: '10-18', daysLeft: 7, kind: 'birthday', people: [fbChip(3, '儿子', '儿子', '#4A7C82')], yearly: true },
{ id: 3, title: '重阳节', date: '10-29', daysLeft: 18, kind: 'holiday', people: [], yearly: true },
{ id: 4, title: '爸爸生日', date: '05-21', daysLeft: 222, kind: 'birthday', people: [fbChip(5, '爸爸', '父亲', '#8A6B4B')], yearly: true },
{ id: 5, title: '搬家纪念日', date: '2019-11-08', daysLeft: 28, kind: 'anniversary', people: [], yearly: true }
];
// ============================================================
// 财务
// ============================================================
export const FB_DEMO_LEDGER_BOOKS: FbLedgerBook[] = [
{ id: 1, name: '家庭总账', netCent: 86420000, myPerm: 'owner', entryCount: 9 },
{ id: 2, name: '我的私房账', netCent: 1260000, myPerm: 'owner', entryCount: 3 },
{ id: 3, name: '爸妈的养老账', netCent: 4580000, myPerm: 'write', entryCount: 5 }
];
export const FB_DEMO_LEDGER_ENTRIES: FbLedgerEntry[] = [
{ id: 1, name: '工资卡', kind: 'deposit', amountCent: 18600000, ownerName: '我', updatedAt: '2026-10-01' },
{ id: 2, name: '公积金', kind: 'deposit', amountCent: 24300000, ownerName: '我', updatedAt: '2026-10-01' },
{ id: 3, name: '指数基金', kind: 'fund', amountCent: 34800000, ownerName: '我', updatedAt: '2026-09-30' },
{ id: 4, name: '重疾险', kind: 'insurance', amountCent: 500000, ownerName: '爱人', updatedAt: '2026-08-12' },
{ id: 5, name: '自住房', kind: 'house', amountCent: 180000000, ownerName: '我们', updatedAt: '2026-06-20' },
{ id: 6, name: '代步车', kind: 'car', amountCent: 8200000, ownerName: '我', updatedAt: '2026-06-20' },
{ id: 7, name: '房贷余额', kind: 'debt', amountCent: -142600000, ownerName: '我们', updatedAt: '2026-10-01' },
{ id: 8, name: '信用卡待还', kind: 'debt', amountCent: -3860000, ownerName: '爱人', updatedAt: '2026-10-08' },
{ id: 9, name: '车位', kind: 'house', amountCent: 12000000, ownerName: '我们', updatedAt: '2026-06-20' }
];
// ============================================================
// 资源
// ============================================================
export const FB_DEMO_RESOURCES: FbResourceItem[] = [
{ id: 1, name: '户口本', kind: 'document', ownerName: '全家', fileCount: 2, shared: true, updatedAt: '2026-09-14' },
{ id: 2, name: '房产证', kind: 'document', ownerName: '我们', fileCount: 3, shared: false, updatedAt: '2026-07-02' },
{ id: 3, name: '儿子学籍资料', kind: 'material', ownerName: '儿子', fileCount: 6, shared: true, updatedAt: '2026-09-01' },
{ id: 4, name: '全家保险合同', kind: 'document', ownerName: '全家', fileCount: 4, shared: true, updatedAt: '2026-08-12' },
{ id: 5, name: '装修图纸', kind: 'material', ownerName: '我', fileCount: 12, shared: false, updatedAt: '2026-05-19' },
{ id: 6, name: '车险单', kind: 'document', ownerName: '我', fileCount: 1, shared: false, updatedAt: '2026-04-08' }
];
// ============================================================
// 读书
// ============================================================
export const FB_DEMO_BOOKS: FbBookItem[] = [
{ id: 1, title: '置身事内', author: '兰小欢', status: 'reading', rate: 5, noteCount: 12, progress: 62 },
{ id: 2, title: '地球的红飘带', author: '魏巍', status: 'reading', rate: 4, noteCount: 5, progress: 28 },
{ id: 3, title: '蛤蟆先生去看心理医生', author: '罗伯特·戴博德', status: 'done', rate: 5, noteCount: 9, progress: 100 },
{ id: 4, title: '一日三秋', author: '刘震云', status: 'done', rate: 4, noteCount: 7, progress: 100 },
{ id: 5, title: '显微镜下的大明', author: '马伯庸', status: 'done', rate: 5, noteCount: 15, progress: 100 },
{ id: 6, title: '也许你该找个人聊聊', author: '洛莉·戈特利布', status: 'want', rate: 0, noteCount: 0, progress: 0 }
];
// ============================================================
// 导航
// ============================================================
export const FB_DEMO_NAV: FbNavLink[] = [
{ id: 1, title: 'Gitea 代码', url: 'https://code.yangyq.net', group: '开发', icon: '' },
{ id: 2, title: '阿里云控制台', url: 'https://ecs.console.aliyun.com', group: '开发', icon: '' },
{ id: 3, title: '腾讯云文档', url: 'https://docs.qq.com', group: '办公', icon: '' },
{ id: 4, title: '学校 OA', url: 'https://oa.example.edu.cn', group: '办公', icon: '' },
{ id: 5, title: '豆瓣', url: 'https://www.douban.com', group: '常用', icon: '' },
{ id: 6, title: '高德地图', url: 'https://www.amap.com', group: '常用', icon: '' },
{ id: 7, title: '知乎', url: 'https://www.zhihu.com', group: '常用', icon: '' },
{ id: 8, title: '国家气象中心', url: 'http://www.nmc.cn', group: '科研', icon: '' }
];
// ============================================================
// 笔记
// ============================================================
export const FB_DEMO_NOTES: FbNoteItem[] = [
{ id: 1, title: '阳台改造清单', excerpt: '1. 换推拉门(断桥铝,考虑隔音)2. 加一组置物架 3. 补两盆绿萝…', tags: ['家务', '装修'], updatedAt: '10-11 09:12', shared: false, attachCount: 2 },
{ id: 2, title: '读书摘抄 · 置身事内', excerpt: '「地方政府的行为,要从它面对的制度约束去理解。」……', tags: ['读书'], updatedAt: '10-10 22:40', shared: true, attachCount: 0 },
{ id: 3, title: '儿子的兴趣班时间表', excerpt: '周一 16:30 数学;周三 17:00 游泳;周六 09:00 钢琴…', tags: ['孩子'], updatedAt: '10-09 18:05', shared: true, attachCount: 1 },
{ id: 4, title: '崂山两日流水', excerpt: '第一天仰口 → 太清宫;第二天巨峰。民宿 680,两餐 218…', tags: ['旅行', '记账'], updatedAt: '10-04 20:30', shared: false, attachCount: 6 },
{ id: 5, title: '车里该备的东西', excerpt: '拖车绳、搭电线、应急灯、保温毯、常用药…', tags: ['车'], updatedAt: '09-25 07:48', shared: false, attachCount: 0 }
];
// ============================================================
// 通知
// ============================================================
export const FB_DEMO_NOTIFICATIONS: FbNotificationItem[] = [
{ id: 1, title: '爱人共享了「家庭日程」', body: '你现在可以看到这本日历的全部内容', timeText: '12 分钟前', unread: true, from: fbChip(2, '爱人', '配偶', '#9A5C7A') },
{ id: 2, title: '任务「换季衣物收纳」已逾期', body: '截止时间 10-09,现在还没完成', timeText: '2 小时前', unread: true, from: fbChip(2, '爱人', '配偶', '#9A5C7A') },
{ id: 3, title: '弟弟 在「观影」里加了一条', body: '《宇宙探索编辑部》', timeText: '昨天 21:04', unread: false, from: fbChip(7, '弟弟', '弟弟', '#3F7D5A') },
{ id: 4, title: '爸爸生日还有 222 天', body: '05-21,要不要提前记一下礼物', timeText: '3 天前', unread: false, from: fbChip(5, '爸爸', '父亲', '#8A6B4B') }
];
// ============================================================
// 分组工具(把平铺数组按某个字段切成 FbGroup)
// ============================================================
/** 按 roster 分组(名册页:家里的人 / 亲友) */
export function fbGroupRoster(list: FbRosterItem[]): FbGroup<FbRosterItem>[] {
return fbGroupBy(list, (it: FbRosterItem): string => it.group);
}
/** 按 status 分组建任务(待办 / 进行中 / 已完成) */
export function fbGroupTask(list: FbTaskItem[]): FbGroup<FbTaskItem>[] {
const order: string[] = ['doing', 'todo', 'done'];
const titles: string[] = ['进行中', '待办', '已完成'];
const groups: FbGroup<FbTaskItem>[] = [];
for (let i = 0; i < order.length; i++) {
const key: string = order[i];
const items: FbTaskItem[] = list.filter((t: FbTaskItem): boolean => t.status === key);
if (items.length > 0) {
groups.push({ key: key, title: titles[i], items: items, count: items.length.toString() + ' 项' });
}
}
return groups;
}
/** 按 kind 分组建资源(证件 / 资料 / 其他) */
export function fbGroupResource(list: FbResourceItem[]): FbGroup<FbResourceItem>[] {
return fbGroupBy(list, (it: FbResourceItem): string => fbResourceKindName(it.kind));
}
/** 按 status 分组建书架(在读 / 想读 / 读完) */
export function fbGroupBook(list: FbBookItem[]): FbGroup<FbBookItem>[] {
const order: string[] = ['reading', 'want', 'done'];
const titles: string[] = ['在读', '想读', '读完'];
const groups: FbGroup<FbBookItem>[] = [];
for (let i = 0; i < order.length; i++) {
const key: string = order[i];
const items: FbBookItem[] = list.filter((b: FbBookItem): boolean => b.status === key);
if (items.length > 0) {
groups.push({ key: key, title: titles[i], items: items, count: items.length.toString() + ' 本' });
}
}
return groups;
}
/** 按 group 分组建导航 */
export function fbGroupNav(list: FbNavLink[]): FbGroup<FbNavLink>[] {
return fbGroupBy(list, (it: FbNavLink): string => it.group);
}
/** 通用分组:保持首次出现的顺序(不做字典序,免得打乱业务排序) */
function fbGroupBy<T>(list: T[], keyOf: (it: T) => string): FbGroup<T>[] {
const keys: string[] = [];
const buckets: T[][] = [];
for (const it of list) {
const k: string = keyOf(it);
let idx: number = keys.indexOf(k);
if (idx < 0) {
keys.push(k);
buckets.push([]);
idx = keys.length - 1;
}
buckets[idx].push(it);
}
const out: FbGroup<T>[] = [];
for (let i = 0; i < keys.length; i++) {
out.push({
key: keys[i],
title: keys[i],
items: buckets[i],
count: buckets[i].length.toString()
});
}
return out;
}
/** 资源类别的中文名 */
export function fbResourceKindName(kind: string): string {
if (kind === 'document') {
return '证件';
}
if (kind === 'material') {
return '资料';
}
return '其他';
}
/** 账目类别的中文名 */
export function fbLedgerKindName(kind: string): string {
if (kind === 'deposit') {
return '存款';
}
if (kind === 'fund') {
return '基金';
}
if (kind === 'insurance') {
return '保险';
}
if (kind === 'house') {
return '房产';
}
if (kind === 'car') {
return '车辆';
}
if (kind === 'debt') {
return '负债';
}
return '其他';
}
/** 分 → 「¥12.3 万」这样的中文金额(Web 端 Ledger::money 同口径) */
export function fbMoney(cent: number): string {
const yuan: number = cent / 100;
const neg: boolean = yuan < 0;
const abs: number = Math.abs(yuan);
let text: string;
if (abs >= 100000000) {
text = (abs / 100000000).toFixed(2) + ' 亿';
} else if (abs >= 10000) {
text = (abs / 10000).toFixed(2) + ' 万';
} else {
text = abs.toFixed(2);
}
return (neg ? '-¥' : '¥') + text;
}
+198
View File
@@ -0,0 +1,198 @@
/**
* 家书 FamilyBook · 模块注册中心(鸿蒙端)
*
* 与 Web 端 `app/Module.php` 一一对应 —— 那边是「有哪些模块 / 谁在导航条上 /
* 各自什么主题色」的**唯一来源**,客户端这份是它在 ArkTS 侧的镜像。
*
* 三条分工(与 Web 端一致,别混):
* · **模块本身**(有哪些、叫什么、默认什么色)→ 这里的 FB_MODULES,写死在代码里;
* · **谁在抽屉里 / 什么顺序** → 后续接到接口后由 user_settings('nav_modules') 决定,
* 没配过就用 DEFAULT_NAV;
* · **主题色** → 后续由 settings('module_colors')(家庭级)下发。
*
* ⚠️ key 必须等于 Web 端路由名(r=xxx):接口对接、页面高亮、「本页属于哪个模块」
* 全都按 key 走,两边各写一份迟早对不上。
*
* ⚠️ 命名前缀:ArkTS 不支持声明合并(arkts-no-decl-merging),不同文件的顶层
* interface/class/struct 名不能重复。本文件所有导出名统一加 `Fb` 前缀。
*/
/** 一个功能模块的静态定义 */
export interface FbModuleDef {
/** 模块 key,等于 Web 端路由名(home / kin / people / …) */
key: string;
/** 抽屉里显示的名字 */
name: string;
/** 默认主题色(家庭级设置可覆盖) */
color: string;
/** 一句话说明(抽屉里那行小灰字) */
desc: string;
}
/**
* 全部模块(顺序与 Web 端 `Module::LIST` 一致)。
*
* ⚠️ 主题色一律挑中低饱和、中低明度的:按钮上是白字,太浅会看不清,
* 太艳会盖过内容的纸墨底色(家书整体是宣纸米 + 砖红的暖色调)。
*/
export const FB_MODULES: FbModuleDef[] = [
{ key: 'home', name: '首页', color: '#A84A28', desc: '今天要看的东西' },
{ key: 'kin', name: '家谱', color: '#8A6B4B', desc: '以我为中心的一张网' },
{ key: 'people', name: '名册', color: '#4A7C82', desc: '家庭名册与亲友通讯录' },
{ key: 'movies', name: '观影', color: '#7A4E8C', desc: '一家人看过的电影' },
{ key: 'trips', name: '旅程', color: '#3F7D5A', desc: '去过的地方与行程' },
{ key: 'calendar', name: '日程', color: '#B0763A', desc: '日历 / 重复日程 / 提醒' },
{ key: 'resources', name: '资源', color: '#5566A8', desc: '证件 / 资料 / 附件' },
{ key: 'tasks', name: '任务', color: '#A8574F', desc: '看板与待办' },
{ key: 'days', name: '纪念日', color: '#B4633C', desc: '生日与重要日子' },
{ key: 'ledger', name: '财务', color: '#5E7A3C', desc: '账本与账目' },
{ key: 'books', name: '读书', color: '#6B5B95', desc: '书架与读书笔记' },
{ key: 'nav', name: '导航', color: '#3E7A9E', desc: '我的个人导航' },
{ key: 'notes', name: '笔记', color: '#7A6A4F', desc: '随手记的分块笔记' }
];
/**
* 默认出现在抽屉里的模块(没自定义过就用它)。
*
* ⚠️ 与 Web 端 `Module::DEFAULT_NAV` 保持一致:**家谱默认不上导航条**
* (用户原话「毕竟,不需要每天都查看家谱」),但它仍在抽屉的「更多」里能勾出来。
*/
export const FB_DEFAULT_NAV: string[] = [
'home', 'people', 'movies', 'trips', 'calendar',
'resources', 'tasks', 'days', 'ledger', 'books', 'nav', 'notes'
];
/**
* 主题色的预设色板(设置页点一下就换)。
*
* ⚠️ 与 Web 端 `Module::PRESET_COLORS` 是**同一套值** —— 两端配色预设必须一致,
* 否则同一个人在手机和电脑上选出的「观影紫」会是两种紫。
* ⚠️ 一律中低饱和、中低明度:fbThemeVars 会把主色压到 L≈0.42 当按钮底色、
* L≈0.28 当字色 —— 太浅则白字在按钮上看不清,太艳则盖过纸墨底的本色。
*/
export const FB_PRESET_COLORS: string[] = [
'#A84A28', '#8A6B4B', '#4A7C82', '#7A4E8C', '#3F7D5A',
'#B0763A', '#5566A8', '#A8574F', '#5C8A48', '#6B5B95',
'#9A5C7A', '#3D6B8E'
];
/** 按 key 取模块定义(认不出返回 undefined,调用方自己兜底) */
export function fbModuleOf(key: string): FbModuleDef | undefined {
return FB_MODULES.find((m: FbModuleDef): boolean => m.key === key);
}
/** 模块名(认不出返回 key 本身,避免抽屉里出现空白条目) */
export function fbModuleName(key: string): string {
const m = fbModuleOf(key);
return m !== undefined ? m.name : key;
}
/** 模块主题色(认不出回落到首页砖红) */
export function fbModuleColor(key: string): string {
const m = fbModuleOf(key);
return m !== undefined ? m.color : '#A84A28';
}
/** 默认抽屉列表对应的模块定义数组(顺序照 FB_DEFAULT_NAV) */
export function fbDefaultNavModules(): FbModuleDef[] {
const out: FbModuleDef[] = [];
for (const k of FB_DEFAULT_NAV) {
const m = fbModuleOf(k);
if (m !== undefined) {
out.push(m);
}
}
return out;
}
/**
* 把「#RRGGBB」压成三档同色系(与 Web 端 `Module::cssVars()` 同一算法)。
*
* 为什么要三档而不是只给一个主色:按钮 hover 色与文字色都是从主色推出来的
* 深浅两档,只换主色会让 hover 变成别的颜色、或者白字压在浅底上看不清。
* 这里统一按 HSL 压明度,保证三档永远同色系。
*/
export interface FbThemeVars {
/** 按钮底色(L≈0.42) */
brand: string;
/** hover / 强调(L≈0.58) */
brand2: string;
/** 文字色(L≈0.28) */
ink: string;
}
/** hex → [h(0..1), s, l] */
function fbHexToHsl(hex: string): number[] {
const s: string = hex.startsWith('#') ? hex.substring(1) : hex;
const r: number = parseInt(s.substring(0, 2), 16) / 255;
const g: number = parseInt(s.substring(2, 4), 16) / 255;
const b: number = parseInt(s.substring(4, 6), 16) / 255;
const mx: number = Math.max(r, g, b);
const mn: number = Math.min(r, g, b);
const l: number = (mx + mn) / 2;
const d: number = mx - mn;
if (d === 0) {
return [0, 0, l];
}
const den: number = 1 - Math.abs(2 * l - 1);
const sat: number = den > 1e-9 ? d / den : 0;
let h: number;
if (mx === r) {
h = ((g - b) / d) % 6;
} else if (mx === g) {
h = ((b - r) / d) + 2;
} else {
h = ((r - g) / d) + 4;
}
h = h * 60;
if (h < 0) {
h = h + 360;
}
return [h / 360, sat, l];
}
/** [h,s,l] → '#rrggbb' */
function fbHslToHex(h: number, sat: number, l: number): string {
const hh: number = h - Math.floor(h);
if (sat <= 0) {
const v: number = Math.round(l * 255);
return fbHex2(v) + fbHex2(v) + fbHex2(v);
}
const q: number = l < 0.5 ? l * (1 + sat) : l + sat - l * sat;
const p: number = 2 * l - q;
const conv = (t0: number): number => {
let t: number = t0 - Math.floor(t0);
if (t < 1 / 6) {
return p + (q - p) * 6 * t;
}
if (t < 1 / 2) {
return q;
}
if (t < 2 / 3) {
return p + (q - p) * (2 / 3 - t) * 6;
}
return p;
};
const r: number = Math.max(0, Math.min(255, Math.round(conv(hh + 1 / 3) * 255)));
const g: number = Math.max(0, Math.min(255, Math.round(conv(hh) * 255)));
const b: number = Math.max(0, Math.min(255, Math.round(conv(hh - 1 / 3) * 255)));
return fbHex2(r) + fbHex2(g) + fbHex2(b);
}
/** 0..255 → 两位大写十六进制 */
function fbHex2(v: number): string {
const t: string = v.toString(16).toUpperCase();
return t.length < 2 ? '0' + t : t;
}
/** 把一个主题色铺成三档 CSS 变量(客户端直接当 ResourceColor 用) */
export function fbThemeVars(hex: string): FbThemeVars {
const hsl: number[] = fbHexToHsl(hex.startsWith('#') ? hex : '#A84A28');
const h: number = hsl[0];
const sat: number = Math.max(0.28, Math.min(0.72, hsl[1]));
return {
brand: '#' + fbHslToHex(h, sat, 0.42),
brand2: '#' + fbHslToHex(h, sat, 0.58),
ink: '#' + fbHslToHex(h, sat, 0.28)
};
}
+447
View File
@@ -0,0 +1,447 @@
/**
* 家书 FamilyBook · 数据类型定义(鸿蒙端)
*
* ⚠️ 命名前缀:ArkTS 把同一 hap 内所有文件编译进**共享全局作用域**,
* 顶层 interface/class 重名会被判定为「声明合并」直接报错
* (arkts-no-decl-merging)。所以本文件所有类型统一加 `Fb` 前缀。
*
* ⚠️ 这份类型表是**照 Web 端页面实际渲染的字段**归纳出来的(读 templates/ 与
* app/ 各自的取数逻辑得到),不是凭空设计的 —— 后续接接口时,服务端返回的
* JSON 应当能直接映射到这些字段上。字段名尽量与 Web 端保持一致(如
* `starts_at` 改成 `startsAt` 只是风格统一,语义不变)。
*/
// ============================================================
// 账号 / 家庭(阶段 0:名册、账号、登录、权限模型)
// ============================================================
/** 一个家庭成员账号(对应 Web 端 `people` + `accounts`) */
export interface FbAccount {
/** persons.id */
id: number;
/** 姓名(Web 端首页显示的是"我叫他什么",这里先用本名) */
name: string;
/** 「我」对他的称呼(relations 派生,如「爸爸」「儿子」) */
kinship: string;
/** 头像色(Web 端 AvatarColor 算法得出) */
avatarColor: string;
/** 角色:admin / member / guest(Web 端 accounts.role) */
role: string;
/** 是否为只读访客(Web 端 Auth::isGuest) */
isGuest: boolean;
/** 是否有写权限(只读账号在页面上要隐藏所有新建/编辑入口) */
canWrite: boolean;
}
/** 当前登录会话 */
export interface FbSession {
/** 是否已登录 */
loggedIn: boolean;
/** 家庭 id */
familyId: number;
/** 家庭名(首页大标题 Auth::homeTitle()) */
familyTitle: string;
/** 家庭格言 / slogan */
slogan: string;
/** 当前登录者 */
me: FbAccount;
}
// ============================================================
// 首页(对应 templates/home.php 的几个区块)
// ============================================================
/** 首页顶部信息面板里的「日子标记」(节气 / 节日 / 纪念日 / 生日) */
export interface FbTodayMark {
/** 标签文案,如「处暑」「中秋」「结婚纪念」 */
word: string;
/** 类别:solar_term / festival / anniversary / birthday */
kind: string;
}
/** 天气(Web 端 Home::weather 三级回落:自定义经纬度 → 内置城市表 → 不显示) */
export interface FbWeather {
/** 地点名 */
place: string;
/** 温度(摄氏度,整数) */
temp: number;
/** 天气描述,如「晴」「多云」 */
text: string;
/** 风向风力,如「东南风 3 级」 */
wind: string;
}
/** 首页统计块(近七天) */
export interface FbWeekStat {
/** 未来七天日程条数 */
events: number;
/** 待办件数 */
todos: number;
/** 逾期件数 */
overdue: number;
}
/** 首页「我的家人」里的一个人(带称谓与称呼) */
export interface FbKinChip {
id: number;
/** 显示名(优先用「我叫他什么」) */
shown: string;
/** 称谓 */
kinship: string;
/** 头像色 */
avatarColor: string;
/** 是否管理员(首页徽标) */
isAdmin: boolean;
}
/** 首页「模块卡片」—— 一块一块的入口,带一句关键数字 */
export interface FbModuleCard {
/** 模块 key(点击后切到哪个模块) */
key: string;
/** 模块名 */
name: string;
/** 大字:关键数字,如「12 部」「3 件在做」 */
big: string;
/** 小字说明(可多行) */
lines: string[];
/** 模块主题色 */
color: string;
}
// ============================================================
// 名册 / 家谱
// ============================================================
/** 名册里的一个人(people/index.php 的行) */
export interface FbRosterItem {
id: number;
name: string;
/** 称谓 */
kinship: string;
/** 分组名(people_groups) */
group: string;
/** 标签(person_tags) */
tags: string[];
/** 头像色 */
avatarColor: string;
/** 归属:member(家里的人)/ friend(亲友通讯录) */
roster: string;
/** 生日('MM-DD' 或 '') */
birthday: string;
/** 是否管理员徽标 */
isAdmin: boolean;
}
/** 家谱连线(kin/relate:父母 / 配偶 / 子女) */
export interface FbKinRelation {
id: number;
/** 从谁出发 */
fromId: number;
fromName: string;
/** 到谁 */
toId: number;
toName: string;
/** 关系类型:parent / spouse / child */
kind: string;
/** 称谓(Kinship::label 算出,如「爷爷」「外甥」) */
label: string;
}
// ============================================================
// 观影(movies)
// ============================================================
/** 观影记录(movies/index.php 的海报卡片) */
export interface FbMovieItem {
id: number;
/** 片名 */
title: string;
/** 观影时间显示文案(Movie::formatWatch),如「2026-10-05 19:30」 */
watchedAt: string;
/** 影院名(可为空) */
cinemaName: string;
/** 影院色标(movies/cinemas) */
cinemaColor: string;
/** 平均评分(0–10,0 表示没有评价) */
rating: number;
/** 评价条数 */
reviewCount: number;
/** 同看的人 */
viewers: FbKinChip[];
/** 海报是否已有(没有则画占位) */
hasPoster: boolean;
}
/** 观影统计(movies/stats) */
export interface FbMovieStat {
/** 每月一部柱状图的月份标签 */
months: string[];
/** 对应月份的数量 */
counts: number[];
/** 年度总览的关键数字 */
total: number;
/** 平均分 */
avgRating: number;
/** 最常去的影院 */
topCinema: string;
}
// ============================================================
// 旅程(trips)
// ============================================================
/** 旅程列表项(trips/index.php) */
export interface FbTripItem {
id: number;
/** 旅程名 */
title: string;
/** 副标题:城市 / 天数 */
sub: string;
/** 时间范围显示文案(从各段行程派生) */
dateRange: string;
/** 行程段数 */
nodeCount: number;
/** 已完成段数 */
doneCount: number;
}
/** 旅程里的一段行程(trips/show 的时间轴节点) */
export interface FbTripNode {
id: number;
/** 段标题,如「出发」「抵达民宿」 */
title: string;
/** 时间文案 */
when: string;
/** 地点 */
place: string;
/** 阶段:plan(计划)/ actual(实际) */
stage: string;
/** 状态:todo / done / skip */
status: string;
/** 交通方式(trips/modes) */
mode: string;
/** 花费(分) */
expenseCent: number;
}
// ============================================================
// 日程(calendar)
// ============================================================
/** 一条日程(calendar/index.php 的格子/时间轴条目) */
export interface FbCalEvent {
id: number;
/** 标题 */
title: string;
/** 日期 'YYYY-MM-DD' */
date: string;
/** 时间 'HH:mm'(全天事件为空串) */
time: string;
/** 所属日历本名 */
calName: string;
/** 日历本色标 */
color: string;
/** 是否全天 */
allDay: boolean;
/** 地点(可为空) */
place: string;
}
/** 日历本(calendar/calendars) */
export interface FbCalBook {
id: number;
name: string;
color: string;
/** 我能不能写(Calendar::canWrite) */
canWrite: boolean;
/** 是否显示(Calendar::hiddenCalendars 的反面) */
visible: boolean;
/** 是否共享给别人 */
shared: boolean;
/** 是否是从 events 派生的「观影」「旅行」两本默认本 */
derived: boolean;
}
// ============================================================
// 任务(tasks)
// ============================================================
/** 任务卡片(tasks/index.php 看板) */
export interface FbTaskItem {
id: number;
title: string;
/** 状态:todo(待办)/ doing(进行中)/ done(已完成) */
status: string;
/** 所属看板名 */
board: string;
/** 看板主题色 */
boardColor: string;
/** 截止时间文案(空表示没有期限,不进日历) */
dueText: string;
/** 是否逾期 */
overdue: boolean;
/** 负责人显示名(空表示未指派) */
assignee: string;
/** 子任务完成数 / 总数 */
childDone: number;
childTotal: number;
}
// ============================================================
// 纪念日(days)
// ============================================================
/** 纪念日(days/index.php + person 生日) */
export interface FbAnniversaryItem {
id: number;
title: string;
/** 日期 'MM-DD' 或 'YYYY-MM-DD' */
date: string;
/** 距今还有几天(0 = 今天) */
daysLeft: number;
/** 类别:holiday(节日)/ anniversary(纪念日)/ birthday(生日)/ solar_term(节气) */
kind: string;
/** 相关的人(生日才有) */
people: FbKinChip[];
/** 是否每年重复 */
yearly: boolean;
}
// ============================================================
// 财务(ledger)
// ============================================================
/** 一本账(ledger/index.php 顶部概览) */
export interface FbLedgerBook {
id: number;
name: string;
/** 净资产(分) */
netCent: number;
/** 我的权限:owner / read / write */
myPerm: string;
/** 账目条数 */
entryCount: number;
}
/** 一条账目 */
export interface FbLedgerEntry {
id: number;
/** 账目名,如「工资卡」「房贷」 */
name: string;
/** 类别:deposit(存款)/ fund(基金)/ insurance(保险)/ house(房子)/ car(车子)/ debt(负债)… */
kind: string;
/** 金额(分;负债为负) */
amountCent: number;
/** 挂名的人 */
ownerName: string;
/** 记录/盘点时间 */
updatedAt: string;
}
// ============================================================
// 资源(resources)
// ============================================================
/** 一项资源(resources/index.php) */
export interface FbResourceItem {
id: number;
/** 名称,如「户口本」 */
name: string;
/** 类别:document(证件)/ material(资料)/ other */
kind: string;
/** 归属人(空表示全家) */
ownerName: string;
/** 附件数 */
fileCount: number;
/** 是否共享给我 */
shared: boolean;
/** 更新时间 */
updatedAt: string;
}
// ============================================================
// 读书(books)
// ============================================================
/** 一本书(books/index.php 书架) */
export interface FbBookItem {
id: number;
title: string;
author: string;
/** 状态:want(想读)/ reading(在读)/ done(读完) */
status: string;
/** 评分 0–5(0 表示未评) */
rate: number;
/** 笔记条数 */
noteCount: number;
/** 阅读进度(0–100,done 时为 100) */
progress: number;
}
// ============================================================
// 导航(nav)
// ============================================================
/** 一条个人导航(nav/index.php) */
export interface FbNavLink {
id: number;
title: string;
url: string;
/** 分组名 */
group: string;
/** 图标名(web 端用 favicon,端上先留空用首字母) */
icon: string;
}
// ============================================================
// 笔记(notes)
// ============================================================
/** 一条笔记 */
export interface FbNoteItem {
id: number;
title: string;
/** 摘要(正文前若干字) */
excerpt: string;
/** 标签 */
tags: string[];
/** 更新时间 */
updatedAt: string;
/** 是否共享给别人 */
shared: boolean;
/** 附件数 */
attachCount: number;
}
// ============================================================
// 通知(notifications)
// ============================================================
/** 一条站内通知(Notification::mine) */
export interface FbNotificationItem {
id: number;
title: string;
body: string;
/** 相对时间,如「3 分钟前」 */
timeText: string;
/** 是否未读 */
unread: boolean;
/** 触发人(头像色 + 显示名) */
from: FbKinChip;
}
// ============================================================
// 通用:列表分组(多个页面共用「按字母/类别分组」的形态)
// ============================================================
/** 一个分组(名册按分组、导航按分类、日历按日历本…都用它) */
export interface FbGroup<T> {
/** 分组键(唯一) */
key: string;
/** 分组标题 */
title: string;
/** 组内条目 */
items: T[];
/** 组标题旁的小字(如「12 人」) */
count: string;
}