中国结坊非遗编结应用基于HarmonyOS API 24深度解析——采用了“单入口 + 多内容区 + 底部导航“的经典移动端布局模式。入口组件 KnotApp 作为整个应用的根容器
中国结坊非遗编结应用基于HarmonyOS API 24深度解析——采用了"单入口 + 多内容区 + 底部导航"的经典移动端布局模式。入口组件 KnotApp 作为整个应用的根容器
本文基于 HarmonyOS NEXT + ArkTS 声明式开发框架,完整实现一个「中国结坊」非遗编结主题 App,涵盖配色系统、状态管理、多 Tab 页面、自定义弹窗、动态颜色映射、三列网格布局等核心知识点,并附完整源码与运行效果截图。
一、引言:非遗编结的数字化传承
在移动互联网与智能设备高速发展的今天,非物质文化遗产的传承与传播已经从单纯的线下演示,演变为一种高度数字化、流程化、个性化的线上线下融合体验。中国结,作为中国传统手工编结技艺的代表,距今已有数千年历史。一枚好结,从选线、染色、合股到编结、缠丝、打穗、穿珠、收口、整烫,需经数十道工序,凝聚着匠人的心血与丝线的温润。同心结的缠绵、盘长结的回环、如意结的顺遂、绣球结的圆满、祥云结的祥瑞,每一枚结都承载着中国人对吉祥的追求与向往。然而,这门古老的技艺正面临着传承人老龄化、受众面狭窄、销售渠道单一等现实困境。
正是在这样的背景下,中国结坊类数字化应用应运而生,并迅速成为非遗编结"活态传承"的数字载体。一个优秀的中国结坊应用,不仅需要覆盖从"结艺墙"到"客评口碑"的完整业务闭环,还要在视觉设计、交互体验、数据呈现上做到细致入微,让用户在指尖滑动间便能感受到丝线的温润与编结的雅致。
本文将深入剖析一个基于鸿蒙 ArkUI 声明式开发框架构建的中国结坊非遗编结应用。该应用以"结艺墙"为核心,横向扩展出流苏架、编绳筐、订单管理、客评口碑五大功能模块,形成了一个完整闭环的编结管理生态。它不仅是一个功能完备的演示项目,更是一份典型的 ArkUI 工程实践范本,涵盖了状态管理、组件化设计、模态弹窗、列表渲染、图表可视化、动态颜色映射、三列网格布局等关键技术点。

二、应用整体架构概览
整个应用采用了"单入口 + 多内容区 + 底部导航"的经典移动端布局模式。入口组件 KnotApp 作为整个应用的根容器,负责管理当前激活的 Tab 状态,并根据状态切换不同的内容区组件。
应用一共划分为五个一级页面:结艺墙(KNOT)、流苏架(TASSEL)、编绳筐(CORD)、订单管理(ORDER)以及客评口碑(REVIEW)。每个页面都是一个独立的 @Component 装饰的自定义组件,内部封装了自己的状态、构建逻辑和子组件。这种"高内聚、低耦合"的组件化设计,使得每个页面可以独立开发、独立测试,同时也便于后续的扩展与维护。
在数据层面,应用定义了一套完整的类型系统,包括色彩主题、绳结元数据、流苏元数据、编绳元数据、订单元数据、客评元数据、周销量元数据、寓意构成元数据、工艺热度元数据、TOP榜元数据等,并通过常量数组将数据与 UI 表现层解耦。所有业务数据以 @Observed 装饰的可观察类 KnotData 为载体,配合五组各 12 条模拟数据,模拟了一个真实的中国结坊数据环境。
在交互层面,应用通过条件渲染(if-else)实现了四类弹窗的叠加显示:新编绳结、修改订单、撤下流苏、编绳详情。弹窗以全屏半透明遮罩 + 居中卡片的形式呈现,与主内容共享同一根 Column,通过布尔状态控制显隐。
在视觉层面,应用独创了"三列网格"的结艺墙设计——绳结页使用 Flex 容器配合 wrap 换行实现三列网格布局,每个卡片宽度 31%,居中对齐,展示绳结图标、名称、价格尺寸和寓意标签,形成了与传统纵向列表截然不同的视觉体验。全局圆角较大(圆角8/12/14/16/20),模仿中国结的圆润造型。

下面逐页展示应用的五大功能模块运行效果。
三、绳结页:三列网格的结艺墙核心陈列
绳结页是整个应用的默认首页,也是布局设计最独特的页面。它包含了本周绳结销量柱状图、结艺墙三列网格列表,以及新编绳结弹窗。
绳结页顶部是"本周绳结销量"柱状图卡片,采用白色背景配 1px 鎏金半透明边框,圆角 20。通过 ForEach 遍历一周七天的销量数据,柱子高度直接绑定数值,销量超过 50 的日期(周五、周日、周六)使用主色中国红 #C0392B 突出显示,其余日期使用 accent 鎏金 #C9A227,形成自然的高低起伏。值得注意的是,周日以 66 件的销量居首(高于周六的 60 件),符合中国结作为节庆礼品在周日热销的真实节奏。
下方是结艺墙列表,采用了独创的三列网格布局——使用 Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) 容器实现自动换行,每个卡片宽度 31%,居中对齐。卡片内包含一个 52×52 的图标方块(圆角 12,背景色按名称动态着色)、绳结名称、价格与尺寸(颜色按名称动态着色)和寓意标签(鎏金色)。这种网格布局让 12 款绳结以"结艺墙"的形式集中展示,视觉密度高且整齐美观,与传统纵向列表形成鲜明对比。
12 款绳结覆盖了从 28 元(纽扣结)到 118 元(祥云结)的完整价格梯队,寓意涵盖永结同心、回环贯通、万事如意、团圆美满、吉祥安康、财源广进、扣住福气、风雅知音、抛得良缘、平步青云、福叠而来、祥瑞临门十二种,尺寸从迷你、小号、中号到大号不等,每款都配有独特的 emoji 图标(同心结用💞、盘长结用♾️、如意结用🍀、绣球结用💠、祥云结用🏮等),让绳结不仅是手工艺品,更是寓意与视觉的双重体验。右上角 ➕ 按钮点击可弹出"新编绳结"弹窗。
四、流苏页:销量排行与撤穗管理
流苏页以"流苏"为核心,顶部是绳结销量 TOP6 横向条形图,下方是流苏架列表,支持撤穗操作。

顶部 TOP6 卡片通过横向进度条展示销量排名前六的绳结品种,同心结以 96 的销量居首,盘长结、如意结紧随其后。进度条使用 layoutWeight 比例法实现,已售部分与剩余部分按比例分配宽度,颜色直接取数据中的配置色,六种绳结各有特色。
下方流苏架列表展示 12 款流苏,卡片采用"左图标右文字"布局。左侧是一个 44×44 的图标方块,背景色通过 getTasselColor 函数按流苏颜色分档着色(正红/绛红/朱红类中国红、鎏金/明黄类鎏金色、藏蓝/玄青/夜蓝类藏蓝色),圆角 10,展示流苏 emoji 图标。右侧展示流苏名称、颜色(正红/鎏金/藏蓝等)、长度与产地(“30cm · 北京 打穗”)和 🗑️ 撤穗按钮。
12 款流苏颜色覆盖正红、鎏金、藏蓝、藕粉、竹青、雪青、五彩、双色、绛红、朱红、明黄、玄青十二种,长度从 20cm 到 40cm 不等,产地从北京、苏州、南京、杭州、成都、洛阳、潍坊、西安、开封、自贡到龙泉,呈现出"流苏天下"的格局。其中璎珞穗长达 40cm,五彩穗长达 35cm,宫绦穗明黄色最为尊贵。点击 🗑️ 按钮弹出"撤下流苏"二次确认弹窗,采用警示型设计语言,"确认撤下"按钮使用危险色深红,防止误操作。
五、编绳页:寓意构成与编绳详情
编绳页以"编绳"为核心,顶部是寓意构成占比图,下方是编绳筐列表,支持查看详情。

顶部寓意构成卡片展示三种寓意的分布占比:福寿 5 款、姻缘 4 款、安康 3 款,合计 12 款,恰好对应绳结陈列的寓意总数。进度条以 12 为满刻度进行比例渲染,比百分比更直观地传达了"几寓意"的语义。
下方编绳筐列表展示 12 款编绳,卡片采用"左图标右文字"布局。左侧是一个 44×44 的图标方块,背景色通过 getCordColor 函数按股线级别分档着色(≥95用中国红、≥91用鎏金色、其余用藏蓝色),圆角 10,展示编绳 emoji 图标。右侧展示编绳名称、颜色(彩色/正红/鎏金等)、股线级别(“股线92级”,颜色按级别动态着色)、材质(五色丝线/棉芯红线等)和 👁️ 详情按钮。
12 款编绳材质覆盖五色丝线、棉芯红线、金线缠芯、蚕丝股线、渐变染线、竹节棉线、珠线合股、织金龙纹、提花云纹、苎麻手搓、双色拧股、夜光丝线十二种,股线级别从 88(素麻绳)到 97(龙纹绳)不等,龙纹绳以 97 级居首,珍珠绳 96 级次之,金芯绳 95 级第三,符合"绳以龙纹为尊、以珍珠为贵"的行业共识。颜色从彩色、正红、鎏金、玄青、渐变、米白、珠白、金红、月白、原麻、红金到夜蓝,几乎囊括了编绳的全部色彩。点击 👁️ 按钮弹出"编绳详情"弹窗,包含编绳展示区、文艺风格描述文案(“选上等丝线,染色晾晒后按股合拧。丝要匀、劲要顺,编出的绳方才圆润不毛。龙纹者以金线织入,云纹者以提花浮起,一根绳里,藏着一整门手艺。”)、产地工坊(“江苏 · 苏州”)和匠人评级(“特级 · 搓绳十八载”)。
六、订单页:编织工艺热度与订单改单
订单页以"订单"为核心,顶部是编织工艺热度排行,下方是订单列表,支持修改订单。

顶部编织工艺热度卡片展示六门工序的热度指数:编结 96、缠丝 95、打穗 93、穿珠 92、收口 90、整烫 88,恰好对应编结制作的完整流程(编结→缠丝→打穗→穿珠→收口→整烫),编结以 96 的热度居首,呈现出"编结为核心工序"的合理分布。六根进度条颜色从中国红到鎏金到藏蓝到蓝色到浅灰到深红,形成了一条色彩渐变的工艺流水线。
下方订单列表展示 12 条订单,卡片采用"左图标右文字"布局。左侧是一个 40×40 的图标方块,背景色通过 getOrderColor 函数按订单金额分档着色(15万以上中国红、11-15万鎏金色、8-11万藏蓝色、8万以下蓝色),圆角 10,展示客户 emoji 图标。右侧展示客户名称、✏️ 编辑按钮、区域标签和订单详情(“· 900 件 · ¥132000”)。
12 类客户覆盖婚庆策划公司、年货批发商、文创礼品店、汉服社团、汽车4S集团、茶楼连锁、国学书院、海外唐人街、景区纪念品铺、旗袍定制坊、幼儿园手工课、直播间年货节。年货批发商以 2200 件的数量和 176000 元的金额双居首,海外唐人街虽然只有 640 件,但金额高达 168000,单价最高,体现了非遗编结在海外市场的高溢价。幼儿园手工课以 2800 件的数量最多但金额只有 64000,单价最低,符合手工课材料包的批量低价特征。点击 ✏️ 按钮弹出"修改订单"弹窗,可调整数量和金额,数量行使用箭头符号"→"直观展示修改前后的对比。
七、客评页:口碑展示与标签体系
客评页是应用中最具"人情味"的页面,展示了 12 条用户好评。

页面顶部是标题行"⭐ 客评口碑"和统计摘要"12条好评 · 平均4.9"。下方 12 条评价卡片采用"左头像右信息"的横向布局:最左侧是用户 emoji 头像,中间是用户名称和日期,右侧是星级评分(通过 String.repeat 一行代码生成对应数量的星星)和评价标签(通过动态颜色函数按标签语义分档着色)。评价内容正文在卡片下方,使用次要色文字和 16px 行高。
12 条评价每条都有独特的标签:“喜宴点睛”、“补货三次”、“配色高级”、“雅致挂件”、“亲子好物”、“年味满满”、“盘扣绝配”、“交车必备”、“开学好礼”、“一夜爆赞”、“五年回头”、“假日卖空”,这些标签既概括了评价核心,又形成了中国结坊的口碑关键词云。
评价内容充满生活气息,如"婚车挂了同心结,长辈们个个夸手艺地道"、“盘长结卖得最快,一上午补了三次货”、“宫绦穗配色高级,压得住织金马面”、“带娃编五彩绳,课程包材料齐全超省心”、“唐人街采购祥云结,年味一下子就来了”、“连买五年本命结,年年都有新花样”,让数据看起来不是冷冰冰的测试桩,而是真实顾客的心声。卡片背景使用白色与浅粉色奇偶交替,整体视觉温馨而雅致。
八、色彩主题系统:中国红鎏金的编结视觉语言
8.1 色彩接口定义
任何一款设计精良的应用,背后都有一套严谨的色彩规范。该应用首先通过一个 KnotPalette 接口,将整个应用所需的色彩抽象为一个结构化的契约:
interface KnotPalette {
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;
knotRed: string;
cordGold: string;
}
- primary:主色调,中国红 #C0392B,用于按钮、强调文字、激活状态、绳结页图标等高优先级视觉元素,是中国结的标志性色彩。
- primaryDark:主色深色变体 #7A1F16,作为顶部头部背景,营造深红结匣的沉稳感。
- accent:强调色,鎏金 #C9A227,与中国红形成暖调对比,用于流苏、鎏金线等元素。
- bg:页面整体背景色 #FCF3EE,粉白暖调,营造丝线般的温润质感。
- knotRed:结红 #C0392B,专用于头部装饰方块,语义上强调"结"的质感。
- cordGold:绳金 #F5E9D0,用于装饰方块的边框色,营造出金线镶边的柔和质感。

8.2 色彩实例化
const COLORS: KnotPalette = {
primary: '#C0392B',
primaryLight: '#E5A79E',
primaryDark: '#7A1F16',
accent: '#C9A227',
accentLight: '#E3C878',
bg: '#FCF3EE',
cardBg: '#FFFFFF',
cardAlt: '#F7E4DC',
textPrimary: '#4A2018',
textSecondary: '#8C6157',
textHint: '#B9988E',
border: '#EFD5CB',
line: '#F5E4DC',
success: '#6E8F5A',
warning: '#B0893F',
danger: '#8E2E26',
white: '#FFFFFF',
knotRed: '#C0392B',
cordGold: '#F5E9D0'
};
主色 #C0392B 是一种醇厚的中国红,既契合"编结"主题(中国结的标志性色彩),又具有传统、喜庆的气质。强调色 #C9A227 鎏金与中国红形成暖调对比,呼应金线、鎏金穗的色彩。cordGold 绳金 #F5E9D0 用于装饰边框,营造出金线镶边的高级感。背景色 #FCF3EE 粉白暖调与主色同暖色系,营造出丝线般的温润氛围。这种将所有色彩集中管理的做法保证了全局一致性,支持主题切换,降低了维护成本。

九、类型定义:构建严谨的数据契约
应用通过一组接口定义,为每一类业务对象建立了明确的数据契约。
9.1 绳结、流苏、编绳元数据
interface KnotItem {
name: string; meaning: string; size: string; price: number; emoji: string;
}
interface TasselItem {
name: string; color: string; length: string; place: string; emoji: string;
}
interface CordItem {
name: string; material: string; level: number; color: string; emoji: string;
}
KnotItem 描述绳结的名称、寓意、尺寸、价格和图标。TasselItem 描述流苏的名称、颜色、长度、产地和图标。CordItem 描述编绳的名称、材质、股线级别、颜色和图标,用 level 字段体现"以级定品"。
9.2 订单、客评与图表元数据
interface KnotOrderItem {
name: string; region: string; count: number; amount: number; emoji: string;
}
interface KnotReviewItem {
name: string; score: number; date: string; content: string; tag: string; emoji: string;
}
interface WeekKnotMeta { day: string; value: number; }
interface MeaningShareMeta { name: string; count: number; color: string; }
interface WeaveHotMeta { name: string; heat: number; color: string; }
interface KnotTopMeta { name: string; sold: number; color: string; }
四组图表元数据(WeekKnotMeta/MeaningShareMeta/WeaveHotMeta/KnotTopMeta)都自带 color 字段,实现了"数据即配色"的图表渲染模式,组件渲染时无需额外的颜色映射逻辑。
十、配置驱动:常量数据与可视化配置
10.1 Tab 导航配置
enum KnotTab { KNOT = 0, TASSEL = 1, CORD = 2, ORDER = 3, REVIEW = 4 }
const TAB_LIST: TabMeta[] = [
{ key: 'knot', icon: '🀄', label: '绳结', color: '#C0392B' },
{ key: 'tassel', icon: '🎋', label: '流苏', color: '#C9A227' },
{ key: 'cord', icon: '🧶', label: '编绳', color: '#4E5D8C' },
{ key: 'order', icon: '📦', label: '订单', color: '#4E7BA8' },
{ key: 'review', icon: '⭐', label: '客评', color: '#C0392B' }
];
枚举提供类型安全的 Tab 标识,TAB_LIST 为每个 Tab 配置独立的图标、标签和主题色:绳结用中国红、流苏用鎏金、编绳用藏蓝、订单用蓝色、客评用中国红,"一 Tab 一色"让每个页面有鲜明的视觉身份。
10.2 周销量、寓意构成、工艺热度与 TOP 榜
const WEEK_SOLD = [
{ day: '周一', value: 18 }, { day: '周二', value: 24 },
{ day: '周三', value: 30 }, { day: '周四', value: 38 },
{ day: '周五', value: 52 }, { day: '周日', value: 66 }, { day: '周六', value: 60 }
];
const MEANING_SHARE = [
{ name: '福寿', count: 5, color: '#C0392B' },
{ name: '姻缘', count: 4, color: '#C9A227' },
{ name: '安康', count: 3, color: '#4E5D8C' }
];
const WEAVE_HOT = [
{ name: '编结', heat: 96 }, { name: '缠丝', heat: 95 }, { name: '打穗', heat: 93 },
{ name: '穿珠', heat: 92 }, { name: '收口', heat: 90 }, { name: '整烫', heat: 88 }
];
const KNOT_TOP = [
{ name: '同心结', sold: 96 }, { name: '盘长结', sold: 93 },
{ name: '如意结', sold: 90 }, { name: '团锦结', sold: 87 },
{ name: '吉祥结', sold: 84 }, { name: '双钱结', sold: 81 }
];
WEAVE_HOT 的六门工艺恰好对应编结制作的完整流程(编结→缠丝→打穗→穿珠→收口→整烫),编结以 96 的热度居首。WEEK_SOLD 的顺序特殊——周日(66)排在周六(60)之前,周日销量高于周六,符合中国结作为节庆礼品在周日热销的真实节奏。KNOT_COL 常量 [0…11] 用于顶部头部的装饰带渲染,按索引模 3 分别渲染:结红圆角矩形 12×12 带 2px 绳金边框(模0)、鎏金圆点 8×8(模1)、竹绿叶圆 8×8(模2),形成一条金线镶边的装饰线,呼应中国结的"镶边"视觉元素。
十一、数据模型:可观察的绳结实体
@Observed
export class KnotData {
knots: KnotItem[] = [ /* 12 款绳结 */ ];
tassels: TasselItem[] = [ /* 12 款流苏 */ ];
cords: CordItem[] = [ /* 12 款编绳 */ ];
orders: KnotOrderItem[] = [ /* 12 条订单 */ ];
reviews: KnotReviewItem[] = [ /* 12 条客评 */ ];
}
KnotData 是核心数据模型,以五个数组承载中国结坊全部业务信息。@Observed 装饰器使得该类实例在被 @State 或 @ObjectLink 引用时,属性变化能自动触发 UI 刷新。每个数组精确包含 12 条数据,"五五对称、各十二条"的设计保证了每个页面都有足够的内容密度。所有数据在类定义时直接初始化,"字段即默认值"的写法避免了构造函数中重复赋值。
十二、模拟数据:构建真实的中国结坊数据环境
12.1 绳结模拟数据
12 款绳结覆盖了从 28 元(纽扣结)到 118 元(祥云结)的完整价格梯队,寓意涵盖永结同心、回环贯通、万事如意、团圆美满、吉祥安康、财源广进、扣住福气、风雅知音、抛得良缘、平步青云、福叠而来、祥瑞临门十二种,尺寸从迷你、小号、中号到大号不等。同心结¥68、盘长结¥98、如意结¥78、团锦结¥48、吉祥结¥72、双钱结¥42、纽扣结¥28、琵琶结¥66、绣球结¥108、流云结¥74、蝴蝶结¥38、祥云结¥118,emoji 图标与绳结名称呼应(同心结用💞、盘长结用♾️、如意结用🍀、绣球结用💠等)。
12.2 流苏与编绳模拟数据
流苏颜色覆盖正红、鎏金、藏蓝、藕粉、竹青、雪青、五彩、双色、绛红、朱红、明黄、玄青十二种,长度从 20cm 到 40cm 不等,产地从北京到龙泉呈现出"流苏天下"的格局。编绳材质覆盖五色丝线、棉芯红线、金线缠芯、蚕丝股线、渐变染线、竹节棉线、珠线合股、织金龙纹、提花云纹、苎麻手搓、双色拧股、夜光丝线十二种,股线级别从 88 到 97 不等,龙纹绳以 97 级居首,颜色从彩色到夜蓝几乎囊括编绳全部色彩。
12.3 订单与客评模拟数据
订单覆盖 12 类客户,年货批发商以 2200 件和 176000 元双居首,海外唐人街单价最高,幼儿园手工课数量最多但单价最低。客评 12 条各有独特标签(喜宴点睛、补货三次、配色高级、雅致挂件、亲子好物、年味满满、盘扣绝配、交车必备、开学好礼、一夜爆赞、五年回头、假日卖空),评价内容充满生活气息,如"婚车挂了同心结,长辈们个个夸手艺地道"、“连买五年本命结,年年都有新花样”。
十三、动态颜色函数:数据驱动的视觉映射
应用通过五个动态颜色函数将业务数据映射为视觉色彩,实现"数据即配色"。
function getKnotColor(name: string): string {
if (name === '同心结' || name === '吉祥结' || name === '绣球结' || name === '祥云结') return '#C0392B';
else if (name === '盘长结' || name === '如意结' || name === '团锦结') return '#C9A227';
else if (name === '纽扣结' || name === '琵琶结' || name === '流云结' || name === '蝴蝶结') return '#4E5D8C';
return '#4E7BA8';
}
function getTasselColor(color: string): string {
if (color === '正红' || color === '绛红' || color === '朱红') return '#C0392B';
else if (color === '鎏金' || color === '明黄') return '#C9A227';
else if (color === '藏蓝' || color === '玄青' || color === '夜蓝') return '#4E5D8C';
return '#4E7BA8';
}
function getCordColor(level: number): string {
if (level >= 95) return '#C0392B';
else if (level >= 91) return '#C9A227';
return '#4E5D8C';
}
function getOrderColor(amount: number): string {
if (amount >= 150000) return '#C0392B';
else if (amount >= 110000) return '#C9A227';
else if (amount >= 80000) return '#4E5D8C';
return '#4E7BA8';
}
function getReviewTagColor(tag: string): string { /* 按标签语义分四档着色 */ }
- getKnotColor:按绳结名称分四组色彩(同心结/吉祥结/绣球结/祥云结类中国红、盘长结/如意结/团锦结类鎏金、纽扣结/琵琶结/流云结/蝴蝶结类藏蓝、其他蓝色)。
- getTasselColor:按流苏颜色分四组(正红/绛红/朱红类中国红、鎏金/明黄类鎏金、藏蓝/玄青/夜蓝类藏蓝、其他蓝色)。
- getCordColor:按股线级别分三档(95以上中国红、91-94鎏金、其余藏蓝),“以色定级”。
- getOrderColor:按订单金额分四档(15万以上中国红、11-15万鎏金、8-11万藏蓝、8万以下蓝色)。
- getReviewTagColor:按评价标签语义分四色系。
值得注意的是,绳结页的图标方块背景色调用 getKnotColor,而卡片整体背景则固定使用白色,形成了"图标多彩、卡片统一白"的视觉层次。这种"三列网格 + 动态颜色图标"的组合是本应用最具辨识度的视觉设计特征。
十四、入口组件:应用的骨架与导航中枢
14.1 组件声明与状态
@Entry
@Component
struct KnotApp {
@State curTab: number = 0;
@State data: KnotData = new KnotData();
@State showAddKnot: boolean = false;
@State showEditOrder: boolean = false;
@State showDeleteTassel: boolean = false;
@State showDetailCord: boolean = false;
@State delTasselName: string = '';
@State detailCordName: string = '';
@State ballPulse: boolean = false;
@State cordSwing: boolean = false;
}
10 个 @State 状态分四类:导航状态(curTab)、数据状态(data)、弹窗控制状态(4个布尔+2个参数字符串)、交互动效状态(ballPulse、cordSwing)。
14.2 顶部头部构建
顶部头部采用 primaryDark #7A1F16 深红色背景。第一层标题栏左侧是标题"中国结坊"和副标题"Knot · 千回百转 结出吉祥",右侧是动效区:红球🔴点击触发缩放脉冲动画(scale 从 0.9 到 1.3,duration 600ms)、编绳🧶点击触发上下浮动动画(translate Y 从 4 到 -8,duration 700ms)、灯笼🏮静态装饰。第二层是"装饰带",通过 ForEach 遍历 KNOT_COL,按模 3 渲染结红圆角矩形 12×12 带 2px 绳金边框、鎏金圆点 8×8、竹绿叶圆 8×8,形成一条金线镶边的装饰线,呼应中国结的"镶边"视觉元素。第三层是标语分割线"🧶 穿缠打穗 · 吉祥如意 🏮"。
14.3 底部 Tab 导航
底部导航顶部有一条 3px 高的中国红色装饰条,与深红色头部形成上下呼应。每个 Tab 激活态有三重视觉反馈:标签文字主题色加粗、背景填充主题色 8% 透明度、主题色实线边框,圆角 20(圆润结艺风格)。非激活态用灰色文字、透明背景、无边框。导航栏高度 60。
14.4 条件渲染弹窗
应用采用条件渲染(if-else)实现四个弹窗:新编绳结(AddKnotModal)、修改订单(EditKnotOrderModal)、撤下流苏(DeleteTasselModal)、编绳详情(DetailCordModal)。弹窗直接叠加在根 Column 末尾,通过布尔状态控制显隐,每个弹窗内部是全屏半透明遮罩(#66000000)+ 居中白色卡片。弹窗卡片统一使用圆角 20,信息行圆角 12。
十五、关键技术特性对比
| 特性维度 | 实现方式 | 技术要点 | 设计价值 |
|---|---|---|---|
| 色彩管理 | KnotPalette 接口 + COLORS 常量 | 集中定义 20 种语义色,含 knotRed/cordGold 装饰色 | 保证视觉一致性,支持主题切换 |
| 数据建模 | @Observed 装饰的 KnotData 类 | 5 个数组各 12 条数据,覆盖编结全业务维度 | 数据与 UI 双向联动,响应式更新 |
| 配置驱动 | 常量数组 + 元数据接口 | WEEK_SOLD/MEANING_SHARE/WEAVE_HOT/KNOT_TOP 自带 color | 数据与表现解耦,扩展零成本 |
| 动态颜色 | 5 个全局映射函数 | 按名称/颜色/级别/金额/标签分档着色,绳结页图标多彩卡片统一白 | 数据即配色,信息密度翻倍 |
| 三列网格布局 | Flex wrap + width 31% | 绳结页三列网格,居中对齐,图标/名称/价格/寓意标签 | 视觉密度高,结艺墙效果 |
| 状态管理 | @State + @ObjectLink | 10 个状态分四类(导航/数据/弹窗/动效) | 细粒度状态控制,自动触发渲染 |
| 组件化 | @Component + @Builder | 5 个页面组件 + 4 个弹窗组件 + KnotTag 通用组件 | 高内聚低耦合,代码复用率高 |
| 模态交互 | 条件渲染弹窗 + 全屏遮罩 | 4 个弹窗 if-else 叠加,#66000000 半透明遮罩 | 灵活可控,完全自定义布局 |
| 列表渲染 | ForEach + 奇偶交替背景 | 12 条数据 × 5 页面,cardBg/cardAlt 交替 | 数据驱动渲染,长列表可读性强 |
| 数据可视化 | 柱状图 + 横向条形图 | height 绑定数据、layoutWeight 比例法、阈值着色 | 多维度呈现,直观易读 |
| 星级渲染 | String.repeat 技巧 | ‘⭐’.repeat(score) 一行代码生成星级 | 简洁高效,无需循环 |
| 防御编程 | 字段默认值 + 类型安全 | 所有数据字段声明即初始化,枚举替代魔法数字 | 健壮性强,无运行时异常 |
| 布局系统 | Column/Row + Flex + layoutWeight | 弹性权重分配、Flex 换行网格、条件渲染弹窗、Scroll 滚动 | 自适应布局,层次分明 |
| 交互动效 | scale + translate + animation | 红球缩放脉冲 600ms、编绳浮动 700ms、EaseOut 曲线 | 静态头部灵动化,趣味性强 |
| 视觉层次 | 装饰带/圆角20/三列网格 | KNOT_COL 模3渲染金线镶边装饰、全局大圆角、绳结页三列网格 | 精致细腻,编结氛围感强 |
完整代码
interface KnotPalette {
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;
knotRed: string;
cordGold: string;
}
const COLORS: KnotPalette = {
primary: '#C0392B',
primaryLight: '#E5A79E',
primaryDark: '#7A1F16',
accent: '#C9A227',
accentLight: '#E3C878',
bg: '#FCF3EE',
cardBg: '#FFFFFF',
cardAlt: '#F7E4DC',
textPrimary: '#4A2018',
textSecondary: '#8C6157',
textHint: '#B9988E',
border: '#EFD5CB',
line: '#F5E4DC',
success: '#6E8F5A',
warning: '#B0893F',
danger: '#8E2E26',
white: '#FFFFFF',
knotRed: '#C0392B',
cordGold: '#F5E9D0'
};
enum KnotTab {
KNOT = 0,
TASSEL = 1,
CORD = 2,
ORDER = 3,
REVIEW = 4
}
interface TabMeta {
key: string;
icon: string;
label: string;
color: string;
}
const TAB_LIST: TabMeta[] = [
{ key: 'knot', icon: '🀄', label: '绳结', color: '#C0392B' },
{ key: 'tassel', icon: '🎋', label: '流苏', color: '#C9A227' },
{ key: 'cord', icon: '🧶', label: '编绳', color: '#4E5D8C' },
{ key: 'order', icon: '📦', label: '订单', color: '#4E7BA8' },
{ key: 'review', icon: '⭐', label: '客评', color: '#C0392B' }
];
const KNOT_COL: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
interface KnotItem {
name: string;
meaning: string;
size: string;
price: number;
emoji: string;
}
interface TasselItem {
name: string;
color: string;
length: string;
place: string;
emoji: string;
}
interface CordItem {
name: string;
material: string;
level: number;
color: string;
emoji: string;
}
interface KnotOrderItem {
name: string;
region: string;
count: number;
amount: number;
emoji: string;
}
interface KnotReviewItem {
name: string;
score: number;
date: string;
content: string;
tag: string;
emoji: string;
}
interface WeekKnotMeta {
day: string;
value: number;
}
interface MeaningShareMeta {
name: string;
count: number;
color: string;
}
interface WeaveHotMeta {
name: string;
heat: number;
color: string;
}
interface KnotTopMeta {
name: string;
sold: number;
color: string;
}
const WEEK_SOLD: WeekKnotMeta[] = [
{ day: '周一', value: 18 },
{ day: '周二', value: 24 },
{ day: '周三', value: 30 },
{ day: '周四', value: 38 },
{ day: '周五', value: 52 },
{ day: '周日', value: 66 },
{ day: '周六', value: 60 }
];
const MEANING_SHARE: MeaningShareMeta[] = [
{ name: '福寿', count: 5, color: '#C0392B' },
{ name: '姻缘', count: 4, color: '#C9A227' },
{ name: '安康', count: 3, color: '#4E5D8C' }
];
const WEAVE_HOT: WeaveHotMeta[] = [
{ name: '编结', heat: 96, color: '#C0392B' },
{ name: '缠丝', heat: 95, color: '#C9A227' },
{ name: '打穗', heat: 93, color: '#4E5D8C' },
{ name: '穿珠', heat: 92, color: '#4E7BA8' },
{ name: '收口', heat: 90, color: '#7C8AA0' },
{ name: '整烫', heat: 88, color: '#8E2E26' }
];
const KNOT_TOP: KnotTopMeta[] = [
{ name: '同心结', sold: 96, color: '#C0392B' },
{ name: '盘长结', sold: 93, color: '#C9A227' },
{ name: '如意结', sold: 90, color: '#4E5D8C' },
{ name: '团锦结', sold: 87, color: '#7C8AA0' },
{ name: '吉祥结', sold: 84, color: '#4E7BA8' },
{ name: '双钱结', sold: 81, color: '#8E2E26' }
];
@Observed
export class KnotData {
knots: KnotItem[] = [
{ name: '同心结', meaning: '永结同心', size: '中号', price: 68, emoji: '💞' },
{ name: '盘长结', meaning: '回环贯通', size: '大号', price: 98, emoji: '♾️' },
{ name: '如意结', meaning: '万事如意', size: '中号', price: 78, emoji: '🍀' },
{ name: '团锦结', meaning: '团圆美满', size: '小号', price: 48, emoji: '🌸' },
{ name: '吉祥结', meaning: '吉祥安康', size: '中号', price: 72, emoji: '🧧' },
{ name: '双钱结', meaning: '财源广进', size: '小号', price: 42, emoji: '🪙' },
{ name: '纽扣结', meaning: '扣住福气', size: '迷你', price: 28, emoji: '🔘' },
{ name: '琵琶结', meaning: '风雅知音', size: '中号', price: 66, emoji: '🎸' },
{ name: '绣球结', meaning: '抛得良缘', size: '大号', price: 108, emoji: '💠' },
{ name: '流云结', meaning: '平步青云', size: '中号', price: 74, emoji: '☁️' },
{ name: '蝴蝶结', meaning: '福叠而来', size: '小号', price: 38, emoji: '🦋' },
{ name: '祥云结', meaning: '祥瑞临门', size: '大号', price: 118, emoji: '🏮' }
];
tassels: TasselItem[] = [
{ name: '红穗流苏', color: '正红', length: '30cm', place: '北京', emoji: '🔻' },
{ name: '金穗流苏', color: '鎏金', length: '25cm', place: '苏州', emoji: '🟡' },
{ name: '蓝穗流苏', color: '藏蓝', length: '30cm', place: '南京', emoji: '🔵' },
{ name: '粉穗流苏', color: '藕粉', length: '20cm', place: '杭州', emoji: '🩷' },
{ name: '绿穗流苏', color: '竹青', length: '25cm', place: '成都', emoji: '🟢' },
{ name: '紫穗流苏', color: '雪青', length: '30cm', place: '洛阳', emoji: '🟣' },
{ name: '五彩穗', color: '五彩', length: '35cm', place: '潍坊', emoji: '🌈' },
{ name: '同心穗', color: '双色', length: '28cm', place: '西安', emoji: '🎯' },
{ name: '璎珞穗', color: '绛红', length: '40cm', place: '开封', emoji: '📿' },
{ name: '灯笼穗', color: '朱红', length: '22cm', place: '自贡', emoji: '🏮' },
{ name: '宫绦穗', color: '明黄', length: '32cm', place: '北京', emoji: '💛' },
{ name: '剑穗', color: '玄青', length: '26cm', place: '龙泉', emoji: '⚔️' }
];
cords: CordItem[] = [
{ name: '五彩绳', material: '五色丝线', level: 92, color: '彩色', emoji: '🌈' },
{ name: '红头绳', material: '棉芯红线', level: 90, color: '正红', emoji: '🧵' },
{ name: '金芯绳', material: '金线缠芯', level: 95, color: '鎏金', emoji: '✨' },
{ name: '玄青绳', material: '蚕丝股线', level: 93, color: '玄青', emoji: '🟦' },
{ name: '渐变绳', material: '渐变染线', level: 94, color: '渐变', emoji: '🎨' },
{ name: '竹节绳', material: '竹节棉线', level: 89, color: '米白', emoji: '🎋' },
{ name: '珍珠绳', material: '珠线合股', level: 96, color: '珠白', emoji: '🤍' },
{ name: '龙纹绳', material: '织金龙纹', level: 97, color: '金红', emoji: '🐲' },
{ name: '云纹绳', material: '提花云纹', level: 91, color: '月白', emoji: '☁️' },
{ name: '素麻绳', material: '苎麻手搓', level: 88, color: '原麻', emoji: '🪢' },
{ name: '双色绳', material: '双色拧股', level: 90, color: '红金', emoji: '🎀' },
{ name: '夜光绳', material: '夜光丝线', level: 92, color: '夜蓝', emoji: '🌙' }
];
orders: KnotOrderItem[] = [
{ name: '婚庆策划公司', region: '华东', count: 900, amount: 132000, emoji: '💍' },
{ name: '年货批发商', region: '华北', count: 2200, amount: 176000, emoji: '🧧' },
{ name: '文创礼品店', region: '华南', count: 760, amount: 98000, emoji: '🎁' },
{ name: '汉服社团', region: '西南', count: 420, amount: 68000, emoji: '👘' },
{ name: '汽车4S集团', region: '华东', count: 1300, amount: 158000, emoji: '🚗' },
{ name: '茶楼连锁', region: '华中', count: 540, amount: 76000, emoji: '🍵' },
{ name: '国学书院', region: '华北', count: 380, amount: 58000, emoji: '📚' },
{ name: '海外唐人街', region: '海外', count: 640, amount: 168000, emoji: '🌏' },
{ name: '景区纪念品铺', region: '西南', count: 1600, amount: 116000, emoji: '🏔️' },
{ name: '旗袍定制坊', region: '华东', count: 320, amount: 88000, emoji: '🧥' },
{ name: '幼儿园手工课', region: '华中', count: 2800, amount: 64000, emoji: '🧒' },
{ name: '直播间年货节', region: '华东', count: 1900, amount: 124000, emoji: '🎥' }
];
reviews: KnotReviewItem[] = [
{ name: '新娘子', score: 5, date: '09-04', content: '婚车挂了同心结,长辈们个个夸手艺地道。', tag: '喜宴点睛', emoji: '💍' },
{ name: '年货摊主', score: 5, date: '09-03', content: '盘长结卖得最快,一上午补了三次货。', tag: '补货三次', emoji: '🧧' },
{ name: '汉服同袍', score: 5, date: '09-02', content: '宫绦穗配色高级,压得住织金马面。', tag: '配色高级', emoji: '👘' },
{ name: '茶楼掌柜', score: 4, 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: 5, date: '08-29', content: '纽扣结圆润周正,盘扣全靠它撑场面。', tag: '盘扣绝配', emoji: '🧥' },
{ name: '汽车销售', score: 4, date: '08-28', content: '交车挂件客户喜欢,若出烫金礼盒更好。', tag: '交车必备', emoji: '🚗' },
{ name: '书院先生', score: 5, date: '08-27', content: '吉祥结当开学礼,孩子们爱不释手。', tag: '开学好礼', emoji: '📚' },
{ name: '手工博主', score: 5, date: '08-26', content: '龙纹绳细节震撼,视频一夜十万赞。', tag: '一夜爆赞', emoji: '🎥' },
{ name: '老主顾', score: 5, date: '08-25', content: '连买五年本命结,年年都有新花样。', tag: '五年回头', emoji: '🀄' },
{ name: '景区店主', score: 5, date: '08-24', content: '绣球结游客抢着买,五一全卖空了。', tag: '假日卖空', emoji: '🏔️' }
];
}
function getKnotColor(name: string): string {
if (name === '同心结' || name === '吉祥结' || name === '绣球结' || name === '祥云结') {
return '#C0392B';
} else if (name === '盘长结' || name === '如意结' || name === '团锦结') {
return '#C9A227';
} else if (name === '纽扣结' || name === '琵琶结' || name === '流云结' || name === '蝴蝶结') {
return '#4E5D8C';
}
return '#4E7BA8';
}
function getTasselColor(color: string): string {
if (color === '正红' || color === '绛红' || color === '朱红') {
return '#C0392B';
} else if (color === '鎏金' || color === '明黄') {
return '#C9A227';
} else if (color === '藏蓝' || color === '玄青' || color === '夜蓝') {
return '#4E5D8C';
}
return '#4E7BA8';
}
function getCordColor(level: number): string {
if (level >= 95) {
return '#C0392B';
} else if (level >= 91) {
return '#C9A227';
}
return '#4E5D8C';
}
function getOrderColor(amount: number): string {
if (amount >= 150000) {
return '#C0392B';
} else if (amount >= 110000) {
return '#C9A227';
} else if (amount >= 80000) {
return '#4E5D8C';
}
return '#4E7BA8';
}
function getReviewTagColor(tag: string): string {
if (tag === '喜宴点睛' || tag === '年味满满' || tag === '五年回头' || tag === '假日卖空') {
return '#C0392B';
} else if (tag === '补货三次' || tag === '配色高级' || tag === '一夜爆赞') {
return '#C9A227';
} else if (tag === '雅致挂件' || tag === '亲子好物' || tag === '交车必备' || tag === '开学好礼') {
return '#4E5D8C';
}
return '#4E7BA8';
}
@Entry
@Component
struct KnotApp {
@State curTab: number = 0;
@State data: KnotData = new KnotData();
@State showAddKnot: boolean = false;
@State showEditOrder: boolean = false;
@State showDeleteTassel: boolean = false;
@State showDetailCord: boolean = false;
@State delTasselName: string = '';
@State detailCordName: string = '';
@State ballPulse: boolean = false;
@State cordSwing: 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('Knot · 千回百转 结出吉祥')
.fontSize(10)
.fontColor(COLORS.primaryLight)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Row() {
Text('🔴')
.fontSize(18)
.onClick(() => {
this.ballPulse = !this.ballPulse;
})
.scale({ x: this.ballPulse ? 1.3 : 0.9, y: this.ballPulse ? 1.3 : 0.9 })
.animation({ duration: 600, curve: Curve.EaseOut })
Text('🧶')
.fontSize(16)
.margin({ left: 10 })
.onClick(() => {
this.cordSwing = !this.cordSwing;
})
.translate({ x: 0, y: this.cordSwing ? -8 : 4 })
.animation({ duration: 700, curve: Curve.EaseOut })
Text('🏮')
.fontSize(14)
.margin({ left: 6 })
}
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.backgroundColor(COLORS.primaryLight + '2E')
.borderRadius(22)
.border({ width: 1, color: COLORS.primaryLight + '80' })
}
.width('100%')
Row() {
ForEach(KNOT_COL, (r: number) => {
Row() {
if (r % 3 === 0) {
Column()
.width(12)
.height(12)
.backgroundColor(COLORS.knotRed)
.borderRadius(3)
.border({ width: 2, color: COLORS.cordGold })
} else if (r % 3 === 1) {
Column()
.width(8)
.height(8)
.backgroundColor(COLORS.accent)
.borderRadius(4)
} else {
Column()
.width(8)
.height(8)
.backgroundColor(COLORS.success)
.borderRadius(4)
}
}
.width(14)
.justifyContent(FlexAlign.Center)
}, (r: number) => 'knot' + 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 === KnotTab.KNOT) {
KnotContent({ data: this.data, onAdd: () => {
this.showAddKnot = true;
} })
} else if (this.curTab === KnotTab.TASSEL) {
TasselContent({ data: this.data, onDel: (n: string) => {
this.delTasselName = n;
this.showDeleteTassel = true;
} })
} else if (this.curTab === KnotTab.CORD) {
CordContent({ data: this.data, onDetail: (n: string) => {
this.detailCordName = n;
this.showDetailCord = true;
} })
} else if (this.curTab === KnotTab.ORDER) {
KnotOrderContent({ data: this.data, onEdit: () => {
this.showEditOrder = true;
} })
} else {
KnotReviewContent({ data: this.data })
}
}
.width('100%')
.height('100%')
Column() {
Column()
.width('100%')
.height(3)
.backgroundColor(COLORS.primary)
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(20)
.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.showAddKnot) {
AddKnotModal({
onClose: () => {
this.showAddKnot = false;
}
})
}
if (this.showEditOrder) {
EditKnotOrderModal({
onClose: () => {
this.showEditOrder = false;
}
})
}
if (this.showDeleteTassel) {
DeleteTasselModal({
title: this.delTasselName, onClose: () => {
this.showDeleteTassel = false;
}
})
}
if (this.showDetailCord) {
DetailCordModal({
name: this.detailCordName, onClose: () => {
this.showDetailCord = false;
}
})
}
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
}
@Component
struct KnotTag {
@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(8)
.border({ width: 1, color: this.color + '40' })
}
}
@Component
struct KnotContent {
@ObjectLink data: KnotData;
onAdd: () => void = () => {};
build() {
Scroll() {
Column() {
Column() {
Row() {
Text('📈 本周绳结销量')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('周日66件')
.fontSize(9)
.fontColor(COLORS.primary)
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
ForEach(WEEK_SOLD, (w: WeekKnotMeta) => {
Column() {
Text(w.value + '')
.fontSize(8)
.fontColor(w.value >= 50 ? COLORS.primary : COLORS.accent)
Column()
.width(16)
.height(w.value)
.backgroundColor(w.value >= 50 ? COLORS.primary : COLORS.accent)
.borderRadius(8)
.margin({ top: 4 })
Text(w.day)
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (w: WeekKnotMeta) => w.day)
}
.width('100%')
.height(150)
.alignItems(VerticalAlign.Bottom)
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(20)
.border({ width: 1, color: COLORS.accent + '66' })
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 })
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.data.knots, (p: KnotItem) => {
Column() {
Column() {
Text(p.emoji)
.fontSize(20)
.textAlign(TextAlign.Center)
}
.width(52)
.height(52)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor(getKnotColor(p.name) + '14')
.borderRadius(12)
.border({ width: 1, color: getKnotColor(p.name) + '66' })
Text(p.name)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.margin({ top: 6 })
Text('¥' + p.price + ' · ' + p.size)
.fontSize(9)
.fontColor(getKnotColor(p.name))
.margin({ top: 2 })
KnotTag({ text: p.meaning, color: COLORS.accent })
.margin({ top: 4 })
}
.width('31%')
.padding({ top: 10, bottom: 10 })
.alignItems(HorizontalAlign.Center)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.border({ width: 1, color: COLORS.line })
.margin({ top: 8 })
}, (p: KnotItem) => 'kn' + p.name)
}
.width('100%')
}
.width('100%')
.margin({ top: 12 })
}
.width('100%')
.constraintSize({ maxHeight: '100%' })
}
.width('100%')
.constraintSize({ maxHeight: '100%' })
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
}
}
@Component
struct TasselContent {
@ObjectLink data: KnotData;
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(KNOT_TOP, (t: KnotTopMeta) => {
Row() {
Text(t.name)
.fontSize(10)
.width(64)
.fontColor(COLORS.textPrimary)
Row() {
Row()
.layoutWeight(t.sold)
.height(12)
.backgroundColor(t.color)
.borderRadius(6)
Row()
.layoutWeight(100 - t.sold)
.height(12)
.backgroundColor(COLORS.line)
.borderRadius(6)
}
.layoutWeight(1)
.height(12)
.margin({ left: 6 })
Text(t.sold + '')
.fontSize(10)
.width(28)
.textAlign(TextAlign.End)
.fontColor(t.color)
}
.width('100%')
.margin({ top: 6 })
}, (t: KnotTopMeta) => t.name)
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(20)
.border({ width: 1, color: COLORS.accent + '66' })
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.tassels, (p: TasselItem, i: number) => {
Row() {
Column() {
Text(p.emoji)
.fontSize(18)
.textAlign(TextAlign.Center)
}
.width(44)
.height(44)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor(getTasselColor(p.color) + '14')
.borderRadius(10)
.border({ width: 1, color: getTasselColor(p.color) + '66' })
Column() {
Row() {
Text(p.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(p.color)
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ left: 8 })
Text('🗑️')
.fontSize(12)
.margin({ left: 8 })
.onClick(() => {
this.onDel(p.name);
})
}
.width('100%')
.justifyContent(FlexAlign.Start)
Row() {
Text(p.length + ' · ' + p.place + ' 打穗')
.fontSize(9)
.fontColor(getTasselColor(p.color))
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(16)
.border({ width: 1, color: COLORS.line })
.margin({ top: 8 })
}, (p: TasselItem, i: number) => 'ts' + 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 CordContent {
@ObjectLink data: KnotData;
onDetail: (n: string) => 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(MEANING_SHARE, (s: MeaningShareMeta) => {
Row() {
Text(s.name)
.fontSize(10)
.width(40)
.fontColor(COLORS.textPrimary)
Row() {
Row()
.layoutWeight(s.count)
.height(14)
.backgroundColor(s.color)
.borderRadius(7)
Row()
.layoutWeight(12 - s.count)
.height(14)
.backgroundColor(COLORS.line)
.borderRadius(7)
}
.layoutWeight(1)
.height(14)
.margin({ left: 6 })
Text(s.count + ' 款')
.fontSize(10)
.width(36)
.textAlign(TextAlign.End)
.fontColor(s.color)
}
.width('100%')
.margin({ top: 6 })
}, (s: MeaningShareMeta) => s.name)
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(20)
.border({ width: 1, color: COLORS.accent + '66' })
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.cords, (p: CordItem, i: number) => {
Row() {
Column() {
Text(p.emoji)
.fontSize(18)
.textAlign(TextAlign.Center)
}
.width(44)
.height(44)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor(getCordColor(p.level) + '14')
.borderRadius(10)
.border({ width: 1, color: getCordColor(p.level) + '66' })
Column() {
Row() {
Text(p.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(p.color)
.fontSize(9)
.fontColor(COLORS.textSecondary)
.margin({ left: 8 })
Text('👁️')
.fontSize(12)
.margin({ left: 8 })
.onClick(() => {
this.onDetail(p.name);
})
}
.width('100%')
.justifyContent(FlexAlign.Start)
Row() {
Text('股线' + p.level + '级')
.fontSize(9)
.fontColor(getCordColor(p.level))
Text(p.material)
.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(16)
.border({ width: 1, color: COLORS.line })
.margin({ top: 8 })
}, (p: CordItem, i: number) => 'cd' + 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 KnotOrderContent {
@ObjectLink data: KnotData;
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(WEAVE_HOT, (h: WeaveHotMeta) => {
Row() {
Text(h.name)
.fontSize(10)
.width(52)
.fontColor(COLORS.textPrimary)
Row() {
Row()
.layoutWeight(h.heat)
.height(10)
.backgroundColor(h.color)
.borderRadius(5)
Row()
.layoutWeight(100 - h.heat)
.height(10)
.backgroundColor(COLORS.line)
.borderRadius(5)
}
.layoutWeight(1)
.height(10)
.margin({ left: 6 })
Text(h.heat + '')
.fontSize(10)
.width(28)
.textAlign(TextAlign.End)
.fontColor(h.color)
}
.width('100%')
.margin({ top: 6 })
}, (h: WeaveHotMeta) => h.name)
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(20)
.border({ width: 1, color: COLORS.accent + '66' })
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.orders, (o: KnotOrderItem, i: number) => {
Row() {
Column() {
Text(o.emoji)
.fontSize(16)
.textAlign(TextAlign.Center)
}
.width(40)
.height(40)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor(getOrderColor(o.amount) + '14')
.borderRadius(10)
.border({ width: 1, color: getOrderColor(o.amount) + '66' })
Column() {
Row() {
Text(o.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('✏️')
.fontSize(12)
.margin({ left: 8 })
.onClick(() => {
this.onEdit();
})
}
.width('100%')
.justifyContent(FlexAlign.Start)
Row() {
Text(o.region)
.fontSize(9)
.fontColor(getOrderColor(o.amount))
Text('· ' + o.count + ' 件 · ¥' + o.amount)
.fontSize(9)
.fontColor(COLORS.textHint)
.margin({ left: 6 })
}
.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(16)
.border({ width: 1, color: COLORS.line })
.margin({ top: 8 })
}, (o: KnotOrderItem, 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 KnotReviewContent {
@ObjectLink data: KnotData;
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: KnotReviewItem, 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)
KnotTag({ 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(16)
.border({ width: 1, color: COLORS.line })
.margin({ top: 8 })
}, (r: KnotReviewItem, i: number) => 'rv' + r.name + i)
}
.width('100%')
.constraintSize({ maxHeight: '100%' })
}
.width('100%')
.constraintSize({ maxHeight: '100%' })
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
}
}
@Component
struct AddKnotModal {
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(12)
.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(12)
.margin({ top: 8 })
Row() {
Text('定价')
.fontSize(11)
.fontColor(COLORS.textSecondary)
.width(56)
Text('¥88')
.fontSize(11)
.fontColor(COLORS.primary)
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.cardAlt)
.borderRadius(12)
.margin({ top: 8 })
Row() {
Text('取消')
.fontSize(12)
.fontColor(COLORS.textSecondary)
.padding({ left: 16, right: 16, top: 7, bottom: 7 })
.borderRadius(14)
.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(14)
.onClick(() => {
this.onClose();
})
}
.width('100%')
.justifyContent(FlexAlign.End)
.margin({ top: 14 })
}
.width('86%')
.padding(16)
.backgroundColor(COLORS.cardBg)
.borderRadius(20)
.constraintSize({ maxHeight: '78%' })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#66000000')
}
}
@Component
struct EditKnotOrderModal {
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(12)
.margin({ top: 14 })
Row() {
Text('数量')
.fontSize(11)
.fontColor(COLORS.textSecondary)
.width(56)
Text('2200 → 2800 件')
.fontSize(11)
.fontColor(COLORS.primary)
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.cardAlt)
.borderRadius(12)
.margin({ top: 8 })
Row() {
Text('金额')
.fontSize(11)
.fontColor(COLORS.textSecondary)
.width(56)
Text('¥224000')
.fontSize(11)
.fontColor(COLORS.accent)
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.cardAlt)
.borderRadius(12)
.margin({ top: 8 })
Row() {
Text('取消')
.fontSize(12)
.fontColor(COLORS.textSecondary)
.padding({ left: 16, right: 16, top: 7, bottom: 7 })
.borderRadius(14)
.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(14)
.onClick(() => {
this.onClose();
})
}
.width('100%')
.justifyContent(FlexAlign.End)
.margin({ top: 14 })
}
.width('86%')
.padding(16)
.backgroundColor(COLORS.cardBg)
.borderRadius(20)
.constraintSize({ maxHeight: '78%' })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#66000000')
}
}
@Component
struct DeleteTasselModal {
@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(14)
.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(14)
.onClick(() => {
this.onClose();
})
}
.width('100%')
.justifyContent(FlexAlign.End)
.margin({ top: 16 })
}
.width('86%')
.padding(16)
.backgroundColor(COLORS.cardBg)
.borderRadius(20)
.constraintSize({ maxHeight: '78%' })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#66000000')
}
}
@Component
struct DetailCordModal {
@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(14)
.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(12)
.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(12)
.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(14)
.margin({ top: 12 })
.onClick(() => {
this.onClose();
})
}
.width('86%')
.padding(16)
.backgroundColor(COLORS.cardBg)
.borderRadius(20)
.constraintSize({ maxHeight: '78%' })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#66000000')
}
}
十六、开发环境搭建
16.1 安装 DevEco Studio
DevEco Studio 是 HarmonyOS 应用开发的官方 IDE。下载安装包后双击运行,进入安装欢迎界面,点击「下一步」继续。

16.2 选择安装路径
选择目标安装目录,建议安装到非系统盘,确认所需空间后点击「下一步」。

16.3 配置安装选项
设置环境变量,勾选创建桌面快捷方式、将 bin 文件夹添加到 PATH、添加上下文菜单,但是需要重启一下电脑使环境变量生效。

16.4 选择项目模板
新建一个空白模板,在模板市场中选择 Empty Ability。

16.5 配置项目信息
设置 API 为 24 的模板项目,项目名称 kfschool,Compatible SDK 6.1.1(24),设备类型勾选 Phone,点击完成后初始化项目,自动下载相关依赖。

十七、总结
通过对这款中国结坊非遗编结应用的深度剖析,我们可以看到,一个优秀的移动端应用远不止是功能的堆砌,它更是架构设计、数据建模、视觉系统、交互逻辑、防御性编程等多维度能力的综合体现。
在架构层面,该应用采用了"配置驱动 + 组件化 + 状态管理"的三层设计模式。配置层将所有可变的数据与视觉属性抽离为常量数组和元数据接口,使得业务逻辑与表现层彻底解耦;组件层将五个功能页面拆分为独立的 @Component,每个组件内部通过 @Builder 进一步复用 UI 片段;状态层利用 ArkUI 的 @State、@Observed 和 @ObjectLink 机制,实现了数据变化到 UI 刷新的自动响应。
在数据层面,KnotData 类作为核心数据模型,以 5 个数组各 12 条数据完整描述了中国结坊的全部业务信息。5 个动态颜色映射函数将业务数据转化为视觉色彩,实现了"数据即配色"的高级渲染模式,这是该应用最具特色的技术亮点之一。
在视觉层面,本应用独创了"三列网格"的结艺墙设计——绳结页使用 Flex 容器配合 wrap 换行实现三列网格布局,每个卡片宽度 31%,居中对齐,展示绳结图标、名称、价格尺寸和寓意标签,形成了与传统纵向列表截然不同的视觉体验。全局圆角较大(圆角8/12/14/16/20),模仿中国结的圆润造型。统一的色彩系统是整个应用的视觉基石,中国红主色搭配鎏金强调色,配合藏蓝语义色和蓝色装饰色,营造出既传统又喜庆的编结主题氛围。顶部头部的金线镶边装饰带、红球缩放脉冲+编绳浮动微动效、三列网格布局等细节处理,使得界面在信息密度较高的同时依然保持精致与舒适,处处透露出"中国结坊"的主题气质。
在交互层面,应用实现了完整的 CRUD 闭环——新编绳结、修改订单、撤下流苏、编绳详情四类弹窗各司其职,配合统一的条件渲染机制和全屏半透明遮罩,交互流畅且一致。撤穗操作的二次确认、订单金额的动态着色、销量阈值的高亮显示等细节,都体现了对用户体验的深入考量。

标签: HarmonyOS · ArkTS · 华为 · 非遗编结 · 移动端开发
更多推荐



所有评论(0)