Files
FamilyBook_harmonyos/entry/src/main/ets/common/Theme.ets
T
laoyang 41e3aa3d95 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 类根因后归零。)
2026-10-11 22:07:48 +08:00

254 lines
9.1 KiB
Plaintext
Raw 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.
/**
* 家书 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');
}
}