鸿蒙原生 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 实现长列表的高效渲染
  • 剪贴板 APIpasteboard 模块提供系统级剪贴板读写能力

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: ['社交', '口碑']
  },
  // ... 更多模板
];

模板设计原则

  1. 占位符标准化:统一使用 {{变量名}} 格式,便于正则替换
  2. 多样性:同一分类下的模板覆盖不同子场景(品质、促销、故事、对比等)
  3. 完整性:模板本身就是一段完整的文案,替换变量后即可直接使用
  4. 带有情感:大量使用 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 isCopiedisFavorited() 方法驱动,用户能立刻感知操作成功。

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);
}

流程解析

  1. 防重复触发isGenerating 开关在生成过程中禁用按钮
  2. 600ms 延时:模拟「创作中」的等待体验——如果瞬间出结果,用户反而会觉得没有「生成」的感觉
  3. 随机选取Math.random() × 模板数量,确保每次生成结果不同,同一关键词也能产出多样化的文案
  4. 兜底关键词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_HTMLMIMETYPE_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 代码,我们实现了以下核心功能:

  1. 72 条专业文案模板,覆盖 9 大生活与工作场景
  2. 40+ 智能变量,通过模板引擎实现关键词驱动的动态填充
  3. 4 种语气风格,为同一文案提供不同情感色彩
  4. 系统剪贴板集成,一键复制到其他应用
  5. 收藏管理系统,支持增删和独立列表展示
  6. 响应式 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 开发心得

在构建「文案自动写」的过程中,几点体会尤为深刻:

  1. 模板质量决定了应用的上限:再好的技术也抵不过糟糕的内容。72 条模板每条都经过手工编写和测试,确保替换变量后读起来自然流畅。技术是骨架,内容才是灵魂。

  2. 响应式编程极大提升了开发效率:在传统的命令式 UI 框架中,要同步多个 UI 元素的状态(分类高亮、结果展示、收藏按钮)需要大量胶水代码。ArkTS 的 @State + 声明式 UI 让这些同步自动完成。

  3. 好交互藏在细节里:600ms 的生成延时(而非瞬间完成)、3 秒后自动恢复的复制状态、空状态的引导文字、颜色加透明度拼接的选中态——这些细节共同塑造了流畅而愉悦的用户体验。

  4. ArkTS 严格模式促使代码更健壮:虽然 arkts-no-destruct-decls 等规则一度让编码不太习惯,但它们防止了运行时可能出现的类型错误。习惯后,编码质量确实提升了。


本文完整项目代码可在 DevEco Studio 中直接打开构建运行。SDK 版本:HarmonyOS NEXT 6.1.1 (API Level 24),开发语言:ArkTS,构建工具:hvigor。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐