HarmonyOS API 24 实战:ArkTS 构建 AI 剧本创作 App 全流程解析



HarmonyOS API 24 实战:ArkTS 构建 AI 剧本创作 App 全流程解析
项目: AI 剧本 (AI Script Writer)
平台: HarmonyOS · ArkTS · API 24
构建: Hvigor 6.1.0 · Stage 模型
包名:com.atomgit.aiplay
源码: 基于demo012项目中的 AI 剧本模块
字数: ≈ 10,500 字
目录
- 引言
- 项目概述与技术栈
- ArkTS 语言与 HarmonyOS 开发环境
- 项目架构与工程结构
- 类型系统与数据模型设计
- AI 剧本生成引擎
- 数据持久化层:AppStorage 实战
- UI 层:ArkUI 声明式界面构建
- 组件化设计与回调传递
- 状态管理深入:@State / @Prop / @Link
- 用户交互与体验优化
- 测试策略与质量保障
- 构建与预览配置
- 踩坑记录与 ArkTS 合规要点
- 总结与展望
1. 引言
2026 年的移动端开发格局中,HarmonyOS 已经从一个新兴平台成长为不可忽视的生态力量。其原生的 ArkTS 语言和 ArkUI 框架,为开发者提供了与 SwiftUI、Jetpack Compose 相似的声明式 UI 开发体验,同时有着独特的语法约束和性能特性。
本文分享的 AI 剧本 App 是运行在 HarmonyOS API 24 上的一款创意工具应用。它利用模板引擎技术,让用户可以在手机上快速生成完整的剧本——包括角色表、场景描述、对话台词和舞台说明。用户只需选择题材和风格,点击"AI 生成"按钮,即可获得一份结构完整的剧本。
本文将从实际开发角度,系统阐述以下内容:
- 如何在 ArkTS 严格模式下构建类型安全的应用
- 如何设计模板驱动的 AI 内容生成引擎
- 如何利用 AppStorage 实现轻量级数据持久化
- ArkUI 声明式 UI 的最佳实践
- 组件化设计与状态管理技巧
无论你是初次接触 HarmonyOS 开发,还是从其他平台迁移过来的经验开发者,都能从本文中获得实用的编码思路。
2. 项目概述与技术栈
2.1 应用功能矩阵
AI 剧本 App 围绕"AI 辅助创作"这一核心场景,提供了一整套创作工具链:
| 功能模块 | 子功能 | 用户交互 |
|---|---|---|
| AI 生成 | 题材选择(10 种)、风格选择(5 种)、参数配置 | 水平滚动选择器、折叠面板、表单输入 |
| 剧本管理 | 剧本库、搜索、收藏、删除 | 列表 + 搜索框 + 标签筛选 |
| 角色管理 | 角色 CRUD、角色卡展示 | 表单编辑 + 卡片展示 |
| 剧本详情 | 剧本阅读、场景导航 | 场景页码导航 + 滚动阅读 |
| AI 续写 | 追加场景 | 一键续写 3 场 |
| 导出分享 | 复制到剪贴板 | pasteboard API |
| 统计面板 | 剧本总数、总字数、收藏数 | 首页 Hero 区域展示 |
2.2 技术栈规格
| 层级 | 技术选型 | 版本/备注 |
|---|---|---|
| 编程语言 | ArkTS | TypeScript 超集,带严格模式约束 |
| UI 框架 | ArkUI | 声明式组件体系 |
| 状态管理 | @State / @Prop / @Link | 组件内响应式 |
| 数据持久化 | AppStorage | 全局 KV 存储,JSON 序列化 |
| 构建系统 | Hvigor | 6.1.0,基于 Gradle 的定制构建 |
| 目标 API | API 24 (HarmonyOS) | Stage 模型 |
| 应用模型 | Stage | 单 Ability + 备份扩展 |
2.3 为何选择模板引擎而非 LLM API?
当前阶段,许多 AI 写作应用都选择接入大语言模型(LLM)API。本项目的"AI"引擎却采用了模板 + 随机组合的技术路线,原因如下:
- 零成本运行:无需 API Key,无需网络请求,完全离线
- 瞬时响应:生成过程在 50ms 内完成(UI 模拟 1200ms 延迟以营造"AI思考"感)
- 内容可控:台词模板经过人工筛选,不存在敏感内容或逻辑断裂
- 开箱即用:用户下载 App 即可使用,无需注册或付费
当然,模板引擎的局限性也很明显——内容重复度较高,缺乏真正的创造性。在未来的版本中,我们计划增加"LLM 模式"作为高级选项。
3. ArkTS 语言与 HarmonyOS 开发环境
3.1 开发环境搭建
API 24 对应 HarmonyOS SDK 的中期版本。推荐使用 DevEco Studio(基于 IntelliJ IDEA 定制)作为 IDE。
关键配置文件:
// hvigor/hvigor-config.json5
{
modelVersion: "6.1.0",
// execution: { ... }
}
// entry/build-profile.json5
{
apiType: "stageMode",
buildOption: {
arkOptions: {
strictMode: "strict" // ArkTS 严格模式
}
}
}
3.2 ArkTS 与 TypeScript 的关键差异
ArkTS 是 TypeScript 的子集,移除了部分动态特性以确保编译期类型安全和运行时性能。以下是必须遵守的规则:
规则 1:禁止解构赋值
// ❌ 编译错误
const { name, age } = person;
// ✅ 正确写法
const name = person.name;
const age = person.age;
这条规则初看繁琐,但它避免了深层嵌套解构带来的隐式引用问题,让数据流向更清晰。
规则 2:禁止展开运算符
// ❌ 编译错误
const newObj = { ...oldObj, extra: true };
// ✅ 正确写法——逐个字段复制
const newObj: MyType = {
field1: oldObj.field1,
field2: oldObj.field2,
extra: true,
};
在本项目的数据存储层中,这条规则的影响尤为明显。每次更新剧本都需要显式复制每个字段:
const updated: Script = {
id: s.id, title: s.title, genre: s.genre, style: s.style,
logline: s.logline, prompt: s.prompt, content: s.content,
characters: s.characters, scenes: s.scenes,
createdAt: s.createdAt, updatedAt: s.updatedAt,
wordCount: s.wordCount, isFavorite: !s.isFavorite, actCount: s.actCount,
};
虽然代码量增加了,但每个字段的赋值都一目了然,减少了因展开运算符隐式合并导致的 bug。
规则 3:禁止 any / unknown
// ❌ 编译错误
let data: any;
// ✅ 必须显式标注类型
let data: string = '';
let count: number = 0;
let result: Script | null = null;
这条规则有效地杜绝了运行时类型错误。在 JSON 解析场景中,我们需要通过类型断言来处理:
export function loadScripts(): Script[] {
try {
const data = AppStorage.get<string>(STORAGE_KEY_SCRIPTS);
if (data) {
const parsed: object = JSON.parse(data);
if (Array.isArray(parsed)) {
const result: Script[] = [];
for (let i = 0; i < parsed.length; i++) {
result.push(parsed[i] as Script);
}
return result;
}
}
} catch (_) {}
return [];
}
注意这里的 as Script 类型断言——ArkTS 允许对已明确类型的变量进行断言,但不允许在不确定的场景下使用。
规则 4:@Builder 内禁止变量声明
// ❌ 编译错误
@Builder
MyView() {
const temp = someValue;
}
// ✅ 逻辑移到 struct 方法中
getFormattedValue(): string {
return someValue.toUpperCase();
}
@Builder
MyView() {
Text(this.getFormattedValue())
}
3.3 严格模式下的额外约束
在 build-profile.json5 中设置 strictMode: "strict" 后,编译器会增加更多检查:
- 索引访问类型禁止:
Script['characters']这种写法不被允许 - 未使用导入报错:导入但未使用的符号会引发警告或错误
- 空值检查增强:使用
!非空断言需确保在前置条件中已判空
4. 项目架构与工程结构
4.1 分层架构设计
┌───────────────────────────────────────────┐
│ UI Layer (ArkUI) │
│ pages/111.ets 主页面 + 6 子页面 │
│ components/ 7 个可复用组件 │
├───────────────────────────────────────────┤
│ State Management Layer │
│ @State / @Prop / @Link │
├───────────────────────────────────────────┤
│ Business Logic Layer │
│ ScriptGenerator AI 生成引擎 │
│ CollectionManager 盲盒管理器 (其他模块) │
├───────────────────────────────────────────┤
│ Data Access Layer │
│ ScriptStorage AppStorage CRUD │
├───────────────────────────────────────────┤
│ HarmonyOS System APIs │
│ pasteboard / preferences / AppStorage │
└───────────────────────────────────────────┘
4.2 工程文件结构
entry/src/main/ets/
├── pages/
│ └── 111.ets # 主入口页面(913 行)
├── components/
│ ├── BlindBoxItem.ets # 盲盒组件
│ ├── FrameThumb.ets # 帧缩略图
│ ├── PlaybackControl.ets # 播放控制
│ ├── ScanResultPanel.ets # AR 识别面板
│ ├── ScriptCard.ets # 剧本卡片(新增)
│ ├── ScriptResultCard.ets # 生成结果卡(新增)
│ ├── StickerCard.ets # 3D 贴纸卡片
│ ├── TimelineBar.ets # 时间轴裁剪
│ └── VideoPreview.ets # 视频预览
├── model/
│ ├── ScriptTypes.ets # 类型定义 + 常量
│ ├── ScriptGenerator.ets # AI 生成引擎(595 行)
│ ├── ScriptStorage.ets # 数据持久化(188 行)
│ ├── CollectionManager.ets # 收藏管理器
│ └── StickerData.ets # 贴纸数据
├── entryability/
│ └── EntryAbility.ets # Ability 生命周期
└── entrybackupability/
└── EntryBackupAbility.ets # 备份扩展
4.3 单组件多页面路由模式
本项目采用了一种略有争议的架构选择——单组件 + 状态驱动的页面路由,而非使用 HarmonyOS 的 router API。
@Entry
@Component
struct AppMain {
@State private page: string = 'home';
build() {
Stack() {
Column() {
if (this.page === 'home') { this.HomePage(); }
else if (this.page === 'generate') { this.GeneratePage(); }
else if (this.page === 'list') { this.ListPage(); }
else if (this.page === 'detail') { this.DetailPage(); }
else if (this.page === 'characters') { this.CharactersPage(); }
else if (this.page === 'char_edit') { this.CharacterEditPage(); }
}
// Toast 浮层
}
}
}
这种模式的优点:
- 所有状态集中在
AppMain中,子页面之间共享数据无需跨路由传参 - 页面切换无动画开销(直接通过条件渲染切换)
- 适合小型应用快速迭代
缺点:
- 单文件较大(本项目 913 行)
- 不适用于超过 10 个页面的大型应用
- @Builder 内无法使用生命周期钩子
5. 类型系统与数据模型设计
5.1 核心类型体系
良好的类型系统是 ArkTS 项目的基石。以下是 AI 剧本的核心类型设计:
// 剧本实体——整个应用的核心数据模型
export interface Script {
id: string; // 唯一标识
title: string; // 标题(由 logline 自动截取)
genre: string; // 题材标识
style: string; // 风格标识
logline: string; // 一句话故事梗概
prompt: string; // 用户自定义提示词
content: string; // 剧本正文(纯文本格式)
characters: ScriptCharacter[]; // 角色列表
scenes: SceneInfo[]; // 场景列表
createdAt: number; // 创建时间戳
updatedAt: number; // 更新时间戳
wordCount: number; // 总字符数
isFavorite: boolean; // 是否收藏
actCount: number; // 幕数
}
// 角色
export interface ScriptCharacter {
id: string; // 角色唯一 ID
name: string; // 角色名
age: string; // 年龄(字符串,如"25岁")
gender: string; // 性别
personality: string; // 性格描述
appearance: string; // 外貌描述(当前保留字段)
background: string; // 背景故事
goals: string; // 角色目标
role: string; // 角色定位:主角/反派/配角/龙套
}
// 场景
export interface SceneInfo {
id: string;
title: string; // 场景标题(如"内景:咖啡馆 - 黄昏")
location: string; // 地点
timeOfDay: string; // 时段
isInterior: boolean; // true=内景,false=外景
actNumber: number; // 所属幕
sceneNumber: number; // 场景序号
summary: string; // 场景摘要
}
5.2 ID 生成策略
在不引入外部 UUID 库的前提下,使用时间戳 + 随机数组合生成唯一 ID:
export function genId(): string {
return 's_' + new Date().getTime() + '_' + Math.floor(Math.random() * 99999);
}
export function charId(): string {
return 'c_' + new Date().getTime() + '_' + Math.floor(Math.random() * 99999);
}
export function sceneId(): string {
return 'sc_' + new Date().getTime() + '_' + Math.floor(Math.random() * 99999);
}
设计考量:
getTime()返回毫秒级时间戳,同一毫秒内可能重复,故加随机数后缀- 使用前缀(
s_/c_/sc_)便于调试时快速区分 ID 类型 - 随机数范围 0-99999,碰撞概率极低
5.3 常量数据组织
题材、风格、角色类型等常量数据集中存在于 ScriptTypes.ets 中,并配有对应的查询函数:
export const GENRE_LIST: GenreItem[] = [
{ id: 'comedy', name: '喜剧', icon: '😄', desc: '轻松幽默、笑中带泪' },
{ id: 'tragedy', name: '悲剧', icon: '😢', desc: '命运无常、深刻感人' },
{ id: 'mystery', name: '悬疑', icon: '🔍', desc: '层层推理、反转不断' },
{ id: 'sci-fi', name: '科幻', icon: '🚀', desc: '未来世界、科技奇想' },
{ id: 'romance', name: '爱情', icon: '💕', desc: '情感纠葛、甜蜜虐心' },
{ id: 'history', name: '历史', icon: '🏛️', desc: '王朝更迭、英雄史诗' },
{ id: 'fantasy', name: '奇幻', icon: '🪄', desc: '魔法世界、冒险传奇' },
{ id: 'horror', name: '恐怖', icon: '👻', desc: '心理恐惧、惊悚氛围' },
{ id: 'action', name: '动作', icon: '💥', desc: '紧张刺激、热血燃爆' },
{ id: 'literary',name: '文艺', icon: '📖', desc: '细腻叙事、艺术表达' },
];
export function genreName(id: string): string {
for (let i = 0; i < GENRE_LIST.length; i++) {
if (GENRE_LIST[i].id === id) return GENRE_LIST[i].name;
}
return id;
}
export function genreIcon(id: string): string {
// 同样的线性查找模式
}
为什么不使用 Record/MAP? 因为 ArkTS 不支持 Map 类型,Record<string, string> 也是一种替代方案,但对于不超过 10 项的列表,线性查找的开销可以忽略不计,且代码更直观。
5.4 时间格式化工具
用户友好的相对时间展示:
export function fmtTime(ts: number): string {
const d = new Date(ts);
const diff = Date.now() - ts;
const m = Math.floor(diff / 60000);
if (m < 60) { return m + '分钟前'; }
const h = Math.floor(diff / 3600000);
if (h < 24) { return h + '小时前'; }
const day = Math.floor(h / 24);
if (day < 30) { return day + '天前'; }
return (d.getMonth() + 1) + '月' + d.getDate() + '日';
}
这段代码实现了"5 分钟前 → 3 小时前 → 2 天前 → 6 月 20 日"的渐进式时间展示,避免了"刚刚"、"昨天"等需要额外字典映射的表述。
6. AI 剧本生成引擎
6.1 引擎架构设计
ScriptGenerator.ets 是整个项目最核心的模块,包含约 600 行代码。其设计理念是 模板驱动 + 随机组合,工作原理如下图所示:
generateScript(params)
│
├── 1. 标题生成
│ 从 logline 截取前 20 字,不足则使用"未命名剧本"
│
├── 2. 角色生成(× characterCount)
│ 随机性别 → 随机姓名 → 随机年龄 → 随机性格 → 设定目标
│
├── 3. 场景列表生成(× sceneCount)
│ 按幕分组 → 随机地点 → 随机时段 → 内景/外景
│
└── 4. 正文生成
├── 角色表章节
├── 逐场景生成内容
│ ├── 动作描述(从题材对应的动作库选取)
│ ├── 对话(从题材对应的对话库选取)
│ └── 舞台说明("灯暗"标记)
└── 全剧终 + 版权信息
6.2 角色生成算法
function generateCharacter(index: number, total: number, genre: string): ScriptCharacter {
const isMale = Math.random() > 0.5;
const names = isMale ? CHINESE_NAMES_MALE : CHINESE_NAMES_FEMALE;
const name = names[Math.floor(Math.random() * names.length)];
const trait = PERSONALITY_TRAITS[Math.floor(Math.random() * PERSONALITY_TRAITS.length)];
const ages = ['20岁', '25岁', '30岁', '35岁', '40岁', '45岁', '50岁', '17岁'];
const age = ages[Math.floor(Math.random() * ages.length)];
// 角色定位规则
let role: string;
if (index === 0) {
role = 'protagonist'; // 第一个角色 = 主角
} else if (index === 1) {
role = total > 2 ? 'antagonist' : 'supporting'; // 第二个 = 反派/配角
} else {
role = 'supporting'; // 其余 = 配角
}
// 角色目标随题材变化
const goalMap: Record<string, string> = {
'comedy': '找到真正的快乐',
'tragedy': '与命运抗争',
'mystery': '揭开真相',
'sci-fi': '拯救人类的未来',
'romance': '找到真爱',
'history': '改变国家的命运',
'fantasy': '守护世界的和平',
'horror': '活着离开',
'action': '拯救世界',
};
goals = goalMap[genre] || '寻找内心的平静';
return {
id: charId(),
name, age,
gender: isMale ? '男' : '女',
personality: trait,
appearance: '',
background: '出生于普通家庭,' + name + '从小就展现出' + trait + '的特质。',
goals, role,
};
}
姓名库的设计:分别维护 15 个男名和 15 个女名,名字风格兼顾现代感与古风(如"陆沉舟"“慕容晴”)。这样的规模足够提供多样性,又不会因为库太大导致"撞名"概率过低——实际上,用户更可能记住出现过的角色名。
性格库:20 个性格词条,覆盖正面(“善良温柔”)到负面(“高傲自负”),以及中性描述(“神秘莫测”)。
6.3 场景地点与时段的题材适配
每个题材有专属的 6 个场景地点,确保风格一致性:
const LOCATIONS: Record<string, string[]> = {
'comedy': ['咖啡馆', '学校教室', '写字楼', '菜市场', '地铁站', '婚宴现场'],
'tragedy': ['废弃医院', '老宅', '悬崖边', '墓地', '雨夜街头', '法院'],
'mystery': ['古堡', '地下室', '密室', '警局', '废弃工厂', '图书馆'],
'sci-fi': ['太空站', '实验室', '虚拟世界', '外星殖民地', '时间管理局', 'AI核心舱'],
'romance': ['海边', '樱花树下', '书店', '音乐厅', '天台', '古镇小巷'],
'history': ['宫殿', '战场', '驿站', '朝堂', '军营', '古城墙'],
'fantasy': ['魔法森林', '龙巢', '精灵神殿', '地下城', '天空之城', '巫师塔'],
'horror': ['闹鬼别墅', '地下室', '荒村', '精神病院', '迷雾森林', '废弃教堂'],
'action': ['摩天大楼', '地下拳场', '高速公路', '码头', '核电站', '驾驶舱'],
'literary':['书房', '花园', '河边', '美术馆', '旧书店', '阁楼'],
};
时段固定为 7 个:['清晨', '正午', '午后', '黄昏', '夜晚', '深夜', '黎明']。
6.4 对话模板系统
每个题材包含 5 组对话模板,每组 2-3 句台词。以喜剧为例:
'comedy': [
['你认真的?这主意比早餐还离谱。', '有时候最离谱的就是最棒的!'],
['我不是在笑你,我是在笑这个世界。', '那我们也够惨的,活在一个笑话里。'],
['你看我像是有$SKILL的人吗?', '不像,但你像是有$SKILL的潜力。'],
['钱不是万能的。', '但没钱是万万不能的——这句话本身就没钱。'],
['你为什么要帮我?', '因为看你倒霉的样子……实在太有喜感了。'],
],
$SKILL 是一个变量占位符,在运行时被替换为随机技能名:
function getRandomSkill(): string {
const skills = ['画画', '唱歌', '编程', '厨艺', '运动', '写作', '弹琴', '跳舞', '做生意', '打架'];
return skills[Math.floor(Math.random() * skills.length)];
}
// 替换后效果:"你看我像是有编程的人吗?"
6.5 动作描述模板
每个题材 6 个动作模板,支持 #NAME# 和 #NAME2# 两个角色名变量:
'horror': [
'#NAME#缓缓转过头——什么都没有。但当#NAME#转回来时,那张脸就在面前。',
'#NAME#听到走廊尽头的房间传来轻轻的脚步声。',
'#NAME#的呼吸在冷空气中化作白雾,虽然暖气明明开着。',
'#NAME#看着镜子,镜中的#NAME#露出一个#NAME#没有做的表情。',
'#NAME#用手机照亮黑暗,光线照到了一双脚——悬在空中。',
'#NAME#猛地醒来,喘着粗气。但那声音还在——从现实的门外传来。',
],
模板替换函数通过字符串的 replace 方法实现:
function replaceNames(template: string, chars: ScriptCharacter[]): string {
let result = template;
if (chars.length > 0) {
result = result.replace(/#NAME#/g, chars[0].name);
}
if (chars.length > 1) {
result = result.replace(/#NAME2#/g, chars[1].name);
} else if (chars.length > 0) {
result = result.replace(/#NAME2#/g, chars[0].name);
}
return result;
}
6.6 正文生成流水线
function generateScriptContent(
genre: string, style: string,
chars: ScriptCharacter[], scenes: SceneInfo[],
includeStageDirections: boolean,
): string {
const lines: string[] = [];
const dialogues = DIALOGUE_TEMPLATES[genre] || DIALOGUE_TEMPLATES['comedy'];
const actions = ACTION_TEMPLATES[genre] || ACTION_TEMPLATES['comedy'];
// STEP 1: 标题
lines.push('《' + getGenreTitle(genre) + '》');
// STEP 2: 角色表
lines.push('【角色表】');
for (let ci = 0; ci < chars.length; ci++) {
lines.push(getRoleLabel(chars[ci]) + ' ' + chars[ci].name + ' ...');
}
// STEP 3: 逐场景生成
for (let si = 0; si < scenes.length; si++) {
// 幕分隔符
if (scene.actNumber !== currentAct) { ... }
// 场景标题
lines.push(scene.sceneNumber + ' ' + scene.title);
// 交替生成动作 + 对话(3-6 轮次)
for (let step = 0; step < 3 + random(4); step++) {
if (step % 2 === 0 && includeStageDirections) {
// 从动作库随机选一条,替换角色名
}
// 从对话库随机选一组,分配说话者
}
if (includeStageDirections) lines.push('(灯暗)');
}
// STEP 4: 结尾
lines.push('【全剧终】');
return lines.join('\n');
}
场景内生成逻辑:每个场景生成 3-6 步(3 + random(4)),交替输出动作描述和对话。这样既不会让场景太短显得空洞,也不会太长导致阅读疲劳。
6.7 续写功能
export function continueScript(existingScript: Script, additionalScenes: number): Script {
// 1. 复制已有场景
const newScenes: SceneInfo[] = [];
for (let i = 0; i < existingScript.scenes.length; i++) {
newScenes.push(existingScript.scenes[i]);
}
// 2. 追加新场景
for (let i = 0; i < additionalScenes; i++) {
newScenes.push({
// 新场景 ID、新的标题
actNumber: existingScript.actCount,
sceneNumber: baseNum + i + 1,
// ...
});
}
// 3. 用全部场景(新旧)重新生成正文
const fullContent = generateScriptContent(
existingScript.genre, existingScript.style,
existingScript.characters, newScenes, true,
);
// 4. 返回更新后的 Script(保留原 id/createdAt)
return { /* ... */ };
}
注意:续写是以全量重生成的方式工作的——不是追加文本,而是用扩增后的场景列表重新调用 generateScriptContent。这样新旧场景的对话风格一致,但代价是之前的台词内容会发生变化。
6.8 导出功能
支持 TXT 和 Markdown 两种格式:
export function exportScript(script: Script, format: string): string {
if (format === 'txt') {
return script.content; // 直接返回正文
}
// Markdown 格式——结构化导出
let md = '# 《' + script.title + '》\n\n';
md += '> **题材**:' + genreName(script.genre) + '\n';
md += '> **角色数**:' + script.characters.length + '\n';
md += '---\n\n## 角色表\n\n';
for (let i = 0; i < script.characters.length; i++) {
md += '- **' + c.name + '**(' + c.age + '):' + c.personality + '\n';
}
md += '\n---\n\n## 剧本正文\n\n' + script.content;
md += '\n\n*由 AI剧本 App 生成*\n';
return md;
}
配合 pasteboard API 实现一键导出到剪贴板:
exportText(text: string): void {
try {
const data = pasteboard.createData({ 'text/plain': text });
pasteboard.getSystemPasteboard().setData(data);
this.showToastMsg('已复制到剪贴板');
} catch (_) {
this.showToastMsg('导出失败');
}
}
7. 数据持久化层:AppStorage 实战
7.1 AppStorage 简介
HarmonyOS 的 AppStorage 是一个全局的、响应式的键值存储系统。与传统的 SharedPreferences 或 localStorage 相比,它的特点在于:
- 全局访问:任何组件、任何模块都可以读写
- 响应式绑定:当存储值变化时,绑定的 UI 会自动更新
- 同步 API:读写操作是同步的,无需 await
7.2 存储层设计
const STORAGE_KEY_SCRIPTS = 'ai_script_list_v3';
export function loadScripts(): Script[] {
try {
const data = AppStorage.get<string>(STORAGE_KEY_SCRIPTS);
if (data) {
const parsed: object = JSON.parse(data);
if (Array.isArray(parsed)) {
const result: Script[] = [];
for (let i = 0; i < parsed.length; i++) {
result.push(parsed[i] as Script);
}
return result;
}
}
} catch (_) {}
return [];
}
版本化 Key:STORAGE_KEY_SCRIPTS 使用 _v3 后缀。当数据模型发生不兼容变更时(比如新增字段、修改字段类型),只需递增版本号,旧数据会被自动忽略(JSON.parse 返回空数组)。这是最简单的数据迁移方案。
7.3 CRUD 操作——全量更新模式
所有写操作都遵循"读取全量 → 修改 → 写入全量"的模式:
export function toggleFavorite(scriptId: string): Script[] {
const list = loadScripts(); // 1. 读取全量
const result: Script[] = []; // 2. 构建新数组
for (let i = 0; i < list.length; i++) {
const s = list[i];
if (s.id === scriptId) {
// 3. 修改目标项(显式复制所有字段)
const updated: Script = {
id: s.id, title: s.title, genre: s.genre, style: s.style,
logline: s.logline, prompt: s.prompt, content: s.content,
characters: s.characters, scenes: s.scenes,
createdAt: s.createdAt, updatedAt: s.updatedAt,
wordCount: s.wordCount, isFavorite: !s.isFavorite, actCount: s.actCount,
};
result.push(updated);
} else {
result.push(s);
}
}
saveScripts(result); // 4. 写入全量
return result;
}
这种模式在数据量不大(通常 < 100 条记录)时完全够用。如果未来需要扩展到数千条,可以考虑引入索引或分页机制。
7.4 统计聚合
export function getStats(): ScriptStats {
const list = loadScripts();
let totalWords = 0;
let favoriteCount = 0;
const genreMap: Record<string, number> = {};
for (let i = 0; i < list.length; i++) {
const s = list[i];
totalWords += s.wordCount;
if (s.isFavorite) favoriteCount++;
genreMap[s.genre] = (genreMap[s.genre] || 0) + 1;
}
// 按更新时间排序取前 5
const sorted = [...list];
sorted.sort((a, b) => b.updatedAt - a.updatedAt);
const recentScripts = sorted.slice(0, 5);
return {
totalScripts: list.length,
totalWords, favoriteCount,
genreDistribution: Object.keys(genreMap).map(k => ({ genre: k, count: genreMap[k] })),
recentScripts,
};
}
统计函数在首页 aboutToAppear 和每次 CRUD 操作后调用,刷新首页的统计摘要。
8. UI 层:ArkUI 声明式界面构建
8.1 设计语言与色彩体系
| Token | 色值 | 用途 |
|---|---|---|
| 深色主色 | #1A1A2E |
导航栏、按钮、标题文字 |
| 紫色辅色 | #5B6ABF |
标签、链接、操作按钮 |
| 页面背景 | #F5F6FA |
全局页面背景 |
| 绿色成功 | #2ED573 |
完成状态、续写标签 |
| 橙色警告 | #FF9F43 |
角色计数、风格标签 |
| 红色危险 | #FF6B81 |
删除按钮 |
8.2 Hero 区域设计
首页顶部采用深色背景的品牌区域,包含 App 名称、副标题和三个快捷入口:
@Builder
HomePage() {
Column() {
// Hero 区域
Column() {
Row() {
Column() {
Text('AI 剧本').fontSize(28).fontWeight(FontWeight.Bold).fontColor(Color.White)
Text('创作你的故事').fontSize(13).fontColor('rgba(255,255,255,0.7)')
}
Text('🎭').fontSize(36)
}
Row() {
this.QBtn('✨', '新剧本')
this.QBtn('📋', '剧本库')
this.QBtn('♥', '收藏')
}
}
.backgroundColor('#1A1A2E')
.borderRadius({ bottomLeft: 24, bottomRight: 24 })
// ...
}
}
borderRadius 只给底部两个角设置圆角(bottomLeft / bottomRight),这是 ArkUI 支持的非对称圆角语法。
8.3 题材网格 Grid
使用 Grid 组件实现 5 列等宽的题材选择器:
Grid() {
ForEach(GENRE_LIST, (g: GenreItem) => {
GridItem() {
Column() {
Text(g.icon).fontSize(28)
Text(g.name).fontSize(12).fontColor('#444')
Text(g.desc).fontSize(9).fontColor('#999')
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
}
.backgroundColor(Color.White).borderRadius(12)
.onClick(() => {
this.genGenreIdx = this.getGenreIndex(g.id);
this.page = 'generate';
})
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.rowsGap(8).columnsGap(8).padding({ left: 16, right: 16 })
columnsTemplate 使用 fr(fraction)单位实现等分,类似 CSS Grid 的 1fr。
8.4 水平滚动选择器
题材和风格的选择器都使用了嵌套的 Scroll(ScrollDirection.Horizontal) 实现水平滚动:
Scroll() {
Row() {
ForEach(STYLE_LIST, (s: StyleItem, i: number) => {
Text(s.name)
.fontColor(i === this.genStyleIdx ? Color.White : '#666')
.backgroundColor(i === this.genStyleIdx ? '#1A1A2E' : '#F0F0F0')
.borderRadius(16)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.onClick(() => { this.genStyleIdx = i; })
})
}
}
.scrollable(ScrollDirection.Horizontal).width('100%').height(36)
选中项使用反色高亮(白色文字 + 深色背景),未选中项使用灰色背景。这种"胶囊"样式的选择器在移动端非常常见。
8.5 折叠高级设置面板
高级设置通过 @State genShowAdvanced 控制展开/折叠:
Row() {
Text('⚙️ 高级设置')
Blank()
Text(this.genShowAdvanced ? '收起 ▲' : '展开 ▼')
}
.onClick(() => { this.genShowAdvanced = !this.genShowAdvanced; })
if (this.genShowAdvanced) {
this.GenAdvancedSettings()
}
GenAdvancedSettings 是一个独立的 @Builder,包含角色数、场次数、幕数的 +/- 调节器,以及复选框选项和自定义提示词输入框。
8.6 搜索与筛选
剧本列表页集成了搜索和题材筛选功能:
getFilteredList(): Script[] {
const all = loadScripts();
let tempList: Script[] = [];
// 收藏筛选 或 题材筛选 或 全部
if (this.listShowFav) {
for (let i = 0; i < all.length; i++) {
if (all[i].isFavorite) tempList.push(all[i]);
}
} else if (this.listTab === 'all') {
// 全部
for (let i = 0; i < all.length; i++) { tempList.push(all[i]); }
} else {
for (let i = 0; i < all.length; i++) {
if (all[i].genre === this.listTab) tempList.push(all[i]);
}
}
// 关键词搜索(大小写不敏感)
if (this.listSearch.trim()) {
const kw = this.listSearch.toLowerCase();
const filtered: Script[] = [];
for (let i = 0; i < tempList.length; i++) {
const s = tempList[i];
if (s.title.toLowerCase().includes(kw) ||
s.logline.toLowerCase().includes(kw)) {
filtered.push(s);
}
}
return filtered;
}
return tempList;
}
这里使用 includes 而非正则表达式进行搜索,确保对中文内容的正确匹配,同时避免正则注入风险。
9. 组件化设计与回调传递
9.1 从内联 @Builder 到独立组件
在项目演进过程中,我们将部分 UI 从 @Builder 内联实现重构为独立的 @Component 组件。以剧本卡片为例:
@Builder 内联版本(旧):
@Builder
ScriptCardSmall(script: Script) {
Row() { /* 卡片 UI */ }
.onClick(() => { /* 跳转详情 */ })
}
独立组件版本(新):
@Component
export struct ScriptCard {
@Prop script: Script = { /* 默认值 */ };
@State private isFav: boolean = false;
onTap: (script: Script) => void = () => {};
onFav: (scriptId: string) => void = () => {};
build() {
Column() {
// 卡片内容
}
.onClick(() => { this.onTap(this.script); })
}
}
9.2 ArkTS 中的回调传递模式
在 ArkTS 中,不能在组件构造后链式调用 setter 方法——因为组件构造函数返回 void。正确的做法是直接在构造函数中传递回调:
// ❌ 错误模式——ArkTS 编译器会报错
ScriptCard({ script: item })
.setOnTap((script) => { ... })
.setOnFav((id) => { ... })
// ✅ 正确模式——在构造参数中直接传递
ScriptCard({
script: item,
onTap: (script: Script) => {
this.detailScript = script;
this.page = 'detail';
},
onFav: (scriptId: string) => {
this.scripts = toggleFavorite(scriptId);
this.refreshStats();
}
})
关键技巧:将回调定义为组件类的公开属性(不加 private),ArkUI 框架会自动将这些属性识别为可构造参数。
// ScriptCard 组件定义
export struct ScriptCard {
@Prop script: Script = {} as Script;
onTap: (script: Script) => void = () => {}; // 公开属性,可构造传参
onFav: (scriptId: string) => void = () => {}; // 同上
}
9.3 ScriptResultCard 组件
生成结果卡片组件的完整实现:
@Component
export struct ScriptResultCard {
@Prop script: Script = {} as Script;
onRegenerate: () => void = () => {}; // "重新生成" 回调
onSave: (script: Script) => void = () => {}; // "保存" 回调
build() {
Column() {
// 完成状态
Row() {
Text('✅ 生成完成').fontColor('#2ED573')
}
// 统计数据(角色数、场景数、幕数、字数)
Row() {
this.StatItem(this.script.characters.length.toString(), '角色')
this.StatItem(this.script.scenes.length.toString(), '场景')
this.StatItem(this.script.actCount.toString(), '幕')
this.StatItem(this.getWordCountStr(this.script.wordCount), '字数')
}
// 操作按钮
Row() {
Button() { Text('🔄 重新生成') }
.onClick(() => { this.onRegenerate(); })
Button() { Text('💾 保存并查看') }
.onClick(() => { this.onSave(this.script); })
}
}
}
@Builder
StatItem(value: string, label: string) {
Column() {
Text(value).fontSize(20).fontWeight(FontWeight.Bold)
Text(label).fontSize(10)
}
}
}
10. 状态管理深入:@State / @Prop / @Link
10.1 三级响应式状态体系
HarmonyOS ArkUI 提供三种状态装饰器,覆盖从本地状态到跨组件共享的全部场景:
| 装饰器 | 作用域 | 特点 |
|---|---|---|
@State |
当前组件 | 私有状态,变化触发 UI 重绘 |
@Prop |
父→子单向 | 父组件传入,子组件可读不可写(写会同步回父?实际上 @Prop 是单向的,子组件修改不会同步到父) |
@Link |
父↔子双向 | 父子组件共享同一状态,任何一方修改都会同步 |
10.2 @State 实践
本项目中 @State 是使用最广泛的装饰器。所有页面级状态都定义在 AppMain struct 中:
@State private page: string = 'home'; // 当前页面
@State private scripts: Script[] = []; // 剧本列表
@State private statsStr: string = '加载中...'; // 统计摘要
// 生成页状态
@State private genGenreIdx: number = 0; // 选中的题材索引
@State private genStyleIdx: number = 0; // 选中的风格索引
@State private genLoading: boolean = false; // 生成加载中
@State private genResult: Script | null = null; // 生成结果
// 列表页状态
@State private listTab: string = 'all'; // 题材筛选
@State private listSearch: string = ''; // 搜索关键词
// 详情页状态
@State private detailScript: Script | null = null;
@State private detailSceneIdx: number = 0;
设计原则:所有跨 @Builder 共享的状态都定义为 @State,确保任何一个 @Builder 中对状态的修改都能触发其他 @Builder 的 UI 更新。
10.3 @Prop 实践
@Prop 用于父组件向子组件传递数据:
// 父组件(111.ets)
ScriptCard({ script: item })
// 子组件(ScriptCard.ets)
@Component
export struct ScriptCard {
@Prop script: Script = {} as Script;
// ...
}
对于需要默认值的 @Prop,必须提供一个完整的默认对象。这里使用了类型断言 as Script 来告诉编译器这个对象字面量符合 Script 接口:
@Prop script: Script = {
id: '', title: '', genre: 'comedy', style: 'classic',
logline: '', prompt: '', content: '',
characters: [], scenes: [],
createdAt: 0, updatedAt: 0, wordCount: 0,
isFavorite: false, actCount: 3,
} as Script;
10.4 状态更新触发 UI 重绘的注意事项
直接修改数组元素不会触发重绘:
// ❌ 不会触发 UI 更新
this.scripts[0].title = '新标题';
// ✅ 会触发 UI 更新——创建新数组替换旧数组
const newList: Script[] = [];
for (let i = 0; i < this.scripts.length; i++) {
if (i === 0) {
newList.push({ ...this.scripts[i], title: '新标题' });
} else {
newList.push(this.scripts[i]);
}
}
this.scripts = newList;
这就是存储层函数全部返回新数组而非修改原数组的原因:
export function toggleFavorite(scriptId: string): Script[] {
const list = loadScripts();
const result: Script[] = []; // 新数组
// ... 构建新数组
saveScripts(result);
return result; // 返回新引用
}
10.5 Toast 轻提示的实现
本项目实现了一个轻量级的 Toast 提示系统,无需引入第三方库:
@State private toastMsg: string = '';
@State private showToast: boolean = false;
showToastMsg(msg: string): void {
this.toastMsg = msg;
this.showToast = true;
setTimeout(() => { this.showToast = false; }, 2000);
}
// 在 build 中渲染
if (this.showToast) {
Text(this.toastMsg)
.fontSize(14).fontColor(Color.White)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.backgroundColor('rgba(0,0,0,0.8)').borderRadius(20)
.position({ x: '50%', y: '88%' }).translate({ x: '-50%' }).zIndex(999)
}
11. 用户交互与体验优化
11.1 加载状态与模拟延迟
为了营造"AI 正在思考"的体验,在模板引擎实际只需几十毫秒的前提下,特意添加了 1200ms 的模拟延迟:
doGenerate(): void {
this.genLoading = true;
this.genShowResult = false;
setTimeout(() => {
const script = generateScript({ /* params */ });
this.genResult = script;
this.genShowResult = true;
this.genLoading = false;
}, 1200); // 模拟 AI 生成延迟
}
生成按钮在加载期间显示 LoadingProgress 并禁用点击:
Button() {
if (this.genLoading) {
LoadingProgress().width(22).height(22).color(Color.White)
} else {
Text('🚀 AI 生成剧本')
}
}
.enabled(!this.genLoading)
11.2 确认对话框
删除操作使用系统 AlertDialog 提供二次确认:
AlertDialog.show({
title: '删除剧本',
message: '确定删除《' + sc.title + '》吗?',
primaryButton: { value: '取消', action: () => {} },
secondaryButton: {
value: '删除', fontColor: '#FF6B81',
action: () => {
this.scripts = deleteScript(sc.id);
this.page = 'list';
this.showToastMsg('已删除');
},
},
});
11.3 列表空态设计
当用户还没有剧本时,展示友好的空态提示和引导操作:
if (this.scripts.length === 0) {
Column() {
Text('📝').fontSize(48)
Text('还没有剧本,点击上方开始创作').fontSize(14).fontColor('#999')
.margin({ top: 8 })
}
.width('100%').padding({ top: 40, bottom: 40 })
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
}
11.4 收藏反馈
收藏按钮的即时视觉反馈:
Text(this.isFav ? '❤️' : '🤍').fontSize(18)
.onClick(() => {
this.isFav = !this.isFav;
this.onFav(this.script.id);
})
点击后图标立即从空心变为实心,同时触发持久化操作。这种即时视觉反馈(乐观更新)让用户感觉操作响应迅速。
12. 测试策略与质量保障
12.1 测试架构
测试文件结构:
entry/src/
├── test/
│ ├── List.test.ets # 集成测试
│ └── LocalUnit.test.ets # 单元测试
├── ohosTest/
│ └── ets/test/
│ └── Ability.test.ets # 鸿蒙原生测试
└── mock/
└── mock-config.json5 # Mock 配置
12.2 单元测试用例
覆盖 AI 引擎的核心逻辑:
// ScriptGeneratorTest
generateScript_should_return_valid_script
→ 验证角色数 = characterCount
→ 验证场景数 = sceneCount
→ 验证幕数 = actCount
→ 验证 wordCount > 100
→ 验证 isFavorite = false
generateScript_all_genres
→ 验证全部 10 种题材都能正常生成
continueScript_should_add_scenes
→ 验证续写后场景数 = 原数 + additionalScenes
→ 验证 id 不变、updatedAt 更新
exportScript_txt_format → 验证 TXT 格式正确
exportScript_markdown_format → 验证 Markdown 包含标题和角色表
genId_should_be_unique
→ 验证 100 次生成的 ID 无重复
12.3 集成测试用例
// ScriptIntegrationTest
create_and_load_script
→ 创建剧本 → 检查列表中存在
toggle_favorite
→ 收藏 → 验证 isFavorite = true → 取消收藏 → 验证 isFavorite = false
delete_script
→ 删除 → 验证 getScript 返回 null
get_stats
→ 创建前后对比 stats.totalScripts 增加
12.4 Mock 策略
使用 @ohos/hamock 模拟系统 API:
// mock-config.json5
{
"mocks": {
"pasteboard": {
"createData": { "return": {} },
"getSystemPasteboard": {
"return": { setData: () => true }
}
}
}
}
13. 构建与预览配置
13.1 Hvigor 构建配置
// hvigor/hvigor-config.json5
{
modelVersion: "6.1.0",
execution: {
// "analyze": "normal",
// "daemon": true,
// "incremental": true,
},
}
13.2 模块配置
// entry/build-profile.json5
{
apiType: "stageMode",
buildOption: {
arkOptions: {
strictMode: "strict"
}
},
targets: [
{
name: "default",
runtimeOS: "HarmonyOS"
}
]
}
13.3 页面路由注册
// resources/base/profile/main_pages.json
{
profile: {
main_pages: {
src: ["pages/111"]
}
}
}
HarmonyOS 的页面路由基于文件名注册。"pages/111" 对应 ets/pages/111.ets。
13.4 预览构建常见问题
预览构建(Preview)与真机构建共享大部分流程,但有以下区别:
- 预览使用
FakeUIAbility而非真实 Ability - 预览无法调用部分系统 API(如 camera)
- 预览的构建产物输出到
entry/.preview/目录
14. 踩坑记录与 ArkTS 合规要点
14.1 错误 1:组件回调链式调用
症状:编译错误 Property 'setOnFav' does not exist on type 'void'
根因:ArkTS 中组件构造函数的返回值是 void,不是组件实例。因此 Component({...}).setXxx() 的链式调用模式不合法。
// ❌ 错误
ScriptCard({ script: item })
.setOnTap(() => {})
.setOnFav(() => {})
// ✅ 正确
ScriptCard({
script: item,
onTap: () => {},
onFav: () => {},
})
解决方案:将回调定义为组件的公开属性(不加 private),在构造时直接传入。
14.2 错误 2:索引访问类型
症状:编译错误 Indexed access types are not supported (arkts-no-aliases-by-index)
根因:Script['characters'] 这种通过索引访问类型的语法在 ArkTS 中不被支持。
// ❌ 错误
getCharNames(chars: Script['characters']): string { }
// ✅ 正确
getCharNames(chars: ScriptCharacter[]): string { }
14.3 错误 3:! 非空断言的使用限制
! 非空断言在 ArkTS 中是允许的,但必须在编译器可以验证的空值检查之后使用:
// 正确(先判断非 null,再使用 !)
if (this.genShowResult && this.genResult !== null) {
ScriptResultCard({ script: this.genResult!, ... })
}
14.4 错误 4:@Builder 内逻辑限制
@Builder 函数内部不允许声明变量、执行赋值操作或使用循环语句。所有逻辑计算应提取到 struct 的普通方法中:
// ❌ 错误
@Builder
MyView() {
const x = this.calculate();
Text(x.toString())
}
// ✅ 正确
@Builder
MyView() {
Text(this.getCalculatedValue())
}
getCalculatedValue(): string {
return this.calculate().toString();
}
14.5 错误 5:数组/对象引用不变不触发重绘
@State 通过引用比较(=== 或 Object.is)判断是否变化。修改数组元素或对象属性而不改变引用,不会触发 UI 重绘:
// ❌ 不会触发重绘
this.scripts[0].title = '新标题';
// ✅ 创建新数组
const newList: Script[] = [];
for (let i = 0; i < this.scripts.length; i++) {
newList.push(this.scripts[i]);
}
newList[0] = { ...this.scripts[0], title: '新标题' };
this.scripts = newList;
14.6 设计决策日志
| 决策点 | 选项 | 选择 | 理由 |
|---|---|---|---|
| 页面路由 | Navigation vs 条件渲染 | 条件渲染 | 小应用,状态共享简单 |
| AI 引擎 | LLM API vs 模板引擎 | 模板引擎 | 离线可用、零成本、可控 |
| 数据存储 | RDB vs AppStorage | AppStorage | 数据结构简单,查询需求低 |
| ID 生成 | UUID vs 时间戳+随机 | 时间戳+随机 | 无外部依赖 |
| 状态管理 | 全局 Store vs @State | @State | 规模小,无需 Redux 模式 |
| 回调传递 | Setter 链式 vs 构造参数 | 构造参数 | ArkTS 语法限制 |
| 导出格式 | PDF vs TXT vs MD | TXT + MD | 实现简单 |
15. 总结与展望
15.1 项目回顾
通过本文,我们完整走了一遍 HarmonyOS API 24 上 AI 剧本创作 App 的开发流程:
- 类型系统设计:定义了
Script、ScriptCharacter、SceneInfo等核心接口,构建了强类型的 ArkTS 应用 - AI 生成引擎:基于模板 + 随机组合的离线生成器,支持 10 种题材 × 5 种风格
- 数据持久化:使用 AppStorage 实现剧本的 CRUD 操作和统计聚合
- UI 构建:通过 ArkUI 声明式语法实现 6 个页面的完整交互
- 组件化:提取
ScriptCard和ScriptResultCard为独立组件,通过构造参数传递回调 - 测试覆盖:9 个测试用例覆盖引擎核心逻辑和存储操作
15.2 可扩展方向
1. 接入 LLM API
将模板引擎替换或增强为调用大语言模型 API,实现真正的 AI 创作。可以设计为"快速模式(模板)"和"深度模式(LLM)"两种选项。
2. 云端同步
基于 HarmonyOS 的云开发能力,实现多设备之间的剧本同步。使用 @kit.NetworkKit 进行云端数据存取。
3. 分布式协作
利用 HarmonyOS 的分布式框架,让多个用户在同一剧本上进行实时协作编辑。
4. 富文本排版
当前剧本内容使用纯文本渲染。可以引入富文本组件,支持字体大小、颜色、粗体等排版功能,让剧本阅读体验更佳。
5. AI 配图
根据剧本的场景描述,使用 AI 绘图模型自动生成场景插画。这可以结合 media API 在 App 内直接展示。
6. 语音朗读
集成 TTS(Text-to-Speech)能力,让 App 朗读剧本对话,模拟"听剧"体验。
15.3 结语
构建 AI 辅助的创意工具,技术实现只是第一步。真正重要的是理解创作者的 workflow,在合适的位置融入 AI 能力。
本项目中的"AI"并非真正的深度学习模型,而是一个精心设计的模板+随机引擎——但这恰恰说明了一个重要观点:AI 辅助体验的核心不在于模型有多强,而在于产品设计能否降低用户的创作门槛,激发他们的表达欲望。
当用户选定题材、调整参数、点击"AI 生成",看到属于自己的剧本跃然屏上时,那种"创作完成"的成就感,无论是来自大模型还是模板引擎,都不会有区别。
附录
A. 关键源码文件索引
| 文件 | 行数 | 职责 |
|---|---|---|
ets/pages/111.ets |
913 | 主页面(6 个子页面路由) |
ets/model/ScriptGenerator.ets |
601 | AI 剧本生成引擎 |
ets/model/ScriptStorage.ets |
188 | AppStorage 持久化 |
ets/model/ScriptTypes.ets |
206 | 类型定义 + 常量 + 工具函数 |
ets/components/ScriptCard.ets |
56 | 剧本卡片组件 |
ets/components/ScriptResultCard.ets |
77 | 生成结果卡片组件 |
ets/entryability/EntryAbility.ets |
39 | Ability 生命周期 |
B. 依赖清单
@ohos/hamock@1.0.0 # 测试 Mock 框架
@ohos/hypium@1.0.25 # 测试运行框架
@kit.BasicServicesKit # 基础服务(pasteboard 等)
@kit.ArkData # 数据存储 API
@kit.AbilityKit # Ability 框架
@kit.PerformanceAnalysisKit # 日志(hilog)
C. 参考资源
本文基于 demo012 项目中的 AI 剧本模块编写
平台: HarmonyOS API 24 | 语言: ArkTS | 构建: Hvigor 6.1.0
许可: MIT License
最后更新: 2026-06-20
更多推荐



所有评论(0)