- 核心新增: ICON_CLICK(PNG图标方案,预渲染资源)/SCRATCH/JIGSAW/CURVE_SLIDER(V1-V3)/ANGLE/CURVE_DRAW/WORD_ORDER_CLICK/PROOF_OF_WORK 等生成器 - 图标点选: classpath PNG 加载替代运行时字体渲染(Linux容器无emoji字体),提示条图作为 templateImage 返回前端 - 新增模块: crypto(AES+RSA)/obfuscator(背景乱序/噪声/扭曲)/risk(风控/IP黑名单/限流)/site(站点管理)/ml(轨迹规则引擎) - 平台后端: 站点管理/验证码API(generate/verify/secondary-verify)/统计/ML轨迹学习 - 前端SDK: TPCaptcha兼容,支持全部新型号渲染与交互 - 工具: tools/icon-render 图标预渲染工具
68 lines
2.2 KiB
Vue
68 lines
2.2 KiB
Vue
<template>
|
|
<n-space vertical :size="20">
|
|
<n-h2>统计监控</n-h2>
|
|
<n-grid :cols="4" :x-gap="16" :y-gap="16">
|
|
<n-gi><n-statistic label="请求总数" :value="stats.total" /></n-gi>
|
|
<n-gi><n-statistic label="通过总数" :value="stats.success" /></n-gi>
|
|
<n-gi><n-statistic label="失败总数" :value="stats.fail" /></n-gi>
|
|
<n-gi><n-statistic label="总通过率" :value="stats.passRate" /></n-gi>
|
|
</n-grid>
|
|
<n-h3>IP黑名单管理</n-h3>
|
|
<n-space>
|
|
<n-input v-model:value="banForm.ip" placeholder="IP地址" style="width:200px" />
|
|
<n-input v-model:value="banForm.reason" placeholder="封禁原因" style="width:200px" />
|
|
<n-button type="error" @click="banIp">封禁IP</n-button>
|
|
</n-space>
|
|
<n-data-table :columns="ipColumns" :data="bannedIps" :bordered="false" />
|
|
</n-space>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, h } from 'vue'
|
|
import { NSpace, NH2, NH3, NGrid, NGi, NStatistic, NDataTable, NButton, NInput, NTag } from 'naive-ui'
|
|
import axios from 'axios'
|
|
|
|
const stats = ref({ total: 0, success: 0, fail: 0, passRate: '0%' })
|
|
const bannedIps = ref([])
|
|
const banForm = ref({ ip: '', reason: '' })
|
|
|
|
const ipColumns = [
|
|
{ title: 'IP', key: 'ip' },
|
|
{ title: '原因', key: 'reason' },
|
|
{ title: '解封时间', key: 'banUntil', render: (row) => row.banUntil ? new Date(row.banUntil).toLocaleString('zh-CN') : '永久' },
|
|
{
|
|
title: '操作', key: 'actions',
|
|
render: (row) => h(NButton, { size: 'small', type: 'warning', onClick: () => unbanIp(row.ip) }, () => '解封')
|
|
},
|
|
]
|
|
|
|
async function loadStats() {
|
|
try {
|
|
const resp = await axios.get('/api/admin/stats?days=7')
|
|
if (resp.data?.code === 200) stats.value = resp.data.data
|
|
} catch {}
|
|
}
|
|
|
|
async function banIp() {
|
|
if (!banForm.value.ip) return
|
|
await axios.post('/api/admin/ip-blacklist', {
|
|
ip: banForm.value.ip,
|
|
reason: banForm.value.reason,
|
|
durationMs: 3600000,
|
|
})
|
|
banForm.value = { ip: '', reason: '' }
|
|
await loadBannedIps()
|
|
}
|
|
|
|
async function unbanIp(ip) {
|
|
await axios.delete(`/api/admin/ip-blacklist/${ip}`)
|
|
await loadBannedIps()
|
|
}
|
|
|
|
async function loadBannedIps() {
|
|
bannedIps.value = []
|
|
}
|
|
|
|
onMounted(() => { loadStats(); loadBannedIps() })
|
|
</script>
|