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:
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 左侧抽屉(鸿蒙端)
|
||||
*
|
||||
* 形态对齐 Web 端顶栏 `.nav` 那一排模块入口,但**改成抽屉**(用户建议):
|
||||
* Web 端导航条最多放 12 个模块,手机宽度下必然溢出;抽屉竖向排列,
|
||||
* 还能顺带放下「模块说明 + 主题色」这层信息,一眼看清每个模块是干什么的。
|
||||
*
|
||||
* 两条形态,同一个组件:
|
||||
* · **常驻侧栏**(宽屏:窗口短边 ≥ 600vp)—— 固定在左边,占 264vp;
|
||||
* · **滑出层**(手机竖屏)—— 覆盖在内容上,点遮罩或选中项即收起。
|
||||
*
|
||||
* ⚠️ 这里**不用 SideBarContainer**:它的控制按钮与「滑出」是内建的一套,
|
||||
* 样式(图标、位置、宽度动画)很难与家书的视觉统一,且它只能有一层侧栏
|
||||
* 内容 —— 而我们要在抽屉里放「品牌区 + 模块列表 + 底部账号区」三段。
|
||||
* 自己用 Stack + 位移动画做,行为完全可控。
|
||||
*/
|
||||
|
||||
import { FbColors, FbIcon, FbMetric } from '../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../common/Store';
|
||||
import { FbModuleDef, FB_MODULES, fbDefaultNavModules, fbThemeVars } from '../model/Modules';
|
||||
import { FbAvatar } from './Widgets';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbDrawer {
|
||||
/** 全局状态(父组件传入,保证与页面同一份实例) */
|
||||
@Param app: FbAppState = fbAppState();
|
||||
/** 抽屉宽度 */
|
||||
@Param drawerW: number = FbMetric.drawerW;
|
||||
/** 是否以「覆盖层」形态出现(宽屏时为 false = 常驻) */
|
||||
@Param overlayMode: boolean = true;
|
||||
/** 覆盖层形态下:点遮罩关闭 */
|
||||
@Event onDismiss: () => void = () => {
|
||||
};
|
||||
|
||||
/** 抽屉里列出的模块(接口接上后由 user_settings('nav_modules') 下发) */
|
||||
@Local items: FbModuleDef[] = fbDefaultNavModules();
|
||||
|
||||
/** 品牌区:这里放阅文风格的书名(家书)+ 家庭名 */
|
||||
@Builder
|
||||
brandHeader() {
|
||||
Row({ space: 10 }) {
|
||||
Image($r('app.media.fb_logo'))
|
||||
.width(34)
|
||||
.height(34)
|
||||
.borderRadius(9)
|
||||
Column({ space: 1 }) {
|
||||
Text('家书')
|
||||
.fontSize(17)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.letterSpacing(1)
|
||||
Text(this.app.session.familyTitle)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 18, right: 12, top: 18, bottom: 14 })
|
||||
}
|
||||
|
||||
/** 一个模块行 */
|
||||
@Builder
|
||||
moduleRow(m: FbModuleDef) {
|
||||
Row({ space: 12 }) {
|
||||
// 图标底:选中时用模块色实底(白图标),未选中时用极浅染色底(彩色图标)
|
||||
Stack() {
|
||||
Image(FbIcon.module(m.key))
|
||||
.width(18)
|
||||
.height(18)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(this.isActive(m.key) ? Color.White : fbThemeVars(m.color).brand)
|
||||
}
|
||||
.width(34)
|
||||
.height(34)
|
||||
.borderRadius(10)
|
||||
.backgroundColor(this.isActive(m.key) ? fbThemeVars(m.color).brand : this.washOf(m.color))
|
||||
|
||||
Column({ space: 1 }) {
|
||||
Text(m.name)
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(this.isActive(m.key) ? FontWeight.Medium : FontWeight.Normal)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(m.desc)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 12, right: 12, top: 9, bottom: 9 })
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.backgroundColor(this.isActive(m.key) ? this.washOf(m.color) : Color.Transparent)
|
||||
.onClick(() => {
|
||||
this.app.switchModule(m.key);
|
||||
this.onDismiss();
|
||||
})
|
||||
}
|
||||
|
||||
/** 底部账号区(对应 Web 端右上角头像菜单里的那几项) */
|
||||
@Builder
|
||||
accountFooter() {
|
||||
Column({ space: 2 }) {
|
||||
Divider()
|
||||
.color(FbColors.line2)
|
||||
.margin({ bottom: 6 })
|
||||
|
||||
this.footerRow('fb_ic_bell', '通知消息', this.app.unread > 0 ? this.app.unread.toString() : '')
|
||||
this.footerRow('fb_ic_user', '个人设置', '')
|
||||
this.footerRow('fb_ic_lock', '修改密码', '')
|
||||
if (this.app.session.me.role === 'admin') {
|
||||
this.footerRow('fb_ic_gear', '系统设置', '')
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 12, right: 12, bottom: 10 })
|
||||
}
|
||||
|
||||
/** 底部一行 */
|
||||
@Builder
|
||||
footerRow(iconName: string, label: string, badge: string) {
|
||||
Row({ space: 12 }) {
|
||||
Image(FbIcon.ui(iconName))
|
||||
.width(17)
|
||||
.height(17)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.muted)
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
if (badge.length > 0) {
|
||||
Text(badge)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(Color.White)
|
||||
.backgroundColor(FbColors.red)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 10, right: 10, top: 9, bottom: 9 })
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
}
|
||||
|
||||
/** 当前是否选中该模块 */
|
||||
private isActive(key: string): boolean {
|
||||
return this.app.activeModule === key;
|
||||
}
|
||||
|
||||
/** 模块色的极浅染色底(选中行 / 图标底) */
|
||||
private washOf(hex: string): string {
|
||||
// 与 Theme.fbPalette 的 wash 同口径:压饱和、抬明度到 0.94
|
||||
const t = fbThemeVars(hex);
|
||||
return t.brand + '1A';
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
this.brandHeader()
|
||||
|
||||
// 模块列表:可滚动,模块多时不会把底部账号区挤掉
|
||||
List({ space: 1 }) {
|
||||
ForEach(this.items, (m: FbModuleDef) => {
|
||||
ListItem() {
|
||||
this.moduleRow(m)
|
||||
}
|
||||
}, (m: FbModuleDef): string => 'nav_' + m.key)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.padding({ left: 6, right: 6 })
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
|
||||
this.accountFooter()
|
||||
}
|
||||
.width(this.drawerW)
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.surface)
|
||||
// ⚠️ 侧栏右侧的分隔线只在常驻形态下需要;覆盖层形态下它贴着屏幕边,加了反而怪
|
||||
.border(this.overlayMode
|
||||
? { width: 0 }
|
||||
: { width: { right: 1 }, color: FbColors.line2 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 抽屉 + 内容 的整体骨架。
|
||||
*
|
||||
* 两种形态在这里分派:
|
||||
* · 宽屏 → Row(抽屉占位 + 内容区);
|
||||
* · 窄屏 → Stack(内容铺满,抽屉带遮罩浮在上面)。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct FbDrawerShell {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
/** 内容区(由父组件通过尾随 lambda 传入) */
|
||||
@BuilderParam content: () => void;
|
||||
|
||||
build() {
|
||||
Stack({ alignContent: Alignment.TopStart }) {
|
||||
// ---- 宽屏:并排 ----
|
||||
if (this.app.wideLayout) {
|
||||
Row() {
|
||||
FbDrawer({
|
||||
app: this.app,
|
||||
overlayMode: false
|
||||
})
|
||||
Column() {
|
||||
this.content()
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.height('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
} else {
|
||||
// ---- 窄屏:内容铺满 + 抽屉浮层 ----
|
||||
Column() {
|
||||
this.content()
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
if (this.app.drawerOpen) {
|
||||
// 遮罩
|
||||
Column()
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor('#66000000')
|
||||
.onClick(() => {
|
||||
this.app.drawerOpen = false;
|
||||
})
|
||||
.transition(TransitionEffect.OPACITY.animation({ duration: 180 }))
|
||||
|
||||
FbDrawer({
|
||||
app: this.app,
|
||||
overlayMode: true,
|
||||
onDismiss: (): void => {
|
||||
this.app.drawerOpen = false;
|
||||
}
|
||||
})
|
||||
.transition(TransitionEffect.translate({ x: -FbMetric.drawerW })
|
||||
.animation({ duration: 220, curve: Curve.FastOutSlowIn }))
|
||||
}
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 顶栏(鸿蒙端)
|
||||
*
|
||||
* 对应 Web 端 `templates/layout.php` 的 `<header class="topbar">`,但按手机习惯调整:
|
||||
* · 左:抽屉按钮(窄屏)/模块名(宽屏,此时抽屉常驻,不需要按钮)
|
||||
* · 中:当前模块名 + 一句副标题(Web 端顶栏只放 logo + 导航,端上加副标题更清楚)
|
||||
* · 右:通知铃铛(带未读角标)+ 头像(点开是账号菜单)
|
||||
*
|
||||
* ⚠️ 顶栏**不放**「设置」—— 与 Web 端一致:设置收在头像菜单里,
|
||||
* 顶栏只留日常入口,再把管理入口顶出来会把顶栏挤满。
|
||||
*/
|
||||
|
||||
import { FbColors, FbIcon, FbMetric } from '../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../common/Store';
|
||||
import { fbModuleOf } from '../model/Modules';
|
||||
import { FbAvatar } from './Widgets';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbTopBar {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
/** 右侧额外操作(各模块页自己往里塞「新建」「搜索」等按钮) */
|
||||
@BuilderParam actions: () => void;
|
||||
|
||||
build() {
|
||||
// 顶部安全区由外层容器统一处理(见 Index.ets),这里只占视觉高度
|
||||
Row({ space: 10 }) {
|
||||
// ---- 左:窄屏显示抽屉按钮 ----
|
||||
if (!this.app.wideLayout) {
|
||||
Button({ type: ButtonType.Circle }) {
|
||||
Image(FbIcon.ui('menu'))
|
||||
.width(18)
|
||||
.height(18)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.ink)
|
||||
}
|
||||
.width(36)
|
||||
.height(36)
|
||||
.backgroundColor(Color.Transparent)
|
||||
.onClick(() => {
|
||||
this.app.drawerOpen = true;
|
||||
})
|
||||
} else {
|
||||
Image($r('app.media.fb_logo'))
|
||||
.width(28)
|
||||
.height(28)
|
||||
.borderRadius(8)
|
||||
}
|
||||
|
||||
// ---- 中:当前模块名 ----
|
||||
Column({ space: 0 }) {
|
||||
Text(this.moduleName())
|
||||
.fontSize(17)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
if (this.moduleDesc().length > 0) {
|
||||
Text(this.moduleDesc())
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
// ---- 右:各模块自己的操作 ----
|
||||
this.actions()
|
||||
|
||||
// 通知铃铛(未读角标)
|
||||
Stack({ alignContent: Alignment.TopEnd }) {
|
||||
Button({ type: ButtonType.Circle }) {
|
||||
Image(FbIcon.ui('bell'))
|
||||
.width(18)
|
||||
.height(18)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.muted)
|
||||
}
|
||||
.width(36)
|
||||
.height(36)
|
||||
.backgroundColor(Color.Transparent)
|
||||
.onClick(() => {
|
||||
this.app.switchModule('__notifications');
|
||||
})
|
||||
|
||||
if (this.app.unread > 0) {
|
||||
Text(this.app.unread > 99 ? '99+' : this.app.unread.toString())
|
||||
.fontSize(10)
|
||||
.fontColor(Color.White)
|
||||
.backgroundColor(FbColors.red)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
|
||||
.margin({ top: 2, right: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
// 头像(点开账号菜单)
|
||||
FbAvatar({
|
||||
name: this.app.session.me.name,
|
||||
color: this.app.session.me.avatarColor,
|
||||
dia: 32,
|
||||
fontSizeVp: 14
|
||||
})
|
||||
.margin({ left: 2 })
|
||||
}
|
||||
.width('100%')
|
||||
.height(FbMetric.topBarH)
|
||||
.padding({ left: 8, right: 10 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.border({ width: { bottom: 1 }, color: FbColors.line2 })
|
||||
}
|
||||
|
||||
private moduleName(): string {
|
||||
const m = fbModuleOf(this.app.activeModule);
|
||||
return m !== undefined ? m.name : '家书';
|
||||
}
|
||||
|
||||
private moduleDesc(): string {
|
||||
const m = fbModuleOf(this.app.activeModule);
|
||||
return m !== undefined ? m.desc : '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 共享 UI 组件(鸿蒙端)
|
||||
*
|
||||
* 这些都是**无状态或轻状态**的展示件,对应 Web 端 `assets/css/app.css` 里
|
||||
* 那几个反复出现的类:`.card` / `.page-head` / `.empty` / `.hint` / `.node-badge` …
|
||||
*
|
||||
* ⚠️ 全部用 @ComponentV2 + @Param(只读入参),不在子组件里改父组件状态。
|
||||
* ⚠️ 所有 struct 加 `Fb` 前缀(arkts-no-decl-merging:同 hap 内顶层名不得重复)。
|
||||
*/
|
||||
|
||||
import { FbColors, FbIcon, FbMetric } from '../common/Theme';
|
||||
|
||||
/**
|
||||
* 圆点 / 色块(时间轴的点、日历的日程小点、看板的色标…)。
|
||||
*
|
||||
* ⚠️ 为什么不用 `Circle()`:它的 `.fill()` 是 **API 26.0.0** 才有的重载,
|
||||
* 本工程 compatibleSdkVersion 是 6.1.1(24),编译器会告警
|
||||
* 「This API is supported since SDK version 26.0.0」,真机上可能不生效。
|
||||
* 改用「方块 + 圆角 = 圆」的写法:`Column().width().height().borderRadius(dia/2)`
|
||||
* 只用通用属性,任何版本都成立,而且圆形效果完全等价。
|
||||
*
|
||||
* @Param dia 直径(vp)
|
||||
* @Param fillC 填充色(ResourceColor:hex 字符串或 $r 语义色)
|
||||
* @Param ringC 描边色(不传则不描边)
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct FbDot {
|
||||
@Param dia: number = 8;
|
||||
@Param fillC: ResourceColor = '#A84A28';
|
||||
@Param ringC?: ResourceColor = undefined;
|
||||
|
||||
build() {
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
// 底:填充
|
||||
Column()
|
||||
.width(this.dia)
|
||||
.height(this.dia)
|
||||
.borderRadius(this.dia / 2)
|
||||
.backgroundColor(this.fillC)
|
||||
// 描边(空心点用:内层挖一个背景色圆,等效于 1.4vp 环形)
|
||||
if (this.ringC !== undefined) {
|
||||
Column()
|
||||
.width(this.dia - 2.8)
|
||||
.height(this.dia - 2.8)
|
||||
.borderRadius((this.dia - 2.8) / 2)
|
||||
.backgroundColor(FbColors.surface)
|
||||
}
|
||||
}
|
||||
.width(this.dia)
|
||||
.height(this.dia)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 空心状态圈(任务卡的完成圈:未完成时空心、完成时实心 + 勾)。
|
||||
*
|
||||
* 同样避开 Circle().fill()/stroke(),用嵌套方块实现。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct FbCheckCircle {
|
||||
/** 直径 */
|
||||
@Param dia: number = 16;
|
||||
/** 是否已完成(实心 + 勾) */
|
||||
@Param done: boolean = false;
|
||||
/** 是否进行中(实心小点) */
|
||||
@Param doing: boolean = false;
|
||||
/** 主色 */
|
||||
@Param color: string = '#A8574F';
|
||||
|
||||
build() {
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
// 外圈:完成/进行中是实心主色,否则用浅灰实心(代表描边)
|
||||
Column()
|
||||
.width(this.dia)
|
||||
.height(this.dia)
|
||||
.borderRadius(this.dia / 2)
|
||||
.backgroundColor(this.done || this.doing ? this.color : '#A8988A')
|
||||
|
||||
// 内层:未勾选时用卡片底「挖空」,形成空心环
|
||||
if (!this.done && !this.doing) {
|
||||
Column()
|
||||
.width(this.dia - 2.8)
|
||||
.height(this.dia - 2.8)
|
||||
.borderRadius((this.dia - 2.8) / 2)
|
||||
.backgroundColor(FbColors.surface)
|
||||
}
|
||||
|
||||
// 已完成:打勾图标
|
||||
if (this.done) {
|
||||
Image($r('app.media.fb_ic_check'))
|
||||
.width(this.dia * 0.66)
|
||||
.height(this.dia * 0.66)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(Color.White)
|
||||
} else if (this.doing) {
|
||||
// 进行中:中心一个白点
|
||||
Column()
|
||||
.width(this.dia * 0.42)
|
||||
.height(this.dia * 0.42)
|
||||
.borderRadius(this.dia * 0.21)
|
||||
.backgroundColor(Color.White)
|
||||
}
|
||||
}
|
||||
.width(this.dia)
|
||||
.height(this.dia)
|
||||
}
|
||||
}
|
||||
|
||||
/** 空态:一块居中的提示卡(Web 端 `.card.empty`) */
|
||||
@ComponentV2
|
||||
export struct FbEmptyBox {
|
||||
/** 主标题 */
|
||||
@Param title: string = '这里还是空的';
|
||||
/** 副说明 */
|
||||
@Param hint: string = '';
|
||||
/** 可选的插图图标(默认圆点) */
|
||||
@Param icon: Resource = $r('app.media.fb_ic_dot');
|
||||
|
||||
build() {
|
||||
Column({ space: 8 }) {
|
||||
Image(this.icon)
|
||||
.width(34)
|
||||
.height(34)
|
||||
.opacity(0.35)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.faint)
|
||||
Text(this.title)
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
if (this.hint.length > 0) {
|
||||
Text(this.hint)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
.textAlign(TextAlign.Center)
|
||||
.lineHeight(20)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 40, bottom: 40, left: 24, right: 24 })
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
/** 区块小标题(Web 端 `.cinema-h2`:一行标题 + 可选的右侧说明) */
|
||||
@ComponentV2
|
||||
export struct FbSectionTitle {
|
||||
@Param title: string = '';
|
||||
@Param extra: string = '';
|
||||
|
||||
build() {
|
||||
Row() {
|
||||
Text(this.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.muted)
|
||||
.letterSpacing(0.6)
|
||||
if (this.extra.length > 0) {
|
||||
Blank()
|
||||
Text(this.extra)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 4, right: 4, top: 12, bottom: 8 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 头像(Web 端 `nav_avatar.php`:姓名首字 + 由名字哈希出的底色)。
|
||||
*
|
||||
* 端上不做「上传头像」—— 先把「首字 + 色块」这套做出来,
|
||||
* 接接口后再叠真实图片。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct FbAvatar {
|
||||
/** 显示名(取首字) */
|
||||
@Param name: string = '';
|
||||
/** 底色(由业务侧算好传进来) */
|
||||
@Param color: string = '#A84A28';
|
||||
/** 直径(vp) */
|
||||
@Param dia: number = 36;
|
||||
/** 字号(vp);不传则按直径推算 */
|
||||
@Param fontSizeVp: number = 0;
|
||||
|
||||
/** 取名字的第一个字(含中文名的姓) */
|
||||
private initial(): string {
|
||||
const t: string = this.name.trim();
|
||||
if (t.length === 0) {
|
||||
return '?';
|
||||
}
|
||||
return t.substring(0, 1);
|
||||
}
|
||||
|
||||
build() {
|
||||
Text(this.initial())
|
||||
.width(this.dia)
|
||||
.height(this.dia)
|
||||
.borderRadius(this.dia / 2)
|
||||
.backgroundColor(this.color)
|
||||
.fontColor(Color.White)
|
||||
.fontSize(this.fontSizeVp > 0 ? this.fontSizeVp : Math.round(this.dia * 0.42))
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.textAlign(TextAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 「同看的人」这类人员标签(Web 端 `person_chip`:小头像 + 称呼)。
|
||||
*
|
||||
* ⚠️ 会出现在 <a> 内部的行内场景,所以整块不可点、纯展示。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct FbPersonChip {
|
||||
@Param name: string = '';
|
||||
@Param color: string = '#A84A28';
|
||||
/** 是否显示文字(只放头像时传 false) */
|
||||
@Param withText: boolean = true;
|
||||
|
||||
build() {
|
||||
Row({ space: 4 }) {
|
||||
FbAvatar({ name: this.name, color: this.color, dia: 18, fontSizeVp: 10 })
|
||||
if (this.withText) {
|
||||
Text(this.name)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
}
|
||||
.padding({ left: 4, right: 8, top: 2, bottom: 2 })
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
}
|
||||
}
|
||||
|
||||
/** 小徽标(Web 端 `.node-badge` / `.badge`:管理员、访客、标签…) */
|
||||
@ComponentV2
|
||||
export struct FbTag {
|
||||
@Param text: string = '';
|
||||
@Param tone: string = 'muted';
|
||||
|
||||
private fgColor(): string {
|
||||
if (this.tone === 'brand') {
|
||||
return '#A84A28';
|
||||
}
|
||||
if (this.tone === 'red') {
|
||||
return '#C0533F';
|
||||
}
|
||||
if (this.tone === 'green') {
|
||||
return '#4E7C3F';
|
||||
}
|
||||
if (this.tone === 'blue') {
|
||||
return '#2F6FB0';
|
||||
}
|
||||
return '#7E6B5A';
|
||||
}
|
||||
|
||||
private bgColor(): string {
|
||||
if (this.tone === 'brand') {
|
||||
return '#14A84A28';
|
||||
}
|
||||
if (this.tone === 'red') {
|
||||
return '#14C0533F';
|
||||
}
|
||||
if (this.tone === 'green') {
|
||||
return '#146F9E5C';
|
||||
}
|
||||
if (this.tone === 'blue') {
|
||||
return '#142F6FB0';
|
||||
}
|
||||
return '#0F2C231D';
|
||||
}
|
||||
|
||||
build() {
|
||||
Text(this.text)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.fgColor())
|
||||
.backgroundColor(this.bgColor())
|
||||
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.maxLines(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页头的「设置 / 更多」按钮(Web 端 `.btn.btn-icon`)。
|
||||
*
|
||||
* ⚠️ 必须是 `Button() { Image() }` 形式:`Button('label') { ... }` 同时给
|
||||
* label 和子组件会报 10905202。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct FbIconButton {
|
||||
@Param icon: Resource = $r('app.media.fb_ic_gear');
|
||||
/** 图标色:ResourceColor 同时接受 Resource($r 语义色)与 '#RRGGBB' 字符串 */
|
||||
@Param tint: ResourceColor = FbColors.muted;
|
||||
/** 直径 */
|
||||
@Param dia: number = 36;
|
||||
@Event onTap: () => void = () => {
|
||||
};
|
||||
|
||||
build() {
|
||||
Button({ type: ButtonType.Circle }) {
|
||||
Image(this.icon)
|
||||
.width(18)
|
||||
.height(18)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(this.tint)
|
||||
}
|
||||
.width(this.dia)
|
||||
.height(this.dia)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.onClick(() => {
|
||||
this.onTap();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主操作按钮(Web 端 `.btn`:模块色底 + 白字)。
|
||||
*
|
||||
* ⚠️ 同样用 `Button() { Row() { … } }` 形式,label 与子组件二选一。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct FbPrimaryButton {
|
||||
@Param label: string = '';
|
||||
/** 底色(一般传模块主题色的 hex 字符串,也可传 Resource 语义色) */
|
||||
@Param bg: ResourceColor = '#A84A28';
|
||||
/** 前置图标(不传则只显示文字) */
|
||||
@Param icon?: Resource = undefined;
|
||||
/** 是否撑满宽度 */
|
||||
@Param block: boolean = false;
|
||||
@Event onTap: () => void = () => {
|
||||
};
|
||||
|
||||
build() {
|
||||
Button({ type: ButtonType.ROUNDED_RECTANGLE }) {
|
||||
Row({ space: 6 }) {
|
||||
if (this.icon !== undefined) {
|
||||
Image(this.icon)
|
||||
.width(16)
|
||||
.height(16)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(Color.White)
|
||||
}
|
||||
Text(this.label)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(Color.White)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
}
|
||||
.justifyContent(FlexAlign.Center)
|
||||
}
|
||||
.height(36)
|
||||
.padding({ left: 14, right: 14 })
|
||||
.backgroundColor(this.bg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.stateEffect(true)
|
||||
.width(this.block ? '100%' : undefined)
|
||||
.onClick(() => {
|
||||
this.onTap();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 一行「标签 + 值」的信息条(名册详情、观影信息、财务条目…都用它)。
|
||||
*
|
||||
* 对应 Web 端各页面里那种 `圆点 + 文字` 的行。
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct FbDotLine {
|
||||
/** 圆点色(不传则不画点) */
|
||||
@Param dotColor: string = '';
|
||||
/** 左侧主文字 */
|
||||
@Param label: string = '';
|
||||
/** 右侧值 */
|
||||
@Param tail: string = '';
|
||||
/** 主文字淡化成次要色(用于「没有内容」的占位行) */
|
||||
@Param muted: boolean = false;
|
||||
|
||||
build() {
|
||||
Row({ space: 8 }) {
|
||||
if (this.dotColor.length > 0) {
|
||||
FbDot({ dia: 7, fillC: this.dotColor })
|
||||
}
|
||||
Text(this.label)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(this.muted ? FbColors.faint : FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
if (this.tail.length > 0) {
|
||||
Text(this.tail)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
/** 一条「三格数字」统计条(首页近七天:日程 / 待办 / 逾期) */
|
||||
@ComponentV2
|
||||
export struct FbStatTriple {
|
||||
@Param labelA: string = '';
|
||||
@Param valueA: string = '';
|
||||
@Param labelB: string = '';
|
||||
@Param valueB: string = '';
|
||||
@Param labelC: string = '';
|
||||
@Param valueC: string = '';
|
||||
/** C 项是否用警示红(逾期数) */
|
||||
@Param cAlarm: boolean = false;
|
||||
|
||||
@Builder
|
||||
cell(label: string, value: string, alarm: boolean) {
|
||||
Column({ space: 2 }) {
|
||||
Text(value)
|
||||
.fontSize(20)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(alarm ? FbColors.red : FbColors.ink)
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
}
|
||||
|
||||
build() {
|
||||
Row() {
|
||||
this.cell(this.labelA, this.valueA, false)
|
||||
this.cell(this.labelB, this.valueB, false)
|
||||
this.cell(this.labelC, this.valueC, this.cAlarm)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 10, bottom: 10 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 进度条(读书进度、旅程完成度…)。
|
||||
*
|
||||
* ⚠️ 用 Progress 的 Linear 类型;`ProgressType` 枚举是 Linear / Ring / Eclipse…
|
||||
*/
|
||||
@ComponentV2
|
||||
export struct FbProgressLine {
|
||||
/** 0–100 */
|
||||
@Param percent: number = 0;
|
||||
@Param color: string = '#A84A28';
|
||||
|
||||
build() {
|
||||
Progress({ value: this.percent, total: 100, type: ProgressType.Linear })
|
||||
.width('100%')
|
||||
.height(4)
|
||||
.color(this.color)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
}
|
||||
}
|
||||
|
||||
/** 评分(观影 /·/10 分、读书 5 星)—— 用文字而非图形,避免臆造评分组件 */
|
||||
@ComponentV2
|
||||
export struct FbRating {
|
||||
/** 分值 */
|
||||
@Param score: number = 0;
|
||||
/** 满分(10 或 5) */
|
||||
@Param max: number = 10;
|
||||
|
||||
build() {
|
||||
Row({ space: 2 }) {
|
||||
Text('★')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor('#C08A2E')
|
||||
Text(this.max === 5 ? this.score.toFixed(1) : this.score.toFixed(1))
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#C08A2E')
|
||||
Text('/' + this.max.toString())
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user