HarmonyOS 6效果实现:限时倒计时/九宫格分类/双列瀑布/拼团套票
引言

本地生活服务赛道在移动互联网时代持续演进,限时秒杀模式已成为餐饮、娱乐、美业等到店服务场景的核心获客手段。相较于传统电商的标准化商品秒杀,本地生活秒杀面临着地理位置感知、到店核销预约、拼团社交裂变、多品类聚合展示等独特的业务挑战。本文将深入剖析一个基于HarmonyOS ArkTS框架构建的本地生活秒杀平台应用,该平台覆盖了今日秒杀、餐饮美食、休闲娱乐、美业丽人、拼团套票、我的订单六大核心业务Tab,通过橙红渐变视觉体系和倒计时驱动机制营造了强烈的限时紧迫感。
在技术架构层面,该应用充分利用了HarmonyOS ArkTS API 24的声明式UI开发能力,采用单入口组件@Entry配合多@Builder方法拆分的架构模式管理六个Tab页面和五个弹窗层。应用通过setInterval定时器实现秒杀倒计时的实时更新,在aboutToAppear生命周期中启动定时器、aboutToDisappear中清理定时器,确保了组件生命周期与定时器资源的正确管理。Stack容器的alignContent: Alignment.Bottom配置使弹窗层能够从底部优雅滑出,叠加在主内容之上形成沉浸式交互体验。
在业务设计层面,应用以"限时紧迫感"为视觉设计核心,通过橙红渐变头部(#FF8A3D到#E63E0B)和红色价格标签(#FF2E00)强化用户的购买冲动。分类九宫格采用3x3布局覆盖美食、火锅、电影、KTV、美甲、按摩、酒店、亲子、洗车九大品类,每个品类通过独立配色背景实现视觉辨识。双列瀑布流卡片在餐饮美食Tab中实现了商户信息的紧凑展示,横滑大卡在休闲娱乐Tab中突出了玩乐项目的沉浸式体验。消费柱状图在订单Tab中通过六个月的数据趋势可视化呈现用户的消费行为分析。整个应用通过精心设计的静态数据集模拟了真实本地生活平台的完整业务闭环。
一、数据结构定义层

应用首先定义了八个核心数据接口,覆盖了分类导航、秒杀商品、商户信息、娱乐项目、美业项目、拼团队伍、订单记录和优惠券等全业务实体。
interface FlashDealItem {
id: number
title: string
sub: string
originPrice: number
nowPrice: number
sold: number
total: number
tag: string
img: string
shopName: string
distance: string
}
interface MerchantCard {
id: number
name: string
category: string
rating: number
monthlySales: number
avgPrice: number
originPrice: number
distance: string
address: string
labels: string[]
hotTag: string
gradient: string[]
}
interface FunCard {
id: number
name: string
type: string
price: number
marketPrice: number
duration: string
suitPeople: string
stockLeft: number
gradient: string[]
desc: string
}
interface GroupTeam {
id: number
title: string
teamSize: number
joined: number
price: number
singlePrice: number
leader: string
endTime: string
avatars: string[]
status: string
}
interface OrderRow {
id: number
shopName: string
goods: string
price: number
count: number
status: string
date: string
statusColor: string
canRate: boolean
}
interface VoucherCard {
id: number
amount: number
condition: string
scope: string
expire: string
got: boolean
color: string
}
interface ChartBar {
month: string
value: number
height: number
color: string
}
FlashDealItem接口定义了秒杀商品的核心数据模型,sold与total字段的比值驱动进度条渲染,distance字段实现基于位置的距离感知。MerchantCard接口的gradient字符串数组存储双色渐变值,每个商户拥有独特的品牌渐变色。GroupTeam接口的avatars数组支持空字符串占位符,表示待加入的拼团成员槽位。OrderRow接口通过statusColor字段实现订单状态的颜色编码,canRate布尔值决定是否展示"去评价"入口。ChartBar接口预存储了柱状图的height值,避免了运行时动态计算的复杂度。
二、设计令牌与配色系统

应用通过常量定义统一的设计令牌系统,确保橙红品牌色在所有模块中的视觉一致性。
const MAIN_ORANGE: string = '#FF5A1E'
const DEEP_ORANGE: string = '#E63E0B'
const LIGHT_BG: string = '#F5F6F8'
const CARD_BG: string = '#FFFFFF'
const TEXT_MAIN: string = '#2B2622'
const TEXT_SUB: string = '#8A837B'
const RED_PRICE: string = '#FF2E00'
const GREEN_TAG: string = '#00A870'
const CARD_SHADOW: ShadowOptions = {
radius: 10,
color: 'rgba(255,90,30,0.10)',
offsetX: 0,
offsetY: 4
}
const SOFT_SHADOW: ShadowOptions = {
radius: 6,
color: 'rgba(0,0,0,0.06)',
offsetX: 0,
offsetY: 2
}
const TAB_NAMES: string[] = ['今日秒杀', '餐饮美食', '休闲娱乐', '美业丽人', '拼团套票', '我的订单']
const TAB_ICONS: string[] = ['⚡', '🍜', '🎮', '💅', '👥', '📋']
设计令牌系统定义了两层阴影方案:CARD_SHADOW使用橙色半透明阴影(rgba(255,90,30,0.10))为关键卡片营造品牌色调的立体感,SOFT_SHADOW使用通用黑色半透明阴影(rgba(0,0,0,0.06))提供柔和的层次效果。MAIN_ORANGE作为主品牌色用于激活状态、按钮和强调元素,RED_PRICE专用于价格展示以强化视觉冲击力,GREEN_TAG用于优惠金额和正向状态标识。
静态数据集
const FLASH_DEALS: FlashDealItem[] = [
{ id: 1, title: '双人火锅套餐 128元抢', sub: '肥牛卷+虾滑+时蔬拼盘', originPrice: 268, nowPrice: 128, sold: 862, total: 1000, tag: '爆品返场', img: '🍲', shopName: '蜀香门第火锅(万达店)', distance: '890m' },
{ id: 2, title: '电影票2张 29.9元', sub: '全国通用·节假日可用', originPrice: 90, nowPrice: 29.9, sold: 2310, total: 3000, tag: '今日必抢', img: '🎬', shopName: '星光国际影城', distance: '1.2km' },
{ id: 3, title: 'KTV中包3小时 58元', sub: '含果盘一份·免唱到打烊', originPrice: 198, nowPrice: 58, sold: 517, total: 800, tag: '限购1份', img: '🎤', shopName: '纯量贩KTV(中心店)', distance: '650m' },
{ id: 4, title: '全身推拿60分钟 69元', sub: '老中医坐诊·含肩颈调理', originPrice: 168, nowPrice: 69, sold: 1243, total: 1500, tag: '回头客榜', img: '💆', shopName: '颐和堂推拿馆', distance: '2.1km' },
{ id: 5, title: '日式寿喜烧自助 79元', sub: '和牛畅吃·饮料无限续', originPrice: 158, nowPrice: 79, sold: 326, total: 500, tag: '新品首发', img: '🍱', shopName: '豚屋寿喜烧', distance: '1.8km' }
]
const MONTH_CHART: ChartBar[] = [
{ month: '3月', value: 486, height: 34, color: '#FFD3B6' },
{ month: '4月', value: 620, height: 44, color: '#FFC0A0' },
{ month: '5月', value: 358, height: 25, color: '#FFD3B6' },
{ month: '6月', value: 742, height: 53, color: '#FFAB7E' },
{ month: '7月', value: 918, height: 66, color: '#FF8A4C' },
{ month: '8月', value: 1180, height: 84, color: '#FF5A1E' }
]
FLASH_DEALS数组存储了8个秒杀商品,每个商品包含原价、秒杀价、已抢数量、总库存和营销标签。MONTH_CHART数组预存储了六个月的消费数据,height字段作为柱状图高度的预设值,color字段从浅橙色(#FFD3B6)渐变到主品牌橙色(#FF5A1E),通过颜色深浅直观传达消费金额的增长趋势。
三、入口组件与生命周期管理

入口组件LocalLifePage管理着六个Tab的状态切换、倒计时定时器和五个弹窗的显示控制,是整个应用的神经中枢。
@Entry
@Component
struct LocalLifePage {
@State currentTab: number = 0
@State remainSec: number = 3875
@State selectedFlash: FlashDealItem = FLASH_DEALS[0]
@State selectedMerchant: MerchantCard = FOOD_MERCHANTS[0]
@State selectedBeauty: BeautyItem = BEAUTY_ITEMS[0]
@State selectedTeam: GroupTeam = GROUP_TEAMS[0]
@State selectedOrder: OrderRow = ORDER_ROWS[0]
@State selectedVoucher: VoucherCard = VOUCHER_WALL[0]
@State showBuyModal: boolean = false
@State showVoucherModal: boolean = false
@State showBookModal: boolean = false
@State showDetailModal: boolean = false
@State showDeleteModal: boolean = false
@State buyCount: number = 1
@State bookedPeople: number = 2
@State bookDate: string = '明天 08-26'
@State orderFilter: number = 0
@State gotVoucherIds: number[] = [3, 5]
private timerId: number = -1
aboutToAppear(): void {
this.timerId = setInterval(() => {
if (this.remainSec > 0) {
this.remainSec = this.remainSec - 1
} else {
this.remainSec = 7200
}
}, 1000)
}
aboutToDisappear(): void {
if (this.timerId >= 0) {
clearInterval(this.timerId)
}
}
build() {
Stack({ alignContent: Alignment.Bottom }) {
Column() {
this.headerBar()
Column() {
if (this.currentTab === 0) {
this.tabFlash()
} else if (this.currentTab === 1) {
this.tabFood()
} else if (this.currentTab === 2) {
this.tabFun()
} else if (this.currentTab === 3) {
this.tabBeauty()
} else if (this.currentTab === 4) {
this.tabGroup()
} else {
this.tabOrders()
}
}
.layoutWeight(1)
.width('100%')
this.bottomTabs()
}
.width('100%')
.height('100%')
if (this.showBuyModal) {
this.buyModalOverlay()
}
if (this.showVoucherModal) {
this.voucherModalOverlay()
}
if (this.showBookModal) {
this.bookModalOverlay()
}
if (this.showDetailModal) {
this.detailModalOverlay()
}
if (this.showDeleteModal) {
this.deleteModalOverlay()
}
}
.width('100%')
.height('100%')
.backgroundColor(LIGHT_BG)
}
}
入口组件的核心设计亮点是生命周期与定时器的协同管理。aboutToAppear在组件实例创建后立即调用setInterval启动每秒递减的倒计时定时器,将定时器ID存储在timerId私有属性中。当剩余秒数remainSec递减至0时自动重置为7200秒(2小时),模拟下一轮秒杀场次的开始。aboutToDisappear在组件销毁前通过clearInterval清理定时器资源,防止内存泄漏。这种"启动-递减-重置-清理"的完整生命周期管理是ArkTS定时器使用的标准范式。
build方法采用Stack容器作为根节点,alignContent: Alignment.Bottom使弹窗层默认从底部对齐。主内容区(头部+Tab内容+底部导航)与弹窗层通过条件渲染if语句层叠在Stack中,实现了弹窗浮于内容之上的视觉效果。
四、渐变头部与搜索条

头部组件headerBar通过渐变背景和搜索条构建了品牌识别区域。
@Builder
headerBar() {
Column() {
Row() {
Column() {
Text('📍 城市万象城')
.fontSize(13)
.fontColor('#FFE3D1')
Text('本地生活 · 限时秒杀')
.fontSize(21)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Column() {
Text('🎫')
.fontSize(22)
Text('卡券')
.fontSize(10)
.fontColor('#FFE3D1')
}
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.showVoucherModal = true
})
Column() {
Text('🔍')
.fontSize(22)
Text('搜索')
.fontSize(10)
.fontColor('#FFE3D1')
}
.alignItems(HorizontalAlign.Center)
.margin({ left: 16 })
.onClick(() => {
this.currentTab = 0
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
Row() {
Text('🔥 搜「火锅」立减20')
.fontSize(13)
.fontColor('#B0A79C')
.layoutWeight(1)
Text('搜索')
.fontSize(13)
.fontColor(Color.White)
.padding({ left: 14, right: 14, top: 4, bottom: 4 })
.borderRadius(14)
.backgroundColor(MAIN_ORANGE)
}
.width('100%')
.padding({ left: 14, right: 6, top: 8, bottom: 8 })
.margin({ left: 16, right: 16 })
.borderRadius(20)
.backgroundColor(Color.White)
.shadow(SOFT_SHADOW)
}
.width('100%')
.padding({ bottom: 12 })
.linearGradient({
angle: 160,
colors: [['#FF8A3D', 0], ['#FF5A1E', 0.6], ['#E63E0B', 1]]
})
}
头部采用160度角的三段渐变(#FF8A3D浅橙到#FF5A1E主橙再到#E63E0B深橙),从左上到右下营造日落般的温暖视觉。城市定位文字使用浅橙色调(#FFE3D1)作为辅助信息层,主标题"本地生活 · 限时秒杀"以21号白色粗体字突出品牌核心。卡券和搜索入口以图标+文字的垂直布局呈现,点击卡券触发优惠券弹窗。搜索条以白色圆角胶囊形态浮于渐变头部之上,内部右侧"搜索"按钮使用主橙色背景,左侧展示营销文案"搜「火锅」立减20"引导用户搜索。
五、今日秒杀Tab与倒计时横条

秒杀TabtabFlash集成了倒计时横条、分类九宫格和秒杀商品列表,是应用的流量入口页面。
@Builder
tabFlash() {
Scroll() {
Column() {
Row() {
Column() {
Text('⚡ 整点秒杀场')
.fontSize(16)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Text('每天10点 / 15点 / 20点开抢')
.fontSize(11)
.fontColor('#FFE3D1')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Row() {
this.countBox(Math.floor(this.remainSec / 3600))
Text(':')
.fontSize(18)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.margin({ left: 3, right: 3 })
this.countBox(Math.floor((this.remainSec % 3600) / 60))
Text(':')
.fontSize(18)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.margin({ left: 3, right: 3 })
this.countBox(this.remainSec % 60)
}
}
.width('100%')
.padding(14)
.borderRadius(16)
.linearGradient({
angle: 120,
colors: [['#FF6A2B', 0], ['#D91E00', 1]]
})
.margin({ left: 12, right: 12, top: 12 })
.shadow(CARD_SHADOW)
Grid() {
ForEach(CATEGORY_LIST, (cat: CategoryIcon) => {
GridItem() {
Column() {
Text(cat.icon)
.fontSize(26)
.width(46)
.height(46)
.textAlign(TextAlign.Center)
.borderRadius(14)
.backgroundColor(cat.color)
Text(cat.name)
.fontSize(11)
.fontColor(TEXT_MAIN)
.margin({ top: 6 })
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.onClick(() => {
if (cat.name === '美食秒杀' || cat.name === '火锅烤肉') {
this.currentTab = 1
} else if (cat.name === 'KTV欢唱' || cat.name === '电影演出') {
this.currentTab = 2
} else if (cat.name === '美发美甲') {
this.currentTab = 3
} else {
this.currentTab = 4
}
})
}
}, (cat: CategoryIcon) => cat.name)
}
.columnsTemplate('1fr 1fr 1fr')
.rowsGap(4)
.columnsGap(4)
.width('100%')
.padding(6)
.margin({ left: 12, right: 12, top: 12 })
.borderRadius(16)
.backgroundColor(Color.White)
.shadow(SOFT_SHADOW)
}
.width('100%')
}
.layoutWeight(1)
.width('100%')
.scrollBar(BarState.Off)
}
@Builder
countBox(num: number) {
Text(num < 10 ? '0' + num : '' + num)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.width(30)
.height(26)
.textAlign(TextAlign.Center)
.borderRadius(6)
.backgroundColor('rgba(0,0,0,0.35)')
}
倒计时横条采用120度角的红橙渐变背景(#FF6A2B到#D91E00),左侧展示场次信息"每天10点 / 15点 / 20点开抢",右侧通过三个countBox Builder渲染时、分、秒数字方块。countBox接收数字参数,小于10时自动补零,使用半透明黑色背景圆角方块呈现数字。倒计时通过Math.floor(this.remainSec / 3600)计算小时、Math.floor((this.remainSec % 3600) / 60)计算分钟、this.remainSec % 60计算秒数,每秒更新驱动UI刷新。
分类九宫格使用Grid容器配置columnsTemplate('1fr 1fr 1fr')实现3列布局,9个品类通过ForEach渲染。每个品类拥有独立的配色背景(如美食秒杀#FFF0E5、火锅烤肉#FFEAF2),点击时根据品类名称智能路由到对应的Tab页面,实现了导航到内容的智能跳转逻辑。
秒杀商品列表与进度条
ForEach(FLASH_DEALS, (item: FlashDealItem) => {
Row() {
Column() {
Text(item.img)
.fontSize(34)
Text(item.tag)
.fontSize(10)
.fontColor(Color.White)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor(DEEP_ORANGE)
.margin({ top: 6 })
}
.width(92)
.height(92)
.justifyContent(FlexAlign.Center)
.borderRadius(14)
.linearGradient({
angle: 135,
colors: [['#FFF3EB', 0], ['#FFE0CC', 1]]
})
Column() {
Text(item.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.sub)
.fontSize(11)
.fontColor(TEXT_SUB)
.margin({ top: 4 })
Row() {
Text(item.shopName)
.fontSize(10)
.fontColor(TEXT_SUB)
Text(item.distance)
.fontSize(10)
.fontColor(MAIN_ORANGE)
.margin({ left: 8 })
}
.margin({ top: 4 })
Column() {
Row() {
Column() {
Text('')
.width((item.sold / item.total) * 100 + '%')
.height(6)
.borderRadius(3)
.linearGradient({
angle: 0,
colors: [['#FFB35C', 0], ['#FF4D1C', 1]]
})
}
.width('100%')
.height(6)
.borderRadius(3)
.backgroundColor('#FFE9DD')
}
.width('100%')
Text('已抢' + item.sold + '/' + item.total + '件')
.fontSize(10)
.fontColor('#FF7A45')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.margin({ top: 6 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
Column() {
Text('¥' + item.nowPrice)
.fontSize(19)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
Text('¥' + item.originPrice)
.fontSize(11)
.fontColor(TEXT_SUB)
.decoration({ type: TextDecorationType.LineThrough })
Text('马上抢')
.fontSize(12)
.fontColor(Color.White)
.padding({ left: 14, right: 14, top: 5, bottom: 5 })
.borderRadius(14)
.backgroundColor(MAIN_ORANGE)
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.borderRadius(16)
.backgroundColor(Color.White)
.margin({ left: 12, right: 12, top: 8 })
.shadow(SOFT_SHADOW)
.onClick(() => {
this.selectedFlash = item
this.showBuyModal = true
})
}, (item: FlashDealItem) => 'flash' + item.id)
秒杀商品卡片采用三列横向布局:左侧图形区使用浅橙渐变背景展示品类emoji和营销标签;中间信息区展示标题、副标题、商户名、距离和抢购进度条;右侧价格区展示秒杀价、原价删除线和"马上抢"按钮。进度条是核心交互元素,通过(item.sold / item.total) * 100 + '%'动态计算填充宽度百分比,使用0度角橙红渐变(#FFB35C到#FF4D1C)填充,外层以浅橙色(#FFE9DD)作为轨道背景,下方文字"已抢862/1000件"提供精确的数字反馈。点击卡片设置selectedFlash并打开购买确认弹窗。
六、餐饮美食Tab与双列瀑布流

餐饮TabtabFood实现了榜单胶囊横滑和双列瀑布流商户卡片。
@Builder
tabFood() {
Scroll() {
Column() {
Scroll() {
Row() {
ForEach(['🔥 热销榜', '⭐ 好评榜', '💰 人均榜', '🆕 新店榜', '🎁 回头客榜', '🌃 深夜食堂'], (t: string) => {
Text(t)
.fontSize(12)
.fontColor(t === '🔥 热销榜' ? Color.White : TEXT_MAIN)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(t === '🔥 热销榜' ? MAIN_ORANGE : Color.White)
.margin({ right: 8 })
}, (t: string) => t)
}
.padding({ left: 12, right: 12 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ top: 12 })
Row() {
Column() {
ForEach(FOOD_MERCHANTS.filter((m: MerchantCard) => m.id % 2 === 1), (m: MerchantCard) => {
this.foodCard(m)
}, (m: MerchantCard) => 'L' + m.id)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column().width(8)
Column() {
ForEach(FOOD_MERCHANTS.filter((m: MerchantCard) => m.id % 2 === 0), (m: MerchantCard) => {
this.foodCard(m)
}, (m: MerchantCard) => 'R' + m.id)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding({ left: 12, right: 12, top: 10 })
.alignItems(VerticalAlign.Top)
}
.width('100%')
}
.layoutWeight(1)
.width('100%')
.scrollBar(BarState.Off)
}
@Builder
foodCard(m: MerchantCard) {
Column() {
Column() {
Text(m.hotTag)
.fontSize(10)
.fontColor(Color.White)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius({ topLeft: 12, bottomRight: 12 })
.backgroundColor('rgba(0,0,0,0.3)')
.margin({ top: 8 })
Column().layoutWeight(1)
Text('🍲 ' + m.category)
.fontSize(13)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 8 })
}
.width('100%')
.height(76)
.borderRadius({ topLeft: 14, topRight: 14 })
.linearGradient({
angle: 135,
colors: [[m.gradient[0], 0], [m.gradient[1], 1]]
})
.alignItems(HorizontalAlign.Start)
.padding({ left: 8, right: 8 })
Column() {
Text(m.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text('★' + m.rating)
.fontSize(11)
.fontColor('#FF9500')
Text('月售' + m.monthlySales)
.fontSize(11)
.fontColor(TEXT_SUB)
.margin({ left: 6 })
Column().layoutWeight(1)
Text(m.distance)
.fontSize(10)
.fontColor(TEXT_SUB)
}
.width('100%')
.margin({ top: 5 })
ForEach(m.labels, (lb: string) => {
Text(lb)
.fontSize(10)
.fontColor(MAIN_ORANGE)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(6)
.backgroundColor('#FFF0E5')
.margin({ top: 4 })
}, (lb: string) => m.id + lb)
Row() {
Text('¥' + m.avgPrice + '/人')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
Text('门市¥' + m.originPrice)
.fontSize(10)
.fontColor(TEXT_SUB)
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 6 })
Column().layoutWeight(1)
Text('抢购')
.fontSize(11)
.fontColor(Color.White)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(12)
.backgroundColor(MAIN_ORANGE)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(10)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.borderRadius(14)
.backgroundColor(Color.White)
.margin({ top: 8 })
.shadow(SOFT_SHADOW)
.onClick(() => {
this.selectedMerchant = m
this.showDetailModal = true
})
}
双列瀑布流通过FOOD_MERCHANTS.filter((m: MerchantCard) => m.id % 2 === 1)将奇数ID商户分到左列、偶数ID分到右列,两列之间以8px宽度的间隔Column分隔。每个商户卡片的头部图形区使用商户专属的gradient数组配置双色渐变背景,通过linearGradient的colors参数动态传入渐变色对。热销标签以半透明黑色背景圆角贴片形式悬浮在左上角。标签列表通过ForEach逐行渲染,每个标签使用浅橙色背景和主橙色文字。价格区展示人均消费、门市原价删除线和"抢购"按钮,点击卡片打开商家详情弹窗。
七、消费柱状图与订单管理
订单TabtabOrders集成了消费概览卡、柱状图、筛选条和订单列表,是应用的数据可视化核心。
@Builder
tabOrders() {
Scroll() {
Column() {
Column() {
Row() {
Column() {
Text('¥1,186')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('8月累计消费')
.fontSize(11)
.fontColor('rgba(255,255,255,0.8)')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Column() {
Text('27单')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('共省 ¥486')
.fontSize(11)
.fontColor('#FFE3D1')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row() {
ForEach(MONTH_CHART, (c: ChartBar) => {
Column() {
Text('¥' + c.value)
.fontSize(9)
.fontColor('rgba(255,255,255,0.85)')
.margin({ bottom: 3 })
Column() {
Column()
.width('100%')
.height(c.height)
.borderRadius({ topLeft: 4, topRight: 4 })
.backgroundColor(c.color)
}
.width(20)
.height(90)
.justifyContent(FlexAlign.End)
Text(c.month)
.fontSize(10)
.fontColor('rgba(255,255,255,0.85)')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (c: ChartBar) => c.month)
}
.width('100%')
.margin({ top: 16 })
}
.width('100%')
.padding(16)
.borderRadius(18)
.linearGradient({
angle: 150,
colors: [['#FF7A45', 0], ['#E63E0B', 1]]
})
.margin({ left: 12, right: 12, top: 12 })
.shadow(CARD_SHADOW)
Row() {
ForEach(['全部', '待使用', '待评价', '退款'], (f: string, i: number) => {
Text(f)
.fontSize(12)
.fontColor(this.orderFilter === i ? Color.White : TEXT_MAIN)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(this.orderFilter === i ? MAIN_ORANGE : Color.White)
.margin({ right: 8 })
.onClick(() => {
this.orderFilter = i
})
}, (f: string) => f)
Column().layoutWeight(1)
Text('🗑')
.fontSize(16)
.padding(6)
.onClick(() => {
this.showDeleteModal = true
})
}
.width('100%')
.padding({ left: 12, right: 12, top: 14 })
ForEach(ORDER_ROWS, (o: OrderRow) => {
Column() {
Row() {
Text('🏪 ' + o.shopName)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.layoutWeight(1)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(o.status)
.fontSize(11)
.fontColor(o.statusColor)
}
.width('100%')
Row() {
Text('🧾')
.fontSize(24)
.width(48)
.height(48)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor('#FFF3EB')
Column() {
Text(o.goods + ' × ' + o.count)
.fontSize(13)
.fontColor(TEXT_MAIN)
Text(o.date)
.fontSize(10)
.fontColor(TEXT_SUB)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({ left: 10 })
Column() {
Text('¥' + o.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
if (o.canRate) {
Text('去评价')
.fontSize(11)
.fontColor(MAIN_ORANGE)
.padding({ left: 10, right: 10, top: 3, bottom: 3 })
.borderRadius(10)
.border({ width: 1, color: MAIN_ORANGE })
.margin({ top: 4 })
} else {
Text('查看券码')
.fontSize(11)
.fontColor('#5B6CFF')
.padding({ left: 10, right: 10, top: 3, bottom: 3 })
.borderRadius(10)
.border({ width: 1, color: '#5B6CFF' })
.margin({ top: 4 })
}
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor(Color.White)
.margin({ left: 12, right: 12, top: 8 })
.shadow(SOFT_SHADOW)
}, (o: OrderRow) => 'order' + o.id + o.status)
}
.width('100%')
}
.layoutWeight(1)
.width('100%')
.scrollBar(BarState.Off)
}
消费概览卡使用150度角橙红渐变背景,左侧展示累计消费金额"¥1,186",右侧展示订单数和节省金额。柱状图通过ForEach遍历MONTH_CHART数组渲染六个月的消费数据,每根柱子使用预设的height值控制高度,color值控制填充色。柱子容器固定高度90px,通过justifyContent(FlexAlign.End)使柱子从底部对齐生长,顶部圆角borderRadius({ topLeft: 4, topRight: 4 })呈现圆润的柱顶效果。柱子上方显示金额数值,下方显示月份标签,整体形成完整的消费趋势可视化。
筛选条通过orderFilter状态值控制激活状态,四个筛选选项以胶囊按钮形式横排,选中项使用主橙色背景。订单列表中每条订单的状态文字使用statusColor字段动态着色(待使用为橙色#FF5A1E、待评价为绿色#00A870、已完成为灰色#8A837B),canRate布尔值决定右侧显示"去评价"(橙色描边)还是"查看券码"(蓝色描边)按钮。
八、购买确认弹窗与优惠券墙
购买确认弹窗buyModalOverlay从底部滑出,包含商品信息、数量选择器、优惠明细和支付按钮。
@Builder
buyModalOverlay() {
Column() {
Column() {
Column()
.width(40)
.height(4)
.borderRadius(2)
.backgroundColor('#E5E0DA')
.margin({ top: 10 })
Row() {
Text('确认订单')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
Column().layoutWeight(1)
this.closeModalBtn()
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
Scroll() {
Column() {
Row() {
Text(this.selectedFlash.img)
.fontSize(30)
.width(56)
.height(56)
.textAlign(TextAlign.Center)
.borderRadius(12)
.backgroundColor('#FFF3EB')
Column() {
Text(this.selectedFlash.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.maxLines(2)
Text(this.selectedFlash.shopName + ' · ' + this.selectedFlash.distance)
.fontSize(11)
.fontColor(TEXT_SUB)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({ left: 10 })
Column() {
Text('¥' + this.selectedFlash.nowPrice)
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
Text('已抢' + this.selectedFlash.sold + '件')
.fontSize(10)
.fontColor(TEXT_SUB)
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FFF8F4')
Row() {
Text('购买数量')
.fontSize(14)
.fontColor(TEXT_MAIN)
Column().layoutWeight(1)
Text('−')
.fontSize(18)
.fontColor(TEXT_SUB)
.width(30)
.height(30)
.textAlign(TextAlign.Center)
.borderRadius(15)
.backgroundColor(LIGHT_BG)
.onClick(() => {
if (this.buyCount > 1) {
this.buyCount = this.buyCount - 1
}
})
Text(' ' + this.buyCount + ' ')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.margin({ left: 12, right: 12 })
Text('+')
.fontSize(18)
.fontColor(Color.White)
.width(30)
.height(30)
.textAlign(TextAlign.Center)
.borderRadius(15)
.backgroundColor(MAIN_ORANGE)
.onClick(() => {
if (this.buyCount < 5) {
this.buyCount = this.buyCount + 1
}
})
}
.width('100%')
.padding({ top: 14, bottom: 14 })
Column() {
Row() {
Text('秒杀立减')
.fontSize(12)
.fontColor(TEXT_SUB)
Column().layoutWeight(1)
Text('-¥' + (this.selectedFlash.originPrice - this.selectedFlash.nowPrice))
.fontSize(12)
.fontColor(GREEN_TAG)
}
.width('100%')
Row() {
Text('新客红包')
.fontSize(12)
.fontColor(TEXT_SUB)
Column().layoutWeight(1)
Text('-¥10')
.fontSize(12)
.fontColor(GREEN_TAG)
}
.width('100%')
.margin({ top: 8 })
Row() {
Text('合计')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
Column().layoutWeight(1)
Text('¥' + (this.selectedFlash.nowPrice * this.buyCount - 10).toFixed(2))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.borderRadius(12)
.backgroundColor('#F5FFF9')
Text('立即支付 ¥' + (this.selectedFlash.nowPrice * this.buyCount - 10).toFixed(2))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 13, bottom: 13 })
.borderRadius(24)
.linearGradient({
angle: 90,
colors: [['#FF8A3D', 0], ['#FF3D00', 1]]
})
.margin({ top: 14 })
.onClick(() => {
this.showBuyModal = false
})
Text('倒计时内未支付将释放库存')
.fontSize(10)
.fontColor(TEXT_SUB)
.margin({ top: 8, bottom: 14 })
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.Top)
}
.width('100%')
.height('78%')
.backgroundColor(CARD_BG)
.borderRadius({ topLeft: 24, topRight: 24 })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(20,10,5,0.55)')
.justifyContent(FlexAlign.End)
.onClick(() => {
this.showBuyModal = false
})
}
购买弹窗从底部滑出,占据78%的屏幕高度,顶部带有40x4的圆角拖拽指示条。商品信息区以浅橙色背景卡片展示商品图标、标题、商户信息和价格。数量选择器通过"−"和"+"按钮控制buyCount状态值,限制范围为1到5,减号使用灰色背景、加号使用主橙色背景形成视觉对比。优惠明细区以浅绿色背景展示"秒杀立减"和"新客红包"两行优惠,合计金额通过(this.selectedFlash.nowPrice * this.buyCount - 10).toFixed(2)实时计算。"立即支付"按钮使用90度角橙红渐变背景,底部警示文案"倒计时内未支付将释放库存"营造限时紧迫感。
应用核心流程图
技术点对比分析
| 技术维度 | 今日秒杀Tab | 餐饮美食Tab | 休闲娱乐Tab | 美业丽人Tab | 拼团套票Tab | 我的订单Tab |
|---|---|---|---|---|---|---|
| 核心布局 | Grid九宫格 + ForEach列表 | 双列瀑布流(Row+Column) | 横滑大卡 + 榜单列表 | 预约式垂直列表 | 队伍列表 + 头像槽位 | 柱状图 + 筛选 + 列表 |
| 数据驱动 | FLASH_DEALS + CATEGORY_LIST | FOOD_MERCHANTS + filter | FUN_CARDS + idx索引 | BEAUTY_ITEMS | GROUP_TEAMS + avatars数组 | ORDER_ROWS + MONTH_CHART |
| 弹窗触发 | showBuyModal(购买确认) | showDetailModal(商家详情) | showBookModal(预约) | showBookModal(预约) | showBuyModal(拼团购买) | showDeleteModal(删除确认) |
| 渐变方案 | 红橙渐变(#FF6A2B→#D91E00) | 商户专属gradient数组 | 项目专属gradient数组 | 粉红渐变(#FF6B9D→#E5486B) | 蓝紫渐变(#5B6CFF→#8A3FFF) | 橙红渐变(#FF7A45→#E63E0B) |
| 交互特色 | 倒计时countBox + 进度条 | filter奇偶分列 + 标签云 | 横滑Scroll + 排名序号 | 日期/人数选择器 | 头像叠加margin(-8) + 状态色 | 柱状图 + 筛选状态切换 |
| ForEach键值 | ‘flash’ + id | ‘L’/‘R’ + id | ‘fun’/‘rank’ + id | ‘beauty’ + id | ‘team’ + id | ‘order’ + id + status |
| 价格展示 | nowPrice + originPrice删除线 | avgPrice/人 + originPrice删除线 | price + marketPrice删除线 | price + marketPrice删除线 | price + singlePrice删除线 | price(无删除线) |
| 阴影方案 | CARD_SHADOW + SOFT_SHADOW | SOFT_SHADOW | SOFT_SHADOW | CARD_SHADOW + SOFT_SHADOW | CARD_SHADOW + SOFT_SHADOW | CARD_SHADOW + SOFT_SHADOW |
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

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

完整代码:
// 场景:本地生活服务平台,限时秒杀餐饮/娱乐/美业套餐
// 布局特色:顶部渐变橙头部 + 分类九宫格 + 秒杀倒计时横条 + 双列卡片瀑布 / 横滑券包 / 订单消费柱状图
// 6个底部Tab:今日秒杀 / 餐饮美食 / 休闲娱乐 / 美业丽人 / 拼团套票 / 我的订单
// ==================== 数据结构 ====================
interface CategoryIcon {
name: string
icon: string
color: string
}
interface FlashDealItem {
id: number
title: string
sub: string
originPrice: number
nowPrice: number
sold: number
total: number
tag: string
img: string
shopName: string
distance: string
}
interface MerchantCard {
id: number
name: string
category: string
rating: number
monthlySales: number
avgPrice: number
originPrice: number
distance: string
address: string
labels: string[]
hotTag: string
gradient: string[]
}
interface FunCard {
id: number
name: string
type: string
price: number
marketPrice: number
duration: string
suitPeople: string
stockLeft: number
gradient: string[]
desc: string
}
interface BeautyItem {
id: number
project: string
shop: string
times: string
price: number
marketPrice: number
teachers: number
booked: number
level: string
effect: string
}
interface GroupTeam {
id: number
title: string
teamSize: number
joined: number
price: number
singlePrice: number
leader: string
endTime: string
avatars: string[]
status: string
}
interface OrderRow {
id: number
shopName: string
goods: string
price: number
count: number
status: string
date: string
statusColor: string
canRate: boolean
}
interface VoucherCard {
id: number
amount: number
condition: string
scope: string
expire: string
got: boolean
color: string
}
interface ChartBar {
month: string
value: number
height: number
color: string
}
// ==================== 设计令牌 ====================
const MAIN_ORANGE: string = '#FF5A1E'
const DEEP_ORANGE: string = '#E63E0B'
const LIGHT_BG: string = '#F5F6F8'
const CARD_BG: string = '#FFFFFF'
const TEXT_MAIN: string = '#2B2622'
const TEXT_SUB: string = '#8A837B'
const RED_PRICE: string = '#FF2E00'
const GREEN_TAG: string = '#00A870'
const CARD_SHADOW: ShadowOptions = {
radius: 10,
color: 'rgba(255,90,30,0.10)',
offsetX: 0,
offsetY: 4
}
const SOFT_SHADOW: ShadowOptions = {
radius: 6,
color: 'rgba(0,0,0,0.06)',
offsetX: 0,
offsetY: 2
}
// ==================== 静态数据 ====================
const CATEGORY_LIST: CategoryIcon[] = [
{ name: '美食秒杀', icon: '🍜', color: '#FFF0E5' },
{ name: '火锅烤肉', icon: '🍲', color: '#FFEAF2' },
{ name: '电影演出', icon: '🎬', color: '#EAF3FF' },
{ name: 'KTV欢唱', icon: '🎤', color: '#F3EFFF' },
{ name: '美发美甲', icon: '💅', color: '#FFF6DE' },
{ name: '按摩足疗', icon: '💆', color: '#E8F8F0' },
{ name: '酒店住宿', icon: '🏨', color: '#FFEDEE' },
{ name: '亲子游乐', icon: '🎠', color: '#EDF6FF' },
{ name: '洗车养车', icon: '🚗', color: '#F0F5E8' }
]
const FLASH_DEALS: FlashDealItem[] = [
{ id: 1, title: '双人火锅套餐 128元抢', sub: '肥牛卷+虾滑+时蔬拼盘', originPrice: 268, nowPrice: 128, sold: 862, total: 1000, tag: '爆品返场', img: '🍲', shopName: '蜀香门第火锅(万达店)', distance: '890m' },
{ id: 2, title: '电影票2张 29.9元', sub: '全国通用·节假日可用', originPrice: 90, nowPrice: 29.9, sold: 2310, total: 3000, tag: '今日必抢', img: '🎬', shopName: '星光国际影城', distance: '1.2km' },
{ id: 3, title: 'KTV中包3小时 58元', sub: '含果盘一份·免唱到打烊', originPrice: 198, nowPrice: 58, sold: 517, total: 800, tag: '限购1份', img: '🎤', shopName: '纯量贩KTV(中心店)', distance: '650m' },
{ id: 4, title: '全身推拿60分钟 69元', sub: '老中医坐诊·含肩颈调理', originPrice: 168, nowPrice: 69, sold: 1243, total: 1500, tag: '回头客榜', img: '💆', shopName: '颐和堂推拿馆', distance: '2.1km' },
{ id: 5, title: '日式寿喜烧自助 79元', sub: '和牛畅吃·饮料无限续', originPrice: 158, nowPrice: 79, sold: 326, total: 500, tag: '新品首发', img: '🍱', shopName: '豚屋寿喜烧', distance: '1.8km' },
{ id: 6, title: '洗车精洗+打蜡 39元', sub: '上门取送·漆面养护', originPrice: 128, nowPrice: 39, sold: 768, total: 900, tag: '车主专区', img: '🚗', shopName: '靓车会汽车美容', distance: '3.4km' },
{ id: 7, title: '亲子乐园通票 49元', sub: '1大1小·海洋球+沙池', originPrice: 120, nowPrice: 49, sold: 982, total: 1200, tag: '遛娃神器', img: '🎠', shopName: '蜜芽亲子乐园', distance: '950m' },
{ id: 8, title: '美甲单色 29元', sub: '任选色号·加固不收费', originPrice: 88, nowPrice: 29, sold: 1580, total: 2000, tag: '手快有', img: '💅', shopName: '指间芭蕾美甲工作室', distance: '410m' }
]
const FOOD_MERCHANTS: MerchantCard[] = [
{ id: 1, name: '湘江大院·地道湘菜', category: '湘菜', rating: 4.8, monthlySales: 3245, avgPrice: 68, originPrice: 98, distance: '720m', address: '中山路步行街88号3F', labels: ['招牌剁椒鱼头', '免费停车', '包厢可订'], hotTag: '区湘菜热销第1', gradient: ['#FF7A45', '#FF4D1C'] },
{ id: 2, name: '牛炙日式烧肉', category: '日式烤肉', rating: 4.9, monthlySales: 1876, avgPrice: 112, originPrice: 168, distance: '1.1km', address: '万象城L4-12', labels: ['M9和牛', '炭火现烤', '周五半价'], hotTag: '新店开业5折', gradient: ['#5B6CFF', '#8A3FFF'] },
{ id: 3, name: '老陈家羊肉汤馆', category: '地方菜', rating: 4.7, monthlySales: 4210, avgPrice: 32, originPrice: 45, distance: '350m', address: '建设北路12号', labels: ['三十年老店', '凌晨营业', '免费加汤'], hotTag: '人均32超值', gradient: ['#FFB35C', '#FF7A00'] },
{ id: 4, name: '粤丰轩早茶', category: '粤菜', rating: 4.6, monthlySales: 2688, avgPrice: 55, originPrice: 88, distance: '1.9km', address: '解放南路200号', labels: ['虾饺皇', '全天茶市', '老字号'], hotTag: '早茶7折', gradient: ['#2EC8B0', '#009E82'] },
{ id: 5, name: '麻辣诱惑·川味火锅', category: '火锅', rating: 4.8, monthlySales: 5102, avgPrice: 79, originPrice: 120, distance: '860m', address: '悦荟广场B1-08', labels: ['牛油老锅', '学生证8折', '深夜营业'], hotTag: '月售5000+', gradient: ['#FF5A5A', '#D81E1E'] },
{ id: 6, name: '江南小馆·本帮菜', category: '江浙菜', rating: 4.5, monthlySales: 1432, avgPrice: 61, originPrice: 92, distance: '2.4km', address: '文化路77号', labels: ['糖醋小排', '环境安静', '适合宴请'], hotTag: '工作日特惠', gradient: ['#6BA3FF', '#2D6BDF'] },
{ id: 7, name: '韩宫宴炭火烤肉', category: '韩国料理', rating: 4.7, monthlySales: 2977, avgPrice: 95, originPrice: 142, distance: '1.5km', address: '印象城5F-01', labels: ['部队锅', '小菜免费续', '两人同行1人半价'], hotTag: '烤肉榜第2', gradient: ['#FF8FAB', '#E5486B'] },
{ id: 8, name: '拾光里·融合西餐', category: '西餐', rating: 4.9, monthlySales: 986, avgPrice: 138, originPrice: 210, distance: '3.1km', address: '滨江东路9号', labels: ['江景位', '生日布置', '黑松露牛排'], hotTag: '约会圣地', gradient: ['#9D8BFF', '#5D3FD3'] },
{ id: 9, name: '大碗宽面·陕西风味', category: '面食', rating: 4.4, monthlySales: 6321, avgPrice: 21, originPrice: 30, distance: '180m', address: '站前路小吃街A6', labels: ['油泼面', '现扯现煮', '免费面汤'], hotTag: '面食人气王', gradient: ['#FFC53D', '#FA8C16'] },
{ id: 10, name: '椰鸡部落·椰子鸡', category: '火锅', rating: 4.8, monthlySales: 3890, avgPrice: 87, originPrice: 132, distance: '1.3km', address: '天悦广场2F-15', labels: ['三个椰子一只鸡', '蘸料灵魂', '排队王'], hotTag: '海南直供', gradient: ['#4DD8A6', '#0BA360'] }
]
const FUN_CARDS: FunCard[] = [
{ id: 1, name: '真人CS野战体验', type: '户外运动', price: 88, marketPrice: 260, duration: '3小时', suitPeople: '6-16人', stockLeft: 12, gradient: ['#4A5DFF', '#2B3CD8'], desc: '丛林攻防战+解救人质双模式' },
{ id: 2, name: '轰趴馆包栋畅玩', type: '聚会派对', price: 599, marketPrice: 1580, duration: '10小时', suitPeople: '10-25人', stockLeft: 3, gradient: ['#FF6B9D', '#C4326E'], desc: 'KTV+桌游+麻将+烧烤台一价全含' },
{ id: 3, name: '室内蹦床公园', type: '亲子游乐', price: 45, marketPrice: 118, duration: '全天畅玩', suitPeople: '不限', stockLeft: 58, gradient: ['#00C2A8', '#00867A'], desc: '海绵池+攀岩+滑梯三十个项目' },
{ id: 4, name: '密室逃脱·盗墓笔记', type: '沉浸剧情', price: 68, marketPrice: 128, duration: '90分钟', suitPeople: '4-8人', stockLeft: 21, gradient: ['#7B61FF', '#4B2FD6'], desc: '机械机关+NPC追击恐怖主题' },
{ id: 5, name: 'livehouse现场演出票', type: '演出赛事', price: 120, marketPrice: 280, duration: '3小时', suitPeople: '一人一票', stockLeft: 7, gradient: ['#FF4D6D', '#C9184A'], desc: '独立乐队巡演站·前区站席' },
{ id: 6, name: '电竞网咖包夜', type: '电竞', price: 39, marketPrice: 98, duration: '12小时', suitPeople: '一人一机', stockLeft: 44, gradient: ['#2E8BFF', '#0057D9'], desc: '4090主机+2K240hz屏+免费饮料' }
]
const BEAUTY_ITEMS: BeautyItem[] = [
{ id: 1, project: '韩式剪裁+造型', shop: 'MJ造型(旗舰店)', times: '周一至周日', price: 59, marketPrice: 188, teachers: 12, booked: 2103, level: '总监级', effect: '含洗剪吹+造型拍照' },
{ id: 2, project: '全身精油SPA 90分钟', shop: '悦SPA养生会所', times: '提前1天预约', price: 158, marketPrice: 498, teachers: 8, booked: 876, level: '高级技师', effect: '进口精油+独立SPA房' },
{ id: 3, project: '光子嫩肤单次体验', shop: '焕颜医美(持证)', times: '首次到店', price: 99, marketPrice: 680, teachers: 3, booked: 154, level: '执业医师', effect: '三甲背景医生操作' },
{ id: 4, project: '日式嫁接睫毛', shop: '喵呜美睫工作室', times: '每日限量10单', price: 39, marketPrice: 158, teachers: 5, booked: 1988, level: '金牌美睫师', effect: '3D山茶花款式任选' },
{ id: 5, project: '皮肤清洁小气泡', shop: '净肤管理中心', times: '工作日可用', price: 29, marketPrice: 128, teachers: 6, booked: 3120, level: '资深美容师', effect: '深层清洁+补水面膜' },
{ id: 6, project: '男士理发+修面', shop: 'BARBERSHOP老派', times: '营业时间内', price: 49, marketPrice: 138, teachers: 4, booked: 654, level: '主理人', effect: '热毛巾修面+发型打理' }
]
const GROUP_TEAMS: GroupTeam[] = [
{ id: 1, title: '蜀香门第火锅 4人豪华餐', teamSize: 4, joined: 2, price: 199, singlePrice: 258, leader: '干饭狂魔小张', endTime: '还剩 00:42:18', avatars: ['🦁', '🐻', '', ''], status: '拼单中' },
{ id: 2, title: '星光影城 IMAX 双人票', teamSize: 2, joined: 1, price: 45, singlePrice: 66, leader: '电影发烧友Lily', endTime: '还剩 01:15:03', avatars: ['🦊', ''], status: '差1人成团' },
{ id: 3, title: '纯K量贩 中包欢唱5小时', teamSize: 6, joined: 4, price: 39, singlePrice: 58, leader: '麦霸本霸', endTime: '还剩 02:30:41', avatars: ['🐱', '🐼', '🐯', '🐨', '', ''], status: '差2人' },
{ id: 4, title: '颐和堂推拿 双人足疗', teamSize: 2, joined: 1, price: 99, singlePrice: 138, leader: '养生达人王叔', endTime: '还剩 00:08:55', avatars: ['🐢', ''], status: '即将成团' },
{ id: 5, title: '豚屋寿喜烧 6人的和牛局', teamSize: 6, joined: 5, price: 75, singlePrice: 99, leader: '碳水教父', endTime: '还剩 03:02:27', avatars: ['🐷', '🐮', '🐔', '🐸', '🐙', ''], status: '差1人成团' },
{ id: 6, title: '蜜芽亲子乐园 3组家庭票', teamSize: 3, joined: 1, price: 128, singlePrice: 168, leader: '二宝妈咪', endTime: '还剩 05:44:10', avatars: ['🧸', '', ''], status: '拼单中' }
]
const ORDER_ROWS: OrderRow[] = [
{ id: 1, shopName: '麻辣诱惑·川味火锅', goods: '双人牛油锅套餐', price: 128, count: 1, status: '待使用', date: '08-24 19:32', statusColor: '#FF5A1E', canRate: false },
{ id: 2, shopName: '星光国际影城', goods: '电影票2张(9排)', price: 29.9, count: 1, status: '待使用', date: '08-24 14:05', statusColor: '#FF5A1E', canRate: false },
{ id: 3, shopName: '指间芭蕾美甲', goods: '日式单色美甲', price: 29, count: 1, status: '待评价', date: '08-22 16:48', statusColor: '#00A870', canRate: true },
{ id: 4, shopName: '老陈家羊肉汤馆', goods: '羊肉汤+烧饼套餐', price: 32, count: 2, status: '已完成', date: '08-20 07:15', statusColor: '#8A837B', canRate: true },
{ id: 5, shopName: 'MJ造型(旗舰店)', goods: '韩式剪裁+造型', price: 59, count: 1, status: '已完成', date: '08-18 11:02', statusColor: '#8A837B', canRate: true },
{ id: 6, shopName: '拾光里·融合西餐', goods: '双人晚餐套餐', price: 138, count: 1, status: '退款成功', date: '08-15 19:44', statusColor: '#5B6CFF', canRate: false },
{ id: 7, shopName: '靓车会汽车美容', goods: '精洗+打蜡', price: 39, count: 1, status: '已完成', date: '08-13 10:27', statusColor: '#8A837B', canRate: true },
{ id: 8, shopName: '密室逃脱·盗墓笔记', goods: '4人组队票', price: 68, count: 1, status: '待使用', date: '08-12 21:10', statusColor: '#FF5A1E', canRate: false },
{ id: 9, shopName: '大碗宽面·陕西风味', goods: '油泼面+肉夹馍', price: 21, count: 1, status: '已完成', date: '08-10 12:33', statusColor: '#8A837B', canRate: true }
]
const VOUCHER_WALL: VoucherCard[] = [
{ id: 1, amount: 20, condition: '满100可用', scope: '全场通用', expire: '09-30到期', got: false, color: '#FF5A1E' },
{ id: 2, amount: 8, condition: '无门槛', scope: '美食类目', expire: '09-15到期', got: false, color: '#E63E0B' },
{ id: 3, amount: 50, condition: '满299可用', scope: '丽人美业', expire: '10-01到期', got: true, color: '#FF8A3D' },
{ id: 4, amount: 15, condition: '满59可用', scope: '休闲娱乐', expire: '09-20到期', got: false, color: '#F76B1C' },
{ id: 5, amount: 30, condition: '满150可用', scope: '酒店门票', expire: '09-25到期', got: true, color: '#FF6B2C' }
]
const MONTH_CHART: ChartBar[] = [
{ month: '3月', value: 486, height: 34, color: '#FFD3B6' },
{ month: '4月', value: 620, height: 44, color: '#FFC0A0' },
{ month: '5月', value: 358, height: 25, color: '#FFD3B6' },
{ month: '6月', value: 742, height: 53, color: '#FFAB7E' },
{ month: '7月', value: 918, height: 66, color: '#FF8A4C' },
{ month: '8月', value: 1180, height: 84, color: '#FF5A1E' }
]
const TAB_NAMES: string[] = ['今日秒杀', '餐饮美食', '休闲娱乐', '美业丽人', '拼团套票', '我的订单']
const TAB_ICONS: string[] = ['⚡', '🍜', '🎮', '💅', '👥', '📋']
@Entry
@Component
struct LocalLifePage {
@State currentTab: number = 0
@State remainSec: number = 3875
@State selectedFlash: FlashDealItem = FLASH_DEALS[0]
@State selectedMerchant: MerchantCard = FOOD_MERCHANTS[0]
@State selectedBeauty: BeautyItem = BEAUTY_ITEMS[0]
@State selectedTeam: GroupTeam = GROUP_TEAMS[0]
@State selectedOrder: OrderRow = ORDER_ROWS[0]
@State selectedVoucher: VoucherCard = VOUCHER_WALL[0]
@State showBuyModal: boolean = false
@State showVoucherModal: boolean = false
@State showBookModal: boolean = false
@State showDetailModal: boolean = false
@State showDeleteModal: boolean = false
@State buyCount: number = 1
@State bookedPeople: number = 2
@State bookDate: string = '明天 08-26'
@State orderFilter: number = 0
@State gotVoucherIds: number[] = [3, 5]
private timerId: number = -1
aboutToAppear(): void {
this.timerId = setInterval(() => {
if (this.remainSec > 0) {
this.remainSec = this.remainSec - 1
} else {
this.remainSec = 7200
}
}, 1000)
}
aboutToDisappear(): void {
if (this.timerId >= 0) {
clearInterval(this.timerId)
}
}
// ============ 工具方法 ============
@Builder
modalOverlay(onClose: () => void) {
Column() {
Column() {
this.modalBody()
}
.width('100%')
.constraintSize({ maxHeight: '80%' })
.backgroundColor(CARD_BG)
.borderRadius({ topLeft: 24, topRight: 24 })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(20,10,5,0.55)')
.onClick(() => {
onClose()
})
.justifyContent(FlexAlign.End)
}
@Builder
modalBody() {
// 根据 this.currentTab 与弹框状态分发 —— 实际由 modalOverlay 调用处控制
// 这里通过独立 Builder 组合,具体渲染在调用侧决定
Text('')
.fontSize(1)
.visibility(Visibility.None)
}
@Builder
closeModalBtn() {
Text('✕')
.fontSize(18)
.fontColor(TEXT_SUB)
.width(34)
.height(34)
.textAlign(TextAlign.Center)
.borderRadius(17)
.backgroundColor(LIGHT_BG)
.onClick(() => {
this.showBuyModal = false
this.showVoucherModal = false
this.showBookModal = false
this.showDetailModal = false
this.showDeleteModal = false
})
}
build() {
Stack({ alignContent: Alignment.Bottom }) {
Column() {
// ============ 顶部头部(静态,电商风格) ============
this.headerBar()
// ============ 内容区 ============
Column() {
if (this.currentTab === 0) {
this.tabFlash()
} else if (this.currentTab === 1) {
this.tabFood()
} else if (this.currentTab === 2) {
this.tabFun()
} else if (this.currentTab === 3) {
this.tabBeauty()
} else if (this.currentTab === 4) {
this.tabGroup()
} else {
this.tabOrders()
}
}
.layoutWeight(1)
.width('100%')
// ============ 底部Tab ============
this.bottomTabs()
}
.width('100%')
.height('100%')
// ============ 弹框层 ============
if (this.showBuyModal) {
this.buyModalOverlay()
}
if (this.showVoucherModal) {
this.voucherModalOverlay()
}
if (this.showBookModal) {
this.bookModalOverlay()
}
if (this.showDetailModal) {
this.detailModalOverlay()
}
if (this.showDeleteModal) {
this.deleteModalOverlay()
}
}
.width('100%')
.height('100%')
.backgroundColor(LIGHT_BG)
}
// ==================== 头部 ====================
@Builder
headerBar() {
Column() {
Row() {
Column() {
Text('📍 城市万象城')
.fontSize(13)
.fontColor('#FFE3D1')
Text('本地生活 · 限时秒杀')
.fontSize(21)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Column() {
Text('🎫')
.fontSize(22)
Text('卡券')
.fontSize(10)
.fontColor('#FFE3D1')
}
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.showVoucherModal = true
})
Column() {
Text('🔍')
.fontSize(22)
Text('搜索')
.fontSize(10)
.fontColor('#FFE3D1')
}
.alignItems(HorizontalAlign.Center)
.margin({ left: 16 })
.onClick(() => {
this.currentTab = 0
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
// 搜索条
Row() {
Text('🔥 搜「火锅」立减20')
.fontSize(13)
.fontColor('#B0A79C')
.layoutWeight(1)
Text('搜索')
.fontSize(13)
.fontColor(Color.White)
.padding({ left: 14, right: 14, top: 4, bottom: 4 })
.borderRadius(14)
.backgroundColor(MAIN_ORANGE)
}
.width('100%')
.padding({ left: 14, right: 6, top: 8, bottom: 8 })
.margin({ left: 16, right: 16 })
.borderRadius(20)
.backgroundColor(Color.White)
.shadow(SOFT_SHADOW)
}
.width('100%')
.padding({ bottom: 12 })
.linearGradient({
angle: 160,
colors: [['#FF8A3D', 0], ['#FF5A1E', 0.6], ['#E63E0B', 1]]
})
}
// ==================== Tab0 今日秒杀 ====================
@Builder
tabFlash() {
Scroll() {
Column() {
// 倒计时横条
Row() {
Column() {
Text('⚡ 整点秒杀场')
.fontSize(16)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Text('每天10点 / 15点 / 20点开抢')
.fontSize(11)
.fontColor('#FFE3D1')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Row() {
this.countBox(Math.floor(this.remainSec / 3600))
Text(':')
.fontSize(18)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.margin({ left: 3, right: 3 })
this.countBox(Math.floor((this.remainSec % 3600) / 60))
Text(':')
.fontSize(18)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.margin({ left: 3, right: 3 })
this.countBox(this.remainSec % 60)
}
}
.width('100%')
.padding(14)
.borderRadius(16)
.linearGradient({
angle: 120,
colors: [['#FF6A2B', 0], ['#D91E00', 1]]
})
.margin({ left: 12, right: 12, top: 12 })
.shadow(CARD_SHADOW)
// 分类九宫格
Grid() {
ForEach(CATEGORY_LIST, (cat: CategoryIcon) => {
GridItem() {
Column() {
Text(cat.icon)
.fontSize(26)
.width(46)
.height(46)
.textAlign(TextAlign.Center)
.borderRadius(14)
.backgroundColor(cat.color)
Text(cat.name)
.fontSize(11)
.fontColor(TEXT_MAIN)
.margin({ top: 6 })
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.onClick(() => {
if (cat.name === '美食秒杀' || cat.name === '火锅烤肉') {
this.currentTab = 1
} else if (cat.name === 'KTV欢唱' || cat.name === '电影演出') {
this.currentTab = 2
} else if (cat.name === '美发美甲') {
this.currentTab = 3
} else {
this.currentTab = 4
}
})
}
}, (cat: CategoryIcon) => cat.name)
}
.columnsTemplate('1fr 1fr 1fr')
.rowsGap(4)
.columnsGap(4)
.width('100%')
.padding(6)
.margin({ left: 12, right: 12, top: 12 })
.borderRadius(16)
.backgroundColor(Color.White)
.shadow(SOFT_SHADOW)
// 限时秒杀列表
Row() {
Text('秒杀爆款')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
Text('低至1折 · 手慢无')
.fontSize(11)
.fontColor(MAIN_ORANGE)
.margin({ left: 8 })
Column().layoutWeight(1)
Text('全部 >')
.fontSize(12)
.fontColor(TEXT_SUB)
}
.width('100%')
.padding({ left: 16, right: 16, top: 18, bottom: 8 })
ForEach(FLASH_DEALS, (item: FlashDealItem) => {
Row() {
// 左侧图形
Column() {
Text(item.img)
.fontSize(34)
Text(item.tag)
.fontSize(10)
.fontColor(Color.White)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor(DEEP_ORANGE)
.margin({ top: 6 })
}
.width(92)
.height(92)
.justifyContent(FlexAlign.Center)
.borderRadius(14)
.linearGradient({
angle: 135,
colors: [['#FFF3EB', 0], ['#FFE0CC', 1]]
})
Column() {
Text(item.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.sub)
.fontSize(11)
.fontColor(TEXT_SUB)
.margin({ top: 4 })
Row() {
Text(item.shopName)
.fontSize(10)
.fontColor(TEXT_SUB)
Text(item.distance)
.fontSize(10)
.fontColor(MAIN_ORANGE)
.margin({ left: 8 })
}
.margin({ top: 4 })
// 进度条
Column() {
Row() {
Column() {
Text('')
.width((item.sold / item.total) * 100 + '%')
.height(6)
.borderRadius(3)
.linearGradient({
angle: 0,
colors: [['#FFB35C', 0], ['#FF4D1C', 1]]
})
}
.width('100%')
.height(6)
.borderRadius(3)
.backgroundColor('#FFE9DD')
}
.width('100%')
Text('已抢' + item.sold + '/' + item.total + '件')
.fontSize(10)
.fontColor('#FF7A45')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.margin({ top: 6 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
Column() {
Text('¥' + item.nowPrice)
.fontSize(19)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
Text('¥' + item.originPrice)
.fontSize(11)
.fontColor(TEXT_SUB)
.decoration({ type: TextDecorationType.LineThrough })
Text('马上抢')
.fontSize(12)
.fontColor(Color.White)
.padding({ left: 14, right: 14, top: 5, bottom: 5 })
.borderRadius(14)
.backgroundColor(MAIN_ORANGE)
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.borderRadius(16)
.backgroundColor(Color.White)
.margin({ left: 12, right: 12, top: 8 })
.shadow(SOFT_SHADOW)
.onClick(() => {
this.selectedFlash = item
this.showBuyModal = true
})
}, (item: FlashDealItem) => 'flash' + item.id)
Column()
.height(20)
}
.width('100%')
}
.layoutWeight(1)
.width('100%')
.scrollBar(BarState.Off)
}
@Builder
countBox(num: number) {
Text(num < 10 ? '0' + num : '' + num)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.width(30)
.height(26)
.textAlign(TextAlign.Center)
.borderRadius(6)
.backgroundColor('rgba(0,0,0,0.35)')
}
// ==================== Tab1 餐饮美食(双列卡片) ====================
@Builder
tabFood() {
Scroll() {
Column() {
// 顶部榜单胶囊
Scroll() {
Row() {
ForEach(['🔥 热销榜', '⭐ 好评榜', '💰 人均榜', '🆕 新店榜', '🎁 回头客榜', '🌃 深夜食堂'], (t: string) => {
Text(t)
.fontSize(12)
.fontColor(t === '🔥 热销榜' ? Color.White : TEXT_MAIN)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(t === '🔥 热销榜' ? MAIN_ORANGE : Color.White)
.margin({ right: 8 })
}, (t: string) => t)
}
.padding({ left: 12, right: 12 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ top: 12 })
// 双列瀑布卡片
Row() {
// 左列
Column() {
ForEach(FOOD_MERCHANTS.filter((m: MerchantCard) => m.id % 2 === 1), (m: MerchantCard) => {
this.foodCard(m)
}, (m: MerchantCard) => 'L' + m.id)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column()
.width(8)
// 右列
Column() {
ForEach(FOOD_MERCHANTS.filter((m: MerchantCard) => m.id % 2 === 0), (m: MerchantCard) => {
this.foodCard(m)
}, (m: MerchantCard) => 'R' + m.id)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding({ left: 12, right: 12, top: 10 })
.alignItems(VerticalAlign.Top)
}
.width('100%')
}
.layoutWeight(1)
.width('100%')
.scrollBar(BarState.Off)
}
@Builder
foodCard(m: MerchantCard) {
Column() {
// 店铺图形区
Column() {
Text(m.hotTag)
.fontSize(10)
.fontColor(Color.White)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius({ topLeft: 12, bottomRight: 12 })
.backgroundColor('rgba(0,0,0,0.3)')
.margin({ top: 8 })
Column().layoutWeight(1)
Text('🍲 ' + m.category)
.fontSize(13)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 8 })
}
.width('100%')
.height(76)
.borderRadius({ topLeft: 14, topRight: 14 })
.linearGradient({
angle: 135,
colors: [[m.gradient[0], 0], [m.gradient[1], 1]]
})
.alignItems(HorizontalAlign.Start)
.padding({ left: 8, right: 8 })
Column() {
Text(m.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text('★' + m.rating)
.fontSize(11)
.fontColor('#FF9500')
Text('月售' + m.monthlySales)
.fontSize(11)
.fontColor(TEXT_SUB)
.margin({ left: 6 })
Column().layoutWeight(1)
Text(m.distance)
.fontSize(10)
.fontColor(TEXT_SUB)
}
.width('100%')
.margin({ top: 5 })
ForEach(m.labels, (lb: string) => {
Text(lb)
.fontSize(10)
.fontColor(MAIN_ORANGE)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(6)
.backgroundColor('#FFF0E5')
.margin({ top: 4 })
}, (lb: string) => m.id + lb)
Row() {
Text('¥' + m.avgPrice + '/人')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
Text('门市¥' + m.originPrice)
.fontSize(10)
.fontColor(TEXT_SUB)
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 6 })
Column().layoutWeight(1)
Text('抢购')
.fontSize(11)
.fontColor(Color.White)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(12)
.backgroundColor(MAIN_ORANGE)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(10)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.borderRadius(14)
.backgroundColor(Color.White)
.margin({ top: 8 })
.shadow(SOFT_SHADOW)
.onClick(() => {
this.selectedMerchant = m
this.showDetailModal = true
})
}
// ==================== Tab2 休闲娱乐(横滑大卡+列表) ====================
@Builder
tabFun() {
Scroll() {
Column() {
// 横滑大卡
Text('🎮 精选玩乐')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 8 })
Scroll() {
Row() {
ForEach(FUN_CARDS, (f: FunCard) => {
Column() {
Column() {
Text(f.type)
.fontSize(10)
.fontColor(Color.White)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(10)
.backgroundColor('rgba(255,255,255,0.25)')
Column().layoutWeight(1)
Text(f.name)
.fontSize(15)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
Text(f.desc)
.fontSize(10)
.fontColor('rgba(255,255,255,0.85)')
.margin({ top: 4 })
.maxLines(1)
}
.width(150)
.height(110)
.padding(10)
.borderRadius(16)
.alignItems(HorizontalAlign.Start)
.linearGradient({
angle: 140,
colors: [[f.gradient[0], 0], [f.gradient[1], 1]]
})
Row() {
Text('¥' + f.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
Text('¥' + f.marketPrice)
.fontSize(10)
.fontColor(TEXT_SUB)
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 4 })
}
.margin({ top: 6 })
Text('剩' + f.stockLeft + '单 · ' + f.suitPeople + ' · ' + f.duration)
.fontSize(10)
.fontColor(TEXT_SUB)
.margin({ top: 3 })
}
.width(150)
.margin({ right: 10 })
.onClick(() => {
this.showBookModal = true
})
}, (f: FunCard) => 'fun' + f.id)
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
// 榜单列表
Row() {
Text('🏆 玩乐热榜')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
Column().layoutWeight(1)
Text('每2小时更新')
.fontSize(10)
.fontColor(TEXT_SUB)
}
.width('100%')
.padding({ left: 16, right: 16, top: 20, bottom: 8 })
ForEach(FUN_CARDS, (f: FunCard, idx: number) => {
Row() {
Text('' + (idx + 1))
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(idx < 3 ? '#FF5A1E' : '#C9C2B8')
.width(34)
.textAlign(TextAlign.Center)
Column() {
Text(f.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
Text(f.desc + ' · ' + f.duration + ' · ' + f.suitPeople)
.fontSize(11)
.fontColor(TEXT_SUB)
.margin({ top: 3 })
Row() {
Text('仅剩' + f.stockLeft + '单')
.fontSize(10)
.fontColor(DEEP_ORANGE)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(6)
.backgroundColor('#FFEDE5')
}
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text('¥' + f.price)
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
Text('¥' + f.marketPrice)
.fontSize(10)
.fontColor(TEXT_SUB)
.decoration({ type: TextDecorationType.LineThrough })
Text('组队')
.fontSize(11)
.fontColor(Color.White)
.padding({ left: 12, right: 12, top: 4, bottom: 4 })
.borderRadius(12)
.backgroundColor('#5B6CFF')
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor(Color.White)
.margin({ left: 12, right: 12, top: 8 })
.shadow(SOFT_SHADOW)
.onClick(() => {
this.currentTab = 4
})
}, (f: FunCard) => 'rank' + f.id)
Column()
.height(20)
}
.width('100%')
}
.layoutWeight(1)
.width('100%')
.scrollBar(BarState.Off)
}
// ==================== Tab3 美业丽人(预约式列表) ====================
@Builder
tabBeauty() {
Scroll() {
Column() {
// 预约提示卡
Row() {
Text('💄')
.fontSize(30)
Column() {
Text('今日丽人特价')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('新客首单再减10元,在线预约免排队')
.fontSize(11)
.fontColor('rgba(255,255,255,0.85)')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
.layoutWeight(1)
Text('领取')
.fontSize(12)
.fontColor('#E63E0B')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(Color.White)
.onClick(() => {
this.showVoucherModal = true
})
}
.width('100%')
.padding(14)
.borderRadius(16)
.linearGradient({
angle: 130,
colors: [['#FF6B9D', 0], ['#E5486B', 1]]
})
.margin({ left: 12, right: 12, top: 12 })
.shadow(CARD_SHADOW)
Text('匠心项目')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.width('100%')
.padding({ left: 16, right: 16, top: 18, bottom: 8 })
ForEach(BEAUTY_ITEMS, (b: BeautyItem) => {
Column() {
Row() {
Column() {
Text(b.project)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
Text(b.shop)
.fontSize(12)
.fontColor(TEXT_SUB)
.margin({ top: 3 })
Row() {
Text('🏅 ' + b.level)
.fontSize(10)
.fontColor('#00A870')
Text('技师' + b.teachers + '人')
.fontSize(10)
.fontColor(TEXT_SUB)
.margin({ left: 8 })
Text('已约' + b.booked)
.fontSize(10)
.fontColor(TEXT_SUB)
.margin({ left: 8 })
}
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text('¥' + b.price)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
Text('¥' + b.marketPrice)
.fontSize(10)
.fontColor(TEXT_SUB)
.decoration({ type: TextDecorationType.LineThrough })
Text('预约')
.fontSize(12)
.fontColor(Color.White)
.padding({ left: 14, right: 14, top: 5, bottom: 5 })
.borderRadius(14)
.backgroundColor('#E5486B')
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row() {
Text('🕐 ' + b.times)
.fontSize(10)
.fontColor(TEXT_SUB)
Column().layoutWeight(1)
Text(b.effect)
.fontSize(10)
.fontColor('#E5486B')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('100%')
.padding({ top: 8, bottom: 8 })
.margin({ top: 8 })
.borderRadius(10)
.backgroundColor('#FFF5F7')
}
.width('100%')
.padding(12)
.borderRadius(16)
.backgroundColor(Color.White)
.margin({ left: 12, right: 12, top: 8 })
.shadow(SOFT_SHADOW)
.onClick(() => {
this.selectedBeauty = b
this.showBookModal = true
})
}, (b: BeautyItem) => 'beauty' + b.id)
Column()
.height(20)
}
.width('100%')
}
.layoutWeight(1)
.width('100%')
.scrollBar(BarState.Off)
}
// ==================== Tab4 拼团套票(队伍列表) ====================
@Builder
tabGroup() {
Scroll() {
Column() {
// 拼团规则卡
Row() {
ForEach(['①发起拼团', '②邀请好友', '③人满成团', '④到店核销'], (s: string) => {
Text(s)
.fontSize(11)
.fontColor(Color.White)
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor('rgba(255,255,255,0.2)')
.margin({ right: 6 })
.layoutWeight(1)
.textAlign(TextAlign.Center)
}, (s: string) => s)
}
.width('100%')
.padding(12)
.borderRadius(16)
.linearGradient({
angle: 120,
colors: [['#5B6CFF', 0], ['#8A3FFF', 1]]
})
.margin({ left: 12, right: 12, top: 12 })
.shadow(CARD_SHADOW)
Text('🔥 正在拼的团')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.width('100%')
.padding({ left: 16, right: 16, top: 18, bottom: 8 })
ForEach(GROUP_TEAMS, (g: GroupTeam) => {
Column() {
Row() {
Column() {
Text(g.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text('团长:' + g.leader)
.fontSize(11)
.fontColor(TEXT_SUB)
.margin({ top: 3 })
Text(g.endTime)
.fontSize(11)
.fontColor(DEEP_ORANGE)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text(g.status)
.fontSize(11)
.fontColor(Color.White)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(10)
.backgroundColor(g.status === '差1人成团' ? '#FF5A1E' : '#5B6CFF')
}
}
.width('100%')
// 头像槽位
Row() {
ForEach(g.avatars, (a: string, i: number) => {
Text(a === '' ? '?' : a)
.fontSize(16)
.fontColor(a === '' ? TEXT_SUB : Color.White)
.width(34)
.height(34)
.textAlign(TextAlign.Center)
.borderRadius(17)
.backgroundColor(a === '' ? '#F0EDE8' : '#FFD9C2')
.border({ width: 2, color: Color.White, radius: 17 })
.margin({ right: -8 })
}, (a: string, i: number) => 'av' + g.id + '-' + i)
Text(g.joined + '/' + g.teamSize + '人')
.fontSize(11)
.fontColor(TEXT_SUB)
.margin({ left: 20 })
Column().layoutWeight(1)
Column() {
Row() {
Text('拼团价 ')
.fontSize(10)
.fontColor(TEXT_SUB)
Text('¥' + g.price)
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
}
Text('单买¥' + g.singlePrice)
.fontSize(10)
.fontColor(TEXT_SUB)
.decoration({ type: TextDecorationType.LineThrough })
Text('去拼单')
.fontSize(12)
.fontColor(Color.White)
.padding({ left: 16, right: 16, top: 5, bottom: 5 })
.borderRadius(14)
.backgroundColor(DEEP_ORANGE)
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(12)
.borderRadius(16)
.backgroundColor(Color.White)
.margin({ left: 12, right: 12, top: 8 })
.shadow(SOFT_SHADOW)
.onClick(() => {
this.selectedTeam = g
this.showBuyModal = true
})
}, (g: GroupTeam) => 'team' + g.id)
Column()
.height(20)
}
.width('100%')
}
.layoutWeight(1)
.width('100%')
.scrollBar(BarState.Off)
}
// ==================== Tab5 我的订单(筛选+图表+列表) ====================
@Builder
tabOrders() {
Scroll() {
Column() {
// 消费概览卡 + 柱状图
Column() {
Row() {
Column() {
Text('¥1,186')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('8月累计消费')
.fontSize(11)
.fontColor('rgba(255,255,255,0.8)')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Column() {
Text('27单')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('共省 ¥486')
.fontSize(11)
.fontColor('#FFE3D1')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
// 柱状图
Row() {
ForEach(MONTH_CHART, (c: ChartBar) => {
Column() {
Text('¥' + c.value)
.fontSize(9)
.fontColor('rgba(255,255,255,0.85)')
.margin({ bottom: 3 })
Column() {
Column()
.width('100%')
.height(c.height)
.borderRadius({ topLeft: 4, topRight: 4 })
.backgroundColor(c.color)
}
.width(20)
.height(90)
.justifyContent(FlexAlign.End)
Text(c.month)
.fontSize(10)
.fontColor('rgba(255,255,255,0.85)')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (c: ChartBar) => c.month)
}
.width('100%')
.margin({ top: 16 })
}
.width('100%')
.padding(16)
.borderRadius(18)
.linearGradient({
angle: 150,
colors: [['#FF7A45', 0], ['#E63E0B', 1]]
})
.margin({ left: 12, right: 12, top: 12 })
.shadow(CARD_SHADOW)
// 筛选条
Row() {
ForEach(['全部', '待使用', '待评价', '退款'], (f: string, i: number) => {
Text(f)
.fontSize(12)
.fontColor(this.orderFilter === i ? Color.White : TEXT_MAIN)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(this.orderFilter === i ? MAIN_ORANGE : Color.White)
.margin({ right: 8 })
.onClick(() => {
this.orderFilter = i
})
}, (f: string) => f)
Column().layoutWeight(1)
Text('🗑')
.fontSize(16)
.padding(6)
.onClick(() => {
this.showDeleteModal = true
})
}
.width('100%')
.padding({ left: 12, right: 12, top: 14 })
// 订单列表
ForEach(ORDER_ROWS, (o: OrderRow) => {
Column() {
Row() {
Text('🏪 ' + o.shopName)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.layoutWeight(1)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(o.status)
.fontSize(11)
.fontColor(o.statusColor)
}
.width('100%')
Row() {
Text('🧾')
.fontSize(24)
.width(48)
.height(48)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor('#FFF3EB')
Column() {
Text(o.goods + ' × ' + o.count)
.fontSize(13)
.fontColor(TEXT_MAIN)
Text(o.date)
.fontSize(10)
.fontColor(TEXT_SUB)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({ left: 10 })
Column() {
Text('¥' + o.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
if (o.canRate) {
Text('去评价')
.fontSize(11)
.fontColor(MAIN_ORANGE)
.padding({ left: 10, right: 10, top: 3, bottom: 3 })
.borderRadius(10)
.border({ width: 1, color: MAIN_ORANGE })
.margin({ top: 4 })
} else {
Text('查看券码')
.fontSize(11)
.fontColor('#5B6CFF')
.padding({ left: 10, right: 10, top: 3, bottom: 3 })
.borderRadius(10)
.border({ width: 1, color: '#5B6CFF' })
.margin({ top: 4 })
}
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor(Color.White)
.margin({ left: 12, right: 12, top: 8 })
.shadow(SOFT_SHADOW)
.onClick(() => {
this.selectedOrder = o
this.showDeleteModal = true
})
}, (o: OrderRow) => 'order' + o.id + o.status)
Column()
.height(20)
}
.width('100%')
}
.layoutWeight(1)
.width('100%')
.scrollBar(BarState.Off)
}
// ==================== 底部Tab ====================
@Builder
bottomTabs() {
Row() {
ForEach(TAB_NAMES, (name: string, i: number) => {
Column() {
Text(TAB_ICONS[i])
.fontSize(20)
Text(name)
.fontSize(10)
.fontColor(this.currentTab === i ? MAIN_ORANGE : TEXT_SUB)
.fontWeight(this.currentTab === i ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 2 })
// 指示条
Column()
.width(this.currentTab === i ? 18 : 0)
.height(3)
.borderRadius(2)
.backgroundColor(MAIN_ORANGE)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 8, bottom: 6 })
.onClick(() => {
this.currentTab = i
})
}, (name: string, i: number) => name + i)
}
.width('100%')
.backgroundColor(Color.White)
.shadow({
radius: 12,
color: 'rgba(0,0,0,0.06)',
offsetX: 0,
offsetY: -4
})
}
// ==================== 弹框1:购买确认(秒杀下单) ====================
@Builder
buyModalOverlay() {
Column() {
Column() {
// 顶部把手
Column()
.width(40)
.height(4)
.borderRadius(2)
.backgroundColor('#E5E0DA')
.margin({ top: 10 })
Row() {
Text('确认订单')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
Column().layoutWeight(1)
this.closeModalBtn()
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
Scroll() {
Column() {
// 商品卡
Row() {
Text(this.selectedFlash.img)
.fontSize(30)
.width(56)
.height(56)
.textAlign(TextAlign.Center)
.borderRadius(12)
.backgroundColor('#FFF3EB')
Column() {
Text(this.selectedFlash.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.maxLines(2)
Text(this.selectedFlash.shopName + ' · ' + this.selectedFlash.distance)
.fontSize(11)
.fontColor(TEXT_SUB)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({ left: 10 })
Column() {
Text('¥' + this.selectedFlash.nowPrice)
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
Text('已抢' + this.selectedFlash.sold + '件')
.fontSize(10)
.fontColor(TEXT_SUB)
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FFF8F4')
// 数量选择
Row() {
Text('购买数量')
.fontSize(14)
.fontColor(TEXT_MAIN)
Column().layoutWeight(1)
Text('−')
.fontSize(18)
.fontColor(TEXT_SUB)
.width(30)
.height(30)
.textAlign(TextAlign.Center)
.borderRadius(15)
.backgroundColor(LIGHT_BG)
.onClick(() => {
if (this.buyCount > 1) {
this.buyCount = this.buyCount - 1
}
})
Text(' ' + this.buyCount + ' ')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.margin({ left: 12, right: 12 })
Text('+')
.fontSize(18)
.fontColor(Color.White)
.width(30)
.height(30)
.textAlign(TextAlign.Center)
.borderRadius(15)
.backgroundColor(MAIN_ORANGE)
.onClick(() => {
if (this.buyCount < 5) {
this.buyCount = this.buyCount + 1
}
})
}
.width('100%')
.padding({ top: 14, bottom: 14 })
// 优惠明细
Column() {
Row() {
Text('秒杀立减')
.fontSize(12)
.fontColor(TEXT_SUB)
Column().layoutWeight(1)
Text('-¥' + (this.selectedFlash.originPrice - this.selectedFlash.nowPrice))
.fontSize(12)
.fontColor(GREEN_TAG)
}
.width('100%')
Row() {
Text('新客红包')
.fontSize(12)
.fontColor(TEXT_SUB)
Column().layoutWeight(1)
Text('-¥10')
.fontSize(12)
.fontColor(GREEN_TAG)
}
.width('100%')
.margin({ top: 8 })
Row() {
Text('合计')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
Column().layoutWeight(1)
Text('¥' + (this.selectedFlash.nowPrice * this.buyCount - 10).toFixed(2))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.borderRadius(12)
.backgroundColor('#F5FFF9')
// 支付按钮
Row() {
Text('微信支付')
.fontSize(14)
.fontColor(TEXT_MAIN)
.layoutWeight(1)
Text('¥' + (this.selectedFlash.nowPrice * this.buyCount - 10).toFixed(2))
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FFF3EB')
.margin({ top: 12 })
Text('立即支付 ¥' + (this.selectedFlash.nowPrice * this.buyCount - 10).toFixed(2))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 13, bottom: 13 })
.borderRadius(24)
.linearGradient({
angle: 90,
colors: [['#FF8A3D', 0], ['#FF3D00', 1]]
})
.margin({ top: 14 })
.onClick(() => {
this.showBuyModal = false
})
Text('倒计时内未支付将释放库存')
.fontSize(10)
.fontColor(TEXT_SUB)
.margin({ top: 8, bottom: 14 })
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.Top)
}
.width('100%')
.height('78%')
.backgroundColor(CARD_BG)
.borderRadius({ topLeft: 24, topRight: 24 })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(20,10,5,0.55)')
.justifyContent(FlexAlign.End)
.onClick(() => {
this.showBuyModal = false
})
}
// ==================== 弹框2:领券中心(横滑券墙) ====================
@Builder
voucherModalOverlay() {
Column() {
Column() {
Row() {
Column() {
Text('🎫 领券中心')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('先领券再下单,最高省50元')
.fontSize(11)
.fontColor('rgba(255,255,255,0.8)')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
this.closeModalBtn()
}
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 14 })
.linearGradient({
angle: 135,
colors: [['#FF6A2B', 0], ['#D91E00', 1]]
})
.borderRadius({ topLeft: 24, topRight: 24 })
Scroll() {
Column() {
ForEach(VOUCHER_WALL, (v: VoucherCard) => {
Row() {
// 左侧金额
Column() {
Text('¥')
.fontSize(12)
.fontColor(Color.White)
Text('' + v.amount)
.fontSize(26)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text(v.condition)
.fontSize(10)
.fontColor('rgba(255,255,255,0.9)')
}
.width(88)
.padding({ top: 14, bottom: 14 })
.justifyContent(FlexAlign.Center)
.linearGradient({
angle: 180,
colors: [[v.color, 0], ['#D14E12', 1]]
})
.borderRadius({ topLeft: 14, bottomLeft: 14 })
// 右侧信息
Column() {
Text(v.scope)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
Text('限到店核销使用 · ' + v.expire)
.fontSize(10)
.fontColor(TEXT_SUB)
.margin({ top: 3 })
Text(v.got ? '已领取 · 可在「卡券」查看' : '每人限领1张 · 领取后自动放入卡包')
.fontSize(9)
.fontColor(TEXT_SUB)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ left: 12, right: 8 })
Text(v.got ? '已领' : '领取')
.fontSize(12)
.fontColor(v.got ? TEXT_SUB : Color.White)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(v.got ? LIGHT_BG : v.color)
.margin({ right: 12 })
.onClick(() => {
if (!v.got) {
this.gotVoucherIds.push(v.id)
v.got = true
}
})
}
.width('100%')
.height(76)
.borderRadius(14)
.backgroundColor(Color.White)
.margin({ top: 10 })
.shadow(SOFT_SHADOW)
}, (v: VoucherCard) => 'vc' + v.id + v.got)
Text('— 已展示全部可领优惠券 —')
.fontSize(10)
.fontColor(TEXT_SUB)
.margin({ top: 16, bottom: 16 })
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.Top)
}
.width('100%')
.height('64%')
.backgroundColor('#FFF8F4')
.borderRadius({ topLeft: 24, topRight: 24 })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(20,10,5,0.55)')
.justifyContent(FlexAlign.End)
.onClick(() => {
this.showVoucherModal = false
})
}
// ==================== 弹框3:预约编辑(日期人数可改) ====================
@Builder
bookModalOverlay() {
Column() {
Column() {
Column()
.width(40)
.height(4)
.borderRadius(2)
.backgroundColor('#E5E0DA')
.margin({ top: 10 })
Text('📝 在线预约')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.margin({ top: 12 })
Text(this.selectedBeauty.project + ' · ' + this.selectedBeauty.shop)
.fontSize(12)
.fontColor(TEXT_SUB)
.margin({ top: 4 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 日期选择
Text('预约日期')
.fontSize(13)
.fontColor(TEXT_MAIN)
.width('100%')
.margin({ top: 16 })
Row() {
ForEach(['今天', '明天 08-26', '后天 08-27', '周六 08-29'], (d: string) => {
Text(d)
.fontSize(12)
.fontColor(this.bookDate === d ? Color.White : TEXT_MAIN)
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.borderRadius(12)
.backgroundColor(this.bookDate === d ? '#E5486B' : LIGHT_BG)
.margin({ right: 8 })
.onClick(() => {
this.bookDate = d
})
}, (d: string) => d)
}
.width('100%')
.margin({ top: 8 })
Text('到店人数')
.fontSize(13)
.fontColor(TEXT_MAIN)
.width('100%')
.margin({ top: 16 })
Row() {
ForEach([1, 2, 3, 4, 5], (n: number) => {
Text(n + '人')
.fontSize(12)
.fontColor(this.bookedPeople === n ? Color.White : TEXT_MAIN)
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(12)
.backgroundColor(this.bookedPeople === n ? '#E5486B' : LIGHT_BG)
.margin({ right: 8 })
.onClick(() => {
this.bookedPeople = n
})
}, (n: number) => 'p' + n)
}
.width('100%')
.margin({ top: 8 })
// 备注输入区(展示型)
Column() {
Text('预约备注')
.fontSize(13)
.fontColor(TEXT_MAIN)
.width('100%')
Text('如有特殊需求请写在备注里,商家会提前联系确认')
.fontSize(11)
.fontColor(TEXT_SUB)
.margin({ top: 6 })
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor(LIGHT_BG)
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Row() {
Column() {
Text('预约金')
.fontSize(11)
.fontColor(TEXT_SUB)
Text('¥' + this.selectedBeauty.price)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
}
.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Text('提交预约')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.padding({ left: 30, right: 30, top: 12, bottom: 12 })
.borderRadius(22)
.linearGradient({
angle: 90,
colors: [['#FF6B9D', 0], ['#E5486B', 1]]
})
.onClick(() => {
this.showBookModal = false
})
}
.width('100%')
.margin({ top: 18, bottom: 18 })
}
.width('100%')
.padding({ left: 20, right: 20 })
.backgroundColor(CARD_BG)
.borderRadius({ topLeft: 24, topRight: 24 })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(20,10,5,0.55)')
.justifyContent(FlexAlign.End)
.onClick(() => {
this.showBookModal = false
})
}
// ==================== 弹框4:商家详情(图文详情) ====================
@Builder
detailModalOverlay() {
Column() {
Column() {
// 头图
Column() {
Text('🏪')
.fontSize(40)
.margin({ top: 20 })
Text(this.selectedMerchant.name)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.margin({ top: 8 })
Text(this.selectedMerchant.address + ' · ' + this.selectedMerchant.distance)
.fontSize(11)
.fontColor('rgba(255,255,255,0.85)')
.margin({ top: 4 })
}
.width('100%')
.padding({ top: 10, bottom: 22 })
.linearGradient({
angle: 135,
colors: [[this.selectedMerchant.gradient[0], 0], [this.selectedMerchant.gradient[1], 1]]
})
.borderRadius({ topLeft: 24, topRight: 24 })
Scroll() {
Column() {
// 评分条
Row() {
Column() {
Text('' + this.selectedMerchant.rating)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FF9500')
Text('综合评分')
.fontSize(10)
.fontColor(TEXT_SUB)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
Column().layoutWeight(1)
Column() {
Text('' + this.selectedMerchant.monthlySales)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
Text('月售单数')
.fontSize(10)
.fontColor(TEXT_SUB)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
Column().layoutWeight(1)
Column() {
Text('¥' + this.selectedMerchant.avgPrice)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
Text('人均消费')
.fontSize(10)
.fontColor(TEXT_SUB)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding(16)
.borderRadius(14)
.backgroundColor(Color.White)
.margin({ top: 12 })
.shadow(SOFT_SHADOW)
// 招牌标签
Text('店铺亮点')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.width('100%')
.margin({ top: 16 })
Row() {
ForEach(this.selectedMerchant.labels, (lb: string) => {
Text('✓ ' + lb)
.fontSize(11)
.fontColor(MAIN_ORANGE)
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.borderRadius(10)
.backgroundColor('#FFF0E5')
.margin({ right: 8 })
}, (lb: string) => 'dl' + lb)
}
.width('100%')
.margin({ top: 8 })
// 套餐推荐
Text('到店套餐')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.width('100%')
.margin({ top: 16 })
ForEach(['双人套餐A:招牌菜2荤1素+饮品', '4人套餐B:硬菜4道+主食+甜点', '6人聚会套餐C:全席12道+酒水'], (pk: string, i: number) => {
Row() {
Text(pk)
.fontSize(12)
.fontColor(TEXT_MAIN)
.layoutWeight(1)
Text('¥' + (this.selectedMerchant.avgPrice * (i + 2)))
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(RED_PRICE)
}
.width('100%')
.padding(12)
.borderRadius(10)
.backgroundColor(Color.White)
.margin({ top: 8 })
}, (pk: string) => pk)
Text('立即订座享5折')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 13, bottom: 13 })
.borderRadius(24)
.backgroundColor(this.selectedMerchant.gradient[1])
.margin({ top: 18, bottom: 20 })
.onClick(() => {
this.showDetailModal = false
this.showBuyModal = true
})
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.Top)
}
.width('100%')
.height('76%')
.backgroundColor('#FFF8F4')
.borderRadius({ topLeft: 24, topRight: 24 })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(20,10,5,0.55)')
.justifyContent(FlexAlign.End)
.onClick(() => {
this.showDetailModal = false
})
}
// ==================== 弹框5:删除确认(居中小窗) ====================
@Builder
deleteModalOverlay() {
Column() {
Column() {
Text('🗑️')
.fontSize(36)
.margin({ top: 22 })
Text('确认删除这条订单吗?')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_MAIN)
.margin({ top: 12 })
Text('「' + this.selectedOrder.shopName + '」的订单记录删除后将无法恢复,券码未使用的订单请谨慎操作。')
.fontSize(12)
.fontColor(TEXT_SUB)
.textAlign(TextAlign.Center)
.margin({ top: 8, left: 24, right: 24 })
Row() {
Text('取消')
.fontSize(14)
.fontColor(TEXT_SUB)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({ top: 12, bottom: 12 })
.borderRadius(22)
.backgroundColor(LIGHT_BG)
.onClick(() => {
this.showDeleteModal = false
})
Column().width(12)
Text('确认删除')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({ top: 12, bottom: 12 })
.borderRadius(22)
.backgroundColor(DEEP_ORANGE)
.onClick(() => {
this.showDeleteModal = false
})
}
.width('100%')
.padding({ left: 20, right: 20, top: 20, bottom: 20 })
}
.width('86%')
.borderRadius(20)
.backgroundColor(CARD_BG)
.shadow({
radius: 20,
color: 'rgba(0,0,0,0.18)',
offsetX: 0,
offsetY: 8
})
}
.width('100%')
.height('100%')
.backgroundColor('rgba(20,10,5,0.55)')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.showDeleteModal = false
})
}
}
总结

本文深入剖析了一个基于HarmonyOS ArkTS API 24声明式UI框架的本地生活秒杀平台应用,从数据结构定义、设计令牌配置、入口组件生命周期管理到六大功能Tab和五大弹窗的完整实现进行了逐段代码分析。应用的核心技术亮点在于setInterval定时器与aboutToAppear/aboutToDisappear生命周期的协同管理,实现了秒杀倒计时的实时递减与自动重置机制。Stack容器的alignContent: Alignment.Bottom配置配合条件渲染if语句,使五个弹窗能够从底部优雅滑出并浮于主内容之上,构建了沉浸式的交互体验。
在业务架构层面,应用以"限时紧迫感"为核心设计理念,通过橙红渐变头部、红色价格标签和倒计时数字方块三重视觉元素强化用户的购买冲动。分类九宫格的智能路由跳转实现了从导航到内容的无缝衔接,双列瀑布流通过filter方法按奇偶ID分列实现了商户卡片的紧凑展示。消费柱状图通过预设的height和color值实现了六个月消费趋势的可视化呈现,颜色从浅橙到主橙的渐变直观传达了消费金额的增长趋势。购买确认弹窗的数量选择器、优惠明细和实时合计计算构建了完整的下单流程闭环。
从工程实践角度来看,该应用的代码架构展现了ArkTS声明式UI在复杂多模块场景下的开发效率优势。单入口组件通过@Builder方法拆分管理六个Tab和五个弹窗,@State装饰器统一管理所有可变状态,包括Tab索引、倒计时秒数、选中商品、弹窗开关、数量选择器等。ForEach的键值生成策略确保了列表渲染的性能和正确性,filter方法的奇偶分列实现了瀑布流布局的简洁实现。整个应用为后续接入真实后端API提供了清晰的数据接口定义,aboutToDisappear中的clearInterval确保了组件销毁时的资源清理,体现了ArkTS在移动端资源管理方面的工程规范。
更多推荐




所有评论(0)