Files
ProxyPool/frontend/src/views/CrawlerTasks.vue
祀梦 209f03a238 优化前端代码架构 - 提取CSS变量和工具类
主要改进:
- 新增 variables.css 统一管理所有主题相关的CSS变量
- 新增 utilities.css 提供可复用的工具类组件
- 重构所有Vue组件,移除重复的CSS代码
- 统一使用CSS变量实现一致的粉色主题(#FF6B9D)
- 改进代码组织结构,提升可维护性
- 优化样式继承和复用机制

修改文件:
- 新增:frontend/src/styles/variables.css, utilities.css
- 重构:App.vue, 所有视图组件和组件文件
- 更新:style.css, element-plus.css

技术亮点:
- 模块化CSS架构,使用@import导入
- 统一的颜色、间距、阴影、过渡效果变量
- 卡片、按钮、布局等通用工具类
- 响应式设计支持
2026-01-27 21:58:28 +08:00

386 lines
8.8 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page-container">
<PageHeader title="任务管理" icon="🎀" />
<el-card class="control-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="card-title">🎮 任务控制</span>
<el-tag :type="crawler.running ? 'success' : 'info'" size="large">
{{ crawler.running ? '运行中' : '已停止' }}
</el-tag>
</div>
</template>
<div class="control-content">
<div class="control-item">
<label class="control-label">验证并发数</label>
<el-input-number
v-model="numValidators"
:min="10"
:max="200"
:step="10"
size="large"
class="control-input"
/>
</div>
<div class="control-actions">
<el-button
type="primary"
size="large"
@click="handleStart"
:loading="crawler.running"
:disabled="crawler.running"
>
<span class="btn-icon">🚀</span>
开始任务
</el-button>
<el-button
type="danger"
size="large"
@click="handleStop"
:disabled="!crawler.running"
>
<span class="btn-icon"></span>
停止任务
</el-button>
</div>
</div>
</el-card>
<el-card class="progress-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="card-title">📊 任务进度</span>
</div>
</template>
<div class="progress-content">
<div class="progress-item">
<div class="progress-label">爬取进度</div>
<el-progress
:percentage="crawlProgress"
:stroke-width="24"
class="progress-bar"
color="#FF6B9D"
>
<span class="progress-text">成功率 {{ crawler.progress.success_rate }}%</span>
</el-progress>
</div>
<div class="progress-item">
<div class="progress-label">验证统计</div>
<div class="stats-grid">
<div class="stat-item success">
<span class="stat-label">发现</span>
<span class="stat-value">{{ crawler.progress.found }}</span>
</div>
<div class="stat-item verified">
<span class="stat-label">验证通过</span>
<span class="stat-value">{{ crawler.progress.verified }}</span>
</div>
</div>
</div>
<div class="status-box">
<div class="status-item">
<span class="status-label">状态</span>
<span class="status-value">{{ crawler.statusMessage || '等待中...' }}</span>
</div>
<div class="status-item" v-if="crawler.stats.start_time">
<span class="status-label">开始时间</span>
<span class="status-value">{{ formatTime(crawler.stats.start_time) }}</span>
</div>
<div class="status-item" v-if="crawler.stats.plugins?.length">
<span class="status-label">加载插件</span>
<span class="status-value">{{ crawler.stats.plugins.length }} </span>
</div>
</div>
</div>
</el-card>
<el-card class="scheduled-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="card-title"> 定时任务</span>
<el-switch
v-model="crawler.scheduled"
@change="handleSchedulerChange"
size="large"
active-color="#FF6B9D"
inactive-color="#dcdfe6"
/>
</div>
</template>
<div class="scheduled-content">
<div class="scheduled-item">
<label class="scheduled-label">执行间隔分钟</label>
<el-input-number
v-model="crawler.intervalMinutes"
:min="10"
:max="1440"
:step="10"
size="large"
:disabled="!crawler.scheduled"
class="scheduled-input"
@change="handleIntervalChange"
/>
</div>
<div class="scheduled-info">
<el-alert
:title="crawler.scheduled ? '定时任务已启用' : '定时任务已停用'"
:type="crawler.scheduled ? 'success' : 'info'"
:description="crawler.scheduled ? `每 ${crawler.intervalMinutes} 分钟自动执行一次爬取任务~` : '开启定时任务可以自动定期更新代理池哦~'"
show-icon
:closable="false"
/>
</div>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useCrawlerStore } from '../stores/crawler'
import PageHeader from '../components/PageHeader.vue'
const crawler = useCrawlerStore()
const numValidators = ref(50)
const crawlProgress = computed(() => {
if (!crawler.running || crawler.progress.total === 0) return 0
return Math.round((crawler.progress.current / crawler.progress.total) * 100)
})
function formatTime(timeStr) {
if (!timeStr) return '-'
const date = new Date(timeStr)
return date.toLocaleString('zh-CN')
}
async function handleStart() {
const success = await crawler.startCrawler(numValidators.value)
if (success) {
ElMessage.success('爬虫任务开始啦~')
}
}
async function handleStop() {
const success = await crawler.stopCrawler()
if (success) {
ElMessage.success('爬虫任务已停止~')
}
}
async function handleSchedulerChange(enabled) {
const success = await crawler.setScheduler(enabled, crawler.intervalMinutes)
if (success) {
ElMessage.success(enabled ? '定时任务已启动~' : '定时任务已停止~')
}
}
async function handleIntervalChange() {
if (crawler.scheduled) {
const success = await crawler.setScheduler(true, crawler.intervalMinutes)
if (success) {
ElMessage.success(`定时任务间隔已更新为 ${crawler.intervalMinutes} 分钟~`)
}
}
}
onMounted(async () => {
await crawler.fetchStatus()
await crawler.fetchSchedulerStatus()
crawler.connectWebSocket()
})
onUnmounted(() => {
crawler.disconnectWebSocket()
})
</script>
<style scoped>
.control-card {
margin-bottom: 20px;
border-radius: var(--radius-xl);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: var(--primary);
}
.control-content {
padding: 20px;
}
.control-item {
display: flex;
align-items: center;
margin-bottom: 30px;
}
.control-label {
font-size: 16px;
color: var(--text-muted);
margin-right: 20px;
min-width: 100px;
}
.control-input {
width: 200px;
}
.control-actions {
display: flex;
gap: 20px;
justify-content: center;
}
.btn-icon {
font-size: 20px;
margin-right: 8px;
}
.progress-card {
margin-bottom: 20px;
border-radius: var(--radius-xl);
}
.progress-content {
padding: 20px;
}
.progress-item {
margin-bottom: 30px;
}
.progress-label {
font-size: 16px;
color: var(--text-muted);
margin-bottom: 15px;
font-weight: 600;
}
.progress-bar {
margin-bottom: 10px;
}
.progress-text {
font-size: 14px;
color: var(--primary);
font-weight: 600;
}
.status-box {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
padding: 20px;
background: #FFF0F5;
border-radius: 12px;
}
.status-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.status-label {
font-size: 14px;
color: var(--text-secondary);
}
.status-value {
font-size: 16px;
color: var(--primary);
font-weight: 600;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.stat-item {
padding: 15px;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.stat-item.success {
background: rgba(52, 211, 153, 0.1);
border: 2px solid var(--green);
}
.stat-item.verified {
background: rgba(255, 107, 157, 0.1);
border: 2px solid var(--primary);
}
.stat-label {
font-size: 14px;
color: var(--text-muted);
font-weight: 600;
}
.stat-value {
font-size: 24px;
font-weight: 700;
}
.stat-item.success .stat-value {
color: var(--green);
}
.stat-item.verified .stat-value {
color: var(--primary);
}
.scheduled-card {
border-radius: var(--radius-xl);
}
.scheduled-content {
padding: 20px;
}
.scheduled-item {
display: flex;
align-items: center;
margin-bottom: 30px;
}
.scheduled-label {
font-size: 16px;
color: var(--text-muted);
margin-right: 20px;
min-width: 150px;
}
.scheduled-input {
width: 200px;
}
.scheduled-info {
padding: 10px;
}
</style>