基于HarmonyOS API 24与ArkTS的球鞋洗护社区应用开发:上门取送与修复改色的全链路技术实践
引言:HarmonyOS 6.1.1时代的垂直生活服务技术架构
HarmonyOS 6.1.1在全场景智慧生活中扮演着越来越重要的角色,其ArkTS声明式开发框架经过API 24的持续打磨,在组件化渲染、状态管理粒度和动画系统性能上均达到了业界领先水准。在本地生活服务领域,HarmonyOS ArkTS的声明式范式尤其适合构建包含大量列表项、多维度数据展示和复杂交互弹框的垂直社区应用。本项目以球鞋洗护·上门取送与修复改色社区「球鞋急诊室」为业务蓝本,完整展示了如何基于美团app交互模式,将一项高度垂直的球鞋后市场服务以HarmonyOS ArkTS技术栈实现从数据建模到UI交付的全流程开发。
ArkTS作为HarmonyOS的主力应用开发语言,在类型安全、状态追踪和UI声明三个维度提供了系统级支持。@Observed装饰器实现了类实例属性级别的响应式追踪,@State装饰器在组件内提供了状态变量的自动观察与UI联动更新,@Builder装饰器将UI构建逻辑封装为可复用的函数单元。这三个核心装饰器构成了ArkTS声明式开发的基石。在「球鞋急诊室」应用中,我们以沥青蓝(#37474F)为主色调,搭配荧光橙(#FF6D00)为强调色,构建出工业感与潮流感并存的视觉语言——沥青蓝传达出洗护车间的专业沉稳,荧光橙则注入了球鞋文化的街头活力。这种双色对比策略使得应用在视觉上既能建立服务可信度,又能吸引年轻球鞋爱好者群体。
从业务价值维度分析,球鞋后市场是一个被严重低估的垂直赛道。随着球鞋文化的普及和收藏市场的成熟,一双AJ1芝加哥的市值可达数千元,而其日常护理成本仅几十元,护理与不护理的成色差异直接影响估值。传统球鞋洗护依赖线下门店到店送取,用户面临排队等待、取送不便、进度不透明等痛点。「球鞋急诊室」通过上门取送服务解决了物流痛点,通过洗护订单状态追踪解决了进度透明痛点,通过球鞋档案与成色评分系统解决了估值管理痛点,通过修复案例社区解决了服务信任痛点。这四个痛点的数字化解决构成了应用的核心价值闭环。
在技术架构层面,本项目采用七Tab内容切换架构——洗护、修复改色、真实案例、取送、球鞋档案、急救攻略、会员权益,覆盖了从下单到售后到社区运营的完整服务链条。底部五Tab导航对应订单、案例、加号发布、鞋档案、会员五个高频入口。四个弹框(新建洗护订单WASH ORDER、编辑球鞋档案SNEAKER FILE、删除球鞋档案黑橙警示卡、预约上门取送PICKUP SLIP)通过统一的modalOverlay管理。特效层fxLayer通过定时器驱动气泡上升、刷子晃动和闪光效果,为洗护车间场景注入了动态生命力。整个应用以沥青蓝渐变头部+白色卡片列表+荧光橙强调色的视觉层次展开,构建出专业、可信赖、有活力的球鞋服务社区体验。
一、色彩体系与@Observed数据模型
1.1 ColorPalette接口与COLORS常量实例化
interface ColorPalette {
primary: string;
primaryLight: string;
primaryDark: string;
accent: string;
accentLight: string;
bg: string;
cardBg: string;
textPrimary: string;
textSecondary: string;
textHint: string;
border: string;
success: string;
warning: string;
danger: string;
white: string;
gold: string;
}
const COLORS: ColorPalette = {
primary: '#37474F',
primaryLight: '#ECEFF1',
primaryDark: '#263238',
accent: '#FF6D00',
accentLight: '#FFF3E0',
bg: '#F5F7F8',
cardBg: '#FFFFFF',
textPrimary: '#22313A',
textSecondary: '#607680',
textHint: '#AEBFC6',
border: '#DFE7EA',
success: '#43A047',
warning: '#FB8C00',
danger: '#E53935',
white: '#FFFFFF',
gold: '#FFC400'
};

ColorPalette接口定义了16个色彩字段,覆盖了应用所需的全部色彩语义。primary取值#37474F是Material Design Blue Grey色板中的800级色调,介于深灰与深蓝之间,传达出洗护车间的工业专业感。primaryDark取值#263238用于渐变起点和深色背景区域,比主色更深一层。accent取值#FF6D00是Orange色板A700级,饱和度极高,是球鞋文化中常见的潮流荧光色,用于按钮、标签、价格等需要视觉突出的元素。accentLight取值#FFF3E0为浅橙色背景,用于标签底色和弹框头部。textPrimary取值#22313A是带蓝调的深色文本色,textSecondary取值#607680和textHint取值#AEBFC6构成三级文本灰阶。success/warning/danger三个功能色分别用于洗护完成(绿色)、清洗中(橙色)、取消订单(红色)的状态标记。gold金色用于会员等级标识。通过interface定义类型约束,所有色彩字段在编译期获得类型检查,杜绝了运行时因色彩字段缺失或拼写错误导致的问题。
1.2 @Observed数据模型类群
「球鞋急诊室」的七个@Observed数据模型类覆盖了洗护订单、修复服务、案例展示、取送时段、球鞋档案、攻略文章和月度消费统计等全部业务实体。
@Observed
class OrderItem {
id: number = 0;
shoe: string = '';
icon: string = '';
service: string = '';
price: number = 0;
status: number = 0;
eta: string = '';
constructor(id: number, shoe: string, icon: string, service: string,
price: number, status: number, eta: string) {
this.id = id; this.shoe = shoe; this.icon = icon;
this.service = service; this.price = price;
this.status = status; this.eta = eta;
}
}
@Observed
class ShoeItem {
id: number = 0;
name: string = '';
icon: string = '';
cw: string = '';
cond: number = 0;
value: number = 0;
constructor(id: number, name: string, icon: string, cw: string,
cond: number, value: number) {
this.id = id; this.name = name; this.icon = icon;
this.cw = cw; this.cond = cond; this.value = value;
}
}
@Observed
class SlotItem {
id: number = 0;
time: string = '';
quota: number = 0;
left: number = 0;
booked: number = 0;
constructor(id: number, time: string, quota: number,
left: number, booked: number) {
this.id = id; this.time = time; this.quota = quota;
this.left = left; this.booked = booked;
}
}

OrderItem是洗护订单的核心模型,shoe字段存储鞋款名称如"AJ1芝加哥",service字段存储服务类型如"深度清洁+去氧化",status用0/1区分"清洗中"和"已完成",eta字段存储预计送回时间如"明天18:00前送回"。ShoeItem是球鞋档案模型,cw字段存储配色英文名如"Chicago Red",cond字段存储成色评分(0-100),value字段存储估值金额。SlotItem是取送时段模型,quota字段存储该时段总名额,left字段存储剩余名额,booked字段标记当前用户是否已预约。@Observed装饰确保这些属性变更时自动触发UI重渲染——例如用户点击"预约"按钮后,SlotItem的booked属性从0变为1,left属性减1,对应的时段卡片会立即更新显示"已预约"状态和剩余名额数。
1.3 静态数据数组与辅助函数
const ORDER_LIST: OrderItem[] = [
new OrderItem(1, 'AJ1 芝加哥', '👟', '深度清洁+去氧化', 158, 0, '明天 18:00 前送回'),
new OrderItem(2, 'Yeezy 350 斑鸠', '🩶', '中底去黄', 128, 0, '后天 12:00 前送回'),
new OrderItem(3, 'Dunk 熊猫', '🐼', '标准水洗+除臭', 88, 1, '已送回'),
new OrderItem(4, 'NewBalance 990', '🇺🇸', '麂皮专用护理', 138, 0, '明天 20:00 前送回'),
new OrderItem(5, 'AJ4 黑红', '🔥', '深度清洁+补色', 198, 1, '已送回'),
];
const FIX_LIST: FixItem[] = [
new FixItem(1, '爆裂纹补色', '🎨', 'AJ3/AJ4 经典爆裂纹精补', 268, 7, 0),
new FixItem(2, '中底去黄', '🧪', '过氧化还原术,还原出厂白', 128, 5, 0),
new FixItem(3, '大底换底', '🔧', '整片换 Vibram 耐磨底', 380, 10, 0),
new FixItem(4, '鞋头改色', '🖌️', '定制改色,颜色任选', 218, 6, 0),
];
const SLOT_LIST: SlotItem[] = [
new SlotItem(1, '今天 10:00-12:00', 12, 5, 0),
new SlotItem(2, '今天 14:00-16:00', 12, 2, 0),
new SlotItem(3, '今天 18:00-20:00', 16, 9, 0),
new SlotItem(4, '明天 09:00-11:00', 12, 8, 0),
];
function spentBar(n: number): number {
return n / 20 + 10;
}
function condWord(c: number): string {
if (c >= 85) {
return '成色优秀';
}
if (c >= 70) {
return '成色良好';
}
return '需重点护理';
}

ORDER_LIST预置了12条洗护订单数据,覆盖AJ1、Yeezy、Dunk、NB等热门鞋款,服务类型从标准水洗到深度清洁到去氧化到补色修复,价格区间从68元到268元。FIX_LIST预置了8项修复服务,每项包含名称、描述、价格和工期天数,工期从1天到10天不等,为用户提供了清晰的时间预期。SLOT_LIST预置了8个取送时段,quota字段记录总名额(12或16),left字段记录剩余名额,当left小于4时前端会用accent色突出显示"名额紧张"。辅助函数spentBar将消费金额映射为柱状图高度(n/20+10),condWord将成色分数转换为文字描述——85分以上"成色优秀",70分以上"成色良好",70分以下"需重点护理"。这些辅助函数将业务规则封装为纯函数,保证逻辑的可测试性和复用性。
二、组件状态与生命周期管理
2.1 @State状态变量声明体系
@Entry
@Component
struct PageSneakerER {
@State currentTab: number = 0;
@State bottomTab: number = 0;
@State orderList: OrderItem[] = ORDER_LIST;
@State fixList: FixItem[] = FIX_LIST;
@State caseList: CaseItem[] = CASE_LIST;
@State slotList: SlotItem[] = SLOT_LIST;
@State shoeList: ShoeItem[] = SHOE_LIST;
@State guideList: GuideItem[] = GUIDE_LIST;
@State addOpen: boolean = false;
@State editOpen: boolean = false;
@State delOpen: boolean = false;
@State bizOpen: boolean = false;
@State addService: number = 1;
@State addPair: number = 1;
@State editCond: number = 85;
@State delIndex: number = 0;
@State bizTime: number = 0;
@State bizUrgent: number = 0;
@State fxTick: number = 0;
private timer: number = -1;

PageSneakerER组件声明了七个列表型@State变量(orderList到guideList),分别引用外部const数组作为初始值。四个布尔弹框状态变量(addOpen/editOpen/delOpen/bizOpen)控制四种弹框的显隐。addService存储洗护弹框中选中的服务类型索引(默认1即深度清洁),addPair存储鞋数(默认1双),editCond存储档案编辑弹框中的成色评分(默认85分),bizTime存储取送弹框中选中的时间索引,bizUrgent用0/1标记是否加急。fxTick是特效驱动变量。每个@State变量在变更时都会自动触发引用了该变量的UI区域重渲染,这种细粒度的响应式更新是ArkTS的核心优势——例如修改addPair从1到2时,只有弹框中显示鞋数和合计金额的Text组件会重渲染,其他UI元素不受影响。
2.2 生命周期定时器与业务方法
aboutToAppear(): void {
this.timer = setInterval(() => {
this.fxTick += 1;
}, 110);
}
aboutToDisappear(): void {
if (this.timer >= 0) {
clearInterval(this.timer);
}
}
doAdd(): void {
this.orderList.unshift(new OrderItem(997, 'AJ1 芝加哥', '👟',
ADD_SERVICES[this.addService],
(this.addService + 1) * 60 * this.addPair, 0, '48 小时内送回'));
this.addOpen = false;
}
doEdit(): void {
if (this.shoeList.length > 0) {
this.shoeList[0].cond = this.editCond;
this.shoeList[0].value = this.editCond * 48;
this.shoeList.splice(0, 1, this.shoeList[0]);
}
this.editOpen = false;
}
doDel(): void {
if (this.delIndex >= 0 && this.delIndex < this.shoeList.length) {
this.shoeList.splice(this.delIndex, 1);
}
this.delOpen = false;
}

定时器以110毫秒间隔递增fxTick,驱动气泡上升、刷子晃动和闪光特效动画。doAdd方法在提交洗护订单时,根据选中的服务类型索引和鞋数计算价格((addService+1)60addPair),通过unshift将新订单插入列表头部并关闭弹框。doEdit方法将编辑弹框中的成色评分editCond写入shoeList首项的cond属性,并根据成色重新计算估值(editCond*48),通过splice触发重渲染。doDel方法从shoeList中删除指定索引的球鞋档案。这些业务方法的共同模式是:修改@Observed类实例属性 → splice替换数组元素 → 关闭弹框。这一三步操作序列是ArkTS中处理列表数据变更的标准范式。
三、特效层与头部导航
3.1 fxLayer特效构建器
@Builder
fxLayer() {
Column() {
Text('🫧')
.fontSize(16)
.opacity(this.fxTick % 2 === 0 ? 1 : 0.25)
.translate({ y: this.fxTick % 4 === 0 ? 0 : -12 })
.position({ x: 40, y: 560 })
Text('🫧')
.fontSize(11)
.opacity(this.fxTick % 2 === 1 ? 1 : 0.2)
.translate({ y: this.fxTick % 3 === 0 ? 0 : -9 })
.position({ x: 330, y: 610 })
Text('🧽')
.fontSize(20)
.rotate({ angle: (this.fxTick % 4) * 14 - 21,
centerX: '50%', centerY: '50%' })
.position({ x: 30, y: 630 })
Text('✨')
.fontSize(14)
.fontColor(COLORS.gold)
.opacity(this.fxTick % 3 === 0 ? 1 : 0.15)
.position({ x: 300, y: 78 })
Text('👟')
.fontSize(18)
.rotate({ angle: (this.fxTick % 6) * 8 - 20,
centerX: '50%', centerY: '50%' })
.position({ x: 320, y: 640 })
}
.width('100%')
.height('100%')
.hitTestBehavior(HitTestMode.None)
.position({ x: 0, y: 0 })
}
fxLayer实现了五种特效元素,生动地模拟了洗护车间的动态场景。两个气泡🫧通过translate的y轴位移实现上升效果——第一个气泡以4为周期在0和-12之间交替位移,配合opacity在1和0.25之间闪烁;第二个气泡以3为周期在0和-9之间交替,配合opacity在1和0.2之间交替但相位相反,两个气泡的非同步运动产生了自然的漂浮感。刷子🧽通过rotate实现晃动效果,angle值根据fxTick对4取模乘以14减去21,产生从-21度到21度的摆动范围。闪光✨用金色字体配合3周期的opacity闪烁,模拟洗护完成后的闪亮效果。球鞋👟通过6周期的旋转实现轻微摇摆,angle从-20度到20度循环,模拟球鞋在传送带上的运动感。整个特效层设置HitTestMode.None确保不拦截触摸事件,position绝对定位铺满全屏。
3.2 header头部构建器
@Builder
header() {
Column() {
Row() {
Column() {
Text('👟 球鞋急诊室')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
Text('已抢救 48,291 双 · 今日还可上门取鞋 312 单')
.fontSize(11)
.fontColor('#CFD8DC')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
Row() {
Text('🛵')
.fontSize(18)
.padding(8)
.backgroundColor('#33FFFFFF')
.borderRadius(18)
.onClick(() => {
this.bizOpen = true;
})
Text('🔔')
.fontSize(18)
.padding(8)
.backgroundColor('#33FFFFFF')
.borderRadius(18)
.margin({ left: 8 })
}
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 16, right: 16, top: 14 })
Row() {
Text('🔍 搜鞋款 / 查修复案例 / 约取送')
.fontSize(12)
.fontColor('#B4C4CB')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.backgroundColor('#33FFFFFF')
.borderRadius(18)
.layoutWeight(1)
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
Row() {
Text('⚡ 2 小时上门')
.fontSize(10)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor('#33FFFFFF')
.borderRadius(12)
Text('🧪 中底去黄 5 折')
.fontSize(10)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor('#33FFFFFF')
.borderRadius(12)
.margin({ left: 6 })
Text('💳 学生认证立减 20')
.fontSize(10)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor('#33FFFFFF')
.borderRadius(12)
.margin({ left: 6 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 10 })
}
.width('100%')
.linearGradient({
angle: 160,
colors: [['#263238', 0], ['#37474F', 0.6], ['#546E7A', 1]]
})
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
.padding({ bottom: 14 })
}

header构建器以linearGradient渐变背景呈现,从#263238(最深沥青蓝)过渡到#546E7A(浅蓝灰),160度方向营造出从上到下由深到浅的层次感。顶部Row左侧展示应用名"👟 球鞋急诊室"和运营数据"已抢救48,291双·今日还可上门取鞋312单",右侧放置取送按钮🛵(点击触发取送弹框)和通知铃铛🔔。搜索框以半透明白色#33FFFFFF叠加渐变背景上,提示文案"搜鞋款/查修复案例/约取送"涵盖了三大核心功能。底部促销标签行展示三个运营信息——"⚡2小时上门"强调取送速度、"🧪中底去黄5折"突出单项优惠、"💳学生认证立减20"针对学生客群。底部圆角24实现与内容区的柔和过渡。
3.3 hangTab吊牌式Tab与subNav
@Builder
hangTab(name: string, icon: string, idx: number) {
Column() {
Row() {
Circle()
.width(5)
.height(5)
.fill(idx === this.currentTab ? COLORS.accent : COLORS.textHint)
Row()
.width(16)
.height(2)
.backgroundColor(idx === this.currentTab ?
COLORS.accent : COLORS.textHint)
.margin({ left: 2 })
}
.justifyContent(FlexAlign.Center)
.margin({ top: 4 })
Column() {
Text(icon)
.fontSize(16)
Text(name)
.fontSize(10)
.fontWeight(idx === this.currentTab ?
FontWeight.Bold : FontWeight.Normal)
.fontColor(idx === this.currentTab ?
COLORS.white : COLORS.textSecondary)
.margin({ top: 2 })
}
.padding({ left: 8, right: 8, top: 4, bottom: 6 })
.backgroundColor(idx === this.currentTab ?
COLORS.primaryDark : COLORS.cardBg)
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.currentTab = idx;
})
}
@Builder
subNav() {
Column() {
Row() {
this.hangTab('洗护', '🧼', 0)
this.hangTab('修复改色', '🎨', 1)
this.hangTab('真实案例', '📸', 2)
this.hangTab('取送', '🛵', 3)
}
.width('100%')
Row() {
this.hangTab('球鞋档案', '🗂️', 4)
this.hangTab('急救攻略', '🩹', 5)
this.hangTab('会员权益', '💳', 6)
Column()
.layoutWeight(1)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding({ left: 10, right: 10, top: 10, bottom: 10 })
.backgroundColor(COLORS.bg)
}

hangTab构建器是「球鞋急诊室」最具辨识度的UI组件——吊牌式Tab设计。每个Tab由顶部的圆孔+挂绳线和下方的标签牌组成,模拟球鞋盒上的吊牌形态。圆孔用一个5px的Circle表示,挂绳用16x2的Row表示,选中态为accent橙色,未选中态为textHint灰色。标签牌用Column承载icon和name,选中态背景为primaryDark深色(模拟吊牌翻转),未选中态为cardBg白色。subNav以4+3双排布局排列七个吊牌Tab,第一排洗护、修复改色、真实案例、取送,第二排球鞋档案、急救攻略、会员权益加一个空占位。这种吊牌式设计将球鞋零售的物理元素融入数字界面,使得应用的视觉语言与业务场景高度统一。
四、内容页面构建器群
4.1 pageWash洗护订单页面
@Builder
pageWash() {
Column() {
Row() {
Text('🧼 洗护订单')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('新建订单')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.backgroundColor(COLORS.accent)
.borderRadius(12)
.margin({ left: 'auto' })
.onClick(() => {
this.addOpen = true;
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
Row() {
Column() {
Text('88')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text('标准水洗起')
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
// ... 其余三列同理
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ top: 10 })
ForEach(this.orderList, (o: OrderItem, oi: number) => {
Column() {
Row() {
Text(o.icon)
.fontSize(26)
.padding(12)
.backgroundColor(COLORS.primaryLight)
.borderRadius(12)
Column() {
Text(o.shoe)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.maxLines(1)
Text(o.service)
.fontSize(10)
.fontColor(COLORS.textSecondary)
.margin({ top: 3 })
Text('🕐 ' + o.eta)
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text('¥' + o.price.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text(o.status === 1 ? '已完成' : '清洗中')
.fontSize(9)
.fontColor(o.status === 1 ? COLORS.success : COLORS.warning)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor(o.status === 1 ? '#E8F5E9' : '#FFF8E1')
.borderRadius(8)
.margin({ top: 4 })
.onClick(() => {
o.status = o.status === 1 ? 0 : 1;
this.orderList.splice(oi, 1, o);
})
}
.alignItems(HorizontalAlign.End)
}
.width('100%')

pageWash页面顶部是标题行和"新建订单"按钮(accent橙底白字),点击触发addOpen弹框。价格概览卡片以四列等宽布局展示88元/128元/268元/380元四档价格锚点,帮助用户快速建立价格预期。订单列表ForEach渲染每条订单,左侧emoji图标以primaryLight背景圆角展示,中间信息区展示鞋款名、服务类型和预计送回时间,右侧展示价格(accent橙色加粗)和状态标签。状态标签通过status属性的0/1切换——status为1时显示绿色"已完成"(success色#E8F5E9底),status为0时显示橙色"清洗中"(warning色#FFF8E1底)。点击状态标签可切换状态,配合splice触发重渲染。底部操作行包含"🛵上门取送"按钮(触发取送弹框)和"取消订单"按钮(红色danger直接splice删除),以及"师傅已接单1,203次"的信任提示。
4.2 pageCase真实案例页面
@Builder
pageCase() {
Column() {
Row() {
Text('📸 抢救实录')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('换一批')
.fontSize(11)
.fontColor(COLORS.primary)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(COLORS.primaryLight)
.borderRadius(12)
.margin({ left: 'auto' })
.onClick(() => {
this.doChangeCase();
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
ForEach(this.caseList, (c: CaseItem) => {
Column() {
Row() {
Text('🧼 修复前')
.fontSize(9)
.fontColor(COLORS.white)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(COLORS.primaryDark)
.borderRadius(8)
Text('→')
.fontSize(12)
.fontColor(COLORS.textHint)
.margin({ left: 8 })
Text('✨ 修复后')
.fontSize(9)
.fontColor(COLORS.white)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(COLORS.accent)
.borderRadius(8)
.margin({ left: 8 })
Text(c.days.toString() + ' 天交付')
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ left: 'auto' })
}
.width('100%')
Text(c.shoe + ' · ' + c.issue)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 10 })
Row() {
Text('师傅:金牌修复师大飞')
.fontSize(9)
.fontColor(COLORS.textSecondary)
Text(c.liked === 1 ? '❤️ ' + (c.likes + 1).toString() :
'🤍 ' + c.likes.toString())
.fontSize(11)
.fontColor(c.liked === 1 ? COLORS.accent : COLORS.textSecondary)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(c.liked === 1 ?
COLORS.accentLight : COLORS.bg)
.borderRadius(10)
.margin({ left: 'auto' })
.onClick(() => {
c.liked = c.liked === 1 ? 0 : 1;
this.caseList.splice(c.id - 1, 1, c);
})
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ top: 10 })
}, (c: CaseItem) => 'cs' + c.id.toString() + '_' + c.liked.toString())
}
.width('100%')
.padding({ left: 12, right: 12, bottom: 16 })
}
pageCase页面是社区信任建设的核心——通过真实修复案例展示服务能力。顶部"换一批"按钮调用doChangeCase方法轮换案例列表。每张案例卡片顶部以"修复前→修复后"的标签行展示修复对比效果——"🧼修复前"用primaryDark深色底白字,“✨修复后"用accent橙色底白字,中间用箭头连接,直观呈现修复前后的变化。卡片中部展示鞋款名和问题描述(如"AJ1阴阳·鞋头发黄严重”),底部展示修复师信息和点赞按钮。点赞按钮通过liked属性的0/1切换——未点赞时显示🤍+点赞数(灰色bg底),点赞后显示❤️+点赞数+1(accent橙色accentLight底)。ForEach键值拼接了id和liked,确保点赞状态变化时正确触发diff更新。
4.3 pageFile球鞋档案与成色评分
@Builder
pageFile() {
Column() {
Row() {
Text('🗂️ 我的球鞋档案')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(this.shoeList.length.toString() + ' 双')
.fontSize(10)
.fontColor(COLORS.textHint)
.margin({ left: 'auto' })
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
ForEach(this.shoeList, (sh: ShoeItem, si: number) => {
Column() {
Row() {
Text(sh.icon)
.fontSize(30)
.padding(14)
.backgroundColor(COLORS.primaryLight)
.borderRadius(14)
Column() {
Text(sh.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(sh.cw)
.fontSize(10)
.fontColor(COLORS.textSecondary)
.margin({ top: 2 })
Text(condWord(sh.cond) + ' · 估值 ¥' + sh.value.toString())
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text(sh.cond.toString() + '分')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(sh.cond >= 85 ? COLORS.success :
(sh.cond >= 70 ? COLORS.warning : COLORS.danger))
Text('成色分')
.fontSize(8)
.fontColor(COLORS.textHint)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row() {
Text('✏️ 编辑档案')
.fontSize(10)
.fontColor(COLORS.primary)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(COLORS.primaryLight)
.borderRadius(8)
.onClick(() => {
this.editOpen = true;
})
Text('🗑️ 删除档案')
.fontSize(10)
.fontColor(COLORS.danger)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(COLORS.accentLight)
.borderRadius(8)
.margin({ left: 8 })
.onClick(() => {
this.delIndex = si;
this.delOpen = true;
})
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ top: 10 })
}, (sh: ShoeItem) => 'sh' + sh.id.toString() + '_' + sh.cond.toString())
pageFile页面是球鞋档案管理的核心——每双鞋的数字身份卡。档案卡片左侧以30号字体的emoji图标配合primaryLight背景展示鞋款形象,中间信息区展示鞋款名称、配色英文名(cw)和成色文字描述+估值。右侧成色分数以16号加粗字体展示,颜色根据分数动态变化——85分以上success绿色、70分以上warning橙色、70分以下danger红色,实现了成色分数到视觉色彩的直观映射。卡片底部"编辑档案"按钮触发editOpen弹框,"删除档案"按钮记录delIndex并触发delOpen警示弹框。ForEach键值拼接了id和cond,确保成色评分修改时正确刷新。
五、弹框系统与底部导航
5.1 modalBodyAdd洗护订单弹框
@Builder
modalBodyAdd() {
Column() {
Column() {
Text('WASH ORDER · 洗护单')
.fontSize(9)
.letterSpacing(2)
.fontColor(COLORS.accent)
Text('🧼 新建洗护订单')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 4 })
Text('48 小时焕新送回 · 支持 12 类鞋面材质')
.fontSize(10)
.fontColor(COLORS.textSecondary)
.margin({ top: 3 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.accentLight)
.borderRadius(12)
Column() {
Text('服务类型')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Row() {
ForEach(ADD_SERVICES, (sv: string, si: number) => {
Text(sv)
.fontSize(11)
.fontColor(this.addService === si ?
COLORS.white : COLORS.textSecondary)
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.backgroundColor(this.addService === si ?
COLORS.primary : COLORS.bg)
.borderRadius(10)
.margin({ right: 6 })
.onClick(() => {
this.addService = si;
})
}, (sv: string) => 'sv' + sv)
}
.margin({ top: 8 })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.margin({ top: 14 })
WASH ORDER弹框头部以accentLight浅橙底展示弹框代号"WASH ORDER"、功能标题"🧼新建洗护订单"和承诺文案"48小时焕新送回·支持12类鞋面材质"。服务类型选择区通过ForEach遍历ADD_SERVICES数组(标准水洗/深度清洁/中底去黄/补色修复),选中态为primary深色底白字,未选中态为bg背景底次要色字。鞋数选择器采用"-数值+"三段式步进器,decPair和incPair方法在1到5之间调整。合计金额实时计算((addService+1)60addPair)并展示在primaryLight底的提示行中,"含防尘袋+鞋撑送回·赠除臭一次"作为增值服务说明。底部"取消"和"提交订单"按钮分别关闭弹框和调用doAdd方法。
5.2 modalBodyDel删除档案黑橙警示卡
@Builder
modalBodyDel() {
Column() {
Column() {
Text('⚠️ DELETE FILE · 删除档案')
.fontSize(9)
.letterSpacing(2)
.fontColor(COLORS.danger)
Text('确认删除这双鞋的档案吗?')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 6 })
}
.width('100%')
.padding(14)
.backgroundColor('#263238')
.borderRadius(12)
Row() {
Column() {
Text('12')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.danger)
Text('护理记录将清空')
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text('¥4,200')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.danger)
Text('估值档案将移除')
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text('36')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text('合影照片保留')
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.margin({ top: 14 })
Text('删除后不可恢复,洗护订单记录将归档到「历史订单」,球鞋合影仍在相册中。')
.fontSize(10)
.fontColor(COLORS.textSecondary)
.margin({ top: 12 })
删除档案弹框采用了黑橙警示卡的设计语言——与麻将项目的红绿警示卡不同,这里使用#263238深黑色头部背景配合danger红色标签,传递出工业级删除操作的严肃感。三列数据卡片展示删除影响:左列"12条护理记录将清空"用danger红色,中列"¥4,200估值档案将移除"用danger红色强调财产损失,右列"36张合影照片保留"用accent橙色表示不受影响的数据。这种黑底红字的警示设计比传统的纯红色弹框更加沉稳,适合球鞋收藏这一对财产敏感的场景。底部说明文案详细解释了删除后数据流向——“洗护订单记录归档到历史订单”“球鞋合影仍在相册中”,通过透明的数据说明降低用户的删除焦虑。
5.3 modalOverlay与bottomBar
@Builder
modalOverlay() {
Stack({ alignContent: Alignment.Bottom }) {
Column()
.width('100%')
.height('100%')
.backgroundColor('#66000000')
.onClick(() => {
this.addOpen = false;
this.editOpen = false;
this.delOpen = false;
this.bizOpen = false;
})
Column() {
if (this.addOpen) {
this.modalBodyAdd()
}
if (this.editOpen) {
this.modalBodyEdit()
}
if (this.delOpen) {
this.modalBodyDel()
}
if (this.bizOpen) {
this.modalBodyBiz()
}
}
.width('92%')
.margin({ bottom: 20 })
}
.width('100%')
.height('100%')
}
@Builder
bottomBar() {
Row() {
Column() {
Text('🧼')
.fontSize(20)
Text('订单')
.fontSize(10)
.fontColor(this.bottomTab === 0 ? COLORS.primary : COLORS.textHint)
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => {
this.bottomTab = 0;
this.currentTab = 0;
})
// ... 其他Tab
Column() {
Column() {
Text('➕')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
}
.width(48)
.height(48)
.justifyContent(FlexAlign.Center)
.backgroundColor(COLORS.accent)
.borderRadius(24)
.offset({ y: -14 })
.onClick(() => {
this.addOpen = true;
})
}
.layoutWeight(1)
}
.width('100%')
.padding({ top: 8, bottom: 8 })
.backgroundColor(COLORS.cardBg)
.borderRadius({ topLeft: 20, topRight: 20 })
}
modalOverlay的架构与麻将项目一致——Stack底层为半透明遮罩,顶层为92%宽度的弹框内容区。bottomBar底部导航五个Tab为订单、案例、凸起加号、鞋档案、会员,中间加号按钮使用accent橙色圆形背景向上偏移14px,点击触发addOpen弹框。选中态Tab文字使用primary主色,未选中态使用textHint提示色。底部导航与内容Tab的映射关系为:订单→Tab0洗护、案例→Tab2真实案例、鞋档案→Tab4球鞋档案、会员→Tab6会员权益,实现了底部5Tab与内容7Tab的交叉导航。
六、build主架构与页面路由
@Builder
mainContent() {
Column() {
this.header()
this.subNav()
Scroll() {
Column() {
if (this.currentTab === 0) {
this.pageWash()
}
if (this.currentTab === 1) {
this.pageFix()
}
if (this.currentTab === 2) {
this.pageCase()
}
if (this.currentTab === 3) {
this.pagePickup()
}
if (this.currentTab === 4) {
this.pageFile()
}
if (this.currentTab === 5) {
this.pageGuide()
}
if (this.currentTab === 6) {
this.pageVip()
}
}
.width('100%')
}
.layoutWeight(1)
.scrollBar(BarState.Off)
}
.width('100%')
.height('100%')
}
build() {
Stack() {
this.mainContent()
this.fxLayer()
if (this.addOpen || this.editOpen || this.delOpen || this.bizOpen) {
this.modalOverlay()
}
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
}
mainContent组织了七个内容页面的条件渲染,Scroll组件包裹所有页面,scrollBar隐藏保证视觉洁净。build方法以Stack三层叠加架构组织应用——mainContent主内容层、fxLayer特效层、条件渲染的modalOverlay弹框层。七个内容页面覆盖了球鞋洗护的完整服务链:pageWash洗护订单管理、pageFix修复改色服务、pageCase真实案例社区、pagePickup上门取送预约、pageFile球鞋档案管理、pageGuide急救攻略内容、pageVip会员权益体系。这种从交易到内容到会员的全链路页面布局,体现了垂直社区应用的产品完整性。
七、组件架构流程图
八、数据模型对比表
| 数据模型 | 字段数量 | 核心字段 | 业务场景 | 交互状态字段 |
|---|---|---|---|---|
| OrderItem | 7 | shoe, service, price, status, eta | 洗护订单列表 | status (0清洗中/1已完成) |
| FixItem | 7 | name, desc, price, days | 修复改色服务列表 | booked (0/1预约状态) |
| CaseItem | 6 | shoe, issue, days, likes | 抢救案例社区 | liked (0/1点赞状态) |
| SlotItem | 5 | time, quota, left | 取送时段列表 | booked (0/1预约状态) |
| ShoeItem | 6 | name, cw, cond, value | 球鞋档案管理 | 无 (通过editCond间接管理) |
| GuideItem | 6 | title, author, reads, saves | 攻略文章列表 | saved (0/1收藏状态) |
| MonthItem | 4 | month, bills, spent | 月度消费柱状图 | 无 (纯展示数据) |
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

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

完整代码:
// 风格:沥青蓝+荧光橙;内容tab:7个双排4+3鞋盒吊牌式(顶部圆孔+挂绳线,选中吊牌整体变深);底部5主tab
// 弹框:新建洗护订单(洗护单WASH ORDER)/ 编辑球鞋档案(档案卡SNEAKER FILE)/ 删除球鞋档案(黑橙警示卡)/ 预约上门取送(取送单PICKUP SLIP)
// 特效:气泡上升 + 刷子晃动 + 闪光
interface ColorPalette {
primary: string;
primaryLight: string;
primaryDark: string;
accent: string;
accentLight: string;
bg: string;
cardBg: string;
textPrimary: string;
textSecondary: string;
textHint: string;
border: string;
success: string;
warning: string;
danger: string;
white: string;
gold: string;
}
const COLORS: ColorPalette = {
primary: '#37474F',
primaryLight: '#ECEFF1',
primaryDark: '#263238',
accent: '#FF6D00',
accentLight: '#FFF3E0',
bg: '#F5F7F8',
cardBg: '#FFFFFF',
textPrimary: '#22313A',
textSecondary: '#607680',
textHint: '#AEBFC6',
border: '#DFE7EA',
success: '#43A047',
warning: '#FB8C00',
danger: '#E53935',
white: '#FFFFFF',
gold: '#FFC400'
};
@Observed
class OrderItem {
id: number = 0;
shoe: string = '';
icon: string = '';
service: string = '';
price: number = 0;
status: number = 0;
eta: string = '';
constructor(id: number, shoe: string, icon: string, service: string, price: number, status: number, eta: string) {
this.id = id; this.shoe = shoe; this.icon = icon; this.service = service; this.price = price;
this.status = status; this.eta = eta;
}
}
@Observed
class FixItem {
id: number = 0;
name: string = '';
icon: string = '';
desc: string = '';
price: number = 0;
days: number = 0;
booked: number = 0;
constructor(id: number, name: string, icon: string, desc: string, price: number, days: number, booked: number) {
this.id = id; this.name = name; this.icon = icon; this.desc = desc; this.price = price; this.days = days; this.booked = booked;
}
}
@Observed
class CaseItem {
id: number = 0;
shoe: string = '';
issue: string = '';
days: number = 0;
likes: number = 0;
liked: number = 0;
constructor(id: number, shoe: string, issue: string, days: number, likes: number, liked: number) {
this.id = id; this.shoe = shoe; this.issue = issue; this.days = days; this.likes = likes; this.liked = liked;
}
}
@Observed
class SlotItem {
id: number = 0;
time: string = '';
quota: number = 0;
left: number = 0;
booked: number = 0;
constructor(id: number, time: string, quota: number, left: number, booked: number) {
this.id = id; this.time = time; this.quota = quota; this.left = left; this.booked = booked;
}
}
@Observed
class ShoeItem {
id: number = 0;
name: string = '';
icon: string = '';
cw: string = '';
cond: number = 0;
value: number = 0;
constructor(id: number, name: string, icon: string, cw: string, cond: number, value: number) {
this.id = id; this.name = name; this.icon = icon; this.cw = cw; this.cond = cond; this.value = value;
}
}
@Observed
class GuideItem {
id: number = 0;
title: string = '';
author: string = '';
reads: number = 0;
saves: number = 0;
saved: number = 0;
constructor(id: number, title: string, author: string, reads: number, saves: number, saved: number) {
this.id = id; this.title = title; this.author = author; this.reads = reads; this.saves = saves; this.saved = saved;
}
}
@Observed
class MonthItem {
id: number = 0;
month: string = '';
bills: number = 0;
spent: number = 0;
constructor(id: number, month: string, bills: number, spent: number) {
this.id = id; this.month = month; this.bills = bills; this.spent = spent;
}
}
const ORDER_LIST: OrderItem[] = [
new OrderItem(1, 'AJ1 芝加哥', '👟', '深度清洁+去氧化', 158, 0, '明天 18:00 前送回'),
new OrderItem(2, 'Yeezy 350 斑鸠', '🩶', '中底去黄', 128, 0, '后天 12:00 前送回'),
new OrderItem(3, 'Dunk 熊猫', '🐼', '标准水洗+除臭', 88, 1, '已送回'),
new OrderItem(4, 'NewBalance 990', '🇺🇸', '麂皮专用护理', 138, 0, '明天 20:00 前送回'),
new OrderItem(5, 'AJ4 黑红', '🔥', '深度清洁+补色', 198, 1, '已送回'),
new OrderItem(6, '椰子 700 静电灰', '🩶', '鞋面去污+除臭', 98, 0, '明天 18:00 前送回'),
new OrderItem(7, '匡威 1970s 黑高', '🖤', '标准水洗', 68, 1, '已送回'),
new OrderItem(8, 'Vans 棋盘格', '♟️', '大底胶边清洁', 78, 0, '后天 12:00 前送回'),
new OrderItem(9, 'AJ3 白水泥', '🤍', '爆裂纹补色修复', 268, 0, '周五 18:00 前送回'),
new OrderItem(10, '巴黎世家 老爹鞋', '🏙️', '整体深度护理', 228, 1, '已送回'),
new OrderItem(11, ' Timberland 大黄靴', '🥾', '防水蜡重新打蜡', 188, 0, '周六 12:00 前送回'),
new OrderItem(12, '拖鞋全体 cleanup', '🩴', '三双打包水洗', 108, 0, '明天 18:00 前送回')
];
const FIX_LIST: FixItem[] = [
new FixItem(1, '爆裂纹补色', '🎨', 'AJ3/AJ4 经典爆裂纹精补', 268, 7, 0),
new FixItem(2, '中底去黄', '🧪', '过氧化还原术,还原出厂白', 128, 5, 0),
new FixItem(3, '大底换底', '🔧', '整片换 Vibram 耐磨底', 380, 10, 0),
new FixItem(4, '鞋头改色', '🖌️', '定制改色,颜色任选', 218, 6, 0),
new FixItem(5, '气垫换新', '💨', '全掌/后掌气垫更换', 320, 8, 0),
new FixItem(6, '鞋带定制', '🪢', '原厂规格定制染色鞋带', 58, 2, 0),
new FixItem(7, '麂皮翻新', '🧲', '麂皮刷毛恢复+补色', 138, 4, 0),
new FixItem(8, '除臭杀菌', '🦠', '臭氧+紫外线双重杀菌', 68, 1, 0)
];
const CASE_LIST: CaseItem[] = [
new CaseItem(1, 'AJ1 阴阳', '鞋头发黄严重', 6, 3210, 0),
new CaseItem(2, 'Dunk 熊猫', '中底开胶掉渣', 8, 2870, 0),
new CaseItem(3, 'AJ4 白红', '爆裂纹剥落', 7, 4503, 0),
new CaseItem(4, 'Yeezy 350', '编织面破洞', 9, 1932, 0),
new CaseItem(5, 'AJ11 康扣', '漆皮起皱发白', 10, 5120, 0),
new CaseItem(6, 'NB 990', '麂皮油渍渗透', 5, 1678, 0),
new CaseItem(7, 'AJ6 樱桃', '透明底氧化发黄', 12, 6203, 0),
new CaseItem(8, '椰子 700', '鞋身整体脏污', 4, 1145, 0),
new CaseItem(9, '匡威 1970s', '鞋头橡胶开裂', 6, 2388, 0),
new CaseItem(10, 'AJ5 联名', '反光银涂层脱落', 9, 3860, 0)
];
const SLOT_LIST: SlotItem[] = [
new SlotItem(1, '今天 10:00-12:00', 12, 5, 0),
new SlotItem(2, '今天 14:00-16:00', 12, 2, 0),
new SlotItem(3, '今天 18:00-20:00', 16, 9, 0),
new SlotItem(4, '明天 09:00-11:00', 12, 8, 0),
new SlotItem(5, '明天 15:00-17:00', 12, 3, 0),
new SlotItem(6, '明天 19:00-21:00', 16, 11, 0),
new SlotItem(7, '后天 10:00-12:00', 12, 10, 0),
new SlotItem(8, '后天 16:00-18:00', 12, 12, 0)
];
const SHOE_LIST: ShoeItem[] = [
new ShoeItem(1, 'AJ1 芝加哥', '👟', 'Chicago Red', 85, 4200),
new ShoeItem(2, 'Yeezy 350 斑鸠', '🩶', 'Zebra', 70, 2600),
new ShoeItem(3, 'Dunk 熊猫', '🐼', 'Panda', 90, 1300),
new ShoeItem(4, 'AJ4 黑红', '🔥', 'Bred', 65, 3100),
new ShoeItem(5, 'AJ3 白水泥', '🤍', 'White Cement', 55, 3800),
new ShoeItem(6, 'NB 990 元祖灰', '🇺🇸', 'Grey', 75, 1800),
new ShoeItem(7, 'AJ11 康扣', '🐧', 'Concord', 60, 2400),
new ShoeItem(8, '巴黎世家 老爹鞋', '🏙️', 'Triple S', 80, 5600)
];
const GUIDE_LIST: GuideItem[] = [
new GuideItem(1, '白鞋发黄别扔!厨房纸+这瓶水搞定', '洗护主理人老陈', 82000, 15000, 0),
new GuideItem(2, '麂皮沾油第一时间这样做', '球鞋医生阿Ken', 46000, 9800, 0),
new GuideItem(3, 'AJ 漆皮起皱的日常预防手法', '收藏家大白', 39000, 7600, 0),
new GuideItem(4, '中底氧化还原的 3 个段位', '实验室小周', 67000, 12000, 0),
new GuideItem(5, '鞋柜防潮除臭终极方案', '生活家大熊', 28000, 5100, 0),
new GuideItem(6, '爆裂纹补色自己能动手吗', '修复师大飞', 51000, 8900, 0),
new GuideItem(7, '透明底水晶底的保存禁忌', '收藏家大白', 73000, 13000, 0),
new GuideItem(8, '一鞋一档:估值怎么算', '鉴定师兰姐', 33000, 6200, 0)
];
const MONTH_LIST: MonthItem[] = [
new MonthItem(1, '3月', 4, 520),
new MonthItem(2, '4月', 6, 880),
new MonthItem(3, '5月', 5, 640),
new MonthItem(4, '6月', 7, 1020),
new MonthItem(5, '7月', 6, 760),
new MonthItem(6, '8月', 9, 1240)
];
const ADD_SERVICES: string[] = ['🧼 标准水洗', '✨ 深度清洁', '🧪 中底去黄', '🎨 补色修复'];
const PICK_TIMES: string[] = ['今天', '明天', '后天'];
const VIP_RIGHTS: string[] = ['上门取送免运费', '洗护 88 折', '修复免手工费', '生日月双倍积分'];
function spentBar(n: number): number {
return n / 20 + 10;
}
function condWord(c: number): string {
if (c >= 85) {
return '成色优秀';
}
if (c >= 70) {
return '成色良好';
}
return '需重点护理';
}
@Entry
@Component
struct PageSneakerER {
@State currentTab: number = 0;
@State bottomTab: number = 0;
@State orderList: OrderItem[] = ORDER_LIST;
@State fixList: FixItem[] = FIX_LIST;
@State caseList: CaseItem[] = CASE_LIST;
@State slotList: SlotItem[] = SLOT_LIST;
@State shoeList: ShoeItem[] = SHOE_LIST;
@State guideList: GuideItem[] = GUIDE_LIST;
@State addOpen: boolean = false;
@State editOpen: boolean = false;
@State delOpen: boolean = false;
@State bizOpen: boolean = false;
@State addService: number = 1;
@State addPair: number = 1;
@State editCond: number = 85;
@State delIndex: number = 0;
@State bizTime: number = 0;
@State bizUrgent: number = 0;
@State fxTick: number = 0;
private timer: number = -1;
aboutToAppear(): void {
this.timer = setInterval(() => {
this.fxTick += 1;
}, 110);
}
aboutToDisappear(): void {
if (this.timer >= 0) {
clearInterval(this.timer);
}
}
doAdd(): void {
this.orderList.unshift(new OrderItem(997, 'AJ1 芝加哥', '👟', ADD_SERVICES[this.addService], (this.addService + 1) * 60 * this.addPair, 0, '48 小时内送回'));
this.addOpen = false;
}
doEdit(): void {
if (this.shoeList.length > 0) {
this.shoeList[0].cond = this.editCond;
this.shoeList[0].value = this.editCond * 48;
this.shoeList.splice(0, 1, this.shoeList[0]);
}
this.editOpen = false;
}
doDel(): void {
if (this.delIndex >= 0 && this.delIndex < this.shoeList.length) {
this.shoeList.splice(this.delIndex, 1);
}
this.delOpen = false;
}
doBiz(): void {
if (this.slotList.length > 0) {
this.slotList[0].booked = 1;
this.slotList.splice(0, 1, this.slotList[0]);
}
this.bizOpen = false;
}
doChangeCase(): void {
if (this.caseList.length > 2) {
this.caseList.unshift(this.caseList[this.caseList.length - 1]);
this.caseList.splice(this.caseList.length - 1, 1);
}
}
incPair(): void {
if (this.addPair < 5) {
this.addPair += 1;
}
}
decPair(): void {
if (this.addPair > 1) {
this.addPair -= 1;
}
}
incCond(): void {
if (this.editCond < 98) {
this.editCond += 3;
}
}
decCond(): void {
if (this.editCond > 40) {
this.editCond -= 3;
}
}
@Builder
fxLayer() {
Column() {
Text('🫧')
.fontSize(16)
.opacity(this.fxTick % 2 === 0 ? 1 : 0.25)
.translate({ y: this.fxTick % 4 === 0 ? 0 : -12 })
.position({ x: 40, y: 560 })
Text('🫧')
.fontSize(11)
.opacity(this.fxTick % 2 === 1 ? 1 : 0.2)
.translate({ y: this.fxTick % 3 === 0 ? 0 : -9 })
.position({ x: 330, y: 610 })
Text('🧽')
.fontSize(20)
.rotate({ angle: (this.fxTick % 4) * 14 - 21, centerX: '50%', centerY: '50%' })
.position({ x: 30, y: 630 })
Text('✨')
.fontSize(14)
.fontColor(COLORS.gold)
.opacity(this.fxTick % 3 === 0 ? 1 : 0.15)
.position({ x: 300, y: 78 })
Text('👟')
.fontSize(18)
.rotate({ angle: (this.fxTick % 6) * 8 - 20, centerX: '50%', centerY: '50%' })
.position({ x: 320, y: 640 })
}
.width('100%')
.height('100%')
.hitTestBehavior(HitTestMode.None)
.position({ x: 0, y: 0 })
}
@Builder
header() {
Column() {
Row() {
Column() {
Text('👟 球鞋急诊室')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
Text('已抢救 48,291 双 · 今日还可上门取鞋 312 单')
.fontSize(11)
.fontColor('#CFD8DC')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
Row() {
Text('🛵')
.fontSize(18)
.padding(8)
.backgroundColor('#33FFFFFF')
.borderRadius(18)
.onClick(() => {
this.bizOpen = true;
})
Text('🔔')
.fontSize(18)
.padding(8)
.backgroundColor('#33FFFFFF')
.borderRadius(18)
.margin({ left: 8 })
}
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 16, right: 16, top: 14 })
Row() {
Text('🔍 搜鞋款 / 查修复案例 / 约取送')
.fontSize(12)
.fontColor('#B4C4CB')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.backgroundColor('#33FFFFFF')
.borderRadius(18)
.layoutWeight(1)
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
Row() {
Text('⚡ 2 小时上门')
.fontSize(10)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor('#33FFFFFF')
.borderRadius(12)
Text('🧪 中底去黄 5 折')
.fontSize(10)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor('#33FFFFFF')
.borderRadius(12)
.margin({ left: 6 })
Text('💳 学生认证立减 20')
.fontSize(10)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor('#33FFFFFF')
.borderRadius(12)
.margin({ left: 6 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 10 })
}
.width('100%')
.linearGradient({
angle: 160,
colors: [['#263238', 0], ['#37474F', 0.6], ['#546E7A', 1]]
})
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
.padding({ bottom: 14 })
}
@Builder
hangTab(name: string, icon: string, idx: number) {
Column() {
Row() {
Circle()
.width(5)
.height(5)
.fill(idx === this.currentTab ? COLORS.accent : COLORS.textHint)
Row()
.width(16)
.height(2)
.backgroundColor(idx === this.currentTab ? COLORS.accent : COLORS.textHint)
.margin({ left: 2 })
}
.justifyContent(FlexAlign.Center)
.margin({ top: 4 })
Column() {
Text(icon)
.fontSize(16)
Text(name)
.fontSize(10)
.fontWeight(idx === this.currentTab ? FontWeight.Bold : FontWeight.Normal)
.fontColor(idx === this.currentTab ? COLORS.white : COLORS.textSecondary)
.margin({ top: 2 })
}
.padding({ left: 8, right: 8, top: 4, bottom: 6 })
.backgroundColor(idx === this.currentTab ? COLORS.primaryDark : COLORS.cardBg)
.borderRadius(8)
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.currentTab = idx;
})
}
@Builder
subNav() {
Column() {
Row() {
this.hangTab('洗护', '🧼', 0)
this.hangTab('修复改色', '🎨', 1)
this.hangTab('真实案例', '📸', 2)
this.hangTab('取送', '🛵', 3)
}
.width('100%')
Row() {
this.hangTab('球鞋档案', '🗂️', 4)
this.hangTab('急救攻略', '🩹', 5)
this.hangTab('会员权益', '💳', 6)
Column()
.layoutWeight(1)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding({ left: 10, right: 10, top: 10, bottom: 10 })
.backgroundColor(COLORS.bg)
}
@Builder
pageWash() {
Column() {
Row() {
Text('🧼 洗护订单')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('新建订单')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.backgroundColor(COLORS.accent)
.borderRadius(12)
.margin({ left: 'auto' })
.onClick(() => {
this.addOpen = true;
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
Row() {
Column() {
Text('88')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text('标准水洗起')
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text('128')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text('中底去黄')
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text('268')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text('爆裂纹补色')
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text('380')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text('整底更换')
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ top: 10 })
ForEach(this.orderList, (o: OrderItem, oi: number) => {
Column() {
Row() {
Text(o.icon)
.fontSize(26)
.padding(12)
.backgroundColor(COLORS.primaryLight)
.borderRadius(12)
Column() {
Text(o.shoe)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.maxLines(1)
Text(o.service)
.fontSize(10)
.fontColor(COLORS.textSecondary)
.margin({ top: 3 })
Text('🕐 ' + o.eta)
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text('¥' + o.price.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text(o.status === 1 ? '已完成' : '清洗中')
.fontSize(9)
.fontColor(o.status === 1 ? COLORS.success : COLORS.warning)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor(o.status === 1 ? '#E8F5E9' : '#FFF8E1')
.borderRadius(8)
.margin({ top: 4 })
.onClick(() => {
o.status = o.status === 1 ? 0 : 1;
this.orderList.splice(oi, 1, o);
})
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row() {
Text('🛵 上门取送')
.fontSize(10)
.fontColor(COLORS.primary)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(COLORS.primaryLight)
.borderRadius(8)
.onClick(() => {
this.bizOpen = true;
})
Text('取消订单')
.fontSize(10)
.fontColor(COLORS.danger)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(COLORS.accentLight)
.borderRadius(8)
.margin({ left: 8 })
.onClick(() => {
this.orderList.splice(oi, 1);
})
Text('师傅已接单 1,203 次')
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ left: 'auto' })
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ top: 10 })
}, (o: OrderItem) => 'od' + o.id.toString() + '_' + o.status.toString())
}
.width('100%')
.padding({ left: 12, right: 12, bottom: 16 })
}
@Builder
pageFix() {
Column() {
Row() {
Text('🎨 修复 & 改色工坊')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('均含上门取送')
.fontSize(10)
.fontColor(COLORS.textHint)
.margin({ left: 'auto' })
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
ForEach(this.fixList, (f: FixItem) => {
Row() {
Text(f.icon)
.fontSize(26)
.padding(12)
.backgroundColor(COLORS.accentLight)
.borderRadius(12)
Column() {
Text(f.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(f.desc)
.fontSize(10)
.fontColor(COLORS.textSecondary)
.margin({ top: 3 })
.maxLines(1)
Text('工期 ' + f.days.toString() + ' 天 · 已完成 ' + (f.id * 327 + 56).toString() + ' 单')
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Text(f.booked === 1 ? '已预约' : '预约')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(f.booked === 1 ? COLORS.white : COLORS.accent)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.backgroundColor(f.booked === 1 ? COLORS.primaryDark : COLORS.accentLight)
.borderRadius(12)
.onClick(() => {
f.booked = f.booked === 1 ? 0 : 1;
this.fixList.splice(f.id - 1, 1, f);
})
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ top: 10 })
}, (f: FixItem) => 'fx' + f.id.toString() + '_' + f.booked.toString())
}
.width('100%')
.padding({ left: 12, right: 12, bottom: 16 })
}
@Builder
pageCase() {
Column() {
Row() {
Text('📸 抢救实录')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('换一批')
.fontSize(11)
.fontColor(COLORS.primary)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(COLORS.primaryLight)
.borderRadius(12)
.margin({ left: 'auto' })
.onClick(() => {
this.doChangeCase();
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
ForEach(this.caseList, (c: CaseItem) => {
Column() {
Row() {
Text('🧼 修复前')
.fontSize(9)
.fontColor(COLORS.white)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(COLORS.primaryDark)
.borderRadius(8)
Text('→')
.fontSize(12)
.fontColor(COLORS.textHint)
.margin({ left: 8 })
Text('✨ 修复后')
.fontSize(9)
.fontColor(COLORS.white)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(COLORS.accent)
.borderRadius(8)
.margin({ left: 8 })
Text(c.days.toString() + ' 天交付')
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ left: 'auto' })
}
.width('100%')
Text(c.shoe + ' · ' + c.issue)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 10 })
Row() {
Text('师傅:金牌修复师大飞')
.fontSize(9)
.fontColor(COLORS.textSecondary)
Text(c.liked === 1 ? '❤️ ' + (c.likes + 1).toString() : '🤍 ' + c.likes.toString())
.fontSize(11)
.fontColor(c.liked === 1 ? COLORS.accent : COLORS.textSecondary)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(c.liked === 1 ? COLORS.accentLight : COLORS.bg)
.borderRadius(10)
.margin({ left: 'auto' })
.onClick(() => {
c.liked = c.liked === 1 ? 0 : 1;
this.caseList.splice(c.id - 1, 1, c);
})
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ top: 10 })
}, (c: CaseItem) => 'cs' + c.id.toString() + '_' + c.liked.toString())
}
.width('100%')
.padding({ left: 12, right: 12, bottom: 16 })
}
@Builder
pagePickup() {
Column() {
Row() {
Text('🛵 上门取送')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('雨天照常取件')
.fontSize(10)
.fontColor(COLORS.textHint)
.margin({ left: 'auto' })
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
Column() {
Text('🧑🔧 你的专属取送员')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
Text('王师傅 · 5.0 分 · 1,203 单 · 电动车 32 分钟可达')
.fontSize(10)
.fontColor('#ECEFF1')
.margin({ top: 4 })
Text('电话联系')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primaryDark)
.padding({ left: 14, right: 14, top: 5, bottom: 5 })
.backgroundColor(COLORS.white)
.borderRadius(12)
.margin({ top: 8 })
.onClick(() => {
this.bizOpen = true;
})
}
.width('100%')
.padding(14)
.alignItems(HorizontalAlign.Start)
.backgroundColor(COLORS.primary)
.borderRadius(14)
.margin({ top: 10 })
ForEach(this.slotList, (s: SlotItem) => {
Row() {
Column() {
Text(s.time)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('剩余 ' + s.left.toString() + '/' + s.quota.toString() + ' 个取件名额')
.fontSize(9)
.fontColor(s.left < 4 ? COLORS.accent : COLORS.textHint)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text(s.booked === 1 ? '已预约' : '预约')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(s.booked === 1 ? COLORS.white : COLORS.accent)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.backgroundColor(s.booked === 1 ? COLORS.primaryDark : COLORS.accentLight)
.borderRadius(12)
.onClick(() => {
if (s.booked === 0 && s.left > 0) {
s.booked = 1;
s.left -= 1;
this.slotList.splice(s.id - 1, 1, s);
}
})
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ top: 10 })
}, (s: SlotItem) => 'sl' + s.id.toString() + '_' + s.booked.toString() + '_' + s.left.toString())
}
.width('100%')
.padding({ left: 12, right: 12, bottom: 16 })
}
@Builder
pageFile() {
Column() {
Row() {
Text('🗂️ 我的球鞋档案')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(this.shoeList.length.toString() + ' 双')
.fontSize(10)
.fontColor(COLORS.textHint)
.margin({ left: 'auto' })
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
ForEach(this.shoeList, (sh: ShoeItem, si: number) => {
Column() {
Row() {
Text(sh.icon)
.fontSize(30)
.padding(14)
.backgroundColor(COLORS.primaryLight)
.borderRadius(14)
Column() {
Text(sh.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(sh.cw)
.fontSize(10)
.fontColor(COLORS.textSecondary)
.margin({ top: 2 })
Text(condWord(sh.cond) + ' · 估值 ¥' + sh.value.toString())
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text(sh.cond.toString() + '分')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(sh.cond >= 85 ? COLORS.success : (sh.cond >= 70 ? COLORS.warning : COLORS.danger))
Text('成色分')
.fontSize(8)
.fontColor(COLORS.textHint)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row() {
Text('✏️ 编辑档案')
.fontSize(10)
.fontColor(COLORS.primary)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(COLORS.primaryLight)
.borderRadius(8)
.onClick(() => {
this.editOpen = true;
})
Text('🗑️ 删除档案')
.fontSize(10)
.fontColor(COLORS.danger)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(COLORS.accentLight)
.borderRadius(8)
.margin({ left: 8 })
.onClick(() => {
this.delIndex = si;
this.delOpen = true;
})
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ top: 10 })
}, (sh: ShoeItem) => 'sh' + sh.id.toString() + '_' + sh.cond.toString())
}
.width('100%')
.padding({ left: 12, right: 12, bottom: 16 })
}
@Builder
pageGuide() {
Column() {
Row() {
Text('🩹 球鞋急救攻略')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('收藏数破万')
.fontSize(10)
.fontColor(COLORS.textHint)
.margin({ left: 'auto' })
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
ForEach(this.guideList, (g: GuideItem) => {
Row() {
Text('🩹')
.fontSize(22)
.padding(10)
.backgroundColor(COLORS.accentLight)
.borderRadius(12)
Column() {
Text(g.title)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.maxLines(1)
Text(g.author + ' · ' + (g.reads / 10000).toFixed(1) + ' 万阅读')
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Text(g.saved === 1 ? '已收藏' : '收藏')
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(g.saved === 1 ? COLORS.white : COLORS.accent)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor(g.saved === 1 ? COLORS.primaryDark : COLORS.accentLight)
.borderRadius(10)
.onClick(() => {
g.saved = g.saved === 1 ? 0 : 1;
this.guideList.splice(g.id - 1, 1, g);
})
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.margin({ top: 8 })
}, (g: GuideItem) => 'gd' + g.id.toString() + '_' + g.saved.toString())
}
.width('100%')
.padding({ left: 12, right: 12, bottom: 16 })
}
@Builder
pageVip() {
Column() {
Row() {
Text('💳 会员权益')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('SLV.4 白金鞋友')
.fontSize(10)
.fontColor(COLORS.gold)
.margin({ left: 'auto' })
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
Column() {
Row() {
Column() {
Text('SOLE VIP')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.gold)
.letterSpacing(2)
Text('积分 12,480 · 距 SLV.5 还差 3,520')
.fontSize(10)
.fontColor('#ECEFF1')
.margin({ top: 5 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('👟')
.fontSize(34)
}
.width('100%')
}
.width('100%')
.padding(16)
.linearGradient({
angle: 120,
colors: [['#263238', 0], ['#455A64', 1]]
})
.borderRadius(16)
.margin({ top: 10 })
Column() {
ForEach(VIP_RIGHTS, (r: string, ri: number) => {
Row() {
Text(ri === 0 ? '🛵' : (ri === 1 ? '🧼' : (ri === 2 ? '🎨' : '🎂')))
.fontSize(16)
Text(r)
.fontSize(11)
.fontColor(COLORS.textPrimary)
.margin({ left: 10 })
Text('已生效')
.fontSize(9)
.fontColor(COLORS.success)
.margin({ left: 'auto' })
}
.width('100%')
.padding({ top: 10, bottom: 10 })
}, (r: string) => 'vr' + r)
}
.width('100%')
.padding({ left: 14, right: 14, top: 4, bottom: 8 })
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ top: 10 })
Column() {
Text('📊 近 6 个月洗护消费')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Row() {
ForEach(MONTH_LIST, (m: MonthItem) => {
Column() {
Text('¥' + m.spent.toString())
.fontSize(8)
.fontColor(COLORS.accent)
Column()
.width(18)
.height(spentBar(m.spent))
.backgroundColor(m.month === '8月' ? COLORS.accent : COLORS.primary)
.borderRadius(4)
.margin({ top: 3 })
Text(m.month)
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (m: MonthItem) => 'mo' + m.id.toString())
}
.margin({ top: 8 })
Text('累计洗护 37 单 · 消费 ¥5,060 · 省下 ¥1,260')
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.alignItems(HorizontalAlign.Start)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.margin({ top: 10 })
}
.width('100%')
.padding({ left: 12, right: 12, bottom: 16 })
}
@Builder
modalBodyAdd() {
Column() {
Column() {
Text('WASH ORDER · 洗护单')
.fontSize(9)
.letterSpacing(2)
.fontColor(COLORS.accent)
Text('🧼 新建洗护订单')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 4 })
Text('48 小时焕新送回 · 支持 12 类鞋面材质')
.fontSize(10)
.fontColor(COLORS.textSecondary)
.margin({ top: 3 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.accentLight)
.borderRadius(12)
Column() {
Text('服务类型')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Row() {
ForEach(ADD_SERVICES, (sv: string, si: number) => {
Text(sv)
.fontSize(11)
.fontColor(this.addService === si ? COLORS.white : COLORS.textSecondary)
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.backgroundColor(this.addService === si ? COLORS.primary : COLORS.bg)
.borderRadius(10)
.margin({ right: 6 })
.onClick(() => {
this.addService = si;
})
}, (sv: string) => 'sv' + sv)
}
.margin({ top: 8 })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.margin({ top: 14 })
Column() {
Text('鞋数(双)')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Row() {
Text('-')
.fontSize(16)
.fontColor(COLORS.textSecondary)
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
.backgroundColor(COLORS.bg)
.borderRadius(10)
.onClick(() => {
this.decPair();
})
Text(this.addPair.toString() + ' 双')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primary)
.padding({ left: 20, right: 20 })
Text('+')
.fontSize(16)
.fontColor(COLORS.textSecondary)
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
.backgroundColor(COLORS.bg)
.borderRadius(10)
.onClick(() => {
this.incPair();
})
}
.margin({ top: 8 })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.margin({ top: 14 })
Row() {
Text('含防尘袋+鞋撑送回 · 赠除臭一次')
.fontSize(10)
.fontColor(COLORS.textSecondary)
Text('合计 ¥' + ((this.addService + 1) * 60 * this.addPair).toString())
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
.margin({ left: 'auto' })
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.primaryLight)
.borderRadius(10)
.margin({ top: 14 })
Row() {
Text('取消')
.fontSize(13)
.fontColor(COLORS.textSecondary)
.padding({ left: 18, right: 18, top: 10, bottom: 10 })
.backgroundColor(COLORS.bg)
.borderRadius(20)
.onClick(() => {
this.addOpen = false;
})
Text('提交订单')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 22, right: 22, top: 10, bottom: 10 })
.backgroundColor(COLORS.accent)
.borderRadius(20)
.margin({ left: 12 })
.onClick(() => {
this.doAdd();
})
}
.width('100%')
.justifyContent(FlexAlign.End)
.margin({ top: 18 })
}
.width('100%')
.padding(16)
.backgroundColor(COLORS.cardBg)
.borderRadius(20)
.constraintSize({ maxHeight: '80%' })
}
@Builder
modalBodyEdit() {
Column() {
Column() {
Text('SNEAKER FILE · 球鞋档案')
.fontSize(9)
.letterSpacing(2)
.fontColor(COLORS.primary)
Text('✏️ 编辑球鞋档案')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 4 })
Text('当前编辑:AJ1 芝加哥 · Chicago Red')
.fontSize(10)
.fontColor(COLORS.textSecondary)
.margin({ top: 3 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.primaryLight)
.borderRadius(12)
Column() {
Text('成色评分(40-98)')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Row() {
Text('-')
.fontSize(16)
.fontColor(COLORS.textSecondary)
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
.backgroundColor(COLORS.bg)
.borderRadius(10)
.onClick(() => {
this.decCond();
})
Text(this.editCond.toString() + ' 分')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primary)
.padding({ left: 20, right: 20 })
Text('+')
.fontSize(16)
.fontColor(COLORS.textSecondary)
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
.backgroundColor(COLORS.bg)
.borderRadius(10)
.onClick(() => {
this.incCond();
})
}
.margin({ top: 8 })
Text('当前判定:' + condWord(this.editCond))
.fontSize(10)
.fontColor(COLORS.textSecondary)
.margin({ top: 6 })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.margin({ top: 14 })
Row() {
Text('调整后估值')
.fontSize(11)
.fontColor(COLORS.textSecondary)
Text('¥' + (this.editCond * 48).toString())
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
.margin({ left: 'auto' })
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.accentLight)
.borderRadius(10)
.margin({ top: 14 })
Row() {
Text('取消')
.fontSize(13)
.fontColor(COLORS.textSecondary)
.padding({ left: 18, right: 18, top: 10, bottom: 10 })
.backgroundColor(COLORS.bg)
.borderRadius(20)
.onClick(() => {
this.editOpen = false;
})
Text('保存档案')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 22, right: 22, top: 10, bottom: 10 })
.backgroundColor(COLORS.primary)
.borderRadius(20)
.margin({ left: 12 })
.onClick(() => {
this.doEdit();
})
}
.width('100%')
.justifyContent(FlexAlign.End)
.margin({ top: 18 })
}
.width('100%')
.padding(16)
.backgroundColor(COLORS.cardBg)
.borderRadius(20)
.constraintSize({ maxHeight: '80%' })
}
@Builder
modalBodyDel() {
Column() {
Column() {
Text('⚠️ DELETE FILE · 删除档案')
.fontSize(9)
.letterSpacing(2)
.fontColor(COLORS.danger)
Text('确认删除这双鞋的档案吗?')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 6 })
}
.width('100%')
.padding(14)
.backgroundColor('#263238')
.borderRadius(12)
Row() {
Column() {
Text('12')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.danger)
Text('护理记录将清空')
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text('¥4,200')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.danger)
Text('估值档案将移除')
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text('36')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
Text('合影照片保留')
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.margin({ top: 14 })
Text('删除后不可恢复,洗护订单记录将归档到「历史订单」,球鞋合影仍在相册中。')
.fontSize(10)
.fontColor(COLORS.textSecondary)
.margin({ top: 12 })
Row() {
Text('取消')
.fontSize(13)
.fontColor(COLORS.textSecondary)
.padding({ left: 18, right: 18, top: 10, bottom: 10 })
.backgroundColor(COLORS.bg)
.borderRadius(20)
.onClick(() => {
this.delOpen = false;
})
Text('确认删除')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 22, right: 22, top: 10, bottom: 10 })
.backgroundColor(COLORS.danger)
.borderRadius(20)
.margin({ left: 12 })
.onClick(() => {
this.doDel();
})
}
.width('100%')
.justifyContent(FlexAlign.End)
.margin({ top: 18 })
}
.width('100%')
.padding(16)
.backgroundColor(COLORS.cardBg)
.borderRadius(20)
.constraintSize({ maxHeight: '80%' })
}
@Builder
modalBodyBiz() {
Column() {
Column() {
Text('PICKUP SLIP · 取送单')
.fontSize(9)
.letterSpacing(2)
.fontColor(COLORS.primary)
Text('🛵 预约上门取送')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 4 })
Text('王师傅接单 · 免费防尘袋包装')
.fontSize(10)
.fontColor(COLORS.textSecondary)
.margin({ top: 3 })
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.primaryLight)
.borderRadius(12)
Column() {
Text('取件时间')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Row() {
ForEach(PICK_TIMES, (pt: string, pi: number) => {
Text(pt)
.fontSize(11)
.fontColor(this.bizTime === pi ? COLORS.white : COLORS.textSecondary)
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
.backgroundColor(this.bizTime === pi ? COLORS.accent : COLORS.bg)
.borderRadius(10)
.margin({ right: 8 })
.onClick(() => {
this.bizTime = pi;
})
}, (pt: string) => 'pt' + pt)
}
.margin({ top: 8 })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.margin({ top: 14 })
Column() {
Text('📦 取件地址')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('望京 SOHO T3 · 1808(出门放前台即可)')
.fontSize(11)
.fontColor(COLORS.textSecondary)
.margin({ top: 6 })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.margin({ top: 14 })
Row() {
Text('⏰ 加急 2 小时达')
.fontSize(11)
.fontColor(COLORS.textPrimary)
Text(this.bizUrgent === 1 ? '已开启' : '已关闭')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(this.bizUrgent === 1 ? COLORS.accent : COLORS.textHint)
.padding({ left: 12, right: 12, top: 4, bottom: 4 })
.backgroundColor(this.bizUrgent === 1 ? COLORS.accentLight : COLORS.bg)
.borderRadius(10)
.margin({ left: 'auto' })
.onClick(() => {
this.bizUrgent = this.bizUrgent === 1 ? 0 : 1;
})
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(10)
.margin({ top: 14 })
Row() {
Text('加急费 ¥' + (this.bizUrgent === 1 ? 15 : 0).toString())
.fontSize(10)
.fontColor(COLORS.textSecondary)
Text('会员免取送费')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
.margin({ left: 'auto' })
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.accentLight)
.borderRadius(10)
.margin({ top: 14 })
Row() {
Text('取消')
.fontSize(13)
.fontColor(COLORS.textSecondary)
.padding({ left: 18, right: 18, top: 10, bottom: 10 })
.backgroundColor(COLORS.bg)
.borderRadius(20)
.onClick(() => {
this.bizOpen = false;
})
Text('确认预约')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 22, right: 22, top: 10, bottom: 10 })
.backgroundColor(COLORS.primary)
.borderRadius(20)
.margin({ left: 12 })
.onClick(() => {
this.doBiz();
})
}
.width('100%')
.justifyContent(FlexAlign.End)
.margin({ top: 18 })
}
.width('100%')
.padding(16)
.backgroundColor(COLORS.cardBg)
.borderRadius(20)
.constraintSize({ maxHeight: '80%' })
}
@Builder
modalOverlay() {
Stack({ alignContent: Alignment.Bottom }) {
Column()
.width('100%')
.height('100%')
.backgroundColor('#66000000')
.onClick(() => {
this.addOpen = false;
this.editOpen = false;
this.delOpen = false;
this.bizOpen = false;
})
Column() {
if (this.addOpen) {
this.modalBodyAdd()
}
if (this.editOpen) {
this.modalBodyEdit()
}
if (this.delOpen) {
this.modalBodyDel()
}
if (this.bizOpen) {
this.modalBodyBiz()
}
}
.width('92%')
.margin({ bottom: 20 })
}
.width('100%')
.height('100%')
}
@Builder
bottomBar() {
Row() {
Column() {
Text('🧼')
.fontSize(20)
Text('订单')
.fontSize(10)
.fontColor(this.bottomTab === 0 ? COLORS.primary : COLORS.textHint)
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => {
this.bottomTab = 0;
this.currentTab = 0;
})
Column() {
Text('📸')
.fontSize(20)
Text('案例')
.fontSize(10)
.fontColor(this.bottomTab === 1 ? COLORS.primary : COLORS.textHint)
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => {
this.bottomTab = 1;
this.currentTab = 2;
})
Column() {
Column() {
Text('➕')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
}
.width(48)
.height(48)
.justifyContent(FlexAlign.Center)
.backgroundColor(COLORS.accent)
.borderRadius(24)
.offset({ y: -14 })
.onClick(() => {
this.addOpen = true;
})
}
.layoutWeight(1)
Column() {
Text('🗂️')
.fontSize(20)
Text('鞋档案')
.fontSize(10)
.fontColor(this.bottomTab === 3 ? COLORS.primary : COLORS.textHint)
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => {
this.bottomTab = 3;
this.currentTab = 4;
})
Column() {
Text('💳')
.fontSize(20)
Text('会员')
.fontSize(10)
.fontColor(this.bottomTab === 4 ? COLORS.primary : COLORS.textHint)
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => {
this.bottomTab = 4;
this.currentTab = 6;
})
}
.width('100%')
.padding({ top: 8, bottom: 8 })
.backgroundColor(COLORS.cardBg)
.borderRadius({ topLeft: 20, topRight: 20 })
}
@Builder
mainContent() {
Column() {
this.header()
this.subNav()
Scroll() {
Column() {
if (this.currentTab === 0) {
this.pageWash()
}
if (this.currentTab === 1) {
this.pageFix()
}
if (this.currentTab === 2) {
this.pageCase()
}
if (this.currentTab === 3) {
this.pagePickup()
}
if (this.currentTab === 4) {
this.pageFile()
}
if (this.currentTab === 5) {
this.pageGuide()
}
if (this.currentTab === 6) {
this.pageVip()
}
}
.width('100%')
}
.layoutWeight(1)
.scrollBar(BarState.Off)
}
.width('100%')
.height('100%')
}
build() {
Stack() {
this.mainContent()
this.fxLayer()
if (this.addOpen || this.editOpen || this.delOpen || this.bizOpen) {
this.modalOverlay()
}
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
}

结语:
从「球鞋急诊室」项目的完整实现来看,HarmonyOS ArkTS在垂直生活服务社区应用开发中展现出了卓越的工程效率与表现力。声明式UI范式使得七Tab内容切换、四弹框状态管理、复杂列表渲染等业务场景的实现代码结构清晰、可维护性强。@Observed装饰器在OrderItem、ShoeItem、CaseItem等模型类上的应用,确保了订单状态切换、成色评分修改、案例点赞等交互操作能够以最小粒度触发UI更新,在包含十余条数据的列表中保持了流畅的渲染性能。@Builder装饰器对hangTab吊牌组件、header头部、fxLayer特效层等通用UI片段的封装复用,保证了七Tab视觉的一致性和特效的统一管理。
从产品设计的完整性角度审视,「球鞋急诊室」的七个内容Tab构成了从交易到内容到会员的完整服务闭环。pageWash和pageFix承载核心交易功能,pageCase和pageGuide承载社区内容运营,pagePickup解决物流场景,pageFile管理用户资产,pageVip构建会员体系。这种全链路页面布局不是简单的功能堆砌,而是基于用户旅程的系统性设计——用户从了解服务(案例/攻略)到下单(洗护/修复)到物流(取送)到资产管理(档案)到长期留存(会员),每一步都有对应的页面支撑。四个弹框分别对应创建(洗护订单)、编辑(球鞋档案)、删除(档案警示)、预约(取送)四类核心操作,覆盖了CRUD操作的全部场景。
在视觉设计层面,沥青蓝+荧光橙的双色对比策略是本项目的核心视觉创新。沥青蓝#37474F传达出洗护车间的工业专业感,荧光橙#FF6D00注入了球鞋文化的街头活力,两者的反差对比使得应用在视觉上既可信又年轻。linearGradient渐变头部、吊牌式Tab设计、黑橙警示弹框、气泡刷子闪光特效,这些视觉元素都紧密围绕"球鞋洗护车间"这一核心场景展开,形成了高度统一的视觉语言。特别是hangTab吊牌设计,将球鞋盒吊牌的物理形态融入数字界面,是业务场景与UI设计深度融合的典范。
在交互体验层面,本项目通过fxTick定时器驱动的五种特效元素(气泡上升、刷子晃动、闪光闪烁、球鞋摇摆),为洗护车间场景注入了持续的动态生命力。这些特效虽然实现简单(仅通过opacity、translate、rotate、scale属性动画),但通过不同周期的取模运算实现了非同步运动,产生了自然的随机感。hitTestBehavior(HitTestMode.None)确保特效层不拦截触摸事件,实现了视觉增强与交互不受干扰的双赢。这种轻量级特效方案在不需要复杂物理动画的社区应用中具有很高的性价比。
更多推荐



所有评论(0)