## 一、引言:从晨光中开启高效一天 在移动互联网与智能穿戴设备高度普及的今天,“习惯养成“已经从一个模糊的自我管理理念,演变为一个可量化、可追踪、可视化的数字化产品赛道。越来越多的人希望借助手
48小时极限挑战:用HarmonyOS 6.1.1构建一个线上拍卖会——从零到落槌的全栈极限实现之路
黑客松的本质是什么?是48小时内把一个疯狂的想法变成可以落槌的产品。当裁判喊出"开始"的那一刻,我们的主题是"线上拍卖会"——不是简单的商品交易,而是带着艺术馆气质的实时竞价体验。目标:用HarmonyOS ArkTS声明式UI,在限定时间内交付一个完整的线上拍卖平台。
这不是一次优雅的代码之旅,而是一场和时间赛跑的极限编码。2000行代码,6个Tab页面,5个弹窗交互,完整的出价/送拍/撤拍业务闭环——全部在48小时内从空白文件到可以演示。这篇文章复盘的是这场挑战的技术决策、极限实现和创意突破。
如果你也在参加黑客松或者需要在极短时间内交付一个功能完整的原型,这篇复盘会告诉你:哪些架构决策让你快速起飞,哪些技术债你不得不背,以及如何在"能跑就行"和"工程素养"之间找到那个微妙的平衡点。
引言:挑战开始——为什么是线上拍卖会

赛题分析与创意决策
黑客松开幕时,裁判给出的主题是"云端会议场景创新"。大部分队伍选择了在线教育、远程医疗这些热门方向,但我们决定剑走偏锋——线上拍卖会。理由很简单:拍卖场景天然具备"实时竞价"的高频交互、"价高者得"的竞争悬念和"一槌定音"的仪式感,是展示实时交互能力的最佳载体。
从技术挑战角度看,线上拍卖会需要解决三个核心问题:第一,实时竞价数据的展示和更新(出价阶梯、当前价、出价次数);第二,多角色多场景的Tab导航(拍卖厅/拍品/专场/行情/藏品/我的);第三,复杂的表单交互(出价参数配置/送拍托付/藏品编辑/撤拍确认)。这三个问题恰好覆盖了HarmonyOS ArkTS的核心能力面——条件渲染、状态管理、弹窗系统和列表交互。
技术选型与架构决策

技术栈锁定HarmonyOS ArkTS API 24,原因有三:声明式UI写起来快(省掉了命令式UI的样板代码),@State自动触发重绘(省掉了手动DOM操作),组件化天然适合多Tab拆分。48小时挑战的核心原则是"减少决策"——越少的技术选型意味着越少的犹豫时间。
架构决策用"单入口+多子组件"模式。一个 Index189 入口组件持有所有状态和弹窗绑定,六个子组件各自负责一个Tab页面的UI构建。父子间通信用 @Prop 传数据、回调函数传事件。没有引入任何状态管理框架——48小时内学习一个新框架的成本太高,原生 @State 足够撑起这个体量的应用。
视觉设计与配色策略

配色方案选了"勃艮第红#7B1F2B × 香槟金#C9A227 × 象牙白#FBF6EF"三色体系。勃艮第红传达高端拍卖的庄重感,香槟金作为强调色用在按钮和关键价格上,象牙白做背景。这个配色在48小时内的实现策略是"全场硬编码"——没有时间做颜色常量管理,所有色值直接写在组件里。这是技术债,但在黑客松语境下完全可接受。
Tab造型用了"价签"概念——每个Tab卡片顶部有一个小圆孔(6x6的圆点),模拟真实价签的穿绳孔,配合 borderRadius({ topLeft: 16, topRight: 5, bottomLeft: 5, bottomRight: 5 }) 的不对称圆角,形成斜切价签造型。这种"用CSS模拟物理形态"的创意是黑客松中的加分项。
第一回合:数据层极速搭建——九个interface五分钟搞定

极限实现:interface批量定义
interface TabItem189 {
icon: string;
name: string;
}
interface Lot189 {
id: string;
no: string; // 图录号
title: string; // 拍品标题
artist: string; // 艺术家/产地
est: number; // 估价低
estHigh: number; // 估价高
color: string; // 标识色
specialty: string; // 专类
hot: number; // 竞拍热度
}
interface Special189 {
id: string;
no: string; // 期号
name: string; // 专场名称
date: string; // 开槌时间
lots: number; // 拍品数
soldPct: number; // 成交率
state: string; // 状态:拍卖中/已结拍
}
interface BidRec189 {
id: string;
bidder: string; // 竞拍人
price: number; // 出价
time: string; // 出价时间
agency: boolean; // 是否代理出价
}
interface Coll189 {
id: string;
name: string;
artist: string;
value: number; // 估值(万元)
color: string;
insured: boolean; // 是否投保
}
interface Trend189 {
season: string;
price: number;
}
黑客松第一天的前30分钟,全部用来定义数据结构。九个interface一口气写完,每个字段都标注了注释。这个阶段的决策原则是"字段宁多勿少"——后期加字段意味着要改所有使用该interface的地方,而在黑客松时间压力下,改一处接口定义可能引发连锁的编译错误,调试成本极高。
Lot189 是核心实体,包含了拍品从估价到竞拍热度的完整信息。BidRec189 记录出价历史,agency 布尔字段区分"本人出价"和"委托代拍"。Coll189 是用户藏品实体,insured 字段控制保险状态。这些字段在后期UI构建时被直接引用到条件渲染和颜色切换中。
黑客松Tips:interface定义阶段是整个开发中"思考密度最高"的环节。花15分钟想清楚数据结构,能省掉后面数小时的返工。在这个挑战中,数据结构的完整定义让我们在后续UI编码时几乎没有回头改过interface。
静态数据填充——用真实感内容替代占位符

const LOTS189: Lot189[] = [
{ id: 'l1', no: '086', title: '《山涧听雨》', artist: '林慕白', est: 12, estHigh: 18, color: '#7B1F2B', specialty: '现当代书画', hot: 96 },
{ id: 'l2', no: '087', title: '青釉莲瓣盏', artist: '南宋官窑', est: 30, estHigh: 50, color: '#5D4037', specialty: '古董珍玩', hot: 91 },
{ id: 'l3', no: '088', title: '《午夜 No.7》', artist: '苏黎', est: 8, estHigh: 12, color: '#283593', specialty: '当代油画', hot: 88 },
{ id: 'l4', no: '089', title: '田黄瑞兽钮章', artist: '佚名', est: 60, estHigh: 90, color: '#C9A227', specialty: '古董珍玩', hot: 94 },
{ id: 'l5', no: '090', title: '《极简·白》', artist: '陈其', est: 5, estHigh: 8, color: '#455A64', specialty: '当代雕塑', hot: 76 },
{ id: 'l6', no: '091', title: '粉彩花鸟瓶', artist: '清光绪', est: 20, estHigh: 30, color: '#AD1457', specialty: '古董珍玩', hot: 85 },
{ id: 'l7', no: '092', title: '《地铁纪事》', artist: '何小鱼', est: 3, estHigh: 5, color: '#00695C', specialty: '版画插画', hot: 72 },
{ id: 'l8', no: '093', title: '星陨铁陨石原石', artist: '天然', est: 15, estHigh: 22, color: '#37474F', specialty: '奇石珠宝', hot: 81 }
];
const BIDRECS189: BidRec189[] = [
{ id: 'b1', bidder: '藏家 8**2', price: 21, time: '20:41:12', agency: false },
{ id: 'b2', bidder: '藏家 1**7', price: 19, time: '20:40:55', agency: false },
{ id: 'b3', bidder: '委托代拍', price: 17, time: '20:40:31', agency: true },
{ id: 'b4', bidder: '藏家 6**3', price: 15, time: '20:39:58', agency: false },
{ id: 'b5', bidder: '藏家 3**9', price: 13, time: '20:39:20', agency: false },
{ id: 'b6', bidder: '藏家 9**1', price: 12, time: '20:38:47', agency: false }
];
const TRENDS189: Trend189[] = [
{ season: '25春', price: 14 },
{ season: '25秋', price: 16 },
{ season: '26春', price: 19 },
{ season: '26秋', price: 21 }
];
数据填充阶段有个重要的创意决策:用真实感的内容替代"test1/test2"占位符。拍品名"《山涧听雨》““田黄瑞兽钮章”“星陨铁陨石原石”,艺术家"林慕白”“苏黎”,竞拍人用"藏家 8**2"这种脱敏格式——这些内容让Demo在演示时立刻有沉浸感。在黑客松评审环节,评委看到的第一眼很重要,真实感的数据让产品立刻有说服力。
极限实现技巧:出价记录的
agency: true字段配合UI层的天秤座emoji"♎"标识,让"委托代拍"在出价列表中一眼可辨。这种"数据层一个布尔值+UI层一个emoji"的模式在快速开发中极其高效。
第二回合:入口组件——21个状态变量一小时搞定

极限实现:@State批量声明
@Entry
@Component
struct Index189 {
@State curTab: number = 0;
@State collections: Coll189[] = COLLS189;
@State showBidSheet: boolean = false;
@State showConsignSheet: boolean = false;
@State showEditSheet: boolean = false;
@State showWithdrawDialog: boolean = false;
@State showDetailDialog: boolean = false;
@State selLot: Lot189 = LOTS189[0];
@State selColl: Coll189 = COLLS189[0];
@State bidStep: number = 0;
@State agencyOn: boolean = false;
@State secretOn: boolean = false;
@State mindPrice: number = 25;
@State coName: string = '';
@State coCate: number = 0;
@State coStart: number = 10;
@State coReserve: boolean = true;
@State coPickup: boolean = false;
@State edName: string = '';
@State edCate: number = 0;
@State edInsure: boolean = true;
@State edValue: number = 5;
@State wdSync: boolean = true;
21个状态变量,按功能分为四组:弹窗显隐(5个show变量)、选中数据(selLot/selColl)、出价表单(bidStep/agencyOn/secretOn/mindPrice)、送拍表单(co系列)、编辑表单(ed系列)。在黑客松语境下,集中管理所有状态是最快的选择——不需要花时间设计状态分层架构,所有数据都在一个组件里,调试时一断点就能看到全部状态。
创意突破:价签造型Tab

Scroll() {
Row({ space: 10 }) {
ForEach(TABS189, (t: TabItem189, i: number) => {
Column() {
// 价签穿绳孔
Text('')
.width(6)
.height(6)
.borderRadius(3)
.backgroundColor('#E7D8BC')
.margin({ bottom: 6 })
Row({ space: 4 }) {
Text(t.icon)
.fontSize(14)
Text(t.name)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(this.curTab === i ? '#FFE082' : '#7B1F2B')
}
.padding({ left: 10, right: 12, top: 7, bottom: 8 })
}
.borderRadius({ topLeft: 16, topRight: 5, bottomLeft: 5, bottomRight: 5 })
.backgroundColor(this.curTab === i ? '#7B1F2B' : '#FFFFFF')
.border({
width: 1,
color: this.curTab === i ? '#C9A227' : '#EDE0CC'
})
.shadow({
radius: this.curTab === i ? 8 : 0,
color: this.curTab === i ? '#337B1F2B' : '#00000000',
offsetY: 3
})
.onClick(() => {
this.curTab = i;
})
}, (t: TabItem189) => t.name)
}
.padding({ left: 14, right: 14 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ top: 12 })
.constraintSize({ maxHeight: 72 })
这是整个挑战中最有创意的UI细节——价签造型Tab。实现原理是三个技巧的组合:
- 穿绳孔:顶部一个6x6的圆点
Text(''),用backgroundColor('#E7D8BC')香槟浅金色,模拟价签上的穿绳孔。 - 不对称圆角:
borderRadius({ topLeft: 16, topRight: 5, bottomLeft: 5, bottomRight: 5 })左上角大圆角(16),其余三角小圆角(5),形成"左上圆角斜切"的价签轮廓。 - 选中态金边:选中Tab用勃艮第红底+香槟金边框+8px阴影浮起,未选中用白底+浅色边框。
黑客松加分项:评委对"有故事感的设计"的评分远高于"标准组件直接用"。价签造型让这个Tab导航从"又一个横滑Tab"变成了"拍卖会专属的视觉符号"。这个创意的代码成本只有几行borderRadius配置,性价比极高。
条件渲染六Tab切换
if (this.curTab === 0) {
LiveTab189({
onBid: (l: Lot189) => {
this.selLot = l;
this.showBidSheet = true;
}
})
} else if (this.curTab === 1) {
LotTab189({
onDetail: (l: Lot189) => {
this.selLot = l;
this.showDetailDialog = true;
}
})
} else if (this.curTab === 2) {
SpecialTab189()
} else if (this.curTab === 3) {
MarketTab189()
} else if (this.curTab === 4) {
CollTab189({
collections: this.collections,
onEdit: (c: Coll189) => {
this.selColl = c;
this.edName = c.name;
this.edValue = Math.round(c.value);
this.showEditSheet = true;
},
onWithdraw: (c: Coll189) => {
this.selColl = c;
this.showWithdrawDialog = true;
}
})
} else {
MineTab189({
onConsign: () => {
this.showConsignSheet = true;
}
})
}
六个Tab分支用if-else链切换,每个Tab通过回调函数向父组件传递事件。注意 CollTab189 的 onEdit 回调中有一步 this.edValue = Math.round(c.value) ——这是因为藏品估值是浮点数(如4.2万),但编辑表单的加减步进是整数,需要先取整。这种"数据转换放在回调中"的快速处理在黑客松中很常见。
极限实现策略:条件渲染的if-else链在Tab数量不超过10个时完全够用。如果超过10个,可以考虑用对象映射表替代,但在这个挑战中六层if-else的代码可读性反而更好。
第三回合:出价抽屉——核心交互的极限实现
bindSheet绑定策略
.bindSheet($$this.showBidSheet, this.bidSheet189(), {
height: 580,
dragBar: true,
showClose: false,
backgroundColor: '#FFFFFF'
})
.bindSheet($$this.showConsignSheet, this.consignSheet189(), {
height: 560,
dragBar: true,
showClose: false,
backgroundColor: '#FFFFFF'
})
.bindSheet($$this.showEditSheet, this.editSheet189(), {
height: 540,
dragBar: true,
showClose: false,
backgroundColor: '#FFFFFF'
})
.bindContentCover($$this.showWithdrawDialog, this.withdrawDialog189(), {
})
.bindContentCover($$this.showDetailDialog, this.detailDialog189(), {
})
弹窗绑定策略遵循"抽屉表单、居中确认"原则。三个抽屉(出价/送拍/编辑)用 bindSheet,高度分别为580/560/540——出价抽屉最高因为它需要展示拍品信息+当前价+加价幅度+代理/保密开关+心理价位,内容最多。两个居中弹窗(撤拍确认/拍品详情)用 bindContentCover 全屏覆盖。$$ 双向绑定让抽屉下滑关闭时自动同步状态,在黑客松中这是省时间的关键——不需要手写关闭逻辑。
出价表单的交互设计
@Builder
bidSheet189() {
Column() {
// 拖拽条
Row() {
Column() {
Text('')
.width(36)
.height(4)
.borderRadius(2)
.backgroundColor('#E0E0E0')
}
.width('100%')
.alignItems(HorizontalAlign.Center)
.margin({ top: 8 })
}
// 拍品信息卡
Row({ space: 10 }) {
Text('🖼️')
.fontSize(24)
.width(56)
.height(56)
.textAlign(TextAlign.Center)
.borderRadius(12)
.backgroundColor(this.selLot.color)
Column() {
Text('图录号 ' + this.selLot.no + ' · ' + this.selLot.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
Text(this.selLot.artist + ' · ' + this.selLot.specialty)
.fontSize(10)
.fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
// 当前价信息
Row() {
Column() {
Text('当前价')
.fontSize(10)
.fontColor('#999999')
Text('¥21万')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#C62828')
}
.alignItems(HorizontalAlign.Start)
Text('')
.layoutWeight(1)
Column() {
Text('已有 32 次出价')
.fontSize(10)
Text('估价 ¥' + this.selLot.est + '-' + this.selLot.estHigh + '万')
.fontSize(11)
.fontColor('#7B1F2B')
}
.alignItems(HorizontalAlign.End)
}
.padding(12)
.borderRadius(12)
.backgroundColor('#FBF6EF')
// 加价幅度选择
Text('加价幅度')
.fontSize(13)
.fontWeight(FontWeight.Bold)
Row({ space: 8 }) {
ForEach(['+1万', '+2万', '+5万', '+10万'], (c: string, i: number) => {
Text(c)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(this.bidStep === i ? '#FFFFFF' : '#7B1F2B')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor(this.bidStep === i ? '#7B1F2B' : '#F7EBDD')
.border({ width: 1, color: this.bidStep === i ? '#C9A227' : '#E7D8BC' })
.onClick(() => {
this.bidStep = i;
})
}, (c: string) => c)
}
// 代理出价与保密出价开关
Row() {
Column() {
Text('代理出价')
Text('落槌价高于此价时自动放弃')
}
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.agencyOn })
.onChange((v: boolean) => { this.agencyOn = v; })
}
Row() {
Column() {
Text('保密出价')
Text('出价记录仅自己可见')
}
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.secretOn })
.onChange((v: boolean) => { this.secretOn = v; })
}
// 心理价位上限加减器
Row() {
Text('心理价位上限')
Text('')
.layoutWeight(1)
Text('−')
.onClick(() => {
if (this.mindPrice > this.selLot.estHigh) {
this.mindPrice = this.mindPrice - 1;
}
})
Text(' ¥' + this.mindPrice + '万 ')
Text('+')
.onClick(() => {
if (this.mindPrice < 99) {
this.mindPrice = this.mindPrice + 1;
}
})
}
// 确认出价按钮——动态计算总价
Text('确认出价 ¥' + (21 + BIDSTEPS189[this.bidStep]) + ' 万')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.width('100%')
.height(46)
.textAlign(TextAlign.Center)
.borderRadius(23)
.linearGradient({
angle: 90,
colors: [['#7B1F2B', 0], ['#A6384B', 1]]
})
.onClick(() => {
this.showBidSheet = false;
})
}
}
出价表单是这个挑战的核心交互,设计思路是"信息层次+操作层次"双递进:
信息层:拍品信息卡(图录号/标题/艺术家/专类)-> 当前价信息(当前价+出价次数+估价区间)-> 加价幅度选择(1/2/5/10万四档)。这三层信息让用户在出价前充分了解拍品和市场状态。
操作层:代理出价开关(落槌价超限自动放弃)-> 保密出价开关(出价记录仅自己可见)-> 心理价位上限加减器 -> 确认出价按钮。按钮文案动态计算:'确认出价 ¥' + (21 + BIDSTEPS189[this.bidStep]) + ' 万',用当前价(21万)加上选中的加价幅度(1/2/5/10万),实时显示用户将要出的总价。
黑客松创意加分:心理价位加减器的下限保护
this.mindPrice > this.selLot.estHigh确保心理价不会低于估价上限,上限保护this.mindPrice < 99防止溢出。这种"边界保护"在快速开发中容易被忽略,但在演示时如果出现负数或异常值会非常尴尬。
第四回合:实时拍卖厅——LiveTab189的沉浸式体验
极限实现:正在拍卖卡片+出价阶梯
@Component
struct LiveTab189 {
onBid: (l: Lot189) => void = () => {};
build() {
Column() {
// 正在拍卖主卡片
Column() {
Row() {
Text('🔴 正在拍卖')
.fontSize(12)
.fontColor('#FFE082')
Text('')
.layoutWeight(1)
Text('第28期 · 会议室在线 346 人')
.fontSize(10)
.fontColor('#E7CDB1')
}
Row() {
Text('🖼️')
.fontSize(36)
.width(110)
.height(110)
.textAlign(TextAlign.Center)
.borderRadius(14)
.backgroundColor('#3D0A11')
Column() {
Text('《山涧听雨》')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('林慕白 · 现当代书画 · 图录号 086')
.fontSize(10)
.fontColor('#E7CDB1')
Text('当前价')
.fontSize(10)
.fontColor('#E7CDB1')
.margin({ top: 10 })
Text('¥21万')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FFE082')
Row({ space: 6 }) {
Text('🔥 热度 96')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('#C62828')
Text('⚡ 32 次出价')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('#C9A227')
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
}
Row({ space: 12 }) {
Text('出价')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
.layoutWeight(1)
.height(42)
.textAlign(TextAlign.Center)
.borderRadius(21)
.linearGradient({
angle: 135,
colors: [['#E7CDB1', 0], ['#C9A227', 1]]
})
.onClick(() => {
this.onBid(LOTS189[0]);
})
Text('♎ 委托代拍')
.fontSize(12)
.fontColor('#FFFFFF')
.padding({ left: 16, right: 16, top: 11, bottom: 11 })
.borderRadius(21)
.backgroundColor('#33FFFFFF')
}
}
.linearGradient({
angle: 130,
colors: [['#5A1019', 0], ['#8E2B38', 1]]
})
.shadow({ radius: 12, color: '#337B1F2B', offsetY: 6 })
// 实时出价阶梯
Text('实时出价阶梯')
.fontSize(13)
.fontWeight(FontWeight.Bold)
ForEach(BIDRECS189, (b: BidRec189) => {
Row() {
Text(b.agency ? '♎' : '🪧')
.fontSize(10)
.width(34)
.fontColor(b.agency ? '#C9A227' : '#999999')
Column() {
Text(b.bidder + (b.agency ? '(代理)' : ''))
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(b.id === 'b1' ? '#C62828' : '#333333')
Text(b.time + ' 出价')
.fontSize(9)
.fontColor('#BBBBBB')
}
.layoutWeight(1)
.margin({ left: 6 })
Text('¥' + b.price + ' 万')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(b.id === 'b1' ? '#C62828' : '#7B1F2B')
}
.padding(12)
.borderRadius(12)
.backgroundColor(b.id === 'b1' ? '#FFF8F0' : '#FFFFFF')
.margin({ top: 8 })
.shadow({ radius: 4, color: '#147B1F2B', offsetY: 2 })
}, (b: BidRec189) => b.id)
LiveTab189是整个应用"最像拍卖会"的页面。设计上有几个关键决策:
沉浸式主卡片:用深勃艮第渐变背景 linearGradient({ angle: 130, colors: [['#5A1019', 0], ['#8E2B38', 1]] }),拍品图片用110x110的色块替代(Demo无真实图片),当前价"¥21万"用24号香槟金色大字,配合"🔥热度96"和"⚡32次出价"两个标签,营造紧张感。底部"出价"按钮用香槟金渐变+layoutWeight(1)撑满左侧空间,右侧"♎委托代拍"用半透明背景。
出价阶梯列表:每条出价记录显示竞拍人(代理出价用天秤座emoji标识)、出价时间、出价金额。最高出价者(b1)用红色字+暖色背景高亮,让用户一眼看到当前领先者。这种"第一条高亮"的模式在实时竞价场景中极其重要。
黑客松极限策略:
b.id === 'b1' ? highlight : normal这种"用id硬编码判断最高出价者"的写法在生产环境中绝对不行,但在48小时挑战中,这种"能跑能演示"的硬编码是最快的选择。演示时数据是静态的,不会出错。
即将上拍横滑列表
Text('即将上拍')
.fontSize(13)
.fontWeight(FontWeight.Bold)
Scroll() {
Row({ space: 10 }) {
ForEach(LOTS189, (l: Lot189) => {
Column() {
Text('🖼️')
.fontSize(24)
.width(96)
.height(72)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(l.color)
Text('No.' + l.no)
.fontSize(9)
.fontColor('#C9A227')
.margin({ top: 6 })
Text(l.title)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width(96)
Text('¥' + l.est + '-' + l.estHigh + '万')
.fontSize(9)
.fontColor('#C62828')
.margin({ top: 3 })
}
.padding(8)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.shadow({ radius: 4, color: '#147B1F2B', offsetY: 2 })
}, (l: Lot189) => l.id)
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
即将上拍用横滑卡片列表展示,每张卡片96px宽,显示图录号、标题(单行省略)、估价。maxLines(1) 和 textOverflow({ overflow: TextOverflow.Ellipsis }) 的组合是ArkTS中处理长文本溢出的标准方案——单行显示,超出部分用省略号。
第五回合:拍品网格与专场列表
LotTab189——两列网格+卡片点击
@Component
struct LotTab189 {
onDetail: (l: Lot189) => void = () => {};
build() {
Column() {
Row() {
Text('全部拍品 · ' + LOTS189.length + ' 件')
.fontSize(13)
.fontWeight(FontWeight.Bold)
Text('')
.layoutWeight(1)
Text('图录号排序 ↓')
.fontSize(10)
.fontColor('#C4A98A')
}
Grid() {
ForEach(LOTS189, (l: Lot189) => {
GridItem() {
Column() {
Text('🖼️')
.fontSize(30)
.width('100%')
.height(96)
.textAlign(TextAlign.Center)
.borderRadius({ topLeft: 12, topRight: 12 })
.backgroundColor(l.color)
Column() {
Row() {
Text('No.' + l.no)
.fontSize(9)
.fontColor('#C9A227')
Text('')
.layoutWeight(1)
Text('🔥' + l.hot)
.fontSize(9)
.fontColor('#C62828')
}
Text(l.title)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(l.artist + ' · ' + l.specialty)
.fontSize(9)
.fontColor('#999999')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text('估价 ¥' + l.est + '-' + l.estHigh + '万')
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
.margin({ top: 6 })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding(10)
}
.width('100%')
.borderRadius(12)
.backgroundColor('#FFFFFF')
.shadow({ radius: 6, color: '#147B1F2B', offsetY: 2 })
.onClick(() => {
this.onDetail(l);
})
}
}, (l: Lot189) => l.id)
}
.columnsTemplate('1fr 1fr')
.rowsGap(12)
.columnsGap(12)
.width('100%')
.margin({ top: 12 })
}
}
}
拍品页用 Grid 组件做两列网格布局。columnsTemplate('1fr 1fr') 定义两列等宽,rowsGap(12) 和 columnsGap(12) 分别设置行间距和列间距。每张卡片顶部是96px高的色块(替代图片),下方是拍品信息——图录号+热度并排,标题单行省略,艺术家+专类单行省略,估价用勃艮第红粗体。
极限实现技巧:Grid的
columnsTemplate('1fr 1fr')用CSS Grid的fr单位语法,1fr表示"占一份剩余空间"。两列各1fr就是等分宽度。这个语法在ArkTS中完全支持,比手动计算50%-6px之类的宽度方便太多。
SpecialTab189——专场列表+成交率进度条
ForEach(SPECIALS189, (s: Special189) => {
Column() {
Row() {
Column() {
Text(s.no)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
Text(s.date + ' 开槌')
.fontSize(10)
.fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)
Text('')
.layoutWeight(1)
Text(s.state)
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.backgroundColor(stateTag189(s.state))
}
Text(s.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.margin({ top: 8 })
Row() {
Text(s.lots + ' 件拍品')
Text('')
.layoutWeight(1)
Text(s.state === '已结拍' ? '成交率 ' + s.soldPct + '%' : '预展/拍卖中')
.fontColor(s.state === '已结拍' ? '#C62828' : '#FF8F00')
}
// 成交率进度条
Column() {
Row() {
Text('')
.width(s.state === '已结拍' ? s.soldPct + '%' : '0%')
.height(6)
.borderRadius(3)
.backgroundColor(s.soldPct >= 90 ? '#C9A227' : '#7B1F2B')
}
.width('100%')
.height(6)
.borderRadius(3)
.backgroundColor('#F7EBDD')
}
}
.padding(14)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.shadow({ radius: 6, color: '#147B1F2B', offsetY: 2 })
})
专场列表的设计亮点是状态标签的动态着色——stateTag189(s.state) 函数根据专场状态返回不同颜色:拍卖中用红色 #C62828,预展中用橙色 #FF8F00,已结拍用灰色 #9E9E9E。成交率进度条的颜色也有阈值切换:超过90%用香槟金,否则用勃艮第红。
黑客松策略:
stateTag189和specC189这种"状态到颜色"的映射函数在48小时内是最快的颜色管理方案。虽然不如常量对象优雅,但在"每个状态对应一个颜色"的场景中,函数式映射比对象查找更直观。
第六回合:行情数据可视化——纯布局图表的极限挑战
极限实现:柱状图+堆叠条+横向进度条三件套
@Component
struct MarketTab189 {
build() {
Column() {
// 近六期成交率柱状图
Column() {
Text('近六期专场成交率')
.fontSize(13)
.fontWeight(FontWeight.Bold)
Row({ space: 12 }) {
ForEach(SOLDRATES189, (s: SoldRate189) => {
Column({ space: 4 }) {
Column() {
Text('')
.width('100%')
.height(s.pct + '%')
.borderRadius({ topLeft: 3, topRight: 3 })
.backgroundColor(s.pct >= 90 ? '#C9A227' : '#7B1F2B')
}
.width(28)
.height(88)
.justifyContent(FlexAlign.End)
Text(s.no)
.fontSize(9)
.fontColor('#666666')
}
}, (s: SoldRate189) => s.no)
}
.justifyContent(FlexAlign.Center)
}
.padding(14)
.borderRadius(16)
.backgroundColor('#FFFFFF')
// 成交额构成堆叠条
Column() {
Text('本月成交额构成(万元)')
.fontSize(13)
.fontWeight(FontWeight.Bold)
Row() {
ForEach(CATES189, (c: Cate189) => {
Text('')
.layoutWeight(c.w)
.height(14)
.backgroundColor(c.c)
}, (c: Cate189) => c.label)
}
.width('100%')
.clip(true)
.borderRadius(7)
Row({ space: 12 }) {
ForEach(CATES189, (c: Cate189) => {
Row() {
Text('')
.width(8)
.height(8)
.borderRadius(4)
.backgroundColor(c.c)
Text(' ' + c.label + ' ' + c.w + '%')
.fontSize(10)
}
})
}
}
// 热门品类横向进度条
Column() {
Text('热门品类成交额(万元)')
.fontSize(13)
.fontWeight(FontWeight.Bold)
ForEach(HOTCATES189, (h: HotCate189) => {
Row() {
Text(h.name)
.fontSize(11)
.width(88)
Column() {
Row() {
Text('')
.width(h.amount / 4280 * 100 + '%')
.height(8)
.borderRadius(4)
.backgroundColor(h.color)
}
.width('100%')
.height(8)
.borderRadius(4)
.backgroundColor('#F7EBDD')
}
.layoutWeight(1)
Text(h.amount + '')
.fontSize(10)
.fontColor('#999999')
.margin({ left: 8 })
.width(40)
}
})
}
// 保证金卡片
Column() {
Row() {
Text('💰 我的保证金')
.fontColor('#FFFFFF')
Text('¥20,000')
.fontColor('#FFE082')
}
Text('出价资格保证金 · 未中标部分将在专场结束后 3 个工作日内原路退回')
.fontSize(10)
.fontColor('#E7CDB1')
}
.linearGradient({ angle: 130, colors: [['#5A1019', 0], ['#7B1F2B', 1]] })
}
}
}
行情页是数据可视化密度最高的页面,三种图表全部用纯布局组件实现:
柱状图(成交率):外层 Column 固定高度88px + justifyContent(FlexAlign.End) 底部对齐,内层 Text('') 空文本作为柱体,高度用 s.pct + '%' 百分比。这是ArkTS黑客松中最实用的图表技巧——零依赖、零计算成本、视觉效果够用。
堆叠条(成交额构成):用 Row 包裹多个 Text(''),每个用 layoutWeight(c.w) 按权重分配宽度,配合 clip(true) 和 borderRadius(7) 做圆角堆叠条。权重值的约束是加起来必须等于100(38+31+18+13=100)。
横向进度条(热门品类):每条用 Row 布局——左侧名称(固定宽度88),中间进度条(layoutWeight(1) 撑满),右侧数值(固定宽度40)。进度条宽度用 h.amount / 4280 * 100 + '%' 当前值除以最大值再转百分比。
黑客松极限策略:在48小时内不可能引入图表库(学习成本+集成成本太高),纯布局图表是唯一选择。关键公式就三个:柱状图=固定容器+百分比高度+底部对齐;堆叠条=layoutWeight权重分配;横向条=除法百分比宽度。掌握这三个公式,90%的数据可视化需求都能在10分钟内实现。
第七回合:藏品管理与撤拍逻辑
极限实现:藏品CRUD+总估值计算
@Component
struct CollTab189 {
@Prop collections: Coll189[];
onEdit: (c: Coll189) => void = () => {};
onWithdraw: (c: Coll189) => void = () => {};
build() {
Column() {
Row() {
Text('我的藏品 · ' + this.collections.length + ' 件')
Text('')
.layoutWeight(1)
Text('总估值 ¥' + collTotal189(this.collections) + ' 万')
.fontColor('#C62828')
}
ForEach(this.collections, (c: Coll189) => {
Row() {
Text('📦')
.fontSize(22)
.width(52).height(52)
.borderRadius(12)
.backgroundColor(c.color)
Column() {
Row({ space: 6 }) {
Text(c.name)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(c.insured ? '已投保' : '未投保')
.fontSize(8)
.fontColor(c.insured ? '#2E7D32' : '#9E9E9E')
.backgroundColor(c.insured ? '#E8F5E9' : '#F5F5F5')
}
.width('86%')
Text(c.artist + ' · 估值 ¥' + c.value + ' 万')
Row({ space: 8 }) {
Text('编辑')
.onClick(() => { this.onEdit(c); })
Text('撤拍')
.fontColor('#C62828')
.onClick(() => { this.onWithdraw(c); })
}
}
.layoutWeight(1)
}
})
}
}
}
function collTotal189(list: Coll189[]): string {
let t: number = 0;
for (const c of list) {
t = t + c.value;
}
return (Math.round(t * 10) / 10) + '';
}
藏品列表顶部显示总数和总估值。collTotal189 是一个独立函数——遍历藏品列表累加 value,用 Math.round(t * 10) / 10 保留一位小数。在ArkTS中,独立函数(不在struct内的function)可以在文件级别被多个组件复用。这是在没有状态管理框架的情况下实现"纯逻辑复用"的快速方案。
藏品卡片的信息布局是:左侧色块图标 -> 右侧信息区(名称+投保状态/艺术家+估值/编辑+撤拍按钮)。投保状态用 c.insured 布尔值控制标签颜色——已投保用绿色,未投保用灰色。这种"一个布尔值驱动两种视觉状态"的模式在快速开发中极其常见。
极限实现技巧:
width('86%')在名称+标签的Row上设置,是为了给右侧的操作按钮留出空间。这种百分比宽度在ArkTS中比计算像素值方便得多。
撤拍确认弹窗——违约金计算
@Builder
withdrawDialog189() {
Column() {
Text('⚖️')
.fontSize(34)
.width(64).height(64)
.borderRadius(32)
.backgroundColor('#F7EBDD')
Text('确认撤拍该藏品?')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text(this.selColl.name)
.fontColor('#7B1F2B')
Column() {
Text('拍品已进入预展图录,撤拍将收取保留价 5% 的违约金(约 ¥' + Math.round(this.selColl.value * 0.05 * 10) / 10 + ' 万),并从本专场图录中移除。')
.fontSize(11)
.fontColor('#C62828')
.lineHeight(18)
}
.backgroundColor('#FDECEA')
Row() {
Text('同步删除出价记录')
Toggle({ type: ToggleType.Switch, isOn: this.wdSync })
.onChange((v: boolean) => { this.wdSync = v; })
}
Row({ space: 12 }) {
Text('取消')
.backgroundColor('#F5F5F5')
.onClick(() => { this.showWithdrawDialog = false; })
Text('确认撤拍')
.backgroundColor('#C62828')
.onClick(() => {
this.collections = this.collections.filter((x: Coll189) => {
return x.id !== this.selColl.id;
});
this.showWithdrawDialog = false;
})
}
}
}
撤拍弹窗的核心亮点是违约金的动态计算:Math.round(this.selColl.value * 0.05 * 10) / 10。这个公式的含义是:藏品估值乘以5%得到违约金,再乘以10取整再除以10,保留一位小数。比如估值4.2万,违约金=4.20.05=0.21万,Math.round(0.2110)/10=0.2万。
确认撤拍的逻辑用 filter 过滤掉目标藏品并重新赋值 this.collections,触发 @State 重绘。"同步删除出价记录"的 wdSync 开关在Demo中没有实际使用(纯UI展示),在真实场景中这个值会传给后端决定是否级联删除。
黑客松时间管理:违约金计算公式
Math.round(x * 0.05 * 10) / 10看起来复杂,但实际上是"保留一位小数"的标准技巧。在48小时挑战中,这类小算法不应该花太多时间纠结——能算出正确数字就行。
拍卖会核心业务流程图
技术点对比表格
| 技术维度 | 黑客松极限方案 | 正常工程方案 | 48h内取舍理由 |
|---|---|---|---|
| 状态管理 | 21个@State集中入口组件 | AppStorage+@Provide/@Consume分层 | 集中管理断点调试快,分层学习成本高 |
| Tab导航 | 价签造型不对称圆角+ForEach横滑 | 标准Tab组件或底部导航 | 创意造型是黑客松加分项 |
| 弹窗系统 | bindSheet(抽屉)+bindContentCover(居中) | 自定义弹窗组件库 | 内置组件零学习成本,$$自动同步 |
| 数据可视化 | 空Text柱体+layoutWeight堆叠条+百分比横向条 | 第三方图表库Canvas绘制 | 纯布局零依赖,10分钟实现三种图表 |
| 颜色管理 | 全硬编码十六进制+三目表达式 | 颜色常量对象+暗色模式 | 硬编码写起来最快,暗色模式非必须 |
| 数据更新 | concat/filter/map重新赋值 | 状态管理框架不可变更新 | 原生数组方法够用,零框架依赖 |
| 枚举模拟 | 数组索引映射 [‘古董’,‘书画’][idx] | TypeScript enum | ArkTS无enum语法,数组索引最直接 |
| 图标系统 | emoji直接硬编码 | 图标库或SVG资源 | emoji零资源成本,跨平台一致 |
| 函数复用 | 文件级function(如collTotal189) | 工具类/服务层 | 文件级函数最简单,无需import |
| 违约金计算 | Math.round(x0.0510)/10 | 精度库decimal.js | 一位小数够用,精度库引入成本高 |
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

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

完整代码:
// 场景:艺术藏家进入线上拍卖会议室,实时竞拍、送拍托付、专场竞买与行情查询
// 配色:勃艮第红 #7B1F2B × 香槟金 #C9A227 × 象牙白 #FBF6EF
// Tab布局:顶部「价签」造型tab(顶部打孔圆点 + 斜切圆角 topLeft 大圆角,选中勃艮第底金字)
// 弹框:出价(抽屉)/送拍(抽屉)/编辑藏品(抽屉)/撤拍确认(居中)/拍品详情(居中)
interface TabItem189 {
icon: string;
name: string;
}
interface Lot189 {
id: string;
no: string;
title: string;
artist: string;
est: number;
estHigh: number;
color: string;
specialty: string;
hot: number;
}
interface Special189 {
id: string;
no: string;
name: string;
date: string;
lots: number;
soldPct: number;
state: string;
}
interface BidRec189 {
id: string;
bidder: string;
price: number;
time: string;
agency: boolean;
}
interface Cate189 {
label: string;
w: number;
c: string;
}
interface SoldRate189 {
no: string;
pct: number;
}
interface HotCate189 {
name: string;
amount: number;
color: string;
}
interface Coll189 {
id: string;
name: string;
artist: string;
value: number;
color: string;
insured: boolean;
}
interface Trend189 {
season: string;
price: number;
}
const TABS189: TabItem189[] = [
{ icon: '🔨', name: '拍卖厅' },
{ icon: '🖼️', name: '拍品' },
{ icon: '📜', name: '专场' },
{ icon: '📊', name: '行情' },
{ icon: '📦', name: '藏品' },
{ icon: '👤', name: '我的' }
];
const LOTS189: Lot189[] = [
{ id: 'l1', no: '086', title: '《山涧听雨》', artist: '林慕白', est: 12, estHigh: 18, color: '#7B1F2B', specialty: '现当代书画', hot: 96 },
{ id: 'l2', no: '087', title: '青釉莲瓣盏', artist: '南宋官窑', est: 30, estHigh: 50, color: '#5D4037', specialty: '古董珍玩', hot: 91 },
{ id: 'l3', no: '088', title: '《午夜 No.7》', artist: '苏黎', est: 8, estHigh: 12, color: '#283593', specialty: '当代油画', hot: 88 },
{ id: 'l4', no: '089', title: '田黄瑞兽钮章', artist: '佚名', est: 60, estHigh: 90, color: '#C9A227', specialty: '古董珍玩', hot: 94 },
{ id: 'l5', no: '090', title: '《极简·白》', artist: '陈其', est: 5, estHigh: 8, color: '#455A64', specialty: '当代雕塑', hot: 76 },
{ id: 'l6', no: '091', title: '粉彩花鸟瓶', artist: '清光绪', est: 20, estHigh: 30, color: '#AD1457', specialty: '古董珍玩', hot: 85 },
{ id: 'l7', no: '092', title: '《地铁纪事》', artist: '何小鱼', est: 3, estHigh: 5, color: '#00695C', specialty: '版画插画', hot: 72 },
{ id: 'l8', no: '093', title: '星陨铁陨石原石', artist: '天然', est: 15, estHigh: 22, color: '#37474F', specialty: '奇石珠宝', hot: 81 }
];
const SPECIALS189: Special189[] = [
{ id: 's1', no: '第28期', name: '现当代艺术专场', date: '08-25 20:00', lots: 68, soldPct: 0, state: '拍卖中' },
{ id: 's2', no: '第27期', name: '古董珍玩之夜', date: '08-18 20:00', lots: 92, soldPct: 93, state: '已结拍' },
{ id: 's3', no: '第26期', name: '新锐版画专场', date: '08-11 20:00', lots: 54, soldPct: 87, state: '已结拍' },
{ id: 's4', no: '第25期', name: '珠宝翡翠专场', date: '08-04 20:00', lots: 47, soldPct: 78, state: '已结拍' },
{ id: 's5', no: '第24期', name: '名人手迹专场', date: '07-28 20:00', lots: 36, soldPct: 96, state: '已结拍' },
{ id: 's6', no: '第23期', name: '当代雕塑专场', date: '07-21 20:00', lots: 29, soldPct: 69, state: '已结拍' }
];
const BIDRECS189: BidRec189[] = [
{ id: 'b1', bidder: '藏家 8**2', price: 21, time: '20:41:12', agency: false },
{ id: 'b2', bidder: '藏家 1**7', price: 19, time: '20:40:55', agency: false },
{ id: 'b3', bidder: '委托代拍', price: 17, time: '20:40:31', agency: true },
{ id: 'b4', bidder: '藏家 6**3', price: 15, time: '20:39:58', agency: false },
{ id: 'b5', bidder: '藏家 3**9', price: 13, time: '20:39:20', agency: false },
{ id: 'b6', bidder: '藏家 9**1', price: 12, time: '20:38:47', agency: false }
];
const CATES189: Cate189[] = [
{ label: '古董珍玩', w: 38, c: '#7B1F2B' },
{ label: '现当代艺术', w: 31, c: '#A6384B' },
{ label: '珠宝翡翠', w: 18, c: '#D4865B' },
{ label: '其他', w: 13, c: '#E7CDB1' }
];
const SOLDRATES189: SoldRate189[] = [
{ no: '23期', pct: 69 },
{ no: '24期', pct: 96 },
{ no: '25期', pct: 78 },
{ no: '26期', pct: 87 },
{ no: '27期', pct: 93 },
{ no: '28期', pct: 82 }
];
const HOTCATES189: HotCate189[] = [
{ name: '古董珍玩', amount: 4280, color: '#7B1F2B' },
{ name: '现当代书画', amount: 3510, color: '#A6384B' },
{ name: '珠宝翡翠', amount: 2060, color: '#C9A227' },
{ name: '当代油画', amount: 1720, color: '#5D4037' },
{ name: '版画插画', amount: 980, color: '#D4865B' }
];
const COLLS189: Coll189[] = [
{ id: 'c1', name: '《地铁纪事》版画', artist: '何小鱼', value: 4.2, color: '#00695C', insured: true },
{ id: 'c2', name: '青釉小盏(残)', artist: '宋', value: 6.8, color: '#5D4037', insured: true },
{ id: 'c3', name: '《极简·白》小稿', artist: '陈其', value: 2.5, color: '#455A64', insured: false },
{ id: 'c4', name: '星陨铁切片', artist: '天然', value: 1.8, color: '#37474F', insured: false }
];
const TRENDS189: Trend189[] = [
{ season: '25春', price: 14 },
{ season: '25秋', price: 16 },
{ season: '26春', price: 19 },
{ season: '26秋', price: 21 }
];
const STATCHIPS189: string[] = ['全部专场', '拍卖中', '已结拍', '预展中'];
const BIDSTEPS189: number[] = [1, 2, 5, 10];
function specC189(spec: string): string {
if (spec === '古董珍玩') {
return '#7B1F2B';
}
if (spec === '现当代书画') {
return '#A6384B';
}
if (spec === '珠宝翡翠') {
return '#C9A227';
}
return '#5D4037';
}
function stateTag189(state: string): string {
if (state === '拍卖中') {
return '#C62828';
}
if (state === '预展中') {
return '#FF8F00';
}
return '#9E9E9E';
}
@Entry
@Component
struct Index189 {
@State curTab: number = 0;
@State collections: Coll189[] = COLLS189;
@State showBidSheet: boolean = false;
@State showConsignSheet: boolean = false;
@State showEditSheet: boolean = false;
@State showWithdrawDialog: boolean = false;
@State showDetailDialog: boolean = false;
@State selLot: Lot189 = LOTS189[0];
@State selColl: Coll189 = COLLS189[0];
@State bidStep: number = 0;
@State agencyOn: boolean = false;
@State secretOn: boolean = false;
@State mindPrice: number = 25;
@State coName: string = '';
@State coCate: number = 0;
@State coStart: number = 10;
@State coReserve: boolean = true;
@State coPickup: boolean = false;
@State edName: string = '';
@State edCate: number = 0;
@State edInsure: boolean = true;
@State edValue: number = 5;
@State wdSync: boolean = true;
build() {
Column() {
Column() {
Row() {
Text('🔨 一槌·云拍')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('')
.layoutWeight(1)
Text('🔮')
.fontSize(16)
.width(34)
.height(34)
.textAlign(TextAlign.Center)
.borderRadius(17)
.backgroundColor('#33FFFFFF')
}
.width('100%')
Row() {
Column() {
Text('第28期 · 现当代艺术专场')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('距开槌下一件拍品')
.fontSize(10)
.fontColor('#E7CDB1')
.margin({ top: 4 })
Text('02:14:36')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FFE082')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('🔔\n开槌\n提醒')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
.width(64)
.height(64)
.textAlign(TextAlign.Center)
.borderRadius(32)
.linearGradient({
angle: 135,
colors: [['#E7CDB1', 0], ['#C9A227', 1]]
})
.shadow({ radius: 10, color: '#66C9A227', offsetY: 4 })
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding({ left: 14, right: 14, top: 10, bottom: 14 })
.linearGradient({
angle: 135,
colors: [['#5A1019', 0], ['#7B1F2B', 1]]
})
Row() {
Text('🔍')
.fontSize(14)
.margin({ left: 12 })
Text(' 搜索图录号 / 艺术家 / 专场')
.fontSize(12)
.fontColor('#C4A98A')
.margin({ left: 6 })
}
.width('100%')
.height(36)
.borderRadius(18)
.backgroundColor('#FFFFFF')
.margin({ top: 10 })
Scroll() {
Row({ space: 10 }) {
ForEach(TABS189, (t: TabItem189, i: number) => {
Column() {
Text('')
.width(6)
.height(6)
.borderRadius(3)
.backgroundColor('#E7D8BC')
.margin({ bottom: 6 })
Row({ space: 4 }) {
Text(t.icon)
.fontSize(14)
Text(t.name)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(this.curTab === i ? '#FFE082' : '#7B1F2B')
}
.padding({ left: 10, right: 12, top: 7, bottom: 8 })
}
.borderRadius({ topLeft: 16, topRight: 5, bottomLeft: 5, bottomRight: 5 })
.backgroundColor(this.curTab === i ? '#7B1F2B' : '#FFFFFF')
.border({
width: 1,
color: this.curTab === i ? '#C9A227' : '#EDE0CC'
})
.shadow({
radius: this.curTab === i ? 8 : 0,
color: this.curTab === i ? '#337B1F2B' : '#00000000',
offsetY: 3
})
.onClick(() => {
this.curTab = i;
})
}, (t: TabItem189) => t.name)
}
.padding({ left: 14, right: 14 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ top: 12 })
.constraintSize({ maxHeight: 72 })
Scroll() {
Column() {
if (this.curTab === 0) {
LiveTab189({
onBid: (l: Lot189) => {
this.selLot = l;
this.showBidSheet = true;
}
})
} else if (this.curTab === 1) {
LotTab189({
onDetail: (l: Lot189) => {
this.selLot = l;
this.showDetailDialog = true;
}
})
} else if (this.curTab === 2) {
SpecialTab189()
} else if (this.curTab === 3) {
MarketTab189()
} else if (this.curTab === 4) {
CollTab189({
collections: this.collections,
onEdit: (c: Coll189) => {
this.selColl = c;
this.edName = c.name;
this.edValue = Math.round(c.value);
this.showEditSheet = true;
},
onWithdraw: (c: Coll189) => {
this.selColl = c;
this.showWithdrawDialog = true;
}
})
} else {
MineTab189({
onConsign: () => {
this.showConsignSheet = true;
}
})
}
}
.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 24 })
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.layoutWeight(1)
.align(Alignment.TopStart)
}
.width('100%')
.height('100%')
.backgroundColor('#FBF6EF')
.bindSheet($$this.showBidSheet, this.bidSheet189(), {
height: 580,
dragBar: true,
showClose: false,
backgroundColor: '#FFFFFF'
})
.bindSheet($$this.showConsignSheet, this.consignSheet189(), {
height: 560,
dragBar: true,
showClose: false,
backgroundColor: '#FFFFFF'
})
.bindSheet($$this.showEditSheet, this.editSheet189(), {
height: 540,
dragBar: true,
showClose: false,
backgroundColor: '#FFFFFF'
})
.bindContentCover($$this.showWithdrawDialog, this.withdrawDialog189(), {
})
.bindContentCover($$this.showDetailDialog, this.detailDialog189(), {
})
}
@Builder
bidSheet189() {
Column() {
Row() {
Column() {
Text('')
.width(36)
.height(4)
.borderRadius(2)
.backgroundColor('#E0E0E0')
}
.width('100%')
.alignItems(HorizontalAlign.Center)
.margin({ top: 8 })
}
.width('100%')
Row({ space: 10 }) {
Text('🖼️')
.fontSize(24)
.width(56)
.height(56)
.textAlign(TextAlign.Center)
.borderRadius(12)
.backgroundColor(this.selLot.color)
Column() {
Text('图录号 ' + this.selLot.no + ' · ' + this.selLot.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
Text(this.selLot.artist + ' · ' + this.selLot.specialty)
.fontSize(10)
.fontColor('#999999')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.margin({ top: 10 })
Row() {
Column() {
Text('当前价')
.fontSize(10)
.fontColor('#999999')
Text('¥21万')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#C62828')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Text('')
.layoutWeight(1)
Column() {
Text('已有 32 次出价')
.fontSize(10)
.fontColor('#999999')
Text('估价 ¥' + this.selLot.est + '-' + this.selLot.estHigh + '万')
.fontSize(11)
.fontColor('#7B1F2B')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FBF6EF')
.margin({ top: 14 })
Text('加价幅度')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.margin({ top: 18 })
Row({ space: 8 }) {
ForEach(['+1万', '+2万', '+5万', '+10万'], (c: string, i: number) => {
Text(c)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(this.bidStep === i ? '#FFFFFF' : '#7B1F2B')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor(this.bidStep === i ? '#7B1F2B' : '#F7EBDD')
.border({ width: 1, color: this.bidStep === i ? '#C9A227' : '#E7D8BC' })
.onClick(() => {
this.bidStep = i;
})
}, (c: string) => c)
}
.margin({ top: 8 })
Row() {
Column() {
Text('代理出价')
.fontSize(13)
.fontColor('#333333')
Text('落槌价高于此价时自动放弃')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.agencyOn })
.onChange((v: boolean) => {
this.agencyOn = v;
})
}
.width('100%')
.margin({ top: 18 })
Row() {
Column() {
Text('保密出价')
.fontSize(13)
.fontColor('#333333')
Text('出价记录仅自己可见')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.secretOn })
.onChange((v: boolean) => {
this.secretOn = v;
})
}
.width('100%')
.margin({ top: 14 })
Row() {
Text('心理价位上限')
.fontSize(13)
.fontColor('#333333')
Text('')
.layoutWeight(1)
Text('−')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
.width(30)
.height(30)
.textAlign(TextAlign.Center)
.borderRadius(15)
.backgroundColor('#F7EBDD')
.onClick(() => {
if (this.mindPrice > this.selLot.estHigh) {
this.mindPrice = this.mindPrice - 1;
}
})
Text(' ¥' + this.mindPrice + '万 ')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#C62828')
Text('+')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.width(30)
.height(30)
.textAlign(TextAlign.Center)
.borderRadius(15)
.backgroundColor('#7B1F2B')
.onClick(() => {
if (this.mindPrice < 99) {
this.mindPrice = this.mindPrice + 1;
}
})
}
.width('100%')
.margin({ top: 18 })
Text('确认出价 ¥' + (21 + BIDSTEPS189[this.bidStep]) + ' 万')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.width('100%')
.height(46)
.textAlign(TextAlign.Center)
.borderRadius(23)
.linearGradient({
angle: 90,
colors: [['#7B1F2B', 0], ['#A6384B', 1]]
})
.margin({ top: 24 })
.onClick(() => {
this.showBidSheet = false;
})
}
.width('100%')
.padding({ left: 20, right: 20, bottom: 24 })
}
@Builder
consignSheet189() {
Column() {
Row() {
Column() {
Text('')
.width(36)
.height(4)
.borderRadius(2)
.backgroundColor('#E0E0E0')
}
.width('100%')
.alignItems(HorizontalAlign.Center)
.margin({ top: 8 })
}
.width('100%')
Text('送拍托付')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
.margin({ top: 10 })
Text('拍品名称')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.margin({ top: 16 })
TextInput({ placeholder: '例如:粉彩花鸟瓶(清光绪)', text: this.coName })
.fontSize(13)
.height(42)
.borderRadius(10)
.backgroundColor('#FBF6EF')
.margin({ top: 8 })
.onChange((v: string) => {
this.coName = v;
})
Text('拍品分类')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.margin({ top: 18 })
Row({ space: 8 }) {
ForEach(['古董珍玩', '现当代书画', '珠宝翡翠', '当代油画'], (c: string, i: number) => {
Text(c)
.fontSize(11)
.fontColor(this.coCate === i ? '#FFFFFF' : '#7B1F2B')
.padding({ left: 12, right: 12, top: 7, bottom: 7 })
.borderRadius(14)
.backgroundColor(this.coCate === i ? '#7B1F2B' : '#F7EBDD')
.onClick(() => {
this.coCate = i;
})
}, (c: string) => c)
}
.margin({ top: 8 })
Row() {
Text('建议起拍价')
.fontSize(13)
.fontColor('#333333')
Text('')
.layoutWeight(1)
Text('−')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
.width(30)
.height(30)
.textAlign(TextAlign.Center)
.borderRadius(15)
.backgroundColor('#F7EBDD')
.onClick(() => {
if (this.coStart > 1) {
this.coStart = this.coStart - 1;
}
})
Text(' ¥' + this.coStart + '万 ')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
Text('+')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.width(30)
.height(30)
.textAlign(TextAlign.Center)
.borderRadius(15)
.backgroundColor('#7B1F2B')
.onClick(() => {
if (this.coStart < 88) {
this.coStart = this.coStart + 1;
}
})
}
.width('100%')
.margin({ top: 18 })
Row() {
Column() {
Text('设置保留价')
.fontSize(13)
.fontColor('#333333')
Text('低于保留价不成交,流拍退还')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.coReserve })
.onChange((v: boolean) => {
this.coReserve = v;
})
}
.width('100%')
.margin({ top: 18 })
Row() {
Column() {
Text('上门取货')
.fontSize(13)
.fontColor('#333333')
Text('专业艺术品运输上门取件')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.coPickup })
.onChange((v: boolean) => {
this.coPickup = v;
})
}
.width('100%')
.margin({ top: 14 })
Text('提交送拍申请')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.width('100%')
.height(46)
.textAlign(TextAlign.Center)
.borderRadius(23)
.linearGradient({
angle: 90,
colors: [['#7B1F2B', 0], ['#A6384B', 1]]
})
.margin({ top: 24 })
.onClick(() => {
this.showConsignSheet = false;
})
}
.width('100%')
.padding({ left: 20, right: 20, bottom: 24 })
}
@Builder
editSheet189() {
Column() {
Row() {
Column() {
Text('')
.width(36)
.height(4)
.borderRadius(2)
.backgroundColor('#E0E0E0')
}
.width('100%')
.alignItems(HorizontalAlign.Center)
.margin({ top: 8 })
}
.width('100%')
Text('编辑藏品信息')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
.margin({ top: 10 })
Text('藏品名称')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.margin({ top: 16 })
TextInput({ placeholder: '请输入藏品名称', text: this.edName })
.fontSize(13)
.height(42)
.borderRadius(10)
.backgroundColor('#FBF6EF')
.margin({ top: 8 })
.onChange((v: string) => {
this.edName = v;
})
Text('藏品分类')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.margin({ top: 18 })
Row({ space: 8 }) {
ForEach(['古董珍玩', '书画', '珠宝', '当代艺术'], (c: string, i: number) => {
Text(c)
.fontSize(11)
.fontColor(this.edCate === i ? '#FFFFFF' : '#7B1F2B')
.padding({ left: 12, right: 12, top: 7, bottom: 7 })
.borderRadius(14)
.backgroundColor(this.edCate === i ? '#7B1F2B' : '#F7EBDD')
.onClick(() => {
this.edCate = i;
})
}, (c: string) => c)
}
.margin({ top: 8 })
Row() {
Text('当前估值')
.fontSize(13)
.fontColor('#333333')
Text('')
.layoutWeight(1)
Text('−')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
.width(30)
.height(30)
.textAlign(TextAlign.Center)
.borderRadius(15)
.backgroundColor('#F7EBDD')
.onClick(() => {
if (this.edValue > 1) {
this.edValue = this.edValue - 1;
}
})
Text(' ¥' + this.edValue + '万 ')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
Text('+')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.width(30)
.height(30)
.textAlign(TextAlign.Center)
.borderRadius(15)
.backgroundColor('#7B1F2B')
.onClick(() => {
if (this.edValue < 99) {
this.edValue = this.edValue + 1;
}
})
}
.width('100%')
.margin({ top: 18 })
Row() {
Column() {
Text('藏品投保')
.fontSize(13)
.fontColor('#333333')
Text('按估值 0.3%/年 计保费')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.edInsure })
.onChange((v: boolean) => {
this.edInsure = v;
})
}
.width('100%')
.margin({ top: 18 })
Text('保存修改')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.width('100%')
.height(46)
.textAlign(TextAlign.Center)
.borderRadius(23)
.linearGradient({
angle: 90,
colors: [['#7B1F2B', 0], ['#A6384B', 1]]
})
.margin({ top: 24 })
.onClick(() => {
const nc: Coll189 = {
id: this.selColl.id,
name: this.edName === '' ? this.selColl.name : this.edName,
artist: this.selColl.artist,
value: this.edValue,
color: this.selColl.color,
insured: this.edInsure
};
this.collections = this.collections.map((x: Coll189) => {
return x.id === nc.id ? nc : x;
});
this.showEditSheet = false;
})
}
.width('100%')
.padding({ left: 20, right: 20, bottom: 24 })
}
@Builder
withdrawDialog189() {
Column() {
Text('⚖️')
.fontSize(34)
.width(64)
.height(64)
.textAlign(TextAlign.Center)
.borderRadius(32)
.backgroundColor('#F7EBDD')
.margin({ top: 24 })
Text('确认撤拍该藏品?')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ top: 12 })
Text(this.selColl.name)
.fontSize(12)
.fontColor('#7B1F2B')
.margin({ top: 4 })
Column() {
Text('拍品已进入预展图录,撤拍将收取保留价 5% 的违约金(约 ¥' + Math.round(this.selColl.value * 0.05 * 10) / 10 + ' 万),并从本专场图录中移除。')
.fontSize(11)
.fontColor('#C62828')
.lineHeight(18)
.textAlign(TextAlign.Center)
}
.width('100%')
.alignItems(HorizontalAlign.Center)
.padding(12)
.borderRadius(12)
.backgroundColor('#FDECEA')
.margin({ top: 14 })
Row() {
Text('同步删除出价记录')
.fontSize(12)
.fontColor('#333333')
Text('')
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.wdSync })
.onChange((v: boolean) => {
this.wdSync = v;
})
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 14 })
Row({ space: 12 }) {
Text('取消')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#666666')
.layoutWeight(1)
.height(42)
.textAlign(TextAlign.Center)
.borderRadius(21)
.backgroundColor('#F5F5F5')
.onClick(() => {
this.showWithdrawDialog = false;
})
Text('确认撤拍')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.layoutWeight(1)
.height(42)
.textAlign(TextAlign.Center)
.borderRadius(21)
.backgroundColor('#C62828')
.onClick(() => {
this.collections = this.collections.filter((x: Coll189) => {
return x.id !== this.selColl.id;
});
this.showWithdrawDialog = false;
})
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 20, bottom: 24 })
}
.width('86%')
.borderRadius(20)
.backgroundColor('#FFFFFF')
}
@Builder
detailDialog189() {
Column() {
Column() {
Text('图录号 ' + this.selLot.no)
.fontSize(12)
.fontColor('#FFE082')
Text(this.selLot.title)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ top: 6 })
Text(this.selLot.artist + ' · ' + this.selLot.specialty)
.fontSize(11)
.fontColor('#E7CDB1')
.margin({ top: 4 })
}
.width('100%')
.alignItems(HorizontalAlign.Center)
.padding({ top: 24, bottom: 18 })
.borderRadius({ topLeft: 20, topRight: 20 })
.linearGradient({
angle: 135,
colors: [['#5A1019', 0], ['#7B1F2B', 1]]
})
Row() {
Column() {
Text('¥' + this.selLot.est + '-' + this.selLot.estHigh + '万')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#C62828')
Text('估价区间')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text(this.selLot.hot + '')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
Text('竞拍热度')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('32')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
Text('出价次数')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
}
.width('100%')
.padding({ top: 14, bottom: 14 })
.backgroundColor('#FBF6EF')
Column() {
Text('同类拍品近四季成交走势(万元)')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
Row({ space: 14 }) {
ForEach(TRENDS189, (t: Trend189) => {
Column({ space: 4 }) {
Column() {
Text('')
.width('100%')
.height(t.price / 21 * 100 + '%')
.borderRadius({ topLeft: 3, topRight: 3 })
.backgroundColor(t.season === '26秋' ? '#C9A227' : '#7B1F2B')
}
.width(26)
.height(72)
.justifyContent(FlexAlign.End)
Text(t.season)
.fontSize(9)
.fontColor('#666666')
}
}, (t: Trend189) => t.season)
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 12 })
}
.width('100%')
.padding(16)
Column() {
Text('图录信息')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
Row() {
Text('尺寸')
.fontSize(11)
.fontColor('#999999')
.width(60)
Text('68 × 45 cm')
.fontSize(11)
.fontColor('#444444')
}
.width('100%')
.margin({ top: 8 })
Row() {
Text('创作年代')
.fontSize(11)
.fontColor('#999999')
.width(60)
Text('2024 年')
.fontSize(11)
.fontColor('#444444')
}
.width('100%')
.margin({ top: 6 })
Row() {
Text('签名')
.fontSize(11)
.fontColor('#999999')
.width(60)
Text('右下角含款识')
.fontSize(11)
.fontColor('#444444')
}
.width('100%')
.margin({ top: 6 })
Row() {
Text('保证金')
.fontSize(11)
.fontColor('#999999')
.width(60)
Text('¥2 万(未成交退还)')
.fontSize(11)
.fontColor('#C62828')
}
.width('100%')
.margin({ top: 6 })
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 16 })
Text('立即出价')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.width('86%')
.height(42)
.textAlign(TextAlign.Center)
.borderRadius(21)
.linearGradient({
angle: 90,
colors: [['#7B1F2B', 0], ['#A6384B', 1]]
})
.margin({ bottom: 20 })
.onClick(() => {
this.showDetailDialog = false;
this.showBidSheet = true;
})
}
.width('86%')
.borderRadius(20)
.backgroundColor('#FFFFFF')
.constraintSize({ maxHeight: '85%' })
}
}
@Component
struct LiveTab189 {
onBid: (l: Lot189) => void = () => {
};
build() {
Column() {
Column() {
Row() {
Text('🔴 正在拍卖')
.fontSize(12)
.fontColor('#FFE082')
Text('')
.layoutWeight(1)
Text('第28期 · 会议室在线 346 人')
.fontSize(10)
.fontColor('#E7CDB1')
}
.width('100%')
Row() {
Text('🖼️')
.fontSize(36)
.width(110)
.height(110)
.textAlign(TextAlign.Center)
.borderRadius(14)
.backgroundColor('#3D0A11')
Column() {
Text('《山涧听雨》')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('林慕白 · 现当代书画 · 图录号 086')
.fontSize(10)
.fontColor('#E7CDB1')
.margin({ top: 4 })
Text('当前价')
.fontSize(10)
.fontColor('#E7CDB1')
.margin({ top: 10 })
Text('¥21万')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FFE082')
Row({ space: 6 }) {
Text('🔥 热度 96')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('#C62828')
Text('⚡ 32 次出价')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('#C9A227')
}
.margin({ top: 8 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
}
.width('100%')
.margin({ top: 12 })
Row({ space: 12 }) {
Text('出价')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
.layoutWeight(1)
.height(42)
.textAlign(TextAlign.Center)
.borderRadius(21)
.linearGradient({
angle: 135,
colors: [['#E7CDB1', 0], ['#C9A227', 1]]
})
.onClick(() => {
this.onBid(LOTS189[0]);
})
Text('♎ 委托代拍')
.fontSize(12)
.fontColor('#FFFFFF')
.padding({ left: 16, right: 16, top: 11, bottom: 11 })
.borderRadius(21)
.backgroundColor('#33FFFFFF')
}
.width('100%')
.margin({ top: 14 })
}
.width('100%')
.padding(14)
.borderRadius(18)
.linearGradient({
angle: 130,
colors: [['#5A1019', 0], ['#8E2B38', 1]]
})
.shadow({ radius: 12, color: '#337B1F2B', offsetY: 6 })
Text('实时出价阶梯')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.margin({ top: 14 })
ForEach(BIDRECS189, (b: BidRec189) => {
Row() {
Text(b.agency ? '♎' : '🪧')
.fontSize(10)
.width(34)
.fontColor(b.agency ? '#C9A227' : '#999999')
Column() {
Text(b.bidder + (b.agency ? '(代理)' : ''))
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(b.id === 'b1' ? '#C62828' : '#333333')
Text(b.time + ' 出价')
.fontSize(9)
.fontColor('#BBBBBB')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 6 })
Text('¥' + b.price + ' 万')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(b.id === 'b1' ? '#C62828' : '#7B1F2B')
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor(b.id === 'b1' ? '#FFF8F0' : '#FFFFFF')
.margin({ top: 8 })
.shadow({ radius: 4, color: '#147B1F2B', offsetY: 2 })
}, (b: BidRec189) => b.id)
Text('即将上拍')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.margin({ top: 14 })
Scroll() {
Row({ space: 10 }) {
ForEach(LOTS189, (l: Lot189) => {
Column() {
Text('🖼️')
.fontSize(24)
.width(96)
.height(72)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(l.color)
Text('No.' + l.no)
.fontSize(9)
.fontColor('#C9A227')
.margin({ top: 6 })
Text(l.title)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width(96)
Text('¥' + l.est + '-' + l.estHigh + '万')
.fontSize(9)
.fontColor('#C62828')
.margin({ top: 3 })
}
.padding(8)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.shadow({ radius: 4, color: '#147B1F2B', offsetY: 2 })
}, (l: Lot189) => l.id)
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ top: 10 })
}
.width('100%')
}
}
@Component
struct LotTab189 {
onDetail: (l: Lot189) => void = () => {
};
build() {
Column() {
Row() {
Text('全部拍品 · ' + LOTS189.length + ' 件')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text('')
.layoutWeight(1)
Text('图录号排序 ↓')
.fontSize(10)
.fontColor('#C4A98A')
}
.width('100%')
Grid() {
ForEach(LOTS189, (l: Lot189) => {
GridItem() {
Column() {
Text('🖼️')
.fontSize(30)
.width('100%')
.height(96)
.textAlign(TextAlign.Center)
.borderRadius({ topLeft: 12, topRight: 12 })
.backgroundColor(l.color)
Column() {
Row() {
Text('No.' + l.no)
.fontSize(9)
.fontColor('#C9A227')
Text('')
.layoutWeight(1)
Text('🔥' + l.hot)
.fontSize(9)
.fontColor('#C62828')
}
.width('100%')
Text(l.title)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 4 })
Text(l.artist + ' · ' + l.specialty)
.fontSize(9)
.fontColor('#999999')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 3 })
Text('估价 ¥' + l.est + '-' + l.estHigh + '万')
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
.margin({ top: 6 })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding(10)
}
.width('100%')
.borderRadius(12)
.backgroundColor('#FFFFFF')
.shadow({ radius: 6, color: '#147B1F2B', offsetY: 2 })
.onClick(() => {
this.onDetail(l);
})
}
}, (l: Lot189) => l.id)
}
.columnsTemplate('1fr 1fr')
.rowsGap(12)
.columnsGap(12)
.width('100%')
.margin({ top: 12 })
}
.width('100%')
}
}
@Component
struct SpecialTab189 {
@State filterIdx: number = 0;
build() {
Column() {
Scroll() {
Row({ space: 8 }) {
ForEach(STATCHIPS189, (c: string, i: number) => {
Text(c)
.fontSize(11)
.fontColor(this.filterIdx === i ? '#FFFFFF' : '#7B1F2B')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(this.filterIdx === i ? '#7B1F2B' : '#FFFFFF')
.onClick(() => {
this.filterIdx = i;
})
}, (c: string) => c)
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
ForEach(SPECIALS189, (s: Special189) => {
Column() {
Row() {
Column() {
Text(s.no)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
Text(s.date + ' 开槌')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
Text('')
.layoutWeight(1)
Text(s.state)
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.backgroundColor(stateTag189(s.state))
}
.width('100%')
Text(s.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.margin({ top: 8 })
Row() {
Text(s.lots + ' 件拍品')
.fontSize(10)
.fontColor('#999999')
Text('')
.layoutWeight(1)
Text(s.state === '已结拍' ? '成交率 ' + s.soldPct + '%' : '预展/拍卖中')
.fontSize(10)
.fontColor(s.state === '已结拍' ? '#C62828' : '#FF8F00')
}
.width('100%')
.margin({ top: 6 })
Column() {
Row() {
Text('')
.width(s.state === '已结拍' ? s.soldPct + '%' : '0%')
.height(6)
.borderRadius(3)
.backgroundColor(s.soldPct >= 90 ? '#C9A227' : '#7B1F2B')
}
.width('100%')
.height(6)
.borderRadius(3)
.backgroundColor('#F7EBDD')
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(14)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.margin({ top: 10 })
.shadow({ radius: 6, color: '#147B1F2B', offsetY: 2 })
}, (s: Special189) => s.id)
}
.width('100%')
}
}
@Component
struct MarketTab189 {
build() {
Column() {
Column() {
Text('近六期专场成交率')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
Row({ space: 12 }) {
ForEach(SOLDRATES189, (s: SoldRate189) => {
Column({ space: 4 }) {
Column() {
Text('')
.width('100%')
.height(s.pct + '%')
.borderRadius({ topLeft: 3, topRight: 3 })
.backgroundColor(s.pct >= 90 ? '#C9A227' : '#7B1F2B')
}
.width(28)
.height(88)
.justifyContent(FlexAlign.End)
Text(s.no)
.fontSize(9)
.fontColor('#666666')
}
}, (s: SoldRate189) => s.no)
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 12 })
}
.width('100%')
.padding(14)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.shadow({ radius: 8, color: '#147B1F2B', offsetY: 3 })
Column() {
Text('本月成交额构成(万元)')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
Row() {
ForEach(CATES189, (c: Cate189) => {
Text('')
.layoutWeight(c.w)
.height(14)
.backgroundColor(c.c)
}, (c: Cate189) => c.label)
}
.width('100%')
.clip(true)
.borderRadius(7)
.margin({ top: 10 })
Row({ space: 12 }) {
ForEach(CATES189, (c: Cate189) => {
Row() {
Text('')
.width(8)
.height(8)
.borderRadius(4)
.backgroundColor(c.c)
Text(' ' + c.label + ' ' + c.w + '%')
.fontSize(10)
.fontColor('#666666')
}
}, (c: Cate189) => c.label + 'm')
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(14)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.shadow({ radius: 8, color: '#147B1F2B', offsetY: 3 })
.margin({ top: 12 })
Column() {
Text('热门品类成交额(万元)')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
ForEach(HOTCATES189, (h: HotCate189) => {
Row() {
Text(h.name)
.fontSize(11)
.fontColor('#666666')
.width(88)
Column() {
Row() {
Text('')
.width(h.amount / 4280 * 100 + '%')
.height(8)
.borderRadius(4)
.backgroundColor(h.color)
}
.width('100%')
.height(8)
.borderRadius(4)
.backgroundColor('#F7EBDD')
}
.layoutWeight(1)
Text(h.amount + '')
.fontSize(10)
.fontColor('#999999')
.margin({ left: 8 })
.width(40)
}
.width('100%')
.margin({ top: 10 })
}, (h: HotCate189) => h.name)
}
.width('100%')
.padding(14)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.shadow({ radius: 8, color: '#147B1F2B', offsetY: 3 })
.margin({ top: 12 })
Column() {
Row() {
Text('💰 我的保证金')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('')
.layoutWeight(1)
Text('¥20,000')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFE082')
}
.width('100%')
Text('出价资格保证金 · 未中标部分将在专场结束后 3 个工作日内原路退回')
.fontSize(10)
.fontColor('#E7CDB1')
.lineHeight(16)
.margin({ top: 8 })
}
.width('100%')
.padding(14)
.borderRadius(16)
.linearGradient({
angle: 130,
colors: [['#5A1019', 0], ['#7B1F2B', 1]]
})
.shadow({ radius: 8, color: '#227B1F2B', offsetY: 4 })
.margin({ top: 12 })
}
.width('100%')
}
}
@Component
struct CollTab189 {
@Prop collections: Coll189[];
onEdit: (c: Coll189) => void = () => {
};
onWithdraw: (c: Coll189) => void = () => {
};
build() {
Column() {
Row() {
Text('我的藏品 · ' + this.collections.length + ' 件')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text('')
.layoutWeight(1)
Text('总估值 ¥' + collTotal189(this.collections) + ' 万')
.fontSize(10)
.fontColor('#C62828')
}
.width('100%')
ForEach(this.collections, (c: Coll189) => {
Row() {
Text('📦')
.fontSize(22)
.width(52)
.height(52)
.textAlign(TextAlign.Center)
.borderRadius(12)
.backgroundColor(c.color)
Column() {
Row({ space: 6 }) {
Text(c.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(c.insured ? '已投保' : '未投保')
.fontSize(8)
.fontColor(c.insured ? '#2E7D32' : '#9E9E9E')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(6)
.backgroundColor(c.insured ? '#E8F5E9' : '#F5F5F5')
}
.width('86%')
Text(c.artist + ' · 估值 ¥' + c.value + ' 万')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 4 })
Row({ space: 8 }) {
Text('编辑')
.fontSize(10)
.fontColor('#7B1F2B')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(11)
.border({ width: 1, color: '#7B1F2B' })
.onClick(() => {
this.onEdit(c);
})
Text('撤拍')
.fontSize(10)
.fontColor('#C62828')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(11)
.border({ width: 1, color: '#C62828' })
.onClick(() => {
this.onWithdraw(c);
})
}
.margin({ top: 8 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ top: 10 })
.shadow({ radius: 6, color: '#147B1F2B', offsetY: 2 })
}, (c: Coll189) => c.id)
}
.width('100%')
}
}
function collTotal189(list: Coll189[]): string {
let t: number = 0;
for (const c of list) {
t = t + c.value;
}
return (Math.round(t * 10) / 10) + '';
}
@Component
struct MineTab189 {
onConsign: () => void = () => {
};
build() {
Column() {
Column() {
Row() {
Text('🎩')
.fontSize(26)
.width(60)
.height(60)
.textAlign(TextAlign.Center)
.borderRadius(30)
.backgroundColor('#33FFFFFF')
Column() {
Text('藏家 6**3')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('竞拍号牌 A-063 · 已参与 12 期专场')
.fontSize(10)
.fontColor('#E7CDB1')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
Text('')
.layoutWeight(1)
Column() {
Text('3')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFE082')
Text('中标')
.fontSize(9)
.fontColor('#E7CDB1')
.margin({ top: 2 })
}
}
.width('100%')
}
.width('100%')
.padding(16)
.borderRadius(18)
.linearGradient({
angle: 135,
colors: [['#5A1019', 0], ['#7B1F2B', 1]]
})
.shadow({ radius: 10, color: '#227B1F2B', offsetY: 4 })
Row() {
Column() {
Text('12')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
Text('参与专场')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('3')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
Text('中标拍品')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('38')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
Text('累计出价')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('2件')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#C9A227')
Text('送拍中')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
}
.width('100%')
.padding(14)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.margin({ top: 12 })
.shadow({ radius: 6, color: '#147B1F2B', offsetY: 2 })
Column() {
Text('🔨 我的出价记录')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
ForEach([['《午夜 No.7》', '¥17万', '已出局'], ['田黄瑞兽钮章', '¥62万', '竞拍中'], ['粉彩花鸟瓶', '¥24万', '已中标'], ['《极简·白》', '¥6万', '未达保留价']], (r: string[]) => {
Row() {
Text(r[0])
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.layoutWeight(1)
Text(r[1])
.fontSize(11)
.fontColor('#7B1F2B')
.margin({ right: 8 })
Text(r[2])
.fontSize(9)
.fontColor(r[2] === '已中标' ? '#2E7D32' : (r[2] === '竞拍中' ? '#FF8F00' : '#9E9E9E'))
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor(r[2] === '已中标' ? '#E8F5E9' : (r[2] === '竞拍中' ? '#FFF8E1' : '#F5F5F5'))
}
.width('100%')
.padding({ top: 10, bottom: 10 })
}, (r: string[]) => r[0])
}
.width('100%')
.padding(14)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.margin({ top: 12 })
.shadow({ radius: 6, color: '#147B1F2B', offsetY: 2 })
Text('+ 送拍我的藏品')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#7B1F2B')
.width('100%')
.height(46)
.textAlign(TextAlign.Center)
.borderRadius(23)
.border({ width: 1.5, color: '#C9A227' })
.backgroundColor('#FFF8EF')
.margin({ top: 14 })
.onClick(() => {
this.onConsign();
})
}
.width('100%')
}
}
总结

48小时黑客松挑战完成。这个线上拍卖会应用从空白文件到可演示产品,经历了九个interface定义、八个静态数据数组、二十一个状态变量、六个子Tab组件、五个弹窗交互(三个抽屉+两个居中弹窗)、三种纯布局图表(柱状图/堆叠条/横向进度条)的完整实现。代码总量约2000行,覆盖了HarmonyOS 6.1.1 ArkTS API 24的核心能力面——声明式UI、条件渲染、状态管理、弹窗系统、Grid/Scroll布局、ForEach列表渲染、Toggle开关、linearGradient渐变。
回顾整个挑战,最正确的决策是"数据结构先行"——前30分钟定义完所有interface和静态数据后,后续UI编码几乎没有回头改过数据结构,这为48小时的时间预算争取了大量空间。最创意的突破是"价签造型Tab"——用一个6x6圆点+不对称borderRadius,就把普通横滑Tab变成了拍卖会专属的视觉符号。最极限的实现是"纯布局三件套图表"——零依赖、零学习成本、10分钟内完成三种数据可视化,这是在没有图表库可用时的最佳应急方案。
当然,技术债也是客观存在的。颜色值全硬编码、状态变量过度集中、函数级硬编码判断最高出价者(b1)、wdSync开关无实际逻辑——这些都是48小时挑战中不得不背的债。在黑客松语境下这些债完全可接受(演示数据是静态的,不会触发边界问题),但如果要演化为生产级应用,颜色常量化、状态分层、出价逻辑后端化、违约金精度处理这四项是必须优先偿还的技术债。总的来说,这个挑战证明了HarmonyOS ArkTS在"极限开发"场景下的生产力——声明式UI的自动重绘、$$双向绑定的状态同步、ForEach+Grid的列表渲染、纯布局组件的数据可视化,这些能力组合起来足以在48小时内交付一个功能完整、视觉精良、可交互演示的产品级原型。
更多推荐




所有评论(0)