feat: 添加移动端响应式布局和样式优化
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
<title>投标</title>
|
<title>投标</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,10 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container class="layout-container" style="height: 100vh">
|
<div class="layout-container" :class="{ 'is-mobile': isMobile }">
|
||||||
<el-aside width="200px" style="background-color: #545c64">
|
<!-- 移动端顶部导航栏 -->
|
||||||
<div class="logo">投标信息一览</div>
|
<el-header v-if="isMobile" class="mobile-header">
|
||||||
<el-menu active-text-color="#ffd04b" background-color="#545c64" class="el-menu-vertical-demo" default-active="1"
|
<div class="mobile-header-content">
|
||||||
text-color="#fff" @select="handleSelect">
|
<el-button type="primary" link @click="toggleSidebar">
|
||||||
|
<el-icon :size="24"><Fold /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<span class="mobile-title">投标信息一览</span>
|
||||||
|
<el-button type="primary" link @click="handleLogout" v-if="currentUser">
|
||||||
|
<el-icon :size="24"><SwitchButton /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
|
||||||
|
<!-- 移动端侧边栏遮罩层 -->
|
||||||
|
<div v-if="isMobile && sidebarVisible" class="sidebar-overlay" @click="toggleSidebar"></div>
|
||||||
|
|
||||||
|
<!-- 侧边栏 - 桌面端固定显示,移动端可滑动 -->
|
||||||
|
<el-aside :class="{ 'mobile-sidebar': isMobile, 'sidebar-visible': sidebarVisible }" width="200px">
|
||||||
|
<div class="logo">投标信息一览</div>
|
||||||
|
<el-menu
|
||||||
|
active-text-color="#ffd04b"
|
||||||
|
background-color="#545c64"
|
||||||
|
class="el-menu-vertical-demo"
|
||||||
|
:default-active="activeIndex"
|
||||||
|
text-color="#fff"
|
||||||
|
@select="handleSelect"
|
||||||
|
:collapse="isMobile"
|
||||||
|
>
|
||||||
<el-menu-item index="1">
|
<el-menu-item index="1">
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<MagicStick />
|
<MagicStick />
|
||||||
@@ -39,31 +62,44 @@
|
|||||||
</el-aside>
|
</el-aside>
|
||||||
|
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header style="text-align: right; font-size: 12px">
|
<!-- 桌面端顶部导航栏 -->
|
||||||
<span v-if="currentUser">{{ currentUser }}</span>
|
<el-header v-if="!isMobile" class="desktop-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<span v-if="currentUser" class="username">{{ currentUser }}</span>
|
||||||
<el-button v-if="currentUser" type="primary" link @click="handleLogout">退出登录</el-button>
|
<el-button v-if="currentUser" type="primary" link @click="handleLogout">退出登录</el-button>
|
||||||
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<el-main>
|
<el-main>
|
||||||
<DashboardAI v-if="activeIndex === '1'" :bids="bids" />
|
<DashboardAI v-if="activeIndex === '1'" :bids="bids" />
|
||||||
<Dashboard v-if="activeIndex === '2'" :today-bids="todayBids"
|
<Dashboard
|
||||||
:keywords="keywords" :loading="loading" :is-crawling="isCrawling" @refresh="fetchData"
|
v-if="activeIndex === '2'"
|
||||||
@update-bids="updateBidsByDateRange" />
|
:today-bids="todayBids"
|
||||||
|
:keywords="keywords"
|
||||||
|
:loading="loading"
|
||||||
|
:is-crawling="isCrawling"
|
||||||
|
@refresh="fetchData"
|
||||||
|
@update-bids="updateBidsByDateRange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Bids
|
||||||
|
v-if="activeIndex === '3'"
|
||||||
<Bids v-if="activeIndex === '3'" :bids="bids" :source-options="sourceOptions" :loading="loading" :total="total"
|
:bids="bids"
|
||||||
@fetch="handleFetchBids" />
|
:source-options="sourceOptions"
|
||||||
|
:loading="loading"
|
||||||
|
:total="total"
|
||||||
|
@fetch="handleFetchBids"
|
||||||
|
/>
|
||||||
|
|
||||||
<Keywords v-if="activeIndex === '4'" :keywords="keywords" :loading="loading" @refresh="fetchData" />
|
<Keywords v-if="activeIndex === '4'" :keywords="keywords" :loading="loading" @refresh="fetchData" />
|
||||||
|
|
||||||
<CrawlInfo v-if="activeIndex === '5'" />
|
<CrawlInfo v-if="activeIndex === '5'" />
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
</el-container>
|
</div>
|
||||||
|
|
||||||
<!-- 登录对话框 -->
|
<!-- 登录对话框 -->
|
||||||
<el-dialog v-model="loginDialogVisible" title="用户登录" width="400px" :close-on-click-modal="false" :show-close="false">
|
<el-dialog v-model="loginDialogVisible" title="用户登录" width="90%" :style="{ maxWidth: '400px' }" :close-on-click-modal="false" :show-close="false">
|
||||||
<el-form :model="loginForm" label-width="80px">
|
<el-form :model="loginForm" label-width="80px">
|
||||||
<el-form-item label="用户名">
|
<el-form-item label="用户名">
|
||||||
<el-input v-model="loginForm.username" placeholder="请输入用户名" @keyup.enter="handleLogin" />
|
<el-input v-model="loginForm.username" placeholder="请输入用户名" @keyup.enter="handleLogin" />
|
||||||
@@ -82,7 +118,7 @@
|
|||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import api, { setAuthCredentials, clearAuthCredentials, isAuthenticated } from './utils/api'
|
import api, { setAuthCredentials, clearAuthCredentials, isAuthenticated } from './utils/api'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { DataBoard, Document, Setting, MagicStick, Connection } from '@element-plus/icons-vue'
|
import { DataBoard, Document, Setting, MagicStick, Connection, Fold, SwitchButton } from '@element-plus/icons-vue'
|
||||||
import Dashboard from './components/Dashboard.vue'
|
import Dashboard from './components/Dashboard.vue'
|
||||||
import DashboardAI from './components/Dashboard-AI.vue'
|
import DashboardAI from './components/Dashboard-AI.vue'
|
||||||
import Bids from './components/Bids.vue'
|
import Bids from './components/Bids.vue'
|
||||||
@@ -98,6 +134,10 @@ const isCrawling = ref(false)
|
|||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const sourceOptions = ref<string[]>([])
|
const sourceOptions = ref<string[]>([])
|
||||||
|
|
||||||
|
// 移动端状态
|
||||||
|
const isMobile = ref(false)
|
||||||
|
const sidebarVisible = ref(false)
|
||||||
|
|
||||||
// 登录相关状态
|
// 登录相关状态
|
||||||
const loginDialogVisible = ref(false)
|
const loginDialogVisible = ref(false)
|
||||||
const loginLoading = ref(false)
|
const loginLoading = ref(false)
|
||||||
@@ -107,8 +147,24 @@ const loginForm = ref({
|
|||||||
})
|
})
|
||||||
const currentUser = ref<string | null>(null)
|
const currentUser = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 检测屏幕宽度
|
||||||
|
const checkScreenSize = () => {
|
||||||
|
isMobile.value = window.innerWidth < 768
|
||||||
|
if (!isMobile.value) {
|
||||||
|
sidebarVisible.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
sidebarVisible.value = !sidebarVisible.value
|
||||||
|
}
|
||||||
|
|
||||||
const handleSelect = (key: string) => {
|
const handleSelect = (key: string) => {
|
||||||
activeIndex.value = key
|
activeIndex.value = key
|
||||||
|
// 移动端选择后关闭侧边栏
|
||||||
|
if (isMobile.value) {
|
||||||
|
sidebarVisible.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFetchBids = async (page: number, limit: number, source?: string) => {
|
const handleFetchBids = async (page: number, limit: number, source?: string) => {
|
||||||
@@ -256,14 +312,29 @@ onMounted(() => {
|
|||||||
|
|
||||||
// 监听认证要求事件
|
// 监听认证要求事件
|
||||||
window.addEventListener('auth-required', handleAuthRequired)
|
window.addEventListener('auth-required', handleAuthRequired)
|
||||||
|
|
||||||
|
// 监听屏幕大小变化
|
||||||
|
checkScreenSize()
|
||||||
|
window.addEventListener('resize', checkScreenSize)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('auth-required', handleAuthRequired)
|
window.removeEventListener('auth-required', handleAuthRequired)
|
||||||
|
window.removeEventListener('resize', checkScreenSize)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.layout-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-container .el-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.layout-container .el-header {
|
.layout-container .el-header {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
@@ -273,6 +344,13 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.layout-container .el-aside {
|
.layout-container .el-aside {
|
||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-container .el-menu {
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
@@ -284,4 +362,86 @@ onUnmounted(() => {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
background-color: #434a50;
|
background-color: #434a50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 桌面端样式 */
|
||||||
|
.desktop-header .header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
margin-right: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端样式 */
|
||||||
|
.is-mobile .el-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-header {
|
||||||
|
background-color: #434a50;
|
||||||
|
color: white;
|
||||||
|
padding: 0;
|
||||||
|
height: 50px !important;
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 1000;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar.sidebar-visible {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-mobile .el-main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element Plus 菜单在移动端的样式调整 */
|
||||||
|
.is-mobile .el-menu {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-mobile .el-menu-item {
|
||||||
|
padding: 0 20px !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="bids-container">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
<div class="bids-header">
|
||||||
<h2 style="margin: 0;">All Bids</h2>
|
<h2 class="bids-title">All Bids</h2>
|
||||||
<el-select v-model="selectedSource" placeholder="Filter by Source" clearable style="width: 200px" @change="handleSourceChange">
|
<el-select v-model="selectedSource" placeholder="按来源筛选" clearable class="source-select" @change="handleSourceChange">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="source in sourceOptions"
|
v-for="source in sourceOptions"
|
||||||
:key="source"
|
:key="source"
|
||||||
@@ -11,14 +11,14 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="bids" v-loading="loading" style="width: 100%">
|
<el-table :data="bids" v-loading="loading" style="width: 100%" class="bids-table">
|
||||||
<el-table-column label="Pin" width="60" align="center">
|
<el-table-column label="Pin" width="50" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-icon
|
<el-icon
|
||||||
:style="{
|
:style="{
|
||||||
color: scope.row.pin ? '#f56c6c' : '#909399',
|
color: scope.row.pin ? '#f56c6c' : '#909399',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
fontSize: '18px'
|
fontSize: '16px'
|
||||||
}"
|
}"
|
||||||
@click="togglePin(scope.row)"
|
@click="togglePin(scope.row)"
|
||||||
>
|
>
|
||||||
@@ -31,8 +31,8 @@
|
|||||||
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
|
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="source" label="Source" width="200" />
|
<el-table-column prop="source" label="Source" width="100" />
|
||||||
<el-table-column prop="publishDate" label="Date" width="150">
|
<el-table-column prop="publishDate" label="Date" width="100">
|
||||||
<template #default="scope">{{ formatDate(scope.row.publishDate) }}</template>
|
<template #default="scope">{{ formatDate(scope.row.publishDate) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
layout="total, sizes, prev, pager, next, jumper"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
@current-change="handlePageChange"
|
@current-change="handlePageChange"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
style="margin-top: 20px; justify-content: flex-end;"
|
class="pagination"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -103,6 +103,41 @@ const togglePin = async (item: any) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.bids-container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bids-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bids-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-select {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bids-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -111,4 +146,32 @@ a {
|
|||||||
a:hover {
|
a:hover {
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端响应式样式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.bids-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bids-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-select {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table .cell {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="crawl-info">
|
<div class="crawl-info">
|
||||||
<el-card>
|
<el-card class="crawl-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>爬虫统计信息</span>
|
<span class="card-title">爬虫统计信息</span>
|
||||||
<el-button type="primary" size="small" @click="fetchCrawlStats" :loading="loading">
|
<el-button type="primary" size="small" @click="fetchCrawlStats" :loading="loading">
|
||||||
<el-icon><Refresh /></el-icon>
|
<el-icon><Refresh /></el-icon>
|
||||||
刷新
|
刷新
|
||||||
@@ -11,33 +11,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-table :data="crawlStats" stripe style="width: 100%" v-loading="loading">
|
<el-table :data="crawlStats" stripe style="width: 100%" v-loading="loading" class="crawl-table">
|
||||||
<el-table-column prop="source" label="爬虫来源" width="200" />
|
<el-table-column prop="source" label="爬虫来源" width="120" />
|
||||||
<el-table-column prop="count" label="本次获取数量" width="120" sortable />
|
<el-table-column prop="count" label="本次获取数量" width="100" sortable />
|
||||||
<el-table-column label="最近更新时间" width="180">
|
<el-table-column label="最近更新时间" width="140">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatDateTime(row.latestUpdate) }}
|
{{ formatDateTime(row.latestUpdate) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="最新工程时间" width="180">
|
<el-table-column label="最新工程时间" width="140">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatDateTime(row.latestPublishDate) }}
|
{{ formatDateTime(row.latestPublishDate) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="状态" width="100">
|
<el-table-column label="状态" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.error && row.error.trim() ? 'danger' : (row.count > 0 ? 'success' : 'info')">
|
<el-tag :type="row.error && row.error.trim() ? 'danger' : (row.count > 0 ? 'success' : 'info')">
|
||||||
{{ row.error && row.error.trim() ? '出错' : (row.count > 0 ? '正常' : '无数据') }}
|
{{ row.error && row.error.trim() ? '出错' : (row.count > 0 ? '正常' : '无数据') }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="错误信息" min-width="200">
|
<el-table-column label="错误信息" min-width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row.error && row.error.trim()" style="color: #f56c6c">{{ row.error }}</span>
|
<span v-if="row.error && row.error.trim()" style="color: #f56c6c; font-size: 12px;">{{ row.error }}</span>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="100">
|
<el-table-column label="操作" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -47,14 +47,13 @@
|
|||||||
:disabled="crawlingSources.has(row.source)"
|
:disabled="crawlingSources.has(row.source)"
|
||||||
>
|
>
|
||||||
<el-icon><Refresh /></el-icon>
|
<el-icon><Refresh /></el-icon>
|
||||||
更新
|
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="summary" v-if="crawlStats.length > 0">
|
<div class="summary" v-if="crawlStats.length > 0">
|
||||||
<el-descriptions :column="3" border>
|
<el-descriptions :column="2" border class="descriptions">
|
||||||
<el-descriptions-item label="爬虫来源总数">
|
<el-descriptions-item label="爬虫来源总数">
|
||||||
{{ crawlStats.length }}
|
{{ crawlStats.length }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -168,7 +167,11 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.crawl-info {
|
.crawl-info {
|
||||||
padding: 20px;
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crawl-card {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
@@ -177,7 +180,58 @@ onBeforeUnmount(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crawl-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.descriptions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端响应式样式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table .cell {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-descriptions {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="dashboard-ai-container">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
<div class="dashboard-header">
|
||||||
<h2 style="margin: 0;">Dashboard AI</h2>
|
<h2 class="dashboard-title">Dashboard AI</h2>
|
||||||
<el-button type="primary" :loading="loading" @click="fetchAIRecommendations">
|
<el-button type="primary" :loading="loading" @click="fetchAIRecommendations">
|
||||||
<el-icon style="margin-right: 5px"><MagicStick /></el-icon>
|
<el-icon style="margin-right: 5px"><MagicStick /></el-icon>
|
||||||
获取 AI 推荐
|
获取 AI 推荐
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<PinnedProject ref="pinnedProjectRef" @pin-changed="handlePinChanged" />
|
<PinnedProject ref="pinnedProjectRef" @pin-changed="handlePinChanged" />
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20" class="ai-section">
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-card class="box-card" shadow="hover">
|
<el-card class="box-card" shadow="hover">
|
||||||
<!-- <template #header> -->
|
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>AI 推荐项目</span>
|
<span>AI 推荐项目</span>
|
||||||
<el-tag type="success">{{ aiRecommendations.length }} 个推荐</el-tag>
|
<el-tag type="success">{{ aiRecommendations.length }} 个推荐</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<!-- </template> -->
|
|
||||||
<div v-if="loading" style="text-align: center; padding: 40px;">
|
<div v-if="loading" style="text-align: center; padding: 40px;">
|
||||||
<el-icon class="is-loading" :size="30"><Loading /></el-icon>
|
<el-icon class="is-loading" :size="30"><Loading /></el-icon>
|
||||||
<p style="margin-top: 10px; color: #909399;">AI 正在分析中...</p>
|
<p style="margin-top: 10px; color: #909399;">AI 正在分析中...</p>
|
||||||
@@ -27,14 +25,14 @@
|
|||||||
<p style="font-size: 12px; margin-top: 5px;">点击上方按钮获取 AI 推荐</p>
|
<p style="font-size: 12px; margin-top: 5px;">点击上方按钮获取 AI 推荐</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<el-table :data="aiRecommendations" style="width: 100%" size="small">
|
<el-table :data="aiRecommendations" style="width: 100%" size="small" class="ai-table">
|
||||||
<el-table-column label="Pin" width="60" align="center">
|
<el-table-column label="Pin" width="50" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-icon
|
<el-icon
|
||||||
:style="{
|
:style="{
|
||||||
color: scope.row.pin ? '#f56c6c' : '#909399',
|
color: scope.row.pin ? '#f56c6c' : '#909399',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
fontSize: '18px'
|
fontSize: '16px'
|
||||||
}"
|
}"
|
||||||
@click="togglePin(scope.row)"
|
@click="togglePin(scope.row)"
|
||||||
>
|
>
|
||||||
@@ -47,13 +45,13 @@
|
|||||||
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
|
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="source" label="来源" width="200" />
|
<el-table-column prop="source" label="来源" width="100" />
|
||||||
<el-table-column prop="publishDate" label="发布日期" width="180">
|
<el-table-column prop="publishDate" label="发布日期" width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ formatDate(scope.row.publishDate) }}
|
{{ formatDate(scope.row.publishDate) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="confidence" label="推荐度" width="120">
|
<el-table-column prop="confidence" label="推荐度" width="80">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="getConfidenceType(scope.row.confidence)">
|
<el-tag :type="getConfidenceType(scope.row.confidence)">
|
||||||
{{ scope.row.confidence }}%
|
{{ scope.row.confidence }}%
|
||||||
@@ -65,20 +63,21 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div class="filter-section">
|
||||||
<h3 style="margin: 0;">选择日期范围</h3>
|
<h3 class="filter-title">选择日期范围</h3>
|
||||||
<div style="display: flex; gap: 10px;">
|
<div class="filter-controls">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="dateRange"
|
v-model="dateRange"
|
||||||
type="daterange"
|
type="daterange"
|
||||||
range-separator="To"
|
range-separator="至"
|
||||||
start-placeholder="Start Date"
|
start-placeholder="开始日期"
|
||||||
end-placeholder="End Date"
|
end-placeholder="结束日期"
|
||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
clearable
|
clearable
|
||||||
style="width: 240px;"
|
class="date-picker"
|
||||||
/>
|
/>
|
||||||
|
<div class="button-group">
|
||||||
<el-button type="primary" @click="setLast3Days">3天</el-button>
|
<el-button type="primary" @click="setLast3Days">3天</el-button>
|
||||||
<el-button type="primary" @click="setLast7Days">7天</el-button>
|
<el-button type="primary" @click="setLast7Days">7天</el-button>
|
||||||
<el-button type="success" :loading="bidsLoading" @click="fetchBidsByDateRange">
|
<el-button type="success" :loading="bidsLoading" @click="fetchBidsByDateRange">
|
||||||
@@ -87,6 +86,7 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<el-row v-if="showAllBids" :gutter="20" style="margin-top: 20px;">
|
<el-row v-if="showAllBids" :gutter="20" style="margin-top: 20px;">
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-card class="box-card" shadow="hover">
|
<el-card class="box-card" shadow="hover">
|
||||||
@@ -105,14 +105,14 @@
|
|||||||
<p style="margin-top: 10px;">该时间范围内暂无工程</p>
|
<p style="margin-top: 10px;">该时间范围内暂无工程</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<el-table :data="bidsByDateRange" style="width: 100%" size="small">
|
<el-table :data="bidsByDateRange" style="width: 100%" size="small" class="bids-table">
|
||||||
<el-table-column prop="title" label="项目名称">
|
<el-table-column prop="title" label="项目名称">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
|
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="source" label="来源" width="200" />
|
<el-table-column prop="source" label="来源" width="100" />
|
||||||
<el-table-column prop="publishDate" label="发布日期" width="180">
|
<el-table-column prop="publishDate" label="发布日期" width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ formatDate(scope.row.publishDate) }}
|
{{ formatDate(scope.row.publishDate) }}
|
||||||
</template>
|
</template>
|
||||||
@@ -358,11 +358,32 @@ const togglePin = async (item: AIRecommendation) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.dashboard-ai-container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-card {
|
.box-card {
|
||||||
@@ -370,6 +391,39 @@ const togglePin = async (item: AIRecommendation) => {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-title {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-table,
|
||||||
|
.bids-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -378,4 +432,43 @@ a {
|
|||||||
a:hover {
|
a:hover {
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端响应式样式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dashboard-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table .cell {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="dashboard-container">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
<div class="dashboard-header">
|
||||||
<h2 style="margin: 0;">Dashboard</h2>
|
<h2 class="dashboard-title">Dashboard</h2>
|
||||||
<el-button type="primary" :loading="crawling" :disabled="isCrawling" @click="handleCrawl">
|
<el-button type="primary" :loading="crawling" :disabled="isCrawling" @click="handleCrawl">
|
||||||
<el-icon style="margin-right: 5px"><Refresh /></el-icon>
|
<el-icon style="margin-right: 5px"><Refresh /></el-icon>
|
||||||
立刻抓取
|
立刻抓取
|
||||||
@@ -9,34 +9,36 @@
|
|||||||
</div>
|
</div>
|
||||||
<PinnedProject ref="pinnedProjectRef" @pin-changed="handlePinChanged" />
|
<PinnedProject ref="pinnedProjectRef" @pin-changed="handlePinChanged" />
|
||||||
<el-divider />
|
<el-divider />
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div class="filter-section">
|
||||||
<h3 style="margin: 0;">Today's Bids</h3>
|
<h3 class="filter-title">Today's Bids</h3>
|
||||||
<div style="display: flex; gap: 10px;">
|
<div class="filter-controls">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="dateRange"
|
v-model="dateRange"
|
||||||
type="daterange"
|
type="daterange"
|
||||||
range-separator="To"
|
range-separator="至"
|
||||||
start-placeholder="Start Date"
|
start-placeholder="开始日期"
|
||||||
end-placeholder="End Date"
|
end-placeholder="结束日期"
|
||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
clearable
|
clearable
|
||||||
style="width: 240px;"
|
class="date-picker"
|
||||||
/>
|
/>
|
||||||
|
<div class="button-group">
|
||||||
<el-button type="primary" @click="setLast3Days">3天</el-button>
|
<el-button type="primary" @click="setLast3Days">3天</el-button>
|
||||||
<el-button type="primary" @click="setLast7Days">7天</el-button>
|
<el-button type="primary" @click="setLast7Days">7天</el-button>
|
||||||
<el-button type="success" :loading="updating" @click="updateBidsByDateRange">
|
<el-button type="success" :loading="updating" @click="updateBidsByDateRange">
|
||||||
<el-icon style="margin-right: 5px"><Refresh /></el-icon>
|
<el-icon style="margin-right: 5px"><Refresh /></el-icon>
|
||||||
更新
|
更新
|
||||||
</el-button>
|
</el-button>
|
||||||
|
</div>
|
||||||
<el-select
|
<el-select
|
||||||
v-model="selectedKeywords"
|
v-model="selectedKeywords"
|
||||||
multiple
|
multiple
|
||||||
collapse-tags
|
collapse-tags
|
||||||
collapse-tags-tooltip
|
collapse-tags-tooltip
|
||||||
placeholder="Filter by Keywords"
|
placeholder="按关键字筛选"
|
||||||
clearable
|
clearable
|
||||||
style="width: 300px;"
|
class="keyword-select"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="keyword in keywords"
|
v-for="keyword in keywords"
|
||||||
@@ -47,7 +49,7 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="filteredTodayBids" v-loading="loading" style="width: 100%">
|
<el-table :data="filteredTodayBids" v-loading="loading" style="width: 100%" class="bids-table">
|
||||||
<el-table-column label="Pin" width="60" align="center">
|
<el-table-column label="Pin" width="60" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-icon
|
<el-icon
|
||||||
@@ -67,8 +69,8 @@
|
|||||||
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
|
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="source" label="Source" width="220" />
|
<el-table-column prop="source" label="Source" width="120" />
|
||||||
<el-table-column prop="publishDate" label="Date" width="150">
|
<el-table-column prop="publishDate" label="Date" width="120">
|
||||||
<template #default="scope">{{ formatDate(scope.row.publishDate) }}</template>
|
<template #default="scope">{{ formatDate(scope.row.publishDate) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -122,7 +124,7 @@ watch(dateRange, (newDateRange) => {
|
|||||||
localStorage.setItem('dashboard_dateRange', JSON.stringify(newDateRange))
|
localStorage.setItem('dashboard_dateRange', JSON.stringify(newDateRange))
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
// 从 localStorage 加载保存的关键字
|
// 从 localStorage 保存的关键字
|
||||||
const loadSavedKeywords = () => {
|
const loadSavedKeywords = () => {
|
||||||
const saved = localStorage.getItem('selectedKeywords')
|
const saved = localStorage.getItem('selectedKeywords')
|
||||||
if (saved) {
|
if (saved) {
|
||||||
@@ -320,6 +322,60 @@ if (!dateRange.value) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.dashboard-container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-title {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-select {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bids-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -334,4 +390,41 @@ a {
|
|||||||
a:hover {
|
a:hover {
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端响应式样式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dashboard-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-select {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table .cell {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="keywords-container">
|
||||||
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
<div class="keywords-header">
|
||||||
<h2>Keyword Management</h2>
|
<h2 class="keywords-title">Keyword Management</h2>
|
||||||
<el-button type="primary" @click="dialogVisible = true">Add Keyword</el-button>
|
<el-button type="primary" @click="dialogVisible = true">添加关键字</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-loading="loading" style="min-height: 200px;">
|
<div v-loading="loading" style="min-height: 200px;" class="keywords-list">
|
||||||
<el-tag
|
<el-tag
|
||||||
v-for="keyword in keywords"
|
v-for="keyword in keywords"
|
||||||
:key="keyword.id"
|
:key="keyword.id"
|
||||||
closable
|
closable
|
||||||
:type="getTagType(keyword.weight)"
|
:type="getTagType(keyword.weight)"
|
||||||
@close="handleDeleteKeyword(keyword.id)"
|
@close="handleDeleteKeyword(keyword.id)"
|
||||||
style="margin: 5px;"
|
class="keyword-tag"
|
||||||
>
|
>
|
||||||
{{ keyword.word }}
|
{{ keyword.word }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-empty v-if="keywords.length === 0" description="No keywords" />
|
<el-empty v-if="keywords.length === 0" description="暂无关键字" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="dialogVisible" title="Add Keyword" width="30%">
|
<el-dialog v-model="dialogVisible" title="添加关键字" width="90%" :style="{ maxWidth: '400px' }">
|
||||||
<el-form :model="form" label-width="120px">
|
<el-form :model="form" label-width="80px">
|
||||||
<el-form-item label="Keyword">
|
<el-form-item label="关键字">
|
||||||
<el-input v-model="form.word" />
|
<el-input v-model="form.word" placeholder="请输入关键字" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Weight">
|
<el-form-item label="权重">
|
||||||
<el-input-number v-model="form.weight" :min="1" :max="5" />
|
<el-input-number v-model="form.weight" :min="1" :max="5" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
<el-button @click="dialogVisible = false">Cancel</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="handleAddKeyword">Confirm</el-button>
|
<el-button type="primary" @click="handleAddKeyword">确认</el-button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -72,36 +72,86 @@ const getTagType = (weight: number) => {
|
|||||||
|
|
||||||
const handleAddKeyword = async () => {
|
const handleAddKeyword = async () => {
|
||||||
if (!form.word) {
|
if (!form.word) {
|
||||||
ElMessage.warning('Please enter a keyword')
|
ElMessage.warning('请输入关键字')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await api.post('/api/keywords', form)
|
await api.post('/api/keywords', form)
|
||||||
ElMessage.success('Keyword added')
|
ElMessage.success('关键字添加成功')
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
form.word = ''
|
form.word = ''
|
||||||
form.weight = 1
|
form.weight = 1
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('Failed to add keyword')
|
ElMessage.error('添加关键字失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteKeyword = async (id: string) => {
|
const handleDeleteKeyword = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
await api.delete(`/api/keywords/${id}`)
|
await api.delete(`/api/keywords/${id}`)
|
||||||
ElMessage.success('Keyword deleted')
|
ElMessage.success('关键字删除成功')
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('Failed to delete keyword')
|
ElMessage.error('删除关键字失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.card-header {
|
.keywords-container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keywords-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keywords-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keywords-list {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-tag {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端响应式样式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.keywords-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keywords-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-tag {
|
||||||
|
margin: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keywords-list {
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<el-card class="box-card" shadow="hover">
|
<el-card class="box-card" shadow="hover">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>Pinned</span>
|
<span class="card-title">Pinned</span>
|
||||||
<el-tag type="danger">{{ pinnedBids.length }} 个置顶</el-tag>
|
<el-tag type="danger">{{ pinnedBids.length }} 个置顶</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -15,14 +15,14 @@
|
|||||||
<p style="margin-top: 10px;">暂无置顶项目</p>
|
<p style="margin-top: 10px;">暂无置顶项目</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<el-table :data="pinnedBids" style="width: 100%" size="small">
|
<el-table :data="pinnedBids" style="width: 100%" size="small" class="pinned-table">
|
||||||
<el-table-column label="Pin" width="60" align="center">
|
<el-table-column label="Pin" width="50" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-icon
|
<el-icon
|
||||||
:style="{
|
:style="{
|
||||||
color: '#f56c6c',
|
color: '#f56c6c',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
fontSize: '18px'
|
fontSize: '16px'
|
||||||
}"
|
}"
|
||||||
@click="togglePin(scope.row)"
|
@click="togglePin(scope.row)"
|
||||||
>
|
>
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
|
<a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="source" label="来源" width="200" />
|
<el-table-column prop="source" label="来源" width="100" />
|
||||||
<el-table-column prop="publishDate" label="发布日期" width="180">
|
<el-table-column prop="publishDate" label="发布日期" width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ formatSimpleDate(scope.row.publishDate) }}
|
{{ formatSimpleDate(scope.row.publishDate) }}
|
||||||
</template>
|
</template>
|
||||||
@@ -107,11 +107,20 @@ defineExpose({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.box-card {
|
.box-card {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pinned-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -120,4 +129,30 @@ a {
|
|||||||
a:hover {
|
a:hover {
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端响应式样式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table .cell {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-card {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* 基础样式 */
|
||||||
:root {
|
:root {
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@@ -15,15 +16,39 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
/* 移动端基础样式 */
|
||||||
font-weight: 500;
|
@media (max-width: 768px) {
|
||||||
color: #646cff;
|
:root {
|
||||||
text-decoration: inherit;
|
font-size: 14px;
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: flex-start;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 桌面端保持原有样式 */
|
||||||
|
@media (min-width: 769px) {
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -32,10 +57,31 @@ body {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 3.2em;
|
font-size: 3.2em;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -56,17 +102,6 @@ button:focus-visible {
|
|||||||
outline: 4px auto -webkit-focus-ring-color;
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
:root {
|
:root {
|
||||||
color: #213547;
|
color: #213547;
|
||||||
|
|||||||
Reference in New Issue
Block a user