Electron + Vue3 + AI 做了一个新闻生成器:从 0 到 1 的完整实战记录

0 阅读11分钟

Electron + Vue3 + AI 做了一个新闻生成器:从 0 到 1 的完整实战记录

一个周末,一杯咖啡,一个能自动生成精美新闻的桌面应用诞生了。本文将完整记录这个项目从构思到落地的全过程,包含技术选型、踩坑记录、设计思路和代码实现。


目录

  1. 项目背景:为什么要做这个东西?
  2. 技术选型:为什么选 Electron + Vue3?
  3. 架构设计:主进程与渲染进程的分工
  4. 核心功能实现:AI 新闻生成
  5. UI 设计:iPhone 模拟器的巧妙实现
  6. 踩坑记录:那些让我抓狂的问题
  7. 项目总结与展望

项目背景:为什么要做这个东西?

事情是这样的。

上周我在刷技术资讯的时候,突然想到一个问题:能不能做一个工具,让我输入一个主题,它就自动帮我搜索相关新闻,然后生成一个精美的、可以直接展示的 HTML。

主界面.png 图:应用主界面,左侧输入新闻主题,右侧显示生成的新闻列表

这个想法来自于几个痛点:

  1. 信息过载:每天要刷十几个技术公众号、论坛、新闻网站,信息太分散,看得头大
  2. 展示不统一:不同来源的新闻格式各异,复制粘贴后排版乱七八糟,还得重新调样式
  3. 时间成本高:手动整理新闻、设计展示样式太费时间,有这时间不如多写两行代码

于是,我决定动手做一个智能新闻生成器

核心需求很简单:

  • 输入主题(比如"人工智能最新进展")
  • 自动搜索相关新闻
  • 生成美观的 HTML 展示页面
  • 支持在桌面端查看

技术选型:为什么选 Electron + Vue3?

桌面端方案对比

在开始前,我对比了几种桌面端开发方案:

方案优点缺点适用场景
Electron生态成熟、Vue/React 都能用、跨平台包体积大、内存占用高复杂桌面应用
Tauri包体积小、性能好、Rust 安全生态较新、学习成本高轻量级应用
Flutter DesktopUI 统一、性能不错Dart 学习成本、生态不成熟跨平台应用
原生开发性能最好、体验最佳开发成本高、维护困难大型专业软件

最终选择 Electron 的原因:

  1. 技术栈熟悉:我对 Vue3 + TypeScript 很熟,可以直接上手
  2. 生态完善:electron-vite 让项目搭建变得超级简单
  3. 跨平台:一套代码跑 Windows/macOS/Linux
  4. 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 返回文字,而是希望:

  1. 联网搜索:获取最新的真实新闻,而不是训练数据里的旧信息
  2. 结构化输出:返回 JSON 格式,方便前端解析
  3. 自动生成 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 直接渲染就行,超级方便!

新闻列表.png 图:输入主题后,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 手机样式.png 图:点击新闻后弹出 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 次 paddingborder-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,包括:

  1. 明确的角色设定:"你是一个专业的新闻内容生成助手"
  2. 详细的设计规范:颜色、字体、间距、阴影都给出具体数值
  3. 完整的 HTML 示例:直接给一个样板代码让 AI 参考
  4. 强调约束条件:移动端优先、内联样式、不要占满屏幕等

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)

技术收获

这次做下来,学到了不少东西:

  1. Electron 开发流程:主进程/渲染进程分工、IPC 通信、窗口管理,一开始搞不清谁干啥,后来就顺手了
  2. AI Prompt 工程:调Prompt调到想砸键盘,改了十几次才稳定,终于知道怎么让AI乖乖返回JSON了
  3. Vue3 + TypeScript:组合式API用起来挺爽的,类型安全确实能少踩坑
  4. 桌面端 UI 设计:无边框窗口、自定义标题栏、拖动区域,这些细节调起来真费劲

未来可以做的优化

🔲 缓存机制:避免重复搜索相同主题 🔲 导出功能:支持导出为 PDF 或图片 🔲 历史记录:保存生成过的新闻 🔲 自定义样式:让用户选择不同的 HTML 模板 🔲 多语言支持:英文新闻生成


写在最后

这个项目从构思到完成,大概花了一个周末的时间。

说实话,最耗时的不是写代码,而是调试 Prompt——让 AI 稳定地返回符合要求的 JSON 和 HTML,真的需要反复尝试。调到想砸键盘,改了十几次才稳定。

但看到最终效果的那一刻,值了!

输入"量子计算最新进展",点击生成,过一段时间后,一个精美的 iPhone 界面弹出,里面是最新的量子计算新闻,排版精美、信息完整。

e__Project_electron_news-html_docs_无标题-2025-12-28-1420.png 图:完整的新闻生成和展示流程

虽然过程有点折腾,但做出来之后确实挺爽的。


源码与使用

项目已开源,欢迎 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

感谢阅读!如果觉得有用,欢迎点赞、收藏、转发~

有任何问题,欢迎在评论区留言交流!