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,183 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 主界面(鸿蒙端)
|
||||
*
|
||||
* 骨架:**左侧抽屉 + 右侧内容**,两套形态由窗口短边决定(见 FbAppState.wideLayout):
|
||||
* · 宽屏(平板 / 折叠屏展开 / 横屏手机):抽屉常驻,内容区并排;
|
||||
* · 窄屏:内容铺满,抽屉从左侧滑出(点遮罩或选中模块即收起)。
|
||||
*
|
||||
* 内容区 = 顶栏 + 当前模块页。此处只做**模块分发**,各模块的版面在各自文件里。
|
||||
*
|
||||
* ⚠️ 当前全部为**演示数据 / 静态界面**:还没有接口,所有「新建 / 编辑 / 删除」
|
||||
* 只做入口与样式,不落任何数据。接口到位后只要替换各页面里的数据源即可。
|
||||
*/
|
||||
|
||||
import { display, window } from '@kit.ArkUI';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { FbColors, FbMetric } from '../common/Theme';
|
||||
import { FbAppState, FB_WIDE_BREAKPOINT, fbAppState } from '../common/Store';
|
||||
import { FbDrawerShell } from '../components/Drawer';
|
||||
import { FbTopBar } from '../components/TopBar';
|
||||
import { FbHomePage } from './home/FbHomePage';
|
||||
import { FbMoviesPage } from './movies/FbMoviesPage';
|
||||
import { FbTripsPage } from './trips/FbTripsPage';
|
||||
import { FbCalendarPage } from './calendar/FbCalendarPage';
|
||||
import { FbTasksPage } from './tasks/FbTasksPage';
|
||||
import { FbPeoplePage } from './people/FbPeoplePage';
|
||||
import { FbResourcesPage } from './resources/FbResourcesPage';
|
||||
import { FbDaysPage } from './days/FbDaysPage';
|
||||
import { FbLedgerPage } from './ledger/FbLedgerPage';
|
||||
import { FbBooksPage } from './books/FbBooksPage';
|
||||
import { FbNavPage, FbNotesPage, FbNotificationsPage } from './misc/FbMiscPages';
|
||||
import { FbSettingsPage } from './settings/FbSettingsPage';
|
||||
|
||||
@Entry
|
||||
@ComponentV2
|
||||
struct Index {
|
||||
/** 全局状态(抽屉 / 当前模块 / 宽窄屏) */
|
||||
@Local app: FbAppState = fbAppState();
|
||||
/** 顶部安全区高度(状态栏),由窗口避让信息算出 */
|
||||
@Local topInset: number = 0;
|
||||
/** 底部安全区高度(手势条) */
|
||||
@Local bottomInset: number = 0;
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.measureInsets();
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// 顶部安全区:把内容推到状态栏下方(状态栏底色由 EntryAbility 设成页面底色)
|
||||
Column()
|
||||
.width('100%')
|
||||
.height(this.topInset)
|
||||
.backgroundColor(FbColors.surface)
|
||||
|
||||
// 主骨架:抽屉 + 内容
|
||||
Column() {
|
||||
FbDrawerShell({ app: this.app }) {
|
||||
this.mainArea()
|
||||
}
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
|
||||
// 底部安全区
|
||||
Column()
|
||||
.width('100%')
|
||||
.height(this.bottomInset)
|
||||
.backgroundColor(FbColors.surface)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
}
|
||||
|
||||
/** 内容区:顶栏 + 当前模块页 */
|
||||
@Builder
|
||||
mainArea() {
|
||||
Column() {
|
||||
// ---- 顶栏(右侧插槽由各模块页填充;这里统一留空,
|
||||
// 因为「新建/搜索」这类操作已经放进各页自己的操作条里了)----
|
||||
FbTopBar({ app: this.app }) {
|
||||
}
|
||||
|
||||
// ---- 模块页 ----
|
||||
Column() {
|
||||
this.moduleContent()
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
}
|
||||
|
||||
/** 按当前模块渲染对应页面(用 if/else 分发:ArkUI 的 build 里不允许 switch) */
|
||||
@Builder
|
||||
moduleContent() {
|
||||
if (this.app.activeModule === 'home') {
|
||||
FbHomePage({ app: this.app }) {
|
||||
}
|
||||
} else if (this.app.activeModule === 'people' || this.app.activeModule === 'kin') {
|
||||
FbPeoplePage({ app: this.app })
|
||||
} else if (this.app.activeModule === 'movies') {
|
||||
FbMoviesPage({ app: this.app })
|
||||
} else if (this.app.activeModule === 'trips') {
|
||||
FbTripsPage({ app: this.app })
|
||||
} else if (this.app.activeModule === 'calendar') {
|
||||
FbCalendarPage({ app: this.app })
|
||||
} else if (this.app.activeModule === 'tasks') {
|
||||
FbTasksPage({ app: this.app })
|
||||
} else if (this.app.activeModule === 'resources') {
|
||||
FbResourcesPage({ app: this.app })
|
||||
} else if (this.app.activeModule === 'days') {
|
||||
FbDaysPage({ app: this.app })
|
||||
} else if (this.app.activeModule === 'ledger') {
|
||||
FbLedgerPage({ app: this.app })
|
||||
} else if (this.app.activeModule === 'books') {
|
||||
FbBooksPage({ app: this.app })
|
||||
} else if (this.app.activeModule === 'nav') {
|
||||
FbNavPage({ app: this.app })
|
||||
} else if (this.app.activeModule === 'notes') {
|
||||
FbNotesPage({ app: this.app })
|
||||
} else if (this.app.activeModule === '__notifications') {
|
||||
FbNotificationsPage({ app: this.app })
|
||||
} else if (this.app.activeModule === '__settings') {
|
||||
FbSettingsPage({ app: this.app })
|
||||
} else {
|
||||
// 兜底:模块还没做页面时给一句提示,而不是白屏
|
||||
Column({ space: 8 }) {
|
||||
Text('这个模块还没有页面')
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontColor(FbColors.muted)
|
||||
Text('当前模块:' + this.app.activeModule)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 量安全区与宽窄屏。
|
||||
*
|
||||
* ⚠️ 状态栏高度**不写死数字**:不同机型(刘海、挖孔、无手势条)差很多,
|
||||
* 写死必然在某些机型上错位。这里按窗口避让信息取真实值。
|
||||
*
|
||||
* ⚠️ 窗口引用从 EntryAbility 放进 AppStorage(`fbMainWindow`)后再取 ——
|
||||
* `window.getLastWindow()` 只有异步版,在 aboutToAppear 里 await 会把
|
||||
* 首帧的布局延后一帧;而 `windowStage.getMainWindowSync()` 是同步的,
|
||||
* 由 Ability 侧取一次存下来最省事。
|
||||
*/
|
||||
private measureInsets(): void {
|
||||
try {
|
||||
const d: display.Display = display.getDefaultDisplaySync();
|
||||
const density: number = d.densityPixels;
|
||||
if (density <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 宽窄屏:按窗口短边(短边旋转时不变,故比例接近 1:1 的设备横竖屏布局天然一致)
|
||||
const shortSide: number = Math.min(d.width, d.height) / density;
|
||||
this.app.wideLayout = shortSide >= FB_WIDE_BREAKPOINT;
|
||||
|
||||
// 安全区:取窗口的真实避让矩形
|
||||
const win: window.Window | undefined =
|
||||
AppStorage.get<window.Window>('fbMainWindow');
|
||||
if (win !== undefined) {
|
||||
const top: window.AvoidArea = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
|
||||
this.topInset = top.topRect.height / density;
|
||||
const bottom: window.AvoidArea =
|
||||
win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
|
||||
this.bottomInset = bottom.bottomRect.height / density;
|
||||
}
|
||||
} catch (err) {
|
||||
// 取不到就退到「只判宽窄屏、安全区按 0」—— 页面自带 12vp 顶部留白,不会贴边
|
||||
hilog.warn(0x0000, 'FamilyBook', 'measureInsets failed: %{public}s', JSON.stringify(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 读书模块(鸿蒙端)
|
||||
*
|
||||
* 对应 Web 端 `templates/books/index.php`(书架 + 上传解析 + 在线读 + 笔记 + 统计)。
|
||||
* 端上先做**书架**这一层:按状态分组(在读 / 想读 / 读完),
|
||||
* 每本书显示作者、评分、笔记数、阅读进度。
|
||||
*
|
||||
* 「在线读」「上传 EPUB 解析」「笔记导入」这些属于交互+文件能力,留给第二轮。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbBookItem, FbGroup } from '../../model/Types';
|
||||
import { FB_DEMO_BOOKS, fbGroupBook } from '../../model/DemoData';
|
||||
import { FbEmptyBox, FbIconButton, FbProgressLine, FbRating, FbTag } from '../../components/Widgets';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbBooksPage {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
|
||||
/** 模块主题色(读书 = 藕紫) */
|
||||
private accent: string = '#6B5B95';
|
||||
private all: FbBookItem[] = FB_DEMO_BOOKS;
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// ---- 页头 ----
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('书架')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(this.all.length.toString() + ' 本 · '
|
||||
+ this.readingCount().toString() + ' 本在读 · '
|
||||
+ this.noteCount().toString() + ' 条笔记')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
// 上传 / 统计(Web 端在设置菜单里)
|
||||
FbIconButton({ icon: $r('app.media.fb_ic_chart'), tint: FbColors.muted })
|
||||
FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// ---- 分组列表 ----
|
||||
if (this.all.length === 0) {
|
||||
FbEmptyBox({
|
||||
title: '书架还是空的',
|
||||
hint: '传一本进来,或先记下想读的书',
|
||||
icon: $r('app.media.fb_ic_books')
|
||||
})
|
||||
} else {
|
||||
List({ space: 0 }) {
|
||||
ForEach(this.groups(), (g: FbGroup<FbBookItem>) => {
|
||||
ListItemGroup({ header: this.groupHeader(g.title, g.count) }) {
|
||||
ForEach(g.items, (b: FbBookItem) => {
|
||||
ListItem() {
|
||||
this.bookRow(b)
|
||||
}
|
||||
}, (b: FbBookItem): string => 'bk_' + b.id.toString())
|
||||
}
|
||||
}, (g: FbGroup<FbBookItem>): string => 'bg_' + g.key)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 })
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
/** 一本书:书脊色块(代替封面)+ 书名/作者 + 评分/笔记/进度 */
|
||||
@Builder
|
||||
bookRow(b: FbBookItem) {
|
||||
Row({ space: 12 }) {
|
||||
// 书脊:用书名首字 + 模块色底(与 Web 端的 book-cover-empty 占位同思路)
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
Column()
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.linearGradient({
|
||||
angle: 160,
|
||||
colors: [[this.accent, 0.0], ['#4A3D6B', 1.0]]
|
||||
})
|
||||
Text(b.title.substring(0, 1))
|
||||
.fontSize(20)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#FFFFFFD9')
|
||||
}
|
||||
.width(44)
|
||||
.height(60)
|
||||
.borderRadius(6)
|
||||
.clip(true)
|
||||
|
||||
Column({ space: 5 }) {
|
||||
Text(b.title)
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
|
||||
Text(b.author)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
|
||||
Row({ space: 10 }) {
|
||||
if (b.rate > 0) {
|
||||
FbRating({ score: b.rate, max: 5 })
|
||||
}
|
||||
if (b.noteCount > 0) {
|
||||
Row({ space: 3 }) {
|
||||
Image($r('app.media.fb_ic_book'))
|
||||
.width(11)
|
||||
.height(11)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.faint)
|
||||
Text(b.noteCount.toString())
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
}
|
||||
if (b.status === 'reading') {
|
||||
FbTag({ text: b.progress.toString() + '%', tone: 'brand' })
|
||||
}
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
if (b.status === 'reading') {
|
||||
FbProgressLine({ percent: b.progress, color: this.accent })
|
||||
}
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Image($r('app.media.fb_ic_forward'))
|
||||
.width(13)
|
||||
.height(13)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(14)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
|
||||
@Builder
|
||||
groupHeader(title: string, count: string) {
|
||||
Row({ space: 8 }) {
|
||||
Text(title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(count)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 4, right: 4, top: 12, bottom: 8 })
|
||||
.backgroundColor(FbColors.paper)
|
||||
}
|
||||
|
||||
private groups(): FbGroup<FbBookItem>[] {
|
||||
return fbGroupBook(this.all);
|
||||
}
|
||||
|
||||
private readingCount(): number {
|
||||
return this.all.filter((b: FbBookItem): boolean => b.status === 'reading').length;
|
||||
}
|
||||
|
||||
private noteCount(): number {
|
||||
let n: number = 0;
|
||||
for (const b of this.all) {
|
||||
n = n + b.noteCount;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 日程模块(鸿蒙端)
|
||||
*
|
||||
* 对应 Web 端 `templates/calendar/index.php`。Web 端是「月 / 周 / 天」三视图,
|
||||
* 左侧的「我的日历本」那一栏已被用户要求撤掉、功能收进右侧设置 —— 端上照此:
|
||||
* 日历占满宽度,日历本显隐 / 颜色 / 导入都在页头那个齿轮里。
|
||||
*
|
||||
* 端上保留了月视图(月历格子)与「选中日」的日程列表上下两块:
|
||||
* 手机屏幕放不下「月历 + 当天时间轴」并排,上下排列最自然。
|
||||
*
|
||||
* ⚠️ 月历格子用 7 列 Grid,每格显示「日号 + 该日条目数的小点」。
|
||||
* 真正的周/天时间轴视图(按半小时定位)建议放到第二轮做 —— 它需要
|
||||
* 手势与拖动建日程,属于交互实现,不在本轮「先出界面」的范围。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric, fbPalette } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbCalBook, FbCalEvent } from '../../model/Types';
|
||||
import { FB_DEMO_CAL_BOOKS, FB_DEMO_EVENTS_SOON, FB_DEMO_EVENTS_TODAY } from '../../model/DemoData';
|
||||
import { FbDot, FbEmptyBox, FbIconButton, FbPrimaryButton } from '../../components/Widgets';
|
||||
|
||||
/** 月历上「非本月」日期点的灰(hex 常量:格子数据结构里存的是字符串色,不能用 Resource) */
|
||||
const FB_FAINT: string = '#A8988A';
|
||||
const FB_MUTED: string = '#7E6B5A';
|
||||
|
||||
/** 月历格子(一个日期) */
|
||||
interface FbCalCell {
|
||||
/** 日期数字 */
|
||||
day: number;
|
||||
/** 是否属于当月 */
|
||||
inMonth: boolean;
|
||||
/** 是否今天 */
|
||||
isToday: boolean;
|
||||
/** 该日的事件数(0 不显示点) */
|
||||
count: number;
|
||||
/** 该日第一个事件的色(画小点用) */
|
||||
dotColor: string;
|
||||
}
|
||||
|
||||
@ComponentV2
|
||||
export struct FbCalendarPage {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
|
||||
/** 模块主题色(日程 = 琥珀) */
|
||||
private accent: string = '#B0763A';
|
||||
private books: FbCalBook[] = FB_DEMO_CAL_BOOKS;
|
||||
private todayEvents: FbCalEvent[] = FB_DEMO_EVENTS_TODAY;
|
||||
private soonEvents: FbCalEvent[] = FB_DEMO_EVENTS_SOON;
|
||||
|
||||
/** 当前显示的月份(首次进入取本月) */
|
||||
@Local showYear: number = new Date().getFullYear();
|
||||
@Local showMonth: number = new Date().getMonth() + 1;
|
||||
/** 选中的日期(0 = 今天) */
|
||||
@Local pickedDay: number = new Date().getDate();
|
||||
|
||||
private weekLabels: string[] = ['一', '二', '三', '四', '五', '六', '日'];
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// ---- 页头:月份 + 视图切换 + 新建 + 设置 ----
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text(this.showYear.toString() + ' 年 ' + this.showMonth.toString() + ' 月')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(this.visibleBooks().length.toString() + ' 本日历可见 · 本月 '
|
||||
+ this.monthEventCount().toString() + ' 条')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
FbPrimaryButton({
|
||||
label: '新建',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
})
|
||||
FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Scroll() {
|
||||
Column({ space: 0 }) {
|
||||
|
||||
// ---- 月历卡 ----
|
||||
Column({ space: 0 }) {
|
||||
// 月份切换条
|
||||
Row() {
|
||||
this.navBtn('上一月', -1)
|
||||
Blank()
|
||||
Text('今天')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(this.accent)
|
||||
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
|
||||
.backgroundColor(fbPalette(this.accent).brand + '14')
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick(() => {
|
||||
const d: Date = new Date();
|
||||
this.showYear = d.getFullYear();
|
||||
this.showMonth = d.getMonth() + 1;
|
||||
this.pickedDay = d.getDate();
|
||||
})
|
||||
Blank()
|
||||
this.navBtn('下一月', 1)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ bottom: 10 })
|
||||
|
||||
// 星期表头
|
||||
Row() {
|
||||
ForEach(this.weekLabels, (w: string) => {
|
||||
Text(w)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.layoutWeight(1)
|
||||
.textAlign(TextAlign.Center)
|
||||
}, (w: string): string => 'wk_' + w)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ bottom: 6 })
|
||||
|
||||
// 日期网格(7 列)
|
||||
Grid() {
|
||||
ForEach(this.cells(), (c: FbCalCell) => {
|
||||
GridItem() {
|
||||
this.dayCell(c)
|
||||
}
|
||||
}, (c: FbCalCell): string => 'cell_' + c.day.toString() + '_'
|
||||
+ (c.inMonth ? '1' : '0'))
|
||||
}
|
||||
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
|
||||
.rowsGap(2)
|
||||
.width('100%')
|
||||
.height(this.gridHeight())
|
||||
.scrollBar(BarState.Off)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad })
|
||||
|
||||
// ---- 日历本(可见的几本,横向排列当作图例)----
|
||||
Column({ space: 10 }) {
|
||||
Row({ space: 8 }) {
|
||||
ForEach(this.visibleBooks(), (b: FbCalBook) => {
|
||||
Row({ space: 5 }) {
|
||||
FbDot({ dia: 7, fillC: b.color })
|
||||
Text(b.name)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
}
|
||||
.padding({ right: 10 })
|
||||
}, (b: FbCalBook): string => 'book_' + b.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad + 4, right: FbMetric.pagePad + 4, top: 12 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
// ---- 选中日的日程 ----
|
||||
Column({ space: 0 }) {
|
||||
Row() {
|
||||
Text(this.pickedDay.toString() + ' 日')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Blank()
|
||||
Text(this.todayEvents.length.toString() + ' 条日程')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 4, right: 4, top: 12, bottom: 8 })
|
||||
|
||||
Column({ space: 0 }) {
|
||||
if (this.todayEvents.length === 0) {
|
||||
FbEmptyBox({
|
||||
title: '这天没有安排',
|
||||
hint: '在月历里点一下某天,就能在那天加日程',
|
||||
icon: $r('app.media.fb_ic_calendar')
|
||||
})
|
||||
} else {
|
||||
ForEach(this.todayEvents, (ev: FbCalEvent, idx: number) => {
|
||||
this.eventRow(ev, idx === this.todayEvents.length - 1)
|
||||
}, (ev: FbCalEvent): string => 'cev_' + ev.id.toString())
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, bottom: 6 })
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
|
||||
// ---- 未来七天 ----
|
||||
Column({ space: 0 }) {
|
||||
Row() {
|
||||
Text('未来七天')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Blank()
|
||||
Text('共 ' + this.soonEvents.length.toString() + ' 条')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 4, right: 4, top: 12, bottom: 8 })
|
||||
|
||||
Column({ space: 0 }) {
|
||||
ForEach(this.soonEvents, (ev: FbCalEvent, idx: number) => {
|
||||
this.eventRow(ev, idx === this.soonEvents.length - 1)
|
||||
}, (ev: FbCalEvent): string => 'sev_' + ev.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, bottom: 6 })
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 28 })
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
.align(Alignment.TopStart)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
/** 上一月 / 下一月按钮 */
|
||||
@Builder
|
||||
navBtn(label: string, delta: number) {
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick(() => {
|
||||
let m: number = this.showMonth + delta;
|
||||
let y: number = this.showYear;
|
||||
if (m < 1) {
|
||||
m = 12;
|
||||
y = y - 1;
|
||||
} else if (m > 12) {
|
||||
m = 1;
|
||||
y = y + 1;
|
||||
}
|
||||
this.showMonth = m;
|
||||
this.showYear = y;
|
||||
this.pickedDay = 0;
|
||||
})
|
||||
}
|
||||
|
||||
/** 一个日期格子 */
|
||||
@Builder
|
||||
dayCell(c: FbCalCell) {
|
||||
Column({ space: 3 }) {
|
||||
Text(c.day.toString())
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(c.isToday || c.day === this.pickedDay ? FontWeight.Medium : FontWeight.Normal)
|
||||
.fontColor(c.isToday
|
||||
? Color.White
|
||||
: (c.inMonth ? FbColors.ink : FbColors.faint))
|
||||
.width(26)
|
||||
.height(26)
|
||||
.textAlign(TextAlign.Center)
|
||||
.borderRadius(13)
|
||||
.backgroundColor(c.isToday
|
||||
? this.accent
|
||||
: (c.day === this.pickedDay && c.inMonth
|
||||
? fbPalette(this.accent).brand + '1F'
|
||||
: Color.Transparent))
|
||||
|
||||
// 事件小点(最多 3 个)
|
||||
Row({ space: 2 }) {
|
||||
if (c.count > 0) {
|
||||
ForEach(this.dots(c.count), (i: number) => {
|
||||
FbDot({ dia: 4, fillC: c.dotColor })
|
||||
}, (i: number): string => 'dot_' + c.day.toString() + '_' + i.toString())
|
||||
}
|
||||
}
|
||||
.height(5)
|
||||
}
|
||||
.width('100%')
|
||||
.height(44)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
.onClick(() => {
|
||||
this.pickedDay = c.day;
|
||||
})
|
||||
}
|
||||
|
||||
/** 一条日程行:时间 + 色点 + 标题 + 日历本标签 */
|
||||
@Builder
|
||||
eventRow(ev: FbCalEvent, isLast: boolean) {
|
||||
Column({ space: 0 }) {
|
||||
Row({ space: 10 }) {
|
||||
Text(ev.allDay ? '全天' : ev.time)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.width(44)
|
||||
FbDot({ dia: 8, fillC: ev.color })
|
||||
Text(ev.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(ev.calName)
|
||||
.fontSize(10)
|
||||
.fontColor(Color.White)
|
||||
.backgroundColor(ev.color)
|
||||
.borderRadius(4)
|
||||
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
|
||||
.maxLines(1)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 11, bottom: 11 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
if (!isLast) {
|
||||
Divider()
|
||||
.color(FbColors.line2)
|
||||
.margin({ left: 54 })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
/** 只列「显示着的」日历本(与 Web 端 Calendar::hiddenCalendars 的反面同口径) */
|
||||
private visibleBooks(): FbCalBook[] {
|
||||
return this.books.filter((b: FbCalBook): boolean => b.visible);
|
||||
}
|
||||
|
||||
/** 本月事件数(演示数据里用未来七天 + 今天的条数代表示意) */
|
||||
private monthEventCount(): number {
|
||||
return this.todayEvents.length + this.soonEvents.length;
|
||||
}
|
||||
|
||||
/** 造「点」的索引数组(ForEach 需要数组,不能直接循环次数) */
|
||||
private dots(count: number): number[] {
|
||||
const n: number = Math.min(3, count);
|
||||
const out: number[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
out.push(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 网格高度 = 行数 × 行高(6 行时 6×46) */
|
||||
private gridHeight(): number {
|
||||
const rows: number = Math.ceil(this.cells().length / 7);
|
||||
return rows * 46;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成当月月历格子。
|
||||
*
|
||||
* 周一起始(与 Web 端一致 —— 中文日历习惯周一为第一列)。
|
||||
*/
|
||||
private cells(): FbCalCell[] {
|
||||
const out: FbCalCell[] = [];
|
||||
const y: number = this.showYear;
|
||||
const m: number = this.showMonth;
|
||||
|
||||
const first: Date = new Date(y, m - 1, 1);
|
||||
// JS 的 getDay(): 0=周日 … 6=周六 ⇒ 换算成「周一为 0」
|
||||
const lead: number = (first.getDay() + 6) % 7;
|
||||
const daysInMonth: number = new Date(y, m, 0).getDate();
|
||||
const prevDays: number = new Date(y, m - 1, 0).getDate();
|
||||
|
||||
const now: Date = new Date();
|
||||
const isThisMonth: boolean = now.getFullYear() === y && (now.getMonth() + 1) === m;
|
||||
|
||||
// 前置:上月的尾巴
|
||||
for (let i = lead - 1; i >= 0; i--) {
|
||||
out.push({
|
||||
day: prevDays - i,
|
||||
inMonth: false,
|
||||
isToday: false,
|
||||
count: 0,
|
||||
dotColor: FB_MUTED
|
||||
});
|
||||
}
|
||||
// 本月
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const cnt: number = this.eventCountOf(d, isThisMonth);
|
||||
out.push({
|
||||
day: d,
|
||||
inMonth: true,
|
||||
isToday: isThisMonth && now.getDate() === d,
|
||||
count: cnt,
|
||||
dotColor: cnt > 0 ? this.accent : FB_FAINT
|
||||
});
|
||||
}
|
||||
// 补齐到整周(补下月的头)
|
||||
const tail: number = (7 - (out.length % 7)) % 7;
|
||||
for (let d = 1; d <= tail; d++) {
|
||||
out.push({
|
||||
day: d,
|
||||
inMonth: false,
|
||||
isToday: false,
|
||||
count: 0,
|
||||
dotColor: FB_FAINT
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 某天的日程条数(演示数据:今天取 todayEvents,其余从 soonEvents 的 'MM-DD' 匹配) */
|
||||
private eventCountOf(day: number, isThisMonth: boolean): number {
|
||||
if (!isThisMonth) {
|
||||
return 0;
|
||||
}
|
||||
const now: Date = new Date();
|
||||
if (now.getDate() === day) {
|
||||
return this.todayEvents.length;
|
||||
}
|
||||
const mm: string = this.showMonth < 10
|
||||
? '0' + this.showMonth.toString()
|
||||
: this.showMonth.toString();
|
||||
const dd: string = day < 10 ? '0' + day.toString() : day.toString();
|
||||
const key: string = mm + '-' + dd;
|
||||
let n: number = 0;
|
||||
for (const ev of this.soonEvents) {
|
||||
if (ev.date === key) {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 纪念日模块(鸿蒙端)
|
||||
*
|
||||
* 对应 Web 端 `templates/days/index.php`。
|
||||
*
|
||||
* Web 端的结构:**「即将到来」横条在顶**(30 天内的纪念日,含名册里每个人的生日),
|
||||
* 下面是全部纪念日的列表(可按类别筛)。
|
||||
*
|
||||
* 端上照此,但把「即将到来」做成**大数字卡片**(还剩几天用大字),
|
||||
* 因为「还有几天」是这个模块唯一真正要一眼看到的信息。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbAnniversaryItem, FbKinChip } from '../../model/Types';
|
||||
import { FB_DEMO_ANNIVERSARIES } from '../../model/DemoData';
|
||||
import { FbAvatar, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbDaysPage {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
|
||||
/** 模块主题色(纪念日 = 陶土橙) */
|
||||
private accent: string = '#B4633C';
|
||||
private all: FbAnniversaryItem[] = FB_DEMO_ANNIVERSARIES;
|
||||
/** 类别筛选('' = 全部) */
|
||||
@Local kindFilter: string = '';
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// ---- 页头 ----
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('纪念日')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text('节日 / 纪念日 / 生日,都在这一条时间线上')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
FbPrimaryButton({
|
||||
label: '添加',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
})
|
||||
FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Scroll() {
|
||||
Column({ space: 0 }) {
|
||||
|
||||
// ---- 即将到来(30 天内)----
|
||||
Column({ space: 0 }) {
|
||||
Row({ space: 8 }) {
|
||||
Text('即将到来')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Blank()
|
||||
Text('30 天内')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 4, right: 4, bottom: 10 })
|
||||
|
||||
if (this.upcoming().length === 0) {
|
||||
Text('三十天内没有纪念日')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
} else {
|
||||
// 横向排列的「大数字」卡
|
||||
Scroll() {
|
||||
Row({ space: 10 }) {
|
||||
ForEach(this.upcoming(), (a: FbAnniversaryItem) => {
|
||||
this.upcomingCard(a)
|
||||
}, (a: FbAnniversaryItem): string => 'up_' + a.id.toString())
|
||||
}
|
||||
}
|
||||
.scrollable(ScrollDirection.Horizontal)
|
||||
.scrollBar(BarState.Off)
|
||||
.width('100%')
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
// ---- 全部纪念日 ----
|
||||
Row({ space: 8 }) {
|
||||
this.kindChip('全部', '')
|
||||
this.kindChip('生日', 'birthday')
|
||||
this.kindChip('纪念日', 'anniversary')
|
||||
this.kindChip('节日', 'holiday')
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 14, bottom: 8 })
|
||||
|
||||
if (this.filtered().length === 0) {
|
||||
FbEmptyBox({
|
||||
title: '这里还没有纪念日',
|
||||
hint: '名册里填上生日,它就会自动出现在这儿',
|
||||
icon: $r('app.media.fb_ic_days')
|
||||
})
|
||||
} else {
|
||||
Column({ space: 0 }) {
|
||||
ForEach(this.filtered(), (a: FbAnniversaryItem, idx: number) => {
|
||||
this.annRow(a, idx === this.filtered().length - 1)
|
||||
}, (a: FbAnniversaryItem): string => 'ann_' + a.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, top: 4, bottom: 8 })
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 28 })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
.align(Alignment.TopStart)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
/** 即将到来的大数字卡 */
|
||||
@Builder
|
||||
upcomingCard(a: FbAnniversaryItem) {
|
||||
Column({ space: 4 }) {
|
||||
if (a.daysLeft === 0) {
|
||||
Text('今天')
|
||||
.fontSize(24)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(Color.White)
|
||||
} else {
|
||||
Row({ space: 1 }) {
|
||||
Text(a.daysLeft.toString())
|
||||
.fontSize(26)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(Color.White)
|
||||
Text('天')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor('#FFFFFFCC')
|
||||
.margin({ bottom: 4 })
|
||||
}
|
||||
.alignItems(VerticalAlign.Bottom)
|
||||
}
|
||||
|
||||
Text(a.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(Color.White)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(a.date)
|
||||
.fontSize(10)
|
||||
.fontColor('#FFFFFFB3')
|
||||
}
|
||||
.width(96)
|
||||
.padding(14)
|
||||
.backgroundColor(this.accent)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
/** 一行纪念日:日期 + 标题 + 相关人 + 天数 */
|
||||
@Builder
|
||||
annRow(a: FbAnniversaryItem, isLast: boolean) {
|
||||
Column({ space: 0 }) {
|
||||
Row({ space: 12 }) {
|
||||
// 日期块
|
||||
Column({ space: 0 }) {
|
||||
Text(this.monthDay(a.date))
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(this.yearText(a.date))
|
||||
.fontSize(9)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width(52)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
Column({ space: 4 }) {
|
||||
Row({ space: 6 }) {
|
||||
Text(a.title)
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
FbTag({ text: this.kindName(a.kind), tone: this.kindTone(a.kind) })
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// 相关人(生日才有)
|
||||
if (a.people.length > 0) {
|
||||
Row({ space: 6 }) {
|
||||
ForEach(a.people, (p: FbKinChip) => {
|
||||
Row({ space: 3 }) {
|
||||
FbAvatar({ name: p.shown, color: p.avatarColor, dia: 16, fontSizeVp: 9 })
|
||||
Text(p.shown)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.muted)
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (p: FbKinChip): string => 'ap_' + a.id.toString() + '_' + p.id.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
// 剩余天数
|
||||
Text(a.daysLeft === 0 ? '今天' : a.daysLeft.toString() + ' 天')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontWeight(a.daysLeft <= 7 ? FontWeight.Medium : FontWeight.Normal)
|
||||
.fontColor(a.daysLeft <= 7 ? this.accent : FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 12, bottom: 12 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
if (!isLast) {
|
||||
Divider().color(FbColors.line2).margin({ left: 64 })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
@Builder
|
||||
kindChip(label: string, key: string) {
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(this.kindFilter === key ? Color.White : FbColors.muted)
|
||||
.backgroundColor(this.kindFilter === key ? this.accent : FbColors.inputBg)
|
||||
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick(() => {
|
||||
this.kindFilter = key;
|
||||
})
|
||||
}
|
||||
|
||||
/** 'MM-DD' → '10-17';'YYYY-MM-DD' → '11-08' */
|
||||
private monthDay(date: string): string {
|
||||
const parts: string[] = date.split('-');
|
||||
if (parts.length === 3) {
|
||||
return parts[1] + '-' + parts[2];
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
/** 带年份的日期显示年份(如 '2019-11-08' → '2019'),不带则显示「每年」 */
|
||||
private yearText(date: string): string {
|
||||
const parts: string[] = date.split('-');
|
||||
if (parts.length === 3) {
|
||||
return parts[0];
|
||||
}
|
||||
return '每年';
|
||||
}
|
||||
|
||||
private kindName(kind: string): string {
|
||||
if (kind === 'birthday') {
|
||||
return '生日';
|
||||
}
|
||||
if (kind === 'holiday') {
|
||||
return '节日';
|
||||
}
|
||||
if (kind === 'solar_term') {
|
||||
return '节气';
|
||||
}
|
||||
return '纪念日';
|
||||
}
|
||||
|
||||
private kindTone(kind: string): string {
|
||||
if (kind === 'birthday') {
|
||||
return 'brand';
|
||||
}
|
||||
if (kind === 'holiday') {
|
||||
return 'red';
|
||||
}
|
||||
if (kind === 'solar_term') {
|
||||
return 'green';
|
||||
}
|
||||
return 'blue';
|
||||
}
|
||||
|
||||
private upcoming(): FbAnniversaryItem[] {
|
||||
return this.all.filter((a: FbAnniversaryItem): boolean => a.daysLeft <= 30);
|
||||
}
|
||||
|
||||
private filtered(): FbAnniversaryItem[] {
|
||||
if (this.kindFilter.length === 0) {
|
||||
return this.all;
|
||||
}
|
||||
return this.all.filter((a: FbAnniversaryItem): boolean => a.kind === this.kindFilter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 首页(鸿蒙端)
|
||||
*
|
||||
* 对应 Web 端 `templates/home.php`。版面照搬它的三段:
|
||||
* ① hero(家庭名 + 格言 + 我的资料/家谱两个快捷入口)
|
||||
* ② 信息面板(问候 + 日期/星期/农历 + 日子标记 | 右侧天气)
|
||||
* ③ 今天的日程 + 近期任务(两块并排;窄屏上下堆叠)
|
||||
* ④ 「往年的今天」回忆条
|
||||
* ⑤ 模块入口卡片(12 个模块的关键数字)
|
||||
*
|
||||
* ⚠️ 只做**界面**,不做交互实现:卡片点了切模块(前端状态),不发请求。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric, fbPalette } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbModuleCard, FbCalEvent, FbTaskItem, FbKinChip } from '../../model/Types';
|
||||
import {
|
||||
FB_DEMO_EVENTS_TODAY, FB_DEMO_EVENTS_SOON, FB_DEMO_KIN, FB_DEMO_MARKS,
|
||||
FB_DEMO_MODULE_CARDS, FB_DEMO_TASKS, FB_DEMO_WEATHER, FB_DEMO_WEEK_STAT
|
||||
} from '../../model/DemoData';
|
||||
import { FbAvatar, FbDot, FbDotLine, FbSectionTitle, FbStatTriple, FbTag } from '../../components/Widgets';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbHomePage {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
/** 顶栏右侧插槽(由外层传入,本页不用,留空占位以保持签名统一) */
|
||||
@BuilderParam topActions: () => void;
|
||||
|
||||
private cards: FbModuleCard[] = FB_DEMO_MODULE_CARDS;
|
||||
private todayEvents: FbCalEvent[] = FB_DEMO_EVENTS_TODAY;
|
||||
private soonEvents: FbCalEvent[] = FB_DEMO_EVENTS_SOON;
|
||||
private tasks: FbTaskItem[] = FB_DEMO_TASKS;
|
||||
|
||||
/** 问候语时段(与 Web 端 home.php 里那段 PHP 同口径) */
|
||||
private hello(): string {
|
||||
const h: number = new Date().getHours();
|
||||
if (h < 5) {
|
||||
return '夜深了';
|
||||
}
|
||||
if (h < 11) {
|
||||
return '早上好';
|
||||
}
|
||||
if (h < 13) {
|
||||
return '中午好';
|
||||
}
|
||||
if (h < 18) {
|
||||
return '下午好';
|
||||
}
|
||||
return '晚上好';
|
||||
}
|
||||
|
||||
/** 今天的日期与星期 */
|
||||
private dateLine(): string {
|
||||
const d: Date = new Date();
|
||||
const week: string[] = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
||||
return (d.getMonth() + 1).toString() + ' 月 ' + d.getDate().toString() + ' 日 · '
|
||||
+ week[d.getDay()];
|
||||
}
|
||||
|
||||
/** 天气图标(按文案粗略映射到已有的几个图标上,不引外部图) */
|
||||
private weatherIcon(): Resource {
|
||||
// TODO(接口):真实天气码 → 图标映射。现在只区分「有太阳/没太阳」两种。
|
||||
return this.weather().text.indexOf('晴') >= 0
|
||||
? $r('app.media.fb_ic_days')
|
||||
: $r('app.media.fb_ic_calendar');
|
||||
}
|
||||
|
||||
private weather() {
|
||||
return FB_DEMO_WEATHER;
|
||||
}
|
||||
|
||||
build() {
|
||||
Scroll() {
|
||||
Column({ space: 0 }) {
|
||||
|
||||
// ==================== ① hero ====================
|
||||
Row({ space: 12 }) {
|
||||
Image($r('app.media.fb_logo'))
|
||||
.width(52)
|
||||
.height(52)
|
||||
.borderRadius(14)
|
||||
|
||||
Column({ space: 3 }) {
|
||||
Text(this.app.session.familyTitle)
|
||||
.fontSize(FbMetric.fontH1)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(this.app.session.slogan)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
// 两个快捷入口:我的资料 / 我的家谱
|
||||
Row({ space: 8 }) {
|
||||
this.heroAction($r('app.media.fb_ic_user'), 'people')
|
||||
this.heroAction($r('app.media.fb_ic_kin'), 'kin')
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 16, bottom: 14 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// ==================== ② 信息面板 ====================
|
||||
Column({ space: 12 }) {
|
||||
Row() {
|
||||
Column({ space: 6 }) {
|
||||
Row({ space: 4 }) {
|
||||
Text(this.hello())
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(',' + this.app.session.me.name)
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontColor(FbColors.muted)
|
||||
}
|
||||
Row({ space: 8 }) {
|
||||
Text(this.dateLine())
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
Text('八月廿二')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
// 日子标记(节气 / 节日 / 生日)
|
||||
Flex({ wrap: FlexWrap.Wrap }) {
|
||||
ForEach(FB_DEMO_MARKS, (mk: string, idx: number) => {
|
||||
FbTag({ text: mk, tone: idx === 0 ? 'green' : 'brand' })
|
||||
.margin({ right: 6, top: 2 })
|
||||
}, (mk: string, idx: number): string => 'mk_' + idx.toString() + '_' + mk)
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
// 天气靠右
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 0 }) {
|
||||
Text(this.weather().temp.toString() + '°')
|
||||
.fontSize(26)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(this.weather().text + ' · ' + this.weather().wind)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
}
|
||||
.alignItems(HorizontalAlign.End)
|
||||
|
||||
Image(this.weatherIcon())
|
||||
.width(30)
|
||||
.height(30)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(fbPalette('#B0763A').brand)
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Top)
|
||||
|
||||
Divider().color(FbColors.line2)
|
||||
|
||||
FbStatTriple({
|
||||
labelA: '未来七天日程',
|
||||
valueA: FB_DEMO_WEEK_STAT.events.toString(),
|
||||
labelB: '待办',
|
||||
valueB: FB_DEMO_WEEK_STAT.todos.toString(),
|
||||
labelC: '逾期',
|
||||
valueC: FB_DEMO_WEEK_STAT.overdue.toString(),
|
||||
cAlarm: FB_DEMO_WEEK_STAT.overdue > 0
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad })
|
||||
|
||||
// ==================== ③ 今天的日程 + 近期任务 ====================
|
||||
Column({ space: 0 }) {
|
||||
FbSectionTitle({ title: '今天的日程', extra: this.todayEvents.length.toString() + ' 条' })
|
||||
Column({ space: 10 }) {
|
||||
if (this.todayEvents.length === 0) {
|
||||
FbDotLine({ label: '今天没有安排,过得轻松一点', muted: true })
|
||||
} else {
|
||||
ForEach(this.todayEvents, (ev: FbCalEvent) => {
|
||||
Row({ space: 10 }) {
|
||||
Text(ev.allDay ? '全天' : ev.time)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.width(46)
|
||||
Column() {
|
||||
FbDot({ dia: 8, fillC: ev.color })
|
||||
}
|
||||
.width(8)
|
||||
|
||||
Text(ev.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
|
||||
Text(ev.calName)
|
||||
.fontSize(10)
|
||||
.fontColor(Color.White)
|
||||
.backgroundColor(ev.color)
|
||||
.borderRadius(4)
|
||||
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (ev: FbCalEvent): string => 'tev_' + ev.id.toString())
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 8 })
|
||||
|
||||
Column({ space: 0 }) {
|
||||
FbSectionTitle({
|
||||
title: '近期任务',
|
||||
extra: '进行中与已逾期'
|
||||
})
|
||||
Column({ space: 10 }) {
|
||||
ForEach(this.recentTasks(), (t: FbTaskItem) => {
|
||||
Row({ space: 10 }) {
|
||||
FbDot({ dia: 8, fillC: t.overdue ? '#C0533F' : t.boardColor })
|
||||
|
||||
Text(t.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
|
||||
if (t.childTotal > 0) {
|
||||
Text(t.childDone.toString() + '/' + t.childTotal.toString())
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
if (t.dueText.length > 0) {
|
||||
Text(t.dueText)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(t.overdue ? FbColors.red : FbColors.faint)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (t: FbTaskItem): string => 'htk_' + t.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
|
||||
// ==================== ④ 我的家人 ====================
|
||||
Column({ space: 0 }) {
|
||||
FbSectionTitle({ title: '我的家人', extra: '以我为中心 · 上下三代' })
|
||||
Flex({ wrap: FlexWrap.Wrap }) {
|
||||
ForEach(FB_DEMO_KIN, (k: FbKinChip) => {
|
||||
Column({ space: 4 }) {
|
||||
FbAvatar({ name: k.shown, color: k.avatarColor, dia: 42, fontSizeVp: 17 })
|
||||
Text(k.shown)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(1)
|
||||
Text(k.kinship)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
}
|
||||
.width(64)
|
||||
.padding({ top: 6, bottom: 6 })
|
||||
}, (k: FbKinChip): string => 'kin_' + k.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
|
||||
// ==================== ⑤ 未来七天 ====================
|
||||
Column({ space: 0 }) {
|
||||
FbSectionTitle({ title: '未来七天', extra: this.soonEvents.length.toString() + ' 条' })
|
||||
Column({ space: 10 }) {
|
||||
ForEach(this.soonEvents, (ev: FbCalEvent) => {
|
||||
Row({ space: 10 }) {
|
||||
Text(ev.date)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.width(44)
|
||||
Text(ev.allDay ? '全天' : ev.time)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width(40)
|
||||
FbDot({ dia: 7, fillC: ev.color })
|
||||
Text(ev.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (ev: FbCalEvent): string => 'sev_' + ev.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
|
||||
// ==================== ⑥ 模块入口 ====================
|
||||
Column({ space: 0 }) {
|
||||
FbSectionTitle({ title: '全部模块', extra: this.cards.length.toString() + ' 个' })
|
||||
|
||||
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
|
||||
ForEach(this.cards, (c: FbModuleCard) => {
|
||||
this.moduleCard(c)
|
||||
}, (c: FbModuleCard): string => 'mc_' + c.key)
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 28 })
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
.align(Alignment.TopStart)
|
||||
}
|
||||
|
||||
/** hero 上的方形快捷入口 */
|
||||
@Builder
|
||||
heroAction(icon: Resource, target: string) {
|
||||
Button({ type: ButtonType.ROUNDED_RECTANGLE }) {
|
||||
Image(icon)
|
||||
.width(20)
|
||||
.height(20)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.muted)
|
||||
}
|
||||
.width(40)
|
||||
.height(40)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.onClick(() => {
|
||||
this.app.switchModule(target);
|
||||
})
|
||||
}
|
||||
|
||||
/** 一块模块入口卡(图标 + 模块名 + 一句关键数字) */
|
||||
@Builder
|
||||
moduleCard(c: FbModuleCard) {
|
||||
Column({ space: 8 }) {
|
||||
Row({ space: 8 }) {
|
||||
Image(this.cardIcon(c.key))
|
||||
.width(18)
|
||||
.height(18)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(fbPalette(c.color).brand)
|
||||
Text(c.name)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Text(c.big)
|
||||
.fontSize(20)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(fbPalette(c.color).brand)
|
||||
.maxLines(1)
|
||||
|
||||
Text(c.lines.length > 0 ? c.lines[0] : '')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.width('48.5%')
|
||||
.padding(14)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ bottom: 10 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.onClick(() => {
|
||||
this.app.switchModule(c.key);
|
||||
})
|
||||
}
|
||||
|
||||
/** 卡片图标(按模块 key 取) */
|
||||
private cardIcon(key: string): Resource {
|
||||
switch (key) {
|
||||
case 'people': return $r('app.media.fb_ic_people');
|
||||
case 'kin': return $r('app.media.fb_ic_kin');
|
||||
case 'movies': return $r('app.media.fb_ic_movies');
|
||||
case 'trips': return $r('app.media.fb_ic_trips');
|
||||
case 'calendar': return $r('app.media.fb_ic_calendar');
|
||||
case 'resources': return $r('app.media.fb_ic_resources');
|
||||
case 'tasks': return $r('app.media.fb_ic_tasks');
|
||||
case 'days': return $r('app.media.fb_ic_days');
|
||||
case 'ledger': return $r('app.media.fb_ic_ledger');
|
||||
case 'books': return $r('app.media.fb_ic_books');
|
||||
case 'nav': return $r('app.media.fb_ic_nav');
|
||||
case 'notes': return $r('app.media.fb_ic_notes');
|
||||
default: return $r('app.media.fb_ic_home');
|
||||
}
|
||||
}
|
||||
|
||||
/** 首页只放「进行中 + 已逾期」两类任务(与 Web 端同一取舍) */
|
||||
private recentTasks(): FbTaskItem[] {
|
||||
return this.tasks.filter((t: FbTaskItem): boolean => t.status === 'doing' || t.overdue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 财务模块(鸿蒙端)
|
||||
*
|
||||
* 对应 Web 端 `templates/ledger/index.php`。Web 端的结构(用户明确要求过的):
|
||||
* 「上面是概览,下面就是具体账目…新建账目和设置都在右侧;
|
||||
* 每个账目的金额变化与总金额变化,在账本列表上面画出折线图。」
|
||||
*
|
||||
* 端上保留这个层次:
|
||||
* ① 账本横条(多本账可横向切换)
|
||||
* ② 概览卡:净资产大字 + 资产/负债分列(折线图位置留给第二轮,需要图表能力)
|
||||
* ③ 账目表:按类别分组,每行带「盘点」入口
|
||||
*
|
||||
* ⚠️ 金额全程以**分**参与计算(`Ledger` 那套约定),显示时才转成元;
|
||||
* 任何地方都不要拿「元」去加总 —— 浮点误差会越滚越大。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbGroup, FbLedgerBook, FbLedgerEntry } from '../../model/Types';
|
||||
import {
|
||||
FB_DEMO_LEDGER_BOOKS, FB_DEMO_LEDGER_ENTRIES, fbLedgerKindName, fbMoney
|
||||
} from '../../model/DemoData';
|
||||
import { FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbLedgerPage {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
|
||||
/** 模块主题色(财务 = 橄榄绿) */
|
||||
private accent: string = '#5E7A3C';
|
||||
private books: FbLedgerBook[] = FB_DEMO_LEDGER_BOOKS;
|
||||
private entries: FbLedgerEntry[] = FB_DEMO_LEDGER_ENTRIES;
|
||||
/** 当前账本 id(0 = 用第一本) */
|
||||
@Local bookId: number = 0;
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// ---- 页头 ----
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text(this.currentBook().name)
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(this.books.length.toString() + ' 本账 · '
|
||||
+ this.currentBook().entryCount.toString() + ' 条账目')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
FbPrimaryButton({
|
||||
label: '记一笔',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
})
|
||||
FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// ---- 账本切换(横向)----
|
||||
Scroll() {
|
||||
Row({ space: 8 }) {
|
||||
ForEach(this.books, (b: FbLedgerBook) => {
|
||||
Row({ space: 6 }) {
|
||||
Text(b.name)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(this.isCurrent(b.id) ? Color.White : FbColors.muted)
|
||||
.maxLines(1)
|
||||
if (b.myPerm !== 'owner') {
|
||||
FbTag({
|
||||
text: b.myPerm === 'write' ? '可写' : '只读',
|
||||
tone: this.isCurrent(b.id) ? 'brand' : 'muted'
|
||||
})
|
||||
}
|
||||
}
|
||||
.padding({ left: 12, right: 12, top: 7, bottom: 7 })
|
||||
.backgroundColor(this.isCurrent(b.id) ? this.accent : FbColors.surface)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick(() => {
|
||||
this.bookId = b.id;
|
||||
})
|
||||
}, (b: FbLedgerBook): string => 'bk_' + b.id.toString())
|
||||
}
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad })
|
||||
}
|
||||
.scrollable(ScrollDirection.Horizontal)
|
||||
.scrollBar(BarState.Off)
|
||||
.width('100%')
|
||||
.padding({ bottom: 10 })
|
||||
|
||||
Scroll() {
|
||||
Column({ space: 0 }) {
|
||||
|
||||
// ---- 概览卡 ----
|
||||
Column({ space: 12 }) {
|
||||
Column({ space: 4 }) {
|
||||
Text('净资产')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
Text(fbMoney(this.netCent()))
|
||||
.fontSize(30)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.width('100%')
|
||||
|
||||
// 资产 / 负债分列
|
||||
Row({ space: 0 }) {
|
||||
Column({ space: 3 }) {
|
||||
Text('资产')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
Text(fbMoney(this.assetCent()))
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.green)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
Column({ space: 3 }) {
|
||||
Text('负债')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
Text(fbMoney(this.debtCent()))
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.red)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
// 折线图占位(第二轮接图表能力后填)
|
||||
Column({ space: 4 }) {
|
||||
Row({ space: 6 }) {
|
||||
Image($r('app.media.fb_ic_chart'))
|
||||
.width(13)
|
||||
.height(13)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.faint)
|
||||
Text('净资产变化(盘点成线)')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Column()
|
||||
.width('100%')
|
||||
.height(56)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
// ---- 账目(按类别分组)----
|
||||
Row({ space: 8 }) {
|
||||
Text('账目')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Blank()
|
||||
Text(this.entries.length.toString() + ' 条 · 按类别')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad + 4, right: FbMetric.pagePad + 4, top: 14, bottom: 8 })
|
||||
|
||||
if (this.entries.length === 0) {
|
||||
FbEmptyBox({
|
||||
title: '这本账还没有账目',
|
||||
hint: '存款、基金、保险、房子、车子都可以记进来',
|
||||
icon: $r('app.media.fb_ic_ledger')
|
||||
})
|
||||
} else {
|
||||
ForEach(this.groups(), (g: FbGroup<FbLedgerEntry>) => {
|
||||
Column({ space: 0 }) {
|
||||
// 类别小标题 + 该类别小计
|
||||
Row({ space: 8 }) {
|
||||
Text(g.title)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(fbMoney(this.sumOf(g.items)))
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 10, bottom: 6 })
|
||||
|
||||
Column({ space: 0 }) {
|
||||
ForEach(g.items, (e: FbLedgerEntry, idx: number) => {
|
||||
this.entryRow(e, idx === g.items.length - 1)
|
||||
}, (e: FbLedgerEntry): string => 'le_' + e.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 12 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}, (g: FbGroup<FbLedgerEntry>): string => 'lg_' + g.key)
|
||||
|
||||
Blank().height(16)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
.align(Alignment.TopStart)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
/** 一条账目:名字 + 归属人 + 金额 + 盘点入口 */
|
||||
@Builder
|
||||
entryRow(e: FbLedgerEntry, isLast: boolean) {
|
||||
Column({ space: 0 }) {
|
||||
Row({ space: 10 }) {
|
||||
Column({ space: 3 }) {
|
||||
Row({ space: 6 }) {
|
||||
Text(e.name)
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
if (e.ownerName.length > 0) {
|
||||
FbTag({ text: e.ownerName, tone: 'muted' })
|
||||
}
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Text('更新于 ' + e.updatedAt)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Text(fbMoney(e.amountCent))
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(e.amountCent < 0 ? FbColors.red : FbColors.ink)
|
||||
|
||||
// 「盘点」:Web 端是每行一个入口,端上保留成一个轻按钮
|
||||
Text('盘点')
|
||||
.fontSize(10)
|
||||
.fontColor(this.accent)
|
||||
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
|
||||
.backgroundColor(this.accent + '14')
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 11, bottom: 11 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
if (!isLast) {
|
||||
Divider().color(FbColors.line2)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
private isCurrent(id: number): boolean {
|
||||
return this.currentId() === id;
|
||||
}
|
||||
|
||||
private currentId(): number {
|
||||
if (this.bookId > 0) {
|
||||
return this.bookId;
|
||||
}
|
||||
return this.books.length > 0 ? this.books[0].id : 0;
|
||||
}
|
||||
|
||||
private currentBook(): FbLedgerBook {
|
||||
const hit: FbLedgerBook | undefined = this.books.find((b: FbLedgerBook): boolean => b.id === this.currentId());
|
||||
if (hit !== undefined) {
|
||||
return hit;
|
||||
}
|
||||
return { id: 0, name: '账本', netCent: 0, myPerm: 'owner', entryCount: 0 };
|
||||
}
|
||||
|
||||
private netCent(): number {
|
||||
return this.currentBook().netCent;
|
||||
}
|
||||
|
||||
private assetCent(): number {
|
||||
let sum: number = 0;
|
||||
for (const e of this.entries) {
|
||||
if (e.amountCent > 0) {
|
||||
sum = sum + e.amountCent;
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
private debtCent(): number {
|
||||
let sum: number = 0;
|
||||
for (const e of this.entries) {
|
||||
if (e.amountCent < 0) {
|
||||
sum = sum + e.amountCent;
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
private sumOf(items: FbLedgerEntry[]): number {
|
||||
let sum: number = 0;
|
||||
for (const e of items) {
|
||||
sum = sum + e.amountCent;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
private groups(): FbGroup<FbLedgerEntry>[] {
|
||||
const keys: string[] = [];
|
||||
const buckets: FbLedgerEntry[][] = [];
|
||||
for (const e of this.entries) {
|
||||
const k: string = fbLedgerKindName(e.kind);
|
||||
let idx: number = keys.indexOf(k);
|
||||
if (idx < 0) {
|
||||
keys.push(k);
|
||||
buckets.push([]);
|
||||
idx = keys.length - 1;
|
||||
}
|
||||
buckets[idx].push(e);
|
||||
}
|
||||
const out: FbGroup<FbLedgerEntry>[] = [];
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 导航 / 笔记 / 通知(鸿蒙端)
|
||||
*
|
||||
* 这三个模块的 Web 页面(`templates/nav/index.php`、`notes/index.php`、
|
||||
* `notifications/index.php`)体量都不大,端上把它们放在同一个文件里 ——
|
||||
* 都是「一列条目 + 一个入口」的形态,拆成三个文件反而要重复一堆样板。
|
||||
*
|
||||
* ⚠️ 三个 struct 各自独立,由 Index.ets 按当前模块选一个渲染。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbGroup, FbNavLink, FbNoteItem, FbNotificationItem } from '../../model/Types';
|
||||
import {
|
||||
FB_DEMO_NAV, FB_DEMO_NOTES, FB_DEMO_NOTIFICATIONS, fbGroupNav
|
||||
} from '../../model/DemoData';
|
||||
import { FbAvatar, FbDot, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
|
||||
// ============================================================
|
||||
// 导航
|
||||
// ============================================================
|
||||
|
||||
@ComponentV2
|
||||
export struct FbNavPage {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
|
||||
/** 模块主题色(导航 = 蓝) */
|
||||
private accent: string = '#3E7A9E';
|
||||
private all: FbNavLink[] = FB_DEMO_NAV;
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('我的导航')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(this.all.length.toString() + ' 个站点 · 按分组排列')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
FbPrimaryButton({
|
||||
label: '添加',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
List({ space: 0 }) {
|
||||
ForEach(this.groups(), (g: FbGroup<FbNavLink>) => {
|
||||
ListItemGroup({ header: this.groupHeader(g.title, g.count + ' 个') }) {
|
||||
ForEach(g.items, (n: FbNavLink) => {
|
||||
ListItem() {
|
||||
this.navRow(n)
|
||||
}
|
||||
}, (n: FbNavLink): string => 'nv_' + n.id.toString())
|
||||
}
|
||||
}, (g: FbGroup<FbNavLink>): string => 'ng_' + g.key)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 })
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
@Builder
|
||||
navRow(n: FbNavLink) {
|
||||
Row({ space: 12 }) {
|
||||
// 首字母方块(Web 端用站点 favicon,端上先用首字,避免引网络图)
|
||||
Text(n.title.substring(0, 1))
|
||||
.width(38)
|
||||
.height(38)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.backgroundColor(this.accent + '1A')
|
||||
.fontColor(this.accent)
|
||||
.fontSize(16)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.textAlign(TextAlign.Center)
|
||||
|
||||
Column({ space: 3 }) {
|
||||
Text(n.title)
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(n.url)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Image($r('app.media.fb_ic_forward'))
|
||||
.width(13)
|
||||
.height(13)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(14)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
|
||||
@Builder
|
||||
groupHeader(title: string, count: string) {
|
||||
Row({ space: 8 }) {
|
||||
Text(title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(count)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 4, right: 4, top: 12, bottom: 8 })
|
||||
.backgroundColor(FbColors.paper)
|
||||
}
|
||||
|
||||
private groups(): FbGroup<FbNavLink>[] {
|
||||
return fbGroupNav(this.all);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 笔记
|
||||
// ============================================================
|
||||
|
||||
@ComponentV2
|
||||
export struct FbNotesPage {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
|
||||
/** 模块主题色(笔记 = 褐灰) */
|
||||
private accent: string = '#7A6A4F';
|
||||
private all: FbNoteItem[] = FB_DEMO_NOTES;
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('笔记')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(this.all.length.toString() + ' 条 · 分块 + 标签')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
FbPrimaryButton({
|
||||
label: '记一条',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 10 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
if (this.all.length === 0) {
|
||||
FbEmptyBox({
|
||||
title: '还没有笔记',
|
||||
hint: '随手记点什么,按标签归类和检索',
|
||||
icon: $r('app.media.fb_ic_notes')
|
||||
})
|
||||
} else {
|
||||
// ⚠️ 用**两列定尺寸网格**而不是一列长卡:Web 端用户明确要求过
|
||||
// 「笔记列表要么按列表展示,要么按块展示,一行几个块,但块大小要确定,
|
||||
// 显示不下就显示简要信息」—— 一列长卡一条占大半屏,很难扫。
|
||||
Grid() {
|
||||
ForEach(this.all, (n: FbNoteItem) => {
|
||||
GridItem() {
|
||||
this.noteCard(n)
|
||||
}
|
||||
}, (n: FbNoteItem): string => 'nt_' + n.id.toString())
|
||||
}
|
||||
.columnsTemplate('1fr 1fr')
|
||||
.columnsGap(10)
|
||||
.rowsGap(10)
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 })
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
@Builder
|
||||
noteCard(n: FbNoteItem) {
|
||||
// 定尺寸卡片:标题 + 摘要 + 底部标签/时间。内容多就截断(用户要求过「块大小要确定」)
|
||||
Column({ space: 6 }) {
|
||||
Row({ space: 6 }) {
|
||||
Text(n.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
if (n.shared) {
|
||||
FbTag({ text: '共享', tone: 'green' })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Top)
|
||||
|
||||
Text(n.excerpt)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.lineHeight(19)
|
||||
.maxLines(4)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
|
||||
// 底部:标签(最多两个)+ 时间
|
||||
Row({ space: 6 }) {
|
||||
ForEach(n.tags.slice(0, 2), (t: string) => {
|
||||
FbTag({ text: '#' + t, tone: 'muted' })
|
||||
}, (t: string): string => 'ntg_' + n.id.toString() + '_' + t)
|
||||
Blank()
|
||||
if (n.attachCount > 0) {
|
||||
Text(n.attachCount.toString())
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
Text(n.updatedAt)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
.width('100%')
|
||||
.height(168)
|
||||
.padding(14)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 通知
|
||||
// ============================================================
|
||||
|
||||
@ComponentV2
|
||||
export struct FbNotificationsPage {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
|
||||
private all: FbNotificationItem[] = FB_DEMO_NOTIFICATIONS;
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('通知消息')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(this.unreadCount().toString() + ' 条未读 · 共 '
|
||||
+ this.all.length.toString() + ' 条')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.unreadCount() > 0 ? FbColors.red : FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Text('全部已读')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.brand)
|
||||
.padding({ left: 12, right: 12, top: 7, bottom: 7 })
|
||||
.backgroundColor(FbColors.brand + '14')
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick(() => {
|
||||
// 本轮只做界面:把本地未读标记清掉即可
|
||||
this.app.unread = 0;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 10 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
if (this.all.length === 0) {
|
||||
FbEmptyBox({
|
||||
title: '没有新消息',
|
||||
hint: '别人共享给你的资源、日程与任务都会出现在这儿',
|
||||
icon: $r('app.media.fb_ic_bell')
|
||||
})
|
||||
} else {
|
||||
List({ space: 10 }) {
|
||||
ForEach(this.all, (n: FbNotificationItem) => {
|
||||
ListItem() {
|
||||
this.notifCard(n)
|
||||
}
|
||||
}, (n: FbNotificationItem): string => 'nf_' + n.id.toString())
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 })
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
@Builder
|
||||
notifCard(n: FbNotificationItem) {
|
||||
Row({ space: 12 }) {
|
||||
FbAvatar({ name: n.from.shown, color: n.from.avatarColor, dia: 38, fontSizeVp: 15 })
|
||||
|
||||
Column({ space: 4 }) {
|
||||
Row({ space: 6 }) {
|
||||
Text(n.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(n.unread ? FontWeight.Medium : FontWeight.Normal)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
if (n.unread) {
|
||||
FbDot({ dia: 7, fillC: '#C0533F' })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Text(n.body)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
|
||||
Text(n.timeText)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(14)
|
||||
.backgroundColor(n.unread ? FbColors.surface : FbColors.surfaceAlt)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.alignItems(VerticalAlign.Top)
|
||||
}
|
||||
|
||||
private unreadCount(): number {
|
||||
return this.all.filter((n: FbNotificationItem): boolean => n.unread).length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 观影模块(鸿蒙端)
|
||||
*
|
||||
* 对应 Web 端 `templates/movies/index.php`。Web 端是**海报墙**(waterfall 瀑布流),
|
||||
* 端上改成**两列网格**:手机宽度下两列海报刚好一屏看清 4–6 部,
|
||||
* 比瀑布流(列宽随图片高度浮动)更整齐、也更好点。
|
||||
*
|
||||
* 页头右侧两个入口:搜索、设置(影院管理 / 统计 / 年度报告 —— 与 Web 端一致,
|
||||
* 「影院」不单独占抽屉位置,它是观影模块的一部分)。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric, fbPalette } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbMovieItem, FbKinChip } from '../../model/Types';
|
||||
import { FB_DEMO_CINEMAS, FB_DEMO_MOVIES } from '../../model/DemoData';
|
||||
import { FbDot, FbEmptyBox, FbIconButton, FbPersonChip, FbPrimaryButton, FbRating, FbSectionTitle } from '../../components/Widgets';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbMoviesPage {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
|
||||
private movies: FbMovieItem[] = FB_DEMO_MOVIES;
|
||||
private cinemas: string[] = FB_DEMO_CINEMAS;
|
||||
/** 模块主题色(观影 = 紫) */
|
||||
private accent: string = '#7A4E8C';
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// ---- 页头:标题 + 记一场电影 + 设置 ----
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('观影记录')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text('共 ' + this.movies.length.toString() + ' 部 · 按观影时间排序')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
FbPrimaryButton({
|
||||
label: '记一场',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
})
|
||||
// 「影院」不单独占抽屉位置 —— 它是观影模块的一部分,从这里进
|
||||
FbIconButton({
|
||||
icon: $r('app.media.fb_ic_gear'),
|
||||
tint: FbColors.muted,
|
||||
dia: 36
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// ---- 搜索条 ----
|
||||
Row({ space: 6 }) {
|
||||
Image($r('app.media.fb_ic_search'))
|
||||
.width(15)
|
||||
.height(15)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.faint)
|
||||
Text('搜索片名、影院、观影人…')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
}
|
||||
.width('100%')
|
||||
.height(36)
|
||||
.padding({ left: 12, right: 12 })
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 4 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// ---- 影院数提示 ----
|
||||
Row({ space: 6 }) {
|
||||
Blank()
|
||||
Text('影院 ' + this.cinemas.length.toString() + ' 家')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad + 4, right: FbMetric.pagePad + 4, top: 6, bottom: 6 })
|
||||
|
||||
// ---- 海报网格 ----
|
||||
if (this.movies.length === 0) {
|
||||
FbEmptyBox({
|
||||
title: '还没有观影记录',
|
||||
hint: '一家人一起看过的电影,都会以海报墙的方式排在这里',
|
||||
icon: $r('app.media.fb_ic_movies')
|
||||
})
|
||||
} else {
|
||||
Grid() {
|
||||
ForEach(this.movies, (mv: FbMovieItem) => {
|
||||
GridItem() {
|
||||
this.movieCard(mv)
|
||||
}
|
||||
}, (mv: FbMovieItem): string => 'mv_' + mv.id.toString())
|
||||
}
|
||||
.columnsTemplate('1fr 1fr')
|
||||
.columnsGap(12)
|
||||
.rowsGap(12)
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 })
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
/** 一张电影卡:海报位(2:3)+ 片名 + 元信息 + 同看的人 */
|
||||
@Builder
|
||||
movieCard(mv: FbMovieItem) {
|
||||
Column({ space: 0 }) {
|
||||
// 海报位:有海报就放图,没有就画占位(Web 端用「影」字占位,端上用图标)
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
if (mv.hasPoster) {
|
||||
// TODO(接口):接上 media.php 的海报地址后换掉这个纯色块
|
||||
Column()
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.linearGradient({
|
||||
angle: 145,
|
||||
colors: [[fbPalette(this.accent).brand2, 0.0], [fbPalette(this.accent).ink, 1.0]]
|
||||
})
|
||||
} else {
|
||||
Column()
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
}
|
||||
|
||||
Column({ space: 6 }) {
|
||||
Image($r('app.media.fb_ic_movies'))
|
||||
.width(mv.hasPoster ? 26 : 22)
|
||||
.height(mv.hasPoster ? 26 : 22)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(mv.hasPoster ? '#FFFFFFB3' : FbColors.faint)
|
||||
if (!mv.hasPoster) {
|
||||
Text(mv.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
.textAlign(TextAlign.Center)
|
||||
.maxLines(3)
|
||||
.padding({ left: 10, right: 10 })
|
||||
}
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.aspectRatio(0.75)
|
||||
.clip(true)
|
||||
.borderRadius({ topLeft: FbMetric.radius, topRight: FbMetric.radius })
|
||||
|
||||
// 卡片正文
|
||||
Column({ space: 5 }) {
|
||||
Text(mv.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
|
||||
Row({ space: 6 }) {
|
||||
Text(mv.watchedAt)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
if (mv.cinemaName.length > 0) {
|
||||
Row({ space: 4 }) {
|
||||
FbDot({ dia: 6, fillC: mv.cinemaColor })
|
||||
Text(mv.cinemaName)
|
||||
.fontSize(10)
|
||||
.fontColor(mv.cinemaColor)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
Row({ space: 8 }) {
|
||||
if (mv.reviewCount > 0) {
|
||||
FbRating({ score: mv.rating, max: 10 })
|
||||
}
|
||||
Blank()
|
||||
// 同看的人(只出头像,空间紧)
|
||||
Row({ space: 2 }) {
|
||||
ForEach(mv.viewers.slice(0, 3), (v: FbKinChip) => {
|
||||
FbPersonChip({ name: v.shown, color: v.avatarColor, withText: false })
|
||||
.padding({ left: 0, right: 0 })
|
||||
}, (v: FbKinChip): string => 'vw_' + mv.id.toString() + '_' + v.id.toString())
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(10)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.clip(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 名册 / 家谱(鸿蒙端)
|
||||
*
|
||||
* 对应 Web 端 `templates/people/index.php`(名册)与 `templates/kin/tree.php`(家谱)。
|
||||
*
|
||||
* 这两页在 Web 端是两个模块,但**看的是同一批人**:名册是「平铺的名单」(通讯录视角),
|
||||
* 家谱是「以我为中心的连线」(关系视角)。端上放在同一个文件里,用内部分段切换 ——
|
||||
* 手机上在两页之间来回跳没有意义,切一下就能对照看。
|
||||
*
|
||||
* ⚠️ 家谱的树形图(Web 端是 HTML 折线)在手机窄屏上排不下。
|
||||
* 端上改成**「以我为中心的三圈」**:我 → 直系(父母/配偶/子女)→ 其他亲属,
|
||||
* 每圈一张卡,卡里按人列。关系表达上等价,宽度可控。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbGroup, FbKinRelation, FbRosterItem } from '../../model/Types';
|
||||
import { FB_DEMO_KIN_RELATIONS, FB_DEMO_ROSTER, fbGroupRoster } from '../../model/DemoData';
|
||||
import { FbAvatar, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
|
||||
/** 家谱里一个人一圈的分组 */
|
||||
interface FbKinRing {
|
||||
/** 圈名:我 / 直系 / 亲属 */
|
||||
title: string;
|
||||
/** 说明 */
|
||||
hint: string;
|
||||
/** 圈内的关系条目 */
|
||||
rels: FbKinRelation[];
|
||||
}
|
||||
|
||||
@ComponentV2
|
||||
export struct FbPeoplePage {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
|
||||
/** 子视图:名册(roster)/ 家谱(kin) */
|
||||
@Local subView: string = 'roster';
|
||||
/** 名册范围:member(家里的人)/ friend(亲友) */
|
||||
@Local rosterScope: string = 'member';
|
||||
/** 搜索词(本轮只做界面,不过滤) */
|
||||
@Local keyword: string = '';
|
||||
|
||||
private all: FbRosterItem[] = FB_DEMO_ROSTER;
|
||||
private rels: FbKinRelation[] = FB_DEMO_KIN_RELATIONS;
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// ---- 分段切换(名册 / 家谱)----
|
||||
Row({ space: 4 }) {
|
||||
this.segBtn('名册', 'roster', '#A84A28')
|
||||
this.segBtn('家谱', 'kin', '#A84A28')
|
||||
}
|
||||
.padding(3)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
|
||||
if (this.subView === 'roster') {
|
||||
this.rosterView()
|
||||
} else {
|
||||
this.kinView()
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 名册
|
||||
// ============================================================
|
||||
|
||||
@Builder
|
||||
rosterView() {
|
||||
Column() {
|
||||
// 操作条
|
||||
Row({ space: 8 }) {
|
||||
Row({ space: 6 }) {
|
||||
Image($r('app.media.fb_ic_search'))
|
||||
.width(15)
|
||||
.height(15)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.faint)
|
||||
TextInput({ placeholder: '搜索姓名、手机、备注', text: this.keyword })
|
||||
.placeholderColor(FbColors.faint)
|
||||
.placeholderFont({ size: FbMetric.fontSmall })
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.backgroundColor(Color.Transparent)
|
||||
.padding(0)
|
||||
.layoutWeight(1)
|
||||
.onChange((v: string) => {
|
||||
this.keyword = v;
|
||||
})
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.height(36)
|
||||
.padding({ left: 12, right: 12 })
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
FbPrimaryButton({
|
||||
label: '添加',
|
||||
bg: '#4A7C82',
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
})
|
||||
FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// 范围切换(家里的人 / 亲友)
|
||||
Row({ space: 8 }) {
|
||||
this.scopeChip('家里的人', 'member')
|
||||
this.scopeChip('亲友', 'friend')
|
||||
Blank()
|
||||
Text(this.byScope().length.toString() + ' 人')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 6 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// 分组列表
|
||||
List({ space: 0 }) {
|
||||
ForEach(this.groups(), (g: FbGroup<FbRosterItem>) => {
|
||||
ListItemGroup({ header: this.groupHeader(g.title, g.count + ' 人') }) {
|
||||
ForEach(g.items, (p: FbRosterItem) => {
|
||||
ListItem() {
|
||||
this.personRow(p)
|
||||
}
|
||||
}, (p: FbRosterItem): string => 'pr_' + p.id.toString())
|
||||
}
|
||||
}, (g: FbGroup<FbRosterItem>): string => 'rg_' + g.key)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 })
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
}
|
||||
|
||||
/** 一个人一行:头像 + 名字/称谓 + 标签 + 生日 */
|
||||
@Builder
|
||||
personRow(p: FbRosterItem) {
|
||||
Row({ space: 12 }) {
|
||||
FbAvatar({ name: p.name, color: p.avatarColor, dia: 40, fontSizeVp: 16 })
|
||||
|
||||
Column({ space: 3 }) {
|
||||
Row({ space: 6 }) {
|
||||
Text(p.name)
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontColor(FbColors.ink)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.maxLines(1)
|
||||
if (p.isAdmin) {
|
||||
FbTag({ text: '管理员', tone: 'brand' })
|
||||
}
|
||||
if (p.kinship !== p.name) {
|
||||
FbTag({ text: p.kinship, tone: 'muted' })
|
||||
}
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Row({ space: 8 }) {
|
||||
if (p.tags.length > 0) {
|
||||
ForEach(p.tags, (t: string) => {
|
||||
FbTag({ text: t, tone: 'blue' })
|
||||
}, (t: string): string => 'tg_' + p.id.toString() + '_' + t)
|
||||
}
|
||||
if (p.birthday.length > 0) {
|
||||
Text('生日 ' + p.birthday)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Image($r('app.media.fb_ic_forward'))
|
||||
.width(13)
|
||||
.height(13)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(14)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 家谱
|
||||
// ============================================================
|
||||
|
||||
@Builder
|
||||
kinView() {
|
||||
Column() {
|
||||
// 说明条
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text('以我为中心')
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(this.rels.length.toString() + ' 条连线 · 父母 / 配偶 / 子女填上就能长出来')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
FbPrimaryButton({
|
||||
label: '补关系',
|
||||
bg: '#8A6B4B',
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Scroll() {
|
||||
Column({ space: 0 }) {
|
||||
ForEach(this.rings(), (r: FbKinRing) => {
|
||||
this.ringCard(r)
|
||||
}, (r: FbKinRing): string => 'ring_' + r.title)
|
||||
|
||||
// 完整连线表(家谱的「原始数据」,便于核对填得对不对)
|
||||
Column({ space: 0 }) {
|
||||
Row() {
|
||||
Text('全部连线')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Blank()
|
||||
Text(this.rels.length.toString() + ' 条')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 4, right: 4, bottom: 8 })
|
||||
|
||||
Column({ space: 0 }) {
|
||||
ForEach(this.rels, (r: FbKinRelation, idx: number) => {
|
||||
Row({ space: 10 }) {
|
||||
Text(r.fromName)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
Image($r('app.media.fb_ic_forward'))
|
||||
.width(12)
|
||||
.height(12)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.faint)
|
||||
Text(r.toName)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
FbTag({ text: r.label, tone: 'muted' })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 10, bottom: 10 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
if (idx < this.rels.length - 1) {
|
||||
Divider().color(FbColors.line2)
|
||||
}
|
||||
}, (r: FbKinRelation): string => 'rel_' + r.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 28 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
.align(Alignment.TopStart)
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
}
|
||||
|
||||
/** 一圈亲属卡 */
|
||||
@Builder
|
||||
ringCard(r: FbKinRing) {
|
||||
Column({ space: 10 }) {
|
||||
Row({ space: 8 }) {
|
||||
Text(r.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(r.hint)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
Blank()
|
||||
Text(r.rels.length.toString())
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
if (r.rels.length === 0) {
|
||||
Text('还没有连线')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
} else {
|
||||
Flex({ wrap: FlexWrap.Wrap }) {
|
||||
ForEach(r.rels, (rel: FbKinRelation) => {
|
||||
Row({ space: 6 }) {
|
||||
FbAvatar({ name: rel.toName, color: this.colorOf(rel.toId), dia: 28, fontSizeVp: 12 })
|
||||
Column({ space: 1 }) {
|
||||
Text(rel.toName)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(1)
|
||||
Text(rel.label)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.padding({ left: 6, right: 12, top: 5, bottom: 5 })
|
||||
.backgroundColor(FbColors.surfaceAlt)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.margin({ right: 8, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (rel: FbKinRelation): string => 'kr_' + rel.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 共用小件
|
||||
// ============================================================
|
||||
|
||||
@Builder
|
||||
segBtn(label: string, key: string, color: string) {
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(this.subView === key ? FontWeight.Medium : FontWeight.Normal)
|
||||
.fontColor(this.subView === key ? Color.White : FbColors.muted)
|
||||
.textAlign(TextAlign.Center)
|
||||
.height(30)
|
||||
.layoutWeight(1)
|
||||
.backgroundColor(this.subView === key ? color : Color.Transparent)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick(() => {
|
||||
this.subView = key;
|
||||
})
|
||||
}
|
||||
|
||||
@Builder
|
||||
scopeChip(label: string, key: string) {
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.rosterScope === key ? Color.White : FbColors.muted)
|
||||
.backgroundColor(this.rosterScope === key ? '#4A7C82' : FbColors.inputBg)
|
||||
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick(() => {
|
||||
this.rosterScope = key;
|
||||
})
|
||||
}
|
||||
|
||||
@Builder
|
||||
groupHeader(title: string, count: string) {
|
||||
Row({ space: 8 }) {
|
||||
Text(title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(count)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 4, right: 4, top: 12, bottom: 8 })
|
||||
.backgroundColor(FbColors.paper)
|
||||
}
|
||||
|
||||
private byScope(): FbRosterItem[] {
|
||||
return this.all.filter((p: FbRosterItem): boolean => p.roster === this.rosterScope);
|
||||
}
|
||||
|
||||
private groups(): FbGroup<FbRosterItem>[] {
|
||||
return fbGroupRoster(this.byScope());
|
||||
}
|
||||
|
||||
/** 三圈:我 / 直系 / 其他亲属 */
|
||||
private rings(): FbKinRing[] {
|
||||
const direct: string[] = ['parent', 'spouse', 'child'];
|
||||
return [
|
||||
{ title: '直系', hint: '父母 / 配偶 / 子女', rels: this.rels.filter((r: FbKinRelation): boolean => direct.indexOf(r.kind) >= 0) },
|
||||
{ title: '其他亲属', hint: '兄弟姐妹与姻亲', rels: this.rels.filter((r: FbKinRelation): boolean => direct.indexOf(r.kind) < 0) }
|
||||
];
|
||||
}
|
||||
|
||||
/** 取某个人的头像色(从名册里查,查不到用默认砖红) */
|
||||
private colorOf(id: number): string {
|
||||
const hit: FbRosterItem | undefined = this.all.find((p: FbRosterItem): boolean => p.id === id);
|
||||
return hit !== undefined ? hit.avatarColor : '#A84A28';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 资源模块(鸿蒙端)
|
||||
*
|
||||
* 对应 Web 端 `templates/resources/index.php`。
|
||||
* 资源 = 证件 / 资料 / 附件的集合(户口本、房产证、学籍资料…),可共享给某个成员。
|
||||
*
|
||||
* 端上形态:**按类别分组的卡片列表**,卡片右侧显示归属人与附件数;
|
||||
* 那一排「类别筛选胶囊」对应 Web 端页头的分类入口。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbGroup, FbResourceItem } from '../../model/Types';
|
||||
import { FB_DEMO_RESOURCES, fbGroupResource } from '../../model/DemoData';
|
||||
import { FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbResourcesPage {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
|
||||
/** 模块主题色(资源 = 靛蓝) */
|
||||
private accent: string = '#5566A8';
|
||||
private all: FbResourceItem[] = FB_DEMO_RESOURCES;
|
||||
/** 当前筛选类别('' = 全部) */
|
||||
@Local kindFilter: string = '';
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// ---- 页头 ----
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text(this.all.length.toString() + ' 项资源')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text('证件 / 资料 / 附件,可共享给家庭成员')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
FbPrimaryButton({
|
||||
label: '添加',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
})
|
||||
FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// ---- 类别筛选 ----
|
||||
Row({ space: 8 }) {
|
||||
this.kindChip('全部', '')
|
||||
this.kindChip('证件', 'document')
|
||||
this.kindChip('资料', 'material')
|
||||
Blank()
|
||||
Text('共享给我 ' + this.sharedCount().toString() + ' 项')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// ---- 列表 ----
|
||||
if (this.filtered().length === 0) {
|
||||
FbEmptyBox({
|
||||
title: '这里还没有资源',
|
||||
hint: '家里的证件、资料和附件都可以收进来,需要时一键共享',
|
||||
icon: $r('app.media.fb_ic_resources')
|
||||
})
|
||||
} else {
|
||||
List({ space: 0 }) {
|
||||
ForEach(this.groups(), (g: FbGroup<FbResourceItem>) => {
|
||||
ListItemGroup({ header: this.groupHeader(g.title, g.items.length.toString() + ' 项') }) {
|
||||
ForEach(g.items, (r: FbResourceItem) => {
|
||||
ListItem() {
|
||||
this.resourceCard(r)
|
||||
}
|
||||
}, (r: FbResourceItem): string => 'res_' + r.id.toString())
|
||||
}
|
||||
}, (g: FbGroup<FbResourceItem>): string => 'rsg_' + g.key)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 })
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
/** 一项资源卡 */
|
||||
@Builder
|
||||
resourceCard(r: FbResourceItem) {
|
||||
Row({ space: 12 }) {
|
||||
// 类别图标底
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
Image(this.kindIcon(r.kind))
|
||||
.width(19)
|
||||
.height(19)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(this.accent)
|
||||
}
|
||||
.width(42)
|
||||
.height(42)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.backgroundColor(this.accent + '14')
|
||||
|
||||
Column({ space: 4 }) {
|
||||
Row({ space: 6 }) {
|
||||
Text(r.name)
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
if (r.shared) {
|
||||
FbTag({ text: '已共享', tone: 'green' })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Row({ space: 10 }) {
|
||||
Text('归属 ' + r.ownerName)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.muted)
|
||||
Row({ space: 3 }) {
|
||||
Image($r('app.media.fb_ic_resources'))
|
||||
.width(11)
|
||||
.height(11)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.faint)
|
||||
Text(r.fileCount.toString() + ' 个附件')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
Text(r.updatedAt)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Image($r('app.media.fb_ic_forward'))
|
||||
.width(13)
|
||||
.height(13)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(14)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
|
||||
@Builder
|
||||
kindChip(label: string, key: string) {
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(this.kindFilter === key ? Color.White : FbColors.muted)
|
||||
.backgroundColor(this.kindFilter === key ? this.accent : FbColors.inputBg)
|
||||
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick(() => {
|
||||
this.kindFilter = key;
|
||||
})
|
||||
}
|
||||
|
||||
@Builder
|
||||
groupHeader(title: string, count: string) {
|
||||
Row({ space: 8 }) {
|
||||
Text(title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(count)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 4, right: 4, top: 12, bottom: 8 })
|
||||
.backgroundColor(FbColors.paper)
|
||||
}
|
||||
|
||||
private kindIcon(kind: string): Resource {
|
||||
if (kind === 'document') {
|
||||
return $r('app.media.fb_ic_lock');
|
||||
}
|
||||
if (kind === 'material') {
|
||||
return $r('app.media.fb_ic_book');
|
||||
}
|
||||
return $r('app.media.fb_ic_resources');
|
||||
}
|
||||
|
||||
private filtered(): FbResourceItem[] {
|
||||
if (this.kindFilter.length === 0) {
|
||||
return this.all;
|
||||
}
|
||||
return this.all.filter((r: FbResourceItem): boolean => r.kind === this.kindFilter);
|
||||
}
|
||||
|
||||
private groups(): FbGroup<FbResourceItem>[] {
|
||||
return fbGroupResource(this.filtered());
|
||||
}
|
||||
|
||||
private sharedCount(): number {
|
||||
return this.all.filter((r: FbResourceItem): boolean => r.shared).length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 设置页(鸿蒙端)
|
||||
*
|
||||
* 对应 Web 端两处设置:
|
||||
* · `templates/settings/me.php`(个人设置:我的资料、昵称、模块显隐与排序、主题色)
|
||||
* · `templates/settings/_modules.php`(模块勾选 + 拖动排序 + 每模块配色)
|
||||
*
|
||||
* 端上把它做成**个人设置**一页(通知、模块显隐、主题、关于),
|
||||
* 并把「哪些模块出现在抽屉里」这一项真正接上 —— 因为它是抽屉形态的核心配置:
|
||||
* 抽屉里列什么,正是由这里决定的。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric, fbPalette } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbModuleDef, FB_MODULES, FB_PRESET_COLORS } from '../../model/Modules';
|
||||
import { FbAvatar, FbDot, FbTag } from '../../components/Widgets';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbSettingsPage {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
|
||||
/** 抽屉里勾选着的模块 key */
|
||||
@Local checked: string[] = ['home', 'people', 'movies', 'trips', 'calendar',
|
||||
'resources', 'tasks', 'days', 'ledger', 'books', 'nav', 'notes'];
|
||||
/** 当前展开的配色分组('' = 都没展开) */
|
||||
@Local colorOpenKey: string = '';
|
||||
|
||||
build() {
|
||||
Scroll() {
|
||||
Column({ space: 0 }) {
|
||||
|
||||
// ==================== 我的资料 ====================
|
||||
Row({ space: 14 }) {
|
||||
FbAvatar({
|
||||
name: this.app.session.me.name,
|
||||
color: this.app.session.me.avatarColor,
|
||||
dia: 56,
|
||||
fontSizeVp: 22
|
||||
})
|
||||
Column({ space: 4 }) {
|
||||
Row({ space: 6 }) {
|
||||
Text(this.app.session.me.name)
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
if (this.app.session.me.role === 'admin') {
|
||||
FbTag({ text: '管理员', tone: 'brand' })
|
||||
}
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
Text(this.app.session.familyTitle + ' · ' + this.app.session.me.kinship)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Image($r('app.media.fb_ic_forward'))
|
||||
.width(14)
|
||||
.height(14)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
|
||||
// ==================== 通知 ====================
|
||||
Column({ space: 0 }) {
|
||||
this.rowTitle('通知')
|
||||
this.switchRow('新消息提醒', '共享 / 任务 / 日程变动时通知我', true)
|
||||
this.switchRow('到期提醒', '日程开始前 10 分钟提醒', true)
|
||||
this.switchRow('逾期提醒', '任务逾期时提醒我', true)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
|
||||
// ==================== 抽屉里显示哪些模块 ====================
|
||||
Column({ space: 0 }) {
|
||||
Row({ space: 8 }) {
|
||||
Text('抽屉里的模块')
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Blank()
|
||||
Text(this.checked.length.toString() + '/' + FB_MODULES.length.toString())
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ bottom: 4 })
|
||||
|
||||
Text('首页与会话相关的入口始终保留;家谱默认收起(不需要每天看)')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
.padding({ bottom: 8 })
|
||||
|
||||
ForEach(FB_MODULES, (m: FbModuleDef) => {
|
||||
this.moduleToggleRow(m)
|
||||
}, (m: FbModuleDef): string => 'set_' + m.key)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
// ==================== 模块主题色 ====================
|
||||
Column({ space: 0 }) {
|
||||
Text('模块主题色')
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.width('100%')
|
||||
Text('一个家认同一套配色 —— 换个色,整个模块的按钮与强调色一起变')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
.padding({ top: 4, bottom: 8 })
|
||||
|
||||
ForEach(FB_MODULES, (m: FbModuleDef) => {
|
||||
this.colorRow(m)
|
||||
}, (m: FbModuleDef): string => 'col_' + m.key)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
// ==================== 关于 ====================
|
||||
Column({ space: 0 }) {
|
||||
this.rowTitle('关于')
|
||||
this.plainRow('版本', '0.1.0(界面框架)')
|
||||
this.plainRow('数据来源', '待接入 · 当前为演示数据')
|
||||
this.plainRow('服务器', '未配置')
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 28 })
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
.align(Alignment.TopStart)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 行件
|
||||
// ============================================================
|
||||
|
||||
/** 一个模块的「显隐」开关行 */
|
||||
@Builder
|
||||
moduleToggleRow(m: FbModuleDef) {
|
||||
Row({ space: 12 }) {
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
Image(this.iconOf(m.key))
|
||||
.width(17)
|
||||
.height(17)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(fbPalette(m.color).brand)
|
||||
}
|
||||
.width(32)
|
||||
.height(32)
|
||||
.borderRadius(9)
|
||||
.backgroundColor(fbPalette(m.color).brand + '14')
|
||||
|
||||
Column({ space: 2 }) {
|
||||
Text(m.name)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(m.desc)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
// 首页不可关(它是落地页)
|
||||
if (m.key === 'home') {
|
||||
Text('常驻')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
} else {
|
||||
Toggle({ type: ToggleType.Switch, isOn: this.isChecked(m.key) })
|
||||
.selectedColor(fbPalette(m.color).brand)
|
||||
.onChange((on: boolean) => {
|
||||
this.toggle(m.key, on);
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 9, bottom: 9 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
|
||||
/** 一个模块的配色行(点标题展开预设色板) */
|
||||
@Builder
|
||||
colorRow(m: FbModuleDef) {
|
||||
Column({ space: 0 }) {
|
||||
Row({ space: 10 }) {
|
||||
FbDot({ dia: 16, fillC: m.color })
|
||||
Text(m.name)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
Text(this.colorOpenKey === m.key ? '收起' : '换色')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.brand)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 9, bottom: 9 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick(() => {
|
||||
this.colorOpenKey = this.colorOpenKey === m.key ? '' : m.key;
|
||||
})
|
||||
|
||||
if (this.colorOpenKey === m.key) {
|
||||
Flex({ wrap: FlexWrap.Wrap }) {
|
||||
ForEach(FB_PRESET_COLORS, (c: string) => {
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
FbDot({ dia: 26, fillC: c })
|
||||
if (c === m.color) {
|
||||
Image($r('app.media.fb_ic_check'))
|
||||
.width(13)
|
||||
.height(13)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(Color.White)
|
||||
}
|
||||
}
|
||||
.margin({ right: 8, bottom: 8 })
|
||||
}, (c: string): string => 'sw_' + m.key + '_' + c)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ bottom: 6 })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
/** 小标题 */
|
||||
@Builder
|
||||
rowTitle(title: string) {
|
||||
Text(title)
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.width('100%')
|
||||
.padding({ bottom: 6 })
|
||||
}
|
||||
|
||||
/** 带开关的设置行 */
|
||||
@Builder
|
||||
switchRow(title: string, hint: string, on: boolean) {
|
||||
Row({ space: 12 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text(title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(hint)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Toggle({ type: ToggleType.Switch, isOn: on })
|
||||
.selectedColor(FbColors.brand)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 9, bottom: 9 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
|
||||
/** 只读信息行 */
|
||||
@Builder
|
||||
plainRow(label: string, value: string) {
|
||||
Row() {
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
Blank()
|
||||
Text(value)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 9, bottom: 9 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
|
||||
private isChecked(key: string): boolean {
|
||||
return this.checked.indexOf(key) >= 0;
|
||||
}
|
||||
|
||||
private toggle(key: string, on: boolean): void {
|
||||
if (on) {
|
||||
if (this.checked.indexOf(key) < 0) {
|
||||
this.checked.push(key);
|
||||
}
|
||||
} else {
|
||||
this.checked = this.checked.filter((k: string): boolean => k !== key);
|
||||
}
|
||||
}
|
||||
|
||||
private iconOf(key: string): Resource {
|
||||
switch (key) {
|
||||
case 'home': return $r('app.media.fb_ic_home');
|
||||
case 'kin': return $r('app.media.fb_ic_kin');
|
||||
case 'people': return $r('app.media.fb_ic_people');
|
||||
case 'movies': return $r('app.media.fb_ic_movies');
|
||||
case 'trips': return $r('app.media.fb_ic_trips');
|
||||
case 'calendar': return $r('app.media.fb_ic_calendar');
|
||||
case 'resources': return $r('app.media.fb_ic_resources');
|
||||
case 'tasks': return $r('app.media.fb_ic_tasks');
|
||||
case 'days': return $r('app.media.fb_ic_days');
|
||||
case 'ledger': return $r('app.media.fb_ic_ledger');
|
||||
case 'books': return $r('app.media.fb_ic_books');
|
||||
case 'nav': return $r('app.media.fb_ic_nav');
|
||||
case 'notes': return $r('app.media.fb_ic_notes');
|
||||
default: return $r('app.media.fb_ic_dot');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 任务模块(鸿蒙端)
|
||||
*
|
||||
* 对应 Web 端 `templates/tasks/index.php`。Web 端是**看板**(按看板分列、卡片拖动改状态),
|
||||
* 端上改成**按状态分组的三段列表**(进行中 / 待办 / 已完成):
|
||||
* 手机宽度放不下多列看板,横向滚动又容易误触;
|
||||
* 而用户真正关心的是「哪件事做到哪一步了」—— 按状态分组正对这个。
|
||||
*
|
||||
* 看板名仍保留(卡片上那个小色块标签),点它可以切到别的看板。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbGroup, FbTaskItem } from '../../model/Types';
|
||||
import { FB_DEMO_TASK_BOARDS, FB_DEMO_TASKS, fbGroupTask } from '../../model/DemoData';
|
||||
import { FbCheckCircle, FbDot, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbTasksPage {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
|
||||
private boards: string[] = FB_DEMO_TASK_BOARDS;
|
||||
/** 模块主题色(任务 = 赭红) */
|
||||
private accent: string = '#A8574F';
|
||||
private tasks: FbTaskItem[] = FB_DEMO_TASKS;
|
||||
/** 当前筛选的看板('' = 全部) */
|
||||
@Local boardFilter: string = '';
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// ---- 页头 ----
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
Text(this.doingCount().toString() + ' 项在做')
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(this.todoCount().toString() + ' 项待办'
|
||||
+ (this.overdueCount() > 0 ? ' · ' + this.overdueCount().toString() + ' 项逾期' : ''))
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.overdueCount() > 0 ? FbColors.red : FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
FbPrimaryButton({
|
||||
label: '新建任务',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
})
|
||||
FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// ---- 看板筛选条(横向滚动)----
|
||||
Scroll() {
|
||||
Row({ space: 8 }) {
|
||||
this.boardChip('全部', '')
|
||||
ForEach(this.boards, (b: string) => {
|
||||
this.boardChip(b, b)
|
||||
}, (b: string): string => 'bd_' + b)
|
||||
}
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad })
|
||||
}
|
||||
.scrollable(ScrollDirection.Horizontal)
|
||||
.scrollBar(BarState.Off)
|
||||
.width('100%')
|
||||
.padding({ bottom: 10 })
|
||||
|
||||
// ---- 按状态分组的列表 ----
|
||||
if (this.filtered().length === 0) {
|
||||
FbEmptyBox({
|
||||
title: '这里没有任务',
|
||||
hint: '新建时会顺带指定它属于哪个看板、做到哪一步',
|
||||
icon: $r('app.media.fb_ic_tasks')
|
||||
})
|
||||
} else {
|
||||
List({ space: 0 }) {
|
||||
ForEach(this.groups(), (g: FbGroup<FbTaskItem>) => {
|
||||
ListItemGroup({ header: this.groupHeader(g) }) {
|
||||
ForEach(g.items, (t: FbTaskItem) => {
|
||||
ListItem() {
|
||||
this.taskCard(t)
|
||||
}
|
||||
}, (t: FbTaskItem): string => 'tk_' + t.id.toString())
|
||||
}
|
||||
}, (g: FbGroup<FbTaskItem>): string => 'grp_' + g.key)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 })
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
/** 看板筛选胶囊 */
|
||||
@Builder
|
||||
boardChip(label: string, key: string) {
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(this.boardFilter === key ? Color.White : FbColors.muted)
|
||||
.backgroundColor(this.boardFilter === key ? this.accent : FbColors.surface)
|
||||
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick(() => {
|
||||
this.boardFilter = key;
|
||||
})
|
||||
}
|
||||
|
||||
/** 分组头(粘在列表顶部,滚动时保持可见) */
|
||||
@Builder
|
||||
groupHeader(g: FbGroup<FbTaskItem>) {
|
||||
Row({ space: 8 }) {
|
||||
Text(g.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(g.count)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 4, right: 4, top: 12, bottom: 8 })
|
||||
.backgroundColor(FbColors.paper)
|
||||
}
|
||||
|
||||
/** 一张任务卡 */
|
||||
@Builder
|
||||
taskCard(t: FbTaskItem) {
|
||||
Column({ space: 8 }) {
|
||||
Row({ space: 10 }) {
|
||||
// 状态圈:完成=实心+勾、进行中=实心+白点、待办=空心
|
||||
FbCheckCircle({
|
||||
dia: 16,
|
||||
done: t.status === 'done',
|
||||
doing: t.status === 'doing',
|
||||
color: this.accent
|
||||
})
|
||||
|
||||
Text(t.title)
|
||||
.fontSize(FbMetric.fontBody)
|
||||
.fontColor(t.status === 'done' ? FbColors.faint : FbColors.ink)
|
||||
.decoration({
|
||||
type: t.status === 'done' ? TextDecorationType.LineThrough : TextDecorationType.None
|
||||
})
|
||||
.layoutWeight(1)
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// 第二行:看板标签 + 负责人 + 截止 + 子任务进度
|
||||
Row({ space: 8 }) {
|
||||
Row({ space: 4 }) {
|
||||
FbDot({ dia: 6, fillC: t.boardColor })
|
||||
Text(t.board)
|
||||
.fontSize(10)
|
||||
.fontColor(t.boardColor)
|
||||
}
|
||||
|
||||
if (t.assignee.length > 0) {
|
||||
FbTag({ text: t.assignee, tone: 'muted' })
|
||||
}
|
||||
if (t.dueText.length > 0) {
|
||||
Row({ space: 3 }) {
|
||||
Text(t.dueText)
|
||||
.fontSize(10)
|
||||
.fontColor(t.overdue ? FbColors.red : FbColors.faint)
|
||||
if (t.overdue) {
|
||||
FbTag({ text: '逾期', tone: 'red' })
|
||||
}
|
||||
}
|
||||
}
|
||||
Blank()
|
||||
if (t.childTotal > 0) {
|
||||
Row({ space: 3 }) {
|
||||
Image($r('app.media.fb_ic_check'))
|
||||
.width(11)
|
||||
.height(11)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.faint)
|
||||
Text(t.childDone.toString() + '/' + t.childTotal.toString())
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(14)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ bottom: 8 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
private filtered(): FbTaskItem[] {
|
||||
if (this.boardFilter.length === 0) {
|
||||
return this.tasks;
|
||||
}
|
||||
return this.tasks.filter((t: FbTaskItem): boolean => t.board === this.boardFilter);
|
||||
}
|
||||
|
||||
private groups(): FbGroup<FbTaskItem>[] {
|
||||
return fbGroupTask(this.filtered());
|
||||
}
|
||||
|
||||
private doingCount(): number {
|
||||
return this.tasks.filter((t: FbTaskItem): boolean => t.status === 'doing').length;
|
||||
}
|
||||
|
||||
private todoCount(): number {
|
||||
return this.tasks.filter((t: FbTaskItem): boolean => t.status === 'todo').length;
|
||||
}
|
||||
|
||||
private overdueCount(): number {
|
||||
return this.tasks.filter((t: FbTaskItem): boolean => t.overdue).length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 旅程模块(鸿蒙端)
|
||||
*
|
||||
* 对应 Web 端 `templates/trips/index.php` + `trips/show.php`。
|
||||
*
|
||||
* 术语照 Web 端**固定下来**的那套(用户明确要求):
|
||||
* · **旅程** = 列表里的一条(容器,如「青岛·崂山 两日」);
|
||||
* · **行程** = 旅程里面的一段一段(详情页时间轴上的节点)。
|
||||
*
|
||||
* 端上的形态:**上列表 / 下详情同屏**不现实(手机高度不够),
|
||||
* 所以这页做「列表 + 展开选中项的行程摘要」;点某条旅程展开它的行程时间轴。
|
||||
* 时间轴节点分「计划 / 实际」两阶段 —— 与 Web 端一致(v41 加的四种动作按钮)。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbTripItem, FbTripNode } from '../../model/Types';
|
||||
import { FB_DEMO_TRIPS, FB_DEMO_TRIP_NODES, fbMoney } from '../../model/DemoData';
|
||||
import { FbDot, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbTripsPage {
|
||||
@Param app: FbAppState = fbAppState();
|
||||
|
||||
private trips: FbTripItem[] = FB_DEMO_TRIPS;
|
||||
private nodes: FbTripNode[] = FB_DEMO_TRIP_NODES;
|
||||
/** 模块主题色(旅程 = 绿) */
|
||||
private accent: string = '#3F7D5A';
|
||||
/** 当前展开的旅程 id(0 = 都没展开) */
|
||||
@Local expandedId: number = 0;
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// ---- 页头操作条 ----
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 1 }) {
|
||||
Text('共 ' + this.trips.length.toString() + ' 个旅程')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
Text('每个旅程里可以有若干段行程')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
FbPrimaryButton({
|
||||
label: '新建旅程',
|
||||
bg: this.accent,
|
||||
icon: $r('app.media.fb_ic_plus')
|
||||
})
|
||||
FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 10 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
if (this.trips.length === 0) {
|
||||
FbEmptyBox({
|
||||
title: '还没有旅程',
|
||||
hint: '去过的地方会按时间排在这里,点开能看到每一段行程',
|
||||
icon: $r('app.media.fb_ic_trips')
|
||||
})
|
||||
} else {
|
||||
List({ space: 12 }) {
|
||||
ForEach(this.trips, (t: FbTripItem) => {
|
||||
ListItem() {
|
||||
this.tripCard(t)
|
||||
}
|
||||
}, (t: FbTripItem): string => 'trip_' + t.id.toString())
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 })
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(FbColors.paper)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
/** 一张旅程卡:标题 + 时间 + 进度;展开后接行程时间轴 */
|
||||
@Builder
|
||||
tripCard(t: FbTripItem) {
|
||||
Column({ space: 0 }) {
|
||||
// 卡头
|
||||
Row({ space: 10 }) {
|
||||
// 左侧色条(模块色的竖条,一眼区分旅程)
|
||||
Column()
|
||||
.width(4)
|
||||
.height(38)
|
||||
.borderRadius(2)
|
||||
.backgroundColor(this.accent)
|
||||
|
||||
Column({ space: 3 }) {
|
||||
Text(t.title)
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Row({ space: 8 }) {
|
||||
Text(t.dateRange)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
Text(t.sub)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Column({ space: 3 }) {
|
||||
if (t.nodeCount > 0) {
|
||||
Text(t.doneCount.toString() + '/' + t.nodeCount.toString())
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(this.accent)
|
||||
Text('行程')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
} else {
|
||||
FbTag({ text: '还没行程', tone: 'muted' })
|
||||
}
|
||||
}
|
||||
.alignItems(HorizontalAlign.End)
|
||||
|
||||
Image($r('app.media.fb_ic_forward'))
|
||||
.width(14)
|
||||
.height(14)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.faint)
|
||||
.rotate({ angle: this.isExpanded(t.id) ? 90 : 0 })
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick(() => {
|
||||
// 展开/收起:只改本地状态,不发请求
|
||||
this.expandedId = this.isExpanded(t.id) ? 0 : t.id;
|
||||
})
|
||||
|
||||
// 展开区:行程时间轴
|
||||
if (this.isExpanded(t.id)) {
|
||||
Divider()
|
||||
.color(FbColors.line2)
|
||||
.margin({ left: FbMetric.cardPad, right: FbMetric.cardPad })
|
||||
|
||||
if (this.nodes.length === 0) {
|
||||
Column() {
|
||||
Text('这个旅程还没有排行程')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
} else {
|
||||
Column({ space: 0 }) {
|
||||
// 分两段列:「实际」(已发生的)在前,「计划」在后 —— 与 Web 端顺序一致
|
||||
this.stageBlock('实际', 'actual')
|
||||
this.stageBlock('计划', 'plan')
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, bottom: FbMetric.cardPad })
|
||||
}
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.clip(true)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
/** 一段阶段块(实际 / 计划)+ 它下面的节点时间轴 */
|
||||
@Builder
|
||||
stageBlock(title: string, stage: string) {
|
||||
if (this.nodesOf(stage).length > 0) {
|
||||
Column({ space: 0 }) {
|
||||
Row({ space: 6 }) {
|
||||
FbTag({
|
||||
text: title,
|
||||
tone: stage === 'actual' ? 'green' : 'blue'
|
||||
})
|
||||
Text(this.nodesOf(stage).length.toString() + ' 段')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 10, bottom: 6 })
|
||||
|
||||
ForEach(this.nodesOf(stage), (n: FbTripNode) => {
|
||||
this.nodeRow(n)
|
||||
}, (n: FbTripNode): string => 'tn_' + stage + '_' + n.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
}
|
||||
|
||||
/** 一个行程节点:左侧圆点 + 内容块,内容块的左边框连成时间轴竖线 */
|
||||
@Builder
|
||||
nodeRow(n: FbTripNode) {
|
||||
Row({ space: 8 }) {
|
||||
// 圆点:顶对齐,与标题首行齐平
|
||||
FbDot({
|
||||
dia: 9,
|
||||
fillC: n.status === 'done' ? this.accent : '#A8988A'
|
||||
})
|
||||
.margin({ top: 5 })
|
||||
|
||||
// 内容块:左边框 = 时间轴的竖线(节点之间无间距,边框首尾相接成为连续线)
|
||||
Column({ space: 4 }) {
|
||||
Row({ space: 8 }) {
|
||||
Text(n.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(n.when)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Text(n.place)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
|
||||
Row({ space: 6 }) {
|
||||
if (n.mode.length > 0) {
|
||||
FbTag({ text: n.mode, tone: 'blue' })
|
||||
}
|
||||
if (n.status === 'done') {
|
||||
FbTag({ text: '已完成', tone: 'green' })
|
||||
} else if (n.status === 'skip') {
|
||||
FbTag({ text: '跳过', tone: 'muted' })
|
||||
} else {
|
||||
FbTag({ text: '待办', tone: 'muted' })
|
||||
}
|
||||
if (n.expenseCent > 0) {
|
||||
Text(fbMoney(n.expenseCent))
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.muted)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.padding({ left: 12, bottom: 16 })
|
||||
.border({ width: { left: 1 }, color: FbColors.line })
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Top)
|
||||
}
|
||||
|
||||
private isExpanded(id: number): boolean {
|
||||
return this.expandedId === id;
|
||||
}
|
||||
|
||||
private nodesOf(stage: string): FbTripNode[] {
|
||||
return this.nodes.filter((n: FbTripNode): boolean => n.stage === stage);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user