从零打造鸿蒙 PC AI 应用:HarmonyOS 智慧校园助手开发实战
从零打造鸿蒙 PC AI 应用:HarmonyOS 智慧校园助手开发实战
本文完整记录一款「智慧校园助手」应用在鸿蒙 PC(HUAWEI MateBook Pro,HarmonyOS 电脑)上从架构设计、大模型接入、五大功能模块实现到真机运行的全过程,包含大量可复现的 ArkTS 代码与鸿蒙 PC 真机运行截图。不同于常见的鸿蒙手机 Demo,本文聚焦电脑端(2in1 形态) 的开发与适配,适合作为设计参考,也适合想在 HarmonyOS 电脑上落地 AI 应用的开发者。

一、写在前面:为什么在鸿蒙 PC 上做一个「校园助手」
大学生活里,信息是分散的:课表在教务系统,作业 deadline 在各科群里,图书馆规则在公众号,请假流程要问辅导员……学生每天要在十几个入口之间来回切换。而大语言模型的成熟,让「用一句自然语言问出答案」成为可能。
与此同时,鸿蒙生态正在从手机向 PC 延伸。搭载 HarmonyOS 的电脑(如 MateBook Pro)已经成为一个真实的开发与使用场景。相比手机,电脑有更大的屏幕、更适合长文本阅读与录入的键鼠交互——这恰恰非常契合"查课表、管待办、和 AI 深入对话"这类需要沉下心处理信息的校园场景。学生在电脑上写作业、查资料时,一个常驻的智能助手比掏手机更顺手。
于是我萌生了一个想法:在鸿蒙 PC 上做一款把课表、待办、校园问答和 AI 对话整合在一起的智能校园助手。技术上,它要同时具备三种能力:
- 本地数据管理(课表、待办的增删改查与持久化);
- 端云 AI 交互(接入大模型,实现智能问答);
- 面向电脑大屏的良好体验(充分利用 PC 的屏幕空间与交互特性,同时保持一次开发多端可用)。
我选择用 HarmonyOS + ArkTS/ArkUI 来实现——它的声明式 UI 开发效率高,「一次开发、多端部署」的特性又让同一套代码能覆盖手机、平板到 PC。而本文的全部开发与运行,都是在鸿蒙 PC 真机(MateBook Pro) 上完成的。
先看一眼成品。下图是应用的 AI 助手主界面,以窗口形式运行在鸿蒙 PC 桌面上:

可以看到,应用以窗口化的形式运行在鸿蒙 PC 桌面环境中,对话界面会主动介绍助手能提供的服务分类(学习支持、校园服务指南、生活与成长支持),底部还有快捷问题引导。这是一款完成度较高、可以真正在电脑上日常使用的应用,而不是停留在概念阶段的 Demo。
二、总体架构设计
在动手写代码前,先做架构设计。一个可维护的应用,必须有清晰的分层。我把整个工程分为四层:

各层职责如下:
- entryability:处理 Ability 生命周期,初始化本地存储、配置沉浸式窗口、计算安全区。
- common:
Theme.ets统一管理配色与尺寸;AIConfig.ets集中管理大模型服务的地址、密钥、模型名与系统提示词。 - model:
AIService.ets封装大模型的网络请求与响应解析;CampusStore.ets负责课程与待办的数据模型和持久化。 - pages:五个功能页(助手 / 课表 / 问答 / 待办 / 我的)+ 一个 Tab 主壳。
这种分层的核心原则是关注点分离:UI 层只管展示与交互,不直接碰网络和存储;数据获取和 AI 请求全部收敛到 model 层;配置集中在 common 层,改起来一处即可。
下图是工程在 DevEco Studio 中的结构,可以看到清晰的目录组织:

三、大模型接入层:全应用的智能核心
这是整个应用最关键的部分。所有的智能问答能力,都建立在与大模型的稳定交互之上。
3.1 配置集中化
我把所有与模型服务相关的配置抽到一个 AIConfig 类里。这样做的好处是:换服务、换模型、改提示词,都只需要动一个文件。
export class AIConfig {
/** 模型服务地址(OpenAI 兼容 chat/completions 接口) */
static readonly endpoint: string =
'https://xxxxxxxx/compatible-mode/v1/chat/completions';
/** 访问密钥(可在设置页运行时覆盖) */
static readonly apiKey: string = 'sk-****************';
/** 模型标识 */
static readonly model: string = 'qwen-plus';
/** 系统提示词:把模型约束为"校园助手"身份 */
static readonly systemPrompt: string =
'你是"智慧校园助手",一款面向高校学生的智能助手。你熟悉校园生活的方方面面:' +
'选课、考试、图书馆、请假流程、社团活动、宿舍管理、校园卡、四六级、奖学金等。' +
'请用简洁、友好、条理清晰的中文回答学生的问题。';
static readonly temperature: number = 0.7;
static readonly maxTokens: number = 800;
static readonly timeoutMs: number = 30000;
}
这里有几个设计要点值得强调:
其一,采用 OpenAI 兼容协议。 现在主流的大模型服务大多提供 OpenAI 兼容的 chat/completions 接口。基于这个协议开发,意味着应用不绑定任何特定的模型服务商——今天用这家,明天想换一家,只要它兼容该协议,改一下 endpoint 和 model 即可,业务代码一行不用动。这是一个很重要的架构解耦。
其二,用 systemPrompt 塑造助手人格。 大模型本身是通用的,但通过一段精心设计的系统提示词,可以把它约束成一个特定角色。这段提示词告诉模型:“你是校园助手,聚焦校园场景,用简洁友好的中文回答”。这就是所谓的"提示工程"——用自然语言给 AI 定义行为边界,是 AI 应用开发的一项核心技能。
其三,密钥可运行时覆盖。 apiKey 虽然有内置默认值,但应用支持在设置页填写覆盖(存到 AppStorage,请求时优先使用)。这让应用具备了灵活性,也为后续把密钥迁移到更安全的存储留了口子。
3.2 网络请求与响应解析
有了配置,接下来是真正发起请求的 AIService。鸿蒙用 @kit.NetworkKit 的 http 模块发起网络请求。
先定义清晰的数据结构。这里有个 ArkTS 的强约束需要注意:ArkTS 严格模式不允许无类型的对象字面量(arkts-no-untyped-obj-literals),所以请求体也要显式定义接口:
/** 一条对话消息 */
export interface ChatMessage {
role: string; // system | user | assistant
content: string;
}
/** chat/completions 请求体 */
interface ChatRequest {
model: string;
messages: ChatMessage[];
temperature: number;
max_tokens: number;
}
然后是核心的 chat 方法:
export class AIService {
static async chat(history: ChatMessage[]): Promise<string> {
// 组织消息:system 提示词 + 历史对话
const messages: ChatMessage[] = [
{ role: 'system', content: AIConfig.systemPrompt }
];
for (const m of history) {
messages.push(m);
}
const body: ChatRequest = {
model: currentModel(),
messages: messages,
temperature: AIConfig.temperature,
max_tokens: AIConfig.maxTokens
};
const req = http.createHttp();
try {
const resp = await req.request(AIConfig.endpoint, {
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + currentKey()
},
extraData: JSON.stringify(body),
connectTimeout: AIConfig.timeoutMs,
readTimeout: AIConfig.timeoutMs
});
if (resp.responseCode !== 200) {
return `(请求失败:${resp.responseCode})请检查网络或服务配置。`;
}
const text = AIService.parseContent(`${resp.result}`);
return text !== '' ? text : '(助手暂时没有返回内容,请重试)';
} catch (e) {
return '(网络异常)请检查设备网络连接后重试。';
} finally {
req.destroy(); // 务必释放 http 请求对象
}
}
}
几个工程细节:
- 多轮对话上下文:每次请求都把完整的历史消息带上(system + 若干轮 user/assistant),模型才能理解上下文、进行连贯的多轮对话。
- 超时与异常兜底:设置了连接和读取超时;任何异常都返回友好的中文提示,而不是让应用崩溃或卡死。这对 AI 应用尤其重要——网络请求天然不稳定,优雅降级是必须的。
- 资源释放:
finally里req.destroy()释放 http 对象,避免内存泄漏。
响应解析要从 OpenAI 兼容格式的 JSON 里取出 choices[0].message.content:
private static parseContent(raw: string): string {
try {
const obj = JSON.parse(raw) as Record<string, Object>;
const choices = obj['choices'] as Object[];
if (choices !== undefined && choices.length > 0) {
const first = choices[0] as Record<string, Object>;
const msg = first['message'] as Record<string, Object>;
if (msg !== undefined && msg['content'] !== undefined) {
return `${msg['content']}`.trim();
}
}
} catch (e) {
// 解析失败返回空,由上层兜底
}
return '';
}
至此,全应用的"智能核心"就搭好了。任何页面只要调用 AIService.chat(messages),就能拿到大模型的回答。
四、功能模块一:AI 对话助手
这是应用的门面。它本质上是一个聊天界面:用户输入问题,追加到消息列表,调用 AIService 拿到回复,再刷新界面。
4.1 数据结构与状态
interface Bubble {
role: string; // user | assistant
content: string;
loading: boolean; // 是否处于"正在思考"状态
}
@State bubbles: Bubble[] = [];
@State input: string = '';
@State sending: boolean = false;
private scroller: Scroller = new Scroller();
Bubble 里的 loading 字段是个巧妙设计:当用户发送问题后,先插入一个 loading: true 的空 AI 气泡显示"正在思考…",等真正的回复回来后,再把这个气泡的内容替换掉。这样用户能立刻得到反馈,体验流畅。
4.2 发送逻辑
async onSend(): Promise<void> {
const q = this.input.trim();
if (q === '' || this.sending) { return; }
this.input = '';
this.sending = true;
// 追加用户气泡 + AI loading 占位气泡
this.bubbles.push({ role: 'user', content: q, loading: false });
this.bubbles.push({ role: 'assistant', content: '', loading: true });
this.bubbles = this.bubbles.slice(); // 触发数组刷新
this.scrollToEnd();
// 组织历史消息
const history: ChatMessage[] = [];
for (const b of this.bubbles) {
if (!b.loading && b.content !== '') {
history.push({ role: b.role, content: b.content });
}
}
// 请求大模型
const answer = await AIService.chat(history);
// 用回复替换 loading 气泡
const last = this.bubbles[this.bubbles.length - 1];
last.content = answer;
last.loading = false;
this.bubbles = this.bubbles.slice();
this.sending = false;
this.scrollToEnd();
}
这里 this.bubbles = this.bubbles.slice() 是 ArkUI 的一个常见技巧:对 @State 数组做浅拷贝赋值,才能可靠触发 UI 刷新。直接 push 有时不会触发重渲染。
4.3 气泡 UI 与自动滚动
聊天气泡用 @Builder 封装,用户消息靠右(蓝色),助手消息靠左(白底)。发送后通过 Scroller.scrollEdge(Edge.Bottom) 自动滚到底部。
实际运行效果如下——问"期末复习有什么建议?",助手给出了结构化、带 emoji 的实用回答:

可以看到大模型的回答质量相当高:制定计划、善用资源、主动输出,条理清晰。这正是 systemPrompt 约束 + 大模型能力共同作用的结果。
五、功能模块二:课程表
课表是校园应用的刚需。这个模块要实现:周课表网格展示 + 课程增删 + 本地持久化。
5.1 数据模型与持久化
用 Preferences(鸿蒙轻量键值存储)做持久化。课程模型:
export interface Course {
id: string;
name: string; // 课程名
teacher: string; // 教师
location: string; // 地点
day: number; // 周几:1-7
startSection: number; // 起始节次
endSection: number; // 结束节次
colorIndex: number; // 配色索引
}
CampusStore 封装了读写。一个实用细节是首次运行自动写入示例课程——避免用户第一次打开面对空白课表,直接就有内容可看:
static async listCourses(): Promise<Course[]> {
if (CampusStore.store === null) { return demoCourses(); }
const raw = await CampusStore.store.get('courses', '') as string;
if (raw === '') {
const seed = demoCourses(); // 首次:写入示例
await CampusStore.saveCourses(seed);
return seed;
}
return JSON.parse(raw) as Course[];
}
5.2 课表网格的实现
课表网格是这个模块的技术难点。核心思路:左侧是节次列,右侧是 7 天列,每一天是一个 Stack——底层铺背景格子,上层根据课程的起止节次绝对定位课程块。
课程块的高度和上边距根据节次计算:
@Builder
courseBlock(c: Course) {
Column({ space: 2 }) {
Text(c.name).fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
Text(c.location).fontSize(9).fontColor('#FFFFFFDD')
}
.width('92%').padding(5)
.height((c.endSection - c.startSection + 1) * 58 - 4) // 高度=跨越节次数×格高
.margin({ top: (c.startSection - 1) * 58 + 2 }) // 上边距=起始节次偏移
.backgroundColor(C.courseColors[c.colorIndex % C.courseColors.length])
.borderRadius(8)
.onClick(() => { this.onDelete(c); })
}
不同课程用轮转的配色区分,视觉上一目了然。运行效果:

高数、大学英语、数据结构、操作系统、体育五门课,用蓝、绿、橙、紫、青五种颜色排布在周课表上,跨节次的课程(如数据结构占第 3-5 节)也正确地拉伸了高度。点击右下角"+ 添加课程"可以弹出表单新增。
六、功能模块三:校园问答
问答模块解决"高频问题快速查"的需求。它的设计有个巧思:预置答案 + AI 兜底的混合模式。
常见问题分成"教务、生活、图书馆、证书考试"四类。每个问题有两种处理方式:
- 有预置答案的(如"如何借书续借"),直接展示,无需联网、秒开;
- 预置答案为空的(如"图书馆开放时间"这种因校而异的),标记为
AI,点击时实时调用大模型生成回答。
async openAnswer(qa: QA): Promise<void> {
this.answerQ = qa.q;
this.showAnswer = true;
if (qa.a !== '') {
this.answer = qa.a; // 预置答案,直接显示
} else {
this.loading = true; // 走 AI
const history: ChatMessage[] = [{ role: 'user', content: qa.q }];
this.answer = await AIService.chat(history);
this.loading = false;
}
}
这种混合设计非常实用:高频固定的问题用本地答案(快、省、稳),个性化或长尾的问题交给 AI(灵活、覆盖广)。既保证了体验,又控制了 AI 调用成本。
运行效果如下,点击"校园卡丢了怎么办?"弹出预置答案:

带 AI 标记的问题(如"食堂几点开饭")则会调用大模型返回答案。
七、功能模块四:待办日程
待办模块管理作业、考试、事项三类日程,支持增删、完成勾选、截止提醒。
数据模型简单:
export interface Todo {
id: string;
title: string;
type: string; // 作业 | 考试 | 事项
dueTime: number; // 截止时间 ms
done: boolean;
}
列表按截止时间升序排列。一个体验细节是临期标红:距离截止不到 2 天的未完成待办,日期文字会变成红色,起到提醒作用:
isUrgent(t: Todo): boolean {
return !t.done && (t.dueTime - Date.now()) < 2 * 86400000;
}
dueText(ms: number): string {
const diff = Math.ceil((ms - Date.now()) / 86400000);
if (diff < 0) { return '已过期'; }
if (diff === 0) { return '今天'; }
if (diff === 1) { return '明天'; }
return `${diff}天后`;
}
不同类型用不同颜色的标签区分(考试红、作业蓝、事项青),左滑可删除,点击圆形勾选框标记完成。运行效果:

可以看到"英语四级报名(明天)"“高数第三章习题(2天后)”"数据结构期中考(7天后)"三条待办,分别标注了类型和临期状态。
八、功能模块五:我的与设置
"我的"页承担个人信息展示、数据统计和模型服务配置。
最有价值的是模型配置功能。它允许用户在不改代码的情况下,运行时覆盖 API 密钥和模型名——这些覆盖值存入 AppStorage,AIService 请求时优先使用:
function currentKey(): string {
const k = AppStorage.get<string>('cfgApiKey');
return (k !== undefined && k !== '') ? k : AIConfig.apiKey;
}
设置页还实时显示"服务状态"(是否已配置密钥),方便排查问题。运行效果:

顶部是个人卡片,中间是课程数、待办数、助手状态三个统计卡,下方是可展开的模型服务配置区。整体信息层次清晰。
九、应用入口与多端适配
9.1 入口初始化
EntryAbility 在 onWindowStageCreate 里做三件事:初始化本地存储、加载首页、配置沉浸式全屏并计算安全区。
async onWindowStageCreate(windowStage: window.WindowStage): Promise<void> {
await CampusStore.init(this.context); // 初始化存储
windowStage.loadContent('pages/Index', (err) => {
if (err.code) { return; }
const win = windowStage.getMainWindowSync();
win.setWindowLayoutFullScreen(true); // 沉浸式全屏
// 计算安全区(状态栏/导航条高度),存入全局
const top = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
const bottom = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
AppStorage.setOrCreate('safeTop', px2vp(top.topRect.height));
AppStorage.setOrCreate('safeBottom', px2vp(bottom.bottomRect.height));
});
}
计算安全区并存入全局状态,各页面通过 @StorageProp('safeTop') 统一避让——这是适配不同设备(手机刘海、PC 窗口边框)的关键。
9.2 面向鸿蒙 PC 的适配实践
这是本项目区别于普通鸿蒙手机 Demo 的重点。应用的主要运行环境是鸿蒙 PC(2in1 形态),因此适配的核心,是让一套代码在电脑大屏上也有良好表现。
首先在 module.json5 里声明支持的设备类型,把 2in1(PC 形态)纳入:
"deviceTypes": ["phone", "tablet", "2in1"]
其次,真正让应用在鸿蒙 PC 上"不违和",靠的是以下几点工程实践:
其一,全程弹性布局,不写死尺寸。 应用中的宽度全部使用 layoutWeight、百分比与 Flex 自动换行,不出现硬编码的固定像素宽度。这样在鸿蒙 PC 的大窗口下,内容能自然铺展,而不是被拉伸变形或挤在一角。例如课表的 7 天列用 layoutWeight(1) 均分,无论窗口多宽都能等分排布。
其二,窗口化运行与安全区适配。 鸿蒙 PC 上应用以窗口形式运行,非安全区不再是手机的刘海,而是窗口边框。前面 getWindowAvoidArea 计算安全区、存入全局的做法,在 PC 上同样生效——页面统一避让,标题栏不会被窗口边框遮挡。同一套安全区逻辑,覆盖了手机刘海到 PC 窗口边框的差异。
其三,键鼠交互的天然兼容。 ArkUI 的点击、输入、滚动事件在鸿蒙 PC 上会自动映射到鼠标与键盘。比如 AI 对话的输入框,在 PC 上就是用物理键盘输入、回车发送——这对"和 AI 深入对话"这种需要大量文字录入的场景,体验远好于手机虚拟键盘。这也是我选择把校园助手做到 PC 上的重要原因之一。
其四,大屏信息密度。 电脑屏幕大,能一屏承载更多信息。五 Tab 的结构 + 卡片式布局,在 PC 的大窗口里显得从容、不拥挤,课表、待办列表都能一览无余,减少滚动。
本文所有截图,实际上都是在华为 MateBook Pro(鸿蒙 PC) 上运行截取的——从对话、课表到设置页,都是电脑端的真实表现。这正是"一次开发、多端部署"落到 PC 场景的直接体现:我没有为 PC 单独写一套界面,同一套 ArkTS 代码就能在电脑上流畅运行。
在 DevEco Studio 的设备列表里,可以看到鸿蒙生态丰富的目标设备形态,从穿戴、手机一直到 PC:

我的目标设备正是列表里那台真机 HUAWEI MateBook Pro。值得一提的是,SDK 版本已经到了 HarmonyOS 6.1.0,鸿蒙 PC 的开发支持已经相当成熟。选中它直接 Run,应用就以窗口形式跑在了电脑桌面上。
十、五 Tab 主壳
最后用一个 Tabs 把五个页面组织起来,底部导航:
@Entry
@Component
struct Index {
@State current: number = 0;
build() {
Tabs({ barPosition: BarPosition.End, index: this.current }) {
TabContent() { ChatTab() }.tabBar(this.bar('助手', '🤖', 0))
TabContent() { ScheduleTab() }.tabBar(this.bar('课表', '📚', 1))
TabContent() { QATab() }.tabBar(this.bar('问答', '💬', 2))
TabContent() { TodoTab() }.tabBar(this.bar('待办', '📝', 3))
TabContent() { ProfileTab() }.tabBar(this.bar('我的', '🎓', 4))
}
.scrollable(false)
.onChange((idx: number) => { this.current = idx; })
}
}
scrollable(false) 禁用了 Tab 间的滑动切换,避免和页面内的滚动手势冲突——这是个容易被忽略但影响体验的细节。
十一、开发过程中的几个坑
真实开发难免踩坑,记录几个有代表性的,供后来者避雷。
坑一:ArkTS 严格类型,禁止无类型对象字面量。 编译时报 arkts-no-untyped-obj-literals,就是因为直接写了个 const body = { ... } 没有类型。ArkTS 比 TypeScript 严格得多,所有对象字面量必须对应一个明确的 interface 或 class。解决办法就是老老实实为每个数据结构定义接口。
坑二:app.json5 的 SDK 字段。 早期版本我在 app.json5 里写了 compileSdkVersion 等字段,结果 schema 校验报错——这些字段属于 build-profile.json5,app.json5 里只认 minAPIVersion / targetAPIVersion。鸿蒙的配置文件 schema 很严格,字段放错位置就编译不过。
坑三:@State 数组刷新。 直接对数组 push 有时不触发 UI 重渲染,需要 this.arr = this.arr.slice() 做一次浅拷贝赋值。这在聊天列表、待办列表里反复用到。
坑四:http 请求对象要释放。 每次 http.createHttp() 创建的对象,用完必须在 finally 里 destroy(),否则频繁请求会泄漏资源。
坑五:构建环境的干扰。 曾遇到 NODE_OPTIONS 环境变量被外部工具污染,导致 hvigor 构建报 ERR_WORKER_INVALID_EXEC_ARGV,连 IDE 内部 Sync 都失败。这类问题和代码无关,但排查很费时——彻底重启 IDE、清理环境变量即可。
十二、可扩展方向与总结
这款「智慧校园助手」已经是一个五模块齐全、可在鸿蒙 PC 上真机运行的完整应用。如果要继续深化,还有不少方向:
- 流式输出:目前 AI 回复是一次性返回,可改造成 SSE 流式,实现打字机效果,在 PC 大屏上阅读长回答的体验会更好。
- 本地知识库 + RAG:把学校的规章制度做成本地知识库,检索后拼进提示词,让 AI 回答更贴合本校实际。
- 课表智能导入:拍照识别或粘贴文本,用大模型解析成结构化课表,免去手动录入。
- 多端协同:结合鸿蒙分布式能力,实现手机与鸿蒙 PC 间的数据流转与任务接续——比如手机上加的待办,电脑端实时同步。
- PC 端窗口能力深化:利用鸿蒙 PC 的多窗口、窗口分栏等特性,把课表与 AI 对话并排展示,进一步发挥大屏优势。
回顾整个开发过程,这个项目虽然不大,却完整覆盖了一款现代 AI 应用的关键技术栈:声明式 UI、本地持久化、大模型端云交互、提示工程、面向 PC 大屏的多端适配。更重要的是,它验证了一件事——在鸿蒙 PC 上落地一款体验完整的 AI 应用,是完全可行的,而且开发效率相当高。相比拥挤的手机小屏,电脑端的大屏和键鼠交互,反而让"校园助手"这类信息管理型应用有了更好的用武之地。
对于想做类似项目(尤其是毕业设计)的同学,我的建议是:先把架构分层想清楚,把 AI 接入层做扎实(配置集中、异常兜底、协议解耦),再逐个模块堆功能。 同时,不妨像本文一样,把目光投向鸿蒙 PC——这是一个正在快速成熟、却还相对蓝海的方向,做出来的作品也更有辨识度。
希望这篇基于鸿蒙 PC 的实战复盘对你有帮助。如果你也在 HarmonyOS 电脑上探索 AI 应用,欢迎交流。
更多推荐



所有评论(0)