对应服务端 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 类根因后归零。)
254 lines
9.1 KiB
Plaintext
254 lines
9.1 KiB
Plaintext
/**
|
||
* 家书 FamilyBook · 主题与设计令牌(鸿蒙端)
|
||
*
|
||
* 对应 Web 端 `assets/css/app.css` 里的 `:root { --paper / --ink / --brand … }`。
|
||
*
|
||
* 配色取自图标:宣纸米 / 砖红封面 / 深褐书脊 / 纸白。
|
||
* 原则:扁平、留白充足、字重克制(正文 400,标题 500-600)。
|
||
*
|
||
* ⚠️ 深浅色两套值取自资源文件(`resources/base|dark/element/color.json`),
|
||
* 这里只做**语义名 → $r 资源**的映射,不硬编码 hex ——
|
||
* 这样系统切深色时不用改任何页面代码。
|
||
*/
|
||
|
||
/** 语义色令牌(全部指向资源,随系统深浅色自动切换) */
|
||
export class FbColors {
|
||
/** 宣纸底(页面背景) */
|
||
static readonly paper: Resource = $r('app.color.page_bg');
|
||
/** 纸白(卡片背景) */
|
||
static readonly surface: Resource = $r('app.color.surface');
|
||
/** 略暖的白(次级卡片 / 内嵌块) */
|
||
static readonly surfaceAlt: Resource = $r('app.color.surface_alt');
|
||
/** 输入框底 */
|
||
static readonly inputBg: Resource = $r('app.color.input_bg');
|
||
|
||
/** 主文字(墨) */
|
||
static readonly ink: Resource = $r('app.color.ink');
|
||
/** 次级文字 */
|
||
static readonly muted: Resource = $r('app.color.muted');
|
||
/** 最弱的文字(占位、时间戳) */
|
||
static readonly faint: Resource = $r('app.color.faint');
|
||
|
||
/** 品牌色(砖红)—— 未进具体模块时的默认色 */
|
||
static readonly brand: Resource = $r('app.color.brand');
|
||
static readonly brand2: Resource = $r('app.color.brand_2');
|
||
static readonly brandInk: Resource = $r('app.color.brand_ink');
|
||
|
||
/** 分割线 */
|
||
static readonly line: Resource = $r('app.color.line');
|
||
static readonly line2: Resource = $r('app.color.line_2');
|
||
|
||
/** 语义色:绿=成功 / 红=删除 / 蓝=编辑 / 黄=提醒 */
|
||
static readonly green: Resource = $r('app.color.green');
|
||
static readonly red: Resource = $r('app.color.red');
|
||
static readonly blue: Resource = $r('app.color.blue');
|
||
static readonly yellow: Resource = $r('app.color.yellow');
|
||
}
|
||
|
||
/** 间距 / 圆角 / 字号令牌(数值常量,vp/fp 由调用处加单位) */
|
||
export class FbMetric {
|
||
/** 页面左右内边距 */
|
||
static readonly pagePad: number = 16;
|
||
/** 卡片内边距 */
|
||
static readonly cardPad: number = 16;
|
||
/** 卡片间距 */
|
||
static readonly cardGap: number = 12;
|
||
|
||
/** 大圆角(卡片) */
|
||
static readonly radius: number = 16;
|
||
/** 小圆角(内嵌块 / 输入框) */
|
||
static readonly radiusS: number = 10;
|
||
/** 胶囊圆角 */
|
||
static readonly radiusPill: number = 999;
|
||
|
||
/** 正文 */
|
||
static readonly fontBody: number = 15;
|
||
/** 次级信息 */
|
||
static readonly fontSmall: number = 13;
|
||
/** 最小号(时间戳 / 计数) */
|
||
static readonly fontTiny: number = 11;
|
||
/** 卡片标题 */
|
||
static readonly fontTitle: number = 17;
|
||
/** 页面大标题 */
|
||
static readonly fontH1: number = 24;
|
||
|
||
/** 顶栏高度 */
|
||
static readonly topBarH: number = 56;
|
||
/** 抽屉宽度 */
|
||
static readonly drawerW: number = 264;
|
||
}
|
||
|
||
/**
|
||
* 模块主题色 → 客户端可用的三档调色板。
|
||
*
|
||
* 与 Web 端 `Module::cssVars()` 的差别:那边填进 CSS 变量(浏览器按色值渲染),
|
||
* 这边直接算成 hex 字符串给 ArkUI 用(ArkUI 的 ResourceColor 接受 '#RRGGBB')。
|
||
* 算法本身共用(见 `Modules.ets::fbThemeVars`)。
|
||
*/
|
||
export interface FbPalette {
|
||
/** 模块主色(图标 / 强调) */
|
||
main: string;
|
||
/** 按钮底色 */
|
||
brand: string;
|
||
/** hover / 浅底 */
|
||
brand2: string;
|
||
/** 文字色(深一档,保证在浅底上看得清) */
|
||
ink: string;
|
||
/** 极浅底色(选中行背景) */
|
||
wash: string;
|
||
}
|
||
|
||
/**
|
||
* 由模块主题色算出整套调色板。
|
||
*
|
||
* `wash` 是「选中行 / 标签底」用的极浅色 —— 做法是把主色降饱和、抬明度到 L≈0.94,
|
||
* 这样在宣纸米底色上只是一层淡淡的染色,不会抢内容。
|
||
*/
|
||
export function fbPalette(moduleColor: string): FbPalette {
|
||
const v = fbThemeVarsLocal(moduleColor);
|
||
return {
|
||
main: moduleColor,
|
||
brand: v.brand,
|
||
brand2: v.brand2,
|
||
ink: v.ink,
|
||
wash: fbWashOf(moduleColor)
|
||
};
|
||
}
|
||
|
||
/** 极浅染色底(L≈0.94,饱和度压到 0.35 上限) */
|
||
function fbWashOf(hex: string): string {
|
||
const hsl: number[] = fbHexToHslLocal(hex);
|
||
const sat: number = Math.min(0.35, hsl[1]);
|
||
return fbHslToHexLocal(hsl[0], sat, 0.94);
|
||
}
|
||
|
||
/** 局部副本:避免 Theme 依赖 Modules(两文件都能独立使用) */
|
||
function fbThemeVarsLocal(hex: string): FbThemeVarsLocal {
|
||
const hsl: number[] = fbHexToHslLocal(hex.startsWith('#') ? hex : '#A84A28');
|
||
const h: number = hsl[0];
|
||
const sat: number = Math.max(0.28, Math.min(0.72, hsl[1]));
|
||
return {
|
||
brand: '#' + fbHslToHexLocal(h, sat, 0.42),
|
||
brand2: '#' + fbHslToHexLocal(h, sat, 0.58),
|
||
ink: '#' + fbHslToHexLocal(h, sat, 0.28)
|
||
};
|
||
}
|
||
|
||
interface FbThemeVarsLocal {
|
||
brand: string;
|
||
brand2: string;
|
||
ink: string;
|
||
}
|
||
|
||
function fbHexToHslLocal(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];
|
||
}
|
||
|
||
function fbHslToHexLocal(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 fbH2(v) + fbH2(v) + fbH2(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 '#' + fbH2(r) + fbH2(g) + fbH2(b);
|
||
}
|
||
|
||
function fbH2(v: number): string {
|
||
const t: string = v.toString(16).toUpperCase();
|
||
return t.length < 2 ? '0' + t : t;
|
||
}
|
||
|
||
/** 图标资源名 → Resource(模块图标与通用图标都在这张表里) */
|
||
export class FbIcon {
|
||
/** 取模块图标(key 同 FB_MODULES 的 key) */
|
||
static module(key: string): Resource {
|
||
return fbIconOf('fb_ic_' + key);
|
||
}
|
||
|
||
/** 取通用图标(search / menu / close / bell / user / gear …) */
|
||
static ui(name: string): Resource {
|
||
return fbIconOf('fb_ic_' + name);
|
||
}
|
||
}
|
||
|
||
/** 动态资源名 → Resource(`$r` 需要字面量,这里用 switch 展开) */
|
||
function fbIconOf(name: string): Resource {
|
||
switch (name) {
|
||
case 'fb_ic_home': return $r('app.media.fb_ic_home');
|
||
case 'fb_ic_kin': return $r('app.media.fb_ic_kin');
|
||
case 'fb_ic_people': return $r('app.media.fb_ic_people');
|
||
case 'fb_ic_movies': return $r('app.media.fb_ic_movies');
|
||
case 'fb_ic_trips': return $r('app.media.fb_ic_trips');
|
||
case 'fb_ic_calendar': return $r('app.media.fb_ic_calendar');
|
||
case 'fb_ic_resources': return $r('app.media.fb_ic_resources');
|
||
case 'fb_ic_tasks': return $r('app.media.fb_ic_tasks');
|
||
case 'fb_ic_days': return $r('app.media.fb_ic_days');
|
||
case 'fb_ic_ledger': return $r('app.media.fb_ic_ledger');
|
||
case 'fb_ic_books': return $r('app.media.fb_ic_books');
|
||
case 'fb_ic_nav': return $r('app.media.fb_ic_nav');
|
||
case 'fb_ic_notes': return $r('app.media.fb_ic_notes');
|
||
case 'fb_ic_search': return $r('app.media.fb_ic_search');
|
||
case 'fb_ic_menu': return $r('app.media.fb_ic_menu');
|
||
case 'fb_ic_close': return $r('app.media.fb_ic_close');
|
||
case 'fb_ic_back': return $r('app.media.fb_ic_back');
|
||
case 'fb_ic_forward': return $r('app.media.fb_ic_forward');
|
||
case 'fb_ic_plus': return $r('app.media.fb_ic_plus');
|
||
case 'fb_ic_bell': return $r('app.media.fb_ic_bell');
|
||
case 'fb_ic_gear': return $r('app.media.fb_ic_gear');
|
||
case 'fb_ic_user': return $r('app.media.fb_ic_user');
|
||
case 'fb_ic_chart': return $r('app.media.fb_ic_chart');
|
||
case 'fb_ic_check': return $r('app.media.fb_ic_check');
|
||
case 'fb_ic_dot': return $r('app.media.fb_ic_dot');
|
||
case 'fb_ic_share': return $r('app.media.fb_ic_share');
|
||
case 'fb_ic_filter': return $r('app.media.fb_ic_filter');
|
||
case 'fb_ic_lock': return $r('app.media.fb_ic_lock');
|
||
case 'fb_ic_wallet': return $r('app.media.fb_ic_wallet');
|
||
case 'fb_ic_book': return $r('app.media.fb_ic_book');
|
||
case 'fb_ic_edit': return $r('app.media.fb_ic_edit');
|
||
case 'fb_ic_trash': return $r('app.media.fb_ic_trash');
|
||
default: return $r('app.media.fb_ic_dot');
|
||
}
|
||
} |