年画坊非遗木版年画应用基于HarmonyOS API 24深度解析——采用了"单入口 + 多内容区 + 底部导航"的经典移动端布局模式。入口组件 PrintApp 作为整个应用的根容器

本文基于 HarmonyOS NEXT + ArkTS 声明式开发框架,完整实现一个「年画坊」非遗木版年画主题 App,涵盖配色系统、状态管理、多 Tab 页面、自定义弹窗、动态颜色映射等核心知识点,并附完整源码与运行效果截图。


一、引言:非遗木版年画的数字化传承

在移动互联网与智能设备高速发展的今天,非物质文化遗产的传承与传播已经从单纯的线下演示,演变为一种高度数字化、流程化、个性化的线上线下融合体验。木版年画,作为中国民间艺术的瑰宝,距今已有千余年历史。一张好年画,从选材、刻版、套印、描金到晾纸、装裱,需经数十道工序,凝聚着匠人的心血与墨色的温润。朱红的门神、金黄的福禄寿、翠绿的花鸟,每一张年画都承载着中国人对新年的期盼与祝福。然而,这门古老的技艺正面临着传承人老龄化、受众面狭窄、销售渠道单一等现实困境。

正是在这样的背景下,年画坊类数字化应用应运而生,并迅速成为非遗木版年画"活态传承"的数字载体。一个优秀的年画坊应用,不仅需要覆盖从"年画陈列"到"客评口碑"的完整业务闭环,还要在视觉设计、交互体验、数据呈现上做到细致入微,让用户在指尖滑动间便能感受到墨色的温润与年味的醇厚。
年画模块 - 周年画销量柱状图与年画陈列列表

本文将深入剖析一个基于鸿蒙 ArkUI 声明式开发框架构建的年画坊非遗木版年画应用。该应用以"年画陈列"为核心,横向扩展出版样架、木版库、订单管理、客评口碑五大功能模块,形成了一个完整闭环的年画管理生态。它不仅是一个功能完备的演示项目,更是一份典型的 ArkUI 工程实践范本,涵盖了状态管理、组件化设计、模态弹窗、列表渲染、图表可视化、动态颜色映射等关键技术点。


二、应用整体架构概览

整个应用采用了"单入口 + 多内容区 + 底部导航"的经典移动端布局模式。入口组件 PrintApp 作为整个应用的根容器,负责管理当前激活的 Tab 状态,并根据状态切换不同的内容区组件。

应用一共划分为五个一级页面:年画陈列(PRINT)、版样架(SAMPLE)、木版库(WOOD)、订单管理(ORDER)以及客评口碑(REVIEW)。每个页面都是一个独立的 @Component 装饰的自定义组件,内部封装了自己的状态、构建逻辑和子组件。这种"高内聚、低耦合"的组件化设计,使得每个页面可以独立开发、独立测试,同时也便于后续的扩展与维护。

在数据层面,应用定义了一套完整的类型系统,包括色彩主题、年画元数据、版样元数据、木版元数据、订单元数据、客评元数据、周年销量元数据、题材构成元数据、工艺热度元数据、TOP榜元数据等,并通过常量数组将数据与 UI 表现层解耦。所有业务数据以 @Observed 装饰的可观察类 PrintData 为载体,配合五组各 12 条模拟数据,模拟了一个真实的年画坊数据环境。

在交互层面,应用通过条件渲染(if-else)实现了四类弹窗的叠加显示:添新画、修改订单、撤下版样、木版详情。弹窗以全屏半透明遮罩 + 居中卡片的形式呈现,与主内容共享同一根 Column,通过布尔状态控制显隐。

下面逐页展示应用的五大功能模块运行效果。


三、年画页:应用的核心陈列中枢

年画页是整个应用的默认首页,也是信息密度最高的页面之一。它包含了周年画销量柱状图、年画陈列列表,以及添新画弹窗。

年画模块 - 周年画销量柱状图与年画陈列列表

年画页顶部是"周年画销量"柱状图卡片,通过 ForEach 遍历一周七天的销量数据,柱子高度直接绑定销量数值,销量超过 45 的日期(周五、周六、周日)使用主色朱红 #B03A2E 突出显示,其余日期使用墨色 #33302E,形成自然的高低起伏。周六以 62 张的销量居首,符合年画作为年货在周末热销的真实节奏。

下方是年画陈列列表,12 款年画卡片采用"左图标右文字"的横向布局。左侧图标区展示年画 emoji 图标,背景色通过动态颜色函数按年画名称分档着色,图标下方还有一条墨色横线装饰,模仿木版年画的"墨线"特征。右侧信息区展示年画名称、价格(通过动态颜色函数按价格分档着色)、题材标签、工艺标签和"一纸年味"提示文字。卡片背景使用白色与浅米色奇偶交替,提升长列表可读性。

12 款年画覆盖了从 28 元(迎春纳福)到 98 元(百子图)的完整价格梯队,题材涵盖神仙(福禄寿、门神秦琼/敬德、灶王送福)、祥瑞(连年有余、麒麟送子、鲤鱼跳龙门)、花鸟(喜上眉梢、花开富贵)、农事(五谷丰登)、婴戏(百子图)、福字(迎春纳福)六大类,每款都配有独特的工艺描述(木版套印、线版拓印、套印描金、单版刷印),让年画不仅是视觉艺术品,更是工艺传承的载体。右上角 ➕ 按钮点击可弹出"添新画"弹窗。


四、版样页:刀口定级与撤样管理

版样页以"版样"为核心,顶部是年画销量 TOP6 横向条形图,下方是版样架列表,支持撤样操作。

版样模块 - 年画销量TOP6横向条形图与版样架列表

顶部 TOP6 卡片通过横向进度条展示销量排名前六的年画品种,福禄寿年画以 96 的销量居首,连年有余、门神秦琼紧随其后。进度条使用 layoutWeight 比例法实现,已售部分与剩余部分按比例分配宽度,颜色直接取数据中的配置色,六种年画各有特色。

下方版样架列表展示 12 款版样,卡片采用"左图标右文字"布局。左侧展示版样 emoji 图标,背景色通过动态颜色函数按版样名称分档着色。右侧展示版样名称、刻法(阳刻/阴刻/线刻)、题材与产地(“瑞兽 · 杨家埠”)和 🗑️ 撤样按钮。

12 款版样纹样覆盖龙纹、凤纹、云头、缠枝、娃娃、财神、花篮、回纹、蝙蝠、双鱼、如意、鹤寿十二种传统纹样,刻法分为阳刻、阴刻、线刻三类,产地覆盖中国四大年画产地(杨家埠、朱仙镇、杨柳青、桃花坞)以及武强、滩头、佛山、东阳等地方年画产地,呈现出"版样天下"的格局。点击 🗑️ 按钮弹出"撤下版样"二次确认弹窗,采用警示型设计语言,"确认撤下"按钮使用危险色深褐红,防止误操作。


五、木版页:题材构成与刻版详情

木版页以"木版"为核心,顶部是题材构成占比图,下方是木版库列表,支持查看详情。

木版模块 - 题材构成占比图与木版库列表

顶部题材构成卡片展示三种题材的分布占比:神仙 5 类、祥瑞 4 类、花鸟 3 类,合计 12 类,恰好对应年画陈列的题材总数。进度条以 12 为满刻度进行比例渲染,比百分比更直观地传达了"几类题材"的语义。

下方木版库列表展示 12 款木版,卡片采用"左图标右文字"布局。左侧展示木版 emoji 图标,背景色通过 getSampleColor 函数按匠级分数分档着色(≥96用朱红、≥94用墨黑、其余用竹叶绿)。右侧展示木版名称、木料(梨木/枣木/梓木/杜梨等)、匠级评分(“匠级96”,颜色按匠级动态着色)、产地(“山东 · 刻坊”)和 👁️ 详情按钮。

12 款木版木料覆盖梨木、枣木、梓木、杜梨、椴木、桦木、楠木、榆木、桃木、杉木、乌木、桐木十二种,匠级从 88(桐木轻版)到 98(乌木刻版)不等,乌木刻版以 98 级居首,楠木金版 97 级次之,符合"木以硬为贵、版以细为精"的行业共识。产地从山东、河北、河南、天津、江苏、东北、四川、山西、福建、广东、安徽延伸,呈现出"刻版天下"的格局。点击 👁️ 按钮弹出"木版详情"弹窗,包含木版展示区、文艺风格描述文案(“取深山老木顺纹剖版,阴干三冬始可下刀。刀锋过处墨线立现,一版可印千纸而锋棱不钝。”)、产地信息和匠人评级(“甲级 · 刻版卅载”)。


六、订单页:年画工艺热度与订单改单

订单页以"订单"为核心,顶部是年画工艺热度排行,下方是订单列表,支持修改订单。

订单模块 - 年画工艺热度排行与年画订单列表

顶部年画工艺热度卡片展示六门工序的热度指数:套印 96、线版 95、描金 94、单版 93、拓印 92、裱装 90,恰好对应木版年画制作的完整流程(套印→线版→描金→单版→拓印→裱装),热度依次递减,呈现出"核心工序最热、收尾工序稍低"的合理分布。六根进度条都接近满格,视觉上呈现出"六道工序皆热"的繁荣感,颜色从朱红到墨黑到金棕到竹叶绿到蓝色到浅褐,形成了一条色彩渐变的工艺流水线。

下方订单列表展示 12 条订单,卡片采用"左图标右文字"布局。左侧展示客户 emoji 图标,背景色通过动态颜色函数按订单金额分档着色(12万以上朱红、9-12万金棕、6-9万竹叶绿、6万以下蓝色)。右侧展示客户名称、✏️ 编辑按钮、区域标签和订单详情(“· 1200 张 · ¥96000”)。

12 类客户覆盖年俗商店、民俗博物馆、景区年货节、文创公司、春联集市、非遗展馆、酒店年味部、出版社、直播年货场、庙会组委会、老城商铺、海外文化节。直播年货场以 1800 张的数量居首,非遗展馆以 130000 元的金额居首,海外文化节虽然只有 350 张,但金额高达 156000,单价最高,体现了非遗年画在海外市场的高溢价。点击 ✏️ 按钮弹出"修改订单"弹窗,可调整数量和金额,数量行使用箭头符号"→"直观展示修改前后的对比。


七、客评页:口碑展示与标签体系

客评页是应用中最具"人情味"的页面,展示了 12 条用户好评。

客评模块 - 客评口碑列表

页面顶部是标题行"⭐ 客评口碑"和统计摘要"12条好评 · 平均4.9"。下方 12 条评价卡片采用"左头像右信息"的横向布局:最左侧是用户 emoji 头像,中间是用户名称和日期,右侧是星级评分(通过 String.repeat 一行代码生成对应数量的星星)和评价标签(通过动态颜色函数按标签语义分档着色)。评价内容正文在卡片下方,使用次要色文字和 16px 行高。

12 条评价每条都有独特的标签:“年味立现”、“刀口利落”、“住客合影”、“围观两层”、“最见功力”、“应景喜气”、“墨香纸香”、“供不应求”、“配茶正好”、“拓印生动”、“年年都来”、“值得收藏”,这些标签既概括了评价核心,又形成了年画坊的口碑关键词云。

评价内容充满生活气息,如"朱拓门神一上墙,年味立刻就有了"、“版样刀口利落,套色误差几乎没有”、“讲了半辈子年画,这套版样最见功力”、“杨家埠的墨色,闻着就有纸香”、“每年都来请一对门神,二十年未断”、“乌木刻版纹路温润,值得压箱底”,让数据看起来不是冷冰冰的测试桩,而是真实顾客的心声。卡片背景使用白色与浅米色奇偶交替,整体视觉温馨而舒适。


八、色彩主题系统:朱红墨黑的年画视觉语言

8.1 色彩接口定义

任何一款设计精良的应用,背后都有一套严谨的色彩规范。该应用首先通过一个 PrintPalette 接口,将整个应用所需的色彩抽象为一个结构化的契约:

interface PrintPalette {
  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;
  ink: string;
  paperWhite: string;
}
  • primary:主色调,朱红 #B03A2E,用于按钮、强调文字、激活状态等高优先级视觉元素,是年画的标志性色彩。
  • primaryDark:主色深色变体 #5E1E18,作为顶部头部背景,营造深红年画招牌的沉稳感。
  • accent:强调色,墨黑 #33302E,与朱红形成强烈对比,用于墨线、版样等元素。
  • bg:页面整体背景色 #F4EDDE,宣纸米色,营造年画宣纸的温润质感。
  • ink:墨色 #33302E,专用于墨线装饰和柱状图非高亮色,语义上强调"墨"的质感。
  • paperWhite:纸白 #F7F0E2,用于装饰方块的边框色,营造出宣纸边缘的柔和质感。

8.2 色彩实例化

const COLORS: PrintPalette = {
  primary: '#B03A2E',
  primaryLight: '#D98A7C',
  primaryDark: '#5E1E18',
  accent: '#33302E',
  accentLight: '#6E6660',
  bg: '#F4EDDE',
  cardBg: '#FFFFFF',
  cardAlt: '#EDE2CB',
  textPrimary: '#3A2E28',
  textSecondary: '#7A6A5E',
  textHint: '#A89A8A',
  border: '#E0D2B8',
  line: '#EAE0CC',
  success: '#6E8F5A',
  warning: '#C98A2D',
  danger: '#8E2E26',
  white: '#FFFFFF',
  ink: '#33302E',
  paperWhite: '#F7F0E2'
};

请添加图片描述

主色 #B03A2E 是一种醇厚的朱红,既契合"年画"主题(门神、福字的标志性色彩),又具有传统、喜庆的气质。强调色 #33302E 墨黑与朱红形成强烈对比,呼应木版年画的"墨线"特征。背景色 #F4EDDE 宣纸米色与主色同暖色系,营造出年画宣纸的温润氛围。这种将所有色彩集中管理的做法保证了全局一致性,支持主题切换,降低了维护成本。


九、类型定义:构建严谨的数据契约

应用通过一组接口定义,为每一类业务对象建立了明确的数据契约。

9.1 年画、版样、木版元数据

interface PrintItem {
  name: string; theme: string; craft: string; price: number; emoji: string;
}
interface SampleItem {
  name: string; theme: string; carve: string; place: string; emoji: string;
}
interface WoodItem {
  name: string; wood: string; level: number; place: string; emoji: string;
}

PrintItem 描述年画的名称、题材、工艺、价格和图标。SampleItem 描述版样的名称、题材、刻法(阳刻/阴刻/线刻)、产地和图标。WoodItem 描述木版的名称、木料、匠级评分、产地和图标,用 level 字段体现"以级定品"。

9.2 订单、客评与图表元数据

interface PrintOrderItem {
  name: string; region: string; count: number; amount: number; emoji: string;
}
interface PrintReviewItem {
  name: string; score: number; date: string; content: string; tag: string; emoji: string;
}
interface WeekPrintMeta { day: string; value: number; }
interface ThemeShareMeta { name: string; count: number; color: string; }
interface PrintHotMeta { name: string; heat: number; color: string; }
interface PrintTopMeta { name: string; sold: number; color: string; }

请添加图片描述

四组图表元数据(WeekPrintMeta/ThemeShareMeta/PrintHotMeta/PrintTopMeta)都自带 color 字段,实现了"数据即配色"的图表渲染模式,组件渲染时无需额外的颜色映射逻辑。


十、配置驱动:常量数据与可视化配置

10.1 Tab 导航配置

enum PrintTab { PRINT = 0, SAMPLE = 1, WOOD = 2, ORDER = 3, REVIEW = 4 }

const TAB_LIST: TabMeta[] = [
  { key: 'print', icon: '🖼️', label: '年画', color: '#B03A2E' },
  { key: 'sample', icon: '📜', label: '版样', color: '#33302E' },
  { key: 'wood', icon: '🪵', label: '木版', color: '#6E8F5A' },
  { key: 'order', icon: '📦', label: '订单', color: '#4E7BA8' },
  { key: 'review', icon: '⭐', label: '客评', color: '#C98A2D' }
];

枚举提供类型安全的 Tab 标识,TAB_LIST 为每个 Tab 配置独立的图标、标签和主题色:年画用朱红、版样用墨黑、木版用竹叶绿、订单用蓝色、客评用金棕,"一 Tab 一色"让每个页面有鲜明的视觉身份。

10.2 周年销量、题材构成、工艺热度与 TOP 榜

const WEEK_SOLD = [
  { day: '周一', value: 12 }, { day: '周二', value: 17 },
  { day: '周三', value: 24 }, { day: '周四', value: 33 },
  { day: '周五', value: 46 }, { day: '周六', value: 62 }, { day: '周日', value: 55 }
];
const THEME_SHARE = [
  { name: '神仙', count: 5, color: '#B03A2E' },
  { name: '祥瑞', count: 4, color: '#33302E' },
  { name: '花鸟', count: 3, color: '#6E8F5A' }
];
const PRINT_HOT = [
  { name: '套印', heat: 96 }, { name: '线版', heat: 95 }, { name: '描金', heat: 94 },
  { name: '单版', heat: 93 }, { name: '拓印', heat: 92 }, { name: '裱装', heat: 90 }
];
const PRINT_TOP = [
  { name: '福禄寿年画', sold: 96 }, { name: '连年有余', sold: 93 },
  { name: '门神秦琼', sold: 90 }, { name: '百子图', sold: 87 },
  { name: '鲤鱼跳龙门', sold: 84 }, { name: '花开富贵', sold: 81 }
];

PRINT_HOT 的六门工艺恰好对应木版年画制作的完整流程(套印→线版→描金→单版→拓印→裱装),热度依次递减。PRINT_COL 常量 [0…11] 用于顶部头部的装饰带渲染,按索引模 3 分别渲染:墨色方块带纸白边框(模0)、朱红圆点(模1)、绿色圆点(模2),形成一条色彩斑斓的装饰线,呼应年画的"三色套印"视觉元素。


十一、数据模型:可观察的年画实体

@Observed
export class PrintData {
  prints: PrintItem[] = [ /* 12 款年画 */ ];
  samples: SampleItem[] = [ /* 12 款版样 */ ];
  woods: WoodItem[] = [ /* 12 款木版 */ ];
  orders: PrintOrderItem[] = [ /* 12 条订单 */ ];
  reviews: PrintReviewItem[] = [ /* 12 条客评 */ ];
}

PrintData 是核心数据模型,以五个数组承载年画坊全部业务信息。@Observed 装饰器使得该类实例在被 @State 或 @ObjectLink 引用时,属性变化能自动触发 UI 刷新。每个数组精确包含 12 条数据,"五五对称、各十二条"的设计保证了每个页面都有足够的内容密度。所有数据在类定义时直接初始化,"字段即默认值"的写法避免了构造函数中重复赋值。


十二、模拟数据:构建真实的年画坊数据环境

12.1 年画模拟数据

12 款年画覆盖了从 28 元(迎春纳福)到 98 元(百子图)的完整价格梯队,题材涵盖神仙、祥瑞、花鸟、农事、婴戏、福字六大类,每款都配有独特的工艺描述(木版套印、线版拓印、套印描金、单版刷印)。福禄寿年画¥68、连年有余¥58、五谷丰登¥48、门神秦琼¥88、门神敬德¥88、喜上眉梢¥38、麒麟送子¥72、鲤鱼跳龙门¥66、迎春纳福¥28、灶王送福¥45、花开富贵¥52、百子图¥98,emoji 图标与年画名称呼应(门神用🛡️⚔️、连年有余用🐟、喜上眉梢用🐦、百子图用👶)。

12.2 版样与木版模拟数据

版样纹样覆盖龙纹、凤纹、云头、缠枝、娃娃、财神、花篮、回纹、蝙蝠、双鱼、如意、鹤寿十二种传统纹样,刻法分为阳刻、阴刻、线刻三类,产地覆盖中国四大年画产地(杨家埠、朱仙镇、杨柳青、桃花坞)以及武强、滩头、佛山、东阳等地方年画产地。木版木料覆盖梨木、枣木、梓木、杜梨、椴木、桦木、楠木、榆木、桃木、杉木、乌木、桐木十二种,匠级从 88 到 98 不等,乌木刻版以 98 级居首,符合"木以硬为贵、版以细为精"的行业共识。

12.3 订单与客评模拟数据

订单覆盖 12 类客户,直播年货场以 1800 张的数量居首,非遗展馆以 130000 元的金额居首,海外文化节单价最高。客评 12 条各有独特标签(年味立现、刀口利落、住客合影、围观两层、最见功力、应景喜气、墨香纸香、供不应求、配茶正好、拓印生动、年年都来、值得收藏),评价内容充满生活气息,如"朱拓门神一上墙,年味立刻就有了"、“每年都来请一对门神,二十年未断”。


十三、动态颜色函数:数据驱动的视觉映射

应用通过五个动态颜色函数将业务数据映射为视觉色彩,实现"数据即配色"。

function getPrintColor(name: string): string {
  if (name === '福禄寿年画' || name === '门神秦琼' || name === '门神敬德') return '#B03A2E';
  else if (name === '百子图' || name === '麒麟送子' || name === '灶王送福') return '#33302E';
  else if (name === '喜上眉梢' || name === '花开富贵' || name === '迎春纳福') return '#6E8F5A';
  return '#4E7BA8';
}
function getSampleColor(level: number): string {
  if (level >= 96) return '#B03A2E';
  else if (level >= 94) return '#33302E';
  return '#6E8F5A';
}
function getPriceColor(price: number): string {
  if (price >= 80) return '#B03A2E';
  else if (price >= 60) return '#C98A2D';
  else if (price >= 45) return '#6E8F5A';
  return '#4E7BA8';
}
function getOrderColor(amount: number): string {
  if (amount >= 120000) return '#B03A2E';
  else if (amount >= 90000) return '#C98A2D';
  else if (amount >= 60000) return '#6E8F5A';
  return '#4E7BA8';
}
function getReviewTagColor(tag: string): string { /* 按标签语义分五档着色 */ }
  • getPrintColor:按年画名称分四组色彩(神仙门神类朱红、婴戏神仙类墨黑、花鸟福字类竹叶绿、其他蓝色)。
  • getSampleColor:按匠级分数分三档(96以上朱红、94-95墨黑、其余竹叶绿),“以色定级”。
  • getPriceColor:按价格分四档(80以上朱红、60-79金棕、45-59竹叶绿、45以下蓝色)。
  • getOrderColor:按订单金额分四档(12万以上朱红、9-12万金棕、6-9万竹叶绿、6万以下蓝色)。
  • getReviewTagColor:按评价标签语义分五色系。

值得注意的是,木版页调用 getSampleColor(p.level) 复用版样颜色函数。由于木版匠级 88-98 分布在三档阈值之间,能自然触发三档颜色,为木版生成稳定且有区分度的色彩。这种"函数复用"的技巧避免了为木版单独编写颜色函数。


十四、入口组件:应用的骨架与导航中枢

14.1 组件声明与状态

@Entry
@Component
struct PrintApp {
  @State curTab: number = 0;
  @State data: PrintData = new PrintData();
  @State showAddPrint: boolean = false;
  @State showEditOrder: boolean = false;
  @State showDeleteSample: boolean = false;
  @State showDetailWood: boolean = false;
  @State delSampleName: string = '';
  @State detailWoodName: string = '';
  @State newYearSpin: boolean = false;
  @State fireworkPulse: boolean = false;
}

10 个 @State 状态分四类:导航状态(curTab)、数据状态(data)、弹窗控制状态(4个布尔+2个参数字符串)、交互动效状态(newYearSpin、fireworkPulse)。

14.2 顶部头部构建

顶部头部采用 primaryDark #5E1E18 深红色背景。第一层标题栏左侧是标题"年画坊"和副标题"Print · 一纸年味 百年门神",右侧是动效区:红包🧧点击触发 180 度旋转动画(duration 700ms)、烟花🎆点击触发 1.5 倍缩放脉冲动画(duration 600ms)、鞭炮🧨静态装饰。第二层是"装饰带",通过 ForEach 遍历 PRINT_COL,按模 3 渲染墨色方块带纸白边框、朱红圆点、绿色圆点,形成一条色彩斑斓的装饰线,呼应年画的"三色套印"。第三层是标语分割线"🎨 墨线之间 · 年味入纸 🖼️"。

14.3 底部 Tab 导航

底部导航顶部有一条 3px 高的朱红色装饰条,与深红色头部形成上下呼应。每个 Tab 激活态有三重视觉反馈:标签文字主题色加粗、背景填充主题色 8% 透明度、主题色实线边框,圆角 4(年画直方风格)。非激活态用灰色文字、透明背景、无边框。导航栏高度 60。
在这里插入图片描述

14.4 条件渲染弹窗

应用采用条件渲染(if-else)实现四个弹窗:添新画(AddPrintModal)、修改订单(EditPrintOrderModal)、撤下版样(DeleteSampleModal)、木版详情(DetailWoodModal)。弹窗直接叠加在根 Column 末尾,通过布尔状态控制显隐,每个弹窗内部是全屏半透明遮罩(#66000000)+ 居中白色卡片。弹窗卡片统一使用圆角 18,信息行圆角 10。


十五、关键技术特性对比

特性维度 实现方式 技术要点 设计价值
色彩管理 PrintPalette 接口 + COLORS 常量 集中定义 20 种语义色,含 ink/paperWhite 装饰色 保证视觉一致性,支持主题切换
数据建模 @Observed 装饰的 PrintData 类 5 个数组各 12 条数据,覆盖年画全业务维度 数据与 UI 双向联动,响应式更新
配置驱动 常量数组 + 元数据接口 WEEK_SOLD/THEME_SHARE/PRINT_HOT/PRINT_TOP 自带 color 数据与表现解耦,扩展零成本
动态颜色 5 个全局映射函数 按名称/匠级/价格/金额/标签分档着色,木版复用 getSampleColor 数据即配色,信息密度翻倍
状态管理 @State + @ObjectLink 10 个状态分四类(导航/数据/弹窗/动效) 细粒度状态控制,自动触发渲染
组件化 @Component + @Builder 5 个页面组件 + 4 个弹窗组件 + PrintTag 通用组件 高内聚低耦合,代码复用率高
模态交互 条件渲染弹窗 + 全屏遮罩 4 个弹窗 if-else 叠加,#66000000 半透明遮罩 灵活可控,完全自定义布局
列表渲染 ForEach + 奇偶交替背景 12 条数据 × 5 页面,cardBg/cardAlt 交替 数据驱动渲染,长列表可读性强
数据可视化 柱状图 + 横向条形图 height 绑定数据、layoutWeight 比例法、阈值着色 多维度呈现,直观易读
星级渲染 String.repeat 技巧 ‘⭐’.repeat(score) 一行代码生成星级 简洁高效,无需循环
防御编程 字段默认值 + 类型安全 所有数据字段声明即初始化,枚举替代魔法数字 健壮性强,无运行时异常
布局系统 Column/Row + layoutWeight 弹性权重分配、条件渲染弹窗、Scroll 滚动容器 自适应布局,层次分明
交互动效 rotate + scale + animation 红包旋转180度 700ms、烟花脉冲 600ms、EaseOut 曲线 静态头部灵动化,趣味性强
视觉层次 装饰带/圆角4/墨线装饰 PRINT_COL 模3渲染装饰、卡片圆角4、墨色横线模仿墨线 精致细腻,年画氛围感强

完整代码

interface PrintPalette {
  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;
  ink: string;
  paperWhite: string;
}

const COLORS: PrintPalette = {
  primary: '#B03A2E',
  primaryLight: '#D98A7C',
  primaryDark: '#5E1E18',
  accent: '#33302E',
  accentLight: '#6E6660',
  bg: '#F4EDDE',
  cardBg: '#FFFFFF',
  cardAlt: '#EDE2CB',
  textPrimary: '#3A2E28',
  textSecondary: '#7A6A5E',
  textHint: '#A89A8A',
  border: '#E0D2B8',
  line: '#EAE0CC',
  success: '#6E8F5A',
  warning: '#C98A2D',
  danger: '#8E2E26',
  white: '#FFFFFF',
  ink: '#33302E',
  paperWhite: '#F7F0E2'
};

enum PrintTab {
  PRINT = 0,
  SAMPLE = 1,
  WOOD = 2,
  ORDER = 3,
  REVIEW = 4
}

interface TabMeta {
  key: string;
  icon: string;
  label: string;
  color: string;
}

const TAB_LIST: TabMeta[] = [
  { key: 'print', icon: '🖼️', label: '年画', color: '#B03A2E' },
  { key: 'sample', icon: '📜', label: '版样', color: '#33302E' },
  { key: 'wood', icon: '🪵', label: '木版', color: '#6E8F5A' },
  { key: 'order', icon: '📦', label: '订单', color: '#4E7BA8' },
  { key: 'review', icon: '⭐', label: '客评', color: '#C98A2D' }
];

const PRINT_COL: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];

interface PrintItem {
  name: string;
  theme: string;
  craft: string;
  price: number;
  emoji: string;
}

interface SampleItem {
  name: string;
  theme: string;
  carve: string;
  place: string;
  emoji: string;
}

interface WoodItem {
  name: string;
  wood: string;
  level: number;
  place: string;
  emoji: string;
}

interface PrintOrderItem {
  name: string;
  region: string;
  count: number;
  amount: number;
  emoji: string;
}

interface PrintReviewItem {
  name: string;
  score: number;
  date: string;
  content: string;
  tag: string;
  emoji: string;
}

interface WeekPrintMeta {
  day: string;
  value: number;
}

interface ThemeShareMeta {
  name: string;
  count: number;
  color: string;
}

interface PrintHotMeta {
  name: string;
  heat: number;
  color: string;
}

interface PrintTopMeta {
  name: string;
  sold: number;
  color: string;
}

const WEEK_SOLD: WeekPrintMeta[] = [
  { day: '周一', value: 12 },
  { day: '周二', value: 17 },
  { day: '周三', value: 24 },
  { day: '周四', value: 33 },
  { day: '周五', value: 46 },
  { day: '周六', value: 62 },
  { day: '周日', value: 55 }
];

const THEME_SHARE: ThemeShareMeta[] = [
  { name: '神仙', count: 5, color: '#B03A2E' },
  { name: '祥瑞', count: 4, color: '#33302E' },
  { name: '花鸟', count: 3, color: '#6E8F5A' }
];

const PRINT_HOT: PrintHotMeta[] = [
  { name: '套印', heat: 96, color: '#B03A2E' },
  { name: '线版', heat: 95, color: '#33302E' },
  { name: '描金', heat: 94, color: '#C98A2D' },
  { name: '单版', heat: 93, color: '#6E8F5A' },
  { name: '拓印', heat: 92, color: '#4E7BA8' },
  { name: '裱装', heat: 90, color: '#B08B5A' }
];

const PRINT_TOP: PrintTopMeta[] = [
  { name: '福禄寿年画', sold: 96, color: '#B03A2E' },
  { name: '连年有余', sold: 93, color: '#33302E' },
  { name: '门神秦琼', sold: 90, color: '#C98A2D' },
  { name: '百子图', sold: 87, color: '#B08B5A' },
  { name: '鲤鱼跳龙门', sold: 84, color: '#6E8F5A' },
  { name: '花开富贵', sold: 81, color: '#4E7BA8' }
];

@Observed
export class PrintData {
  prints: PrintItem[] = [
    { name: '福禄寿年画', theme: '神仙', craft: '木版套印', price: 68, emoji: '🧧' },
    { name: '连年有余', theme: '祥瑞', craft: '木版套印', price: 58, emoji: '🐟' },
    { name: '五谷丰登', theme: '农事', craft: '线版拓印', price: 48, emoji: '🌾' },
    { name: '门神秦琼', theme: '门神', craft: '套印描金', price: 88, emoji: '🛡️' },
    { name: '门神敬德', theme: '门神', craft: '套印描金', price: 88, emoji: '⚔️' },
    { name: '喜上眉梢', theme: '花鸟', craft: '线版拓印', price: 38, emoji: '🐦' },
    { name: '麒麟送子', theme: '婴戏', craft: '套印', price: 72, emoji: '🐲' },
    { name: '鲤鱼跳龙门', theme: '祥瑞', craft: '套印描金', price: 66, emoji: '🐠' },
    { name: '迎春纳福', theme: '福字', craft: '单版刷印', price: 28, emoji: '🌸' },
    { name: '灶王送福', theme: '神仙', craft: '线版拓印', price: 45, emoji: '🔥' },
    { name: '花开富贵', theme: '花鸟', craft: '套印', price: 52, emoji: '🌺' },
    { name: '百子图', theme: '婴戏', craft: '套印', price: 98, emoji: '👶' }
  ];

  samples: SampleItem[] = [
    { name: '龙纹版样', theme: '瑞兽', carve: '阳刻', place: '杨家埠', emoji: '🐉' },
    { name: '凤纹版样', theme: '瑞兽', carve: '阳刻', place: '朱仙镇', emoji: '🦚' },
    { name: '云头版样', theme: '纹样', carve: '阴刻', place: '杨柳青', emoji: '☁️' },
    { name: '缠枝版样', theme: '纹样', carve: '阳刻', place: '桃花坞', emoji: '🌿' },
    { name: '娃娃版样', theme: '婴戏', carve: '阳刻', place: '杨家埠', emoji: '👶' },
    { name: '财神版样', theme: '神仙', carve: '线刻', place: '朱仙镇', emoji: '🧧' },
    { name: '花篮版样', theme: '花鸟', carve: '阳刻', place: '桃花坞', emoji: '🧺' },
    { name: '回纹版样', theme: '纹样', carve: '阴刻', place: '杨柳青', emoji: '🔁' },
    { name: '蝙蝠版样', theme: '瑞兽', carve: '阳刻', place: '武强', emoji: '🦇' },
    { name: '双鱼版样', theme: '祥瑞', carve: '阳刻', place: '滩头', emoji: '🐟' },
    { name: '如意版样', theme: '纹样', carve: '阴刻', place: '佛山', emoji: '🪷' },
    { name: '鹤寿版样', theme: '花鸟', carve: '阳刻', place: '杨柳青', emoji: '🦢' }
  ];

  woods: WoodItem[] = [
    { name: '梨木套版', wood: '梨木', level: 96, place: '山东', emoji: '🪵' },
    { name: '枣木主版', wood: '枣木', level: 95, place: '河北', emoji: '🌳' },
    { name: '梓木副版', wood: '梓木', level: 94, place: '河南', emoji: '🍂' },
    { name: '杜梨细版', wood: '杜梨', level: 96, place: '天津', emoji: '🍐' },
    { name: '椴木试版', wood: '椴木', level: 90, place: '江苏', emoji: '🌲' },
    { name: '桦木边版', wood: '桦木', level: 91, place: '东北', emoji: '🪵' },
    { name: '楠木金版', wood: '楠木', level: 97, place: '四川', emoji: '🌴' },
    { name: '榆木老版', wood: '榆木', level: 93, place: '山西', emoji: '🌰' },
    { name: '桃木辟邪版', wood: '桃木', level: 92, place: '河南', emoji: '🍑' },
    { name: '杉木衬版', wood: '杉木', level: 89, place: '福建', emoji: '🎋' },
    { name: '乌木刻版', wood: '乌木', level: 98, place: '广东', emoji: '⚫' },
    { name: '桐木轻版', wood: '桐木', level: 88, place: '安徽', emoji: '🌼' }
  ];

  orders: PrintOrderItem[] = [
    { name: '年俗商店', region: '华北', count: 1200, amount: 96000, emoji: '🏮' },
    { name: '民俗博物馆', region: '华东', count: 300, amount: 120000, emoji: '🏛️' },
    { name: '景区年货节', region: '华南', count: 860, amount: 68000, emoji: '🎪' },
    { name: '文创公司', region: '华东', count: 500, amount: 98000, emoji: '🎁' },
    { name: '春联集市', region: '华中', count: 1500, amount: 72000, emoji: '🧧' },
    { name: '非遗展馆', region: '华北', count: 260, amount: 130000, emoji: '🏅' },
    { name: '酒店年味部', region: '华南', count: 640, amount: 51000, emoji: '🏨' },
    { name: '出版社', region: '华东', count: 420, amount: 105000, emoji: '📚' },
    { name: '直播年货场', region: '华东', count: 1800, amount: 88000, emoji: '📱' },
    { name: '庙会组委会', region: '西南', count: 920, amount: 64000, emoji: '🏮' },
    { name: '老城商铺', region: '华北', count: 780, amount: 46000, emoji: '🏘️' },
    { name: '海外文化节', region: '海外', count: 350, amount: 156000, emoji: '🌏' }
  ];

  reviews: PrintReviewItem[] = [
    { 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: 5, date: '09-01', content: '百子图摊开展台,围观的人围了两层。', tag: '围观两层', emoji: '🎪' },
    { name: '非遗讲师', score: 5, date: '08-31', content: '讲了半辈子年画,这套版样最见功力。', tag: '最见功力', emoji: '🎓' },
    { name: '婚庆策划', score: 4, date: '08-30', content: '喜鹊登梅很应景,若能加裱装更完美。', tag: '应景喜气', emoji: '💒' },
    { name: '老画迷', score: 5, date: '08-29', content: '杨家埠的墨色,闻着就有纸香。', tag: '墨香纸香', emoji: '🖌️' },
    { name: '景区店主', score: 5, date: '08-28', content: '连年有余一天走六七十张,供不应求。', tag: '供不应求', emoji: '🏞️' },
    { name: '茶楼掌柜', score: 4, 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 getPrintColor(name: string): string {
  if (name === '福禄寿年画' || name === '门神秦琼' || name === '门神敬德') {
    return '#B03A2E';
  } else if (name === '百子图' || name === '麒麟送子' || name === '灶王送福') {
    return '#33302E';
  } else if (name === '喜上眉梢' || name === '花开富贵' || name === '迎春纳福') {
    return '#6E8F5A';
  }
  return '#4E7BA8';
}

function getSampleColor(level: number): string {
  if (level >= 96) {
    return '#B03A2E';
  } else if (level >= 94) {
    return '#33302E';
  }
  return '#6E8F5A';
}

function getPriceColor(price: number): string {
  if (price >= 80) {
    return '#B03A2E';
  } else if (price >= 60) {
    return '#C98A2D';
  } else if (price >= 45) {
    return '#6E8F5A';
  }
  return '#4E7BA8';
}

function getOrderColor(amount: number): string {
  if (amount >= 120000) {
    return '#B03A2E';
  } else if (amount >= 90000) {
    return '#C98A2D';
  } else if (amount >= 60000) {
    return '#6E8F5A';
  }
  return '#4E7BA8';
}

function getReviewTagColor(tag: string): string {
  if (tag === '年味立现' || tag === '刀口利落' || tag === '墨香纸香' || tag === '值得收藏') {
    return '#B03A2E';
  } else if (tag === '围观两层' || tag === '最见功力' || tag === '供不应求') {
    return '#33302E';
  } else if (tag === '住客合影' || tag === '配茶正好' || tag === '拓印生动' || tag === '年年都来') {
    return '#6E8F5A';
  } else if (tag === '应景喜气') {
    return '#4E7BA8';
  }
  return '#C98A2D';
}

@Entry
@Component
struct PrintApp {
  @State curTab: number = 0;
  @State data: PrintData = new PrintData();
  @State showAddPrint: boolean = false;
  @State showEditOrder: boolean = false;
  @State showDeleteSample: boolean = false;
  @State showDetailWood: boolean = false;
  @State delSampleName: string = '';
  @State detailWoodName: string = '';
  @State newYearSpin: boolean = false;
  @State fireworkPulse: 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('Print · 一纸年味 百年门神')
                .fontSize(10)
                .fontColor(COLORS.primaryLight)
                .margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Row() {
              Text('🧧')
                .fontSize(18)
                .onClick(() => {
                  this.newYearSpin = !this.newYearSpin;
                })
                .rotate({ angle: this.newYearSpin ? 180 : 0 })
                .animation({ duration: 700, curve: Curve.EaseOut })
              Text('🎆')
                .fontSize(16)
                .margin({ left: 10 })
                .onClick(() => {
                  this.fireworkPulse = !this.fireworkPulse;
                })
                .scale({ x: this.fireworkPulse ? 1.5 : 1, y: this.fireworkPulse ? 1.5 : 1 })
                .animation({ duration: 600, 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(PRINT_COL, (r: number) => {
              Row() {
                if (r % 3 === 0) {
                  Column()
                    .width(12)
                    .height(12)
                    .backgroundColor(COLORS.ink)
                    .borderRadius(2)
                    .border({ width: 2, color: COLORS.paperWhite })
                } else if (r % 3 === 1) {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.primary)
                    .borderRadius(4)
                } else {
                  Column()
                    .width(8)
                    .height(8)
                    .backgroundColor(COLORS.success)
                    .borderRadius(4)
                }
              }
              .width(14)
              .justifyContent(FlexAlign.Center)
            }, (r: number) => 'print' + 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 === PrintTab.PRINT) {
          PrintContent({ data: this.data, onAdd: () => {
            this.showAddPrint = true;
          } })
        } else if (this.curTab === PrintTab.SAMPLE) {
          SampleContent({ data: this.data, onDel: (n: string) => {
            this.delSampleName = n;
            this.showDeleteSample = true;
          } })
        } else if (this.curTab === PrintTab.WOOD) {
          WoodContent({ data: this.data, onDetail: (n: string) => {
            this.detailWoodName = n;
            this.showDetailWood = true;
          } })
        } else if (this.curTab === PrintTab.ORDER) {
          PrintOrderContent({ data: this.data, onEdit: () => {
            this.showEditOrder = true;
          } })
        } else {
          PrintReviewContent({ 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(4)
            .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.showAddPrint) {
        AddPrintModal({
          onClose: () => {
            this.showAddPrint = false;
          }
        })
      }
      if (this.showEditOrder) {
        EditPrintOrderModal({
          onClose: () => {
            this.showEditOrder = false;
          }
        })
      }
      if (this.showDeleteSample) {
        DeleteSampleModal({
          title: this.delSampleName, onClose: () => {
            this.showDeleteSample = false;
          }
        })
      }
      if (this.showDetailWood) {
        DetailWoodModal({
          name: this.detailWoodName, onClose: () => {
            this.showDetailWood = false;
          }
        })
      }
    }
  }
}

@Component
struct PrintTag {
  @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 PrintContent {
  @ObjectLink data: PrintData;
  onAdd: () => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Column()
              .width(12)
              .height(12)
              .backgroundColor(COLORS.ink)
              .borderRadius(2)
              .border({ width: 2, color: COLORS.paperWhite })
            Column()
              .layoutWeight(1)
              .height(2)
              .backgroundColor(COLORS.primary + '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.success)
              .borderRadius(2)
              .border({ width: 2, color: COLORS.paperWhite })
            Column()
              .layoutWeight(1)
              .height(2)
              .backgroundColor(COLORS.primary + '55')
              .margin({ left: 4, right: 4 })
            Column()
              .width(12)
              .height(12)
              .backgroundColor(COLORS.ink)
              .borderRadius(2)
              .border({ width: 2, color: COLORS.paperWhite })
          }
          .width('100%')
          .margin({ bottom: 10 })
          Row() {
            Text('📈 周年画销量')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('周六62张')
              .fontSize(9)
              .fontColor(COLORS.primary)
              .margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          Row() {
            ForEach(WEEK_SOLD, (w: WeekPrintMeta) => {
              Column() {
                Text(w.value + '')
                  .fontSize(8)
                  .fontColor(w.value >= 45 ? COLORS.primary : COLORS.ink)
                Column()
                  .width(16)
                  .height(w.value)
                  .backgroundColor(w.value >= 45 ? COLORS.primary : COLORS.ink)
                  .borderRadius(8)
                  .margin({ top: 4 })
                Text(w.day)
                  .fontSize(9)
                  .fontColor(COLORS.textSecondary)
                  .margin({ top: 4 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
            }, (w: WeekPrintMeta) => 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.primary + '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.prints, (p: PrintItem, i: number) => {
            Row() {
              Column() {
                Column() {
                  Text(p.emoji)
                    .fontSize(18)
                    .textAlign(TextAlign.Center)
                }
                .width(46)
                .height(46)
                .justifyContent(FlexAlign.Center)
                .alignItems(HorizontalAlign.Center)
                .backgroundColor(getPrintColor(p.name) + '14')
                .borderRadius(8)
                .border({ width: 1, color: getPrintColor(p.name) + '66' })
                Row() {
                  Column()
                    .layoutWeight(1)
                    .height(3)
                    .backgroundColor(COLORS.ink)
                    .margin({ left: 4, right: 4 })
                }
                .width(40)
                .margin({ top: 5 })
              }
              .width(64)
              .padding({ top: 10, bottom: 6 })
              .backgroundColor(getPrintColor(p.name) + '0D')
              .borderRadius(8)
              .border({ width: 1, color: getPrintColor(p.name) + '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() {
                  PrintTag({ text: p.theme, color: getPrintColor(p.name) })
                  PrintTag({ 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: PrintItem, i: number) => 'pr' + 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 SampleContent {
  @ObjectLink data: PrintData;
  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(PRINT_TOP, (t: PrintTopMeta) => {
            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: PrintTopMeta) => t.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .border({ width: 1, color: COLORS.primary + '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.samples, (p: SampleItem, i: number) => {
            Row() {
              Column() {
                Text(p.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
              }
              .width(42)
              .height(42)
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)
              .backgroundColor(getPrintColor(p.name) + '14')
              .borderRadius(8)
              .border({ width: 1, color: getPrintColor(p.name) + '66' })

              Column() {
                Row() {
                  Text(p.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(p.carve)
                    .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.theme + ' · ' + p.place)
                    .fontSize(9)
                    .fontColor(getPrintColor(p.name))
                  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: SampleItem, i: number) => 'sp' + 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 WoodContent {
  @ObjectLink data: PrintData;
  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(THEME_SHARE, (s: ThemeShareMeta) => {
            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: ThemeShareMeta) => s.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .border({ width: 1, color: COLORS.primary + '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.woods, (p: WoodItem, i: number) => {
            Row() {
              Column() {
                Text(p.emoji)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
              }
              .width(42)
              .height(42)
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)
              .backgroundColor(getSampleColor(p.level) + '14')
              .borderRadius(8)
              .border({ width: 1, color: getSampleColor(p.level) + '66' })

              Column() {
                Row() {
                  Text(p.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(p.wood)
                    .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(getSampleColor(p.level))
                  Text(p.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 })
            }
            .width('100%')
            .padding(10)
            .backgroundColor(i % 2 === 0 ? COLORS.cardBg : COLORS.cardAlt)
            .borderRadius(14)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (p: WoodItem, i: number) => 'wd' + 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 PrintOrderContent {
  @ObjectLink data: PrintData;
  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(PRINT_HOT, (h: PrintHotMeta) => {
            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: PrintHotMeta) => h.name)
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .border({ width: 1, color: COLORS.primary + '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.orders, (o: PrintOrderItem, i: number) => {
            Row() {
              Column() {
                Text(o.emoji)
                  .fontSize(16)
                  .textAlign(TextAlign.Center)
              }
              .width(38)
              .height(38)
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)
              .backgroundColor(getOrderColor(o.amount) + '14')
              .borderRadius(19)
              .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(14)
            .border({ width: 1, color: COLORS.line })
            .margin({ top: 8 })
          }, (o: PrintOrderItem, 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 PrintReviewContent {
  @ObjectLink data: PrintData;

  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: PrintReviewItem, 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)
              PrintTag({ 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: PrintReviewItem, i: number) => 'rv' + r.name + i)
      }
      .width('100%')
      .constraintSize({ maxHeight: '100%' })
    }
    .width('100%')
    .constraintSize({ maxHeight: '100%' })
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
  }
}

@Component
struct AddPrintModal {
  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('¥68')
            .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 EditPrintOrderModal {
  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('260 → 320 张')
            .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('¥160000')
            .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 DeleteSampleModal {
  @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 DetailWoodModal {
  @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')
  }
}


十六、开发环境搭建

16.1 安装 DevEco Studio

DevEco Studio 是 HarmonyOS 应用开发的官方 IDE。下载安装包后双击运行,进入安装欢迎界面,点击「下一步」继续。

DevEco Studio 安装欢迎界面

16.2 选择安装路径

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

选择安装位置

16.3 配置安装选项

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

安装选项配置

16.4 选择项目模板

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

选择 Empty Ability 模板

16.5 配置项目信息

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

配置项目信息


十七、总结

通过对这款年画坊非遗木版年画应用的深度剖析,我们可以看到,一个优秀的移动端应用远不止是功能的堆砌,它更是架构设计、数据建模、视觉系统、交互逻辑、防御性编程等多维度能力的综合体现。

在架构层面,该应用采用了"配置驱动 + 组件化 + 状态管理"的三层设计模式。配置层将所有可变的数据与视觉属性抽离为常量数组和元数据接口,使得业务逻辑与表现层彻底解耦;组件层将五个功能页面拆分为独立的 @Component,每个组件内部通过 @Builder 进一步复用 UI 片段;状态层利用 ArkUI 的 @State、@Observed 和 @ObjectLink 机制,实现了数据变化到 UI 刷新的自动响应。

在数据层面,PrintData 类作为核心数据模型,以 5 个数组各 12 条数据完整描述了年画坊的全部业务信息。5 个动态颜色映射函数将业务数据转化为视觉色彩,实现了"数据即配色"的高级渲染模式,这是该应用最具特色的技术亮点之一。

在交互层面,应用实现了完整的 CRUD 闭环——添新画、修改订单、撤下版样、木版详情四类弹窗各司其职,配合统一的条件渲染机制和全屏半透明遮罩,交互流畅且一致。撤样操作的二次确认、订单金额的动态着色、销量阈值的高亮显示等细节,都体现了对用户体验的深入考量。顶部头部红包旋转、烟花脉冲等微动效的加入,更是为静态界面增添了一丝灵动与喜庆。
年画模块 - 周年画销量柱状图与年画陈列列表

在视觉层面,统一的色彩系统是整个应用的视觉基石。朱红主色搭配墨黑强调色,配合竹叶绿语义色和金棕装饰色,营造出既传统又喜庆的年画主题氛围。顶部头部的三色装饰带、墨色横线模仿墨线、4px 小圆角等细节处理,使得界面在信息密度较高的同时依然保持精致与舒适,处处透露出"年画坊"的主题气质。


标签: HarmonyOS · ArkTS · 华为 · 非遗年画 · 移动端开发

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐