HarmonyOS APP<<古今职鉴定>>开源教程第27篇:【完整案例】新年习俗专题开发
本篇开发新年习俗专题模块,展示八朝年俗文化

图:【完整案例】新年习俗专题开发 的关键流程与实现要点。
学习目标
- ✅ 设计年俗数据结构
- ✅ 实现朝代切换展示
- ✅ 开发年俗详情页面
- ✅ 集成诗词与活动展示
预计学习时间
约 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课开发握姿祝福完整功能,包括握姿感应、祝福卡片、隔空投送。
项目开源地址
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)