Files
bidding_watcher/frontend/src/components/Dashboard.vue
dmy eca3f4f9fd feat(electron): 添加Electron桌面应用支持
- 新增Electron主进程、预加载脚本和构建配置
- 修改前端配置以支持Electron打包
- 更新项目文档和依赖
- 重构API调用使用统一axios实例
2026-01-15 00:35:19 +08:00

342 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0;">Dashboard</h2>
<el-button type="primary" :loading="crawling" :disabled="isCrawling" @click="handleCrawl">
<el-icon style="margin-right: 5px"><Refresh /></el-icon>
立刻抓取
</el-button>
</div>
<PinnedProject ref="pinnedProjectRef" @pin-changed="handlePinChanged" />
<el-divider />
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h3 style="margin: 0;">Today's Bids</h3>
<div style="display: flex; gap: 10px;">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="To"
start-placeholder="Start Date"
end-placeholder="End Date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
style="width: 240px;"
/>
<el-button type="primary" @click="setLast3Days">3天</el-button>
<el-button type="primary" @click="setLast7Days">7天</el-button>
<el-button type="success" :loading="updating" @click="updateBidsByDateRange">
<el-icon style="margin-right: 5px"><Refresh /></el-icon>
更新
</el-button>
<el-select
v-model="selectedKeywords"
multiple
collapse-tags
collapse-tags-tooltip
placeholder="Filter by Keywords"
clearable
style="width: 300px;"
>
<el-option
v-for="keyword in keywords"
:key="keyword.id"
:label="keyword.word"
:value="keyword.word"
/>
</el-select>
</div>
</div>
<el-table :data="filteredTodayBids" v-loading="loading" style="width: 100%">
<el-table-column label="Pin" width="60" align="center">
<template #default="scope">
<el-icon
:style="{
color: scope.row.pin ? '#f56c6c' : '#909399',
cursor: 'pointer',
fontSize: '18px'
}"
@click="togglePin(scope.row)"
>
<Paperclip />
</el-icon>
</template>
</el-table-column>
<el-table-column prop="title" label="Title">
<template #default="scope">
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
</template>
</el-table-column>
<el-table-column prop="source" label="Source" width="220" />
<el-table-column prop="publishDate" label="Date" width="150">
<template #default="scope">{{ formatDate(scope.row.publishDate) }}</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import api from '../utils/api'
import { ElMessage } from 'element-plus'
import { Refresh, Paperclip } from '@element-plus/icons-vue'
import PinnedProject from './PinnedProject.vue'
interface Props {
todayBids: any[]
keywords: any[]
loading: boolean
isCrawling: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
crawl: []
refresh: []
updateBids: [startDate: string, endDate?: string, keywords?: string[]]
}>()
const selectedKeywords = ref<string[]>([])
const dateRange = ref<[string, string] | null>(null)
const crawling = ref(false)
const updating = ref(false)
const isInitialized = ref(false)
const isManualClick = ref(false)
// 从 localStorage 加载保存的日期范围
const loadSavedDateRange = () => {
const saved = localStorage.getItem('dashboard_dateRange')
if (saved) {
try {
dateRange.value = JSON.parse(saved)
} catch (e) {
console.error('Failed to parse saved date range:', e)
}
}
}
// 监听日期范围变化并保存到 localStorage
watch(dateRange, (newDateRange) => {
localStorage.setItem('dashboard_dateRange', JSON.stringify(newDateRange))
}, { deep: true })
// 从 localStorage 加载保存的关键字
const loadSavedKeywords = () => {
const saved = localStorage.getItem('selectedKeywords')
if (saved) {
try {
selectedKeywords.value = JSON.parse(saved)
} catch (e) {
console.error('Failed to parse saved keywords:', e)
}
}
}
// 监听关键字变化并保存到 localStorage
watch(selectedKeywords, (newKeywords) => {
localStorage.setItem('selectedKeywords', JSON.stringify(newKeywords))
}, { deep: true })
// 监听日期范围变化并显示提示
watch(dateRange, () => {
// 初始化时不显示提示
if (!isInitialized.value) {
isInitialized.value = true
return
}
// 手动点击时不显示提示(避免和按钮点击重复)
if (isManualClick.value) {
isManualClick.value = false
return
}
const totalBids = props.todayBids.length
const filteredCount = filteredTodayBids.value.length
if (totalBids > 0 && filteredCount < totalBids) {
ElMessage.info(`筛选结果:共 ${filteredCount} 条数据(总共 ${totalBids} 条)`)
}
})
const formatDate = (dateString: string) => {
if (!dateString) return '-'
return new Date(dateString).toLocaleDateString()
}
// 过滤 Today's Bids只显示包含所选关键字的项目并且在日期范围内
const filteredTodayBids = computed(() => {
let result = props.todayBids
// 按关键字筛选
if (selectedKeywords.value.length > 0) {
result = result.filter(bid => {
return selectedKeywords.value.some(keyword =>
bid.title.toLowerCase().includes(keyword.toLowerCase())
)
})
}
// 按日期范围筛选(只限制开始时间,不限制结束时间)
if (dateRange.value && dateRange.value.length === 2) {
const [startDate] = dateRange.value
result = result.filter(bid => {
if (!bid.publishDate) return false
const bidDate = new Date(bid.publishDate)
const start = new Date(startDate)
// 设置时间为当天的开始
start.setHours(0, 0, 0, 0)
return bidDate >= start
})
}
return result
})
// 设置日期范围为最近3天
const setLast3Days = async () => {
isManualClick.value = true
const endDate = new Date()
const startDate = new Date()
startDate.setDate(startDate.getDate() - 2) // 最近3天包括今天
const formatDateForPicker = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
dateRange.value = [formatDateForPicker(startDate), formatDateForPicker(endDate)]
console.log('setLast3Days called, todayBids:', props.todayBids.length, 'dateRange:', dateRange.value)
// 调用更新函数
await updateBidsByDateRange()
}
// 设置日期范围为最近7天
const setLast7Days = async () => {
isManualClick.value = true
const endDate = new Date()
const startDate = new Date()
startDate.setDate(startDate.getDate() - 6) // 最近7天包括今天
const formatDateForPicker = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
dateRange.value = [formatDateForPicker(startDate), formatDateForPicker(endDate)]
console.log('setLast7Days called, todayBids:', props.todayBids.length, 'dateRange:', dateRange.value)
// 调用更新函数
await updateBidsByDateRange()
}
// 根据日期范围更新投标信息
const updateBidsByDateRange = async () => {
if (!dateRange.value || dateRange.value.length !== 2) {
ElMessage.warning('请先选择日期范围')
return
}
updating.value = true
try {
const [startDate, endDate] = dateRange.value
// 检查 endDate 是否是今天
const today = new Date()
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
// 如果 endDate 是今天,则不传递 endDate 参数(不限制截止时间)
if (endDate === todayStr) {
emit('updateBids', startDate, undefined, selectedKeywords.value)
} else {
emit('updateBids', startDate, endDate, selectedKeywords.value)
}
ElMessage.success('更新成功')
} catch (error) {
ElMessage.error('更新失败')
} finally {
updating.value = false
}
}
const handleCrawl = async () => {
if (props.isCrawling) {
ElMessage.warning('Crawl is already running')
return
}
crawling.value = true
try {
await api.post('/api/crawler/run')
ElMessage.success('Crawl completed successfully')
emit('refresh') // Refresh data after crawl
} catch (error) {
ElMessage.error('Failed to run crawl task')
} finally {
crawling.value = false
}
}
// PinnedProject 组件引用
const pinnedProjectRef = ref<any>(null)
// 处理 PinnedProject 组件的 pin 状态改变事件
const handlePinChanged = async (title: string) => {
// 更新 todayBids 中对应项目的 pin 状态
const bid = props.todayBids.find(b => b.title === title)
if (bid) {
bid.pin = false
}
}
// 切换 Today's Bids 的 Pin 状态
const togglePin = async (item: any) => {
try {
const newPinStatus = !item.pin
await api.patch(`/api/bids/${encodeURIComponent(item.title)}/pin`, { pin: newPinStatus })
item.pin = newPinStatus
ElMessage.success(newPinStatus ? '已置顶' : '已取消置顶')
// 刷新 PinnedProject 组件的数据
if (pinnedProjectRef.value) {
pinnedProjectRef.value.loadPinnedBids()
}
} catch (error) {
ElMessage.error('操作失败')
}
}
// 初始化时加载保存的关键字和日期范围
loadSavedKeywords()
loadSavedDateRange()
// 如果没有保存的日期范围则设置默认为最近3天
if (!dateRange.value) {
setLast3Days()
}
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
a {
text-decoration: none;
color: inherit;
}
a:hover {
color: #409eff;
}
</style>