任务管理页面后端优化:提升进度更新频率和状态详细程度

1. 提高爬取阶段进度更新频率:从每10个改为每5个代理更新一次
2. 提高验证阶段进度更新频率:从每5个改为每验证1个代理就更新一次
3. 添加进度百分比计算所需字段:在progress消息中添加current和total字段
4. 增强状态信息详细程度:
   - 添加connecting状态:正在连接插件源
   - 添加starting状态:正在启动爬虫
   - 添加crawling_start状态:开始爬取代理
   - 添加validating_start状态:开始验证代理
   - 在进度消息中添加message字段,显示更详细的进度描述

这些改进可以让前端显示更实时、更详细的任务进度和状态信息
This commit is contained in:
祀梦
2026-01-27 23:15:43 +08:00
parent 466c77b28d
commit b5932a95b2
6 changed files with 844 additions and 51 deletions

View File

@@ -6,6 +6,19 @@ const api = axios.create({
timeout: 30000
})
api.interceptors.request.use(
config => {
const apiKey = localStorage.getItem('api_key')
if (apiKey) {
config.headers['X-API-Key'] = apiKey
}
return config
},
error => {
return Promise.reject(error)
}
)
api.interceptors.response.use(
response => response.data,
error => {
@@ -20,14 +33,22 @@ export const statsAPI = {
}
export const proxiesAPI = {
getProxies: (params) => api.post('/api/proxies', params),
getProxies: (params) => {
const cleanedParams = {}
Object.keys(params).forEach(key => {
if (params[key] !== null && params[key] !== undefined && params[key] !== '') {
cleanedParams[key] = params[key]
}
})
return api.post('/api/proxies', cleanedParams)
},
getRandomProxy: () => api.get('/api/proxies/random'),
getProxyDetail: (ip, port) => api.get(`/api/proxies/${ip}/${port}`),
deleteProxy: (ip, port) => api.delete(`/api/proxies/${ip}/${port}`),
batchDeleteProxies: (proxies) => api.post('/api/proxies/batch-delete', { proxies }),
cleanInvalidProxies: () => api.delete('/api/proxies/clean-invalid'),
exportProxies: (format, protocol) => api.get(`/api/proxies/export/${format}`, {
params: { protocol },
params: protocol ? { protocol } : {},
responseType: 'blob'
})
}

View File

@@ -1,6 +1,7 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
import router from './router'
import './style.css'
@@ -12,6 +13,8 @@ const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(ElementPlus)
app.use(ElementPlus, {
locale: zhCn,
})
app.mount('#app')

View File

@@ -5,7 +5,7 @@
<el-card class="filter-card" shadow="hover">
<el-form :inline="true" :model="filterForm" class="form-row">
<el-form-item label="协议类型">
<el-select v-model="filterForm.protocol" placeholder="全部" clearable style="width: 120px">
<el-select v-model="filterForm.protocol" placeholder="全部" clearable style="width: 120px" @change="handleSearch">
<el-option label="全部" value=""></el-option>
<el-option label="HTTP" value="http"></el-option>
<el-option label="HTTPS" value="https"></el-option>
@@ -14,24 +14,14 @@
</el-select>
</el-form-item>
<el-form-item label="最低分数">
<el-input-number v-model="filterForm.minScore" :min="0" :max="10" style="width: 120px" />
<el-input-number v-model="filterForm.minScore" :min="0" :max="10" style="width: 120px" @change="handleSearch" />
</el-form-item>
<el-form-item label="排序方式">
<el-select v-model="filterForm.sortBy" style="width: 140px">
<el-select v-model="filterForm.sortBy" style="width: 140px" @change="handleSearch">
<el-option label="更新时间" value="last_check"></el-option>
<el-option label="分数" value="score"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<span class="btn-icon">🔍</span>
搜索
</el-button>
<el-button @click="handleReset">
<span class="btn-icon">🔄</span>
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
@@ -40,19 +30,24 @@
<div class="card-header">
<span class="card-title">代理详情</span>
<div class="header-actions">
<el-button type="danger" size="small" @click="handleBatchDelete" :disabled="selectedProxies.length === 0">
批量删除
</el-button>
<el-dropdown @command="handleExport" split-button type="success">
导出
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="txt">TXT格式</el-dropdown-item>
<el-dropdown-item command="csv">CSV格式</el-dropdown-item>
<el-dropdown-item command="json">JSON格式</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button-group>
<el-button type="danger" @click="handleBatchDelete" :disabled="selectedProxies.length === 0">
批量删除
</el-button>
<el-dropdown trigger="click" @command="handleExport">
<el-button type="success">
导出
<el-icon class="el-icon--right"><component :is="ArrowDownIcon" /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="txt">TXT格式</el-dropdown-item>
<el-dropdown-item command="csv">CSV格式</el-dropdown-item>
<el-dropdown-item command="json">JSON格式</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-button-group>
</div>
</div>
</template>
@@ -76,15 +71,14 @@
</el-table-column>
<el-table-column prop="score" label="分数" width="100">
<template #default="scope">
<el-rate
:model-value="scope.row.score || 0"
disabled
show-score
:score-template="scope.row.score ? '{value}' : '0'"
/>
<span class="score-value">{{ scope.row.score || 0 }}</span>
</template>
</el-table-column>
<el-table-column prop="last_check" label="最后检查时间">
<template #default="scope">
{{ formatDateTime(scope.row.last_check) }}
</template>
</el-table-column>
<el-table-column prop="last_check" label="最后检查时间" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button
@@ -123,10 +117,13 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowDown } from '@element-plus/icons-vue'
import { useProxyStore } from '../stores/proxy'
import PageHeader from '../components/PageHeader.vue'
const proxyStore = useProxyStore()
const ArrowDownIcon = ArrowDown
const proxyStore = useProxyStore()
const currentPage = ref(1)
const pageSize = ref(20)
@@ -149,11 +146,23 @@ function getProtocolType(protocol) {
return types[protocol] || 'info'
}
function formatDateTime(dateTimeStr) {
if (!dateTimeStr) return '-'
const date = new Date(dateTimeStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
async function fetchProxies() {
await proxyStore.fetchProxies({
page: currentPage.value,
page_size: pageSize.value,
protocol: filterForm.protocol || undefined,
protocol: filterForm.protocol || null,
min_score: filterForm.minScore,
sort_by: filterForm.sortBy,
sort_order: filterForm.sortOrder
@@ -223,7 +232,7 @@ async function handleBatchDelete() {
}
async function handleExport(format) {
const success = await proxyStore.exportProxies(format, filterForm.protocol || undefined)
const success = await proxyStore.exportProxies(format, filterForm.protocol || null)
if (success) {
ElMessage.success(`导出 ${format.toUpperCase()} 格式成功啦~`)
}

View File

@@ -14,6 +14,17 @@
</template>
<el-form :model="settings" label-width="150px" class="settings-form">
<el-form-item label="管理员API Key">
<el-input
v-model="settings.api_key"
placeholder="请输入管理员API Key"
type="password"
show-password
class="setting-input"
/>
<div class="setting-hint">用于执行管理操作的API Key</div>
</el-form-item>
<el-form-item label="数据库路径">
<el-input v-model="settings.db_path" placeholder="数据库文件路径" />
</el-form-item>
@@ -92,6 +103,7 @@ import PageHeader from '../components/PageHeader.vue'
const loading = ref(false)
const saving = ref(false)
const settings = reactive({
api_key: '',
db_path: '',
crawl_timeout: 30,
validation_timeout: 10,
@@ -109,6 +121,7 @@ async function fetchSettings() {
const data = await response.json()
Object.assign(settings, data)
}
settings.api_key = localStorage.getItem('api_key') || ''
} finally {
loading.value = false
}
@@ -117,12 +130,19 @@ async function fetchSettings() {
async function handleSave() {
saving.value = true
try {
if (settings.api_key) {
localStorage.setItem('api_key', settings.api_key)
} else {
localStorage.removeItem('api_key')
}
const { api_key, ...settingsToSend } = settings
const response = await fetch('http://localhost:8923/api/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(settings)
body: JSON.stringify(settingsToSend)
})
if (response.ok) {
@@ -171,6 +191,13 @@ onMounted(() => {
color: var(--text-secondary);
}
.setting-hint {
margin-top: 8px;
font-size: 12px;
color: var(--text-secondary);
line-height: 1.4;
}
.btn-icon {
font-size: 20px;
margin-right: 8px;