基于HarmonyOS API 24的幻影AI数字照相馆深度解析——紫色渐变主题、六Tab全差异化布局与AI写真生成闭环的完整实现

AI写真,一键生成你的专属大片。本文完整拆解一个基于鸿蒙 HarmonyOS API 24 与 ArkUI 声明式框架构建的「幻影AI」数字照相馆应用,覆盖紫色渐变主题配色系统、三色渐变头部、六Tab全差异化布局、AI写真大Banner、热门模板横滑、风格分类、精选作品、三列模板网格、四列相册网格、月度生成统计柱状图、创意工坊双渐变卡、会员权益卡、四动画状态联动、积分消耗闭环、Stack弹窗叠加、生成AI写真/修改订单/删除作品三大弹窗等核心技术点,附完整源码与运行效果截图。


一、引言:AI写真的数字化浪潮

在人工智能技术高速发展的今天,AI写真已经从传统的"去影楼拍照、等几天取片",演变为一种即时化、个性化、大众化的数字消费新形态。AI写真,以其"无需出门、一键生成、风格多样、成本低廉"的特点,正在成为数字内容创作的关键赛道。从古风汉服到赛博朋克,从日系动漫到油画肖像,从复古港风到婚纱大片,每一种风格都能满足用户对不同自我形象的探索与表达。然而,模板质量参差不齐、生成效果不可控、积分消耗不透明、作品管理混乱,正成为用户获取可靠AI写真服务的现实痛点。

正是在这样的背景下,幻影AI类数字照相馆应用应运而生,并迅速成为AI内容创作的数字基础设施。一个优秀的AI写真应用,不仅需要覆盖从"选模板"到"生成"再到"管理"的完整业务闭环,还要在视觉设计、交互体验、数据呈现上做到科技而浪漫,让用户在指尖滑动间便能完成选模板、传照片、看进度、管作品的全流程。

本文将深入剖析一个基于鸿蒙 HarmonyOS API 24 与 ArkUI 声明式开发框架构建的幻影AI数字照相馆应用。该应用以"首页"为核心,横向扩展出写真、相册、订单、工坊、我的六大功能模块,每个模块采用完全不同的布局风格——首页大Banner+热门横滑+风格分类+精选作品、写真三列网格+全部模板列表、相册四列网格+收藏作品、订单月度柱状图+订单列表、工坊双渐变卡+创意工具列表、我的用户卡+统计+会员权益+菜单,形成了一个完整闭环的AI写真生态。它不仅是一个功能完备的演示项目,更是一份典型的 ArkUI 工程实践范本,涵盖了状态管理、组件化设计、Stack弹窗叠加、四动画状态联动、紫色渐变视觉语言、积分消耗闭环、月度柱状图三档着色等关键技术点。

幻影AI应用整体运行效果 - 紫色三色渐变头部与AI写真新体验大Banner及热门模板横滑


二、应用整体架构概览

整个应用采用了"单入口 + 六Tab全差异化布局 + Stack弹窗叠加"的AI内容创作应用架构。入口组件 AIPhotoStudioApp 作为整个应用的根容器,负责管理当前激活的 Tab 状态、积分余额、四个动画状态以及三个弹窗的显隐状态。

应用一共划分为六个一级页面:首页(HOME)、写真(PHOTO)、相册(ALBUM)、订单(ORDER)、工坊(WORKSHOP)以及我的(PROFILE)。与之前项目"页面布局风格统一或半差异化"不同,本应用的六个 Tab 每个布局完全不同——首页采用大Banner+热门横滑+风格分类+精选作品、写真采用三列网格+全部模板列表、相册采用四列网格+收藏作品、订单采用月度柱状图+订单列表、工坊采用双渐变卡+创意工具列表、我的采用用户卡+统计+会员权益+菜单,这种"一页一型"的差异化设计最大程度还原了真实AI写真App的交互体验。底部导航采用单排六Tab布局,激活Tab有紫色下划线指示器,简洁统一。

在数据层面,应用定义了一套完整的类型系统,包括色彩主题、写真模板元数据、用户作品元数据、订单元数据、创意工具元数据、月度统计接口等,并通过常量数组将数据与 UI 表现层解耦。所有业务数据以 @Observed 装饰的可观察类为载体(PhotoTemplate/UserPhoto/OrderInfo/CreativeTemplate),配合 10 个写真模板、12 个用户作品、10 条订单、8 个创意工具的模拟数据,模拟了一个真实的AI写真数据环境。所有数据类都带有 constructor 构造函数,使用 new PhotoTemplate(...) 方式实例化。MonthStat 采用普通 interface 接口而非 @Observed 类,因为月度数据是静态统计数据无需响应式更新。

在交互层面,应用通过 Stack 布局叠加实现了三类弹窗的显示:生成AI写真、修改订单、删除作品。与之前项目"每个弹窗独立 Stack 叠加"不同,本应用独创了 modalOverlay 统一封装——一个 @Builder modalOverlay(onClose) 构建器,内部通过 if-else 判断当前显示哪个弹窗内容,外层统一全屏 rgba(0,0,0,0.4) 遮罩 + 居中 90% 宽卡片 maxHeight 80%,点击遮罩同时关闭所有三个弹窗状态。这种统一封装大幅减少了重复代码,三个弹窗共享同一套遮罩与卡片样式。生成AI写真弹窗确认后 credits -= selectedTemplate.price(积分消耗闭环),修改订单弹窗展示订单信息只读,删除作品弹窗二次确认后关闭,形成了完整的交互闭环。

在视觉层面,应用独创了"紫色渐变主题"设计——顶部 header 使用 linearGradient 90 度从 primaryDark #4A148C 深紫黑 → primary #7B1FA2 深紫(0.5位置)→ accent #E040FB 亮紫品红的三色斜向渐变,"幻影AI"白色标题 + "AI数字照相馆"白色副标题 0.85 透明度,右侧积分胶囊"🎨 580"白字半透明白底圆角 16,是本应用最具辨识度的视觉元素。首页大Banner使用 linearGradient 135 度从 primaryDark 到 accent 的两色渐变,工坊两大卡片分别使用 135 度两色渐变(primary→accent / primaryDark→primary),会员权益卡使用 90 度两色渐变(primary→accent),形成了统一的紫色渐变视觉语言,贯穿整个应用。chart1-6 六种图表色全是紫色系(#7B1FA2/#E040FB/#9C27B0/#AB47BC/#CE93D8/#BA68C8),数据可视化统一紫色调。应用还独创了"四动画状态联动"设计——aboutToAppear 中 setInterval 1500ms 同步切换四个布尔动画状态(breathAnim/blinkAnim/pulseAnim/floatAnim),分别用于不同元素:blinkAnim 用于收藏作品星星缩放和订单生成中状态缩放、pulseAnim 用于热门模板tag/全部模板tag/创意工具按钮/VIP标签缩放、floatAnim 用于精选作品收藏星星缩放和月度柱状图缩放,四个动画同步切换形成整体协调的联动动效体系。应用还独创了"积分消耗闭环"设计——header 显示积分胶囊,生成AI写真弹窗确认后 credits -= price,我的页显示积分,形成积分获取-消耗-展示的完整闭环。

幻影AI生成AI写真弹窗运行效果 - 弹窗叠加首页显示古风汉服写真模板信息与积分消耗

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


三、写真页:三列模板网格与全部模板列表

写真页以"AI写真模板"为核心,独创"三列网格+全部模板列表"的双层布局。

写真模块 - AI写真模板三列网格与全部模板详细列表

页面顶部第一部分是标题"📸 AI写真模板"加粗 16 号。下方第二部分是三列模板网格,使用三排 Row 布局,每排三个模板(slice(0,3)/slice(3,6)/slice(6,9)),每个模板卡片 layoutWeight 1 均分宽度,padding 上下 14,白底圆角 12,margin 右 8,包含:图标 36 号 margin 下 4 + 模板名称(11 号 maxLines 1 省略)+ 价格(primary 深紫 12 号加粗)。三列网格让用户快速浏览模板缩略图和价格,点击卡片触发 generateModal 生成AI写真弹窗,selectedTemplate 记录当前模板。

下方第三部分是全部模板列表,标题"📋 全部模板"加粗 16 号 margin 下 12。遍历 10 个模板,每个模板横向列表布局,padding 12,白底圆角 12,margin 下 8,包含:

左侧:图标 32 号 margin 右 12 + 模板信息列(layoutWeight 1,左对齐):模板名称(加粗 14 号)+ 标签行(风格标签 tagBg 紫底 tagText 深紫字圆角 4 + tag 标签 primary 紫底白字圆角 4 margin 左 4,tag 标签带 pulseAnim 缩放动画 this.pulseAnim ? { x: 1.05, y: 1.05 } : { x: 1.0, y: 1.0 } duration 600ms)+ 评分行(★评分 warning 橙 10 号 + 使用人数 textSub 灰 10 号 margin 左 6)。

右侧:价格按钮列(右对齐):价格(primary 深紫 15 号加粗)+ 生成按钮(56×26 primary 紫底圆角 13 margin 上 6,点击触发 generateModal)。

10 个模板覆盖十大风格:古风汉服写真(国风,¥99,4.9分,3580人用,热门)、赛博朋克霓虹(科幻,¥129,4.8分,2890人用,新品)、日系动漫二次元(二次元,¥89,4.9分,4520人用,热销)、油画肖像风格(艺术,¥159,4.7分,1680人用,高端)、复古港风胶片(复古,¥109,4.6分,2340人用,经典)、水彩梦境少女(梦幻,¥119,4.8分,1950人用,推荐)、暗黑哥特风格(暗黑,¥139,4.5分,890人用,限定)、工笔花鸟国风(国风,¥99,4.6分,1230人用,新品)、婚纱写真大片(婚纱,¥199,4.9分,3120人用,热门)、黑白纪实摄影(纪实,¥89,4.4分,670人用,经典)。价格从 ¥89 到 ¥199,评分从 4.4 到 4.9,5 个 isHot 热门模板,完整还原了AI写真模板市场的多样性。全部模板列表的 tag 标签 pulseAnim 缩放动画让静态的模板列表有了"热门推荐"的动态感。


四、相册页:四列作品网格与收藏作品列表

相册页以"我的相册"为核心,独创"四列作品网格+收藏作品列表"的双层布局。

相册模块 - 我的相册四列网格三排与收藏作品列表

页面顶部第一部分是标题行"🖼 我的相册"加粗 16 号 layoutWeight 1 + "12张"textSub 灰 12 号(右对齐),margin 下 12。

下方第二部分是四列作品网格,使用三排 Row 布局,每排四个作品(slice(0,4)/slice(4,8)/slice(8,12)),每个作品卡片 layoutWeight 1 均分宽度,padding 上下 12,白底圆角 10,margin 右 6,包含:图标 28 号 margin 下 4 + 作品标题(9 号 maxLines 1 省略)+ ❤点赞数(danger 红 8 号 margin 上 2)。四列网格让用户快速浏览作品缩略图,第三排作品点击触发 deletePhotoModal 删除作品弹窗,selectedPhotoId 记录当前作品 ID。

下方第三部分是收藏作品列表,标题"⭐ 收藏作品"加粗 15 号 margin 下 12。遍历 this.photos.filter((p: UserPhoto) => p.isFavorite) 筛选出 4 个收藏作品,每个作品横向列表布局,padding 12,白底圆角 12,margin 下 8,包含:

左侧:图标 32 号 margin 右 12 + 作品信息列(layoutWeight 1,左对齐):作品标题(加粗 14 号)+ 模板名(textSub 灰 11 号 margin 上 2)+ 点赞行(❤点赞数 danger 红 10 号 + 日期 textSub 灰 10 号 margin 左 8)。

右侧:★星星(warning 橙 18 号,带 blinkAnim 缩放动画 this.blinkAnim ? { x: 1.1, y: 1.1 } : { x: 1.0, y: 1.0 } duration 500ms)。

12 个作品从 08-19 到 08-08 按时间倒序排列,4 个收藏作品(紫禁之巅/樱花飘雪/梦境水彩/白色誓言/霓虹都市2,共5个?让我数一下:p1紫禁之巅true、p3樱花飘雪true、p6梦境水彩true、p9白色誓言true、p12霓虹都市2true,共5个收藏),点赞数从 67 到 489,模板覆盖十大风格,完整还原了用户的AI写真作品库。收藏作品的 ★星星 blinkAnim 缩放动画让收藏列表有了"闪烁"的动态感,是本应用动效设计的亮点之一。


五、订单页:月度生成统计柱状图与订单记录列表

订单页以"AI写真订单"为核心,独创"月度生成统计柱状图+订单记录列表"的复合布局。

订单模块 - 月度生成统计柱状图三档着色与订单记录列表

页面顶部第一部分是月度生成统计柱状图卡片,标题"月度生成统计"加粗 15 号 margin 下 16。下方 Row 横向排列 6 个月柱子,每栏 layoutWeight 1 居中对齐,卡片 height 160,alignItems Bottom 让柱子从底部生长。每个柱子包含:

  • 柱子本体:width 26,height = stat.count * 1.5(生成数乘以 1.5 作为像素高度),backgroundColor 按生成数分三档动态着色(count > 60 用 danger 红色:5月62/8月78;count > 40 用 primary 深紫:4月45/7月55;其余用 accentLight 浅品红:3月28/6月38),borderRadius 4,justifyContent End 让文字从底部对齐,padding 上 2,柱子内白字显示生成数(textWhite 9 号)。柱子带 floatAnim 缩放动画(this.floatAnim ? { x: 1.03, y: 1.03 } : { x: 1.0, y: 1.0 } duration 600ms),营造出轻微的浮动感。
  • 月份标签:stat.month(3月/4月/5月/6月/7月/8月)textSub 灰 9 号,margin 上 4。

6 个月数据从 3 月到 8 月,生成数从 28 到 78,平均评分从 4.5 到 4.9,5 月(62,五一假期)和 8 月(78,暑期)用红色突出显示,符合AI写真"假期和暑期高峰"的使用规律。三档着色(红/紫/浅品红)让生成量高低一目了然,柱子内白字显示数值避免了额外标注,floatAnim 缩放动画让静态柱状图有了轻微的呼吸感。

下方第二部分是订单记录列表,标题行"📋 订单记录"加粗 16 号 layoutWeight 1 + "共10单"textSub 灰 12 号(右对齐),margin 下 12。遍历 10 条订单,每条订单卡片布局,padding 12,白底圆角 12,margin 下 8,包含:

顶部行:订单号(textSub 灰 10 号 layoutWeight 1)+ 状态(动态着色:已完成 success 绿/生成中 warning 橙/已退款 danger 红,加粗 10 号,生成中状态带 blinkAnim 缩放动画 order.status === '生成中' && this.blinkAnim ? { x: 1.05, y: 1.05 } : { x: 1.0, y: 1.0 } duration 500ms)。

中部行:📸图标 28 号 margin 右 12 + 订单信息列(layoutWeight 1,左对齐):模板名(加粗 14 号)+ 数量 x count(textSub 灰 11 号 margin 上 4)+ 日期金额行(日期 textSub 灰 11 号 layoutWeight 1 + 金额 primary 深紫 15 号加粗,右对齐,margin 上 4)。

底部行:生成中订单显示"⏳ AI正在生成中…"(warning 橙 11 号 layoutWeight 1),已完成订单显示编辑按钮(primary 紫底 60×24 圆角 12 margin 左 8,点击触发 editOrderModal 修改订单弹窗,selectedOrder 记录当前订单),所有订单显示白底边框按钮(60×24 圆角 12)。

10 条订单从 08-19 到 08-10 按时间倒序排列,状态涵盖已完成(8 条)、生成中(1 条,油画肖像风格 08-16)、已退款(1 条,复古港风胶片 08-13),金额从 ¥89 到 ¥199,数量从 1 到 3,完整还原了用户的AI写真订单历史。生成中状态的 blinkAnim 缩放动画 + "⏳ AI正在生成中…"提示让用户一眼就能判断订单流转状态,是本应用交互设计的亮点之一。


六、工坊页:双渐变卡与创意工具列表

工坊页以"创意工坊"为核心,独创"双渐变卡+创意工具列表"的双层布局。

工坊模块 - 创意工坊双渐变卡片与创意工具列表

页面顶部第一部分是标题"🎨 创意工坊"加粗 16 号 margin 下 12。

下方第二部分是双渐变卡,使用 Row 横向布局,两张卡片各 layoutWeight 1:

左卡片"AI智能生成":Column 居中对齐,padding 上下 20,linearGradient 135 度从 primary #7B1FA2 深紫到 accent #E040FB 亮紫品红,圆角 16,包含:🤖图标 44 号 margin 下 8 + "AI智能生成"白字 15 号加粗 + "上传照片·一键写真"白字 11 号 0.85 透明度 margin 上 4 + 白色按钮 100×32 圆角 16 margin 上 12(点击触发 generateModal 生成AI写真弹窗)。

右卡片"批量处理":Column 居中对齐,padding 上下 20,linearGradient 135 度从 primaryDark #4A148C 深紫黑到 primary #7B1FA2 深紫,圆角 16,margin 左 10,包含:📊图标 44 号 margin 下 8 + "批量处理"白字 15 号加粗 + "多图·一键搞定"白字 11 号 0.85 透明度 margin 上 4 + 白色按钮 100×32 圆角 16 margin 上 12。

双渐变卡使用两种不同的紫色渐变(左卡 primary→accent 亮渐变,右卡 primaryDark→primary 深渐变),形成深浅对比,让创意工坊的两大核心功能一目了然。左卡"AI智能生成"是主要入口,使用更亮的渐变吸引用户点击。

下方第三部分是创意工具列表,标题"🛠 创意工具"加粗 15 号 margin 下 12。遍历 8 个创意工具,每个工具横向列表布局,padding 12,白底圆角 12,margin 下 8,包含:

左侧:图标 32 号 margin 右 12 + 工具信息列(layoutWeight 1,左对齐):工具名(加粗 14 号)+ 描述(textSub 灰 11 号 margin 上 2)+ 分类下载行(分类标签 tagBg 紫底 tagText 深紫字圆角 4 + 下载次数 textSub 灰 10 号 margin 左 6)。

右侧:圆形按钮(28×28 primary 紫底圆角 14,带 pulseAnim 缩放动画 this.pulseAnim ? { x: 1.05, y: 1.05 } : { x: 1.0, y: 1.0 } duration 700ms)。

8 个创意工具覆盖四大类别:趣味(AI换脸8560次/AI漫画脸5670次/AI婚纱预览3450次/AI头像生成6780次)、修复(AI老照片修复6230次/AI黑白上色4890次)、编辑(AI背景替换7120次)、实用(AI证件照9340次,下载量最高)。下载量从 3450 到 9340,完整还原了AI创意工具的热度分布。创意工具按钮的 pulseAnim 缩放动画让静态的工具列表有了"可点击"的动态感,双渐变卡的深浅对比让工坊页视觉层次丰富。


七、我的页:用户卡、四栏统计、会员权益卡与菜单列表

我的页以"个人中心"为核心,采用"用户卡+四栏统计+会员权益卡+菜单列表"的四层布局。

我的模块 - 用户卡、四栏统计、会员权益渐变卡与菜单列表

页面顶部第一部分是用户卡,横向布局,padding 16,白底圆角 16,margin 下 16:Circle 头像 64×64 fill primaryLight 浅紫 + 用户信息列(layoutWeight 1,margin 左 12,左对齐):用户名"幻影用户"加粗 18 号 + 标签行(VIP 标签 primary 紫底白字圆角 8 padding 左右 6 上下 2,带 pulseAnim 缩放动画 this.pulseAnim ? { x: 1.05, y: 1.05 } : { x: 1.0, y: 1.0 } duration 700ms + "积分 580"textSub 灰 10 号 margin 左 8,margin 上 4)。

第二部分是四栏统计卡片,横向排列四个数据指标,每栏 layoutWeight 1 居中对齐,padding 上下 16,白底圆角 16,margin 下 16:作品数 12(primary 深紫 22 号加粗)、订单数 10(accent 亮紫 22 号加粗)、积分 580(warning 橙 22 号加粗)、收藏 4(danger 红 22 号加粗),每个指标下方配文字标签(作品数/订单数/积分/收藏)textSub 灰 10 号 margin 上 2。四栏统计用四种颜色对应四种数据维度,一目了然。

第三部分是会员权益卡,Column 左对齐,padding 16,linearGradient 90 度从 primary #7B1FA2 深紫到 accent #E040FB 亮紫品红,圆角 16,margin 下 16:"👑 会员权益"白字 15 号加粗 + "每日无限生成 · 优先队列 · 专属模板"白字 11 号 0.85 透明度 margin 上 4。会员权益卡使用 90 度紫色渐变,与 header 的 90 度三色渐变呼应,形成统一的紫色渐变视觉语言。

第四部分是菜单列表,遍历 5 项(我的作品/积分商城/邀请好友/帮助反馈/关于我们),每项横向布局,padding 上下 14 左右 16,白底圆角 12,margin 下 8:功能名 text 14 号 layoutWeight 1 + ">"textSub 灰 14 号(右箭头)。菜单列表简洁明了,用户可点击进入对应功能页面。

我的页四层布局(用户卡+统计+会员权益+菜单)完整覆盖了个人中心的核心功能,VIP 标签的 pulseAnim 缩放动画让用户卡有了"会员尊贵"的动态感,会员权益卡的 90 度紫色渐变与 header 呼应,四栏统计的四种颜色让数据维度一目了然。


八、色彩主题系统:紫色渐变主题与六色紫系图表的科技浪漫视觉语言

8.1 色彩接口定义

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

interface ColorPalette {
  bg: string; cardBg: string;
  primary: string; primaryLight: string; primaryDark: string;
  accent: string; accentLight: string;
  text: string; textSub: string; textWhite: string;
  border: string; tagBg: string; tagText: string; shadow: string;
  success: string; warning: string; danger: string;
  chart1: string; chart2: string; chart3: string;
  chart4: string; chart5: string; chart6: string;
}
  • bg:页面整体背景色 #F3E5F5,紫白底,营造AI数字照相馆的科技浪漫氛围。
  • primary:主色调 #7B1FA2,深紫,用于按钮、强调文字、激活Tab、价格、渐变头部中间色等高优先级视觉元素,是AI写真的标志性色彩(神秘、创意、高端)。
  • primaryDark:深主色 #4A148C,深紫黑,用于渐变头部起始色、工坊右卡片渐变起始色,与 primary 形成同色系深浅对比。
  • accent:辅助色 #E040FB,亮紫品红,用于渐变头部结束色、首页Banner渐变结束色、工坊左卡片渐变结束色、会员权益卡渐变结束色、订单数统计、收藏星星等,与深紫形成"深浅紫"的渐变组合,象征AI的科技感与创造力。
  • text:文字主色 #1A1A2E,深蓝黑,用于主标题文字,与紫白底形成高对比度。
  • shadow:阴影色 #D1C4E9,紫阴影,用于卡片阴影,营造柔和的紫色阴影效果。
  • chart1-chart6:六种图表色全是紫色系(#7B1FA2/#E040FB/#9C27B0/#AB47BC/#CE93D8/#BA68C8),数据可视化统一紫色调,保证图表与整体主题的一致性。

幻影AI修改订单弹窗运行效果 - 弹窗叠加订单页显示订单号模板金额状态信息

8.2 色彩实例化

const COLORS: ColorPalette = {
  bg: '#F3E5F5', cardBg: '#FFFFFF',
  primary: '#7B1FA2', primaryLight: '#CE93D8', primaryDark: '#4A148C',
  accent: '#E040FB', accentLight: '#EA80FC',
  text: '#1A1A2E', textSub: '#9E9E9E', textWhite: '#FFFFFF',
  border: '#E1BEE7', tagBg: '#F3E5F5', tagText: '#7B1FA2', shadow: '#D1C4E9',
  success: '#4CAF50', warning: '#FF9800', danger: '#F44336',
  chart1: '#7B1FA2', chart2: '#E040FB', chart3: '#9C27B0',
  chart4: '#AB47BC', chart5: '#CE93D8', chart6: '#BA68C8',
};

主色 #7B1FA2 是一种神秘的深紫,既契合"AI数字照相馆"主题(神秘、创意、高端、科技的标志性色彩),又具有优雅、浪漫的气质。辅助色 #E040FB 亮紫品红与深紫形成"深浅紫"的渐变组合,是本应用最具辨识度的色彩组合。bg #F3E5F5 紫白底与主色同紫色系,营造出AI数字照相馆的科技浪漫氛围。

值得注意的是,本应用的渐变设计是色彩系统的核心亮点——header 使用 linearGradient 90 度从 primaryDark #4A148C 深紫黑 → primary #7B1FA2 深紫(0.5位置)→ accent #E040FB 亮紫品红的三色斜向渐变,是本应用最具辨识度的视觉元素;首页大Banner 使用 linearGradient 135 度从 primaryDark 到 accent 的两色渐变;工坊两大卡片分别使用 135 度两色渐变(左卡 primary→accent 亮渐变,右卡 primaryDark→primary 深渐变);会员权益卡使用 90 度两色渐变(primary→accent)。五种不同角度和配色的紫色渐变贯穿整个应用,形成了统一而丰富的紫色渐变视觉语言,是本应用色彩设计的核心特色。

本应用的动态颜色逻辑也十分丰富——月度柱状图按生成数分三档着色(>60 danger 红/>40 primary 深紫/其余 accentLight 浅品红),订单状态按状态分三档着色(已完成 success 绿/生成中 warning 橙/已退款 danger 红),风格分类分两排着色(第一排 primary 深紫/第二排 accent 亮紫),四栏统计分四色(作品数 primary/订单数 accent/积分 warning/收藏 danger),形成了"以色定量级、以色定状态、以色定分类"的多层语义着色模式。formatPrice 工具函数统一价格格式为 ¥XX.XX,价格统一使用 primary 深紫加粗,形成了一致的价格视觉语言。chart1-6 六种图表色全是紫色系,保证数据可视化与整体主题的一致性。这种将所有色彩集中管理、配合多维度渐变与动态着色的做法保证了全局一致性,降低了维护成本。

幻影AI首页紫色渐变主题效果 - 三色渐变头部与大Banner及风格分类的紫色配色体系


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

应用通过一组带 constructor 的 @Observed 类,为每一类业务对象建立了明确的数据契约。

9.1 写真模板、用户作品、订单元数据

@Observed
class PhotoTemplate {
  id: string; name: string; style: string; price: number;
  image: string; uses: number; rating: number; tag: string; isHot: boolean;
  constructor(...) { ... }
}
@Observed
class UserPhoto {
  id: string; title: string; template: string; date: string;
  url: string; likes: number; isFavorite: boolean;
  constructor(...) { ... }
}
@Observed
class OrderInfo {
  id: string; orderNo: string; templateName: string; amount: number;
  status: string; date: string; count: number;
  constructor(...) { ... }
}

PhotoTemplate 描述写真模板的 ID、名称、风格、价格、图标、使用人数、评分、标签、是否热门,用 price 字段体现模板价格,uses/rating/isHot 三维度体现模板热度与品质。UserPhoto 描述用户作品的 ID、标题、模板名、日期、图标、点赞数、是否收藏,用 isFavorite 布尔字段体现收藏状态,likes 字段体现作品热度。OrderInfo 描述订单的 ID、订单号、模板名、金额、状态、日期、数量,用 status 字段体现订单状态(已完成/生成中/已退款),count 字段体现生成数量。

9.2 创意工具元数据与月度统计接口

@Observed
class CreativeTemplate {
  id: string; name: string; desc: string; image: string;
  category: string; downloads: number;
  constructor(...) { ... }
}
interface MonthStat {
  month: string; count: number; avgScore: number;
}
function formatPrice(price: number): string {
  return '¥' + price.toFixed(2);
}

CreativeTemplate 描述创意工具的 ID、名称、描述、图标、分类、下载次数,用 category 字段体现工具分类(趣味/修复/编辑/实用),downloads 字段体现工具热度。MonthStat 采用普通 interface 接口而非 @Observed 类,包含月份、生成数、平均评分三个字段,因为月度数据是静态统计数据无需响应式更新,这种"按需选择 @Observed 或 interface"的设计体现了对响应式机制的精准理解,避免了不必要的响应式开销。formatPrice 工具函数统一价格格式为 ¥XX.XX,toFixed(2) 保证两位小数,是本应用唯一的工具函数。所有数据类都带有 constructor 构造函数,使用 new ClassName(...) 方式实例化,配合 @Observed 装饰器实现响应式更新。


十、配置驱动:常量数据与工具函数

10.1 Tab 导航配置

private tabItems: string[] = ['首页', '写真', '相册', '订单', '工坊', '我的'];

与之前项目"TabMeta 对象数组配置"不同,本应用的 Tab 导航采用最简的字符串数组配置,仅包含 Tab 名称,共 6 个 Tab。底部 tabBar 通过 ForEach 遍历 tabItems,每个 Tab 用 Column 包裹文字(11 号,激活态 primary 深紫加粗,非激活 textSub 灰),height 52,layoutWeight 1 均分,点击切换 currentTab。特别的是,激活 Tab 下方有紫色下划线指示器(Row width 20 height 3 primary 深紫圆角 2 margin 上 4),是本应用底部导航的特色设计,让用户一眼就能判断当前所在页面。

10.2 模拟数据与工具函数

private templates: PhotoTemplate[] = [ new PhotoTemplate('t1', '古风汉服写真', '国风', 99, '🏮', 3580, 4.9, '热门', true), ... ];
private photos: UserPhoto[] = [ new UserPhoto('p1', '紫禁之巅', '古风汉服写真', '08-19', '🏮', 328, true), ... ];
private orders: OrderInfo[] = [ new OrderInfo('o1', 'AI20260819001', '古风汉服写真', 99, '已完成', '08-19', 1), ... ];
private creativeTemplates: CreativeTemplate[] = [ new CreativeTemplate('c1', 'AI换脸', '上传照片一键换脸', '😊', '趣味', 8560), ... ];
private monthStats: MonthStat[] = [ { month: '3月', count: 28, avgScore: 4.6 }, ... ];

六组常量数据承载了幻影AI的全部业务信息:templates 10 个写真模板(覆盖国风/科幻/二次元/艺术/复古/梦幻/暗黑/婚纱/纪实十大风格,价格从 ¥89 到 ¥199,评分从 4.4 到 4.9,5 个 isHot 热门)、photos 12 个用户作品(5 个收藏,点赞数从 67 到 489)、orders 10 条订单(8 已完成/1 生成中/1 已退款,金额从 ¥89 到 ¥199)、creativeTemplates 8 个创意工具(覆盖趣味/修复/编辑/实用四大类别,下载量从 3450 到 9340)、monthStats 6 个月数据(生成数从 28 到 78,平均评分从 4.5 到 4.9)。所有 @Observed 类数据使用 new ClassName(...) 构造函数实例化,MonthStat 使用对象字面量。formatPrice 工具函数统一价格格式,是本应用唯一的工具函数。


十一、动态颜色与四动画状态:数据驱动的视觉映射与联动动效

应用通过动态颜色逻辑和四动画状态联动,将业务数据转化为视觉色彩与动态效果,实现"数据即配色、状态即动效"。

11.1 动态颜色逻辑

本应用的动态颜色逻辑分布在多个维度:

  • 月度柱状图着色stat.count > 60 ? COLORS.danger : stat.count > 40 ? COLORS.primary : COLORS.accentLight,按生成数分三档(>60 红/>40 深紫/其余浅品红),5 月 62 和 8 月 78 用红色突出,符合假期和暑期高峰规律。
  • 订单状态着色order.status === '已完成' ? COLORS.success : order.status === '生成中' ? COLORS.warning : COLORS.danger,按订单状态分三档(已完成绿/生成中橙/已退款红)。
  • 风格分类着色:第一排国风/二次元/科幻/复古用 primary 深紫字,第二排艺术/梦幻/暗黑/婚纱用 accent 亮紫字,两排颜色区分形成视觉层次。
  • 四栏统计着色:作品数 primary 深紫/订单数 accent 亮紫/积分 warning 橙/收藏 danger 红,四种颜色对应四种数据维度。
  • 热门模板tag着色:热门模板的 tag 标签用 danger 红底白字,position 定位右上角,突出热门属性。

11.2 四动画状态联动

aboutToAppear(): void {
  this.timerId = setInterval(() => {
    this.breathAnim = !this.breathAnim;
    this.blinkAnim = !this.blinkAnim;
    this.pulseAnim = !this.pulseAnim;
    this.floatAnim = !this.floatAnim;
  }, 1500);
}

aboutToAppear 中启动 setInterval 1500ms 同步切换四个布尔动画状态(breathAnim/blinkAnim/pulseAnim/floatAnim),aboutToDisappear 中 clearInterval 清理定时器避免内存泄漏。四个动画状态分别用于不同元素:

  • breathAnim:定义但未在 UI 中使用(预留扩展),体现了"预留动画状态"的工程习惯。
  • blinkAnim:用于收藏作品列表的 ★星星缩放(this.blinkAnim ? { x: 1.1, y: 1.1 } : { x: 1.0, y: 1.0 },duration 500ms)和订单生成中状态缩放(order.status === '生成中' && this.blinkAnim ? { x: 1.05, y: 1.05 } : { x: 1.0, y: 1.0 },duration 500ms),两个元素共用同一 blinkAnim 状态,缩放比例不同(星星 1.1/状态 1.05),营造出"闪烁"的动态感。
  • pulseAnim:用于热门模板 tag 缩放(scale 1.1,duration 600ms)、全部模板列表的 tag 缩放(scale 1.05,duration 600ms)、创意工具按钮缩放(scale 1.05,duration 700ms)、VIP 标签缩放(scale 1.05,duration 700ms),四个元素共用同一 pulseAnim 状态,缩放比例和 duration 不同,营造出"脉冲"的动态感。
  • floatAnim:用于精选作品的收藏 ★星星缩放(this.floatAnim && photo.isFavorite ? { x: 1.1, y: 1.1 } : { x: 1.0, y: 1.0 },仅收藏的作品缩放,duration 500ms)和月度柱状图缩放(scale 1.03,duration 600ms),两个元素共用同一 floatAnim 状态,缩放比例不同,营造出"浮动"的动态感。

四个动画状态同步切换(1500ms 周期),分别用于不同元素,形成了整体协调的联动动效体系。blinkAnim 的收藏星星闪烁和订单生成中状态脉冲、pulseAnim 的热门标签脉冲和 VIP 标签缩放、floatAnim 的精选作品星星浮动和柱状图浮动,三种动效各司其职,让静态的页面有了"鲜活"的动态感,是本应用动效设计的核心亮点。特别的是,精选作品的收藏星星用 floatAnim(仅收藏的缩放),收藏作品列表的星星用 blinkAnim(全部缩放),两种不同的动效用于不同页面的收藏星星,体现了动效设计的细致考量。


十二、入口组件:应用的骨架与六Tab全差异化布局

12.1 组件声明与状态

@Entry
@Component
struct AIPhotoStudioApp {
  @State currentTab: number = 0;
  @State showGenerateModal: boolean = false;
  @State showEditOrderModal: boolean = false;
  @State showDeletePhotoModal: boolean = false;
  @State selectedTemplate: PhotoTemplate = new PhotoTemplate('t0', '', '', 0, '', 0, 0, '', false);
  @State selectedOrder: OrderInfo = new OrderInfo('o0', '', '', 0, '', '', 0);
  @State selectedPhotoId: string = '';
  @State credits: number = 580;
  @State breathAnim: boolean = false;
  @State blinkAnim: boolean = false;
  @State pulseAnim: boolean = false;
  @State floatAnim: boolean = false;
  private timerId: number = -1;
}

14 个 @State 状态分六类:导航状态(currentTab)、弹窗控制状态(showGenerateModal/showEditOrderModal/showDeletePhotoModal)、弹窗数据状态(selectedTemplate/selectedOrder/selectedPhotoId,selectedTemplate 和 selectedOrder 用 new ClassName(...) 初始化空对象避免空引用)、业务数据状态(credits 积分余额 580)、动画状态(breathAnim/blinkAnim/pulseAnim/floatAnim)、定时器引用(private timerId)。private 字段(templates/photos/orders/creativeTemplates/monthStats/tabItems)存储静态模拟数据,不触发响应式更新。

12.2 生命周期与四动画联动

aboutToAppear(): void {
  this.timerId = setInterval(() => {
    this.breathAnim = !this.breathAnim;
    this.blinkAnim = !this.blinkAnim;
    this.pulseAnim = !this.pulseAnim;
    this.floatAnim = !this.floatAnim;
  }, 1500);
}
aboutToDisappear(): void {
  if (this.timerId !== -1) {
    clearInterval(this.timerId);
    this.timerId = -1;
  }
}

aboutToAppear 中启动 setInterval 1500ms 同步切换四个动画状态,实现四动画联动。aboutToDisappear 中判断 timerId 不为 -1 时 clearInterval 并重置为 -1,避免内存泄漏和重复启动。这种"生命周期管理定时器"的模式是 ArkUI 动效的标准实践。

12.3 紫色三色渐变头部(@Builder header)

顶部头部通过 @Builder header() 抽取,Row 横向布局,height 56,padding 左右 20,linearGradient 90 度从 primaryDark #4A148C 深紫黑 → primary #7B1FA2 深紫(0.5位置)→ accent #E040FB 亮紫品红的三色渐变,alignItems Center。内部 Column 左对齐 layoutWeight 1:"幻影AI"白色 22 号加粗 + "AI数字照相馆"白色 11 号 0.85 透明度 margin 上 2。右侧积分胶囊 Row:"🎨 " + this.credits(动态显示积分余额)白字 12 号,padding 左右 10 上下 6,半透明白底 rgba(255,255,255,0.2),圆角 16。紫色三色渐变头部是本应用最具辨识度的视觉元素,从深紫黑到深紫到亮紫品红的 90 度三色渐变,象征AI从神秘到创造的完整光谱,右侧积分胶囊动态显示积分余额,形成积分展示闭环。

12.4 底部 Tab 导航(带下划线指示器)与 Stack 弹窗

底部导航通过 @Builder tabBar() 构建,Row 内 ForEach 遍历 tabItems 6 个 Tab,每个 Tab Column 包裹:文字(11 号,激活 primary 深紫加粗,非激活 textSub 灰)+ 激活态下划线指示器(if currentTab === index 时显示 Row width 20 height 3 primary 深紫圆角 2 margin 上 4),height 52 layoutWeight 1 均分,点击切换 currentTab。底部 0.5px border 顶部分隔线。激活 Tab 的紫色下划线指示器是本应用底部导航的特色设计,比单纯文字变色更直观。

build() 使用 Stack 布局,主 Column 包含 header + if-else 切换 6 个 @Builder 页面(currentTab 0-5 对应 homeTab/photoTab/albumTab/orderTab/workshopTab/profileTab)+ tabBar,width/height 100%,bg 紫白底。弹窗条件渲染:if (this.showGenerateModal || this.showEditOrderModal || this.showDeletePhotoModal) 任一为 true 时渲染 this.modalOverlay(...),modalOverlay 内部 if-else 判断当前显示哪个弹窗内容,点击遮罩同时关闭所有三个弹窗状态。这种"统一 modalOverlay 封装 + 条件渲染"的模式大幅减少了重复代码,三个弹窗共享同一套遮罩与卡片样式。


十三、关键技术特性对比

特性维度 实现方式 技术要点 设计价值
色彩管理 ColorPalette 接口 + COLORS 常量 集中定义 24 种语义色,含 chart1-6 六色紫系 保证视觉一致性,支持主题切换
渐变头部 linearGradient 90度三色渐变 primaryDark→primary→accent,白字标题+积分胶囊 最具辨识度的视觉元素,AI神秘创造光谱
数据建模 @Observed 类 + constructor + interface 4 个 @Observed 类(PhotoTemplate/UserPhoto/OrderInfo/CreativeTemplate)+ MonthStat interface 按需选择响应式或静态,精准理解响应式机制
六Tab差异化 每页独立 @Builder 布局 首页Banner+横滑+分类+作品/写真三列网格+列表/相册四列网格+收藏/订单柱状图+列表/工坊双渐变卡+列表/我的用户卡+统计+权益+菜单 一页一型,还原真实AI写真App体验
底部导航 激活Tab下划线指示器 width20 height3 primary色圆角2,比文字变色更直观 用户一眼判断当前页面
月度柱状图 三档着色 + 柱子内白字 + floatAnim count>60红/>40紫/其余浅品红,height=count*1.5,scale1.03 以色定量级,假期暑期高峰红色突出
四动画联动 setInterval 1500ms同步切换 breathAnim/blinkAnim/pulseAnim/floatAnim四状态,分别用于不同元素 整体协调的联动动效体系
积分消耗闭环 credits状态 + 生成弹窗消耗 header显示积分胶囊,生成后credits-=price,我的页显示积分 积分获取-消耗-展示完整闭环
统一弹窗封装 modalOverlay @Builder + if-else 一个构建器封装三个弹窗内容,共享遮罩卡片样式,点击遮罩全关 大幅减少重复代码,样式统一
三大弹窗 生成AI写真/修改订单/删除作品 生成消耗积分/修改只读展示/删除二次确认 完整交互闭环
双渐变卡 工坊页两大卡片 左卡primary→accent亮渐变/右卡primaryDark→primary深渐变 深浅对比,主要入口更亮吸引点击
会员权益卡 90度紫色渐变 primary→accent,与header 90度渐变呼应 统一紫色渐变视觉语言
工具函数 formatPrice ¥ + price.toFixed(2),统一价格格式 价格视觉语言一致
状态管理 @State + @Observed + private 14个@State分六类,private存静态数据不触发更新 细粒度状态控制,性能优化
组件化 @Component + @Builder 1个入口组件 + 6个页面@Builder + 3个弹窗内容@Builder + header/tabBar/modalOverlay 高内聚低耦合,代码复用率高
列表渲染 ForEach + 网格/横滑/列表 热门横滑/三列网格/四列网格/订单列表/创意工具列表/收藏列表 数据驱动渲染,布局灵活
数据可视化 柱状图 + 四栏统计 月度柱状图三档着色/我的页四栏统计四色 多维度呈现,直观易读
文本省略 maxLines + textOverflow 模板名maxLines1/作品标题maxLines1省略 长文本优雅截断,布局整齐
生命周期 aboutToAppear/aboutToDisappear setInterval启动/clearInterval清理,timerId判-1 健壮性强,无资源泄漏
布局系统 Column/Row + Stack + Scroll + Circle 弹性布局、Stack叠加、Scroll纵向滚动、Circle绘制头像/圆点 自适应布局,层次分明

完整代码

// 815.ets - 幻影AI·数字照相馆
// 紫色渐变主题 | 6tab单排 | 影视头部

interface ColorPalette {
  bg: string;
  cardBg: string;
  primary: string;
  primaryLight: string;
  primaryDark: string;
  accent: string;
  accentLight: string;
  text: string;
  textSub: string;
  textWhite: string;
  border: string;
  tagBg: string;
  tagText: string;
  shadow: string;
  success: string;
  warning: string;
  danger: string;
  chart1: string;
  chart2: string;
  chart3: string;
  chart4: string;
  chart5: string;
  chart6: string;
}

const COLORS: ColorPalette = {
  bg: '#F3E5F5',
  cardBg: '#FFFFFF',
  primary: '#7B1FA2',
  primaryLight: '#CE93D8',
  primaryDark: '#4A148C',
  accent: '#E040FB',
  accentLight: '#EA80FC',
  text: '#1A1A2E',
  textSub: '#9E9E9E',
  textWhite: '#FFFFFF',
  border: '#E1BEE7',
  tagBg: '#F3E5F5',
  tagText: '#7B1FA2',
  shadow: '#D1C4E9',
  success: '#4CAF50',
  warning: '#FF9800',
  danger: '#F44336',
  chart1: '#7B1FA2',
  chart2: '#E040FB',
  chart3: '#9C27B0',
  chart4: '#AB47BC',
  chart5: '#CE93D8',
  chart6: '#BA68C8',
};

@Observed
class PhotoTemplate {
  id: string;
  name: string;
  style: string;
  price: number;
  image: string;
  uses: number;
  rating: number;
  tag: string;
  isHot: boolean;

  constructor(id: string, name: string, style: string, price: number,
    image: string, uses: number, rating: number, tag: string, isHot: boolean) {
    this.id = id;
    this.name = name;
    this.style = style;
    this.price = price;
    this.image = image;
    this.uses = uses;
    this.rating = rating;
    this.tag = tag;
    this.isHot = isHot;
  }
}

@Observed
class UserPhoto {
  id: string;
  title: string;
  template: string;
  date: string;
  url: string;
  likes: number;
  isFavorite: boolean;

  constructor(id: string, title: string, template: string, date: string,
    url: string, likes: number, isFavorite: boolean) {
    this.id = id;
    this.title = title;
    this.template = template;
    this.date = date;
    this.url = url;
    this.likes = likes;
    this.isFavorite = isFavorite;
  }
}

@Observed
class OrderInfo {
  id: string;
  orderNo: string;
  templateName: string;
  amount: number;
  status: string;
  date: string;
  count: number;

  constructor(id: string, orderNo: string, templateName: string, amount: number,
    status: string, date: string, count: number) {
    this.id = id;
    this.orderNo = orderNo;
    this.templateName = templateName;
    this.amount = amount;
    this.status = status;
    this.date = date;
    this.count = count;
  }
}

@Observed
class CreativeTemplate {
  id: string;
  name: string;
  desc: string;
  image: string;
  category: string;
  downloads: number;

  constructor(id: string, name: string, desc: string, image: string,
    category: string, downloads: number) {
    this.id = id;
    this.name = name;
    this.desc = desc;
    this.image = image;
    this.category = category;
    this.downloads = downloads;
  }
}

interface MonthStat {
  month: string;
  count: number;
  avgScore: number;
}

function formatPrice(price: number): string {
  return '¥' + price.toFixed(2);
}

@Entry
@Component
struct AIPhotoStudioApp {
  @State currentTab: number = 0;
  @State showGenerateModal: boolean = false;
  @State showEditOrderModal: boolean = false;
  @State showDeletePhotoModal: boolean = false;
  @State selectedTemplate: PhotoTemplate = new PhotoTemplate('t0', '', '', 0, '', 0, 0, '', false);
  @State selectedOrder: OrderInfo = new OrderInfo('o0', '', '', 0, '', '', 0);
  @State selectedPhotoId: string = '';
  @State credits: number = 580;
  @State breathAnim: boolean = false;
  @State blinkAnim: boolean = false;
  @State pulseAnim: boolean = false;
  @State floatAnim: boolean = false;
  private timerId: number = -1;

  private templates: PhotoTemplate[] = [
    new PhotoTemplate('t1', '古风汉服写真', '国风', 99, '🏮', 3580, 4.9, '热门', true),
    new PhotoTemplate('t2', '赛博朋克霓虹', '科幻', 129, '🌃', 2890, 4.8, '新品', true),
    new PhotoTemplate('t3', '日系动漫二次元', '二次元', 89, '🌸', 4520, 4.9, '热销', true),
    new PhotoTemplate('t4', '油画肖像风格', '艺术', 159, '🎨', 1680, 4.7, '高端', false),
    new PhotoTemplate('t5', '复古港风胶片', '复古', 109, '📸', 2340, 4.6, '经典', false),
    new PhotoTemplate('t6', '水彩梦境少女', '梦幻', 119, '💧', 1950, 4.8, '推荐', true),
    new PhotoTemplate('t7', '暗黑哥特风格', '暗黑', 139, '🦇', 890, 4.5, '限定', false),
    new PhotoTemplate('t8', '工笔花鸟国风', '国风', 99, '🖌', 1230, 4.6, '新品', false),
    new PhotoTemplate('t9', '婚纱写真大片', '婚纱', 199, '💒', 3120, 4.9, '热门', true),
    new PhotoTemplate('t10', '黑白纪实摄影', '纪实', 89, '🖤', 670, 4.4, '经典', false),
  ];

  private photos: UserPhoto[] = [
    new UserPhoto('p1', '紫禁之巅', '古风汉服写真', '08-19', '🏮', 328, true),
    new UserPhoto('p2', '霓虹都市', '赛博朋克霓虹', '08-18', '🌃', 256, false),
    new UserPhoto('p3', '樱花飘雪', '日系动漫二次元', '08-17', '🌸', 489, true),
    new UserPhoto('p4', '油画肖像', '油画肖像风格', '08-16', '🎨', 178, false),
    new UserPhoto('p5', '港风回忆', '复古港风胶片', '08-15', '📸', 142, false),
    new UserPhoto('p6', '梦境水彩', '水彩梦境少女', '08-14', '💧', 267, true),
    new UserPhoto('p7', '暗夜玫瑰', '暗黑哥特风格', '08-13', '🦇', 89, false),
    new UserPhoto('p8', '花鸟工笔', '工笔花鸟国风', '08-12', '🖌', 156, false),
    new UserPhoto('p9', '白色誓言', '婚纱写真大片', '08-11', '💒', 432, true),
    new UserPhoto('p10', '黑白纪实', '黑白纪实摄影', '08-10', '🖤', 67, false),
    new UserPhoto('p11', '紫禁之巅2', '古风汉服写真', '08-09', '🏮', 298, false),
    new UserPhoto('p12', '霓虹都市2', '赛博朋克霓虹', '08-08', '🌃', 312, true),
  ];

  private orders: OrderInfo[] = [
    new OrderInfo('o1', 'AI20260819001', '古风汉服写真', 99, '已完成', '08-19', 1),
    new OrderInfo('o2', 'AI20260818002', '赛博朋克霓虹', 129, '已完成', '08-18', 1),
    new OrderInfo('o3', 'AI20260817003', '日系动漫二次元', 89, '已完成', '08-17', 2),
    new OrderInfo('o4', 'AI20260816004', '油画肖像风格', 159, '生成中', '08-16', 1),
    new OrderInfo('o5', 'AI20260815005', '水彩梦境少女', 119, '已完成', '08-15', 1),
    new OrderInfo('o6', 'AI20260814006', '婚纱写真大片', 199, '已完成', '08-14', 3),
    new OrderInfo('o7', 'AI20260813007', '复古港风胶片', 109, '已退款', '08-13', 1),
    new OrderInfo('o8', 'AI20260812008', '黑白纪实摄影', 89, '已完成', '08-12', 1),
    new OrderInfo('o9', 'AI20260811009', '工笔花鸟国风', 99, '已完成', '08-11', 2),
    new OrderInfo('o10', 'AI20260810010', '暗黑哥特风格', 139, '已完成', '08-10', 1),
  ];

  private creativeTemplates: CreativeTemplate[] = [
    new CreativeTemplate('c1', 'AI换脸', '上传照片一键换脸', '😊', '趣味', 8560),
    new CreativeTemplate('c2', 'AI老照片修复', '修复模糊老照片', '📷', '修复', 6230),
    new CreativeTemplate('c3', 'AI黑白上色', '黑白照片自动上色', '🌈', '修复', 4890),
    new CreativeTemplate('c4', 'AI背景替换', '智能抠图换背景', '🖼', '编辑', 7120),
    new CreativeTemplate('c5', 'AI漫画脸', '照片转漫画风', '📝', '趣味', 5670),
    new CreativeTemplate('c6', 'AI证件照', '一键生成证件照', '🪪', '实用', 9340),
    new CreativeTemplate('c7', 'AI婚纱预览', '试穿虚拟婚纱', '👰', '趣味', 3450),
    new CreativeTemplate('c8', 'AI头像生成', '生成个性头像', '🎭', '趣味', 6780),
  ];

  private monthStats: MonthStat[] = [
    { month: '3月', count: 28, avgScore: 4.6 },
    { month: '4月', count: 45, avgScore: 4.7 },
    { month: '5月', count: 62, avgScore: 4.8 },
    { month: '6月', count: 38, avgScore: 4.5 },
    { month: '7月', count: 55, avgScore: 4.7 },
    { month: '8月', count: 78, avgScore: 4.9 },
  ];

  private tabItems: string[] = ['首页', '写真', '相册', '订单', '工坊', '我的'];

  aboutToAppear(): void {
    this.timerId = setInterval(() => {
      this.breathAnim = !this.breathAnim;
      this.blinkAnim = !this.blinkAnim;
      this.pulseAnim = !this.pulseAnim;
      this.floatAnim = !this.floatAnim;
    }, 1500);
  }

  aboutToDisappear(): void {
    if (this.timerId !== -1) {
      clearInterval(this.timerId);
      this.timerId = -1;
    }
  }

  @Builder
  header() {
    Row() {
      Column() {
        Text('幻影AI')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textWhite)
        Text('AI数字照相馆')
          .fontSize(11)
          .fontColor(COLORS.textWhite)
          .opacity(0.85)
          .margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      Row() {
        Text('🎨 ' + this.credits)
          .fontSize(12)
          .fontColor(COLORS.textWhite)
          .padding({ left: 10, right: 10, top: 6, bottom: 6 })
          .backgroundColor('rgba(255,255,255,0.2)')
          .borderRadius(16)
      }
    }
    .width('100%')
    .height(56)
    .padding({ left: 20, right: 20 })
    .linearGradient({
      angle: 90,
      colors: [[COLORS.primaryDark, 0.0], [COLORS.primary, 0.5], [COLORS.accent, 1.0]]
    })
    .alignItems(VerticalAlign.Center)
  }

  @Builder
  tabBar() {
    Row() {
      ForEach(this.tabItems, (item: string, index: number) => {
        Column() {
          Text(item)
            .fontSize(11)
            .fontWeight(this.currentTab === index ? FontWeight.Bold : FontWeight.Normal)
            .fontColor(this.currentTab === index ? COLORS.primary : COLORS.textSub)
          if (this.currentTab === index) {
            Row()
              .width(20)
              .height(3)
              .backgroundColor(COLORS.primary)
              .borderRadius(2)
              .margin({ top: 4 })
          }
        }
        .layoutWeight(1)
        .height(52)
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.currentTab = index;
        })
      }, (item: string) => item)
    }
    .width('100%')
    .height(52)
    .backgroundColor(COLORS.cardBg)
    .border({ width: 0.5, color: COLORS.border })
  }

  @Builder
  homeTab() {
    Scroll() {
      Column() {
        // 大Banner
        Column() {
          Text('🎬 AI写真新体验')
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textWhite)
            .margin({ top: 20 })
          Text('一键生成你的专属大片')
            .fontSize(13)
            .fontColor(COLORS.textWhite)
            .opacity(0.85)
            .margin({ top: 6, bottom: 16 })
          Row() {
            Button()
              .width(120)
              .height(40)
              .backgroundColor(COLORS.textWhite)
              .borderRadius(20)
              .onClick(() => {
                this.showGenerateModal = true;
              })
          }
          .margin({ bottom: 20 })
        }
        .width('100%')
        .linearGradient({ angle: 135, colors: [[COLORS.primaryDark, 0.0], [COLORS.accent, 1.0]] })
        .borderRadius(20)
        .margin({ bottom: 16 })
        .alignItems(HorizontalAlign.Center)

        // 热门模板
        Row() {
          Text('🔥 热门模板')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.text)
            .layoutWeight(1)
          Text('更多 >')
            .fontSize(12)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        .margin({ bottom: 12 })

        Scroll() {
          Row() {
            ForEach(this.templates.filter((t: PhotoTemplate) => t.isHot), (tpl: PhotoTemplate) => {
              Column() {
                Text(tpl.image)
                  .fontSize(40)
                  .margin({ bottom: 6 })
                Text(tpl.tag)
                  .fontSize(9)
                  .fontColor(COLORS.textWhite)
                  .backgroundColor(COLORS.danger)
                  .borderRadius(4)
                  .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                  .position({ x: 70, y: 8 })
                  .scale(this.pulseAnim ? { x: 1.1, y: 1.1 } : { x: 1.0, y: 1.0 })
                  .animation({ duration: 600, iterations: 1 })
                Text(tpl.name)
                  .fontSize(12)
                  .fontColor(COLORS.text)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Text(tpl.style)
                  .fontSize(10)
                  .fontColor(COLORS.textSub)
                  .margin({ top: 2 })
                Row() {
                  Text('★' + tpl.rating.toFixed(1))
                    .fontSize(10)
                    .fontColor(COLORS.warning)
                  Text(' ' + tpl.uses + '人用')
                    .fontSize(9)
                    .fontColor(COLORS.textSub)
                }
                .margin({ top: 4 })
                Text(formatPrice(tpl.price))
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.primary)
                  .margin({ top: 4 })
              }
              .width(100)
              .padding(10)
              .backgroundColor(COLORS.cardBg)
              .borderRadius(14)
              .margin({ right: 10 })
              .onClick(() => {
                this.selectedTemplate = tpl;
                this.showGenerateModal = true;
              })
            }, (tpl: PhotoTemplate) => tpl.id)
          }
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
        .width('100%')
        .margin({ bottom: 16 })

        // 风格分类
        Text('🎭 风格分类')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.text)
          .margin({ bottom: 12 })

        Row() {
          ForEach(['国风', '二次元', '科幻', '复古'], (style: string) => {
            Column() {
              Text(style)
                .fontSize(13)
                .fontColor(COLORS.primary)
                .fontWeight(FontWeight.Bold)
            }
            .layoutWeight(1)
            .height(40)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(COLORS.tagBg)
            .borderRadius(10)
            .margin({ right: 8 })
          }, (style: string) => style)
        }
        .width('100%')
        .margin({ bottom: 8 })

        Row() {
          ForEach(['艺术', '梦幻', '暗黑', '婚纱'], (style: string) => {
            Column() {
              Text(style)
                .fontSize(13)
                .fontColor(COLORS.accent)
                .fontWeight(FontWeight.Bold)
            }
            .layoutWeight(1)
            .height(40)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(COLORS.tagBg)
            .borderRadius(10)
            .margin({ right: 8 })
          }, (style: string) => style)
        }
        .width('100%')
        .margin({ bottom: 16 })

        // 精选作品
        Text('✨ 精选作品')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.text)
          .margin({ bottom: 12 })

        Column() {
          ForEach(this.photos.slice(0, 6), (photo: UserPhoto) => {
            Row() {
              Text(photo.url)
                .fontSize(32)
                .margin({ right: 12 })
              Column() {
                Text(photo.title)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.text)
                Text(photo.template)
                  .fontSize(11)
                  .fontColor(COLORS.textSub)
                  .margin({ top: 2 })
                Row() {
                  Text('❤ ' + photo.likes)
                    .fontSize(10)
                    .fontColor(COLORS.danger)
                  Text(' ' + photo.date)
                    .fontSize(10)
                    .fontColor(COLORS.textSub)
                    .margin({ left: 8 })
                }
                .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)

              Text(photo.isFavorite ? '★' : '☆')
                .fontSize(20)
                .fontColor(photo.isFavorite ? COLORS.warning : COLORS.textSub)
                .scale(this.floatAnim && photo.isFavorite ? { x: 1.1, y: 1.1 } : { x: 1.0, y: 1.0 })
                .animation({ duration: 500, iterations: 1 })
            }
            .width('100%')
            .padding(12)
            .backgroundColor(COLORS.cardBg)
            .borderRadius(12)
            .margin({ bottom: 8 })
          }, (photo: UserPhoto) => photo.id)
        }
        .width('100%')
      }
      .padding({ left: 16, right: 16, top: 12, bottom: 16 })
      .alignItems(HorizontalAlign.Start)
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .layoutWeight(1)
  }

  @Builder
  photoTab() {
    Scroll() {
      Column() {
        Text('📸 AI写真模板')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.text)
          .margin({ bottom: 12 })

        // 三列网格
        Row() {
          ForEach(this.templates.slice(0, 3), (tpl: PhotoTemplate) => {
            Column() {
              Text(tpl.image)
                .fontSize(36)
                .margin({ bottom: 4 })
              Text(tpl.name)
                .fontSize(11)
                .fontColor(COLORS.text)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(formatPrice(tpl.price))
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.primary)
            }
            .layoutWeight(1)
            .padding({ top: 14, bottom: 14 })
            .backgroundColor(COLORS.cardBg)
            .borderRadius(12)
            .margin({ right: 8 })
            .onClick(() => {
              this.selectedTemplate = tpl;
              this.showGenerateModal = true;
            })
          }, (tpl: PhotoTemplate) => tpl.id)
        }
        .width('100%')
        .margin({ bottom: 8 })

        Row() {
          ForEach(this.templates.slice(3, 6), (tpl: PhotoTemplate) => {
            Column() {
              Text(tpl.image)
                .fontSize(36)
                .margin({ bottom: 4 })
              Text(tpl.name)
                .fontSize(11)
                .fontColor(COLORS.text)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(formatPrice(tpl.price))
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.primary)
            }
            .layoutWeight(1)
            .padding({ top: 14, bottom: 14 })
            .backgroundColor(COLORS.cardBg)
            .borderRadius(12)
            .margin({ right: 8 })
            .onClick(() => {
              this.selectedTemplate = tpl;
              this.showGenerateModal = true;
            })
          }, (tpl: PhotoTemplate) => tpl.id)
        }
        .width('100%')
        .margin({ bottom: 8 })

        Row() {
          ForEach(this.templates.slice(6, 9), (tpl: PhotoTemplate) => {
            Column() {
              Text(tpl.image)
                .fontSize(36)
                .margin({ bottom: 4 })
              Text(tpl.name)
                .fontSize(11)
                .fontColor(COLORS.text)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(formatPrice(tpl.price))
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.primary)
            }
            .layoutWeight(1)
            .padding({ top: 14, bottom: 14 })
            .backgroundColor(COLORS.cardBg)
            .borderRadius(12)
            .margin({ right: 8 })
            .onClick(() => {
              this.selectedTemplate = tpl;
              this.showGenerateModal = true;
            })
          }, (tpl: PhotoTemplate) => tpl.id)
        }
        .width('100%')
        .margin({ bottom: 16 })

        // 详细模板列表
        Text('📋 全部模板')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.text)
          .margin({ bottom: 12 })

        ForEach(this.templates, (tpl: PhotoTemplate) => {
          Row() {
            Text(tpl.image)
              .fontSize(32)
              .margin({ right: 12 })
            Column() {
              Text(tpl.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.text)
              Row() {
                Text(tpl.style)
                  .fontSize(9)
                  .fontColor(COLORS.tagText)
                  .backgroundColor(COLORS.tagBg)
                  .borderRadius(4)
                  .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                Text(tpl.tag)
                  .fontSize(9)
                  .fontColor(COLORS.textWhite)
                  .backgroundColor(COLORS.primary)
                  .borderRadius(4)
                  .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                  .margin({ left: 4 })
                  .scale(this.pulseAnim ? { x: 1.05, y: 1.05 } : { x: 1.0, y: 1.0 })
                  .animation({ duration: 600, iterations: 1 })
              }
              .margin({ top: 4 })
              Row() {
                Text('★' + tpl.rating.toFixed(1))
                  .fontSize(10)
                  .fontColor(COLORS.warning)
                Text(' ' + tpl.uses + '人使用')
                  .fontSize(10)
                  .fontColor(COLORS.textSub)
                  .margin({ left: 6 })
              }
              .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Column() {
              Text(formatPrice(tpl.price))
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.primary)
              Button()
                .width(56)
                .height(26)
                .backgroundColor(COLORS.primary)
                .borderRadius(13)
                .margin({ top: 6 })
                .onClick(() => {
                  this.selectedTemplate = tpl;
                  this.showGenerateModal = true;
                })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .padding(12)
          .backgroundColor(COLORS.cardBg)
          .borderRadius(12)
          .margin({ bottom: 8 })
        }, (tpl: PhotoTemplate) => tpl.id)
      }
      .padding({ left: 16, right: 16, top: 12, bottom: 16 })
      .alignItems(HorizontalAlign.Start)
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .layoutWeight(1)
  }

  @Builder
  albumTab() {
    Scroll() {
      Column() {
        Row() {
          Text('🖼 我的相册')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.text)
            .layoutWeight(1)
          Text(this.photos.length + '张')
            .fontSize(12)
            .fontColor(COLORS.textSub)
        }
        .width('100%')
        .margin({ bottom: 12 })

        // 四列网格
        Row() {
          ForEach(this.photos.slice(0, 4), (photo: UserPhoto) => {
            Column() {
              Text(photo.url)
                .fontSize(28)
                .margin({ bottom: 4 })
              Text(photo.title)
                .fontSize(9)
                .fontColor(COLORS.text)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Text('❤' + photo.likes)
                .fontSize(8)
                .fontColor(COLORS.danger)
                .margin({ top: 2 })
            }
            .layoutWeight(1)
            .padding({ top: 12, bottom: 12 })
            .backgroundColor(COLORS.cardBg)
            .borderRadius(10)
            .margin({ right: 6 })
          }, (photo: UserPhoto) => photo.id)
        }
        .width('100%')
        .margin({ bottom: 6 })

        Row() {
          ForEach(this.photos.slice(4, 8), (photo: UserPhoto) => {
            Column() {
              Text(photo.url)
                .fontSize(28)
                .margin({ bottom: 4 })
              Text(photo.title)
                .fontSize(9)
                .fontColor(COLORS.text)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Text('❤' + photo.likes)
                .fontSize(8)
                .fontColor(COLORS.danger)
                .margin({ top: 2 })
            }
            .layoutWeight(1)
            .padding({ top: 12, bottom: 12 })
            .backgroundColor(COLORS.cardBg)
            .borderRadius(10)
            .margin({ right: 6 })
          }, (photo: UserPhoto) => photo.id)
        }
        .width('100%')
        .margin({ bottom: 6 })

        Row() {
          ForEach(this.photos.slice(8, 12), (photo: UserPhoto) => {
            Column() {
              Text(photo.url)
                .fontSize(28)
                .margin({ bottom: 4 })
              Text(photo.title)
                .fontSize(9)
                .fontColor(COLORS.text)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Text('❤' + photo.likes)
                .fontSize(8)
                .fontColor(COLORS.danger)
                .margin({ top: 2 })
            }
            .layoutWeight(1)
            .padding({ top: 12, bottom: 12 })
            .backgroundColor(COLORS.cardBg)
            .borderRadius(10)
            .margin({ right: 6 })
            .onClick(() => {
              this.selectedPhotoId = photo.id;
              this.showDeletePhotoModal = true;
            })
          }, (photo: UserPhoto) => photo.id)
        }
        .width('100%')
        .margin({ bottom: 16 })

        // 收藏列表
        Text('⭐ 收藏作品')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.text)
          .margin({ bottom: 12 })

        ForEach(this.photos.filter((p: UserPhoto) => p.isFavorite), (photo: UserPhoto) => {
          Row() {
            Text(photo.url)
              .fontSize(32)
              .margin({ right: 12 })
            Column() {
              Text(photo.title)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.text)
              Text(photo.template)
                .fontSize(11)
                .fontColor(COLORS.textSub)
                .margin({ top: 2 })
              Row() {
                Text('❤ ' + photo.likes)
                  .fontSize(10)
                  .fontColor(COLORS.danger)
                Text(' ' + photo.date)
                  .fontSize(10)
                  .fontColor(COLORS.textSub)
                  .margin({ left: 8 })
              }
              .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Text('★')
              .fontSize(18)
              .fontColor(COLORS.warning)
              .scale(this.blinkAnim ? { x: 1.1, y: 1.1 } : { x: 1.0, y: 1.0 })
              .animation({ duration: 500, iterations: 1 })
          }
          .width('100%')
          .padding(12)
          .backgroundColor(COLORS.cardBg)
          .borderRadius(12)
          .margin({ bottom: 8 })
        }, (photo: UserPhoto) => photo.id)
      }
      .padding({ left: 16, right: 16, top: 12, bottom: 16 })
      .alignItems(HorizontalAlign.Start)
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .layoutWeight(1)
  }

  @Builder
  orderTab() {
    Scroll() {
      Column() {
        // 柱状图
        Column() {
          Text('月度生成统计')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.text)
            .margin({ bottom: 16 })

          Row() {
            ForEach(this.monthStats, (stat: MonthStat) => {
              Column() {
                Column() {
                  Text(stat.count.toString())
                    .fontSize(9)
                    .fontColor(COLORS.textWhite)
                }
                .width(26)
                .height(stat.count * 1.5)
                .backgroundColor(stat.count > 60 ? COLORS.danger : stat.count > 40 ? COLORS.primary : COLORS.accentLight)
                .borderRadius(4)
                .justifyContent(FlexAlign.End)
                .padding({ top: 2 })
                .scale(this.floatAnim ? { x: 1.03, y: 1.03 } : { x: 1.0, y: 1.0 })
                .animation({ duration: 600, iterations: 1 })

                Text(stat.month)
                  .fontSize(9)
                  .fontColor(COLORS.textSub)
                  .margin({ top: 4 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
            }, (stat: MonthStat) => stat.month)
          }
          .width('100%')
          .height(160)
          .alignItems(VerticalAlign.Bottom)
        }
        .width('100%')
        .padding(16)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .margin({ bottom: 16 })

        // 订单列表
        Row() {
          Text('📋 订单记录')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.text)
            .layoutWeight(1)
          Text('共' + this.orders.length + '单')
            .fontSize(12)
            .fontColor(COLORS.textSub)
        }
        .width('100%')
        .margin({ bottom: 12 })

        ForEach(this.orders, (order: OrderInfo) => {
          Column() {
            Row() {
              Text(order.orderNo)
                .fontSize(10)
                .fontColor(COLORS.textSub)
                .layoutWeight(1)
              Text(order.status)
                .fontSize(10)
                .fontColor(order.status === '已完成' ? COLORS.success : order.status === '生成中' ? COLORS.warning : COLORS.danger)
                .fontWeight(FontWeight.Bold)
                .scale(order.status === '生成中' && this.blinkAnim ? { x: 1.05, y: 1.05 } : { x: 1.0, y: 1.0 })
                .animation({ duration: 500, iterations: 1 })
            }
            .width('100%')

            Row() {
              Text('📸')
                .fontSize(28)
                .margin({ right: 12 })
              Column() {
                Text(order.templateName)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.text)
                Text('数量 x' + order.count)
                  .fontSize(11)
                  .fontColor(COLORS.textSub)
                  .margin({ top: 4 })
                Row() {
                  Text(order.date)
                    .fontSize(11)
                    .fontColor(COLORS.textSub)
                    .layoutWeight(1)
                  Text(formatPrice(order.amount))
                    .fontSize(15)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.primary)
                }
                .width('100%')
                .margin({ top: 4 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
            }
            .margin({ top: 8 })

            Row() {
              if (order.status === '生成中') {
                Text('⏳ AI正在生成中...')
                  .fontSize(11)
                  .fontColor(COLORS.warning)
                  .layoutWeight(1)
              } else {
                Text('')
                  .layoutWeight(1)
              }
              Button()
                .width(60)
                .height(24)
                .backgroundColor(COLORS.cardBg)
                .border({ width: 0.5, color: COLORS.border })
                .borderRadius(12)
              if (order.status === '已完成') {
                Button()
                  .width(60)
                  .height(24)
                  .backgroundColor(COLORS.primary)
                  .borderRadius(12)
                  .margin({ left: 8 })
                  .onClick(() => {
                    this.selectedOrder = order;
                    this.showEditOrderModal = true;
                  })
              }
            }
            .margin({ top: 8 })
          }
          .width('100%')
          .padding(12)
          .backgroundColor(COLORS.cardBg)
          .borderRadius(12)
          .margin({ bottom: 8 })
        }, (order: OrderInfo) => order.id)
      }
      .padding({ left: 16, right: 16, top: 12, bottom: 16 })
      .alignItems(HorizontalAlign.Start)
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .layoutWeight(1)
  }

  @Builder
  workshopTab() {
    Scroll() {
      Column() {
        Text('🎨 创意工坊')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.text)
          .margin({ bottom: 12 })

        // 创意工具大卡片
        Row() {
          Column() {
            Text('🤖')
              .fontSize(44)
              .margin({ bottom: 8 })
            Text('AI智能生成')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textWhite)
            Text('上传照片·一键写真')
              .fontSize(11)
              .fontColor(COLORS.textWhite)
              .opacity(0.85)
              .margin({ top: 4 })
            Button()
              .width(100)
              .height(32)
              .backgroundColor(COLORS.textWhite)
              .borderRadius(16)
              .margin({ top: 12 })
              .onClick(() => {
                this.showGenerateModal = true;
              })
          }
          .layoutWeight(1)
          .padding({ top: 20, bottom: 20 })
          .linearGradient({ angle: 135, colors: [[COLORS.primary, 0.0], [COLORS.accent, 1.0]] })
          .borderRadius(16)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text('📊')
              .fontSize(44)
              .margin({ bottom: 8 })
            Text('批量处理')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textWhite)
            Text('多图·一键搞定')
              .fontSize(11)
              .fontColor(COLORS.textWhite)
              .opacity(0.85)
              .margin({ top: 4 })
            Button()
              .width(100)
              .height(32)
              .backgroundColor(COLORS.textWhite)
              .borderRadius(16)
              .margin({ top: 12 })
          }
          .layoutWeight(1)
          .padding({ top: 20, bottom: 20 })
          .linearGradient({ angle: 135, colors: [[COLORS.primaryDark, 0.0], [COLORS.primary, 1.0]] })
          .borderRadius(16)
          .alignItems(HorizontalAlign.Center)
          .margin({ left: 10 })
        }
        .width('100%')
        .margin({ bottom: 16 })

        // 创意模板列表
        Text('🛠 创意工具')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.text)
          .margin({ bottom: 12 })

        ForEach(this.creativeTemplates, (tpl: CreativeTemplate) => {
          Row() {
            Text(tpl.image)
              .fontSize(32)
              .margin({ right: 12 })
            Column() {
              Text(tpl.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.text)
              Text(tpl.desc)
                .fontSize(11)
                .fontColor(COLORS.textSub)
                .margin({ top: 2 })
              Row() {
                Text(tpl.category)
                  .fontSize(9)
                  .fontColor(COLORS.tagText)
                  .backgroundColor(COLORS.tagBg)
                  .borderRadius(4)
                  .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                Text(' ' + tpl.downloads + '次下载')
                  .fontSize(10)
                  .fontColor(COLORS.textSub)
                  .margin({ left: 6 })
              }
              .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Button()
              .width(28)
              .height(28)
              .backgroundColor(COLORS.primary)
              .borderRadius(14)
              .scale(this.pulseAnim ? { x: 1.05, y: 1.05 } : { x: 1.0, y: 1.0 })
              .animation({ duration: 700, iterations: 1 })
          }
          .width('100%')
          .padding(12)
          .backgroundColor(COLORS.cardBg)
          .borderRadius(12)
          .margin({ bottom: 8 })
        }, (tpl: CreativeTemplate) => tpl.id)
      }
      .padding({ left: 16, right: 16, top: 12, bottom: 16 })
      .alignItems(HorizontalAlign.Start)
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .layoutWeight(1)
  }

  @Builder
  profileTab() {
    Scroll() {
      Column() {
        // 用户卡
        Row() {
          Circle({ width: 64, height: 64 })
            .fill(COLORS.primaryLight)

          Column() {
            Text('幻影用户')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.text)
            Row() {
              Text('VIP')
                .fontSize(10)
                .fontColor(COLORS.textWhite)
                .backgroundColor(COLORS.primary)
                .borderRadius(8)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .scale(this.pulseAnim ? { x: 1.05, y: 1.05 } : { x: 1.0, y: 1.0 })
                .animation({ duration: 700, iterations: 1 })
              Text('积分 ' + this.credits)
                .fontSize(10)
                .fontColor(COLORS.textSub)
                .margin({ left: 8 })
            }
            .margin({ top: 4 })
          }
          .layoutWeight(1)
          .margin({ left: 12 })
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .padding(16)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .margin({ bottom: 16 })

        // 统计四宫格
        Row() {
          Column() {
            Text(this.photos.length.toString())
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.primary)
            Text('作品数')
              .fontSize(10)
              .fontColor(COLORS.textSub)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text(this.orders.length.toString())
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.accent)
            Text('订单数')
              .fontSize(10)
              .fontColor(COLORS.textSub)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text('580')
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.warning)
            Text('积分')
              .fontSize(10)
              .fontColor(COLORS.textSub)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text('4')
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.danger)
            Text('收藏')
              .fontSize(10)
              .fontColor(COLORS.textSub)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
        }
        .width('100%')
        .padding({ top: 16, bottom: 16 })
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .margin({ bottom: 16 })

        // 会员权益
        Column() {
          Text('👑 会员权益')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textWhite)
          Text('每日无限生成 · 优先队列 · 专属模板')
            .fontSize(11)
            .fontColor(COLORS.textWhite)
            .opacity(0.85)
            .margin({ top: 4 })
        }
        .width('100%')
        .padding(16)
        .linearGradient({ angle: 90, colors: [[COLORS.primary, 0.0], [COLORS.accent, 1.0]] })
        .borderRadius(16)
        .alignItems(HorizontalAlign.Start)
        .margin({ bottom: 16 })

        // 菜单列表
        ForEach(['我的作品', '积分商城', '邀请好友', '帮助反馈', '关于我们'], (item: string) => {
          Row() {
            Text(item)
              .fontSize(14)
              .fontColor(COLORS.text)
              .layoutWeight(1)
            Text('>')
              .fontSize(14)
              .fontColor(COLORS.textSub)
          }
          .width('100%')
          .padding({ top: 14, bottom: 14, left: 16, right: 16 })
          .backgroundColor(COLORS.cardBg)
          .borderRadius(12)
          .margin({ bottom: 8 })
        }, (item: string) => item)
      }
      .padding({ left: 16, right: 16, top: 12, bottom: 16 })
      .alignItems(HorizontalAlign.Start)
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .layoutWeight(1)
  }

  @Builder
  modalOverlay(onClose: () => void) {
    Column() {
      Column() {
        if (this.showGenerateModal) {
          this.generateModalContent()
        } else if (this.showEditOrderModal) {
          this.editOrderModalContent()
        } else if (this.showDeletePhotoModal) {
          this.deletePhotoModalContent()
        }
      }
      .width('90%')
      .constraintSize({ maxHeight: '80%' })
      .backgroundColor(COLORS.cardBg)
      .borderRadius(20)
      .padding(20)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('rgba(0,0,0,0.4)')
    .onClick(() => {
      onClose();
    })
  }

  @Builder
  generateModalContent() {
    Column() {
      Text('生成AI写真')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.text)
        .margin({ bottom: 16 })

      Row() {
        Text(this.selectedTemplate.image)
          .fontSize(48)
          .margin({ right: 12 })
        Column() {
          Text(this.selectedTemplate.name)
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.text)
          Text(this.selectedTemplate.style)
            .fontSize(12)
            .fontColor(COLORS.textSub)
            .margin({ top: 2 })
          Text(formatPrice(this.selectedTemplate.price))
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.primary)
            .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
      }
      .margin({ bottom: 16 })

      Row() {
        Text('上传照片')
          .fontSize(13)
          .fontColor(COLORS.text)
          .layoutWeight(1)
        Text('📷 选择')
          .fontSize(13)
          .fontColor(COLORS.primary)
      }
      .margin({ bottom: 12 })

      Row() {
        Text('生成数量')
          .fontSize(13)
          .fontColor(COLORS.text)
          .layoutWeight(1)
        Text('4张')
          .fontSize(13)
          .fontColor(COLORS.primary)
          .fontWeight(FontWeight.Bold)
      }
      .margin({ bottom: 12 })

      Row() {
        Text('消耗积分')
          .fontSize(13)
          .fontColor(COLORS.text)
          .layoutWeight(1)
        Text('-' + this.selectedTemplate.price + '积分')
          .fontSize(13)
          .fontColor(COLORS.danger)
          .fontWeight(FontWeight.Bold)
      }
      .margin({ bottom: 20 })

      Row() {
        Button()
          .width('45%')
          .height(40)
          .backgroundColor(COLORS.cardBg)
          .border({ width: 0.5, color: COLORS.border })
          .borderRadius(20)
          .onClick(() => {
            this.showGenerateModal = false;
          })
        Button()
          .width('45%')
          .height(40)
          .backgroundColor(COLORS.primary)
          .borderRadius(20)
          .margin({ left: 10 })
          .onClick(() => {
            this.credits -= this.selectedTemplate.price;
            this.showGenerateModal = false;
          })
      }
      .width('100%')
    }
    .alignItems(HorizontalAlign.Start)
  }

  @Builder
  editOrderModalContent() {
    Column() {
      Text('修改订单')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.text)
        .margin({ bottom: 16 })

      Row() {
        Text('订单号')
          .fontSize(13)
          .fontColor(COLORS.text)
          .width(60)
        Text(this.selectedOrder.orderNo)
          .fontSize(13)
          .fontColor(COLORS.textSub)
          .layoutWeight(1)
      }
      .margin({ bottom: 12 })

      Row() {
        Text('模板')
          .fontSize(13)
          .fontColor(COLORS.text)
          .width(60)
        Text(this.selectedOrder.templateName)
          .fontSize(13)
          .fontColor(COLORS.textSub)
          .layoutWeight(1)
      }
      .margin({ bottom: 12 })

      Row() {
        Text('金额')
          .fontSize(13)
          .fontColor(COLORS.text)
          .width(60)
        Text(formatPrice(this.selectedOrder.amount))
          .fontSize(13)
          .fontColor(COLORS.primary)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
      }
      .margin({ bottom: 12 })

      Row() {
        Text('状态')
          .fontSize(13)
          .fontColor(COLORS.text)
          .width(60)
        Text(this.selectedOrder.status)
          .fontSize(13)
          .fontColor(COLORS.success)
          .layoutWeight(1)
      }
      .margin({ bottom: 20 })

      Row() {
        Button()
          .width('45%')
          .height(40)
          .backgroundColor(COLORS.cardBg)
          .border({ width: 0.5, color: COLORS.border })
          .borderRadius(20)
          .onClick(() => {
            this.showEditOrderModal = false;
          })
        Button()
          .width('45%')
          .height(40)
          .backgroundColor(COLORS.accent)
          .borderRadius(20)
          .margin({ left: 10 })
          .onClick(() => {
            this.showEditOrderModal = false;
          })
      }
      .width('100%')
    }
    .alignItems(HorizontalAlign.Start)
  }

  @Builder
  deletePhotoModalContent() {
    Column() {
      Text('删除作品')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.text)
        .margin({ bottom: 16 })

      Text('确定删除作品 ' + this.selectedPhotoId + ' 吗?')
        .fontSize(14)
        .fontColor(COLORS.textSub)
        .margin({ bottom: 8 })

      Text('删除后无法恢复')
        .fontSize(12)
        .fontColor(COLORS.danger)
        .margin({ bottom: 20 })

      Row() {
        Button()
          .width('45%')
          .height(40)
          .backgroundColor(COLORS.cardBg)
          .border({ width: 0.5, color: COLORS.border })
          .borderRadius(20)
          .onClick(() => {
            this.showDeletePhotoModal = false;
          })
        Button()
          .width('45%')
          .height(40)
          .backgroundColor(COLORS.danger)
          .borderRadius(20)
          .margin({ left: 10 })
          .onClick(() => {
            this.showDeletePhotoModal = false;
          })
      }
      .width('100%')
    }
    .alignItems(HorizontalAlign.Start)
  }

  build() {
    Stack() {
      Column() {
        this.header()

        if (this.currentTab === 0) {
          this.homeTab()
        } else if (this.currentTab === 1) {
          this.photoTab()
        } else if (this.currentTab === 2) {
          this.albumTab()
        } else if (this.currentTab === 3) {
          this.orderTab()
        } else if (this.currentTab === 4) {
          this.workshopTab()
        } else {
          this.profileTab()
        }

        this.tabBar()
      }
      .width('100%')
      .height('100%')
      .backgroundColor(COLORS.bg)

      if (this.showGenerateModal || this.showEditOrderModal || this.showDeletePhotoModal) {
        this.modalOverlay(() => {
          this.showGenerateModal = false;
          this.showEditOrderModal = false;
          this.showDeletePhotoModal = false;
        })
      }
    }
    .width('100%')
    .height('100%')
  }
}

十四、开发环境搭建

14.1 安装 DevEco Studio

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

DevEco Studio 安装欢迎界面

14.2 选择安装路径

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

选择安装位置

14.3 配置安装选项

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

安装选项配置

14.4 选择项目模板

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

选择 Empty Ability 模板

14.5 配置项目信息

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

配置项目信息


十五、总结

通过对这款幻影AI数字照相馆应用的深度拆解,我们可以看到,一个优秀的移动端应用远不止是功能的堆砌,它更是架构设计、数据建模、视觉系统、交互逻辑、动效设计、防御性编程等多维度能力的综合体现。

在架构层面,该应用采用了"配置驱动 + 组件化 + 状态管理"的三层设计模式。配置层将所有可变的数据与视觉属性抽离为常量数组和元数据接口,使得业务逻辑与表现层彻底解耦;组件层将六个功能页面拆分为独立的 @Builder 构建器,每个页面采用完全不同的布局风格,实现"一页一型"的差异化设计;状态层利用 ArkUI 的 @State、@Observed 机制,实现了数据变化到 UI 刷新的自动响应,特别是四个动画状态的联动管理(setInterval 1500ms 同步切换 breathAnim/blinkAnim/pulseAnim/floatAnim)和弹窗数据状态的空对象初始化(new PhotoTemplate('t0',...) / new OrderInfo('o0',...) 避免空引用),体现了对状态管理的精准理解。

在数据层面,四个 @Observed 数据类(PhotoTemplate/UserPhoto/OrderInfo/CreativeTemplate)以 constructor 构造函数方式实例化,完整描述了幻影AI的全部业务信息。特别的是,MonthStat 采用普通 interface 接口而非 @Observed 类,因为月度数据是静态统计数据无需响应式更新,这种"按需选择 @Observed 或 interface"的设计体现了对响应式机制的精准理解,避免了不必要的响应式开销。formatPrice 工具函数统一价格格式为 ¥XX.XX,是本应用唯一的工具函数,体现了"最小工具集"的设计原则。

在视觉层面,本应用独创了"紫色渐变主题"设计——顶部 header 使用 linearGradient 90 度从 primaryDark #4A148C 深紫黑 → primary #7B1FA2 深紫(0.5位置)→ accent #E040FB 亮紫品红的三色斜向渐变,是本应用最具辨识度的视觉元素,从深紫黑到深紫到亮紫品红的 90 度三色渐变象征AI从神秘到创造的完整光谱。首页大Banner、工坊两大卡片、会员权益卡分别使用不同角度和配色的紫色渐变,形成了统一而丰富的紫色渐变视觉语言,贯穿整个应用。chart1-6 六种图表色全是紫色系,保证数据可视化与整体主题的一致性。应用的动态颜色逻辑也十分丰富——月度柱状图按生成数分三档着色(>60红/>40紫/其余浅品红)、订单状态按状态分三档着色(已完成绿/生成中橙/已退款红)、风格分类分两排着色(第一排深紫/第二排亮紫)、四栏统计分四色,形成了"以色定量级、以色定状态、以色定分类"的多层语义着色模式。统一的色彩系统是整个应用的视觉基石,深紫主色搭配亮紫品红辅助色,配合紫白底和多维度紫色渐变,营造出既神秘又浪漫的AI数字照相馆氛围。

在交互层面,应用独创了"统一 modalOverlay 封装"设计——一个 @Builder modalOverlay(onClose) 构建器,内部通过 if-else 判断当前显示哪个弹窗内容,外层统一全屏遮罩 + 居中卡片,点击遮罩同时关闭所有三个弹窗状态。这种统一封装大幅减少了重复代码,三个弹窗(生成AI写真/修改订单/删除作品)共享同一套遮罩与卡片样式。生成AI写真弹窗确认后 credits -= selectedTemplate.price(积分消耗闭环),header 积分胶囊实时更新积分余额,我的页显示积分,形成积分获取-消耗-展示的完整闭环。修改订单弹窗展示订单信息只读,删除作品弹窗二次确认后关闭,形成了完整的交互闭环。应用还独创了"四动画状态联动"设计——blinkAnim 用于收藏星星闪烁和订单生成中状态脉冲、pulseAnim 用于热门标签脉冲和 VIP 标签缩放、floatAnim 用于精选作品星星浮动和柱状图浮动,四个动画同步切换分别用于不同元素,形成了整体协调的联动动效体系,让静态的页面有了"鲜活"的动态感。月度柱状图按生成数分三档着色,柱子内白字显示数值,floatAnim 缩放动画让数据有了轻微的呼吸感;订单生成中状态的 blinkAnim 缩放 + "⏳ AI正在生成中…"提示让用户一眼就能判断订单流转状态;收藏星星的 blinkAnim 闪烁让收藏列表有了"闪烁"的动态感。激活 Tab 的紫色下划线指示器比单纯文字变色更直观,让用户一眼就能判断当前所在页面。

幻影AI应用整体运行效果 - 紫色三色渐变头部与AI写真新体验大Banner及热门模板横滑


标签: HarmonyOS · API 24 · ArkTS · 华为 · AI写真 · 数字照相馆 · 移动端开发

Logo

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

更多推荐