HarmonyOS 6.1.1 ArkTS 声明式 UI 实战:汉服国风体验馆朱红鎏金主题与朝代筛选日历预约地址 CRUD 全维度深度解析
引言
鸿蒙生态在过去数年中经历了从探索期到成熟期的跨越式发展,HarmonyOS 6.1.1 作为当前最新的稳定版本,在 ArkUI 声明式开发框架中引入了大量面向复杂业务场景的增强能力。开发者现在可以利用 @Component、@State、@Builder、@Entry、export 等装饰器语法体系,构建出结构清晰、状态可控、组件可复用的高质量移动应用界面。基于 HarmonyOS API 24 的 ArkTS 语言,不仅继承了 TypeScript 的静态类型检查能力,更在编译期和运行时层面针对声明式 UI 的 diff 算法进行了深度优化,使得界面更新效率大幅提升,即使在包含大量动态弹窗和状态切换的复杂交互场景下,也能保持流畅的用户体验。
ArkTS 声明式 UI 的核心理念是"UI 是状态的函数映射"。开发者只需声明界面结构如何依赖状态数据,当 @State 修饰的变量发生变化时,框架自动追踪依赖关系并触发对应组件的重新渲染。这一机制在汉服国风体验馆这类应用中显得尤为重要——该应用包含了朝代筛选、尺码表查看、日历预约时段选择、摄影师挑选、地址增删改查等多个高度交互性的功能模块,每个模块都涉及大量的状态变量和弹窗交互,声明式状态管理使得这些复杂交互的实现变得简洁而可靠。
汉服国风体验馆是一个面向汉服爱好者的一站式服务平台,集成了今日上新、服饰租赁、簪花铺子、写真预约、订单管理和个人中心六大核心模块。在视觉设计上,该应用采用了朱红(#C8102E)与鎏金(#E8B34B)的双主色搭配方案,底色为暖米色 #FFF7EC——朱红色承袭中国传统漆器与宫墙的经典色调,鎏金色则呼应古代金箔与铜镀金的工艺美感,两者在暖米色底纸上营造出浓郁的新中式国风美学。与游戏点卡充值中心不同,该应用的所有子组件都使用了 export struct 导出修饰,且所有模态弹窗均以内联方式直接写在各组件的 build() 方法中而非抽取为独立的 @Builder 方法,同时定义了一个全局的 @Builder function modalOverlay 作为跨组件共享的遮罩层构建器。
本文将从接口定义层、全局常量与数据配置、全局纯函数与全局构建器、各 Tab 组件的逐段源码解析、主入口页面架构、流程图绘制以及技术对比等维度,对该汉服国风体验馆的完整源码进行深度技术解读。
接口定义层分析
汉服国风体验馆共定义了十四个接口,是三个应用中接口数量最多的。这些接口覆盖了朝代筛选、汉服商品、租金走势、尺码表、配饰商品、写真套餐、摄影师、日历日期、订单记录、优惠券、积分明细、收货地址、Tab 配置和消息推送等全部业务领域。
朝代筛选与汉服商品接口
interface DynastyChip {
name: string
count: number
}
interface HanfuItem {
id: number
title: string
dynasty: string
price: number
originPrice: number
rentDay: number
score: number
rentCount: number
tags: string[]
hot: boolean
color: string
}

DynastyChip 接口定义了朝代筛选标签的数据结构,name 字段存储朝代名称(全部朝代、唐制、宋制、明制、魏晋、清汉女),count 字段存储该朝代的在架商品数量,在 UI 上以"唐制 92"的形式展示,让用户在选择前就能了解各朝代的商品丰富度。
HanfuItem 接口是汉服商品的核心数据结构,字段最为丰富。dynasty 字段标识所属朝代,用于筛选逻辑;price 和 originPrice 分别表示租金和原价,通过 decoration({ type: TextDecorationType.LineThrough }) 实现原价划线效果;rentDay 字段表示基础租赁天数(通常为 3 天),在租赁弹窗中用于计算 price * Math.ceil(rentNum / rentDay) 的总价;score 和 rentCount 分别表示评分和租赁次数,用于商品排序和信任度展示;tags 是字符串数组,存储如"新品首发"、“含发饰”、"飘逸"等标签,通过 ForEach 循环渲染为小标签;color 字段存储商品的代表性颜色十六进制值,作为商品图片区域的纯色背景,在无网络图片的场景下实现了视觉区分。
租金走势与尺码表接口
interface RentTrend {
day: string
price: number
}
interface SizeRow {
size: string
height: string
bust: string
stock: number
}
RentTrend 接口定义了一周七天的租金走势数据,price 字段表示当日租金均价,周末价格显著高于工作日(周五 138、周六 168、周日 152 vs 工作日 72-96),通过 rentBarH() 函数映射为柱状图高度,通过 trendBarColor() 函数映射为柱状图颜色。
SizeRow 接口定义了尺码表的数据结构,size 字段为尺码代号(XS/S/M/L/XL),height 和 bust 分别表示对应身高和胸围范围,stock 字段表示该尺码的库存数量。当 stock > 0 时显示"可租 X 件",否则显示"已约满"。M 码行会以浅黄色背景高亮,表示推荐尺码。
配饰与写真套餐接口
interface AccessoryItem {
id: number
name: string
material: string
price: number
stock: number
grade: string
color: string
}
interface PhotoPackage {
id: number
name: string
scene: string
price: number
originPrice: number
duration: string
photos: string
retouch: string
hot: boolean
color: string
}

AccessoryItem 接口定义了簪花配饰商品的结构。material 字段存储材质信息(铜镀金·珍珠、非遗绒花、手作缠花等),在商品卡片中以小字展示,传达工艺品质;grade 字段标识等级(匠心工艺、非遗联名、手作限定、孤品等),用于商品的差异化定位。
PhotoPackage 接口定义了写真套餐的结构,字段最为丰富。scene 字段描述拍摄场景(园林实景、花墙+茶室双景、中式庭院烛光等),duration 字段表示拍摄时长,photos 字段描述精修张数和底片政策,retouch 字段标识修图等级(轻奢精修、杂志级修图、电影级调色等),这些字段共同构成了写真套餐的完整营销信息。
摄影师与日历接口
interface Photographer {
id: number
name: string
years: number
style: string
price: number
works: number
rating: number
}
interface CalendarDay {
idx: number
label: string
date: string
left: number
}
Photographer 接口定义了签约摄影师的信息结构。years 字段表示从业年限,用于在卡片上以"从业 X 年"的标签形式展示资历;style 字段描述拍摄风格(电影感·情绪光影、清冷宋制·留白美学等),帮助用户匹配自己的审美偏好;works 字段表示作品数量,rating 字段表示评分。
CalendarDay 接口定义了日历日期的数据结构。idx 作为唯一标识,label 字段为日期标签(今天、明天、周四等),date 字段为日期字符串(8/24、8/25等),left 字段表示剩余可约数量。当 left === 0 时,该日期会显示"已约满"并禁用选择。这个接口在试穿预约弹窗和写真预约弹窗中均被使用,是跨 Tab 共享的日历数据源。
订单与优惠接口
interface OrderRecord {
id: string
item: string
cover: string
date: string
days: number
status: string
statusColor: string
amount: number
deposit: number
}
interface CouponMeta {
id: number
title: string
amount: number
condition: string
expire: string
color: string
}
interface PointLog {
id: number
title: string
delta: number
date: string
}

OrderRecord 接口定义了订单记录的结构。与游戏充值中心不同,这里的 id 是字符串类型(如"HF2026082301"),更贴近真实订单号格式。cover 字段存储订单封面的颜色值,用于在无图片时以纯色块展示商品视觉。statusColor 字段直接将状态颜色存储在数据中(如租赁中为 #C8102E、已归还为 #5C7B8A),避免了在渲染时进行条件映射。deposit 字段表示押金金额,在订单卡片中根据 deposit > 0 条件决定是否展示押金信息。
CouponMeta 接口定义了优惠券的结构,color 字段存储优惠券的主题色,使得不同类型优惠券可以有不同的视觉风格。PointLog 接口定义了积分变动记录,delta 字段为正数表示获得积分,为负数表示消耗积分,通过 delta > 0 ? '+' : '' 的条件表达式在 UI 上展示带正负号的变动值。
地址与配置接口
interface AddrMeta {
id: number
name: string
phone: string
region: string
detail: string
tag: string
}
interface TabMeta {
name: string
icon: string
}
interface MsgMeta {
id: number
title: string
content: string
time: string
read: boolean
}

AddrMeta 接口定义了收货地址的结构。tag 字段标识地址类型(默认、公司、家),以小标签形式展示。region 字段存储省市信息,detail 字段存储详细地址。TabMeta 接口定义了底部导航配置,与游戏充值中心不同的是,icon 字段使用中文文字而非 emoji(“上新”、“租赁”、"簪花"等),更符合国风应用的文字美学。MsgMeta 接口新增了 read 布尔字段,用于标识消息的已读/未读状态,未读消息会在消息中心以红色圆点标记。
全局常量与枚举分析
朝代与汉服数据配置
const DYNASTY_CHIPS: DynastyChip[] = [
{ name: '全部朝代', count: 286 },
{ name: '唐制', count: 92 },
{ name: '宋制', count: 74 },
{ name: '明制', count: 68 },
{ name: '魏晋', count: 31 },
{ name: '清汉女', count: 21 }
]
DYNASTY_CHIPS 常量数组定义了六个朝代筛选标签。从数量分布可以看出,唐制(92 套)和宋制(74 套)是商品最丰富的朝代,魏晋(31 套)和清汉女(21 套)则相对小众。这种数据驱动的筛选标签设计,让用户在选择朝代时就能直观感知商品库的深度,有助于做出快速决策。
const NEW_HANFU: HanfuItem[] = [
{ id: 1, title: '齐胸襦裙·烟霞染', dynasty: '唐制', price: 88, originPrice: 158, rentDay: 3, score: 4.9, rentCount: 2317, tags: ['新品首发', '含发饰', '飘逸'], hot: true, color: '#F5C8D0' },
{ id: 2, title: '马面裙·山河绣', dynasty: '明制', price: 68, originPrice: 128, rentDay: 3, score: 4.8, rentCount: 4521, tags: ['爆款返场', '织金'], hot: true, color: '#D9B382' },
// ... 其余汉服数据
]
const RENT_LIST: HanfuItem[] = [
{ id: 101, title: '全套唐制·花朝宴', dynasty: '唐制', price: 128, originPrice: 228, rentDay: 3, score: 4.9, rentCount: 3216, tags: ['全套含鞋', '送造型'], hot: true, color: '#F2B8C6' },
// ... 其余租赁数据
]
NEW_HANFU 和 RENT_LIST 分别定义了今日上新 Tab 和服饰租赁 Tab 的商品数据。值得注意的是,两个数组的 id 编号体系不同——上新商品从 1 开始,租赁商品从 101 开始,避免了 ID 冲突。color 字段为每件汉服配置了代表性颜色(如烟霞染的 #F5C8D0 浅粉色、竹影青的 #BFD8C2 淡绿色),在无图片时作为纯色封面展示,同时通过颜色名称传达汉服的意境美感。
走势与尺码数据配置
const RENT_TREND: RentTrend[] = [
{ day: '周一', price: 96 },
{ day: '周二', price: 78 },
{ day: '周三', price: 72 },
{ day: '周四', price: 88 },
{ day: '周五', price: 138 },
{ day: '周六', price: 168 },
{ day: '周日', price: 152 }
]
const SIZE_ROWS: SizeRow[] = [
{ size: 'XS', height: '150-158cm', bust: '80-84cm', stock: 12 },
{ size: 'S', height: '158-163cm', bust: '84-88cm', stock: 26 },
{ size: 'M', height: '163-168cm', bust: '88-92cm', stock: 31 },
{ size: 'L', height: '168-173cm', bust: '92-96cm', stock: 18 },
{ size: 'XL', height: '173-178cm', bust: '96-100cm', stock: 7 }
]

RENT_TREND 的数据分布清晰地反映了汉服租赁的周末溢价现象——工作日均价在 72-96 元之间,周末则飙升至 138-168 元,涨幅接近一倍。这种数据特征通过柱状图可视化后,能够有效引导用户选择工作日租赁以节省费用。SIZE_ROWS 定义了从 XS 到 XL 的五档尺码,M 码库存最充足(31 件),XL 码库存最紧张(7 件),通过 stock > 0 的条件判断展示可租或约满状态。
配饰与写真数据配置
const ACCESSORY_ITEMS: AccessoryItem[] = [
{ id: 1, name: '鎏金发簪·并蒂莲', material: '铜镀金·珍珠', price: 39, stock: 126, grade: '匠心工艺', color: '#E8B34B' },
{ id: 2, name: '绒花发钗·石榴红', material: '非遗绒花', price: 45, stock: 88, grade: '非遗联名', color: '#D94F5C' },
// ... 其余配饰数据
]
const ACCESSORY_CATS: string[] = ['全部', '发饰', '璎珞', '道具', '团扇', '鞋履']
const PHOTO_PACKAGES: PhotoPackage[] = [
{ id: 1, name: '单人国风写真', scene: '园林实景 3 选 1', price: 399, originPrice: 699, duration: '120 分钟', photos: '精修 9 张 + 底片全送', retouch: '轻奢精修', hot: true, color: '#F5C8D0' },
// ... 其余套餐数据
]
const PHOTOGRAPHERS: Photographer[] = [
{ id: 1, name: '阿棠', years: 8, style: '电影感·情绪光影', price: 200, works: 1860, rating: 4.9 },
// ... 其余摄影师数据
]
配饰数据覆盖了发簪、发钗、步摇、璎珞、玉佩、折扇、油纸伞和披帛八种品类,材质涵盖铜镀金、非遗绒花、手作缠花、岫玉、真丝绡等多种传统工艺。写真套餐从 399 元的单人写真到 1299 元的情侣婚照,覆盖了不同消费层级的需求。摄影师数据展示了四位签约摄影师,从业年限从 4 年到 8 年不等,风格各异,价格从 120 元到 200 元/组。
日历与订单数据配置
const CALENDAR_DAYS: CalendarDay[] = [
{ idx: 0, label: '今天', date: '8/24', left: 6 },
{ idx: 1, label: '明天', date: '8/25', left: 12 },
{ idx: 2, label: '周四', date: '8/26', left: 9 },
{ idx: 3, label: '周五', date: '8/27', left: 3 },
{ idx: 4, label: '周六', date: '8/28', left: 0 },
{ idx: 5, label: '周日', date: '8/29', left: 2 },
{ idx: 6, label: '周一', date: '8/30', left: 14 }
]
const ORDER_LIST: OrderRecord[] = [
{ id: 'HF2026082301', item: '全套唐制·花朝宴', cover: '#F2B8C6', date: '08/23 - 08/25', days: 3, status: '租赁中', statusColor: '#C8102E', amount: 128, deposit: 200 },
// ... 其余订单数据
]
const ORDER_FILTERS: string[] = ['全部', '租赁中', '待取片', '已归还', '退款']

CALENDAR_DAYS 定义了未来七天的日历数据,周六(8/28)的 left 为 0,表示当日已约满,在 UI 上会以灰色禁用状态展示。ORDER_LIST 定义了五条订单记录,覆盖了租赁中、已归还、待取片、已完成和退款成功五种状态,每种状态都有对应的 statusColor 颜色值。ORDER_FILTERS 定义了五个筛选标签。
优惠与积分数据配置
const COUPON_LIST: CouponMeta[] = [
{ id: 1, title: '汉服租赁立减券', amount: 30, condition: '满 128 可用', expire: '09/30 前有效', color: '#C8102E' },
{ id: 2, title: '写真定金膨胀券', amount: 100, condition: '满 399 可用', expire: '10/15 前有效', color: '#E8B34B' },
{ id: 3, title: '簪花铺子满赠券', amount: 10, condition: '满 39 可用', expire: '08/31 前有效', color: '#D94F5C' },
{ id: 4, title: '新人首单 5 折券', amount: 20, condition: '无门槛', expire: '09/15 前有效', color: '#8E0E20' }
]
const POINT_LOGS: PointLog[] = [
{ id: 1, title: '归还加分·衣物完好', delta: 20, date: '08/25 10:32' },
{ id: 2, title: '签到得积分', delta: 5, date: '08/24 09:00' },
{ id: 3, title: '写真订单返积分', delta: 120, date: '08/10 18:20' },
{ id: 4, title: '积分兑换发簪', delta: -200, date: '08/02 12:41' },
{ id: 5, title: '评价晒图奖励', delta: 15, date: '08/01 21:05' }
]

优惠券数据涵盖了租赁、写真和配饰三种业务场景,每张优惠券都有独立的主题色,在优惠券卡片左侧以纯色块展示金额。积分明细记录了五种积分变动场景,其中 delta 为负数(-200)的记录表示积分兑换消耗,在 UI 上以蓝色(#8FA3C4)展示,正数则以朱红色展示。
导航与消息配置
const MAIN_TABS: TabMeta[] = [
{ name: '今日上新', icon: '上新' },
{ name: '服饰租赁', icon: '租赁' },
{ name: '簪花铺子', icon: '簪花' },
{ name: '写真预约', icon: '写真' },
{ name: '订单', icon: '订单' },
{ name: '我的', icon: '我的' }
]
const MSG_LIST: MsgMeta[] = [
{ id: 1, title: '租赁提醒', content: '您的「花朝宴」还有 1 天到期,顺丰上门取件已预约', time: '10:12', read: false },
// ... 其余消息数据
]
MAIN_TABS 的 icon 字段使用中文文字而非 emoji,这在三个应用中独树一帜——文字图标更符合国风应用的文人气韵,也与朱红鎏金的古典配色相得益彰。MSG_LIST 中前两条消息的 read 为 false(未读),对应主入口页面头部的未读红点指示器。
全局纯函数与全局构建器分析
租金柱状图高度函数 rentBarH
function rentBarH(p: number): number {
if (p >= 160) {
return 84
}
if (p >= 130) {
return 66
}
if (p >= 90) {
return 46
}
return 28
}
rentBarH 函数将租金价格映射为柱状图高度,采用阶梯式映射策略而非线性映射。160 元以上映射为 84 像素最高柱,130-159 元映射为 66 像素,90-129 元映射为 46 像素,90 元以下映射为 28 像素最低柱。阶梯式映射的优势在于视觉区分度更高——工作日和周末的价格差异在视觉上表现为明显的"高低跳跃"效果,比线性映射更能突出周末溢价现象。
趋势柱状图颜色函数 trendBarColor
function trendBarColor(p: number): string {
if (p >= 130) {
return '#C8102E'
}
return '#E8B34B'
}

trendBarColor 函数将价格映射为柱状图渐变色。130 元以上(周末档)使用朱红色渐变 ['#E8556D', '#C8102E'],130 元以下(工作日档)使用鎏金色渐变 ['#F2D088', '#E8B34B']。这种双色映射与高度阶梯函数配合,形成了"高柱红色、低柱金色"的视觉模式,用户一眼即可识别周末高峰。
全局遮罩构建器 modalOverlay
@Builder
function modalOverlay(onClose: () => void) {
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(30,10,12,0.55)')
.onClick(() => {
onClose()
})
}
这是整个应用中最重要的全局构建器。与游戏充值中心在每个 Tab 内部各自定义 modalOverlay 不同,汉服体验馆将遮罩层抽取为全局 @Builder function,所有子组件共享同一个遮罩构建器。遮罩背景色为 rgba(30,10,12,0.55)——暗红黑色调的半透明覆盖,与朱红主色保持视觉协调。这种全局共享构建器的设计,消除了代码重复,是组件复用理念在声明式 UI 中的具体体现。
各 Tab 组件深度分析
今日上新 Tab(NewTab)
今日上新 Tab 是应用的首页,展示本周新上架的汉服商品,支持朝代筛选和租赁确认。
@State 变量定义
@Component
export struct NewTab {
@State dynastyIdx: number = 0
@State showRent: boolean = false
@State showSize: boolean = false
@State showFav: boolean = false
@State rentNum: number = 3
@State favCount: number = 328
@State isFav: boolean = false
@State selHanfu: HanfuItem = NEW_HANFU[0]
dynastyIdx 管理朝代筛选的选中索引,showRent、showSize、showFav 分别控制租赁弹窗、尺码表弹窗和上新提醒弹窗的显隐。rentNum 管理租赁天数,初始值为 3(对应基础租期),范围限制在 1-15 天。selHanfu 存储当前选中的汉服对象引用,作为弹窗数据的来源。
build() 方法关键代码
朝代筛选区域:
Scroll() {
Row({ space: 8 }) {
ForEach(DYNASTY_CHIPS, (chip: DynastyChip) => {
Text(chip.name + ' ' + chip.count)
.fontSize(12)
.fontColor(this.dynastyIdx === DYNASTY_CHIPS.indexOf(chip) ? '#FFFFFF' : '#8E0E20')
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.backgroundColor(this.dynastyIdx === DYNASTY_CHIPS.indexOf(chip) ? '#C8102E' : '#FDF1E3')
.borderRadius(16)
.onClick(() => {
this.dynastyIdx = DYNASTY_CHIPS.indexOf(chip)
})
}, (chip: DynastyChip) => chip.name)
}
}
.scrollable(ScrollDirection.Horizontal)
朝代筛选使用横向可滚动的标签条,选中状态为朱红色背景白色文字,未选中为浅米色背景深红色文字。ForEach 的第三个参数提供了 key 生成器 chip.name,确保列表项的高效 diff。注意这里使用了 DYNASTY_CHIPS.indexOf(chip) 而非索引参数来匹配选中状态——这是一种安全的写法,避免了 ForEach 索引与数据索引不一致的问题。
商品列表使用纵向排列的卡片布局,每张卡片包含纯色封面(item.color)、朝代标签、HOT 标签、商品标题、标签云、评分和租赁信息。租赁弹窗以内联方式直接写在 build() 中:
if (this.showRent) {
modalOverlay(() => { this.showRent = false })
Column({ space: 14 }) {
Row() {
Text('租赁确认')
Row().layoutWeight(1)
Text('×').onClick(() => { this.showRent = false })
}
Row({ space: 10 }) {
Column().width(64).height(80).backgroundColor(this.selHanfu.color)
Column({ space: 4 }) {
Text(this.selHanfu.title)
Text(this.selHanfu.dynasty + ' · 含全套配饰')
Text('押金 ¥200(归还全额退)')
}
}
Row() {
Text('租赁天数')
Row().layoutWeight(1)
Row({ space: 0 }) {
Text('−').onClick(() => { if (this.rentNum > 1) this.rentNum-- })
Text(this.rentNum + ' 天')
Text('+').onClick(() => { if (this.rentNum < 15) this.rentNum++ })
}
}
Row() {
Text('查看尺码表').onClick(() => { this.showSize = true })
Row().layoutWeight(1)
Text('顺丰双向包邮 · 破损险已含')
}
Row() {
Column({ space: 1 }) {
Text('合计')
Text('¥' + (this.selHanfu.price * Math.ceil(this.rentNum / 3)))
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#C8102E')
}
Row().layoutWeight(1)
Text('确认租赁')
.linearGradient({ angle: 90, colors: [['#C8102E', 0.0], ['#E8556D', 1.0]] })
}
}
.backgroundColor('#FFF9F0').borderRadius(16).width('92%')
}
租赁弹窗的租赁天数加减器是典型的步进器交互,通过 rentNum > 1 和 rentNum < 15 的条件判断限制操作范围。总价计算使用 this.selHanfu.price * Math.ceil(this.rentNum / 3) 公式——每 3 天为一个计费周期,不足 3 天按 3 天计算。这种"向上取整"的定价策略在租赁行业非常常见。尺码表弹窗通过 showSize 状态嵌套控制,在租赁弹窗内再弹出尺码表,形成了二级弹窗的交互层级。
服饰租赁 Tab(RentTab)
服饰租赁 Tab 展示了全套汉服的租赁信息,包含租金走势图和到店试穿预约。
@Component
export struct RentTab {
@State filterIdx: number = 0
@State showTry: boolean = false
@State showDetail: boolean = false
@State selItem: HanfuItem = RENT_LIST[0]
@State tryDate: string = '今天'
@State trySlot: string = '14:00'
tryDate 和 trySlot 分别管理试穿预约的日期和时段选择。租金走势柱状图:
Row({ space: 10 }) {
ForEach(RENT_TREND, (t: RentTrend) => {
Column({ space: 4 }) {
Text('¥' + t.price)
.fontColor(t.price >= 130 ? '#C8102E' : '#A8896A')
Column()
.width(18)
.height(rentBarH(t.price))
.borderRadius({ topLeft: 4, topRight: 4 })
.linearGradient({
angle: 180,
colors: trendBarColor(t.price) === '#C8102E'
? [['#E8556D', 0.0], ['#C8102E', 1.0]]
: [['#F2D088', 0.0], ['#E8B34B', 1.0]]
})
Text(t.day)
.fontColor('#A8896A')
}
}, (t: RentTrend) => t.day)
}
柱状图通过 ForEach 遍历 RENT_TREND 数据渲染,每根柱子使用 rentBarH() 计算高度、trendBarColor() 选择渐变色。柱子顶部圆角(topLeft: 4, topRight: 4)使得视觉更加柔和。价格标签在柱子上方,当价格达到 130 元以上时变为朱红色以突出周末溢价。
试穿预约弹窗包含日期选择和时段选择两部分:
Column({ space: 8 }) {
Text('选择日期')
Row({ space: 8 }) {
ForEach(CALENDAR_DAYS, (d: CalendarDay) => {
Column({ space: 3 }) {
Text(d.label)
.fontColor(this.tryDate === d.date ? '#FFFFFF' : '#5C4A3E')
Text(d.date)
.fontColor(this.tryDate === d.date ? '#FFE3C8' : '#A8896A')
}
.backgroundColor(this.tryDate === d.date ? '#C8102E' : '#FDF6EA')
.onClick(() => { this.tryDate = d.date })
}, (d: CalendarDay) => d.idx.toString())
}
}
Column({ space: 8 }) {
Text('选择时段')
Row({ space: 8 }) {
ForEach(['10:00', '14:00', '16:00', '19:00'], (slot: string) => {
Text(slot)
.fontColor(this.trySlot === slot ? '#FFFFFF' : '#5C4A3E')
.backgroundColor(this.trySlot === slot ? '#8E0E20' : '#FDF6EA')
.onClick(() => { this.trySlot = slot })
}, (slot: string) => slot)
}
}
日期选择以横排卡片形式展示未来七天,选中日期以朱红色背景高亮。时段选择则使用更深的暗红色(#8E0E20)作为选中背景色,与日期选择形成视觉层次区分。地址信息以纯文字形式展示在弹窗底部。
簪花铺子 Tab(PinTab)
簪花铺子 Tab 展示了传统配饰商品,支持分类筛选和购物车操作。
@Component
export struct PinTab {
@State catIdx: number = 0
@State showCart: boolean = false
@State showCare: boolean = false
@State buyNum: number = 1
@State selAcc: AccessoryItem = ACCESSORY_ITEMS[0]
商品布局使用 Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) 实现双列网格:
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(ACCESSORY_ITEMS, (acc: AccessoryItem) => {
Column({ space: 8 }) {
Column()
.width('100%')
.height(110)
.backgroundColor(acc.color)
Column({ space: 4 }) {
Text(acc.name).maxLines(1)
Text(acc.material)
Row() {
Text('¥' + acc.price).fontColor('#C8102E')
Row().layoutWeight(1)
Text('加入购物车')
.backgroundColor('#8E0E20')
.onClick(() => {
this.selAcc = acc
this.showCart = true
})
}
}
}
.width('48.5%')
}, (acc: AccessoryItem) => acc.id.toString())
}
每张商品卡片宽度为 48.5%,两列排列。购物车弹窗包含商品信息、数量加减器(上限为 selAcc.stock)、定制刻字提示和小计金额。养护贴士弹窗以五条文字列表展示配饰保养知识,是纯信息展示型弹窗的典型实现。
写真预约 Tab(PhotoTab)
写真预约 Tab 是功能最复杂的模块之一,集成了套餐展示、摄影师选择和预约确认。
@Component
export struct PhotoTab {
@State showBook: boolean = false
@State showPhoto: boolean = false
@State showDate: boolean = false
@State selPkg: PhotoPackage = PHOTO_PACKAGES[0]
@State selPhotographer: Photographer = PHOTOGRAPHERS[0]
@State bookDate: string = '8/28'
selPkg 和 selPhotographer 分别管理选中的套餐和摄影师,两者在预约弹窗中联合展示。套餐展示使用横向滚动的卡片列表:
Scroll() {
Row({ space: 10 }) {
ForEach(PHOTO_PACKAGES, (pkg: PhotoPackage) => {
Column({ space: 8 }) {
Column().width(150).height(96).backgroundColor(pkg.color)
Column({ space: 4 }) {
Row({ space: 6 }) {
Text(pkg.name)
if (pkg.hot) {
Text('HOT').backgroundColor('#C8102E')
}
}
Text(pkg.scene)
Text(pkg.duration + ' · ' + pkg.photos).maxLines(1)
Row() {
Text('¥' + pkg.price)
Text('¥' + pkg.originPrice).decoration({ type: TextDecorationType.LineThrough })
Row().layoutWeight(1)
Text('预约')
.backgroundColor('#C8102E')
.onClick(() => {
this.selPkg = pkg
this.showBook = true
})
}
}
.width(150)
}
}, (pkg: PhotoPackage) => pkg.id.toString())
}
}
.scrollable(ScrollDirection.Horizontal)
摄影师列表使用纵向排列的卡片,每张卡片包含圆形头像、姓名、从业年限标签、风格描述、评分、作品数量和"选 TA"按钮。预约弹窗综合展示了套餐信息、摄影师信息和拍摄日期选择,日期卡片还展示了剩余可约数量(d.left),当 left === 0 时以灰色禁用样式展示。
订单 Tab(OrderTab)
订单 Tab 展示了租赁和写真订单的历史记录,支持按状态筛选和多种操作。
@Component
export struct OrderTab {
@State filterIdx: number = 0
@State showDetail: boolean = false
@State showRefund: boolean = false
@State showRenew: boolean = false
@State selOrder: OrderRecord = ORDER_LIST[0]
订单卡片的操作按钮根据状态动态展示:
Row({ space: 8 }) {
Row().layoutWeight(1)
Text('订单详情')
.border({ width: 1, color: '#E3D5C2' })
.onClick(() => { this.selOrder = o; this.showDetail = true })
if (o.status === '租赁中') {
Text('续租')
.border({ width: 1, color: '#E8B34B' })
.onClick(() => { this.selOrder = o; this.showRenew = true })
}
if (o.status === '待取片' || o.status === '租赁中') {
Text('申请退订')
.backgroundColor('#C8102E')
.onClick(() => { this.selOrder = o; this.showRefund = true })
}
}
续租弹窗提供了三种续租方案(+1 天、+3 天、+7 天),价格通过条件表达式动态计算:
ForEach(['+1 天', '+3 天', '+7 天'], (d: string) => {
Text(d + ' ¥' + (this.selOrder.amount * (d === '+1 天' ? 0.4 : (d === '+3 天' ? 1 : 2))).toFixed(0))
})
退订弹窗展示了退订规则(24 小时前全免、24 小时内收 30% 违约金、已使用不可退),让用户在确认前充分了解退订政策。
我的 Tab(MineTab)
我的 Tab 是个人中心,集成了会员信息、积分明细、优惠券、收货地址 CRUD 和会员特权。
@Component
export struct MineTab {
@State showCoupon: boolean = false
@State showPoint: boolean = false
@State showAddr: boolean = false
@State showEditAddr: boolean = false
@State showDelAddr: boolean = false
@State showVip: boolean = false
@State inputName: string = ''
@State inputPhone: string = ''
@State inputDetail: string = ''
@State editIdx: number = -1
@State delIdx: number = -1
@State addrList: AddrMeta[] = [
{ id: 1, name: '沈知意', phone: '138****6621', region: '四川 成都', detail: '锦江区青羊坊 12 号院 3 栋 502', tag: '默认' },
{ id: 2, name: '沈知意', phone: '138****6621', region: '四川 成都', detail: '高新区天府三街新希望大厦 B 座 21F', tag: '公司' }
]
addrList 是一个可变状态数组,支持完整的增删改查操作。三个方法 saveAddr()、updateAddr() 和 removeAddr() 实现了地址的 CRUD:
saveAddr() {
const r: AddrMeta[] = []
for (let i = 0; i < this.addrList.length; i++) {
r.push(this.addrList[i])
}
r.push({
id: this.addrList.length + 1,
name: this.inputName === '' ? '沈知意' : this.inputName,
phone: this.inputPhone === '' ? '138****6621' : this.inputPhone,
region: '四川 成都',
detail: this.inputDetail === '' ? '锦江区春熙路 88 号' : this.inputDetail,
tag: '家'
})
this.addrList = r
this.showAddr = false
}
updateAddr() {
const r: AddrMeta[] = []
for (let i = 0; i < this.addrList.length; i++) {
if (i === this.editIdx) {
r.push({
id: this.addrList[i].id,
name: this.inputName === '' ? this.addrList[i].name : this.inputName,
phone: this.inputPhone === '' ? this.addrList[i].phone : this.inputPhone,
region: this.addrList[i].region,
detail: this.inputDetail === '' ? this.addrList[i].detail : this.inputDetail,
tag: this.addrList[i].tag
})
} else {
r.push(this.addrList[i])
}
}
this.addrList = r
this.showEditAddr = false
}
removeAddr() {
const r: AddrMeta[] = []
for (let i = 0; i < this.addrList.length; i++) {
if (i !== this.delIdx) {
r.push(this.addrList[i])
}
}
this.addrList = r
this.showDelAddr = false
}
三个方法都采用了不可变更新模式——创建新数组、遍历过滤或修改、整体赋值给 this.addrList。saveAddr 通过 push 在末尾追加新地址,updateAddr 通过 editIdx 匹配修改项,removeAddr 通过 delIdx 排除删除项。空输入时使用默认值或保留原值的回退策略,提升了用户体验。
会员特权弹窗使用朱红到鎏金的渐变背景,与主入口头部保持视觉一致性:
if (this.showVip) {
modalOverlay(() => { this.showVip = false })
Column({ space: 12 }) {
Text('金穗会员特权').fontColor('#FFFFFF')
Column({ space: 8 }) {
Text('· 全场租金 95 折').fontColor('#FFE3C8')
Text('· 生日当月免 1 次押金').fontColor('#FFE3C8')
Text('· 新品优先租(提前 24h)').fontColor('#FFE3C8')
Text('· 写真跟拍 9 折').fontColor('#FFE3C8')
}
.backgroundColor('rgba(142,14,32,0.5)')
Text('知道了').backgroundColor('#F2D088')
}
.linearGradient({ angle: 135, colors: [['#C8102E', 0.0], ['#E8B34B', 1.0]] })
}
主入口页面分析
@Entry
@Component
struct HanfuPage {
@State activeTab: number = 0
@State showSearch: boolean = false
@State showMsg: boolean = false
@State searchKey: string = ''
@State unRead: number = 2
build() {
Column() {
Column({ space: 0 }) {
Row({ space: 10 }) {
Text('青羊坊')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Row({ space: 6 }) {
Text('🔍').fontSize(13)
TextInput({ placeholder: '马面裙 / 绒花 / 唐制写真' })
.fontSize(12)
.height(34)
.layoutWeight(1)
.backgroundColor('#FBE9EA')
.borderRadius(17)
.onChange((v: string) => { this.searchKey = v })
}
.layoutWeight(1)
.backgroundColor('#FFFFFF')
.borderRadius(17)
Stack({ alignContent: Alignment.TopEnd }) {
Text('✉').fontColor('#FFFFFF')
if (this.unRead > 0) {
Column()
.width(8)
.height(8)
.borderRadius(4)
.backgroundColor('#E8B34B')
}
}
.onClick(() => { this.showMsg = true })
}
.padding({ left: 14, right: 14, top: 10, bottom: 10 })
}
.linearGradient({ angle: 135, colors: [['#C8102E', 0.0], ['#8E0E20', 1.0]] })
if (this.activeTab === 0) {
NewTab()
} else if (this.activeTab === 1) {
RentTab()
} else if (this.activeTab === 2) {
PinTab()
} else if (this.activeTab === 3) {
PhotoTab()
} else if (this.activeTab === 4) {
OrderTab()
} else {
MineTab()
}
Row({ space: 0 }) {
ForEach(MAIN_TABS, (tab: TabMeta, idx: number) => {
Column({ space: 3 }) {
Text(tab.icon)
.fontColor(this.activeTab === idx ? '#C8102E' : '#B08A5A')
Text(tab.name)
.fontColor(this.activeTab === idx ? '#C8102E' : '#B08A5A')
if (this.activeTab === idx) {
Column()
.width(16).height(3).borderRadius(2)
.backgroundColor('#C8102E')
} else {
Column()
.width(16).height(3).borderRadius(2)
.backgroundColor('rgba(0,0,0,0)')
}
}
.layoutWeight(1)
.onClick(() => { this.activeTab = idx })
}, (tab: TabMeta, idx: number) => (tab.name + idx).toString())
}
.backgroundColor('#FFFFFF')
if (this.showSearch) { /* 搜索弹窗 */ }
if (this.showMsg) {
modalOverlay(() => { this.showMsg = false; this.unRead = 0 })
Column({ space: 12 }) {
Row() {
Text('消息中心')
Row().layoutWeight(1)
Text('全部已读').onClick(() => { this.unRead = 0 })
}
ForEach(MSG_LIST, (m: MsgMeta) => {
Row({ space: 10 }) {
Column({ space: 2 }) {
Text(m.title)
Text(m.content).maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
}
.layoutWeight(1)
Column({ space: 4 }) {
Text(m.time)
if (!m.read) {
Column().width(7).height(7).backgroundColor('#C8102E')
}
}
}
}, (m: MsgMeta) => m.id.toString())
}
.backgroundColor('#FFF9F0').borderRadius(16).width('92%')
}
}
.backgroundColor('#FFF7EC')
}
}
主入口 HanfuPage 的 build() 方法结构与游戏充值中心类似,但有几个关键差异。第一,头部区域直接内嵌了 TextInput 搜索框(而非点击弹出搜索弹窗),用户可以直接在头部输入关键词。第二,消息铃铛使用 unRead 数字状态管理未读数量,点击消息弹窗后自动清零。第三,底部导航栏的选中指示器使用条件渲染(if-else)而非透明色覆盖,选中 Tab 显示朱红色指示条,未选中则完全不渲染指示条。第四,ForEach 的 key 生成器使用 (tab.name + idx).toString(),确保即使两个 Tab 名称相同也能保持唯一 key。
消息弹窗中的未读红点根据 m.read 布尔值条件渲染——未读消息右侧显示一个 7x7 像素的朱红色圆点,已读消息则无此标记。"全部已读"按钮点击后将 unRead 设为 0,同时消息弹窗的遮罩回调也将 unRead 清零。
流程图
应用架构流程图
弹框交互流程图
技术对比表格
| 技术维度 | 今日上新 | 服饰租赁 | 簪花铺子 | 写真预约 | 订单 | 我的 |
|---|---|---|---|---|---|---|
| 核心交互 | 朝代筛选+租赁确认 | 走势图+试穿预约 | 分类筛选+购物车 | 套餐+摄影师+预约 | 筛选+续租+退订 | 地址CRUD+特权 |
| 状态变量数 | 8 | 6 | 5 | 6 | 5 | 12 |
| 弹窗数 | 3 (租赁/尺码/提醒) | 2 (试穿/详情) | 2 (购物车/养护) | 3 (预约/客片/档期) | 3 (详情/退订/续租) | 6 (券/积分/地址/编辑/删除/VIP) |
| 弹窗实现方式 | 内联在 build() 中 | 内联在 build() 中 | 内联在 build() 中 | 内联在 build() 中 | 内联在 build() 中 | 内联在 build() 中 |
| 二级弹窗 | 有 (尺码表嵌套租赁) | 无 | 无 | 无 | 无 | 有 (编辑/删除嵌套地址) |
| 数据更新模式 | 纯读取 | 纯读取 | 纯读取 | 纯读取 | 纯读取 | 不可变数组CRUD |
| 特色组件 | 朝代筛选横滑 | 租金柱状图 | Flex双列网格 | 套餐横滑+摄影师列表 | 续租价格计算 | 地址CRUD全流程 |
| 全局函数调用 | 无 | rentBarH/trendBarColor | 无 | 无 | 无 | 无 |
| export 修饰 | 是 | 是 | 是 | 是 | 是 | 是 |
| 遮罩层来源 | 全局 modalOverlay | 全局 modalOverlay | 全局 modalOverlay | 全局 modalOverlay | 全局 modalOverlay | 全局 modalOverlay |
安装DevEco Studio程序

选择目标安装目录:

设置环境变量,但是需要重启一下:

新建一个空白模板:

设置API为24的模板项目:
初始化项目,自动下载相关依赖:

完整代码:
// 主题:朱红 #C8102E × 鎏金 #E8B34B,底色 #FFF7EC,国风新中式
// 6 个底部 Tab:今日上新 / 服饰租赁 / 簪花铺子 / 写真预约 / 订单 / 我的
interface DynastyChip {
name: string
count: number
}
interface HanfuItem {
id: number
title: string
dynasty: string
price: number
originPrice: number
rentDay: number
score: number
rentCount: number
tags: string[]
hot: boolean
color: string
}
interface RentTrend {
day: string
price: number
}
interface SizeRow {
size: string
height: string
bust: string
stock: number
}
interface AccessoryItem {
id: number
name: string
material: string
price: number
stock: number
grade: string
color: string
}
interface PhotoPackage {
id: number
name: string
scene: string
price: number
originPrice: number
duration: string
photos: string
retouch: string
hot: boolean
color: string
}
interface Photographer {
id: number
name: string
years: number
style: string
price: number
works: number
rating: number
}
interface CalendarDay {
idx: number
label: string
date: string
left: number
}
interface OrderRecord {
id: string
item: string
cover: string
date: string
days: number
status: string
statusColor: string
amount: number
deposit: number
}
interface CouponMeta {
id: number
title: string
amount: number
condition: string
expire: string
color: string
}
interface PointLog {
id: number
title: string
delta: number
date: string
}
interface AddrMeta {
id: number
name: string
phone: string
region: string
detail: string
tag: string
}
interface TabMeta {
name: string
icon: string
}
interface MsgMeta {
id: number
title: string
content: string
time: string
read: boolean
}
const DYNASTY_CHIPS: DynastyChip[] = [
{ name: '全部朝代', count: 286 },
{ name: '唐制', count: 92 },
{ name: '宋制', count: 74 },
{ name: '明制', count: 68 },
{ name: '魏晋', count: 31 },
{ name: '清汉女', count: 21 }
]
const NEW_HANFU: HanfuItem[] = [
{ id: 1, title: '齐胸襦裙·烟霞染', dynasty: '唐制', price: 88, originPrice: 158, rentDay: 3, score: 4.9, rentCount: 2317, tags: ['新品首发', '含发饰', '飘逸'], hot: true, color: '#F5C8D0' },
{ id: 2, title: '马面裙·山河绣', dynasty: '明制', price: 68, originPrice: 128, rentDay: 3, score: 4.8, rentCount: 4521, tags: ['爆款返场', '织金'], hot: true, color: '#D9B382' },
{ id: 3, title: '褙子·竹影青', dynasty: '宋制', price: 56, originPrice: 99, rentDay: 3, score: 4.7, rentCount: 1876, tags: ['清冷感', '含披帛'], hot: false, color: '#BFD8C2' },
{ id: 4, title: '大袖衫·流云白', dynasty: '魏晋', price: 96, originPrice: 176, rentDay: 3, score: 4.9, rentCount: 1203, tags: ['仙气', '写真首选'], hot: true, color: '#EDE7DA' },
{ id: 5, title: '袄裙·海棠红', dynasty: '明制', price: 72, originPrice: 138, rentDay: 3, score: 4.6, rentCount: 986, tags: ['新年款', '拜年服'], hot: false, color: '#E39A94' },
{ id: 6, title: '坦领·葡萄紫', dynasty: '唐制', price: 62, originPrice: 118, rentDay: 3, score: 4.7, rentCount: 1532, tags: ['显白', '含义髻'], hot: false, color: '#C7A6D9' }
]
const RENT_LIST: HanfuItem[] = [
{ id: 101, title: '全套唐制·花朝宴', dynasty: '唐制', price: 128, originPrice: 228, rentDay: 3, score: 4.9, rentCount: 3216, tags: ['全套含鞋', '送造型'], hot: true, color: '#F2B8C6' },
{ id: 102, title: '全套宋制·雨过天青', dynasty: '宋制', price: 108, originPrice: 198, rentDay: 3, score: 4.8, rentCount: 2054, tags: ['全套含冠', '通勤可穿'], hot: false, color: '#AEC8C2' },
{ id: 103, title: '全套明制·鸾凤和鸣', dynasty: '明制', price: 158, originPrice: 288, rentDay: 3, score: 5.0, rentCount: 4102, tags: ['婚礼级', '含凤冠'], hot: true, color: '#D9826C' },
{ id: 104, title: '全套魏晋·谪仙引', dynasty: '魏晋', price: 136, originPrice: 246, rentDay: 3, score: 4.8, rentCount: 1677, tags: ['大袖', '武侠感'], hot: false, color: '#C9CBB8' },
{ id: 105, title: '男士圆领袍·凌云志', dynasty: '唐制', price: 78, originPrice: 148, rentDay: 3, score: 4.7, rentCount: 1390, tags: ['情侣款', '含幞头'], hot: true, color: '#8FA3C4' },
{ id: 106, title: '亲子装·小锦鲤', dynasty: '明制', price: 92, originPrice: 168, rentDay: 3, score: 4.9, rentCount: 2208, tags: ['亲子', '全家福'], hot: false, color: '#F4B26A' }
]
const RENT_TREND: RentTrend[] = [
{ day: '周一', price: 96 },
{ day: '周二', price: 78 },
{ day: '周三', price: 72 },
{ day: '周四', price: 88 },
{ day: '周五', price: 138 },
{ day: '周六', price: 168 },
{ day: '周日', price: 152 }
]
const SIZE_ROWS: SizeRow[] = [
{ size: 'XS', height: '150-158cm', bust: '80-84cm', stock: 12 },
{ size: 'S', height: '158-163cm', bust: '84-88cm', stock: 26 },
{ size: 'M', height: '163-168cm', bust: '88-92cm', stock: 31 },
{ size: 'L', height: '168-173cm', bust: '92-96cm', stock: 18 },
{ size: 'XL', height: '173-178cm', bust: '96-100cm', stock: 7 }
]
const ACCESSORY_ITEMS: AccessoryItem[] = [
{ id: 1, name: '鎏金发簪·并蒂莲', material: '铜镀金·珍珠', price: 39, stock: 126, grade: '匠心工艺', color: '#E8B34B' },
{ id: 2, name: '绒花发钗·石榴红', material: '非遗绒花', price: 45, stock: 88, grade: '非遗联名', color: '#D94F5C' },
{ id: 3, name: '缠花步摇·青竹雪', material: '手作缠花', price: 52, stock: 64, grade: '手作限定', color: '#9CBF9E' },
{ id: 4, name: '璎珞项圈·流苏月', material: '合金·仿玉', price: 36, stock: 152, grade: '人气爆款', color: '#C7A6D9' },
{ id: 5, name: '禁步玉佩·云纹佩', material: '岫玉·流苏', price: 58, stock: 43, grade: '孤品', color: '#A8C8D8' },
{ id: 6, name: '折扇·墨梅图', material: '竹骨·绢面', price: 29, stock: 210, grade: '写真道具', color: '#E5DCC8' },
{ id: 7, name: '油纸伞·烟雨蓝', material: '棉纸·竹柄', price: 48, stock: 97, grade: '道具人气', color: '#8FA8C8' },
{ id: 8, name: '披帛·云雾绡', material: '真丝绡', price: 32, stock: 178, grade: '租赁随赠', color: '#F2C9D4' }
]
const ACCESSORY_CATS: string[] = ['全部', '发饰', '璎珞', '道具', '团扇', '鞋履']
const PHOTO_PACKAGES: PhotoPackage[] = [
{ id: 1, name: '单人国风写真', scene: '园林实景 3 选 1', price: 399, originPrice: 699, duration: '120 分钟', photos: '精修 9 张 + 底片全送', retouch: '轻奢精修', hot: true, color: '#F5C8D0' },
{ id: 2, name: '闺蜜双人写真', scene: '花墙 + 茶室双景', price: 599, originPrice: 1099, duration: '150 分钟', photos: '精修 18 张', retouch: '杂志级修图', hot: true, color: '#E8B34B' },
{ id: 3, name: '情侣汉服婚照', scene: '中式庭院烛光', price: 1299, originPrice: 2399, duration: '240 分钟', photos: '精修 30 张 + 相册', retouch: '电影级调色', hot: false, color: '#D9826C' },
{ id: 4, name: '亲子全家福', scene: '古风街景全家福', price: 699, originPrice: 1299, duration: '180 分钟', photos: '精修 20 张 + 摆台', retouch: '温馨影调', hot: false, color: '#F4B26A' }
]
const PHOTOGRAPHERS: Photographer[] = [
{ id: 1, name: '阿棠', years: 8, style: '电影感·情绪光影', price: 200, works: 1860, rating: 4.9 },
{ id: 2, name: '鹿鸣', years: 6, style: '清冷宋制·留白美学', price: 160, works: 1240, rating: 4.8 },
{ id: 3, name: '陈拾', years: 5, style: '浓颜唐风·华丽构图', price: 180, works: 980, rating: 4.7 },
{ id: 4, name: '小满', years: 4, style: '日系汉服·自然光', price: 120, works: 760, rating: 4.8 }
]
const CALENDAR_DAYS: CalendarDay[] = [
{ idx: 0, label: '今天', date: '8/24', left: 6 },
{ idx: 1, label: '明天', date: '8/25', left: 12 },
{ idx: 2, label: '周四', date: '8/26', left: 9 },
{ idx: 3, label: '周五', date: '8/27', left: 3 },
{ idx: 4, label: '周六', date: '8/28', left: 0 },
{ idx: 5, label: '周日', date: '8/29', left: 2 },
{ idx: 6, label: '周一', date: '8/30', left: 14 }
]
const ORDER_LIST: OrderRecord[] = [
{ id: 'HF2026082301', item: '全套唐制·花朝宴', cover: '#F2B8C6', date: '08/23 - 08/25', days: 3, status: '租赁中', statusColor: '#C8102E', amount: 128, deposit: 200 },
{ id: 'HF2026081802', item: '马面裙·山河绣', cover: '#D9B382', date: '08/18 - 08/20', days: 3, status: '已归还', statusColor: '#5C7B8A', amount: 68, deposit: 100 },
{ id: 'HF2026081003', item: '写真·单人国风', cover: '#F5C8D0', date: '08/10 拍摄', days: 1, status: '待取片', statusColor: '#E8B34B', amount: 399, deposit: 0 },
{ id: 'HF2026080204', item: '簪花发钗·石榴红', cover: '#D94F5C', date: '08/02 购买', days: 1, status: '已完成', statusColor: '#5C7B8A', amount: 45, deposit: 0 },
{ id: 'HF2026072805', item: '全套魏晋·谪仙引', cover: '#C9CBB8', date: '07/28 - 07/30', days: 3, status: '退款成功', statusColor: '#8FA3C4', amount: 136, deposit: 200 }
]
const ORDER_FILTERS: string[] = ['全部', '租赁中', '待取片', '已归还', '退款']
const COUPON_LIST: CouponMeta[] = [
{ id: 1, title: '汉服租赁立减券', amount: 30, condition: '满 128 可用', expire: '09/30 前有效', color: '#C8102E' },
{ id: 2, title: '写真定金膨胀券', amount: 100, condition: '满 399 可用', expire: '10/15 前有效', color: '#E8B34B' },
{ id: 3, title: '簪花铺子满赠券', amount: 10, condition: '满 39 可用', expire: '08/31 前有效', color: '#D94F5C' },
{ id: 4, title: '新人首单 5 折券', amount: 20, condition: '无门槛', expire: '09/15 前有效', color: '#8E0E20' }
]
const POINT_LOGS: PointLog[] = [
{ id: 1, title: '归还加分·衣物完好', delta: 20, date: '08/25 10:32' },
{ id: 2, title: '签到得积分', delta: 5, date: '08/24 09:00' },
{ id: 3, title: '写真订单返积分', delta: 120, date: '08/10 18:20' },
{ id: 4, title: '积分兑换发簪', delta: -200, date: '08/02 12:41' },
{ id: 5, title: '评价晒图奖励', delta: 15, date: '08/01 21:05' }
]
const MAIN_TABS: TabMeta[] = [
{ name: '今日上新', icon: '上新' },
{ name: '服饰租赁', icon: '租赁' },
{ name: '簪花铺子', icon: '簪花' },
{ name: '写真预约', icon: '写真' },
{ name: '订单', icon: '订单' },
{ name: '我的', icon: '我的' }
]
const MSG_LIST: MsgMeta[] = [
{ id: 1, title: '租赁提醒', content: '您的「花朝宴」还有 1 天到期,顺丰上门取件已预约', time: '10:12', read: false },
{ id: 2, title: '写真取片', content: '您的 9 张精修已上传云端,有效期 30 天', time: '昨天', read: false },
{ id: 3, title: '优惠券到账', content: '中秋档租赁 8 折券已发放,先囤后约', time: '昨天', read: true },
{ id: 4, title: '上新预告', content: '「宋制百迭裙·暮山紫」周五 10 点首发', time: '08/22', read: true },
{ id: 5, title: '积分变动', content: '归还加 20 分,当前 1240 分', time: '08/20', read: true },
{ id: 6, title: '会员升级', content: '再租 1 单即升级「金穗会员」,享免押金', time: '08/18', read: true }
]
function rentBarH(p: number): number {
if (p >= 160) {
return 84
}
if (p >= 130) {
return 66
}
if (p >= 90) {
return 46
}
return 28
}
function trendBarColor(p: number): string {
if (p >= 130) {
return '#C8102E'
}
return '#E8B34B'
}
@Builder
function modalOverlay(onClose: () => void) {
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(30,10,12,0.55)')
.onClick(() => {
onClose()
})
}
// ==================== Tab 1 今日上新 ====================
@Component
export struct NewTab {
@State dynastyIdx: number = 0
@State showRent: boolean = false
@State showSize: boolean = false
@State showFav: boolean = false
@State rentNum: number = 3
@State favCount: number = 328
@State isFav: boolean = false
@State selHanfu: HanfuItem = NEW_HANFU[0]
build() {
Scroll() {
Column({ space: 12 }) {
Row() {
Column({ space: 2 }) {
Text('国风上新 · 每周五 10 点')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#8E0E20')
Text('本周上新 12 套 · 免押金租 3 日起')
.fontSize(11)
.fontColor('#A8896A')
}
.alignItems(HorizontalAlign.Start)
Row()
.layoutWeight(1)
Text('上新日历')
.fontSize(12)
.fontColor('#C8102E')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('#FBE9EA')
.borderRadius(14)
.onClick(() => {
this.showFav = true
})
}
.width('100%')
Scroll() {
Row({ space: 8 }) {
ForEach(DYNASTY_CHIPS, (chip: DynastyChip) => {
Text(chip.name + ' ' + chip.count)
.fontSize(12)
.fontColor(this.dynastyIdx === DYNASTY_CHIPS.indexOf(chip) ? '#FFFFFF' : '#8E0E20')
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.backgroundColor(this.dynastyIdx === DYNASTY_CHIPS.indexOf(chip) ? '#C8102E' : '#FDF1E3')
.borderRadius(16)
.onClick(() => {
this.dynastyIdx = DYNASTY_CHIPS.indexOf(chip)
})
}, (chip: DynastyChip) => chip.name)
}
}
.scrollable(ScrollDirection.Horizontal)
.width('100%')
Row({ space: 10 }) {
Column({ space: 6 }) {
Text('限时 5 折起')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('唐制专场')
.fontSize(11)
.fontColor('#FFE3C8')
}
.alignItems(HorizontalAlign.Start)
.padding(12)
.borderRadius(12)
.linearGradient({
angle: 135,
colors: [['#C8102E', 0.0], ['#8E0E20', 1.0]]
})
.layoutWeight(1)
.height(84)
.onClick(() => {
this.dynastyIdx = 1
})
Column({ space: 6 }) {
Text('新人 0 元')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#8E0E20')
Text('簪花随租送')
.fontSize(11)
.fontColor('#A8896A')
}
.alignItems(HorizontalAlign.Start)
.padding(12)
.borderRadius(12)
.backgroundColor('#FBF0DB')
.border({ width: 1, color: '#E8B34B' })
.layoutWeight(1)
.height(84)
.onClick(() => {
this.showFav = true
})
}
.width('100%')
Row() {
Text('本周爆款榜')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Row()
.layoutWeight(1)
Text('按租赁次数')
.fontSize(11)
.fontColor('#A8896A')
}
.width('100%')
ForEach(NEW_HANFU, (item: HanfuItem) => {
Row({ space: 12 }) {
Column()
.width(96)
.height(120)
.borderRadius(10)
.backgroundColor(item.color)
Column({ space: 6 }) {
Row({ space: 6 }) {
Text(item.dynasty)
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#C8102E')
.borderRadius(4)
if (item.hot) {
Text('HOT')
.fontSize(10)
.fontColor('#8E0E20')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#FBF0DB')
.borderRadius(4)
}
}
Text(item.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row({ space: 6 }) {
ForEach(item.tags, (tag: string) => {
Text(tag)
.fontSize(10)
.fontColor('#B08A5A')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#FDF6EA')
.borderRadius(4)
}, (tag: string) => tag)
}
Row({ space: 8 }) {
Text('★ ' + item.score.toFixed(1))
.fontSize(11)
.fontColor('#E8B34B')
Text(item.rentCount + ' 次租')
.fontSize(11)
.fontColor('#A8896A')
}
Row() {
Column({ space: 1 }) {
Row({ space: 3 }) {
Text('¥' + item.price)
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#C8102E')
Text('/' + item.rentDay + '天')
.fontSize(10)
.fontColor('#A8896A')
}
Text('¥' + item.originPrice)
.fontSize(10)
.fontColor('#CBB9A4')
.decoration({ type: TextDecorationType.LineThrough })
}
.alignItems(HorizontalAlign.Start)
Row()
.layoutWeight(1)
Text('立即租')
.fontSize(12)
.fontColor('#FFFFFF')
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.borderRadius(15)
.linearGradient({
angle: 90,
colors: [['#C8102E', 0.0], ['#E8556D', 1.0]]
})
.onClick(() => {
this.selHanfu = item
this.showRent = true
})
}
.width('100%')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding(10)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.shadow({
radius: 8,
color: 'rgba(200,16,46,0.06)',
offsetX: 0,
offsetY: 2
})
}, (item: HanfuItem) => item.id.toString())
if (this.showRent) {
modalOverlay(() => {
this.showRent = false
})
Column({ space: 14 }) {
Row() {
Text('租赁确认')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Row()
.layoutWeight(1)
Text('×')
.fontSize(18)
.fontColor('#A8896A')
.onClick(() => {
this.showRent = false
})
}
.width('100%')
Row({ space: 10 }) {
Column()
.width(64)
.height(80)
.borderRadius(8)
.backgroundColor(this.selHanfu.color)
Column({ space: 4 }) {
Text(this.selHanfu.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Text(this.selHanfu.dynasty + ' · 含全套配饰')
.fontSize(11)
.fontColor('#A8896A')
Text('押金 ¥200(归还全额退)')
.fontSize(11)
.fontColor('#B08A5A')
}
.alignItems(HorizontalAlign.Start)
}
.width('100%')
Row() {
Text('租赁天数')
.fontSize(13)
.fontColor('#5C4A3E')
Row()
.layoutWeight(1)
Row({ space: 0 }) {
Text('−')
.fontSize(16)
.fontColor(this.rentNum <= 1 ? '#D8C9B8' : '#C8102E')
.padding(8)
.onClick(() => {
if (this.rentNum > 1) {
this.rentNum--
}
})
Text(this.rentNum + ' 天')
.fontSize(13)
.fontColor('#3A2A22')
.padding({ left: 10, right: 10 })
Text('+')
.fontSize(16)
.fontColor('#C8102E')
.padding(8)
.onClick(() => {
if (this.rentNum < 15) {
this.rentNum++
}
})
}
.borderRadius(8)
.backgroundColor('#FDF6EA')
}
.width('100%')
Row() {
Text('查看尺码表')
.fontSize(12)
.fontColor('#C8102E')
.onClick(() => {
this.showSize = true
})
Row()
.layoutWeight(1)
Text('顺丰双向包邮 · 破损险已含')
.fontSize(11)
.fontColor('#A8896A')
}
.width('100%')
Row() {
Column({ space: 1 }) {
Text('合计')
.fontSize(11)
.fontColor('#A8896A')
Text('¥' + (this.selHanfu.price * Math.ceil(this.rentNum / 3)))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#C8102E')
}
.alignItems(HorizontalAlign.Start)
Row()
.layoutWeight(1)
Text('确认租赁')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 22, right: 22, top: 9, bottom: 9 })
.borderRadius(18)
.linearGradient({
angle: 90,
colors: [['#C8102E', 0.0], ['#E8556D', 1.0]]
})
.onClick(() => {
this.showRent = false
})
}
.width('100%')
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.constraintSize({ maxHeight: '80%' })
.width('92%')
}
if (this.showSize) {
modalOverlay(() => {
this.showSize = false
})
Column({ space: 12 }) {
Row() {
Text('尺码表(女款)')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Row()
.layoutWeight(1)
Text('×')
.fontSize(18)
.fontColor('#A8896A')
.onClick(() => {
this.showSize = false
})
}
.width('100%')
ForEach(SIZE_ROWS, (row: SizeRow) => {
Row() {
Text(row.size)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#C8102E')
.width(48)
Text(row.height)
.fontSize(12)
.fontColor('#5C4A3E')
.layoutWeight(1)
Text(row.bust)
.fontSize(12)
.fontColor('#5C4A3E')
.layoutWeight(1)
Text(row.stock > 0 ? '可租 ' + row.stock + ' 件' : '已约满')
.fontSize(11)
.fontColor(row.stock > 0 ? '#B08A5A' : '#D8C9B8')
}
.width('100%')
.padding({ top: 8, bottom: 8 })
.borderRadius(8)
.backgroundColor(row.size === 'M' ? '#FDF1E3' : '#FFFFFF')
}, (row: SizeRow) => row.size)
Text('腰围超标可加钱定制裙长,联系客服备注即可')
.fontSize(11)
.fontColor('#A8896A')
.width('100%')
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.constraintSize({ maxHeight: '70%' })
.width('88%')
}
if (this.showFav) {
modalOverlay(() => {
this.showFav = false
})
Column({ space: 12 }) {
Text('上新提醒已开启')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Text('周五 10 点首发提醒会推送到消息中心\n首发前 30 分钟下单再减 ¥10')
.fontSize(12)
.fontColor('#A8896A')
.textAlign(TextAlign.Center)
Row({ space: 10 }) {
Text('知道了')
.fontSize(13)
.fontColor('#8E0E20')
.padding({ left: 22, right: 22, top: 9, bottom: 9 })
.borderRadius(18)
.backgroundColor('#FDF1E3')
.onClick(() => {
this.showFav = false
})
}
}
.padding(22)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.alignItems(HorizontalAlign.Center)
.width('76%')
}
}
.padding(12)
}
.backgroundColor('#FFF7EC')
.layoutWeight(1)
}
}
// ==================== Tab 2 服饰租赁 ====================
@Component
export struct RentTab {
@State filterIdx: number = 0
@State showTry: boolean = false
@State showDetail: boolean = false
@State selItem: HanfuItem = RENT_LIST[0]
@State tryDate: string = '今天'
@State trySlot: string = '14:00'
build() {
Scroll() {
Column({ space: 12 }) {
Column({ space: 10 }) {
Row() {
Text('本周租金行情(元/3天)')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Row()
.layoutWeight(1)
Text('周末溢价')
.fontSize(10)
.fontColor('#C8102E')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#FBE9EA')
.borderRadius(4)
}
.width('100%')
Row({ space: 10 }) {
ForEach(RENT_TREND, (t: RentTrend) => {
Column({ space: 4 }) {
Text('¥' + t.price)
.fontSize(9)
.fontColor(t.price >= 130 ? '#C8102E' : '#A8896A')
Column()
.width(18)
.height(rentBarH(t.price))
.borderRadius({ topLeft: 4, topRight: 4 })
.linearGradient({
angle: 180,
colors: trendBarColor(t.price) === '#C8102E'
? [['#E8556D', 0.0], ['#C8102E', 1.0]]
: [['#F2D088', 0.0], ['#E8B34B', 1.0]]
})
Text(t.day)
.fontSize(10)
.fontColor('#A8896A')
}
.alignItems(HorizontalAlign.Center)
}, (t: RentTrend) => t.day)
}
.width('100%')
.padding({ top: 6, bottom: 4 })
}
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
Row({ space: 8 }) {
ForEach(DYNASTY_CHIPS, (chip: DynastyChip) => {
Text(chip.name)
.fontSize(12)
.fontColor(this.filterIdx === DYNASTY_CHIPS.indexOf(chip) ? '#8E0E20' : '#A8896A')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(this.filterIdx === DYNASTY_CHIPS.indexOf(chip) ? '#FDF1E3' : '#FFFFFF')
.borderRadius(14)
.border({ width: 1, color: this.filterIdx === DYNASTY_CHIPS.indexOf(chip) ? '#C8102E' : '#EFE3D0' })
.onClick(() => {
this.filterIdx = DYNASTY_CHIPS.indexOf(chip)
})
}, (chip: DynastyChip) => chip.name)
}
.width('100%')
Row() {
Text('到店试穿免费 · 免费改裙长')
.fontSize(11)
.fontColor('#B08A5A')
Row()
.layoutWeight(1)
Text('预约试穿')
.fontSize(12)
.fontColor('#FFFFFF')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor('#C8102E')
.onClick(() => {
this.showTry = true
})
}
.width('100%')
.padding(10)
.borderRadius(10)
.backgroundColor('#FBF0DB')
ForEach(RENT_LIST, (item: HanfuItem) => {
Row({ space: 12 }) {
Column()
.width(110)
.height(140)
.borderRadius(10)
.backgroundColor(item.color)
Column({ space: 6 }) {
Row({ space: 6 }) {
Text(item.dynasty + '全套')
.fontSize(10)
.fontColor('#8E0E20')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#FBF0DB')
.borderRadius(4)
Text('★ ' + item.score.toFixed(1))
.fontSize(11)
.fontColor('#E8B34B')
}
Text(item.title)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Row({ space: 6 }) {
ForEach(item.tags, (tag: string) => {
Text(tag)
.fontSize(10)
.fontColor('#C8102E')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#FBE9EA')
.borderRadius(4)
}, (tag: string) => tag)
}
Text('押金 ¥200 · 归还全退 · 含清洗')
.fontSize(11)
.fontColor('#A8896A')
Row() {
Row({ space: 3 }) {
Text('¥' + item.price)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#C8102E')
Text('/3天')
.fontSize(10)
.fontColor('#A8896A')
}
Row()
.layoutWeight(1)
Text('查看详情')
.fontSize(12)
.fontColor('#8E0E20')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(13)
.border({ width: 1, color: '#C8102E' })
.onClick(() => {
this.selItem = item
this.showDetail = true
})
}
.width('100%')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding(10)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.shadow({ radius: 6, color: 'rgba(58,42,34,0.05)', offsetY: 2 })
}, (item: HanfuItem) => item.id.toString())
if (this.showTry) {
modalOverlay(() => {
this.showTry = false
})
Column({ space: 14 }) {
Text('预约到店试穿')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
.width('100%')
Column({ space: 8 }) {
Text('选择日期')
.fontSize(12)
.fontColor('#A8896A')
.width('100%')
Row({ space: 8 }) {
ForEach(CALENDAR_DAYS, (d: CalendarDay) => {
Column({ space: 3 }) {
Text(d.label)
.fontSize(12)
.fontColor(this.tryDate === d.date ? '#FFFFFF' : '#5C4A3E')
Text(d.date)
.fontSize(10)
.fontColor(this.tryDate === d.date ? '#FFE3C8' : '#A8896A')
}
.padding({ left: 10, right: 10, top: 7, bottom: 7 })
.borderRadius(10)
.backgroundColor(this.tryDate === d.date ? '#C8102E' : '#FDF6EA')
.onClick(() => {
this.tryDate = d.date
})
}, (d: CalendarDay) => d.idx.toString())
}
}
.width('100%')
Column({ space: 8 }) {
Text('选择时段')
.fontSize(12)
.fontColor('#A8896A')
.width('100%')
Row({ space: 8 }) {
ForEach(['10:00', '14:00', '16:00', '19:00'], (slot: string) => {
Text(slot)
.fontSize(12)
.fontColor(this.trySlot === slot ? '#FFFFFF' : '#5C4A3E')
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.borderRadius(10)
.backgroundColor(this.trySlot === slot ? '#8E0E20' : '#FDF6EA')
.onClick(() => {
this.trySlot = slot
})
}, (slot: string) => slot)
}
}
.width('100%')
Text('地址:锦官城·青羊坊汉服馆 2 楼(地铁 4 号线 B 口)')
.fontSize(11)
.fontColor('#A8896A')
.width('100%')
Text('提交预约')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 0, right: 0, top: 10, bottom: 10 })
.borderRadius(20)
.linearGradient({
angle: 90,
colors: [['#C8102E', 0.0], ['#E8B34B', 1.0]]
})
.width('100%')
.textAlign(TextAlign.Center)
.onClick(() => {
this.showTry = false
})
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.constraintSize({ maxHeight: '80%' })
.width('92%')
}
if (this.showDetail) {
modalOverlay(() => {
this.showDetail = false
})
Column({ space: 12 }) {
Row() {
Text('套装详情')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Row()
.layoutWeight(1)
Text('×')
.fontSize(18)
.fontColor('#A8896A')
.onClick(() => {
this.showDetail = false
})
}
.width('100%')
Column()
.width('100%')
.height(120)
.borderRadius(10)
.backgroundColor(this.selItem.color)
Row({ space: 10 }) {
Column({ space: 2 }) {
Text('上衣裙装')
.fontSize(12)
.fontColor('#3A2A22')
Text('含')
.fontSize(11)
.fontColor('#C8102E')
}
.layoutWeight(1)
Column({ space: 2 }) {
Text('发冠发饰')
.fontSize(12)
.fontColor('#3A2A22')
Text('含')
.fontSize(11)
.fontColor('#C8102E')
}
.layoutWeight(1)
Column({ space: 2 }) {
Text('绣鞋')
.fontSize(12)
.fontColor('#3A2A22')
Text('含')
.fontSize(11)
.fontColor('#C8102E')
}
.layoutWeight(1)
Column({ space: 2 }) {
Text('妆造')
.fontSize(12)
.fontColor('#3A2A22')
Text('¥39 加购')
.fontSize(11)
.fontColor('#B08A5A')
}
.layoutWeight(1)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor('#FDF6EA')
Text('· 归还时无需清洗,污渍由专业机构处理\n· 熨烫后顺丰上门取件,全程可跟踪\n· 连续租 3 次以上自动升级金穗会员免押金')
.fontSize(11)
.fontColor('#A8896A')
.lineHeight(18)
.width('100%')
Row() {
Text('¥' + this.selItem.price)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#C8102E')
Text('/3天 · 已租 ' + this.selItem.rentCount + ' 次')
.fontSize(11)
.fontColor('#A8896A')
Row()
.layoutWeight(1)
Text('去下单')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#C8102E')
.onClick(() => {
this.showDetail = false
})
}
.width('100%')
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.constraintSize({ maxHeight: '80%' })
.width('92%')
}
}
.padding(12)
}
.backgroundColor('#FFF7EC')
.layoutWeight(1)
}
}
// ==================== Tab 3 簪花铺子 ====================
@Component
export struct PinTab {
@State catIdx: number = 0
@State showCart: boolean = false
@State showCare: boolean = false
@State buyNum: number = 1
@State selAcc: AccessoryItem = ACCESSORY_ITEMS[0]
build() {
Scroll() {
Column({ space: 12 }) {
Row({ space: 8 }) {
ForEach(ACCESSORY_CATS, (cat: string) => {
Text(cat)
.fontSize(12)
.fontColor(this.catIdx === ACCESSORY_CATS.indexOf(cat) ? '#FFFFFF' : '#8E0E20')
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.borderRadius(15)
.backgroundColor(this.catIdx === ACCESSORY_CATS.indexOf(cat) ? '#C8102E' : '#FDF1E3')
.onClick(() => {
this.catIdx = ACCESSORY_CATS.indexOf(cat)
})
}, (cat: string) => cat)
}
.width('100%')
Row({ space: 10 }) {
Column({ space: 4 }) {
Text('非遗绒花')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#8E0E20')
Text('匠人手作 限量 88 件')
.fontSize(10)
.fontColor('#B08A5A')
}
.alignItems(HorizontalAlign.Start)
.padding(12)
.borderRadius(12)
.backgroundColor('#FBF0DB')
.layoutWeight(1)
.height(72)
.onClick(() => {
this.showCare = true
})
Column({ space: 4 }) {
Text('簪花随租送')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('租汉服免费借 3 件')
.fontSize(10)
.fontColor('#FFE3C8')
}
.alignItems(HorizontalAlign.Start)
.padding(12)
.borderRadius(12)
.linearGradient({
angle: 135,
colors: [['#C8102E', 0.0], ['#E8B34B', 1.0]]
})
.layoutWeight(1)
.height(72)
.onClick(() => {
this.showCare = true
})
}
.width('100%')
Row() {
Text('掌柜推荐')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Row()
.layoutWeight(1)
Text('养护贴士')
.fontSize(11)
.fontColor('#C8102E')
.onClick(() => {
this.showCare = true
})
}
.width('100%')
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(ACCESSORY_ITEMS, (acc: AccessoryItem) => {
Column({ space: 8 }) {
Column()
.width('100%')
.height(110)
.borderRadius(10)
.backgroundColor(acc.color)
Column({ space: 4 }) {
Text(acc.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
.maxLines(1)
Text(acc.material)
.fontSize(10)
.fontColor('#A8896A')
Row() {
Text('¥' + acc.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#C8102E')
Row()
.layoutWeight(1)
Text('加入购物车')
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(10)
.backgroundColor('#8E0E20')
.onClick(() => {
this.selAcc = acc
this.showCart = true
})
}
.width('100%')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
}
.padding(10)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.width('48.5%')
.margin({ bottom: 10 })
}, (acc: AccessoryItem) => acc.id.toString())
}
.width('100%')
Text('· 现货 48 小时内发货 · 七天无理由退换 · 支持定制刻字')
.fontSize(10)
.fontColor('#CBB9A4')
.width('100%')
.textAlign(TextAlign.Center)
if (this.showCart) {
modalOverlay(() => {
this.showCart = false
})
Column({ space: 14 }) {
Row({ space: 10 }) {
Column()
.width(60)
.height(60)
.borderRadius(8)
.backgroundColor(this.selAcc.color)
Column({ space: 3 }) {
Text(this.selAcc.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Text(this.selAcc.material + ' · ' + this.selAcc.grade)
.fontSize(10)
.fontColor('#A8896A')
Text('剩余 ' + this.selAcc.stock + ' 件')
.fontSize(10)
.fontColor('#B08A5A')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Row() {
Text('购买数量')
.fontSize(13)
.fontColor('#5C4A3E')
Row()
.layoutWeight(1)
Row({ space: 0 }) {
Text('−')
.fontSize(15)
.fontColor(this.buyNum <= 1 ? '#D8C9B8' : '#C8102E')
.padding(7)
.onClick(() => {
if (this.buyNum > 1) {
this.buyNum--
}
})
Text(this.buyNum.toString())
.fontSize(13)
.fontColor('#3A2A22')
.padding({ left: 12, right: 12 })
Text('+')
.fontSize(15)
.fontColor(this.buyNum >= this.selAcc.stock ? '#D8C9B8' : '#C8102E')
.padding(7)
.onClick(() => {
if (this.buyNum < this.selAcc.stock) {
this.buyNum++
}
})
}
.borderRadius(8)
.backgroundColor('#FDF6EA')
}
.width('100%')
Row() {
Text('定制刻字(免费 8 字)')
.fontSize(12)
.fontColor('#5C4A3E')
Row()
.layoutWeight(1)
Text('「长安」')
.fontSize(11)
.fontColor('#B08A5A')
}
.width('100%')
Row() {
Text('小计 ¥' + (this.selAcc.price * this.buyNum))
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#C8102E')
Row()
.layoutWeight(1)
Text('加入购物车')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.linearGradient({
angle: 90,
colors: [['#C8102E', 0.0], ['#E8556D', 1.0]]
})
.onClick(() => {
this.showCart = false
})
}
.width('100%')
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.width('92%')
}
if (this.showCare) {
modalOverlay(() => {
this.showCare = false
})
Column({ space: 10 }) {
Text('配饰养护贴士')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Text('1. 绒花怕水,雨天请收进锦囊\n2. 鎏金簪避免与香水直接接触\n3. 缠花可用软毛刷轻扫浮尘\n4. 玉佩绳结松脱可到店免费更换\n5. 长期不用请平放于原盒避光保存')
.fontSize(12)
.fontColor('#5C4A3E')
.lineHeight(20)
Text('明白了')
.fontSize(13)
.fontColor('#8E0E20')
.padding({ left: 26, right: 26, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#FDF1E3')
.onClick(() => {
this.showCare = false
})
}
.padding(20)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.alignItems(HorizontalAlign.Center)
.width('80%')
}
}
.padding(12)
}
.backgroundColor('#FFF7EC')
.layoutWeight(1)
}
}
// ==================== Tab 4 写真预约 ====================
@Component
export struct PhotoTab {
@State showBook: boolean = false
@State showPhoto: boolean = false
@State showDate: boolean = false
@State selPkg: PhotoPackage = PHOTO_PACKAGES[0]
@State selPhotographer: Photographer = PHOTOGRAPHERS[0]
@State bookDate: string = '8/28'
build() {
Scroll() {
Column({ space: 12 }) {
Column({ space: 8 }) {
Row({ space: 8 }) {
Column()
.width(56)
.height(56)
.borderRadius(28)
.backgroundColor('#E8B34B')
Column({ space: 3 }) {
Text('国风写真节 · 第 5 季')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('园林 / 茶室 / 烛光夜拍 三大主题任选')
.fontSize(10)
.fontColor('#FFE3C8')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Row({ space: 10 }) {
Text('立减 ¥300')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#8E0E20')
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('#FFFFFF')
Text('赠妆造')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#8E0E20')
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('#FFFFFF')
Text('底片全送')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#8E0E20')
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('#FFFFFF')
}
}
.padding(14)
.borderRadius(14)
.linearGradient({
angle: 135,
colors: [['#C8102E', 0.0], ['#8E0E20', 1.0]]
})
.width('100%')
Row() {
Text('热门套餐')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Row()
.layoutWeight(1)
Text('档期日历')
.fontSize(11)
.fontColor('#C8102E')
.onClick(() => {
this.showDate = true
})
}
.width('100%')
Scroll() {
Row({ space: 10 }) {
ForEach(PHOTO_PACKAGES, (pkg: PhotoPackage) => {
Column({ space: 8 }) {
Column()
.width(150)
.height(96)
.borderRadius(10)
.backgroundColor(pkg.color)
Column({ space: 4 }) {
Row({ space: 6 }) {
Text(pkg.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
if (pkg.hot) {
Text('HOT')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(3)
.backgroundColor('#C8102E')
}
}
Text('删除')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(10)
.backgroundColor('#8E0E20')
.onClick(() => {
this.delIdx = idx
this.showDelAddr = true
})
}
.width('100%')
}
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.width('100%')
}, (a: AddrMeta) => a.id.toString())
Text('新增地址')
.fontSize(12)
.fontColor('#FFFFFF')
.padding({ left: 0, right: 0, top: 9, bottom: 9 })
.borderRadius(16)
.backgroundColor('#C8102E')
.width('100%')
.textAlign(TextAlign.Center)
.onClick(() => {
this.saveAddr()
})
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.constraintSize({ maxHeight: '78%' })
.width('92%')
}
if (this.showEditAddr) {
modalOverlay(() => {
this.showEditAddr = false
})
Column({ space: 12 }) {
Text('编辑地址')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
.width('100%')
TextInput({ placeholder: '收货人姓名', text: this.inputName })
.fontSize(12)
.height(38)
.borderRadius(8)
.backgroundColor('#FDF6EA')
.onChange((v: string) => {
this.inputName = v
})
.width('100%')
TextInput({ placeholder: '手机号', text: this.inputPhone })
.fontSize(12)
.height(38)
.borderRadius(8)
.backgroundColor('#FDF6EA')
.onChange((v: string) => {
this.inputPhone = v
})
.width('100%')
TextInput({ placeholder: '详细地址', text: this.inputDetail })
.fontSize(12)
.height(38)
.borderRadius(8)
.backgroundColor('#FDF6EA')
.onChange((v: string) => {
this.inputDetail = v
})
.width('100%')
Row({ space: 10 }) {
Text('取消')
.fontSize(13)
.fontColor('#5C4A3E')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#FDF6EA')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showEditAddr = false
})
Text('保存')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#C8102E')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.updateAddr()
})
}
.width('100%')
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.width('88%')
}
if (this.showDelAddr) {
modalOverlay(() => {
this.showDelAddr = false
})
Column({ space: 12 }) {
Text('删除地址')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Text('删除后无法恢复,确认删除该地址?')
.fontSize(12)
.fontColor('#A8896A')
Row({ space: 10 }) {
Text('取消')
.fontSize(13)
.fontColor('#5C4A3E')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#FDF6EA')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showDelAddr = false
})
Text('删除')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#8E0E20')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.removeAddr()
})
}
.width('100%')
}
.padding(18)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.width('80%')
}
if (this.showVip) {
modalOverlay(() => {
this.showVip = false
})
Column({ space: 12 }) {
Text('金穗会员特权')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.width('100%')
Column({ space: 8 }) {
Text('· 全场租金 95 折')
.fontSize(12)
.fontColor('#FFE3C8')
.width('100%')
Text('· 生日当月免 1 次押金')
.fontSize(12)
.fontColor('#FFE3C8')
.width('100%')
Text('· 新品优先租(提前 24h)')
.fontSize(12)
.fontColor('#FFE3C8')
.width('100%')
Text('· 写真跟拍 9 折')
.fontSize(12)
.fontColor('#FFE3C8')
.width('100%')
}
.padding(14)
.borderRadius(10)
.backgroundColor('rgba(142,14,32,0.5)')
.width('100%')
Text('知道了')
.fontSize(13)
.fontColor('#8E0E20')
.padding({ left: 26, right: 26, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#F2D088')
.onClick(() => {
this.showVip = false
})
}
.padding(20)
.borderRadius(16)
.linearGradient({
angle: 135,
colors: [['#C8102E', 0.0], ['#E8B34B', 1.0]]
})
.width('84%')
}
}
.padding(12)
}
.backgroundColor('#FFF7EC')
.layoutWeight(1)
}
}
// ==================== 主页面 ====================
@Entry
@Component
struct HanfuPage {
@State activeTab: number = 0
@State showSearch: boolean = false
@State showMsg: boolean = false
@State searchKey: string = ''
@State unRead: number = 2
build() {
Column() {
Column({ space: 0 }) {
Row({ space: 10 }) {
Text('青羊坊')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Row({ space: 6 }) {
Text('🔍')
.fontSize(13)
TextInput({ placeholder: '马面裙 / 绒花 / 唐制写真' })
.fontSize(12)
.height(34)
.layoutWeight(1)
.backgroundColor('#FBE9EA')
.borderRadius(17)
.onChange((v: string) => {
this.searchKey = v
})
}
.layoutWeight(1)
.padding({ left: 6, right: 6 })
.backgroundColor('#FFFFFF')
.borderRadius(17)
Stack({ alignContent: Alignment.TopEnd }) {
Text('✉')
.fontSize(18)
.fontColor('#FFFFFF')
if (this.unRead > 0) {
Column()
.width(8)
.height(8)
.borderRadius(4)
.backgroundColor('#E8B34B')
.margin({ top: -2, right: -2 })
}
}
.width(28)
.height(28)
.onClick(() => {
this.showMsg = true
})
}
.width('100%')
.padding({ left: 14, right: 14, top: 10, bottom: 10 })
}
.linearGradient({
angle: 135,
colors: [['#C8102E', 0.0], ['#8E0E20', 1.0]]
})
.width('100%')
if (this.activeTab === 0) {
NewTab()
} else if (this.activeTab === 1) {
RentTab()
} else if (this.activeTab === 2) {
PinTab()
} else if (this.activeTab === 3) {
PhotoTab()
} else if (this.activeTab === 4) {
OrderTab()
} else {
MineTab()
}
Row({ space: 0 }) {
ForEach(MAIN_TABS, (tab: TabMeta, idx: number) => {
Column({ space: 3 }) {
Text(tab.icon)
.fontSize(15)
.fontColor(this.activeTab === idx ? '#C8102E' : '#B08A5A')
Text(tab.name)
.fontSize(9)
.fontColor(this.activeTab === idx ? '#C8102E' : '#B08A5A')
if (this.activeTab === idx) {
Column()
.width(16)
.height(3)
.borderRadius(2)
.backgroundColor('#C8102E')
} else {
Column()
.width(16)
.height(3)
.borderRadius(2)
.backgroundColor('rgba(0,0,0,0)')
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 6, bottom: 6 })
.onClick(() => {
this.activeTab = idx
})
}, (tab: TabMeta, idx: number) => (tab.name + idx).toString())
}
.width('100%')
.backgroundColor('#FFFFFF')
if (this.showSearch) {
modalOverlay(() => {
this.showSearch = false
})
Column({ space: 12 }) {
Text('搜索')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
.width('100%')
TextInput({ placeholder: '输入汉服 / 配饰关键词' })
.fontSize(12)
.height(38)
.borderRadius(10)
.backgroundColor('#FDF6EA')
.width('100%')
Row({ space: 8 }) {
ForEach(['马面裙', '唐制', '绒花', '写真'], (k: string) => {
Text(k)
.fontSize(12)
.fontColor('#8E0E20')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor('#FDF1E3')
}, (k: string) => k)
}
.width('100%')
Text('搜索')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 0, right: 0, top: 9, bottom: 9 })
.borderRadius(16)
.backgroundColor('#C8102E')
.width('100%')
.textAlign(TextAlign.Center)
.onClick(() => {
this.showSearch = false
})
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.width('90%')
}
if (this.showMsg) {
modalOverlay(() => {
this.showMsg = false
this.unRead = 0
})
Column({ space: 12 }) {
Row() {
Text('消息中心')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Row()
.layoutWeight(1)
Text('全部已读')
.fontSize(11)
.fontColor('#C8102E')
.onClick(() => {
this.unRead = 0
})
}
.width('100%')
ForEach(MSG_LIST, (m: MsgMeta) => {
Row({ space: 10 }) {
Column({ space: 2 }) {
Text(m.title)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Text(m.content)
.fontSize(11)
.fontColor('#A8896A')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 4 }) {
Text(m.time)
.fontSize(9)
.fontColor('#CBB9A4')
if (!m.read) {
Column()
.width(7)
.height(7)
.borderRadius(4)
.backgroundColor('#C8102E')
}
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding({ top: 8, bottom: 8 })
}, (m: MsgMeta) => m.id.toString())
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.constraintSize({ maxHeight: '75%' })
.width('92%')
}
}
.width('100%')
.height('100%')
.backgroundColor('#FFF7EC')
}
}

总结
本文全面解析了基于 HarmonyOS 6.1.1 ArkTS 声明式 UI 框架开发的汉服国风体验馆应用。从架构设计层面看,该应用采用了"全局共享构建器 + export 导出子组件 + 内联弹窗"的独特架构模式。与游戏充值中心在每个 Tab 内部各自定义 modalOverlay 不同,汉服体验馆将遮罩层抽取为全局 @Builder function modalOverlay,所有子组件共享同一个遮罩构建器实例,消除了大量重复代码。同时,所有六个 Tab 组件都使用 export struct 导出,意味着这些组件可以被其他页面或模块引用,为未来扩展(如独立详情页、分享卡片等)预留了组件复用的可能性。
从数据建模层面看,十四个接口定义是三个应用中最多的,体现了汉服租赁业务的数据复杂度——一个商品(HanfuItem)就包含了十一个字段,涵盖朝代、租金、原价、租期、评分、租赁次数、标签数组、热度、代表色等维度。特别是 OrderRecord 接口中的 statusColor 字段,将状态颜色直接存储在数据中而非在渲染时通过条件映射计算,这是一种"数据冗余换渲染效率"的设计权衡。地址管理模块的 AddrMeta 接口配合 saveAddr、updateAddr、removeAddr 三个方法,实现了完整的不可变数组 CRUD 操作,是 ArkTS 声明式状态管理在数据增删改场景下的标准实践范式。
从交互设计层面看,该应用最具特色的是弹窗的内联实现方式——所有模态弹窗都直接写在各组件的 build() 方法中,通过 if (this.showXxx) 条件判断控制显隐,而非抽取为独立的 @Builder 方法。这种方式的优势是弹窗代码与页面上下文紧密耦合,可以直接访问 this 引用的所有状态变量,无需通过参数传递;同时,二级弹窗(如尺码表嵌套在租赁弹窗中、编辑/删除弹窗嵌套在地址列表中)的实现更加自然。代价是 build() 方法变得较长,但对于单页面单职责的组件来说,这种长度是可接受的。写真预约弹窗展示了套餐选择、摄影师选择和日历日期选择的三维交互,是应用中复杂度最高的弹窗模块。
更多推荐



所有评论(0)