refactor: 将置顶项目功能提取为独立组件
This commit is contained in:
@@ -7,55 +7,7 @@
|
|||||||
获取 AI 推荐
|
获取 AI 推荐
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-row :gutter="20">
|
<PinnedProject />
|
||||||
<el-col :span="24">
|
|
||||||
<el-card class="box-card" shadow="hover">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span>Pined</span>
|
|
||||||
<el-tag type="danger">{{ pinnedBids.length }} 个置顶</el-tag>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div v-if="pinnedLoading" style="text-align: center; padding: 40px;">
|
|
||||||
<el-icon class="is-loading" :size="30"><Loading /></el-icon>
|
|
||||||
<p style="margin-top: 10px; color: #909399;">加载中...</p>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="pinnedBids.length === 0" style="text-align: center; padding: 40px; color: #909399;">
|
|
||||||
<el-icon :size="40"><InfoFilled /></el-icon>
|
|
||||||
<p style="margin-top: 10px;">暂无置顶项目</p>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<el-table :data="pinnedBids" style="width: 100%" size="small">
|
|
||||||
<el-table-column label="Pin" width="60" align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-icon
|
|
||||||
:style="{
|
|
||||||
color: '#f56c6c',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: '18px'
|
|
||||||
}"
|
|
||||||
@click="togglePinnedPin(scope.row)"
|
|
||||||
>
|
|
||||||
<Paperclip />
|
|
||||||
</el-icon>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="title" label="项目名称">
|
|
||||||
<template #default="scope">
|
|
||||||
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="source" label="来源" width="200" />
|
|
||||||
<el-table-column prop="publishDate" label="发布日期" width="180">
|
|
||||||
<template #default="scope">
|
|
||||||
{{ formatDate(scope.row.publishDate) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-card class="box-card" shadow="hover">
|
<el-card class="box-card" shadow="hover">
|
||||||
@@ -178,6 +130,7 @@ import { ref, watch } from 'vue'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { MagicStick, Loading, InfoFilled, List, ArrowDown, Paperclip } from '@element-plus/icons-vue'
|
import { MagicStick, Loading, InfoFilled, List, ArrowDown, Paperclip } from '@element-plus/icons-vue'
|
||||||
|
import PinnedProject from './PinnedProject.vue'
|
||||||
|
|
||||||
|
|
||||||
interface AIRecommendation {
|
interface AIRecommendation {
|
||||||
@@ -201,8 +154,6 @@ const dateRange = ref<[string, string] | null>(null)
|
|||||||
const showAllBids = ref(false)
|
const showAllBids = ref(false)
|
||||||
const bidsLoading = ref(false)
|
const bidsLoading = ref(false)
|
||||||
const bidsByDateRange = ref<any[]>([])
|
const bidsByDateRange = ref<any[]>([])
|
||||||
const pinnedBids = ref<any[]>([])
|
|
||||||
const pinnedLoading = ref(false)
|
|
||||||
|
|
||||||
// 从 localStorage 加载保存的日期范围
|
// 从 localStorage 加载保存的日期范围
|
||||||
const loadSavedDateRange = () => {
|
const loadSavedDateRange = () => {
|
||||||
@@ -241,23 +192,9 @@ const loadLatestRecommendations = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载置顶项目
|
// 初始化时加载保存的日期范围和最新的 AI 推荐
|
||||||
const loadPinnedBids = async () => {
|
|
||||||
pinnedLoading.value = true
|
|
||||||
try {
|
|
||||||
const response = await axios.get('/api/bids/pinned')
|
|
||||||
pinnedBids.value = response.data
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load pinned bids:', error)
|
|
||||||
} finally {
|
|
||||||
pinnedLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化时加载保存的日期范围、最新的 AI 推荐和置顶项目
|
|
||||||
loadSavedDateRange()
|
loadSavedDateRange()
|
||||||
loadLatestRecommendations()
|
loadLatestRecommendations()
|
||||||
loadPinnedBids()
|
|
||||||
|
|
||||||
// 设置日期范围为最近3天
|
// 设置日期范围为最近3天
|
||||||
const setLast3Days = () => {
|
const setLast3Days = () => {
|
||||||
@@ -397,46 +334,11 @@ const togglePin = async (item: AIRecommendation) => {
|
|||||||
const newPinStatus = !item.pin
|
const newPinStatus = !item.pin
|
||||||
await axios.patch(`/api/bids/${encodeURIComponent(item.title)}/pin`, { pin: newPinStatus })
|
await axios.patch(`/api/bids/${encodeURIComponent(item.title)}/pin`, { pin: newPinStatus })
|
||||||
item.pin = newPinStatus
|
item.pin = newPinStatus
|
||||||
if (newPinStatus) {
|
|
||||||
// 添加到置顶列表
|
|
||||||
pinnedBids.value.unshift({
|
|
||||||
title: item.title,
|
|
||||||
url: item.url,
|
|
||||||
source: item.source,
|
|
||||||
publishDate: item.publishDate,
|
|
||||||
pin: true
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 从置顶列表移除
|
|
||||||
const index = pinnedBids.value.findIndex(b => b.title === item.title)
|
|
||||||
if (index !== -1) {
|
|
||||||
pinnedBids.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ElMessage.success(newPinStatus ? '已置顶' : '已取消置顶')
|
ElMessage.success(newPinStatus ? '已置顶' : '已取消置顶')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('操作失败')
|
ElMessage.error('操作失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换置顶列表的 Pin 状态
|
|
||||||
const togglePinnedPin = async (item: any) => {
|
|
||||||
try {
|
|
||||||
await axios.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)
|
|
||||||
}
|
|
||||||
// 同时更新 AI 推荐列表中的状态
|
|
||||||
const aiItem = aiRecommendations.value.find(r => r.title === item.title)
|
|
||||||
if (aiItem) {
|
|
||||||
aiItem.pin = false
|
|
||||||
}
|
|
||||||
ElMessage.success('已取消置顶')
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('操作失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
立刻抓取
|
立刻抓取
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
<PinnedProject />
|
||||||
<el-divider />
|
<el-divider />
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
<h3 style="margin: 0;">Today's Bids</h3>
|
<h3 style="margin: 0;">Today's Bids</h3>
|
||||||
@@ -65,6 +66,7 @@ import { ref, computed, watch } from 'vue'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Refresh, ArrowDown } from '@element-plus/icons-vue'
|
import { Refresh, ArrowDown } from '@element-plus/icons-vue'
|
||||||
|
import PinnedProject from './PinnedProject.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
todayBids: any[]
|
todayBids: any[]
|
||||||
|
|||||||
121
frontend/src/components/PinnedProject.vue
Normal file
121
frontend/src/components/PinnedProject.vue
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<template>
|
||||||
|
<el-card class="box-card" shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>Pined</span>
|
||||||
|
<el-tag type="danger">{{ pinnedBids.length }} 个置顶</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-if="pinnedLoading" style="text-align: center; padding: 40px;">
|
||||||
|
<el-icon class="is-loading" :size="30"><Loading /></el-icon>
|
||||||
|
<p style="margin-top: 10px; color: #909399;">加载中...</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="pinnedBids.length === 0" style="text-align: center; padding: 40px; color: #909399;">
|
||||||
|
<el-icon :size="40"><InfoFilled /></el-icon>
|
||||||
|
<p style="margin-top: 10px;">暂无置顶项目</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<el-table :data="pinnedBids" style="width: 100%" size="small">
|
||||||
|
<el-table-column label="Pin" width="60" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-icon
|
||||||
|
:style="{
|
||||||
|
color: '#f56c6c',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '18px'
|
||||||
|
}"
|
||||||
|
@click="togglePin(scope.row)"
|
||||||
|
>
|
||||||
|
<Paperclip />
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="title" label="项目名称">
|
||||||
|
<template #default="scope">
|
||||||
|
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="source" label="来源" width="200" />
|
||||||
|
<el-table-column prop="publishDate" label="发布日期" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.publishDate) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Loading, InfoFilled, Paperclip } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const pinnedBids = ref<any[]>([])
|
||||||
|
const pinnedLoading = ref(false)
|
||||||
|
|
||||||
|
// 加载置顶项目
|
||||||
|
const loadPinnedBids = async () => {
|
||||||
|
pinnedLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/bids/pinned')
|
||||||
|
pinnedBids.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load pinned bids:', error)
|
||||||
|
} finally {
|
||||||
|
pinnedLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换置顶列表的 Pin 状态
|
||||||
|
const togglePin = async (item: any) => {
|
||||||
|
try {
|
||||||
|
await axios.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)
|
||||||
|
}
|
||||||
|
ElMessage.success('已取消置顶')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期,只显示年月日
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
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}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化时加载置顶项目
|
||||||
|
onMounted(() => {
|
||||||
|
loadPinnedBids()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-card {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user