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:
2025-12-27 01:56:22 +08:00
parent 759d99e8af
commit 4ba933b001
22 changed files with 938 additions and 54 deletions

View 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
}

View File

@@ -7,6 +7,7 @@ import {
ChatLineSquare,
Document,
List,
Lock,
Message,
Setting,
Tools,
@@ -104,6 +105,7 @@ const menuItems = [
{ path: '/logs', label: '任务日志', icon: List },
{ path: '/announcements', label: '公告', icon: Bell },
{ path: '/email', label: '邮件', icon: Message },
{ path: '/security', label: '安全防护', icon: Lock },
{ path: '/system', label: '系统配置', icon: Tools },
{ path: '/settings', label: '设置', icon: Setting },
]

View 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>

View File

@@ -8,6 +8,7 @@ const FeedbacksPage = () => import('../pages/FeedbacksPage.vue')
const LogsPage = () => import('../pages/LogsPage.vue')
const AnnouncementsPage = () => import('../pages/AnnouncementsPage.vue')
const EmailPage = () => import('../pages/EmailPage.vue')
const SecurityPage = () => import('../pages/SecurityPage.vue')
const SystemPage = () => import('../pages/SystemPage.vue')
const SettingsPage = () => import('../pages/SettingsPage.vue')
@@ -25,6 +26,7 @@ const routes = [
{ path: '/logs', name: 'logs', component: LogsPage },
{ path: '/announcements', name: 'announcements', component: AnnouncementsPage },
{ path: '/email', name: 'email', component: EmailPage },
{ path: '/security', name: 'security', component: SecurityPage },
{ path: '/system', name: 'system', component: SystemPage },
{ path: '/settings', name: 'settings', component: SettingsPage },
],

View File

@@ -1,34 +1,34 @@
{
"_email-BsKBHU5S.js": {
"file": "assets/email-BsKBHU5S.js",
"_email-DoKk83fr.js": {
"file": "assets/email-DoKk83fr.js",
"name": "email",
"imports": [
"index.html"
]
},
"_tasks-DpslJtm_.js": {
"file": "assets/tasks-DpslJtm_.js",
"_tasks-Bgkd54ac.js": {
"file": "assets/tasks-Bgkd54ac.js",
"name": "tasks",
"imports": [
"index.html"
]
},
"_update-DcFD-YxU.js": {
"file": "assets/update-DcFD-YxU.js",
"_update-BVJ0Pp6O.js": {
"file": "assets/update-BVJ0Pp6O.js",
"name": "update",
"imports": [
"index.html"
]
},
"_users-CC9BckjT.js": {
"file": "assets/users-CC9BckjT.js",
"_users-Bw5HW1mw.js": {
"file": "assets/users-Bw5HW1mw.js",
"name": "users",
"imports": [
"index.html"
]
},
"index.html": {
"file": "assets/index-CdjS44Uj.js",
"file": "assets/index-CDhtYQo-.js",
"name": "index",
"src": "index.html",
"isEntry": true,
@@ -39,15 +39,16 @@
"src/pages/LogsPage.vue",
"src/pages/AnnouncementsPage.vue",
"src/pages/EmailPage.vue",
"src/pages/SecurityPage.vue",
"src/pages/SystemPage.vue",
"src/pages/SettingsPage.vue"
],
"css": [
"assets/index-EWm4DZW8.css"
"assets/index-DiIt7W4Z.css"
]
},
"src/pages/AnnouncementsPage.vue": {
"file": "assets/AnnouncementsPage-Djmq3Wb7.js",
"file": "assets/AnnouncementsPage-BSLa6sED.js",
"name": "AnnouncementsPage",
"src": "src/pages/AnnouncementsPage.vue",
"isDynamicEntry": true,
@@ -59,12 +60,12 @@
]
},
"src/pages/EmailPage.vue": {
"file": "assets/EmailPage-q6nJlTue.js",
"file": "assets/EmailPage-BHdschU6.js",
"name": "EmailPage",
"src": "src/pages/EmailPage.vue",
"isDynamicEntry": true,
"imports": [
"_email-BsKBHU5S.js",
"_email-DoKk83fr.js",
"index.html"
],
"css": [
@@ -72,7 +73,7 @@
]
},
"src/pages/FeedbacksPage.vue": {
"file": "assets/FeedbacksPage-Drw6uvSR.js",
"file": "assets/FeedbacksPage-CLu2KtuG.js",
"name": "FeedbacksPage",
"src": "src/pages/FeedbacksPage.vue",
"isDynamicEntry": true,
@@ -84,13 +85,13 @@
]
},
"src/pages/LogsPage.vue": {
"file": "assets/LogsPage-DQd9IS3I.js",
"file": "assets/LogsPage-CbeqOQSe.js",
"name": "LogsPage",
"src": "src/pages/LogsPage.vue",
"isDynamicEntry": true,
"imports": [
"_users-CC9BckjT.js",
"_tasks-DpslJtm_.js",
"_users-Bw5HW1mw.js",
"_tasks-Bgkd54ac.js",
"index.html"
],
"css": [
@@ -98,22 +99,34 @@
]
},
"src/pages/ReportPage.vue": {
"file": "assets/ReportPage-Dnk3wsl3.js",
"file": "assets/ReportPage-DVC8Kawd.js",
"name": "ReportPage",
"src": "src/pages/ReportPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html",
"_email-BsKBHU5S.js",
"_tasks-DpslJtm_.js",
"_update-DcFD-YxU.js"
"_email-DoKk83fr.js",
"_tasks-Bgkd54ac.js",
"_update-BVJ0Pp6O.js"
],
"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": {
"file": "assets/SettingsPage-YOW1Apwk.js",
"file": "assets/SettingsPage-cKC6DywW.js",
"name": "SettingsPage",
"src": "src/pages/SettingsPage.vue",
"isDynamicEntry": true,
@@ -125,12 +138,12 @@
]
},
"src/pages/SystemPage.vue": {
"file": "assets/SystemPage-DCcH_SAQ.js",
"file": "assets/SystemPage-2XepJLfC.js",
"name": "SystemPage",
"src": "src/pages/SystemPage.vue",
"isDynamicEntry": true,
"imports": [
"_update-DcFD-YxU.js",
"_update-BVJ0Pp6O.js",
"index.html"
],
"css": [
@@ -138,12 +151,12 @@
]
},
"src/pages/UsersPage.vue": {
"file": "assets/UsersPage-DhTO_5zp.js",
"file": "assets/UsersPage-CGNuB954.js",
"name": "UsersPage",
"src": "src/pages/UsersPage.vue",
"isDynamicEntry": true,
"imports": [
"_users-CC9BckjT.js",
"_users-Bw5HW1mw.js",
"index.html"
],
"css": [

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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}

File diff suppressed because one or more lines are too long

View File

@@ -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};

View File

@@ -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_per_account}

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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};

View File

@@ -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};

View File

@@ -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};

View File

@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>后台管理 - 知识管理平台</title>
<script type="module" crossorigin src="./assets/index-CdjS44Uj.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-EWm4DZW8.css">
<script type="module" crossorigin src="./assets/index-CDhtYQo-.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DiIt7W4Z.css">
</head>
<body>
<div id="app"></div>