From eca3f4f9fda1f1d8e0c78ef735eba2cec731b397 Mon Sep 17 00:00:00 2001 From: dmy Date: Thu, 15 Jan 2026 00:35:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(electron):=20=E6=B7=BB=E5=8A=A0Electron?= =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E5=BA=94=E7=94=A8=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增Electron主进程、预加载脚本和构建配置 - 修改前端配置以支持Electron打包 - 更新项目文档和依赖 - 重构API调用使用统一axios实例 --- .gitignore | 1 + README.md | 133 ++++++++++----- app/electron-builder.json | 39 +++++ app/main.js | 172 ++++++++++++++++++++ app/package.json | 13 ++ app/preload.js | 14 ++ frontend/README.md | 5 - frontend/src/App.vue | 16 +- frontend/src/components/Bids.vue | 4 +- frontend/src/components/CrawlInfo.vue | 6 +- frontend/src/components/Dashboard-AI.vue | 14 +- frontend/src/components/Dashboard.vue | 6 +- frontend/src/components/Keywords.vue | 6 +- frontend/src/components/PinnedProject.vue | 6 +- frontend/src/utils/api.ts | 34 ++++ frontend/vite.config.ts | 1 + package.json | 10 +- src/common/logger/winston.config.ts | 6 +- src/crawler/services/bid-crawler.service.ts | 14 +- src/database/database.module.ts | 6 +- tsconfig.json | 5 +- widget/looker/README.md | 19 --- 22 files changed, 421 insertions(+), 109 deletions(-) create mode 100644 app/electron-builder.json create mode 100644 app/main.js create mode 100644 app/package.json create mode 100644 app/preload.js delete mode 100644 frontend/README.md create mode 100644 frontend/src/utils/api.ts delete mode 100644 widget/looker/README.md diff --git a/.gitignore b/.gitignore index 2348b87..f18448b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ build *-lock.json *.woff2 widget/looker/frontend/src/assets/fonts/OFL.txt +dist-electron \ No newline at end of file diff --git a/README.md b/README.md index 67734a1..2793d28 100644 --- a/README.md +++ b/README.md @@ -15,23 +15,33 @@ ### 前端 - **框架**: Vue.js 3 - **构建工具**: Vite -- **UI**: Tailwind CSS -- **状态管理**: Pinia +- **UI**: Element Plus +- **图标**: @element-plus/icons-vue +- **HTTP 客户端**: Axios +- **Tailwind CSS**: 用于样式辅助 ## 项目结构 ``` src/ ├── ai/ # AI 模块 -│ ├── ai.controller.ts -│ ├── ai.service.ts +│ ├── entities/ +│ │ └── ai-recommendation.entity.ts │ ├── Prompt.ts -│ └── entities/ +│ ├── ai.controller.ts +│ ├── ai.module.ts +│ └── ai.service.ts ├── bids/ # 投标业务模块 │ ├── controllers/ +│ │ └── bid.controller.ts │ ├── entities/ -│ └── services/ +│ │ └── bid-item.entity.ts +│ ├── services/ +│ │ └── bid.service.ts +│ └── bids.module.ts ├── crawler/ # 爬虫模块 +│ ├── entities/ +│ │ └── crawl-info-add.entity.ts │ ├── services/ │ │ ├── bid-crawler.service.ts │ │ ├── cdt_target.ts @@ -46,32 +56,63 @@ src/ │ │ ├── powerbeijing_target.ts │ │ ├── sdicc_target.ts │ │ └── szecp_target.ts -│ └── entities/ +│ ├── crawler.controller.ts +│ └── crawler.module.ts ├── database/ # 数据库模块 +│ └── database.module.ts ├── keywords/ # 关键词管理模块 +│ ├── keyword.entity.ts +│ ├── keywords.controller.ts +│ ├── keywords.module.ts +│ └── keywords.service.ts ├── schedule/ # 定时任务 -│ └── tasks/ -│ └── bid-crawl.task.ts +│ ├── tasks/ +│ │ └── bid-crawl.task.ts +│ └── schedule.module.ts ├── scripts/ # 脚本工具 │ ├── ai-recommendations.ts │ ├── crawl.ts │ ├── deploy.ps1 │ ├── remove-duplicates.ts +│ ├── sync.ts │ └── update-source.ts -└── common/ # 公共模块 - └── logger/ +├── common/ # 公共模块 +│ └── logger/ +│ ├── logger.module.ts +│ ├── logger.service.ts +│ └── winston.config.ts +├── app.controller.ts +├── app.module.ts +├── app.service.ts +└── main.ts frontend/ -└── src/ - ├── components/ - │ ├── Dashboard.vue - │ ├── Dashboard-AI.vue - │ ├── PinnedProject.vue - │ ├── Bids.vue - │ ├── Keywords.vue - │ └── CrawlInfo.vue - ├── App.vue - └── main.ts +├── src/ +│ ├── assets/ +│ │ └── vue.svg +│ ├── components/ +│ │ ├── Dashboard.vue +│ │ ├── Dashboard-AI.vue +│ │ ├── PinnedProject.vue +│ │ ├── Bids.vue +│ │ ├── Keywords.vue +│ │ └── CrawlInfo.vue +│ ├── utils/ +│ │ └── api.ts +│ ├── App.vue +│ ├── main.ts +│ └── style.css +├── .env +├── .env.example +├── .gitignore +├── README.md +├── index.html +├── package.json +├── postcss.config.js +├── tsconfig.app.json +├── tsconfig.json +├── tsconfig.node.json +└── vite.config.ts ``` ## 快速开始 @@ -141,26 +182,28 @@ npm run start:prod ### 智能爬虫模块 - **多源爬取**: 支持 12 个主流招标网站 - - 中国大唐集团电子商务平台 (CDT) - - 中国华能集团有限公司电子商务平台 (CHNG) - - 中国南方电网电子商务平台 (CSG) - - 中国海洋石油集团有限公司 (CNOOC) - 中国华电集团有限公司电子商务平台 (CHDTP) + - 中国华能集团有限公司电子商务平台 (CHNG) + - 深圳交易集团有限公司 (SZECP) + - 中国大唐集团电子商务平台 (CDT) + - 中国电力招标网 (EPS) - 国家能源投资集团有限责任公司 (CNNCECP) - - 中国核工业集团有限公司 (CNNC) - - 中国电力建设集团有限公司 (POWERCHINA) + - 中国石油天然气集团有限公司 (CGNPC) - 中国能源建设集团有限公司 (CEIC) - - 中国石油天然气集团有限公司 (CNPC) - - 国家电网有限公司 (SGCC) + - 中国电力建设集团有限公司 (ESPIC) - 北京电力交易中心 (POWERBEIJING) + - 山东能源集团有限公司 (SDICC) + - 中国海洋石油集团有限公司 (CNOOC) - **智能防封策略**: - - 随机请求间隔 (3-8 秒) - - 轮换 User-Agent + - 随机请求间隔 (1-3 秒) + - 固定 User-Agent - 异常检测与自动重试机制 - 代理支持 -- **定时任务**: 每 30 分钟自动执行爬取 +- **定时任务**: + - 爬虫任务:已暂停(默认每天午夜执行) + - 数据清理:每天午夜自动执行 ### 数据处理与存储 @@ -168,10 +211,10 @@ npm run start:prod - 投标项目标题 - 详细页面 URL - 发布时间 - - 招标单位 - - 截止日期 - - 关键词匹配 - - 优先级评分 + - 来源网站 + - 置顶标记 + - 创建时间 + - 更新时间 - **增量存储**: - 通过 URL 哈希值判断是否为新数据 @@ -215,8 +258,12 @@ npm run start:prod ### 投标信息 - `GET /api/bids` - 获取投标列表(支持分页、筛选) -- `GET /api/bids/high-priority` - 获取高优先级投标 -- `GET /api/bids/today` - 获取今日投标 +- `GET /api/bids/recent` - 获取最近投标 +- `GET /api/bids/pinned` - 获取置顶投标 +- `GET /api/bids/sources` - 获取来源列表 +- `GET /api/bids/by-date-range` - 按日期范围获取投标 +- `GET /api/bids/crawl-info-stats` - 获取爬取信息统计 +- `PATCH /api/bids/:title/pin` - 更新置顶状态 ### 关键词管理 - `GET /api/keywords` - 获取所有关键词 @@ -224,12 +271,14 @@ npm run start:prod - `DELETE /api/keywords/:id` - 删除关键词 ### AI 服务 -- `GET /api/ai/recommendations` - 获取 AI 推荐投标 -- `POST /api/ai/analyze` - 分析投标信息 +- `POST /api/ai/recommendations` - 获取 AI 推荐 +- `POST /api/ai/save-recommendations` - 保存 AI 推荐 +- `GET /api/ai/latest-recommendations` - 获取最新 AI 推荐 ### 爬虫管理 -- `GET /api/crawler/info` - 获取爬取信息 -- `POST /api/crawler/trigger` - 手动触发爬取 +- `GET /api/crawler/status` - 获取爬虫状态 +- `POST /api/crawler/run` - 运行爬虫 +- `POST /api/crawler/crawl/:sourceName` - 爬取单个来源 ## 前端路由 diff --git a/app/electron-builder.json b/app/electron-builder.json new file mode 100644 index 0000000..939cd9a --- /dev/null +++ b/app/electron-builder.json @@ -0,0 +1,39 @@ +{ + "productName": "投标应用", + "appId": "com.bidding.app", + "directories": { + "output": "dist-electron", + "app": "./app" + }, + "files": [ + "dist/**/*", + "frontend/**/*", + ".env", + "node_modules/**/*", + "package.json", + "app/**/*" + ], + "win": { + "target": "nsis", + "icon": "frontend/public/favicon.ico", + "requestedExecutionLevel": "asInvoker" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true, + "shortcutName": "投标应用" + }, + "extraResources": [ + { + "from": ".env", + "to": ".env", + "filter": ["**/*"] + } + ], + "publish": { + "provider": "generic", + "url": "http://localhost:3000/" + } +} \ No newline at end of file diff --git a/app/main.js b/app/main.js new file mode 100644 index 0000000..73e9344 --- /dev/null +++ b/app/main.js @@ -0,0 +1,172 @@ +const { app, BrowserWindow, ipcMain } = require('electron'); +const path = require('path'); +const { spawn } = require('child_process'); +const dotenv = require('dotenv'); +const fs = require('fs'); + +// 加载环境变量 +const envPath = path.join(__dirname, '..', '.env'); +if (fs.existsSync(envPath)) { + dotenv.config({ path: envPath }); +} + +let mainWindow; +let backendProcess; + +/** + * 创建Electron主窗口 + */ +function createWindow() { + mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + nodeIntegration: false, + contextIsolation: true, + }, + autoHideMenuBar: false, + title: '投标应用', + }); + + // 加载前端页面 + const indexPath = path.join(__dirname, '..', 'frontend', 'dist', 'index.html'); + mainWindow.loadFile(indexPath); + + // 开发环境下打开开发者工具 + if (process.env.NODE_ENV === 'development') { + mainWindow.webContents.openDevTools(); + } + + mainWindow.on('closed', () => { + mainWindow = null; + }); +} + +/** + * 等待后端服务启动 + */ +function waitForBackend(port = 3000, maxRetries = 30, interval = 1000) { + return new Promise((resolve, reject) => { + let retries = 0; + + const checkBackend = () => { + const net = require('net'); + const client = new net.Socket(); + + client.once('connect', () => { + client.destroy(); + console.log('后端服务已启动'); + resolve(); + }); + + client.once('error', () => { + client.destroy(); + retry(); + }); + + client.once('timeout', () => { + client.destroy(); + retry(); + }); + + client.connect(port, 'localhost'); + client.setTimeout(1000); + + function retry() { + retries++; + if (retries >= maxRetries) { + reject(new Error('后端服务启动超时')); + } else { + console.log(`等待后端服务启动... (${retries}/${maxRetries})`); + setTimeout(checkBackend, interval); + } + } + }; + + checkBackend(); + }); +} + +/** + * 启动后端服务 + */ +async function startBackend() { + const backendPath = path.join(__dirname, '..', 'dist', 'main.js'); + + // 检查后端构建文件是否存在 + if (!fs.existsSync(backendPath)) { + console.error('后端服务构建文件不存在,请先执行 npm run build'); + return; + } + + // 启动后端服务 + backendProcess = spawn('node', [backendPath], { + env: { + ...process.env, + NODE_ENV: process.env.NODE_ENV || 'production', + }, + stdio: 'inherit', + }); + + backendProcess.on('error', (error) => { + console.error('后端服务启动失败:', error); + }); + + backendProcess.on('exit', (code) => { + console.log(`后端服务退出,退出码: ${code}`); + backendProcess = null; + }); + + // 等待后端服务启动完成 + try { + await waitForBackend(); + } catch (error) { + console.error('等待后端服务启动失败:', error.message); + } +} + +/** + * 停止后端服务 + */ +function stopBackend() { + if (backendProcess) { + console.log('正在停止后端服务...'); + backendProcess.kill(); + backendProcess = null; + } +} + +// 应用就绪时启动后端服务,然后创建窗口 +app.on('ready', async () => { + await startBackend(); + createWindow(); +}); + +// 所有窗口关闭时退出应用 +app.on('window-all-closed', () => { + stopBackend(); + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +// MacOS上点击dock图标时重新创建窗口 +app.on('activate', async () => { + if (BrowserWindow.getAllWindows().length === 0) { + if (!backendProcess) { + await startBackend(); + } + createWindow(); + } +}); + +// 应用退出前停止后端服务 +app.on('before-quit', () => { + stopBackend(); +}); + +// 处理来自渲染进程的IPC消息 +ipcMain.handle('get-env', (event, key) => { + return process.env[key]; +}); diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..6f3f5dc --- /dev/null +++ b/app/package.json @@ -0,0 +1,13 @@ +{ + "name": "bidding-app", + "version": "0.0.1", + "description": "投标应用Electron版本", + "main": "main.js", + "scripts": { + "start": "electron .", + "build": "electron-builder" + }, + "keywords": ["electron", "bidding", "app"], + "author": "", + "license": "UNLICENSED" +} \ No newline at end of file diff --git a/app/preload.js b/app/preload.js new file mode 100644 index 0000000..04b19d0 --- /dev/null +++ b/app/preload.js @@ -0,0 +1,14 @@ +const { contextBridge, ipcRenderer } = require('electron'); + +/** + * 预加载脚本,用于在渲染进程和主进程之间通信 + * 提供安全的API给渲染进程访问主进程功能 + */ +contextBridge.exposeInMainWorld('electronAPI', { + /** + * 获取环境变量值 + * @param {string} key - 环境变量名称 + * @returns {Promise} - 环境变量值 + */ + getEnv: (key) => ipcRenderer.invoke('get-env', key), +}); diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 33895ab..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Vue 3 + TypeScript + Vite - -This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `