feat: 添加移动端响应式布局和样式优化

This commit is contained in:
dmy
2026-01-19 13:47:41 +08:00
parent fffc17b9ad
commit 95dfcd0278
9 changed files with 732 additions and 149 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,18 +358,72 @@ 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 {
margin-top: 10px; margin-top: 10px;
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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,6 +16,64 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
/* 移动端基础样式 */
@media (max-width: 768px) {
:root {
font-size: 14px;
}
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 {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.card {
padding: 2em;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
}
a { a {
font-weight: 500; font-weight: 500;
color: #646cff; color: #646cff;
@@ -24,19 +83,6 @@ a:hover {
color: #535bf2; color: #535bf2;
} }
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button { button {
border-radius: 8px; border-radius: 8px;
border: 1px solid transparent; border: 1px solid transparent;
@@ -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;