HarmonyOS 6盲盒机功能通过 Math.random() 实现了 SSR/SR/R 三档概率抽取(SSR 6%、SR 24%、R 70%),还原了二次元谷子圈“抽谷“的随机乐趣
引言

随着二次元文化在国内的蓬勃发展,“谷子”(goods 的谐音,指动漫周边商品)交易已成为一个活跃的细分市场。吧唧(徽章)、立牌、色纸、票谷等周边商品的收藏与交易需求日益旺盛,但传统交易渠道分散在闲鱼、微博超话等非专业平台,存在真伪难辨、价格不透明、交易风险高等痛点。本应用以 HarmonyOS ArkTS API 24 为技术底座,构建了一个专业的二次元周边交易平台——谷子集市,覆盖了今日上新、行情走势、求购发布、出谷寄售、订单管理和个人中心六大核心业务场景。
从技术架构层面来看,本应用采用了 ArkTS 的声明式组件体系,主入口组件 GPage 通过 @Entry 和 @Component 装饰器注册为应用根组件,内部维护 activeTab 状态变量驱动六个子组件的条件渲染。与简单的页面切换不同,每个子组件(TodayTab、MarketTab、WantedTab、SellTab、GOrderTab、GMineTab)都是功能完备的独立模块,各自管理着自己的 @State 状态集合和业务逻辑方法。这种架构不仅保证了模块间的低耦合高内聚,还使得每个 Tab 可以独立进行状态更新而互不影响。应用的视觉主题采用活力橙 #FF6A00 搭配靛蓝 #2563EB 的撞色方案,底色为 #F5F7FF 的淡蓝白色,营造出二次元特有的活力感。
在业务设计层面,应用创新性地引入了"谷子指数"概念——类似于股票指数的综合性行情指标,配合 7 日价格趋势柱状图和涨跌幅数据,将周边交易从单纯的买卖行为升级为具备投资视角的行情分析。盲盒机功能通过 Math.random() 实现了 SSR/SR/R 三档概率抽取(SSR 6%、SR 24%、R 70%),还原了二次元谷子圈"抽谷"的随机乐趣。出谷模块提供了完整的寄售管理流程,包括上架、改价、议价回复、下架等操作,其中议价功能支持一键折扣(95 折/9 折/85 折/8 折)和手动输入两种回价方式,兼顾了效率和灵活性。订单模块集成了物流轨迹时间线展示和售后申请流程,构建了完整的交易闭环。
逐段代码分析

一、数据模型与类型定义层
应用首先定义了涵盖二次元周边交易全领域的接口类型集合,每个接口对应一个业务实体。
interface IpChip {
name: string
count: number
}
interface GoodsItem {
id: number
name: string
ip: string
type: string
grade: string
price: number
originPrice: number
stock: number
sold: number
hot: boolean
color: string
}
interface PriceTrend {
day: string
price: number
}
interface MarketRow {
id: number
name: string
ip: string
lastPrice: number
change: number
deals: number
grade: string
}
interface WantedRow {
id: number
ip: string
type: string
budget: string
note: string
count: number
}
interface SellRow {
id: number
name: string
grade: string
price: number
status: string
statusColor: string
asks: number
views: number
}
上述接口定义体现了二次元周边交易领域的业务特征。GoodsItem 接口中的 grade 字段表示商品稀有度等级(SSR/SR/R),这是二次元谷子文化的核心概念。originPrice 与 price 的组合实现了划线价展示。MarketRow 接口中的 change 字段为涨跌幅百分比,正值表示上涨、负值表示下跌,配合 deals 成交量字段构成了行情分析的基础数据。WantedRow 接口定义了求购单结构,budget 采用字符串类型存储预算区间(如"¥50-80"),note 字段用于补充品相和角色要求。SellRow 接口中的 status 和 statusColor 配对使用,通过颜色区分"寄售中"“已预定”“已下架”"已售出"等状态,asks 和 views 分别记录议价条数和浏览量,为卖家提供运营数据参考。
二、静态数据与档位颜色配置

const IP_CHIPS: IpChip[] = [
{ name: '全部 IP', count: 1024 },
{ name: '时光代理人', count: 186 },
{ name: '咒术回战', count: 204 },
{ name: '排球少年', count: 168 },
{ name: '天官赐福', count: 122 },
{ name: '文豪野犬', count: 96 }
]
const TODAY_GOODS: GoodsItem[] = [
{ id: 1, name: '程小时 吧唧·雨夜', ip: '时光代理人', type: '吧唧', grade: 'SSR',
price: 68, originPrice: 128, stock: 42, sold: 1682, hot: true, color: '#FFB88C' },
{ id: 2, name: '五条悟 Q版立牌', ip: '咒术回战', type: '立牌', grade: 'SSR',
price: 88, originPrice: 158, stock: 26, sold: 2341, hot: true, color: '#A5C8F0' },
{ id: 3, name: '影山飞雄 拼图吧唧', ip: '排球少年', type: '吧唧', grade: 'SR',
price: 35, originPrice: 68, stock: 88, sold: 3210, hot: false, color: '#BFD8A8' },
{ id: 4, name: '花城 色纸·血雨探花', ip: '天官赐福', type: '色纸', grade: 'SSR',
price: 128, originPrice: 228, stock: 12, sold: 862, hot: true, color: '#E8A0A8' },
{ id: 5, name: '太宰治 亚克力钥匙扣', ip: '文豪野犬', type: '周边', grade: 'SR',
price: 42, originPrice: 78, stock: 65, sold: 1207, hot: false, color: '#C5B3E8' },
{ id: 6, name: '陆光 镭射票根', ip: '时光代理人', type: '票谷', grade: 'R',
price: 19, originPrice: 36, stock: 210, sold: 4560, hot: false, color: '#A8D8D8' }
]
const PRICE_TREND: PriceTrend[] = [
{ day: '8/18', price: 52 },
{ day: '8/19', price: 61 },
{ day: '8/20', price: 58 },
{ day: '8/21', price: 73 },
{ day: '8/22', price: 82 },
{ day: '8/23', price: 96 },
{ day: '8/24', price: 88 }
]
const GRADE_COLORS: string[] = ['#FF6A00', '#2563EB', '#8B5CF6']
IP_CHIPS 定义了 IP 分类标签,涵盖了时光代理人、咒术回战、排球少年等热门 IP。TODAY_GOODS 是今日上新页面的数据源,每条记录包含了商品名称、所属 IP、品类、稀有度等级、当前价格、原价、库存、已售数量和热度标记。color 字段为每个商品分配了一个独特的背景色值,在卡片渲染时作为商品图片占位区的背景色,实现了视觉上的快速区分。PRICE_TREND 数组提供了 7 日价格趋势数据,用于行情页的柱状图渲染。GRADE_COLORS 数组将稀有度等级映射为颜色——SSR 对应橙色 #FF6A00、SR 对应靛蓝 #2563EB、R 对应紫色 #8B5CF6,这种配色方案贯穿了整个应用的档位标识系统。
三、档位颜色函数与趋势图工具函数

function gradeColor(g: string): string {
if (g === 'SSR') {
return '#FF6A00'
}
if (g === 'SR') {
return '#2563EB'
}
return '#8B5CF6'
}
function trendBarH(p: number): number {
if (p >= 90) {
return 88
}
if (p >= 75) {
return 72
}
if (p >= 60) {
return 56
}
if (p >= 45) {
return 40
}
return 28
}
@Builder
function modalOverlay(onClose: () => void) {
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(20,24,40,0.55)')
.onClick(() => {
onClose()
})
}
gradeColor 函数是整个应用的档位颜色核心映射器,接收档位字符串返回对应的颜色值。与静态数组 GRADE_COLORS 不同,函数版本提供了更灵活的调用方式,可以直接传入任意档位字符串获取颜色。trendBarH 函数根据价格值返回柱状图高度,将价格区间映射为 28-88 像素的高度值,价格越高柱子越高。modalOverlay 是公共的模态遮罩 @Builder 函数,使用 rgba(20,24,40,0.55) 的半透明深色背景,点击时触发传入的 onClose 回调关闭弹窗。这个遮罩组件被所有弹窗复用,保证了交互行为的一致性。
四、今日谷 Tab——TodayTab 组件

今日谷 Tab 是应用的首屏页面,展示每日上新商品列表、盲盒机入口和闪购预告。
@Component
export struct TodayTab {
@State ipIdx: number = 0
@State showBuy: boolean = false
@State showLuck: boolean = false
@State showFlash: boolean = false
@State buyNum: number = 1
@State selGoods: GoodsItem = TODAY_GOODS[0]
@State luckyGrade: string = ''
build() {
Scroll() {
Column({ space: 12 }) {
Column({ space: 10 }) {
Row({ space: 10 }) {
Column({ space: 3 }) {
Text('今日吃谷日')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('每日 10:00 / 20:00 两波上新')
.fontSize(11)
.fontColor('#FFE3CF')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('盲盒机')
.fontSize(12)
.fontColor('#2563EB')
.padding({ left: 12, right: 12, top: 7, bottom: 7 })
.borderRadius(14)
.backgroundColor('#FFFFFF')
.onClick(() => {
this.showLuck = true
})
}
.width('100%')
Row({ space: 8 }) {
Column({ space: 2 }) {
Text('16:42:08')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('晚场倒计时')
.fontSize(9)
.fontColor('#FFE3CF')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 8, bottom: 8 })
.borderRadius(10)
.backgroundColor('rgba(255,255,255,0.2)')
.onClick(() => {
this.showFlash = true
})
Column({ space: 2 }) {
Text('2.6 万')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('今日吃谷人数')
.fontSize(9)
.fontColor('#FFE3CF')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 2 }) {
Text('5 折')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('晚场专区')
.fontSize(9)
.fontColor('#FFE3CF')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.showFlash = true
})
}
.width('100%')
}
.padding(14)
.borderRadius(14)
.linearGradient({
angle: 135,
colors: [['#FF6A00', 0.0], ['#2563EB', 1.0]]
})
.width('100%')
TodayTab 组件头部使用了从橙色到靛蓝的 135 度渐变背景,呼应了应用的双主色撞色主题。头部区域分为两层:上层是标题"今日吃谷日"与盲盒机入口按钮的左右布局,下层是三列等宽的统计数据栏(晚场倒计时、今日吃谷人数、晚场专区折扣)。倒计时区域使用了 rgba(255,255,255,0.2) 的半透明白色背景,点击后打开闪购预告弹窗。luckyGrade 状态变量初始为空字符串,在盲盒机弹窗中根据随机结果赋值为 ‘SSR’、‘SR’ 或 ‘R’,驱动弹窗内文案和视觉的动态变化。
五、商品瀑布流卡片与购买弹窗

Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(TODAY_GOODS, (g: GoodsItem) => {
Column({ space: 8 }) {
Stack({ }) {
Column()
.width('100%')
.height(120)
.borderRadius(10)
.backgroundColor(g.color)
Text(g.grade)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 7, right: 7, top: 3, bottom: 3 })
.borderRadius({ topLeft: 10, bottomRight: 10 })
.backgroundColor(gradeColor(g.grade))
if (g.hot) {
Text('HOT')
.fontSize(9)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 3, bottom: 3 })
.borderRadius(4)
.backgroundColor('#EF4444')
.margin(6)
}
}
.width('100%')
Column({ space: 4 }) {
Text(g.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(g.ip + ' · ' + g.type)
.fontSize(10)
.fontColor('#94A3B8')
Row() {
Text('¥' + g.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6A00')
Text('¥' + g.originPrice)
.fontSize(10)
.fontColor('#CBD5E1')
.decoration({ type: TextDecorationType.LineThrough })
Row()
.layoutWeight(1)
Text('抢')
.fontSize(12)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('#FF6A00')
.onClick(() => {
this.selGoods = g
this.showBuy = true
})
}
.width('100%')
Text('已售 ' + g.sold + ' · 库存 ' + g.stock)
.fontSize(9)
.fontColor('#94A3B8')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
}
.padding(10)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.width('48.5%')
.margin({ bottom: 10 })
}, (g: GoodsItem) => g.id.toString())
}
.width('100%')
商品列表使用 Flex 布局配合 FlexWrap.Wrap 和 SpaceBetween 实现了双列瀑布流效果。每个卡片宽度设为 48.5%,通过 margin({ bottom: 10 }) 控制垂直间距。卡片顶部的商品图片区域使用 Stack 组件实现层叠效果——底层是纯色背景块(颜色来自 g.color),上层左上角放置档位标签(使用 gradeColor 函数动态着色,并设置 borderRadius 只圆角左上和右下),右上角条件渲染 HOT 标签。价格区域同时显示当前价格(橙色加粗)和原价(灰色带删除线,通过 TextDecorationType.LineThrough 实现),划线价设计是电商场景的经典交互。点击"抢"按钮后将选中商品赋值给 selGoods 并打开购买弹窗。
六、盲盒机抽谷弹窗

if (this.showLuck) {
modalOverlay(() => {
this.showLuck = false
})
Column({ space: 14 }) {
Text('谷子盲盒机')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
Text('¥39/抽 · SSR 概率 6%')
.fontSize(12)
.fontColor('#94A3B8')
Row({ space: 12 }) {
Column({ space: 5 }) {
Column()
.width(76)
.height(76)
.borderRadius(12)
.backgroundColor(this.luckyGrade === '' ? '#FDBA74' : '#FF6A00')
Text(this.luckyGrade === '' ? '?' : this.luckyGrade)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6A00')
}
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.justifyContent(FlexAlign.Center)
Text(this.luckyGrade === 'SSR' ? '欧皇附体!抽中 SSR 吧唧!' : (this.luckyGrade === 'SR' ? '不错哦,SR 入手!' : (this.luckyGrade === 'R' ? '再接再厉~' : '点击下方按钮开始抽')))
.fontSize(12)
.fontColor('#2563EB')
Row({ space: 10 }) {
Text('抽一次 ¥39')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 9, bottom: 9 })
.borderRadius(18)
.backgroundColor('#2563EB')
.onClick(() => {
const r: number = Math.random()
if (r < 0.06) {
this.luckyGrade = 'SSR'
} else if (r < 0.3) {
this.luckyGrade = 'SR'
} else {
this.luckyGrade = 'R'
}
})
Text('收手')
.fontSize(13)
.fontColor('#FF6A00')
.padding({ left: 20, right: 20, top: 9, bottom: 9 })
.borderRadius(18)
.backgroundColor('#FFF3E8')
.onClick(() => {
this.showLuck = false
})
}
}
.padding(20)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.alignItems(HorizontalAlign.Center)
.width('84%')
}
盲盒机弹窗是今日谷 Tab 最具趣味性的功能。初始状态下,抽谷区域显示一个橙色色块和问号"?“。点击"抽一次"按钮后,通过 Math.random() 生成一个 0-1 的随机数:小于 0.06(6%概率)抽中 SSR,0.06-0.3 之间(24%概率)抽中 SR,大于 0.3(70%概率)抽中 R。抽中后,色块变为更深的橙色(#FF6A00),显示档位文字,并动态更新提示文案——SSR 显示"欧皇附体”,SR 显示"不错哦",R 显示"再接再厉"。这个概率设计忠实还原了二次元游戏中的抽卡机制,"收手"按钮的文案设计也充满了二次元圈子的自嘲幽默。
七、行情 Tab——MarketTab 组件
行情 Tab 提供了谷子指数、7 日价格趋势图和热门成交列表。
@Component
export struct MarketTab {
@State ipIdx: number = 0
@State showDeal: boolean = false
@State showTip: boolean = false
@State selRow: MarketRow = MARKET_ROWS[0]
build() {
Scroll() {
Column({ space: 12 }) {
Column({ space: 12 }) {
Row() {
Text('谷子指数 1186.4')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
Text('+2.8%')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
.backgroundColor('#EF4444')
Row()
.layoutWeight(1)
Text('7 日')
.fontSize(10)
.fontColor('#94A3B8')
}
.width('100%')
Row({ space: 12 }) {
ForEach(PRICE_TREND, (t: PriceTrend) => {
Column({ space: 4 }) {
Text('¥' + t.price)
.fontSize(8)
.fontColor(t.price >= 85 ? '#FF6A00' : '#94A3B8')
Column()
.width(20)
.height(trendBarH(t.price))
.borderRadius({ topLeft: 4, topRight: 4 })
.linearGradient({
angle: 180,
colors: t.price >= 85
? [['#FF9A4D', 0.0], ['#FF6A00', 1.0]]
: [['#93C5FD', 0.0], ['#2563EB', 1.0]]
})
Text(t.day)
.fontSize(8)
.fontColor('#94A3B8')
}
.alignItems(HorizontalAlign.Center)
}, (t: PriceTrend) => t.day)
}
.width('100%')
.padding({ top: 4 })
}
.padding(12)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.width('100%')
行情头部区域展示了"谷子指数 1186.4"和涨幅标签"+2.8%",涨幅使用红色背景标签突出显示。下方的 7 日趋势柱状图是该组件的核心可视化元素。每个柱子的高度由 trendBarH(t.price) 动态计算,颜色根据价格阈值进行条件分支——价格大于等于 85 的柱子使用橙色渐变(从 #FF9A4D 到 #FF6A00),低于 85 的使用蓝色渐变(从 #93C5FD 到 #2563EB)。柱子顶部还标注了价格数值,同样根据价格阈值变色。这种双色柱状图设计使得用户能够直观地识别高价日和低价日,价格波动趋势一目了然。热门成交列表中,每条记录的涨跌幅通过 row.change > 0 ? '#EF4444' : '#16A34A' 三元表达式决定颜色——上涨为红色、下跌为绿色,符合国内股市的配色习惯。
八、求购 Tab——WantedTab 组件与 CRUD 操作
求购 Tab 实现了求购单的完整增删改查(CRUD)操作,是该应用中业务逻辑最复杂的模块。
@Component
export struct WantedTab {
@State showPub: boolean = false
@State showEdit: boolean = false
@State showDel: boolean = false
@State editIdx: number = -1
@State delIdx: number = -1
@State inputIp: string = ''
@State inputType: string = '吧唧'
@State inputBudget: string = ''
@State inputNote: string = ''
@State typeIdx: number = 0
@State wantedList: WantedRow[] = [
{ id: 1, ip: '时光代理人', type: '吧唧', budget: '¥50-80', note: '要程小时雨夜,品相 9 成新以上', count: 2 },
{ id: 2, ip: '天官赐福', type: '色纸', budget: '¥100-150', note: '花城血雨探花,可小刀', count: 1 },
{ id: 3, ip: '排球少年', type: '立牌', budget: '¥30-60', note: '影山/及川都可,量大收', count: 4 }
]
publishWanted() {
const r: WantedRow[] = []
for (let i = 0; i < this.wantedList.length; i++) {
r.push(this.wantedList[i])
}
r.push({
id: this.wantedList.length + 1,
ip: this.inputIp === '' ? '咒术回战' : this.inputIp,
type: this.inputType,
budget: this.inputBudget === '' ? '¥40-100' : this.inputBudget,
note: this.inputNote === '' ? '长期有效,价优可秒' : this.inputNote,
count: 1
})
this.wantedList = r
this.showPub = false
}
editWanted() {
const r: WantedRow[] = []
for (let i = 0; i < this.wantedList.length; i++) {
if (i === this.editIdx) {
r.push({
id: this.wantedList[i].id,
ip: this.inputIp === '' ? this.wantedList[i].ip : this.inputIp,
type: this.inputType === '' ? this.wantedList[i].type : this.inputType,
budget: this.inputBudget === '' ? this.wantedList[i].budget : this.inputBudget,
note: this.inputNote === '' ? this.wantedList[i].note : this.inputNote,
count: this.wantedList[i].count
})
} else {
r.push(this.wantedList[i])
}
}
this.wantedList = r
this.showEdit = false
}
removeWanted() {
const r: WantedRow[] = []
for (let i = 0; i < this.wantedList.length; i++) {
if (i !== this.delIdx) {
r.push(this.wantedList[i])
}
}
this.wantedList = r
this.showDel = false
}
WantedTab 组件的 wantedList 是一个可变状态数组,初始包含 3 条求购记录。三个方法 publishWanted、editWanted、removeWanted 分别实现了求购单的新增、编辑和删除操作。这三个方法都采用了相同的不可变更新模式——先创建一个空数组 r,遍历原数组将元素复制到新数组中,在遍历过程中根据条件进行插入、替换或跳过操作,最后将新数组赋值给 this.wantedList 触发 UI 更新。这种模式虽然在代码量上略显冗长,但保证了状态更新的可预测性——每次更新都产生全新的数组引用,避免了直接修改原数组可能导致的引用比较失效问题。publishWanted 方法中还对空输入进行了默认值兜底处理,当用户未填写 IP、预算或备注时自动填充合理的默认值。
九、出谷 Tab——SellTab 组件与寄售管理
出谷 Tab 提供了完整的卖家寄售管理功能,包括上架、改价、议价回复和下架。
@Component
export struct SellTab {
@State sellList: SellRow[] = [
{ id: 1, name: '程小时 吧唧·雨夜', grade: 'SSR', price: 68, status: '寄售中', statusColor: '#FF6A00', asks: 12, views: 863 },
{ id: 2, name: '五条悟 Q版立牌', grade: 'SSR', price: 88, status: '寄售中', statusColor: '#FF6A00', asks: 23, views: 1521 },
{ id: 3, name: '影山飞雄 拼图吧唧', grade: 'SR', price: 35, status: '已预定', statusColor: '#2563EB', asks: 5, views: 402 },
{ id: 4, name: '花城 色纸·血雨探花', grade: 'SSR', price: 128, status: '已下架', statusColor: '#9CA3AF', asks: 0, views: 98 },
{ id: 5, name: '太宰治 亚克力钥匙扣', grade: 'SR', price: 42, status: '已售出', statusColor: '#16A34A', asks: 0, views: 655 },
{ id: 6, name: '陆光 镭射票根', grade: 'R', price: 19, status: '寄售中', statusColor: '#FF6A00', asks: 8, views: 231 }
]
@State showSell: boolean = false
@State showBargain: boolean = false
@State showOff: boolean = false
@State showEdit: boolean = false
@State showAsk: boolean = false
@State bargainIdx: number = -1
@State offIdx: number = -1
@State editIdx: number = -1
@State inputName: string = ''
@State inputPrice: string = ''
@State editPrice: string = ''
@State inputGrade: string = 'SSR'
@State gradeIdx: number = 0
@State typeIdx: number = 0
@State sellType: string = '吧唧'
@State bargainPrice: string = ''
applySell() {
const r: SellRow[] = []
for (let i = 0; i < this.sellList.length; i++) {
r.push(this.sellList[i])
}
r.unshift({
id: this.sellList.length + 1,
name: this.inputName === '' ? '未命名谷子' : this.inputName,
grade: this.inputGrade,
price: Number(this.inputPrice === '' ? '0' : this.inputPrice),
status: '寄售中',
statusColor: '#FF6A00',
asks: 0,
views: 0
})
this.sellList = r
this.showSell = false
this.inputName = ''
this.inputPrice = ''
}
setDiscount(rate: number) {
const p: number = this.sellList[this.bargainIdx].price
this.bargainPrice = String(Math.round(p * rate))
}
SellTab 是该应用状态最丰富的组件,拥有 16 个 @State 变量。sellList 初始包含 6 条寄售记录,涵盖四种状态。applySell 方法使用 r.unshift() 将新上架商品插入到数组头部,使得新上架的商品在列表中优先展示。上架弹窗中提供了档位选择(SSR/SR/R 三档通过 gradeIdx 控制)、品类选择(吧唧/立牌/色纸/票谷/其他周边五类通过 typeIdx 控制)和价格输入。setDiscount 方法是议价功能的核心——接收一个折扣率参数(如 0.95 表示 95 折),从当前选中商品的原价计算折后价格,使用 Math.round 进行四舍五入,并将结果转为字符串赋值给 bargainPrice 驱动输入框更新。这种"一键折扣+手动输入"的双重回价机制,既满足了快速响应的场景,也保留了精确报价的灵活性。
十、订单 Tab——GOrderTab 组件与物流轨迹
订单 Tab 提供了订单筛选、详情查看、售后申请和物流轨迹展示。
ForEach(ORDER_ROWS, (o: OrderRow, idx: number) => {
Column({ space: 10 }) {
Row({ space: 6 }) {
Text(o.seller)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.layoutWeight(1)
Text(o.status)
.fontSize(11)
.fontColor(o.statusColor)
}
.width('100%')
Row({ space: 10 }) {
Column()
.width(4)
.height(52)
.borderRadius(2)
.backgroundColor(gradeColor(o.grade))
Column({ space: 4 }) {
Text(o.goods)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text('订单号 ' + o.id)
.fontSize(10)
.fontColor('#94A3B8')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 3 }) {
Text('¥' + o.price)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6A00')
Text('x' + o.num)
.fontSize(10)
.fontColor('#94A3B8')
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row({ space: 8 }) {
Text(o.grade)
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor(gradeColor(o.grade))
Row()
.layoutWeight(1)
Text('查物流')
.fontSize(11)
.fontColor('#2563EB')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(12)
.border({ width: 1, color: '#BFDBFE' })
.onClick(() => {
this.detailIdx = idx
this.showLog = true
})
Text('售后')
.fontSize(11)
.fontColor('#EF4444')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(12)
.border({ width: 1, color: '#FECACA' })
.onClick(() => {
this.detailIdx = idx
this.reasonIdx = -1
this.showAfter = true
})
Text('详情')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('#FF6A00')
.onClick(() => {
this.detailIdx = idx
this.showDetail = true
})
}
.width('100%')
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.shadow({ radius: 4, color: 'rgba(31,41,55,0.05)', offsetX: 0, offsetY: 2 })
}, (o: OrderRow) => o.id)
订单卡片的左侧使用了一个 4 像素宽的竖条,颜色由 gradeColor(o.grade) 决定,通过这种视觉编码让用户快速识别订单商品的稀有度等级。每张卡片底部提供"查物流"“售后”"详情"三个操作按钮,分别用不同颜色区分——物流用蓝色边框、售后用红色边框、详情用橙色实底。物流轨迹弹窗使用了时间线布局,每个节点包含一个彩色圆点和状态文字,已完成节点显示橙色(#FF6A00),运输中节点显示蓝色(#2563EB),待完成节点显示灰色——这种渐进式的颜色变化清晰表达了物流进度。售后申请弹窗提供了四个原因选项(商品与描述不符、运输破损、发错货/漏发、不想要了),通过 reasonIdx 管理选中状态,提交按钮在未选中原因时显示浅红色(#FCA5A5)不可用状态,选中后变为正常红色(#EF4444),实现了表单校验的视觉反馈。
十一、我的 Tab——GMineTab 组件与提现功能
我的 Tab 展示用户信息、钱包余额、消费账单、常逛店铺,以及提现、徽章墙和设置弹窗。
Row({ space: 8 }) {
ForEach(WALLET_CARDS, (w: WalletCard) => {
Column({ space: 5 }) {
Text(w.title)
.fontSize(11)
.fontColor('#64748B')
Text(w.value)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
Text(w.desc)
.fontSize(9)
.fontColor('#FF6A00')
}
.layoutWeight(1)
.padding({ top: 12, bottom: 12 })
.borderRadius(12)
.backgroundColor('#FFFFFF')
.alignItems(HorizontalAlign.Center)
.shadow({ radius: 4, color: 'rgba(31,41,55,0.05)', offsetX: 0, offsetY: 2 })
}, (w: WalletCard) => w.id.toString())
}
.width('100%')
钱包卡片区域使用 ForEach 渲染三张卡片——可用余额、冻结中和谷子积分,每张卡片独立带阴影效果。本月吃谷账单区域使用横向进度条展示了不同品类的消费占比——吧唧占 62% 使用橙色渐变、立牌占 48% 使用蓝色渐变、色纸占 30% 使用紫色渐变、票谷占 12% 使用绿色渐变,每条进度条宽度通过百分比直接控制。提现弹窗提供了微信和银行卡两种提现渠道选择,通过 channelIdx 管理选中状态,并配有快捷金额按钮(¥100/¥500/全部),"全部"按钮会将余额完整金额"1286.50"填入输入框。头部个人卡片使用了三色渐变背景(从 #FF6A00 经过 #FF8F3C 到 #2563EB),将应用的双主色通过 140 度渐变完美融合。
十二、主页面 GPage 与底部导航
@Entry
@Component
struct GPage {
@State activeTab: number = 0
@State showSearch: boolean = false
@State showMsg: boolean = false
@State searchKw: string = ''
build() {
Column() {
Column({ space: 0 }) {
Row({ space: 10 }) {
Text('谷子集市')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Row({ space: 6 }) {
Text('🔍')
.fontSize(13)
Text('搜吧唧 / 立牌 / 色纸')
.fontSize(11)
.fontColor('#FDBA74')
.layoutWeight(1)
}
.layoutWeight(1)
.height(32)
.padding({ left: 12, right: 12 })
.borderRadius(16)
.backgroundColor('rgba(255,255,255,0.22)')
.onClick(() => {
this.showSearch = true
})
Column() {
Text('🔔')
.fontSize(16)
Column()
.width(6)
.height(6)
.borderRadius(3)
.backgroundColor('#FF3B30')
}
.onClick(() => {
this.showMsg = true
})
}
.width('100%')
.padding({ left: 14, right: 14, top: 10, bottom: 10 })
}
.width('100%')
.linearGradient({
angle: 120,
colors: [['#FF6A00', 0], ['#FF8F3C', 0.6], ['#2563EB', 1]]
})
Column() {
if (this.activeTab === 0) {
TodayTab()
} else if (this.activeTab === 1) {
MarketTab()
} else if (this.activeTab === 2) {
WantedTab()
} else if (this.activeTab === 3) {
SellTab()
} else if (this.activeTab === 4) {
GOrderTab()
} else {
GMineTab()
}
}
.layoutWeight(1)
.width('100%')
Row({ space: 0 }) {
ForEach(MAIN_TABS, (t: TabMeta, i: number) => {
Column({ space: 3 }) {
Text(t.icon)
.fontSize(18)
Text(t.name)
.fontSize(10)
.fontWeight(this.activeTab === i ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.activeTab === i ? '#FF6A00' : '#94A3B8')
if (this.activeTab === i) {
Column()
.width(16)
.height(3)
.borderRadius(2)
.backgroundColor('#FF6A00')
}
}
.layoutWeight(1)
.padding({ top: 8, bottom: 8 })
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.activeTab = i
})
}, (t: TabMeta) => t.name)
}
.width('100%')
.backgroundColor('#FFFFFF')
}
.width('100%')
.height('100%')
.backgroundColor('#F5F7FF')
}
}
主页面 GPage 的头部导航栏使用了 120 度三段式渐变(橙到浅橙到靛蓝),搜索栏使用半透明白色背景并显示橙色占位文字。消息图标的红点指示器使用了 #FF3B30 的鲜红色,比应用主色更醒目。搜索弹窗提供了历史搜索和热门搜索两个维度——历史搜索使用 Flex 布局排列标签流,热门搜索使用带排名序号的列表形式,前三名分别用红色、橙色和琥珀色标注序号。底部导航栏使用 Emoji 图标(🥞📈📢📤📋👤)替代了传统的图标字体,选中状态使用橙色 #FF6A00 配合底部指示条,与主色保持一致。
整体业务流程图
技术点对比表格
| 技术维度 | 今日谷Tab (TodayTab) | 行情Tab (MarketTab) | 求购Tab (WantedTab) | 出谷Tab (SellTab) | 订单Tab (GOrderTab) | 我的Tab (GMineTab) |
|---|---|---|---|---|---|---|
| 核心数据结构 | GoodsItem | MarketRow/PriceTrend | WantedRow | SellRow | OrderRow | WalletCard/BadgeWall |
| 状态变量数量 | 7个 | 4个 | 10个 | 16个 | 6个 | 4个 |
| 弹窗数量 | 3个(购买/盲盒/闪购) | 2个(收购/说明) | 3个(发布/编辑/删除) | 5个(上架/议价/改价/下架/攻略) | 3个(详情/售后/物流) | 3个(提现/徽章/设置) |
| CRUD操作 | 无 | 无 | 完整CRUD | 完整CRUD | 无 | 无 |
| 数据可视化 | 商品瀑布流 | 双色柱状图 | 无 | 状态色彩编码 | 物流时间线 | 消费账单进度条 |
| 交互特色 | 盲盒机随机抽取 | 谷子指数行情 | 不可变数组更新 | 一键折扣议价 | 售后原因校验 | 三色渐变头部 |
| 列表布局 | Flex双列瀑布流 | Row等宽排列 | Column垂直列表 | Column垂直列表 | Column垂直列表 | ForEach卡片/菜单 |
| 渐变效果 | 135度橙蓝渐变 | 白色卡片 | 白色卡片 | 135度橙浅橙渐变 | 白色卡片 | 140度三色渐变 |
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

设置API为24的模板项目:

初始化项目,自动下载相关依赖:

完整代码:
// 主题:活力橙 #FF6A00 × 靛蓝 #2563EB,底色 #F5F7FF,二次元撞色风
// 6 个底部 Tab:今日谷 / 行情 / 求购 / 出谷 / 订单 / 我的
interface IpChip {
name: string
count: number
}
interface GoodsItem {
id: number
name: string
ip: string
type: string
grade: string
price: number
originPrice: number
stock: number
sold: number
hot: boolean
color: string
}
interface PriceTrend {
day: string
price: number
}
interface MarketRow {
id: number
name: string
ip: string
lastPrice: number
change: number
deals: number
grade: string
}
interface WantedRow {
id: number
ip: string
type: string
budget: string
note: string
count: number
}
interface SellRow {
id: number
name: string
grade: string
price: number
status: string
statusColor: string
asks: number
views: number
}
interface OrderRow {
id: string
goods: string
grade: string
price: number
num: number
status: string
statusColor: string
seller: string
}
interface WalletCard {
id: number
title: string
value: string
desc: string
}
interface BadgeWall {
id: number
name: string
lit: boolean
color: string
}
interface TabMeta {
name: string
icon: string
}
interface MsgMeta {
id: number
title: string
content: string
time: string
read: boolean
}
const IP_CHIPS: IpChip[] = [
{ name: '全部 IP', count: 1024 },
{ name: '时光代理人', count: 186 },
{ name: '咒术回战', count: 204 },
{ name: '排球少年', count: 168 },
{ name: '天官赐福', count: 122 },
{ name: '文豪野犬', count: 96 }
]
const TODAY_GOODS: GoodsItem[] = [
{ id: 1, name: '程小时 吧唧·雨夜', ip: '时光代理人', type: '吧唧', grade: 'SSR', price: 68, originPrice: 128, stock: 42, sold: 1682, hot: true, color: '#FFB88C' },
{ id: 2, name: '五条悟 Q版立牌', ip: '咒术回战', type: '立牌', grade: 'SSR', price: 88, originPrice: 158, stock: 26, sold: 2341, hot: true, color: '#A5C8F0' },
{ id: 3, name: '影山飞雄 拼图吧唧', ip: '排球少年', type: '吧唧', grade: 'SR', price: 35, originPrice: 68, stock: 88, sold: 3210, hot: false, color: '#BFD8A8' },
{ id: 4, name: '花城 色纸·血雨探花', ip: '天官赐福', type: '色纸', grade: 'SSR', price: 128, originPrice: 228, stock: 12, sold: 862, hot: true, color: '#E8A0A8' },
{ id: 5, name: '太宰治 亚克力钥匙扣', ip: '文豪野犬', type: '周边', grade: 'SR', price: 42, originPrice: 78, stock: 65, sold: 1207, hot: false, color: '#C5B3E8' },
{ id: 6, name: '陆光 镭射票根', ip: '时光代理人', type: '票谷', grade: 'R', price: 19, originPrice: 36, stock: 210, sold: 4560, hot: false, color: '#A8D8D8' }
]
const PRICE_TREND: PriceTrend[] = [
{ day: '8/18', price: 52 },
{ day: '8/19', price: 61 },
{ day: '8/20', price: 58 },
{ day: '8/21', price: 73 },
{ day: '8/22', price: 82 },
{ day: '8/23', price: 96 },
{ day: '8/24', price: 88 }
]
const MARKET_ROWS: MarketRow[] = [
{ id: 1, name: '程小时 吧唧·雨夜', ip: '时光代理人', lastPrice: 68, change: 12.5, deals: 326, grade: 'SSR' },
{ id: 2, name: '五条悟 Q版立牌', ip: '咒术回战', lastPrice: 88, change: 8.2, deals: 541, grade: 'SSR' },
{ id: 3, name: '花城 色纸·血雨探花', ip: '天官赐福', lastPrice: 128, change: -4.8, deals: 189, grade: 'SSR' },
{ id: 4, name: '影山飞雄 拼图吧唧', ip: '排球少年', lastPrice: 35, change: 3.1, deals: 762, grade: 'SR' },
{ id: 5, name: '太宰治 亚克力钥匙扣', ip: '文豪野犬', lastPrice: 42, change: -1.5, deals: 298, grade: 'SR' },
{ id: 6, name: '陆光 镭射票根', ip: '时光代理人', lastPrice: 19, change: 6.9, deals: 1103, grade: 'R' }
]
const GRADE_COLORS: string[] = ['#FF6A00', '#2563EB', '#8B5CF6']
function gradeColor(g: string): string {
if (g === 'SSR') {
return '#FF6A00'
}
if (g === 'SR') {
return '#2563EB'
}
return '#8B5CF6'
}
function trendBarH(p: number): number {
if (p >= 90) {
return 88
}
if (p >= 75) {
return 72
}
if (p >= 60) {
return 56
}
if (p >= 45) {
return 40
}
return 28
}
@Builder
function modalOverlay(onClose: () => void) {
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(20,24,40,0.55)')
.onClick(() => {
onClose()
})
}
// ==================== Tab 1 今日谷 ====================
@Component
export struct TodayTab {
@State ipIdx: number = 0
@State showBuy: boolean = false
@State showLuck: boolean = false
@State showFlash: boolean = false
@State buyNum: number = 1
@State selGoods: GoodsItem = TODAY_GOODS[0]
@State luckyGrade: string = ''
build() {
Scroll() {
Column({ space: 12 }) {
Column({ space: 10 }) {
Row({ space: 10 }) {
Column({ space: 3 }) {
Text('今日吃谷日')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('每日 10:00 / 20:00 两波上新')
.fontSize(11)
.fontColor('#FFE3CF')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('盲盒机')
.fontSize(12)
.fontColor('#2563EB')
.padding({ left: 12, right: 12, top: 7, bottom: 7 })
.borderRadius(14)
.backgroundColor('#FFFFFF')
.onClick(() => {
this.showLuck = true
})
}
.width('100%')
Row({ space: 8 }) {
Column({ space: 2 }) {
Text('16:42:08')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('晚场倒计时')
.fontSize(9)
.fontColor('#FFE3CF')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 8, bottom: 8 })
.borderRadius(10)
.backgroundColor('rgba(255,255,255,0.2)')
.onClick(() => {
this.showFlash = true
})
Column({ space: 2 }) {
Text('2.6 万')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('今日吃谷人数')
.fontSize(9)
.fontColor('#FFE3CF')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 2 }) {
Text('5 折')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('晚场专区')
.fontSize(9)
.fontColor('#FFE3CF')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.showFlash = true
})
}
.width('100%')
}
.padding(14)
.borderRadius(14)
.linearGradient({
angle: 135,
colors: [['#FF6A00', 0.0], ['#2563EB', 1.0]]
})
.width('100%')
Scroll() {
Row({ space: 8 }) {
ForEach(IP_CHIPS, (chip: IpChip) => {
Text(chip.name + ' ' + chip.count)
.fontSize(12)
.fontColor(this.ipIdx === IP_CHIPS.indexOf(chip) ? '#FFFFFF' : '#2563EB')
.padding({ left: 13, right: 13, top: 7, bottom: 7 })
.borderRadius(15)
.backgroundColor(this.ipIdx === IP_CHIPS.indexOf(chip) ? '#2563EB' : '#FFFFFF')
.onClick(() => {
this.ipIdx = IP_CHIPS.indexOf(chip)
})
}, (chip: IpChip) => chip.name)
}
}
.scrollable(ScrollDirection.Horizontal)
.width('100%')
Row() {
Text('今日上新 6 款')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
Row()
.layoutWeight(1)
Text('真伪包赔 · 拆封可退')
.fontSize(10)
.fontColor('#94A3B8')
}
.width('100%')
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(TODAY_GOODS, (g: GoodsItem) => {
Column({ space: 8 }) {
Stack({ }) {
Column()
.width('100%')
.height(120)
.borderRadius(10)
.backgroundColor(g.color)
Text(g.grade)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 7, right: 7, top: 3, bottom: 3 })
.borderRadius({ topLeft: 10, bottomRight: 10 })
.backgroundColor(gradeColor(g.grade))
if (g.hot) {
Text('HOT')
.fontSize(9)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 3, bottom: 3 })
.borderRadius(4)
.backgroundColor('#EF4444')
.margin(6)
}
}
.width('100%')
Column({ space: 4 }) {
Text(g.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(g.ip + ' · ' + g.type)
.fontSize(10)
.fontColor('#94A3B8')
Row() {
Text('¥' + g.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6A00')
Text('¥' + g.originPrice)
.fontSize(10)
.fontColor('#CBD5E1')
.decoration({ type: TextDecorationType.LineThrough })
Row()
.layoutWeight(1)
Text('抢')
.fontSize(12)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('#FF6A00')
.onClick(() => {
this.selGoods = g
this.showBuy = true
})
}
.width('100%')
Text('已售 ' + g.sold + ' · 库存 ' + g.stock)
.fontSize(9)
.fontColor('#94A3B8')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
}
.padding(10)
.borderRadius(12)
.backgroundColor('#FFFFFF')
.width('48.5%')
.margin({ bottom: 10 })
}, (g: GoodsItem) => g.id.toString())
}
.width('100%')
if (this.showBuy) {
modalOverlay(() => {
this.showBuy = false
})
Column({ space: 14 }) {
Row() {
Text('购买确认')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
Row()
.layoutWeight(1)
Text('×')
.fontSize(18)
.fontColor('#94A3B8')
.onClick(() => {
this.showBuy = false
})
}
.width('100%')
Row({ space: 10 }) {
Column()
.width(64)
.height(80)
.borderRadius(10)
.backgroundColor(this.selGoods.color)
Column({ space: 4 }) {
Text(this.selGoods.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
Text(this.selGoods.ip + ' · ' + this.selGoods.grade + ' 级')
.fontSize(11)
.fontColor('#94A3B8')
Text('每单限购 3 件 · 顺丰包邮')
.fontSize(10)
.fontColor('#2563EB')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Row() {
Text('数量')
.fontSize(13)
.fontColor('#475569')
Row()
.layoutWeight(1)
Row({ space: 0 }) {
Text('−')
.fontSize(16)
.fontColor(this.buyNum <= 1 ? '#E2E8F0' : '#FF6A00')
.padding(8)
.onClick(() => {
if (this.buyNum > 1) {
this.buyNum--
}
})
Text(this.buyNum.toString())
.fontSize(13)
.fontColor('#1F2937')
.padding({ left: 12, right: 12 })
Text('+')
.fontSize(16)
.fontColor(this.buyNum >= 3 ? '#E2E8F0' : '#FF6A00')
.padding(8)
.onClick(() => {
if (this.buyNum < 3) {
this.buyNum++
}
})
}
.borderRadius(8)
.backgroundColor('#F1F5F9')
}
.width('100%')
Row() {
Column({ space: 1 }) {
Text('合计')
.fontSize(11)
.fontColor('#94A3B8')
Text('¥' + (this.selGoods.price * this.buyNum))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6A00')
}
.alignItems(HorizontalAlign.Start)
Row()
.layoutWeight(1)
Text('立即支付')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 22, right: 22, top: 9, bottom: 9 })
.borderRadius(18)
.linearGradient({
angle: 90,
colors: [['#FF6A00', 0.0], ['#FF9A4D', 1.0]]
})
.onClick(() => {
this.showBuy = false
})
}
.width('100%')
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.width('92%')
}
if (this.showLuck) {
modalOverlay(() => {
this.showLuck = false
})
Column({ space: 14 }) {
Text('谷子盲盒机')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
Text('¥39/抽 · SSR 概率 6%')
.fontSize(12)
.fontColor('#94A3B8')
Row({ space: 12 }) {
Column({ space: 5 }) {
Column()
.width(76)
.height(76)
.borderRadius(12)
.backgroundColor(this.luckyGrade === '' ? '#FDBA74' : '#FF6A00')
Text(this.luckyGrade === '' ? '?' : this.luckyGrade)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6A00')
}
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.justifyContent(FlexAlign.Center)
Text(this.luckyGrade === 'SSR' ? '欧皇附体!抽中 SSR 吧唧!' : (this.luckyGrade === 'SR' ? '不错哦,SR 入手!' : (this.luckyGrade === 'R' ? '再接再厉~' : '点击下方按钮开始抽')))
.fontSize(12)
.fontColor('#2563EB')
Row({ space: 10 }) {
Text('抽一次 ¥39')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 9, bottom: 9 })
.borderRadius(18)
.backgroundColor('#2563EB')
.onClick(() => {
const r: number = Math.random()
if (r < 0.06) {
this.luckyGrade = 'SSR'
} else if (r < 0.3) {
this.luckyGrade = 'SR'
} else {
this.luckyGrade = 'R'
}
})
Text('收手')
.fontSize(13)
.fontColor('#FF6A00')
.padding({ left: 20, right: 20, top: 9, bottom: 9 })
.borderRadius(18)
.backgroundColor('#FFF3E8')
.onClick(() => {
this.showLuck = false
})
}
}
.padding(20)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.alignItems(HorizontalAlign.Center)
.width('84%')
}
if (this.showFlash) {
modalOverlay(() => {
this.showFlash = false
})
Column({ space: 10 }) {
Text('晚场 20:00 闪购预告')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
ForEach(['夏油杰 吧唧·白衣 · 5 折 ¥59', '谢怜 色纸·花城 ×2 · 7 折 ¥96', '中原中也 立牌·Q 版 · 6 折 ¥52'], (t: string) => {
Text('· ' + t)
.fontSize(12)
.fontColor('#475569')
.width('100%')
}, (t: string) => t)
Text('开抢提醒已开启')
.fontSize(11)
.fontColor('#2563EB')
Text('好的')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 26, right: 26, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#FF6A00')
.onClick(() => {
this.showFlash = false
})
}
.padding(20)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.width('84%')
}
}
.padding(12)
}
.backgroundColor('#F5F7FF')
.layoutWeight(1)
}
}
// ==================== Tab 2 行情 ====================
@Component
export struct MarketTab {
@State ipIdx: number = 0
@State showDeal: boolean = false
@State showTip: boolean = false
@State selRow: MarketRow = MARKET_ROWS[0]
build() {
Scroll() {
Column({ space: 12 }) {
Column({ space: 12 }) {
Row() {
Text('谷子指数 1186.4')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
Text('+2.8%')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
.backgroundColor('#EF4444')
Row()
.layoutWeight(1)
Text('7 日')
.fontSize(10)
.fontColor('#94A3B8')
}
.width('100%')
Row({ space: 12 }) {
ForEach(PRICE_TREND, (t: PriceTrend) => {
Column({ space: 4 }) {
Text('¥' + t.price)
.fontSize(8)
.fontColor(t.price >= 85 ? '#FF6A00' : '#94A3B8')
Column()
.width(20)
.height(trendBarH(t.price))
.borderRadius({ topLeft: 4, topRight: 4 })
.linearGradient({
angle: 180,
colors: t.price >= 85
? [['#FF9A4D', 0.0], ['#FF6A00', 1.0]]
: [['#93C5FD', 0.0], ['#2563EB', 1.0]]
})
Text(t.day)
.fontSize(8)
.fontColor('#94A3B8')
}
.alignItems(HorizontalAlign.Center)
}, (t: PriceTrend) => t.day)
}
.width('100%')
.padding({ top: 4 })
}
.padding(12)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.width('100%')
Row({ space: 8 }) {
ForEach(IP_CHIPS, (chip: IpChip) => {
Text(chip.name)
.fontSize(11)
.fontColor(this.ipIdx === IP_CHIPS.indexOf(chip) ? '#FFFFFF' : '#475569')
.padding({ left: 11, right: 11, top: 6, bottom: 6 })
.borderRadius(13)
.backgroundColor(this.ipIdx === IP_CHIPS.indexOf(chip) ? '#FF6A00' : '#FFFFFF')
.onClick(() => {
this.ipIdx = IP_CHIPS.indexOf(chip)
})
}, (chip: IpChip) => chip.name)
}
.width('100%')
Row() {
Text('热门成交')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
Row()
.layoutWeight(1)
Text('行情说明')
.fontSize(11)
.fontColor('#2563EB')
.onClick(() => {
this.showTip = true
})
}
.width('100%')
ForEach(MARKET_ROWS, (row: MarketRow) => {
Row({ space: 12 }) {
Column()
.width(52)
.height(52)
.borderRadius(10)
.backgroundColor(gradeColor(row.grade))
Column({ space: 4 }) {
Row({ space: 6 }) {
Text(row.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(row.grade)
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(3)
.backgroundColor(gradeColor(row.grade))
}
Text(row.ip)
.fontSize(11)
.fontColor('#94A3B8')
Text('近 7 日成交 ' + row.deals + ' 笔')
.fontSize(10)
.fontColor('#94A3B8')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 4 }) {
Text('¥' + row.lastPrice)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(row.change > 0 ? '#EF4444' : '#16A34A')
Text((row.change > 0 ? '↑ +' : '↓ ') + row.change + '%')
.fontSize(11)
.fontColor(row.change > 0 ? '#EF4444' : '#16A34A')
Text('收购')
.fontSize(10)
.fontColor('#2563EB')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(9)
.backgroundColor('#EAF1FE')
.onClick(() => {
this.selRow = row
this.showDeal = true
})
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
}, (row: MarketRow) => row.id.toString())
if (this.showDeal) {
modalOverlay(() => {
this.showDeal = false
})
Column({ space: 12 }) {
Text('收购出价')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.width('100%')
Text('「' + this.selRow.name + '」当前 ¥' + this.selRow.lastPrice)
.fontSize(12)
.fontColor('#475569')
.width('100%')
Row({ space: 8 }) {
ForEach(['¥' + this.selRow.lastPrice, '+5%', '+10%', 'Custom'], (b: string) => {
Text(b)
.fontSize(12)
.fontColor('#2563EB')
.padding({ left: 12, right: 12, top: 7, bottom: 7 })
.borderRadius(10)
.backgroundColor('#EAF1FE')
}, (b: string) => b)
}
.width('100%')
Text('· 收购单 7 天内有效,卖家接单即锁定\n· 平台担保交易,假一赔三')
.fontSize(11)
.fontColor('#94A3B8')
.lineHeight(18)
.width('100%')
Text('发布收购')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 0, right: 0, top: 9, bottom: 9 })
.borderRadius(18)
.linearGradient({
angle: 90,
colors: [['#2563EB', 0.0], ['#FF6A00', 1.0]]
})
.width('100%')
.textAlign(TextAlign.Center)
.onClick(() => {
this.showDeal = false
})
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.width('88%')
}
if (this.showTip) {
modalOverlay(() => {
this.showTip = false
})
Column({ space: 10 }) {
Text('行情说明')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
Text('1. 行情价 = 近 7 日平台真实成交均价\n2. 涨跌幅对比昨日行情价\n3. 成交量仅统计担保交易订单\n4. 恶意炒价订单会被剔除')
.fontSize(12)
.fontColor('#475569')
.lineHeight(20)
Text('知道了')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 26, right: 26, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#2563EB')
.onClick(() => {
this.showTip = false
})
}
.padding(20)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.width('84%')
}
}
.padding(12)
}
.backgroundColor('#F5F7FF')
.layoutWeight(1)
}
}
// ==================== Tab 3 求购 ====================
@Component
export struct WantedTab {
@State showPub: boolean = false
@State showEdit: boolean = false
@State showDel: boolean = false
@State editIdx: number = -1
@State delIdx: number = -1
@State inputIp: string = ''
@State inputType: string = '吧唧'
@State inputBudget: string = ''
@State inputNote: string = ''
@State typeIdx: number = 0
@State wantedList: WantedRow[] = [
{ id: 1, ip: '时光代理人', type: '吧唧', budget: '¥50-80', note: '要程小时雨夜,品相 9 成新以上', count: 2 },
{ id: 2, ip: '天官赐福', type: '色纸', budget: '¥100-150', note: '花城血雨探花,可小刀', count: 1 },
{ id: 3, ip: '排球少年', type: '立牌', budget: '¥30-60', note: '影山/及川都可,量大收', count: 4 }
]
publishWanted() {
const r: WantedRow[] = []
for (let i = 0; i < this.wantedList.length; i++) {
r.push(this.wantedList[i])
}
r.push({
id: this.wantedList.length + 1,
ip: this.inputIp === '' ? '咒术回战' : this.inputIp,
type: this.inputType,
budget: this.inputBudget === '' ? '¥40-100' : this.inputBudget,
note: this.inputNote === '' ? '长期有效,价优可秒' : this.inputNote,
count: 1
})
this.wantedList = r
this.showPub = false
}
editWanted() {
const r: WantedRow[] = []
for (let i = 0; i < this.wantedList.length; i++) {
if (i === this.editIdx) {
r.push({
id: this.wantedList[i].id,
ip: this.inputIp === '' ? this.wantedList[i].ip : this.inputIp,
type: this.inputType === '' ? this.wantedList[i].type : this.inputType,
budget: this.inputBudget === '' ? this.wantedList[i].budget : this.inputBudget,
note: this.inputNote === '' ? this.wantedList[i].note : this.inputNote,
count: this.wantedList[i].count
})
} else {
r.push(this.wantedList[i])
}
}
this.wantedList = r
this.showEdit = false
}
removeWanted() {
const r: WantedRow[] = []
for (let i = 0; i < this.wantedList.length; i++) {
if (i !== this.delIdx) {
r.push(this.wantedList[i])
}
}
this.wantedList = r
this.showDel = false
}
build() {
Scroll() {
Column({ space: 12 }) {
Row({ space: 10 }) {
Column({ space: 3 }) {
Text('我的求购')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
Text('发布求购让卖家找上门')
.fontSize(10)
.fontColor('#94A3B8')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('发布求购')
.fontSize(12)
.fontColor('#FFFFFF')
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.borderRadius(14)
.backgroundColor('#FF6A00')
.onClick(() => {
this.inputIp = ''
this.inputType = '吧唧'
this.inputBudget = ''
this.inputNote = ''
this.showPub = true
})
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
ForEach(this.wantedList, (w: WantedRow, idx: number) => {
Column({ space: 10 }) {
Row({ space: 8 }) {
Text(w.ip)
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(6)
.backgroundColor('#2563EB')
Text(w.type)
.fontSize(11)
.fontColor('#FF6A00')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(6)
.backgroundColor('#FFF3E8')
Row()
.layoutWeight(1)
Text('求 ' + w.count + ' 件')
.fontSize(11)
.fontColor('#94A3B8')
}
.width('100%')
Text(w.note)
.fontSize(12)
.fontColor('#475569')
.width('100%')
Row() {
Text('预算 ' + w.budget)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6A00')
Row()
.layoutWeight(1)
Text('编辑')
.fontSize(11)
.fontColor('#2563EB')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(10)
.border({ width: 1, color: '#2563EB' })
.onClick(() => {
this.editIdx = idx
this.inputIp = w.ip
this.inputType = w.type
this.inputBudget = w.budget
this.inputNote = w.note
this.showEdit = true
})
Text('删除')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(10)
.backgroundColor('#EF4444')
.onClick(() => {
this.delIdx = idx
this.showDel = true
})
}
.width('100%')
}
.padding(12)
.borderRadius(12)
.backgroundColor('#FFFFFF')
}, (w: WantedRow) => ('w' + w.id).toString())
Text('· 求购单 30 天有效,可随时关闭\n· 有卖家接单会第一时间推送')
.fontSize(10)
.fontColor('#94A3B8')
.lineHeight(16)
.width('100%')
if (this.showPub) {
modalOverlay(() => {
this.showPub = false
})
Column({ space: 12 }) {
Text('发布求购')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.width('100%')
TextInput({ placeholder: 'IP 名称(如:时光代理人)', text: this.inputIp })
.fontSize(12)
.height(38)
.borderRadius(10)
.backgroundColor('#F1F5F9')
.onChange((v: string) => {
this.inputIp = v
})
.width('100%')
Column({ space: 8 }) {
Text('谷子类型')
.fontSize(12)
.fontColor('#94A3B8')
.width('100%')
Row({ space: 8 }) {
ForEach(['吧唧', '立牌', '色纸', '票谷', '其他'], (t: string) => {
Text(t)
.fontSize(12)
.fontColor(this.typeIdx === ['吧唧', '立牌', '色纸', '票谷', '其他'].indexOf(t) ? '#FFFFFF' : '#475569')
.padding({ left: 13, right: 13, top: 6, bottom: 6 })
.borderRadius(10)
.backgroundColor(this.typeIdx === ['吧唧', '立牌', '色纸', '票谷', '其他'].indexOf(t) ? '#FF6A00' : '#F1F5F9')
.onClick(() => {
this.typeIdx = ['吧唧', '立牌', '色纸', '票谷', '其他'].indexOf(t)
this.inputType = t
})
}, (t: string) => t)
}
}
.width('100%')
TextInput({ placeholder: '预算区间(如:¥50-80)', text: this.inputBudget })
.fontSize(12)
.height(38)
.borderRadius(10)
.backgroundColor('#F1F5F9')
.onChange((v: string) => {
this.inputBudget = v
})
.width('100%')
TextInput({ placeholder: '补充说明(品相/角色要求)', text: this.inputNote })
.fontSize(12)
.height(38)
.borderRadius(10)
.backgroundColor('#F1F5F9')
.onChange((v: string) => {
this.inputNote = v
})
.width('100%')
Text('发布')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 0, right: 0, top: 9, bottom: 9 })
.borderRadius(18)
.backgroundColor('#FF6A00')
.width('100%')
.textAlign(TextAlign.Center)
.onClick(() => {
this.publishWanted()
})
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.constraintSize({ maxHeight: '80%' })
.width('92%')
}
if (this.showEdit) {
modalOverlay(() => {
this.showEdit = false
})
Column({ space: 12 }) {
Text('编辑求购')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.width('100%')
TextInput({ placeholder: 'IP 名称', text: this.inputIp })
.fontSize(12)
.height(38)
.borderRadius(10)
.backgroundColor('#F1F5F9')
.onChange((v: string) => {
this.inputIp = v
})
.width('100%')
TextInput({ placeholder: '预算区间', text: this.inputBudget })
.fontSize(12)
.height(38)
.borderRadius(10)
.backgroundColor('#F1F5F9')
.onChange((v: string) => {
this.inputBudget = v
})
.width('100%')
TextInput({ placeholder: '补充说明', text: this.inputNote })
.fontSize(12)
.height(38)
.borderRadius(10)
.backgroundColor('#F1F5F9')
.onChange((v: string) => {
this.inputNote = v
})
.width('100%')
Row({ space: 10 }) {
Text('取消')
.fontSize(13)
.fontColor('#475569')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#F1F5F9')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showEdit = false
})
Text('保存')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#2563EB')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.editWanted()
})
}
.width('100%')
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.constraintSize({ maxHeight: '80%' })
.width('92%')
}
if (this.showDel) {
modalOverlay(() => {
this.showDel = false
})
Column({ space: 12 }) {
Text('删除求购单')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
Text('删除后已接单的卖家将收到通知,确认删除?')
.fontSize(12)
.fontColor('#94A3B8')
.textAlign(TextAlign.Center)
Row({ space: 10 }) {
Text('取消')
.fontSize(13)
.fontColor('#475569')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#F1F5F9')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showDel = false
})
Text('删除')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#EF4444')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.removeWanted()
})
}
.width('100%')
}
.padding(18)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.width('84%')
}
}
.padding(12)
}
.backgroundColor('#F5F7FF')
.layoutWeight(1)
}
}
// ==================== Tab 4 出谷 ====================
@Component
export struct SellTab {
@State sellList: SellRow[] = [
{ id: 1, name: '程小时 吧唧·雨夜', grade: 'SSR', price: 68, status: '寄售中', statusColor: '#FF6A00', asks: 12, views: 863 },
{ id: 2, name: '五条悟 Q版立牌', grade: 'SSR', price: 88, status: '寄售中', statusColor: '#FF6A00', asks: 23, views: 1521 },
{ id: 3, name: '影山飞雄 拼图吧唧', grade: 'SR', price: 35, status: '已预定', statusColor: '#2563EB', asks: 5, views: 402 },
{ id: 4, name: '花城 色纸·血雨探花', grade: 'SSR', price: 128, status: '已下架', statusColor: '#9CA3AF', asks: 0, views: 98 },
{ id: 5, name: '太宰治 亚克力钥匙扣', grade: 'SR', price: 42, status: '已售出', statusColor: '#16A34A', asks: 0, views: 655 },
{ id: 6, name: '陆光 镭射票根', grade: 'R', price: 19, status: '寄售中', statusColor: '#FF6A00', asks: 8, views: 231 }
]
@State showSell: boolean = false
@State showBargain: boolean = false
@State showOff: boolean = false
@State showEdit: boolean = false
@State showAsk: boolean = false
@State bargainIdx: number = -1
@State offIdx: number = -1
@State editIdx: number = -1
@State inputName: string = ''
@State inputPrice: string = ''
@State editPrice: string = ''
@State inputGrade: string = 'SSR'
@State gradeIdx: number = 0
@State typeIdx: number = 0
@State sellType: string = '吧唧'
@State bargainPrice: string = ''
applySell() {
const r: SellRow[] = []
for (let i = 0; i < this.sellList.length; i++) {
r.push(this.sellList[i])
}
r.unshift({
id: this.sellList.length + 1,
name: this.inputName === '' ? '未命名谷子' : this.inputName,
grade: this.inputGrade,
price: Number(this.inputPrice === '' ? '0' : this.inputPrice),
status: '寄售中',
statusColor: '#FF6A00',
asks: 0,
views: 0
})
this.sellList = r
this.showSell = false
this.inputName = ''
this.inputPrice = ''
}
applyOff() {
const r: SellRow[] = []
for (let i = 0; i < this.sellList.length; i++) {
if (i === this.offIdx) {
r.push({
id: this.sellList[i].id,
name: this.sellList[i].name,
grade: this.sellList[i].grade,
price: this.sellList[i].price,
status: '已下架',
statusColor: '#9CA3AF',
asks: this.sellList[i].asks,
views: this.sellList[i].views
})
} else {
r.push(this.sellList[i])
}
}
this.sellList = r
this.showOff = false
}
applyEdit() {
const r: SellRow[] = []
for (let i = 0; i < this.sellList.length; i++) {
if (i === this.editIdx) {
r.push({
id: this.sellList[i].id,
name: this.sellList[i].name,
grade: this.sellList[i].grade,
price: Number(this.editPrice === '' ? '0' : this.editPrice),
status: this.sellList[i].status,
statusColor: this.sellList[i].statusColor,
asks: this.sellList[i].asks,
views: this.sellList[i].views
})
} else {
r.push(this.sellList[i])
}
}
this.sellList = r
this.showEdit = false
}
setDiscount(rate: number) {
const p: number = this.sellList[this.bargainIdx].price
this.bargainPrice = String(Math.round(p * rate))
}
build() {
Scroll() {
Column({ space: 12 }) {
Column({ space: 10 }) {
Row({ space: 0 }) {
Column({ space: 3 }) {
Text('4')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('在售中')
.fontSize(10)
.fontColor('#FFE3CF')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 3 }) {
Text('1')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('已售出')
.fontSize(10)
.fontColor('#FFE3CF')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 3 }) {
Text('3870')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('总浏览')
.fontSize(10)
.fontColor('#FFE3CF')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
Row({ space: 8 }) {
Text('+ 上架好谷')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6A00')
.padding({ left: 18, right: 18, top: 9, bottom: 9 })
.borderRadius(18)
.backgroundColor('#FFFFFF')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showSell = true
})
Text('出谷攻略')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 18, right: 18, top: 9, bottom: 9 })
.borderRadius(18)
.backgroundColor('rgba(255,255,255,0.25)')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showAsk = true
})
}
.width('100%')
}
.padding(14)
.borderRadius(16)
.linearGradient({
angle: 135,
colors: [['#FF6A00', 0], ['#FF9A44', 1]]
})
.shadow({ radius: 12, color: 'rgba(255,106,0,0.28)', offsetX: 0, offsetY: 5 })
.width('100%')
Row({ space: 6 }) {
Text('我的谷柜')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.layoutWeight(1)
Text('共 ' + this.sellList.length + ' 件')
.fontSize(11)
.fontColor('#6B7280')
}
.width('100%')
.padding({ left: 4, right: 4 })
ForEach(this.sellList, (s: SellRow, idx: number) => {
Row({ space: 12 }) {
Column({ space: 4 }) {
Text(s.grade)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text(s.price + '元')
.fontSize(10)
.fontColor('#FFF3E8')
}
.width(58)
.padding({ top: 12, bottom: 12 })
.borderRadius(12)
.backgroundColor(gradeColor(s.grade))
.alignItems(HorizontalAlign.Center)
.shadow({ radius: 6, color: 'rgba(37,99,235,0.18)', offsetX: 0, offsetY: 3 })
Column({ space: 5 }) {
Row({ space: 6 }) {
Text(s.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
Text(s.status)
.fontSize(9)
.fontColor(s.statusColor)
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor('#F5F7FF')
}
.width('100%')
Row({ space: 10 }) {
Text('浏览 ' + s.views)
.fontSize(10)
.fontColor('#9CA3AF')
Text('议价 ' + s.asks + ' 条')
.fontSize(10)
.fontColor('#FF6A00')
}
.width('100%')
Row({ space: 6 }) {
Text('议价')
.fontSize(10)
.fontColor('#2563EB')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.border({ width: 1, color: '#BFDBFE' })
.onClick(() => {
this.bargainIdx = idx
this.bargainPrice = ''
this.showBargain = true
})
Text('改价')
.fontSize(10)
.fontColor('#FF6A00')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.border({ width: 1, color: '#FED7AA' })
.onClick(() => {
this.editIdx = idx
this.editPrice = String(s.price)
this.showEdit = true
})
if (s.status === '寄售中' || s.status === '已预定') {
Text('下架')
.fontSize(10)
.fontColor('#EF4444')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.border({ width: 1, color:'#FECACA' })
.onClick(() => {
this.offIdx = idx
this.showOff = true
})
}
}
.width('100%')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor(idx % 2 === 0 ? '#FFFFFF' : '#F0F5FF')
.shadow({ radius: 5, color: 'rgba(31,41,55,0.05)', offsetX: 0, offsetY: 2 })
}, (s: SellRow) => s.id.toString())
Column({ space: 8 }) {
Row({ space: 6 }) {
Text('寄售小贴士')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#2563EB')
Text('· 发货前请拍摄打包视频')
.fontSize(10)
.fontColor('#6B7280')
.layoutWeight(1)
}
.width('100%')
Row({ space: 6 }) {
Text('审核时效')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6A00')
Text('· 工作日 2 小时内完成审核')
.fontSize(10)
.fontColor('#6B7280')
.layoutWeight(1)
}
.width('100%')
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#E5E7EB' })
if (this.showSell) {
modalOverlay(() => {
this.showSell = false
})
Column({ space: 12 }) {
Text('上架好谷')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.width('100%')
TextInput({ placeholder: '谷子名称(如:程小时 吧唧·雨夜)', text: this.inputName })
.fontSize(12)
.height(38)
.borderRadius(10)
.backgroundColor('#F1F5F9')
.onChange((v: string) => {
this.inputName = v
})
Column({ space: 8 }) {
Text('档位')
.fontSize(12)
.fontColor('#475569')
.width('100%')
Row({ space: 8 }) {
Text('SSR')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(this.gradeIdx === 0 ? '#FFFFFF' : '#FF6A00')
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor(this.gradeIdx === 0 ? '#FF6A00' : '#FFF3E8')
.onClick(() => {
this.gradeIdx = 0
this.inputGrade = 'SSR'
})
Text('SR')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(this.gradeIdx === 1 ? '#FFFFFF' : '#2563EB')
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor(this.gradeIdx === 1 ? '#2563EB' : '#EFF4FF')
.onClick(() => {
this.gradeIdx = 1
this.inputGrade = 'SR'
})
Text('R')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(this.gradeIdx === 2 ? '#FFFFFF' : '#8B5CF6')
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor(this.gradeIdx === 2 ? '#8B5CF6' : '#F5F0FF')
.onClick(() => {
this.gradeIdx = 2
this.inputGrade = 'R'
})
}
.width('100%')
}
.width('100%')
Column({ space: 8 }) {
Text('品类')
.fontSize(12)
.fontColor('#475569')
.width('100%')
Flex({ wrap: FlexWrap.Wrap, direction: FlexDirection.Row }) {
Text('吧唧')
.fontSize(11)
.fontColor(this.typeIdx === 0 ? '#FFFFFF' : '#475569')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(10)
.backgroundColor(this.typeIdx === 0 ? '#FF6A00' : '#F1F5F9')
.margin(4)
.onClick(() => {
this.typeIdx = 0
this.sellType = '吧唧'
})
Text('立牌')
.fontSize(11)
.fontColor(this.typeIdx === 1 ? '#FFFFFF' : '#475569')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(10)
.backgroundColor(this.typeIdx === 1 ? '#FF6A00' : '#F1F5F9')
.margin(4)
.onClick(() => {
this.typeIdx = 1
this.sellType = '立牌'
})
Text('色纸')
.fontSize(11)
.fontColor(this.typeIdx === 2 ? '#FFFFFF' : '#475569')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(10)
.backgroundColor(this.typeIdx === 2 ? '#FF6A00' : '#F1F5F9')
.margin(4)
.onClick(() => {
this.typeIdx = 2
this.sellType = '色纸'
})
Text('票谷')
.fontSize(11)
.fontColor(this.typeIdx === 3 ? '#FFFFFF' : '#475569')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(10)
.backgroundColor(this.typeIdx === 3 ? '#FF6A00' : '#F1F5F9')
.margin(4)
.onClick(() => {
this.typeIdx = 3
this.sellType = '票谷'
})
Text('其他周边')
.fontSize(11)
.fontColor(this.typeIdx === 4 ? '#FFFFFF' : '#475569')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(10)
.backgroundColor(this.typeIdx === 4 ? '#FF6A00' : '#F1F5F9')
.margin(4)
.onClick(() => {
this.typeIdx = 4
this.sellType = '其他周边'
})
}
.width('100%')
}
.width('100%')
TextInput({ placeholder: '心理价(元)', text: this.inputPrice })
.fontSize(12)
.height(38)
.borderRadius(10)
.backgroundColor('#F1F5F9')
.onChange((v: string) => {
this.inputPrice = v
})
Row({ space: 10 }) {
Text('取消')
.fontSize(13)
.fontColor('#64748B')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#F1F5F9')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showSell = false
})
Text('确认上架')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.linearGradient({
angle: 90,
colors: [['#FF6A00', 0.0], ['#FF9A4D', 1.0]]
})
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.applySell()
})
}
.width('100%')
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.width('92%')
.constraintSize({ maxHeight: '80%' })
}
if (this.showBargain) {
modalOverlay(() => {
this.showBargain = false
})
Column({ space: 12 }) {
Text('收到议价')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.width('100%')
Column({ space: 4 }) {
Text(this.sellList[this.bargainIdx].name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
Text('当前挂价 ¥' + this.sellList[this.bargainIdx].price + ' · 已收到 ' + this.sellList[this.bargainIdx].asks + ' 条议价')
.fontSize(11)
.fontColor('#94A3B8')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#F0F5FF')
Text('一键回价')
.fontSize(12)
.fontColor('#475569')
.width('100%')
Flex({ wrap: FlexWrap.Wrap, direction: FlexDirection.Row }) {
Text('95 折')
.fontSize(12)
.fontColor('#FF6A00')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor('#FFF3E8')
.margin(4)
.onClick(() => {
this.setDiscount(0.95)
})
Text('9 折')
.fontSize(12)
.fontColor('#FF6A00')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor('#FFF3E8')
.margin(4)
.onClick(() => {
this.setDiscount(0.9)
})
Text('85 折')
.fontSize(12)
.fontColor('#2563EB')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor('#EFF4FF')
.margin(4)
.onClick(() => {
this.setDiscount(0.85)
})
Text('8 折')
.fontSize(12)
.fontColor('#2563EB')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(12)
.backgroundColor('#EFF4FF')
.margin(4)
.onClick(() => {
this.setDiscount(0.8)
})
}
.width('100%')
TextInput({ placeholder: '手动输入回价(元)', text: this.bargainPrice })
.fontSize(12)
.height(38)
.borderRadius(10)
.backgroundColor('#F1F5F9')
.onChange((v: string) => {
this.bargainPrice = v
})
Row({ space: 10 }) {
Text('暂不议价')
.fontSize(13)
.fontColor('#64748B')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#F1F5F9')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showBargain = false
})
Text('同意 ' + (this.bargainPrice === '' ? '议价' : '¥' + this.bargainPrice))
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#2563EB')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showBargain = false
})
}
.width('100%')
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.width('92%')
.constraintSize({ maxHeight: '80%' })
}
if (this.showEdit) {
modalOverlay(() => {
this.showEdit = false
})
Column({ space: 12 }) {
Text('修改挂价')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.width('100%')
Text(this.sellList[this.editIdx].name)
.fontSize(12)
.fontColor('#64748B')
.width('100%')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
TextInput({ placeholder: '新价格(元)', text: this.editPrice })
.fontSize(12)
.height(38)
.borderRadius(10)
.backgroundColor('#F1F5F9')
.onChange((v: string) => {
this.editPrice = v
})
Text('提示:降价会重新推送给关注买家')
.fontSize(10)
.fontColor('#94A3B8')
.width('100%')
Row({ space: 10 }) {
Text('取消')
.fontSize(13)
.fontColor('#64748B')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#F1F5F9')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showEdit = false
})
Text('确认改价')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#FF6A00')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.applyEdit()
})
}
.width('100%')
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.width('88%')
}
if (this.showOff) {
modalOverlay(() => {
this.showOff = false
})
Column({ space: 14 }) {
Text('确认下架?')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#EF4444')
.width('100%')
.textAlign(TextAlign.Center)
Text('下架后商品将不再展示,已有议价与预定记录将自动取消,24 小时内可重新上架。')
.fontSize(12)
.fontColor('#64748B')
.lineHeight(18)
.textAlign(TextAlign.Center)
Row({ space: 10 }) {
Text('再想想')
.fontSize(13)
.fontColor('#64748B')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#F1F5F9')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showOff = false
})
Text('确认下架')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#EF4444')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.applyOff()
})
}
.width('100%')
}
.padding(18)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.width('84%')
}
if (this.showAsk) {
modalOverlay(() => {
this.showAsk = false
})
Column({ space: 10 }) {
Text('出谷攻略')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.width('100%')
Column({ space: 6 }) {
Text('① 拍清六面图')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6A00')
Text('正面 / 背面 / 吧唧别针 / 色纸签名位都要拍,减少纠纷')
.fontSize(11)
.fontColor('#64748B')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(10)
.borderRadius(10)
.backgroundColor('#FFF7ED')
Column({ space: 6 }) {
Text('② 定价参考行情')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#2563EB')
Text('挂价高于近 7 日成交均价 15% 时,曝光会明显下降')
.fontSize(11)
.fontColor('#64748B')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(10)
.borderRadius(10)
.backgroundColor('#EFF6FF')
Column({ space: 6 }) {
Text('③ 包装要硬核')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#8B5CF6')
Text('吧唧套气泡膜,立牌夹硬纸板,色纸装卡套再发货')
.fontSize(11)
.fontColor('#64748B')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(10)
.borderRadius(10)
.backgroundColor('#F5F3FF')
Text('我知道了')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#FF6A00')
.width('100%')
.textAlign(TextAlign.Center)
.onClick(() => {
this.showAsk = false
})
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.width('90%')
.constraintSize({ maxHeight: '80%' })
}
}
.padding(12)
}
.backgroundColor('#F5F7FF')
.layoutWeight(1)
}
}
const ORDER_ROWS: OrderRow[] = [
{ id: 'GD2026082401', goods: '程小时 吧唧·雨夜', grade: 'SSR', price: 68, num: 1, status: '待收货', statusColor: '#FF6A00', seller: '时光小卖部' },
{ id: 'GD2026082307', goods: '五条悟 Q版立牌', grade: 'SSR', price: 88, num: 1, status: '已发货', statusColor: '#2563EB', seller: '咒术周边屋' },
{ id: 'GD2026082203', goods: '影山飞雄 拼图吧唧', grade: 'SR', price: 35, num: 2, status: '已完结', statusColor: '#16A34A', seller: '排球竞技谷' },
{ id: 'GD2026082115', goods: '花城 色纸·血雨探花', grade: 'SSR', price: 128, num: 1, status: '退款中', statusColor: '#EF4444', seller: '天官阁' },
{ id: 'GD2026082019', goods: '太宰治 亚克力钥匙扣', grade: 'SR', price: 42, num: 1, status: '已完结', statusColor: '#16A34A', seller: '文豪古本店' },
{ id: 'GD2026081922', goods: '陆光 镭射票根', grade: 'R', price: 19, num: 3, status: '已完结', statusColor: '#16A34A', seller: '时光小卖部' }
]
const ORDER_FILTERS: string[] = ['全部', '待收货', '退款/售后', '已完结']
const WALLET_CARDS: WalletCard[] = [
{ id: 1, title: '可用余额', value: '¥1,286.50', desc: '可提现' },
{ id: 2, title: '冻结中', value: '¥340.00', desc: '待确认收货' },
{ id: 3, title: '谷子积分', value: '8,620', desc: '可兑换包材' }
]
const BADGE_LIST: BadgeWall[] = [
{ id: 1, name: '初食谷', lit: true, color: '#FF6A00' },
{ id: 2, name: '吧唧控', lit: true, color: '#2563EB' },
{ id: 3, name: '立牌党', lit: true, color: '#8B5CF6' },
{ id: 4, name: '色纸收藏家', lit: false, color: '#9CA3AF' },
{ id: 5, name: '百单大佬', lit: true, color: '#16A34A' },
{ id: 6, name: '闪电发货', lit: false, color: '#9CA3AF' },
{ id: 7, name: '诚信卖家', lit: true, color: '#F59E0B' },
{ id: 8, name: '年度吃谷王', lit: false, color: '#9CA3AF' }
]
const MSG_LIST: MsgMeta[] = [
{ id: 1, title: '议价提醒', content: '有人对你的「五条悟 Q版立牌」出价 ¥75', time: '2分钟前', read: false },
{ id: 2, title: '上新预告', content: '今晚 20:00 时光代理人系列补货 200 件', time: '1小时前', read: false },
{ id: 3, title: '物流更新', content: '「咒术周边屋」已发货,顺丰 SF1328...', time: '昨天 18:20', read: true },
{ id: 4, title: '行情播报', content: '天官赐福色纸今日上涨 4.8%,注意仓位', time: '昨天 09:00', read: true }
]
// ==================== Tab 5 订单 ====================
@Component
export struct GOrderTab {
@State ordIdx: number = 0
@State showDetail: boolean = false
@State showAfter: boolean = false
@State showLog: boolean = false
@State detailIdx: number = 0
@State reasonIdx: number = -1
build() {
Scroll() {
Column({ space: 12 }) {
Row({ space: 8 }) {
ForEach(ORDER_FILTERS, (f: string, i: number) => {
Text(f)
.fontSize(12)
.fontWeight(this.ordIdx === i ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.ordIdx === i ? '#FFFFFF' : '#64748B')
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.borderRadius(14)
.backgroundColor(this.ordIdx === i ? '#FF6A00' : '#FFFFFF')
.shadow(this.ordIdx === i ? { radius: 6, color: 'rgba(255,106,0,0.3)', offsetX: 0, offsetY: 3 } : { radius: 2, color: 'rgba(31,41,55,0.05)', offsetX: 0, offsetY: 1 })
.onClick(() => {
this.ordIdx = i
})
}, (f: string) => f)
}
.width('100%')
ForEach(ORDER_ROWS, (o: OrderRow, idx: number) => {
Column({ space: 10 }) {
Row({ space: 6 }) {
Text(o.seller)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.layoutWeight(1)
Text(o.status)
.fontSize(11)
.fontColor(o.statusColor)
}
.width('100%')
Row({ space: 10 }) {
Column()
.width(4)
.height(52)
.borderRadius(2)
.backgroundColor(gradeColor(o.grade))
Column({ space: 4 }) {
Text(o.goods)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text('订单号 ' + o.id)
.fontSize(10)
.fontColor('#94A3B8')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 3 }) {
Text('¥' + o.price)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6A00')
Text('x' + o.num)
.fontSize(10)
.fontColor('#94A3B8')
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row({ space: 8 }) {
Text(o.grade)
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor(gradeColor(o.grade))
Row()
.layoutWeight(1)
Text('查物流')
.fontSize(11)
.fontColor('#2563EB')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(12)
.border({ width: 1, color: '#BFDBFE' })
.onClick(() => {
this.detailIdx = idx
this.showLog = true
})
Text('售后')
.fontSize(11)
.fontColor('#EF4444')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(12)
.border({ width: 1, color: '#FECACA' })
.onClick(() => {
this.detailIdx = idx
this.reasonIdx = -1
this.showAfter = true
})
Text('详情')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('#FF6A00')
.onClick(() => {
this.detailIdx = idx
this.showDetail = true
})
}
.width('100%')
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor('#FFFFFF')
.shadow({ radius: 4, color: 'rgba(31,41,55,0.05)', offsetX: 0, offsetY: 2 })
}, (o: OrderRow) => o.id)
if (this.showDetail) {
modalOverlay(() => {
this.showDetail = false
})
Column({ space: 12 }) {
Text('订单详情')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.width('100%')
Column({ space: 6 }) {
Row({ space: 6 }) {
Text('商品')
.fontSize(12)
.fontColor('#94A3B8')
.width(64)
Text(ORDER_ROWS[this.detailIdx].goods)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.layoutWeight(1)
}
.width('100%')
Row({ space: 6 }) {
Text('卖家')
.fontSize(12)
.fontColor('#94A3B8')
.width(64)
Text(ORDER_ROWS[this.detailIdx].seller)
.fontSize(12)
.fontColor('#1F2937')
.layoutWeight(1)
}
.width('100%')
Row({ space: 6 }) {
Text('数量')
.fontSize(12)
.fontColor('#94A3B8')
.width(64)
Text('x' + ORDER_ROWS[this.detailIdx].num)
.fontSize(12)
.fontColor('#1F2937')
.layoutWeight(1)
}
.width('100%')
Row({ space: 6 }) {
Text('实付')
.fontSize(12)
.fontColor('#94A3B8')
.width(64)
Text('¥' + ORDER_ROWS[this.detailIdx].price * ORDER_ROWS[this.detailIdx].num)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6A00')
.layoutWeight(1)
}
.width('100%')
}
.width('100%')
.padding(12)
.borderRadius(12)
.backgroundColor('#F8FAFC')
Text('确认收货后货款将结算给卖家,请先验货再确认')
.fontSize(10)
.fontColor('#94A3B8')
.width('100%')
Text('好的')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#FF6A00')
.width('100%')
.textAlign(TextAlign.Center)
.onClick(() => {
this.showDetail = false
})
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.width('88%')
}
if (this.showAfter) {
modalOverlay(() => {
this.showAfter = false
})
Column({ space: 12 }) {
Text('申请售后')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.width('100%')
Text('选择售后原因')
.fontSize(12)
.fontColor('#475569')
.width('100%')
Column({ space: 8 }) {
Text('商品与描述不符')
.fontSize(12)
.fontColor(this.reasonIdx === 0 ? '#FF6A00' : '#475569')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(10)
.backgroundColor(this.reasonIdx === 0 ? '#FFF3E8' : '#F8FAFC')
.width('100%')
.onClick(() => {
this.reasonIdx = 0
})
Text('运输破损')
.fontSize(12)
.fontColor(this.reasonIdx === 1 ? '#FF6A00' : '#475569')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(10)
.backgroundColor(this.reasonIdx === 1 ? '#FFF3E8' : '#F8FAFC')
.width('100%')
.onClick(() => {
this.reasonIdx = 1
})
Text('发错货 / 漏发')
.fontSize(12)
.fontColor(this.reasonIdx === 2 ? '#FF6A00' : '#475569')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(10)
.backgroundColor(this.reasonIdx === 2 ? '#FFF3E8' : '#F8FAFC')
.width('100%')
.onClick(() => {
this.reasonIdx = 2
})
Text('不想要了(未发货)')
.fontSize(12)
.fontColor(this.reasonIdx === 3 ? '#FF6A00' : '#475569')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.borderRadius(10)
.backgroundColor(this.reasonIdx === 3 ? '#FFF3E8' : '#F8FAFC')
.width('100%')
.onClick(() => {
this.reasonIdx = 3
})
}
.width('100%')
Row({ space: 10 }) {
Text('取消')
.fontSize(13)
.fontColor('#64748B')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#F1F5F9')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showAfter = false
})
Text('提交申请')
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor(this.reasonIdx === -1 ? '#FCA5A5' : '#EF4444')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => {
if (this.reasonIdx !== -1) {
this.showAfter = false
}
})
}
.width('100%')
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.width('90%')
.constraintSize({ maxHeight: '80%' })
}
if (this.showLog) {
modalOverlay(() => {
this.showLog = false
})
Column({ space: 0 }) {
Text('物流轨迹')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1F2937')
.width('100%')
.padding({ bottom: 12 })
Column({ space: 0 }) {
Row({ space: 10 }) {
Column()
.width(10)
.height(10)
.borderRadius(5)
.backgroundColor('#FF6A00')
Column({ space: 2 }) {
Text('已下单')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6A00')
Text('08-23 10:12 · 时光小卖部已接单')
.fontSize(10)
.fontColor('#94A3B8')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding({ bottom: 14 })
Row({ space: 10 }) {
Column()
.width(10)
.height(10)
.borderRadius(5)
.backgroundColor('#FF6A00')
Column({ space: 2 }) {
Text('打包完成')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#FF6A00')
Text('08-23 16:40 · 气泡膜+硬纸板包装')
.fontSize(10)
.fontColor('#94A3B8')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding({ bottom: 14 })
Row({ space: 10 }) {
Column()
.width(10)
.height(10)
.borderRadius(5)
.backgroundColor('#BFDBFE')
Column({ space: 2 }) {
Text('运输中')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#2563EB')
Text('08-24 08:15 · 顺丰速运已揽收')
.fontSize(10)
.fontColor('#94A3B8')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding({ bottom: 14 })
Row({ space: 10 }) {
Column()
.width(10)
.height(10)
.borderRadius(5)
.backgroundColor('#E2E8F0')
Column({ space: 2 }) {
Text('派送中')
.fontSize(12)
.fontColor('#94A3B8')
Text('预计 08-25 送达')
.fontSize(10)
.fontColor('#94A3B8')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
}
.width('100%')
Text('关闭')
.fontSize(13)
.fontColor('#2563EB')
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#EFF4FF')
.width('100%')
.textAlign(TextAlign.Center)
.margin({ top: 12 })
.onClick(() => {
this.showLog = false
})
}
.padding(16)
.borderRadius(16)
.backgroundColor('#FFFFFF')
.width('88%')
}
}
.padding(12)
}
.backgroundColor('#F5F7FF')
.layoutWeight(1)
}
}
const MAIN_TABS: TabMeta[] = [
{ name: '今日谷', icon: '🥞' },
{ name: '行情', icon: '📈' },
{ name: '求购', icon: '📢' },
{ name: '出谷', icon: '📤' },
{ name: '订单', icon: '📋' },
{ name: '我的', icon: '👤' }
]
// ==================== Tab 6 我的 ====================
@Component
export struct GMineTab {
@State showWithdraw: boolean = false
@State showBadge: boolean = false
@State showSetting: boolean = false
@State channelIdx: number = 0
@State drawAmount: string = ''
build() {
Scroll() {
Column({ space: 12 }) {
Column({ space: 12 }) {
Row({ space: 12 }) {
Column()
.width(56)
.height(56)
.borderRadius(28)
.backgroundColor('rgba(255,255,255,0.3)')
.border({ width: 2, color: '#FFFFFF' })
.justifyContent(FlexAlign.Center)
Column({ space: 5 }) {
Row({ space: 6 }) {
}
}
总结

本文对基于 HarmonyOS ArkTS API 24 的谷子集市二次元周边交易平台进行了全面的代码级深度解析。该应用围绕"吃谷"文化构建了从商品浏览、行情分析、求购发布、寄售管理到订单售后的完整交易闭环,六个功能模块各司其职又相互衔接。应用在数据层通过强类型接口定义保障了编译期安全,在视图层大量运用了 ForEach、Flex、Stack、linearGradient 等声明式能力实现了丰富的动态 UI 效果,在交互层通过条件渲染和状态管理实现了弹窗、表单校验、列表增删改查等复杂业务逻辑。
从技术亮点角度来归纳,该应用展现了几个突出的工程实践。首先是不可变状态更新模式的贯彻使用——所有列表的增删改操作都采用"创建新数组-遍历复制-条件修改-整体赋值"的模式,保证了状态更新的可预测性和 ArkTS 声明式框架的响应式更新机制能够正确触发。其次是公共逻辑的高度复用——gradeColor 函数被四个 Tab 组件的十余处调用、modalOverlay Builder 被所有弹窗共享、trendBarH 工具函数被行情页调用,有效降低了代码冗余。再次是视觉反馈的精心设计——档位颜色系统、涨跌红绿配色、物流节点渐进色变、售后按钮的可用性色变,每一处颜色变化都承载着明确的业务语义。
更多推荐




所有评论(0)