声明式UI范式的核心在于状态驱动视图——开发者只需描述"界面应该是什么样子",框架自动处理从数据变化到UI刷新的完整渲染管线,极大降低了复杂界面的维护成本。
ArkTS在TypeScript基础上扩展了装饰器系统,@Component@State@Builder等注解为组件化开发提供了语义化的支撑,使得UI结构与业务逻辑形成清晰的关注点分离。
在HarmonyOS生态中,多Tab页面架构是电商类应用的典型形态——通过@State管理当前激活页签索引,配合条件渲染实现页面切换,既保证了状态隔离,又实现了组件复用。

引言

国风汉服馆应用是一款面向汉服文化爱好者的垂直电商平台,涵盖了从汉服形制展示、配饰陈列、妆造预约、雅集活动报名到个人衣橱管理的完整业务闭环。该应用采用HarmonyOS ArkTS声明式开发范式,以朱红、黛青、鎏金为主色调,营造出典雅而华贵的国风视觉氛围。应用整体架构由一个入口组件和六个功能子组件构成,每个子组件对应一个独立的业务模块,通过底部导航栏的页签切换机制实现模块间的无缝流转。

从技术架构层面来看,该应用充分运用了ArkTS的装饰器系统——@Entry标记入口组件,@Component声明自定义组件,@State管理组件内部可变状态,@Builder封装可复用的UI构建逻辑。状态管理采用了单一数据流模式:全局常量数据通过接口约束类型,组件内部状态通过@State装饰器实现响应式更新。每个Tab页面组件都维护了自身的状态集合,包括弹窗显隐状态、选中项ID、编辑表单字段等,通过条件渲染配合Stack+zIndex的层叠机制实现模态对话框的弹出与关闭。

从业务设计层面来看,应用的数据模型涵盖了汉服商品(HnItem)、配饰(HnAcc)、妆造风格(HnStyle)、活动事件(HnEvent)、订单(HnOrder)、收藏(HnFav)、热度统计(HnCount)和用户档案(HnProfile)共八个核心实体。每个实体都通过interface明确定义了字段类型,确保了编译期的类型安全。商品列表涵盖了明制马面裙、唐制齐胸襦裙、宋制褙子等从秦汉到清代的十六款经典形制,配饰包含发簪、团扇、璎珞等八件手作精品,妆造司提供花钿妆、远山黛眉等八种古典妆容服务,雅集活动则包括上巳节曲水流觞、花朝节赏红扑蝶等六场传统节令集会。

一、色彩体系与数据模型设计

应用首先定义了一套完整的国风色彩调色板(ColorPalette),通过interface声明所有颜色字段,再以常量对象实例化。这种设计使得主题色统一管理,任何视觉元素需要取色时只需引用调色板常量,便于后续的主题切换与视觉迭代。

interface ColorPalette {
  red: string;
  redDeep: string;
  teal: string;
  gold: string;
  goldLight: string;
  bg: string;
  card: string;
  text: string;
  textSub: string;
  textHint: string;
  line: string;
  danger: string;
  success: string;
  white: string;
}

const HN: ColorPalette = {
  red: '#B03A2E',
  redDeep: '#7E241C',
  teal: '#2F4B4C',
  gold: '#C9A227',
  goldLight: '#EFE3C2',
  bg: '#F6EFE0',
  card: '#FFFFFF',
  text: '#3D2B22',
  textSub: '#8A7463',
  textHint: '#C2B09C',
  line: '#EDE0CC',
  danger: '#A93A2B',
  success: '#6E8B6E',
  white: '#FFFFFF'
};

在这里插入图片描述

色彩体系的设计体现了国风美学的核心——朱红(#B03A2E)作为主色调,象征着喜庆与华贵;黛青(#2F4B4C)作为辅色,带来沉稳与内敛;鎏金(#C9A227)作为点缀色,增添了贵气与精致感。背景色采用温暖的米黄(#F6EFE0),卡片使用纯白以形成层次,文字色从深棕到浅灰形成三级梯度。这种色彩层级体系确保了信息密度的视觉区分——主文字用深色确保可读性,次要信息用中色调,提示性文字用浅色调。

在ArkTS中,interface不仅用于约束对象结构,更是编译期类型检查的基石。通过为所有数据实体定义interface,开发者可以在编码阶段就捕获类型不匹配的错误,避免运行时异常。

接下来定义的是各业务实体的数据模型接口与静态数据集合:

interface HnItem {
  id: number;
  name: string;
  form: string;
  price: number;
  oldPrice: number;
  fabric: string;
  dynasty: string;
  stock: number;
  percent: number;
}

const HN_ITEMS: HnItem[] = [
  { id: 1, name: '鎏金马面裙·云鹤', form: '明制马面裙', price: 699, oldPrice: 899, fabric: '织金缎', dynasty: '明', stock: 32, percent: 97 },
  { id: 2, name: '齐胸襦裙·桃花笺', form: '唐制齐胸襦裙', price: 529, oldPrice: 699, fabric: '雪纺/缎面', dynasty: '唐', stock: 45, percent: 94 },
  { id: 3, name: '明制披风·松间照', form: '明制披风', price: 899, oldPrice: 1199, fabric: '提花呢', dynasty: '明', stock: 18, percent: 92 },
  { id: 12, name: '飞鱼服·锦衣夜行', form: '明制飞鱼服', price: 1899, oldPrice: 2399, fabric: '妆花缎', dynasty: '明', stock: 6, percent: 98 },
  { id: 16, name: '婚服·凤冠霞帔', form: '明制婚服', price: 2999, oldPrice: 3999, fabric: '妆花缎+苏绣', dynasty: '明', stock: 4, percent: 99 }
];

商品数据模型涵盖了商品ID、名称、形制类别、现价、原价、面料材质、所属朝代、库存量和热度百分比八个核心字段。十六款商品跨越了从秦汉到现代改良的各个朝代形制,价格区间从429元的宋制褙子到2999元的明制婚服,覆盖了入门到高端的完整价格带。percent字段用于热度展示,stock字段则用于库存预警——当库存低于10件时,UI会以危险色高亮显示,提示用户抢购紧迫性。

二、工具函数与辅助逻辑

在数据模型之上,应用定义了一系列工具函数来处理通用的格式化和条件逻辑。这些函数不依赖于组件状态,作为纯函数存在于全局作用域中,可以被任意组件调用。

function hnBar(p: number): string {
  let v = p;
  if (v > 100) {
    v = 100;
  }
  return v + '%';
}

function hnPrice(p: number): string {
  return '¥' + p;
}

function hnStatusColor(s: string): string {
  if (s === '已签收' || s === '已完成') {
    return HN.success;
  }
  if (s === '运输中') {
    return HN.gold;
  }
  if (s === '退款中') {
    return HN.danger;
  }
  return HN.textHint;
}

function hnFormColor(f: string): string {
  if (f.indexOf('明') >= 0) {
    return '#B03A2E';
  }
  if (f.indexOf('唐') >= 0) {
    return '#C9A227';
  }
  if (f.indexOf('宋') >= 0) {
    return '#5B7B7C';
  }
  if (f.indexOf('清') >= 0) {
    return '#8B5E3C';
  }
  if (f.indexOf('晋') >= 0) {
    return '#7A6B9C';
  }
  return '#6E8B6E';
}

在这里插入图片描述

hnBar函数用于将百分比数值转换为进度条宽度字符串,同时做了上限保护——超过100的值会被截断为100%,防止进度条溢出容器。hnPrice函数简单地在价格前加上人民币符号。hnStatusColor函数根据订单状态返回对应的语义颜色:已签收和已完成用成功色(绿色),运输中用金色,退款中用危险色,其他状态用提示色。

纯函数是构建可维护前端架构的基石——不依赖外部状态、不产生副作用的函数易于测试、易于组合、易于重构。在ArkTS中,将通用的格式化逻辑提取为全局函数,可以避免在各组件中重复编写相同的条件分支。

hnFormColor函数是一个精妙的设计——它根据形制名称中包含的朝代关键字,返回该朝代对应的主题色。明制用朱红、唐制用鎏金、宋制用黛青、清制用棕色、晋制用紫色。这样在商品卡片上,形制标签的背景色就能直观地传达朝代信息,形成视觉记忆点。

三、入口组件与导航架构

入口组件HanApp是整个应用的骨架,它管理着全局的页签切换状态,并通过条件渲染将不同的业务页面挂载到内容区域。

@Entry
@Component
struct HanApp {
  @State activeTab: number = 0;

  @Builder
  headerBar(title: string) {
    Row() {
      Column() {
        Text(title)
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      Text('🔔')
        .fontSize(18)
        .margin({ right: 12 })
      Text('🧧')
        .fontSize(18)
        .margin({ right: 4 })
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 12, bottom: 12 })
  }

  @Builder
  bottomBar() {
    Row() {
      ForEach(HN_TABS, (tb: HnTab, ti: number) => {
        Column() {
          Text(tb.icon)
            .fontSize(this.activeTab === ti ? 20 : 17)
            .fontColor(this.activeTab === ti ? HN.red : HN.textHint)
          Text(tb.label)
            .fontSize(this.activeTab === ti ? 12 : 10)
            .fontWeight(this.activeTab === ti ? FontWeight.Bold : FontWeight.Normal)
            .fontColor(this.activeTab === ti ? HN.red : HN.textHint)
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .padding({ top: 6, bottom: 6 })
        .onClick(() => {
          this.activeTab = ti;
        })
      }, (tb: HnTab) => tb.key)
    }
    .width('100%')
    .height(58)
    .backgroundColor(HN.card)
    .border({ width: { top: 1 }, color: HN.line })
  }

在这里插入图片描述

底部导航栏是应用的核心交互入口。六个页签通过ForEach循环渲染,每个页签包含图标和文字两行内容。当某个页签被激活时(this.activeTab === ti),其图标字号从17变为20、字重从Normal变为Bold、颜色从提示色变为主色调——通过这种多维度的视觉变化,用户能够清晰地感知当前所处的页面位置。

onClick回调中执行this.activeTab = ti这一简单赋值,但由于@State的响应式机制,该赋值会触发整个build方法的重新执行,条件渲染分支会据此切换到对应的子组件。这种"状态驱动视图"的模式是声明式UI的精髓。

  build() {
    Column() {
      if (this.activeTab === 0) {
        this.headerBar('国风汉服馆')
      } else if (this.activeTab === 1) {
        this.headerBar('汉服形制馆')
      } else if (this.activeTab === 2) {
        this.headerBar('国风配饰集')
      } else if (this.activeTab === 3) {
        this.headerBar('妆造司')
      } else if (this.activeTab === 4) {
        this.headerBar('雅集活动')
      } else {
        this.headerBar('我的衣橱')
      }

      Column() {
        if (this.activeTab === 0) {
          HanHomeTab()
        } else if (this.activeTab === 1) {
          HanItemTab()
        } else if (this.activeTab === 2) {
          HanAccTab()
        } else if (this.activeTab === 3) {
          HanStyleTab()
        } else if (this.activeTab === 4) {
          HanEventTab()
        } else {
          HanMineTab()
        }
      }
      .layoutWeight(1)

      this.bottomBar()
    }
    .width('100%')
    .height('100%')
    .backgroundColor(HN.bg)
  }
}

入口组件的build方法构建了三段式布局:顶部标题栏、中间内容区、底部导航栏。标题栏根据当前页签显示不同的标题文字,内容区通过条件渲染挂载对应的子组件,layoutWeight(1)确保内容区占据所有剩余空间。整个应用的外层容器设置了100%宽高和背景色,形成统一的视觉基底。

四、首页Tab——轮播横幅与动效系统

首页是用户进入应用后看到的第一屏,它集成了品牌横幅、灯笼摆动动效、分类导航宫格、形制热度排行和商品上新列表五个核心模块。

@Component
struct HanHomeTab {
  @State showDetail: boolean = false;
  @State selItem: number = 0;
  @State showRankTip: boolean = false;
  @State favorited: boolean = false;
  @State lanternStep: number = 0;
  @State bloomOn: boolean = false;
  timerId: number = -1;

  aboutToAppear(): void {
    this.timerId = setInterval(() => {
      this.lanternStep = (this.lanternStep + 1) % 4;
      this.bloomOn = !this.bloomOn;
    }, 900);
  }

  aboutToDisappear(): void {
    if (this.timerId >= 0) {
      clearInterval(this.timerId);
    }
  }

在这里插入图片描述

首页组件管理了六个状态变量:商品详情弹窗显隐(showDetail)、选中商品ID(selItem)、排行榜规则弹窗显隐(showRankTip)、收藏状态(favorited)、灯笼动画步进(lanternStep)、花朵绽放开关(bloomOn)。

aboutToAppearaboutToDisappear是ArkTS组件的生命周期回调。前者在组件创建后、build方法执行前调用,后者在组件销毁前调用。在aboutToAppear中启动定时器实现周期性动画,在aboutToDisappear中清除定时器防止内存泄漏——这是资源管理的标准范式。

定时器每900毫秒执行一次,将lanternStep在0-3之间循环递增,同时切换bloomOn的布尔值。这两个状态变量分别驱动灯笼的摆动角度和闪烁效果,由于@State的响应式特性,每次状态更新都会触发相关UI元素的属性动画重新计算。

  @Builder
  scrollBanner() {
    Stack() {
      Column()
        .width('100%')
        .height(150)
        .borderRadius(4)
        .linearGradient({
          angle: 0,
          colors: [[HN.red, 0], [HN.redDeep, 1]]
        })
      Row() {
        Text('❦')
          .fontSize(30)
          .fontColor('#CCFFFFFF')
        Column() {
          Text('华夏衣冠 · 形制之美')
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.white)
          Text('马面裙新品首发 · 织金工艺 8 折')
            .fontSize(12)
            .fontColor('#F0D9B0')
            .margin({ top: 6 })
          Text('限时 3 天 · 支持租借')
            .fontSize(11)
            .fontColor(HN.white)
            .padding({ left: 10, right: 10, top: 4, bottom: 4 })
            .backgroundColor('#33FFFFFF')
            .borderRadius(10)
            .margin({ top: 8 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        Text('❦')
          .fontSize(30)
          .fontColor('#CCFFFFFF')
      }
      .width('100%')
      .padding({ left: 18, right: 18 })
    }
    .width('100%')
    .height(150)
    .margin({ top: 10, left: 16, right: 16 })
  }

  @Builder
  lanternEffect() {
    Row() {
      Text('🏮')
        .fontSize(24)
        .rotate({ angle: this.lanternStep % 2 === 0 ? -6 : 6 })
        .animation({ duration: 800 })
        .margin({ left: 30 })
      Text('🏮')
        .fontSize(18)
        .rotate({ angle: this.lanternStep % 2 === 0 ? 8 : -8 })
        .animation({ duration: 900 })
        .margin({ left: 120 })
      Text('✨')
        .fontSize(14)
        .opacity(this.bloomOn ? 0.35 : 1)
        .animation({ duration: 800 })
        .margin({ left: 180 })
    }
    .width('100%')
    .height(34)
  }

品牌横幅使用Stack层叠布局,底层是一个从朱红到深红渐变的Column作为背景,上层是包含装饰符号"❦"和标题文字的Row。渐变方向设为0度(从上到下),颜色从#B03A2E过渡到#7E241C,营造出深邃的层次感。

灯笼动效是该应用的视觉亮点之一。两个灯笼emoji以不同的偏移量排列在水平方向,通过rotate属性实现摆动——第一个灯笼在lanternStep为偶数时左倾6度、奇数时右倾6度,第二个灯笼则反向摆动。每个灯笼都附加了.animation({ duration: 800 })属性动画,使得角度变化不是瞬间跳变而是平滑过渡。闪烁的"✨"通过opacity在0.35和1之间交替,配合bloomOn状态实现了呼吸般的闪烁效果。

五、形制热度排行榜与分类导航

  @Builder
  formRank() {
    Column() {
      Row() {
        Text('📜 形制热度榜')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)
        Column().layoutWeight(1)
        Text('规则')
          .fontSize(12)
          .fontColor(HN.gold)
          .onClick(() => {
            this.showRankTip = true;
          })
      }
      .width('100%')
      .margin({ bottom: 8 })

      ForEach(HN_COUNTS, (ct: HnCount, ci: number) => {
        Row() {
          Text(ci + 1 + '')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(ci < 3 ? HN.red : HN.textHint)
            .width(22)
          Text(ct.name + '制')
            .fontSize(13)
            .fontColor(HN.text)
            .width(58)
          Stack() {
            Row()
              .width('100%')
              .height(10)
              .backgroundColor('#EDE0CC')
              .borderRadius(5)
            Row()
              .width(hnBar(ct.percent))
              .height(10)
              .backgroundColor(ci < 3 ? HN.red : HN.gold)
              .borderRadius(5)
          }
          .layoutWeight(1)
          .height(10)

          Text(ct.count + ' 款')
            .fontSize(12)
            .fontColor(HN.textSub)
            .width(46)
            .textAlign(TextAlign.End)
        }
        .width('100%')
        .margin({ top: 8 })
      }, (ct: HnCount) => ct.name)
    }
    .width('100%')
    .padding(16)
    .backgroundColor(HN.card)
    .borderRadius(14)
    .margin({ left: 16, right: 16, top: 12 })
  }

在这里插入图片描述

形制热度排行榜使用Stack层叠实现了进度条效果——底层是满宽的浅色Row作为轨道,上层是宽度为hnBar(ct.percent)的彩色Row作为填充。前三名使用朱红色高亮,其余使用鎏金色,形成视觉的优先级区分。排行榜数据来自HN_COUNTS常量,包含明制、唐制、宋制、清制、晋制和秦汉六个形制分类的热度值。

ForEach的第三个参数是键值生成器(keyGenerator),用于为每个列表项生成唯一标识。在ArkTS中,正确的键值生成可以确保列表在数据变化时进行最小化的差异更新,避免不必要的全量重渲染。

六、商品卡片与详情弹窗

商品上新列表通过ForEach遍历HN_ITEMS数组渲染卡片,每个卡片都可以点击打开详情弹窗。

  @Builder
  newItemCard(it: HnItem, idx: number) {
    Row() {
      Column() {
        Text(hnIcon(idx))
          .fontSize(32)
      }
      .width(58)
      .height(58)
      .backgroundColor(idx % 2 === 0 ? '#F6E4DE' : '#DEE9E6')
      .borderRadius(12)
      .justifyContent(FlexAlign.Center)

      Column() {
        Row() {
          Text(it.name)
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.text)
          Text(it.form)
            .fontSize(10)
            .fontColor(HN.white)
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .backgroundColor(hnFormColor(it.form))
            .borderRadius(8)
            .margin({ left: 6 })
        }
        Text(it.fabric + ' · ' + it.dynasty + '制')
          .fontSize(11)
          .fontColor(HN.textSub)
          .margin({ top: 3 })
        Row() {
          Text(hnPrice(it.price))
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.danger)
          Text(hnPrice(it.oldPrice))
            .fontSize(11)
            .fontColor(HN.textHint)
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ left: 8 })
          Text('余 ' + it.stock + ' 件')
            .fontSize(10)
            .fontColor(it.stock < 10 ? HN.danger : HN.success)
            .margin({ left: 8 })
        }
        .margin({ top: 3 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 10 })
    }
    .width('100%')
    .padding(12)
    .backgroundColor(HN.card)
    .borderRadius(14)
    .margin({ left: 16, right: 16, top: 8 })
    .onClick(() => {
      this.selItem = it.id;
      this.showDetail = true;
    })
  }

商品卡片的布局结构是典型的"左图右文"模式——左侧是一个58x58的图标容器,背景色根据索引奇偶性交替使用浅红和浅青两种色调;右侧是商品信息的纵向堆叠,包含名称+形制标签行、面料+朝代行、价格+原价+库存行。

价格区域的设计体现了电商应用的通用模式:现价用红色粗体大字号突出显示,原价用灰色小字号配合TextDecorationType.LineThrough(删除线)表示折扣。库存预警逻辑嵌入在字体颜色中——当it.stock < 10时显示危险色,否则显示成功色,直观传达"余量告急"的紧迫感。

  @Builder
  detailModal() {
    Column() {
      Column() {
        Text(HN_ITEMS[this.selItem - 1].name)
          .fontSize(19)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)
        Text(HN_ITEMS[this.selItem - 1].form + ' · ' + HN_ITEMS[this.selItem - 1].fabric)
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 6 })

        Row() {
          Column() {
            Text('朝代')
              .fontSize(11)
              .fontColor(HN.textHint)
            Text(HN_ITEMS[this.selItem - 1].dynasty + '制')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(HN.red)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          Column() {
            Text('库存')
              .fontSize(11)
              .fontColor(HN.textHint)
            Text(HN_ITEMS[this.selItem - 1].stock + ' 件')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(HN_ITEMS[this.selItem - 1].stock < 10 ? HN.danger : HN.success)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          Column() {
            Text('热度')
              .fontSize(11)
              .fontColor(HN.textHint)
            Text(HN_ITEMS[this.selItem - 1].percent + '%')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(HN.gold)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })
        .backgroundColor('#FBF6EC')
        .borderRadius(12)
        .margin({ top: 12 })

        Row() {
          Text('国风价')
            .fontSize(12)
            .fontColor(HN.textHint)
          Text(hnPrice(HN_ITEMS[this.selItem - 1].price))
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.danger)
            .margin({ left: 8 })
          Text(hnPrice(HN_ITEMS[this.selItem - 1].oldPrice))
            .fontSize(12)
            .fontColor(HN.textHint)
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ left: 8 })
        }
        .width('100%')
        .margin({ top: 12 })

        Row() {
          Text(this.favorited ? '♥ 已收藏' : '♡ 收藏')
            .fontSize(13)
            .fontColor(this.favorited ? HN.danger : HN.red)
            .padding({ left: 18, right: 18, top: 8, bottom: 8 })
            .border({ width: 1, color: this.favorited ? HN.danger : HN.gold })
            .borderRadius(18)
            .onClick(() => {
              this.favorited = !this.favorited;
            })
          Column().layoutWeight(1)
          Text('加入衣橱')
            .fontSize(13)
            .fontColor(HN.white)
            .padding({ left: 18, right: 18, top: 8, bottom: 8 })
            .backgroundColor(HN.red)
            .borderRadius(18)
            .onClick(() => {
              this.showDetail = false;
            })
        }
        .width('100%')
        .margin({ top: 14 })
      }
      .width('88%')
      .padding(18)
      .backgroundColor(HN.card)
      .borderRadius(16)
      .constraintSize({ maxHeight: '80%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

在这里插入图片描述

详情弹窗通过selItem - 1作为索引从HN_ITEMS数组中获取选中商品的数据,展示朝代、库存、热度三项关键指标,以三列均分布局排列在浅色背景的信息条中。底部操作区提供了收藏和加入衣橱两个按钮——收藏按钮根据favorited状态在"♥ 已收藏"和"♡ 收藏"之间切换,同时边框颜色也随之变化。

模态弹窗的实现采用了Stack+条件渲染+zIndex(999)的经典模式:当showDetail为true时,一个半透明遮罩层和弹窗内容被渲染到Stack的最顶层,遮罩层使用#66000000的半透明黑色覆盖全屏,弹窗内容居中显示。这种模式确保了弹窗始终悬浮于主内容之上。

七、汉服形制馆——侧边分类与CRUD操作

汉服形制馆Tab是该应用中最复杂的业务模块之一,它实现了完整的CRUD(创建、读取、更新、删除)操作,配合侧边分类导航和动态筛选功能。

@Component
struct HanItemTab {
  @State curForm: string = '全部形制';
  @State items: HnItem[] = HN_ITEMS.slice();
  @State showDetail: boolean = false;
  @State showAdd: boolean = false;
  @State showDel: boolean = false;
  @State selId: number = 0;
  @State delId: number = 0;
  @State editName: string = '';
  @State editPrice: string = '';
  @State editForm: string = '明制';
  @State ribbonStep: number = 0;
  timerId: number = -1;
  formList: string[] = ['全部形制', '明制', '唐制', '宋制', '清制', '晋制', '秦汉', '改良'];

在这里插入图片描述

该组件管理了多达十个状态变量:当前选中的形制分类(curForm)、商品列表数据(items)、三个弹窗的显隐状态(showDetail/showAdd/showDel)、选中编辑的商品ID(selId)、待删除的商品ID(delId)、三个编辑表单字段(editName/editPrice/editForm)、以及丝带动画步进(ribbonStep)。

值得注意的是items状态使用了HN_ITEMS.slice()来创建数组的浅拷贝——这是因为在ArkTS中,@State装饰的数组需要通过重新赋值才能触发UI更新。如果直接操作原始常量数组,不仅无法触发响应式更新,还会污染全局数据。

function hnFilterItems(form: string): HnItem[] {
  if (form === '全部形制') {
    return HN_ITEMS;
  }
  return HN_ITEMS.filter((o: HnItem) => o.form === form || o.dynasty === form.charAt(0));
}

筛选函数hnFilterItems根据形制名称过滤商品列表:当选择"全部形制"时返回完整列表,否则通过filter方法匹配形制名称或朝代首字。这种双重匹配策略确保了用户选择"明制"时能够同时匹配到形制字段为"明制马面裙"和朝代字段为"明"的商品。

  @Builder
  addModal() {
    Column() {
      Column() {
        Text('🪡 上新汉服')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)

        Text('款式名称')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 12 })
          .alignSelf(ItemAlign.Start)
        TextInput({ placeholder: '例如:月白褙子·听雨' })
          .fontSize(13)
          .height(40)
          .backgroundColor('#FBF6EC')
          .borderRadius(10)
          .onChange((v: string) => {
            this.editName = v;
          })

        Row() {
          Column() {
            Text('价格')
              .fontSize(12)
              .fontColor(HN.textSub)
              .margin({ bottom: 6 })
              .alignSelf(ItemAlign.Start)
            TextInput({ placeholder: '0', text: this.editPrice })
              .fontSize(13)
              .height(40)
              .backgroundColor('#FBF6EC')
              .borderRadius(10)
              .onChange((v: string) => {
                this.editPrice = v;
              })
          }
          .layoutWeight(1)
          Column() {
            Text('形制')
              .fontSize(12)
              .fontColor(HN.textSub)
              .margin({ bottom: 6 })
              .alignSelf(ItemAlign.Start)
            Text(this.editForm)
              .fontSize(13)
              .fontColor(HN.red)
              .height(40)
              .width('100%')
              .backgroundColor('#FBF6EC')
              .borderRadius(10)
              .textAlign(TextAlign.Center)
              .onClick(() => {
                let forms = ['明制', '唐制', '宋制', '清制', '晋制', '秦汉', '改良'];
                let now = forms.indexOf(this.editForm);
                this.editForm = forms[(now + 1) % forms.length];
              })
          }
          .layoutWeight(1)
          .margin({ left: 10 })
        }
        .width('100%')
        .margin({ top: 10 })

        Row() {
          Text('取消')
            .fontSize(13)
            .fontColor(HN.textSub)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .border({ width: 1, color: HN.line })
            .borderRadius(18)
            .onClick(() => {
              this.showAdd = false;
            })
          Column().layoutWeight(1)
          Text('确认上新')
            .fontSize(13)
            .fontColor(HN.white)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .backgroundColor(HN.gold)
            .borderRadius(18)
            .onClick(() => {
              let newId = this.items.length + 200;
              let np = hnPriceNum(this.editPrice);
              this.items.push({ id: newId, name: this.editName === '' ? '新制汉服' : this.editName, form: this.editForm, price: np, oldPrice: np, fabric: '待定面料', dynasty: this.editForm.charAt(0), stock: 20, percent: 80 });
              this.items = this.items.slice();
              this.showAdd = false;
              this.editName = '';
              this.editPrice = '';
            })
        }
        .width('100%')
        .margin({ top: 16 })
      }
      .width('86%')
      .padding(18)
      .backgroundColor(HN.card)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

在这里插入图片描述

新增弹窗提供了款式名称输入、价格输入和形制选择三个表单字段。形制选择采用了"点击循环切换"的交互模式——每次点击形制标签,editForm在七种形制之间循环递增。这种设计比下拉选择器更节省空间,适合移动端的有限屏幕区域。

在新增操作中,this.items.push(...)后紧接着this.items = this.items.slice()是一个关键技巧——push方法直接修改原数组不会触发ArkTS的响应式更新,但通过slice()创建一个新数组引用并重新赋值给@State变量,框架就能检测到变化并触发UI重渲染。这是ArkTS状态管理的重要约束:必须是引用变化而非内容变化。

八、妆造预约与活动报名

妆造司Tab实现了完整的预约流程——用户可以选择妆造风格、预约日期和时段,已预约的妆造会显示勾选状态,并且可以取消预约。

  @Builder
  bookModal() {
    Column() {
      Column() {
        Text('📅 预约妆造')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)
        Text('「' + this.getStyleName(this.selId) + '」 · ' + this.getStyleMaster(this.selId))
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 6 })

        Text('选择日期')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 12 })
          .alignSelf(ItemAlign.Start)
        Row() {
          ForEach(this.dateList, (dt: string, di: number) => {
            Text(dt)
              .fontSize(11)
              .fontColor(this.curDate === dt ? HN.white : HN.textSub)
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .backgroundColor(this.curDate === dt ? HN.red : '#FBF6EC')
              .borderRadius(12)
              .margin({ right: 6 })
              .onClick(() => {
                this.curDate = dt;
              })
          }, (dt: string) => dt)
        }
        .width('100%')
        .margin({ top: 6 })

        Text('选择时段')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 10 })
          .alignSelf(ItemAlign.Start)
        Row() {
          ForEach(this.timeList, (tm: string, ti: number) => {
            Text(tm)
              .fontSize(11)
              .fontColor(this.curTime === tm ? HN.white : HN.textSub)
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .backgroundColor(this.curTime === tm ? HN.teal : '#FBF6EC')
              .borderRadius(12)
              .margin({ right: 6 })
              .onClick(() => {
                this.curTime = tm;
              })
          }, (tm: string) => tm)
        }
        .width('100%')
        .margin({ top: 6 })

        Row() {
          Text('取消')
            .fontSize(13)
            .fontColor(HN.textSub)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .border({ width: 1, color: HN.line })
            .borderRadius(18)
            .onClick(() => {
              this.showBook = false;
            })
          Column().layoutWeight(1)
          Text('确认预约')
            .fontSize(13)
            .fontColor(HN.white)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .backgroundColor(HN.gold)
            .borderRadius(18)
            .onClick(() => {
              this.bookings.push(this.getStyleName(this.selId));
              this.showBook = false;
            })
        }
        .width('100%')
        .margin({ top: 16 })
      }
      .width('86%')
      .padding(18)
      .backgroundColor(HN.card)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

预约弹窗的日期选择使用ForEach渲染六个日期选项(今日、明日、周四至周日),时段选择渲染六个时间段(10:00至19:00)。选中项使用朱红色背景+白色文字,未选中项使用浅色背景+灰色文字。确认预约时,妆造风格名称被推入bookings数组,UI随之更新显示"已预约 ✓"状态。

九、核心业务流程图

以下是应用从用户打开应用到完成汉服购买或活动报名的完整业务流程:

首页

汉服

配饰

妆造

活动

我的

收藏

加入衣橱

编辑

删除

未报名

已报名

应用启动

渲染首页Tab

用户选择页签

浏览品牌横幅与商品上新

侧边选择形制分类

浏览配饰网格陈列

选择妆造风格预约

查看雅集活动报名

查看订单与收藏

点击商品卡片

弹出商品详情弹窗

收藏或加入衣橱

切换收藏状态

关闭弹窗

形制筛选过滤

点击商品查看详情

编辑或删除

修改名称价格形制

确认下架

选择日期与时段

确认预约

更新预约列表

点击活动查看详情

是否已报名

确认报名

保持报名状态

更新报名人数

十、个人中心——订单与收藏管理

个人中心Tab是用户数据的汇聚页面,集成了用户档案卡片、订单管理和收藏管理三大功能。

  @Builder
  profileCard() {
    Column() {
      Row() {
        Column() {
          Text('錦')
            .fontSize(26)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.red)
            .scale({ x: this.sealScale, y: this.sealScale })
            .animation({ duration: 900 })
        }
        .width(62)
        .height(62)
        .backgroundColor('#F3E3D8')
        .border({ width: 2, color: HN.red })
        .borderRadius(8)
        .justifyContent(FlexAlign.Center)

        Column() {
          Text(HN_PROFILE.nick)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.text)
          Text(HN_PROFILE.level)
            .fontSize(12)
            .fontColor(HN.gold)
            .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        .margin({ left: 12 })

        Text('✏️')
          .fontSize(18)
          .fontColor(HN.textSub)
          .onClick(() => {
            this.editNick = HN_PROFILE.nick;
            this.editSig = '衣冠礼乐,尽在方寸';
            this.showEdit = true;
          })
      }
      .width('100%')

      Row() {
        Column() {
          Text(HN_PROFILE.points + '')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.red)
          Text('积分')
            .fontSize(11)
            .fontColor(HN.textSub)
            .margin({ top: 3 })
        }
        .layoutWeight(1)
        Column() {
          Text(HN_PROFILE.coupon + '')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.gold)
          Text('优惠券')
            .fontSize(11)
            .fontColor(HN.textSub)
            .margin({ top: 3 })
        }
        .layoutWeight(1)
        Column() {
          Text(HN_PROFILE.fans + '')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.teal)
          Text('粉丝')
            .fontSize(11)
            .fontColor(HN.textSub)
            .margin({ top: 3 })
        }
        .layoutWeight(1)
        Column() {
          Text(HN_PROFILE.follows + '')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.danger)
          Text('关注')
            .fontSize(11)
            .fontColor(HN.textSub)
            .margin({ top: 3 })
        }
        .layoutWeight(1)
      }
      .width('100%')
      .padding({ top: 14, bottom: 2 })
      .margin({ top: 14 })
      .border({ width: { top: 1 }, color: HN.line })
    }
    .width('100%')
    .padding(16)
    .backgroundColor(HN.card)
    .borderRadius(14)
    .margin({ left: 16, right: 16, top: 12 })
  }

用户档案卡片的设计颇具国风特色——头像区域使用了一个"錦"字印章的效果,带有朱红色双边框和方形圆角。印章通过scale属性实现周期性的缩放呼吸动画,sealScale在0.9和1.08之间交替,配合900毫秒的动画时长,模拟了印章按压的动态感。下方的四列统计信息(积分、优惠券、粉丝、关注)使用不同的主题色进行区分。

在ArkTS中,scale属性接受{ x: number, y: number }对象参数,用于在X和Y方向上等比或非等比缩放元素。配合.animation()属性,可以实现平滑的弹性动画效果。这种属性动画是声明式的——开发者只需指定目标值和持续时间,框架自动处理插值计算和帧渲染。

十一、活动Tab与报名流程

活动Tab展示了六场传统雅集活动,每场活动都有日期、地点、报名人数和容量限制。已报名的活动显示绿色"已报名"标签,未报名的显示红色"热招中"标签。

  @Builder
  eventCard(ev: HnEvent, idx: number) {
    Row() {
      Column() {
        Text(idx + 1 + '')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.joined.indexOf(ev.id) >= 0 ? HN.success : HN.gold)
        Text(ev.date.indexOf('农历') >= 0 ? '节令' : '日常')
          .fontSize(9)
          .fontColor(HN.textHint)
          .margin({ top: 2 })
      }
      .width(44)
      .alignItems(HorizontalAlign.Center)
      .margin({ top: 6 })

      Column() {
        Row() {
          Text(ev.name)
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.text)
          Text(this.joined.indexOf(ev.id) >= 0 ? '已报名' : '热招中')
            .fontSize(10)
            .fontColor(this.joined.indexOf(ev.id) >= 0 ? HN.success : HN.white)
            .padding({ left: 8, right: 8, top: 2, bottom: 2 })
            .backgroundColor(this.joined.indexOf(ev.id) >= 0 ? '#E8F0E8' : HN.red)
            .borderRadius(8)
            .margin({ left: 8 })
        }
        Row() {
          Text('📅 ' + ev.date)
            .fontSize(11)
            .fontColor(HN.textSub)
          Text('📍 ' + ev.place)
            .fontSize(11)
            .fontColor(HN.textSub)
            .margin({ left: 12 })
        }
        .margin({ top: 5 })

        Stack() {
          Row()
            .width('100%')
            .height(8)
            .backgroundColor('#EDE0CC')
            .borderRadius(4)
          Row()
            .width(hnBar(ev.percent))
            .height(8)
            .backgroundColor(this.joined.indexOf(ev.id) >= 0 ? HN.success : HN.gold)
            .borderRadius(4)
        }
        .width('100%')
        .height(8)
        .margin({ top: 8 })

        Row() {
          Text('已报名 ' + ev.sign + '/' + ev.cap + ' 人')
            .fontSize(10)
            .fontColor(HN.textHint)
          Column().layoutWeight(1)
          Text('详情')
            .fontSize(11)
            .fontColor(HN.gold)
            .onClick(() => {
              this.selId = ev.id;
              this.showDetail = true;
            })
        }
        .width('100%')
        .margin({ top: 6 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .padding({ left: 10, bottom: 6 })
      .border({ width: { left: 1 }, color: HN.line })
    }
    .width('100%')
    .padding({ top: 4, bottom: 4, right: 14 })
    .margin({ left: 16, right: 16, top: 6 })
  }

活动卡片采用时间轴式的布局——左侧是序号和活动类型(节令/日常),右侧通过border({ width: { left: 1 } })绘制左侧分隔线,形成时间轴的视觉效果。报名进度条使用金色填充,已报名的活动切换为绿色。报名确认后,joined数组中推入活动ID,同时该活动的sign(已报名人数)通过map方法递增1。

技术点对比

技术维度 实现方式 设计优势 适用场景
状态管理 @State装饰器 + 组件内变量 单向数据流,状态变化自动触发UI更新 组件内部状态,如弹窗显隐、选中项
组件通信 @Builder封装 + 条件渲染 将UI结构提取为可复用方法,减少代码冗余 重复出现的卡片、弹窗等UI结构
列表渲染 ForEach + keyGenerator键值 通过唯一键值实现最小化差异更新 商品列表、订单列表等动态数据集合
模态弹窗 Stack + 条件渲染 + zIndex(999) 层叠覆盖主内容,无需额外路由 详情弹窗、编辑表单、删除确认
定时动画 setInterval + @State + .animation() 周期性更新状态触发属性动画 灯笼摆动、丝带飘动等装饰性动效
数据筛选 全局纯函数 + filter方法 逻辑与视图分离,可独立测试和复用 形制分类、轴体分类等条件过滤
渐变背景 linearGradient + angle参数 角度可控的双色渐变,营造层次感 品牌横幅、页头背景、卡片装饰
表单输入 TextInput + onChange回调 实时同步输入值到状态变量 新增商品、编辑资料等表单场景
数组更新 pushslice重新赋值 确保引用变化触发响应式更新 新增数据项到列表末尾
生命周期 aboutToAppear / aboutToDisappear 资源初始化与清理的确定性时机 定时器管理、数据预加载

安装DevEco Studio程序

在这里插入图片描述
选择目标安装目录:

在这里插入图片描述
设置环境变量,但是需要重启一下:

在这里插入图片描述
新建一个空白模板:

在这里插入图片描述
设置API为24的模板项目:
在这里插入图片描述
初始化项目,自动下载相关依赖:

在这里插入图片描述


完整代码:

interface HnTab {
  key: string;
  label: string;
  icon: string;
}

interface HnItem {
  id: number;
  name: string;
  form: string;
  price: number;
  oldPrice: number;
  fabric: string;
  dynasty: string;
  stock: number;
  percent: number;
}

interface HnAcc {
  id: number;
  name: string;
  kind: string;
  price: number;
  material: string;
  percent: number;
}

interface HnStyle {
  id: number;
  name: string;
  desc: string;
  price: number;
  dur: string;
  master: string;
  percent: number;
}

interface HnEvent {
  id: number;
  name: string;
  date: string;
  place: string;
  sign: number;
  cap: number;
  percent: number;
}

interface HnOrder {
  id: number;
  name: string;
  date: string;
  price: number;
  status: string;
}

interface HnFav {
  id: number;
  name: string;
  price: number;
  form: string;
}

interface HnCount {
  name: string;
  count: number;
  percent: number;
}

interface HnProfile {
  nick: string;
  level: string;
  points: number;
  coupon: number;
  fans: number;
  follows: number;
}

interface ColorPalette {
  red: string;
  redDeep: string;
  teal: string;
  gold: string;
  goldLight: string;
  bg: string;
  card: string;
  text: string;
  textSub: string;
  textHint: string;
  line: string;
  danger: string;
  success: string;
  white: string;
}

const HN: ColorPalette = {
  red: '#B03A2E',
  redDeep: '#7E241C',
  teal: '#2F4B4C',
  gold: '#C9A227',
  goldLight: '#EFE3C2',
  bg: '#F6EFE0',
  card: '#FFFFFF',
  text: '#3D2B22',
  textSub: '#8A7463',
  textHint: '#C2B09C',
  line: '#EDE0CC',
  danger: '#A93A2B',
  success: '#6E8B6E',
  white: '#FFFFFF'
};

const HN_TABS: HnTab[] = [
  { key: 'home', label: '首页', icon: '🏮' },
  { key: 'hanfu', label: '汉服', icon: '👘' },
  { key: 'acc', label: '配饰', icon: '🌸' },
  { key: 'style', label: '妆造', icon: '💄' },
  { key: 'event', label: '活动', icon: '🎐' },
  { key: 'mine', label: '我的', icon: '🧧' }
];

const HN_ITEMS: HnItem[] = [
  { id: 1, name: '鎏金马面裙·云鹤', form: '明制马面裙', price: 699, oldPrice: 899, fabric: '织金缎', dynasty: '明', stock: 32, percent: 97 },
  { id: 2, name: '齐胸襦裙·桃花笺', form: '唐制齐胸襦裙', price: 529, oldPrice: 699, fabric: '雪纺/缎面', dynasty: '唐', stock: 45, percent: 94 },
  { id: 3, name: '明制披风·松间照', form: '明制披风', price: 899, oldPrice: 1199, fabric: '提花呢', dynasty: '明', stock: 18, percent: 92 },
  { id: 4, name: '宋制褙子·清茶', form: '宋制褙子', price: 429, oldPrice: 559, fabric: '天丝棉', dynasty: '宋', stock: 50, percent: 90 },
  { id: 5, name: '唐制坦领·胡旋舞', form: '唐制坦领', price: 589, oldPrice: 759, fabric: '印花纱', dynasty: '唐', stock: 26, percent: 93 },
  { id: 6, name: '曲裾深衣·兰泽', form: '秦汉曲裾', price: 1099, oldPrice: 1399, fabric: '素绉缎', dynasty: '秦汉', stock: 12, percent: 89 },
  { id: 7, name: '道袍·云外客', form: '明制道袍', price: 759, oldPrice: 959, fabric: '棉麻', dynasty: '明', stock: 22, percent: 88 },
  { id: 8, name: '比甲·小满', form: '明制比甲', price: 399, oldPrice: 519, fabric: '织锦', dynasty: '明', stock: 38, percent: 91 },
  { id: 9, name: '袄裙·岁寒', form: '明制袄裙', price: 819, oldPrice: 1049, fabric: '绒面', dynasty: '明', stock: 15, percent: 90 },
  { id: 10, name: '晋制襦裙·鹿鸣', form: '晋制襦裙', price: 619, oldPrice: 799, fabric: '亚麻纱', dynasty: '晋', stock: 20, percent: 86 },
  { id: 11, name: '圆领袍·长安少年', form: '唐制圆领袍', price: 649, oldPrice: 829, fabric: '提花绸', dynasty: '唐', stock: 28, percent: 95 },
  { id: 12, name: '飞鱼服·锦衣夜行', form: '明制飞鱼服', price: 1899, oldPrice: 2399, fabric: '妆花缎', dynasty: '明', stock: 6, percent: 98 },
  { id: 13, name: '斗篷·踏雪寻梅', form: '清制斗篷', price: 999, oldPrice: 1299, fabric: '羊羔绒', dynasty: '清', stock: 14, percent: 92 },
  { id: 14, name: '改良汉服·新中式', form: '改良新中式', price: 459, oldPrice: 589, fabric: '醋酸缎', dynasty: '今', stock: 60, percent: 94 },
  { id: 15, name: '云肩袄·凤来仪', form: '清制云肩袄', price: 1299, oldPrice: 1699, fabric: '苏绣云肩', dynasty: '清', stock: 9, percent: 96 },
  { id: 16, name: '婚服·凤冠霞帔', form: '明制婚服', price: 2999, oldPrice: 3999, fabric: '妆花缎+苏绣', dynasty: '明', stock: 4, percent: 99 }
];

const HN_ACCS: HnAcc[] = [
  { id: 1, name: '鎏金点翠发簪', kind: '发饰', price: 269, material: '铜鎏金/点翠', percent: 95 },
  { id: 2, name: '苏绣团扇·牡丹', kind: '持物', price: 199, material: '真丝/竹骨', percent: 93 },
  { id: 3, name: '珍珠璎珞项圈', kind: '颈饰', price: 359, material: '淡水珠/银', percent: 91 },
  { id: 4, name: '掐丝步摇·蝶恋花', kind: '发饰', price: 429, material: '银/珐琅', percent: 94 },
  { id: 5, name: '织锦腰佩·双鱼', kind: '腰饰', price: 159, material: '织锦/玉坠', percent: 88 },
  { id: 6, name: '绣花弓鞋·并蒂莲', kind: '鞋履', price: 289, material: '缎面/手绣', percent: 90 },
  { id: 7, name: '香囊荷包·桂子', kind: '佩饰', price: 99, material: '真丝/香丸', percent: 89 },
  { id: 8, name: '金箔花钿贴', kind: '面饰', price: 79, material: '金箔/鱼胶', percent: 92 }
];

const HN_STYLES: HnStyle[] = [
  { id: 1, name: '花钿妆', desc: '额间贴花钿,唐宫仕女风', price: 199, dur: '45min', master: '青黛', percent: 96 },
  { id: 2, name: '远山黛眉', desc: '远山含黛,清冷温婉', price: 129, dur: '30min', master: '月白', percent: 92 },
  { id: 3, name: '绛唇点绛', desc: '复古红唇,明艳大气', price: 149, dur: '35min', master: '青黛', percent: 91 },
  { id: 4, name: '飞霞妆面', desc: '腮红晕染,娇俏灵动', price: 169, dur: '40min', master: '阿桃', percent: 90 },
  { id: 5, name: '垂珠妆', desc: '额心垂珠,敦煌飞天感', price: 259, dur: '60min', master: '月白', percent: 95 },
  { id: 6, name: '桃花妆', desc: '粉面桃腮,少女感满格', price: 139, dur: '35min', master: '阿桃', percent: 89 },
  { id: 7, name: '柳叶细眉', desc: '柳叶弯弯,柔情似水', price: 99, dur: '25min', master: '青黛', percent: 87 },
  { id: 8, name: '朱砂美人痣', desc: '眉心一点朱砂,风情万种', price: 119, dur: '30min', master: '阿桃', percent: 93 }
];

const HN_EVENTS: HnEvent[] = [
  { id: 1, name: '上巳节·曲水流觞', date: '农历三月初三', place: '颐和园昆明湖', sign: 186, cap: 300, percent: 62 },
  { id: 2, name: '花朝节·赏红扑蝶', date: '农历二月十五', place: '北海公园', sign: 142, cap: 200, percent: 71 },
  { id: 3, name: '七夕·乞巧市集', date: '农历七月初七', place: '什刹海宋韵街', sign: 260, cap: 300, percent: 87 },
  { id: 4, name: '中秋·拜月祈福', date: '农历八月十五', place: '天坛公园', sign: 98, cap: 150, percent: 65 },
  { id: 5, name: '汉服巡游·长安夜', date: '每周六晚', place: '大唐不夜城', sign: 420, cap: 500, percent: 84 },
  { id: 6, name: '冬至·消寒雅集', date: '农历冬月廿三', place: '紫竹院', sign: 56, cap: 120, percent: 47 }
];

const HN_ORDERS: HnOrder[] = [
  { id: 2001, name: '鎏金马面裙·云鹤 M码', date: '08-21', price: 699, status: '已签收' },
  { id: 2002, name: '苏绣团扇·牡丹', date: '08-19', price: 199, status: '运输中' },
  { id: 2003, name: '花钿妆预约(45min)', date: '08-16', price: 199, status: '已完成' },
  { id: 2004, name: '齐胸襦裙·桃花笺 L码', date: '08-12', price: 529, status: '已签收' },
  { id: 2005, name: '飞鱼服·锦衣夜行', date: '08-08', price: 1899, status: '待付款' },
  { id: 2006, name: '珍珠璎珞项圈', date: '08-03', price: 359, status: '已签收' },
  { id: 2007, name: '曲裾深衣·兰泽', date: '07-28', price: 1099, status: '退款中' },
  { id: 2008, name: '七夕乞巧市集门票', date: '07-22', price: 68, status: '已完成' }
];

const HN_FAVS: HnFav[] = [
  { id: 1, name: '飞鱼服·锦衣夜行', price: 1899, form: '明制' },
  { id: 2, name: '婚服·凤冠霞帔', price: 2999, form: '明制' },
  { id: 3, name: '鎏金点翠发簪', price: 269, form: '配饰' },
  { id: 4, name: '垂珠妆', price: 259, form: '妆造' },
  { id: 5, name: '圆领袍·长安少年', price: 649, form: '唐制' },
  { id: 6, name: '云肩袄·凤来仪', price: 1299, form: '清制' }
];

const HN_COUNTS: HnCount[] = [
  { name: '明制', count: 486, percent: 96 },
  { name: '唐制', count: 402, percent: 88 },
  { name: '宋制', count: 315, percent: 79 },
  { name: '清制', count: 268, percent: 72 },
  { name: '晋制', count: 154, percent: 58 },
  { name: '秦汉', count: 97, percent: 45 }
];

const HN_PROFILE: HnProfile = {
  nick: '锦衣客·阿蘅',
  level: '国风雅士 Lv.7',
  points: 3150,
  coupon: 4,
  fans: 2310,
  follows: 210
};

function hnBar(p: number): string {
  let v = p;
  if (v > 100) {
    v = 100;
  }
  return v + '%';
}

function hnPrice(p: number): string {
  return '¥' + p;
}

function hnStatusColor(s: string): string {
  if (s === '已签收' || s === '已完成') {
    return HN.success;
  }
  if (s === '运输中') {
    return HN.gold;
  }
  if (s === '退款中') {
    return HN.danger;
  }
  return HN.textHint;
}

function hnFormColor(f: string): string {
  if (f.indexOf('明') >= 0) {
    return '#B03A2E';
  }
  if (f.indexOf('唐') >= 0) {
    return '#C9A227';
  }
  if (f.indexOf('宋') >= 0) {
    return '#5B7B7C';
  }
  if (f.indexOf('清') >= 0) {
    return '#8B5E3C';
  }
  if (f.indexOf('晋') >= 0) {
    return '#7A6B9C';
  }
  return '#6E8B6E';
}

function hnIcon(idx: number): string {
  let icons = ['👘', '🌸', '🧥', '👗', '🥻', '🎋', '🀄', '🎐', '🧶', '🪶', '👘', '⚔️', '🧣', '✨', '🦚', '💍'];
  return icons[idx % icons.length];
}

@Entry
@Component
struct HanApp {
  @State activeTab: number = 0;

  @Builder
  headerBar(title: string) {
    Row() {
      Column() {
        Text(title)
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      Text('🔔')
        .fontSize(18)
        .margin({ right: 12 })
      Text('🧧')
        .fontSize(18)
        .margin({ right: 4 })
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 12, bottom: 12 })
  }

  @Builder
  bottomBar() {
    Row() {
      ForEach(HN_TABS, (tb: HnTab, ti: number) => {
        Column() {
          Text(tb.icon)
            .fontSize(this.activeTab === ti ? 20 : 17)
            .fontColor(this.activeTab === ti ? HN.red : HN.textHint)
          Text(tb.label)
            .fontSize(this.activeTab === ti ? 12 : 10)
            .fontWeight(this.activeTab === ti ? FontWeight.Bold : FontWeight.Normal)
            .fontColor(this.activeTab === ti ? HN.red : HN.textHint)
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .padding({ top: 6, bottom: 6 })
        .onClick(() => {
          this.activeTab = ti;
        })
      }, (tb: HnTab) => tb.key)
    }
    .width('100%')
    .height(58)
    .backgroundColor(HN.card)
    .border({ width: { top: 1 }, color: HN.line })
  }

  build() {
    Column() {
      if (this.activeTab === 0) {
        this.headerBar('国风汉服馆')
      } else if (this.activeTab === 1) {
        this.headerBar('汉服形制馆')
      } else if (this.activeTab === 2) {
        this.headerBar('国风配饰集')
      } else if (this.activeTab === 3) {
        this.headerBar('妆造司')
      } else if (this.activeTab === 4) {
        this.headerBar('雅集活动')
      } else {
        this.headerBar('我的衣橱')
      }

      Column() {
        if (this.activeTab === 0) {
          HanHomeTab()
        } else if (this.activeTab === 1) {
          HanItemTab()
        } else if (this.activeTab === 2) {
          HanAccTab()
        } else if (this.activeTab === 3) {
          HanStyleTab()
        } else if (this.activeTab === 4) {
          HanEventTab()
        } else {
          HanMineTab()
        }
      }
      .layoutWeight(1)

      this.bottomBar()
    }
    .width('100%')
    .height('100%')
    .backgroundColor(HN.bg)
  }
}
@Component
struct HanHomeTab {
  @State showDetail: boolean = false;
  @State selItem: number = 0;
  @State showRankTip: boolean = false;
  @State favorited: boolean = false;
  @State lanternStep: number = 0;
  @State bloomOn: boolean = false;
  timerId: number = -1;

  aboutToAppear(): void {
    this.timerId = setInterval(() => {
      this.lanternStep = (this.lanternStep + 1) % 4;
      this.bloomOn = !this.bloomOn;
    }, 900);
  }

  aboutToDisappear(): void {
    if (this.timerId >= 0) {
      clearInterval(this.timerId);
    }
  }

  @Builder
  modalOverlay() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('#66000000')
  }

  @Builder
  scrollBanner() {
    Stack() {
      Column()
        .width('100%')
        .height(150)
        .borderRadius(4)
        .linearGradient({
          angle: 0,
          colors: [[HN.red, 0], [HN.redDeep, 1]]
        })
      Row() {
        Text('❦')
          .fontSize(30)
          .fontColor('#CCFFFFFF')
        Column() {
          Text('华夏衣冠 · 形制之美')
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.white)
          Text('马面裙新品首发 · 织金工艺 8 折')
            .fontSize(12)
            .fontColor('#F0D9B0')
            .margin({ top: 6 })
          Text('限时 3 天 · 支持租借')
            .fontSize(11)
            .fontColor(HN.white)
            .padding({ left: 10, right: 10, top: 4, bottom: 4 })
            .backgroundColor('#33FFFFFF')
            .borderRadius(10)
            .margin({ top: 8 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        Text('❦')
          .fontSize(30)
          .fontColor('#CCFFFFFF')
      }
      .width('100%')
      .padding({ left: 18, right: 18 })
    }
    .width('100%')
    .height(150)
    .margin({ top: 10, left: 16, right: 16 })
  }

  @Builder
  lanternEffect() {
    Row() {
      Text('🏮')
        .fontSize(24)
        .rotate({ angle: this.lanternStep % 2 === 0 ? -6 : 6 })
        .animation({ duration: 800 })
        .margin({ left: 30 })
      Text('🏮')
        .fontSize(18)
        .rotate({ angle: this.lanternStep % 2 === 0 ? 8 : -8 })
        .animation({ duration: 900 })
        .margin({ left: 120 })
      Text('✨')
        .fontSize(14)
        .opacity(this.bloomOn ? 0.35 : 1)
        .animation({ duration: 800 })
        .margin({ left: 180 })
    }
    .width('100%')
    .height(34)
  }

  @Builder
  formRank() {
    Column() {
      Row() {
        Text('📜 形制热度榜')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)
        Column().layoutWeight(1)
        Text('规则')
          .fontSize(12)
          .fontColor(HN.gold)
          .onClick(() => {
            this.showRankTip = true;
          })
      }
      .width('100%')
      .margin({ bottom: 8 })

      ForEach(HN_COUNTS, (ct: HnCount, ci: number) => {
        Row() {
          Text(ci + 1 + '')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(ci < 3 ? HN.red : HN.textHint)
            .width(22)
          Text(ct.name + '制')
            .fontSize(13)
            .fontColor(HN.text)
            .width(58)
          Stack() {
            Row()
              .width('100%')
              .height(10)
              .backgroundColor('#EDE0CC')
              .borderRadius(5)
            Row()
              .width(hnBar(ct.percent))
              .height(10)
              .backgroundColor(ci < 3 ? HN.red : HN.gold)
              .borderRadius(5)
          }
          .layoutWeight(1)
          .height(10)

          Text(ct.count + ' 款')
            .fontSize(12)
            .fontColor(HN.textSub)
            .width(46)
            .textAlign(TextAlign.End)
        }
        .width('100%')
        .margin({ top: 8 })
      }, (ct: HnCount) => ct.name)
    }
    .width('100%')
    .padding(16)
    .backgroundColor(HN.card)
    .borderRadius(14)
    .margin({ left: 16, right: 16, top: 12 })
  }

  @Builder
  catGrid() {
    Row() {
      Column() {
        Text('👘')
          .fontSize(24)
        Text('明制')
          .fontSize(11)
          .fontColor(HN.textSub)
          .margin({ top: 4 })
      }
      .layoutWeight(1)
      .padding({ top: 10, bottom: 10 })
      .backgroundColor('#F6E4DE')
      .borderRadius(12)

      Column() {
        Text('🌸')
          .fontSize(24)
        Text('唐制')
          .fontSize(11)
          .fontColor(HN.textSub)
          .margin({ top: 4 })
      }
      .layoutWeight(1)
      .margin({ left: 8 })
      .padding({ top: 10, bottom: 10 })
      .backgroundColor('#F3ECD8')
      .borderRadius(12)

      Column() {
        Text('🎋')
          .fontSize(24)
        Text('宋制')
          .fontSize(11)
          .fontColor(HN.textSub)
          .margin({ top: 4 })
      }
      .layoutWeight(1)
      .margin({ left: 8 })
      .padding({ top: 10, bottom: 10 })
      .backgroundColor('#DEE9E6')
      .borderRadius(12)

      Column() {
        Text('💄')
          .fontSize(24)
        Text('妆造')
          .fontSize(11)
          .fontColor(HN.textSub)
          .margin({ top: 4 })
      }
      .layoutWeight(1)
      .margin({ left: 8 })
      .padding({ top: 10, bottom: 10 })
      .backgroundColor('#F6E9D8')
      .borderRadius(12)
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 12 })
  }

  @Builder
  newItemCard(it: HnItem, idx: number) {
    Row() {
      Column() {
        Text(hnIcon(idx))
          .fontSize(32)
      }
      .width(58)
      .height(58)
      .backgroundColor(idx % 2 === 0 ? '#F6E4DE' : '#DEE9E6')
      .borderRadius(12)
      .justifyContent(FlexAlign.Center)

      Column() {
        Row() {
          Text(it.name)
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.text)
          Text(it.form)
            .fontSize(10)
            .fontColor(HN.white)
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .backgroundColor(hnFormColor(it.form))
            .borderRadius(8)
            .margin({ left: 6 })
        }
        Text(it.fabric + ' · ' + it.dynasty + '制')
          .fontSize(11)
          .fontColor(HN.textSub)
          .margin({ top: 3 })
        Row() {
          Text(hnPrice(it.price))
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.danger)
          Text(hnPrice(it.oldPrice))
            .fontSize(11)
            .fontColor(HN.textHint)
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ left: 8 })
          Text('余 ' + it.stock + ' 件')
            .fontSize(10)
            .fontColor(it.stock < 10 ? HN.danger : HN.success)
            .margin({ left: 8 })
        }
        .margin({ top: 3 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 10 })
    }
    .width('100%')
    .padding(12)
    .backgroundColor(HN.card)
    .borderRadius(14)
    .margin({ left: 16, right: 16, top: 8 })
    .onClick(() => {
      this.selItem = it.id;
      this.showDetail = true;
    })
  }

  @Builder
  detailModal() {
    Column() {
      Column() {
        Text(HN_ITEMS[this.selItem - 1].name)
          .fontSize(19)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)
        Text(HN_ITEMS[this.selItem - 1].form + ' · ' + HN_ITEMS[this.selItem - 1].fabric)
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 6 })

        Row() {
          Column() {
            Text('朝代')
              .fontSize(11)
              .fontColor(HN.textHint)
            Text(HN_ITEMS[this.selItem - 1].dynasty + '制')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(HN.red)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          Column() {
            Text('库存')
              .fontSize(11)
              .fontColor(HN.textHint)
            Text(HN_ITEMS[this.selItem - 1].stock + ' 件')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(HN_ITEMS[this.selItem - 1].stock < 10 ? HN.danger : HN.success)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          Column() {
            Text('热度')
              .fontSize(11)
              .fontColor(HN.textHint)
            Text(HN_ITEMS[this.selItem - 1].percent + '%')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(HN.gold)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })
        .backgroundColor('#FBF6EC')
        .borderRadius(12)
        .margin({ top: 12 })

        Row() {
          Text('国风价')
            .fontSize(12)
            .fontColor(HN.textHint)
          Text(hnPrice(HN_ITEMS[this.selItem - 1].price))
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.danger)
            .margin({ left: 8 })
          Text(hnPrice(HN_ITEMS[this.selItem - 1].oldPrice))
            .fontSize(12)
            .fontColor(HN.textHint)
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ left: 8 })
        }
        .width('100%')
        .margin({ top: 12 })

        Row() {
          Text(this.favorited ? '♥ 已收藏' : '♡ 收藏')
            .fontSize(13)
            .fontColor(this.favorited ? HN.danger : HN.red)
            .padding({ left: 18, right: 18, top: 8, bottom: 8 })
            .border({ width: 1, color: this.favorited ? HN.danger : HN.gold })
            .borderRadius(18)
            .onClick(() => {
              this.favorited = !this.favorited;
            })
          Column().layoutWeight(1)
          Text('加入衣橱')
            .fontSize(13)
            .fontColor(HN.white)
            .padding({ left: 18, right: 18, top: 8, bottom: 8 })
            .backgroundColor(HN.red)
            .borderRadius(18)
            .onClick(() => {
              this.showDetail = false;
            })
        }
        .width('100%')
        .margin({ top: 14 })
      }
      .width('88%')
      .padding(18)
      .backgroundColor(HN.card)
      .borderRadius(16)
      .constraintSize({ maxHeight: '80%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  rankTipModal() {
    Column() {
      Column() {
        Text('📜 形制榜规则')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)
        Text('每月初一更新,按各朝代形制下在售款式的热度加权:')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 10 })
        Text('收藏 +1 · 租借预约 +2 · 购买 +5 · 晒图 +3')
          .fontSize(13)
          .fontColor(HN.red)
          .margin({ top: 8 })
        Text('榜首形制将推出「复原款」限定系列。')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 8 })
        Text('知道了')
          .fontSize(14)
          .fontColor(HN.white)
          .padding({ left: 32, right: 32, top: 9, bottom: 9 })
          .backgroundColor(HN.gold)
          .borderRadius(20)
          .margin({ top: 16 })
          .onClick(() => {
            this.showRankTip = false;
          })
      }
      .width('84%')
      .padding(20)
      .backgroundColor(HN.card)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  build() {
    Stack() {
      Column() {
        Scroll() {
          Column() {
            this.scrollBanner()
            this.lanternEffect()
            this.catGrid()
            this.formRank()

            Text('🦚 新品上新 · 甄选 16 款')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(HN.text)
              .margin({ left: 16, right: 16, top: 16 })
              .alignSelf(ItemAlign.Start)

            ForEach(HN_ITEMS, (it: HnItem, ii: number) => {
              this.newItemCard(it, ii)
            }, (it: HnItem) => it.id + '')
          }
          .width('100%')
          .padding({ bottom: 20 })
        }
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.Off)
        .width('100%')
      }
      .width('100%')
      .height('100%')

      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          this.detailModal()
        }
        .position({ x: 0, y: 0 })
        .zIndex(999)
        .width('100%')
        .height('100%')
      }

      if (this.showRankTip) {
        Stack() {
          this.modalOverlay()
          this.rankTipModal()
        }
        .position({ x: 0, y: 0 })
        .zIndex(999)
        .width('100%')
        .height('100%')
      }
    }
    .width('100%')
    .height('100%')
  }
}
function hnFilterItems(form: string): HnItem[] {
  if (form === '全部形制') {
    return HN_ITEMS;
  }
  return HN_ITEMS.filter((o: HnItem) => o.form === form || o.dynasty === form.charAt(0));
}

function hnPriceNum(s: string): number {
  let v = parseInt(s);
  if (Number.isNaN(v)) {
    return 0;
  }
  return v;
}

@Component
struct HanItemTab {
  @State curForm: string = '全部形制';
  @State items: HnItem[] = HN_ITEMS.slice();
  @State showDetail: boolean = false;
  @State showAdd: boolean = false;
  @State showDel: boolean = false;
  @State selId: number = 0;
  @State delId: number = 0;
  @State editName: string = '';
  @State editPrice: string = '';
  @State editForm: string = '明制';
  @State ribbonStep: number = 0;
  timerId: number = -1;
  formList: string[] = ['全部形制', '明制', '唐制', '宋制', '清制', '晋制', '秦汉', '改良'];

  aboutToAppear(): void {
    this.timerId = setInterval(() => {
      this.ribbonStep = (this.ribbonStep + 1) % 6;
    }, 700);
  }

  aboutToDisappear(): void {
    if (this.timerId >= 0) {
      clearInterval(this.timerId);
    }
  }

  @Builder
  modalOverlay() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('#55000000')
  }

  @Builder
  sideCatalog() {
    Scroll() {
      Column() {
        Text('形制')
          .fontSize(12)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.gold)
          .margin({ bottom: 8 })
        ForEach(this.formList, (fm: string, fi: number) => {
          Column() {
            Text(fm === '全部形制' ? '全' : fm.charAt(0))
              .fontSize(15)
              .fontWeight(this.curForm === fm ? FontWeight.Bold : FontWeight.Normal)
              .fontColor(this.curForm === fm ? HN.white : HN.textSub)
          }
          .width(52)
          .height(44)
          .backgroundColor(this.curForm === fm ? HN.red : HN.card)
          .borderRadius(10)
          .justifyContent(FlexAlign.Center)
          .margin({ top: 6 })
          .onClick(() => {
            this.curForm = fm;
          })
        }, (fm: string) => fm)
      }
      .width('100%')
      .padding({ top: 12, left: 10, right: 10, bottom: 12 })
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .width(74)
    .height('100%')
    .backgroundColor('#F1E8D6')
  }

  @Builder
  ribbonEffect() {
    Row() {
      Text('🎗️')
        .fontSize(12 + (this.ribbonStep % 3) * 2)
        .opacity(0.8)
        .margin({ left: 20 + this.ribbonStep * 16 })
        .animation({ duration: 600 })
      Text('🎗️')
        .fontSize(11)
        .opacity(0.6)
        .margin({ left: 40 })
        .animation({ duration: 600 })
    }
    .width('100%')
    .height(26)
  }

  @Builder
  listCard(it: HnItem, idx: number) {
    Row() {
      Column() {
        Text(hnIcon(idx))
          .fontSize(30)
      }
      .width(56)
      .height(56)
      .backgroundColor(idx % 2 === 0 ? '#F6E4DE' : '#DEE9E6')
      .borderRadius(12)
      .justifyContent(FlexAlign.Center)

      Column() {
        Row() {
          Text(it.name)
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.text)
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
          Text(it.form)
            .fontSize(10)
            .fontColor(HN.white)
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .backgroundColor(hnFormColor(it.form))
            .borderRadius(8)
            .margin({ left: 6 })
        }
        Text(it.fabric + ' · ' + it.dynasty + '制 · 余' + it.stock + '件')
          .fontSize(11)
          .fontColor(HN.textSub)
          .margin({ top: 3 })
        Row() {
          Text(hnPrice(it.price))
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.danger)
          Text(hnPrice(it.oldPrice))
            .fontSize(10)
            .fontColor(HN.textHint)
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ left: 6 })
        }
        .margin({ top: 3 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 10 })
    }
    .width('100%')
    .padding(12)
    .backgroundColor(HN.card)
    .borderRadius(14)
    .margin({ left: 10, right: 10, top: 8 })
    .onClick(() => {
      this.selId = it.id;
      this.editName = it.name;
      this.editPrice = it.price + '';
      this.editForm = it.form;
      this.showDetail = true;
    })
  }

  @Builder
  detailModal() {
    Column() {
      Column() {
        Text('✏️ 形制档案')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)

        Text('款式名称')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 12 })
          .alignSelf(ItemAlign.Start)
        TextInput({ placeholder: '款式名称', text: this.editName })
          .fontSize(13)
          .height(40)
          .backgroundColor('#FBF6EC')
          .borderRadius(10)
          .onChange((v: string) => {
            this.editName = v;
          })

        Text('价格(元)')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 10 })
          .alignSelf(ItemAlign.Start)
        TextInput({ placeholder: '价格', text: this.editPrice })
          .fontSize(13)
          .height(40)
          .backgroundColor('#FBF6EC')
          .borderRadius(10)
          .onChange((v: string) => {
            this.editPrice = v;
          })

        Text('所属形制')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 10 })
          .alignSelf(ItemAlign.Start)
        Row() {
          ForEach(['明制', '唐制', '宋制', '清制'], (fm: string, fi: number) => {
            Text(fm)
              .fontSize(11)
              .fontColor(this.editForm.indexOf(fm) >= 0 ? HN.white : HN.textSub)
              .padding({ left: 12, right: 12, top: 5, bottom: 5 })
              .backgroundColor(this.editForm.indexOf(fm) >= 0 ? HN.red : '#FBF6EC')
              .borderRadius(12)
              .margin({ right: 8 })
              .onClick(() => {
                this.editForm = fm;
              })
          }, (fm: string) => fm)
        }
        .width('100%')
        .margin({ top: 6 })

        Row() {
          Text('删除')
            .fontSize(13)
            .fontColor(HN.danger)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .border({ width: 1, color: HN.danger })
            .borderRadius(18)
            .onClick(() => {
              this.delId = this.selId;
              this.showDetail = false;
              this.showDel = true;
            })
          Column().layoutWeight(1)
          Text('保存修改')
            .fontSize(13)
            .fontColor(HN.white)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .backgroundColor(HN.red)
            .borderRadius(18)
            .onClick(() => {
              this.items = this.items.map((o: HnItem) => {
                if (o.id === this.selId) {
                  let np = hnPriceNum(this.editPrice);
                  return { id: o.id, name: this.editName, form: this.editForm, price: np, oldPrice: o.oldPrice, fabric: o.fabric, dynasty: o.dynasty, stock: o.stock, percent: o.percent };
                }
                return o;
              });
              this.showDetail = false;
            })
        }
        .width('100%')
        .margin({ top: 16 })
      }
      .width('86%')
      .padding(18)
      .backgroundColor(HN.card)
      .borderRadius(16)
      .constraintSize({ maxHeight: '82%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  addModal() {
    Column() {
      Column() {
        Text('🪡 上新汉服')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)

        Text('款式名称')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 12 })
          .alignSelf(ItemAlign.Start)
        TextInput({ placeholder: '例如:月白褙子·听雨' })
          .fontSize(13)
          .height(40)
          .backgroundColor('#FBF6EC')
          .borderRadius(10)
          .onChange((v: string) => {
            this.editName = v;
          })

        Row() {
          Column() {
            Text('价格')
              .fontSize(12)
              .fontColor(HN.textSub)
              .margin({ bottom: 6 })
              .alignSelf(ItemAlign.Start)
            TextInput({ placeholder: '0', text: this.editPrice })
              .fontSize(13)
              .height(40)
              .backgroundColor('#FBF6EC')
              .borderRadius(10)
              .onChange((v: string) => {
                this.editPrice = v;
              })
          }
          .layoutWeight(1)
          Column() {
            Text('形制')
              .fontSize(12)
              .fontColor(HN.textSub)
              .margin({ bottom: 6 })
              .alignSelf(ItemAlign.Start)
            Text(this.editForm)
              .fontSize(13)
              .fontColor(HN.red)
              .height(40)
              .width('100%')
              .backgroundColor('#FBF6EC')
              .borderRadius(10)
              .textAlign(TextAlign.Center)
              .onClick(() => {
                let forms = ['明制', '唐制', '宋制', '清制', '晋制', '秦汉', '改良'];
                let now = forms.indexOf(this.editForm);
                this.editForm = forms[(now + 1) % forms.length];
              })
          }
          .layoutWeight(1)
          .margin({ left: 10 })
        }
        .width('100%')
        .margin({ top: 10 })

        Row() {
          Text('取消')
            .fontSize(13)
            .fontColor(HN.textSub)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .border({ width: 1, color: HN.line })
            .borderRadius(18)
            .onClick(() => {
              this.showAdd = false;
            })
          Column().layoutWeight(1)
          Text('确认上新')
            .fontSize(13)
            .fontColor(HN.white)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .backgroundColor(HN.gold)
            .borderRadius(18)
            .onClick(() => {
              let newId = this.items.length + 200;
              let np = hnPriceNum(this.editPrice);
              this.items.push({ id: newId, name: this.editName === '' ? '新制汉服' : this.editName, form: this.editForm, price: np, oldPrice: np, fabric: '待定面料', dynasty: this.editForm.charAt(0), stock: 20, percent: 80 });
              this.items = this.items.slice();
              this.showAdd = false;
              this.editName = '';
              this.editPrice = '';
            })
        }
        .width('100%')
        .margin({ top: 16 })
      }
      .width('86%')
      .padding(18)
      .backgroundColor(HN.card)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  delModal() {
    Column() {
      Column() {
        Text('🗑️ 下架汉服')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.danger)
        Text('确定下架「' + this.getItemName(this.delId) + '」吗?')
          .fontSize(13)
          .fontColor(HN.textSub)
          .margin({ top: 10 })

        Row() {
          Text('再想想')
            .fontSize(13)
            .fontColor(HN.textSub)
            .padding({ left: 22, right: 22, top: 8, bottom: 8 })
            .border({ width: 1, color: HN.line })
            .borderRadius(18)
            .onClick(() => {
              this.showDel = false;
            })
          Column().layoutWeight(1)
          Text('确认下架')
            .fontSize(13)
            .fontColor(HN.white)
            .padding({ left: 22, right: 22, top: 8, bottom: 8 })
            .backgroundColor(HN.danger)
            .borderRadius(18)
            .onClick(() => {
              this.items = this.items.filter((o: HnItem) => o.id !== this.delId);
              this.showDel = false;
            })
        }
        .width('100%')
        .margin({ top: 18 })
      }
      .width('84%')
      .padding(20)
      .backgroundColor(HN.card)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  getItemName(id: number): string {
    for (let i = 0; i < this.items.length; i++) {
      if (this.items[i].id === id) {
        return this.items[i].name;
      }
    }
    return '';
  }

  @Builder
  floatAdd() {
    Column() {
      Text('🪡')
        .fontSize(22)
        .fontColor(HN.white)
    }
    .width(52)
    .height(52)
    .backgroundColor(HN.red)
    .borderRadius(26)
    .justifyContent(FlexAlign.Center)
    .position({ x: 320, y: 640 })
    .shadow({ radius: 12, color: '#66B03A2E', offsetY: 4 })
    .onClick(() => {
      this.showAdd = true;
    })
  }

  build() {
    Stack() {
      Row() {
        this.sideCatalog()

        Column() {
          this.ribbonEffect()

          Scroll() {
            Column() {
              Row() {
                Text('「' + this.curForm + '」款式 · ' + hnFilterItems(this.curForm).length + ' 件')
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(HN.text)
                Column().layoutWeight(1)
                Text('❦ 手工缝制')
                  .fontSize(10)
                  .fontColor(HN.gold)
              }
              .width('100%')
              .padding({ left: 10, right: 10 })
              .margin({ top: 8 })

              ForEach(hnFilterItems(this.curForm), (it: HnItem, ii: number) => {
                this.listCard(it, ii)
              }, (it: HnItem) => it.id + '')
            }
            .width('100%')
            .padding({ bottom: 80 })
          }
          .scrollable(ScrollDirection.Vertical)
          .scrollBar(BarState.Off)
          .layoutWeight(1)
        }
        .layoutWeight(1)
        .height('100%')
      }
      .width('100%')
      .height('100%')

      this.floatAdd()

      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          this.detailModal()
        }
        .position({ x: 0, y: 0 })
        .zIndex(999)
        .width('100%')
        .height('100%')
      }

      if (this.showAdd) {
        Stack() {
          this.modalOverlay()
          this.addModal()
        }
        .position({ x: 0, y: 0 })
        .zIndex(999)
        .width('100%')
        .height('100%')
      }

      if (this.showDel) {
        Stack() {
          this.modalOverlay()
          this.delModal()
        }
        .position({ x: 0, y: 0 })
        .zIndex(999)
        .width('100%')
        .height('100%')
      }
    }
    .width('100%')
    .height('100%')
  }
}
function hnAccIcon(idx: number): string {
  let icons = ['🌸', '🪭', '📿', '🦋', '🎗️', '🥿', '🎐', '💮'];
  return icons[idx % icons.length];
}

@Component
struct HanAccTab {
  @State accs: HnAcc[] = HN_ACCS.slice();
  @State showDetail: boolean = false;
  @State showAdd: boolean = false;
  @State showDel: boolean = false;
  @State selId: number = 0;
  @State delId: number = 0;
  @State editName: string = '';
  @State editMaterial: string = '';
  @State fanAngle: number = 0;
  @State fanOpen: boolean = false;
  timerId: number = -1;

  aboutToAppear(): void {
    this.timerId = setInterval(() => {
      this.fanOpen = !this.fanOpen;
      this.fanAngle = this.fanOpen ? 18 : 0;
    }, 1400);
  }

  aboutToDisappear(): void {
    if (this.timerId >= 0) {
      clearInterval(this.timerId);
    }
  }

  @Builder
  modalOverlay() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('#55000000')
  }

  @Builder
  fanHero() {
    Stack() {
      Column()
        .width('100%')
        .height(140)
        .borderRadius(4)
        .linearGradient({
          angle: 90,
          colors: [[HN.teal, 0], [HN.red, 1]]
        })
      Row() {
        Column() {
          Text('国风配饰集')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.white)
          Text('一件配饰 · 一身风骨')
            .fontSize(12)
            .fontColor('#F0D9B0')
            .margin({ top: 6 })
          Text('苏绣手作 · 每件独一')
            .fontSize(11)
            .fontColor(HN.white)
            .padding({ left: 10, right: 10, top: 4, bottom: 4 })
            .backgroundColor('#33FFFFFF')
            .borderRadius(10)
            .margin({ top: 8 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Column() {
          Text('🪭')
            .fontSize(56)
            .rotate({ angle: this.fanAngle })
            .animation({ duration: 1000 })
          Text('团扇开合')
            .fontSize(10)
            .fontColor('#F0D9B0')
            .margin({ top: 2 })
        }
        .justifyContent(FlexAlign.Center)
        .margin({ right: 22 })
      }
      .width('100%')
      .padding({ left: 20 })
    }
    .width('100%')
    .height(140)
    .margin({ top: 10, left: 16, right: 16 })
  }

  @Builder
  accCard(ac: HnAcc, idx: number) {
    Column() {
      Column() {
        Text(hnAccIcon(idx))
          .fontSize(32)
      }
      .width('100%')
      .height(96)
      .backgroundColor(idx % 2 === 0 ? '#F6E4DE' : '#DEE9E6')
      .borderRadius(12)
      .justifyContent(FlexAlign.Center)

      Text(ac.name)
        .fontSize(13)
        .fontWeight(FontWeight.Bold)
        .fontColor(HN.text)
        .margin({ top: 7 })
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
      Text(ac.kind + ' · ' + ac.material)
        .fontSize(10)
        .fontColor(HN.textSub)
        .margin({ top: 2 })
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
      Row() {
        Text(hnPrice(ac.price))
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.danger)
        Column().layoutWeight(1)
        Text(ac.percent + ' 分')
          .fontSize(10)
          .fontColor(HN.gold)
      }
      .width('100%')
      .margin({ top: 4 })
    }
    .width('100%')
    .padding(10)
    .backgroundColor(HN.card)
    .borderRadius(14)
    .onClick(() => {
      this.selId = ac.id;
      this.editName = ac.name;
      this.editMaterial = ac.material;
      this.showDetail = true;
    })
  }

  @Builder
  detailModal() {
    Column() {
      Column() {
        Text('🪭 配饰档案')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)

        Text('配饰名称')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 12 })
          .alignSelf(ItemAlign.Start)
        TextInput({ placeholder: '配饰名称', text: this.editName })
          .fontSize(13)
          .height(40)
          .backgroundColor('#FBF6EC')
          .borderRadius(10)
          .onChange((v: string) => {
            this.editName = v;
          })

        Text('材质工艺')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 10 })
          .alignSelf(ItemAlign.Start)
        TextInput({ placeholder: '材质工艺', text: this.editMaterial })
          .fontSize(13)
          .height(40)
          .backgroundColor('#FBF6EC')
          .borderRadius(10)
          .onChange((v: string) => {
            this.editMaterial = v;
          })

        Row() {
          Column() {
            Text('品类')
              .fontSize(11)
              .fontColor(HN.textHint)
            Text(this.getAccKind(this.selId))
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(HN.red)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          Column() {
            Text('匠人评分')
              .fontSize(11)
              .fontColor(HN.textHint)
            Text(this.getAccPercent(this.selId) + ' 分')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(HN.gold)
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .padding({ top: 10, bottom: 10 })
        .backgroundColor('#FBF6EC')
        .borderRadius(12)
        .margin({ top: 12 })

        Row() {
          Text('删除')
            .fontSize(13)
            .fontColor(HN.danger)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .border({ width: 1, color: HN.danger })
            .borderRadius(18)
            .onClick(() => {
              this.delId = this.selId;
              this.showDetail = false;
              this.showDel = true;
            })
          Column().layoutWeight(1)
          Text('保存修改')
            .fontSize(13)
            .fontColor(HN.white)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .backgroundColor(HN.red)
            .borderRadius(18)
            .onClick(() => {
              this.accs = this.accs.map((o: HnAcc) => {
                if (o.id === this.selId) {
                  return { id: o.id, name: this.editName, kind: o.kind, price: o.price, material: this.editMaterial, percent: o.percent };
                }
                return o;
              });
              this.showDetail = false;
            })
        }
        .width('100%')
        .margin({ top: 16 })
      }
      .width('86%')
      .padding(18)
      .backgroundColor(HN.card)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  addModal() {
    Column() {
      Column() {
        Text('🌸 上新配饰')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)

        Text('配饰名称')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 12 })
          .alignSelf(ItemAlign.Start)
        TextInput({ placeholder: '例如:缠花发钗' })
          .fontSize(13)
          .height(40)
          .backgroundColor('#FBF6EC')
          .borderRadius(10)
          .onChange((v: string) => {
            this.editName = v;
          })

        Text('材质工艺')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 10 })
          .alignSelf(ItemAlign.Start)
        TextInput({ placeholder: '例如:蚕丝缠花' })
          .fontSize(13)
          .height(40)
          .backgroundColor('#FBF6EC')
          .borderRadius(10)
          .onChange((v: string) => {
            this.editMaterial = v;
          })

        Row() {
          Text('取消')
            .fontSize(13)
            .fontColor(HN.textSub)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .border({ width: 1, color: HN.line })
            .borderRadius(18)
            .onClick(() => {
              this.showAdd = false;
            })
          Column().layoutWeight(1)
          Text('确认上新')
            .fontSize(13)
            .fontColor(HN.white)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .backgroundColor(HN.gold)
            .borderRadius(18)
            .onClick(() => {
              let nid = this.accs.length + 30;
              this.accs.push({ id: nid, name: this.editName === '' ? '新配饰' : this.editName, kind: '手作', price: 129, material: this.editMaterial === '' ? '待定' : this.editMaterial, percent: 85 });
              this.accs = this.accs.slice();
              this.showAdd = false;
              this.editName = '';
              this.editMaterial = '';
            })
        }
        .width('100%')
        .margin({ top: 16 })
      }
      .width('86%')
      .padding(18)
      .backgroundColor(HN.card)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  delModal() {
    Column() {
      Column() {
        Text('🗑️ 移除配饰')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.danger)
        Text('确定移除「' + this.getAccName(this.delId) + '」?')
          .fontSize(13)
          .fontColor(HN.textSub)
          .margin({ top: 10 })

        Row() {
          Text('取消')
            .fontSize(13)
            .fontColor(HN.textSub)
            .padding({ left: 22, right: 22, top: 8, bottom: 8 })
            .border({ width: 1, color: HN.line })
            .borderRadius(18)
            .onClick(() => {
              this.showDel = false;
            })
          Column().layoutWeight(1)
          Text('确认移除')
            .fontSize(13)
            .fontColor(HN.white)
            .padding({ left: 22, right: 22, top: 8, bottom: 8 })
            .backgroundColor(HN.danger)
            .borderRadius(18)
            .onClick(() => {
              this.accs = this.accs.filter((o: HnAcc) => o.id !== this.delId);
              this.showDel = false;
            })
        }
        .width('100%')
        .margin({ top: 18 })
      }
      .width('84%')
      .padding(20)
      .backgroundColor(HN.card)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  getAccName(id: number): string {
    for (let i = 0; i < this.accs.length; i++) {
      if (this.accs[i].id === id) {
        return this.accs[i].name;
      }
    }
    return '';
  }

  getAccKind(id: number): string {
    for (let i = 0; i < this.accs.length; i++) {
      if (this.accs[i].id === id) {
        return this.accs[i].kind;
      }
    }
    return '';
  }

  getAccPercent(id: number): number {
    for (let i = 0; i < this.accs.length; i++) {
      if (this.accs[i].id === id) {
        return this.accs[i].percent;
      }
    }
    return 0;
  }

  @Builder
  floatAdd() {
    Column() {
      Text('🌸')
        .fontSize(22)
        .fontColor(HN.white)
    }
    .width(52)
    .height(52)
    .backgroundColor(HN.teal)
    .borderRadius(26)
    .justifyContent(FlexAlign.Center)
    .position({ x: 320, y: 640 })
    .shadow({ radius: 12, color: '#662F4B4C', offsetY: 4 })
    .onClick(() => {
      this.showAdd = true;
    })
  }

  build() {
    Stack() {
      Column() {
        Scroll() {
          Column() {
            this.fanHero()

            Text('🌸 配饰陈列 · ' + this.accs.length + ' 件')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor(HN.text)
              .margin({ left: 16, right: 16, top: 14 })
              .alignSelf(ItemAlign.Start)

            Grid() {
              ForEach(this.accs, (ac: HnAcc, ai: number) => {
                GridItem() {
                  this.accCard(ac, ai)
                }
              }, (ac: HnAcc) => ac.id + '')
            }
            .columnsTemplate('1fr 1fr 1fr')
            .columnsGap(8)
            .rowsGap(10)
            .width('100%')
            .padding({ left: 16, right: 16, top: 4 })

            Text('💡 匠人贴士:真丝配饰请避免暴晒与机洗,可用香樟木箱保存。')
              .fontSize(11)
              .fontColor(HN.gold)
              .padding({ left: 14, right: 14, top: 10, bottom: 10 })
              .margin({ left: 16, right: 16, top: 12 })
              .backgroundColor('#FBF6EC')
              .borderRadius(12)
              .width('90%')
          }
          .width('100%')
          .padding({ bottom: 80 })
        }
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.Off)
        .width('100%')
      }
      .width('100%')
      .height('100%')

      this.floatAdd()

      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          this.detailModal()
        }
        .position({ x: 0, y: 0 })
        .zIndex(999)
        .width('100%')
        .height('100%')
      }

      if (this.showAdd) {
        Stack() {
          this.modalOverlay()
          this.addModal()
        }
        .position({ x: 0, y: 0 })
        .zIndex(999)
        .width('100%')
        .height('100%')
      }

      if (this.showDel) {
        Stack() {
          this.modalOverlay()
          this.delModal()
        }
        .position({ x: 0, y: 0 })
        .zIndex(999)
        .width('100%')
        .height('100%')
      }
    }
    .width('100%')
    .height('100%')
  }
}
@Component
struct HanStyleTab {
  @State styles: HnStyle[] = HN_STYLES.slice();
  @State bookings: string[] = ['花钿妆', '桃花妆'];
  @State showDetail: boolean = false;
  @State showBook: boolean = false;
  @State showCancel: boolean = false;
  @State selId: number = 0;
  @State cancelName: string = '';
  @State editDesc: string = '';
  @State curDate: string = '今日';
  @State curTime: string = '14:00';
  @State blossomOn: boolean = false;
  timerId: number = -1;
  dateList: string[] = ['今日', '明日', '周四', '周五', '周六', '周日'];
  timeList: string[] = ['10:00', '11:30', '14:00', '15:30', '17:00', '19:00'];

  aboutToAppear(): void {
    this.timerId = setInterval(() => {
      this.blossomOn = !this.blossomOn;
    }, 800);
  }

  aboutToDisappear(): void {
    if (this.timerId >= 0) {
      clearInterval(this.timerId);
    }
  }

  @Builder
  modalOverlay() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('#55000000')
  }

  @Builder
  dateBar() {
    Scroll() {
      Row() {
        ForEach(this.dateList, (dt: string, di: number) => {
          Column() {
            Text(dt)
              .fontSize(12)
              .fontColor(this.curDate === dt ? HN.white : HN.textSub)
            Text('·')
              .fontSize(10)
              .fontColor(this.curDate === dt ? '#EED9B0' : HN.textHint)
              .margin({ top: 2 })
          }
          .width(62)
          .height(48)
          .backgroundColor(this.curDate === dt ? HN.red : HN.card)
          .borderRadius(12)
          .justifyContent(FlexAlign.Center)
          .margin({ right: 8 })
          .onClick(() => {
            this.curDate = dt;
          })
        }, (dt: string) => dt)
      }
      .width('100%')
    }
    .scrollable(ScrollDirection.Horizontal)
    .scrollBar(BarState.Off)
    .width('100%')
    .padding({ left: 16, right: 16, top: 10 })
  }

  @Builder
  styleCard(st: HnStyle, idx: number) {
    Row() {
      Column() {
        Text('💄')
          .fontSize(30)
      }
      .width(58)
      .height(58)
      .backgroundColor(idx % 2 === 0 ? '#F6E4DE' : '#F3ECD8')
      .borderRadius(12)
      .justifyContent(FlexAlign.Center)

      Column() {
        Row() {
          Text(st.name)
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.text)
          Text(st.dur)
            .fontSize(10)
            .fontColor(HN.white)
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .backgroundColor(HN.teal)
            .borderRadius(8)
            .margin({ left: 6 })
        }
        Text(st.desc)
          .fontSize(11)
          .fontColor(HN.textSub)
          .margin({ top: 3 })
        Row() {
          Text('妆造师:' + st.master)
            .fontSize(11)
            .fontColor(HN.gold)
          Text(hnPrice(st.price))
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.danger)
            .margin({ left: 10 })
        }
        .margin({ top: 3 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 10 })

      Text(this.bookings.indexOf(st.name) >= 0 ? '已预约 ✓' : '预约')
        .fontSize(11)
        .fontColor(this.bookings.indexOf(st.name) >= 0 ? HN.success : HN.white)
        .padding({ left: 12, right: 12, top: 6, bottom: 6 })
        .backgroundColor(this.bookings.indexOf(st.name) >= 0 ? '#E8F0E8' : HN.red)
        .borderRadius(14)
        .onClick(() => {
          if (this.bookings.indexOf(st.name) >= 0) {
            this.cancelName = st.name;
            this.showCancel = true;
          } else {
            this.selId = st.id;
            this.showBook = true;
          }
        })
    }
    .width('100%')
    .padding(12)
    .backgroundColor(HN.card)
    .borderRadius(14)
    .margin({ left: 16, right: 16, top: 10 })
    .onClick(() => {
      this.selId = st.id;
      this.editDesc = st.desc;
      this.showDetail = true;
    })
  }

  @Builder
  detailModal() {
    Column() {
      Column() {
        Text('💄 妆造档案')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)

        Text('妆造描述')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 12 })
          .alignSelf(ItemAlign.Start)
        TextInput({ placeholder: '妆造描述', text: this.editDesc })
          .fontSize(13)
          .height(40)
          .backgroundColor('#FBF6EC')
          .borderRadius(10)
          .onChange((v: string) => {
            this.editDesc = v;
          })

        Row() {
          Column() {
            Text('妆造师')
              .fontSize(11)
              .fontColor(HN.textHint)
            Text(this.getStyleMaster(this.selId))
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(HN.red)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          Column() {
            Text('耗时')
              .fontSize(11)
              .fontColor(HN.textHint)
            Text(this.getStyleDur(this.selId))
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(HN.teal)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          Column() {
            Text('价格')
              .fontSize(11)
              .fontColor(HN.textHint)
            Text(hnPrice(this.getStylePrice(this.selId)))
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(HN.danger)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .padding({ top: 10, bottom: 10 })
        .backgroundColor('#FBF6EC')
        .borderRadius(12)
        .margin({ top: 12 })

        Row() {
          Text('保存')
            .fontSize(13)
            .fontColor(HN.white)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .backgroundColor(HN.red)
            .borderRadius(18)
            .onClick(() => {
              this.styles = this.styles.map((o: HnStyle) => {
                if (o.id === this.selId) {
                  return { id: o.id, name: o.name, desc: this.editDesc, price: o.price, dur: o.dur, master: o.master, percent: o.percent };
                }
                return o;
              });
              this.showDetail = false;
            })
          Column().layoutWeight(1)
          Text('关闭')
            .fontSize(13)
            .fontColor(HN.textSub)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .border({ width: 1, color: HN.line })
            .borderRadius(18)
            .onClick(() => {
              this.showDetail = false;
            })
        }
        .width('100%')
        .margin({ top: 16 })
      }
      .width('86%')
      .padding(18)
      .backgroundColor(HN.card)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  bookModal() {
    Column() {
      Column() {
        Text('📅 预约妆造')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)
        Text('「' + this.getStyleName(this.selId) + '」 · ' + this.getStyleMaster(this.selId))
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 6 })

        Text('选择日期')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 12 })
          .alignSelf(ItemAlign.Start)
        Row() {
          ForEach(this.dateList, (dt: string, di: number) => {
            Text(dt)
              .fontSize(11)
              .fontColor(this.curDate === dt ? HN.white : HN.textSub)
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .backgroundColor(this.curDate === dt ? HN.red : '#FBF6EC')
              .borderRadius(12)
              .margin({ right: 6 })
              .onClick(() => {
                this.curDate = dt;
              })
          }, (dt: string) => dt)
        }
        .width('100%')
        .margin({ top: 6 })

        Text('选择时段')
          .fontSize(12)
          .fontColor(HN.textSub)
          .margin({ top: 10 })
          .alignSelf(ItemAlign.Start)
        Row() {
          ForEach(this.timeList, (tm: string, ti: number) => {
            Text(tm)
              .fontSize(11)
              .fontColor(this.curTime === tm ? HN.white : HN.textSub)
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .backgroundColor(this.curTime === tm ? HN.teal : '#FBF6EC')
              .borderRadius(12)
              .margin({ right: 6 })
              .onClick(() => {
                this.curTime = tm;
              })
          }, (tm: string) => tm)
        }
        .width('100%')
        .margin({ top: 6 })

        Row() {
          Text('取消')
            .fontSize(13)
            .fontColor(HN.textSub)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .border({ width: 1, color: HN.line })
            .borderRadius(18)
            .onClick(() => {
              this.showBook = false;
            })
          Column().layoutWeight(1)
          Text('确认预约')
            .fontSize(13)
            .fontColor(HN.white)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .backgroundColor(HN.gold)
            .borderRadius(18)
            .onClick(() => {
              this.bookings.push(this.getStyleName(this.selId));
              this.showBook = false;
            })
        }
        .width('100%')
        .margin({ top: 16 })
      }
      .width('86%')
      .padding(18)
      .backgroundColor(HN.card)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  cancelModal() {
    Column() {
      Column() {
        Text('🗑️ 取消预约')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.danger)
        Text('确定取消「' + this.cancelName + '」的预约吗?')
          .fontSize(13)
          .fontColor(HN.textSub)
          .margin({ top: 10 })

        Row() {
          Text('保留预约')
            .fontSize(13)
            .fontColor(HN.textSub)
            .padding({ left: 22, right: 22, top: 8, bottom: 8 })
            .border({ width: 1, color: HN.line })
            .borderRadius(18)
            .onClick(() => {
              this.showCancel = false;
            })
          Column().layoutWeight(1)
          Text('确认取消')
            .fontSize(13)
            .fontColor(HN.white)
            .padding({ left: 22, right: 22, top: 8, bottom: 8 })
            .backgroundColor(HN.danger)
            .borderRadius(18)
            .onClick(() => {
              this.bookings = this.bookings.filter((o: string) => o !== this.cancelName);
              this.showCancel = false;
            })
        }
        .width('100%')
        .margin({ top: 18 })
      }
      .width('84%')
      .padding(20)
      .backgroundColor(HN.card)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  getStyleName(id: number): string {
    for (let i = 0; i < this.styles.length; i++) {
      if (this.styles[i].id === id) {
        return this.styles[i].name;
      }
    }
    return '';
  }

  getStyleMaster(id: number): string {
    for (let i = 0; i < this.styles.length; i++) {
      if (this.styles[i].id === id) {
        return this.styles[i].master;
      }
    }
    return '';
  }

  getStyleDur(id: number): string {
    for (let i = 0; i < this.styles.length; i++) {
      if (this.styles[i].id === id) {
        return this.styles[i].dur;
      }
    }
    return '';
  }

  getStylePrice(id: number): number {
    for (let i = 0; i < this.styles.length; i++) {
      if (this.styles[i].id === id) {
        return this.styles[i].price;
      }
    }
    return 0;
  }

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

        Scroll() {
          Column() {
            Stack() {
              Column()
                .width('100%')
                .height(96)
                .borderRadius(14)
                .linearGradient({
                  angle: 90,
                  colors: [[HN.teal, 0], [HN.red, 1]]
                })
              Row() {
                Column() {
                  Text('💄 妆造司')
                    .fontSize(18)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(HN.white)
                  Text('已预约 ' + this.bookings.length + ' 项 · ' + this.curDate + ' ' + this.curTime)
                    .fontSize(11)
                    .fontColor('#F0D9B0')
                    .margin({ top: 5 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                Text('💮')
                  .fontSize(34)
                  .opacity(this.blossomOn ? 0.55 : 1)
                  .animation({ duration: 700 })
                  .margin({ right: 20 })
              }
              .width('100%')
              .padding({ left: 18 })
            }
            .width('100%')
            .height(96)
            .margin({ left: 16, right: 16, top: 10 })

            Text('🌸 可选妆造 · ' + this.styles.length + ' 款')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor(HN.text)
              .margin({ left: 16, right: 16, top: 14 })
              .alignSelf(ItemAlign.Start)

            ForEach(this.styles, (st: HnStyle, si: number) => {
              this.styleCard(st, si)
            }, (st: HnStyle) => st.id + '')
          }
          .width('100%')
          .padding({ bottom: 24 })
        }
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.Off)
        .width('100%')
      }
      .width('100%')
      .height('100%')

      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          this.detailModal()
        }
        .position({ x: 0, y: 0 })
        .zIndex(999)
        .width('100%')
        .height('100%')
      }

      if (this.showBook) {
        Stack() {
          this.modalOverlay()
          this.bookModal()
        }
        .position({ x: 0, y: 0 })
        .zIndex(999)
        .width('100%')
        .height('100%')
      }

      if (this.showCancel) {
        Stack() {
          this.modalOverlay()
          this.cancelModal()
        }
        .position({ x: 0, y: 0 })
        .zIndex(999)
        .width('100%')
        .height('100%')
      }
    }
    .width('100%')
    .height('100%')
  }
}
@Component
struct HanEventTab {
  @State events: HnEvent[] = HN_EVENTS.slice();
  @State joined: number[] = [1, 5];
  @State showDetail: boolean = false;
  @State showJoin: boolean = false;
  @State selId: number = 0;
  @State petalStep: number = 0;
  timerId: number = -1;

  aboutToAppear(): void {
    this.timerId = setInterval(() => {
      this.petalStep = (this.petalStep + 1) % 6;
    }, 800);
  }

  aboutToDisappear(): void {
    if (this.timerId >= 0) {
      clearInterval(this.timerId);
    }
  }

  @Builder
  modalOverlay() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('#55000000')
  }

  @Builder
  petalEffect() {
    Stack() {
      Text('🌸')
        .fontSize(12)
        .opacity(0.9 - (this.petalStep % 3) * 0.2)
        .position({ x: 40 + (this.petalStep * 22) % 120, y: 10 + (this.petalStep * 9) % 40 })
        .animation({ duration: 700 })
      Text('🌺')
        .fontSize(10)
        .opacity(0.7)
        .position({ x: 200 + (this.petalStep * 15) % 90, y: 5 + (this.petalStep * 11) % 45 })
        .animation({ duration: 700 })
      Text('🌸')
        .fontSize(9)
        .opacity(0.6)
        .position({ x: 300, y: 15 + (this.petalStep * 13) % 35 })
        .animation({ duration: 700 })
    }
    .width('100%')
    .height(60)
  }

  @Builder
  eventCard(ev: HnEvent, idx: number) {
    Row() {
      Column() {
        Text(idx + 1 + '')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.joined.indexOf(ev.id) >= 0 ? HN.success : HN.gold)
        Text(ev.date.indexOf('农历') >= 0 ? '节令' : '日常')
          .fontSize(9)
          .fontColor(HN.textHint)
          .margin({ top: 2 })
      }
      .width(44)
      .alignItems(HorizontalAlign.Center)
      .margin({ top: 6 })

      Column() {
        Row() {
          Text(ev.name)
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(HN.text)
          Text(this.joined.indexOf(ev.id) >= 0 ? '已报名' : '热招中')
            .fontSize(10)
            .fontColor(this.joined.indexOf(ev.id) >= 0 ? HN.success : HN.white)
            .padding({ left: 8, right: 8, top: 2, bottom: 2 })
            .backgroundColor(this.joined.indexOf(ev.id) >= 0 ? '#E8F0E8' : HN.red)
            .borderRadius(8)
            .margin({ left: 8 })
        }
        Row() {
          Text('📅 ' + ev.date)
            .fontSize(11)
            .fontColor(HN.textSub)
          Text('📍 ' + ev.place)
            .fontSize(11)
            .fontColor(HN.textSub)
            .margin({ left: 12 })
        }
        .margin({ top: 5 })

        Stack() {
          Row()
            .width('100%')
            .height(8)
            .backgroundColor('#EDE0CC')
            .borderRadius(4)
          Row()
            .width(hnBar(ev.percent))
            .height(8)
            .backgroundColor(this.joined.indexOf(ev.id) >= 0 ? HN.success : HN.gold)
            .borderRadius(4)
        }
        .width('100%')
        .height(8)
        .margin({ top: 8 })

        Row() {
          Text('已报名 ' + ev.sign + '/' + ev.cap + ' 人')
            .fontSize(10)
            .fontColor(HN.textHint)
          Column().layoutWeight(1)
          Text('详情')
            .fontSize(11)
            .fontColor(HN.gold)
            .onClick(() => {
              this.selId = ev.id;
              this.showDetail = true;
            })
        }
        .width('100%')
        .margin({ top: 6 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .padding({ left: 10, bottom: 6 })
      .border({ width: { left: 1 }, color: HN.line })
    }
    .width('100%')
    .padding({ top: 4, bottom: 4, right: 14 })
    .margin({ left: 16, right: 16, top: 6 })
  }

  @Builder
  detailModal() {
    Column() {
      Column() {
        Text('🎐 雅集详情')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.text)

        Text(this.getEventName(this.selId))
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(HN.red)
          .margin({ top: 10 })

        Row() {
          Column() {
            Text('时间')
              .fontSize(11)
              .fontColor(HN.textHint)
            Text(this.getEventDate(this.selId))
              .fontSize(13)
              .fontColor(HN.text)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          Column() {
            Text('地点')
              .fontSize(11)
              .fontColor(HN.textHint)
            Text(this.getEventPlace(this.selId))
              .fontSize(13)
              .fontColor(HN.text)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .margin({ top: 12 })

        Stack() {
          Row()
         
          this.modalOverlay()
          this.delModal()
        }
        .position({ x: 0, y: 0 })
        .zIndex(999)
        .width('100%')
        .height('100%')
      }
    }
    .width('100%')
    .height('100%')
  }
}








在这里插入图片描述

总结

本文详细解析了一款国风汉服馆电商应用的完整技术架构与实现细节。从色彩体系的系统化设计到八个核心数据模型的interface定义,从工具函数的纯函数设计到入口组件的三段式布局,从首页的灯笼动效到形制馆的CRUD操作,从妆造预约到活动报名,每一个模块都体现了ArkTS声明式UI范式的核心优势——状态驱动视图、组件化复用、类型安全约束。

该应用的技术亮点在于将国风美学与工程化设计有机结合。色彩调色板通过interface+const的统一定义,使得朱红、黛青、鎏金三色体系贯穿全局;形制色彩映射函数hnFormColor通过朝代关键字智能匹配主题色,将业务语义编码为视觉信号;灯笼摆动、丝带飘动、印章呼吸等动效通过setInterval+@State+.animation()的组合实现了低成本高表现力的视觉效果。模态弹窗统一采用Stack层叠+条件渲染+zIndex的范式,确保了交互的一致性和代码的简洁性。

从架构扩展性来看,该应用的六个Tab组件各自独立管理状态,通过入口组件的activeTab索引进行条件挂载,这种设计保证了模块间的低耦合——新增Tab只需定义新的@Component并在build方法中添加条件分支即可。数据模型全部通过interface定义,新增字段只需修改接口声明,编译器会自动检查所有使用该接口的地方是否需要适配。这种渐进式扩展的架构理念,使得应用在后续迭代中能够以最小的改动成本接入新的业务模块。

Logo

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

更多推荐