基于HarmonyOS ArkTS API 24 Tab通过分类chips和双列Flex布局展示配饰商品,支持购物车弹窗和养护贴士
引言

汉服文化近年来在中国呈现爆发式增长,围绕汉服租赁、簪花配饰、国风写真、道具周边等衍生场景,形成了完整的产业链。构建一个覆盖"今日上新、服饰租赁、簪花铺子、写真预约、订单管理、个人中心"全链路的国风体验馆应用,需要在单一的移动端界面中融合电商浏览、预约日历、尺码选型、C2C交易辅助、积分运营等多种业务模式,对ArkTS声明式UI的组件化能力和状态管理精细度提出了极高要求。本文将深度剖析一个基于HarmonyOS ArkTS框架构建的汉服国风体验馆应用,该应用采用朱红(#C8102E)与鎏金(#E8B34B)双色主题,底色#FFF7EC的国风新中式视觉风格,通过底部六Tab导航实现多业务模块的有机整合。
从技术架构层面分析,该应用采用了ArkUI框架的组件化开发范式,以@Component装饰器封装了NewTab、RentTab、PinTab、PhotoTab、OrderTab、MineTab六个独立Tab组件,由主入口HanfuPage统一调度。数据层定义了DynastyChip、HanfuItem、RentTrend、SizeRow、AccessoryItem、PhotoPackage、Photographer、CalendarDay、OrderRecord、CouponMeta、PointLog、AddrMeta等十余种TypeScript interface,覆盖了朝代分类、汉服商品、租金行情、尺码、配饰、写真套餐、摄影师、档期日历、订单、优惠券、积分、地址等全部业务实体,形成了国风垂直领域的完整领域模型。
在业务设计层面,该应用实现了多个创新性的交互模式。今日上新Tab通过朝代chips筛选、双卡促销区、爆款榜单三个层级构建了沉浸式的新品浏览体验,并集成了租赁天数选择器、尺码表弹窗和上新提醒功能。服饰租赁Tab引入了周租金行情柱状图和到店试穿预约日历,将数据可视化与预约流程深度结合。簪花铺子Tab通过分类chips和双列Flex布局展示配饰商品,支持购物车弹窗和养护贴士。写真预约Tab实现了套餐横滑选择、摄影师列表、档期日历和预约确认的完整预约流程。订单和我的Tab则覆盖了订单追踪、续租、退订、优惠券、积分兑换、地址管理、金穗会员等用户运营功能。
整体应用的UI设计遵循了朱红鎏金系国风新中式视觉规范。全局使用@Builder函数定义了可复用的modalOverlay遮罩层组件,所有弹窗通过布尔状态变量控制条件渲染。linearGradient渐变在按钮、Banner和卡片中大量使用,从朱红到鎏金的渐变呼应了国风色彩美学。rentBarH和trendBarColor等辅助函数将业务数据映射为柱状条高度和颜色,实现了数据驱动的动态可视化。
一、数据模型层设计

1.1 朝代分类与汉服商品数据模型
应用的数据层以朝代维度作为汉服商品的核心分类标识,通过DynastyChip和HanfuItem两个interface构建了国风垂直领域的实体模型。
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
}
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' }
]
DynastyChip定义了朝代筛选标签的数据结构,name字段存储朝代名称,count字段存储该朝代下的商品数量。HanfuItem是汉服商品的核心实体,其设计亮点在于color字段——使用十六进制颜色值直接作为商品的主色调,在UI渲染时通过backgroundColor(item.color)为商品封面区域着色,无需图片资源即可实现色彩丰富的视觉呈现。tags字段使用字符串数组存储商品标签,通过ForEach在卡片中渲染为标签chips。
hot布尔字段控制HOT标签的显示,rentCount字段用于排序和热度展示,score字段使用toFixed(1)格式化显示评分。rentDay字段固定为3天,在租赁确认弹窗中通过this.selHanfu.price * Math.ceil(this.rentNum / 3)计算总价,实现按3天为单位的价格计算逻辑。
1.2 租金行情与尺码数据模型
服饰租赁Tab引入了RentTrend和SizeRow两个数据实体,分别支撑周租金行情柱状图和尺码选择功能。
interface RentTrend {
day: string
price: number
}
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 }
]
interface SizeRow {
size: string
height: string
bust: string
stock: number
}
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 }
]
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'
}
rentBarH函数采用阶梯式映射而非线性映射,将价格分为四个区间对应四个固定高度值(84、66、46、28像素)。这种设计使得周末高租金(160+)的柱状条明显高于工作日,形成直观的价格差异可视化。trendBarColor函数以130为阈值返回朱红或鎏金色,高于130的周末租金使用朱红色表示"溢价",低于130的工作日租金使用鎏金色表示"平价"。
SizeRow定义了尺码表的数据结构,size字段存储尺码标签(XS到XL),height和bust分别存储身高和胸围范围,stock字段表示该尺码的库存数量。在尺码表弹窗中,M尺码行通过row.size === 'M'条件判断获得特殊的浅色背景高亮,引导用户关注推荐尺码。
1.3 配饰、写真与摄影师数据模型

簪花铺子和写真预约两个Tab分别定义了AccessoryItem、PhotoPackage和Photographer三个实体,覆盖了配饰购物和写真预约的核心数据。
interface AccessoryItem {
id: number
name: string
material: string
price: number
stock: number
grade: string
color: string
}
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' }
]
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
}
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 }
]
AccessoryItem的grade字段存储品质等级描述(如"匠心工艺"“非遗联名”"孤品"等),在UI中以标签形式展示,为商品赋予文化附加值。color字段同样使用十六进制色值作为商品封面色,与HanfuItem的设计保持一致。
PhotoPackage定义了写真套餐的完整信息,scene字段存储拍摄场景描述,duration字段存储拍摄时长,photos字段存储精修张数和底片政策,retouch字段存储修图风格。Photographer实体定义了摄影师信息,years字段存储从业年限,style字段存储摄影风格描述,rating字段使用toFixed(1)格式化显示评分。
二、今日上新Tab组件深度解析

2.1 朝代筛选与促销卡区
NewTab组件作为应用首屏,通过朝代chips横滑、双卡促销区和爆款榜单三个层级构建了新品浏览体验。组件内部通过dynastyIdx管理当前选中的朝代索引。
@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%')
朝代chips区域通过横向Scroll包裹ForEach渲染DYNASTY_CHIPS数组,每个chip的选中态通过this.dynastyIdx === DYNASTY_CHIPS.indexOf(chip)条件判断:选中时使用白字红底,未选中时使用暗红字米色底。ForEach的第三个参数(chip: DynastyChip) => chip.name作为键值生成器,确保列表渲染的唯一性。点击chip时更新dynastyIdx,触发所有chip的重新渲染以更新选中态视觉。
双卡促销区使用两个等宽的Column(各layoutWeight(1),固定height(84)),左侧卡片使用135度朱红到暗红渐变配合白色文字,点击时设置dynastyIdx为1(唐制)实现快捷筛选。右侧卡片使用米色背景配合鎏金边框,点击时打开上新提醒弹窗。两卡片的视觉对比通过深色渐变vs浅色实色的差异化设计,形成了主次分明的促销信息层次。
2.2 爆款榜单与商品卡片

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())
爆款榜单区域的商品卡片采用了左封面右信息的横向布局。左侧封面区域使用backgroundColor(item.color)直接以商品数据的色值着色,96x120像素的色块配合10像素圆角形成简洁的封面占位。右侧信息区通过嵌套的Column垂直排列朝代标签行、标题、标签chips行、评分租赁次行和价格操作行。
朝代标签使用朱红底白字,HOT标签通过if (item.hot)条件渲染控制显示,使用米色底暗红字与朝代标签形成色彩对比。tags数组通过ForEach渲染为浅米色底棕色字的小标签chips。价格区域使用Column嵌套Row的结构展示租价(朱红色大字+天数小字)和原价(删除线),"立即租"按钮使用90度朱红到粉红渐变。
shadow属性使用rgba(200,16,46,0.06)的朱红色系半透明阴影,呼应了应用主题色,使卡片悬浮感与品牌色彩保持一致。
2.3 租赁确认弹窗与尺码表

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%')
}
租赁确认弹窗使用全局@Builder函数modalOverlay作为遮罩层(注意与组件内@Builder方法的区别),弹窗内容通过if条件渲染控制。弹窗包含商品摘要、天数选择器、尺码表入口、物流说明和价格确认五个区域。
天数选择器的减号按钮fontColor通过this.rentNum <= 1 ? '#D8C9B8' : '#C8102E'条件判断,当天数为1时变为灰色表示不可再减。加号按钮限制了最大15天。合计金额通过this.selHanfu.price * Math.ceil(this.rentNum / 3)计算,使用Math.ceil将天数除以3向上取整,实现"每3天一个计费单位"的定价逻辑——租赁4天也按6天(2个单位)计价。
尺码表弹窗通过showSize布尔变量控制,在租赁弹窗内部嵌套条件渲染。尺码表通过ForEach渲染SIZE_ROWS数组,每行展示尺码、身高范围、胸围范围和库存状态,M尺码行通过row.size === 'M'条件判断获得浅米色背景高亮。
三、服饰租赁Tab与租金行情可视化

3.1 周租金行情柱状图
RentTab组件在顶部渲染了7日租金行情柱状图,通过ForEach遍历RENT_TREND数组生成7个纵向柱状条,实现了数据驱动的可视化呈现。
@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')
柱状图的每个数据项通过Column垂直排列价格文字、柱状条和日期标签。柱状条的高度通过rentBarH函数动态计算,使用阶梯式映射将价格分为四个区间。柱状条的渐变色通过trendBarColor函数判断:价格高于130使用朱红系渐变(从粉红到朱红),低于130使用鎏金系渐变(从浅金到鎏金)。价格文字的颜色也同步切换,高于130显示朱红色,低于130显示棕色。
这种设计通过rentBarH和trendBarColor两个辅助函数的配合,将业务数据(租金价格)映射为两个视觉维度(高度和颜色),使用户能够通过柱状条的高度差异和颜色变化直观感知周末溢价趋势。180度渐变方向(从上到下)使得柱状条呈现出从浅到深的渐变效果,增强了立体感。
3.2 到店试穿预约日历
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%')
}
试穿预约弹窗包含日期选择和时段选择两个选择器。日期选择器通过ForEach渲染CALENDAR_DAYS数组,每个日期项使用Column垂直排列标签(今天/明天/周四等)和日期,选中态使用朱红底白字,未选中使用米色底棕色字。时段选择器渲染四个固定时段,选中态使用暗红底白字,与日期选择器形成主次色彩区分。
提交按钮使用90度朱红到鎏金的渐变,是整个应用中少数使用双色主题渐变的按钮,呼应了国风新中式的色彩美学。弹窗底部还展示了实体店铺地址,将线上预约与线下体验形成了完整的O2O闭环。
四、写真预约Tab与档期管理
4.1 套餐选择与摄影师列表
PhotoTab组件实现了写真预约的完整流程,包含套餐横滑选择、摄影师列表和预约确认弹窗三个核心模块。
@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%')
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(pkg.scene)
.fontSize(10)
.fontColor('#A8896A')
Text(pkg.duration + ' · ' + pkg.photos)
.fontSize(10)
.fontColor('#A8896A')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text('¥' + pkg.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#C8102E')
Text('¥' + pkg.originPrice)
.fontSize(10)
.fontColor('#CBB9A4')
.decoration({ type: TextDecorationType.LineThrough })
Row()
.layoutWeight(1)
Text('预约')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('#C8102E')
.onClick(() => {
this.selPkg = pkg
this.showBook = true
})
}
.width('100%')
}
.alignItems(HorizontalAlign.Start)
.width(150)
}
.padding(8)
.borderRadius(12)
.backgroundColor('#FFFFFF')
}, (pkg: PhotoPackage) => pkg.id.toString())
}
}
.scrollable(ScrollDirection.Horizontal)
.width('100%')
写真节横幅使用135度朱红到暗红渐变,配合鎏金色圆形头像和三个白色促销标签(立减、赠妆造、底片全送),形成国风节日氛围的视觉呈现。套餐横滑区域通过ForEach渲染PHOTO_PACKAGES数组,每个套餐卡片使用color字段的色值作为封面背景,固定宽度150像素,横向排列形成可滑动的套餐选择器。
摄影师列表通过ForEach渲染PHOTOGRAPHERS数组,每个摄影师卡片采用左头像右信息的布局,展示姓名、从业年限、风格描述、评分和作品数量。头像区域使用固定色值#F2D088的圆形色块作为占位,"选TA"按钮通过边框样式区别于填充样式的操作按钮,引导用户进入预约流程。
4.2 预约确认与档期日历
if (this.showBook) {
modalOverlay(() => {
this.showBook = false
})
Column({ space: 14 }) {
Text('预约写真')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
.width('100%')
Row({ space: 10 }) {
Column()
.width(64)
.height(64)
.borderRadius(8)
.backgroundColor(this.selPkg.color)
Column({ space: 3 }) {
Text(this.selPkg.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Text(this.selPkg.scene + ' · ' + this.selPkg.retouch)
.fontSize(10)
.fontColor('#A8896A')
Text('摄影师:' + this.selPhotographer.name + '(' + this.selPhotographer.style + ')')
.fontSize(10)
.fontColor('#B08A5A')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Column({ space: 8 }) {
Text('拍摄日期')
.fontSize(12)
.fontColor('#A8896A')
.width('100%')
Row({ space: 8 }) {
ForEach(CALENDAR_DAYS, (d: CalendarDay) => {
Column({ space: 2 }) {
Text(d.date)
.fontSize(12)
.fontColor(this.bookDate === d.date ? '#FFFFFF' : '#5C4A3E')
Text('余 ' + d.left)
.fontSize(9)
.fontColor(this.bookDate === d.date ? '#FFE3C8' : (d.left === 0 ? '#D8C9B8' : '#B08A5A'))
}
.padding({ left: 9, right: 9, top: 6, bottom: 6 })
.borderRadius(9)
.backgroundColor(this.bookDate === d.date ? '#C8102E' : '#FDF6EA')
.onClick(() => {
this.bookDate = d.date
})
}, (d: CalendarDay) => ('b' + d.idx).toString())
}
}
.width('100%')
Text('· 定金 ¥100 当天可退 · 含 1 次免费改期\n· 加急修片 3 天出片,普通 7 天出片')
.fontSize(11)
.fontColor('#A8896A')
.lineHeight(18)
.width('100%')
Row() {
Column({ space: 1 }) {
Text('预付定金')
.fontSize(11)
.fontColor('#A8896A')
Text('¥100')
.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: [['#8E0E20', 0.0], ['#C8102E', 1.0]]
})
.onClick(() => {
this.showBook = false
})
}
.width('100%')
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.constraintSize({ maxHeight: '80%' })
.width('92%')
}
预约确认弹窗集成了套餐摘要、日期选择和定金确认三个区域。日期选择器通过CALENDAR_DAYS数组的ForEach渲染7天档期,每个日期项展示日期和剩余名额(left字段),满档(left为0)时剩余名额显示为灰色,选中态使用朱红底白字。ForEach的键值生成器使用('b' + d.idx).toString()确保在多个日历组件共存时的唯一性。
弹窗底部的定金区域使用Column嵌套展示"预付定金"标签和金额,预约按钮使用90度暗红到朱红的渐变,与试穿预约弹窗的朱红到鎏金渐变形成色彩差异化,帮助用户通过视觉辨识当前所处的预约场景。
五、订单管理与我的中心
5.1 订单追踪与续租退订流程
OrderTab组件通过订单筛选chips和订单列表实现订单管理功能,支持续租和退订两种操作。
@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]
build() {
Scroll() {
Column({ space: 12 }) {
Row({ space: 8 }) {
ForEach(ORDER_FILTERS, (f: string) => {
Text(f)
.fontSize(12)
.fontColor(this.filterIdx === ORDER_FILTERS.indexOf(f) ? '#FFFFFF' : '#8E0E20')
.padding({ left: 13, right: 13, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(this.filterIdx === ORDER_FILTERS.indexOf(f) ? '#C8102E' : '#FDF1E3')
.onClick(() => {
this.filterIdx = ORDER_FILTERS.indexOf(f)
})
}, (f: string) => f)
}
.width('100%')
ForEach(ORDER_LIST, (o: OrderRecord) => {
Column({ space: 10 }) {
Row() {
Text('单号 ' + o.id)
.fontSize(11)
.fontColor('#A8896A')
Row()
.layoutWeight(1)
Text(o.status)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(o.statusColor)
}
.width('100%')
Row({ space: 10 }) {
Column()
.width(64)
.height(64)
.borderRadius(8)
.backgroundColor(o.cover)
Column({ space: 4 }) {
Text(o.item)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Text(o.date)
.fontSize(11)
.fontColor('#A8896A')
if (o.deposit > 0) {
Text('押金 ¥' + o.deposit)
.fontSize(10)
.fontColor('#B08A5A')
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 3 }) {
Text('¥' + o.amount)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#C8102E')
if (o.days > 1) {
Text('共 ' + o.days + ' 天')
.fontSize(10)
.fontColor('#A8896A')
}
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row({ space: 8 }) {
Row()
.layoutWeight(1)
Text('订单详情')
.fontSize(11)
.fontColor('#5C4A3E')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12)
.border({ width: 1, color: '#E3D5C2' })
.onClick(() => {
this.selOrder = o
this.showDetail = true
})
if (o.status === '租赁中') {
Text('续租')
.fontSize(11)
.fontColor('#8E0E20')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12)
.border({ width: 1, color: '#E8B34B' })
.onClick(() => {
this.selOrder = o
this.showRenew = true
})
}
if (o.status === '待取片' || o.status === '租赁中') {
Text('申请退订')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor('#C8102E')
.onClick(() => {
this.selOrder = o
this.showRefund = true
})
}
}
.width('100%')
}
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
}, (o: OrderRecord) => o.id)
订单列表的操作按钮通过条件渲染根据订单状态动态生成。"续租"按钮仅在status为"租赁中"时显示,使用鎏金边框样式。"申请退订"按钮在status为"待取片"或"租赁中"时显示,使用朱红填充样式。这种基于状态的按钮条件渲染确保用户只能对可操作的订单执行对应操作,避免了无效交互。
订单详情弹窗内嵌了订单进度条,通过"提交订单→发货→当前状态"三步骤展示订单流程节点。续租弹窗提供了+1天、+3天、+7天三个快捷选项,价格通过this.selOrder.amount * (d === '+1 天' ? 0.4 : (d === '+3 天' ? 1 : 2)).toFixed(0)动态计算,按天数的比例系数映射续租价格。
5.2 个人中心与地址CRUD
MineTab组件构建了完整的用户运营界面,包含用户信息卡、订单状态入口、积分明细、地址管理和功能列表等模块。地址管理通过addrList状态数组驱动,实现了完整的增删改查操作。
@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: '公司' }
]
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
}
saveAddr方法在新增地址时对空字段提供默认值:姓名为空时使用’沈知意’,手机为空时使用’138****6621’,详细地址为空时使用’锦江区春熙路 88 号’。这种设计降低了用户的输入负担,同时保证了数据的完整性。updateAddr方法通过editIdx索引定位待修改的地址项,对非空字段进行更新,空字段保留原值。
removeAddr方法通过delIdx索引过滤掉待删除项,创建新数组赋值给addrList。三个方法都采用了不可变更新模式——创建新数组、逐个拷贝元素、整体赋值,确保ArkUI状态管理系统能正确检测变化。地址列表的ForEach渲染中,每个地址项配备"编辑"和"删除"两个操作按钮,分别通过border和backgroundColor样式区分视觉层级。
六、主页面架构与Tab导航
6.1 主入口与Tab路由
主入口HanfuPage采用Column结构,分为头部导航栏、内容区和底部Tab导航三个区域。与其他两个应用不同的是,头部导航栏内嵌了搜索框和消息通知图标。
@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')
}
}
头部导航栏使用135度朱红到暗红渐变,包含品牌名"青羊坊"和内嵌搜索框。搜索框使用TextInput组件配合浅粉色背景和17像素圆角,onChange回调实时更新searchKey状态变量。消息通知图标使用Stack配合Alignment.TopEnd叠加未读指示点——当unRead > 0时显示鎏金色圆点,点击消息图标打开消息中心弹窗后通过遮罩层回调将unRead设为0。
底部Tab导航的选中指示条使用if-else条件渲染:选中Tab显示朱红色指示条,未选中Tab显示透明色指示条(rgba(0,0,0,0))。ForEach的键值生成器使用(tab.name + idx).toString()确保键值的唯一性。Tab的icon字段和name字段均使用文字而非emoji图标,呼应了国风新中式的文字美学。
消息中心弹窗的消息列表通过ForEach渲染MSG_LIST数组,每条消息展示标题、内容、时间和未读指示点。未读消息通过if (!m.read)条件渲染朱红色圆点,"全部已读"按钮的onClick回调将unRead设为0触发头部未读指示点消失。
七、核心业务流程
八、技术点对比总结
| 技术维度 | 今日上新 | 服饰租赁 | 簪花铺子 | 写真预约 | 订单 | 我的 |
|---|---|---|---|---|---|---|
| 核心状态数 | 7个@State | 6个@State | 4个@State | 6个@State | 5个@State | 11个@State |
| 列表渲染 | 纵向ForEach | 纵向ForEach | Flex双列 | 横向Scroll+纵向ForEach | 纵向ForEach | 纵向ForEach |
| 模态弹窗数 | 3个(租赁/尺码/提醒) | 3个(试穿/详情) | 2个(购物车/养护) | 3个(预约/客片/档期) | 3个(详情/退订/续租) | 6个(券/积分/地址×3/会员) |
| 数据可视化 | 无 | 周租金柱状图 | 无 | 无 | 订单进度条 | 积分明细列表 |
| 核心交互逻辑 | 朝代筛选+天数选择 | 行情图+试穿日历 | 分类+购物车 | 套餐+摄影师+档期 | 状态筛选+续租退订 | 地址CRUD+积分+会员 |
| 视觉特色 | color色值封面 | 阶梯式柱状图 | Flex双列瀑布 | 横滑套餐卡 | 状态色映射按钮 | 渐变会员卡 |
| 条件渲染复杂度 | HOT标签+尺码高亮 | 柱状图颜色映射 | 分类筛选联动 | HOT标签+档期余量 | 状态按钮条件渲染 | 多弹窗+默认标记 |
| 辅助函数 | 无 | rentBarH+trendBarColor | 无 | 无 | 无 | 无 |
安装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(pkg.scene)
.fontSize(10)
.fontColor('#A8896A')
Text(pkg.duration + ' · ' + pkg.photos)
.fontSize(10)
.fontColor('#A8896A')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text('¥' + pkg.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#C8102E')
Text('¥' + pkg.originPrice)
.fontSize(10)
.fontColor('#CBB9A4')
.decoration({ type: TextDecorationType.LineThrough })
Row()
.layoutWeight(1)
Text('预约')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('#C8102E')
.onClick(() => {
this.selPkg = pkg
this.showBook = true
})
}
.width('100%')
}
.alignItems(HorizontalAlign.Start)
.width(150)
}
.padding(8)
.borderRadius(12)
.backgroundColor('#FFFFFF')
}, (pkg: PhotoPackage) => pkg.id.toString())
}
}
.scrollable(ScrollDirection.Horizontal)
.width('100%')
Row() {
Text('签约摄影师')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Row()
.layoutWeight(1)
Text('作品集')
.fontSize(11)
.fontColor('#C8102E')
.onClick(() => {
this.showPhoto = true
})
}
.width('100%')
ForEach(PHOTOGRAPHERS, (p: Photographer) => {
Row({ space: 12 }) {
Column()
.width(52)
.height(52)
.borderRadius(26)
.backgroundColor('#F2D088')
Column({ space: 4 }) {
Row({ space: 6 }) {
Text(p.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Text('从业 ' + p.years + ' 年')
.fontSize(10)
.fontColor('#B08A5A')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#FDF6EA')
.borderRadius(4)
}
Text(p.style)
.fontSize(11)
.fontColor('#A8896A')
Row({ space: 10 }) {
Text('★ ' + p.rating.toFixed(1))
.fontSize(11)
.fontColor('#E8B34B')
Text('作品 ' + p.works + ' 组')
.fontSize(11)
.fontColor('#A8896A')
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 4 }) {
Text('¥' + p.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#C8102E')
Text('跟拍/组')
.fontSize(9)
.fontColor('#A8896A')
Text('选 TA')
.fontSize(11)
.fontColor('#8E0E20')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.border({ width: 1, color: '#C8102E' })
.onClick(() => {
this.selPhotographer = p
this.showBook = true
})
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
}, (p: Photographer) => p.id.toString())
if (this.showBook) {
modalOverlay(() => {
this.showBook = false
})
Column({ space: 14 }) {
Text('预约写真')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
.width('100%')
Row({ space: 10 }) {
Column()
.width(64)
.height(64)
.borderRadius(8)
.backgroundColor(this.selPkg.color)
Column({ space: 3 }) {
Text(this.selPkg.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Text(this.selPkg.scene + ' · ' + this.selPkg.retouch)
.fontSize(10)
.fontColor('#A8896A')
Text('摄影师:' + this.selPhotographer.name + '(' + this.selPhotographer.style + ')')
.fontSize(10)
.fontColor('#B08A5A')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Column({ space: 8 }) {
Text('拍摄日期')
.fontSize(12)
.fontColor('#A8896A')
.width('100%')
Row({ space: 8 }) {
ForEach(CALENDAR_DAYS, (d: CalendarDay) => {
Column({ space: 2 }) {
Text(d.date)
.fontSize(12)
.fontColor(this.bookDate === d.date ? '#FFFFFF' : '#5C4A3E')
Text('余 ' + d.left)
.fontSize(9)
.fontColor(this.bookDate === d.date ? '#FFE3C8' : (d.left === 0 ? '#D8C9B8' : '#B08A5A'))
}
.padding({ left: 9, right: 9, top: 6, bottom: 6 })
.borderRadius(9)
.backgroundColor(this.bookDate === d.date ? '#C8102E' : '#FDF6EA')
.onClick(() => {
this.bookDate = d.date
})
}, (d: CalendarDay) => ('b' + d.idx).toString())
}
}
.width('100%')
Text('· 定金 ¥100 当天可退 · 含 1 次免费改期\n· 加急修片 3 天出片,普通 7 天出片')
.fontSize(11)
.fontColor('#A8896A')
.lineHeight(18)
.width('100%')
Row() {
Column({ space: 1 }) {
Text('预付定金')
.fontSize(11)
.fontColor('#A8896A')
Text('¥100')
.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: [['#8E0E20', 0.0], ['#C8102E', 1.0]]
})
.onClick(() => {
this.showBook = false
})
}
.width('100%')
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.constraintSize({ maxHeight: '80%' })
.width('92%')
}
if (this.showPhoto) {
modalOverlay(() => {
this.showPhoto = false
})
Column({ space: 12 }) {
Text('本期精选客片')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(['#F5C8D0', '#D9B382', '#BFD8C2', '#C7A6D9', '#E39A94', '#8FA3C4'], (c: string) => {
Column()
.width(86)
.height(110)
.borderRadius(8)
.backgroundColor(c)
.margin({ bottom: 8 })
}, (c: string) => c)
}
.width('100%')
Text('全部客片 1280 组,进店可看原片')
.fontSize(11)
.fontColor('#A8896A')
Text('关闭')
.fontSize(13)
.fontColor('#8E0E20')
.padding({ left: 26, right: 26, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#FDF1E3')
.onClick(() => {
this.showPhoto = false
})
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.alignItems(HorizontalAlign.Center)
.constraintSize({ maxHeight: '75%' })
.width('86%')
}
if (this.showDate) {
modalOverlay(() => {
this.showDate = false
})
Column({ space: 10 }) {
Text('未来 7 天档期')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
ForEach(CALENDAR_DAYS, (d: CalendarDay) => {
Row() {
Text(d.label + ' ' + d.date)
.fontSize(13)
.fontColor('#5C4A3E')
Row()
.layoutWeight(1)
Text(d.left > 0 ? '可约 ' + d.left + ' 单' : '已约满')
.fontSize(12)
.fontColor(d.left > 0 ? '#C8102E' : '#D8C9B8')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(8)
.backgroundColor(d.left > 0 ? '#FBE9EA' : '#F6F1E8')
}
.width('100%')
.padding({ top: 8, bottom: 8 })
}, (d: CalendarDay) => ('q' + d.idx).toString())
}
.padding(18)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.constraintSize({ maxHeight: '70%' })
.width('84%')
}
}
.padding(12)
}
.backgroundColor('#FFF7EC')
.layoutWeight(1)
}
}
// ==================== Tab 5 订单 ====================
@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]
build() {
Scroll() {
Column({ space: 12 }) {
Row({ space: 8 }) {
ForEach(ORDER_FILTERS, (f: string) => {
Text(f)
.fontSize(12)
.fontColor(this.filterIdx === ORDER_FILTERS.indexOf(f) ? '#FFFFFF' : '#8E0E20')
.padding({ left: 13, right: 13, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(this.filterIdx === ORDER_FILTERS.indexOf(f) ? '#C8102E' : '#FDF1E3')
.onClick(() => {
this.filterIdx = ORDER_FILTERS.indexOf(f)
})
}, (f: string) => f)
}
.width('100%')
ForEach(ORDER_LIST, (o: OrderRecord) => {
Column({ space: 10 }) {
Row() {
Text('单号 ' + o.id)
.fontSize(11)
.fontColor('#A8896A')
Row()
.layoutWeight(1)
Text(o.status)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(o.statusColor)
}
.width('100%')
Row({ space: 10 }) {
Column()
.width(64)
.height(64)
.borderRadius(8)
.backgroundColor(o.cover)
Column({ space: 4 }) {
Text(o.item)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
Text(o.date)
.fontSize(11)
.fontColor('#A8896A')
if (o.deposit > 0) {
Text('押金 ¥' + o.deposit)
.fontSize(10)
.fontColor('#B08A5A')
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 3 }) {
Text('¥' + o.amount)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#C8102E')
if (o.days > 1) {
Text('共 ' + o.days + ' 天')
.fontSize(10)
.fontColor('#A8896A')
}
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row({ space: 8 }) {
Row()
.layoutWeight(1)
Text('订单详情')
.fontSize(11)
.fontColor('#5C4A3E')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12)
.border({ width: 1, color: '#E3D5C2' })
.onClick(() => {
this.selOrder = o
this.showDetail = true
})
if (o.status === '租赁中') {
Text('续租')
.fontSize(11)
.fontColor('#8E0E20')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12)
.border({ width: 1, color: '#E8B34B' })
.onClick(() => {
this.selOrder = o
this.showRenew = true
})
}
if (o.status === '待取片' || o.status === '租赁中') {
Text('申请退订')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor('#C8102E')
.onClick(() => {
this.selOrder = o
this.showRefund = true
})
}
}
.width('100%')
}
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
}, (o: OrderRecord) => o.id)
Text('仅展示近 3 个月订单,更早订单请到 PC 端查看')
.fontSize(10)
.fontColor('#CBB9A4')
.width('100%')
.textAlign(TextAlign.Center)
if (this.showDetail) {
modalOverlay(() => {
this.showDetail = false
})
Column({ space: 12 }) {
Text('订单详情')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
.width('100%')
Row({ space: 4 }) {
Text('提交订单')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 7, right: 7, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('#C8102E')
Text('——')
.fontSize(9)
.fontColor('#D8C9B8')
Text('发货')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 7, right: 7, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('#E8B34B')
Text('——')
.fontSize(9)
.fontColor('#D8C9B8')
Text(this.selOrder.status)
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 7, right: 7, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('#8E0E20')
}
.width('100%')
.justifyContent(FlexAlign.Center)
Column({ space: 6 }) {
Text('商品:' + this.selOrder.item)
.fontSize(13)
.fontColor('#3A2A22')
.width('100%')
Text('周期:' + this.selOrder.date + '(' + this.selOrder.days + ' 天)')
.fontSize(12)
.fontColor('#5C4A3E')
.width('100%')
Text('租金:¥' + this.selOrder.amount)
.fontSize(12)
.fontColor('#5C4A3E')
.width('100%')
Text('押金:¥' + this.selOrder.deposit + '(归还自动退回)')
.fontSize(12)
.fontColor('#5C4A3E')
.width('100%')
Text('物流:顺丰速运 SF1368827451(免运费)')
.fontSize(12)
.fontColor('#5C4A3E')
.width('100%')
}
.padding(12)
.borderRadius(10)
.backgroundColor('#FDF6EA')
.width('100%')
Text('联系客服')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 0, right: 0, top: 9, bottom: 9 })
.borderRadius(18)
.backgroundColor('#C8102E')
.width('100%')
.textAlign(TextAlign.Center)
.onClick(() => {
this.showDetail = false
})
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.width('90%')
}
if (this.showRefund) {
modalOverlay(() => {
this.showRefund = false
})
Column({ space: 12 }) {
Text('申请退订')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
.width('100%')
Text('「' + this.selOrder.item + '」')
.fontSize(13)
.fontColor('#5C4A3E')
Text('· 开拍/发货前 24 小时退订全免\n· 24 小时内退订收取 30% 违约金\n· 已使用不可退,可联系客服协商')
.fontSize(12)
.fontColor('#A8896A')
.lineHeight(19)
.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.showRefund = 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.showRefund = false
})
}
.width('100%')
}
.padding(18)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.width('86%')
}
if (this.showRenew) {
modalOverlay(() => {
this.showRenew = false
})
Column({ space: 12 }) {
Text('续租申请')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3A2A22')
.width('100%')
Text('「' + this.selOrder.item + '」当前押金可直接顺延')
.fontSize(12)
.fontColor('#5C4A3E')
Row({ space: 10 }) {
ForEach(['+1 天', '+3 天', '+7 天'], (d: string) => {
Text(d + ' ¥' + (this.selOrder.amount * (d === '+1 天' ? 0.4 : (d === '+3 天' ? 1 : 2))).toFixed(0))
.fontSize(12)
.fontColor('#8E0E20')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(10)
.backgroundColor('#FDF1E3')
}, (d: string) => d)
}
.width('100%')
Text('提交续租')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 0, right: 0, top: 9, bottom: 9 })
.borderRadius(18)
.linearGradient({
angle: 90,
colors: [['#C8102E', 0.0], ['#E8B34B', 1.0]]
})
.width('100%')
.textAlign(TextAlign.Center)
.onClick(() => {
this.showRenew = false
})
}
.padding(18)
.borderRadius(16)
.backgroundColor('#FFF9F0')
.width('84%')
}
}
.padding(12)
}
.backgroundColor('#FFF7EC')
.layoutWeight(1)
}
}
// ==================== Tab 6 我的 ====================
@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: '公司' }
]
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: '家'
})
## 总结

本文深入解析了一个基于HarmonyOS ArkTS声明式UI的汉服国风体验馆应用的完整架构。该应用通过六个功能Tab构建了从新品上新到个人中心的完整国风服务闭环,涵盖了今日上新、服饰租赁、簪花铺子、写真预约、订单管理和用户中心六大核心业务场景。每个Tab组件都采用了@Component封装的独立struct结构,通过@State状态管理驱动UI的动态渲染,实现了组件内自治的状态管理范式。全局使用@Builder函数定义了可复用的modalOverlay遮罩层,所有弹窗通过布尔状态变量的条件渲染控制显隐,形成了统一的模态交互方案。
在数据可视化方面,服饰租赁Tab的周租金行情柱状图是整个应用的技术亮点。rentBarH函数采用阶梯式映射将价格分为四个区间对应四个固定高度,trendBarColor函数以130为阈值返回朱红或鎏金色,两个函数配合实现了价格数据到高度和颜色两个视觉维度的映射。这种设计使得用户能够通过柱状条的高度差异直观感知周末溢价趋势,通过颜色变化辨识平价与溢价区间,充分展现了ArkUI声明式UI在数据可视化方面的表达能力。
在预约系统的设计上,写真预约Tab实现了套餐选择、摄影师选择和档期日历三维度联动的预约流程。档期日历通过CALENDAR_DAYS数组的ForEach渲染7天档期,每个日期项展示剩余名额,满档时名额文字变灰不可选。预约确认弹窗将套餐摘要、日期选择和定金确认三个区域整合在同一弹窗内,定金金额通过selPkg的color色值着色的封面区域与套餐信息形成视觉关联。到店试穿预约弹窗则采用日期和时段双重选择器,日期选中使用朱红底白字,时段选中使用暗红底白字,通过色彩深浅区分两个选择器的层级关系。
更多推荐




所有评论(0)