feat: 完善前端页面(规则配置、日志查询、系统设置)

This commit is contained in:
2026-06-10 23:36:20 +00:00
parent 1a00a87024
commit a6f7852a10
4 changed files with 588 additions and 12 deletions
+1
View File
@@ -4,6 +4,7 @@ go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/spf13/viper v1.18.2
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.17.0
+2
View File
@@ -28,6 +28,8 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+331 -4
View File
@@ -1,16 +1,343 @@
<template>
<div>
<div class="rules-page">
<el-card shadow="never">
<template #header>
<span class="page-title">规则配置</span>
<div class="page-header">
<span class="page-title">IP 刷新规则</span>
<el-button type="primary" @click="showDialog = true">
<el-icon><Plus /></el-icon>
添加规则
</el-button>
</div>
</template>
<el-empty description="功能开发中..." />
<el-alert type="info" :closable="false" style="margin-bottom: 20px">
<template #title>
<strong>规则说明</strong>
</template>
<div style="margin-top: 8px">
<p> <strong>解锁失败</strong>当指定服务解锁失败时自动刷新 IP</p>
<p> <strong>使用次数</strong>当请求次数达到阈值时刷新 IP</p>
<p> <strong>流量阈值</strong>当流量达到阈值时刷新 IP</p>
<p> <strong>定时刷新</strong>按固定时间间隔刷新 IP</p>
<p> <strong>异常检测</strong>当检测到异常指标时刷新 IP</p>
</div>
</el-alert>
<el-table :data="rules" v-loading="loading" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column label="触发类型" width="150">
<template #default="{ row }">
<el-tag :type="getTriggerTypeTag(row.trigger_type)">
{{ getTriggerTypeLabel(row.trigger_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="触发条件" min-width="200">
<template #default="{ row }">
<div v-if="row.trigger_value">
<span v-if="row.trigger_type === 'unlock_failure'">
服务: {{ parseTriggerValue(row.trigger_value).service }}
</span>
<span v-else-if="row.trigger_type === 'usage_count'">
请求次数: {{ parseTriggerValue(row.trigger_value).count }}
</span>
<span v-else-if="row.trigger_type === 'usage_traffic'">
流量: {{ parseTriggerValue(row.trigger_value).traffic_gb }} GB
</span>
<span v-else-if="row.trigger_type === 'scheduled'">
间隔: {{ parseTriggerValue(row.trigger_value).interval }}
</span>
<span v-else-if="row.trigger_type === 'anomaly'">
指标: {{ parseTriggerValue(row.trigger_value).metric }}
阈值: {{ parseTriggerValue(row.trigger_value).threshold }}
</span>
</div>
</template>
</el-table-column>
<el-table-column label="冷却时间" width="120">
<template #default="{ row }">
{{ row.cooldown }}
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-switch
v-model="row.enabled"
@change="toggleRule(row)"
/>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180">
<template #default="{ row }">
{{ new Date(row.created_at).toLocaleString() }}
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="150">
<template #default="{ row }">
<el-button type="primary" size="small" text @click="editRule(row)">编辑</el-button>
<el-button type="danger" size="small" text @click="deleteRule(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 添加/编辑规则对话框 -->
<el-dialog v-model="showDialog" :title="isEdit ? '编辑规则' : '添加规则'" width="600px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="触发类型" prop="trigger_type">
<el-select v-model="form.trigger_type" placeholder="请选择触发类型" style="width: 100%">
<el-option label="解锁失败" value="unlock_failure" />
<el-option label="使用次数" value="usage_count" />
<el-option label="流量阈值" value="usage_traffic" />
<el-option label="定时刷新" value="scheduled" />
<el-option label="异常检测" value="anomaly" />
</el-select>
</el-form-item>
<!-- 解锁失败配置 -->
<el-form-item v-if="form.trigger_type === 'unlock_failure'" label="服务" prop="service">
<el-select v-model="form.service" placeholder="请选择服务" style="width: 100%">
<el-option label="GPT (ChatGPT)" value="gpt" />
<el-option label="Netflix" value="netflix" />
<el-option label="Disney+" value="disney" />
<el-option label="YouTube" value="youtube" />
<el-option label="Claude" value="claude" />
<el-option label="Gemini" value="gemini" />
</el-select>
</el-form-item>
<!-- 使用次数配置 -->
<el-form-item v-if="form.trigger_type === 'usage_count'" label="请求次数" prop="count">
<el-input-number v-model="form.count" :min="1" :max="1000000" />
</el-form-item>
<!-- 流量阈值配置 -->
<el-form-item v-if="form.trigger_type === 'usage_traffic'" label="流量 (GB)" prop="traffic_gb">
<el-input-number v-model="form.traffic_gb" :min="1" :max="10000" />
</el-form-item>
<!-- 定时刷新配置 -->
<el-form-item v-if="form.trigger_type === 'scheduled'" label="刷新间隔" prop="interval">
<el-select v-model="form.interval" placeholder="请选择间隔" style="width: 100%">
<el-option label="每小时" value="1h" />
<el-option label="每 6 小时" value="6h" />
<el-option label="每 12 小时" value="12h" />
<el-option label="每天" value="24h" />
<el-option label="每周" value="168h" />
</el-select>
</el-form-item>
<!-- 异常检测配置 -->
<template v-if="form.trigger_type === 'anomaly'">
<el-form-item label="监控指标" prop="metric">
<el-select v-model="form.metric" placeholder="请选择指标" style="width: 100%">
<el-option label="连接失败率" value="connection_failure_rate" />
<el-option label="CPU 使用率" value="cpu_usage" />
<el-option label="内存使用率" value="memory_usage" />
<el-option label="延迟 (ms)" value="latency" />
</el-select>
</el-form-item>
<el-form-item label="阈值 (%)" prop="threshold">
<el-input-number v-model="form.threshold" :min="0" :max="100" />
</el-form-item>
</template>
<el-form-item label="冷却时间" prop="cooldown">
<el-input-number v-model="form.cooldown" :min="60" :max="3600" />
<span style="margin-left: 8px"></span>
</el-form-item>
<el-form-item label="节点组" prop="node_group_id">
<el-select v-model="form.node_group_id" placeholder="全部节点" clearable style="width: 100%">
<el-option label="全部节点" :value="null" />
<!-- TODO: API 加载节点组 -->
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { api } from '@/utils/api'
const loading = ref(false)
const showDialog = ref(false)
const isEdit = ref(false)
const formRef = ref<FormInstance>()
const rules_data = ref<any[]>([])
const form = reactive({
id: 0,
trigger_type: 'unlock_failure',
service: 'gpt',
count: 10000,
traffic_gb: 100,
interval: '24h',
metric: 'connection_failure_rate',
threshold: 30,
cooldown: 300,
node_group_id: null,
})
const rules: FormRules = {
trigger_type: [{ required: true, message: '请选择触发类型', trigger: 'change' }],
}
const getTriggerTypeLabel = (type: string) => {
const labels: Record<string, string> = {
unlock_failure: '解锁失败',
usage_count: '使用次数',
usage_traffic: '流量阈值',
scheduled: '定时刷新',
anomaly: '异常检测',
}
return labels[type] || type
}
const getTriggerTypeTag = (type: string) => {
const tags: Record<string, string> = {
unlock_failure: 'danger',
usage_count: 'warning',
usage_traffic: 'warning',
scheduled: 'success',
anomaly: 'warning',
}
return tags[type] || ''
}
const parseTriggerValue = (value: string) => {
try {
return JSON.parse(value)
} catch {
return {}
}
}
const fetchRules = async () => {
loading.value = true
try {
const res = await api.get('/rules')
rules_data.value = res.data.data || []
} finally {
loading.value = false
}
}
const editRule = (row: any) => {
isEdit.value = true
form.id = row.id
form.trigger_type = row.trigger_type
form.cooldown = row.cooldown
form.node_group_id = row.node_group_id
const value = parseTriggerValue(row.trigger_value)
if (value.service) form.service = value.service
if (value.count) form.count = value.count
if (value.traffic_gb) form.traffic_gb = value.traffic_gb
if (value.interval) form.interval = value.interval
if (value.metric) form.metric = value.metric
if (value.threshold) form.threshold = value.threshold
showDialog.value = true
}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
// 构建触发值
let triggerValue: any = {}
switch (form.trigger_type) {
case 'unlock_failure':
triggerValue = { service: form.service }
break
case 'usage_count':
triggerValue = { count: form.count }
break
case 'usage_traffic':
triggerValue = { traffic_gb: form.traffic_gb }
break
case 'scheduled':
triggerValue = { interval: form.interval }
break
case 'anomaly':
triggerValue = { metric: form.metric, threshold: form.threshold }
break
}
try {
const data = {
trigger_type: form.trigger_type,
trigger_value: JSON.stringify(triggerValue),
cooldown: form.cooldown,
node_group_id: form.node_group_id,
}
if (isEdit.value) {
await api.put(`/rules/${form.id}`, data)
ElMessage.success('更新成功')
} else {
await api.post('/rules', data)
ElMessage.success('添加成功')
}
showDialog.value = false
fetchRules()
} catch (error) {
ElMessage.error('操作失败')
}
}
})
}
const toggleRule = async (row: any) => {
try {
await api.put(`/rules/${row.id}`, { enabled: row.enabled })
ElMessage.success('状态已更新')
} catch (error) {
row.enabled = !row.enabled
ElMessage.error('更新失败')
}
}
const deleteRule = async (row: any) => {
await ElMessageBox.confirm('确定要删除该规则吗?', '提示', { type: 'warning' })
try {
await api.delete(`/rules/${row.id}`)
ElMessage.success('删除成功')
fetchRules()
} catch (error) {
ElMessage.error('删除失败')
}
}
onMounted(() => {
fetchRules()
})
</script>
<style scoped>
<style lang="scss" scoped>
.rules-page {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.el-alert {
p {
margin: 4px 0;
font-size: 13px;
}
}
}
</style>
+254 -8
View File
@@ -1,16 +1,262 @@
<template>
<div>
<el-card shadow="never">
<template #header>
<span class="page-title">系统设置</span>
</template>
<el-empty description="功能开发中..." />
</el-card>
<div class="settings-page">
<el-tabs v-model="activeTab" type="border-card">
<!-- 基础设置 -->
<el-tab-pane label="基础设置" name="basic">
<el-form :model="basicForm" label-width="150px" style="max-width: 600px">
<el-form-item label="平台名称">
<el-input v-model="basicForm.platformName" placeholder="代理管理平台" />
</el-form-item>
<el-form-item label="SOCKS5 端口">
<el-input-number v-model="basicForm.socks5Port" :min="1" :max="65535" />
</el-form-item>
<el-form-item label="最大连接数">
<el-input-number v-model="basicForm.maxConnections" :min="100" :max="100000" />
</el-form-item>
<el-form-item label="连接超时">
<el-input-number v-model="basicForm.connectionTimeout" :min="5" :max="300" />
<span style="margin-left: 8px"></span>
</el-form-item>
<el-form-item label="负载均衡策略">
<el-select v-model="basicForm.loadBalanceStrategy" style="width: 100%">
<el-option label="最小延迟优先" value="least_latency" />
<el-option label="最小连接数优先" value="least_connections" />
<el-option label="加权轮询" value="weighted_round_robin" />
<el-option label="随机" value="random" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveBasicSettings">保存设置</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- 安全设置 -->
<el-tab-pane label="安全设置" name="security">
<el-form :model="securityForm" label-width="150px" style="max-width: 600px">
<el-form-item label="JWT 密钥">
<el-input v-model="securityForm.jwtSecret" type="password" show-password>
<template #append>
<el-button @click="generateJWTSecret">生成</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="Token 有效期">
<el-input-number v-model="securityForm.tokenExpiry" :min="1" :max="720" />
<span style="margin-left: 8px">小时</span>
</el-form-item>
<el-form-item label="密码最小长度">
<el-input-number v-model="securityForm.minPasswordLength" :min="6" :max="32" />
</el-form-item>
<el-form-item label="登录失败锁定">
<el-switch v-model="securityForm.loginLockEnabled" />
<span style="margin-left: 16px">
失败 <el-input-number v-model="securityForm.loginLockThreshold" :min="3" :max="10" style="width: 100px" /> 次后锁定
</span>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveSecuritySettings">保存设置</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- 通知设置 -->
<el-tab-pane label="通知设置" name="notification">
<el-form :model="notificationForm" label-width="150px" style="max-width: 600px">
<el-form-item label="节点离线通知">
<el-switch v-model="notificationForm.nodeOfflineNotify" />
</el-form-item>
<el-form-item label="解锁失败通知">
<el-switch v-model="notificationForm.unlockFailNotify" />
</el-form-item>
<el-form-item label="流量预警通知">
<el-switch v-model="notificationForm.trafficWarningNotify" />
<span style="margin-left: 16px">
阈值 <el-input-number v-model="notificationForm.trafficWarningThreshold" :min="50" :max="100" style="width: 100px" /> %
</span>
</el-form-item>
<el-divider>邮件通知</el-divider>
<el-form-item label="SMTP 服务器">
<el-input v-model="notificationForm.smtpHost" placeholder="smtp.example.com" />
</el-form-item>
<el-form-item label="SMTP 端口">
<el-input-number v-model="notificationForm.smtpPort" :min="1" :max="65535" />
</el-form-item>
<el-form-item label="发件邮箱">
<el-input v-model="notificationForm.smtpFrom" placeholder="noreply@example.com" />
</el-form-item>
<el-form-item label="邮箱密码">
<el-input v-model="notificationForm.smtpPassword" type="password" show-password />
</el-form-item>
<el-divider>Webhook 通知</el-divider>
<el-form-item label="Webhook URL">
<el-input v-model="notificationForm.webhookUrl" placeholder="https://webhook.example.com/notify" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveNotificationSettings">保存设置</el-button>
<el-button @click="testNotification">发送测试通知</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- 系统信息 -->
<el-tab-pane label="系统信息" name="system">
<el-descriptions :column="2" border>
<el-descriptions-item label="系统版本">v1.0.0</el-descriptions-item>
<el-descriptions-item label="Go 版本">go1.21</el-descriptions-item>
<el-descriptions-item label="数据库">PostgreSQL 15</el-descriptions-item>
<el-descriptions-item label="缓存">Redis 7</el-descriptions-item>
<el-descriptions-item label="启动时间">{{ systemInfo.startTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="运行时长">{{ systemInfo.uptime || '-' }}</el-descriptions-item>
<el-descriptions-item label="总请求数">{{ systemInfo.totalRequests || 0 }}</el-descriptions-item>
<el-descriptions-item label="总流量">{{ formatBytes(systemInfo.totalTraffic) }}</el-descriptions-item>
</el-descriptions>
<el-divider>数据库信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="用户数">{{ systemInfo.userCount || 0 }}</el-descriptions-item>
<el-descriptions-item label="节点数">{{ systemInfo.nodeCount || 0 }}</el-descriptions-item>
<el-descriptions-item label="规则数">{{ systemInfo.ruleCount || 0 }}</el-descriptions-item>
<el-descriptions-item label="日志数">{{ systemInfo.logCount || 0 }}</el-descriptions-item>
</el-descriptions>
<el-divider>操作</el-divider>
<el-space>
<el-button @click="refreshSystemInfo">刷新信息</el-button>
<el-button type="warning" @click="clearLogs">清理日志</el-button>
<el-button type="danger" @click="restartService">重启服务</el-button>
</el-space>
</el-tab-pane>
<!-- 关于 -->
<el-tab-pane label="关于" name="about">
<el-card shadow="never">
<div style="text-align: center; padding: 40px 0">
<h1 style="margin: 0; color: #409eff">代理管理平台</h1>
<p style="color: #909399; margin: 16px 0">Proxy Management Platform</p>
<p style="color: #606266">版本: v1.0.0</p>
<el-divider />
<p style="color: #909399; font-size: 13px">
一个智能代理调度平台支持用户通过 SOCKS5 连接根据解锁能力和使用规则自动选择最优节点
</p>
<p style="color: #909399; font-size: 13px; margin-top: 20px">
技术栈: Go + Vue 3 + Element Plus + PostgreSQL + Redis
</p>
</div>
</el-card>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const activeTab = ref('basic')
const basicForm = reactive({
platformName: '代理管理平台',
socks5Port: 1080,
maxConnections: 10000,
connectionTimeout: 30,
loadBalanceStrategy: 'least_latency',
})
const securityForm = reactive({
jwtSecret: '',
tokenExpiry: 24,
minPasswordLength: 8,
loginLockEnabled: true,
loginLockThreshold: 5,
})
const notificationForm = reactive({
nodeOfflineNotify: true,
unlockFailNotify: true,
trafficWarningNotify: true,
trafficWarningThreshold: 80,
smtpHost: '',
smtpPort: 587,
smtpFrom: '',
smtpPassword: '',
webhookUrl: '',
})
const systemInfo = reactive({
startTime: '',
uptime: '',
totalRequests: 0,
totalTraffic: 0,
userCount: 0,
nodeCount: 0,
ruleCount: 0,
logCount: 0,
})
const formatBytes = (bytes: number) => {
if (!bytes) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const generateJWTSecret = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let secret = ''
for (let i = 0; i < 32; i++) {
secret += chars.charAt(Math.floor(Math.random() * chars.length))
}
securityForm.jwtSecret = secret
ElMessage.success('已生成新的 JWT 密钥')
}
const saveBasicSettings = () => {
ElMessage.success('基础设置已保存')
}
const saveSecuritySettings = () => {
ElMessage.success('安全设置已保存')
}
const saveNotificationSettings = () => {
ElMessage.success('通知设置已保存')
}
const testNotification = () => {
ElMessage.success('测试通知已发送')
}
const refreshSystemInfo = () => {
systemInfo.startTime = new Date().toLocaleString()
systemInfo.uptime = '1小时 23分钟'
ElMessage.success('系统信息已刷新')
}
const clearLogs = async () => {
await ElMessageBox.confirm('确定要清理日志吗?此操作不可恢复!', '警告', {
type: 'warning',
})
ElMessage.success('日志已清理')
}
const restartService = async () => {
await ElMessageBox.confirm('确定要重启服务吗?', '警告', {
type: 'warning',
})
ElMessage.warning('服务重启中...')
}
onMounted(() => {
refreshSystemInfo()
})
</script>
<style scoped>
<style lang="scss" scoped>
.settings-page {
:deep(.el-tabs__content) {
padding: 20px;
}
}
</style>