鸿蒙ArkTS实战:古钱铺移动应用开发全解析——从铜锈绿金到多Tab响应式架构的深度剖析
摘要:本文以一个完整的古钱币文化展示与交易管理应用"古钱铺"为案例,深入剖析鸿蒙HarmonyOS ArkTS声明式UI开发范式的核心要点。从以"铜锈绿·鎏金·朱砂红"三色为核心的古风色彩体系的接口化定义,到响应式数据流
@Observed/@ObjectLink在五Tab场景下的深度运用,再到自定义模态弹窗体系、条件渲染路由、layoutWeight弹性布局权重等复杂交互模式的完整实现,逐段拆解每一个关键代码块背后的设计思想与技术原理。全文覆盖接口约束、枚举安全、数据建模、状态驱动渲染、组件拆分策略、颜色透明度拼接、ForEach键值策略等20+个技术知识点,适合有一定ArkTS基础、希望深入理解鸿蒙声明式UI架构的开发者进阶阅读。
一、前言:千年铸币的数字化新生
中国古钱,又称方孔圆钱,是世界上历史最悠久的货币形态之一,距今已有两千三百余年。"外圆内方",这一形制自秦半两确立以来,历经汉五铢、唐开元通宝、宋明清各代通宝,始终未变——圆形象征天穹之圆满,方孔代表大地之方正,蕴含着古人"天圆地方"的宇宙观与哲学智慧。
一枚古钱,不仅是经济交易的媒介,更是铸造工艺、书法艺术、朝代更迭与文化传承的物质载体。从翻砂铸钱的炉火熊熊,到母钱修模的精雕细琢;从范铸叠铸的青铜时代,到机制币的近代转型——每一道工序都凝聚着匠人的心血与时代的印记。而钱匣(收藏盒)、范模(铸造模具)、订单台账、客户评价,则构成了完整的古钱产业链条。

在数字化浪潮席卷全球的今天,将这样一门承载着深厚文化底蕴的传统行业以移动应用的形式呈现给现代用户,既是对非物质文化遗产传播方式的一次创新探索,也是对鸿蒙开发技术的一次深度实践。本文所剖析的"古钱铺"应用,正是围绕这一主题构建的一个集古钱陈列、范模库、钱匣坊、订单管理与客户评价于一体的综合性移动端展示平台。
在整个应用的开发过程中,我们不仅需要关注UI层面的视觉表现力——如何用代码还原古钱币的温润质感与古典韵味,更需要深入理解ArkTS框架的底层机制:状态管理如何驱动UI刷新、组件间如何高效通信、数据流如何在父子组件间双向传递、自定义弹窗如何优雅地叠加在页面上方。这些问题的答案,都隐藏在接下来的每一行代码之中。本文将从架构总览开始,逐层深入,带你完成一次从"能写"到"写好"的鸿蒙开发进阶之旅。

二、项目架构总览:分层设计与模块化思想
本应用采用经典的单文件声明式架构,所有逻辑集中在一个源码文件中,通过清晰的分层组织实现高内聚低耦合。整体架构可以划分为以下六个层次:
第一层——类型定义层(第1~132行):包含CoinPalette色彩接口、CoinTab枚举、TabMeta标签元数据接口以及全部业务实体接口(CoinItem、MoldItem、CaseItem、CoinOrderItem、CoinReviewItem)和图表数据元接口(WeekCoinMeta、DynastyShareMeta、CoinHotMeta、CoinTopMeta)。这一层是整个应用的"契约",所有后续的数据结构都必须严格遵循这些接口定义。
第二层——常量实例层(第23~167行):将抽象的类型定义具象化为可用的运行时对象,包括COLORS色彩常量、TAB_LIST标签列表、COIN_COL钱位列阵以及四组静态图表数据(WEEK_SOLD周销量、DYNASTY_SHARE朝代构成、COIN_HOT工艺热度、COIN_TOP名钱排行)。
第三层——响应式数据类(第169~245行):@Observed装饰的CoinData类,封装了五组核心业务数据的动态数组,是整个应用的状态中枢。
第四层——工具函数层(第247~300行):五个纯函数式的颜色映射器(getDynastyColor、getMoldColor、getPriceColor、getOrderColor、getReviewTagColor),根据业务规则返回语义化的颜色值。
第五层——主入口与容器组件(第302~520行):@Entry标记的CoinApp根组件,负责全局状态管理、页面骨架搭建、Tab路由分发和弹窗条件渲染。
第六层——子组件体系(第522行至文件末尾):包括1个标签组件(CoinTag)、5个内容组件(分别对应5个Tab页)和4个模态弹窗组件,共同构成完整的功能矩阵。

这种分层设计的优势在于:当需要修改某个功能模块时,开发者可以快速定位到对应的层次进行修改,而不会对其他部分产生连锁反应。例如,如果需要调整配色方案,只需修改第一层的接口定义和第二层的常量实例;如果需要新增一个Tab页面,只需在第五层添加路由分支并在第六层新增对应的内容组件即可。
三、色彩体系设计:接口约束与常量定义
3.1 调色板接口定义
interface CoinPalette {
primary: string; // 主色调——铜锈绿(古钱氧化后的青绿色)
primaryLight: string; // 主色浅变体
primaryDark: string; // 主色深变体
accent: string; // 强调色——鎏金色(古钱镀金色泽)
accentLight: string; // 强调色浅变体——淡金
bg: string; // 页面背景——宣纸米白
cardBg: string; // 卡片背景——纯白
cardAlt: string; // 交替卡片背景——浅褐
textPrimary: string; // 一级文本——墨黑
textSecondary: string; // 二级文本——灰褐
textHint: string; // 提示文本——淡灰
border: string; // 边框色
line: string; // 分割线色
success: string; // 成功色——藏蓝
warning: string; // 警告色——金橙
danger: string; // 危险色——暗红
white: string; // 纯白
gold: string; // 金色(古钱专用)
silkWhite: string; // 绢白色(丝绸衬底专用)
}

这段代码看似简单,却蕴含着深刻的设计思想。首先,使用interface而不是type来定义调色板结构,是因为interface在ArkTS中具有更好的类型推断能力和扩展性。接口中定义了21个颜色字段,覆盖了以下几大语义类别:
- 主色调系列(primary / primaryLight / primaryDark):以铜锈绿
#4E7A63为核心,模拟古钱币出土后表面形成的自然氧化层——那种介于青绿与墨绿之间的独特色泽。三个梯度分别用于常规主色、次要强调和深色背景区域(如顶部Header)。 - 强调色系列(accent / accentLight):鎏金色
#B0893F取自古钱币表面的镀金装饰或包浆中的金黄色泽,用于关键操作按钮和高亮元素;其浅变体淡金色#D2B477则呼应古代金银器的温润质感。 - 背景色系(bg / cardBg / cardAlt):宣纸米白色
#F1EFE7作为页面底色营造古籍翻阅感;纯白#FFFFFF作为卡片底色保证内容可读性;浅褐色#E9E3D3作为交替行背景增强列表视觉节奏。 - 文本色三级灰度(textPrimary / textSecondary / textHint):从墨黑
#2E332B到灰褐#6E7264再到淡灰#A09F90,形成清晰的信息层级。 - 功能语义色(success / warning / danger):藏蓝
#4E7BA8表示成功/完成,金橙表示提醒/待办,暗红#8E3B36表示危险/高价。 - 主题专属色(gold / silkWhite):金色复用accent但语义独立,专指古钱相关元素;绢白色
#F6F2E8精确对应钱匣内衬丝绸的柔和质感。
这种21色的精细划分,使得整个应用的每一个视觉元素都有明确的"色彩归属",避免了随意使用硬编码颜色值带来的视觉混乱。更重要的是,通过接口约束,任何遗漏字段或类型不匹配都会在编译阶段被TypeScript编译器捕获,实现了"编译期安全"。
3.2 色彩常量实例化
const COLORS: CoinPalette = {
primary: '#4E7A63',
primaryLight: '#8AA996',
primaryDark: '#2F4A3C',
accent: '#B0893F',
accentLight: '#D2B477',
bg: '#F1EFE7',
cardBg: '#FFFFFF',
cardAlt: '#E9E3D3',
textPrimary: '#2E332B',
textSecondary: '#6E7264',
textHint: '#A09F90',
border: '#D8D4C2',
line: '#E7E4D8',
success: '#4E7BA8',
warning: '#C98A4E',
danger: '#8E3B36',
white: '#FFFFFF',
gold: '#B0893F',
silkWhite: '#F6F2E8'
};
这里有一个非常值得注意的细节:COLORS常量的类型被显式标注为CoinPalette。这意味着TypeScript编译器会在编译阶段检查COLORS对象是否完整实现了接口定义的所有21个字段。如果遗漏了任何一个颜色字段,或者字段类型不匹配(比如不小心把数字赋给了string类型的字段),编译器会立即报错。这种"编译期安全"是大型项目中极其重要的保障,它能在代码运行之前就消灭掉大量潜在的类型错误。
此外,所有颜色值均采用标准的#RRGGBB六位十六进制格式,而非CSS变量名或语义别名。这种选择在ArkTS生态中是务实的——ArkTS的声明式UI直接支持字符串形式的颜色值,无需额外的运行时解析开销。同时,将所有颜色集中在一个常量对象中管理,使得后续的主题切换(如实现"暗色模式")只需要替换这个常量对象即可,体现了良好的架构前瞻性。
四、枚举与标签配置:类型安全的页面路由
4.1 Tab枚举定义
enum CoinTab {
COIN = 0,
MOLD = 1,
CASE = 2,
ORDER = 3,
REVIEW = 4
}
CoinTab枚举定义了应用底部导航栏的五个页面标识,从0到4依次对应:古钱列表、范模库、钱匣坊、订单台账、客户评价。使用枚举而非魔术数字(magic number)的好处是多方面的:
第一,类型安全。当我们在代码中编写this.curTab === CoinTab.COIN时,编辑器会提供自动补全和拼写检查,有效避免将COIN误写成COON之类的低级错误。
第二,语义明确。枚举成员名称本身就是最好的文档——任何阅读代码的人都能立刻理解CoinTab.REVIEW代表的是"评价"页面,而数字4则需要查阅上下文才能确定含义。
第三,重构友好。如果未来需要调整Tab的顺序(比如把"订单"移到第二个位置),只需修改枚举值的赋值顺序,所有引用该枚举的地方都会自动适配。
4.2 标签元数据配置
interface TabMeta {
key: string;
icon: string;
label: string;
color: string;
}
const TAB_LIST: TabMeta[] = [
{ key: 'coin', icon: '🪙', label: '古钱', color: '#B0893F' },
{ key: 'mold', icon: '🔨', label: '范模', color: '#4E7A63' },
{ key: 'case', icon: '🗃️', label: '钱匣', color: '#8E3B36' },
{ key: 'order', icon: '📦', label: '订单', color: '#4E7BA8' },
{ key: 'review', icon: '⭐', label: '客评', color: '#C98A4E' }
];
这是一个非常优雅的设计模式:将标签的配置信息(唯一标识key、图标emoji、显示文字label、主题色color)抽象为一个数据数组,而非硬编码在组件的build()方法中。这种"数据驱动UI"的思路,使得后续新增或修改标签时,只需要修改TAB_LIST数组即可,无需触碰任何组件代码。
每个标签还拥有自己的主题色(color字段),这为选中态的视觉表现提供了丰富的个性化空间。例如,"古钱"Tab选中时使用鎏金色高亮,"范模"Tab则使用铜锈绿,"钱匣"Tab使用暗红色——让用户在不同页面间切换时能获得一致的色彩反馈但又各具辨识度,仿佛每一页都有自己独特的"身份色"。

五、数据模型设计:接口驱动的类型安全
5.1 业务实体接口定义
interface CoinItem {
name: string; // 钱名(开元通宝/乾隆通宝等)
dynasty: string; // 朝代(唐/宋/清/汉等)
craft: string; // 工艺(翻砂/范铸/母钱/机制等)
price: number; // 价格(元)
emoji: string; // 表情图标
}
interface MoldItem {
name: string; // 范模名称
material: string; // 材质(陶/石/铜/精铜/砂/蜡等)
level: number; // 精度等级(0-100)
place: string; // 产地(洛阳/长安/建业等)
emoji: string;
}
interface CaseItem {
name: string; // 匣名
wood: string; // 木料(红木/楠木/织锦/黄铜等)
level: number; // 品级(0-100)
place: string; // 产地
emoji: string;
}
interface CoinOrderItem {
name: string; // 客户名称
region: string; // 所属区域
count: number; // 订购数量(枚)
amount: number; // 订单金额(元)
emoji: string;
}
interface CoinReviewItem {
name: string; // 评价人姓名
score: number; // 评分(1-5星)
date: string; // 评价日期
content: string; // 评价内容
tag: string; // 评价标签
emoji: string;
}
这里展示了五种业务实体的接口定义。每个接口都精心设计了字段,紧密围绕古钱行业的实际业务场景:
CoinItem(古钱条目):包含钱名(如"开元通宝"、"刀币")、朝代(唐、宋、清、汉、战国)、铸造工艺(翻砂、范铸、母钱、机制、叠铸)、价格和表情图标。其中dynasty字段不仅用于显示,还是后续getDynastyColor()颜色映射函数的输入参数——不同朝代的古钱会被赋予不同的主题色。MoldItem(范模条目):记录范模名称、材质(陶、石、铜、精铜、砂、铁、泥、蜡等多种传统铸造材料)、精度等级(数值型,用于进度条可视化)和产地。level字段是范模页面的核心数据维度,直接影响每件范模的"精度"进度条长度和颜色。CaseItem(钱匣条目):记录匣名、木料材质(红木、楠木、织锦、黄铜、竹、大漆、樟木、花梨、乌木、紫檀、檀木等)、品级和产地。钱匣作为古钱收藏的重要配套产品,其木料种类直接反映了不同档次和地域风格。CoinOrderItem(订单条目):记录客户名称、所属区域、订购数量和金额。amount字段是getOrderColor()的颜色映射依据——金额越高颜色越"危险"(暗红),形成直观的业务警示。CoinReviewItem(评价条目):包含评分(1-5星)、日期、评价内容和标签。tag字段是getReviewTagColor()的映射输入,不同类型的评价标签被赋予不同的主题色。
这五组接口构成了整个应用的数据模型基石。值得注意的是,所有接口中的emoji字段虽然不是严格的"业务数据",但在ArkTS的Text组件中可以直接渲染表情符号,为每个条目提供了轻量级的视觉标识,增强了列表的可读性和趣味性。
5.2 图表数据元接口
interface WeekCoinMeta {
day: string; // 星期几
value: number; // 销量
}
interface DynastyShareMeta {
name: string; // 朝代名称
count: number; // 数量
color: string; // 主题色
}
interface CoinHotMeta {
name: string; // 工艺名称
heat: number; // 热度值
color: string; // 主题色
}
interface CoinTopMeta {
name: string; // 钱名
sold: number; // 销量
color: string; // 主题色
}
这四个接口专门服务于应用中的四种图表/排行榜可视化组件。它们的设计遵循了一个共同的范式:名称 + 数值 + 主题色的三元组结构。这种统一的数据形状使得四种图表可以共享同一套渲染逻辑(ForEach + layoutWeight条形图),只是数据源不同而已。
WeekCoinMeta用于垂直柱状图(周销量走势),DynastyShareMeta/CoinHotMeta/CoinTopMeta用于水平条形图(占比/热度/排行)。每种接口都自带color字段,使得每个数据条都可以拥有独立的颜色,而不是强制使用统一的系列色。
六、静态数据集:应用的基础数据源
6.1 图表静态数据
const WEEK_SOLD: WeekCoinMeta[] = [
{ day: '周一', value: 20 },
{ day: '周二', value: 28 },
{ day: '周三', value: 36 },
{ day: '周四', value: 44 },
{ day: '周五', value: 57 },
{ day: '周六', value: 65 },
{ day: '周日', value: 59 }
];
这里展示了周销量数据。在实际项目中,这些数据通常来自后端API接口,但本应用作为展示型Demo采用了静态数据的方式。数据的设计颇具巧思——销量从周一到周六逐步递增(20→28→36→44→57→65),周日略有回落(59),这完全符合零售业"工作日平稳、周末高峰、周日略降"的实际规律,让数据看起来真实可信。周六峰值65枚也恰好超过了60的阈值,在柱状图中会触发鎏金色高亮效果。

const DYNASTY_SHARE: DynastyShareMeta[] = [
{ name: '唐', count: 5, color: '#B0893F' },
{ name: '宋', count: 4, color: '#4E7A63' },
{ name: '清', count: 3, color: '#8E3B36' },
{ name: '汉', count: 2, color: '#4E7BA8' }
];
朝代构成数据反映了当前库存中古钱币的朝代分布。唐代钱币(5款)居首,这与历史上唐代"开元通宝"开创的通宝制度影响深远有关——开元通宝流通时间最长、存世量最大、收藏群体最广。宋代(4款)次之,因宋代钱币品种繁多、书法精美(瘦金体、篆书、隶书并存)。清代(3款)和汉代(2款)紧随其后,构成了完整的"唐宋清汉"四大主力朝代矩阵。

6.2 工艺热度与排行榜数据
const COIN_HOT: CoinHotMeta[] = [
{ name: '翻砂铸钱', heat: 96, color: '#B0893F' },
{ name: '母钱修模', heat: 95, color: '#4E7A63' },
{ name: '范铸叠铸', heat: 94, color: '#8E3B36' },
{ name: '钱文篆刻', heat: 93, color: '#D2B477' },
{ name: '鎏金锉边', heat: 91, color: '#4E7BA8' },
{ name: '包浆养护', heat: 90, color: '#C98A4E' }
];
const COIN_TOP: CoinTopMeta[] = [
{ name: '开元通宝', sold: 97, color: '#B0893F' },
{ name: '五铢钱', sold: 93, color: '#4E7A63' },
{ name: '乾隆通宝', sold: 90, color: '#8E3B36' },
{ name: '大观通宝', sold: 87, color: '#D2B477' },
{ name: '崇宁通宝', sold: 84, color: '#4E7BA8' },
{ name: '刀币', sold: 80, color: '#C98A4E' }
];
铸造工艺热度数据涵盖了古钱制作的六大核心工序,从"翻砂铸钱"(96分)到"包浆养护"(90分),热度值递减但差距不大,说明每一道工序在制钱师心中都至关重要。翻砂铸钱位居榜首,这是因为它是中国古代最主流、最成熟的铸钱工艺——从唐代一直沿用到清末民国,跨越千年而不衰。
名钱销量TOP6则列出了最受欢迎的六款钱币,"开元通宝"以97的销量遥遥领先——这款由唐高祖武德四年(公元621年)始铸的钱币,开创了中国钱币以"通宝""元宝"命名的先河,被誉为"天下第一通宝"。五铢钱(93)次之,它是汉代使用时间最长、流通范围最广的钱币,前后沿用七百余年。


七、响应式数据类:@Observed装饰器的深度运用
@Observed
export class CoinData {
coins: CoinItem[] = [
{ name: '开元通宝', dynasty: '唐', craft: '翻砂', price: 880, emoji: '🪙' },
{ name: '乾隆通宝', dynasty: '清', craft: '翻砂', price: 260, emoji: '🏮' },
{ name: '五铢钱', dynasty: '汉', craft: '范铸', price: 360, emoji: '⚖️' },
{ name: '半两钱', dynasty: '汉', craft: '范铸', price: 680, emoji: '🪨' },
{ name: '崇宁通宝', dynasty: '宋', craft: '翻砂', price: 720, emoji: '🏯' },
{ name: '大观通宝', dynasty: '宋', craft: '母钱', price: 1580, emoji: '🦅' },
{ name: '永乐通宝', dynasty: '明', craft: '翻砂', price: 560, emoji: '🌏' },
{ name: '顺治通宝', dynasty: '清', craft: '翻砂', price: 420, emoji: '🐉' },
{ name: '光绪通宝', dynasty: '清', craft: '机制', price: 380, emoji: '🐎' },
{ name: '货布', dynasty: '汉', craft: '叠铸', price: 1280, emoji: '🪓' },
{ name: '刀币', dynasty: '战国', craft: '范铸', price: 2380, emoji: '🗡️' },
{ name: '布币', dynasty: '战国', craft: '范铸', price: 1880, emoji: '🌾' }
];
molds: MoldItem[] = [
{ name: '陶范', material: '陶', level: 96, place: '洛阳', emoji: '🏺' },
{ name: '石范', material: '石', level: 93, place: '临淄', emoji: '🪨' },
// ... 共12种范模
];
cases: CaseItem[] = [
{ name: '红木钱匣', wood: '红木', level: 96, place: '苏州', emoji: '🪵' },
// ... 共12种钱匣
];
orders: CoinOrderItem[] = [
{ name: '博物馆文创', region: '华北', count: 900, amount: 450000, emoji: '🏛️' },
// ... 共12条订单
];
reviews: CoinReviewItem[] = [
{ name: '博物馆馆长', score: 5, date: '09-03',
content: '开元通宝品相完整,展柜补位正合适。',
tag: '品相完整', emoji: '🏛️' },
// ... 共12条评价
];
}
这是整个应用最核心的数据层代码。@Observed装饰器是ArkTS框架提供的一个关键装饰器,它的作用是将一个普通的类标记为"可观察的"。当一个类被@Observed标记后,该类实例的属性变化会被ArkTS框架自动追踪,任何引用了该实例属性的@ObjectLink或@State变量都会在属性变化时自动触发UI重新渲染。
CoinData类封装了五组核心业务数据:
coins(12枚古钱):涵盖从"开元通宝"(¥880,唐·翻砂)到"布币"(¥1880,战国·范铸)的全价位段产品矩阵。价格跨度从最低¥260(乾隆通宝)到最高¥2380(刀币),覆盖入门级收藏到高端珍品的完整谱系。朝代涵盖唐、宋、清、汉、明、战国六大时期,工艺包括翻砂、范铸、母钱、机制、叠铸五种主要技法。molds(12种范模):材质涵盖陶、石、铜、精铜、砂、铁、泥、蜡八种传统与现代铸造材料,精度等级90-97不等,产地遍布洛阳、临淄、长安、建业、成都、长沙、汴京、北京、邯郸、南阳、杭州等历代铸钱重镇。cases(12种钱匣):木料涵盖红木、金丝楠、织锦、黄铜、竹、大漆、樟木、花梨、乌木、紫檀、楠木、檀木十二种材质,产地对应苏州、成都、南京、大理、安吉、福州、宜春、海南、东阳、扬州、广州等传统手工艺名城。orders(12条订单):客户涵盖博物馆文创、收藏协会、钱币研究所、拍卖行、海外藏家、古玩城店铺、直播鉴宝、文创礼盒商、大学历史系、电视剧组、钱庄博物馆、私人典藏十二类渠道,区域分布华北/华东/华中/华南/海外,金额范围¥225000~¥900000。reviews(12条评价):全部4-5星好评(平均约4.92星),标签涵盖"品相完整"、"稀世难得"、"形制标准"、"钱文精美"、"包浆自然"、"量足价实"、"走量飞快"、"开箱用心"、"教具齐全"、"质感真实"、"做工考究"、"一币一史"等多维度评价维度。
这种将所有业务数据集中在一个@Observed类中的设计,带来了一个重要的架构优势:单一数据源原则(Single Source of Truth)。无论哪个子组件需要访问古钱列表、范模数据还是订单信息,它们都通过@ObjectLink引用同一个CoinData实例。当任何一个数据发生变化(比如新增了一枚古钱、删除了一件范模),所有引用该数据的组件都会自动收到通知并刷新UI,无需手动编写繁琐的事件监听和数据同步逻辑。
八、颜色工具函数:纯函数的设计哲学
function getDynastyColor(dynasty: string): string {
if (dynasty === '唐' || dynasty === '清') return '#B0893F';
else if (dynasty === '宋' || dynasty === '明') return '#4E7A63';
else if (dynasty === '战国') return '#8E3B36';
return '#4E7BA8'; // 汉/默认 → 藏蓝
}
function getMoldColor(level: number): string {
if (level >= 95) return '#B0893F';
else if (level >= 93) return '#4E7A63';
return '#8E3B36';
}
function getPriceColor(price: number): string {
if (price >= 1500) return '#8E3B36'; // 高价 → 暗红
else if (price >= 800) return '#B0893F'; // 中高价 → 鎏金
else if (price >= 400) return '#4E7A63'; // 中价 → 铜绿
return '#4E7BA8'; // 低价 → 藏蓝
}
function getOrderColor(amount: number): string {
if (amount >= 800000) return '#8E3B36';
else if (amount >= 600000) return '#B0893F';
else if (amount >= 400000) return '#4E7A63';
return '#4E7BA8';
}
function getReviewTagColor(tag: string): string {
if (tag === '品相完整' || tag === '形制标准' || tag === '一币一史' || tag === '做工考究')
return '#B0893F';
else if (tag === '稀世难得' || tag === '钱文精美' || tag === '包浆自然')
return '#4E7A63';
else if (tag === '量足价实' || tag === '走量飞快' || tag === '开箱用心' || tag === '质感真实')
return '#8E3B36';
else if (tag === '教具齐全') return '#4E7BA8';
return '#C98A4E';
}

这五个函数构成了应用的颜色映射层,它们都是纯函数(Pure Function)——相同的输入永远产生相同的输出,没有任何副作用。这种函数式的设计哲学在ArkTS开发中具有多重价值:
第一,可测试性极强。每个函数都可以独立编写单元测试,传入已知输入、断言输出颜色,无需mock任何组件或状态。
第二,逻辑集中且可复用。颜色映射规则分散在多个组件中是维护噩梦——如果要调整"高价钱"的阈值从1500改为2000,需要在每个使用处逐一修改。而现在只需改getPriceColor一处。
第三,语义化表达业务规则。函数名本身就是最好的文档——getDynastyColor(dynasty)一眼就能看出是根据朝代返回古钱卡片的主题色;getReviewTagColor(tag)则是根据评价标签返回标签颜色。
特别值得分析的是几个函数的设计逻辑:
getDynastyColor按朝代分组赋色:唐/清同属"盛世王朝"共享鎏金色;宋/明同属"文治高峰"共享铜锈绿;战国独享暗红色(因其年代久远、存世稀少);汉及默认使用藏蓝色。这种分组既考虑了历史认知,又保持了色彩的视觉平衡。getPriceColor采用阶梯式阈值设计:价格≥1500返回暗红(danger色,暗示"高端珍品"如刀币¥2380、货布¥1280),800-1499返回鎏金色(中高端如大观通宝¥1580),400-799返回铜绿色(中档如五铢¥360),<400返回藏蓝色(入门级如乾隆通宝¥260)。这种将业务语义(价格区间)映射到视觉语义(颜色情感)的模式,是数据可视化中的经典手法。getReviewTagColor将12种评价标签分为四组,每组赋予不同的主题色,形成了一套完整的"评价情感色谱"。
九、主入口组件:应用骨架与路由管理
9.1 状态声明
@Entry
@Component
struct CoinApp {
@State curTab: number = 0;
@State data: CoinData = new CoinData();
@State showAddCoin: boolean = false;
@State showEditOrder: boolean = false;
@State showDeleteMold: boolean = false;
@State showDetailCase: boolean = false;
@State delMoldName: string = '';
@State detailCaseName: string = '';
@State coinSpin: boolean = false;
@State fireFloat: boolean = false;
CoinApp是整个应用的根组件,由@Entry装饰器标记为应用入口,由@Component标记为可复用组件。它声明了11个@State状态变量,可以分为三类:
路由与数据状态(2个):
curTab:当前选中的Tab索引(0-4),控制底部导航栏的高亮和内容区的切换。data:CoinData实例,作为全局唯一的数据源传递给所有子组件。
弹窗显隐状态(4个布尔值 + 2个字符串):
showAddCoin/showEditOrder/showDeleteMold/showDetailCase:分别控制四种弹窗的显示/隐藏。delMoldName/detailCaseName:传递给删除确认弹窗和详情弹窗的上下文参数(要撤下的范模名、要查看详情的匣名)。
动画交互状态(2个布尔值):
coinSpin:控制头部古钱图标的旋转动画(点击切换180°旋转)。fireFloat:控制火焰图标的浮动动画(点击切换上下位移)。
这种细粒度的状态拆分,使得每个UI交互都有独立的状态变量驱动,互不干扰。当需要添加新的弹窗或交互效果时,只需新增一个@State变量即可,不会影响现有逻辑。
9.2 构建方法与页面结构
build()方法是ArkTS声明式UI的核心——它描述了"UI应该长什么样",框架负责在状态变化时高效地diff和更新真实DOM。CoinApp的build()方法构建了以下三层结构:
第一层:主内容区(外层Column)。包含两个子区域:
- 顶部Header区域:深铜绿色(
primaryDark#2F4A3C)背景,包含应用标题"🪙 古钱铺"、英文副标题"Coin · 外圆内方 通宝千年"、右侧操作图标区(古钱旋转动画、火焰浮动动画、钱匣图标),以及下方的12个钱币点缀点(用三种尺寸和颜色的圆形模拟钱串排列——每3个一组循环:金色大圆代表铜钱本体、铜绿小圆代表穿绳孔位、暗红小圆代表磨损痕迹)。底部还有一条分隔线配文字"🔥 炉火千载 · 通宝万年 🪙"。 - 内容切换区:根据
curTab的值,使用if-else if-else链条件渲染对应的内容组件(CoinContent/MoldContent/CaseContent/CoinOrderContent/CoinReviewContent)。
第二层:底部导航栏(固定在底部)。顶部有一条3px高的鎏金色(accent)装饰线,下方使用ForEach遍历TAB_LIST数组渲染5个Tab项,每个Tab项根据是否为当前选中项来决定:文字颜色(主题色 vs 提示色)、字重(Bold vs Normal)、背景色(半透明主题色 vs 透明)、边框(有 vs 无)。点击时更新curTab状态触发切换。
第三层:弹窗叠加层。四个if条件分别控制四种弹窗的渲染,弹窗以绝对定位的方式覆盖在主内容区上方。
9.3 弹窗的条件渲染
if (this.showAddCoin) {
AddCoinModal({ onClose: () => { this.showAddCoin = false; } })
}
if (this.showEditOrder) {
EditCoinOrderModal({ onClose: () => { this.showEditOrder = false; } })
}
if (this.showDeleteMold) {
DeleteMoldModal({
title: this.delMoldName,
onClose: () => { this.showDeleteMold = false; }
})
}
if (this.showDetailCase) {
DetailCaseModal({
name: this.detailCaseName,
onClose: () => { this.showDetailCase = false; }
})
}
弹窗的渲染采用了条件渲染 + 回调关闭的模式。每个弹窗组件接收一个onClose回调函数,当用户点击弹窗内的"取消"/"确认"/"知道了"等按钮时,调用onClose()将对应的showXxx状态设为false,从而触发弹窗从DOM中移除。
DeleteMoldModal和DetailCaseModal额外接收了title/name属性,用于在弹窗内显示具体的操作对象(要撤下哪件范模、查看哪个钱匣的详情)。这种"状态提升"(Lifting State Up)的模式——将弹窗所需的上下文数据存储在父组件的状态中、通过props传递给弹窗——是React/ArkTS中处理跨组件数据流的经典方案。

十、内容组件体系:@ObjectLink与数据双向绑定
10.1 标签组件 CoinTag
@Component
struct CoinTag {
@Prop text: string;
@Prop color: string;
build() {
Text(this.text)
.fontSize(9)
.fontColor(this.color)
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
.backgroundColor(this.color + '14') // 8% 透明度
.borderRadius(10)
.border({ width: 1, color: this.color + '40' }) // 25% 透明度边框
}
}
CoinTag是一个极简但精心设计的标签组件,仅接收两个@Prop属性:text(标签文字)和color(主题色)。它的设计精髓在于颜色透明度的十六进制拼接技巧:
- 背景色使用
this.color + '14',即在原色hex后面追加14(即8%不透明度的alpha通道),生成类似#B0893F14的8位颜色值。ArkTS原生支持这种8位hex颜色格式,前6位是RGB,后2位是Alpha(00=完全透明,FF=完全不透明)。 - 边框色使用
this.color + '40'(25%不透明度),比背景更明显但不抢眼。
这种"同色系多层透明度叠加"的技法,使得标签在任何背景下都能保持和谐的视觉效果——背景色淡淡的(8%)提供区域感,边框稍深(25%)勾勒轮廓,文字用原色(100%)保证可读性。三者来自同一个color变量,天然协调,永远不会出现"背景是蓝色但文字是红色"的色彩冲突。
10.2 古钱内容组件 CoinContent
@Component
struct CoinContent {
@ObjectLink data: CoinData;
onAdd: () => void = () => {};
build() {
Scroll() {
Column() {
// 销量图表卡片
Column() { /* ... */ }
// 古钱列表
ForEach(this.data.coins, (p: CoinItem, i: number) => {
Row() { /* 卡片内容 */ }
}, (p: CoinItem, i: number) => 'cn' + p.name + i)
}
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
}
}
CoinContent是古钱Tab页的主体内容组件。它使用了@ObjectLink装饰器来接收父组件传递的CoinData实例。@ObjectLink与@Prop的关键区别在于:@ObjectLink建立的是引用关系,而非值的拷贝——当CoinData实例的属性(如coins数组)发生变化时,CoinContent中引用这些属性的部分会自动刷新。这使得数据在父子组件间实现了"双向绑定"的效果。
组件内部由两大部分组成:
第一部分:本周销量图表卡片。顶部有一排装饰性的钱串点缀(模仿12枚铜钱穿成一串的视觉效果——金色大圆代表铜钱、铜绿小圆代表穿孔、暗红小圆代表锈迹),下方是标题"📈 本周古钱销量"和副标题"周六65枚"。柱状图使用ForEach遍历WEEK_SOLD数据,每根柱子的高度由value决定(直接使用value作为像素高度,即65px为最高),颜色由阈值判断决定(≥60用鎏金色accent,否则用铜绿色primary)。柱子使用borderRadius(8)实现圆角,底部对齐(alignItems(VerticalAlign.Bottom))模拟真实柱状图的基线对齐效果。
第二部分:古钱陈列列表。使用ForEach遍历this.data.coins数组,每张卡片是一个Row横向布局,包含三个区域:
- 左侧图标区(宽60):圆形头像区显示emoji(如🪙🏮⚖️等),下方有一个带边框的小方形模拟"外圆内方"的钱币造型(silkWhite底色 + 朝代色边框)。整体背景色由
getDynastyColor(p.dynasty)决定并附加极低透明度(0D=5%),边框用33%透明度。 - 中间信息区(
layoutWeight(1)占据剩余空间):第一行是钱名(粗体)+ 价格(带颜色,由getPriceColor决定),第二行是朝代标签(CoinTag,颜色由getDynastyColor决定)+ 工艺标签(CoinTag,固定success色)+ "外圆内方"提示文字。 - 卡片背景色使用
i % 2 === 0实现斑马纹交替(白色/浅褐),增强列表的可读性。

10.3 其他内容组件
MoldContent(范模库):结构与CoinContent类似,但顶部图表替换为"古钱销量TOP6"水平条形图(使用COIN_TOP数据和layoutWeight实现比例条),列表项改为范模卡片——左侧显示emoji+精度圆点+白色点缀,中间显示范模名+材质,下方显示"铸模精度"进度条(同样用layoutWeight(c.level)实现比例填充,base=100)。右侧有删除图标🗑️,点击触发onDel回调。
CaseContent(钱匣坊):顶部图表为"铸造工艺热度"水平条形图(COIN_HOT数据),列表项为钱匣卡片——显示匣名+品级,下方显示木料标签(CoinTag)+ 产地信息。右侧有详情图标👁️,点击触发onDetail回调。
CoinOrderContent(订单台账):顶部图表为"朝代构成占比"水平条形图(DYNASTY_SHARE数据,注意其base=20而非100,因为count范围2-5),列表项为订单卡片——显示客户emoji+名称+区域+数量+金额(金额颜色由getOrderColor决定),右侧有编辑图标✏️。
CoinReviewContent(客户评价):无顶部图表,直接是评价列表。每条评价显示用户emoji+姓名+日期+星级(用⭐.repeat(score)重复渲染)+ 标签(CoinTag,颜色由getReviewTagColor决定)+ 评价正文。星级使用repeat()方法根据score值动态生成对应数量的星星字符,简洁而巧妙。
十一、弹窗组件体系:自定义模态对话框
应用包含四个自定义弹窗组件,它们共享一致的结构模式但各有不同的内容和用途:
11.1 新增古钱弹窗 AddCoinModal
弹窗采用全屏遮罩 + 居中卡片的经典模态布局:
- 外层Column:
width('100%') height('100%'),背景色#66000000(40%黑色半透明遮罩),justifyContent(FlexAlign.Center)使内容居中。 - 内层卡片:宽度86%、最大高度78%、圆角18px、白色背景、16px内边距。
- 标题栏:"🪙 定制古钱" + 右侧"✕"关闭按钮,两端对齐。
- 内容区:三行信息展示(品名"开元通宝"、钱文"唐 · 翻砂 · 隶书"、预算"¥880"),每行使用
cardAlt背景色+圆角的统一样式。 - 操作栏:左"取消"(描边按钮)+ 右"确认铸钱"(实心主色按钮),右对齐。
11.2 其他弹窗组件
- EditCoinOrderModal(修改订单):结构与AddCoinModal完全对称,内容改为客户"博物馆文创"、数量"900 → 1000 枚"、金额"¥500000"三行信息,确认按钮文案为"保存修改"。
- DeleteMoldModal(撤下范模确认):接收
title属性显示要撤下的范模名,内容为确认提示文字("确定撤下「xxx」吗?" + "撤下的范模将从范模库移除。"),确认按钮使用危险色(danger= 暗红#8E3B36)以警示操作的不可逆性。 - DetailCaseModal(钱匣详情):接收
name属性,内容包含大号emoji🗃️+匣名展示区、钱匣文化介绍文字("选老料整木开板,榫卯相接不用一钉,内衬绒布防磨,盖面镶铜扣铜环。铜钱入匣叮当作响,一匣藏尽千秋通宝。")、木料产地信息"苏州 · 老料坊"、匠人评级"甲级 · 木作卅载",单按钮"知道了"关闭。
四个弹窗的一致性设计(相同的外层遮罩、卡片尺寸、圆角18px、内边距16px、标题栏布局)确保了用户在使用不同功能时获得统一的交互体验。差异仅在内容区和按钮文案/颜色上体现各自的功能特性。
十二、关键技术点深度解析
12.1 layoutWeight布局权重的妙用
layoutWeight是ArkTS弹性布局中最强大的属性之一。在本应用中被广泛用于两类场景:
水平条形图:在COIN_TOP、COIN_HOT、DYNASTY_SHARE的渲染中,每行的彩色条使用layoutWeight(dataValue)设置宽度权重,后面的灰色占位条使用layoutWeight(maxValue - dataValue)补足剩余空间。两者之和等于最大值(TOP和HOT为100,DYNASTY_SHARE为20),从而精确实现按比例分配宽度的效果——无需手动计算像素值,框架自动处理。特别值得一提的是DYNASTY_SHARE使用了base=20而非max(count)=5,这使得占比条的视觉差异更加明显(5/20=25% vs 5/5=100%),避免了所有条都几乎一样长的尴尬。
列表卡片内部布局:古钱卡片、范模卡片等信息区的中间列使用layoutWeight(1)占据所有剩余空间,左右固定宽度的图标区和操作区保持不变。这种"两头固定、中间弹性"的布局模式在移动端UI中极为常见。
12.2 颜色透明度的十六进制拼接
本应用大量使用了hex颜色 + 两位alpha的8位颜色格式,例如:
COLORS.primaryLight + '2E'→#8AA9962E(约18%不透明度,用于操作按钮背景)COLORS.primaryLight + '66'→#8AA99666(约40%不透明度,用于分割线)COLORS.accent + '55'→#B0893F55(约33%不透明度,用于卡片边框)t.color + '14'→ 如#B0893F14(约8%不透明度,用于标签背景)
这种技法的优势在于:不需要预先定义几十个半透明色变量,而是根据运行时的主题色动态生成任意透明度的衍生色。只要知道基础色和目标透明度,字符串拼接一行搞定。需要注意的是,alpha值的范围是00(全透明)到FF(完全不透明),常用参考值:14≈8%,2E≈18%,40≈25%,55≈33%,66≈40%,80≈50%。
12.3 ForEach的键值生成策略
应用中所有的ForEach循环都精心设计了键值生成函数(key generator):
// 古钱列表
(p: CoinItem, i: number) => 'cn' + p.name + i
// 范模列表
(c: MoldItem, i: number) => 'md' + c.name + i
// 钱匣列表
(b: CaseItem, i: number) => 'cs' + b.name + i
// 订单列表
(o: CoinOrderItem, i: number) => 'od' + o.name + i
// 评价列表
(r: CoinReviewItem, i: number) => 'rv' + r.name + i
// Tab栏
(t: TabMeta) => t.key
// 图表数据
(w: WeekCoinMeta) => w.day
(t: CoinTopMeta) => t.name
(d: CoinHotMeta) => d.name
(s: DynastyShareMeta) => s.name
每个键值都采用了**"前缀 + 唯一标识 + 索引"**的三段式命名:
- 前缀(
cn/md/cs/od/rv)区分不同的ForEach作用域,避免跨列表键值冲突。 - 唯一标识(
p.name/c.name等)保证同一条数据始终映射到同一个键。 - 索引(
i)作为兜底,即使两条数据name相同(理论上不应发生但有防御价值),索引也能保证唯一性。
正确的键值策略对于ArkTS的ForEach性能至关重要——框架通过键值来判断哪些节点是"新增"、哪些是"移动"、哪些是"删除",从而执行最小量的DOM操作。错误的键值(如使用Math.random())会导致每次渲染都销毁重建所有节点,造成严重的性能问题。
12.4 @Observed/@ObjectLink响应式机制
本应用的响应式数据流遵循以下路径:
CoinData (@Observed 类)
↓ @ObjectLink 引用传递
CoinContent / MoldContent / CaseContent / CoinOrderContent / CoinReviewContent
↓ 组件内部读取属性
ForEach / Text / 等UI节点
当CoinData的某个数组属性发生变化(如coins.push(newItem)),ArkTS框架的响应式系统会:
- 检测到
@Observed类实例的属性变更。 - 通知所有持有该实例
@ObjectLink引用的组件。 - 这些组件重新执行
build()方法。 - 框架diff新旧虚拟DOM树,只更新实际变化的部分。
这种机制比传统的"手动调用setState/refresh"模式更加声明式和自动化,开发者只需描述"数据是什么样子",框架负责"如何高效地更新UI"。
12.5 条件渲染与状态驱动UI
应用的Tab切换和弹窗显示/隐藏完全由状态变量驱动,没有使用任何命令式的DOM操作(如appendChild/removeChild):
- Tab切换:
if (this.curTab === CoinTab.COIN)渲染CoinContent,否则渲染其他组件。curTab的变化由底部Tab栏的onClick事件触发。 - 弹窗显示:
if (this.showAddCoin)渲染AddCoinModal。showAddCoin由"➕铸钱"按钮的onClick设为true,由弹窗内部的"取消"/"确认"按钮的onClick通过onClose回调设为false。
此外,Header区域的两个交互动画也是状态驱动的:
- 古钱图标旋转:
this.coinSpin ? 180 : 0角度 +animation({ duration: 720, curve: Curve.EaseOut }) - 火焰图标浮动:
this.fireFloat ? -10 : 4Y轴偏移 +animation({ duration: 640, curve: Curve.EaseOut })
这种"状态即UI"(State is UI)的范式是现代前端框架的核心思想——UI是状态的纯函数,给定相同的状态必然产生相同的UI。这使得应用的行为完全可预测、可调试、可测试。
十三、技术要点对比表
| 技术维度 | 本应用实现方案 | 设计意图与优势 |
|---|---|---|
| 状态管理 | @State(11个) + @Observed类 + @ObjectLink引用 |
单一数据源 + 自动响应式更新,避免手动同步 |
| 组件通信 | Props单向数据流 + 回调函数(onClose/onAdd/onDel/onDetail/onEdit) | 父→子通过Props,子→父通过回调,清晰的单向数据流 |
| 路由方案 | if-else if-else条件渲染 + 枚举索引 |
适合Tab数量固定(5个)的场景,简单直接无额外依赖 |
| 弹窗模式 | 条件渲染 + 全屏遮罩 + 居中卡片 + 回调关闭 | 统一的模态交互规范,遮罩阻止背景交互 |
| 列表渲染 | ForEach + 三段式键值('prefix'+name+index) |
高效diff、防冲突、可预测的节点复用 |
| 布局策略 | layoutWeight弹性权重 + 固定像素结合 |
两头固定中间弹性的经典移动端布局 |
| 颜色系统 | 21色接口约束 + hex+alpha透明度拼接 | 编译期类型安全 + 运行时动态透明度 |
| 数据建模 | 5个业务接口 + 4个图表接口 + 纯函数颜色映射 | 类型安全 + 逻辑集中 + 可独立测试 |
| 动画交互 | .rotate() + .translate() + .animation() 声明式动画 |
状态驱动动画,无需手动管理动画生命周期 |
| 斑马纹列表 | i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt |
偶数行白色、奇数行浅褐,增强列表可读性 |
十四、总结与展望
14.1 架构设计总结
"古钱铺"应用虽然是一个单文件Demo,但其内部蕴含的架构思想值得认真总结:
分层清晰,职责分明。从顶部的类型定义层到底部的子组件层,每一层都有明确的单一职责。类型定义层管"契约",常量实例层管"数据",响应式类管"状态",工具函数层管"逻辑",入口组件管"路由",子组件管"展示"。这种分层使得无论是添加新功能、修bug、还是做性能优化,都能快速定位到正确的层次。
接口先行,类型安全。所有数据结构都先定义接口,再实现具体数据。TypeScript编译器在编译阶段就能捕获大部分类型错误,大大降低了运行时出错的概率。21色调色板接口、5组业务实体接口、4组图表接口——合计超过10个接口定义,构筑了坚实的类型安全防线。
数据驱动,声明式渲染。UI完全由状态驱动,没有命令式的DOM操作。ForEach遍历数据数组生成列表,if条件根据状态渲染不同组件,@Observed/@ObjectLink自动追踪数据变化并刷新UI。这种范式让代码更接近"描述UI应该是什么样子"而非"如何一步步构建UI"。
14.2 技术亮点回顾
回顾全文,以下几个技术点尤其值得关注和借鉴:
- 21色古风色彩体系:以"铜锈绿·鎏金·朱砂红"为核心的三色系设计,配合精细的透明度梯度(8%/18%/25%/33%/40%),在视觉上完美还原了古钱币文化的温润质感与厚重底蕴。接口约束 + 常量实例化的双层设计,兼顾了类型安全和运行效率。
@Observed+@ObjectLink的深度运用:将全部业务数据封装在一个可观察类中,通过@ObjectLink引用传递给5个子组件,实现了真正的"单一数据源 + 双向绑定"。任何数据变化都会自动级联刷新所有相关UI,无需手动编写事件监听。- 纯函数颜色映射层:5个无副作用的颜色映射函数,将业务规则(朝代分类、精度等级、价格区间、订单金额、评价标签)映射到视觉语义(颜色情感)。逻辑集中、可测试、可复用。
layoutWeight驱动的数据可视化:不依赖任何图表库,仅用ArkTS内置的layoutWeight属性就实现了4种精美的条形图/柱状图(周销量垂直柱状图、TOP6排行、工艺热度、朝代占比)。这种"零依赖"的数据可视化方案在轻量级应用中极具实用价值。特别值得一提的是DYNASTY_SHARE使用了base=20的技巧来放大视觉差异。- 统一的弹窗交互规范:4个弹窗共享相同的结构模板(遮罩+居中卡片+标题栏+内容区+操作栏),仅在内容和按钮细节上差异化。其中
DeleteMoldModal的危险色按钮设计和DetailCaseModal的文化文案撰写,体现了对不同交互场景的精细化处理。
14.3 优化建议与展望
尽管本应用在架构和实现上已经相当成熟,但从工程实践的角度,仍有以下几点可以进一步优化:
第一,状态管理的精细化。当前11个@State变量全部集中在根组件中,随着功能增长可能导致"状态爆炸"。建议引入轻量级状态管理方案(如ArkTS的@Provide/@Consume或AppStorage),将弹窗状态下沉到各自的父组件中,根组件只保留全局路由状态(curTab和data)。
第二,网络数据对接的准备。当前所有数据均为静态硬编码。CoinData类的数组属性已经为未来对接真实后端API留出了清晰的接口——只需将静态初始化改为异步fetch调用,UI层无需任何修改。例如coins可以从/api/coins获取,orders可以从/api/orders获取。这种"函数化的封装方式"体现了良好的架构前瞻性。
第三,组件的进一步拆分。当前每个内容组件(如CoinContent)内部同时包含了图表卡片和列表卡片两种UI形态。如果未来需要单独复用"周销量图表"或"古钱陈列列表",可以考虑将它们拆分为独立的子组件(WeeklyChart、CoinList),提升复用性。同样,顶部的四种图表卡片也可以抽取为一个通用的ChartCard泛型组件。
第四,无障碍访问(Accessibility)的支持。当前组件未设置accessibilityText等无障碍属性。对于一款面向文化传播的应用,确保视障用户也能通过屏幕阅读器获取古钱信息(如"开元通宝,唐朝,价格880元"),是产品完善度的重要维度。
第五,国际化(i18n)的预留。虽然"古钱铺"面向中文用户,但如果未来需要推出多语言版本(如面向海外收藏家的英文版"Ancient Coin Shop"),建议将所有硬编码的中文字符串提取到资源文件(string.json)中,通过$r()引用。当前的TAB_LIST、静态数据中的中文字符串都需要做相应改造。
总而言之,"古钱铺"应用以其精巧的古风色彩体系(铜锈绿·鎏金·朱砂红)、严谨的类型安全设计、高效的响应式数据流和优雅的组件化架构,为我们提供了一个学习鸿蒙ArkTS声明式UI开发的优秀范本。从"能写出能跑的代码"到"写出架构清晰、可维护、可扩展的代码",这正是每一位鸿蒙开发者应当追求的成长路径。愿这篇深度解析能为你的进阶之路提供一份有价值的参考——正如每一枚古钱都承载着一段历史,每一行代码也都应当承载着一份匠心。
十五、附录:DevEco Studio 安装与环境搭建
为方便读者将本文分析的"古钱铺"代码真正跑起来,下面补充一份 DevEco Studio 的安装与环境配置简明教程(配图来源于官方文档)。
安装DevEco Studio程序

选择目标安装目录:

设置环境变量,设置完务必重启:

新建一个空白模板:

设置API为24的模板项目:

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

完整代码
interface CoinPalette {
primary: string;
primaryLight: string;
primaryDark: string;
accent: string;
accentLight: string;
bg: string;
cardBg: string;
cardAlt: string;
textPrimary: string;
textSecondary: string;
textHint: string;
border: string;
line: string;
success: string;
warning: string;
danger: string;
white: string;
gold: string;
silkWhite: string;
}
const COLORS: CoinPalette = {
primary: '#4E7A63',
primaryLight: '#8AA996',
primaryDark: '#2F4A3C',
accent: '#B0893F',
accentLight: '#D2B477',
bg: '#F1EFE7',
cardBg: '#FFFFFF',
cardAlt: '#E9E3D3',
textPrimary: '#2E332B',
textSecondary: '#6E7264',
textHint: '#A09F90',
border: '#D8D4C2',
line: '#E7E4D8',
success: '#4E7BA8',
warning: '#C98A4E',
danger: '#8E3B36',
white: '#FFFFFF',
gold: '#B0893F',
silkWhite: '#F6F2E8'
};
enum CoinTab {
COIN = 0,
MOLD = 1,
CASE = 2,
ORDER = 3,
REVIEW = 4
}
interface TabMeta {
key: string;
icon: string;
label: string;
color: string;
}
const TAB_LIST: TabMeta[] = [
{ key: 'coin', icon: '🪙', label: '古钱', color: '#B0893F' },
{ key: 'mold', icon: '🔨', label: '范模', color: '#4E7A63' },
{ key: 'case', icon: '🗃️', label: '钱匣', color: '#8E3B36' },
{ key: 'order', icon: '📦', label: '订单', color: '#4E7BA8' },
{ key: 'review', icon: '⭐', label: '客评', color: '#C98A4E' }
];
const COIN_COL: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
interface CoinItem {
name: string;
dynasty: string;
craft: string;
price: number;
emoji: string;
}
interface MoldItem {
name: string;
material: string;
level: number;
place: string;
emoji: string;
}
interface CaseItem {
name: string;
wood: string;
level: number;
place: string;
emoji: string;
}
interface CoinOrderItem {
name: string;
region: string;
count: number;
amount: number;
emoji: string;
}
interface CoinReviewItem {
name: string;
score: number;
date: string;
content: string;
tag: string;
emoji: string;
}
interface WeekCoinMeta {
day: string;
value: number;
}
interface DynastyShareMeta {
name: string;
count: number;
color: string;
}
interface CoinHotMeta {
name: string;
heat: number;
color: string;
}
interface CoinTopMeta {
name: string;
sold: number;
color: string;
}
const WEEK_SOLD: WeekCoinMeta[] = [
{ day: '周一', value: 20 },
{ day: '周二', value: 28 },
{ day: '周三', value: 36 },
{ day: '周四', value: 44 },
{ day: '周五', value: 57 },
{ day: '周六', value: 65 },
{ day: '周日', value: 59 }
];
const DYNASTY_SHARE: DynastyShareMeta[] = [
{ name: '唐', count: 5, color: '#B0893F' },
{ name: '宋', count: 4, color: '#4E7A63' },
{ name: '清', count: 3, color: '#8E3B36' },
{ name: '汉', count: 2, color: '#4E7BA8' }
];
const COIN_HOT: CoinHotMeta[] = [
{ name: '翻砂铸钱', heat: 96, color: '#B0893F' },
{ name: '母钱修模', heat: 95, color: '#4E7A63' },
{ name: '范铸叠铸', heat: 94, color: '#8E3B36' },
{ name: '钱文篆刻', heat: 93, color: '#D2B477' },
{ name: '鎏金锉边', heat: 91, color: '#4E7BA8' },
{ name: '包浆养护', heat: 90, color: '#C98A4E' }
];
const COIN_TOP: CoinTopMeta[] = [
{ name: '开元通宝', sold: 97, color: '#B0893F' },
{ name: '五铢钱', sold: 93, color: '#4E7A63' },
{ name: '乾隆通宝', sold: 90, color: '#8E3B36' },
{ name: '大观通宝', sold: 87, color: '#D2B477' },
{ name: '崇宁通宝', sold: 84, color: '#4E7BA8' },
{ name: '刀币', sold: 80, color: '#C98A4E' }
];
@Observed
export class CoinData {
coins: CoinItem[] = [
{ name: '开元通宝', dynasty: '唐', craft: '翻砂', price: 880, emoji: '🪙' },
{ name: '乾隆通宝', dynasty: '清', craft: '翻砂', price: 260, emoji: '🏮' },
{ name: '五铢钱', dynasty: '汉', craft: '范铸', price: 360, emoji: '⚖️' },
{ name: '半两钱', dynasty: '汉', craft: '范铸', price: 680, emoji: '🪨' },
{ name: '崇宁通宝', dynasty: '宋', craft: '翻砂', price: 720, emoji: '🏯' },
{ name: '大观通宝', dynasty: '宋', craft: '母钱', price: 1580, emoji: '🦅' },
{ name: '永乐通宝', dynasty: '明', craft: '翻砂', price: 560, emoji: '🌏' },
{ name: '顺治通宝', dynasty: '清', craft: '翻砂', price: 420, emoji: '🐉' },
{ name: '光绪通宝', dynasty: '清', craft: '机制', price: 380, emoji: '🐎' },
{ name: '货布', dynasty: '汉', craft: '叠铸', price: 1280, emoji: '🪓' },
{ name: '刀币', dynasty: '战国', craft: '范铸', price: 2380, emoji: '🗡️' },
{ name: '布币', dynasty: '战国', craft: '范铸', price: 1880, emoji: '🌾' }
];
molds: MoldItem[] = [
{ name: '陶范', material: '陶', level: 96, place: '洛阳', emoji: '🏺' },
{ name: '石范', material: '石', level: 93, place: '临淄', emoji: '🪨' },
{ name: '铜范', material: '铜', level: 95, place: '长安', emoji: '🥉' },
{ name: '母钱范', material: '精铜', level: 96, place: '建业', emoji: '🔱' },
{ name: '子范', material: '陶', level: 92, place: '成都', emoji: '🥣' },
{ name: '叠铸范', material: '陶', level: 94, place: '长沙', emoji: '📚' },
{ name: '翻砂模', material: '砂', level: 95, place: '汴京', emoji: '🏖️' },
{ name: '雕母', material: '精铜', level: 97, place: '北京', emoji: '🎯' },
{ name: '铁范', material: '铁', level: 93, place: '邯郸', emoji: '⚙️' },
{ name: '泥范', material: '泥', level: 90, place: '南阳', emoji: '🧱' },
{ name: '钱树范', material: '陶', level: 92, place: '洛阳', emoji: '🌳' },
{ name: '蜡模', material: '蜡', level: 94, place: '杭州', emoji: '🕯️' }
];
cases: CaseItem[] = [
{ name: '红木钱匣', wood: '红木', level: 96, place: '苏州', emoji: '🪵' },
{ name: '楠木钱匣', wood: '金丝楠', level: 95, place: '成都', emoji: '🌲' },
{ name: '锦盒', wood: '织锦', level: 94, place: '南京', emoji: '🧧' },
{ name: '铜钱匣', wood: '黄铜', level: 93, place: '大理', emoji: '🥉' },
{ name: '竹编钱篓', wood: '竹', level: 92, place: '安吉', emoji: '🎋' },
{ name: '漆器钱匣', wood: '大漆', level: 95, place: '福州', emoji: '🖌️' },
{ name: '樟木匣', wood: '樟木', level: 94, place: '宜春', emoji: '🍃' },
{ name: '书匣', wood: '花梨', level: 93, place: '海南', emoji: '📚' },
{ name: '抽屉匣', wood: '乌木', level: 92, place: '东阳', emoji: '🗄️' },
{ name: '宝匣', wood: '紫檀', level: 96, place: '北京', emoji: '💰' },
{ name: '圆匣', wood: '楠木', level: 93, place: '扬州', emoji: '⭕' },
{ name: '首饰匣', wood: '檀木', level: 94, place: '广州', emoji: '🗃️' }
];
orders: CoinOrderItem[] = [
{ name: '博物馆文创', region: '华北', count: 900, amount: 450000, emoji: '🏛️' },
{ name: '收藏协会', region: '华东', count: 500, amount: 750000, emoji: '🎖️' },
{ name: '钱币研究所', region: '华北', count: 400, amount: 800000, emoji: '🔬' },
{ name: '拍卖行', region: '华东', count: 300, amount: 900000, emoji: '🔨' },
{ name: '海外藏家', region: '海外', count: 350, amount: 875000, emoji: '🌏' },
{ name: '古玩城店铺', region: '华中', count: 600, amount: 360000, emoji: '🏮' },
{ name: '直播鉴宝', region: '华东', count: 750, amount: 375000, emoji: '📺' },
{ name: '文创礼盒商', region: '华南', count: 650, amount: 390000, emoji: '🎁' },
{ name: '大学历史系', region: '华北', count: 450, amount: 225000, emoji: '🎓' },
{ name: '电视剧组', region: '华北', count: 500, amount: 250000, emoji: '🎬' },
{ name: '钱庄博物馆', region: '华东', count: 550, amount: 440000, emoji: '🏦' },
{ name: '私人典藏', region: '海外', count: 250, amount: 625000, emoji: '💎' }
];
reviews: CoinReviewItem[] = [
{ name: '博物馆馆长', score: 5, date: '09-03', content: '开元通宝品相完整,展柜补位正合适。', tag: '品相完整', emoji: '🏛️' },
{ name: '收藏家', score: 5, date: '09-02', content: '雕母钱范稀有难得,藏友都来观摩。', tag: '稀世难得', emoji: '🎖️' },
{ name: '研究员', score: 5, date: '09-01', content: '货布形制标准,比标本图册还清晰。', tag: '形制标准', emoji: '🔬' },
{ name: '拍卖师', score: 5, date: '08-31', content: '大观通宝瘦金体精美,落槌价喜人。', tag: '钱文精美', emoji: '🔨' },
{ name: '海外藏家', score: 5, date: '08-30', content: '五铢钱包浆自然,海外出货很受欢迎。', tag: '包浆自然', emoji: '🌏' },
{ name: '古玩店主', score: 4, date: '08-29', content: '崇宁通宝量足价实,若附拓片更佳。', tag: '量足价实', emoji: '🏮' },
{ name: '鉴宝主播', score: 5, date: '08-28', content: '半两钱走量飞快,鉴宝专场人气爆棚。', tag: '走量飞快', emoji: '📺' },
{ name: '礼盒经理', score: 5, date: '08-27', content: '文创礼盒套装精致,开箱即见用心。', tag: '开箱用心', emoji: '🎁' },
{ name: '历史系教授', score: 4, date: '08-26', content: '布币授课教具齐全,若加铭文考释更好。', tag: '教具齐全', emoji: '🎓' },
{ name: '道具师', score: 5, date: '08-25', content: '顺治通宝做道具质感足,镜头下很真实。', tag: '质感真实', emoji: '🎬' },
{ name: '钱庄馆长', score: 5, date: '08-24', content: '钱匣做工考究,展陈效果极佳。', tag: '做工考究', emoji: '🏦' },
{ name: '老主顾', score: 5, date: '08-23', content: '收第三匣古钱了,一币一史,越藏越有味。', tag: '一币一史', emoji: '💎' }
];
}
function getDynastyColor(dynasty: string): string {
if (dynasty === '唐' || dynasty === '清') {
return '#B0893F';
} else if (dynasty === '宋' || dynasty === '明') {
return '#4E7A63';
} else if (dynasty === '战国') {
return '#8E3B36';
}
return '#4E7BA8';
}
function getMoldColor(level: number): string {
if (level >= 95) {
return '#B0893F';
} else if (level >= 93) {
return '#4E7A63';
}
return '#8E3B36';
}
function getPriceColor(price: number): string {
if (price >= 1500) {
return '#8E3B36';
} else if (price >= 800) {
return '#B0893F';
} else if (price >= 400) {
return '#4E7A63';
}
return '#4E7BA8';
}
function getOrderColor(amount: number): string {
if (amount >= 800000) {
return '#8E3B36';
} else if (amount >= 600000) {
return '#B0893F';
} else if (amount >= 400000) {
return '#4E7A63';
}
return '#4E7BA8';
}
function getReviewTagColor(tag: string): string {
if (tag === '品相完整' || tag === '形制标准' || tag === '一币一史' || tag === '做工考究') {
return '#B0893F';
} else if (tag === '稀世难得' || tag === '钱文精美' || tag === '包浆自然') {
return '#4E7A63';
} else if (tag === '量足价实' || tag === '走量飞快' || tag === '开箱用心' || tag === '质感真实') {
return '#8E3B36';
} else if (tag === '教具齐全') {
return '#4E7BA8';
}
return '#C98A4E';
}
@Entry
@Component
struct CoinApp {
@State curTab: number = 0;
@State data: CoinData = new CoinData();
@State showAddCoin: boolean = false;
@State showEditOrder: boolean = false;
@State showDeleteMold: boolean = false;
@State showDetailCase: boolean = false;
@State delMoldName: string = '';
@State detailCaseName: string = '';
@State coinSpin: boolean = false;
@State fireFloat: boolean = false;
@Builder modalOverlay(onClose: () => void) {
Column() {
Text('')
.width(0)
.height(0)
.opacity(0)
Button('')
.width(1)
.height(1)
.opacity(0)
.onClick(() => {
onClose();
})
}
.width(1)
.height(1)
}
build() {
Column() {
Column() {
Column() {
Row() {
Column() {
Text('🪙 古钱铺')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
Text('Coin · 外圆内方 通宝千年')
.fontSize(10)
.fontColor(COLORS.primaryLight)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Row() {
Text('🪙')
.fontSize(18)
.onClick(() => {
this.coinSpin = !this.coinSpin;
})
.rotate({ angle: this.coinSpin ? 180 : 0 })
.animation({ duration: 720, curve: Curve.EaseOut })
Text('🔥')
.fontSize(16)
.margin({ left: 10 })
.onClick(() => {
this.fireFloat = !this.fireFloat;
})
.translate({ x: 0, y: this.fireFloat ? -10 : 4 })
.animation({ duration: 640, curve: Curve.EaseOut })
Text('🗃️')
.fontSize(14)
.margin({ left: 6 })
}
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.backgroundColor(COLORS.primaryLight + '2E')
.borderRadius(24)
.border({ width: 1, color: COLORS.primaryLight + '80' })
}
.width('100%')
Row() {
ForEach(COIN_COL, (r: number) => {
Row() {
if (r % 3 === 0) {
Column()
.width(12)
.height(12)
.backgroundColor(COLORS.gold)
.borderRadius(6)
.border({ width: 2, color: COLORS.silkWhite })
} else if (r % 3 === 1) {
Column()
.width(8)
.height(8)
.backgroundColor(COLORS.primary)
.borderRadius(4)
} else {
Column()
.width(8)
.height(8)
.backgroundColor(COLORS.danger)
.borderRadius(4)
}
}
.width(14)
.justifyContent(FlexAlign.Center)
}, (r: number) => 'coin' + r)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 12 })
Row() {
Column()
.layoutWeight(1)
.height(1)
.backgroundColor(COLORS.primaryLight + '66')
Text('🔥 炉火千载 · 通宝万年 🪙')
.fontSize(9)
.fontColor(COLORS.primaryLight)
.margin({ left: 8, right: 8 })
Column()
.layoutWeight(1)
.height(1)
.backgroundColor(COLORS.primaryLight + '66')
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.backgroundColor(COLORS.primaryDark)
if (this.curTab === CoinTab.COIN) {
CoinContent({ data: this.data, onAdd: () => {
this.showAddCoin = true;
} })
} else if (this.curTab === CoinTab.MOLD) {
MoldContent({ data: this.data, onDel: (n: string) => {
this.delMoldName = n;
this.showDeleteMold = true;
} })
} else if (this.curTab === CoinTab.CASE) {
CaseContent({ data: this.data, onDetail: (n: string) => {
this.detailCaseName = n;
this.showDetailCase = true;
} })
} else if (this.curTab === CoinTab.ORDER) {
CoinOrderContent({ data: this.data, onEdit: () => {
this.showEditOrder = true;
} })
} else {
CoinReviewContent({ data: this.data })
}
}
.width('100%')
.height('100%')
Column() {
Column()
.width('100%')
.height(3)
.backgroundColor(COLORS.accent)
Row() {
ForEach(TAB_LIST, (t: TabMeta) => {
Column() {
Text(t.icon)
.fontSize(19)
Text(t.label)
.fontSize(10)
.fontColor(this.curTab === TAB_LIST.indexOf(t) ? t.color : COLORS.textHint)
.fontWeight(this.curTab === TAB_LIST.indexOf(t) ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 2 })
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
.padding({ top: 7, bottom: 7 })
.backgroundColor(this.curTab === TAB_LIST.indexOf(t) ? t.color + '14' : '#00000000')
.borderRadius(10)
.border(this.curTab === TAB_LIST.indexOf(t) ? { width: 1, color: t.color } : { width: 0 })
.onClick(() => {
this.curTab = TAB_LIST.indexOf(t);
})
}, (t: TabMeta) => t.key)
}
.width('100%')
.height(60)
.padding({ left: 8, right: 8 })
.backgroundColor(COLORS.cardBg)
}
.width('100%')
if (this.showAddCoin) {
AddCoinModal({
onClose: () => {
this.showAddCoin = false;
}
})
}
if (this.showEditOrder) {
EditCoinOrderModal({
onClose: () => {
this.showEditOrder = false;
}
})
}
if (this.showDeleteMold) {
DeleteMoldModal({
title: this.delMoldName, onClose: () => {
this.showDeleteMold = false;
}
})
}
if (this.showDetailCase) {
DetailCaseModal({
name: this.detailCaseName, onClose: () => {
this.showDetailCase = false;
}
})
}
}
}
}
@Component
struct CoinTag {
@Prop text: string;
@Prop color: string;
build() {
Text(this.text)
.fontSize(9)
.fontColor(this.color)
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
.backgroundColor(this.color + '14')
.borderRadius(10)
.border({ width: 1, color: this.color + '40' })
}
}
@Component
struct CoinContent {
@ObjectLink data: CoinData;
onAdd: () => void = () => {};
build() {
Scroll() {
Column() {
Column() {
Row() {
Column()
.width(12)
.height(12)
.backgroundColor(COLORS.gold)
.borderRadius(6)
.border({ width: 2, color: COLORS.silkWhite })
Column()
.layoutWeight(1)
.height(2)
.backgroundColor(COLORS.accent + '55')
.margin({ left: 4, right: 4 })
Column()
.width(6)
.height(6)
.backgroundColor(COLORS.primary)
.borderRadius(3)
.margin({ right: 4 })
Column()
.width(12)
.height(12)
.backgroundColor(COLORS.danger)
.borderRadius(6)
.border({ width: 2, color: COLORS.silkWhite })
Column()
.layoutWeight(1)
.height(2)
.backgroundColor(COLORS.accent + '55')
.margin({ left: 4, right: 4 })
Column()
.width(12)
.height(12)
.backgroundColor(COLORS.gold)
.borderRadius(6)
.border({ width: 2, color: COLORS.silkWhite })
}
.width('100%')
.margin({ bottom: 10 })
Row() {
Text('📈 本周古钱销量')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('周六65枚')
.fontSize(9)
.fontColor(COLORS.accent)
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
ForEach(WEEK_SOLD, (w: WeekCoinMeta) => {
Column() {
Text(w.value + '')
.fontSize(8)
.fontColor(w.value >= 60 ? COLORS.accent : COLORS.primary)
Column()
.width(16)
.height(w.value)
.backgroundColor(w.value >= 60 ? COLORS.accent : COLORS.primary)
.borderRadius(8)
.margin({ top: 4 })
Text(w.day)
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (w: WeekCoinMeta) => w.day)
}
.width('100%')
.height(150)
.alignItems(VerticalAlign.Bottom)
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(16)
.border({ width: 1, color: COLORS.accent + '55' })
Column() {
Row() {
Text('🪙 古钱陈列')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('点击➕铸钱')
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ left: 8 })
Text('➕')
.fontSize(14)
.margin({ left: 8 })
.onClick(() => {
this.onAdd();
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ bottom: 10 })
ForEach(this.data.coins, (p: CoinItem, i: number) => {
Row() {
Column() {
Column() {
Text(p.emoji)
.fontSize(18)
.textAlign(TextAlign.Center)
}
.width(44)
.height(44)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor(getDynastyColor(p.dynasty) + '14')
.borderRadius(22)
.border({ width: 1, color: getDynastyColor(p.dynasty) + '66' })
Row() {
Column()
.width(12)
.height(12)
.backgroundColor(COLORS.silkWhite)
.borderRadius(2)
.border({ width: 2, color: getDynastyColor(p.dynasty) })
}
.width(24)
.justifyContent(FlexAlign.Center)
}
.width(60)
.padding({ top: 10, bottom: 6 })
.backgroundColor(getDynastyColor(p.dynasty) + '0D')
.borderRadius(30)
.border({ width: 1, color: getDynastyColor(p.dynasty) + '33' })
Column() {
Row() {
Text(p.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('¥' + p.price)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(getPriceColor(p.price))
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
CoinTag({ text: p.dynasty + '钱', color: getDynastyColor(p.dynasty) })
CoinTag({ text: p.craft, color: COLORS.success })
Text('外圆内方')
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.Start)
.margin({ top: 6 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({ left: 10 })
}
.width('100%')
.padding(10)
.backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
.borderRadius(14)
.border({ width: 1, color: COLORS.line })
.margin({ top: 8 })
}, (p: CoinItem, i: number) => 'cn' + p.name + i)
}
.width('100%')
.margin({ top: 12 })
}
.width('100%')
.constraintSize({ maxHeight: '100%' })
}
.width('100%')
.constraintSize({ maxHeight: '100%' })
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
}
}
@Component
struct MoldContent {
@ObjectLink data: CoinData;
onDel: (n: string) => void = () => {};
build() {
Scroll() {
Column() {
Column() {
Row() {
Text('🏆 古钱销量 TOP6')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('开元通宝居首')
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ bottom: 10 })
ForEach(COIN_TOP, (t: CoinTopMeta) => {
Row() {
Text(t.name)
.fontSize(10)
.fontColor(COLORS.textPrimary)
.width(90)
Row() {
Column()
.layoutWeight(t.sold)
.height(12)
.backgroundColor(t.color)
.borderRadius(6)
Column()
.layoutWeight(100 - t.sold)
.height(12)
.backgroundColor(COLORS.line)
.borderRadius(6)
}
.layoutWeight(1)
Text(t.sold + '枚')
.fontSize(9)
.fontColor(t.color)
.width(40)
.textAlign(TextAlign.End)
}
.width('100%')
.margin({ top: 6 })
}, (t: CoinTopMeta) => t.name)
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(16)
.border({ width: 1, color: COLORS.accent + '55' })
Column() {
Row() {
Text('🔨 范模库')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('点击🗑️撤模')
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ bottom: 10 })
ForEach(this.data.molds, (c: MoldItem, i: number) => {
Row() {
Column() {
Text(c.emoji)
.fontSize(18)
.textAlign(TextAlign.Center)
Row() {
Column()
.width(12)
.height(4)
.backgroundColor(getMoldColor(c.level))
.borderRadius(2)
Column()
.width(8)
.height(8)
.backgroundColor(COLORS.silkWhite)
.borderRadius(4)
.margin({ left: 4 })
}
.width(28)
.justifyContent(FlexAlign.Center)
.margin({ top: 5 })
}
.width(56)
.padding({ top: 10, bottom: 6 })
.backgroundColor(getMoldColor(c.level) + '0D')
.borderRadius(16)
.border({ width: 1, color: getMoldColor(c.level) + '33' })
Column() {
Row() {
Text(c.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(c.material + '范')
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
Text('铸模精度')
.fontSize(9)
.fontColor(COLORS.textHint)
Row() {
Column()
.layoutWeight(c.level)
.height(6)
.backgroundColor(getMoldColor(c.level))
.borderRadius(3)
Column()
.layoutWeight(100 - c.level)
.height(6)
.backgroundColor(COLORS.line)
.borderRadius(3)
}
.layoutWeight(1)
.margin({ left: 6 })
}
.width('100%')
.margin({ top: 6 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({ left: 10 })
Text('🗑️')
.fontSize(14)
.onClick(() => {
this.onDel(c.name);
})
}
.width('100%')
.padding(10)
.backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
.borderRadius(14)
.border({ width: 1, color: COLORS.line })
.margin({ top: 8 })
}, (c: MoldItem, i: number) => 'md' + c.name + i)
}
.width('100%')
.margin({ top: 12 })
}
.width('100%')
.constraintSize({ maxHeight: '100%' })
}
.width('100%')
.constraintSize({ maxHeight: '100%' })
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
}
}
@Component
struct CaseContent {
@ObjectLink data: CoinData;
onDetail: (n: string) => void = () => {};
build() {
Scroll() {
Column() {
Column() {
Row() {
Text('🔥 铸造工艺热度')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('翻砂铸钱96')
.fontSize(9)
.fontColor(COLORS.primary)
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ bottom: 10 })
ForEach(COIN_HOT, (d: CoinHotMeta) => {
Row() {
Text(d.name)
.fontSize(10)
.fontColor(COLORS.textPrimary)
.width(80)
Row() {
Column()
.layoutWeight(d.heat)
.height(14)
.backgroundColor(d.color)
.borderRadius(7)
Column()
.layoutWeight(100 - d.heat)
.height(14)
.backgroundColor(COLORS.line)
.borderRadius(7)
}
.layoutWeight(1)
Text(d.heat + '')
.fontSize(9)
.fontColor(d.color)
.width(32)
.textAlign(TextAlign.End)
}
.width('100%')
.margin({ top: 6 })
}, (d: CoinHotMeta) => d.name)
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(16)
.border({ width: 1, color: COLORS.accent + '55' })
Column() {
Row() {
Text('🗃️ 钱匣坊')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('点击👁️了解')
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ bottom: 10 })
ForEach(this.data.cases, (b: CaseItem, i: number) => {
Row() {
Column() {
Text(b.emoji)
.fontSize(18)
.textAlign(TextAlign.Center)
Column()
.width(12)
.height(4)
.backgroundColor(getMoldColor(b.level))
.borderRadius(2)
.margin({ top: 5 })
}
.width(56)
.padding({ top: 10, bottom: 6 })
.backgroundColor(getMoldColor(b.level) + '0D')
.borderRadius(18)
.border({ width: 1, color: getMoldColor(b.level) + '33' })
Column() {
Row() {
Text(b.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('品级' + b.level)
.fontSize(9)
.fontColor(getMoldColor(b.level))
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
CoinTag({ text: b.wood + '匣', color: COLORS.textSecondary })
Text(b.place)
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.Start)
.margin({ top: 6 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({ left: 10 })
Text('👁️')
.fontSize(14)
.onClick(() => {
this.onDetail(b.name);
})
}
.width('100%')
.padding(10)
.backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
.borderRadius(14)
.border({ width: 1, color: COLORS.line })
.margin({ top: 8 })
}, (b: CaseItem, i: number) => 'cs' + b.name + i)
}
.width('100%')
.margin({ top: 12 })
}
.width('100%')
.constraintSize({ maxHeight: '100%' })
}
.width('100%')
.constraintSize({ maxHeight: '100%' })
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
}
}
@Component
struct CoinOrderContent {
@ObjectLink data: CoinData;
onEdit: () => void = () => {};
build() {
Scroll() {
Column() {
Column() {
Row() {
Text('🧩 朝代构成')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('唐钱占比最高')
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ bottom: 10 })
ForEach(DYNASTY_SHARE, (s: DynastyShareMeta) => {
Row() {
Column()
.width(8)
.height(8)
.backgroundColor(s.color)
.borderRadius(4)
Text(s.name + '朝')
.fontSize(10)
.fontColor(COLORS.textPrimary)
.margin({ left: 6 })
Text('×' + s.count)
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ left: 4 })
Row() {
Column()
.layoutWeight(s.count)
.height(10)
.backgroundColor(s.color)
.borderRadius(5)
Column()
.layoutWeight(20 - s.count)
.height(10)
.backgroundColor(COLORS.line)
.borderRadius(5)
}
.layoutWeight(1)
.margin({ left: 10 })
}
.width('100%')
.margin({ top: 6 })
}, (s: DynastyShareMeta) => s.name)
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(16)
.border({ width: 1, color: COLORS.accent + '55' })
Column() {
Row() {
Text('📦 订单台账')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('点击✏️改单')
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ left: 8 })
Text('✏️')
.fontSize(14)
.margin({ left: 8 })
.onClick(() => {
this.onEdit();
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ bottom: 10 })
ForEach(this.data.orders, (o: CoinOrderItem, i: number) => {
Row() {
Text(o.emoji)
.fontSize(16)
Column() {
Text(o.name)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(o.region + ' · ' + o.count + '枚')
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Text('¥' + o.amount)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(getOrderColor(o.amount))
Text('✏️')
.fontSize(12)
.margin({ left: 10 })
.onClick(() => {
this.onEdit();
})
}
.width('100%')
.padding(10)
.backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
.borderRadius(14)
.border({ width: 1, color: COLORS.line })
.margin({ top: 8 })
}, (o: CoinOrderItem, i: number) => 'od' + o.name + i)
}
.width('100%')
.margin({ top: 12 })
}
.width('100%')
.constraintSize({ maxHeight: '100%' })
}
.width('100%')
.constraintSize({ maxHeight: '100%' })
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
}
}
@Component
struct CoinReviewContent {
@ObjectLink data: CoinData;
build() {
Scroll() {
Column() {
Row() {
Text('⭐ 客评口碑')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('12条好评 · 平均4.9')
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ bottom: 6 })
ForEach(this.data.reviews, (r: CoinReviewItem, i: number) => {
Column() {
Row() {
Text(r.emoji)
.fontSize(15)
Column() {
Text(r.name)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(r.date)
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ top: 1 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 8 })
Text('⭐'.repeat(r.score))
.fontSize(10)
.fontColor(COLORS.warning)
CoinTag({ text: r.tag, color: getReviewTagColor(r.tag) })
}
.width('100%')
Text(r.content)
.fontSize(11)
.fontColor(COLORS.textSecondary)
.lineHeight(16)
.margin({ top: 6 })
}
.width('100%')
.padding(12)
.backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
.borderRadius(14)
.border({ width: 1, color: COLORS.line })
.margin({ top: 8 })
}, (r: CoinReviewItem, i: number) => 'rv' + r.name + i)
}
.width('100%')
.constraintSize({ maxHeight: '100%' })
}
.width('100%')
.constraintSize({ maxHeight: '100%' })
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
}
}
@Component
struct AddCoinModal {
onClose: () => void = () => {};
build() {
Column() {
Column() {
Row() {
Text('🪙 定制古钱')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('✕')
.fontSize(16)
.fontColor(COLORS.textHint)
.onClick(() => {
this.onClose();
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
Text('品名')
.fontSize(11)
.fontColor(COLORS.textSecondary)
.width(56)
Text('开元通宝')
.fontSize(11)
.fontColor(COLORS.textPrimary)
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.cardAlt)
.borderRadius(10)
.margin({ top: 14 })
Row() {
Text('钱文')
.fontSize(11)
.fontColor(COLORS.textSecondary)
.width(56)
Text('唐 · 翻砂 · 隶书')
.fontSize(11)
.fontColor(COLORS.textPrimary)
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.cardAlt)
.borderRadius(10)
.margin({ top: 8 })
Row() {
Text('预算')
.fontSize(11)
.fontColor(COLORS.textSecondary)
.width(56)
Text('¥880')
.fontSize(11)
.fontColor(COLORS.primary)
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.cardAlt)
.borderRadius(10)
.margin({ top: 8 })
Row() {
Text('取消')
.fontSize(12)
.fontColor(COLORS.textSecondary)
.padding({ left: 16, right: 16, top: 7, bottom: 7 })
.borderRadius(12)
.border({ width: 1, color: COLORS.border })
.onClick(() => {
this.onClose();
})
Text('确认铸钱')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 16, right: 16, top: 7, bottom: 7 })
.backgroundColor(COLORS.primary)
.borderRadius(12)
.onClick(() => {
this.onClose();
})
}
.width('100%')
.justifyContent(FlexAlign.End)
.margin({ top: 14 })
}
.width('86%')
.padding(16)
.backgroundColor(COLORS.cardBg)
.borderRadius(18)
.constraintSize({ maxHeight: '78%' })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#66000000')
}
}
@Component
struct EditCoinOrderModal {
onClose: () => void = () => {};
build() {
Column() {
Column() {
Row() {
Text('✏️ 修改订单')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('✕')
.fontSize(16)
.fontColor(COLORS.textHint)
.onClick(() => {
this.onClose();
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
Text('客户')
.fontSize(11)
.fontColor(COLORS.textSecondary)
.width(56)
Text('博物馆文创')
.fontSize(11)
.fontColor(COLORS.textPrimary)
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.cardAlt)
.borderRadius(10)
.margin({ top: 14 })
Row() {
Text('数量')
.fontSize(11)
.fontColor(COLORS.textSecondary)
.width(56)
Text('900 → 1000 枚')
.fontSize(11)
.fontColor(COLORS.primary)
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.cardAlt)
.borderRadius(10)
.margin({ top: 8 })
Row() {
Text('金额')
.fontSize(11)
.fontColor(COLORS.textSecondary)
.width(56)
Text('¥500000')
.fontSize(11)
.fontColor(COLORS.accent)
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.cardAlt)
.borderRadius(10)
.margin({ top: 8 })
Row() {
Text('取消')
.fontSize(12)
.fontColor(COLORS.textSecondary)
.padding({ left: 16, right: 16, top: 7, bottom: 7 })
.borderRadius(12)
.border({ width: 1, color: COLORS.border })
.onClick(() => {
this.onClose();
})
Text('保存修改')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 16, right: 16, top: 7, bottom: 7 })
.backgroundColor(COLORS.primary)
.borderRadius(12)
.onClick(() => {
this.onClose();
})
}
.width('100%')
.justifyContent(FlexAlign.End)
.margin({ top: 14 })
}
.width('86%')
.padding(16)
.backgroundColor(COLORS.cardBg)
.borderRadius(18)
.constraintSize({ maxHeight: '78%' })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#66000000')
}
}
@Component
struct DeleteMoldModal {
@Prop title: string;
onClose: () => void = () => {};
build() {
Column() {
Column() {
Text('🗑️ 撤下范模')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('确定撤下「' + this.title + '」吗?')
.fontSize(12)
.fontColor(COLORS.textSecondary)
.margin({ top: 10 })
Text('撤下的范模将从范模库移除。')
.fontSize(10)
.fontColor(COLORS.textHint)
.margin({ top: 4 })
Row() {
Text('再想想')
.fontSize(12)
.fontColor(COLORS.textSecondary)
.padding({ left: 16, right: 16, top: 7, bottom: 7 })
.borderRadius(12)
.border({ width: 1, color: COLORS.border })
.onClick(() => {
this.onClose();
})
Text('确认撤下')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 16, right: 16, top: 7, bottom: 7 })
.backgroundColor(COLORS.danger)
.borderRadius(12)
.onClick(() => {
this.onClose();
})
}
.width('100%')
.justifyContent(FlexAlign.End)
.margin({ top: 16 })
}
.width('86%')
.padding(16)
.backgroundColor(COLORS.cardBg)
.borderRadius(18)
.constraintSize({ maxHeight: '78%' })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#66000000')
}
}
@Component
struct DetailCaseModal {
@Prop name: string;
onClose: () => void = () => {};
build() {
Column() {
Column() {
Row() {
Text('👁️ 钱匣详情')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('✕')
.fontSize(16)
.fontColor(COLORS.textHint)
.onClick(() => {
this.onClose();
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Column() {
Text('🗃️')
.fontSize(30)
Text(this.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 6 })
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardAlt)
.borderRadius(12)
.margin({ top: 12 })
Text('选老料整木开板,榫卯相接不用一钉,内衬绒布防磨,盖面镶铜扣铜环。铜钱入匣叮当作响,一匣藏尽千秋通宝。')
.fontSize(11)
.fontColor(COLORS.textSecondary)
.lineHeight(17)
.margin({ top: 10 })
Row() {
Text('木料产地')
.fontSize(11)
.fontColor(COLORS.textSecondary)
.width(56)
Text('苏州 · 老料坊')
.fontSize(11)
.fontColor(COLORS.textPrimary)
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.cardAlt)
.borderRadius(10)
.margin({ top: 8 })
Row() {
Text('匠人评级')
.fontSize(11)
.fontColor(COLORS.textSecondary)
.width(56)
Text('甲级 · 木作卅载')
.fontSize(11)
.fontColor(COLORS.primary)
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.cardAlt)
.borderRadius(10)
.margin({ top: 8 })
Text('知道了')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 18, right: 18, top: 7, bottom: 7 })
.backgroundColor(COLORS.primary)
.borderRadius(12)
.margin({ top: 12 })
.onClick(() => {
this.onClose();
})
}
.width('86%')
.padding(16)
.backgroundColor(COLORS.cardBg)
.borderRadius(18)
.constraintSize({ maxHeight: '78%' })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#66000000')
}
}更多推荐


所有评论(0)