Files
ProxyPool/WebUI/src/views/ProxyList.vue

352 lines
9.9 KiB
Vue

<template>
<div class="page-container">
<PageHeader title="代理列表" :icon="Document" />
<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"
@change="handleSearch"
>
<el-option label="全部" value="" />
<el-option label="HTTP" value="http" />
<el-option label="HTTPS" value="https" />
<el-option label="SOCKS4" value="socks4" />
<el-option label="SOCKS5" value="socks5" />
</el-select>
</el-form-item>
<el-form-item label="最低分数">
<el-input-number
v-model="filterForm.minScore"
:min="0"
:max="100"
style="width: 120px"
@change="handleSearch"
/>
</el-form-item>
<el-form-item label="排序方式">
<el-select v-model="filterForm.sortBy" style="width: 140px" @change="handleSearch">
<el-option label="更新时间" value="last_check" />
<el-option label="分数" value="score" />
</el-select>
</el-form-item>
</el-form>
</el-card>
<el-card class="table-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="card-title">
<el-icon class="header-icon"><List /></el-icon>
代理详情
</span>
<div class="header-actions">
<el-button-group>
<el-button
type="danger"
@click="handleBatchDelete"
:disabled="selectedProxies.length === 0"
>
<el-icon class="btn-icon"><Delete /></el-icon>
批量删除
</el-button>
<el-dropdown trigger="click" @command="handleExport">
<el-button type="success">
<el-icon class="btn-icon"><Download /></el-icon>
导出
<el-icon class="el-icon--right"><ArrowDown /></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>
<el-table
:data="proxyStore.proxies"
style="width: 100%"
v-loading="proxyStore.loading"
@selection-change="handleSelectionChange"
:row-style="{ cursor: 'pointer' }"
empty-text="暂无数据"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="ip" label="IP地址" width="150" />
<el-table-column prop="port" label="端口" width="100" />
<el-table-column prop="protocol" label="协议" width="100">
<template #default="{ row }">
<el-tag :type="getProtocolType(row.protocol)" effect="light" size="small">
{{ row.protocol.toUpperCase() }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="score" label="分数" width="100">
<template #default="{ row }">
<span class="score-value" :class="{ 'score-high': row.score >= 8, 'score-medium': row.score >= 5 && row.score < 8, 'score-low': row.score < 5 }">
{{ row.score || 0 }}
</span>
</template>
</el-table-column>
<el-table-column prop="last_check" label="最后检查时间" min-width="180">
<template #default="{ row }">
{{ formatDateTime(row.last_check) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click.stop="handleCopy(row)"
>
<el-icon class="btn-icon"><CopyDocument /></el-icon>
复制
</el-button>
<el-button
type="danger"
size="small"
@click.stop="handleDelete(row)"
>
<el-icon class="btn-icon"><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="PAGE_SIZE_OPTIONS"
:total="proxyStore.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Document, ArrowDown, List, Delete, Download, CopyDocument } from '@element-plus/icons-vue'
import { useProxyStore } from '../stores/proxy'
import { formatDateTime } from '../utils/format'
import { confirmDelete, confirmBatchDelete } from '../utils/confirm'
import { copyProxy } from '../utils/clipboard'
import PageHeader from '../components/PageHeader.vue'
/** 分页选项 */
const PAGE_SIZE_OPTIONS = [10, 20, 50, 100]
/** 默认分页大小 */
const DEFAULT_PAGE_SIZE = 20
const proxyStore = useProxyStore()
// ==================== 状态 ====================
const currentPage = ref(1)
const pageSize = ref(DEFAULT_PAGE_SIZE)
const selectedProxies = ref([])
let abortController = null
const filterForm = reactive({
protocol: '',
minScore: 0,
sortBy: 'last_check',
sortOrder: 'DESC'
})
// ==================== 协议类型映射 ====================
const PROTOCOL_TYPE_MAP = {
http: 'info',
https: 'success',
socks4: 'warning',
socks5: 'primary'
}
function getProtocolType(protocol) {
return PROTOCOL_TYPE_MAP[protocol] || 'info'
}
// ==================== 数据获取 ====================
async function fetchProxies() {
// 取消之前的请求
if (abortController) {
abortController.abort()
}
abortController = new AbortController()
try {
const success = await proxyStore.fetchProxies({
page: currentPage.value,
page_size: pageSize.value,
protocol: filterForm.protocol || null,
min_score: filterForm.minScore,
sort_by: filterForm.sortBy,
sort_order: filterForm.sortOrder
}, abortController.signal)
if (!success) {
ElMessage.error('获取代理列表失败')
}
} catch (error) {
if (error.name === 'AbortError') {
// 用户主动取消,不提示错误
return
}
throw error
} finally {
abortController = null
}
}
// ==================== 事件处理 ====================
function handleSearch() {
currentPage.value = 1
fetchProxies()
}
function handleSelectionChange(selection) {
selectedProxies.value = selection.map(item => ({ ip: item.ip, port: item.port }))
}
async function handleCopy(proxy) {
await copyProxy(proxy)
}
async function handleDelete(proxy) {
const confirmed = await confirmDelete(`代理 ${proxy.ip}:${proxy.port}`)
if (!confirmed) return
const filters = {
protocol: filterForm.protocol || null,
min_score: filterForm.minScore,
sort_by: filterForm.sortBy,
sort_order: filterForm.sortOrder
}
const success = await proxyStore.deleteProxy(proxy.ip, proxy.port, currentPage.value, pageSize.value, filters)
if (success) {
ElMessage.success('删除成功')
}
}
async function handleBatchDelete() {
const count = selectedProxies.value.length
if (!count) return
const confirmed = await confirmBatchDelete(count, '代理')
if (!confirmed) return
const filters = {
protocol: filterForm.protocol || null,
min_score: filterForm.minScore,
sort_by: filterForm.sortBy,
sort_order: filterForm.sortOrder
}
const deletedCount = await proxyStore.batchDeleteProxies(selectedProxies.value, currentPage.value, pageSize.value, filters)
if (deletedCount > 0) {
ElMessage.success(`已删除 ${deletedCount} 个代理`)
selectedProxies.value = []
}
}
async function handleExport(format) {
const success = await proxyStore.exportProxies(format, filterForm.protocol || null)
if (success) {
ElMessage.success(`导出 ${format.toUpperCase()} 格式成功`)
}
}
function handleSizeChange(size) {
pageSize.value = size
currentPage.value = 1
fetchProxies()
}
function handleCurrentChange(page) {
currentPage.value = page
fetchProxies()
}
// ==================== 生命周期 ====================
onMounted(() => {
fetchProxies()
})
onUnmounted(() => {
if (abortController) {
abortController.abort()
abortController = null
}
})
</script>
<style scoped>
.filter-card {
margin-bottom: 20px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
}
.table-card {
border-radius: var(--radius-lg);
background: var(--surface);
border: 1px solid var(--border);
}
.table-card:hover,
.filter-card:hover {
border-color: var(--border-light);
}
.header-icon {
margin-right: 8px;
color: var(--primary);
}
.header-actions {
display: flex;
gap: 10px;
}
.score-value {
font-weight: 600;
font-size: 14px;
}
.score-high {
color: var(--success);
}
.score-medium {
color: var(--warning);
}
.score-low {
color: var(--danger);
}
.pagination-wrapper {
display: flex;
justify-content: center;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid var(--border);
}
</style>