merge: F2 应用壳四 Tab 与首页入口(FE 第一批)
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* ErrorView —— 通用失败态组件(F2)。
|
||||
*
|
||||
* NFR-UX-02「失败必有原因 + 下一步指引」的落地组件:
|
||||
* - message:失败原因(对应 ApiError.message)
|
||||
* - hint:下一步指引(对应 ApiError.hint,可空则不渲染)
|
||||
* - onRetry:可选「重试」回调,不传则不渲染按钮
|
||||
* 其它 FE 任务可直接 import 本组件复用(目录带 f2 前缀,迁移由协调者做)。
|
||||
*/
|
||||
|
||||
const TEXT_PRIMARY: string = '#222222'
|
||||
const TEXT_SECONDARY: string = '#8A9099'
|
||||
const WARN: string = '#FF9F0A'
|
||||
const WARN_BG: string = '#FFF4E0'
|
||||
const ACCENT: string = '#FF5A79'
|
||||
|
||||
@Component
|
||||
export struct ErrorView {
|
||||
@Prop message: string = '请求失败,请检查网络'
|
||||
@Prop hint: string = ''
|
||||
@Prop retryText: string = '重试'
|
||||
onRetry?: () => void
|
||||
|
||||
build() {
|
||||
Column({ space: 10 }) {
|
||||
Text('!')
|
||||
.fontSize(26)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor(WARN)
|
||||
.width(56)
|
||||
.height(56)
|
||||
.textAlign(TextAlign.Center)
|
||||
.borderRadius(28)
|
||||
.backgroundColor(WARN_BG)
|
||||
Text(this.message)
|
||||
.fontSize(15)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(TEXT_PRIMARY)
|
||||
.textAlign(TextAlign.Center)
|
||||
if (this.hint !== '') {
|
||||
Text(this.hint)
|
||||
.fontSize(13)
|
||||
.fontColor(TEXT_SECONDARY)
|
||||
.textAlign(TextAlign.Center)
|
||||
}
|
||||
if (this.onRetry !== undefined) {
|
||||
Button(this.retryText)
|
||||
.fontSize(15)
|
||||
.fontColor(Color.White)
|
||||
.backgroundColor(ACCENT)
|
||||
.borderRadius(20)
|
||||
.height(40)
|
||||
.padding({ left: 32, right: 32 })
|
||||
.margin({ top: 6 })
|
||||
.onClick(() => {
|
||||
if (this.onRetry !== undefined) {
|
||||
this.onRetry()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.padding({ left: 32, right: 32 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* LoadingView —— 通用加载态组件(F2)。
|
||||
*
|
||||
* 居中 LoadingProgress + 可选文案;text 传空串时只显示转圈。
|
||||
* props 一律 @Prop(父传子单向);颜色硬编码(resources 本批禁改,i18n 后置)。
|
||||
*/
|
||||
|
||||
const ACCENT: string = '#FF5A79'
|
||||
const TEXT_SECONDARY: string = '#8A9099'
|
||||
|
||||
@Component
|
||||
export struct LoadingView {
|
||||
@Prop text: string = '加载中…'
|
||||
|
||||
build() {
|
||||
Column({ space: 12 }) {
|
||||
LoadingProgress()
|
||||
.width(44)
|
||||
.height(44)
|
||||
.color(ACCENT)
|
||||
if (this.text !== '') {
|
||||
Text(this.text)
|
||||
.fontSize(14)
|
||||
.fontColor(TEXT_SECONDARY)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,248 @@
|
||||
/**
|
||||
* HomePage —— 占位桩(FE 批次对应任务填充实现)。
|
||||
* 已注册进 main_pages.json,其它页面可按路径跳转。
|
||||
* HomePage —— 应用主壳:底部四 Tab(首页/报告/AI/我的)+ 首页内容。
|
||||
*
|
||||
* 协作铁律(FE 批次 README §2/§3):
|
||||
* - 页面跳转一律 router.pushUrl({ url: 'pages/XxxPage' }),禁止 import 其它页面文件;
|
||||
* Tab2/3/4 本批用「占位 + 跳页」方案(目标页面是独立路由页,由对应任务实现)
|
||||
* - 请求只走 ApiClient;今日额度失败静默降级为 '—'(首页不打扰用户,不弹错误态)
|
||||
* - 响应 wire 类型用具名 interface(字段对齐后端 JSON);泛型调用显式类型参数
|
||||
*/
|
||||
|
||||
import { router } from '@kit.ArkUI'
|
||||
import { ApiClient } from '../core/ApiClient'
|
||||
|
||||
/** GET /api/v1/quota?quota_key=agent_chat 的响应(字段名对齐后端 JSON)。 */
|
||||
interface QuotaWireResp {
|
||||
quota_key: string
|
||||
used: number
|
||||
limit_value: number
|
||||
bonus_granted: boolean
|
||||
remaining: number
|
||||
reset_at: string
|
||||
}
|
||||
|
||||
const PAGE_BG: string = '#F5F6F8'
|
||||
const ACCENT: string = '#FF5A79'
|
||||
const ACCENT_AI: string = '#6C5CE7'
|
||||
const TEXT_PRIMARY: string = '#222222'
|
||||
const TEXT_SECONDARY: string = '#8A9099'
|
||||
const TEXT_DISABLED: string = '#C2C7CE'
|
||||
const QUOTA_PATH: string = '/api/v1/quota?quota_key=agent_chat'
|
||||
const WEEKDAYS: string[] = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct HomePage {
|
||||
build() {
|
||||
Column() {
|
||||
Text('HomePage(建设中)').fontSize(16).fontColor('#666666')
|
||||
@State currentTabIndex: number = 0
|
||||
@State dateLine: string = ''
|
||||
@State quotaRemaining: string = '…'
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.dateLine = this.buildDateLine()
|
||||
this.loadQuota()
|
||||
}
|
||||
|
||||
/** 返回键:不拦截(返回 false 走系统默认)。Tab 壳是应用首页,再按返回即退出应用——本批不加复杂逻辑。 */
|
||||
onBackPress(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
/** 问候区日期行,如「2026年8月15日 周六」。 */
|
||||
private buildDateLine(): string {
|
||||
const now = new Date()
|
||||
return `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日 ${WEEKDAYS[now.getDay()]}`
|
||||
}
|
||||
|
||||
/** 拉今日 AI 对话额度;失败静默降级为 '—'(首页额度只是辅助信息,不值得打断用户)。 */
|
||||
private async loadQuota(): Promise<void> {
|
||||
try {
|
||||
const data = await ApiClient.get<QuotaWireResp>(QUOTA_PATH)
|
||||
this.quotaRemaining = `${data.remaining}`
|
||||
} catch (_e) {
|
||||
this.quotaRemaining = '—'
|
||||
}
|
||||
}
|
||||
|
||||
/** 底部 Tab 栏项:简单 Text 字形 + 中文标签(本批不引资源)。 */
|
||||
@Builder
|
||||
TabBarItem(icon: string, label: string, index: number) {
|
||||
Column({ space: 3 }) {
|
||||
Text(icon)
|
||||
.fontSize(20)
|
||||
.fontColor(this.currentTabIndex === index ? ACCENT : TEXT_SECONDARY)
|
||||
Text(label)
|
||||
.fontSize(11)
|
||||
.fontColor(this.currentTabIndex === index ? ACCENT : TEXT_SECONDARY)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
}
|
||||
|
||||
/** 首页功能入口卡:图标块 + 标题/副标题 + 右箭头,整卡点击跳 targetUrl。 */
|
||||
@Builder
|
||||
EntryCard(title: string, subtitle: string, icon: string, color: string, targetUrl: string) {
|
||||
Row({ space: 12 }) {
|
||||
Text(icon)
|
||||
.fontSize(20)
|
||||
.fontColor(Color.White)
|
||||
.width(44)
|
||||
.height(44)
|
||||
.textAlign(TextAlign.Center)
|
||||
.borderRadius(12)
|
||||
.backgroundColor(color)
|
||||
Column({ space: 4 }) {
|
||||
Text(title)
|
||||
.fontSize(16)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(TEXT_PRIMARY)
|
||||
Text(subtitle)
|
||||
.fontSize(12)
|
||||
.fontColor(TEXT_SECONDARY)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
Text('›')
|
||||
.fontSize(22)
|
||||
.fontColor(TEXT_DISABLED)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(14)
|
||||
.borderRadius(16)
|
||||
.backgroundColor(Color.White)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick(() => {
|
||||
router.pushUrl({ url: targetUrl })
|
||||
})
|
||||
}
|
||||
|
||||
/** 今日额度小卡:显示「AI 对话剩余 N 次」,失败静默显示 '—'。 */
|
||||
@Builder
|
||||
QuotaCard() {
|
||||
Row() {
|
||||
Column({ space: 4 }) {
|
||||
Text('今日额度')
|
||||
.fontSize(12)
|
||||
.fontColor(TEXT_SECONDARY)
|
||||
Text(`AI 对话剩余 ${this.quotaRemaining} 次`)
|
||||
.fontSize(16)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(TEXT_PRIMARY)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
Text('每日刷新')
|
||||
.fontSize(11)
|
||||
.fontColor(TEXT_DISABLED)
|
||||
.padding({ top: 4, bottom: 4, left: 10, right: 10 })
|
||||
.borderRadius(10)
|
||||
.backgroundColor(PAGE_BG)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(14)
|
||||
.borderRadius(16)
|
||||
.backgroundColor(Color.White)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
|
||||
/** Tab1 首页:问候区 + 两张功能入口卡 + 今日额度小卡。 */
|
||||
@Builder
|
||||
HomeTab() {
|
||||
Scroll() {
|
||||
Column({ space: 12 }) {
|
||||
Column({ space: 6 }) {
|
||||
Text('你好,美搭')
|
||||
.fontSize(26)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor(TEXT_PRIMARY)
|
||||
Text(this.dateLine)
|
||||
.fontSize(13)
|
||||
.fontColor(TEXT_SECONDARY)
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.margin({ top: 8 })
|
||||
|
||||
this.EntryCard('生成我的形象报告', '体型 · 体态 · 穿搭建议,一次看全', '▣', ACCENT, 'pages/ReportCreatePage')
|
||||
this.EntryCard('AI 穿搭顾问', '对话式穿搭建议,随时问', '✦', ACCENT_AI, 'pages/ChatPage')
|
||||
this.QuotaCard()
|
||||
}
|
||||
.width('100%')
|
||||
.padding(16)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.align(Alignment.Top)
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
}
|
||||
|
||||
/** Tab2/3/4 占位页:说明文案 + 居中按钮跳转对应真页面(独立路由页)。 */
|
||||
@Builder
|
||||
PlaceholderTab(icon: string, title: string, desc: string, buttonText: string, targetUrl: string) {
|
||||
Column({ space: 10 }) {
|
||||
Text(icon)
|
||||
.fontSize(44)
|
||||
.fontColor(TEXT_DISABLED)
|
||||
Text(title)
|
||||
.fontSize(18)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(TEXT_PRIMARY)
|
||||
Text(desc)
|
||||
.fontSize(13)
|
||||
.fontColor(TEXT_SECONDARY)
|
||||
.textAlign(TextAlign.Center)
|
||||
Button(buttonText)
|
||||
.fontSize(15)
|
||||
.fontColor(Color.White)
|
||||
.backgroundColor(ACCENT)
|
||||
.borderRadius(22)
|
||||
.height(44)
|
||||
.padding({ left: 28, right: 28 })
|
||||
.margin({ top: 10 })
|
||||
.onClick(() => {
|
||||
router.pushUrl({ url: targetUrl })
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.padding({ left: 32, right: 32 })
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Tabs({ barPosition: BarPosition.End }) {
|
||||
TabContent() {
|
||||
this.HomeTab()
|
||||
}
|
||||
.tabBar(this.TabBarItem('⌂', '首页', 0))
|
||||
|
||||
TabContent() {
|
||||
this.PlaceholderTab('▤', '报告', '形象报告的查看与管理在这里(建设中)\n新建报告请前往报告创建页', '去生成报告', 'pages/ReportCreatePage')
|
||||
}
|
||||
.tabBar(this.TabBarItem('▤', '报告', 1))
|
||||
|
||||
TabContent() {
|
||||
this.PlaceholderTab('✦', 'AI 顾问', '与 AI 穿搭顾问的对话在独立页面进行(建设中)', '进入 AI 对话', 'pages/ChatPage')
|
||||
}
|
||||
.tabBar(this.TabBarItem('✦', 'AI', 2))
|
||||
|
||||
TabContent() {
|
||||
this.PlaceholderTab('☺', '我的', '个人资料与设置(建设中)', '去我的资料', 'pages/ProfilePage')
|
||||
}
|
||||
.tabBar(this.TabBarItem('☺', '我的', 3))
|
||||
}
|
||||
.scrollable(true)
|
||||
.barMode(BarMode.Fixed)
|
||||
.vertical(false)
|
||||
.divider({ strokeWidth: 0.5, color: '#E5E7EB' })
|
||||
.onChange((index: number) => {
|
||||
this.currentTabIndex = index
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor(PAGE_BG)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user