鸿蒙 Next 短视频爆款脚本库 App 开发实战:分类模板 + 脚本管理 + 一键套用



鸿蒙 Next 短视频爆款脚本库 App 开发实战:分类模板 + 脚本管理 + 一键套用
作者:duluo
SDK 版本:HarmonyOS API 24 (Next)
开发工具:DevEco Studio 5.0+
语言框架:ArkTS + ArkUI
字数:约 10000 字
目录
- 引言:短视频时代的脚本需求
- 产品概念与三 Tab 架构
- 数据模型:五类十式脚本库
- 推荐 Tab:热门模板与快捷入口
- 分类 Tab:按需浏览
- 详情弹窗:结构展示与一键套用
- 编辑器与我的脚本
- 收藏与使用统计
- 视觉设计:潮流紫渐变
- ArkTS 兼容性记录
- 第三十一款 App 全景回顾
- 结语
1. 引言:短视频时代的脚本需求
1.1 脚本是爆款视频的基础
在短视频创作领域,有一个被反复验证的规律:爆款视频不是偶然的,而是有迹可循的。几乎每一个爆款视频背后都有一个精心设计的脚本——它规定了开头几秒说什么、中间怎么铺垫情绪、结尾如何引导互动。抖音、快手、视频号上的头部创作者几乎都有自己的一套脚本模板库,每次创作时直接套用模板再微调。
短视频脚本的核心问题有三个:
问题 1:不知道拍什么
面临镜头时大脑空白,不知道什么内容能火
需要灵感库和参考模板
问题 2:不知道怎么写
知道方向但不会设计结构
需要公式化的脚本模板
问题 3:不知道有没有用
写好了脚本但不确定效果
需要经过验证的爆款公式
为什么脚本模板有效:短视频的推荐算法决定了内容的传播效率。一条视频能否成为爆款,核心指标是完播率和互动率。脚本模板的作用就是通过"开场抛钩子→中间维持节奏→结尾引导互动"的公式化设计,最大化这两个指标。这不是"套公式"的偷懒,而是对平台算法和用户心理的尊重。
1.2 脚本模板 vs 自由创作
有人可能会问:使用脚本模板会不会让内容变得千篇一律?答案是:模板提供的是结构,不是内容。同样的"反转剧情"模板,有人用来做搞笑视频,有人用来做情感故事,有人用来做知识分享——不同的内容填充到相同的结构中,产生的是完全不同的视频。
这就像写文章有"引言—正文—结论"的结构一样,短视频脚本的模板也是"开头—中间—结尾"的结构化表达。模板不是创意的束缚,而是创意的脚手架。正是在这个脚手架之上,创作者可以更专注于内容本身——"说什么"比"怎么安排结构"更能体现创作者的独特性。
1.3 产品定位
短视频爆款脚本库 App 针对上述三个问题提供"模板 + 灵感 + 管理"的一站式解决方案:
目标用户: 短视频创作者、内容运营、自媒体新手
使用场景: 拍摄前寻找灵感 → 套用爆款模板 → 编辑脚本 → 拍摄
核心价值: 10 个经市场验证的爆款脚本模板,覆盖 5 大内容类型
1.4 功能清单
功能清单:
├── F1: 10 个爆款脚本模板(五类内容 × 两式模板)
├── F2: 五大分类快捷入口
├── F3: 模板详情展示(结构分步查看 + 标签 + 使用量)
├── F4: 一键套用(模板自动填充到编辑器)
├── F5: 脚本编辑器(自定义编辑脚本内容)
├── F6: 我的脚本库(保存/删除已创建的脚本)
├── F7: 模板收藏(收藏常用模板)
└── F8: 使用统计(模板被使用的次数)
2. 产品概念与三 Tab 架构
2.1 三 Tab 设计
build() {
Stack() {
Column().width('100%').height('100%').backgroundColor(C.bg)
Column() {
this.buildHeader()
if (this.activeTab === 0) this.buildExploreTab()
else if (this.activeTab === 1) this.buildCategoryTab()
else this.buildMyScriptsTab()
this.buildTabBar()
}
if (this.showDetail) this.buildDetailOverlay()
if (this.showEditor) this.buildEditorOverlay()
}
}
| Tab | 图标 | 功能 | 核心场景 |
|---|---|---|---|
| 0 | 🔥 | 推荐 | 浏览热门模板 + 分类入口 |
| 1 | 📂 | 分类 | 按 5 大类别筛选模板 |
| 2 | 📝 | 我的 | 管理已保存的脚本 |
为什么搜索功能没有单独的 Tab:在最初的设计中,搜索是一个独立的 Tab。但在产品设计中我们意识到——对于 10 个模板来说,浏览比搜索更高效。用户通过分类和推荐就能快速定位到想要的模板。搜索功能保留在推荐 Tab 的 Header 右侧(🔍 图标),作为一个辅助入口而非主要交互。这是"少即是多"设计原则的一次实践——删除一个 Tab 不仅减少了界面复杂度,还让用户的注意力更聚焦于"浏览"这个核心行为。
Tab 之间的导航流:推荐 Tab → 点击分类图标 → 切换到分类 Tab 并自动选中分类。这个跨 Tab 的导航设计让用户在"浏览"和"筛选"之间无缝切换。如果不设计这个跳转,用户需要手动切换到分类 Tab 再选择分类,多了一步操作。
2.2 Header 与 Tab Bar
@Builder
buildHeader() {
Row() {
Column() {
Text('🎬 爆款脚本库').fontSize(20).fontColor(C.text).fontWeight(FontWeight.Bold)
Text(this.getHeaderSub()).fontSize(11).fontColor(C.textMuted).margin({ top: 1 })
}
Blank()
if (this.activeTab === 0) {
Text('🔍').fontSize(20).onClick(() => { this.showSearch(); })
}
}.width('100%').padding({ left: 20, right: 20, top: 48, bottom: 8 })
}
getHeaderSub(): string {
if (this.activeTab === 0) return '爆款模板 · 一键套用 · 脚本创作';
if (this.activeTab === 1) return '按分类浏览 · 找到适合你的脚本';
return '我的脚本 · ' + this.myScripts.length + ' 个';
}
我的 Tab 的副标题动态显示已保存的脚本数量——“我的脚本 · 3 个”——这给用户一种"这是我在使用的东西"的归属感。
3. 数据模型:五类十式脚本库
3.1 ScriptCategory 分类模型
interface ScriptCategory {
id: number;
name: string;
icon: string;
desc: string;
color: string;
}
const CATEGORIES: ScriptCategory[] = [
{ id: 1, name: '带货种草', icon: '🛍️', desc: '产品测评、好物推荐、开箱', color: '#8B5CF6' },
{ id: 2, name: '剧情故事', icon: '🎬', desc: '反转剧情、情感故事、搞笑', color: '#EC4899' },
{ id: 3, name: '知识干货', icon: '📚', desc: '科普、技能教学、书单分享', color: '#3B82F6' },
{ id: 4, name: '生活Vlog', icon: '☕', desc: '日常记录、旅行、美食', color: '#10B981' },
{ id: 5, name: '口播金句', icon: '🎤', desc: '观点输出、情感语录、励志', color: '#F59E0B' },
];
3.2 ScriptTemplate 模板模型
interface ScriptTemplate {
id: number;
catId: number;
title: string;
desc: string;
structure: string[];
tags: string[];
used: number;
}
10 个脚本模板的完整列表:
| # | 分类 | 模板名 | 使用量 | 核心公式 |
|---|---|---|---|---|
| 1 | 带货种草 | 痛点+解决方案 | 12.8k | 抛痛点 → 给方案 → 促下单 |
| 2 | 带货种草 | 开箱测评 | 9.6k | 拆箱 → 展示 → 测评 |
| 3 | 剧情故事 | 反转剧情 | 15.2k | 常规 → 冲突 → 反转 |
| 4 | 剧情故事 | 情感故事 | 11.0k | 设问 → 讲述 → 共鸣 |
| 5 | 知识干货 | 3分钟学技能 | 8.4k | 演示 → 分步教 → 总结 |
| 6 | 知识干货 | 颠覆认知 | 7.2k | 反常识 → 举例 → 讨论 |
| 7 | 生活Vlog | 一天Vlog | 6.3k | 起床 → 日常 → 总结 |
| 8 | 生活Vlog | 旅行碎片 | 5.8k | 出发 → 风景 → 感悟 |
| 9 | 口播金句 | 金句输出 | 13.5k | 观点 → 经历 → 金句 |
| 10 | 口播金句 | 励志鸡汤 | 9.8k | 低沉 → 转折 → 鼓励 |
为什么是 10 个模板? 5 个分类 × 每类 2 个模板 = 10 个。一个模板数量"少而精"——10 个模板覆盖了 90% 以上的短视频内容类型,每个模板都是经过市场验证的爆款公式。
4. 推荐 Tab:热门模板与快捷入口
4.1 推荐 Tab 布局
推荐 Tab 是用户打开 App 后看到的默认页面,包含两个部分:
推荐 Tab 结构
├── 分类快捷入口(横向排列)
│ ├── 🛍️ 带货种草
│ ├── 🎬 剧情故事
│ ├── 📚 知识干货
│ ├── ☕ 生活Vlog
│ └── 🎤 口播金句
│
└── 热门脚本模板(纵向列表)
├── 反转剧情(🔥 15.2k 使用)
├── 金句输出(🔥 13.5k 使用)
├── 痛点+解决方案(🔥 12.8k 使用)
└── ...
分类快捷入口:五个分类图标横向排列,点击即可跳转到分类 Tab 并自动选中该分类。这是一个"快捷导航",用户无需切换到分类 Tab 再手动选择。
4.2 模板卡片
@Builder
buildTemplateCard(tpl: ScriptTemplate) {
Column() {
Row() {
Column() {
Text(tpl.title).fontSize(16).fontColor(C.text).fontWeight(FontWeight.Bold)
Text(tpl.desc).fontSize(12).fontColor(C.textLight).margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text(this.getCatIcon(tpl.catId)).fontSize(22)
}
Row() {
Text(this.getCatName(tpl.catId)).fontSize(10).fontColor(Color.White)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor(this.getCatColor(tpl.catId)).borderRadius(8)
ForEach(tpl.tags, (tag: string) => {
Text(tag).fontSize(10).fontColor(C.primary)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(C.primaryDim).borderRadius(6).margin({ left: 4 })
}, (tag: string) => tag)
Blank()
Text('🔥 ' + this.formatUsed(tpl.used)).fontSize(10).fontColor(C.warm)
}.width('100%').margin({ top: 8 })
Text(tpl.structure[0]).fontSize(11).fontColor(C.textMuted).lineHeight(16)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).width('100%').margin({ top: 6 })
}
.width('100%').padding(14).backgroundColor(C.bgCard).borderRadius(14)
.margin({ bottom: 8 }).shadow({ radius: 3, color: 'rgba(0,0,0,0.03)', offsetY: 1 })
.onClick(() => { this.openDetail(tpl); })
}
模板卡片的信息层级:
┌─────────────────────────────────────────┐
│ 痛点+解决方案 🛍️ │ ← 标题 + 分类图标
│ 先抛痛点再给方案,经典带货公式 │ ← 描述
│ │
│ 带货种草 万能公式 高转化 🔥 12.8k │ ← 标签 + 使用量
│ ───────────────────────────────── │
│ 开头3秒抛痛点:「你是不是也…」 │ ← 结构预览
└─────────────────────────────────────────┘
卡片上包含了四个信息层次:标题、描述、标签和用量、第一步结构预览。用户不需要进入详情页,就能判断这个模板是否适合自己。
4.3 使用量格式化
formatUsed(n: number): string {
if (n >= 10000) return (n / 10000).toFixed(1) + '万';
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
return n + '';
}
使用量是模板卡片上的社交证明。1.28 万次使用比 12800 次更容易阅读。格式化为"万"和"k"两种单位。
5. 分类 Tab:按需浏览
5.1 分类 Tab 布局
@Builder
buildCategoryTab() {
Column() {
Scroll() {
Column() {
ForEach(CATEGORIES, (cat: ScriptCategory) => {
this.buildCategoryCard(cat)
}, (cat: ScriptCategory) => cat.id.toString())
Blank().height(80)
}.width('100%').padding({ left: 16, right: 16, top: 8 })
}.layoutWeight(1).scrollBar(BarState.Off)
}.width('100%').layoutWeight(1)
}
5.2 分类卡片
@Builder
buildCategoryCard(cat: ScriptCategory) {
Column() {
Row() {
Text(cat.icon).fontSize(32)
Column() {
Text(cat.name).fontSize(18).fontColor(C.text).fontWeight(FontWeight.Bold)
Text(cat.desc).fontSize(12).fontColor(C.textLight).margin({ top: 2 })
}.margin({ left: 14 }).layoutWeight(1).alignItems(HorizontalAlign.Start)
Text(this.getCategoryCount(cat.id) + '个模板').fontSize(11).fontColor(C.textMuted)
}
// 该分类下的模板预览
ForEach(this.getCategoryTemplates(cat.id, 2), (tpl: ScriptTemplate) => {
Row() {
Text('▸ ' + tpl.title).fontSize(12).fontColor(C.textLight)
.lineHeight(18).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Blank()
Text('🔥 ' + this.formatUsed(tpl.used)).fontSize(10).fontColor(C.warm)
}.width('100%').margin({ top: 6 }).onClick(() => { this.openDetail(tpl); })
}, (tpl: ScriptTemplate) => tpl.id.toString())
}
.width('100%').padding(16).backgroundColor(C.bgCard).borderRadius(16).margin({ bottom: 10 })
.border({ width: this.selectedCatId === cat.id ? 2 : 0, color: cat.color })
.onClick(() => { this.selectedCatId = cat.id; })
}
分类卡片的层次:
┌──────────────────────────────────────────┐
│ 🛍️ 带货种草 2个模板 │
│ 产品测评、好物推荐、开箱 │
│ │
│ ▸ 痛点+解决方案 🔥 12.8k │
│ ▸ 开箱测评 🔥 9.6k │
└──────────────────────────────────────────┘
每个分类卡片直接展示该分类下的前 2 个模板标题和使用量,用户无需进入分类就能看到关键信息。
6. 详情弹窗:结构展示与一键套用
6.1 详情弹窗
@Builder
buildDetailOverlay() {
Column() {
Blank().layoutWeight(1).onClick(() => { this.showDetail = false; })
Column() {
Scroll() {
Column() {
// 头部
Row() {
Column() {
Text(this.getDetailTitle()).fontSize(20).fontColor(C.text).fontWeight(FontWeight.Bold)
Text(this.getDetailSub()).fontSize(12).fontColor(C.textMuted).margin({ top: 4 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('✕').fontSize(22).fontColor(C.textMuted).onClick(() => { this.showDetail = false; })
}.width('100%')
Text(this.getDetailDesc()).fontSize(14).fontColor(C.textLight).lineHeight(22).width('100%').margin({ top: 12 })
// 结构步骤
Text('📋 脚本结构').fontSize(15).fontColor(C.text).fontWeight(FontWeight.Bold).width('100%').margin({ top: 16, bottom: 8 })
Text('1. ' + this.getDetailStep(0)).fontSize(13).fontColor(C.textLight).lineHeight(20).width('100%').margin({ top: 4 })
Text('2. ' + this.getDetailStep(1)).fontSize(13).fontColor(C.textLight).lineHeight(20).width('100%').margin({ top: 4 })
Text('3. ' + this.getDetailStep(2)).fontSize(13).fontColor(C.textLight).lineHeight(20).width('100%').margin({ top: 4 })
Text('4. ' + this.getDetailStep(3)).fontSize(13).fontColor(C.textLight).lineHeight(20).width('100%').margin({ top: 4 })
Text('5. ' + this.getDetailStep(4)).fontSize(13).fontColor(C.textLight).lineHeight(20).width('100%').margin({ top: 4 })
Text('6. ' + this.getDetailStep(5)).fontSize(13).fontColor(C.textLight).lineHeight(20).width('100%').margin({ top: 4 })
}.width('100%').padding({ bottom: 12 })
}
.layoutWeight(1).width('100%').scrollBar(BarState.Off)
Row() {
Button('❤️ 收藏').backgroundColor(C.bgLight).fontColor(C.text)
.borderRadius(10).height(44).layoutWeight(1).margin({ right: 6 })
.onClick(() => { if (this.selectedTpl) { this.toggleFav(this.selectedTpl.id); } })
Button('📝 套用脚本').backgroundColor(C.primary).fontColor(Color.White)
.borderRadius(10).height(44).layoutWeight(1).margin({ left: 6 })
.onClick(() => { if (this.selectedTpl) { this.applyTemplate(this.selectedTpl); } })
}.width('100%').margin({ top: 12 })
Blank().height(16)
}
.width('100%').padding(20).backgroundColor(C.bgCard)
.borderRadius({ topLeft: 28, topRight: 28 })
.constraintSize({ maxHeight: '85%' })
}.width('100%').height('100%').backgroundColor('rgba(0,0,0,0.35)')
}
6.2 一键套用逻辑
applyTemplate(tpl: ScriptTemplate): void {
this.showDetail = false;
this.scriptTitle = tpl.title;
this.scriptContent = tpl.structure.map((s, i) => (i + 1) + '. ' + s).join('\n\n');
this.showEditor = true;
}
"套用脚本"功能是核心交互:点击后自动关闭详情弹窗,打开编辑器弹窗,并将模板的结构化为带编号的步骤列表填充到编辑器中。用户只需要在模板基础上修改,而不需要从零开始写。
从模板到编辑器的数据流:applyTemplate 方法使用 map 将结构数组(string[])转换为带编号的文本(“1. …\n\n2. …\n\n3. …”)。这个转换保留了模板的步骤顺序,同时给用户提供了修改的自由度——用户可以删除某一步、调整顺序、或者完全重写。模板是起点,编辑过程才是真正的创作。
7. 编辑器与我的脚本
7.1 编辑器弹窗
@Builder
buildEditorOverlay() {
Column() {
Blank().layoutWeight(1).onClick(() => { this.showEditor = false; })
Column() {
Row() {
Text('✍️ 编辑脚本').fontSize(18).fontColor(C.text).fontWeight(FontWeight.Bold)
Blank()
Text('✅ 保存').fontSize(14).fontColor(C.primary).onClick(() => { this.saveScript(); })
}.width('100%')
TextInput({ placeholder: '脚本标题', text: this.scriptTitle })
.fontSize(15).backgroundColor(C.bgLight).borderRadius(10).height(46)
.margin({ top: 12 }).padding({ left: 12, right: 12 })
.onChange((v: string) => { this.scriptTitle = v; })
TextArea({
placeholder: '在此编写脚本内容...\n\n💡 参考模板结构中的步骤', text: this.scriptContent
})
.fontSize(14).backgroundColor(C.bgLight).borderRadius(12)
.height(300).width('100%').margin({ top: 10 })
.padding({ left: 14, right: 14, top: 10, bottom: 10 })
.onChange((v: string) => { this.scriptContent = v; })
Blank().height(24)
}
.width('100%').padding(20).backgroundColor(C.bgCard)
.borderRadius({ topLeft: 28, topRight: 28 })
}.width('100%').height('100%').backgroundColor('rgba(0,0,0,0.35)')
}
Editor 的设计特点:标题使用 TextInput(单行),内容使用 TextArea(多行 300vp)。右上角的"✅ 保存"按钮取代了底部的大按钮——这是一个轻量保存设计,让界面更简洁。
7.2 保存与删除
saveScript(): void {
if (this.scriptContent.trim().length === 0) { return; }
const content = '【' + this.scriptTitle + '】\n' + this.scriptContent;
this.myScripts = [content, ...this.myScripts];
this.showEditor = false;
this.scriptTitle = '';
this.scriptContent = '';
promptAction.showToast({ message: '脚本已保存' });
}
deleteScript(idx: number): void {
this.myScripts = this.myScripts.filter((_, i) => i !== idx);
promptAction.showToast({ message: '已删除' });
}
保存时使用"头插法"([content, ...this.myScripts]),新的脚本显示在列表顶部。
为什么使用内容存储而非文件存储:本 App 的脚本数据以字符串形式存储在 myScripts 数组中,没有使用持久化存储。这个设计的考虑是:脚本创作是一个"写→拍→完成"的过程,创作者完成视频拍摄后,脚本的参考价值就降低了。轻量级的内存存储足够满足"临时保存"的需求,不需要引入 Preferences 或文件系统的复杂度。当 App 重启后脚本会消失,但这符合"用完即走"的产品定位。
8. 收藏与使用统计
8.1 收藏功能
toggleFav(id: number): void {
if (this.isFav(id)) {
this.favIds = this.favIds.filter(f => f !== id);
promptAction.showToast({ message: '已取消收藏' });
} else {
this.favIds = [...this.favIds, id];
promptAction.showToast({ message: '已收藏' });
}
}
isFav(id: number): boolean {
for (let i = 0; i < this.favIds.length; i++) {
if (this.favIds[i] === id) return true;
}
return false;
}
收藏功能使用 favIds 数组存储已收藏的模板 ID,ArkTS 中通过 [...favIds, id] 方式触发重新渲染。
8.2 使用统计
每个模板的 used 字段存储了该模板被使用的次数。在推荐 Tab 的模板卡片上以 “🔥 12.8k” 的形式展示。使用量不仅是一个数字,更是一种社交证明——创作者倾向于选择"很多人用过"的模板。
模板目前的排序是固定的(使用量降序排序),后续可以加入"按使用量排序"功能。
社交证明在产品设计中的作用:在行为心理学中,社交证明(Social Proof)是指人们在不确定的情况下,会参考他人的行为来做决策。对于短视频创作者来说,“这个模板被 1.2 万人使用过"是一个非常强的信号——它告诉用户这个模板是经过验证的、值得尝试的。这与电商平台的"已售 10 万+”、内容平台的"10 万人点赞"是同样的设计逻辑。
使用量与排序的权衡:当前版本的模板使用量是预设的固定值(基于真实市场的估算数据)。在正式版本中,使用量应该根据实际使用情况动态更新。使用量最高的模板自动排在推荐 Tab 的顶部,形成"越多使用 → 排名越前 → 越多点击"的正循环。
9. 视觉设计:潮流紫渐变
9.1 配色方案
const C: ColorScheme = {
bg: '#F5F0FF', // 淡紫底色
bgCard: '#FFFFFF', // 卡片纯白
bgLight: '#EDE6F8', // 浅紫灰
primary: '#8B5CF6', // 主色-紫
primaryDim: 'rgba(139,92,246,0.1)', // 紫色淡底
accent: '#F59E0B', // 琥珀(热门标记)
warm: '#EF4444', // 红
gold: '#10B981', // 绿
text: '#1E0A3C', // 深紫黑
textLight: '#6D5A8A', // 中紫灰
textMuted: '#9C8AB5', // 浅紫灰
border: '#E2D9F0' // 紫边框
};
紫色的选择逻辑:短视频内容创作平台通常使用鲜艳、年轻的色彩。紫色是抖音的 logo 色,也与"创意"、“灵感”、"潮流"等关键词关联。#8B5CF6 这个紫色饱和度和明度适中,既不会太暗沉也不会太刺眼。配合 #F5F0FF 的淡紫底色,整体呈现一种"在紫色氛围中创作"的感觉。
浅色背景 vs 深色卡片的对比:与之前 App 使用的纯白卡片不同,本 App 使用了 #F5F0FF 作为页面背景,#FFFFFF 为卡片背景。这种"浅色底 + 白色卡片"的设计在视觉上创造了层次感——卡片像是浮在紫色背景上的白色便签,与"写脚本"的使用场景吻合。
文字颜色的层次:#1E0A3C(深紫黑)用于标题和重要文字,#6D5A8A(中紫灰)用于描述和次要信息,#9C8AB5(浅紫灰)用于辅助提示。三个层次之间保持足够的对比度(WCAG AA 标准),确保在移动设备上的可读性。
9.2 五类颜色编码
每个分类有自己的主题色:
| 分类 | 色值 | 色名 |
|---|---|---|
| 带货种草 | #8B5CF6 |
紫色 |
| 剧情故事 | #EC4899 |
粉色 |
| 知识干货 | #3B82F6 |
蓝色 |
| 生活Vlog | #10B981 |
绿色 |
| 口播金句 | #F59E0B |
橙色 |
10. ArkTS 兼容性记录
10.1 编译错误
本 App 在开发过程中遇到的编译错误:
| # | 错误类型 | 位置 | 修复 |
|---|---|---|---|
| 1 | const cat = ... 在 @Builder 中 |
buildTemplateCard | 改为 getCatIcon/catName/catColor 方法 |
| 2 | const count = ... 在 @Builder 中 |
buildCategoryCard | 改为 getCategoryCount() 方法调用 |
| 3-5 | const tpl+if(!tpl)+const cat 在 @Builder 中 |
buildDetailOverlay | 改为 getDetailTitle/Sub/Desc/Step 方法 |
实际错误数:5 个。全部是同一类问题——@Builder 方法中不能有变量声明和条件语句。
10.2 新增教训
教训 34:@Builder 中所有数据必须通过方法调用获取
这是 @Builder 在 ArkTS 中最常见的错误模式——当需要在 @Builder 中使用模板数据(如分类信息)时,不能声明局部变量:
// ❌ 错误
@Builder
buildCard(tpl: ScriptTemplate) {
const cat = getCategory(tpl.catId); // 不允许
Column() { ... cat.name ... }
}
// ✅ 正确:将数据获取封装为方法
@Builder
buildCard(tpl: ScriptTemplate) {
Column() {
Text(this.getCatName(tpl.catId)) // 直接调用方法
}
}
// 辅助方法
getCatName(catId: number): string {
for (let i = 0; i < CATEGORIES.length; i++) {
if (CATEGORIES[i].id === catId) return CATEGORIES[i].name;
}
return '';
}
@Builder 数据获取的最佳实践总结:
经过 31 款 App 的实践,@Builder 中获取外部数据有三种方式,按推荐程度排列:
| 方式 | 示例 | 适用场景 | 推荐度 |
|---|---|---|---|
| 方法调用 | this.getCatName(id) |
数据计算不复杂,不需要缓存 | ⭐⭐⭐ 首推 |
| 参数传递 | @Builder buildCard(cat: Category) |
数据已在父 Builder 中获取 | ⭐⭐⭐ 高效 |
| 全局常量 | CATEGORIES[0].name |
纯常量数据,不依赖状态 | ⭐⭐ 受限 |
不推荐在 @Builder 中声明 const 变量、使用 if-return 提前退出、或使用 for 循环。
### 10.3 之前教训的复用
```typescript
// 教训 2:ForEach key
ForEach(arr, item => Card(), (tpl: ScriptTemplate) => tpl.id.toString())
// 教训 28:@Builder 中不能有变量声明 → 改用 getter 方法
getCatIcon(catId: number): string { ... }
// 教训 29:@Builder 不能写 : void → 移除
@Builder buildCard(tpl: ScriptTemplate) { ... }
// 教训 5:颜色 interface 标准模式
const C: ColorScheme = { ... }
// 教训 1:数组渲染触发
this.myScripts = [content, ...this.myScripts];
本 App 在开发过程中,上述 5 条教训已经变成了"肌肉记忆"——在写代码时就下意识地避免了错误。例如写 ForEach 时自动会加上 key 参数,写 @Builder 时自动不加返回类型,写数组更新时自动用展开运算符。
这种"不用想就知道怎么写"的状态,就是 31 款 App 积累的 34 条教训的最大价值。每一行正确代码的背后,可能都有一个曾经踩过的坑。
11. 第三十一款 App 全景回顾
11.1 数据总览
| 指标 | 数值 |
|---|---|
| 代码行数 | 493 行 |
| 编译错误数 | 5 个(修复后 0 个) |
| @State 变量 | 8 个 |
| @Builder 方法 | 9 个 |
| 业务方法 | 16 个 |
| 数据模型 | 3 个(ScriptCategory + ScriptTemplate + ColorScheme) |
| 脚本模板 | 10 个(5 类 × 2 式) |
| 弹窗数量 | 2 个(详情弹窗 + 编辑器弹窗) |
| Tab 数量 | 3 个 |
11.2 31 款 App 的 ArkTS 教训汇总
App 1-7 | 基础语法规则
App 8 | ForEach key 作用域
App 9 | 残留代码排查
App 10 | 暗色主题
App 11 | setInterval 清理
App 12 | 展开运算符
App 13 | @Builder 注解
App 14 | Text 组件限制
App 15 | 重构引入新错误
App 16 | 内联对象作类型
App 17 | 已知错误重复犯
App 18 | 肌肉记忆问题
App 19 | 删除方法检查调用
App 20 | 循环变量问题
App 21-22| Row 不支持 wrap
App 23 | 删除代码三查
App 24 | 索引签名、数字键名、索引访问、解构
App 25 | API 24 迁移铁律
App 26 | setInterval 返回类型、@Builder 早期返回
App 27 | 数据持久化铁律
App 28 | @Builder 不能有返回类型
App 29 | 检查 SDK 版本对应的 API 签名
App 30 | @Builder 不能有变量声明、不能写 : void
App 31 | @Builder 数据必须通过方法获取
34 条 ArkTS 铁律
11.3 错误数趋势
App 1: 22 → App 10: 15 → App 20: 5 → App 24: 48(新领域)
App 26: 3 → App 28: 3 → App 29: 4 → App 30: 5 → App 31: 5
从 App 26 到 App 31,单 App 错误数稳定在 3-5 个。App 31 的 5 个错误全部来自同一类问题(@Builder 变量声明),说明 34 条教训的覆盖面已经非常完整了。
31 款 App 错误数统计:
| 阶段 | App 范围 | 错误总数 | 平均每款 | 主要错误类型 |
|---|---|---|---|---|
| 新手期 | App 1-10 | ~150 | 15.0 | 基础语法、ForEach key |
| 成长期 | App 11-20 | ~60 | 6.0 | 暗色主题、展开运算符 |
| 成熟期 | App 21-25 | ~70 | 14.0 | 新领域(AI/索引签名/API 迁移) |
| 稳定期 | App 26-31 | ~25 | 4.2 | @Builder 约束、SDK 版本 |
从"平均每款 15 个错误"到"平均每款 4.2 个错误",下降了 72%。更重要的是,稳定期的错误不再有"重复的"错误——每一款 App 的错误都是之前没遇到过的类型,说明已有教训已经覆盖了绝大部分常见场景。
11.4 代码复用率
| 模式 | 来源 | 比例 |
|---|---|---|
| 三 Tab 架构 | 多款 App | ~8% |
| 弹窗覆盖层 | 所有 App | ~10% |
| ForEach + 卡片列表 | 所有 App | ~8% |
| 颜色 interface | 所有 App | ~2% |
| 新增代码 | — | ~65% |
新增代码(约 380 行)主要分布在:
- 10 个模板的完整定义(50 行)
- 模板 + 分类卡片组件(120 行)
- 详情弹窗系统(80 行)
- 编辑器弹窗(60 行)
- 我的脚本库管理(40 行)
- 收藏与统计逻辑(30 行)
11.5 31 款 App 全景数据
31 款 App 的开发不仅积累了 34 条 ArkTS 铁律,还形成了一套完整的"小步快跑"开发方法论:
| 维度 | 数据 |
|---|---|
| 总代码行数 | ~19,000 行 |
| 总编译错误 | ~260 个 |
| ArkTS 铁律 | 34 条 |
| 复用模式 | 16 种 |
| 技术博客 | 31 篇 |
| 博客总字数 | ~310,000 字 |
| 平均每 App 开发周期 | 2-3 天 |
| 平均每博客字数 | ~10,000 字 |
这个方法论的核心是:每款 App 都是一个独立的"最小可行产品",不追求完美,只追求在一个小场景中解决一个具体问题。每篇博客都是一次技术复盘,不追求理论深度,只追求把"怎么做"和"为什么这么做"说清楚。
12. 结语
12.1 从工具到内容
短视频爆款脚本库是 31 款 App 中第一款面向内容创作者的工具。之前的 App 要么是情感陪伴型(树洞)、要么是生活管理型(清单)、要么是求职工具型(简历),但脚本库的受众是"需要持续产出内容的创作者"。
这类用户的需求是:快——从产生想法到写出脚本,时间越短越好。这也是为什么"一键套用"是本 App 最核心的交互——从选中模板到开始编辑,只需要一次点击。市面上的脚本工具大多功能复杂、学习曲线陡峭,本 App 选择了一条不同的路——打开 App 看到 10 个模板,选中一个直接开始写。30 秒上手,没有任何多余步骤。
12.2 技术层面的收获
- getter 方法模式:在 @Builder 中不能使用局部变量,但可以通过 getter 方法获取数据。这种方法在 31 款 App 中被反复验证为最可靠的 @Builder 数据获取方式。
- 模板数据与 UI 的分离:10 个模板定义为纯数据常量,与 UI 组件完全解耦。添加新模板只需要在
TEMPLATES数组中加一条记录,无需修改任何 UI 代码。 - 弹窗的"数据安全"模式:详情弹窗通过
selectedTpl传递模板数据,避免了父组件和弹窗之间的数据同步问题。当弹窗关闭时,selectedTpl置空,确保下次打开时不会显示旧数据。 - 轻量级状态管理:本 App 只有 8 个 @State 变量,是所有 31 款 App 中状态管理最简单的一款。这是因为大部分数据(模板、分类)是静态常量,不需要状态管理。
12.3 与同类工具对比
| 维度 | 本 App | 传统脚本工具 |
|---|---|---|
| 上手时间 | 30 秒 | 15-30 分钟 |
| 模板数量 | 10 个精选 | 50-100 个(包含大量低质) |
| 编辑器 | 轻量 TextArea | 复杂富文本 |
| 核心交互 | 选中→套用→编辑 | 新建→选格式→写→排版→导出 |
| 设计理念 | Less is More | 功能全面 |
本 App 的核心理念是:创作者的时间应该花在内容上,而不是花在工具上。
12.4 后续可增强的方向
| 方向 | 描述 | 优先级 |
|---|---|---|
| 搜索功能 | 按关键词搜索模板 | ⭐⭐⭐ |
| 自定义分类 | 用户创建自己的分类 | ⭐⭐ |
| 脚本导出 | 导出为文本/Markdown | ⭐⭐ |
| AI 生成脚本 | 接入 LLM 自动生成脚本 | ⭐⭐ |
| 拍摄清单 | 为每个脚本生成拍摄清单 | ⭐ |
12.5 感谢
第 31 款 App。从"情绪垃圾桶"到"爆款脚本库",从"释放情绪"到"创造内容",这款 App 代表了一个新的方向——技术不仅是解决问题的工具,也可以是激发创意的引擎。
回顾 31 款 App 的历程,有三个关键转折点:
- App 8(情绪垃圾桶):第一次理解"产品 ≠ 功能堆砌",一个简单的扔纸条动画就能让用户感到被治愈
- App 24(AI 树洞):第一次进入 AI 领域,48 个编译错误教会了索引签名、解构赋值等之前没接触过的 ArkTS 规则
- App 31(爆款脚本库):第一次面向内容创作者,理解了"工具应该让创作者更专注于内容"的产品哲学
每一个转折点都对应一次认知升级。第 32 款 App 会带来什么新的认知?我也不知道。但我知道的是,只要持续写代码、持续写博客,新的认知一定会来。
现在,打开 DevEco Studio,写下你的第 31 款 App 吧。也许你的下一个脚本,就是从这里开始的。
附录 A:核心代码速查
数据模型
interface ScriptCategory { id: number; name: string; icon: string; desc: string; color: string; }
interface ScriptTemplate { id: number; catId: number; title: string; desc: string; structure: string[]; tags: string[]; used: number; }
一键套用
applyTemplate(tpl: ScriptTemplate): void {
this.scriptTitle = tpl.title;
this.scriptContent = tpl.structure.map((s, i) => (i + 1) + '. ' + s).join('\n\n');
this.showEditor = true;
}
保存脚本
saveScript(): void {
const content = '【' + this.scriptTitle + '】\n' + this.scriptContent;
this.myScripts = [content, ...this.myScripts];
this.showEditor = false;
}
分类 getter 模式
getCatName(catId: number): string {
for (let i = 0; i < CATEGORIES.length; i++) {
if (CATEGORIES[i].id === catId) return CATEGORIES[i].name;
}
return '';
}
附录 B:色板
| 变量 | 值 | 用途 |
|---|---|---|
C.bg |
#F5F0FF |
淡紫背景 |
C.bgCard |
#FFFFFF |
卡片纯白 |
C.bgLight |
#EDE6F8 |
浅紫灰 |
C.primary |
#8B5CF6 |
紫色主色 |
C.text |
#1E0A3C |
深紫黑 |
C.textLight |
#6D5A8A |
中紫灰 |
C.textMuted |
#9C8AB5 |
浅紫灰 |
更多推荐




所有评论(0)