Electron + Vue3 + AI 做了一个新闻生成器:从 0 到 1 的完整实战记录
一个周末,一杯咖啡,一个能自动生成精美新闻的桌面应用诞生了。本文将完整记录这个项目从构思到落地的全过程,包含技术选型、踩坑记录、设计思路和代码实现。
目录
- 项目背景:为什么要做这个东西?
- 技术选型:为什么选 Electron + Vue3?
- 架构设计:主进程与渲染进程的分工
- 核心功能实现:AI 新闻生成
- UI 设计:iPhone 模拟器的巧妙实现
- 踩坑记录:那些让我抓狂的问题
- 项目总结与展望
项目背景:为什么要做这个东西?
事情是这样的。
上周我在刷技术资讯的时候,突然想到一个问题:能不能做一个工具,让我输入一个主题,它就自动帮我搜索相关新闻,然后生成一个精美的、可以直接展示的 HTML。
图:应用主界面,左侧输入新闻主题,右侧显示生成的新闻列表
这个想法来自于几个痛点:
- 信息过载:每天要刷十几个技术公众号、论坛、新闻网站,信息太分散,看得头大
- 展示不统一:不同来源的新闻格式各异,复制粘贴后排版乱七八糟,还得重新调样式
- 时间成本高:手动整理新闻、设计展示样式太费时间,有这时间不如多写两行代码
于是,我决定动手做一个智能新闻生成器。
核心需求很简单:
- 输入主题(比如"人工智能最新进展")
- 自动搜索相关新闻
- 生成美观的 HTML 展示页面
- 支持在桌面端查看
技术选型:为什么选 Electron + Vue3?
桌面端方案对比
在开始前,我对比了几种桌面端开发方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Electron | 生态成熟、Vue/React 都能用、跨平台 | 包体积大、内存占用高 | 复杂桌面应用 |
| Tauri | 包体积小、性能好、Rust 安全 | 生态较新、学习成本高 | 轻量级应用 |
| Flutter Desktop | UI 统一、性能不错 | Dart 学习成本、生态不成熟 | 跨平台应用 |
| 原生开发 | 性能最好、体验最佳 | 开发成本高、维护困难 | 大型专业软件 |
最终选择 Electron 的原因:
- 技术栈熟悉:我对 Vue3 + TypeScript 很熟,可以直接上手
- 生态完善:electron-vite 让项目搭建变得超级简单
- 跨平台:一套代码跑 Windows/macOS/Linux
- AI 集成方便:Node.js 环境下调用 OpenAI API 很顺畅
项目初始化
用 electron-vite 脚手架,一行命令搞定:
npm create electron-vite@latest news-html -- --template vue-ts
这个项目模板已经配置好了:
- Electron 主进程 + 预加载脚本 + 渲染进程的结构
- Vite 作为构建工具(热更新飞快)
- TypeScript 类型支持
- Vue 3 组合式 API
架构设计:主进程与渲染进程的分工
Electron 的核心概念是**主进程(Main Process)和渲染进程(Renderer Process)**的分离:
┌─────────────────────────────────────────────────────────────┐
│ Electron App │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │ Main Process│ │ Renderer Process │ │
│ │ (Node.js) │◄───────►│ (Chromium + Vue3) │ │
│ │ │ IPC │ │ │
│ │ • 窗口管理 │ │ • UI 渲染 │ │
│ │ • API 调用 │ │ • 用户交互 │ │
│ │ • 文件系统 │ │ • 数据展示 │ │
│ └──────────────┘ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
我的分工设计
主进程(src/main/)负责:
- 创建和管理应用窗口
- 调用 OpenAI API 生成新闻
- 处理 IPC 通信
渲染进程(src/renderer/)负责:
- 用户界面展示
- 用户输入处理
- 新闻列表渲染
关键代码:主进程窗口创建
// src/main/index.ts
function createWindow(): void {
const mainWindow = new BrowserWindow({
width: 1280,
height: 720,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
// 加载渲染进程页面
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
IPC 通信:连接两个世界
主进程和渲染进程通过 IPC(进程间通信)交换数据:
// 渲染进程发送消息
window.electron.ipcRenderer.send('generate-news', '人工智能最新进展')
// 主进程监听并处理
ipcMain.on('generate-news', (event, arg) => {
generateNews(arg).then((news) => {
event.reply('generate-news', news)
})
})
核心功能实现:AI 新闻生成
这是整个项目最核心、也最有趣的部分。
设计思路
我不想只是简单地调用 API 返回文字,而是希望:
- 联网搜索:获取最新的真实新闻,而不是训练数据里的旧信息
- 结构化输出:返回 JSON 格式,方便前端解析
- 自动生成 HTML:每条新闻都附带精美的 HTML 样式
OpenAI API 调用
使用了 OpenAI 的 Responses API,支持联网搜索:
// src/main/client.ts
const response = await openai.responses.create({
model: MODEL_ID,
input: [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: USER_PROMPT_TEMPLATE(input) }
],
// 启用联网搜索
tools: [{ type: 'web_search' }],
// 强制返回 JSON 格式
text: {
format: {
type: 'json_schema',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
title: { type: 'string' },
source: { type: 'string' },
date: { type: 'string' },
content: { type: 'string' },
url: { type: 'string' },
html: { type: 'string' } // 关键:包含 HTML 样式
}
}
}
}
}
})
Prompt 工程:让 AI 生成漂亮的 HTML
Prompt 设计是这个功能的灵魂。我花了很长时间调试,最终版本包含:
1. 明确的角色设定
你是一个专业的新闻内容生成助手,擅长搜索最新新闻并设计精美的HTML展示页面。
2. 详细的设计规范
- 移动端优先(375-430px 宽度适配)
- 内联样式(所有 CSS 用 style 属性)
- 视觉层次清晰
- 现代简约风格
3. 具体的样式参数
卡片设计:
- 背景:白色或 #f8f9fa
- 边框:1px solid #e9ecef
- 阴影:0 2px 8px rgba(0,0,0,0.05)
- 圆角:8-12px
- 内边距:20-24px
标题设计:
- 字体大小:20-22px
- 字重:600-700
- 颜色:#212529
效果展示
输入:"帮我查找 3 条最近一周关于 AI 的新闻"
输出:结构化的 JSON 数据,每条新闻包含:
- 标题、来源、日期、正文
- 一段完整的、带内联样式的 HTML 代码
这样前端只需要用 v-html 直接渲染就行,超级方便!
图:输入主题后,AI 自动搜索并生成新闻列表
UI 设计:iPhone 模拟器的巧妙实现
为了让新闻展示更有沉浸感,我设计了一个 iPhone 模拟器界面。
设计思路
- 用一个 SVG 作为 iPhone 外框
- 内部是一个可滚动的内容区域
- 新闻 HTML 渲染在"手机屏幕"里
组件结构
<!-- src/renderer/src/components/Iphone.vue -->
<template>
<div class="iphone-container">
<!-- iPhone 外框 SVG -->
<img src="@/assets/iphone.svg" class="iphone" />
<!-- 拖动区域 -->
<div class="drag-area"></div>
<!-- 关闭按钮 -->
<div class="close-btn" @click="closeWindow"></div>
<!-- 屏幕内容区 -->
<div class="iphone-screen">
<div class="screen-top"></div>
<div class="screen-content">
<slot></slot> <!-- 新闻内容插槽 -->
</div>
<div class="screen-bottom"></div>
</div>
</div>
</template>
关键 CSS 技巧
.iphone-screen {
position: absolute;
padding-left: 23px; // 左边框宽度
padding-right: 22px; // 右边框宽度
.screen-content {
flex: 1;
overflow-y: auto; // 可滚动
border-radius: 0 0 49px 51px; // 底部圆角适配
// 隐藏滚动条
&::-webkit-scrollbar {
display: none;
}
}
}
窗口拖动实现
Electron 支持 -webkit-app-region: drag 让元素可拖动:
.drag-area {
position: absolute;
top: 0;
width: 280px;
height: 40px;
-webkit-app-region: drag; // 关键属性
}
这样用户就可以拖动 iPhone 顶部的"刘海"区域来移动窗口。
图:点击新闻后弹出 iPhone 模拟器窗口,展示新闻详情
踩过的坑
坑 1:同一个 Vue 应用要同时充当主界面和详情界面
问题:我的设计是点击新闻列表后,弹出一个新窗口显示详情。但两个窗口都加载同一个 index.html,怎么区分哪个是主窗口、哪个是详情窗口?
解决:用 IPC 通信传递窗口类型标识:
// 主进程创建详情窗口后,等页面加载完发送窗口类型
detailWindow.webContents.on('did-finish-load', () => {
detailWindow.webContents.send('window-type', 'detail')
detailWindow.webContents.send('load-news-detail', news)
})
// 渲染进程根据类型渲染不同内容
const isDetailWindow = ref(false)
onMounted(() => {
window.electron.ipcRenderer.on('window-type', (_, type) => {
if (type === 'detail') {
isDetailWindow.value = true
}
})
})
然后用 v-if 判断渲染主界面还是详情界面:
<template>
<div v-if="isDetailWindow && detailNews" class="news-detail">
<Iphone>
<div class="phone-content" v-html="detailNews.html"></div>
</Iphone>
</div>
<el-container v-else class="container">
<!-- 主界面内容 -->
</el-container>
</template>
坑 2:AI 返回的 JSON 格式不稳定
问题:我要求 OpenAI 返回 JSON 格式,但有时候它会返回 markdown 代码块(json ... ),有时候又会漏掉某些字段,导致 JSON.parse 报错。
解决:在 parser.ts 里写了一个健壮的解析函数:
export function parseNewsResponse(response: any): ParsedNewsResponse {
try {
const outputText = response.output_text || response.output?.[0]?.text
if (!outputText) {
return { success: false, data: [], error: '响应中没有找到输出内容' }
}
let parsedData: any
try {
parsedData = JSON.parse(outputText)
} catch (parseError) {
// 尝试清理 markdown 代码块标记后再解析
const cleaned = outputText.replace(/```json\s*|\s*```/g, '')
parsedData = JSON.parse(cleaned)
}
// 防御性编程:确保每个字段都有值
const newsItems: NewsItem[] = []
for (const item of parsedData) {
newsItems.push({
title: item.title || '',
source: item.source || '',
date: item.date || '',
content: item.content || '',
url: item.url || '',
html: item.html || ''
})
}
return { success: true, data: newsItems }
} catch (error) {
return { success: false, data: [], error: `解析失败: ${error}` }
}
}
坑 3:iPhone 屏幕内容区域尺寸调死人
问题:iPhone 的 SVG 外框有边框宽度,内容区域要正好卡在屏幕里面,不能超出圆角,也不能留太多白边。我调了 N 次 padding 和 border-radius。
解决:最终试出来的参数:
.iphone-screen {
padding-left: 23px; // 左边框宽度
padding-right: 22px; // 右边框宽度(右边比左边少 1px,因为 SVG 不对称)
.screen-content {
border-bottom-right-radius: 49px;
border-bottom-left-radius: 51px; // 左右圆角也不一样大!
}
}
这个真的是像素级调试,每次改完都要重启应用看效果,烦死了。
坑 4:关闭按钮和拖动区域重叠
问题:iPhone 顶部的刘海区域要支持拖动窗口,但右上角又要放关闭按钮。如果拖动区域盖住了关闭按钮,就点不到关闭;如果关闭按钮在拖动区域上面,又拖不动窗口。
解决:用 z-index 分层,并且把拖动区域做窄一点:
.drag-area {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 280px; // 只覆盖中间部分
height: 40px;
z-index: 20;
-webkit-app-region: drag;
}
.close-btn {
position: absolute;
top: 0;
right: 0;
width: 30px;
height: 30px;
z-index: 30; // 比拖动区域高
cursor: pointer;
}
拖动区域只覆盖顶部中间(刘海位置),关闭按钮放在右上角,两者不重叠。
坑 5:Prompt 调了好几个小时
问题:AI 生成的 HTML 样式总是不符合预期,有时候卡片太宽占满屏幕,有时候颜色太花哨,有时候又忘记加按钮。
解决:在 prompt.ts 里写了一个超详细的 Prompt,包括:
- 明确的角色设定:"你是一个专业的新闻内容生成助手"
- 详细的设计规范:颜色、字体、间距、阴影都给出具体数值
- 完整的 HTML 示例:直接给一个样板代码让 AI 参考
- 强调约束条件:移动端优先、内联样式、不要占满屏幕等
Prompt 工程真的是个体力活,改了十几次才稳定。
坑 6:详情窗口关闭后再打不开
问题:一开始我用 win.close() 关闭详情窗口,但再次点击新闻时,窗口对象还在内存里,只是隐藏了,再次 show() 会报错。
解决:改用 win.destroy() 彻底销毁窗口,每次打开详情都创建新实例:
ipcMain.on('close-current-window', (event) => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
if (win) {
win.destroy() // 彻底销毁,不是 close()
}
})
项目总结与展望
已完成的功能
✅ 智能新闻搜索与生成 ✅ 精美的 HTML 自动渲染 ✅ iPhone 模拟器展示效果 ✅ 多窗口管理(主窗口 + 详情窗口) ✅ 跨平台支持(Windows/macOS/Linux)
技术收获
这次做下来,学到了不少东西:
- Electron 开发流程:主进程/渲染进程分工、IPC 通信、窗口管理,一开始搞不清谁干啥,后来就顺手了
- AI Prompt 工程:调Prompt调到想砸键盘,改了十几次才稳定,终于知道怎么让AI乖乖返回JSON了
- Vue3 + TypeScript:组合式API用起来挺爽的,类型安全确实能少踩坑
- 桌面端 UI 设计:无边框窗口、自定义标题栏、拖动区域,这些细节调起来真费劲
未来可以做的优化
🔲 缓存机制:避免重复搜索相同主题 🔲 导出功能:支持导出为 PDF 或图片 🔲 历史记录:保存生成过的新闻 🔲 自定义样式:让用户选择不同的 HTML 模板 🔲 多语言支持:英文新闻生成
写在最后
这个项目从构思到完成,大概花了一个周末的时间。
说实话,最耗时的不是写代码,而是调试 Prompt——让 AI 稳定地返回符合要求的 JSON 和 HTML,真的需要反复尝试。调到想砸键盘,改了十几次才稳定。
但看到最终效果的那一刻,值了!
输入"量子计算最新进展",点击生成,过一段时间后,一个精美的 iPhone 界面弹出,里面是最新的量子计算新闻,排版精美、信息完整。
图:完整的新闻生成和展示流程
虽然过程有点折腾,但做出来之后确实挺爽的。
源码与使用
项目已开源,欢迎 Star 和 PR:
# 克隆项目
git clone https://github.com/black542684/news-html.git
cd news-html
# 安装依赖
yarn
# 配置 OpenAI API Key
cp .env.example .env
# 编辑 .env 文件,填入你的 API Key
# 启动开发模式
yarn dev
# 打包构建
yarn build:win # Windows
yarn build:mac # macOS
yarn build:linux # Linux
感谢阅读!如果觉得有用,欢迎点赞、收藏、转发~
有任何问题,欢迎在评论区留言交流!