diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets index 3aa22e7..001bb55 100644 --- a/entry/src/main/ets/entryability/EntryAbility.ets +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -4,6 +4,11 @@ import { HttpClient, AuthInterceptor, SessionManager, Logger } from 'common'; import { ServiceLocator } from '../di/ServiceLocator'; import { UserApi } from '../data/api/UserApi'; import { UserRepositoryImpl } from '../data/repository/UserRepository'; +import { SessionRepositoryImpl } from '../data/repository/SessionRepository'; +import { FakeHomeRepository } from '../data/repository/HomeRepository'; +import { FakeCheckinRepository } from '../data/repository/CheckinRepository'; +import { FakePlanRepository } from '../data/repository/PlanRepository'; +import { FakeProfileRepository } from '../data/repository/ProfileRepository'; const TAG: string = 'EntryAbility'; @@ -29,9 +34,16 @@ export default class EntryAbility extends UIAbility { httpClient.addInterceptor(new AuthInterceptor()); ServiceLocator.register('httpClient', httpClient); - // 数据层 + // 数据层(真实示例) const userApi = new UserApi(httpClient); ServiceLocator.register('userRepository', new UserRepositoryImpl(userApi)); + + // 数据层(明缘四入口,本地 mock) + ServiceLocator.register('sessionRepository', new SessionRepositoryImpl()); + ServiceLocator.register('homeRepository', new FakeHomeRepository()); + ServiceLocator.register('checkinRepository', new FakeCheckinRepository()); + ServiceLocator.register('planRepository', new FakePlanRepository()); + ServiceLocator.register('profileRepository', new FakeProfileRepository()); } onDestroy(): void { diff --git a/entry/src/main/ets/pages/CheckinPage.ets b/entry/src/main/ets/pages/CheckinPage.ets new file mode 100644 index 0000000..0e3bfdb --- /dev/null +++ b/entry/src/main/ets/pages/CheckinPage.ets @@ -0,0 +1,232 @@ +import { AppColor, AppSpacing, AppFontSize, AppRadius, LoadingView, ErrorState, LoggedOutState, EmptyState, SegmentedControl, Avatar, StatusBadge, StatusPill } from 'uikit'; +import { SessionManager, SessionStore } from 'common'; +import { CheckinViewModel } from '../viewmodel/CheckinViewModel'; +import { CheckinStatus, GroupMember, PersonalCheckinRecord } from '../model/CheckinModels'; + +/** + * 打卡状态 -> 颜色映射(领域逻辑放在页面层,保持 uikit 通用)。 + */ +function checkinStatusColor(status: string): string { + switch (status) { + case CheckinStatus.NORMAL: + return AppColor.statusNormal; + case CheckinStatus.LATE: + return AppColor.statusLate; + case CheckinStatus.LEAVE: + return AppColor.statusLeave; + case CheckinStatus.EARLY_LEAVE: + return AppColor.statusEarlyLeave; + case CheckinStatus.ABSENT: + return AppColor.statusAbsent; + default: + return AppColor.gold; + } +} + +/** + * 打卡页:顶部打卡卡 + 小组/个人记录。 + */ +@ComponentV2 +export struct CheckinPage { + @Local viewModel: CheckinViewModel = CheckinViewModel.create(); + @Local session: SessionStore = SessionManager.get(); + @Local checkinTab: number = 0; + @Event onGoLogin: () => void; + + aboutToAppear(): void { + this.viewModel.load(); + } + + @Monitor('session.token') + onSessionChange(): void { + this.viewModel.load(); + } + + build() { + Column() { + if (!this.viewModel.loggedIn) { + LoggedOutState({ message: '登录后即可打卡', onLogin: () => { this.onGoLogin(); } }) + } else if (this.viewModel.loading) { + Column() { + LoadingView({ text: '加载中...' }) + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Center) + } else if (this.viewModel.error.length > 0) { + ErrorState({ message: this.viewModel.error, onRetry: () => { this.viewModel.load(); } }) + } else { + Scroll() { + Column({ space: AppSpacing.md }) { + this.statusCard() + this.records() + } + .width('100%') + .padding(AppSpacing.lg) + } + .width('100%') + .height('100%') + .scrollBar(BarState.Off) + } + } + .width('100%') + .height('100%') + .backgroundColor(AppColor.background) + } + + @Builder + statusCard() { + Column({ space: AppSpacing.md }) { + Row({ space: AppSpacing.md }) { + Column({ space: 2 }) { + Text(this.viewModel.record.weekday) + .fontSize(AppFontSize.caption) + .fontColor(AppColor.goldLight) + Text(this.viewModel.record.dayNum) + .fontSize(24) + .fontWeight(FontWeight.Bold) + .fontColor(AppColor.white) + } + .width(64) + .padding({ top: 12, bottom: 12 }) + .backgroundColor(AppColor.deepBrown) + .borderRadius(AppRadius.md) + .alignItems(HorizontalAlign.Center) + + Column({ space: 4 }) { + Text(this.viewModel.record.checked ? '已打卡' : '未打卡') + .fontSize(22) + .fontWeight(FontWeight.Bold) + .fontColor(this.viewModel.record.checked ? AppColor.statusNormal : AppColor.gold) + Text(this.viewModel.record.checked ? '已完成今日打卡' : '请选择状态完成打卡') + .fontSize(AppFontSize.caption) + .fontColor(AppColor.textMuted) + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + } + + Column({ space: 8 }) { + Row({ space: 8 }) { + ForEach(CheckinStatus.ALL.slice(0, 3), (status: string) => { + StatusPill({ + label: status, + color: checkinStatusColor(status), + selected: this.viewModel.record.status === status, + onTap: () => { this.viewModel.submit(status); } + }) + }, (status: string) => status) + } + Row({ space: 8 }) { + ForEach(CheckinStatus.ALL.slice(3), (status: string) => { + StatusPill({ + label: status, + color: checkinStatusColor(status), + selected: this.viewModel.record.status === status, + onTap: () => { this.viewModel.submit(status); } + }) + }, (status: string) => status) + } + } + } + .width('100%') + .padding(AppSpacing.md) + .backgroundColor(AppColor.white) + .borderRadius(AppRadius.lg) + } + + @Builder + records() { + Column({ space: AppSpacing.md }) { + SegmentedControl({ + items: ['小组', '个人'], + selectedIndex: this.checkinTab, + onSelect: (index: number) => { this.checkinTab = index; } + }) + + if (this.checkinTab === 0) { + this.groupTab() + } else { + this.personalTab() + } + } + .width('100%') + } + + @Builder + groupTab() { + Column() { + if (this.viewModel.record.groupMembers.length === 0) { + EmptyState({ emoji: '📭', message: '暂无成员数据' }) + } else { + ForEach(this.viewModel.record.groupMembers, (m: GroupMember) => { + Row({ space: AppSpacing.md }) { + Avatar({ name: m.name, diameter: 40 }) + Column({ space: 2 }) { + Text(m.name) + .fontSize(AppFontSize.body) + .fontWeight(FontWeight.Medium) + .fontColor(AppColor.textPrimary) + if (m.checkedIn) { + Text('打卡 ' + m.checkinTime) + .fontSize(AppFontSize.caption) + .fontColor(AppColor.textMuted) + } + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + + if (m.checkedIn) { + StatusBadge({ label: m.status, color: checkinStatusColor(m.status) }) + } else { + Text('未打卡') + .fontSize(AppFontSize.caption) + .fontColor(AppColor.textMuted) + .padding({ left: 10, right: 10, top: 4, bottom: 4 }) + .backgroundColor(AppColor.neutralChip) + .borderRadius(AppRadius.pill) + } + } + .width('100%') + .padding({ top: 12, bottom: 12 }) + }, (m: GroupMember) => m.id) + } + } + .width('100%') + .backgroundColor(AppColor.white) + .borderRadius(AppRadius.lg) + .padding({ left: 16, right: 16, top: 4, bottom: 4 }) + } + + @Builder + personalTab() { + Column({ space: AppSpacing.sm }) { + if (this.viewModel.record.personalHistory.length === 0) { + EmptyState({ emoji: '📒', message: '暂无个人打卡记录' }) + } else { + ForEach(this.viewModel.record.personalHistory, (h: PersonalCheckinRecord) => { + Row() { + Column({ space: 2 }) { + Text(h.date) + .fontSize(AppFontSize.body) + .fontWeight(FontWeight.Medium) + .fontColor(AppColor.textPrimary) + Text(h.time) + .fontSize(AppFontSize.caption) + .fontColor(AppColor.textMutedLight) + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + + StatusBadge({ label: h.status, color: checkinStatusColor(h.status) }) + } + .width('100%') + .padding(AppSpacing.md) + .backgroundColor(AppColor.white) + .borderRadius(AppRadius.md) + }, (h: PersonalCheckinRecord) => h.id) + } + } + .width('100%') + } +} diff --git a/entry/src/main/ets/pages/HomePage.ets b/entry/src/main/ets/pages/HomePage.ets new file mode 100644 index 0000000..83732ed --- /dev/null +++ b/entry/src/main/ets/pages/HomePage.ets @@ -0,0 +1,155 @@ +import { AppColor, AppSpacing, AppFontSize, AppRadius, LoadingView, ErrorState } from 'uikit'; +import { SessionManager, SessionStore } from 'common'; +import { HomeViewModel } from '../viewmodel/HomeViewModel'; +import { HomeModule, IntroCard } from '../model/HomeModels'; + +/** + * 首页:未登录走游客介绍页,已登录走原生首页(Hero + 模块 + 产品介绍)。 + */ +@ComponentV2 +export struct HomePage { + @Local viewModel: HomeViewModel = HomeViewModel.create(); + @Local session: SessionStore = SessionManager.get(); + @Event onGoLogin: () => void; + + aboutToAppear(): void { + this.viewModel.load(); + } + + @Monitor('session.token') + onSessionChange(): void { + this.viewModel.load(); + } + + build() { + Column() { + if (this.viewModel.loading) { + LoadingView({ text: '加载中...' }) + } else if (this.viewModel.error.length > 0) { + ErrorState({ message: this.viewModel.error, onRetry: () => { this.viewModel.load(); } }) + } else { + Scroll() { + Column({ space: AppSpacing.md }) { + this.hero() + this.moduleGrid() + this.introSection() + } + .width('100%') + .padding(AppSpacing.lg) + } + .width('100%') + .height('100%') + .scrollBar(BarState.Off) + } + } + .width('100%') + .height('100%') + .backgroundColor(AppColor.background) + } + + @Builder + hero() { + Column({ space: AppSpacing.sm }) { + Text(this.viewModel.data.greeting) + .fontSize(AppFontSize.caption) + .fontColor(AppColor.goldLight) + .padding({ left: 12, right: 12, top: 6, bottom: 6 }) + .backgroundColor(AppColor.goldChip) + .borderRadius(AppRadius.pill) + + Text(this.viewModel.data.displayName) + .fontSize(32) + .fontWeight(FontWeight.Bold) + .fontColor(AppColor.white) + + Text(this.viewModel.data.encouragement) + .fontSize(AppFontSize.body) + .fontColor(AppColor.goldLight) + + if (this.viewModel.isGuest) { + Text('登录账号解锁完整功能 ›') + .fontSize(AppFontSize.caption) + .fontColor(AppColor.goldLight) + .padding({ left: 16, right: 16, top: 8, bottom: 8 }) + .backgroundColor(AppColor.goldChipStrong) + .borderRadius(AppRadius.pill) + .onClick(() => { + this.onGoLogin(); + }) + } + } + .width('100%') + .alignItems(HorizontalAlign.Start) + .padding(AppSpacing.lg) + .borderRadius(AppRadius.xl) + .linearGradient({ angle: 135, colors: [[AppColor.heroStart, 0.0], [AppColor.heroMid, 0.55], [AppColor.heroEnd, 1.0]] }) + } + + @Builder + moduleGrid() { + Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) { + ForEach(this.viewModel.data.modules, (m: HomeModule) => { + Column({ space: AppSpacing.sm }) { + Stack({ alignContent: Alignment.Center }) { + Text(m.emoji) + .fontSize(20) + } + .width(48) + .height(48) + .borderRadius(24) + .backgroundColor(AppColor.neutralChip) + + Text(m.title) + .fontSize(AppFontSize.body) + .fontWeight(FontWeight.Bold) + .fontColor(AppColor.textPrimary) + + Text(m.description) + .fontSize(AppFontSize.caption) + .fontColor(AppColor.textMuted) + } + .width('31%') + .alignItems(HorizontalAlign.Center) + .padding({ top: 20, bottom: 20, left: 8, right: 8 }) + .backgroundColor(AppColor.white) + .borderRadius(AppRadius.lg) + }, (m: HomeModule) => m.id) + } + .width('100%') + } + + @Builder + introSection() { + Column({ space: AppSpacing.md }) { + Text('产品介绍') + .fontSize(AppFontSize.title) + .fontWeight(FontWeight.Bold) + .fontColor(AppColor.textPrimary) + + ForEach(this.viewModel.data.intros, (intro: IntroCard) => { + Row({ space: AppSpacing.md }) { + Text(intro.emoji) + .fontSize(22) + Column({ space: AppSpacing.xs }) { + Text(intro.heading) + .fontSize(AppFontSize.body) + .fontWeight(FontWeight.Bold) + .fontColor(AppColor.gold) + Text(intro.body) + .fontSize(AppFontSize.caption) + .fontColor(AppColor.textSecondary) + .lineHeight(20) + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + } + .width('100%') + .padding(AppSpacing.md) + .backgroundColor(AppColor.white) + .borderRadius(AppRadius.lg) + }, (intro: IntroCard) => intro.heading) + } + .width('100%') + .alignItems(HorizontalAlign.Start) + } +} diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 8e56a14..2619a9e 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -1,63 +1,60 @@ -import { AppColor, AppSpacing, AppButton, LoadingView, EmptyView } from 'uikit'; -import { HomeViewModel } from '../viewmodel/HomeViewModel'; -import { User } from '../model/User'; -import { AppRouter } from '../router/AppRouter'; -import { RouteMap, RouteNames } from '../router/RouteMap'; -import { StatusCard } from '../view/StatusCard'; +import { AppColor, AppFontSize } from 'uikit'; +import { HomePage } from './HomePage'; +import { CheckinPage } from './CheckinPage'; +import { PlanPage } from './PlanPage'; +import { ProfilePage } from './ProfilePage'; /** - * 首页(@Entry):Navigation 根容器,仅编排。 - * 演示「页面 -> ViewModel -> Repository -> Api -> HttpClient」完整链路。 + * 应用入口:底部四 tab 壳(首页 / 打卡 / 计划 / 我的)。 */ @Entry @ComponentV2 struct Index { - @Local pathStack: NavPathStack = new NavPathStack(); - @Local viewModel: HomeViewModel = HomeViewModel.create(); - @Local title: string = 'Harmony Client'; - - aboutToAppear(): void { - AppRouter.init(this.pathStack); - this.viewModel.load(); - } + @Local currentIndex: number = 0; build() { - Navigation(this.pathStack) { - Column({ space: AppSpacing.md }) { - Text(this.title) - .fontSize(24) - .fontWeight(FontWeight.Bold) - .fontColor(AppColor.textPrimary) - - if (this.viewModel.loading) { - LoadingView({ text: '加载中...' }) - } else if (this.viewModel.error.length > 0) { - EmptyView({ text: this.viewModel.error }) - } else { - ForEach(this.viewModel.users, (user: User) => { - StatusCard({ title: user.name, subtitle: user.email }) - }, (user: User) => user.id) - } - - AppButton({ - text: '进入详情页', - onTap: () => { - AppRouter.push(RouteNames.DETAIL, '来自首页的参数'); - } - }) + Tabs({ barPosition: BarPosition.End, index: this.currentIndex }) { + TabContent() { + HomePage({ onGoLogin: () => { this.currentIndex = 3; } }) } - .width('100%') - .height('100%') - .padding(AppSpacing.lg) - .backgroundColor(AppColor.background) + .tabBar(this.tabBarBuilder(0, '首页', '🏠')) + + TabContent() { + CheckinPage({ onGoLogin: () => { this.currentIndex = 3; } }) + } + .tabBar(this.tabBarBuilder(1, '打卡', '✅')) + + TabContent() { + PlanPage({ onGoLogin: () => { this.currentIndex = 3; } }) + } + .tabBar(this.tabBarBuilder(2, '计划', '📝')) + + TabContent() { + ProfilePage() + } + .tabBar(this.tabBarBuilder(3, '我的', '👤')) } - .navDestination(this.routeMap) - .mode(NavigationMode.Stack) - .hideTitleBar(true) + .onChange((index: number) => { + this.currentIndex = index; + }) + .width('100%') + .height('100%') + .backgroundColor(AppColor.background) } @Builder - routeMap(name: string, param: Object) { - RouteMap(name, param) + tabBarBuilder(index: number, title: string, icon: string) { + Column({ space: 2 }) { + Text(icon) + .fontSize(20) + Text(title) + .fontSize(AppFontSize.caption) + .fontColor(index === this.currentIndex ? AppColor.tabActive : AppColor.tabInactive) + .fontWeight(index === this.currentIndex ? FontWeight.Bold : FontWeight.Normal) + } + .width('100%') + .height(52) + .justifyContent(FlexAlign.Center) + .backgroundColor(AppColor.tabBg) } } diff --git a/entry/src/main/ets/pages/PlanPage.ets b/entry/src/main/ets/pages/PlanPage.ets new file mode 100644 index 0000000..e9167ba --- /dev/null +++ b/entry/src/main/ets/pages/PlanPage.ets @@ -0,0 +1,183 @@ +import { AppColor, AppSpacing, AppFontSize, AppRadius, LoadingView, ErrorState, LoggedOutState, SegmentedControl, ProgressBar } from 'uikit'; +import { SessionManager, SessionStore } from 'common'; +import { PlanViewModel } from '../viewmodel/PlanViewModel'; +import { PlanType, PlanGroup } from '../model/PlanModels'; + +/** + * 计划页:周/月/年切换 + 我的计划状态 + 小组进度。 + */ +@ComponentV2 +export struct PlanPage { + @Local viewModel: PlanViewModel = PlanViewModel.create(); + @Local session: SessionStore = SessionManager.get(); + @Event onGoLogin: () => void; + + aboutToAppear(): void { + this.viewModel.load(); + } + + @Monitor('session.token') + onSessionChange(): void { + this.viewModel.load(); + } + + build() { + Column() { + if (!this.viewModel.loggedIn) { + LoggedOutState({ message: '登录后即可提交计划', onLogin: () => { this.onGoLogin(); } }) + } else if (this.viewModel.loading) { + Column() { + LoadingView({ text: '加载中...' }) + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Center) + } else if (this.viewModel.error.length > 0) { + ErrorState({ message: this.viewModel.error, onRetry: () => { this.viewModel.load(); } }) + } else { + Scroll() { + Column({ space: AppSpacing.md }) { + SegmentedControl({ + items: PlanType.ALL, + selectedIndex: PlanType.ALL.indexOf(this.viewModel.selectedType), + onSelect: (index: number) => { this.viewModel.selectType(PlanType.ALL[index]); } + }) + this.head() + this.myStatusCard() + if (this.viewModel.record.message.length > 0) { + Text(this.viewModel.record.message) + .fontSize(AppFontSize.caption) + .fontColor(AppColor.danger) + .width('100%') + .padding({ top: 10, bottom: 10 }) + .textAlign(TextAlign.Center) + .backgroundColor(AppColor.warningBg) + .borderRadius(AppRadius.md) + } + this.groupList() + } + .width('100%') + .padding(AppSpacing.lg) + } + .width('100%') + .height('100%') + .scrollBar(BarState.Off) + } + } + .width('100%') + .height('100%') + .backgroundColor(AppColor.creamDark) + } + + @Builder + head() { + Row() { + Column({ space: 2 }) { + Text(this.viewModel.selectedType) + .fontSize(24) + .fontWeight(FontWeight.Bold) + .fontColor(AppColor.textPrimary) + Text(this.viewModel.record.periodKey) + .fontSize(AppFontSize.caption) + .fontColor(AppColor.textMutedLight) + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + + Text(this.viewModel.record.myChecked ? '已提交' : '待提交') + .fontSize(AppFontSize.caption) + .fontColor(this.viewModel.record.myChecked ? AppColor.statusNormal : AppColor.gold) + .padding({ left: 12, right: 12, top: 6, bottom: 6 }) + .backgroundColor(this.viewModel.record.myChecked ? AppColor.successBg : AppColor.warningBg) + .borderRadius(AppRadius.pill) + } + .width('100%') + .padding({ left: 4, right: 4 }) + } + + @Builder + myStatusCard() { + Row({ space: AppSpacing.md }) { + Stack({ alignContent: Alignment.Center }) { + Text(this.viewModel.record.myChecked ? '✓' : '●') + .fontSize(20) + .fontWeight(FontWeight.Bold) + .fontColor(AppColor.white) + } + .width(48) + .height(48) + .borderRadius(24) + .backgroundColor(this.viewModel.record.myChecked ? AppColor.statusNormal : AppColor.danger) + + Column({ space: 4 }) { + Text(this.viewModel.record.myChecked ? '已完成' : '未完成') + .fontSize(AppFontSize.caption) + .fontColor(AppColor.textMuted) + Text('我的' + this.viewModel.selectedType) + .fontSize(AppFontSize.title) + .fontWeight(FontWeight.Bold) + .fontColor(AppColor.textPrimary) + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + + Text(this.viewModel.record.myChecked ? '修改' : '提交计划') + .fontSize(AppFontSize.body) + .fontColor(AppColor.white) + .padding({ left: 20, right: 20, top: 10, bottom: 10 }) + .backgroundColor(AppColor.gold) + .borderRadius(AppRadius.pill) + } + .width('100%') + .padding(AppSpacing.md) + .backgroundColor(AppColor.white) + .borderRadius(AppRadius.lg) + } + + @Builder + groupList() { + Column({ space: AppSpacing.sm }) { + Text('小组统计') + .fontSize(AppFontSize.title) + .fontWeight(FontWeight.Bold) + .fontColor(AppColor.textPrimary) + .width('100%') + + ForEach(this.viewModel.record.groups, (g: PlanGroup, index: number) => { + Column({ space: AppSpacing.sm }) { + Row({ space: AppSpacing.sm }) { + Text((index + 1).toString()) + .fontSize(AppFontSize.caption) + .fontWeight(FontWeight.Bold) + .fontColor(AppColor.gold) + .width(36) + .height(36) + .borderRadius(10) + .textAlign(TextAlign.Center) + .backgroundColor(AppColor.neutralChip) + + Text(g.name) + .fontSize(AppFontSize.body) + .fontWeight(FontWeight.Bold) + .fontColor(AppColor.textPrimary) + + Blank() + + Text('已提交 ' + g.checkedCount + ' · ' + g.memberCount + '人') + .fontSize(AppFontSize.caption) + .fontColor(AppColor.textMuted) + } + .width('100%') + + ProgressBar({ percent: g.memberCount > 0 ? Math.round(g.checkedCount / g.memberCount * 100) : 0 }) + } + .width('100%') + .padding(AppSpacing.md) + .backgroundColor(AppColor.white) + .borderRadius(AppRadius.lg) + }, (g: PlanGroup) => g.id) + } + .width('100%') + .alignItems(HorizontalAlign.Start) + } +} diff --git a/entry/src/main/ets/pages/ProfilePage.ets b/entry/src/main/ets/pages/ProfilePage.ets new file mode 100644 index 0000000..2e3dc06 --- /dev/null +++ b/entry/src/main/ets/pages/ProfilePage.ets @@ -0,0 +1,184 @@ +import { AppColor, AppSpacing, AppFontSize, AppRadius, LoadingView, ErrorState, Avatar, LabeledRow } from 'uikit'; +import { SessionManager, SessionStore } from 'common'; +import { ProfileViewModel } from '../viewmodel/ProfileViewModel'; +import { ClassGroup } from '../model/ProfileModels'; + +/** + * 我的页:资料展示 + 模拟登录 / 退出登录。 + */ +@ComponentV2 +export struct ProfilePage { + @Local viewModel: ProfileViewModel = ProfileViewModel.create(); + @Local session: SessionStore = SessionManager.get(); + + aboutToAppear(): void { + this.viewModel.load(); + } + + @Monitor('session.token') + onSessionChange(): void { + this.viewModel.load(); + } + + build() { + Column() { + if (!this.viewModel.loggedIn) { + this.loginCard() + } else if (this.viewModel.loading) { + Column() { + LoadingView({ text: '加载中...' }) + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Center) + } else if (this.viewModel.error.length > 0) { + ErrorState({ message: this.viewModel.error, onRetry: () => { this.viewModel.load(); } }) + } else { + Scroll() { + Column({ space: AppSpacing.md }) { + this.profileHeader() + this.infoCard() + if (this.viewModel.profile.classGroups.length > 0) { + this.classGroupCard() + } + this.logoutButton() + } + .width('100%') + .padding(AppSpacing.lg) + } + .width('100%') + .height('100%') + .scrollBar(BarState.Off) + } + } + .width('100%') + .height('100%') + .backgroundColor(AppColor.background) + } + + @Builder + loginCard() { + Column({ space: AppSpacing.md }) { + Text('👤') + .fontSize(40) + Text('未登录') + .fontSize(AppFontSize.title) + .fontWeight(FontWeight.Bold) + .fontColor(AppColor.textPrimary) + Text('登录后即可查看个人信息') + .fontSize(AppFontSize.body) + .fontColor(AppColor.textMuted) + Text('模拟登录') + .fontSize(AppFontSize.body) + .fontColor(AppColor.white) + .padding({ left: 32, right: 32, top: 10, bottom: 10 }) + .backgroundColor(AppColor.gold) + .borderRadius(AppRadius.pill) + .onClick(() => { + this.viewModel.login(); + }) + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Center) + } + + @Builder + profileHeader() { + Row({ space: AppSpacing.md }) { + Avatar({ name: this.viewModel.profile.displayName, diameter: 56 }) + Column({ space: 4 }) { + Row({ space: 8 }) { + Text(this.viewModel.profile.displayName) + .fontSize(AppFontSize.headline) + .fontWeight(FontWeight.Bold) + .fontColor(AppColor.textPrimary) + Text(this.viewModel.profile.role) + .fontSize(AppFontSize.caption) + .fontColor(AppColor.gold) + .padding({ left: 10, right: 10, top: 4, bottom: 4 }) + .backgroundColor(AppColor.neutralChip) + .borderRadius(AppRadius.pill) + } + Text(this.viewModel.profile.studentId) + .fontSize(AppFontSize.caption) + .fontColor(AppColor.textMuted) + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + } + .width('100%') + .padding(AppSpacing.md) + .backgroundColor(AppColor.white) + .borderRadius(AppRadius.lg) + } + + @Builder + infoCard() { + Column({ space: 0 }) { + Text('个人信息') + .fontSize(AppFontSize.title) + .fontWeight(FontWeight.Bold) + .fontColor(AppColor.textPrimary) + .width('100%') + .margin({ bottom: 4 }) + + LabeledRow({ label: '姓名', value: this.viewModel.profile.name }) + LabeledRow({ label: '学号', value: this.viewModel.profile.studentId }) + LabeledRow({ label: '手机号', value: this.viewModel.profile.phone }) + LabeledRow({ label: '角色', value: this.viewModel.profile.role }) + LabeledRow({ label: '小组', value: this.viewModel.profile.group }) + LabeledRow({ label: '登录方式', value: this.viewModel.profile.loginType }) + } + .width('100%') + .padding(AppSpacing.md) + .backgroundColor(AppColor.white) + .borderRadius(AppRadius.lg) + } + + @Builder + classGroupCard() { + Column({ space: 0 }) { + Text('小组分组') + .fontSize(AppFontSize.title) + .fontWeight(FontWeight.Bold) + .fontColor(AppColor.textPrimary) + .width('100%') + .margin({ bottom: 4 }) + + ForEach(this.viewModel.profile.classGroups, (cg: ClassGroup) => { + Row() { + Text(cg.period) + .fontSize(AppFontSize.body) + .fontColor(AppColor.textMuted) + Blank() + Text(cg.group + ' · ' + cg.role) + .fontSize(AppFontSize.body) + .fontColor(AppColor.textPrimary) + } + .width('100%') + .padding({ top: 12, bottom: 12 }) + }, (cg: ClassGroup) => cg.period + cg.group) + } + .width('100%') + .padding(AppSpacing.md) + .backgroundColor(AppColor.white) + .borderRadius(AppRadius.lg) + } + + @Builder + logoutButton() { + Text('退出登录') + .fontSize(AppFontSize.body) + .fontColor(AppColor.danger) + .width('100%') + .height(48) + .textAlign(TextAlign.Center) + .backgroundColor(AppColor.white) + .borderRadius(AppRadius.md) + .border({ width: 1, color: AppColor.danger }) + .onClick(() => { + this.viewModel.logout(); + }) + } +}