HarmonyOS NEXT 实战:从零构建校园专属 AI 图文助手与趣味互动应用

在这里插入图片描述

目录

  1. 项目背景与整体架构
  2. 开发环境搭建与项目结构
  3. 核心 AI 服务层:AIChatService 深度解析
  4. 校园 AI 图文助手首页:Index.ets 全解
  5. AI 修仙功法:CultivationGuide 沉浸式对话
  6. AI 万能生活手册:AIHandbook 多场景问答
  7. 祝福自动生成器:BlessingGenerator 创意工具
  8. 分手模拟器:BreakupSimulator 互动叙事
  9. 舔狗模拟器:SimpSimulator 社交模拟游戏
  10. ArkTS 布局专题:ColumnStart 与 Row 垂直居中
  11. 路由导航与页面间跳转
  12. 总结与最佳实践

1. 项目背景与整体架构

1.1 项目定位

本项目是一个基于 HarmonyOS NEXT 原生能力的多功能 AI 应用集合。它以「校园 AI 图文助手」作为首页入口,同时集成了多个风格各异的子应用:

  • 校园 AI 图文助手 — 面向大学生的 AI 问答助手,覆盖学习辅导、文案创作、海报设计、校园生活等场景
  • AI 修仙功法 — 仙侠主题的沉浸式 AI 问答,模拟修真世界中的功法修炼指南
  • AI 万能生活手册 — 覆盖情感、职场、健康、法律等八大领域的实用问答
  • 祝福生成器 — 一键生成生日、新年、结婚等场景的祝福语,支持收藏与复制
  • 分手模拟器 — 互动叙事游戏,通过选择影响「心碎值」与「恢复进度」
  • 舔狗模拟器 — 养成类社交模拟游戏,通过聊天、送礼提升好感度
  • 布局演示 — ColumnStart 垂直排列布局和 Row 垂直居中布局的教学示例

1.2 技术架构全景

┌─────────────────────────────────────────────────────────┐
│                   应用层(9 个页面)                     │
├─────────┬─────────┬────────┬────────┬──────────────────┤
│Index.ets│Cultivation│AIHand-│Blessing│ BreakupSimulator │
│(首页)   │Guide     │book   │Generator│ (分手模拟器)     │
├─────────┼─────────┼────────┼────────┼──────────────────┤
│SimpSim- │ColumnStart│RowCent│        │                  │
│ulator   │Demo      │erVerti│        │                  │
│(舔狗)   │(布局演示)│calDemo│        │                  │
├─────────┴─────────┴────────┴────────┴──────────────────┤
│                核心服务层:AIChatService                │
│        SSE 流式 AI 对话 · HTTP 网络请求 · API 封装     │
├─────────────────────────────────────────────────────────┤
│                HarmonyOS NEXT API 层                    │
│   @kit.NetworkKit  ·  @ohos.router  ·  @ohos.promptAct │
│   @ohos.pasteboard ·  ArkUI 声明式 UI  ·  状态管理     │
└─────────────────────────────────────────────────────────┘

1.3 设计理念

  • 组件复用最大化 — 所有 AI 对话页面共享同一套 AIChatService 服务,仅通过不同的 system prompt 实现差异化
  • 状态驱动 UI — 使用 ArkTS 的 @State 装饰器管理页面状态,状态变化自动触发 UI 刷新
  • SSE 流式响应 — 采用 Server-Sent Events 协议实现 AI 对话的逐字输出效果,提升用户体验
  • 数据与视图分离 — 每个页面的数据模型(Interface)、状态管理(@State)、UI 构建(@Builder)清晰分层

2. 开发环境搭建与项目结构

2.1 环境要求

  • 操作系统: Windows 10/11、macOS 12+ 或 Ubuntu 22.04+
  • 开发工具: DevEco Studio 5.0+(基于 IntelliJ IDEA)
  • SDK 版本: HarmonyOS NEXT 6.1.1(API 24)
  • Node.js: 18.x 或更高(DevEco Studio 内置)
  • 构建工具: hvigor 6.24.2(HarmonyOS 专用构建系统)

2.2 项目文件结构

MyApplication4/
├── build-profile.json5              # 项目级构建配置
├── code-linter.json5                # 代码检查配置
├── oh-package.json5                 # 依赖管理
├── oh-package-lock.json5            # 依赖锁定
└── entry/
    ├── build-profile.json5          # 模块级构建配置
    └── src/main/ets/
        ├── entryability/
        │   └── EntryAbility.ets     # 应用入口 Ability
        ├── entrybackupability/
        │   └── EntryBackupAbility.ets
        └── pages/
            ├── Index.ets            # 校园 AI 图文助手(首页)
            ├── AIChatService.ets    # AI 核心服务(SSE 流式对话)
            ├── CultivationGuide.ets # AI 修仙功法
            ├── AIHandbook.ets       # AI 万能生活手册
            ├── BlessingGenerator.ets# 祝福生成器
            ├── BreakupSimulator.ets # 分手模拟器
            ├── SimpSimulator.ets    # 舔狗模拟器
            ├── ColumnStartDemo.ets  # ColumnStart 布局演示
            └── RowCenterVerticalDemo.ets # Row 垂直居中布局演示

2.3 依赖导入说明

HarmonyOS NEXT 提供了全新的 Kit 化 API 体系。在本项目中,我们主要使用以下核心 Kit:

Kit / 模块 用途 导入方式
@kit.NetworkKit HTTP 网络请求 import { http } from '@kit.NetworkKit'
@kit.BasicServicesKit 基础类型支持 import { BusinessError } from '@kit.BasicServicesKit'
@ohos.router 页面路由跳转 import router from '@ohos.router'
@ohos.promptAction 轻提示弹窗 import promptAction from '@ohos.promptAction'
@ohos.pasteboard 剪贴板操作 import pasteboard from '@ohos.pasteboard'

3. 核心 AI 服务层:AIChatService 深度解析

AIChatService.ets 是整个应用的 AI 引擎。它封装了 HTTP 请求、SSE 流式解析、非流式回退等核心逻辑,为所有 AI 对话页面提供统一调用接口。

3.1 接口定义

首先定义清晰的数据结构:

/** 聊天消息结构体 */
export interface ChatMessage {
  role: string;      // 'system' | 'user' | 'assistant'
  content: string;
}

/** 请求体结构体(对应 OpenAI API 格式) */
export interface ChatCompletionRequest {
  model: string;
  messages: ChatMessage[];
  stream: boolean;
  max_tokens: number;
  temperature: number;
  top_p: number;
  frequency_penalty: number;
  thinking_budget: number;
}

/** SSE 解析结果回调 */
export interface AICallbacks {
  /** 每次收到新的 token 内容时触发 */
  onData: (text: string) => void;
  /** 流式响应结束时触发 */
  onDone: () => void;
  /** 发生错误时触发 */
  onError: (errMsg: string) => void;
}

设计亮点: 使用回调接口 AICallbacks 而非 Promise/async-await,是因为 SSE 流式场景需要多次回调,回调模式更加自然。

3.2 API 配置与系统提示词

/** API 配置常量 */
const API_URL = 'https://api-ai.gitcode.com/v1/chat/completions';
const API_KEY = 'your-api-key-here';  // 需要替换为自己的 API Key

/** 系统提示词:校园专属AI图文助手 */
const SYSTEM_PROMPT: string =
  '你是一个「校园专属AI图文助手」,专为在校大学生提供学习、生活和校园资讯相关的图文帮助。\n\n' +
  '你的能力包括:\n' +
  '1. 📝 学习辅导:帮助理解课程知识点、解答作业难题、整理学习笔记、撰写论文大纲\n' +
  '2. 📄 文案创作:撰写校园推文、活动宣传文案、社团招新文案、个人简历、PPT文稿\n' +
  '3. 🎨 图片描述与创意:描述图片内容、提供设计灵感(海报、展板、Logo等)、讲解构图配色\n' +
  '4. 🌸 校园生活指南:推荐食堂美食、介绍校园景点、提供选课建议、分享考证考研经验\n' +
  '5. 🎪 活动策划:帮策划社团活动、班级团建、校园比赛等\n' +
  '6. 📰 校园资讯:回答关于校园规章制度、办事流程、校园文化等方面的问题\n\n' +
  '回答要求:\n' +
  '1. 语气亲切活泼,像学长学姐一样耐心解答,适当使用校园流行语和表情符号\n' +
  '2. 内容准确实用,条理清晰,可适当分点列出或使用表格\n' +
  '3. 涉及图片/设计相关问题时,给出具体的构图建议、配色方案和风格描述\n' +
  '4. 学术类问题要严谨,但要用通俗易懂的方式解释\n' +
  '5. 鼓励用户多思考、多实践,提供可落地的建议\n' +
  '6. 不需要返回 JSON 格式,直接以自然语言对话即可\n' +
  '7. 如果用户需要生成图片,请详细描述画面构图、色彩、风格等要素,方便后续绘图\n\n' +
  '你不仅是工具,更是校园里的贴心伙伴!让每次交流都充满温度和收获 🎉';

关键设计: 系统提示词以自然语言描述了 AI 的身份、能力和回答风格。不同的页面可以通过 queryAI 函数的 customPrompt 参数传入不同的系统提示词,实现「一人千面」的效果。

3.3 SSE 流式解析

SSE(Server-Sent Events)是一种服务器推送协议,AI 对话通过 data: 行逐 token 返回内容:

/**
 * 解析 SSE data 行,提取 content 增量
 * @param line - 以 "data:" 开头的行(不含前缀)
 * @returns 提取到的 content,或 null
 */
function parseSSEDataLine(line: string): string | null {
  const jsonStr = line.slice(5).trim();  // 去掉 "data:" 前缀
  if (!jsonStr) {
    return null;
  }
  try {
    const parsed: Record<string, Object> = JSON.parse(jsonStr) as Record<string, Object>;
    const choices: Object[] = parsed.choices as Object[];
    if (choices && choices.length > 0) {
      const choice: Record<string, Object> = choices[0] as Record<string, Object>;
      // 兼容 delta 和 message 两种格式(流式 vs 非流式)
      const delta: Record<string, Object> = choice.delta as Record<string, Object>;
      if (delta) {
        return delta.content as string;
      }
      const message: Record<string, Object> = choice.message as Record<string, Object>;
      if (message) {
        return message.content as string;
      }
    }
  } catch (_) {
    // JSON 解析失败,跳过
  }
  return null;
}

兼容性处理: 同一个函数同时处理流式格式(delta 字段)和非流式格式(message 字段),避免重复解析逻辑。

3.4 完整响应体解析(非流式回退)

在某些 HarmonyOS 版本的 HTTP 模块中,SSE 的 dataReceive 事件可能不会触发。因此我们设计了双重回退机制:

/**
 * 从完整响应体中解析所有 SSE data 行
 * @param body - 整个响应体字符串
 * @returns 拼接后的 content
 */
function parseFullSSEBody(body: string): string {
  let result = '';
  const lines = body.split('\n');
  for (const line of lines) {
    const trimmed = line.trim();
    if (trimmed.startsWith('data:')) {
      if (trimmed === 'data:[DONE]') {
        break;  // SSE 结束标记
      }
      const content = parseSSEDataLine(trimmed);
      if (content) {
        result += content;
      }
    }
  }
  return result;
}

/**
 * 从完整 JSON 响应体中提取 content(非流式回退)
 */
function parseNonStreamingBody(body: string): string | null {
  try {
    const parsed: Record<string, Object> = JSON.parse(body) as Record<string, Object>;
    const choices: Object[] = parsed.choices as Object[];
    if (choices && choices.length > 0) {
      const choice: Record<string, Object> = choices[0] as Record<string, Object>;
      const message: Record<string, Object> = choice.message as Record<string, Object>;
      if (message) {
        return message.content as string;
      }
      // 也可能是流式格式 (delta 而非 message)
      const delta: Record<string, Object> = choice.delta as Record<string, Object>;
      if (delta) {
        return delta.content as string;
      }
    }
  } catch (_) {
    // 不是纯 JSON,可能是 SSE 格式
  }
  return null;
}

3.5 ArrayBuffer 解码

HTTP 响应体以 ArrayBuffer 形式接收,需要转换为字符串:

function arrayBufferToString(buffer: ArrayBuffer): string {
  const uint8Arr = new Uint8Array(buffer);
  let text = '';
  for (let i = 0; i < uint8Arr.length; i++) {
    text += String.fromCharCode(uint8Arr[i]);
  }
  return text;
}

3.6 queryAI 核心函数

这是整个服务的核心导出函数,整合了上述所有能力:

export function queryAI(
  callbacks: AICallbacks,
  messages: ChatMessage[],
  customPrompt?: string,
): void {
  // 取消上一次未完成的请求
  if (httpRequestTask) {
    try {
      httpRequestTask.destroy();
    } catch (_) { /* ignore */ }
    httpRequestTask = null;
  }

  const httpRequest = http.createHttp();
  httpRequestTask = httpRequest;

  // 构建请求体,合并系统提示词 + 用户聊天历史
  const fullMessages: ChatMessage[] = [
    { role: 'system', content: customPrompt ?? SYSTEM_PROMPT },
    ...messages,
  ];
  const requestBody: ChatCompletionRequest = {
    model: 'deepseek-ai/DeepSeek-V3',
    messages: fullMessages,
    stream: true,
    max_tokens: 2048,
    temperature: 0.6,
    top_p: 0.95,
    frequency_penalty: 0,
    thinking_budget: 2048,
  };

  let isDone: boolean = false;
  let receivedAnyData: boolean = false;
  let buffer = '';

  // 监听 SSE 流式数据
  httpRequest.on('dataReceive', (data: ArrayBuffer) => {
    const text = arrayBufferToString(data);
    buffer += text;
    receivedAnyData = true;

    const lines = buffer.split('\n');
    buffer = lines.pop() ?? '';

    for (const line of lines) {
      const trimmed = line.trim();
      if (!trimmed.startsWith('data:')) continue;
      if (trimmed === 'data:[DONE]') {
        if (!isDone) { isDone = true; callbacks.onDone(); }
        continue;
      }
      const content = parseSSEDataLine(trimmed);
      if (content) callbacks.onData(content);
    }
  });

  // 数据接收完毕
  httpRequest.on('dataEnd', () => {
    if (!isDone) {
      isDone = true;
      callbacks.onDone();
    }
    httpRequestTask = null;
  });

  // 发起 POST 请求
  httpRequest.request(
    API_URL,
    {
      method: http.RequestMethod.POST,
      header: {
        Authorization: `Bearer ${API_KEY}`,
        'Content-Type': 'application/json',
        Accept: 'text/event-stream',
      },
      extraData: JSON.stringify(requestBody),
      connectTimeout: 30000,
      readTimeout: 120000,
    },
    (err: BusinessError | null, resp: http.HttpResponse) => {
      // ...(错误处理与非流式回退逻辑)
    },
  );
}

设计亮点:

  1. 请求取消 — 每次发起新请求前先销毁上一次的请求,防止内存泄漏和并发冲突
  2. 双重解析 — 优先使用 SSE 流式 dataReceive 事件,不触发时自动回退到完整响应体解析
  3. 缓冲区管理 — 使用 buffer = lines.pop() 处理 SSE 的不完整行(最后一行可能只接收到一半)
  4. 兼容性 — 同时支持 deltamessage 两种 JSON 格式

3.7 取消请求

export function cancelAI(): void {
  if (httpRequestTask) {
    try {
      httpRequestTask.destroy();
    } catch (_) { /* ignore */ }
    httpRequestTask = null;
  }
}

这个函数在页面离开或用户清空对话时调用,确保网络资源及时释放。


4. 校园 AI 图文助手首页:Index.ets 全解

首页是应用的「门面」,采用了清新明亮的校园风格设计。

4.1 数据结构定义

interface CampusScene {
  emoji: string;       // 场景图标表情符
  title: string;       // 场景标题
  prompt: string;      // 点击后发送的提示词
  color: string;       // 主题色
  bgColor: string;     // 背景色
  borderColor: string; // 边框色(带透明度)
}

interface NavItem {
  label: string;
  url: string;
  color: string;
}

4.2 快捷场景配置

8 个校园场景覆盖了大学生最常遇到的 AI 需求:

const QUICK_SCENES: CampusScene[] = [
  {
    emoji: '📝', title: '论文润色',
    prompt: '帮我润色一段课程论文,主题是关于人工智能对教育的影响,我需要更学术化的表达和清晰的结构,请给出修改建议。',
    color: '#4A90D9', bgColor: '#F0F7FF', borderColor: '#4A90D966',
  },
  {
    emoji: '🎨', title: '海报设计',
    prompt: '我们社团要举办一场校园音乐节,需要设计一张宣传海报。请帮我描述海报的整体构图、配色方案(校园青春风)、字体风格和元素布局。',
    color: '#FF6B9D', bgColor: '#FFF0F5', borderColor: '#FF6B9D66',
  },
  {
    emoji: '📖', title: '高数辅导',
    prompt: '我是大一新生,正在学高等数学中的极限部分,对夹逼定理不太理解,能用一个简单的例子帮我解释一下吗?',
    color: '#5B8FF9', bgColor: '#F0F8FF', borderColor: '#5B8FF966',
  },
  {
    emoji: '✍️', title: '文案创作',
    prompt: '我们班要准备一个班级风采展示的推文,想写得有趣又有感染力,能帮我写一段介绍我们班级的文案吗?我们是计算机科学与技术专业2班。',
    color: '#F5A623', bgColor: '#FFFDF0', borderColor: '#F5A62366',
  },
  {
    emoji: '🍜', title: '校园美食',
    prompt: '我们学校食堂二楼新开了几家窗口,有麻辣烫、螺蛳粉、煲仔饭,我选择困难症犯了,能帮我推荐一下并说说各有什么特色吗?',
    color: '#FF7F50', bgColor: '#FFF5F0', borderColor: '#FF7F5066',
  },
  {
    emoji: '🎯', title: '考证规划',
    prompt: '我是大二计算机专业的学生,想在大三之前考一些有用的证书,能帮我做一个考证规划吗?包括时间安排和备考建议。',
    color: '#36CFC9', bgColor: '#F0FFFE', borderColor: '#36CFC966',
  },
  {
    emoji: '🎪', title: '活动策划',
    prompt: '我们班想办一次期末前的团建活动,预算500元以内,20人左右,有什么好玩的策划方案推荐吗?要有创意且不冷场。',
    color: '#B37FEB', bgColor: '#F5F0FF', borderColor: '#B37FEB66',
  },
  {
    emoji: '💡', title: 'PPT美化',
    prompt: '我下周要做课程展示PPT,主题是"碳中和与可持续发展",请给我一些PPT结构建议、每一页的内容要点和排版美化思路。',
    color: '#00B894', bgColor: '#F0FFF4', borderColor: '#00B89466',
  },
];

设计原则: 每个场景的 prompt 都是真实、具体的大学生提问,而不是模糊的关键词。这样用户点击后能得到有价值的回答,而不是泛泛而谈。

4.3 页面状态管理

@Entry
@Component
struct Index {
  @State messages: ChatMessage[] = [];    // 对话消息列表
  @State inputText: string = '';          // 输入框内容
  @State isLoading: boolean = false;      // 是否正在加载
  @State showScenes: boolean = true;      // 是否显示快捷场景

  private scrollController: Scroller = new Scroller();

状态设计要点:

  • @State 标记的变量变化会自动触发 UI 重新渲染
  • showScenes 控制快捷场景面板的显隐——用户发出一条消息后自动隐藏
  • scrollController 用于在接收到新消息时自动滚动到底部
  • 使用 Scroller 而非 ScrolledgeScroll,可以编程式控制滚动位置

4.4 欢迎消息

aboutToAppear(): void {
  this.messages.push({
    role: 'assistant',
    content: '🎓 你好呀!我是你的「校园AI图文助手」学长/学姐~\n\n' +
      '📌 我能帮你做这些事:\n' +
      '  • 📝 论文润色 · 学习辅导 · 笔记整理\n' +
      '  • 🎨 海报设计 · 文案创作 · PPT美化\n' +
      '  • 🍜 美食推荐 · 活动策划 · 考证规划\n' +
      '  • 💬 校园生活 · 社团工作 · 各种疑问\n\n' +
      '👇 点击下方场景试试,或直接输入你的问题:',
  });
}

生命周期回调: aboutToAppear() 是 ArkTS 提供的生命周期方法,在组件即将显示时调用,适合做初始化工作。

4.5 发送消息的核心逻辑

private sendMessage(text?: string): void {
  const msg = (text || this.inputText).trim();
  if (!msg || this.isLoading) return;

  // 1. 添加用户消息
  this.messages.push({ role: 'user', content: msg });
  this.inputText = '';
  this.showScenes = false;

  // 2. 添加空的 AI 占位消息
  const aiIndex = this.messages.length;
  this.messages.push({ role: 'assistant', content: '' });
  this.isLoading = true;

  // 3. 构建历史消息(排除空占位)
  const history = this.messages
    .filter(m => m.content.length > 0)
    .slice(0, -1);

  // 4. 配置回调
  const callbacks: AICallbacks = {
    onData: (chunk: string) => {
      this.messages[aiIndex] = {
        role: 'assistant',
        content: this.messages[aiIndex].content + chunk,
      };
      setTimeout(() => {
        this.scrollController.scrollEdge(Edge.Bottom);
      }, 50);
    },
    onDone: () => {
      this.isLoading = false;
      if (!this.messages[aiIndex].content) {
        this.messages[aiIndex] = { 
          role: 'assistant', 
          content: '(稍等一下哦,让学长再帮你想想~请重新提问)' 
        };
      }
    },
    onError: (err: string) => {
      this.isLoading = false;
      this.messages[aiIndex] = { 
        role: 'assistant', 
        content: `❌ 网络好像开小差了:${err}` 
      };
    },
  };

  // 5. 调用 AI 服务
  queryAI(callbacks, history);
}

关键技巧:

  • 占位消息:先插入一条空消息,onData 回调中逐步追加内容,实现了「打字机效果」
  • 防重复提交:通过 this.isLoading 状态阻止用户在一次请求未完成时再次发送
  • 滚动到底部:每次收到新 token 时用 setTimeout 延迟 50ms 滚动,确保新内容已渲染

4.6 UI 构建整体结构

build() {
  Column() {
    // ── 顶部工具栏 ──
    Row() {
      Text('🎓 校园AI图文助手')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#2C3E50')
      Blank()
      Button({ type: ButtonType.Normal, stateEffect: true }) {
        Text('🗑️').fontSize(18)
      }
      .width(40).height(40)
      .backgroundColor('#00000000')
      .onClick(() => { this.clearChat(); })
    }
    .width('100%')
    .padding({ left: 16, right: 12, top: 8, bottom: 8 })
    .backgroundColor('#FFFFFF')
    .shadow({ radius: 3, color: 'rgba(0,0,0,0.04)', offsetX: 0, offsetY: 2 })

    // ── 消息列表(可滚动)──
    Scroll(this.scrollController) {
      Column() {
        // 装饰顶部分隔
        Row() {
          Text('——  校 园 日 常  ——')
            .fontSize(12).fontColor('#A0AEC0').fontWeight(FontWeight.Medium)
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ top: 8, bottom: 4 })

        ForEach(this.messages, (msg, index) => {
          this.buildMessageItem(msg, index)
        }, (msg, index) => msg.role + index)

        if (this.showScenes) {
          this.buildScenesPanel()
        }
        this.buildNavSection()
      }
      .width('100%').padding({ bottom: 16 })
    }
    .scrollBar(BarState.Off).layoutWeight(1).width('100%')
    .backgroundColor('#F7FAFC')

    // ── 输入区域 ──
    Row() { /* TextInput + Button */ }
    .width('100%').padding({ left: 12, right: 12, top: 8, bottom: 12 })
    .backgroundColor('#FFFFFF')
    .shadow({ radius: 4, color: 'rgba(0,0,0,0.08)', offsetX: 0, offsetY: -2 })
  }
  .width('100%').height('100%').backgroundColor('#F7FAFC')
}

布局层次:

  1. 最外层 Column — 垂直排列顶部工具栏、消息列表、输入区
  2. Scroll 包裹消息列表 — 当消息过多时支持滚动
  3. 输入区域固定底部 — 使用 layoutWeight(1) 让消息列表占据除工具栏和输入区外的所有空间

4.7 消息气泡 @Builder

@Builder
buildMessageItem(msg: ChatMessage, index: number) {
  if (msg.role === 'user') {
    // 用户消息:右对齐,蓝色气泡
    Row() {
      Blank()
      Column() {
        Text(msg.content)
          .fontSize(15).fontColor('#FFFFFF').lineHeight(22)
      }
      .padding({ left: 16, right: 16, top: 10, bottom: 10 })
      .backgroundColor('#4A90D9')   // 蓝底白字
      .borderRadius({ topLeft: 16, topRight: 16, bottomLeft: 16, bottomRight: 4 })
      .margin({ left: 60 })
    }
    .alignItems(VerticalAlign.Bottom)
    .width('100%').padding({ left: 16, right: 16, top: 4, bottom: 4 })
  } else {
    // AI 消息:左对齐,白色气泡 + 头像
    Row() {
      // 头像
      Column() {
        Text('🎓').fontSize(22)
        Text('学长').fontSize(8).fontColor('#4A90D9').margin({ top: 2 })
      }
      .width(44).alignItems(HorizontalAlign.Center)

      Column() {
        if (msg.content) {
          Text(msg.content).fontSize(15).fontColor('#2D3748').lineHeight(24)
        } else {
          Row() {
            LoadingProgress().width(16).height(16).color('#4A90D9')
            Text('  思考中...').fontSize(14).fontColor('#A0AEC0').margin({ left: 6 })
          }
          .alignItems(VerticalAlign.Center).height(36)
        }
      }
      .padding({ left: 14, right: 14, top: 10, bottom: 10 })
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 4, topRight: 16, bottomLeft: 16, bottomRight: 16 })
      .margin({ left: 8 }).layoutWeight(1)
      .shadow({ radius: 2, color: 'rgba(0,0,0,0.06)', offsetX: 0, offsetY: 1 })
    }
    .alignItems(VerticalAlign.Top)
    .width('100%').padding({ left: 12, right: 16, top: 4, bottom: 4 })
  }
}

气泡设计要点:

  • 用户气泡: 右对齐、蓝色底色、圆角只有左下角是直角(模拟对话框风格)
  • AI 气泡: 左对齐、白色底色、左上角直角、右侧圆角(与用户气泡对称)
  • 加载状态: 内容为空时显示 LoadingProgress 旋转动画 + “思考中…” 文字
  • 排版细节: 使用 lineHeight 控制行距(24px),比默认值更舒适;气泡添加轻微阴影增加层次感

4.8 快捷场景面板 @Builder

@Builder
buildScenesPanel() {
  Column() {
    Row() {
      Text('———  学 长 帮 忙   ———')
        .fontSize(14).fontColor('#A0AEC0').fontWeight(FontWeight.Bold)
    }
    .width('100%').justifyContent(FlexAlign.Center)
    .margin({ top: 24, bottom: 16 })

    ForEach(
      QUICK_SCENES,
      (scene: CampusScene) => {
        Column() {
          Text(scene.emoji).fontSize(32)
          Text(scene.title)
            .fontSize(13).fontColor('#2D3748').fontWeight(FontWeight.Medium)
            .margin({ top: 6 })
        }
        .width('44%')
        .padding({ top: 16, bottom: 16 })
        .backgroundColor(scene.bgColor)
        .borderRadius(14)
        .margin({ bottom: 12 })
        .border({ width: 1, color: scene.borderColor })
        .onClick(() => { this.onSceneTap(scene); })
        .shadow({ radius: 4, color: 'rgba(0,0,0,0.05)', offsetX: 0, offsetY: 2 })
      },
      (scene: CampusScene) => scene.title
    )
  }
  .width('100%').alignItems(HorizontalAlign.Center)
}

布局技巧: 使用 width('44%') 实现两列网格效果(每行放两个场景卡片),ForEach 循环遍历 QUICK_SCENES 数组自动生成卡片列表。

4.9 清空对话

private clearChat(): void {
  cancelAI();           // 取消正在进行的 AI 请求
  this.messages = [];   // 清空消息列表
  this.isLoading = false;
  this.showScenes = true;  // 重新显示快捷场景
  this.messages.push({
    role: 'assistant',
    content: '🎓 你好呀!我是你的「校园AI图文助手」学长/学姐~\n\n有什么学习或校园生活上的问题,尽管问我吧!💪',
  });
}

5. AI 修仙功法:CultivationGuide 沉浸式对话

5.1 独白的系统提示词

修仙功法的核心魅力在于其独特的仙侠语境系统提示词:

const CULTIVATION_PROMPT: string =
  '你是一位修行万年的「修仙功法大能」,道号「玄机真人」,通晓三界六道一切修炼法门。\n\n' +
  '你精通以下修仙领域:\n' +
  '1. 功法修炼:各种灵根对应的功法选择、修炼心法口诀、经脉运行路线、灵气吐纳之法\n' +
  '2. 境界突破:练气→筑基→金丹→元婴→化神→大乘→渡劫各阶段的突破要领与瓶颈破解\n' +
  '3. 炼丹炼器:丹方配方、灵药辨识、火候掌控、法器祭炼、符箓绘制、法宝蕴养\n' +
  '4. 阵法禁制:聚灵阵、护山大阵、传送阵、幻阵、禁制破解与布置要点\n' +
  '5. 灵兽培育:灵兽认主、血脉进化、妖兽驯服、灵宠养成、兽丹炼化\n' +
  '6. 秘境探险:秘境规则解析、天材地宝辨识、机缘获取、危险规避、遗迹解密\n' +
  '7. 宗门事务:门派管理、弟子培养、资源分配、宗门大比、外交合纵\n' +
  '8. 天劫应对:天劫种类(四九/六九/九九)、渡劫准备、心魔化解、飞升之道\n\n' +
  '回复原则:\n' +
  '1. 以修仙世界为背景,用仙侠语境回复,但建议要符合内在逻辑。\n' +
  '2. 具体修炼方法要详细(如灵气在经脉中的运行路线、灵药的配比火候等),避免空洞口诀。\n' +
  '3. 对于严重后果(走火入魔、天劫失败、丹炉炸裂等),给出预防和补救方案。\n' +
  '4. 语气仙风道骨,偶尔引用一些「古籍所载」「据为师所知」增加沉浸感,又不失亲切。\n' +
  '5. 回复结构清晰,用「首先→其次→最后」或「第一→第二→第三」的方式组织。\n' +
  '6. 适当使用修仙术语(灵气、丹田、神识、紫府、元婴、法宝等),但要在上下文中自然融入。\n' +
  '7. 可以根据问题主动推荐相关的功法名称、丹方名称、秘境名称(虚构但合理)。\n\n' +
  '请用中文回复。现在,开始指点有缘人的修仙之路吧!';

提示设计要点:

  • 角色设定明确:道号「玄机真人」,修行万载,赋予 AI 一个清晰的「人格」
  • 知识领域完整:8 大修仙领域覆盖用户可能问到的所有方向
  • 交互风格指引:仙风道骨的语气要求 + 结构化回复模板
  • 术语使用规范:要求自然融入修仙术语,同时保持内在逻辑

5.2 调用方式差异

修仙功法在调用 queryAI 时传入了自定义的系统提示词:

// 在 sendMessage 中调用 AI 服务时传入自定义 prompt
queryAI(callbacks, history, CULTIVATION_PROMPT);

而 Index 首页使用的是默认的校园助手 prompt。这就是「一人千面」的实现方式——同一个 queryAI 函数,不同的 customPrompt,产出完全不同的回答风格。

5.3 视觉主题

修仙功法的 UI 采用了仙侠风格配色:

  • 背景色#F5F0E8(仿古宣纸色)
  • 用户气泡#8B6914(暗金色)
  • AI 气泡#FFFCF5(宣纸白底),文字色 #3C2415(墨色)
  • 装饰文字#C4A97D(古铜色)
  • 分割线—— 仙 缘 一 线 ——(仙侠分隔符)
  • 头像:🌄 真人
// 欢迎消息
content: '🌄 道友有礼了!贫道玄机真人,修行万载,通晓三界六道一切修炼法门。\n\n' +
  '凡有所问——功法困惑、境界瓶颈、炼丹炼器、阵法符箓、灵兽培育、天劫应对……\n' +
  '尽管道来,本座自当为你一一指点。\n\n' +
  '👇 选择下方场景试试,或直接输入你的疑问:',

// 加载状态
Text('  掐指推算中...')
  .fontSize(14).fontColor('#8B7355')

// 错误提示
content: `❌ 天机紊乱,传讯受阻:${err}`

沉浸感打造: 从欢迎语到加载提示到错误提示,所有文案都统一采用仙侠语境,让用户完全沉浸在「修仙问道」的体验中。


6. AI 万能生活手册:AIHandbook 多场景问答

6.1 场景配置

AIHandbook 覆盖了 8 大生活领域,每个场景都对应真实的生活痛点:

const QUICK_SCENES: QuickScene[] = [
  {
    emoji: '💔', title: '情感关系',
    prompt: '我和伴侣最近经常因为小事吵架,感觉沟通出现了问题,该怎么办?',
    color: '#FF6B6B', bgColor: '#FFF0F0',
  },
  {
    emoji: '💼', title: '职场发展',
    prompt: '我工作两年了,感觉遇到了瓶颈,不知道该继续深耕还是换工作,能给些建议吗?',
    color: '#0984E3', bgColor: '#F0F8FF',
  },
  {
    emoji: '🏥', title: '健康养生',
    prompt: '我最近总是失眠,入睡困难,白天精神很差,有什么改善睡眠的方法吗?',
    color: '#00B894', bgColor: '#F0FFF4',
  },
  {
    emoji: '🍳', title: '生活技能',
    prompt: '我是一个人住,想学做一些简单的家常菜,有什么适合新手的菜谱推荐吗?',
    color: '#FDCB6E', bgColor: '#FFFDF0',
  },
  {
    emoji: '📚', title: '学习成长',
    prompt: '我想利用碎片时间学习一门新技能,有什么高效的学习方法推荐吗?',
    color: '#6C5CE7', bgColor: '#F5F0FF',
  },
  {
    emoji: '🧠', title: '心理情绪',
    prompt: '最近工作压力很大,经常感到焦虑,有什么缓解焦虑的方法吗?',
    color: '#E84393', bgColor: '#FFF0F7',
  },
  {
    emoji: '⚖️', title: '法律常识',
    prompt: '我租房合同到期了,房东不肯退押金,请问我该怎么维权?',
    color: '#636E72', bgColor: '#F5F6FA',
  },
  {
    emoji: '📱', title: '科技数码',
    prompt: '我想买一台笔记本电脑用来办公和轻度剪辑,3000-4000元预算有什么推荐?',
    color: '#00CEC9', bgColor: '#F0FFFE',
  },
];

6.2 与首页的差异

AIHandbook 使用默认的系统提示词(没有传 customPrompt),但在 UI 设计上有所不同:

  • AI 头像:📖 书本图标(区别于校园助手的 🎓 和修仙的 🌄)
  • 用户气泡#5B8FF9(蓝色系)
  • 背景色#F5F6FA(浅灰色)
  • 输入框占位:「输入你的问题…」
  • 无装饰分隔线:直接显示对话内容
// AI 头像使用不同的图标
Text('📖')
  .fontSize(20).width(36).height(36)
  .textAlign(TextAlign.Center)
  .backgroundColor('#E8F4FD')
  .borderRadius(18)

设计原则: 每个子应用通过不同的颜色体系、头像图标和文案风格来区分定位,而不是复制粘贴同一个模板。用户一眼就能分辨出自己在使用哪个功能。


7. 祝福自动生成器:BlessingGenerator 创意工具

7.1 数据结构

祝福生成器不是 AI 对话应用,而是一个本地数据驱动的工具。它包含 6 个分类,每个分类 8 条祝福语:

interface BlessingCategory {
  name: string;
  emoji: string;
  color: string;
  bgColor: string;
  items: string[];
}

const BLESSING_DATA: BlessingCategory[] = [
  {
    name: '生日祝福',
    emoji: '🎂',
    color: '#FF6B6B',
    bgColor: '#FFF0F0',
    items: [
      '愿你年年岁岁都平安,朝朝暮暮皆如意。生日快乐!🎉',
      '愿你眼里有光,心中有爱,目光所至皆是星辰大海。生日快乐!✨',
      // ... 共 8 条
    ],
  },
  // ... 共 6 个分类
];

7.2 核心状态管理

@Entry
@Component
struct BlessingGenerator {
  @State selectedIndex: number = 0;         // 当前选中的分类
  @State currentBlessing: string = '';      // 当前显示的祝福语
  @State isGenerating: boolean = false;     // 是否正在生成
  @State collectedBlessings: string[] = []; // 收藏列表
  @State showCollected: boolean = false;    // 是否显示收藏视图

7.3 祝福生成逻辑

private generateBlessing(): void {
  this.isGenerating = true;

  const items = this.currentCategory().items;
  setTimeout(() => {
    const randomIndex = Math.floor(Math.random() * items.length);
    this.currentBlessing = items[randomIndex];
    this.isGenerating = false;
  }, 200);  // 200ms 延迟制造动效感
}

private currentCategory(): BlessingCategory {
  return BLESSING_DATA[this.selectedIndex];
}

7.4 剪贴板操作

private copyToClipboard(): void {
  if (!this.currentBlessing) return;

  try {
    const pasteboardApi = pasteboard.getSystemPasteboard();
    const pasteData = pasteboard.createData(
      pasteboard.MIMETYPE_TEXT_PLAIN, 
      this.currentBlessing
    );
    pasteboardApi.setData(pasteData);
    promptAction.showToast({ message: '✅ 已复制到剪贴板', duration: 1500 });
  } catch (_) {
    promptAction.showToast({ message: '复制失败,请重试', duration: 1000 });
  }
}

HarmonyOS NEXT 的剪贴板 API 使用 @ohos.pasteboard 模块,通过 getSystemPasteboard() 获取系统剪贴板实例,然后 createData 创建剪贴板数据。

7.5 收藏功能

private collectBlessing(): void {
  if (!this.currentBlessing) return;
  if (this.collectedBlessings.includes(this.currentBlessing)) return;
  this.collectedBlessings = [this.currentBlessing, ...this.collectedBlessings];
}

private removeCollected(index: number): void {
  this.collectedBlessings.splice(index, 1);
  this.collectedBlessings = [...this.collectedBlessings];  // 触发 @State 更新
}

关键细节: splice 会修改原数组,但不会触发 @State 的重新渲染。通过 [...this.collectedBlessings] 创建新数组赋值给 @State 变量,才能触发 UI 更新。这是 ArkTS 状态管理的一个重要特点——数组需要「替换引用」而非「修改内容」。

7.6 分类选择器 UI

Scroll() {
  Row() {
    ForEach(
      BLESSING_DATA,
      (category: BlessingCategory, index: number) => {
        Column() {
          Text(category.emoji).fontSize(28).margin({ bottom: 4 })
          Text(category.name)
            .fontSize(12)
            .fontColor(this.selectedIndex === index ? category.color : '#636E72')
            .fontWeight(this.selectedIndex === index ? FontWeight.Bold : FontWeight.Normal)
        }
        .padding({ top: 8, bottom: 8, left: 16, right: 16 })
        .backgroundColor(this.selectedIndex === index ? category.bgColor : '#FFFFFF')
        .borderRadius(20)
        .margin({ left: index === 0 ? 16 : 0, right: 8 })
        .shadow({ ... })
        .onClick(() => { this.selectCategory(index); })
      },
      (category: BlessingCategory, index: number) => category.name + index
    )
  }
  .alignItems(VerticalAlign.Center).height(80)
}

交互设计: 点击分类时,如果点击的是当前分类则「重新生成」;点击其他分类则切换到该分类并「生成第一条」。这种设计让用户无论点击哪个分类都会获得新内容,避免「点击了但没反应」的困惑。


8. 分手模拟器:BreakupSimulator 互动叙事

8.1 玩法设计

分手模拟器是一个基于选择的互动叙事游戏,核心机制是:

  1. 心碎值(0~100):初始值 85(刚分手时心碎值很高)
  2. 恢复进度(0~100):初始值 5
  3. 天数:从第 1 天到第 60 天
  4. 三种结局:彻底释怀(good ending)、深陷回忆(normal ending)、重蹈覆辙(bad ending)

游戏通过 10 个精心设计的场景来模拟分手后的心理历程。

8.2 数据结构

interface SceneChoice {
  text: string;            // 选项文字
  heartDelta: number;      // 心碎值变化(负值 = 减轻心碎)
  progressDelta: number;   // 恢复进度变化
  reply: string;           // 选择后的内心独白
  isRelapse?: boolean;     // 是否导致"复发"
}

interface Scene {
  title: string;           // 场景标题(如 "第一天 · 难以置信")
  setting: string;         // 场景描述
  choices: SceneChoice[];  // 3 个可选行动
}

8.3 10 个场景的时间线

天数 场景标题 心理学对应阶段
第一天 难以置信 否认期(Denial)
第三天 愤怒 愤怒期(Anger)
第五天 回忆 回忆期(Memory)
第七天 挣扎 挣扎期(Struggle)
第十天 学着独处 适应期(Adaptation)
第十四天 反思 反思期(Reflection)
第二十一天 习惯 习惯期(Habit)
第二十八天 新生 重生期(Rebirth)
第四十五天 偶遇 考验期(Test)
第六十天 释怀 接受期(Acceptance)

心理学设计: 这 10 个场景暗合了 Kübler-Ross 的悲伤五阶段理论(否认→愤怒→协商→抑郁→接受),并扩展了更多细致的心理阶段,使游戏体验更加真实。

8.4 结局判定逻辑

private selectChoice(choice: SceneChoice): void {
  // ...(状态更新)

  // 检查是否复发(心碎值爆表)
  if (choice.isRelapse && this.heartbreak >= 90) {
    this.isGameOver = true;
    this.endingType = 'relapse';
    this.endingText = '你最终还是没能忍住。那条消息之后,你们又纠缠了一个月,比分手更痛苦。\n\n有些门,关上之后就不该再打开了。';
    return;
  }

  // 检查是否完全恢复
  if (this.recovery >= 100 && this.heartbreak <= 15) {
    this.isGameOver = true;
    this.endingType = 'recovered';
    this.endingText = '你终于走出来了。\n\n那些哭过的夜晚、删掉的照片、走过的路,都成了你的一部分。\n\n—— 分手不是结束,而是新生的开始。';
    return;
  }

  // 心碎值打满但恢复不够 = stuck ending
  if (this.heartbreak <= 0 && this.recovery < 60) {
    this.isGameOver = true;
    this.endingType = 'stuck';
    this.endingText = '你不再心碎了,但也没有真正走出来。\n\n你把那段感情封印在了心底最深处,假装什么都没发生过。\n\n—— 真正的放下,不是忘记,而是坦然面对。';
    return;
  }

  // 进入下一场景
  this.currentScene++;
  if (this.currentScene >= this.scenes.length) {
    this.currentScene = 0;
    this.day += 3;  // 循环后跳过一段时间
  }
}

判定优先级:

  1. 复发结局(触发 isRelapse + 心碎值 ≥ 90)
  2. 完美结局(恢复进度 ≥ 100 + 心碎值 ≤ 15)
  3. 假装释怀结局(心碎值 ≤ 0 + 恢复进度 < 60)
  4. 继续游戏(以上都不满足)

8.5 纪念物品系统

private allMemos: MemoItem[] = [
  { id: 1, name: '电影票根', desc: '第一次一起看电影的票根', emoji: '🎬', kept: true },
  { id: 2, name: '情侣手链', desc: '那对刻着名字的手链', emoji: '📿', kept: true },
  { id: 3, name: '合照相框', desc: '笑得最开心的一张合照', emoji: '🖼️', kept: true },
  { id: 4, name: '日记本', desc: '记录着点点滴滴的日记本', emoji: '📔', kept: true },
  { id: 5, name: '纪念礼物', desc: '生日时送的礼物', emoji: '🎁', kept: true },
];

private discardMemo(index: number): void {
  const memo = this.allMemos[index];
  if (!memo.kept) return;

  memo.kept = false;  // 标记为已丢弃
  this.addDiary(`📦 扔掉了 ${memo.emoji} ${memo.name} —— ${memo.desc}`);
  this.heartbreak = Math.max(0, this.heartbreak - 8);  // 减轻心碎
  this.recovery = Math.min(100, this.recovery + 5);     // 加速恢复
}

象征意义: 每丢弃一件纪念物品,心碎值减少 8,恢复进度增加 5。这模拟了现实生活中「放下过去」的过程——每次放手虽然不舍,但都是愈合的一步。

8.6 日记系统

private writeDiary(): void {
  if (!this.diaryText.trim()) {
    promptAction.showToast({ message: '写点什么吧……', duration: 1000 });
    return;
  }

  this.addDiary(`📝 ${this.diaryText.trim()}`);
  this.heartbreak = Math.max(0, this.heartbreak - 3);  // 写日记减轻心碎
  this.recovery = Math.min(100, this.recovery + 2);     // 写日记加速恢复
  this.diaryText = '';
}

// 随机日记提示
private usePrompt(): void {
  const prompt = this.diaryPrompts[Math.floor(Math.random() * this.diaryPrompts.length)];
  this.diaryText = prompt;
}

写日记的疗愈效果: 每次写日记减少 3 点心碎值、增加 2 点恢复进度,虽然效果不如丢弃纪念物品显著,但可以无限次使用,鼓励用户通过文字表达来治愈自己。

8.7 三种结局的文案设计

// 好结局——彻底释怀
endingText = '你终于走出来了。\n\n' +
  '那些哭过的夜晚、删掉的照片、走过的路,都成了你的一部分。\n\n' +
  '你变得更坚强了。下一次爱情来的时候,你会更好地拥抱它。\n\n' +
  '—— 分手不是结束,而是新生的开始。';

// 中等结局——假装释怀
endingText = '你不再心碎了,但也没有真正走出来。\n\n' +
  '你把那段感情封印在了心底最深处,假装什么都没发生过。\n\n' +
  '可是深夜偶尔梦到的时候,醒来还是会发呆很久。\n\n' +
  '—— 真正的放下,不是忘记,而是坦然面对。';

// 坏结局——重蹈覆辙
endingText = '你最终还是没能忍住。那条消息之后,\n\n' +
  '你们又纠缠了一个月,比分手更痛苦。\n\n' +
  '有些门,关上之后就不该再打开了。';

9. 舔狗模拟器:SimpSimulator 社交模拟游戏

9.1 玩法设计

舔狗模拟器是一款更复杂的社交养成游戏,核心机制:

  1. 好感度(0~100):初始值 30
  2. 零花钱:初始值 ¥100(买礼物、发消息都要花钱)
  3. 天数:随时间推进
  4. 4 个功能面板:聊天、送礼、打卡、成就
  5. 8 个成就:从"初次搭讪"到"完美结局"

9.2 核心状态

@State affection: number = 30;           // 好感度
@State money: number = 100;              // 零花钱
@State day: number = 1;
@State currentScene: number = 0;
@State messages: ChatMessage[] = [];     // 聊天记录
@State isGameOver: boolean = false;
@State isWin: boolean = false;
@State achievements: Achievement[] = [];
@State activePanel: string = 'chat';     // chat | gift | daily | achievement
@State dailyDone: boolean = false;
@State streakDays: number = 0;

复杂度分析: 舔狗模拟器有 12 个状态变量,是项目中最复杂的页面。每个状态变量都代表游戏的一个维度,它们之间相互影响,构成一个完整的游戏循环。

9.3 礼物系统

private giftList: GiftItem[] = [
  { name: '一杯奶茶', cost: 15, affection: 3, emoji: '🧋' },
  { name: '一束鲜花', cost: 30, affection: 6, emoji: '💐' },
  { name: '巧克力礼盒', cost: 25, affection: 5, emoji: '🍫' },
  { name: '精致手链', cost: 50, affection: 10, emoji: '📿' },
  { name: '名牌香水', cost: 80, affection: 15, emoji: '🧴' },
];

private sendGift(index: number): void {
  const gift = this.giftList[index];
  if (this.money < gift.cost) {
    promptAction.showToast({ 
      message: '余额不足,先去做每日任务赚钱吧!', 
      duration: 2000 
    });
    return;
  }

  this.money -= gift.cost;
  this.affection = Math.min(100, this.affection + gift.affection);

  this.addMessage('me', `💝 送出了 ${gift.emoji} ${gift.name}`);
  setTimeout(() => {
    this.addMessage('crush', `你送我的${gift.name}收到了!好喜欢😊`);
    // 贵重礼物触发额外剧情
    if (gift.cost >= 50) {
      setTimeout(() => {
        this.addMessage('crush', '不过……下次别花这么多钱了,我会心疼的 💕');
      }, 600);
    }
  }, 600);
}

游戏平衡设计:

  • 奶茶(¥15/+3)和巧克力(¥25/+5)是日常消耗品,性价比中等
  • 鲜花(¥30/+6)性价比略高
  • 手链(¥50/+10)是贵重礼物,触发额外对话
  • 香水(¥80/+15)是最贵的选择,适合冲刺好感度
  • 每日签到可以获得 ¥20 零花钱,鼓励用户持续参与

9.4 每日签到系统

private doDailyGreeting(): void {
  if (this.dailyDone) {
    promptAction.showToast({ message: '今天已经打过卡啦,明天再来吧!', duration: 1500 });
    return;
  }

  this.dailyDone = true;
  this.streakDays++;

  const greeting = this.dailyGreetings[Math.floor(Math.random() * this.dailyGreetings.length)];
  this.addMessage('me', greeting);

  const affectionGain = 3 + Math.min(this.streakDays, 7); // 连续签到加成
  this.affection = Math.min(100, this.affection + affectionGain);
  this.money += 20;
  // ...
}

连续签到加成机制: 基础好感度奖励 3,连续签到每多一天 +1,上限 7 天。这意味着第 7 天签到可以获得 3+7=10 点好感度,是日常收益最大化的关键。

9.5 成就系统

private allAchievements: Achievement[] = [
  { id: 'first_chat', name: '初次搭讪', desc: '发送第一条消息', unlocked: false, icon: '💬' },
  { id: 'gift_giver', name: '送礼达人', desc: '送出 3 次礼物', unlocked: false, icon: '🎁' },
  { id: 'streak_3', name: '坚持不懈', desc: '连续签到 3 天', unlocked: false, icon: '🔥' },
  { id: 'affection_50', name: '有点意思', desc: '好感度达到 50', unlocked: false, icon: '😊' },
  { id: 'affection_80', name: '好感爆棚', desc: '好感度达到 80', unlocked: false, icon: '💕' },
  { id: 'survive_5', name: '撑过五关', desc: '存活 5 个场景', unlocked: false, icon: '🛡️' },
  { id: 'rich_gift', name: '挥金如土', desc: '单次送礼超过 50 元', unlocked: false, icon: '💰' },
  { id: 'perfect', name: '完美结局', desc: '达成美满结局', unlocked: false, icon: '👑' },
];

成就触发时机:

成就 触发条件 触发位置
初次搭讪 messages.length ≥ 2 checkAchievements
送礼达人 giftCount ≥ 3 checkAchievements
坚持不懈 streakDays ≥ 3 checkAchievements
有点意思 affection ≥ 50 checkAchievements
好感爆棚 affection ≥ 80 checkAchievements
撑过五关 sceneCount ≥ 5 checkAchievements
挥金如土 送礼 cost ≥ 50 checkRichGift
完美结局 isWin === true checkAchievements

9.6 场景轮询与游戏循环

private selectChoice(choice: Choice): void {
  // ...(更新好感度、扣钱)

  this.currentScene++;
  if (this.currentScene >= this.scenes.length) {
    // 所有 10 个场景走完一轮后
    this.currentScene = 0;
    this.day++;
    this.dailyDone = false;
    this.affection = Math.min(100, this.affection + 1);  // 日积月累 +1
    this.addSystemMessage(`📅 第 ${this.day} 天开始了……`);
  }
}

游戏循环设计: 10 个场景为一轮,走完一轮后进入下一天。每天自动增加 1 点好感度(象征时间治愈一切),同时重置签到状态。这种设计让游戏可以无限进行下去,直到达成结局。


10. ArkTS 布局专题:ColumnStart 与 Row 垂直居中

10.1 ColumnStart 布局模式

ColumnStartDemo.ets 演示了 ArkTS 中一种非常实用的布局模式:

Column() {
  // 子组件列表...
}
.width('100%')
// ★★★ 核心配置 ★★★
.justifyContent(FlexAlign.Start)   // 垂直方向:顶部对齐

效果: 所有子组件在容器中「左上角对齐」,形成自然的阅读顺序。

典型应用场景:

  1. 信息流列表 — 标题、摘要、标签全部左对齐
  2. 表单页面 — 标签在上、输入框在下,左侧对齐
  3. 个人资料页 — 头像、姓名、简介等组件左上排列
  4. 设置页面 — 设置项列表,每个条目左对齐

信息流卡片示例:

@Builder
buildFeedCard(item: FeedItem): void {
  Column() {
    Row() {
      Text(item.icon).fontSize(24).margin({ right: 10 })
      Text(item.title).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#2C3E50')
    }
    .margin({ bottom: 6 })

    Text(item.summary)
      .fontSize(13).fontColor('#636E72').lineHeight(20)
      .margin({ bottom: 10 })

    Row() {
      Text(item.tag)
        .fontSize(11).fontColor('#FFFFFF')
        .backgroundColor('#4ECDC4')
        .padding({ left: 8, right: 8, top: 2, bottom: 2 })
        .borderRadius(10)
      Text('·').fontSize(11).fontColor('#BDC3C7').margin({ left: 8, right: 8 })
      Text(item.time).fontSize(11).fontColor('#BDC3C7')
    }
  }
  .width('100%').padding(14)
  .backgroundColor('#FFFFFF').borderRadius(12)
  .border({ width: 1, color: '#EEEEEE' })
  .margin({ bottom: 10 })
}

布局对比演示:

alignItems(Start)     alignItems(Center)      alignItems(End)
┌────────┐            ┌────────┐              ┌────────┐
│A       │            │   A    │              │       A│
│  B     │            │   B    │              │     B  │
│    C   │            │   C    │              │   C    │
└────────┘            └────────┘              └────────┘
(左上对齐)          (上中居中)            (右上对齐)

10.2 Row 垂直居中布局

RowCenterVerticalDemo.ets 详细演示了 RowalignItems(ItemAlign.Center) 布局。

核心概念:

Row 的坐标系:
  主轴(main axis)→ 水平方向(horizontal)
  交叉轴(cross axis)↓ 垂直方向(vertical)

.alignItems(VerticalAlign.Center)
  → 在「交叉轴/垂直方向」上居中对齐所有子组件

场景一:基础垂直居中

Row() {
  // 子组件 A:高度 40px
  Column() { Text('40').fontSize(14).fontColor('#FFFFFF') }
    .width(60).height(40).backgroundColor('#5B8FF9').borderRadius(6)

  // 子组件 B:高度 60px
  Column() { Text('60').fontSize(14).fontColor('#FFFFFF') }
    .width(60).height(60).backgroundColor('#5AD8A6').borderRadius(6)

  // 子组件 C:高度 80px
  Column() { Text('80').fontSize(14).fontColor('#FFFFFF') }
    .width(60).height(80).backgroundColor('#FF9D4D').borderRadius(6)

  // 子组件 D:高度 50px
  Column() { Text('50').fontSize(14).fontColor('#FFFFFF') }
    .width(60).height(50).backgroundColor('#B37FEB').borderRadius(6)
}
.alignItems(VerticalAlign.Center)   // ← 关键:垂直居中
.width('92%').height(100)
.backgroundColor('#FFFFFF').borderRadius(12)

效果说明: Row 高度 100px,子组件高度分别为 40/60/80/50。由于 alignItems(VerticalAlign.Center),四个方块的中线(垂直方向的中点)对齐在同一条水平线上。

场景四:有/无垂直居中的对比

// ❌ 无垂直居中
Row() { /* A(50), B(40), C(60) */ }
.width('100%').height(80)
// .alignItems(VerticalAlign.Center)  ← 没有设置!
// 效果:三个方块顶部对齐

// ✅ 有垂直居中
Row() { /* A(50), B(40), C(60) */ }
.width('100%').height(80)
.alignItems(VerticalAlign.Center)    ← 加上这一行!
// 效果:三个方块中线对齐

实战经验: 在开发 HarmonyOS 应用时,90% 的列表项对齐问题都可以通过以下两种方式解决:

  • Column + alignItems(Start) → 信息流/列表
  • Row + alignItems(VerticalAlign.Center) → 图文混合/消息气泡

11. 路由导航与页面间跳转

11.1 路由注册

在 HarmonyOS NEXT 中,页面路由需要在 main_pages.json 中注册(通常在 entry/src/main/resources/base/profile/main_pages.json):

{
  "src": [
    "pages/Index",
    "pages/CultivationGuide",
    "pages/AIHandbook",
    "pages/BlessingGenerator",
    "pages/BreakupSimulator",
    "pages/SimpSimulator",
    "pages/ColumnStartDemo",
    "pages/RowCenterVerticalDemo"
  ]
}

11.2 页面跳转

import router from '@ohos.router';

// 跳转到指定页面
private goToPage(): void {
  router.pushUrl({ url: 'pages/CultivationGuide' });
}

// 返回上一页
private goBack(): void {
  router.back();
}

11.3 导航栏实现

在 Index.ets 中,导航区域通过 @Builder 组件化实现:

const NAV_ITEMS: NavItem[] = [
  { label: '🍅 番茄钟', url: 'pages/ColumnStartDemo', color: '#4ECDC4' },
  { label: '📏 Row 垂直居中布局', url: 'pages/RowCenterVerticalDemo', color: '#5B8FF9' },
  { label: '🐶 舔狗模拟器', url: 'pages/SimpSimulator', color: '#E74C3C' },
  { label: '💔 分手模拟器', url: 'pages/BreakupSimulator', color: '#8E44AD' },
  { label: '🎉 祝福生成器', url: 'pages/BlessingGenerator', color: '#FDCB6E' },
];

@Builder
buildNavSection() {
  Column() {
    Divider().width('80%').color('#CBD5E0').opacity(0.5)
      .margin({ top: 20, bottom: 16 })

    Text('📂 更多应用')
      .fontSize(14).fontWeight(FontWeight.Bold)
      .fontColor('#2D3748').margin({ bottom: 12 })

    ForEach(NAV_ITEMS, (item: NavItem) => {
      Button(item.label)
        .fontSize(14).fontColor('#FFFFFF')
        .backgroundColor(item.color)
        .borderRadius(20).height(40).width(200)
        .onClick(() => { router.pushUrl({ url: item.url }); })
        .margin({ bottom: 8 })
    }, (item: NavItem) => item.label)
  }
  .width('100%').alignItems(HorizontalAlign.Center)
}

路由最佳实践:

  • 页面跳转使用 router.pushUrl,返回使用 router.back()
  • 在 AI 对话页面中,离开前应调用 cancelAI() 取消正在进行的请求,避免网络资源浪费
  • 导航按钮使用不同的颜色区分功能,提高可识别性

12. 总结与最佳实践

12.1 项目亮点

1. 统一的 AI 服务抽象

AIChatService 通过 queryAI(callbacks, messages, customPrompt?) 提供了统一的 AI 对话接口。三个不同的对话页面(校园助手、修仙功法、万能手册)共享同一套网络请求、SSE 解析、错误处理逻辑,仅通过 customPrompt 实现个性化。

2. 灵活的 @Builder 组件化

每个页面的消息气泡、快捷场景面板、导航区域都使用 @Builder 封装为独立组件,既提高了代码复用性,又使 build() 方法结构清晰。

3. 沉浸式主题设计

每个子应用都有独立的视觉主题:

  • 校园助手 → 清新蓝白风
  • 修仙功法 → 仙侠水墨风
  • 分手模拟器 → 紫色治愈风
  • 舔狗模拟器 → 蓝绿社交风
  • 祝福生成器 → 彩色节日风

4. 互动叙事引擎

分手模拟器和舔狗模拟器展示了基于状态驱动的互动叙事模式。通过精心设计的场景、选择分支和多结局判定,为 AI 应用增添了游戏化的趣味性。

12.2 关键技术决策

决策 选择 理由
状态管理 @State 装饰器 原生支持,无额外依赖,适合中小型应用
AI 通信 SSE 流式 + 回调模式 支持逐字输出,用户体验好
页面路由 @ohos.router HarmonyOS 原生路由,无需第三方框架
布局方式 Column/Row + alignItems 声明式布局,代码直观
组件复用 @Builder 方法 内置支持,无额外开销

12.3 开发注意事项

1. ArrayBuffer 转字符串

ArkTS 中 HTTP 响应体以 ArrayBuffer 形式接收,需要手动转换为字符串。推荐使用 Uint8Array + String.fromCharCode 方式:

function arrayBufferToString(buffer: ArrayBuffer): string {
  const uint8Arr = new Uint8Array(buffer);
  let text = '';
  for (let i = 0; i < uint8Arr.length; i++) {
    text += String.fromCharCode(uint8Arr[i]);
  }
  return text;
}

2. @State 数组更新

ArkTS 的 @State 只能检测到数组引用变化,无法检测到 splicepush 等方法对原数组的修改。解决方法:

// ❌ 不会触发 UI 更新
this.myArray.splice(index, 1);

// ✅ 会触发 UI 更新
this.myArray.splice(index, 1);
this.myArray = [...this.myArray];  // 创建新引用

3. SSE 缓冲区处理

SSE 流式数据可能在任何位置截断(一次 dataReceive 事件可能只发送半行),需要维护缓冲区:

let buffer = '';

httpRequest.on('dataReceive', (data: ArrayBuffer) => {
  const text = arrayBufferToString(data);
  buffer += text;

  const lines = buffer.split('\n');
  buffer = lines.pop() ?? '';  // 最后一行可能不完整,留到下次

  for (const line of lines) {
    // 处理完整的行
  }
});

4. 请求取消与资源释放

页面离开或用户取消时,必须销毁 HTTP 请求:

private goBack(): void {
  cancelAI();     // 取消 AI 请求
  router.back();  // 返回上一页
}

12.4 未来可扩展方向

  1. AI 图片生成 — 在校园助手中集成图片生成 API,将「描述图片」的能力升级为「生成图片」
  2. 用户登录系统 — 添加账号系统,实现多设备同步聊天记录和游戏进度
  3. 更多互动游戏 — 基于分手模拟器和舔狗模拟器的框架,开发更多主题的互动叙事游戏
  4. 国际化支持 — 使用 $t() 资源文件,支持英文等多语言
  5. 离线缓存 — 使用 @ohos.data.storage 缓存 AI 对话历史,支持离线查看

本文档基于 HarmonyOS NEXT 6.1.1(API 24)SDK 编写,所有代码已在 DevEco Studio 5.0+ 中验证通过。完整项目代码可参考 MyApplication4 工程源码。

Logo

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

更多推荐