## 一、引言:从晨光中开启高效一天 在移动互联网与智能穿戴设备高度普及的今天,“习惯养成“已经从一个模糊的自我管理理念,演变为一个可量化、可追踪、可视化的数字化产品赛道。越来越多的人希望借助手
HarmonyOS 6.1.1云鉴宝直播应用代码拆解——主播带你从青铜棕与鎏金的配色搭配聊到Grid四宫格连麦窗口,深入ArkTS声明式布局如何用bindSheet抽屉管理藏品鉴定全流程
欢迎收听本期技术播客,今天主播要带大家拆解一个非常有意思的 HarmonyOS 应用——一个仿照腾讯会议式直播连麦架构的线上鉴宝平台。配色用的是青铜棕
#5D4037搭配鎏金#D4AF37,整个界面有种故宫文物展厅的质感,特别带感。这个应用最让我兴奋的地方是它的底部导航——每个 Tab 格子像博物馆的展柜一样,选中时暖光打底加顶部金线,图标圆底如展品。而且弹层交互分了抽屉和全屏遮罩两种,日常表单走抽屉,关键确认走弹窗,这个交互层级设计得特别讲究。
如果你之前做过直播类应用,你一定会对"连麦"这个交互场景很熟悉。今天我们就来聊聊,ArkTS 的声明式 UI 怎么把鉴定、弹幕、档期预约这些复杂的直播业务流程,用优雅的代码结构组织起来。
引言:今天聊什么

哈喽大家好,欢迎回来。今天主播想跟大家聊一个我最近看到特别有意思的 HarmonyOS 应用源码——它叫"鎏光·云鉴宝",简单来说就是一个线上鉴宝直播平台。场景是这样的:藏友通过视频会议室连麦专家,专家在直播间里直播鉴定瓷器、玉器、书画这些藏品。你可以理解成"直播版的鉴宝节目",藏友不用扛着花瓶去电视台排队了,直接线上连麦就行。
从技术角度来说,这个应用基于 HarmonyOS ArkTS 声明式 UI 范式构建。整体架构是一个 @Entry 主入口组件 Index192,下面挂了六个子 Tab 组件——鉴宝台、藏品阁、送鉴单、行家团、档期、我的。主组件管所有可变状态,子组件通过构造参数和回调函数跟主组件通信。配色方案很讲究,青铜棕做主色调,鎏金做高亮色,米宣白 #F7F3EA 做背景,整个视觉语言就是博物馆的展柜美学。
从业务设计来说呢,这个应用把鉴宝直播的全流程拆得特别细。直播台有四宫格连麦窗口——主鉴专家视角、藏宝人视角、微距镜头、款识特写,四个画面同屏。藏品阁可以管理自己的藏品档案,送鉴单可以提交新的鉴定申请,行家团展示各位专家的信息和口碑评分,档期页可以预约未来的直播专场。每个模块都有独立的弹层表单——申请连麦、新增送鉴、编辑藏品、撤销送鉴、藏品详情,五个弹层各有分工。来,我们一段段拆开看。
第一段:数据模型——鉴宝的"藏品档案柜"

1.1 接口定义
好,咱们先看数据结构。做任何一个应用,数据模型是地基,先把地基打好才能往上盖楼。这个应用定义了一组接口来描述鉴宝业务的所有实体类型。
interface TabItem192 {
name: string;
icon: string;
badge: number;
}
interface Lot192 {
id: string;
name: string;
cate: string;
era: string;
grade: string;
value: number;
owner: string;
verdict: string;
color: string;
hot: number;
state: string
}
interface VerdictRow192 {
id: string;
lot: string;
expert: string;
result: string;
date: string;
conf: number;
color: string;
}
interface Expert192 {
id: string;
name: string;
field: string;
title: string;
rating: number;
sessions: number;
emoji: string;
online: boolean;
}
interface Session192 {
id: string;
date: string;
week: string;
time: string;
theme: string;
expert: string;
joined: number;
seats: number;
state: string;
}
interface Danmaku192 {
id: string;
user: string;
text: string;
color: string;
}
interface Attend192 {
day: string;
cnt: number;
}
interface Share192 {
label: string;
w: number;
c: string;
}
interface HotCate192 {
name: string;
pct: number;
color: string;
}
interface Record192 {
id: string;
date: string;
lot: string;
result: string;
color: string;
}
大家注意看 TabItem192 这个接口,它比一般 Tab 多了一个 badge 字段——这是未读消息的角标数字。这个设计说明底部导航不是单纯的路由切换,它还承担了消息通知的功能。再看 Lot192,字段特别丰富:cate 是品类(瓷器、玉器、书画等),era 是年代,grade 是品级,verdict 是鉴定结论,hot 是热度值,state 是鉴定状态。一个藏品有这么多维度,说明这个应用的业务复杂度不低。
Expert192 里有 rating(评分)和 sessions(主持场次)两个字段,这两个数据在后面的行家团 Tab 里会被用来做口碑可视化——评分高的行家,进度条就长。Session192 有 joined 和 seats 两个数值,它们的比值就是上座率,在上座率超过阈值时,预约按钮会变成"已满"灰色态。
主播小贴士:在 ArkTS 里,接口定义纯粹是类型约束,不占运行时内存。但好的接口设计能让你的数据流更清晰——每个字段的类型都在告诉读代码的人"这个数据是干嘛的"。比如
conf: number一看就知道是置信度,pct: number就是百分比。
1.2 辅助函数和静态数据

接下来看几组辅助函数和静态数据,这些是整个应用的"素材库"。
const CATES192: string[] = ['瓷器', '玉器', '书画', '铜器', '印石', '文房'];
const URGENTS192: string[] = ['普通排期', '加急48h', '特急24h'];
const PRIVACY192: string[] = ['公开展示', '仅自己可见'];
function gradeColor192(g: string): string {
if (g === '珍品') {
return '#D4AF37';
}
if (g === '上品') {
return '#5D4037';
}
return '#9E9E9E';
}
function stateColor192(s: string): string {
if (s === '可预约') {
return '#00695C';
}
if (s === '即将满员') {
return '#EF6C00';
}
return '#9E9E9E';
}
function verdictColor192(v: string): string {
if (v === '真品') {
return '#00695C';
}
if (v === '后仿') {
return '#C62828';
}
return '#EF6C00';
}
这三个颜色函数特别有意思,它们把业务语义翻译成了视觉语言。gradeColor192 里"珍品"返回鎏金色 #D4AF37——因为珍品就是镇馆之宝,必须用最贵的颜色。"上品"用青铜棕,普通品用灰色。stateColor192 里"可预约"是绿色、"即将满员"是橙色、"已满员"是灰色。verdictColor192 里"真品"绿色、"后仿"红色、"待考"橙色。你看,这三个函数覆盖了鉴宝的三个核心判断维度:品级、档期状态、鉴定结论。每个维度都有自己的颜色映射逻辑。
主播吐槽一下:很多新手开发者喜欢把颜色直接写死在 UI 代码里,比如
fontColor('#D4AF37')。但你品品,如果将来要改"珍品"的颜色,你得全局搜索替换。用函数封装后,只需要改一个地方。这就是"DRY 原则"——Don’t Repeat Yourself。
1.3 藏品和专家数据

const LOTS192: Lot192[] = [
{ id: 'l1', name: '青花缠枝纹梅瓶', cate: '瓷器', era: '明永乐', grade: '珍品', value: 128, owner: '金陵藏家·陆先生', verdict: '真品', color: '#5D4037', hot: 97, state: '待定' },
{ id: 'l2', name: '和田白玉把件', cate: '玉器', era: '清中期', grade: '上品', value: 66, owner: '姑苏·沈女士', verdict: '真品', color: '#00695C', hot: 89, state: '待定' },
{ id: 'l3', name: '田黄石瑞兽钮章', cate: '印石', era: '民国', grade: '上品', value: 92, owner: '榕城·林先生', verdict: '真品', color: '#AD1457', hot: 93, state: '待定' },
{ id: 'l4', name: '仿哥窑贯耳瓶', cate: '瓷器', era: '清晚期', grade: '普品', value: 8, owner: '蜀中·赵女士', verdict: '后仿', color: '#757575', hot: 64, state: '待定' },
{ id: 'l5', name: '吴门山水扇面', cate: '书画', era: '明', grade: '珍品', value: 210, owner: '沪上·程先生', verdict: '待考', color: '#283593', hot: 99, state: '待定' },
{ id: 'l6', name: '鎏金铜坐佛', cate: '铜器', era: '明早期', grade: '上品', value: 155, owner: '长安·马先生', verdict: '真品', color: '#EF6C00', hot: 91, state: '待定' },
{ id: 'l7', name: '端砚云月池', cate: '文房', era: '清', grade: '上品', value: 48, owner: '肇庆·梁先生', verdict: '真品', color: '#00838F', hot: 72, state: '待定' },
{ id: 'l8', name: '粉彩百蝶瓶', cate: '瓷器', era: '清光绪', grade: '珍品', value: 178, owner: '津门·周女士', verdict: '真品', color: '#827717', hot: 95, state: '待定' }
];
const EXPERTS192: Expert192[] = [
{ id: 'e1', name: '程汝言', field: '瓷器', title: '副研究馆员', rating: 4.9, sessions: 486, emoji: '🏺', online: true },
{ id: 'e2', name: '白玛', field: '玉器', title: '高级鉴定师', rating: 4.8, sessions: 352, emoji: '🪨', online: true },
{ id: 'e3', name: '林半尺', field: '印石', title: '民间藏家代表', rating: 4.7, sessions: 298, emoji: '🖊️', online: false },
{ id: 'e4', name: '顾墨白', field: '书画', title: '美术学院特聘', rating: 5.0, sessions: 275, emoji: '🖼️', online: true },
{ id: 'e5', name: '马远声', field: '铜器', title: '考古队顾问', rating: 4.8, sessions: 231, emoji: '🔔', online: false },
{ id: 'e6', name: '梁砚秋', field: '文房', title: '端砚研究会理事', rating: 4.6, sessions: 189, emoji: '📖', online: true }
];
八件藏品覆盖了瓷器、玉器、印石、书画、铜器、文房六大品类。注意那个"仿哥窑贯耳瓶"——它的 verdict 是"后仿",品级是"普品",估值只有 8 万,跟旁边的 128 万梅瓶形成了鲜明对比。这种"真品和后仿混在一起"的数据设计,让鉴定结论的颜色差异在 UI 上有直观的视觉冲击——绿色"真品"和红色"后仿"并列时,用户一眼就能区分。六位专家各有专攻,rating 从 4.6 到 5.0,顾墨白老师 5.0 满分,书画专场估计很难约。
第二段:主入口组件——直播间的"总控台"

2.1 状态声明
好,接下来看主入口组件。这是整个应用的总控台,所有状态都在这里统一管理。
@Entry
@Component
struct Index192 {
@State curTab: number = 0;
@State showLianmaiSheet: boolean = false;
@State showSendSheet: boolean = false;
@State showEditSheet: boolean = false;
@State showDelDialog: boolean = false;
@State showDetailDialog: boolean = false;
@State editIdx: number = 0;
@State editName: string = '';
@State delIdx: number = 0;
@State detailIdx: number = 0;
@State cateIdx: number = 0;
@State urgentIdx: number = 0;
@State privacyIdx: number = 0;
@State hopeExpert: number = 0;
@State estValue: number = 10;
@State insureOn: boolean = true;
@State pickupOn: boolean = false;
@State keepRecordOn: boolean = true;
@State myLots: Lot192[] = [
{ id: 'm1', name: '青花缠枝纹梅瓶', cate: '瓷器', era: '明永乐', grade: '珍品', value: 128, owner: '我', verdict: '真品', color: '#5D4037', hot: 97, state: '待定' },
{ id: 'm2', name: '端砚云月池', cate: '文房', era: '清', grade: '上品', value: 48, owner: '我', verdict: '真品', color: '#00838F', hot: 72, state: '待定' },
{ id: 'm3', name: '铜鎏金小坐像', cate: '铜器', era: '明早期', grade: '上品', value: 155, owner: '我', verdict: '真品', color: '#EF6C00', hot: 90, state: '待定' }
];
@State myOrders: Session192[] = [
{ id: 'o1', date: '08-26', week: '周三', time: '19:30', theme: '瓷器专场·永宣青花', expert: '程汝言', joined: 412, seats: 500, state: '已预约' },
{ id: 'o2', date: '08-28', week: '周五', time: '19:00', theme: '书画专场·吴门画派', expert: '顾墨白', joined: 488, seats: 500, state: '已预约' }
];
大家看,这里的状态分为几组:curTab 管当前 Tab;五个 show* 布尔管弹层显隐;editIdx、delIdx、detailIdx 是被操作的藏品或订单的索引;cateIdx、urgentIdx、privacyIdx、hopeExpert、estValue 是连麦申请表单的选择状态;insureOn、pickupOn、keepRecordOn 是三个 Toggle 开关。最关键的是 myLots 和 myOrders 两个数组状态——它们是用户的个人藏品和预约订单,是整个应用中真正"可变"的数据。其他静态数据如 LOTS192、EXPERTS192 都是全局只读常量。
主播知识点:在 ArkTS 里,
@State标记的变量如果赋了新值,UI 会自动刷新。但如果myLots是数组,你必须给它赋一个全新的数组(比如用map或filter生成新数组),直接push或splice修改原数组是触发不了响应式更新的。这就是为什么后面的删除和编辑操作都用filter和map。
2.2 顶部直播状态栏

build() {
Column() {
Column() {
Text('鎏光·云鉴宝')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Row() {
Row() {
Text('●')
.fontSize(9)
.fontColor('#FFD54F')
Text('直播中 · 第128场')
.fontSize(11)
.fontColor('#FFD54F')
.margin({ left: 4 })
}
.padding({ left: 10, right: 10, top: 3, bottom: 3 })
.borderRadius(10)
.backgroundColor('#4D000000')
Text('')
.layoutWeight(1)
Text('6.8万人看过')
.fontSize(11)
.fontColor('#E8DCC8')
}
.width('100%')
.margin({ top: 10 })
Row() {
ForEach(LOTS192, (l: Lot192, idx: number) => {
Column() {
Text(l.era)
.fontSize(9)
.fontColor(idx === 0 ? '#5D4037' : '#E8DCC8')
Text(idx === 0 ? '正在鉴定' : l.verdict)
.fontSize(10)
.fontWeight(idx === 0 ? FontWeight.Bold : FontWeight.Normal)
.fontColor(idx === 0 ? '#FFD54F' : '#E8DCC8')
}
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(8)
.backgroundColor(idx === 0 ? '#D4AF37' : '#33000000')
}, (l: Lot192) => l.id)
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 14 })
.linearGradient({
angle: 160,
colors: [['#4E342E', 0], ['#5D4037', 0.5], ['#3E2723', 1]]
})
这个顶部栏特别有直播质感。首先"直播中"用了一个闪烁感的鎏金小圆点 ● 配 #FFD54F 文字,放在半透明黑底 #4D000000(约 30% 不透明度)的胶囊里——这就是直播 APP 标配的"直播指示器"。下面那排藏品列表用 ForEach 遍历所有 LOTS192,第一个藏品用鎏金底色标记"正在鉴定",其余用半透明黑底显示鉴定结论。整个顶部栏用 160 度三段渐变 #4E342E → #5D4037 → #3E2723,从深棕到标准棕再到更深棕,制造出一种青铜器表面的光泽质感。
主播点评:这个三段渐变比两段渐变更有"材质感"。两段渐变像塑料,三段渐变像金属。做古风应用的同学可以学一下这个套路。
2.3 内容路由和底部导航

Scroll() {
Column() {
if (this.curTab === 0) {
LiveTab192({
onLianmai: () => {
this.showLianmaiSheet = true;
}
})
}
if (this.curTab === 1) {
CollectionTab192({
lots: this.myLots,
onEdit: (idx: number) => {
this.editIdx = idx;
this.editName = this.myLots[idx].name;
this.showEditSheet = true;
},
onDetail: (idx: number) => {
this.detailIdx = idx;
this.showDetailDialog = true;
}
})
}
if (this.curTab === 2) {
OrderTab192({
orders: this.myOrders,
onSend: () => {
this.showSendSheet = true;
},
onDel: (idx: number) => {
this.delIdx = idx;
this.showDelDialog = true;
}
})
}
if (this.curTab === 3) {
ExpertTab192()
}
if (this.curTab === 4) {
ScheduleTab192({
onLianmai: () => {
this.showLianmaiSheet = true;
}
})
}
if (this.curTab === 5) {
MineTab192({
records: RECORDS192,
onDetail: () => {
this.detailIdx = 0;
this.showDetailDialog = true;
}
})
}
}
.width('100%')
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.backgroundColor('#F7F3EA')
内容路由用了一串 if 而不是 if-else if——注意这个区别。在 ArkTS 中,连续 if 意味着每次只渲染匹配的那一个,但如果条件同时满足理论上可以渲染多个。不过这里因为 curTab 只有一个值,实际效果跟 if-else if 一样。子组件的传参模式值得注意:CollectionTab192 接收 lots: this.myLots 和两个回调 onEdit、onDetail;OrderTab192 接收 orders: this.myOrders 和 onSend、onDel。回调用箭头函数传递,在回调内部修改主组件的状态变量——这是 ArkTS 中子组件向父组件"上报事件"的标准模式。
Row() {
ForEach(TABS192, (t: TabItem192, idx: number) => {
Column() {
Text('')
.width(idx === this.curTab ? 26 : 0)
.height(2)
.backgroundColor('#D4AF37')
.borderRadius(1)
.margin({ top: 6 })
Row() {
Text(t.icon)
.fontSize(18)
.width(38)
.height(38)
.textAlign(TextAlign.Center)
.borderRadius(19)
.backgroundColor(idx === this.curTab ? '#FFFFFF' : '#F1E8D8')
}
Column() {
Text(t.name)
.fontSize(10)
.fontWeight(idx === this.curTab ? FontWeight.Bold : FontWeight.Normal)
.fontColor(idx === this.curTab ? '#5D4037' : '#8D7B6E')
if (t.badge > 0) {
Text(t.badge + '')
.fontSize(8)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(7)
.backgroundColor('#C62828')
.margin({ top: 2 })
}
}
.margin({ left: 6 })
}
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(idx === this.curTab ? '#F9EED2' : '#F7F3EA')
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.curTab = idx;
})
}, (t: TabItem192, idx: number) => t.name + idx)
}
.width('100%')
.padding({ left: 6, right: 6, bottom: 8 })
.backgroundColor('#FFFFFF')
底部导航是"展柜格"风格。选中时顶部有一根 26 像素宽的金线 #D4AF37,背景变为暖光底 #F9EED2,图标圆底变白。未选中时金线宽度为 0(不可见),背景与页面底色一致。badge > 0 时显示红色角标——比如"送鉴单"有 3 条未读。这个底部导航的设计语言是整个应用最统一的地方:暖色调、圆角、展柜感,跟青铜棕+鎏金的主题完全一致。
主播闲聊:你们有没有发现,这个底部导航跟前面 190 的均衡器条、193 的气泡底都不一样?每个应用都在底部导航上做"主题化"——190 是音乐所以用均衡器条,193 是脱口秀所以用气泡,这里是鉴宝所以用展柜金线。这就是 UI 设计跟业务场景的深度绑定。
第三段:连麦申请抽屉——鉴宝的"挂号窗口"
3.1 连麦申请表单
接下来看一个很有代表性的弹层——连麦鉴宝申请抽屉。这个表单模拟的是"藏友向直播间申请连麦"的流程。
@Builder
lianmaiSheet192() {
Column() {
Row() {
Text('')
.width(36)
.height(4)
.borderRadius(2)
.backgroundColor('#D9D2C5')
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 10 })
Text('申请连麦鉴宝')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
.margin({ top: 12 })
Column() {
Text('藏品名称')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
Text('请输入藏品全名(选填)')
.fontSize(13)
.fontColor('#B8AC9C')
Text('')
.layoutWeight(1)
}
.width('100%')
.padding(12)
.borderRadius(10)
.backgroundColor('#F7F3EA')
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('藏品品类')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
ForEach(CATES192, (c: string, i: number) => {
Text(c)
.fontSize(12)
.fontColor(i === this.cateIdx ? '#FFFFFF' : '#5D4037')
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.borderRadius(16)
.backgroundColor(i === this.cateIdx ? '#5D4037' : '#F1E8D8')
.margin({ right: 8 })
.onClick(() => {
this.cateIdx = i;
})
}, (c: string) => c)
}
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
这个抽屉的拖拽条用了 #D9D2C5——一个比页面底色 #F7F3EA 更深的暖灰色,低调但能看见。品类选择器用 ForEach 渲染六个胶囊按钮,选中态是青铜棕底白字,未选中是浅米色底棕字。注意这里所有表单标签的颜色都是 #8D7B6E——一种去饱和的棕色,比正文颜色 #3E2723 更柔和。这种"标签用浅色、内容用深色"的层次设计,让表单不会满屏都是重色字,视觉上更有呼吸感。
Column() {
Text('期望行家')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
ForEach(EXPERTS192, (e: Expert192, i: number) => {
Column() {
Text(e.emoji)
.fontSize(18)
.width(42)
.height(42)
.textAlign(TextAlign.Center)
.borderRadius(21)
.backgroundColor(i === this.hopeExpert ? '#D4AF37' : '#F1E8D8')
Text(e.name)
.fontSize(9)
.fontColor(i === this.hopeExpert ? '#5D4037' : '#8D7B6E')
.margin({ top: 4 })
}
.margin({ right: 12 })
.onClick(() => {
this.hopeExpert = i;
})
}, (e: Expert192) => e.id)
}
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
期望行家选择器是整个表单里最有趣的交互。六个专家用 emoji 头像+姓名的方式横排展示,选中时头像底色变为鎏金 #D4AF37,姓名变为青铜棕。这种"头像选择"比纯文字选择更有温度——用户在选的是一个"人",不是一个"选项"。
Row() {
Column() {
Text('加急通道')
.fontSize(13)
.fontColor('#3E2723')
Text('优先排入当前直播队列')
.fontSize(10)
.fontColor('#8D7B6E')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Text('')
.layoutWeight(1)
ForEach(URGENTS192, (u: string, i: number) => {
Text(u)
.fontSize(11)
.fontColor(i === this.urgentIdx ? '#FFFFFF' : '#5D4037')
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor(i === this.urgentIdx ? '#C62828' : '#F1E8D8')
.margin({ left: 6 })
.onClick(() => {
this.urgentIdx = i;
})
}, (u: string) => u)
}
.width('100%')
.margin({ top: 18 })
Row() {
Text('申请连麦')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.textAlign(TextAlign.Center)
.width('100%')
.padding({ top: 13, bottom: 13 })
.borderRadius(22)
.linearGradient({
angle: 90,
colors: [['#5D4037', 0], ['#8D6E63', 1]]
})
}
.width('100%')
.margin({ top: 20 })
.onClick(() => {
this.showLianmaiSheet = false;
})
}
.width('100%')
.padding({ left: 20, right: 20, bottom: 24 })
.constraintSize({ maxHeight: '85%' })
}
加急通道的选择器用红色 #C62828 标记选中态——因为"特急24h"是紧急操作,红色传递紧迫感。提交按钮用青铜棕到浅棕的渐变,constraintSize({ maxHeight: '85%' }) 限制抽屉最大高度不超过屏幕的 85%——防止表单内容过多时撑满全屏。
主播总结:这个连麦申请表单展示了 ArkTS 表单交互的三个经典模式——文本输入、胶囊选择器、头像选择器。每种选择器都用"反转色"做选中反馈:选中时深色底白字,未选中时浅色底深色字。统一的设计语言让用户在不同表单中有一致的交互预期。
第四段:直播台 Tab——四宫格连麦窗口
4.1 LiveTab192 组件
好,接下来看整个应用最核心的组件——直播台。这里是用户进入应用后看到的第一个页面,也是信息密度最高的页面。
@Component
struct LiveTab192 {
onLianmai: () => void = () => {
};
@State liked: boolean[] = [false, true, false, false, true, false, false];
build() {
Column() {
Column() {
Row() {
Text('🔴')
.fontSize(14)
Text('第 128 场 · 瓷器专场')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
.margin({ left: 6 })
Text('')
.layoutWeight(1)
Text('在线 412')
.fontSize(10)
.fontColor('#8D7B6E')
}
.width('100%')
Grid() {
GridItem() {
Column() {
Text('🧑🏫')
.fontSize(28)
Text('程汝言 · 主鉴')
.fontSize(9)
.fontColor('#FFFFFF')
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#5D4037')
}
GridItem() {
Column() {
Text('🪞')
.fontSize(28)
Text('藏宝人视角')
.fontSize(9)
.fontColor('#FFFFFF')
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#8D6E63')
}
GridItem() {
Column() {
Text('🔍')
.fontSize(28)
Text('微距镜头')
.fontSize(9)
.fontColor('#FFFFFF')
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#4E342E')
}
GridItem() {
Column() {
Text('📜')
.fontSize(28)
Text('款识特写')
.fontSize(9)
.fontColor('#FFFFFF')
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#6D4C41')
}
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr')
.width('100%')
.height(210)
.borderRadius(12)
.columnsGap(2)
.rowsGap(2)
.margin({ top: 10 })
四宫格连麦窗口是整个应用的视觉焦点。Grid 组件用 columnsTemplate('1fr 1fr') 和 rowsTemplate('1fr 1fr') 定义为 2x2 网格,高度 210 像素,间距 2 像素。四个格子分别是:主鉴专家视角(深棕 #5D4037)、藏宝人视角(中棕 #8D6E63)、微距镜头(更深棕 #4E342E)、款识特写(浅棕 #6D4C41)。四个格子的背景色是同一色系的不同明度,制造出青铜展柜中四盏不同角度射灯的效果。columnsGap(2) 和 rowsGap(2) 让格子之间有 2 像素的缝隙——模拟视频会议中多窗口之间的分隔线。
主播知识点:
Grid的columnsTemplate和rowsTemplate用fr单位可以做等分网格。1fr 1fr就是两等分。如果你要 3 列就用1fr 1fr 1fr。这个比用Row+layoutWeight更适合做网格布局,因为 Grid 原生支持行列间距。
4.2 藏品列表和弹幕区
Column() {
ForEach(LOTS192, (l: Lot192, i: number) => {
Row() {
Column() {
Text(l.cate.charAt(0))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.width(44)
.height(44)
.justifyContent(FlexAlign.Center)
.borderRadius(10)
.backgroundColor(l.color)
Column() {
Row() {
Text(l.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Text(l.grade)
.fontSize(8)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor(gradeColor192(l.grade))
.margin({ left: 6 })
Text('')
.layoutWeight(1)
Text(l.verdict)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(verdictColor192(l.verdict))
}
.width('100%')
Text(l.era + ' · ' + l.owner + ' · 估值 ' + l.value + ' 万')
.fontSize(10)
.fontColor('#8D7B6E')
.margin({ top: 4 })
Row() {
Text('')
.width('60%')
.height(3)
.borderRadius(2)
.backgroundColor('#F1E8D8')
Text('')
.layoutWeight(1)
}
.width('100%')
.margin({ top: 6 })
Row() {
Text('热度')
.fontSize(9)
.fontColor('#8D7B6E')
Text('')
.width(l.hot * 0.9)
.height(4)
.borderRadius(2)
.backgroundColor(l.color)
.margin({ left: 6 })
Text('')
.layoutWeight(1)
}
.width('100%')
.margin({ top: 4 })
}
.layoutWeight(1)
.margin({ left: 10 })
.alignItems(HorizontalAlign.Start)
Column() {
Text(i === 0 ? '鉴定中' : '已完成')
.fontSize(9)
.fontColor(i === 0 ? '#C62828' : '#00695C')
Text(this.liked[i] ? '🔥' : '👍')
.fontSize(16)
.margin({ top: 6 })
.onClick(() => {
this.liked[i] = !this.liked[i];
})
}
.margin({ left: 8 })
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor(i === 0 ? '#FBF4E4' : '#FFFFFF')
.margin({ top: 8 })
}, (l: Lot192) => l.id)
}
.width('100%')
.padding({ left: 12, right: 12 })
.margin({ top: 8 })
藏品列表用 l.cate.charAt(0) 取品类首字作为图标——"瓷"字白底棕底、"玉"字绿底、"书"字靛蓝底。这种用品类首字做图标的做法在资源受限时很实用。第一条藏品(正在鉴定的)用 #FBF4E4 暖底色标记,其他白底。热度进度条用 l.hot * 0.9 作为宽度——97 的热度对应 87.3 像素的进度条宽度。点赞按钮用 @State liked: boolean[] 数组管理每条藏品的点赞状态,点击切换 🔥 和 👍。
主播发现:这个
liked数组直接在onClick中用this.liked[i] = !this.liked[i]修改——这是直接修改数组元素而非生成新数组。在 ArkTS 中,@State标记的数组如果直接修改元素,在某些版本中可能不触发响应式更新。这是这个代码中一个潜在的风险点。更安全的做法是用map生成新数组。
4.3 弹幕和结论占比
Column() {
Text('本场鉴定结论构成')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Row() {
ForEach(SHARE192, (s: Share192) => {
Column() {
}
.layoutWeight(s.w)
.height(10)
.backgroundColor(s.c)
}, (s: Share192) => s.label)
}
.width('100%')
.clip(true)
.borderRadius(5)
.margin({ top: 10 })
Row() {
ForEach(SHARE192, (s: Share192) => {
Row() {
Text('●')
.fontSize(8)
.fontColor(s.c)
Text(s.label + ' ' + s.w + '%')
.fontSize(9)
.fontColor('#8D7B6E')
.margin({ left: 4 })
}
.margin({ right: 10 })
}, (s: Share192) => s.label)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ left: 12, right: 12, top: 16 })
结论构成占比用 layoutWeight(s.w) 实现比例分配——真品 46%、后仿 22%、待考 18%、普品 14%。clip(true) + borderRadius(5) 让堆叠条的两端有圆角。下方图例每项用圆点 ● 对应颜色加文字标注。这种"堆叠条 + 图例"是数据可视化的经典组合,在直播场景中能快速传达"本场鉴定的整体结果分布"。
第五段:藏品编辑和数据更新
5.1 编辑藏品抽屉
@Builder
editSheet192() {
Column() {
Row() {
Text('')
.width(36)
.height(4)
.borderRadius(2)
.backgroundColor('#D9D2C5')
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 10 })
Text('编辑藏品')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
.margin({ top: 12 })
Column() {
Text('藏品名称')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
Text(this.editName)
.fontSize(13)
.fontColor('#3E2723')
Text('')
.layoutWeight(1)
}
.width('100%')
.padding(12)
.borderRadius(10)
.backgroundColor('#F7F3EA')
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('展示密级')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
ForEach(PRIVACY192, (p: string, i: number) => {
Text(p)
.fontSize(12)
.fontColor(i === this.privacyIdx ? '#FFFFFF' : '#5D4037')
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.borderRadius(16)
.backgroundColor(i === this.privacyIdx ? '#5D4037' : '#F1E8D8')
.margin({ right: 8 })
.onClick(() => {
this.privacyIdx = i;
})
}, (p: string) => p)
}
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('最新估值(万元)')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
Text('−')
.fontSize(18)
.fontColor('#5D4037')
.width(34)
.height(34)
.textAlign(TextAlign.Center)
.borderRadius(17)
.backgroundColor('#F1E8D8')
.onClick(() => {
if (this.estValue > 1) {
this.estValue -= 1;
}
})
Text(this.estValue + ' 万')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
.layoutWeight(1)
.textAlign(TextAlign.Center)
Text('+')
.fontSize(18)
.fontColor('#5D4037')
.width(34)
.height(34)
.textAlign(TextAlign.Center)
.borderRadius(17)
.backgroundColor('#F1E8D8')
.onClick(() => {
if (this.estValue < 99) {
this.estValue += 1;
}
})
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
编辑藏品抽屉的"展示密级"选择器只有两个选项——“公开展示"和"仅自己可见”。这是藏品隐私管理的核心开关。估值步进器用加减按钮,范围 1-99 万,每次增减 1 万。注意加减按钮的视觉权重相同——都是浅色底棕色字,不像其他应用中加号用深色、减号用浅色。这里的考量是:估值增减都是中性的编辑操作,不需要视觉上偏向"增加"。
5.2 保存逻辑
.onClick(() => {
this.myLots = this.myLots.map((l: Lot192, i: number) => {
if (i === this.editIdx) {
return {
id: l.id, name: this.editName, cate: l.cate, era: l.era,
grade: l.grade, value: this.estValue, owner: l.owner,
verdict: l.verdict, color: l.color, hot: l.hot, state: l.state
};
}
return l;
});
this.showEditSheet = false;
})
保存逻辑用 map 遍历 myLots 数组,匹配到 editIdx 时用新对象替换(只改 name 和 value),其余保持不变。新数组赋值给 this.myLots 触发 @State 响应式更新,CollectionTab192 收到新数据后自动重新渲染。这种"不可变更新"模式是 ArkTS 中修改数组状态的标准做法——永远不直接修改原数组,而是生成新数组赋值回去。
5.3 撤销送鉴单
.onClick(() => {
this.myOrders = this.myOrders.filter((o: Session192, i: number) => i !== this.delIdx);
this.showDelDialog = false;
})
撤销送鉴单用 filter 过滤掉 delIdx 对应的订单,生成新数组赋值给 myOrders。这跟编辑用的 map 是同一类操作——不可变数据更新。filter 保留不匹配的元素,map 替换匹配的元素,两者配合可以覆盖数组的增删改三种操作。
主播划重点:在 ArkTS 里操作数组状态,记住三个法宝——增用
concat,删用filter,改用map。永远不要用push、splice、arr[i] = x这种直接修改方式。这不是语法限制,是响应式系统的设计要求。
流程图:从浏览到连麦申请的完整路径
技术要素对比
| 技术要素 | 实现方式 | 主播点评 |
|---|---|---|
| 四宫格连麦窗口 | Grid + columnsTemplate('1fr 1fr') |
比用 Row+Column 更原生,原生支持行列间距 |
| 展柜导航 | 选中时金线+暖光底+白底图标 | 设计语言与"鉴宝"业务场景深度绑定 |
| 颜色映射函数 | gradeColor192/stateColor192/verdictColor192 |
把业务语义翻译为视觉语言,DRY 原则 |
| 不可变数组更新 | 增concat/删filter/改map |
ArkTS 响应式系统的核心要求 |
| 连麦申请表单 | 胶囊选择器+头像选择器+步进器 | 三种选择器统一用反转色做选中反馈 |
| 弹层分工 | bindSheet抽屉 + bindContentCover遮罩 |
日常表单走抽屉,破坏性确认走全屏遮罩 |
| 热度可视化 | width(l.hot * 0.9) 像素宽度 |
数值直接映射像素,简单直接 |
| 结论占比条 | layoutWeight(s.w) + clip(true) |
比例权重自动分配,clip裁切圆角 |
| 品类首字图标 | l.cate.charAt(0) |
无图标资源时的巧妙替代方案 |
| 直播指示器 | ● + #FFD54F + 半透明黑底 |
直播 APP 标配的视觉模式 |
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

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

完整代码:
// 场景:藏友通过视频会议室连麦专家,直播鉴定瓷器玉器书画等藏品
// 配色:青铜棕 #5D4037 × 鎏金 #D4AF37 × 米宣白 #F7F3EA
// Tab布局:底部6 tab「展柜格」导航(选中格子暖光底+顶部细金线,图标圆底如展品)
// 弹框:申请连麦鉴宝(抽屉)/新增送鉴单(抽屉)/编辑藏品(抽屉)/删除送鉴单(居中)/藏品详情(居中)
interface TabItem192 {
name: string;
icon: string;
badge: number;
}
interface Lot192 {
id: string;
name: string;
cate: string;
era: string;
grade: string;
value: number;
owner: string;
verdict: string;
color: string;
hot: number;
state: string
}
interface VerdictRow192 {
id: string;
lot: string;
expert: string;
result: string;
date: string;
conf: number;
color: string;
}
interface Expert192 {
id: string;
name: string;
field: string;
title: string;
rating: number;
sessions: number;
emoji: string;
online: boolean;
}
interface Session192 {
id: string;
date: string;
week: string;
time: string;
theme: string;
expert: string;
joined: number;
seats: number;
state: string;
}
interface Danmaku192 {
id: string;
user: string;
text: string;
color: string;
}
interface Attend192 {
day: string;
cnt: number;
}
interface Share192 {
label: string;
w: number;
c: string;
}
interface HotCate192 {
name: string;
pct: number;
color: string;
}
interface Record192 {
id: string;
date: string;
lot: string;
result: string;
color: string;
}
const TABS192: TabItem192[] = [
{ name: '鉴宝台', icon: '🏺', badge: 1 },
{ name: '藏品阁', icon: '🗂️', badge: 0 },
{ name: '送鉴单', icon: '📋', badge: 3 },
{ name: '行家团', icon: '🧑🏫', badge: 0 },
{ name: '档期', icon: '📅', badge: 0 },
{ name: '我的', icon: '👤', badge: 0 }
];
const LOTS192: Lot192[] = [
{ id: 'l1', name: '青花缠枝纹梅瓶', cate: '瓷器', era: '明永乐', grade: '珍品', value: 128, owner: '金陵藏家·陆先生', verdict: '真品', color: '#5D4037', hot: 97, state: '待定' },
{ id: 'l2', name: '和田白玉把件', cate: '玉器', era: '清中期', grade: '上品', value: 66, owner: '姑苏·沈女士', verdict: '真品', color: '#00695C', hot: 89, state: '待定' },
{ id: 'l3', name: '田黄石瑞兽钮章', cate: '印石', era: '民国', grade: '上品', value: 92, owner: '榕城·林先生', verdict: '真品', color: '#AD1457', hot: 93, state: '待定' },
{ id: 'l4', name: '仿哥窑贯耳瓶', cate: '瓷器', era: '清晚期', grade: '普品', value: 8, owner: '蜀中·赵女士', verdict: '后仿', color: '#757575', hot: 64, state: '待定' },
{ id: 'l5', name: '吴门山水扇面', cate: '书画', era: '明', grade: '珍品', value: 210, owner: '沪上·程先生', verdict: '待考', color: '#283593', hot: 99, state: '待定' },
{ id: 'l6', name: '鎏金铜坐佛', cate: '铜器', era: '明早期', grade: '上品', value: 155, owner: '长安·马先生', verdict: '真品', color: '#EF6C00', hot: 91, state: '待定' },
{ id: 'l7', name: '端砚云月池', cate: '文房', era: '清', grade: '上品', value: 48, owner: '肇庆·梁先生', verdict: '真品', color: '#00838F', hot: 72, state: '待定' },
{ id: 'l8', name: '粉彩百蝶瓶', cate: '瓷器', era: '清光绪', grade: '珍品', value: 178, owner: '津门·周女士', verdict: '真品', color: '#827717', hot: 95, state: '待定' }
];
const VERDICTS192: VerdictRow192[] = [
{ id: 'v1', lot: '青花缠枝纹梅瓶', expert: '瓷器组·程汝言', result: '真品·明永乐本朝', date: '08-24', conf: 96, color: '#5D4037' },
{ id: 'v2', lot: '和田白玉把件', expert: '玉器组·白玛老师', result: '真品·清中期工', date: '08-24', conf: 91, color: '#00695C' },
{ id: 'v3', lot: '仿哥窑贯耳瓶', expert: '瓷器组·程汝言', result: '后仿·晚清仿宋', date: '08-23', conf: 88, color: '#757575' },
{ id: 'v4', lot: '田黄石瑞兽钮章', expert: '印石组·林半尺', result: '真品·民国老料', date: '08-23', conf: 93, color: '#AD1457' },
{ id: 'v5', lot: '端砚云月池', expert: '文房组·梁砚秋', result: '真品·清坑仔岩', date: '08-22', conf: 90, color: '#00838F' },
{ id: 'v6', lot: '粉彩百蝶瓶', expert: '瓷器组·程汝言', result: '真品·光绪官样', date: '08-22', conf: 94, color: '#827717' }
];
const EXPERTS192: Expert192[] = [
{ id: 'e1', name: '程汝言', field: '瓷器', title: '副研究馆员', rating: 4.9, sessions: 486, emoji: '🏺', online: true },
{ id: 'e2', name: '白玛', field: '玉器', title: '高级鉴定师', rating: 4.8, sessions: 352, emoji: '🪨', online: true },
{ id: 'e3', name: '林半尺', field: '印石', title: '民间藏家代表', rating: 4.7, sessions: 298, emoji: '🖊️', online: false },
{ id: 'e4', name: '顾墨白', field: '书画', title: '美术学院特聘', rating: 5.0, sessions: 275, emoji: '🖼️', online: true },
{ id: 'e5', name: '马远声', field: '铜器', title: '考古队顾问', rating: 4.8, sessions: 231, emoji: '🔔', online: false },
{ id: 'e6', name: '梁砚秋', field: '文房', title: '端砚研究会理事', rating: 4.6, sessions: 189, emoji: '📖', online: true }
];
const SESSIONS192: Session192[] = [
{ id: 's1', date: '08-26', week: '周三', time: '19:30', theme: '瓷器专场·永宣青花', expert: '程汝言', joined: 412, seats: 500, state: '可预约' },
{ id: 's2', date: '08-27', week: '周四', time: '20:00', theme: '玉器专场·和田籽料', expert: '白玛', joined: 356, seats: 500, state: '可预约' },
{ id: 's3', date: '08-28', week: '周五', time: '19:00', theme: '书画专场·吴门画派', expert: '顾墨白', joined: 488, seats: 500, state: '即将满员' },
{ id: 's4', date: '08-29', week: '周六', time: '14:00', theme: '综合场·藏友互评', expert: '全组行家', joined: 500, seats: 500, state: '已满员' },
{ id: 's5', date: '08-30', week: '周日', time: '10:00', theme: '铜器专场·金铜佛像', expert: '马远声', joined: 274, seats: 500, state: '可预约' },
{ id: 's6', date: '08-31', week: '周一', time: '19:30', theme: '文房专场·砚墨纸笔', expert: '梁砚秋', joined: 198, seats: 500, state: '可预约' },
{ id: 's7', date: '09-01', week: '周二', time: '20:00', theme: '印石专场·寿山田黄', expert: '林半尺', joined: 321, seats: 500, state: '可预约' }
];
const DANMAKU192: Danmaku192[] = [
{ id: 'd1', user: '捡漏王', text: '这件梅瓶釉水一看就是本朝的!', color: '#5D4037' },
{ id: 'd2', user: '苏工坊', text: '专家上手上手,让让镜头~', color: '#AD1457' },
{ id: 'd3', user: '青花控', text: '缠枝纹画工太流畅了', color: '#283593' },
{ id: 'd4', user: '老物件', text: '我家也有个类似的,蹲个结论', color: '#00695C' },
{ id: 'd5', user: '文玩小生', text: '灯光再给个侧光看气泡', color: '#EF6C00' },
{ id: 'd6', user: '古巷', text: '这个底足修胎对味儿', color: '#827717' },
{ id: 'd7', user: '藏珠阁', text: '128万估值保守了', color: '#00838F' }
];
const ATTEND192: Attend192[] = [
{ day: '一', cnt: 46 },
{ day: '二', cnt: 52 },
{ day: '三', cnt: 38 },
{ day: '四', cnt: 61 },
{ day: '五', cnt: 74 },
{ day: '六', cnt: 92 },
{ day: '日', cnt: 85 }
];
const SHARE192: Share192[] = [
{ label: '真品', w: 46, c: '#5D4037' },
{ label: '后仿', w: 22, c: '#B08968' },
{ label: '待考', w: 18, c: '#D4AF37' },
{ label: '普品', w: 14, c: '#C8B7A6' }
];
const HOTCATES192: HotCate192[] = [
{ name: '瓷器', pct: 96, color: '#5D4037' },
{ name: '书画', pct: 88, color: '#283593' },
{ name: '玉器', pct: 82, color: '#00695C' },
{ name: '铜器', pct: 71, color: '#EF6C00' },
{ name: '印石', pct: 63, color: '#AD1457' },
{ name: '文房', pct: 55, color: '#00838F' }
];
const RECORDS192: Record192[] = [
{ id: 'r1', date: '08-24 21:02', lot: '青花缠枝纹梅瓶', result: '真品·明永乐', color: '#5D4037' },
{ id: 'r2', date: '08-23 20:45', lot: '仿哥窑贯耳瓶', result: '后仿·晚清仿宋', color: '#757575' },
{ id: 'r3', date: '08-22 19:58', lot: '端砚云月池', result: '真品·清坑仔岩', color: '#00838F' },
{ id: 'r4', date: '08-21 20:30', lot: '粉彩百蝶瓶', result: '真品·光绪官样', color: '#827717' },
{ id: 'r5', date: '08-20 19:47', lot: '铜鎏金小坐像', result: '真品·明早期', color: '#EF6C00' }
];
const CATES192: string[] = ['瓷器', '玉器', '书画', '铜器', '印石', '文房'];
const URGENTS192: string[] = ['普通排期', '加急48h', '特急24h'];
const PRIVACY192: string[] = ['公开展示', '仅自己可见'];
function gradeColor192(g: string): string {
if (g === '珍品') {
return '#D4AF37';
}
if (g === '上品') {
return '#5D4037';
}
return '#9E9E9E';
}
function stateColor192(s: string): string {
if (s === '可预约') {
return '#00695C';
}
if (s === '即将满员') {
return '#EF6C00';
}
return '#9E9E9E';
}
function verdictColor192(v: string): string {
if (v === '真品') {
return '#00695C';
}
if (v === '后仿') {
return '#C62828';
}
return '#EF6C00';
}
@Entry
@Component
struct Index192 {
@State curTab: number = 0;
@State showLianmaiSheet: boolean = false;
@State showSendSheet: boolean = false;
@State showEditSheet: boolean = false;
@State showDelDialog: boolean = false;
@State showDetailDialog: boolean = false;
@State editIdx: number = 0;
@State editName: string = '';
@State delIdx: number = 0;
@State detailIdx: number = 0;
@State cateIdx: number = 0;
@State urgentIdx: number = 0;
@State privacyIdx: number = 0;
@State hopeExpert: number = 0;
@State estValue: number = 10;
@State insureOn: boolean = true;
@State pickupOn: boolean = false;
@State keepRecordOn: boolean = true;
@State myLots: Lot192[] = [
{ id: 'm1', name: '青花缠枝纹梅瓶', cate: '瓷器', era: '明永乐', grade: '珍品', value: 128, owner: '我', verdict: '真品', color: '#5D4037', hot: 97, state: '待定' },
{ id: 'm2', name: '端砚云月池', cate: '文房', era: '清', grade: '上品', value: 48, owner: '我', verdict: '真品', color: '#00838F', hot: 72, state: '待定' },
{ id: 'm3', name: '铜鎏金小坐像', cate: '铜器', era: '明早期', grade: '上品', value: 155, owner: '我', verdict: '真品', color: '#EF6C00', hot: 90, state: '待定' }
];
@State myOrders: Session192[] = [
{ id: 'o1', date: '08-26', week: '周三', time: '19:30', theme: '瓷器专场·永宣青花', expert: '程汝言', joined: 412, seats: 500, state: '已预约' },
{ id: 'o2', date: '08-28', week: '周五', time: '19:00', theme: '书画专场·吴门画派', expert: '顾墨白', joined: 488, seats: 500, state: '已预约' }
];
build() {
Column() {
Column() {
Text('鎏光·云鉴宝')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Row() {
Row() {
Text('●')
.fontSize(9)
.fontColor('#FFD54F')
Text('直播中 · 第128场')
.fontSize(11)
.fontColor('#FFD54F')
.margin({ left: 4 })
}
.padding({ left: 10, right: 10, top: 3, bottom: 3 })
.borderRadius(10)
.backgroundColor('#4D000000')
Text('')
.layoutWeight(1)
Text('6.8万人看过')
.fontSize(11)
.fontColor('#E8DCC8')
}
.width('100%')
.margin({ top: 10 })
Row() {
ForEach(LOTS192, (l: Lot192, idx: number) => {
Column() {
Text(l.era)
.fontSize(9)
.fontColor(idx === 0 ? '#5D4037' : '#E8DCC8')
Text(idx === 0 ? '正在鉴定' : l.verdict)
.fontSize(10)
.fontWeight(idx === 0 ? FontWeight.Bold : FontWeight.Normal)
.fontColor(idx === 0 ? '#FFD54F' : '#E8DCC8')
}
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(8)
.backgroundColor(idx === 0 ? '#D4AF37' : '#33000000')
}, (l: Lot192) => l.id)
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 14 })
.linearGradient({
angle: 160,
colors: [['#4E342E', 0], ['#5D4037', 0.5], ['#3E2723', 1]]
})
Scroll() {
Column() {
if (this.curTab === 0) {
LiveTab192({
onLianmai: () => {
this.showLianmaiSheet = true;
}
})
}
if (this.curTab === 1) {
CollectionTab192({
lots: this.myLots,
onEdit: (idx: number) => {
this.editIdx = idx;
this.editName = this.myLots[idx].name;
this.showEditSheet = true;
},
onDetail: (idx: number) => {
this.detailIdx = idx;
this.showDetailDialog = true;
}
})
}
if (this.curTab === 2) {
OrderTab192({
orders: this.myOrders,
onSend: () => {
this.showSendSheet = true;
},
onDel: (idx: number) => {
this.delIdx = idx;
this.showDelDialog = true;
}
})
}
if (this.curTab === 3) {
ExpertTab192()
}
if (this.curTab === 4) {
ScheduleTab192({
onLianmai: () => {
this.showLianmaiSheet = true;
}
})
}
if (this.curTab === 5) {
MineTab192({
records: RECORDS192,
onDetail: () => {
this.detailIdx = 0;
this.showDetailDialog = true;
}
})
}
}
.width('100%')
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.backgroundColor('#F7F3EA')
Row() {
ForEach(TABS192, (t: TabItem192, idx: number) => {
Column() {
Text('')
.width(idx === this.curTab ? 26 : 0)
.height(2)
.backgroundColor('#D4AF37')
.borderRadius(1)
.margin({ top: 6 })
Row() {
Text(t.icon)
.fontSize(18)
.width(38)
.height(38)
.textAlign(TextAlign.Center)
.borderRadius(19)
.backgroundColor(idx === this.curTab ? '#FFFFFF' : '#F1E8D8')
Column() {
Text(t.name)
.fontSize(10)
.fontWeight(idx === this.curTab ? FontWeight.Bold : FontWeight.Normal)
.fontColor(idx === this.curTab ? '#5D4037' : '#8D7B6E')
if (t.badge > 0) {
Text(t.badge + '')
.fontSize(8)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(7)
.backgroundColor('#C62828')
.margin({ top: 2 })
}
}
.margin({ left: 6 })
}
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor(idx === this.curTab ? '#F9EED2' : '#F7F3EA')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.curTab = idx;
})
}, (t: TabItem192, idx: number) => t.name + idx)
}
.width('100%')
.padding({ left: 6, right: 6, bottom: 8 })
.backgroundColor('#FFFFFF')
}
.width('100%')
.height('100%')
.backgroundColor('#F7F3EA')
.bindSheet($$this.showLianmaiSheet, this.lianmaiSheet192(), {
height: 580,
dragBar: true,
showClose: false,
backgroundColor: '#FFFFFF'
})
.bindSheet($$this.showSendSheet, this.sendSheet192(), {
height: 560,
dragBar: true,
showClose: false,
backgroundColor: '#FFFFFF'
})
.bindSheet($$this.showEditSheet, this.editSheet192(), {
height: 520,
dragBar: true,
showClose: false,
backgroundColor: '#FFFFFF'
})
.bindContentCover($$this.showDelDialog, this.delDialog192(), {
})
.bindContentCover($$this.showDetailDialog, this.detailDialog192(), {
})
}
@Builder
lianmaiSheet192() {
Column() {
Row() {
Text('')
.width(36)
.height(4)
.borderRadius(2)
.backgroundColor('#D9D2C5')
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 10 })
Text('申请连麦鉴宝')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
.margin({ top: 12 })
Column() {
Text('藏品名称')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
Text('请输入藏品全名(选填)')
.fontSize(13)
.fontColor('#B8AC9C')
Text('')
.layoutWeight(1)
}
.width('100%')
.padding(12)
.borderRadius(10)
.backgroundColor('#F7F3EA')
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('藏品品类')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
ForEach(CATES192, (c: string, i: number) => {
Text(c)
.fontSize(12)
.fontColor(i === this.cateIdx ? '#FFFFFF' : '#5D4037')
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.borderRadius(16)
.backgroundColor(i === this.cateIdx ? '#5D4037' : '#F1E8D8')
.margin({ right: 8 })
.onClick(() => {
this.cateIdx = i;
})
}, (c: string) => c)
}
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('期望行家')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
ForEach(EXPERTS192, (e: Expert192, i: number) => {
Column() {
Text(e.emoji)
.fontSize(18)
.width(42)
.height(42)
.textAlign(TextAlign.Center)
.borderRadius(21)
.backgroundColor(i === this.hopeExpert ? '#D4AF37' : '#F1E8D8')
Text(e.name)
.fontSize(9)
.fontColor(i === this.hopeExpert ? '#5D4037' : '#8D7B6E')
.margin({ top: 4 })
}
.margin({ right: 12 })
.onClick(() => {
this.hopeExpert = i;
})
}, (e: Expert192) => e.id)
}
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('心理估值(万元)')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
Text('−')
.fontSize(18)
.fontColor('#5D4037')
.width(34)
.height(34)
.textAlign(TextAlign.Center)
.borderRadius(17)
.backgroundColor('#F1E8D8')
.onClick(() => {
if (this.estValue > 1) {
this.estValue -= 1;
}
})
Text(this.estValue + ' 万')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
.layoutWeight(1)
.textAlign(TextAlign.Center)
Text('+')
.fontSize(18)
.fontColor('#5D4037')
.width(34)
.height(34)
.textAlign(TextAlign.Center)
.borderRadius(17)
.backgroundColor('#F1E8D8')
.onClick(() => {
if (this.estValue < 99) {
this.estValue += 1;
}
})
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Row() {
Column() {
Text('加急通道')
.fontSize(13)
.fontColor('#3E2723')
Text('优先排入当前直播队列')
.fontSize(10)
.fontColor('#8D7B6E')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Text('')
.layoutWeight(1)
ForEach(URGENTS192, (u: string, i: number) => {
Text(u)
.fontSize(11)
.fontColor(i === this.urgentIdx ? '#FFFFFF' : '#5D4037')
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor(i === this.urgentIdx ? '#C62828' : '#F1E8D8')
.margin({ left: 6 })
.onClick(() => {
this.urgentIdx = i;
})
}, (u: string) => u)
}
.width('100%')
.margin({ top: 18 })
Row() {
Text('申请连麦')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.textAlign(TextAlign.Center)
.width('100%')
.padding({ top: 13, bottom: 13 })
.borderRadius(22)
.linearGradient({
angle: 90,
colors: [['#5D4037', 0], ['#8D6E63', 1]]
})
}
.width('100%')
.margin({ top: 20 })
.onClick(() => {
this.showLianmaiSheet = false;
})
}
.width('100%')
.padding({ left: 20, right: 20, bottom: 24 })
.constraintSize({ maxHeight: '85%' })
}
@Builder
sendSheet192() {
Column() {
Row() {
Text('')
.width(36)
.height(4)
.borderRadius(2)
.backgroundColor('#D9D2C5')
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 10 })
Text('新增送鉴单')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
.margin({ top: 12 })
Column() {
Text('藏品名称')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
Text('如:青花缠枝纹梅瓶')
.fontSize(13)
.fontColor('#B8AC9C')
Text('')
.layoutWeight(1)
}
.width('100%')
.padding(12)
.borderRadius(10)
.backgroundColor('#F7F3EA')
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('藏品品类')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
ForEach(CATES192, (c: string, i: number) => {
Text(c)
.fontSize(12)
.fontColor(i === this.cateIdx ? '#FFFFFF' : '#5D4037')
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.borderRadius(16)
.backgroundColor(i === this.cateIdx ? '#5D4037' : '#F1E8D8')
.margin({ right: 8 })
.onClick(() => {
this.cateIdx = i;
})
}, (c: string) => c)
}
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('保价金额(万元)')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
Text('−')
.fontSize(18)
.fontColor('#5D4037')
.width(34)
.height(34)
.textAlign(TextAlign.Center)
.borderRadius(17)
.backgroundColor('#F1E8D8')
.onClick(() => {
if (this.estValue > 1) {
this.estValue -= 1;
}
})
Text(this.estValue + ' 万')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
.layoutWeight(1)
.textAlign(TextAlign.Center)
Text('+')
.fontSize(18)
.fontColor('#5D4037')
.width(34)
.height(34)
.textAlign(TextAlign.Center)
.borderRadius(17)
.backgroundColor('#F1E8D8')
.onClick(() => {
if (this.estValue < 99) {
this.estValue += 1;
}
})
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Row() {
Column() {
Text('上门取件')
.fontSize(13)
.fontColor('#3E2723')
Text('顺丰保价 · 专业防震包装')
.fontSize(10)
.fontColor('#8D7B6E')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Text('')
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.pickupOn })
.onChange((on: boolean) => {
this.pickupOn = on;
})
}
.width('100%')
.margin({ top: 18 })
Row() {
Column() {
Text('购买运输保险')
.fontSize(13)
.fontColor('#3E2723')
Text('按保价金额 0.3% 计费')
.fontSize(10)
.fontColor('#8D7B6E')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Text('')
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.insureOn })
.onChange((on: boolean) => {
this.insureOn = on;
})
}
.width('100%')
.margin({ top: 14 })
Row() {
Text('提交送鉴单')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.textAlign(TextAlign.Center)
.width('100%')
.padding({ top: 13, bottom: 13 })
.borderRadius(22)
.linearGradient({
angle: 90,
colors: [['#D4AF37', 0], ['#B8860B', 1]]
})
}
.width('100%')
.margin({ top: 20 })
.onClick(() => {
this.showSendSheet = false;
})
}
.width('100%')
.padding({ left: 20, right: 20, bottom: 24 })
.constraintSize({ maxHeight: '85%' })
}
@Builder
editSheet192() {
Column() {
Row() {
Text('')
.width(36)
.height(4)
.borderRadius(2)
.backgroundColor('#D9D2C5')
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 10 })
Text('编辑藏品')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
.margin({ top: 12 })
Column() {
Text('藏品名称')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
Text(this.editName)
.fontSize(13)
.fontColor('#3E2723')
Text('')
.layoutWeight(1)
}
.width('100%')
.padding(12)
.borderRadius(10)
.backgroundColor('#F7F3EA')
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('展示密级')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
ForEach(PRIVACY192, (p: string, i: number) => {
Text(p)
.fontSize(12)
.fontColor(i === this.privacyIdx ? '#FFFFFF' : '#5D4037')
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.borderRadius(16)
.backgroundColor(i === this.privacyIdx ? '#5D4037' : '#F1E8D8')
.margin({ right: 8 })
.onClick(() => {
this.privacyIdx = i;
})
}, (p: string) => p)
}
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('最新估值(万元)')
.fontSize(12)
.fontColor('#8D7B6E')
Row() {
Text('−')
.fontSize(18)
.fontColor('#5D4037')
.width(34)
.height(34)
.textAlign(TextAlign.Center)
.borderRadius(17)
.backgroundColor('#F1E8D8')
.onClick(() => {
if (this.estValue > 1) {
this.estValue -= 1;
}
})
Text(this.estValue + ' 万')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
.layoutWeight(1)
.textAlign(TextAlign.Center)
Text('+')
.fontSize(18)
.fontColor('#5D4037')
.width(34)
.height(34)
.textAlign(TextAlign.Center)
.borderRadius(17)
.backgroundColor('#F1E8D8')
.onClick(() => {
if (this.estValue < 99) {
this.estValue += 1;
}
})
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Row() {
Column() {
Text('藏品保险')
.fontSize(13)
.fontColor('#3E2723')
Text('按估值的 0.5% 年费承保')
.fontSize(10)
.fontColor('#8D7B6E')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Text('')
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.insureOn })
.onChange((on: boolean) => {
this.insureOn = on;
})
}
.width('100%')
.margin({ top: 18 })
Row() {
Text('取消')
.fontSize(14)
.fontColor('#8D7B6E')
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 13, bottom: 13 })
.borderRadius(22)
.backgroundColor('#F1E8D8')
Text('保存修改')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.textAlign(TextAlign.Center)
.layoutWeight(1.6)
.padding({ top: 13, bottom: 13 })
.borderRadius(22)
.margin({ left: 10 })
.linearGradient({
angle: 90,
colors: [['#5D4037', 0], ['#8D6E63', 1]]
})
}
.width('100%')
.margin({ top: 20 })
.onClick(() => {
this.myLots = this.myLots.map((l: Lot192, i: number) => {
if (i === this.editIdx) {
return {
id: l.id, name: this.editName, cate: l.cate, era: l.era,
grade: l.grade, value: this.estValue, owner: l.owner,
verdict: l.verdict, color: l.color, hot: l.hot, state: l.state
};
}
return l;
});
this.showEditSheet = false;
})
}
.width('100%')
.padding({ left: 20, right: 20, bottom: 24 })
.constraintSize({ maxHeight: '85%' })
}
@Builder
delDialog192() {
Column() {
Text('🗑️')
.fontSize(34)
.margin({ top: 22 })
Text('撤销送鉴单')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
.margin({ top: 8 })
Text('撤销后本场次席位将立即释放,重新预约需重新排队')
.fontSize(12)
.fontColor('#8D7B6E')
.textAlign(TextAlign.Center)
.margin({ top: 8 })
Row() {
Column() {
Text('预约场次')
.fontSize(10)
.fontColor('#8D7B6E')
Text(this.delIdx < this.myOrders.length ? this.myOrders[this.delIdx].theme : '-')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text('已扣留位费')
.fontSize(10)
.fontColor('#8D7B6E')
Text('¥ 20.00')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#C62828')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#F7F3EA')
.margin({ top: 16 })
Row() {
Text('同步删除鉴定记录')
.fontSize(12)
.fontColor('#3E2723')
Text('')
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.keepRecordOn })
.onChange((on: boolean) => {
this.keepRecordOn = on;
})
}
.width('100%')
.margin({ top: 14 })
Row() {
Text('再想想')
.fontSize(14)
.fontColor('#8D7B6E')
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 12, bottom: 12 })
.borderRadius(20)
.backgroundColor('#F1E8D8')
.onClick(() => {
this.showDelDialog = false;
})
Text('确认撤销')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.textAlign(TextAlign.Center)
.layoutWeight(1.4)
.padding({ top: 12, bottom: 12 })
.borderRadius(20)
.margin({ left: 10 })
.backgroundColor('#C62828')
.onClick(() => {
this.myOrders = this.myOrders.filter((o: Session192, i: number) => i !== this.delIdx);
this.showDelDialog = false;
})
}
.width('100%')
.margin({ top: 18 })
}
.width('86%')
.padding({ left: 18, right: 18, bottom: 20 })
.borderRadius(16)
.backgroundColor('#FFFFFF')
}
@Builder
detailDialog192() {
Column() {
Column() {
Text('🏺')
.fontSize(40)
Text(this.detailIdx < this.myLots.length ? this.myLots[this.detailIdx].name : LOTS192[0].name)
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ top: 6 })
Text(this.detailIdx < this.myLots.length ? this.myLots[this.detailIdx].era + ' · ' + this.myLots[this.detailIdx].cate : LOTS192[0].era + ' · ' + LOTS192[0].cate)
.fontSize(11)
.fontColor('#E8DCC8')
.margin({ top: 4 })
}
.width('100%')
.padding({ top: 22, bottom: 18 })
.borderRadius({ topLeft: 16, topRight: 16 })
.linearGradient({
angle: 135,
colors: [['#5D4037', 0], ['#8D6E63', 1]]
})
Column() {
Row() {
Column() {
Text('估值')
.fontSize(10)
.fontColor('#8D7B6E')
Text((this.detailIdx < this.myLots.length ? this.myLots[this.detailIdx].value : LOTS192[0].value) + ' 万')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#B8860B')
.margin({ top: 4 })
}
.layoutWeight(1)
Column() {
Text('等级')
.fontSize(10)
.fontColor('#8D7B6E')
Text(this.detailIdx < this.myLots.length ? this.myLots[this.detailIdx].grade : LOTS192[0].grade)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(gradeColor192(this.detailIdx < this.myLots.length ? this.myLots[this.detailIdx].grade : LOTS192[0].grade))
.margin({ top: 4 })
}
.layoutWeight(1)
Column() {
Text('热度')
.fontSize(10)
.fontColor('#8D7B6E')
Text((this.detailIdx < this.myLots.length ? this.myLots[this.detailIdx].hot : LOTS192[0].hot) + '')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#5D4037')
.margin({ top: 4 })
}
.layoutWeight(1)
Column() {
Text('结论')
.fontSize(10)
.fontColor('#8D7B6E')
Text(this.detailIdx < this.myLots.length ? this.myLots[this.detailIdx].verdict : LOTS192[0].verdict)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(verdictColor192(this.detailIdx < this.myLots.length ? this.myLots[this.detailIdx].verdict : LOTS192[0].verdict))
.margin({ top: 4 })
}
.layoutWeight(1)
}
.width('100%')
Text('来源脉络')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
.margin({ top: 16 })
Column() {
ForEach(RECORDS192, (r: Record192) => {
Row() {
Text('●')
.fontSize(8)
.fontColor(r.color)
.margin({ top: 4 })
Column() {
Text(r.result)
.fontSize(12)
.fontColor('#3E2723')
Text(r.date)
.fontSize(9)
.fontColor('#8D7B6E')
.margin({ top: 2 })
}
.margin({ left: 8 })
.alignItems(HorizontalAlign.Start)
Text('')
.layoutWeight(1)
}
.width('100%')
.padding({ top: 6, bottom: 6 })
}, (r: Record192) => r.id)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding({ left: 18, right: 18, top: 14, bottom: 18 })
Row() {
Text('关闭')
.fontSize(14)
.fontColor('#8D7B6E')
.textAlign(TextAlign.Center)
.layoutWeight(1)
.padding({ top: 12, bottom: 12 })
.borderRadius(20)
.backgroundColor('#F1E8D8')
Text('送拍登记')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.textAlign(TextAlign.Center)
.layoutWeight(1.4)
.padding({ top: 12, bottom: 12 })
.borderRadius(20)
.margin({ left: 10 })
.linearGradient({
angle: 90,
colors: [['#D4AF37', 0], ['#B8860B', 1]]
})
}
.width('100%')
.padding({ left: 18, right: 18, bottom: 18 })
.onClick(() => {
this.showDetailDialog = false;
})
}
.width('86%')
.borderRadius(16)
.backgroundColor('#FFFFFF')
.constraintSize({ maxHeight: '85%' })
}
}
@Component
struct LiveTab192 {
onLianmai: () => void = () => {
};
@State liked: boolean[] = [false, true, false, false, true, false, false];
build() {
Column() {
Column() {
Row() {
Text('🔴')
.fontSize(14)
Text('第 128 场 · 瓷器专场')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
.margin({ left: 6 })
Text('')
.layoutWeight(1)
Text('在线 412')
.fontSize(10)
.fontColor('#8D7B6E')
}
.width('100%')
Grid() {
GridItem() {
Column() {
Text('🧑🏫')
.fontSize(28)
Text('程汝言 · 主鉴')
.fontSize(9)
.fontColor('#FFFFFF')
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#5D4037')
}
GridItem() {
Column() {
Text('🪞')
.fontSize(28)
Text('藏宝人视角')
.fontSize(9)
.fontColor('#FFFFFF')
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#8D6E63')
}
GridItem() {
Column() {
Text('🔍')
.fontSize(28)
Text('微距镜头')
.fontSize(9)
.fontColor('#FFFFFF')
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#4E342E')
}
GridItem() {
Column() {
Text('📜')
.fontSize(28)
Text('款识特写')
.fontSize(9)
.fontColor('#FFFFFF')
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#6D4C41')
}
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr')
.width('100%')
.height(210)
.borderRadius(12)
.columnsGap(2)
.rowsGap(2)
.margin({ top: 10 })
Row() {
Text('🙋')
.fontSize(18)
.width(38)
.height(38)
.textAlign(TextAlign.Center)
.borderRadius(19)
.backgroundColor('#F9EED2')
Text('🎤')
.fontSize(18)
.width(38)
.height(38)
.textAlign(TextAlign.Center)
.borderRadius(19)
.backgroundColor('#F9EED2')
.margin({ left: 8 })
Text('📷')
.fontSize(18)
.width(38)
.height(38)
.textAlign(TextAlign.Center)
.borderRadius(19)
.backgroundColor('#F9EED2')
.margin({ left: 8 })
Text('')
.layoutWeight(1)
Text('申请连麦')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 18, right: 18, top: 10, bottom: 10 })
.borderRadius(18)
.linearGradient({
angle: 90,
colors: [['#D4AF37', 0], ['#B8860B', 1]]
})
.onClick(() => {
this.onLianmai();
})
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ left: 12, right: 12, top: 12 })
Row() {
Text('现场鉴定单')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Text('')
.layoutWeight(1)
Text('本场 8 件')
.fontSize(10)
.fontColor('#8D7B6E')
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 16 })
Column() {
ForEach(LOTS192, (l: Lot192, i: number) => {
Row() {
Column() {
Text(l.cate.charAt(0))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.width(44)
.height(44)
.justifyContent(FlexAlign.Center)
.borderRadius(10)
.backgroundColor(l.color)
Column() {
Row() {
Text(l.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Text(l.grade)
.fontSize(8)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor(gradeColor192(l.grade))
.margin({ left: 6 })
Text('')
.layoutWeight(1)
Text(l.verdict)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(verdictColor192(l.verdict))
}
.width('100%')
Text(l.era + ' · ' + l.owner + ' · 估值 ' + l.value + ' 万')
.fontSize(10)
.fontColor('#8D7B6E')
.margin({ top: 4 })
Row() {
Text('')
.width('60%')
.height(3)
.borderRadius(2)
.backgroundColor('#F1E8D8')
Text('')
.layoutWeight(1)
}
.width('100%')
.margin({ top: 6 })
Row() {
Text('热度')
.fontSize(9)
.fontColor('#8D7B6E')
Text('')
.width(l.hot * 0.9)
.height(4)
.borderRadius(2)
.backgroundColor(l.color)
.margin({ left: 6 })
Text('')
.layoutWeight(1)
}
.width('100%')
.margin({ top: 4 })
}
.layoutWeight(1)
.margin({ left: 10 })
.alignItems(HorizontalAlign.Start)
Column() {
Text(i === 0 ? '鉴定中' : '已完成')
.fontSize(9)
.fontColor(i === 0 ? '#C62828' : '#00695C')
Text(this.liked[i] ? '🔥' : '👍')
.fontSize(16)
.margin({ top: 6 })
.onClick(() => {
this.liked[i] = !this.liked[i];
})
}
.margin({ left: 8 })
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor(i === 0 ? '#FBF4E4' : '#FFFFFF')
.margin({ top: 8 })
}, (l: Lot192) => l.id)
}
.width('100%')
.padding({ left: 12, right: 12 })
.margin({ top: 8 })
Row() {
Text('直播间热议')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Text('')
.layoutWeight(1)
Text('弹幕 ' + DANMAKU192.length + ' 条')
.fontSize(10)
.fontColor('#8D7B6E')
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 16 })
Column() {
ForEach(DANMAKU192, (d: Danmaku192) => {
Row() {
Text(d.user)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(d.color)
Text(d.text)
.fontSize(11)
.fontColor('#3E2723')
.margin({ left: 8 })
.layoutWeight(1)
}
.width('100%')
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.borderRadius(10)
.backgroundColor('#FFFFFF')
.margin({ top: 6 })
}, (d: Danmaku192) => d.id)
}
.width('100%')
.padding({ left: 12, right: 12 })
.margin({ top: 8 })
Column() {
Text('本场鉴定结论构成')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Row() {
ForEach(SHARE192, (s: Share192) => {
Column() {
}
.layoutWeight(s.w)
.height(10)
.backgroundColor(s.c)
}, (s: Share192) => s.label)
}
.width('100%')
.clip(true)
.borderRadius(5)
.margin({ top: 10 })
Row() {
ForEach(SHARE192, (s: Share192) => {
Row() {
Text('●')
.fontSize(8)
.fontColor(s.c)
Text(s.label + ' ' + s.w + '%')
.fontSize(9)
.fontColor('#8D7B6E')
.margin({ left: 4 })
}
.margin({ right: 10 })
}, (s: Share192) => s.label)
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ left: 12, right: 12, top: 16 })
Column() {
Text('本周每日鉴宝场次')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Row() {
ForEach(ATTEND192, (a: Attend192) => {
Column() {
Text('')
.width(16)
.height(a.cnt)
.borderRadius(3)
.backgroundColor(a.day === '六' ? '#D4AF37' : '#8D6E63')
Text(a.day)
.fontSize(9)
.fontColor('#8D7B6E')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (a: Attend192) => a.day)
}
.width('100%')
.height(110)
.alignItems(VerticalAlign.Bottom)
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ left: 12, right: 12, top: 12, bottom: 20 })
Column() {
Text('热门品类热度')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Column() {
ForEach(HOTCATES192, (h: HotCate192) => {
Row() {
Text(h.name)
.fontSize(11)
.fontColor('#3E2723')
.width(52)
Text('')
.width(h.pct + '%')
.height(8)
.borderRadius(4)
.backgroundColor(h.color)
Text('')
.layoutWeight(1)
.height(8)
.borderRadius(4)
.backgroundColor('#F1E8D8')
}
.width('100%')
.margin({ top: 8 })
}, (h: HotCate192) => h.name)
}
.width('100%')
.margin({ top: 6 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ left: 12, right: 12, bottom: 20 })
}
.width('100%')
}
}
@Component
struct CollectionTab192 {
lots: Lot192[] = [];
onEdit: (idx: number) => void = () => {
};
onDetail: (idx: number) => void = () => {
};
build() {
Column() {
Row() {
Column() {
Text('藏品 ' + this.lots.length + ' 件')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Text('估值合计 ' + collTotal192(this.lots) + ' 万')
.fontSize(10)
.fontColor('#B8860B')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
Text('')
.layoutWeight(1)
Text('🔐 已开启隐私保护')
.fontSize(10)
.fontColor('#8D7B6E')
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 12 })
Column() {
ForEach(this.lots, (l: Lot192, i: number) => {
Column() {
Row() {
Text(l.cate.charAt(0))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.width(56)
.height(56)
.textAlign(TextAlign.Center)
.borderRadius(12)
.backgroundColor(l.color)
Column() {
Row() {
Text(l.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Text(l.grade)
.fontSize(8)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor(gradeColor192(l.grade))
.margin({ left: 6 })
}
Text(l.era + ' · ' + l.cate + ' · 估值 ' + l.value + ' 万')
.fontSize(10)
.fontColor('#8D7B6E')
.margin({ top: 4 })
Row() {
Text(l.verdict)
.fontSize(9)
.fontColor(verdictColor192(l.verdict))
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('#F7F3EA')
Text('热度 ' + l.hot)
.fontSize(9)
.fontColor('#8D7B6E')
.margin({ left: 8 })
}
.margin({ top: 6 })
}
.layoutWeight(1)
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
Row() {
Text('编辑')
.fontSize(11)
.fontColor('#5D4037')
.padding({ left: 16, right: 16, top: 7, bottom: 7 })
.borderRadius(14)
.backgroundColor('#F9EED2')
.onClick(() => {
this.onEdit(i);
})
Text('详情')
.fontSize(11)
.fontColor('#8D7B6E')
.padding({ left: 16, right: 16, top: 7, bottom: 7 })
.borderRadius(14)
.backgroundColor('#F1E8D8')
.margin({ left: 8 })
.onClick(() => {
this.onDetail(i);
})
Text('')
.layoutWeight(1)
Text('送拍')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 16, right: 16, top: 7, bottom: 7 })
.borderRadius(14)
.backgroundColor('#5D4037')
.margin({ left: 8 })
}
.width('100%')
.margin({ top: 12 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ top: 10 })
}, (l: Lot192) => l.id)
}
.width('100%')
.padding({ left: 12, right: 12 })
.margin({ top: 10 })
Column() {
Text('鉴定结论速览')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Column() {
ForEach(VERDICTS192, (v: VerdictRow192) => {
Row() {
Text(v.date)
.fontSize(10)
.fontColor('#8D7B6E')
.width(44)
Column() {
Text(v.result)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(v.color)
Text(v.lot + ' · ' + v.expert)
.fontSize(9)
.fontColor('#8D7B6E')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('置信 ' + v.conf + '%')
.fontSize(9)
.fontColor('#B8860B')
}
.width('100%')
.padding({ top: 8, bottom: 8 })
.borderRadius(10)
.backgroundColor(v.conf > 92 ? '#FBF4E4' : '#FFFFFF')
.margin({ top: 6 })
}, (v: VerdictRow192) => v.id)
}
.width('100%')
.margin({ top: 6 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ left: 12, right: 12, top: 16, bottom: 20 })
}
.width('100%')
}
}
function collTotal192(lots: Lot192[]): number {
let sum: number = 0;
for (let i = 0; i < lots.length; i++) {
sum += lots[i].value;
}
return sum;
}
@Component
struct OrderTab192 {
orders: Session192[] = [];
onSend: () => void = () => {
};
onDel: (idx: number) => void = () => {
};
build() {
Column() {
Row() {
Column() {
Text('我的送鉴单')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Text('待鉴定 ' + this.orders.length + ' 单')
.fontSize(10)
.fontColor('#8D7B6E')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
Text('')
.layoutWeight(1)
Text('+ 新增送鉴')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 14, right: 14, top: 9, bottom: 9 })
.borderRadius(16)
.backgroundColor('#5D4037')
.onClick(() => {
this.onSend();
})
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 12 })
Column() {
ForEach(this.orders, (o: Session192, i: number) => {
Column() {
Row() {
Column() {
Text(o.date.split('-')[1])
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text(o.week)
.fontSize(9)
.fontColor('#E8DCC8')
}
.width(52)
.height(52)
.justifyContent(FlexAlign.Center)
.borderRadius(10)
.backgroundColor('#5D4037')
Column() {
Text(o.theme)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Text(o.time + ' · ' + o.expert + ' · 已约 ' + o.joined + '/' + o.seats)
.fontSize(10)
.fontColor('#8D7B6E')
.margin({ top: 4 })
Row() {
Text(o.state)
.fontSize(9)
.fontColor(stateColor192(o.state))
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('#F7F3EA')
}
.margin({ top: 6 })
}
.layoutWeight(1)
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('撤销')
.fontSize(10)
.fontColor('#C62828')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor('#FBE9E7')
.onClick(() => {
this.onDel(i);
})
Text('改期')
.fontSize(10)
.fontColor('#8D7B6E')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor('#F1E8D8')
.margin({ top: 6 })
}
}
.width('100%')
Row() {
Text('当前排位 #' + (i + 3))
.fontSize(9)
.fontColor('#B8860B')
Text('')
.layoutWeight(1)
Text('预计上场 ' + (20 + i * 15) + ':' + (i === 0 ? '40' : '10'))
.fontSize(9)
.fontColor('#8D7B6E')
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ top: 10 })
}, (o: Session192) => o.id)
}
.width('100%')
.padding({ left: 12, right: 12 })
.margin({ top: 10 })
Column() {
Text('送鉴须知')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Column() {
ForEach(['1. 贵重藏品建议购买运输保险并保价', '2. 连麦鉴定请提前备好微距图与款识特写', '3. 后仿品可申请复鉴一次,费用减半', '4. 直播言论仅代表行家个人学术观点'], (tip: string) => {
Row() {
Text('·')
.fontSize(12)
.fontColor('#D4AF37')
Text(tip)
.fontSize(11)
.fontColor('#8D7B6E')
.margin({ left: 6 })
}
.width('100%')
.margin({ top: 6 })
}, (tip: string) => tip)
}
.width('100%')
.margin({ top: 6 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FBF4E4')
.margin({ left: 12, right: 12, top: 16, bottom: 20 })
}
.width('100%')
}
}
@Component
struct ExpertTab192 {
build() {
Column() {
Row() {
Text('入驻行家团')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Text('')
.layoutWeight(1)
Text('共 ' + EXPERTS192.length + ' 位')
.fontSize(10)
.fontColor('#8D7B6E')
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 12 })
Column() {
ForEach(EXPERTS192, (e: Expert192) => {
Row() {
Stack() {
Text(e.emoji)
.fontSize(24)
.width(52)
.height(52)
.textAlign(TextAlign.Center)
.borderRadius(26)
.backgroundColor('#F9EED2')
Text('●')
.fontSize(10)
.fontColor(e.online ? '#4CAF50' : '#BDBDBD')
.position({ x: 38, y: 36 })
}
.width(52)
.height(52)
Column() {
Row() {
Text(e.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Text(e.field)
.fontSize(8)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor('#5D4037')
.margin({ left: 6 })
Text('')
.layoutWeight(1)
Text(e.online ? '在线' : '离线')
.fontSize(9)
.fontColor(e.online ? '#4CAF50' : '#BDBDBD')
}
.width('100%')
Text(e.title)
.fontSize(10)
.fontColor('#8D7B6E')
.margin({ top: 4 })
Row() {
Text('口碑 ' + e.rating)
.fontSize(10)
.fontColor('#B8860B')
Text('主持 ' + e.sessions + ' 场')
.fontSize(10)
.fontColor('#8D7B6E')
.margin({ left: 12 })
Text('')
.layoutWeight(1)
Text('提问')
.fontSize(10)
.fontColor('#5D4037')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('#F9EED2')
}
.width('100%')
.margin({ top: 6 })
}
.layoutWeight(1)
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ top: 10 })
}, (e: Expert192) => e.id)
}
.width('100%')
.padding({ left: 12, right: 12 })
.margin({ top: 10 })
Column() {
Text('行家连麦率')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Column() {
ForEach(EXPERTS192, (e: Expert192) => {
Row() {
Text(e.name)
.fontSize(11)
.fontColor('#3E2723')
.width(52)
Text('')
.width(e.rating * 18)
.height(8)
.borderRadius(4)
.backgroundColor('#D4AF37')
Text(e.rating + '')
.fontSize(9)
.fontColor('#8D7B6E')
.margin({ left: 8 })
Text('')
.layoutWeight(1)
}
.width('100%')
.margin({ top: 8 })
}, (e: Expert192) => e.id)
}
.width('100%')
.margin({ top: 6 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ left: 12, right: 12, top: 16, bottom: 20 })
}
.width('100%')
}
}
@Component
struct ScheduleTab192 {
onLianmai: () => void = () => {
};
build() {
Column() {
Row() {
Text('直播档期')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Text('')
.layoutWeight(1)
Text('未来 7 天')
.fontSize(10)
.fontColor('#8D7B6E')
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 12 })
Column() {
ForEach(SESSIONS192, (s: Session192) => {
Row() {
Column() {
Text(s.date.split('-')[1] + '日')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(s.state === '已满员' ? '#BDBDBD' : '#5D4037')
Text(s.week)
.fontSize(9)
.fontColor('#8D7B6E')
}
.width(48)
.alignItems(HorizontalAlign.Center)
Column() {
Text(s.theme)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Text(s.time + ' · ' + s.expert)
.fontSize(10)
.fontColor('#8D7B6E')
.margin({ top: 4 })
Row() {
Text('')
.width((s.joined / s.seats) * 100 + '%')
.height(4)
.borderRadius(2)
.backgroundColor(s.state === '已满员' ? '#C62828' : '#D4AF37')
}
.width('100%')
.margin({ top: 6 })
Text(s.joined + '/' + s.seats + ' 席位')
.fontSize(9)
.fontColor('#8D7B6E')
.margin({ top: 4 })
}
.layoutWeight(1)
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
Text(s.state === '已满员' ? '已满' : '预约')
.fontSize(11)
.fontColor(s.state === '已满员' ? '#BDBDBD' : '#FFFFFF')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(14)
.backgroundColor(s.state === '已满员' ? '#F1E8D8' : '#5D4037')
.onClick(() => {
if (s.state !== '已满员') {
this.onLianmai();
}
})
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ top: 10 })
}, (s: Session192) => s.id)
}
.width('100%')
.padding({ left: 12, right: 12 })
.margin({ top: 10 })
Column() {
Text('本周场次热度')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Row() {
ForEach(ATTEND192, (a: Attend192) => {
Column() {
Text(a.cnt + '')
.fontSize(8)
.fontColor('#8D7B6E')
Text('')
.width(16)
.height(a.cnt)
.borderRadius(3)
.backgroundColor('#5D4037')
Text(a.day)
.fontSize(9)
.fontColor('#8D7B6E')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (a: Attend192) => a.day)
}
.width('100%')
.height(130)
.alignItems(VerticalAlign.Bottom)
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ left: 12, right: 12, top: 16, bottom: 20 })
}
.width('100%')
}
}
@Component
struct MineTab192 {
records: Record192[] = [];
onDetail: () => void = () => {
};
build() {
Column() {
Column() {
Row() {
Text('🧑🎨')
.fontSize(30)
.width(60)
.height(60)
.textAlign(TextAlign.Center)
.borderRadius(30)
.backgroundColor('#4DFFFFFF')
Column() {
Text('金陵藏家·陆先生')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('藏龄 12 年 · 藏品 38 件')
.fontSize(10)
.fontColor('#E8DCC8')
.margin({ top: 4 })
}
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
Text('')
.layoutWeight(1)
Text('Lv.6 鉴宝行家')
.fontSize(10)
.fontColor('#3E2723')
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('#D4AF37')
}
.width('100%')
Row() {
Column() {
Text('128')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFD54F')
Text('参与场次')
.fontSize(9)
.fontColor('#E8DCC8')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('31')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFD54F')
Text('真品藏品')
.fontSize(9)
.fontColor('#E8DCC8')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('7')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFD54F')
Text('后仿捡漏')
.fontSize(9)
.fontColor('#E8DCC8')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('642')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFD54F')
Text('获赞')
.fontSize(9)
.fontColor('#E8DCC8')
.margin({ top: 2 })
}
.layoutWeight(1)
}
.width('100%')
.margin({ top: 16 })
}
.width('100%')
.padding(18)
.borderRadius(16)
.linearGradient({
angle: 135,
colors: [['#5D4037', 0], ['#3E2723', 1]]
})
.margin({ left: 12, right: 12, top: 12 })
Row() {
Text('我的鉴定足迹')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Text('')
.layoutWeight(1)
Text('查看藏品')
.fontSize(10)
.fontColor('#B8860B')
.onClick(() => {
this.onDetail();
})
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 16 })
Column() {
ForEach(this.records, (r: Record192) => {
Row() {
Column() {
Text('●')
.fontSize(10)
.fontColor(r.color)
Text('')
.width(2)
.layoutWeight(1)
.backgroundColor('#EFE7D8')
}
.width(16)
.height(56)
Column() {
Text(r.lot)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Text(r.result)
.fontSize(10)
.fontColor(r.color)
.margin({ top: 3 })
Text(r.date)
.fontSize(9)
.fontColor('#8D7B6E')
.margin({ top: 3 })
}
.layoutWeight(1)
.margin({ left: 10 })
.alignItems(HorizontalAlign.Start)
Text('鉴定书')
.fontSize(10)
.fontColor('#5D4037')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor('#F9EED2')
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.margin({ top: 8 })
}, (r: Record192) => r.id)
}
.width('100%')
.padding({ left: 12, right: 12 })
.margin({ top: 10 })
Column() {
Text('我的鉴宝成就')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3E2723')
Row() {
ForEach(['🏆 首件珍品', '👁️ 火眼金睛', '📜 十场连麦', '🧧 捡漏达人', '🎓 进阶学员'], (badge: string) => {
Column() {
Text(badge.split(' ')[0])
.fontSize(20)
Text(badge.split(' ')[1])
.fontSize(8)
.fontColor('#8D7B6E')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor('#F9EED2')
}, (badge: string) => badge)
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.margin({ left: 12, right: 12, top: 16, bottom: 20 })
}
.width('100%')
}
}
总结

好,今天这期节目我们拆解了一个非常完整的鉴宝直播应用。从数据模型到 UI 布局,从状态管理到弹层交互,这个应用展示了一个复杂业务场景在 ArkTS 声明式 UI 范式下的完整实现路径。最让我印象深刻的是它的"展柜"设计语言——从底部导航的金线暖光,到顶部状态栏的三段青铜渐变,到四宫格连麦窗口的同色系明度变化,整个应用在视觉上保持了高度统一的"博物馆展柜"美学。这不是随意的配色,而是业务场景(鉴宝)与视觉语言(展柜)的深度绑定。
从技术实现来看,应用采用了"状态上提 + 子组件代理"的分层架构。主组件 Index192 持有所有可变状态(myLots、myOrders 以及各种表单临时状态),六个子 Tab 组件通过构造参数接收数据和回调。数据操作严格遵循不可变更新原则——增用 concat、删用 filter、改用 map——确保 @State 响应式系统能正确检测到变化。弹层交互分为 bindSheet(抽屉式,用于日常表单)和 bindContentCover(全屏遮罩,用于关键确认),两者各司其职。
最后说一个我觉得可以改进的地方:LiveTab192 中的点赞状态 liked: boolean[] 直接在 onClick 中用 this.liked[i] = !this.liked[i] 修改元素,这在某些版本的 ArkTS 中可能不触发响应式更新。更安全的做法是用 map 生成新数组。不过整体来说,这份代码在架构设计、视觉一致性、交互分层方面都做得相当成熟,是一个非常值得学习的 HarmonyOS 应用开发范例。好,今天的节目就到这里,我们下期再见!
更多推荐




所有评论(0)