鸿蒙原生 ArkTS 开发实战:构建「文案自动写」—— 模板引擎与智能创作应用
鸿蒙原生 ArkTS 开发实战:构建「文案自动写」—— 模板引擎与智能创作应用


一、引言
1.1 创作背景
在内容为王的时代,文案写作已成为每个人日常工作与生活中不可或缺的技能——电商运营需要写商品描述、市场人员需要推敲广告语、社交媒体用户需要构思朋友圈文案、每个人都需要在节日给亲友发送祝福。然而,并非所有人都擅长文字创作,面对一张白纸时的「写作恐惧症」普遍存在。
「文案自动写」正是为解决这一痛点而生。基于 HarmonyOS NEXT 6.1.1(API 24)平台,利用 ArkTS 声明式 UI 框架和模板引擎技术,它能在用户输入关键词后,从 70+ 条专业文案模板中智能匹配并生成高质量的成品文案。本文将从架构设计、模板引擎、UI 实现、剪贴板交互、收藏系统等维度,完整还原这款应用的开发全过程。
1.2 为什么选择 ArkTS 构建文本类应用
文案生成类应用的核心交互是「输入 → 处理 → 输出」,辅以分类切换、收藏管理等操作。ArkTS 的特性与之高度契合:
- 响应式状态管理:
@State装饰器让输入值、生成结果、收藏列表等状态变更自动驱动 UI 刷新,无需手动操作 DOM - 声明式 UI 组合:通过 Column、Row、Scroll、List 等布局组件快速搭建清晰的信息层级
- TextInput 输入组件:原生支持键盘输入、占位符、值变化监听,完美匹配关键词输入场景
- List 虚拟滚动:收藏列表使用 List + ForEach 实现长列表的高效渲染
- 剪贴板 API:
pasteboard模块提供系统级剪贴板读写能力
1.3 应用功能总览
| 功能模块 | 子功能 | 技术实现 |
|---|---|---|
| 分类导航 | 9 大类文案(广告/朋友圈/商品/祝福/励志/爱情/搞笑/美食/旅行) | Scroll + ForEach 横向滚动 |
| 风格切换 | 简约/幽默/文艺/正式 四种语气 | 状态驱动的正则后处理 |
| 关键词输入 | 用户输入主题词,自动匹配默认词 | TextInput + onChange |
| 文案生成 | 随机选取模板 + 40+ 变量动态填充 | 模板引擎 + setTimeout 模拟生成 |
| 一键复制 | 复制文案到系统剪贴板 | pasteboard API |
| 收藏管理 | 添加/取消收藏、独立收藏列表页 | 数组增删 + 条件渲染 |
| 统计系统 | 生成次数、收藏数量实时计数 | @State 响应式计数 |
二、项目结构与架构设计
2.1 文件组织
entry/src/main/ets/pages/
├── Index.ets # 创意工坊首页(双应用入口)
├── OneClickDraw.ets # 一键生成画图
└── CopyWriting.ets # 文案自动写(新建,759 行)
2.2 页面路由注册
在 main_pages.json 中注册所有页面:
{
"src": [
"pages/Index",
"pages/OneClickDraw",
"pages/CopyWriting"
]
}
在 CopyWriting.ets 中通过 router.back() 返回首页,在首页通过 router.pushUrl({ url: 'pages/CopyWriting' }) 进入本页面。
2.3 分层架构设计
应用代码按职责清晰分层:
┌─────────────────────────────────────────┐
│ 类型定义层 (Interfaces) │
│ WritingCategory / ToneStyle / │
│ TemplateItem / FavoriteItem │
├─────────────────────────────────────────┤
│ 数据层 (Templates) │
│ AD_TEMPLATES / MOMENT_TEMPLATES / ... │
│ (预定义的 70+ 条模板数据) │
├─────────────────────────────────────────┤
│ UI 层 (build 方法) │
│ 导航栏 / 分类选择器 / 风格切换 / │
│ 输入框 / 结果展示 / 收藏列表 │
├─────────────────────────────────────────┤
│ 业务逻辑层 (Methods) │
│ generateWriting / fillTemplate / │
│ copyToClipboard / toggleFavorite │
└─────────────────────────────────────────┘
三、类型系统:用接口定义数据结构
3.1 类型定义
ArkTS 的静态类型系统是代码质量的基石。整个应用围绕四个核心接口展开:
/** 文案分类 */
interface WritingCategory {
id: string; // 唯一标识,如 'ad', 'moment'
name: string; // 显示名称,如 '广告文案'
icon: string; // Emoji 图标,如 '📢'
color: string; // 主题色,如 '#FF4757'
}
/** 语气风格 */
interface ToneStyle {
id: string; // 'normal' | 'humorous' | 'literary' | 'formal'
name: string; // '简约' | '幽默' | '文艺' | '正式'
icon: string; // '📝' | '😄' | '🌸' | '📋'
}
/** 模板项 */
interface TemplateItem {
title: string; // 模板标题,如 '品质宣言'
content: string; // 模板内容,含 {{keyword}} 等占位符
tags: string[]; // 标签数组,如 ['品质', '高端']
}
/** 收藏项 */
interface FavoriteItem {
id: number; // 唯一标识
category: string; // 所属分类 ID
content: string; // 已填充的完整文案
time: string; // 收藏时间
}
设计考量:
WritingCategory将分类的展示属性(icon、name、color)与业务标识(id)解耦,便于循环渲染时直接绑定样式TemplateItem.content使用双花括号占位符{{keyword}}——这是模板引擎的核心约定FavoriteItem额外保存category字段,便于在收藏列表中显示分类标签
3.2 分类数据
private categories: WritingCategory[] = [
{ id: 'ad', name: '广告文案', icon: '📢', color: '#FF4757' },
{ id: 'moment', name: '朋友圈', icon: '💬', color: '#FF6B81' },
{ id: 'product', name: '商品描述', icon: '🛍️', color: '#FFA502' },
{ id: 'greeting',name: '节日祝福', icon: '🎉', color: '#2ED573' },
{ id: 'inspire', name: '励志语录', icon: '💪', color: '#1E90FF' },
{ id: 'love', name: '爱情语录', icon: '💕', color: '#A55EEA' },
{ id: 'funny', name: '搞笑段子', icon: '😂', color: '#FF6348' },
{ id: 'food', name: '美食文案', icon: '🍜', color: '#E84393' },
{ id: 'travel', name: '旅行文案', icon: '✈️', color: '#00CEC9' },
];
九种分类覆盖了文案写作最常见的场景,每种配以独特的 Emoji 图标和品牌色。这些颜色在 UI 中以两种形式出现:
- 选中状态:作为边框色和高亮背景色(
color + '18'表示叠加 18% 透明度) - 未选中状态:灰色降级,以示未激活
四、模板引擎:从占位符到完整文案
4.1 模板数据设计
模板是应用的核心资产。每个分类预定义 6~10 条模板,总计 72 条。以广告文案为例:
const AD_TEMPLATES: TemplateItem[] = [
{
title: '品质宣言',
content: '{{keyword}},不止于想象。每一处细节,都经得起推敲。{{keyword}}——为更好的你而生。',
tags: ['品质', '高端']
},
{
title: '限时促销',
content: '⏰ 限时抢购!{{keyword}}震撼来袭!原价{{price}}元,限时仅需{{discount}}元!错过今天,再等一年!立即抢购 >>',
tags: ['促销', '限时']
},
{
title: '社交证明',
content: '🔥 已售{{count}}件!{{keyword}}好评率99%!来看看用户怎么说:「{{quote}}」—— 好东西,用过的都知道!',
tags: ['社交', '口碑']
},
// ... 更多模板
];
模板设计原则:
- 占位符标准化:统一使用
{{变量名}}格式,便于正则替换 - 多样性:同一分类下的模板覆盖不同子场景(品质、促销、故事、对比等)
- 完整性:模板本身就是一段完整的文案,替换变量后即可直接使用
- 带有情感:大量使用 Emoji 和标点符号增强感染力
4.2 模板池管理
通过 getTemplates 方法将分类 ID 映射到对应的模板数组:
private getTemplates(catId: string): TemplateItem[] {
const map: Record<string, TemplateItem[]> = {
'ad': AD_TEMPLATES,
'moment': MOMENT_TEMPLATES,
'product': PRODUCT_TEMPLATES,
'greeting': GREETING_TEMPLATES,
'inspire': INSPIRE_TEMPLATES,
'love': LOVE_TEMPLATES,
'funny': FUNNY_TEMPLATES,
'food': FOOD_TEMPLATES,
'travel': TRAVEL_TEMPLATES,
};
return map[catId] || AD_TEMPLATES;
}
Record<string, TemplateItem[]> 是 ArkTS 中字典类型的标准写法,等价于 { [key: string]: TemplateItem[] }。
4.3 变量系统
模板引擎内置 40+ 个变量,分为三类:
用户变量:由用户输入决定
{{keyword}}:用户输入的关键词,如「咖啡」「梦想」「旅行」- 如果用户未输入,则使用每个分类的默认关键词
智能变量:由引擎自动生成
{{price}}:随机 10~510 的数字{{discount}}:随机 5~55 的数字{{count}}:随机 1000~10000 的数字{{year}}:随机 1~30 的数字{{distance}}:随机 100~2100 的数字
固定变量:由模板场景决定
{{place}}:「这座城市」{{name}}:「朋友」{{feature}}:「极致体验」{{pain}}:「烦恼」
完整的变量字典定义:
const vars: Record<string, string> = {
'{{keyword}}': keyword,
'{{brand}}': keyword + '品牌',
'{{name}}': '朋友',
'{{place}}': '这座城市',
'{{price}}': String(Math.floor(Math.random() * 500) + 10),
'{{discount}}': String(Math.floor(Math.random() * 50) + 5),
'{{count}}': String(Math.floor(Math.random() * 9000) + 1000),
'{{year}}': String(Math.floor(Math.random() * 30) + 1),
// ... 共 40+ 个变量
};
4.4 替换算法
const entries = Object.entries(vars);
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const key = entry[0];
const value = entry[1];
result = result.replaceAll(key, value);
}
为什么使用 replaceAll 而非 replace? 因为同一个变量可能在一段模板中出现多次(例如 {{keyword}} 经常出现 2~4 次),replaceAll 能一次性替换所有匹配项。
为什么不用正则表达式一次性替换? 正则虽然更简洁,但在模板变量较多(40+)的情况下,逐一遍历替换的可读性和可维护性更好。
4.5 语气风格处理
生成文案后,根据用户选择的语气风格进行后处理:
switch (tone) {
case 'humorous': // 幽默:句号逗号替换为笑哭表情
result = result.replace(/[。,]/g, '😂')
.replace(/$/, '🤣');
break;
case 'literary': // 文艺:句号后换行,末尾加署名
result = result.replace(/[。!]/g, '。\n')
.replace(/$/, '\n—— 致每一个热爱生活的人');
break;
case 'formal': // 正式:去除所有 Emoji 和特殊符号
result = result.replace(/[\u203C-\u3299]/g, '');
break;
default: // 简约:原文输出
break;
}
风格转换的灵感来源:
- 幽默风格:表情符号替换标点,让文字更轻松活泼
- 文艺风格:句号后换行增加节奏感,末尾署名营造仪式感
- 正式风格:Emoji 主要适用于社交媒体,在正式场合(如商务邮件、产品说明书)中需要被剔除
4.6 默认关键词机制
如果用户未输入关键词,系统会根据当前分类自动选择默认词:
private getDefaultKeyword(): string {
const defaults: Record<string, string> = {
'ad': '品质生活',
'moment': '美好时光',
'product': '精品好物',
'greeting': '幸福快乐',
'inspire': '梦想',
'love': '爱情',
'funny': '快乐',
'food': '美食',
'travel': '风景',
};
return defaults[this.currentCategory] || '美好';
}
这一设计确保了即使不输入任何内容,用户点击「一键生成文案」也能得到完整可用的结果,降低了使用门槛。
五、UI 实现:分层布局与交互设计
5.1 整体布局结构
应用的 UI 从上到下分为四个区域:
┌─────────────────────────────────────────┐
│ 导航栏 ◀ 文案自动写 ♡ │ 56px
├─────────────────────────────────────────┤
│ 分类导航(横向可滚动 9 个分类) │ 68px
├─────────────────────────────────────────┤
│ 风格切换 [简约] [幽默] [文艺] [正式] │ 34px
├─────────────────────────────────────────┤
│ 关键词输入: [____________________] │ 44px
├─────────────────────────────────────────┤
│ 统计:📊 已生成 N 条 ♡ 收藏 M 条 │ 28px
├─────────────────────────────────────────┤
│ ┌───────────────────────────────────┐ │
│ │ 结果展示区(白底圆角卡片) │ │
│ │ → 空状态 / 文案内容 │ │
│ │ → [❤️ 收藏] [📋 复制] │ │
│ └───────────────────────────────────┘ │ ← layoutWeight:1
├─────────────────────────────────────────┤
│ ✨ 一键生成文案 │ 62px
└─────────────────────────────────────────┘
5.2 分类导航(Category Picker)
分类选择器使用 Scroll 包裹 Row 实现横向滚动,每个分类渲染为一个 64×56 的圆角方块:
Scroll() {
Row({ space: 8 }) {
ForEach(this.categories, (cat: WritingCategory) => {
Column({ space: 2 }) {
Text(cat.icon).fontSize(20);
Text(cat.name)
.fontSize(10)
.fontColor(this.currentCategory === cat.id ? cat.color : '#636E72');
}
.width(64).height(56)
.borderRadius(12)
.backgroundColor(
this.currentCategory === cat.id
? cat.color + '18' // 选中:分类色 + 18% 透明度
: '#F5F5F5' // 未选中:浅灰
)
.border({
width: this.currentCategory === cat.id ? 1.5 : 0,
color: cat.color // 选中时显示分类色边框
})
.onClick(() => {
this.currentCategory = cat.id;
this.resultText = ''; // 切换分类时清空结果
this.currentResultTitle = '等待生成...';
});
})
}
}
.scrollable(ScrollDirection.Horizontal)
.height(68);
交互细节:
- 切换分类时自动清空结果文本,并重置标题为「等待生成…」
- 选中状态使用
color + '18'的方式生成半透明背景色——这是 ArkTS 中字符串拼接实现动态透明度的常见技巧 - 未选中状态统一使用
#F5F5F5背景,保持视觉整洁
5.3 风格切换
四种语气显示为一行标签按钮:
Row({ space: 6 }) {
ForEach(this.toneStyles, (tone: ToneStyle) => {
Button(`${tone.icon} ${tone.name}`)
.fontSize(12)
.fontColor(this.currentTone === tone.id ? '#FFFFFF' : '#636E72')
.backgroundColor(this.currentTone === tone.id ? '#6C5CE7' : '#F0F0F0')
.borderRadius(14)
.height(30)
.padding({ left: 10, right: 10 })
.onClick(() => { this.currentTone = tone.id; });
})
}
5.4 关键词输入
使用 ArkUI 的 TextInput 组件,监听值变化实时更新 @State keyword:
TextInput({ placeholder: '输入关键词(如:咖啡、梦想、旅行...)' })
.layoutWeight(1)
.height(40)
.fontSize(14)
.borderRadius(20)
.backgroundColor('#F5F5F5')
.padding({ left: 16, right: 16 })
.onChange((val: string) => { this.keyword = val; });
5.5 结果展示区
结果区是页面的核心区域,使用 layoutWeight(1) 占据工具栏下方和底部按钮上方的所有剩余空间:
Column() {
Scroll() {
Column({ space: 0 }) {
// 标题行
Row({ space: 6 }) {
Text(this.getCurrentCategoryIcon() + ' ').fontSize(16);
Text(this.currentResultTitle)
.fontSize(15).fontWeight(FontWeight.Medium)
.fontColor('#2D3436');
}
.width('100%').margin({ bottom: 12 });
// 内容
if (this.resultText) {
Text(this.resultText)
.fontSize(15).fontColor('#2D3436')
.lineHeight(24).width('100%');
// 操作按钮
Row({ space: 12 }) {
// 收藏按钮 (❤️)
Button() { /* ... */ }
.onClick(() => { this.toggleFavorite(); });
// 复制按钮 (📋)
Button() { /* ... */ }
.onClick(() => { this.copyToClipboard(); });
}
.width('100%').justifyContent(FlexAlign.Center)
.margin({ top: 20 });
} else {
// 空状态
Column({ space: 12 }) {
Text(this.getCurrentCategoryIcon())
.fontSize(48).opacity(0.3);
Text('输入关键词,点击下方按钮生成')
.fontSize(14).fontColor('#B2BEC3');
}
.width('100%').height(180)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center);
}
}
.width('100%').padding(20);
}
.layoutWeight(1).width('100%');
}
.layoutWeight(1)
空状态设计:在没有生成结果时,显示一个半透明的分类图标和引导文字,避免页面空白造成的困惑。
操作按钮状态反转:
| 按钮 | 未激活 | 已激活 |
|---|---|---|
| 收藏 | ❤️ 灰色边框 + 灰色文字 | ❤️ 红色边框 + 红色文字 |
| 复制 | 📋 灰色边框 + 灰色文字 | ✅ 绿色边框 + 绿色文字「已复制」 |
这种「状态反转」设计通过 @State isCopied 和 isFavorited() 方法驱动,用户能立刻感知操作成功。
5.6 收藏列表视图
当用户点击导航栏的 ♡ 按钮时,showFavorites 切换为 true,结果展示区替换为收藏列表:
if (this.showFavorites) {
// 展示收藏列表
if (this.favorites.length === 0) {
// 空收藏状态
Column({ space: 12 }) {
Text('💔').fontSize(48).opacity(0.3);
Text('还没有收藏的文案').fontSize(15).fontColor('#B2BEC3');
Text('生成文案后点击 ❤️ 收藏').fontSize(13).fontColor('#DFE6E9');
}
.width('100%').layoutWeight(1)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center);
} else {
List({ space: 10 }) {
ForEach(this.favorites, (item: FavoriteItem) => {
ListItem() {
// 收藏卡片(分类标签 + 时间 + 删除按钮 + 文案内容)
}
})
}
}
}
收藏列表的每个条目包含:
- 分类图标 + 分类标签(紫色背景标识)
- 收藏时间(HH:mm 格式)
- 删除按钮(✕)
- 完整文案内容
六、一键生成:从点击到文案
6.1 生成流程
generateWriting(): void {
if (this.isGenerating) return;
this.isGenerating = true;
this.isCopied = false;
// 600ms 延时模拟生成过程
setTimeout(() => {
const templates = this.getTemplates(this.currentCategory);
const keyword = this.keyword.trim() || this.getDefaultKeyword();
// 随机选取一条模板
const template = templates[Math.floor(Math.random() * templates.length)];
// 填充模板
const result = this.fillTemplate(template.content, keyword, this.currentTone);
this.resultText = result;
this.currentResultTitle = template.title;
this.genCount++;
this.isGenerating = false;
}, 600);
}
流程解析:
- 防重复触发:
isGenerating开关在生成过程中禁用按钮 - 600ms 延时:模拟「创作中」的等待体验——如果瞬间出结果,用户反而会觉得没有「生成」的感觉
- 随机选取:
Math.random() × 模板数量,确保每次生成结果不同,同一关键词也能产出多样化的文案 - 兜底关键词:
this.keyword.trim() || this.getDefaultKeyword()——如果输入为空,使用分类的默认关键词
6.2 生成过程中的 UI 反馈
if (this.isGenerating) {
Row({ space: 8 }) {
LoadingProgress().width(16).height(16).color('#6C5CE7');
Text('✍️ 正在创作中...').fontSize(13).fontColor('#6C5CE7');
}
.width('100%').justifyContent(FlexAlign.Center)
.margin({ bottom: 4 });
}
LoadingProgress 是 ArkUI 提供的原生加载动画组件,搭配「正在创作中…」文字,让等待过程不再枯燥。
底部的大按钮在生成期间也会变灰并禁用:
.backgroundColor(this.isGenerating ? '#B2BEC3' : '#6C5CE7')
.enabled(!this.isGenerating)
七、一键复制:剪贴板操作
7.1 pasteboard API
HarmonyOS 通过 @kit.BasicServicesKit 提供剪贴板能力:
import { pasteboard } from '@kit.BasicServicesKit';
copyToClipboard(): void {
if (!this.resultText) return;
try {
// 步骤1:创建剪贴板数据对象
const data = pasteboard.createData(
pasteboard.MIMETYPE_TEXT_PLAIN,
this.resultText
);
// 步骤2:获取系统剪贴板实例
const pb = pasteboard.getSystemPasteboard();
// 步骤3:写入数据
pb.setData(data);
// 步骤4:更新 UI 状态
this.isCopied = true;
// 步骤5:3 秒后自动重置复制状态
setTimeout(() => {
this.isCopied = false;
}, 3000);
} catch (e) {
console.error('复制失败: ' + JSON.stringify(e));
}
}
三个关键 API:
| API | 作用 | 参数 |
|---|---|---|
pasteboard.createData(mime, content) |
创建剪贴板数据对象 | MIME 类型 + 文本内容 |
pasteboard.getSystemPasteboard() |
获取系统级剪贴板实例 | 无 |
pb.setData(data) |
将数据写入剪贴板 | PasteboardData 对象 |
7.2 MIME 类型
pasteboard.MIMETYPE_TEXT_PLAIN 表示纯文本格式。如果需要复制富文本或图片,可以使用 MIMETYPE_TEXT_HTML 或 MIMETYPE_IMAGE_PNG。
7.3 复制成功反馈
复制按钮的 UI 在 3 秒内显示绿色「✅ 已复制」状态,之后自动恢复为灰色「📋 复制」,通过 setTimeout 实现状态的自动重置。这种临时状态设计避免了用户需要手动取消的麻烦。
八、收藏系统
8.1 状态管理
收藏系统的核心数据是 @State favorites: FavoriteItem[] 数组。关键状态包括:
@State favorites: FavoriteItem[] = []; // 收藏列表
@State favCount: number = 0; // 收藏计数
private nextFavId: number = 1; // 自增 ID
8.2 添加/取消收藏
toggleFavorite(): void {
if (!this.resultText) return;
const idx = this.favorites.findIndex(f => f.content === this.resultText);
if (idx >= 0) {
// 已收藏 → 取消收藏
this.favorites.splice(idx, 1);
this.favCount--;
} else {
// 未收藏 → 添加收藏
const now = new Date();
const timeStr =
`${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
this.favorites.push({
id: this.nextFavId++,
category: this.currentCategory,
content: this.resultText,
time: timeStr
});
this.favCount++;
}
}
设计细节:
- 使用
findIndex基于内容判断是否已收藏,避免对同一文案重复收藏 - 收藏时记录当前分类和时间,便于在列表中合理展示
- 使用
padStart(2, '0')保证时间格式统一为两位数
8.3 删除收藏
removeFavorite(id: number): void {
const idx = this.favorites.findIndex(f => f.id === id);
if (idx >= 0) {
this.favorites.splice(idx, 1);
this.favCount--;
}
}
九、首页改造:双应用入口卡片
9.1 设计思路
首页从单一应用的启动页,改造成「创意工坊」概念——以卡片形式展示两个工具:
Column({ space: 16 }) {
// 卡片1:一键生成画图
AppCard(
icon: '🎨',
title: '一键生成画图',
desc: '自由绘画 · 智能生成 · 绚丽图案',
tags: '5种笔刷 · 5种图案 · 撤销 · 对称 · 保存',
gradient: ['rgba(108,92,231,0.2)', 'rgba(255,107,129,0.15)'],
onClick: () => router.pushUrl({ url: 'pages/OneClickDraw' })
);
// 卡片2:文案自动写
AppCard(
icon: '📝',
title: '文案自动写',
desc: '智能生成 · 九大类 · 一键复制',
tags: '广告 · 朋友圈 · 商品 · 祝福 · 励志 · 爱情 · 搞笑',
gradient: ['rgba(46,213,115,0.2)', 'rgba(0,206,201,0.15)'],
onClick: () => router.pushUrl({ url: 'pages/CopyWriting' })
);
}
9.2 卡片交互
每个卡片支持点击缩放动画:
.scale({ x: this.buttonScale2, y: this.buttonScale2 })
.onClick(() => {
animateTo({ duration: 80, curve: Curve.Friction },
() => { this.buttonScale2 = 0.95; });
setTimeout(() => {
animateTo({ duration: 80, curve: Curve.Friction },
() => { this.buttonScale2 = 1; });
router.pushUrl({ url: 'pages/CopyWriting' });
}, 160);
});
80ms 的缩放动画模拟了物理按压反馈,160ms 后执行路由跳转,让交互显得敏捷而自然。
十、构建与部署
10.1 构建命令
hvigorw assembleHap --mode module -p product=default --no-daemon
10.2 ArkTS 编译要点
在本应用的开发过程中,遇到并解决了以下 ArkTS 编译规则:
| 规则 | 错误信息 | 解决方案 |
|---|---|---|
arkts-no-destruct-decls |
不支持解构声明 | 将 const [key, value] of Object.entries() 改为索引访问 |
arkts-no-any-unknown |
禁止隐含 any 类型 | catch 子句不标注类型,函数参数显式标注 |
arkts-no-types-in-catch |
catch 不支持类型标注 | 去除 catch (e: Error) 中的类型声明 |
特别是 arkts-no-destruct-decls 规则——ArkTS 出于性能考虑不支持解构赋值。原本优雅的 for (const [key, value] of Object.entries(vars)) 需要改写成:
const entries = Object.entries(vars);
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const key = entry[0];
const value = entry[1];
result = result.replaceAll(key, value);
}
虽然代码变长了一些,但语义依然清晰,且避免了运行时解构的开销。
10.3 构建配置
build-profile.json5 中的关键 SDK 版本配置:
{
"app": {
"products": [
{
"name": "default",
"targetSdkVersion": "6.1.1(24)",
"compatibleSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS"
}
]
}
}
十一、技术亮点与设计模式
11.1 模板引擎的设计模式
本应用的模板引擎采用策略模式 + 模板方法模式的组合:
- 策略模式:四种语气风格(简约、幽默、文艺、正式)对应四种不同的后处理策略
- 模板方法模式:整个生成流程(获取模板 → 选取模板 → 填充变量 → 风格处理 → 更新状态)的结构固定,但每个步骤的具体实现可以独立变化
这种设计的优势在于:新增一种语气风格只需添加一个 case 分支,新增一种模板分类只需添加一个数组——对现有代码零侵入。
11.2 响应式状态驱动的 UI
整个应用的核心交互流完全由 @State 变量驱动:
用户操作 @State 变更 UI 自动更新
───────── ──────────── ────────────
点击分类 → currentCategory 改变 → 分类高亮切换,结果清空
输入关键词 → keyword 改变 → (无直接 UI 变化)
点击生成 → isGenerating 变化 → 显示加载动画,按钮变灰
生成完成 → resultText 赋值 → 显示文案和操作按钮
点击复制 → isCopied 变化 → 按钮变绿「已复制」
点击收藏 → favorites 数组变化 → 收藏按钮状态反转
切换收藏视图 → showFavorites 变化 → 主视图/收藏列表切换
这种单向数据流的模式让状态管理变得可预测,每个 UI 变化都有明确的状态来源。
11.3 使用 ForEach 进行列表渲染
应用中大量使用 ForEach 来遍历数组渲染 UI:
ForEach(this.categories, (cat: WritingCategory) => {
// 分类按钮
})
ForEach(this.toneStyles, (tone: ToneStyle) => {
// 风格标签
})
ForEach(this.favorites, (item: FavoriteItem) => {
// 收藏列表项
})
注意:ArkTS 的 ForEach 默认使用索引作为 key,如果列表项顺序会变化(如收藏列表的删除操作),建议提供自定义 key 生成函数。
十二、总结与展望
12.1 项目复盘
「文案自动写」是 HarmonyOS NEXT 平台上一次完整的文本类应用开发实践。通过 759 行 ArkTS 代码,我们实现了以下核心功能:
- 72 条专业文案模板,覆盖 9 大生活与工作场景
- 40+ 智能变量,通过模板引擎实现关键词驱动的动态填充
- 4 种语气风格,为同一文案提供不同情感色彩
- 系统剪贴板集成,一键复制到其他应用
- 收藏管理系统,支持增删和独立列表展示
- 响应式 UI,所有状态变更自动驱动界面刷新
12.2 技术栈盘点
| 技术 | 用途 |
|---|---|
| ArkTS @Component + @Entry | 页面组件声明 |
| @State 装饰器 | 响应式状态管理 |
| Column / Row / Stack | 线性与层叠布局 |
| Scroll + ForEach | 横向滚动分类导航 |
| TextInput | 关键词输入 |
| List + ForEach | 收藏列表虚拟滚动 |
| pasteboard.createData / setData | 系统剪贴板写入 |
| animateTo | 卡片点击缩放动画 |
| setTimeout | 模拟生成延时 + 状态自动复位 |
12.3 可扩展方向
功能扩展
- AI 生成:接入 HarmonyOS 本地 AI 推理能力(如
@kit.AIKit),实现真正的智能文案创作,而非模板拼接 - 历史记录:记录所有生成过的文案,支持按时间/分类检索
- 多语言:模板支持英文、日文等多语言版本
- 分享到社交平台:一键将文案分享到微信、微博等应用
模板生态
- 用户自定义模板:允许用户创建和导入自己的模板
- 云端模板库:通过云端同步模板数据,不断扩充模板数量
- 模板热度排序:根据用户使用频率智能推荐热门模板
交互优化
- 语音输入:使用 HarmonyOS 语音识别能力替代键盘输入
- 批量生成:一次生成多条文案供用户选择
- 文案评价:用户可以对生成结果评分,反馈数据用于优化模板匹配
12.4 开发心得
在构建「文案自动写」的过程中,几点体会尤为深刻:
-
模板质量决定了应用的上限:再好的技术也抵不过糟糕的内容。72 条模板每条都经过手工编写和测试,确保替换变量后读起来自然流畅。技术是骨架,内容才是灵魂。
-
响应式编程极大提升了开发效率:在传统的命令式 UI 框架中,要同步多个 UI 元素的状态(分类高亮、结果展示、收藏按钮)需要大量胶水代码。ArkTS 的
@State+ 声明式 UI 让这些同步自动完成。 -
好交互藏在细节里:600ms 的生成延时(而非瞬间完成)、3 秒后自动恢复的复制状态、空状态的引导文字、颜色加透明度拼接的选中态——这些细节共同塑造了流畅而愉悦的用户体验。
-
ArkTS 严格模式促使代码更健壮:虽然
arkts-no-destruct-decls等规则一度让编码不太习惯,但它们防止了运行时可能出现的类型错误。习惯后,编码质量确实提升了。
本文完整项目代码可在 DevEco Studio 中直接打开构建运行。SDK 版本:HarmonyOS NEXT 6.1.1 (API Level 24),开发语言:ArkTS,构建工具:hvigor。
更多推荐


所有评论(0)