本篇开发新年习俗专题模块,展示八朝年俗文化

【完整案例】新年习俗专题开发 教程结构图

图:【完整案例】新年习俗专题开发 的关键流程与实现要点。

学习目标

  • ✅ 设计年俗数据结构
  • ✅ 实现朝代切换展示
  • ✅ 开发年俗详情页面
  • ✅ 集成诗词与活动展示

预计学习时间

约 100 分钟


实战一:设计年俗数据结构

第一步:定义数据接口

entry/src/main/ets/data/NewYearCustoms.ets 中定义:

// 诗词信息接口
export interface PoemInfo {
  text: string;      // 诗词内容
  author: string;    // 作者
  source: string;    // 出处
}

// 新年习俗接口
export interface NewYearCustom {
  id: number;
  dynasty: string;        // 朝代名称
  dynastyEn: string;      // 英文名称
  period: string;         // 时期
  customs: string[];      // 习俗列表
  highlight: string;      // 特色亮点
  description: string;    // 详细描述
  foods: string[];        // 年节食物
  activities: string[];   // 庆祝活动
  poem: PoemInfo | null;  // 相关诗词
  image: string;          // 背景图片
}

原理解释

  • 每个朝代包含习俗、食物、活动、诗词等多维度信息
  • highlight 用于卡片展示的亮点标签
  • poem 可为空,部分朝代可能没有流传的诗词

第二步:填充八朝年俗数据

export const NEW_YEAR_CUSTOMS: NewYearCustom[] = [
  {
    id: 1,
    dynasty: '秦',
    dynastyEn: 'Qin Dynasty',
    period: '公元前221年-公元前207年',
    customs: ['腊祭祈福', '驱傩逐疫', '饮椒柏酒', '祭祀祖先'],
    highlight: '以十月为岁首',
    description: '秦朝以十月为岁首,新年在农历十月。秦人重视腊祭,通过祭祀百神祈求来年风调雨顺。驱傩仪式是秦代新年重要活动,人们戴面具扮演方相氏驱逐疫鬼,保佑平安。',
    foods: ['腊肉', '椒柏酒', '黍米饭'],
    activities: ['腊祭', '驱傩', '占卜', '祭祖'],
    poem: {
      text: '腊祭百神,驱傩逐疫,椒柏酒香迎新岁。',
      author: '秦俗',
      source: '《秦代新年习俗》'
    },
    image: 'newyear_qin'
  },
  {
    id: 2,
    dynasty: '汉',
    dynastyEn: 'Han Dynasty',
    period: '公元前202年-公元220年',
    customs: ['爆竹驱邪', '桃符镇宅', '饮屠苏酒', '拜年贺岁'],
    highlight: '爆竹声中一岁除',
    description: '汉代确立以正月初一为新年,奠定了后世春节的基础。燃放爆竹驱邪避凶,门上悬挂桃符镇宅辟邪。全家饮屠苏酒,从年幼者开始,象征尊老爱幼。',
    foods: ['屠苏酒', '五辛盘', '胶牙饧', '饺子'],
    activities: ['燃爆竹', '挂桃符', '拜年', '守岁'],
    poem: {
      text: '爆竹声中一岁除,春风送暖入屠苏。千门万户曈曈日,总把新桃换旧符。',
      author: '王安石',
      source: '《元日》'
    },
    image: 'newyear_han'
  },
  // ... 魏晋、唐、宋、元、明、清 共8个朝代
];

第三步:添加数据查询方法

// 根据朝代名称获取年俗
export function getNewYearCustomByDynasty(dynasty: string): NewYearCustom | undefined {
  return NEW_YEAR_CUSTOMS.find(item => item.dynasty === dynasty);
}

// 获取所有年俗数据
export function getAllNewYearCustoms(): NewYearCustom[] {
  return NEW_YEAR_CUSTOMS;
}

预期效果:数据结构完整,包含8个朝代的年俗信息。

预期效果:数据结构完整,包含8个朝代的年俗信息。

案例效果:数据结构示意:

┌── NewYearCustom ──────────────────────┐
│ dynasty: "汉"                         │
│ period: "前202-220"                   │
│ highlight: "爆竹声中一岁除"           │
│ customs: [爆竹驱邪, 桃符镇宅, ...]    │
│ foods: [屠苏酒, 五辛盘, 饺子, ...]    │
│ activities: [燃爆竹, 挂桃符, ...]     │
│ poem: { text: "爆竹声中一岁除...",    │
│         author: "王安石" }            │
└───────────────────────────────────────┘

实战二:实现年俗详情页

第一步:创建页面基础结构

import { NEW_YEAR_CUSTOMS, NewYearCustom } from '../data/NewYearCustoms';

@Component
struct NewYearCustomPage {
  @Consume('mainNavPathStack') mainNavPathStack: NavPathStack;
  @StorageLink('isDarkMode') isDarkMode: boolean = true;
  
  @State currentIndex: number = 0;
  @State customData: NewYearCustom = NEW_YEAR_CUSTOMS[0];

  aboutToAppear() {
    // 从路由参数获取初始朝代
    const params = this.mainNavPathStack.getParamByName('NewYearCustomPage');
    if (params && params.length > 0) {
      const dynastyIndex = (params[0] as { dynastyIndex: number }).dynastyIndex;
      if (dynastyIndex !== undefined) {
        this.currentIndex = dynastyIndex;
        this.customData = NEW_YEAR_CUSTOMS[this.currentIndex];
      }
    }
  }

  build() {
    NavDestination() {
      Stack() {
        // 背景
        Column()
          .width('100%')
          .height('100%')
          .backgroundColor(this.isDarkMode ? '#221210' : '#f8f6f5')

        Column() {
          this.HeaderBar()
          Scroll() {
            Column({ space: 20 }) {
              this.HeroSection()
              this.CustomsCard()
              this.FoodsCard()
              this.ActivitiesCard()
              if (this.customData.poem) {
                this.PoemCard()
              }
            }
            .padding({ left: 16, right: 16, bottom: 100 })
          }
          .layoutWeight(1)
        }

        // 底部朝代切换
        this.BottomDynastyNav()
      }
    }
    .hideTitleBar(true)
  }
}

第二步:实现英雄区域(Hero Section)

@Builder
HeroSection() {
  Stack() {
    // 背景图
    Image(this.getDynastyImage())
      .width('100%')
      .height('100%')
      .objectFit(ImageFit.Cover)
      .borderRadius(16)

    // 渐变遮罩
    Column()
      .width('100%')
      .height('100%')
      .linearGradient({
        direction: GradientDirection.Bottom,
        colors: [['rgba(0,0,0,0.2)', 0], ['rgba(0,0,0,0.8)', 1]]
      })
      .borderRadius(16)

    // 内容
    Column({ space: 8 }) {
      Blank()

      // 朝代标签
      Text(this.customData.dynastyEn)
        .fontSize(10)
        .fontWeight(FontWeight.Bold)
        .fontColor(Color.White)
        .backgroundColor('#c41e3a')
        .padding({ left: 10, right: 10, top: 4, bottom: 4 })
        .borderRadius(4)

      // 标题
      Text(this.customData.dynasty + '朝新年')
        .fontSize(32)
        .fontWeight(FontWeight.Bold)
        .fontColor(Color.White)

      // 时期
      Text(this.customData.period)
        .fontSize(14)
        .fontColor('rgba(255, 255, 255, 0.8)')

      // 特色亮点
      Row({ space: 6 }) {
        Image($r('app.media.ic_star'))
          .width(16)
          .height(16)
          .fillColor('#fbbf24')
        Text(this.customData.highlight)
          .fontSize(14)
          .fontWeight(FontWeight.Medium)
          .fontColor('#fbbf24')
      }
      .margin({ top: 8 })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .alignItems(HorizontalAlign.Start)
  }
  .width('100%')
  .height(220)
  .borderRadius(16)
  .shadow({ radius: 16, color: 'rgba(0, 0, 0, 0.2)', offsetY: 4 })
}

// 获取朝代背景图
getDynastyImage(): Resource {
  const images: Record<string, Resource> = {
    '秦': $r('app.media.newyear_qin'),
    '汉': $r('app.media.newyear_han'),
    '魏晋': $r('app.media.newyear_weijin'),
    '唐': $r('app.media.newyear_tang'),
    '宋': $r('app.media.newyear_song'),
    '元': $r('app.media.newyear_yuan'),
    '明': $r('app.media.newyear_ming'),
    '清': $r('app.media.newyear_qing')
  };
  return images[this.customData.dynasty] || $r('app.media.newyear_qin');
}

原理解释

  • 使用 Stack 叠加图片、渐变遮罩和文字
  • linearGradient 创建从透明到黑色的渐变,确保文字可读
  • Record<string, Resource> 实现朝代到图片的映射

案例效果:英雄区域(Hero Section)展示如下:

┌──────────────────────────────────────┐
│  ← 新年习俗                         │
├──────────────────────────────────────┤
│  ┌══════════════════════════════┐   │
│  ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║   │
│  ║▒▒▒▒▒ 背景图片 ▒▒▒▒▒▒▒▒▒▒▒▒║   │
│  ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║   │
│  ║                              ║   │
│  ║  ┌──────────┐               ║   │
│  ║  │Han Dynasty│  ← 红色标签   ║   │
│  ║  └──────────┘               ║   │
│  ║  汉朝新年        ← 白色大字  ║   │
│  ║  前202年-220年   ← 半透明白   ║   │
│  ║  ⭐ 爆竹声中一岁除  ← 金色   ║   │
│  └══════════════════════════════┘   │
│        (底部渐变遮罩保证文字可读)    │

效果说明:<br />- 使用 Stack 叠加:背景图 → 渐变遮罩 → 文字内容<br />- 渐变遮罩从上方透明到底部黑色(rgba(0,0,0,0.8))<br />- 朝代英文标签使用红色(#c41e3a)背景<br />- 特色亮点使用金色(#fbbf24)⭐图标 + 金色文字

第三步:实现习俗列表卡片

@Builder
CustomsCard() {
  Column({ space: 16 }) {
    // 标题
    Row({ space: 8 }) {
      Column()
        .width(4)
        .height(20)
        .backgroundColor('#c41e3a')
        .borderRadius(2)
      Text('传统习俗')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.isDarkMode ? Color.White : '#1e293b')
    }

    // 描述
    Text(this.customData.description)
      .fontSize(14)
      .lineHeight(24)
      .fontColor(this.isDarkMode ? '#9ca3af' : '#64748b')

    // 习俗列表
    Column({ space: 12 }) {
      ForEach(this.customData.customs, (custom: string, index: number) => {
        Row({ space: 12 }) {
          // 序号
          Text((index + 1).toString())
            .fontSize(12)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.White)
            .textAlign(TextAlign.Center)
            .width(24)
            .height(24)
            .backgroundColor('#c41e3a')
            .borderRadius(12)

          // 习俗名称
          Text(custom)
            .fontSize(15)
            .fontWeight(FontWeight.Medium)
            .fontColor(this.isDarkMode ? Color.White : '#1e293b')
        }
        .width('100%')
        .padding(12)
        .backgroundColor(this.isDarkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)')
        .borderRadius(12)
      })
    }
  }
  .width('100%')
  .padding(20)
  .backgroundColor(this.isDarkMode ? '#2d1f1d' : Color.White)
  .borderRadius(16)
}

第四步:实现年节食物卡片

@Builder
FoodsCard() {
  Column({ space: 16 }) {
    // 标题
    Row({ space: 8 }) {
      Column()
        .width(4)
        .height(20)
        .backgroundColor('#22c55e')
        .borderRadius(2)
      Text('年节食物')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.isDarkMode ? Color.White : '#1e293b')
    }

    // 食物标签(使用 Flex 换行布局)
    Flex({ wrap: FlexWrap.Wrap, space: { main: LengthMetrics.vp(10), cross: LengthMetrics.vp(10) } }) {
      ForEach(this.customData.foods, (food: string, index: number) => {
        Row({ space: 6 }) {
          Text(this.getFoodIcon(food))
            .fontSize(14)
          Text(food)
            .fontSize(14)
            .fontColor(this.isDarkMode ? '#d1d5db' : '#374151')
        }
        .padding({ left: 12, right: 12, top: 8, bottom: 8 })
        .backgroundColor(this.isDarkMode ? 'rgba(34, 197, 94, 0.1)' : 'rgba(34, 197, 94, 0.08)')
        .borderRadius(20)
        .border({ width: 1, color: 'rgba(34, 197, 94, 0.2)' })
      })
    }
  }
  .width('100%')
  .padding(20)
  .backgroundColor(this.isDarkMode ? '#2d1f1d' : Color.White)
  .borderRadius(16)
}

// 根据食物名称返回对应图标
getFoodIcon(food: string): string {
  if (food.includes('酒')) return '🍶';
  if (food.includes('肉') || food.includes('羊')) return '🍖';
  if (food.includes('饺子')) return '🥟';
  if (food.includes('年糕') || food.includes('糕')) return '🍰';
  if (food.includes('米') || food.includes('饭')) return '🍚';
  return '🍽';
}

案例效果:食物标签卡片展示如下:

┌──────────────────────────────────────┐
│  ┌──────────────────────────────┐   │
│  │ ▍年节食物                    │   │  ← 绿色竖线标题
│  │                              │   │
│  │  ┌──────────┐ ┌────────┐   │   │
│  │  │ 🍶 屠苏酒│ │🥟 饺子 │   │   │
│  │  └──────────┘ └────────┘   │   │
│  │  ┌────────────┐ ┌──────┐  │   │
│  │  │ 🍽 五辛盘  │ │🍰 饧 │  │   │
│  │  └────────────┘ └──────┘  │   │
│  │                              │   │
│  │  (绿色半透明背景 + 绿色边框)  │   │
│  └──────────────────────────────┘   │
└──────────────────────────────────────┘

效果说明:<br />- 标题使用绿色(#22c55e)竖线装饰<br />- 食物标签使用 Flex 换行布局,自动适配屏幕宽度<br />- 每个标签带 emoji 图标,根据食物名称智能匹配<br />- 标签使用绿色半透明背景 + 绿色细边框

原理解释

  • Flex 配合 FlexWrap.Wrap 实现标签自动换行
  • LengthMetrics.vp() 设置间距单位
  • 根据食物名称动态匹配 emoji 图标

第五步:实现诗词卡片

@Builder
PoemCard() {
  Column({ space: 16 }) {
    // 标题
    Row({ space: 8 }) {
      Column()
        .width(4)
        .height(20)
        .backgroundColor('#a855f7')
        .borderRadius(2)
      Text('诗词名句')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.isDarkMode ? Color.White : '#1e293b')
    }

    // 诗词内容
    Column({ space: 12 }) {
      // 装饰引号
      Text('"')
        .fontSize(48)
        .fontColor('#c41e3a')
        .opacity(0.3)
        .margin({ left: -8 })

      // 诗词文本
      Text(this.customData.poem?.text || '')
        .fontSize(16)
        .fontStyle(FontStyle.Italic)
        .lineHeight(28)
        .fontColor(this.isDarkMode ? '#e5e7eb' : '#374151')
        .textAlign(TextAlign.Center)
        .padding({ left: 16, right: 16 })

      // 出处
      Row({ space: 8 }) {
        Text('——')
          .fontSize(14)
          .fontColor(this.isDarkMode ? '#6b7280' : '#9ca3af')
        Text(this.customData.poem?.author || '')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#c41e3a')
        Text(this.customData.poem?.source || '')
          .fontSize(12)
          .fontColor(this.isDarkMode ? '#6b7280' : '#9ca3af')
      }
      .justifyContent(FlexAlign.Center)
      .width('100%')
    }
    .width('100%')
    .padding({ top: 8, bottom: 16 })
    .backgroundColor(this.isDarkMode ? 'rgba(168, 85, 247, 0.05)' : 'rgba(168, 85, 247, 0.03)')
    .borderRadius(12)
  }
  .width('100%')
  .padding(20)
  .backgroundColor(this.isDarkMode ? '#2d1f1d' : Color.White)
  .borderRadius(16)
}

案例效果:诗词卡片展示如下:

┌──────────────────────────────────────┐
│  ┌──────────────────────────────┐   │
│  │ ▍诗词名句                    │   │  ← 紫色竖线标题
│  │                              │   │
│  │  ┌────────────────────────┐ │   │
│  │  │   ❝                    │ │   │  ← 大号装饰引号
│  │  │                        │ │   │     (半透明红色)
│  │  │ 爆竹声中一岁除,        │ │   │
│  │  │ 春风送暖入屠苏。        │ │   │  ← 斜体居中
│  │  │ 千门万户曈曈日,        │ │   │
│  │  │ 总把新桃换旧符。        │ │   │
│  │  │                        │ │   │
│  │  │    —— 王安石 《元日》   │ │   │  ← 作者红色,出处灰色
│  │  └────────────────────────┘ │   │
│  │     (紫色半透明背景区域)      │   │
│  └──────────────────────────────┘   │
└──────────────────────────────────────┘

实战三:实现朝代切换导航

第一步:底部朝代导航栏

@Builder
BottomDynastyNav() {
  Column() {
    Row({ space: 0 }) {
      ForEach(NEW_YEAR_CUSTOMS, (item: NewYearCustom, index: number) => {
        Column({ space: 2 }) {
          Text(item.dynasty)
            .fontSize(16)
            .fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
            .fontColor(this.currentIndex === index ? '#c41e3a' :
              (this.isDarkMode ? '#6b7280' : '#9ca3af'))

          // 选中指示器
          Column()
            .width(this.currentIndex === index ? 20 : 0)
            .height(3)
            .backgroundColor('#c41e3a')
            .borderRadius(1.5)
            .animation({ duration: 200 })
        }
        .layoutWeight(1)
        .padding({ top: 12, bottom: 8 })
        .onClick(() => {
          this.currentIndex = index;
          this.customData = NEW_YEAR_CUSTOMS[index];
        })
      })
    }
    .width('100%')
  }
  .width('100%')
  .backgroundColor(this.isDarkMode ? '#1a0e0c' : Color.White)
  .border({ width: { top: 1 }, color: this.isDarkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)' })
  .position({ x: 0, y: '100%' })
  .translate({ y: -50 })
}

原理解释

  • 使用 layoutWeight(1) 平均分配宽度
  • 选中指示器通过 animation 实现平滑过渡
  • position + translate 固定在底部

案例效果:底部朝代导航栏展示如下:

┌──────────────────────────────────────┐
│           ( 页面内容区 )            │
│                                      │
├──────────────────────────────────────┤
│  秦   汉   魏晋  唐   宋  元  明  清│
│             ▔▔                       │
│       ← 选中的「魏晋」下方有红色指示条 │
│          点击切换,指示条带动画滑动    │
└──────────────────────────────────────┘

效果说明:<br />- 8个朝代平均分布,使用 layoutWeight(1)<br />- 当前选中朝代文字加粗 + 红色(#c41e3a)<br />- 选中指示条宽 20px,高 3px,红色圆角<br />- 指示条切换时带 200ms 动画过渡(宽度从 0→20)<br />- 切换朝代时内容区有淡出淡入过渡(150ms + 200ms)

第二步:添加切换动画

// 切换朝代时添加过渡效果
switchDynasty(index: number) {
  if (index === this.currentIndex) return;
  
  // 先淡出
  animateTo({
    duration: 150,
    curve: Curve.EaseOut
  }, () => {
    this.contentOpacity = 0;
  });

  // 切换数据
  setTimeout(() => {
    this.currentIndex = index;
    this.customData = NEW_YEAR_CUSTOMS[index];
    
    // 再淡入
    animateTo({
      duration: 200,
      curve: Curve.EaseIn
    }, () => {
      this.contentOpacity = 1;
    });
  }, 150);
}

实战四:首页年俗入口

第一步:在首页添加年俗入口卡片

@Builder
NewYearCustomsEntry() {
  Column() {
    // 标题
    Row() {
      Text('🏮')
        .fontSize(20)
      Text('新年习俗')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.isDarkMode ? Color.White : '#1e293b')
        .margin({ left: 8 })
      Blank()
      Text('查看全部')
        .fontSize(12)
        .fontColor('#c41e3a')
    }
    .width('100%')
    .margin({ bottom: 12 })

    // 朝代卡片横向滚动
    Scroll() {
      Row({ space: 12 }) {
        ForEach(NEW_YEAR_CUSTOMS, (item: NewYearCustom, index: number) => {
          this.DynastyMiniCard(item, index)
        })
      }
      .padding({ right: 16 })
    }
    .scrollable(ScrollDirection.Horizontal)
    .scrollBar(BarState.Off)
  }
  .width('100%')
  .padding(16)
}

@Builder
DynastyMiniCard(item: NewYearCustom, index: number) {
  Column({ space: 8 }) {
    // 朝代图片
    Image(this.getDynastyImage(item.dynasty))
      .width(100)
      .height(80)
      .objectFit(ImageFit.Cover)
      .borderRadius(8)

    // 朝代名称
    Text(item.dynasty + '朝')
      .fontSize(14)
      .fontWeight(FontWeight.Medium)
      .fontColor(this.isDarkMode ? Color.White : '#1e293b')

    // 亮点
    Text(item.highlight)
      .fontSize(10)
      .fontColor(this.isDarkMode ? '#9ca3af' : '#64748b')
      .maxLines(1)
      .textOverflow({ overflow: TextOverflow.Ellipsis })
  }
  .width(100)
  .padding(8)
  .backgroundColor(this.isDarkMode ? '#2d1f1d' : Color.White)
  .borderRadius(12)
  .onClick(() => {
    this.mainNavPathStack.pushPathByName('NewYearCustomPage', { dynastyIndex: index });
  })
}

完整代码汇总

NewYearCustoms.ets 数据文件

export interface PoemInfo {
  text: string;
  author: string;
  source: string;
}

export interface NewYearCustom {
  id: number;
  dynasty: string;
  dynastyEn: string;
  period: string;
  customs: string[];
  highlight: string;
  description: string;
  foods: string[];
  activities: string[];
  poem: PoemInfo | null;
  image: string;
}

export const NEW_YEAR_CUSTOMS: NewYearCustom[] = [
  // 秦、汉、魏晋、唐、宋、元、明、清 共8个朝代数据
];

export function getNewYearCustomByDynasty(dynasty: string): NewYearCustom | undefined {
  return NEW_YEAR_CUSTOMS.find(item => item.dynasty === dynasty);
}

本课小结

功能 实现方式
数据结构 接口定义 + 数组存储
英雄区域 Stack 叠加 + 渐变遮罩
习俗列表 ForEach + 序号标签
食物标签 Flex 换行 + emoji 图标
朝代切换 底部导航 + 动画过渡

课后练习

1. 为每个朝代添加更多习俗详情<br />2. 实现年俗内容的搜索功能<br />3. 添加年俗收藏功能


下一课预告

第28课开发握姿祝福完整功能,包括握姿感应、祝福卡片、隔空投送。


项目开源地址

https://gitcode.com/daleishen/gujinzhijian

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐