feat(electron): 添加Electron桌面应用支持

- 新增Electron主进程、预加载脚本和构建配置
- 修改前端配置以支持Electron打包
- 更新项目文档和依赖
- 重构API调用使用统一axios实例
This commit is contained in:
dmy
2026-01-15 00:35:19 +08:00
parent f736f30248
commit eca3f4f9fd
22 changed files with 421 additions and 109 deletions

View File

@@ -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 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

View File

@@ -64,7 +64,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import api from './utils/api'
import { ElMessage } from 'element-plus'
import { DataBoard, Document, Setting, MagicStick, Connection } from '@element-plus/icons-vue'
import Dashboard from './components/Dashboard.vue'
@@ -89,7 +89,7 @@ const handleSelect = (key: string) => {
const handleFetchBids = async (page: number, limit: number, source?: string) => {
loading.value = true
try {
const res = await axios.get('/api/bids', {
const res = await api.get('/api/bids', {
params: {
page,
limit,
@@ -109,16 +109,16 @@ const fetchData = async () => {
loading.value = true
try {
const [bidsRes, recentRes, kwRes, sourcesRes, statusRes] = await Promise.all([
axios.get('/api/bids', {
api.get('/api/bids', {
params: {
page: 1,
limit: 10
}
}),
axios.get('/api/bids/recent'),
axios.get('/api/keywords'),
axios.get('/api/bids/sources'),
axios.get('/api/crawler/status')
api.get('/api/bids/recent'),
api.get('/api/keywords'),
api.get('/api/bids/sources'),
api.get('/api/crawler/status')
])
bids.value = bidsRes.data.items
total.value = bidsRes.data.total
@@ -145,7 +145,7 @@ const updateBidsByDateRange = async (startDate: string, endDate?: string, keywor
params.keywords = keywords.join(',')
}
const response = await axios.get('/api/bids/by-date-range', { params })
const response = await api.get('/api/bids/by-date-range', { params })
todayBids.value = response.data
ElMessage.success(`更新成功,共 ${response.data.length} 条数据`)
} catch (error) {

View File

@@ -51,7 +51,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
import api from '../utils/api'
import { ElMessage } from 'element-plus'
import { Paperclip } from '@element-plus/icons-vue'
@@ -97,7 +97,7 @@ const handleSizeChange = (size: number) => {
const togglePin = async (item: any) => {
try {
const newPinStatus = !item.pin
await axios.patch(`/api/bids/${encodeURIComponent(item.title)}/pin`, { pin: newPinStatus })
await api.patch(`/api/bids/${encodeURIComponent(item.title)}/pin`, { pin: newPinStatus })
item.pin = newPinStatus
ElMessage.success(newPinStatus ? '已置顶' : '已取消置顶')
} catch (error) {

View File

@@ -75,7 +75,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import axios from 'axios'
import api from '../utils/api'
import { ElMessage } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
@@ -118,7 +118,7 @@ const formatDate = (dateStr: string | null) => {
const fetchCrawlStats = async () => {
loading.value = true
try {
const res = await axios.get('/api/bids/crawl-info-stats')
const res = await api.get('/api/bids/crawl-info-stats')
crawlStats.value = res.data
} catch (error) {
console.error('Failed to fetch crawl stats:', error)
@@ -132,7 +132,7 @@ const crawlSingleSource = async (sourceName: string) => {
crawlingSources.value.add(sourceName)
try {
ElMessage.info(`正在更新 ${sourceName}...`)
const res = await axios.post(`/api/crawler/crawl/${encodeURIComponent(sourceName)}`)
const res = await api.post(`/api/crawler/crawl/${encodeURIComponent(sourceName)}`)
if (res.data.success) {
ElMessage.success(`${sourceName} 更新成功,获取 ${res.data.count} 条数据`)

View File

@@ -127,7 +127,7 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import axios from 'axios'
import api from '../utils/api'
import { ElMessage } from 'element-plus'
import { MagicStick, Loading, InfoFilled, List, Paperclip } from '@element-plus/icons-vue'
import PinnedProject from './PinnedProject.vue'
@@ -175,11 +175,11 @@ watch(dateRange, (newDateRange) => {
// 从数据库加载最新的 AI 推荐
const loadLatestRecommendations = async () => {
try {
const response = await axios.get('/api/ai/latest-recommendations')
const response = await api.get('/api/ai/latest-recommendations')
const recommendations = response.data
// 获取所有置顶的项目
const pinnedResponse = await axios.get('/api/bids/pinned')
const pinnedResponse = await api.get('/api/bids/pinned')
const pinnedTitles = new Set(pinnedResponse.data.map((b: any) => b.title))
// 更新每个推荐项目的 pin 状态
@@ -240,7 +240,7 @@ const fetchAIRecommendations = async () => {
}))
// 调用后端 API
const response = await axios.post('/api/ai/recommendations', {
const response = await api.post('/api/ai/recommendations', {
bids: bidsData
})
@@ -267,7 +267,7 @@ const fetchAIRecommendations = async () => {
aiRecommendations.value = recommendations
// 保存推荐结果到数据库
await axios.post('/api/ai/save-recommendations', {
await api.post('/api/ai/save-recommendations', {
recommendations
})
@@ -301,7 +301,7 @@ const fetchBidsByDateRange = async () => {
params.endDate = endDate
}
const response = await axios.get('/api/bids/by-date-range', { params })
const response = await api.get('/api/bids/by-date-range', { params })
bidsByDateRange.value = response.data
ElMessage.success(`获取成功,共 ${response.data.length} 个工程`)
} catch (error: any) {
@@ -344,7 +344,7 @@ const handlePinChanged = async (title: string) => {
const togglePin = async (item: AIRecommendation) => {
try {
const newPinStatus = !item.pin
await axios.patch(`/api/bids/${encodeURIComponent(item.title)}/pin`, { pin: newPinStatus })
await api.patch(`/api/bids/${encodeURIComponent(item.title)}/pin`, { pin: newPinStatus })
item.pin = newPinStatus
ElMessage.success(newPinStatus ? '已置顶' : '已取消置顶')
// 刷新 PinnedProject 组件的数据

View File

@@ -77,7 +77,7 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import axios from 'axios'
import api from '../utils/api'
import { ElMessage } from 'element-plus'
import { Refresh, Paperclip } from '@element-plus/icons-vue'
import PinnedProject from './PinnedProject.vue'
@@ -275,7 +275,7 @@ const handleCrawl = async () => {
}
crawling.value = true
try {
await axios.post('/api/crawler/run')
await api.post('/api/crawler/run')
ElMessage.success('Crawl completed successfully')
emit('refresh') // Refresh data after crawl
} catch (error) {
@@ -301,7 +301,7 @@ const handlePinChanged = async (title: string) => {
const togglePin = async (item: any) => {
try {
const newPinStatus = !item.pin
await axios.patch(`/api/bids/${encodeURIComponent(item.title)}/pin`, { pin: newPinStatus })
await api.patch(`/api/bids/${encodeURIComponent(item.title)}/pin`, { pin: newPinStatus })
item.pin = newPinStatus
ElMessage.success(newPinStatus ? '已置顶' : '已取消置顶')
// 刷新 PinnedProject 组件的数据

View File

@@ -40,7 +40,7 @@
<script setup lang="ts">
import { ref, reactive } from 'vue'
import axios from 'axios'
import api from '../utils/api'
import { ElMessage } from 'element-plus'
interface Props {
@@ -76,7 +76,7 @@ const handleAddKeyword = async () => {
return
}
try {
await axios.post('/api/keywords', form)
await api.post('/api/keywords', form)
ElMessage.success('Keyword added')
dialogVisible.value = false
form.word = ''
@@ -89,7 +89,7 @@ const handleAddKeyword = async () => {
const handleDeleteKeyword = async (id: string) => {
try {
await axios.delete(`/api/keywords/${id}`)
await api.delete(`/api/keywords/${id}`)
ElMessage.success('Keyword deleted')
emit('refresh')
} catch (error) {

View File

@@ -48,7 +48,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import api from '../utils/api'
import { ElMessage } from 'element-plus'
import { Loading, InfoFilled, Paperclip } from '@element-plus/icons-vue'
@@ -63,7 +63,7 @@ const pinnedLoading = ref(false)
const loadPinnedBids = async () => {
pinnedLoading.value = true
try {
const response = await axios.get('/api/bids/pinned')
const response = await api.get('/api/bids/pinned')
pinnedBids.value = response.data
} catch (error) {
console.error('Failed to load pinned bids:', error)
@@ -75,7 +75,7 @@ const loadPinnedBids = async () => {
// 切换置顶列表的 Pin 状态
const togglePin = async (item: any) => {
try {
await axios.patch(`/api/bids/${encodeURIComponent(item.title)}/pin`, { pin: false })
await api.patch(`/api/bids/${encodeURIComponent(item.title)}/pin`, { pin: false })
const index = pinnedBids.value.findIndex(b => b.title === item.title)
if (index !== -1) {
pinnedBids.value.splice(index, 1)

34
frontend/src/utils/api.ts Normal file
View File

@@ -0,0 +1,34 @@
import axios from 'axios'
/**
* API配置
* 配置axios实例设置baseURL和请求拦截器
*/
const api = axios.create({
baseURL: 'http://localhost:3000', // 设置后端服务地址
timeout: 10000, // 请求超时时间
})
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 可以在这里添加认证信息等
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
(response) => {
return response
},
(error) => {
console.error('API请求错误:', error)
return Promise.reject(error)
}
)
export default api

View File

@@ -3,6 +3,7 @@ import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
base: './',
plugins: [vue()],
server: {
proxy: {