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,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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user