feat: 添加安全仪表板前端页面
- 新增 SecurityPage.vue: 统计卡片、威胁事件表格、封禁管理、风险查询 - 新增 api/security.js: 安全相关API封装 - 路由添加 /security 页面 - 侧边栏添加"安全防护"菜单项 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
59
admin-frontend/src/api/security.js
Normal file
59
admin-frontend/src/api/security.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { api } from './client'
|
||||||
|
|
||||||
|
export async function getDashboard() {
|
||||||
|
const { data } = await api.get('/admin/security/dashboard')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getThreats(params) {
|
||||||
|
const { data } = await api.get('/admin/security/threats', { params })
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBannedIps() {
|
||||||
|
const { data } = await api.get('/admin/security/banned-ips')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBannedUsers() {
|
||||||
|
const { data } = await api.get('/admin/security/banned-users')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function banIp(payload) {
|
||||||
|
const { data } = await api.post('/admin/security/ban-ip', payload)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unbanIp(ip) {
|
||||||
|
const { data } = await api.post('/admin/security/unban-ip', { ip })
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function banUser(payload) {
|
||||||
|
const { data } = await api.post('/admin/security/ban-user', payload)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unbanUser(userId) {
|
||||||
|
const { data } = await api.post('/admin/security/unban-user', { user_id: userId })
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getIpRisk(ip) {
|
||||||
|
const safeIp = encodeURIComponent(String(ip || '').trim())
|
||||||
|
const { data } = await api.get(`/admin/security/ip-risk/${safeIp}`)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserRisk(userId) {
|
||||||
|
const safeUserId = encodeURIComponent(String(userId || '').trim())
|
||||||
|
const { data } = await api.get(`/admin/security/user-risk/${safeUserId}`)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cleanup() {
|
||||||
|
const { data } = await api.post('/admin/security/cleanup', {})
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
ChatLineSquare,
|
ChatLineSquare,
|
||||||
Document,
|
Document,
|
||||||
List,
|
List,
|
||||||
|
Lock,
|
||||||
Message,
|
Message,
|
||||||
Setting,
|
Setting,
|
||||||
Tools,
|
Tools,
|
||||||
@@ -104,6 +105,7 @@ const menuItems = [
|
|||||||
{ path: '/logs', label: '任务日志', icon: List },
|
{ path: '/logs', label: '任务日志', icon: List },
|
||||||
{ path: '/announcements', label: '公告', icon: Bell },
|
{ path: '/announcements', label: '公告', icon: Bell },
|
||||||
{ path: '/email', label: '邮件', icon: Message },
|
{ path: '/email', label: '邮件', icon: Message },
|
||||||
|
{ path: '/security', label: '安全防护', icon: Lock },
|
||||||
{ path: '/system', label: '系统配置', icon: Tools },
|
{ path: '/system', label: '系统配置', icon: Tools },
|
||||||
{ path: '/settings', label: '设置', icon: Setting },
|
{ path: '/settings', label: '设置', icon: Setting },
|
||||||
]
|
]
|
||||||
|
|||||||
804
admin-frontend/src/pages/SecurityPage.vue
Normal file
804
admin-frontend/src/pages/SecurityPage.vue
Normal file
@@ -0,0 +1,804 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
import {
|
||||||
|
banIp,
|
||||||
|
banUser,
|
||||||
|
cleanup,
|
||||||
|
getBannedIps,
|
||||||
|
getBannedUsers,
|
||||||
|
getDashboard,
|
||||||
|
getIpRisk,
|
||||||
|
getThreats,
|
||||||
|
getUserRisk,
|
||||||
|
unbanIp,
|
||||||
|
unbanUser,
|
||||||
|
} from '../api/security'
|
||||||
|
|
||||||
|
const pageSize = 20
|
||||||
|
|
||||||
|
const activeTab = ref('threats')
|
||||||
|
|
||||||
|
const dashboardLoading = ref(false)
|
||||||
|
const dashboard = ref(null)
|
||||||
|
|
||||||
|
const threatsLoading = ref(false)
|
||||||
|
const threatItems = ref([])
|
||||||
|
const threatTotal = ref(0)
|
||||||
|
const threatPage = ref(1)
|
||||||
|
const threatTypeFilter = ref('')
|
||||||
|
const threatSeverityFilter = ref('')
|
||||||
|
|
||||||
|
const bansLoading = ref(false)
|
||||||
|
const bannedIps = ref([])
|
||||||
|
const bannedUsers = ref([])
|
||||||
|
const banTab = ref('ips')
|
||||||
|
|
||||||
|
const banDialogOpen = ref(false)
|
||||||
|
const banSubmitting = ref(false)
|
||||||
|
const banForm = ref({
|
||||||
|
kind: 'ip',
|
||||||
|
ip: '',
|
||||||
|
user_id: '',
|
||||||
|
reason: '',
|
||||||
|
duration_hours: 24,
|
||||||
|
permanent: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const riskTab = ref('ip')
|
||||||
|
const riskLoading = ref(false)
|
||||||
|
const riskIpInput = ref('')
|
||||||
|
const riskUserIdInput = ref('')
|
||||||
|
const riskResult = ref(null)
|
||||||
|
const riskResultKind = ref('')
|
||||||
|
|
||||||
|
const commonThreatTypes = [
|
||||||
|
'sql_injection',
|
||||||
|
'xss',
|
||||||
|
'path_traversal',
|
||||||
|
'command_injection',
|
||||||
|
'ssrf',
|
||||||
|
'scanner',
|
||||||
|
'bruteforce',
|
||||||
|
'csrf',
|
||||||
|
'xxe',
|
||||||
|
'file_upload',
|
||||||
|
]
|
||||||
|
|
||||||
|
function normalizeCount(value) {
|
||||||
|
const n = Number(value)
|
||||||
|
return Number.isFinite(n) ? n : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreMeta(score) {
|
||||||
|
const n = Number(score || 0)
|
||||||
|
if (n >= 80) return { label: '高', type: 'danger' }
|
||||||
|
if (n >= 50) return { label: '中', type: 'warning' }
|
||||||
|
return { label: '低', type: 'success' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatExpires(expiresAt) {
|
||||||
|
const text = String(expiresAt || '').trim()
|
||||||
|
return text ? text : '永久'
|
||||||
|
}
|
||||||
|
|
||||||
|
function payloadTooltip(row) {
|
||||||
|
const parts = []
|
||||||
|
if (row?.field_name) parts.push(`字段: ${row.field_name}`)
|
||||||
|
if (row?.rule) parts.push(`规则: ${row.rule}`)
|
||||||
|
if (row?.matched) parts.push(`匹配: ${row.matched}`)
|
||||||
|
if (row?.value_preview) parts.push(`值: ${row.value_preview}`)
|
||||||
|
return parts.length ? parts.join(' · ') : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathText(row) {
|
||||||
|
const method = String(row?.request_method || '').trim()
|
||||||
|
const path = String(row?.request_path || '').trim()
|
||||||
|
const combined = `${method} ${path}`.trim()
|
||||||
|
return combined || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const threatTypeOptions = computed(() => {
|
||||||
|
const seen = new Set(commonThreatTypes)
|
||||||
|
const recent = dashboard.value?.recent_threat_events || []
|
||||||
|
for (const item of recent) {
|
||||||
|
const t = String(item?.threat_type || '').trim()
|
||||||
|
if (t) seen.add(t)
|
||||||
|
}
|
||||||
|
for (const item of threatItems.value || []) {
|
||||||
|
const t = String(item?.threat_type || '').trim()
|
||||||
|
if (t) seen.add(t)
|
||||||
|
}
|
||||||
|
return Array.from(seen)
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
.map((t) => ({ label: t, value: t }))
|
||||||
|
})
|
||||||
|
|
||||||
|
const dashboardCards = computed(() => {
|
||||||
|
const d = dashboard.value || {}
|
||||||
|
return [
|
||||||
|
{ key: 'threat_events_24h', label: '最近24小时威胁事件', value: normalizeCount(d.threat_events_24h) },
|
||||||
|
{ key: 'banned_ip_count', label: '当前封禁IP数', value: normalizeCount(d.banned_ip_count) },
|
||||||
|
{ key: 'banned_user_count', label: '当前封禁用户数', value: normalizeCount(d.banned_user_count) },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const threatTotalPages = computed(() => Math.max(1, Math.ceil((threatTotal.value || 0) / pageSize)))
|
||||||
|
|
||||||
|
async function loadDashboard() {
|
||||||
|
dashboardLoading.value = true
|
||||||
|
try {
|
||||||
|
dashboard.value = await getDashboard()
|
||||||
|
} catch {
|
||||||
|
dashboard.value = null
|
||||||
|
} finally {
|
||||||
|
dashboardLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadThreats() {
|
||||||
|
threatsLoading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: threatPage.value,
|
||||||
|
per_page: pageSize,
|
||||||
|
}
|
||||||
|
if (threatTypeFilter.value) params.event_type = threatTypeFilter.value
|
||||||
|
if (threatSeverityFilter.value) params.severity = threatSeverityFilter.value
|
||||||
|
|
||||||
|
const data = await getThreats(params)
|
||||||
|
threatItems.value = data?.items || []
|
||||||
|
threatTotal.value = data?.total || 0
|
||||||
|
} catch {
|
||||||
|
threatItems.value = []
|
||||||
|
threatTotal.value = 0
|
||||||
|
} finally {
|
||||||
|
threatsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBans() {
|
||||||
|
if (bansLoading.value) return
|
||||||
|
bansLoading.value = true
|
||||||
|
try {
|
||||||
|
const [ipsRes, usersRes] = await Promise.allSettled([getBannedIps(), getBannedUsers()])
|
||||||
|
bannedIps.value = ipsRes.status === 'fulfilled' ? ipsRes.value?.items || [] : []
|
||||||
|
bannedUsers.value = usersRes.status === 'fulfilled' ? usersRes.value?.items || [] : []
|
||||||
|
} finally {
|
||||||
|
bansLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAll() {
|
||||||
|
await Promise.allSettled([loadDashboard(), loadThreats(), loadBans()])
|
||||||
|
}
|
||||||
|
|
||||||
|
function onThreatFilter() {
|
||||||
|
threatPage.value = 1
|
||||||
|
loadThreats()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onThreatReset() {
|
||||||
|
threatTypeFilter.value = ''
|
||||||
|
threatSeverityFilter.value = ''
|
||||||
|
threatPage.value = 1
|
||||||
|
loadThreats()
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetBanForm() {
|
||||||
|
banForm.value = {
|
||||||
|
kind: 'ip',
|
||||||
|
ip: '',
|
||||||
|
user_id: '',
|
||||||
|
reason: '',
|
||||||
|
duration_hours: 24,
|
||||||
|
permanent: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBanDialog(kind = 'ip', preset = {}) {
|
||||||
|
resetBanForm()
|
||||||
|
banForm.value.kind = kind === 'user' ? 'user' : 'ip'
|
||||||
|
if (banForm.value.kind === 'ip') {
|
||||||
|
banForm.value.ip = String(preset.ip || '').trim()
|
||||||
|
} else {
|
||||||
|
banForm.value.user_id = String(preset.user_id || '').trim()
|
||||||
|
}
|
||||||
|
if (preset.reason) banForm.value.reason = String(preset.reason || '').trim()
|
||||||
|
banDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitBan() {
|
||||||
|
const kind = banForm.value.kind
|
||||||
|
const reason = String(banForm.value.reason || '').trim()
|
||||||
|
const permanent = Boolean(banForm.value.permanent)
|
||||||
|
const durationHours = Number(banForm.value.duration_hours || 24)
|
||||||
|
|
||||||
|
if (!reason) {
|
||||||
|
ElMessage.error('原因不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === 'ip') {
|
||||||
|
const ip = String(banForm.value.ip || '').trim()
|
||||||
|
if (!ip) {
|
||||||
|
ElMessage.error('IP不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
banSubmitting.value = true
|
||||||
|
try {
|
||||||
|
await banIp({ ip, reason, duration_hours: durationHours, permanent })
|
||||||
|
ElMessage.success('IP已封禁')
|
||||||
|
banDialogOpen.value = false
|
||||||
|
await Promise.allSettled([loadDashboard(), loadBans()])
|
||||||
|
} catch {
|
||||||
|
// handled by interceptor
|
||||||
|
} finally {
|
||||||
|
banSubmitting.value = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const userIdRaw = String(banForm.value.user_id || '').trim()
|
||||||
|
const userId = Number.parseInt(userIdRaw, 10)
|
||||||
|
if (!Number.isFinite(userId)) {
|
||||||
|
ElMessage.error('用户ID无效')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
banSubmitting.value = true
|
||||||
|
try {
|
||||||
|
await banUser({ user_id: userId, reason, duration_hours: durationHours, permanent })
|
||||||
|
ElMessage.success('用户已封禁')
|
||||||
|
banDialogOpen.value = false
|
||||||
|
await Promise.allSettled([loadDashboard(), loadBans()])
|
||||||
|
} catch {
|
||||||
|
// handled by interceptor
|
||||||
|
} finally {
|
||||||
|
banSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUnbanIp(ip) {
|
||||||
|
const ipText = String(ip || '').trim()
|
||||||
|
if (!ipText) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定解除对 IP ${ipText} 的封禁吗?`, '解除封禁', {
|
||||||
|
confirmButtonText: '解除',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await unbanIp(ipText)
|
||||||
|
ElMessage.success('已解除IP封禁')
|
||||||
|
await Promise.allSettled([loadDashboard(), loadBans()])
|
||||||
|
} catch {
|
||||||
|
// handled by interceptor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUnbanUser(userId) {
|
||||||
|
const id = Number.parseInt(String(userId || '').trim(), 10)
|
||||||
|
if (!Number.isFinite(id)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定解除对 用户ID ${id} 的封禁吗?`, '解除封禁', {
|
||||||
|
confirmButtonText: '解除',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await unbanUser(id)
|
||||||
|
ElMessage.success('已解除用户封禁')
|
||||||
|
await Promise.allSettled([loadDashboard(), loadBans()])
|
||||||
|
} catch {
|
||||||
|
// handled by interceptor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function jumpToIpRisk(ip) {
|
||||||
|
const ipText = String(ip || '').trim()
|
||||||
|
if (!ipText) return
|
||||||
|
activeTab.value = 'risk'
|
||||||
|
riskTab.value = 'ip'
|
||||||
|
riskIpInput.value = ipText
|
||||||
|
queryIpRisk()
|
||||||
|
}
|
||||||
|
|
||||||
|
function jumpToUserRisk(userId) {
|
||||||
|
const idText = String(userId || '').trim()
|
||||||
|
if (!idText) return
|
||||||
|
activeTab.value = 'risk'
|
||||||
|
riskTab.value = 'user'
|
||||||
|
riskUserIdInput.value = idText
|
||||||
|
queryUserRisk()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryIpRisk() {
|
||||||
|
const ip = String(riskIpInput.value || '').trim()
|
||||||
|
if (!ip) {
|
||||||
|
ElMessage.error('请输入IP')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
riskLoading.value = true
|
||||||
|
try {
|
||||||
|
riskResult.value = await getIpRisk(ip)
|
||||||
|
riskResultKind.value = 'ip'
|
||||||
|
} catch {
|
||||||
|
riskResult.value = null
|
||||||
|
riskResultKind.value = ''
|
||||||
|
} finally {
|
||||||
|
riskLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryUserRisk() {
|
||||||
|
const raw = String(riskUserIdInput.value || '').trim()
|
||||||
|
const userId = Number.parseInt(raw, 10)
|
||||||
|
if (!Number.isFinite(userId)) {
|
||||||
|
ElMessage.error('请输入有效的用户ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
riskLoading.value = true
|
||||||
|
try {
|
||||||
|
riskResult.value = await getUserRisk(userId)
|
||||||
|
riskResultKind.value = 'user'
|
||||||
|
} catch {
|
||||||
|
riskResult.value = null
|
||||||
|
riskResultKind.value = ''
|
||||||
|
} finally {
|
||||||
|
riskLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBanFromRisk() {
|
||||||
|
if (!riskResult.value || !riskResultKind.value) return
|
||||||
|
if (riskResultKind.value === 'ip') {
|
||||||
|
openBanDialog('ip', { ip: riskResult.value?.ip, reason: '风险查询手动封禁' })
|
||||||
|
} else {
|
||||||
|
openBanDialog('user', { user_id: riskResult.value?.user_id, reason: '风险查询手动封禁' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unbanFromRisk() {
|
||||||
|
if (!riskResult.value || !riskResultKind.value) return
|
||||||
|
if (riskResultKind.value === 'ip') {
|
||||||
|
await onUnbanIp(riskResult.value?.ip)
|
||||||
|
await queryIpRisk()
|
||||||
|
} else {
|
||||||
|
await onUnbanUser(riskResult.value?.user_id)
|
||||||
|
await queryUserRisk()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanupLoading = ref(false)
|
||||||
|
|
||||||
|
async function onCleanup() {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'确定清理过期封禁记录,并衰减风险分吗?\n\n该操作不会影响仍在有效期内的封禁。',
|
||||||
|
'清理过期记录',
|
||||||
|
{ confirmButtonText: '清理', cancelButtonText: '取消', type: 'warning' },
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupLoading.value = true
|
||||||
|
try {
|
||||||
|
await cleanup()
|
||||||
|
ElMessage.success('清理完成')
|
||||||
|
await refreshAll()
|
||||||
|
} catch {
|
||||||
|
// handled by interceptor
|
||||||
|
} finally {
|
||||||
|
cleanupLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await refreshAll()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-stack">
|
||||||
|
<div class="app-page-title">
|
||||||
|
<h2>安全防护</h2>
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-button @click="refreshAll">刷新</el-button>
|
||||||
|
<el-button type="warning" plain :loading="cleanupLoading" @click="onCleanup">清理过期记录</el-button>
|
||||||
|
<el-button type="primary" @click="openBanDialog()">手动封禁</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-row :gutter="12" class="stats-row">
|
||||||
|
<el-col v-for="it in dashboardCards" :key="it.key" :xs="24" :sm="8" :md="8" :lg="8" :xl="8">
|
||||||
|
<el-card shadow="never" class="stat-card" :body-style="{ padding: '14px' }">
|
||||||
|
<div class="stat-value">
|
||||||
|
<el-skeleton v-if="dashboardLoading" :rows="1" animated />
|
||||||
|
<template v-else>{{ it.value }}</template>
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">{{ it.label }}</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card">
|
||||||
|
<el-tabs v-model="activeTab">
|
||||||
|
<el-tab-pane label="威胁事件" name="threats">
|
||||||
|
<div class="filters">
|
||||||
|
<el-select
|
||||||
|
v-model="threatTypeFilter"
|
||||||
|
placeholder="类型"
|
||||||
|
style="width: 220px"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
>
|
||||||
|
<el-option label="全部" value="" />
|
||||||
|
<el-option v-for="t in threatTypeOptions" :key="t.value" :label="t.label" :value="t.value" />
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<el-select v-model="threatSeverityFilter" placeholder="严重程度" style="width: 200px" clearable>
|
||||||
|
<el-option label="全部" value="" />
|
||||||
|
<el-option label="高风险(>=80)" value="high" />
|
||||||
|
<el-option label="中风险(50-79)" value="medium" />
|
||||||
|
<el-option label="低风险(<50)" value="low" />
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<el-button type="primary" @click="onThreatFilter">筛选</el-button>
|
||||||
|
<el-button @click="onThreatReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<el-table :data="threatItems" v-loading="threatsLoading" style="width: 100%">
|
||||||
|
<el-table-column prop="created_at" label="时间" width="180" />
|
||||||
|
<el-table-column label="类型" width="170">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag effect="light" type="info">{{ row.threat_type || 'unknown' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="严重程度" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="scoreMeta(row.score).type" effect="light">
|
||||||
|
{{ scoreMeta(row.score).label }} ({{ row.score ?? 0 }})
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="IP" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link v-if="row.ip" type="primary" :underline="false" @click="jumpToIpRisk(row.ip)">
|
||||||
|
{{ row.ip }}
|
||||||
|
</el-link>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="用户" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link
|
||||||
|
v-if="row.user_id !== null && row.user_id !== undefined"
|
||||||
|
type="primary"
|
||||||
|
:underline="false"
|
||||||
|
@click="jumpToUserRisk(row.user_id)"
|
||||||
|
>
|
||||||
|
{{ row.user_id }}
|
||||||
|
</el-link>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作路径" min-width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tooltip :content="pathText(row)" placement="top" :show-after="300">
|
||||||
|
<span class="mono ellipsis">{{ pathText(row) }}</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="Payload预览" min-width="240">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tooltip :content="payloadTooltip(row)" placement="top" :show-after="300">
|
||||||
|
<span class="ellipsis">{{ row.value_preview || '-' }}</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="threatPage"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:total="threatTotal"
|
||||||
|
layout="prev, pager, next, jumper, ->, total"
|
||||||
|
@current-change="loadThreats"
|
||||||
|
/>
|
||||||
|
<div class="page-hint app-muted">第 {{ threatPage }} / {{ threatTotalPages }} 页</div>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="封禁管理" name="bans">
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-button @click="loadBans">刷新封禁列表</el-button>
|
||||||
|
<el-button type="primary" @click="openBanDialog()">手动封禁</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-tabs v-model="banTab" class="inner-tabs">
|
||||||
|
<el-tab-pane label="IP黑名单" name="ips">
|
||||||
|
<div class="table-wrap">
|
||||||
|
<el-table :data="bannedIps" v-loading="bansLoading" style="width: 100%">
|
||||||
|
<el-table-column label="IP" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link type="primary" :underline="false" @click="jumpToIpRisk(row.ip)">
|
||||||
|
{{ row.ip || '-' }}
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="reason" label="原因" min-width="260" />
|
||||||
|
<el-table-column label="过期时间" width="190">
|
||||||
|
<template #default="{ row }">{{ formatExpires(row.expires_at) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" type="danger" plain @click="onUnbanIp(row.ip)">解除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="用户黑名单" name="users">
|
||||||
|
<div class="table-wrap">
|
||||||
|
<el-table :data="bannedUsers" v-loading="bansLoading" style="width: 100%">
|
||||||
|
<el-table-column label="用户ID" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link type="primary" :underline="false" @click="jumpToUserRisk(row.user_id)">
|
||||||
|
{{ row.user_id ?? '-' }}
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="reason" label="原因" min-width="260" />
|
||||||
|
<el-table-column label="过期时间" width="190">
|
||||||
|
<template #default="{ row }">{{ formatExpires(row.expires_at) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" type="danger" plain @click="onUnbanUser(row.user_id)">解除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="风险查询" name="risk">
|
||||||
|
<el-tabs v-model="riskTab" class="inner-tabs">
|
||||||
|
<el-tab-pane label="IP查询" name="ip">
|
||||||
|
<div class="filters">
|
||||||
|
<el-input v-model="riskIpInput" placeholder="输入IP,如 1.2.3.4" style="width: 260px" clearable />
|
||||||
|
<el-button type="primary" :loading="riskLoading" @click="queryIpRisk">查询</el-button>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="用户查询" name="user">
|
||||||
|
<div class="filters">
|
||||||
|
<el-input v-model="riskUserIdInput" placeholder="输入用户ID,如 123" style="width: 260px" clearable />
|
||||||
|
<el-button type="primary" :loading="riskLoading" @click="queryUserRisk">查询</el-button>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<el-card v-if="riskResult" shadow="never" :body-style="{ padding: '16px' }" class="sub-card">
|
||||||
|
<div class="risk-head">
|
||||||
|
<div class="risk-title">
|
||||||
|
<strong v-if="riskResultKind === 'ip'">IP: {{ riskResult.ip }}</strong>
|
||||||
|
<strong v-else>用户ID: {{ riskResult.user_id }}</strong>
|
||||||
|
<span class="app-muted">风险分</span>
|
||||||
|
<el-tag :type="scoreMeta(riskResult.risk_score).type" effect="light">
|
||||||
|
{{ riskResult.risk_score ?? 0 }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-if="riskResult.is_banned" type="danger" effect="light">已封禁</el-tag>
|
||||||
|
<el-tag v-else type="success" effect="light">未封禁</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-button v-if="!riskResult.is_banned" type="primary" plain @click="openBanFromRisk">封禁</el-button>
|
||||||
|
<el-button v-else type="danger" plain @click="unbanFromRisk">解除封禁</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<el-table :data="riskResult.threat_history || []" v-loading="riskLoading" style="width: 100%">
|
||||||
|
<el-table-column prop="created_at" label="时间" width="180" />
|
||||||
|
<el-table-column label="类型" width="170">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag effect="light" type="info">{{ row.threat_type || 'unknown' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="严重程度" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="scoreMeta(row.score).type" effect="light">
|
||||||
|
{{ scoreMeta(row.score).label }} ({{ row.score ?? 0 }})
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作路径" min-width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tooltip :content="pathText(row)" placement="top" :show-after="300">
|
||||||
|
<span class="mono ellipsis">{{ pathText(row) }}</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="Payload预览" min-width="240">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tooltip :content="payloadTooltip(row)" placement="top" :show-after="300">
|
||||||
|
<span class="ellipsis">{{ row.value_preview || '-' }}</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog v-model="banDialogOpen" title="手动封禁" width="min(520px, 92vw)" @closed="resetBanForm">
|
||||||
|
<el-form label-width="120px">
|
||||||
|
<el-form-item label="类型">
|
||||||
|
<el-radio-group v-model="banForm.kind">
|
||||||
|
<el-radio-button label="ip">IP</el-radio-button>
|
||||||
|
<el-radio-button label="user">用户</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="banForm.kind === 'ip'" label="IP">
|
||||||
|
<el-input v-model="banForm.ip" placeholder="例如 1.2.3.4" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-else label="用户ID">
|
||||||
|
<el-input v-model="banForm.user_id" placeholder="例如 123" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="原因">
|
||||||
|
<el-input v-model="banForm.reason" type="textarea" :rows="3" placeholder="请输入封禁原因" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="永久封禁">
|
||||||
|
<el-switch v-model="banForm.permanent" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="!banForm.permanent" label="持续(小时)">
|
||||||
|
<el-input-number v-model="banForm.duration_hours" :min="1" :max="8760" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<el-button @click="banDialogOpen = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="banSubmitting" @click="submitBan">确认封禁</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-row {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: var(--app-radius);
|
||||||
|
border: 1px solid var(--app-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-card {
|
||||||
|
margin-top: 12px;
|
||||||
|
border-radius: var(--app-radius);
|
||||||
|
border: 1px solid var(--app-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
border-radius: var(--app-radius);
|
||||||
|
border: 1px solid var(--app-border);
|
||||||
|
box-shadow: var(--app-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--app-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-tabs {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,6 +8,7 @@ const FeedbacksPage = () => import('../pages/FeedbacksPage.vue')
|
|||||||
const LogsPage = () => import('../pages/LogsPage.vue')
|
const LogsPage = () => import('../pages/LogsPage.vue')
|
||||||
const AnnouncementsPage = () => import('../pages/AnnouncementsPage.vue')
|
const AnnouncementsPage = () => import('../pages/AnnouncementsPage.vue')
|
||||||
const EmailPage = () => import('../pages/EmailPage.vue')
|
const EmailPage = () => import('../pages/EmailPage.vue')
|
||||||
|
const SecurityPage = () => import('../pages/SecurityPage.vue')
|
||||||
const SystemPage = () => import('../pages/SystemPage.vue')
|
const SystemPage = () => import('../pages/SystemPage.vue')
|
||||||
const SettingsPage = () => import('../pages/SettingsPage.vue')
|
const SettingsPage = () => import('../pages/SettingsPage.vue')
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ const routes = [
|
|||||||
{ path: '/logs', name: 'logs', component: LogsPage },
|
{ path: '/logs', name: 'logs', component: LogsPage },
|
||||||
{ path: '/announcements', name: 'announcements', component: AnnouncementsPage },
|
{ path: '/announcements', name: 'announcements', component: AnnouncementsPage },
|
||||||
{ path: '/email', name: 'email', component: EmailPage },
|
{ path: '/email', name: 'email', component: EmailPage },
|
||||||
|
{ path: '/security', name: 'security', component: SecurityPage },
|
||||||
{ path: '/system', name: 'system', component: SystemPage },
|
{ path: '/system', name: 'system', component: SystemPage },
|
||||||
{ path: '/settings', name: 'settings', component: SettingsPage },
|
{ path: '/settings', name: 'settings', component: SettingsPage },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
{
|
{
|
||||||
"_email-BsKBHU5S.js": {
|
"_email-DoKk83fr.js": {
|
||||||
"file": "assets/email-BsKBHU5S.js",
|
"file": "assets/email-DoKk83fr.js",
|
||||||
"name": "email",
|
"name": "email",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"_tasks-DpslJtm_.js": {
|
"_tasks-Bgkd54ac.js": {
|
||||||
"file": "assets/tasks-DpslJtm_.js",
|
"file": "assets/tasks-Bgkd54ac.js",
|
||||||
"name": "tasks",
|
"name": "tasks",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"_update-DcFD-YxU.js": {
|
"_update-BVJ0Pp6O.js": {
|
||||||
"file": "assets/update-DcFD-YxU.js",
|
"file": "assets/update-BVJ0Pp6O.js",
|
||||||
"name": "update",
|
"name": "update",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"_users-CC9BckjT.js": {
|
"_users-Bw5HW1mw.js": {
|
||||||
"file": "assets/users-CC9BckjT.js",
|
"file": "assets/users-Bw5HW1mw.js",
|
||||||
"name": "users",
|
"name": "users",
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html"
|
"index.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"index.html": {
|
"index.html": {
|
||||||
"file": "assets/index-CdjS44Uj.js",
|
"file": "assets/index-CDhtYQo-.js",
|
||||||
"name": "index",
|
"name": "index",
|
||||||
"src": "index.html",
|
"src": "index.html",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
@@ -39,15 +39,16 @@
|
|||||||
"src/pages/LogsPage.vue",
|
"src/pages/LogsPage.vue",
|
||||||
"src/pages/AnnouncementsPage.vue",
|
"src/pages/AnnouncementsPage.vue",
|
||||||
"src/pages/EmailPage.vue",
|
"src/pages/EmailPage.vue",
|
||||||
|
"src/pages/SecurityPage.vue",
|
||||||
"src/pages/SystemPage.vue",
|
"src/pages/SystemPage.vue",
|
||||||
"src/pages/SettingsPage.vue"
|
"src/pages/SettingsPage.vue"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/index-EWm4DZW8.css"
|
"assets/index-DiIt7W4Z.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/AnnouncementsPage.vue": {
|
"src/pages/AnnouncementsPage.vue": {
|
||||||
"file": "assets/AnnouncementsPage-Djmq3Wb7.js",
|
"file": "assets/AnnouncementsPage-BSLa6sED.js",
|
||||||
"name": "AnnouncementsPage",
|
"name": "AnnouncementsPage",
|
||||||
"src": "src/pages/AnnouncementsPage.vue",
|
"src": "src/pages/AnnouncementsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -59,12 +60,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/EmailPage.vue": {
|
"src/pages/EmailPage.vue": {
|
||||||
"file": "assets/EmailPage-q6nJlTue.js",
|
"file": "assets/EmailPage-BHdschU6.js",
|
||||||
"name": "EmailPage",
|
"name": "EmailPage",
|
||||||
"src": "src/pages/EmailPage.vue",
|
"src": "src/pages/EmailPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_email-BsKBHU5S.js",
|
"_email-DoKk83fr.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
@@ -72,7 +73,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/FeedbacksPage.vue": {
|
"src/pages/FeedbacksPage.vue": {
|
||||||
"file": "assets/FeedbacksPage-Drw6uvSR.js",
|
"file": "assets/FeedbacksPage-CLu2KtuG.js",
|
||||||
"name": "FeedbacksPage",
|
"name": "FeedbacksPage",
|
||||||
"src": "src/pages/FeedbacksPage.vue",
|
"src": "src/pages/FeedbacksPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -84,13 +85,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/LogsPage.vue": {
|
"src/pages/LogsPage.vue": {
|
||||||
"file": "assets/LogsPage-DQd9IS3I.js",
|
"file": "assets/LogsPage-CbeqOQSe.js",
|
||||||
"name": "LogsPage",
|
"name": "LogsPage",
|
||||||
"src": "src/pages/LogsPage.vue",
|
"src": "src/pages/LogsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_users-CC9BckjT.js",
|
"_users-Bw5HW1mw.js",
|
||||||
"_tasks-DpslJtm_.js",
|
"_tasks-Bgkd54ac.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
@@ -98,22 +99,34 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/ReportPage.vue": {
|
"src/pages/ReportPage.vue": {
|
||||||
"file": "assets/ReportPage-Dnk3wsl3.js",
|
"file": "assets/ReportPage-DVC8Kawd.js",
|
||||||
"name": "ReportPage",
|
"name": "ReportPage",
|
||||||
"src": "src/pages/ReportPage.vue",
|
"src": "src/pages/ReportPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"index.html",
|
"index.html",
|
||||||
"_email-BsKBHU5S.js",
|
"_email-DoKk83fr.js",
|
||||||
"_tasks-DpslJtm_.js",
|
"_tasks-Bgkd54ac.js",
|
||||||
"_update-DcFD-YxU.js"
|
"_update-BVJ0Pp6O.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/ReportPage-TpqQWWvU.css"
|
"assets/ReportPage-TpqQWWvU.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"src/pages/SecurityPage.vue": {
|
||||||
|
"file": "assets/SecurityPage-CuXCrXIZ.js",
|
||||||
|
"name": "SecurityPage",
|
||||||
|
"src": "src/pages/SecurityPage.vue",
|
||||||
|
"isDynamicEntry": true,
|
||||||
|
"imports": [
|
||||||
|
"index.html"
|
||||||
|
],
|
||||||
|
"css": [
|
||||||
|
"assets/SecurityPage-CH3QeiaV.css"
|
||||||
|
]
|
||||||
|
},
|
||||||
"src/pages/SettingsPage.vue": {
|
"src/pages/SettingsPage.vue": {
|
||||||
"file": "assets/SettingsPage-YOW1Apwk.js",
|
"file": "assets/SettingsPage-cKC6DywW.js",
|
||||||
"name": "SettingsPage",
|
"name": "SettingsPage",
|
||||||
"src": "src/pages/SettingsPage.vue",
|
"src": "src/pages/SettingsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -125,12 +138,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/SystemPage.vue": {
|
"src/pages/SystemPage.vue": {
|
||||||
"file": "assets/SystemPage-DCcH_SAQ.js",
|
"file": "assets/SystemPage-2XepJLfC.js",
|
||||||
"name": "SystemPage",
|
"name": "SystemPage",
|
||||||
"src": "src/pages/SystemPage.vue",
|
"src": "src/pages/SystemPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_update-DcFD-YxU.js",
|
"_update-BVJ0Pp6O.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
@@ -138,12 +151,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/UsersPage.vue": {
|
"src/pages/UsersPage.vue": {
|
||||||
"file": "assets/UsersPage-DhTO_5zp.js",
|
"file": "assets/UsersPage-CGNuB954.js",
|
||||||
"name": "UsersPage",
|
"name": "UsersPage",
|
||||||
"src": "src/pages/UsersPage.vue",
|
"src": "src/pages/UsersPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_users-CC9BckjT.js",
|
"_users-Bw5HW1mw.js",
|
||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
static/admin/assets/SecurityPage-CH3QeiaV.css
Normal file
1
static/admin/assets/SecurityPage-CH3QeiaV.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.page-stack[data-v-ea9240bd]{display:flex;flex-direction:column;gap:12px}.toolbar[data-v-ea9240bd]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.stats-row[data-v-ea9240bd]{margin-bottom:2px}.card[data-v-ea9240bd]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.sub-card[data-v-ea9240bd]{margin-top:12px;border-radius:var(--app-radius);border:1px solid var(--app-border)}.stat-card[data-v-ea9240bd]{border-radius:var(--app-radius);border:1px solid var(--app-border);box-shadow:var(--app-shadow)}.stat-value[data-v-ea9240bd]{font-size:22px;font-weight:800;line-height:1.1}.stat-label[data-v-ea9240bd]{margin-top:6px;font-size:12px;color:var(--app-muted)}.filters[data-v-ea9240bd]{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:12px}.table-wrap[data-v-ea9240bd]{overflow-x:auto}.ellipsis[data-v-ea9240bd]{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mono[data-v-ea9240bd]{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.pagination[data-v-ea9240bd]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;flex-wrap:wrap}.page-hint[data-v-ea9240bd]{font-size:12px}.inner-tabs[data-v-ea9240bd]{margin-top:6px}.risk-head[data-v-ea9240bd]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap}.risk-title[data-v-ea9240bd]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.dialog-actions[data-v-ea9240bd]{display:flex;align-items:center;gap:10px}.spacer[data-v-ea9240bd]{flex:1}
|
||||||
3
static/admin/assets/SecurityPage-CuXCrXIZ.js
Normal file
3
static/admin/assets/SecurityPage-CuXCrXIZ.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{S as m,_ as T,r as p,e as u,f as h,g as k,h as r,j as a,w as s,p as x,L as i,K as b}from"./index-CdjS44Uj.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function P(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function U(){const{data:o}=await m.post("/logout");return o}const E={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),d=p(""),n=p(!1);async function f(){try{await U()}catch{}finally{window.location.href="/yuyx"}}async function V(){const l=t.value.trim();if(!l){i.error("请输入新用户名");return}try{await b.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),i.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){const l=d.value;if(!l){i.error("请输入新密码");return}if(l.length<6){i.error("密码至少6个字符");return}try{await b.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await P(l),i.success("密码修改成功,请重新登录"),d.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(l,e)=>{const w=u("el-input"),v=u("el-form-item"),y=u("el-form"),_=u("el-button"),g=u("el-card");return k(),h("div",E,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新用户名"},{default:s(()=>[a(w,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=c=>t.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新密码"},{default:s(()=>[a(w,{modelValue:d.value,"onUpdate:modelValue":e[1]||(e[1]=c=>d.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:B},{default:s(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码(至少8位且包含字母与数字)。",-1))]),_:1})])}}},A=T(N,[["__scopeId","data-v-2f4b840f"]]);export{A as default};
|
import{S as m,_ as T,r as p,e as u,f as h,g as k,h as r,j as a,w as s,p as x,L as i,K as b}from"./index-CDhtYQo-.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function P(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function U(){const{data:o}=await m.post("/logout");return o}const E={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),d=p(""),n=p(!1);async function f(){try{await U()}catch{}finally{window.location.href="/yuyx"}}async function V(){const l=t.value.trim();if(!l){i.error("请输入新用户名");return}try{await b.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),i.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){const l=d.value;if(!l){i.error("请输入新密码");return}if(l.length<6){i.error("密码至少6个字符");return}try{await b.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await P(l),i.success("密码修改成功,请重新登录"),d.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(l,e)=>{const w=u("el-input"),v=u("el-form-item"),y=u("el-form"),_=u("el-button"),g=u("el-card");return k(),h("div",E,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新用户名"},{default:s(()=>[a(w,{modelValue:t.value,"onUpdate:modelValue":e[0]||(e[0]=c=>t.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:V},{default:s(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(g,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:s(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(y,{"label-width":"120px"},{default:s(()=>[a(v,{label:"新密码"},{default:s(()=>[a(w,{modelValue:d.value,"onUpdate:modelValue":e[1]||(e[1]=c=>d.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(_,{type:"primary",loading:n.value,onClick:B},{default:s(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码(至少8位且包含字母与数字)。",-1))]),_:1})])}}},A=T(N,[["__scopeId","data-v-2f4b840f"]]);export{A as default};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import{f as Ue,u as Q,e as Pe,a as Ce,b as he,c as Be,r as Te,d as Ie}from"./update-DcFD-YxU.js";import{S as X,_ as Ne,r,c as Se,o as Ae,U as Le,e as m,I as $e,J as se,g as u,f as C,h as i,j as l,w as t,p as n,m as v,n as x,F as je,v as Ee,q as y,K as L,L as b}from"./index-CdjS44Uj.js";async function Re(){const{data:h}=await X.get("/proxy/config");return h}async function De(h){const{data:w}=await X.post("/proxy/config",h);return w}async function Fe(h){const{data:w}=await X.post("/proxy/test",h);return w}const He={class:"page-stack"},Me={class:"app-page-title"},qe={class:"row-actions"},ze={class:"row-actions"},Oe={class:"row-actions",style:{"align-items":"center"}},We={class:"row-actions"},Ge={key:0},Je={key:1},Ke={key:2},Qe={key:3,class:"help"},Xe={key:4,class:"help"},Ye={__name:"SystemPage",setup(h){const w=r(!1),$=r(2),j=r(1),E=r(3),g=r(!1),R=r("02:00"),B=r("应读"),k=r(["1","2","3","4","5","6","7"]),T=r(!1),V=r(""),D=r(3),F=r(!1),H=r(10),M=r(7),G=r(!1),U=r(!1),_=r(null),q=r(""),d=r(null),z=r(""),J=r(!1),O=r(!1);let I=null;const re=[{label:"周一",value:"1"},{label:"周二",value:"2"},{label:"周三",value:"3"},{label:"周四",value:"4"},{label:"周五",value:"5"},{label:"周六",value:"6"},{label:"周日",value:"7"}],de={1:"周一",2:"周二",3:"周三",4:"周四",5:"周五",6:"周六",7:"周日"},ie=Se(()=>(k.value||[]).map(a=>de[Number(a)]||a).join("、"));function me(a){return String(a)==="注册前未读"?"注册前未读":"应读"}function N(a){const e=String(a||"").trim();return e?e.length>12?`${e.slice(0,12)}…`:e:"-"}async function S({withLog:a=!0}={}){G.value=!0,q.value="";try{const[e,s]=await Promise.all([Ce(),he()]);e?.ok?_.value=e.data||null:(_.value=null,q.value=e?.error||"未发现更新状态(Update-Agent 可能未运行)"),d.value=s?.ok?s.data:null;const f=d.value?.job_id;if(a&&f){const p=await Be({job_id:f,max_bytes:2e5});z.value=p?.log||"",J.value=!!p?.truncated}else z.value="",J.value=!1}catch{}finally{G.value=!1}}function Y(){I||(I=setInterval(async()=>{d.value?.status==="running"&&await S()},5e3))}function pe(){I&&(clearInterval(I),I=null)}async function Z(){w.value=!0;try{const[a,e]=await Promise.all([Ue(),Re()]);$.value=a.max_concurrent_global??2,j.value=a.max_concurrent_per_account??1,E.value=a.max_screenshot_concurrent??3,g.value=(a.schedule_enabled??0)===1,R.value=a.schedule_time||"02:00",B.value=me(a.schedule_browse_type);const s=String(a.schedule_weekdays||"1,2,3,4,5,6,7").split(",").map(f=>f.trim()).filter(Boolean);k.value=s.length?s:["1","2","3","4","5","6","7"],F.value=(a.auto_approve_enabled??0)===1,H.value=a.auto_approve_hourly_limit??10,M.value=a.auto_approve_vip_days??7,T.value=(e.proxy_enabled??0)===1,V.value=e.proxy_api_url||"",D.value=e.proxy_expire_minutes??3,await S({withLog:!1}),Y()}catch{}finally{w.value=!1}}async function ce(){const a={max_concurrent_global:Number($.value),max_concurrent_per_account:Number(j.value),max_screenshot_concurrent:Number(E.value)};try{await L.confirm(`确定更新并发配置吗?
|
import{f as Ue,u as Q,e as Pe,a as Ce,b as he,c as Be,r as Te,d as Ie}from"./update-BVJ0Pp6O.js";import{S as X,_ as Ne,r,c as Se,o as Ae,U as Le,e as m,I as $e,J as se,g as u,f as C,h as i,j as l,w as t,p as n,m as v,n as x,F as je,v as Ee,q as y,K as L,L as b}from"./index-CDhtYQo-.js";async function Re(){const{data:h}=await X.get("/proxy/config");return h}async function De(h){const{data:w}=await X.post("/proxy/config",h);return w}async function Fe(h){const{data:w}=await X.post("/proxy/test",h);return w}const He={class:"page-stack"},Me={class:"app-page-title"},qe={class:"row-actions"},ze={class:"row-actions"},Oe={class:"row-actions",style:{"align-items":"center"}},We={class:"row-actions"},Ge={key:0},Je={key:1},Ke={key:2},Qe={key:3,class:"help"},Xe={key:4,class:"help"},Ye={__name:"SystemPage",setup(h){const w=r(!1),$=r(2),j=r(1),E=r(3),g=r(!1),R=r("02:00"),B=r("应读"),k=r(["1","2","3","4","5","6","7"]),T=r(!1),V=r(""),D=r(3),F=r(!1),H=r(10),M=r(7),G=r(!1),U=r(!1),_=r(null),q=r(""),d=r(null),z=r(""),J=r(!1),O=r(!1);let I=null;const re=[{label:"周一",value:"1"},{label:"周二",value:"2"},{label:"周三",value:"3"},{label:"周四",value:"4"},{label:"周五",value:"5"},{label:"周六",value:"6"},{label:"周日",value:"7"}],de={1:"周一",2:"周二",3:"周三",4:"周四",5:"周五",6:"周六",7:"周日"},ie=Se(()=>(k.value||[]).map(a=>de[Number(a)]||a).join("、"));function me(a){return String(a)==="注册前未读"?"注册前未读":"应读"}function N(a){const e=String(a||"").trim();return e?e.length>12?`${e.slice(0,12)}…`:e:"-"}async function S({withLog:a=!0}={}){G.value=!0,q.value="";try{const[e,s]=await Promise.all([Ce(),he()]);e?.ok?_.value=e.data||null:(_.value=null,q.value=e?.error||"未发现更新状态(Update-Agent 可能未运行)"),d.value=s?.ok?s.data:null;const f=d.value?.job_id;if(a&&f){const p=await Be({job_id:f,max_bytes:2e5});z.value=p?.log||"",J.value=!!p?.truncated}else z.value="",J.value=!1}catch{}finally{G.value=!1}}function Y(){I||(I=setInterval(async()=>{d.value?.status==="running"&&await S()},5e3))}function pe(){I&&(clearInterval(I),I=null)}async function Z(){w.value=!0;try{const[a,e]=await Promise.all([Ue(),Re()]);$.value=a.max_concurrent_global??2,j.value=a.max_concurrent_per_account??1,E.value=a.max_screenshot_concurrent??3,g.value=(a.schedule_enabled??0)===1,R.value=a.schedule_time||"02:00",B.value=me(a.schedule_browse_type);const s=String(a.schedule_weekdays||"1,2,3,4,5,6,7").split(",").map(f=>f.trim()).filter(Boolean);k.value=s.length?s:["1","2","3","4","5","6","7"],F.value=(a.auto_approve_enabled??0)===1,H.value=a.auto_approve_hourly_limit??10,M.value=a.auto_approve_vip_days??7,T.value=(e.proxy_enabled??0)===1,V.value=e.proxy_api_url||"",D.value=e.proxy_expire_minutes??3,await S({withLog:!1}),Y()}catch{}finally{w.value=!1}}async function ce(){const a={max_concurrent_global:Number($.value),max_concurrent_per_account:Number(j.value),max_screenshot_concurrent:Number(E.value)};try{await L.confirm(`确定更新并发配置吗?
|
||||||
|
|
||||||
全局并发数: ${a.max_concurrent_global}
|
全局并发数: ${a.max_concurrent_global}
|
||||||
单账号并发数: ${a.max_concurrent_per_account}
|
单账号并发数: ${a.max_concurrent_per_account}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{S as n}from"./index-CdjS44Uj.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{o as a,i as b,l as c,c as f,e as u};
|
import{S as n}from"./index-CDhtYQo-.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{o as a,i as b,l as c,c as f,e as u};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{S as a}from"./index-CdjS44Uj.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{r as a,c as b,e as c,i as d,f as e,o as f};
|
import{S as a}from"./index-CDhtYQo-.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function o(){const{data:t}=await a.get("/task/stats");return t}async function r(){const{data:t}=await a.get("/task/running");return t}async function i(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function f(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{r as a,c as b,e as c,i as d,f as e,o as f};
|
||||||
@@ -1 +1 @@
|
|||||||
import{S as a}from"./index-CdjS44Uj.js";async function s(){const{data:t}=await a.get("/system/config");return t}async function c(t){const{data:e}=await a.post("/system/config",t);return e}async function u(){const{data:t}=await a.post("/schedule/execute",{});return t}async function o(){const{data:t}=await a.get("/update/status");return t}async function r(){const{data:t}=await a.get("/update/result");return t}async function d(t={}){const{data:e}=await a.get("/update/log",{params:t});return e}async function i(){const{data:t}=await a.post("/update/check",{});return t}async function f(t={}){const{data:e}=await a.post("/update/run",t);return e}export{o as a,r as b,d as c,f as d,u as e,s as f,i as r,c as u};
|
import{S as a}from"./index-CDhtYQo-.js";async function s(){const{data:t}=await a.get("/system/config");return t}async function c(t){const{data:e}=await a.post("/system/config",t);return e}async function u(){const{data:t}=await a.post("/schedule/execute",{});return t}async function o(){const{data:t}=await a.get("/update/status");return t}async function r(){const{data:t}=await a.get("/update/result");return t}async function d(t={}){const{data:e}=await a.get("/update/log",{params:t});return e}async function i(){const{data:t}=await a.post("/update/check",{});return t}async function f(t={}){const{data:e}=await a.post("/update/run",t);return e}export{o as a,r as b,d as c,f as d,u as e,s as f,i as r,c as u};
|
||||||
@@ -1 +1 @@
|
|||||||
import{S as t}from"./index-CdjS44Uj.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};
|
import{S as t}from"./index-CDhtYQo-.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>后台管理 - 知识管理平台</title>
|
<title>后台管理 - 知识管理平台</title>
|
||||||
<script type="module" crossorigin src="./assets/index-CdjS44Uj.js"></script>
|
<script type="module" crossorigin src="./assets/index-CDhtYQo-.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-EWm4DZW8.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-DiIt7W4Z.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user