feat(entry): 复刻四大主页面与底部导航
This commit is contained in:
@@ -4,6 +4,11 @@ import { HttpClient, AuthInterceptor, SessionManager, Logger } from 'common';
|
|||||||
import { ServiceLocator } from '../di/ServiceLocator';
|
import { ServiceLocator } from '../di/ServiceLocator';
|
||||||
import { UserApi } from '../data/api/UserApi';
|
import { UserApi } from '../data/api/UserApi';
|
||||||
import { UserRepositoryImpl } from '../data/repository/UserRepository';
|
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';
|
const TAG: string = 'EntryAbility';
|
||||||
|
|
||||||
@@ -29,9 +34,16 @@ export default class EntryAbility extends UIAbility {
|
|||||||
httpClient.addInterceptor(new AuthInterceptor());
|
httpClient.addInterceptor(new AuthInterceptor());
|
||||||
ServiceLocator.register('httpClient', httpClient);
|
ServiceLocator.register('httpClient', httpClient);
|
||||||
|
|
||||||
// 数据层
|
// 数据层(真实示例)
|
||||||
const userApi = new UserApi(httpClient);
|
const userApi = new UserApi(httpClient);
|
||||||
ServiceLocator.register('userRepository', new UserRepositoryImpl(userApi));
|
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 {
|
onDestroy(): void {
|
||||||
|
|||||||
232
entry/src/main/ets/pages/CheckinPage.ets
Normal file
232
entry/src/main/ets/pages/CheckinPage.ets
Normal file
@@ -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%')
|
||||||
|
}
|
||||||
|
}
|
||||||
155
entry/src/main/ets/pages/HomePage.ets
Normal file
155
entry/src/main/ets/pages/HomePage.ets
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,63 +1,60 @@
|
|||||||
import { AppColor, AppSpacing, AppButton, LoadingView, EmptyView } from 'uikit';
|
import { AppColor, AppFontSize } from 'uikit';
|
||||||
import { HomeViewModel } from '../viewmodel/HomeViewModel';
|
import { HomePage } from './HomePage';
|
||||||
import { User } from '../model/User';
|
import { CheckinPage } from './CheckinPage';
|
||||||
import { AppRouter } from '../router/AppRouter';
|
import { PlanPage } from './PlanPage';
|
||||||
import { RouteMap, RouteNames } from '../router/RouteMap';
|
import { ProfilePage } from './ProfilePage';
|
||||||
import { StatusCard } from '../view/StatusCard';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 首页(@Entry):Navigation 根容器,仅编排。
|
* 应用入口:底部四 tab 壳(首页 / 打卡 / 计划 / 我的)。
|
||||||
* 演示「页面 -> ViewModel -> Repository -> Api -> HttpClient」完整链路。
|
|
||||||
*/
|
*/
|
||||||
@Entry
|
@Entry
|
||||||
@ComponentV2
|
@ComponentV2
|
||||||
struct Index {
|
struct Index {
|
||||||
@Local pathStack: NavPathStack = new NavPathStack();
|
@Local currentIndex: number = 0;
|
||||||
@Local viewModel: HomeViewModel = HomeViewModel.create();
|
|
||||||
@Local title: string = 'Harmony Client';
|
|
||||||
|
|
||||||
aboutToAppear(): void {
|
|
||||||
AppRouter.init(this.pathStack);
|
|
||||||
this.viewModel.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
Navigation(this.pathStack) {
|
Tabs({ barPosition: BarPosition.End, index: this.currentIndex }) {
|
||||||
Column({ space: AppSpacing.md }) {
|
TabContent() {
|
||||||
Text(this.title)
|
HomePage({ onGoLogin: () => { this.currentIndex = 3; } })
|
||||||
.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, '来自首页的参数');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
.width('100%')
|
.tabBar(this.tabBarBuilder(0, '首页', '🏠'))
|
||||||
.height('100%')
|
|
||||||
.padding(AppSpacing.lg)
|
TabContent() {
|
||||||
.backgroundColor(AppColor.background)
|
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)
|
.onChange((index: number) => {
|
||||||
.mode(NavigationMode.Stack)
|
this.currentIndex = index;
|
||||||
.hideTitleBar(true)
|
})
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundColor(AppColor.background)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Builder
|
@Builder
|
||||||
routeMap(name: string, param: Object) {
|
tabBarBuilder(index: number, title: string, icon: string) {
|
||||||
RouteMap(name, param)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
183
entry/src/main/ets/pages/PlanPage.ets
Normal file
183
entry/src/main/ets/pages/PlanPage.ets
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
184
entry/src/main/ets/pages/ProfilePage.ets
Normal file
184
entry/src/main/ets/pages/ProfilePage.ets
Normal file
@@ -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();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user