添加报表页面,更新用户管理和注册功能

This commit is contained in:
2025-12-15 21:39:32 +08:00
parent 738eaa5211
commit 9aa28f5b9e
63 changed files with 1018 additions and 403 deletions

View File

@@ -8,8 +8,8 @@ const props = defineProps({
const items = computed(() => [ const items = computed(() => [
{ key: 'total_users', label: '总用户数' }, { key: 'total_users', label: '总用户数' },
{ key: 'approved_users', label: '已审核' }, { key: 'new_users_today', label: '今日注册' },
{ key: 'pending_users', label: '待审核' }, { key: 'new_users_7d', label: '近7天注册' },
{ key: 'total_accounts', label: '总账号数' }, { key: 'total_accounts', label: '总账号数' },
{ key: 'vip_users', label: 'VIP用户' }, { key: 'vip_users', label: 'VIP用户' },
]) ])
@@ -49,4 +49,3 @@ const items = computed(() => [
color: var(--app-muted); color: var(--app-muted);
} }
</style> </style>

View File

@@ -103,8 +103,8 @@ onBeforeUnmount(() => {
}) })
const menuItems = [ const menuItems = [
{ path: '/pending', label: '待审核', icon: Document, badgeKey: 'pending' }, { path: '/reports', label: '报表', icon: Document },
{ path: '/users', label: '用户', icon: User }, { path: '/users', label: '用户', icon: User, badgeKey: 'resets' },
{ path: '/feedbacks', label: '反馈', icon: ChatLineSquare, badgeKey: 'feedbacks' }, { path: '/feedbacks', label: '反馈', icon: ChatLineSquare, badgeKey: 'feedbacks' },
{ path: '/stats', label: '统计', icon: DataAnalysis }, { path: '/stats', label: '统计', icon: DataAnalysis },
{ path: '/logs', label: '任务日志', icon: List }, { path: '/logs', label: '任务日志', icon: List },
@@ -118,9 +118,7 @@ const activeMenu = computed(() => route.path)
function badgeFor(item) { function badgeFor(item) {
if (!item?.badgeKey) return 0 if (!item?.badgeKey) return 0
if (item.badgeKey === 'pending') { if (item.badgeKey === 'resets') return Number(pendingResetsCount.value || 0)
return Number(stats.value?.pending_users || 0) + Number(pendingResetsCount.value || 0)
}
if (item.badgeKey === 'feedbacks') { if (item.badgeKey === 'feedbacks') {
return Number(pendingFeedbackCount.value || 0) return Number(pendingFeedbackCount.value || 0)
} }

View File

@@ -1,228 +0,0 @@
<script setup>
import { inject, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { fetchPendingUsers, approveUser, rejectUser } from '../api/users'
import { approvePasswordReset, fetchPasswordResets, rejectPasswordReset } from '../api/passwordResets'
import { parseSqliteDateTime } from '../utils/datetime'
const refreshStats = inject('refreshStats', null)
const refreshNavBadges = inject('refreshNavBadges', null)
const pendingUsers = ref([])
const passwordResets = ref([])
const loadingPending = ref(false)
const loadingResets = ref(false)
function isVip(user) {
const expire = user?.vip_expire_time
if (!expire) return false
if (String(expire).startsWith('2099-12-31')) return true
const dt = parseSqliteDateTime(expire)
return dt ? dt.getTime() > Date.now() : false
}
async function loadPending() {
loadingPending.value = true
try {
pendingUsers.value = await fetchPendingUsers()
} catch {
pendingUsers.value = []
} finally {
loadingPending.value = false
}
}
async function loadResets() {
loadingResets.value = true
try {
passwordResets.value = await fetchPasswordResets()
} catch {
passwordResets.value = []
} finally {
loadingResets.value = false
}
}
async function refreshAll() {
await Promise.all([loadPending(), loadResets()])
await refreshNavBadges?.({ pendingResets: passwordResets.value.length })
}
async function onApproveUser(row) {
try {
await ElMessageBox.confirm(`确定通过用户「${row.username}」的注册申请吗?`, '审核通过', {
confirmButtonText: '通过',
cancelButtonText: '取消',
type: 'success',
})
} catch {
return
}
try {
await approveUser(row.id)
ElMessage.success('用户审核通过')
await refreshAll()
await refreshStats?.()
} catch {
// handled by interceptor
}
}
async function onRejectUser(row) {
try {
await ElMessageBox.confirm(`确定拒绝用户「${row.username}」的注册申请吗?`, '拒绝申请', {
confirmButtonText: '拒绝',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
await rejectUser(row.id)
ElMessage.success('已拒绝用户')
await refreshAll()
await refreshStats?.()
} catch {
// handled by interceptor
}
}
async function onApproveReset(row) {
try {
await ElMessageBox.confirm(`确定批准「${row.username}」的密码重置申请吗?`, '批准重置', {
confirmButtonText: '批准',
cancelButtonText: '取消',
type: 'success',
})
} catch {
return
}
try {
const res = await approvePasswordReset(row.id)
ElMessage.success(res?.message || '密码重置申请已批准')
await loadResets()
await refreshNavBadges?.({ pendingResets: passwordResets.value.length })
} catch {
// handled by interceptor
}
}
async function onRejectReset(row) {
try {
await ElMessageBox.confirm(`确定拒绝「${row.username}」的密码重置申请吗?`, '拒绝重置', {
confirmButtonText: '拒绝',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
const res = await rejectPasswordReset(row.id)
ElMessage.success(res?.message || '密码重置申请已拒绝')
await loadResets()
await refreshNavBadges?.({ pendingResets: passwordResets.value.length })
} catch {
// handled by interceptor
}
}
onMounted(refreshAll)
</script>
<template>
<div class="page-stack">
<div class="app-page-title">
<h2>待审核</h2>
<div>
<el-button @click="refreshAll">刷新</el-button>
</div>
</div>
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card">
<h3 class="section-title">用户注册审核</h3>
<div class="table-wrap">
<el-table :data="pendingUsers" v-loading="loadingPending" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column label="用户名" min-width="200">
<template #default="{ row }">
<div class="user-cell">
<strong>{{ row.username }}</strong>
<el-tag v-if="isVip(row)" type="warning" effect="light" size="small">VIP</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="email" label="邮箱" min-width="220">
<template #default="{ row }">{{ row.email || '-' }}</template>
</el-table-column>
<el-table-column prop="created_at" label="注册时间" min-width="180" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button type="success" size="small" @click="onApproveUser(row)">通过</el-button>
<el-button type="danger" size="small" @click="onRejectUser(row)">拒绝</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card">
<h3 class="section-title">密码重置审核</h3>
<div class="table-wrap">
<el-table :data="passwordResets" v-loading="loadingResets" style="width: 100%">
<el-table-column prop="id" label="申请ID" width="90" />
<el-table-column prop="username" label="用户名" min-width="200" />
<el-table-column prop="email" label="邮箱" min-width="220">
<template #default="{ row }">{{ row.email || '-' }}</template>
</el-table-column>
<el-table-column prop="created_at" label="申请时间" min-width="180" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button type="success" size="small" @click="onApproveReset(row)">批准</el-button>
<el-button type="danger" size="small" @click="onRejectReset(row)">拒绝</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
</template>
<style scoped>
.page-stack {
display: flex;
flex-direction: column;
gap: 12px;
}
.card {
border-radius: var(--app-radius);
border: 1px solid var(--app-border);
}
.section-title {
margin: 0 0 12px;
font-size: 14px;
font-weight: 800;
}
.table-wrap {
overflow-x: auto;
}
.user-cell {
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
</style>

View File

@@ -0,0 +1,687 @@
<script setup>
import { computed, inject, onMounted, ref } from 'vue'
import { fetchFeedbackStats } from '../api/feedbacks'
import { fetchEmailStats } from '../api/email'
import { fetchPasswordResets } from '../api/passwordResets'
import { fetchDockerStats, fetchRunningTasks, fetchServerInfo, fetchTaskStats } from '../api/tasks'
import { fetchSystemConfig } from '../api/system'
import { fetchUpdateResult, fetchUpdateStatus } from '../api/update'
const refreshStats = inject('refreshStats', null)
const adminStats = inject('adminStats', null)
const refreshNavBadges = inject('refreshNavBadges', null)
const loading = ref(false)
const lastUpdatedAt = ref('')
const taskStats = ref(null)
const runningTasks = ref(null)
const emailStats = ref(null)
const feedbackStats = ref(null)
const serverInfo = ref(null)
const dockerStats = ref(null)
const systemConfig = ref(null)
const updateStatus = ref(null)
const updateStatusError = ref('')
const updateResult = ref(null)
const passwordResetsCount = ref(0)
function recordUpdatedAt() {
try {
lastUpdatedAt.value = new Date().toLocaleString('zh-CN', { hour12: false, timeZone: 'Asia/Shanghai' })
} catch {
lastUpdatedAt.value = ''
}
}
function normalizeCount(value) {
const n = Number(value)
return Number.isFinite(n) ? n : 0
}
function parsePercent(value) {
const text = String(value ?? '').trim()
if (!text) return 0
const cleaned = text.endsWith('%') ? text.slice(0, -1) : text
const n = Number(cleaned)
if (!Number.isFinite(n)) return 0
if (n < 0) return 0
if (n > 1000) return 1000
return n
}
function shortCommit(value) {
const text = String(value ?? '').trim()
if (!text) return '-'
return text.length > 12 ? `${text.slice(0, 12)}` : text
}
function sourceLabel(source) {
const raw = String(source ?? '').trim()
if (!raw) return '手动'
if (raw === 'manual') return '手动'
if (raw === 'scheduled') return '系统定时'
if (raw === 'batch') return '批量执行'
if (raw === 'resumed') return '断点续跑'
if (raw.startsWith('user_scheduled:')) return '用户定时'
return raw
}
const overviewCards = computed(() => {
const s = adminStats?.value || {}
return [
{ label: '总用户数', value: normalizeCount(s.total_users) },
{ label: '今日注册', value: normalizeCount(s.new_users_today) },
{ label: '近7天注册', value: normalizeCount(s.new_users_7d) },
{ label: '总账号数', value: normalizeCount(s.total_accounts) },
{ label: 'VIP用户', value: normalizeCount(s.vip_users) },
{ label: '运行中任务', value: normalizeCount(runningTasks.value?.running_count) },
{ label: '排队任务', value: normalizeCount(runningTasks.value?.queuing_count) },
{ label: '密码重置待处理', value: normalizeCount(passwordResetsCount.value) },
]
})
const taskToday = computed(() => taskStats.value?.today || {})
const taskTotal = computed(() => taskStats.value?.total || {})
const runningTaskList = computed(() => runningTasks.value?.running || [])
const queuingTaskList = computed(() => runningTasks.value?.queuing || [])
const taskTodaySuccessRate = computed(() => {
const success = normalizeCount(taskToday.value.success_tasks)
const failed = normalizeCount(taskToday.value.failed_tasks)
const total = success + failed
return total > 0 ? Math.round((success / total) * 1000) / 10 : 0
})
const emailSuccessRate = computed(() => normalizeCount(emailStats.value?.success_rate))
const scheduleEnabled = computed(() => (systemConfig.value?.schedule_enabled ?? 0) === 1)
const scheduleTime = computed(() => systemConfig.value?.schedule_time || '-')
const scheduleBrowseType = computed(() => systemConfig.value?.schedule_browse_type || '-')
const scheduleWeekdays = computed(() => String(systemConfig.value?.schedule_weekdays || '').trim())
const scheduleWeekdaysDisplay = computed(() => {
const raw = scheduleWeekdays.value
if (!raw) return ''
const map = {
1: '周一',
2: '周二',
3: '周三',
4: '周四',
5: '周五',
6: '周六',
7: '周日',
}
const parts = raw
.split(',')
.map((x) => x.trim())
.filter(Boolean)
if (!parts.length) return raw
return parts.map((p) => map[Number(p)] || p).join('、')
})
const proxyEnabled = computed(() => (systemConfig.value?.proxy_enabled ?? 0) === 1)
const proxyApiUrl = computed(() => systemConfig.value?.proxy_api_url || '')
const proxyExpireMinutes = computed(() => normalizeCount(systemConfig.value?.proxy_expire_minutes))
const maxConcurrentGlobal = computed(() => normalizeCount(systemConfig.value?.max_concurrent_global))
const maxConcurrentPerAccount = computed(() => normalizeCount(systemConfig.value?.max_concurrent_per_account))
const maxScreenshotConcurrent = computed(() => normalizeCount(systemConfig.value?.max_screenshot_concurrent))
const runningCountsLabel = computed(() => {
const runningCount = normalizeCount(runningTasks.value?.running_count)
const queuingCount = normalizeCount(runningTasks.value?.queuing_count)
const maxGlobal = normalizeCount(runningTasks.value?.max_concurrent)
return `运行中 ${runningCount} / 排队 ${queuingCount} / 并发上限 ${maxGlobal || maxConcurrentGlobal.value || '-'}`
})
async function refreshAll() {
if (loading.value) return
loading.value = true
try {
const [
taskResult,
runningResult,
emailResult,
feedbackResult,
resetsResult,
serverResult,
dockerResult,
configResult,
updateStatusResult,
updateResultResult,
] = await Promise.allSettled([
fetchTaskStats(),
fetchRunningTasks(),
fetchEmailStats(),
fetchFeedbackStats(),
fetchPasswordResets(),
fetchServerInfo(),
fetchDockerStats(),
fetchSystemConfig(),
fetchUpdateStatus(),
fetchUpdateResult(),
])
taskStats.value = taskResult.status === 'fulfilled' ? taskResult.value : null
runningTasks.value = runningResult.status === 'fulfilled' ? runningResult.value : null
emailStats.value = emailResult.status === 'fulfilled' ? emailResult.value : null
feedbackStats.value = feedbackResult.status === 'fulfilled' ? feedbackResult.value : null
passwordResetsCount.value = resetsResult.status === 'fulfilled' ? (Array.isArray(resetsResult.value) ? resetsResult.value.length : 0) : 0
serverInfo.value = serverResult.status === 'fulfilled' ? serverResult.value : null
dockerStats.value = dockerResult.status === 'fulfilled' ? dockerResult.value : null
systemConfig.value = configResult.status === 'fulfilled' ? configResult.value : null
if (updateStatusResult.status === 'fulfilled') {
const res = updateStatusResult.value
if (res?.ok) {
updateStatus.value = res.data || null
updateStatusError.value = ''
} else {
updateStatus.value = null
updateStatusError.value = res?.error || '未发现更新状态Update-Agent 可能未运行)'
}
} else {
updateStatus.value = null
updateStatusError.value = ''
}
updateResult.value = updateResultResult.status === 'fulfilled' && updateResultResult.value?.ok ? updateResultResult.value.data : null
await refreshNavBadges?.({ pendingResets: passwordResetsCount.value })
await refreshStats?.()
recordUpdatedAt()
} finally {
loading.value = false
}
}
onMounted(refreshAll)
</script>
<template>
<div class="page-stack">
<div class="app-page-title">
<div class="title-group">
<h2>报表</h2>
<span class="app-muted">{{ lastUpdatedAt ? `更新时间:${lastUpdatedAt}` : '' }}</span>
</div>
<div class="toolbar">
<el-button :loading="loading" @click="refreshAll">刷新</el-button>
</div>
</div>
<el-row :gutter="12">
<el-col v-for="c in overviewCards" :key="c.label" :xs="12" :sm="12" :md="6">
<el-card shadow="never" class="card" :body-style="{ padding: '16px' }">
<div class="metric-label">{{ c.label }}</div>
<div class="metric-value">{{ c.value }}</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :xs="24" :md="14">
<el-card shadow="never" class="card" :body-style="{ padding: '16px' }">
<div class="section-head">
<h3 class="section-title">系统状态</h3>
<el-tag v-if="serverInfo?.uptime" effect="light" type="info">运行 {{ serverInfo.uptime }}</el-tag>
</div>
<div class="sys-grid">
<div class="sys-item">
<div class="sys-k app-muted">CPU</div>
<div class="sys-v">
<el-progress
:percentage="Math.round(parsePercent(serverInfo?.cpu_percent))"
:status="parsePercent(serverInfo?.cpu_percent) >= 90 ? 'exception' : parsePercent(serverInfo?.cpu_percent) >= 75 ? 'warning' : 'success'"
/>
</div>
<div class="sys-sub app-muted">{{ Math.round(parsePercent(serverInfo?.cpu_percent)) }}%</div>
</div>
<div class="sys-item">
<div class="sys-k app-muted">内存</div>
<div class="sys-v">
<el-progress
:percentage="Math.round(parsePercent(serverInfo?.memory_percent))"
:status="parsePercent(serverInfo?.memory_percent) >= 90 ? 'exception' : parsePercent(serverInfo?.memory_percent) >= 75 ? 'warning' : 'success'"
/>
</div>
<div class="sys-sub app-muted">
{{ serverInfo?.memory_used || '-' }} / {{ serverInfo?.memory_total || '-' }}{{ Math.round(parsePercent(serverInfo?.memory_percent)) }}%
</div>
</div>
<div class="sys-item">
<div class="sys-k app-muted">磁盘</div>
<div class="sys-v">
<el-progress
:percentage="Math.round(parsePercent(serverInfo?.disk_percent))"
:status="parsePercent(serverInfo?.disk_percent) >= 90 ? 'exception' : parsePercent(serverInfo?.disk_percent) >= 75 ? 'warning' : 'success'"
/>
</div>
<div class="sys-sub app-muted">
{{ serverInfo?.disk_used || '-' }} / {{ serverInfo?.disk_total || '-' }}{{ Math.round(parsePercent(serverInfo?.disk_percent)) }}%
</div>
</div>
</div>
<div class="divider"></div>
<div class="section-head">
<h3 class="section-title">容器状态</h3>
<el-tag v-if="dockerStats?.status" effect="light" :type="dockerStats?.running ? 'success' : 'info'">
{{ dockerStats?.status }}
</el-tag>
</div>
<el-descriptions border :column="2" size="small">
<el-descriptions-item label="容器名">{{ dockerStats?.container_name || '-' }}</el-descriptions-item>
<el-descriptions-item label="运行时长">{{ dockerStats?.uptime || '-' }}</el-descriptions-item>
<el-descriptions-item label="CPU">{{ dockerStats?.cpu_percent || '-' }}</el-descriptions-item>
<el-descriptions-item label="内存">{{ dockerStats?.memory_usage || '-' }}</el-descriptions-item>
<el-descriptions-item label="内存上限">{{ dockerStats?.memory_limit || '-' }}</el-descriptions-item>
<el-descriptions-item label="内存占比">{{ dockerStats?.memory_percent || '-' }}</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
<el-col :xs="24" :md="10">
<el-card shadow="never" class="card" :body-style="{ padding: '16px' }">
<div class="section-head">
<h3 class="section-title">配置与更新</h3>
<el-tag v-if="updateStatus?.update_available" effect="light" type="warning">发现新版本</el-tag>
</div>
<el-descriptions border :column="1" size="small">
<el-descriptions-item label="定时任务">
<el-tag v-if="scheduleEnabled" type="success" effect="light">启用</el-tag>
<el-tag v-else type="info" effect="light">关闭</el-tag>
<span class="desc-inline app-muted"> {{ scheduleTime }} / {{ scheduleBrowseType }}</span>
</el-descriptions-item>
<el-descriptions-item label="执行日期">
<span>{{ scheduleWeekdaysDisplay || scheduleWeekdays || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="并发配置">
<span>全局 {{ maxConcurrentGlobal || '-' }} / 单账号 {{ maxConcurrentPerAccount || '-' }} / 截图 {{ maxScreenshotConcurrent || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="代理">
<el-tag v-if="proxyEnabled" type="success" effect="light">启用</el-tag>
<el-tag v-else type="info" effect="light">关闭</el-tag>
<span v-if="proxyEnabled && proxyApiUrl" class="desc-inline app-muted"> {{ proxyApiUrl }}</span>
</el-descriptions-item>
<el-descriptions-item label="代理有效期">{{ proxyExpireMinutes || '-' }} 分钟</el-descriptions-item>
</el-descriptions>
<div class="divider"></div>
<div class="sub-title">版本信息</div>
<el-descriptions border :column="1" size="small">
<el-descriptions-item label="本地版本(commit)">{{ shortCommit(updateStatus?.local_commit) }}</el-descriptions-item>
<el-descriptions-item label="远端版本(commit)">{{ shortCommit(updateStatus?.remote_commit) }}</el-descriptions-item>
<el-descriptions-item label="最近检查时间">{{ updateStatus?.checked_at || '-' }}</el-descriptions-item>
<el-descriptions-item v-if="updateStatusError" label="更新状态">{{ updateStatusError }}</el-descriptions-item>
<el-descriptions-item v-if="updateResult?.job_id" label="最近更新">
<span>job {{ updateResult.job_id }} / {{ updateResult?.status || '-' }}</span>
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :xs="24" :md="14">
<el-card shadow="never" class="card" :body-style="{ padding: '16px' }">
<div class="section-head">
<h3 class="section-title">任务报表今日</h3>
<el-tag v-if="taskTodaySuccessRate > 0" effect="light" type="success">成功率 {{ taskTodaySuccessRate }}%</el-tag>
</div>
<el-row :gutter="12">
<el-col :xs="12" :sm="6">
<div class="kv">
<div class="kv-v ok">{{ normalizeCount(taskToday.success_tasks) }}</div>
<div class="kv-k app-muted">成功任务</div>
</div>
</el-col>
<el-col :xs="12" :sm="6">
<div class="kv">
<div class="kv-v err">{{ normalizeCount(taskToday.failed_tasks) }}</div>
<div class="kv-k app-muted">失败任务</div>
</div>
</el-col>
<el-col :xs="12" :sm="6">
<div class="kv">
<div class="kv-v">{{ normalizeCount(taskToday.total_tasks) }}</div>
<div class="kv-k app-muted">总任务</div>
</div>
</el-col>
<el-col :xs="12" :sm="6">
<div class="kv">
<div class="kv-v">{{ normalizeCount(taskToday.total_items) }}</div>
<div class="kv-k app-muted">浏览内容</div>
</div>
</el-col>
<el-col :xs="12" :sm="6">
<div class="kv">
<div class="kv-v">{{ normalizeCount(taskToday.total_attachments) }}</div>
<div class="kv-k app-muted">查看附件</div>
</div>
</el-col>
</el-row>
<div class="divider"></div>
<div class="section-head">
<h3 class="section-title">任务报表累计</h3>
</div>
<el-row :gutter="12">
<el-col :xs="12" :sm="6">
<div class="kv">
<div class="kv-v ok">{{ normalizeCount(taskTotal.success_tasks) }}</div>
<div class="kv-k app-muted">成功任务</div>
</div>
</el-col>
<el-col :xs="12" :sm="6">
<div class="kv">
<div class="kv-v err">{{ normalizeCount(taskTotal.failed_tasks) }}</div>
<div class="kv-k app-muted">失败任务</div>
</div>
</el-col>
<el-col :xs="12" :sm="6">
<div class="kv">
<div class="kv-v">{{ normalizeCount(taskTotal.total_tasks) }}</div>
<div class="kv-k app-muted">总任务</div>
</div>
</el-col>
<el-col :xs="12" :sm="6">
<div class="kv">
<div class="kv-v">{{ normalizeCount(taskTotal.total_items) }}</div>
<div class="kv-k app-muted">浏览内容</div>
</div>
</el-col>
<el-col :xs="12" :sm="6">
<div class="kv">
<div class="kv-v">{{ normalizeCount(taskTotal.total_attachments) }}</div>
<div class="kv-k app-muted">查看附件</div>
</div>
</el-col>
</el-row>
<div class="divider"></div>
<div class="section-head">
<h3 class="section-title">当前队列</h3>
<span class="app-muted">{{ runningCountsLabel }}</span>
</div>
<div v-if="runningTaskList.length || queuingTaskList.length" class="table-wrap">
<el-table :data="runningTaskList.slice(0, 8)" size="small" style="width: 100%">
<el-table-column label="用户" min-width="140">
<template #default="{ row }">{{ row.user_username || '-' }}</template>
</el-table-column>
<el-table-column label="账号" min-width="160">
<template #default="{ row }">{{ row.username || '-' }}</template>
</el-table-column>
<el-table-column label="来源" width="100">
<template #default="{ row }">{{ sourceLabel(row.source) }}</template>
</el-table-column>
<el-table-column label="类型" width="100">
<template #default="{ row }">{{ row.browse_type || '-' }}</template>
</el-table-column>
<el-table-column label="进度" width="110">
<template #default="{ row }">{{ row.progress_items }}/{{ row.progress_attachments }}</template>
</el-table-column>
<el-table-column label="耗时" width="110">
<template #default="{ row }">{{ row.elapsed_display || '-' }}</template>
</el-table-column>
<el-table-column label="状态" min-width="140">
<template #default="{ row }">{{ row.detail_status || row.status || '-' }}</template>
</el-table-column>
</el-table>
<div v-if="queuingTaskList.length" class="help app-muted">排队中{{ queuingTaskList.length }} 个任务</div>
</div>
<div v-else class="help app-muted">当前无运行/排队任务</div>
</el-card>
</el-col>
<el-col :xs="24" :md="10">
<el-card shadow="never" class="card" :body-style="{ padding: '16px' }">
<div class="section-head">
<h3 class="section-title">邮件报表</h3>
<el-tag v-if="emailSuccessRate > 0" effect="light" type="success">成功率 {{ emailSuccessRate }}%</el-tag>
</div>
<el-row :gutter="12">
<el-col :span="8">
<div class="kv">
<div class="kv-v">{{ normalizeCount(emailStats?.total_sent) }}</div>
<div class="kv-k app-muted">总发送</div>
</div>
</el-col>
<el-col :span="8">
<div class="kv">
<div class="kv-v ok">{{ normalizeCount(emailStats?.total_success) }}</div>
<div class="kv-k app-muted">成功</div>
</div>
</el-col>
<el-col :span="8">
<div class="kv">
<div class="kv-v err">{{ normalizeCount(emailStats?.total_failed) }}</div>
<div class="kv-k app-muted">失败</div>
</div>
</el-col>
</el-row>
<div class="divider"></div>
<div class="sub-title">类型统计</div>
<div class="type-grid">
<div class="type-item">
<div class="type-v">{{ normalizeCount(emailStats?.register_sent) }}</div>
<div class="type-k app-muted">注册验证</div>
</div>
<div class="type-item">
<div class="type-v">{{ normalizeCount(emailStats?.reset_sent) }}</div>
<div class="type-k app-muted">密码重置</div>
</div>
<div class="type-item">
<div class="type-v">{{ normalizeCount(emailStats?.bind_sent) }}</div>
<div class="type-k app-muted">邮箱绑定</div>
</div>
<div class="type-item">
<div class="type-v">{{ normalizeCount(emailStats?.task_complete_sent) }}</div>
<div class="type-k app-muted">任务完成</div>
</div>
</div>
<div class="divider"></div>
<div class="section-head">
<h3 class="section-title">反馈概览</h3>
</div>
<el-row :gutter="12">
<el-col :span="8">
<div class="kv">
<div class="kv-v">{{ normalizeCount(feedbackStats?.total) }}</div>
<div class="kv-k app-muted">总反馈</div>
</div>
</el-col>
<el-col :span="8">
<div class="kv">
<div class="kv-v warn">{{ normalizeCount(feedbackStats?.pending) }}</div>
<div class="kv-k app-muted">待处理</div>
</div>
</el-col>
<el-col :span="8">
<div class="kv">
<div class="kv-v ok">{{ normalizeCount(feedbackStats?.replied) }}</div>
<div class="kv-k app-muted">已回复</div>
</div>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
</div>
</template>
<style scoped>
.page-stack {
display: flex;
flex-direction: column;
gap: 12px;
}
.title-group {
display: flex;
flex-direction: column;
gap: 2px;
}
.toolbar {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.card {
border-radius: var(--app-radius);
border: 1px solid var(--app-border);
}
.metric-label {
font-size: 12px;
color: var(--app-muted);
}
.metric-value {
margin-top: 6px;
font-size: 22px;
font-weight: 900;
line-height: 1.1;
}
.section-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
}
.section-title {
margin: 0;
font-size: 14px;
font-weight: 800;
}
.kv {
border: 1px solid var(--app-border);
border-radius: 12px;
padding: 12px;
background: #fff;
}
.kv-v {
font-size: 18px;
font-weight: 900;
line-height: 1.1;
}
.kv-k {
margin-top: 6px;
font-size: 12px;
}
.ok {
color: #047857;
}
.warn {
color: #b45309;
}
.err {
color: #b91c1c;
}
.divider {
height: 1px;
background: var(--app-border);
margin: 14px 0;
}
.sys-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.sys-item {
border: 1px solid var(--app-border);
border-radius: 12px;
padding: 12px;
background: #fff;
}
.sys-k {
font-size: 12px;
}
.sys-sub {
margin-top: 8px;
font-size: 12px;
}
.desc-inline {
margin-left: 8px;
}
.sub-title {
font-size: 13px;
font-weight: 800;
margin-bottom: 10px;
}
.type-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.type-item {
border: 1px solid var(--app-border);
border-radius: 12px;
padding: 12px;
background: #fff;
}
.type-v {
font-size: 16px;
font-weight: 900;
}
.type-k {
margin-top: 6px;
font-size: 12px;
}
.table-wrap {
overflow-x: auto;
}
.help {
margin-top: 10px;
font-size: 12px;
}
@media (max-width: 768px) {
.sys-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -295,7 +295,7 @@ async function saveAutoApprove() {
try { try {
const res = await updateSystemConfig(payload) const res = await updateSystemConfig(payload)
ElMessage.success(res?.message || '自动审核配置已保存') ElMessage.success(res?.message || '注册设置已保存')
} catch { } catch {
// handled by interceptor // handled by interceptor
} }
@@ -438,12 +438,12 @@ onBeforeUnmount(stopUpdatePolling)
</el-card> </el-card>
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card"> <el-card shadow="never" :body-style="{ padding: '16px' }" class="card">
<h3 class="section-title">注册自动审核</h3> <h3 class="section-title">注册设置</h3>
<el-form label-width="130px"> <el-form label-width="130px">
<el-form-item label="启用自动审核"> <el-form-item label="注册赠送VIP">
<el-switch v-model="autoApproveEnabled" /> <el-switch v-model="autoApproveEnabled" />
<div class="help">开启后新用户注册将自动通过审核无需管理员手动审批</div> <div class="help">开启后新用户注册成功后将赠送下方设置的VIP天数注册已默认无需审核</div>
</el-form-item> </el-form-item>
<el-form-item label="每小时注册限制"> <el-form-item label="每小时注册限制">
@@ -455,7 +455,7 @@ onBeforeUnmount(stopUpdatePolling)
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-button type="primary" @click="saveAutoApprove">保存自动审核配</el-button> <el-button type="primary" @click="saveAutoApprove">保存注册设</el-button>
</el-card> </el-card>
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card" v-loading="updateLoading"> <el-card shadow="never" :body-style="{ padding: '16px' }" class="card" v-loading="updateLoading">

View File

@@ -4,21 +4,26 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { import {
adminResetUserPassword, adminResetUserPassword,
approveUser,
deleteUser, deleteUser,
fetchAllUsers, fetchAllUsers,
approveUser,
rejectUser, rejectUser,
removeUserVip, removeUserVip,
setUserVip, setUserVip,
} from '../api/users' } from '../api/users'
import { approvePasswordReset, fetchPasswordResets, rejectPasswordReset } from '../api/passwordResets'
import { parseSqliteDateTime } from '../utils/datetime' import { parseSqliteDateTime } from '../utils/datetime'
import { validatePasswordStrength } from '../utils/password' import { validatePasswordStrength } from '../utils/password'
const refreshStats = inject('refreshStats', null) const refreshStats = inject('refreshStats', null)
const refreshNavBadges = inject('refreshNavBadges', null)
const loading = ref(false) const loading = ref(false)
const users = ref([]) const users = ref([])
const resetLoading = ref(false)
const passwordResets = ref([])
function isVip(user) { function isVip(user) {
const expire = user?.vip_expire_time const expire = user?.vip_expire_time
if (!expire) return false if (!expire) return false
@@ -38,9 +43,8 @@ function vipLabel(user) {
} }
function statusMeta(status) { function statusMeta(status) {
if (status === 'approved') return { label: '已通过', type: 'success' } if (status === 'rejected') return { label: '禁用', type: 'danger' }
if (status === 'rejected') return { label: '已拒绝', type: 'danger' } return { label: '正常', type: 'success' }
return { label: '待审核', type: 'warning' }
} }
async function loadUsers() { async function loadUsers() {
@@ -54,10 +58,27 @@ async function loadUsers() {
} }
} }
async function onApprove(row) { async function loadResets() {
resetLoading.value = true
try { try {
await ElMessageBox.confirm(`确定通过用户「${row.username}」的注册申请吗?`, '审核通过', { const list = await fetchPasswordResets()
confirmButtonText: '通过', passwordResets.value = Array.isArray(list) ? list : []
} catch {
passwordResets.value = []
} finally {
resetLoading.value = false
}
}
async function refreshAll() {
await Promise.all([loadUsers(), loadResets()])
await refreshNavBadges?.({ pendingResets: passwordResets.value.length })
}
async function onEnableUser(row) {
try {
await ElMessageBox.confirm(`确定启用用户「${row.username}」吗?启用后用户可正常登录。`, '启用用户', {
confirmButtonText: '启用',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'success', type: 'success',
}) })
@@ -67,7 +88,7 @@ async function onApprove(row) {
try { try {
await approveUser(row.id) await approveUser(row.id)
ElMessage.success('用户审核通过') ElMessage.success('用户已启用')
await loadUsers() await loadUsers()
await refreshStats?.() await refreshStats?.()
} catch { } catch {
@@ -75,10 +96,10 @@ async function onApprove(row) {
} }
} }
async function onReject(row) { async function onDisableUser(row) {
try { try {
await ElMessageBox.confirm(`确定拒绝用户「${row.username}的注册申请吗?`, '拒绝申请', { await ElMessageBox.confirm(`确定禁用用户「${row.username}」吗?禁用后用户将无法登录。`, '禁用用户', {
confirmButtonText: '拒绝', confirmButtonText: '禁用',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
}) })
@@ -88,7 +109,7 @@ async function onReject(row) {
try { try {
await rejectUser(row.id) await rejectUser(row.id)
ElMessage.success('已拒绝用户') ElMessage.success('用户已禁用')
await loadUsers() await loadUsers()
await refreshStats?.() await refreshStats?.()
} catch { } catch {
@@ -96,6 +117,48 @@ async function onReject(row) {
} }
} }
async function onApproveReset(row) {
try {
await ElMessageBox.confirm(`确定批准「${row.username}」的密码重置申请吗?`, '批准重置', {
confirmButtonText: '批准',
cancelButtonText: '取消',
type: 'success',
})
} catch {
return
}
try {
const res = await approvePasswordReset(row.id)
ElMessage.success(res?.message || '密码重置申请已批准')
await loadResets()
await refreshNavBadges?.({ pendingResets: passwordResets.value.length })
} catch {
// handled by interceptor
}
}
async function onRejectReset(row) {
try {
await ElMessageBox.confirm(`确定拒绝「${row.username}」的密码重置申请吗?`, '拒绝重置', {
confirmButtonText: '拒绝',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
const res = await rejectPasswordReset(row.id)
ElMessage.success(res?.message || '密码重置申请已拒绝')
await loadResets()
await refreshNavBadges?.({ pendingResets: passwordResets.value.length })
} catch {
// handled by interceptor
}
}
async function onDelete(row) { async function onDelete(row) {
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(
@@ -200,7 +263,7 @@ async function onResetPassword(row) {
} }
} }
onMounted(loadUsers) onMounted(refreshAll)
</script> </script>
<template> <template>
@@ -208,7 +271,7 @@ onMounted(loadUsers)
<div class="app-page-title"> <div class="app-page-title">
<h2>用户</h2> <h2>用户</h2>
<div> <div>
<el-button @click="loadUsers">刷新</el-button> <el-button @click="refreshAll">刷新</el-button>
</div> </div>
</div> </div>
@@ -239,17 +302,20 @@ onMounted(loadUsers)
<el-table-column label="时间" min-width="220"> <el-table-column label="时间" min-width="220">
<template #default="{ row }"> <template #default="{ row }">
<div>{{ row.created_at }}</div> <div>{{ row.created_at }}</div>
<div v-if="row.approved_at" class="app-muted">审核: {{ row.approved_at }}</div> <div v-if="row.vip_expire_time" class="app-muted">VIP到期: {{ row.vip_expire_time }}</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="280" fixed="right"> <el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<div class="actions"> <div class="actions">
<template v-if="row.status === 'pending'"> <el-button
<el-button type="success" size="small" @click="onApprove(row)">通过</el-button> v-if="row.status === 'rejected'"
<el-button type="warning" size="small" @click="onReject(row)">拒绝</el-button> type="success"
</template> size="small"
@click="onEnableUser(row)"
>启用</el-button>
<el-button v-else type="warning" size="small" @click="onDisableUser(row)">禁用</el-button>
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<el-button size="small">VIP</el-button> <el-button size="small">VIP</el-button>
@@ -272,6 +338,27 @@ onMounted(loadUsers)
</el-table> </el-table>
</div> </div>
</el-card> </el-card>
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card">
<h3 class="section-title">密码重置申请</h3>
<div class="table-wrap">
<el-table :data="passwordResets" v-loading="resetLoading" style="width: 100%">
<el-table-column prop="id" label="申请ID" width="90" />
<el-table-column prop="username" label="用户名" min-width="200" />
<el-table-column prop="email" label="邮箱" min-width="220">
<template #default="{ row }">{{ row.email || '-' }}</template>
</el-table-column>
<el-table-column prop="created_at" label="申请时间" min-width="180" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button type="success" size="small" @click="onApproveReset(row)">批准</el-button>
<el-button type="danger" size="small" @click="onRejectReset(row)">拒绝</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="help app-muted">当未启用邮件找回密码时用户会提交申请由管理员在此处处理</div>
</el-card>
</div> </div>
</template> </template>
@@ -287,6 +374,17 @@ onMounted(loadUsers)
border: 1px solid var(--app-border); border: 1px solid var(--app-border);
} }
.section-title {
margin: 0 0 12px;
font-size: 14px;
font-weight: 800;
}
.help {
margin-top: 10px;
font-size: 12px;
}
.table-wrap { .table-wrap {
overflow-x: auto; overflow-x: auto;
} }

View File

@@ -2,7 +2,7 @@ import { createRouter, createWebHashHistory } from 'vue-router'
import AdminLayout from '../layouts/AdminLayout.vue' import AdminLayout from '../layouts/AdminLayout.vue'
const PendingPage = () => import('../pages/PendingPage.vue') const ReportPage = () => import('../pages/ReportPage.vue')
const UsersPage = () => import('../pages/UsersPage.vue') const UsersPage = () => import('../pages/UsersPage.vue')
const FeedbacksPage = () => import('../pages/FeedbacksPage.vue') const FeedbacksPage = () => import('../pages/FeedbacksPage.vue')
const StatsPage = () => import('../pages/StatsPage.vue') const StatsPage = () => import('../pages/StatsPage.vue')
@@ -17,8 +17,9 @@ const routes = [
path: '/', path: '/',
component: AdminLayout, component: AdminLayout,
children: [ children: [
{ path: '', redirect: '/pending' }, { path: '', redirect: '/reports' },
{ path: '/pending', name: 'pending', component: PendingPage }, { path: '/pending', redirect: '/reports' },
{ path: '/reports', name: 'reports', component: ReportPage },
{ path: '/users', name: 'users', component: UsersPage }, { path: '/users', name: 'users', component: UsersPage },
{ path: '/feedbacks', name: 'feedbacks', component: FeedbacksPage }, { path: '/feedbacks', name: 'feedbacks', component: FeedbacksPage },
{ path: '/stats', name: 'stats', component: StatsPage }, { path: '/stats', name: 'stats', component: StatsPage },

View File

@@ -25,7 +25,7 @@ const successTitle = ref('')
const successDesc = ref('') const successDesc = ref('')
const emailLabel = computed(() => (emailVerifyEnabled.value ? '邮箱 *' : '邮箱(可选)')) const emailLabel = computed(() => (emailVerifyEnabled.value ? '邮箱 *' : '邮箱(可选)'))
const emailHint = computed(() => (emailVerifyEnabled.value ? '必填,用于账号验证' : '选填,用于接收审核通知')) const emailHint = computed(() => (emailVerifyEnabled.value ? '必填,用于账号验证' : '选填,用于找回密码和接收通知'))
async function refreshCaptcha() { async function refreshCaptcha() {
try { try {

View File

@@ -120,7 +120,7 @@ config = get_config()
DB_FILE = config.DB_FILE DB_FILE = config.DB_FILE
# 数据库版本 (用于迁移管理) # 数据库版本 (用于迁移管理)
DB_VERSION = 10 DB_VERSION = 11
# ==================== 系统配置缓存P1 / O-03 ==================== # ==================== 系统配置缓存P1 / O-03 ====================

View File

@@ -107,8 +107,23 @@ def get_system_stats() -> dict:
cursor.execute("SELECT COUNT(*) as count FROM users WHERE status = 'approved'") cursor.execute("SELECT COUNT(*) as count FROM users WHERE status = 'approved'")
approved_users = cursor.fetchone()["count"] approved_users = cursor.fetchone()["count"]
cursor.execute("SELECT COUNT(*) as count FROM users WHERE status = 'pending'") cursor.execute(
pending_users = cursor.fetchone()["count"] """
SELECT COUNT(*) as count
FROM users
WHERE date(created_at) = date('now', 'localtime')
"""
)
new_users_today = cursor.fetchone()["count"]
cursor.execute(
"""
SELECT COUNT(*) as count
FROM users
WHERE datetime(created_at) >= datetime('now', 'localtime', '-7 days')
"""
)
new_users_7d = cursor.fetchone()["count"]
cursor.execute("SELECT COUNT(*) as count FROM accounts") cursor.execute("SELECT COUNT(*) as count FROM accounts")
total_accounts = cursor.fetchone()["count"] total_accounts = cursor.fetchone()["count"]
@@ -125,7 +140,8 @@ def get_system_stats() -> dict:
return { return {
"total_users": total_users, "total_users": total_users,
"approved_users": approved_users, "approved_users": approved_users,
"pending_users": pending_users, "new_users_today": new_users_today,
"new_users_7d": new_users_7d,
"total_accounts": total_accounts, "total_accounts": total_accounts,
"vip_users": vip_users, "vip_users": vip_users,
} }

View File

@@ -66,6 +66,9 @@ def migrate_database(conn, target_version: int) -> None:
if current_version < 10: if current_version < 10:
_migrate_to_v10(conn) _migrate_to_v10(conn)
current_version = 10 current_version = 10
if current_version < 11:
_migrate_to_v11(conn)
current_version = 11
if current_version != int(target_version): if current_version != int(target_version):
set_current_version(conn, int(target_version)) set_current_version(conn, int(target_version))
@@ -450,3 +453,26 @@ def _migrate_to_v10(conn):
if changed: if changed:
conn.commit() conn.commit()
def _migrate_to_v11(conn):
"""迁移到版本11 - 取消注册待审核:历史 pending 用户直接置为 approved"""
cursor = conn.cursor()
now_str = get_cst_now_str()
try:
cursor.execute(
"""
UPDATE users
SET status = 'approved',
approved_at = COALESCE(NULLIF(approved_at, ''), ?)
WHERE status = 'pending'
""",
(now_str,),
)
updated = cursor.rowcount
conn.commit()
if updated:
print(f" ✓ 已将 {updated} 个 pending 用户迁移为 approved")
except sqlite3.OperationalError as e:
print(f" ⚠️ v11 迁移跳过: {e}")

View File

@@ -33,7 +33,7 @@ def ensure_schema(conn) -> None:
email TEXT, email TEXT,
email_verified INTEGER DEFAULT 0, email_verified INTEGER DEFAULT 0,
email_notify_enabled INTEGER DEFAULT 1, email_notify_enabled INTEGER DEFAULT 1,
status TEXT DEFAULT 'pending', status TEXT DEFAULT 'approved',
vip_expire_time TIMESTAMP, vip_expire_time TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
approved_at TIMESTAMP approved_at TIMESTAMP

View File

@@ -148,7 +148,7 @@ def get_user_vip_info(user_id):
def create_user(username, password, email=""): def create_user(username, password, email=""):
"""创建新用户(待审核状态,赠送默认VIP)""" """创建新用户(默认直接通过,赠送默认VIP)"""
cst_tz = pytz.timezone("Asia/Shanghai") cst_tz = pytz.timezone("Asia/Shanghai")
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
@@ -168,10 +168,10 @@ def create_user(username, password, email=""):
try: try:
cursor.execute( cursor.execute(
""" """
INSERT INTO users (username, password_hash, email, status, vip_expire_time, created_at) INSERT INTO users (username, password_hash, email, status, vip_expire_time, created_at, approved_at)
VALUES (?, ?, ?, 'pending', ?, ?) VALUES (?, ?, ?, 'approved', ?, ?, ?)
""", """,
(username, password_hash, email, vip_expire_time, cst_time), (username, password_hash, email, vip_expire_time, cst_time, cst_time),
) )
conn.commit() conn.commit()
return cursor.lastrowid return cursor.lastrowid

View File

@@ -70,38 +70,36 @@ def register():
auto_approve_hourly_limit = system_config.get("auto_approve_hourly_limit", 10) auto_approve_hourly_limit = system_config.get("auto_approve_hourly_limit", 10)
auto_approve_vip_days = system_config.get("auto_approve_vip_days", 7) auto_approve_vip_days = system_config.get("auto_approve_vip_days", 7)
if auto_approve_enabled or email_verify_enabled: hourly_limit = int(auto_approve_hourly_limit) if isinstance(auto_approve_hourly_limit, int) else 10
if hourly_limit > 0:
hourly_count = database.get_hourly_registration_count() hourly_count = database.get_hourly_registration_count()
if hourly_count >= auto_approve_hourly_limit: if hourly_count >= hourly_limit:
return jsonify({"error": f"注册人数过多,请稍后再试(每小时限制{auto_approve_hourly_limit}人)"}), 429 return jsonify({"error": f"注册人数过多,请稍后再试(每小时限制{hourly_limit}人)"}), 429
user_id = database.create_user(username, password, email) user_id = database.create_user(username, password, email)
if user_id: if user_id:
if auto_approve_enabled:
if auto_approve_vip_days > 0:
database.set_user_vip(user_id, auto_approve_vip_days)
if email_verify_enabled and email: if email_verify_enabled and email:
result = email_service.send_register_verification_email(email=email, username=username, user_id=user_id) result = email_service.send_register_verification_email(email=email, username=username, user_id=user_id)
if result["success"]: if result["success"]:
return jsonify( message = "注册成功!验证邮件已发送(可直接登录,建议完成邮箱验证)"
{ if auto_approve_enabled and auto_approve_vip_days > 0:
"success": True, message += f",赠送{auto_approve_vip_days}天VIP"
"message": "注册成功!验证邮件已发送,请查收邮箱并点击链接完成验证", return jsonify({"success": True, "message": message, "need_verify": True})
"need_verify": True,
}
)
logger.error(f"注册验证邮件发送失败: {result['error']}") logger.error(f"注册验证邮件发送失败: {result['error']}")
return jsonify( message = f"注册成功,但验证邮件发送失败({result['error']})。你仍可直接登录"
{ if auto_approve_enabled and auto_approve_vip_days > 0:
"success": True, message += f",赠送{auto_approve_vip_days}天VIP"
"message": f"注册成功,但验证邮件发送失败({result['error']})。请稍后在登录页面重新发送验证邮件", return jsonify({"success": True, "message": message, "need_verify": True})
"need_verify": True,
} message = "注册成功!可直接登录"
) if auto_approve_enabled and auto_approve_vip_days > 0:
if auto_approve_enabled: message += f",赠送{auto_approve_vip_days}天VIP"
database.approve_user(user_id) return jsonify({"success": True, "message": message})
if auto_approve_vip_days > 0:
database.set_user_vip(user_id, auto_approve_vip_days)
return jsonify({"success": True, "message": f"注册成功!已自动审核通过,赠送{auto_approve_vip_days}天VIP"})
return jsonify({"success": True, "message": "注册成功!已自动审核通过"})
return jsonify({"success": True, "message": "注册成功,请等待管理员审核"})
return jsonify({"error": "用户名已存在"}), 400 return jsonify({"error": "用户名已存在"}), 400

View File

@@ -1,29 +1,43 @@
{ {
"_datetime-ZCuLLiQt.js": { "_email-DiVz51rK.js": {
"file": "assets/datetime-ZCuLLiQt.js", "file": "assets/email-DiVz51rK.js",
"name": "datetime" "name": "email",
},
"_taskSource-B7bFDyX8.js": {
"file": "assets/taskSource-B7bFDyX8.js",
"name": "taskSource",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_users-BKWiZCGY.js": { "_taskSource-CFicR2zp.js": {
"file": "assets/users-BKWiZCGY.js", "file": "assets/taskSource-CFicR2zp.js",
"name": "taskSource"
},
"_tasks-Bpfaxqqb.js": {
"file": "assets/tasks-Bpfaxqqb.js",
"name": "tasks",
"imports": [
"index.html"
]
},
"_update-B-ZRn1LV.js": {
"file": "assets/update-B-ZRn1LV.js",
"name": "update",
"imports": [
"index.html"
]
},
"_users-CgASQeNW.js": {
"file": "assets/users-CgASQeNW.js",
"name": "users", "name": "users",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"index.html": { "index.html": {
"file": "assets/index-DMMQbxWA.js", "file": "assets/index-Do26tg8I.js",
"name": "index", "name": "index",
"src": "index.html", "src": "index.html",
"isEntry": true, "isEntry": true,
"dynamicImports": [ "dynamicImports": [
"src/pages/PendingPage.vue", "src/pages/ReportPage.vue",
"src/pages/UsersPage.vue", "src/pages/UsersPage.vue",
"src/pages/FeedbacksPage.vue", "src/pages/FeedbacksPage.vue",
"src/pages/StatsPage.vue", "src/pages/StatsPage.vue",
@@ -34,11 +48,11 @@
"src/pages/SettingsPage.vue" "src/pages/SettingsPage.vue"
], ],
"css": [ "css": [
"assets/index-BIDpnzAs.css" "assets/index-C73IFBwi.css"
] ]
}, },
"src/pages/AnnouncementsPage.vue": { "src/pages/AnnouncementsPage.vue": {
"file": "assets/AnnouncementsPage-L5yVXHzU.js", "file": "assets/AnnouncementsPage-BoMMAWcs.js",
"name": "AnnouncementsPage", "name": "AnnouncementsPage",
"src": "src/pages/AnnouncementsPage.vue", "src": "src/pages/AnnouncementsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -50,11 +64,12 @@
] ]
}, },
"src/pages/EmailPage.vue": { "src/pages/EmailPage.vue": {
"file": "assets/EmailPage-qHIhKChc.js", "file": "assets/EmailPage-VvSoafV1.js",
"name": "EmailPage", "name": "EmailPage",
"src": "src/pages/EmailPage.vue", "src": "src/pages/EmailPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_email-DiVz51rK.js",
"index.html" "index.html"
], ],
"css": [ "css": [
@@ -62,7 +77,7 @@
] ]
}, },
"src/pages/FeedbacksPage.vue": { "src/pages/FeedbacksPage.vue": {
"file": "assets/FeedbacksPage-DPKEyWIv.js", "file": "assets/FeedbacksPage-DSgG1B6b.js",
"name": "FeedbacksPage", "name": "FeedbacksPage",
"src": "src/pages/FeedbacksPage.vue", "src": "src/pages/FeedbacksPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -74,35 +89,37 @@
] ]
}, },
"src/pages/LogsPage.vue": { "src/pages/LogsPage.vue": {
"file": "assets/LogsPage-EUt-yBa-.js", "file": "assets/LogsPage-D1qK6xGt.js",
"name": "LogsPage", "name": "LogsPage",
"src": "src/pages/LogsPage.vue", "src": "src/pages/LogsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-BKWiZCGY.js", "_users-CgASQeNW.js",
"_taskSource-B7bFDyX8.js", "_tasks-Bpfaxqqb.js",
"_taskSource-CFicR2zp.js",
"index.html" "index.html"
], ],
"css": [ "css": [
"assets/LogsPage-Cmm-qHAH.css" "assets/LogsPage-Cmm-qHAH.css"
] ]
}, },
"src/pages/PendingPage.vue": { "src/pages/ReportPage.vue": {
"file": "assets/PendingPage-D8-nvUo0.js", "file": "assets/ReportPage-BtciWYlz.js",
"name": "PendingPage", "name": "ReportPage",
"src": "src/pages/PendingPage.vue", "src": "src/pages/ReportPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-BKWiZCGY.js",
"index.html", "index.html",
"_datetime-ZCuLLiQt.js" "_email-DiVz51rK.js",
"_tasks-Bpfaxqqb.js",
"_update-B-ZRn1LV.js"
], ],
"css": [ "css": [
"assets/PendingPage-C_mZDlzP.css" "assets/ReportPage-Ds4jOTh9.css"
] ]
}, },
"src/pages/SettingsPage.vue": { "src/pages/SettingsPage.vue": {
"file": "assets/SettingsPage-8bHYJpQZ.js", "file": "assets/SettingsPage-Bw9iP0hP.js",
"name": "SettingsPage", "name": "SettingsPage",
"src": "src/pages/SettingsPage.vue", "src": "src/pages/SettingsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -114,12 +131,13 @@
] ]
}, },
"src/pages/StatsPage.vue": { "src/pages/StatsPage.vue": {
"file": "assets/StatsPage-D_TFEGc9.js", "file": "assets/StatsPage-BumsyjN6.js",
"name": "StatsPage", "name": "StatsPage",
"src": "src/pages/StatsPage.vue", "src": "src/pages/StatsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_taskSource-B7bFDyX8.js", "_tasks-Bpfaxqqb.js",
"_taskSource-CFicR2zp.js",
"index.html" "index.html"
], ],
"css": [ "css": [
@@ -127,29 +145,29 @@
] ]
}, },
"src/pages/SystemPage.vue": { "src/pages/SystemPage.vue": {
"file": "assets/SystemPage-CbqDATVe.js", "file": "assets/SystemPage-CosZ9Vtj.js",
"name": "SystemPage", "name": "SystemPage",
"src": "src/pages/SystemPage.vue", "src": "src/pages/SystemPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_update-B-ZRn1LV.js",
"index.html" "index.html"
], ],
"css": [ "css": [
"assets/SystemPage-CdLP1b5Y.css" "assets/SystemPage-BjTkcmTG.css"
] ]
}, },
"src/pages/UsersPage.vue": { "src/pages/UsersPage.vue": {
"file": "assets/UsersPage-COyIOdOo.js", "file": "assets/UsersPage-C6sG2ovw.js",
"name": "UsersPage", "name": "UsersPage",
"src": "src/pages/UsersPage.vue", "src": "src/pages/UsersPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_users-BKWiZCGY.js", "_users-CgASQeNW.js",
"_datetime-ZCuLLiQt.js",
"index.html" "index.html"
], ],
"css": [ "css": [
"assets/UsersPage-D2Xg1a62.css" "assets/UsersPage-CbiPbpuj.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

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.page-stack[data-v-f2aa6820]{display:flex;flex-direction:column;gap:12px}.card[data-v-f2aa6820]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-f2aa6820]{margin:0 0 12px;font-size:14px;font-weight:800}.table-wrap[data-v-f2aa6820]{overflow-x:auto}.user-cell[data-v-f2aa6820]{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}

View File

@@ -1 +0,0 @@
import{f as E,a as I,r as A}from"./users-BKWiZCGY.js";import{_ as M,r as p,o as q,c as W,a as i,b as t,w as a,d,i as T,f as F,e as G,g as f,h as r,j as $,k as x,l as H,t as k,E as m,m as g,n as J,p as K}from"./index-DMMQbxWA.js";import{p as L}from"./datetime-ZCuLLiQt.js";const O={class:"page-stack"},Q={class:"app-page-title"},X={class:"table-wrap"},Y={class:"user-cell"},Z={class:"table-wrap"},ee={__name:"PendingPage",setup(te){const B=T("refreshStats",null),_=T("refreshNavBadges",null),v=p([]),c=p([]),w=p(!1),y=p(!1);function D(s){const e=s?.vip_expire_time;if(!e)return!1;if(String(e).startsWith("2099-12-31"))return!0;const o=L(e);return o?o.getTime()>Date.now():!1}async function j(){w.value=!0;try{v.value=await E()}catch{v.value=[]}finally{w.value=!1}}async function h(){y.value=!0;try{c.value=await F()}catch{c.value=[]}finally{y.value=!1}}async function u(){await Promise.all([j(),h()]),await _?.({pendingResets:c.value.length})}async function N(s){try{await m.confirm(`确定通过用户「${s.username}」的注册申请吗?`,"审核通过",{confirmButtonText:"通过",cancelButtonText:"取消",type:"success"})}catch{return}try{await I(s.id),g.success("用户审核通过"),await u(),await B?.()}catch{}}async function U(s){try{await m.confirm(`确定拒绝用户「${s.username}」的注册申请吗?`,"拒绝申请",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{await A(s.id),g.success("已拒绝用户"),await u(),await B?.()}catch{}}async function V(s){try{await m.confirm(`确定批准「${s.username}」的密码重置申请吗?`,"批准重置",{confirmButtonText:"批准",cancelButtonText:"取消",type:"success"})}catch{return}try{const e=await J(s.id);g.success(e?.message||"密码重置申请已批准"),await h(),await _?.({pendingResets:c.value.length})}catch{}}async function z(s){try{await m.confirm(`确定拒绝「${s.username}」的密码重置申请吗?`,"拒绝重置",{confirmButtonText:"拒绝",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await K(s.id);g.success(e?.message||"密码重置申请已拒绝"),await h(),await _?.({pendingResets:c.value.length})}catch{}}return q(u),(s,e)=>{const o=d("el-button"),l=d("el-table-column"),S=d("el-tag"),R=d("el-table"),C=d("el-card"),P=G("loading");return f(),W("div",O,[i("div",Q,[e[1]||(e[1]=i("h2",null,"待审核",-1)),i("div",null,[t(o,{onClick:u},{default:a(()=>[...e[0]||(e[0]=[r("刷新",-1)])]),_:1})])]),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[5]||(e[5]=i("h3",{class:"section-title"},"用户注册审核",-1)),i("div",X,[$((f(),x(R,{data:v.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"ID",width:"80"}),t(l,{label:"用户名","min-width":"200"},{default:a(({row:n})=>[i("div",Y,[i("strong",null,k(n.username),1),D(n)?(f(),x(S,{key:0,type:"warning",effect:"light",size:"small"},{default:a(()=>[...e[2]||(e[2]=[r("VIP",-1)])]),_:1})):H("",!0)])]),_:1}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"注册时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>N(n)},{default:a(()=>[...e[3]||(e[3]=[r("通过",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>U(n)},{default:a(()=>[...e[4]||(e[4]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,w.value]])])]),_:1}),t(C,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:a(()=>[e[8]||(e[8]=i("h3",{class:"section-title"},"密码重置审核",-1)),i("div",Z,[$((f(),x(R,{data:c.value,style:{width:"100%"}},{default:a(()=>[t(l,{prop:"id",label:"申请ID",width:"90"}),t(l,{prop:"username",label:"用户名","min-width":"200"}),t(l,{prop:"email",label:"邮箱","min-width":"220"},{default:a(({row:n})=>[r(k(n.email||"-"),1)]),_:1}),t(l,{prop:"created_at",label:"申请时间","min-width":"180"}),t(l,{label:"操作",width:"180",fixed:"right"},{default:a(({row:n})=>[t(o,{type:"success",size:"small",onClick:b=>V(n)},{default:a(()=>[...e[6]||(e[6]=[r("批准",-1)])]),_:1},8,["onClick"]),t(o,{type:"danger",size:"small",onClick:b=>z(n)},{default:a(()=>[...e[7]||(e[7]=[r("拒绝",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[P,y.value]])])]),_:1})])}}},le=M(ee,[["__scopeId","data-v-f2aa6820"]]);export{le as default};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.page-stack[data-v-e84f7d23]{display:flex;flex-direction:column;gap:12px}.title-group[data-v-e84f7d23]{display:flex;flex-direction:column;gap:2px}.toolbar[data-v-e84f7d23]{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.card[data-v-e84f7d23]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.metric-label[data-v-e84f7d23]{font-size:12px;color:var(--app-muted)}.metric-value[data-v-e84f7d23]{margin-top:6px;font-size:22px;font-weight:900;line-height:1.1}.section-head[data-v-e84f7d23]{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:12px}.section-title[data-v-e84f7d23]{margin:0;font-size:14px;font-weight:800}.kv[data-v-e84f7d23]{border:1px solid var(--app-border);border-radius:12px;padding:12px;background:#fff}.kv-v[data-v-e84f7d23]{font-size:18px;font-weight:900;line-height:1.1}.kv-k[data-v-e84f7d23]{margin-top:6px;font-size:12px}.ok[data-v-e84f7d23]{color:#047857}.warn[data-v-e84f7d23]{color:#b45309}.err[data-v-e84f7d23]{color:#b91c1c}.divider[data-v-e84f7d23]{height:1px;background:var(--app-border);margin:14px 0}.sys-grid[data-v-e84f7d23]{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.sys-item[data-v-e84f7d23]{border:1px solid var(--app-border);border-radius:12px;padding:12px;background:#fff}.sys-k[data-v-e84f7d23]{font-size:12px}.sys-sub[data-v-e84f7d23]{margin-top:8px;font-size:12px}.desc-inline[data-v-e84f7d23]{margin-left:8px}.sub-title[data-v-e84f7d23]{font-size:13px;font-weight:800;margin-bottom:10px}.type-grid[data-v-e84f7d23]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.type-item[data-v-e84f7d23]{border:1px solid var(--app-border);border-radius:12px;padding:12px;background:#fff}.type-v[data-v-e84f7d23]{font-size:16px;font-weight:900}.type-k[data-v-e84f7d23]{margin-top:6px;font-size:12px}.table-wrap[data-v-e84f7d23]{overflow-x:auto}.help[data-v-e84f7d23]{margin-top:10px;font-size:12px}@media(max-width:768px){.sys-grid[data-v-e84f7d23]{grid-template-columns:1fr}}

View File

@@ -1 +1 @@
import{C as m,_ as T,r as p,c as h,a as r,b as a,w as s,d as u,g as k,h as b,m as d,E as x}from"./index-DMMQbxWA.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function E(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function P(){const{data:o}=await m.post("/logout");return o}const U={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),i=p(""),n=p(!1);async function f(){try{await P()}catch{}finally{window.location.href="/yuyx"}}async function V(){const l=t.value.trim();if(!l){d.error("请输入新用户名");return}try{await x.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),d.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){const l=i.value;if(!l){d.error("请输入新密码");return}if(l.length<6){d.error("密码至少6个字符");return}try{await x.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await E(l),d.success("密码修改成功,请重新登录"),i.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",U,[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]=[b("保存用户名",-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:i.value,"onUpdate:modelValue":e[1]||(e[1]=c=>i.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]=[b("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码至少8位且包含字母与数字。",-1))]),_:1})])}}},M=T(N,[["__scopeId","data-v-2f4b840f"]]);export{M as default}; import{D as m,_ as T,r as p,a as h,b as r,d as a,w as s,e as u,h as k,j as b,q as d,E as x}from"./index-Do26tg8I.js";async function C(o){const{data:t}=await m.put("/admin/username",{new_username:o});return t}async function E(o){const{data:t}=await m.put("/admin/password",{new_password:o});return t}async function P(){const{data:o}=await m.post("/logout");return o}const U={class:"page-stack"},N={__name:"SettingsPage",setup(o){const t=p(""),i=p(""),n=p(!1);async function f(){try{await P()}catch{}finally{window.location.href="/yuyx"}}async function V(){const l=t.value.trim();if(!l){d.error("请输入新用户名");return}try{await x.confirm(`确定将管理员用户名修改为「${l}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await C(l),d.success("用户名修改成功,请重新登录"),t.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){const l=i.value;if(!l){d.error("请输入新密码");return}if(l.length<6){d.error("密码至少6个字符");return}try{await x.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await E(l),d.success("密码修改成功,请重新登录"),i.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",U,[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]=[b("保存用户名",-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:i.value,"onUpdate:modelValue":e[1]||(e[1]=c=>i.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]=[b("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码至少8位且包含字母与数字。",-1))]),_:1})])}}},M=T(N,[["__scopeId","data-v-2f4b840f"]]);export{M as default};

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-d88590f1]{display:flex;flex-direction:column;gap:12px}.card[data-v-d88590f1]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-d88590f1]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-d88590f1]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-d88590f1]{display:flex;flex-wrap:wrap;gap:10px}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.page-stack[data-v-3b98b1dc]{display:flex;flex-direction:column;gap:12px}.card[data-v-3b98b1dc]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-3b98b1dc]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-3b98b1dc]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-3b98b1dc]{display:flex;flex-wrap:wrap;gap:10px}

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

View File

@@ -0,0 +1 @@
.page-stack[data-v-84b2f73a]{display:flex;flex-direction:column;gap:12px}.card[data-v-84b2f73a]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.section-title[data-v-84b2f73a]{margin:0 0 12px;font-size:14px;font-weight:800}.help[data-v-84b2f73a]{margin-top:10px;font-size:12px}.table-wrap[data-v-84b2f73a]{overflow-x:auto}.user-block[data-v-84b2f73a]{display:flex;flex-direction:column;gap:2px}.user-main[data-v-84b2f73a]{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}.user-sub[data-v-84b2f73a]{font-size:12px}.vip-sub[data-v-84b2f73a]{font-size:12px;color:#7c3aed}.actions[data-v-84b2f73a]{display:flex;flex-wrap:wrap;gap:8px}

View File

@@ -1 +0,0 @@
.page-stack[data-v-e62c5723]{display:flex;flex-direction:column;gap:12px}.card[data-v-e62c5723]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.table-wrap[data-v-e62c5723]{overflow-x:auto}.user-block[data-v-e62c5723]{display:flex;flex-direction:column;gap:2px}.user-main[data-v-e62c5723]{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}.user-sub[data-v-e62c5723]{font-size:12px}.vip-sub[data-v-e62c5723]{font-size:12px;color:#7c3aed}.actions[data-v-e62c5723]{display:flex;flex-wrap:wrap;gap:8px}

View File

@@ -1 +0,0 @@
function s(n){if(!n)return null;if(n instanceof Date)return n;let e=String(n).trim();if(!e)return null;/^\d{4}-\d{2}-\d{2}$/.test(e)&&(e=`${e}T00:00:00`);let t=e.includes("T")?e:e.replace(" ","T");t=t.replace(/\.(\d{3})\d+/,".$1"),/([zZ]|[+-]\d{2}:\d{2})$/.test(t)||(t=`${t}+08:00`);const i=new Date(t);return Number.isNaN(i.getTime())?null:i}export{s as p};

View File

@@ -0,0 +1 @@
import{D as n}from"./index-Do26tg8I.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{i as a,o 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 +0,0 @@
import{C as e}from"./index-DMMQbxWA.js";async function l(){const{data:t}=await e.get("/server/info");return t}async function d(){const{data:t}=await e.get("/docker_stats");return t}async function p(){const{data:t}=await e.get("/task/stats");return t}async function f(){const{data:t}=await e.get("/task/running");return t}async function g(t){const{data:a}=await e.get("/task/logs",{params:t});return a}async function h(t){const{data:a}=await e.post("/task/logs/clear",{days:t});return a}function r(t){return String(t||"").trim()}function c(t){return!t.startsWith("user_scheduled")||!t.includes(":")?"":t.split(":",2)[1]||""}function m(t){const a=r(t);if(!a||a==="manual")return{group:"manual",label:"手动",type:"success",tooltip:""};if(a==="scheduled")return{group:"scheduled",label:"定时任务",type:"primary",tooltip:"系统定时"};if(a.startsWith("user_scheduled")){const n=c(a),s=String(n||"").replace(/^batch_/,"");return{group:"scheduled",label:"定时任务",type:"primary",tooltip:s?`用户定时批次:${s}`:"用户定时"}}return{group:"manual",label:"手动",type:"success",tooltip:{batch:"手动批量",manual_screenshot:"手动截图",immediate:"立即执行",resumed:"断点恢复"}[a]||a}}export{d as a,p as b,f as c,g as d,h as e,l as f,m as g};

View File

@@ -0,0 +1 @@
function s(t){return String(t||"").trim()}function a(t){return!t.startsWith("user_scheduled")||!t.includes(":")?"":t.split(":",2)[1]||""}function i(t){const e=s(t);if(!e||e==="manual")return{group:"manual",label:"手动",type:"success",tooltip:""};if(e==="scheduled")return{group:"scheduled",label:"定时任务",type:"primary",tooltip:"系统定时"};if(e.startsWith("user_scheduled")){const u=a(e),r=String(u||"").replace(/^batch_/,"");return{group:"scheduled",label:"定时任务",type:"primary",tooltip:r?`用户定时批次:${r}`:"用户定时"}}return{group:"manual",label:"手动",type:"success",tooltip:{batch:"手动批量",manual_screenshot:"手动截图",immediate:"立即执行",resumed:"断点恢复"}[e]||e}}export{i as g};

View File

@@ -0,0 +1 @@
import{D as a}from"./index-Do26tg8I.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

@@ -0,0 +1 @@
import{D as a}from"./index-Do26tg8I.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 +0,0 @@
import{C as a}from"./index-DMMQbxWA.js";async function r(){const{data:s}=await a.get("/users");return s}async function c(){const{data:s}=await a.get("/users/pending");return s}async function o(s){const{data:t}=await a.post(`/users/${s}/approve`);return t}async function i(s){const{data:t}=await a.post(`/users/${s}/reject`);return t}async function u(s){const{data:t}=await a.delete(`/users/${s}`);return t}async function d(s,t){const{data:e}=await a.post(`/users/${s}/vip`,{days:t});return e}async function p(s){const{data:t}=await a.delete(`/users/${s}/vip`);return t}async function f(s,t){const{data:e}=await a.post(`/users/${s}/reset_password`,{new_password:t});return e}export{o as a,r as b,p as c,f as d,u as e,c as f,i as r,d as s};

View File

@@ -0,0 +1 @@
import{D as t}from"./index-Do26tg8I.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" /> <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-DMMQbxWA.js"></script> <script type="module" crossorigin src="./assets/index-Do26tg8I.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BIDpnzAs.css"> <link rel="stylesheet" crossorigin href="./assets/index-C73IFBwi.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@@ -1,13 +1,13 @@
{ {
"_accounts-CDwCkd9q.js": { "_accounts-nj5kn4js.js": {
"file": "assets/accounts-CDwCkd9q.js", "file": "assets/accounts-nj5kn4js.js",
"name": "accounts", "name": "accounts",
"imports": [ "imports": [
"index.html" "index.html"
] ]
}, },
"_auth-U2Kna0qf.js": { "_auth-B-8S3dtx.js": {
"file": "assets/auth-U2Kna0qf.js", "file": "assets/auth-B-8S3dtx.js",
"name": "auth", "name": "auth",
"imports": [ "imports": [
"index.html" "index.html"
@@ -18,7 +18,7 @@
"name": "password" "name": "password"
}, },
"index.html": { "index.html": {
"file": "assets/index-BzB0auqv.js", "file": "assets/index-oLrocmqc.js",
"name": "index", "name": "index",
"src": "index.html", "src": "index.html",
"isEntry": true, "isEntry": true,
@@ -36,12 +36,12 @@
] ]
}, },
"src/pages/AccountsPage.vue": { "src/pages/AccountsPage.vue": {
"file": "assets/AccountsPage-BRy_ANjo.js", "file": "assets/AccountsPage-Ci46ulXO.js",
"name": "AccountsPage", "name": "AccountsPage",
"src": "src/pages/AccountsPage.vue", "src": "src/pages/AccountsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_accounts-CDwCkd9q.js", "_accounts-nj5kn4js.js",
"index.html" "index.html"
], ],
"css": [ "css": [
@@ -49,13 +49,13 @@
] ]
}, },
"src/pages/LoginPage.vue": { "src/pages/LoginPage.vue": {
"file": "assets/LoginPage-DQyjJkcn.js", "file": "assets/LoginPage-CSQ5MWiZ.js",
"name": "LoginPage", "name": "LoginPage",
"src": "src/pages/LoginPage.vue", "src": "src/pages/LoginPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"index.html", "index.html",
"_auth-U2Kna0qf.js", "_auth-B-8S3dtx.js",
"_password-7ryi82gE.js" "_password-7ryi82gE.js"
], ],
"css": [ "css": [
@@ -63,26 +63,26 @@
] ]
}, },
"src/pages/RegisterPage.vue": { "src/pages/RegisterPage.vue": {
"file": "assets/RegisterPage-Bbjpgkc1.js", "file": "assets/RegisterPage-DMaHEdd1.js",
"name": "RegisterPage", "name": "RegisterPage",
"src": "src/pages/RegisterPage.vue", "src": "src/pages/RegisterPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"index.html", "index.html",
"_auth-U2Kna0qf.js" "_auth-B-8S3dtx.js"
], ],
"css": [ "css": [
"assets/RegisterPage-CVjBOq6i.css" "assets/RegisterPage-yylt2w7b.css"
] ]
}, },
"src/pages/ResetPasswordPage.vue": { "src/pages/ResetPasswordPage.vue": {
"file": "assets/ResetPasswordPage-qMRn5I3l.js", "file": "assets/ResetPasswordPage-lLB2G9rF.js",
"name": "ResetPasswordPage", "name": "ResetPasswordPage",
"src": "src/pages/ResetPasswordPage.vue", "src": "src/pages/ResetPasswordPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"index.html", "index.html",
"_auth-U2Kna0qf.js", "_auth-B-8S3dtx.js",
"_password-7ryi82gE.js" "_password-7ryi82gE.js"
], ],
"css": [ "css": [
@@ -90,12 +90,12 @@
] ]
}, },
"src/pages/SchedulesPage.vue": { "src/pages/SchedulesPage.vue": {
"file": "assets/SchedulesPage-BUgXa5ne.js", "file": "assets/SchedulesPage-BKXmBVuW.js",
"name": "SchedulesPage", "name": "SchedulesPage",
"src": "src/pages/SchedulesPage.vue", "src": "src/pages/SchedulesPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
"imports": [ "imports": [
"_accounts-CDwCkd9q.js", "_accounts-nj5kn4js.js",
"index.html" "index.html"
], ],
"css": [ "css": [
@@ -103,7 +103,7 @@
] ]
}, },
"src/pages/ScreenshotsPage.vue": { "src/pages/ScreenshotsPage.vue": {
"file": "assets/ScreenshotsPage-C_eU28nf.js", "file": "assets/ScreenshotsPage-4zEBaZjf.js",
"name": "ScreenshotsPage", "name": "ScreenshotsPage",
"src": "src/pages/ScreenshotsPage.vue", "src": "src/pages/ScreenshotsPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -115,7 +115,7 @@
] ]
}, },
"src/pages/VerifyResultPage.vue": { "src/pages/VerifyResultPage.vue": {
"file": "assets/VerifyResultPage-xHPsEifw.js", "file": "assets/VerifyResultPage-BuyLCSRI.js",
"name": "VerifyResultPage", "name": "VerifyResultPage",
"src": "src/pages/VerifyResultPage.vue", "src": "src/pages/VerifyResultPage.vue",
"isDynamicEntry": true, "isDynamicEntry": true,

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{_ as M,r as j,a as p,c as B,o as A,b as S,d as t,w as o,e as m,u as H,f as g,g as n,h as U,i as x,j as N,t as q,k as E,E as d}from"./index-BzB0auqv.js";import{g as z,f as F,c as G}from"./auth-U2Kna0qf.js";const J={class:"auth-wrap"},O={class:"hint app-muted"},Q={class:"captcha-row"},W=["src"],X={class:"actions"},Y={__name:"RegisterPage",setup(Z){const T=H(),a=j({username:"",password:"",confirm_password:"",email:"",captcha:""}),v=p(!1),f=p(""),b=p(""),h=p(!1),l=p(""),_=p(""),V=p(""),K=B(()=>v.value?"邮箱 *":"邮箱(可选)"),P=B(()=>v.value?"必填,用于账号验证":"选填,用于接收审核通知");async function w(){try{const u=await z();b.value=u?.session_id||"",f.value=u?.captcha_image||"",a.captcha=""}catch{b.value="",f.value=""}}async function R(){try{const u=await F();v.value=!!u?.register_verify_enabled}catch{v.value=!1}}function D(){l.value="",_.value="",V.value=""}async function k(){D();const u=a.username.trim(),e=a.password,y=a.confirm_password,s=a.email.trim(),i=a.captcha.trim();if(u.length<3){l.value="用户名至少3个字符",d.error(l.value);return}if(e.length<6){l.value="密码至少6个字符",d.error(l.value);return}if(e!==y){l.value="两次输入的密码不一致",d.error(l.value);return}if(v.value&&!s){l.value="请填写邮箱地址用于账号验证",d.error(l.value);return}if(s&&!s.includes("@")){l.value="邮箱格式不正确",d.error(l.value);return}if(!i){l.value="请输入验证码",d.error(l.value);return}h.value=!0;try{const c=await G({username:u,password:e,email:s,captcha_session:b.value,captcha:i});_.value=c?.message||"注册成功",V.value=c?.need_verify?"请检查您的邮箱(包括垃圾邮件文件夹)":"",d.success("注册成功"),a.username="",a.password="",a.confirm_password="",a.email="",a.captcha="",setTimeout(()=>{window.location.href="/login"},3e3)}catch(c){const C=c?.response?.data;l.value=C?.error||"注册失败",d.error(l.value),await w()}finally{h.value=!1}}function I(){T.push("/login")}return A(async()=>{await w(),await R()}),(u,e)=>{const y=m("el-alert"),s=m("el-input"),i=m("el-form-item"),c=m("el-button"),C=m("el-form"),L=m("el-card");return g(),S("div",J,[t(L,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:o(()=>[e[11]||(e[11]=n("div",{class:"brand"},[n("div",{class:"brand-title"},"知识管理平台"),n("div",{class:"brand-sub app-muted"},"用户注册")],-1)),l.value?(g(),U(y,{key:0,type:"error",closable:!1,title:l.value,"show-icon":"",class:"alert"},null,8,["title"])):x("",!0),_.value?(g(),U(y,{key:1,type:"success",closable:!1,title:_.value,description:V.value,"show-icon":"",class:"alert"},null,8,["title","description"])):x("",!0),t(C,{"label-position":"top"},{default:o(()=>[t(i,{label:"用户名 *"},{default:o(()=>[t(s,{modelValue:a.username,"onUpdate:modelValue":e[0]||(e[0]=r=>a.username=r),placeholder:"至少3个字符",autocomplete:"username"},null,8,["modelValue"]),e[5]||(e[5]=n("div",{class:"hint app-muted"},"至少3个字符",-1))]),_:1}),t(i,{label:"密码 *"},{default:o(()=>[t(s,{modelValue:a.password,"onUpdate:modelValue":e[1]||(e[1]=r=>a.password=r),type:"password","show-password":"",placeholder:"至少6个字符",autocomplete:"new-password"},null,8,["modelValue"]),e[6]||(e[6]=n("div",{class:"hint app-muted"},"至少6个字符",-1))]),_:1}),t(i,{label:"确认密码 *"},{default:o(()=>[t(s,{modelValue:a.confirm_password,"onUpdate:modelValue":e[2]||(e[2]=r=>a.confirm_password=r),type:"password","show-password":"",placeholder:"请再次输入密码",autocomplete:"new-password",onKeyup:N(k,["enter"])},null,8,["modelValue"])]),_:1}),t(i,{label:K.value},{default:o(()=>[t(s,{modelValue:a.email,"onUpdate:modelValue":e[3]||(e[3]=r=>a.email=r),placeholder:"name@example.com",autocomplete:"email"},null,8,["modelValue"]),n("div",O,q(P.value),1)]),_:1},8,["label"]),t(i,{label:"验证码 *"},{default:o(()=>[n("div",Q,[t(s,{modelValue:a.captcha,"onUpdate:modelValue":e[4]||(e[4]=r=>a.captcha=r),placeholder:"请输入验证码",onKeyup:N(k,["enter"])},null,8,["modelValue"]),f.value?(g(),S("img",{key:0,class:"captcha-img",src:f.value,alt:"验证码",title:"点击刷新",onClick:w},null,8,W)):x("",!0),t(c,{onClick:w},{default:o(()=>[...e[7]||(e[7]=[E("刷新",-1)])]),_:1})])]),_:1})]),_:1}),t(c,{type:"primary",class:"submit-btn",loading:h.value,onClick:k},{default:o(()=>[...e[8]||(e[8]=[E("注册",-1)])]),_:1},8,["loading"]),n("div",X,[e[10]||(e[10]=n("span",{class:"app-muted"},"已有账号?",-1)),t(c,{link:"",type:"primary",onClick:I},{default:o(()=>[...e[9]||(e[9]=[E("立即登录",-1)])]),_:1})])]),_:1})])}}},ae=M(Y,[["__scopeId","data-v-32684b4d"]]);export{ae as default};

View File

@@ -1 +0,0 @@
.auth-wrap[data-v-32684b4d]{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}.auth-card[data-v-32684b4d]{width:100%;max-width:420px;border-radius:var(--app-radius);border:1px solid var(--app-border);box-shadow:var(--app-shadow)}.brand[data-v-32684b4d]{margin-bottom:14px}.brand-title[data-v-32684b4d]{font-size:18px;font-weight:900}.brand-sub[data-v-32684b4d]{margin-top:4px;font-size:12px}.alert[data-v-32684b4d]{margin-bottom:12px}.hint[data-v-32684b4d]{margin-top:6px;font-size:12px}.captcha-row[data-v-32684b4d]{display:flex;align-items:center;gap:10px;width:100%}.captcha-img[data-v-32684b4d]{height:40px;border:1px solid var(--app-border);border-radius:8px;cursor:pointer;-webkit-user-select:none;user-select:none}.submit-btn[data-v-32684b4d]{width:100%;margin-top:4px}.actions[data-v-32684b4d]{margin-top:14px;display:flex;align-items:center;justify-content:center;gap:6px}

View File

@@ -0,0 +1 @@
import{_ as M,r as j,a as p,c as B,o as A,b as S,d as t,w as o,e as m,u as H,f as g,g as n,h as U,i as x,j as N,t as q,k as E,E as d}from"./index-oLrocmqc.js";import{g as z,f as F,c as G}from"./auth-B-8S3dtx.js";const J={class:"auth-wrap"},O={class:"hint app-muted"},Q={class:"captcha-row"},W=["src"],X={class:"actions"},Y={__name:"RegisterPage",setup(Z){const T=H(),a=j({username:"",password:"",confirm_password:"",email:"",captcha:""}),v=p(!1),f=p(""),h=p(""),b=p(!1),l=p(""),_=p(""),V=p(""),K=B(()=>v.value?"邮箱 *":"邮箱(可选)"),P=B(()=>v.value?"必填,用于账号验证":"选填,用于找回密码和接收通知");async function w(){try{const u=await z();h.value=u?.session_id||"",f.value=u?.captcha_image||"",a.captcha=""}catch{h.value="",f.value=""}}async function R(){try{const u=await F();v.value=!!u?.register_verify_enabled}catch{v.value=!1}}function D(){l.value="",_.value="",V.value=""}async function k(){D();const u=a.username.trim(),e=a.password,y=a.confirm_password,s=a.email.trim(),i=a.captcha.trim();if(u.length<3){l.value="用户名至少3个字符",d.error(l.value);return}if(e.length<6){l.value="密码至少6个字符",d.error(l.value);return}if(e!==y){l.value="两次输入的密码不一致",d.error(l.value);return}if(v.value&&!s){l.value="请填写邮箱地址用于账号验证",d.error(l.value);return}if(s&&!s.includes("@")){l.value="邮箱格式不正确",d.error(l.value);return}if(!i){l.value="请输入验证码",d.error(l.value);return}b.value=!0;try{const c=await G({username:u,password:e,email:s,captcha_session:h.value,captcha:i});_.value=c?.message||"注册成功",V.value=c?.need_verify?"请检查您的邮箱(包括垃圾邮件文件夹)":"",d.success("注册成功"),a.username="",a.password="",a.confirm_password="",a.email="",a.captcha="",setTimeout(()=>{window.location.href="/login"},3e3)}catch(c){const C=c?.response?.data;l.value=C?.error||"注册失败",d.error(l.value),await w()}finally{b.value=!1}}function I(){T.push("/login")}return A(async()=>{await w(),await R()}),(u,e)=>{const y=m("el-alert"),s=m("el-input"),i=m("el-form-item"),c=m("el-button"),C=m("el-form"),L=m("el-card");return g(),S("div",J,[t(L,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:o(()=>[e[11]||(e[11]=n("div",{class:"brand"},[n("div",{class:"brand-title"},"知识管理平台"),n("div",{class:"brand-sub app-muted"},"用户注册")],-1)),l.value?(g(),U(y,{key:0,type:"error",closable:!1,title:l.value,"show-icon":"",class:"alert"},null,8,["title"])):x("",!0),_.value?(g(),U(y,{key:1,type:"success",closable:!1,title:_.value,description:V.value,"show-icon":"",class:"alert"},null,8,["title","description"])):x("",!0),t(C,{"label-position":"top"},{default:o(()=>[t(i,{label:"用户名 *"},{default:o(()=>[t(s,{modelValue:a.username,"onUpdate:modelValue":e[0]||(e[0]=r=>a.username=r),placeholder:"至少3个字符",autocomplete:"username"},null,8,["modelValue"]),e[5]||(e[5]=n("div",{class:"hint app-muted"},"至少3个字符",-1))]),_:1}),t(i,{label:"密码 *"},{default:o(()=>[t(s,{modelValue:a.password,"onUpdate:modelValue":e[1]||(e[1]=r=>a.password=r),type:"password","show-password":"",placeholder:"至少6个字符",autocomplete:"new-password"},null,8,["modelValue"]),e[6]||(e[6]=n("div",{class:"hint app-muted"},"至少6个字符",-1))]),_:1}),t(i,{label:"确认密码 *"},{default:o(()=>[t(s,{modelValue:a.confirm_password,"onUpdate:modelValue":e[2]||(e[2]=r=>a.confirm_password=r),type:"password","show-password":"",placeholder:"请再次输入密码",autocomplete:"new-password",onKeyup:N(k,["enter"])},null,8,["modelValue"])]),_:1}),t(i,{label:K.value},{default:o(()=>[t(s,{modelValue:a.email,"onUpdate:modelValue":e[3]||(e[3]=r=>a.email=r),placeholder:"name@example.com",autocomplete:"email"},null,8,["modelValue"]),n("div",O,q(P.value),1)]),_:1},8,["label"]),t(i,{label:"验证码 *"},{default:o(()=>[n("div",Q,[t(s,{modelValue:a.captcha,"onUpdate:modelValue":e[4]||(e[4]=r=>a.captcha=r),placeholder:"请输入验证码",onKeyup:N(k,["enter"])},null,8,["modelValue"]),f.value?(g(),S("img",{key:0,class:"captcha-img",src:f.value,alt:"验证码",title:"点击刷新",onClick:w},null,8,W)):x("",!0),t(c,{onClick:w},{default:o(()=>[...e[7]||(e[7]=[E("刷新",-1)])]),_:1})])]),_:1})]),_:1}),t(c,{type:"primary",class:"submit-btn",loading:b.value,onClick:k},{default:o(()=>[...e[8]||(e[8]=[E("注册",-1)])]),_:1},8,["loading"]),n("div",X,[e[10]||(e[10]=n("span",{class:"app-muted"},"已有账号?",-1)),t(c,{link:"",type:"primary",onClick:I},{default:o(()=>[...e[9]||(e[9]=[E("立即登录",-1)])]),_:1})])]),_:1})])}}},ae=M(Y,[["__scopeId","data-v-75731a6d"]]);export{ae as default};

View File

@@ -0,0 +1 @@
.auth-wrap[data-v-75731a6d]{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}.auth-card[data-v-75731a6d]{width:100%;max-width:420px;border-radius:var(--app-radius);border:1px solid var(--app-border);box-shadow:var(--app-shadow)}.brand[data-v-75731a6d]{margin-bottom:14px}.brand-title[data-v-75731a6d]{font-size:18px;font-weight:900}.brand-sub[data-v-75731a6d]{margin-top:4px;font-size:12px}.alert[data-v-75731a6d]{margin-bottom:12px}.hint[data-v-75731a6d]{margin-top:6px;font-size:12px}.captcha-row[data-v-75731a6d]{display:flex;align-items:center;gap:10px;width:100%}.captcha-img[data-v-75731a6d]{height:40px;border:1px solid var(--app-border);border-radius:8px;cursor:pointer;-webkit-user-select:none;user-select:none}.submit-btn[data-v-75731a6d]{width:100%;margin-top:4px}.actions[data-v-75731a6d]{margin-top:14px;display:flex;align-items:center;justify-content:center;gap:6px}

View File

@@ -1 +1 @@
import{_ as L,a as n,l as M,r as U,c as j,o as F,m as K,b as v,d as s,w as a,e as l,u as D,f as m,g as w,F as T,k,h as q,i as x,j as z,t as G,E as y}from"./index-BzB0auqv.js";import{d as H}from"./auth-U2Kna0qf.js";import{v as J}from"./password-7ryi82gE.js";const O={class:"auth-wrap"},Q={class:"actions"},W={class:"actions"},X={key:0,class:"app-muted"},Y={__name:"ResetPasswordPage",setup(Z){const B=M(),A=D(),r=n(String(B.params.token||"")),i=n(!0),b=n(""),t=U({newPassword:"",confirmPassword:""}),g=n(!1),f=n(""),d=n(0);let u=null;function C(){if(typeof window>"u")return null;const o=window.__APP_INITIAL_STATE__;return!o||typeof o!="object"?null:(window.__APP_INITIAL_STATE__=null,o)}const I=j(()=>!!(i.value&&r.value&&!f.value));function S(){A.push("/login")}function N(){d.value=3,u=window.setInterval(()=>{d.value-=1,d.value<=0&&(window.clearInterval(u),u=null,window.location.href="/login")},1e3)}async function V(){if(!I.value)return;const o=t.newPassword,e=t.confirmPassword,c=J(o);if(!c.ok){y.error(c.message);return}if(o!==e){y.error("两次输入的密码不一致");return}g.value=!0;try{await H({token:r.value,new_password:o}),f.value="密码重置成功3秒后跳转到登录页面...",y.success("密码重置成功"),N()}catch(p){const _=p?.response?.data;y.error(_?.error||"重置失败")}finally{g.value=!1}}return F(()=>{const o=C();o?.page==="reset_password"?(r.value=String(o?.token||r.value||""),i.value=!!o?.valid,b.value=o?.error_message||(i.value?"":"重置链接无效或已过期,请重新申请密码重置")):r.value||(i.value=!1,b.value="重置链接无效或已过期,请重新申请密码重置")}),K(()=>{u&&window.clearInterval(u)}),(o,e)=>{const c=l("el-alert"),p=l("el-button"),_=l("el-input"),h=l("el-form-item"),R=l("el-form"),E=l("el-card");return m(),v("div",O,[s(E,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:a(()=>[e[5]||(e[5]=w("div",{class:"brand"},[w("div",{class:"brand-title"},"知识管理平台"),w("div",{class:"brand-sub app-muted"},"重置密码")],-1)),i.value?(m(),v(T,{key:1},[f.value?(m(),q(c,{key:0,type:"success",closable:!1,title:"重置成功",description:f.value,"show-icon":"",class:"alert"},null,8,["description"])):x("",!0),s(R,{"label-position":"top"},{default:a(()=>[s(h,{label:"新密码至少8位且包含字母和数字"},{default:a(()=>[s(_,{modelValue:t.newPassword,"onUpdate:modelValue":e[0]||(e[0]=P=>t.newPassword=P),type:"password","show-password":"",placeholder:"请输入新密码",autocomplete:"new-password"},null,8,["modelValue"])]),_:1}),s(h,{label:"确认密码"},{default:a(()=>[s(_,{modelValue:t.confirmPassword,"onUpdate:modelValue":e[1]||(e[1]=P=>t.confirmPassword=P),type:"password","show-password":"",placeholder:"请再次输入新密码",autocomplete:"new-password",onKeyup:z(V,["enter"])},null,8,["modelValue"])]),_:1})]),_:1}),s(p,{type:"primary",class:"submit-btn",loading:g.value,disabled:!I.value,onClick:V},{default:a(()=>[...e[3]||(e[3]=[k(" 确认重置 ",-1)])]),_:1},8,["loading","disabled"]),w("div",W,[s(p,{link:"",type:"primary",onClick:S},{default:a(()=>[...e[4]||(e[4]=[k("返回登录",-1)])]),_:1}),d.value>0?(m(),v("span",X,G(d.value)+" 秒后自动跳转…",1)):x("",!0)])],64)):(m(),v(T,{key:0},[s(c,{type:"error",closable:!1,title:"链接已失效",description:b.value,"show-icon":""},null,8,["description"]),w("div",Q,[s(p,{type:"primary",onClick:S},{default:a(()=>[...e[2]||(e[2]=[k("返回登录",-1)])]),_:1})])],64))]),_:1})])}}},se=L(Y,[["__scopeId","data-v-0bbb511c"]]);export{se as default}; import{_ as L,a as n,l as M,r as U,c as j,o as F,m as K,b as v,d as s,w as a,e as l,u as D,f as m,g as w,F as T,k,h as q,i as x,j as z,t as G,E as y}from"./index-oLrocmqc.js";import{d as H}from"./auth-B-8S3dtx.js";import{v as J}from"./password-7ryi82gE.js";const O={class:"auth-wrap"},Q={class:"actions"},W={class:"actions"},X={key:0,class:"app-muted"},Y={__name:"ResetPasswordPage",setup(Z){const B=M(),A=D(),r=n(String(B.params.token||"")),i=n(!0),b=n(""),t=U({newPassword:"",confirmPassword:""}),g=n(!1),f=n(""),d=n(0);let u=null;function C(){if(typeof window>"u")return null;const o=window.__APP_INITIAL_STATE__;return!o||typeof o!="object"?null:(window.__APP_INITIAL_STATE__=null,o)}const I=j(()=>!!(i.value&&r.value&&!f.value));function S(){A.push("/login")}function N(){d.value=3,u=window.setInterval(()=>{d.value-=1,d.value<=0&&(window.clearInterval(u),u=null,window.location.href="/login")},1e3)}async function V(){if(!I.value)return;const o=t.newPassword,e=t.confirmPassword,c=J(o);if(!c.ok){y.error(c.message);return}if(o!==e){y.error("两次输入的密码不一致");return}g.value=!0;try{await H({token:r.value,new_password:o}),f.value="密码重置成功3秒后跳转到登录页面...",y.success("密码重置成功"),N()}catch(p){const _=p?.response?.data;y.error(_?.error||"重置失败")}finally{g.value=!1}}return F(()=>{const o=C();o?.page==="reset_password"?(r.value=String(o?.token||r.value||""),i.value=!!o?.valid,b.value=o?.error_message||(i.value?"":"重置链接无效或已过期,请重新申请密码重置")):r.value||(i.value=!1,b.value="重置链接无效或已过期,请重新申请密码重置")}),K(()=>{u&&window.clearInterval(u)}),(o,e)=>{const c=l("el-alert"),p=l("el-button"),_=l("el-input"),h=l("el-form-item"),R=l("el-form"),E=l("el-card");return m(),v("div",O,[s(E,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:a(()=>[e[5]||(e[5]=w("div",{class:"brand"},[w("div",{class:"brand-title"},"知识管理平台"),w("div",{class:"brand-sub app-muted"},"重置密码")],-1)),i.value?(m(),v(T,{key:1},[f.value?(m(),q(c,{key:0,type:"success",closable:!1,title:"重置成功",description:f.value,"show-icon":"",class:"alert"},null,8,["description"])):x("",!0),s(R,{"label-position":"top"},{default:a(()=>[s(h,{label:"新密码至少8位且包含字母和数字"},{default:a(()=>[s(_,{modelValue:t.newPassword,"onUpdate:modelValue":e[0]||(e[0]=P=>t.newPassword=P),type:"password","show-password":"",placeholder:"请输入新密码",autocomplete:"new-password"},null,8,["modelValue"])]),_:1}),s(h,{label:"确认密码"},{default:a(()=>[s(_,{modelValue:t.confirmPassword,"onUpdate:modelValue":e[1]||(e[1]=P=>t.confirmPassword=P),type:"password","show-password":"",placeholder:"请再次输入新密码",autocomplete:"new-password",onKeyup:z(V,["enter"])},null,8,["modelValue"])]),_:1})]),_:1}),s(p,{type:"primary",class:"submit-btn",loading:g.value,disabled:!I.value,onClick:V},{default:a(()=>[...e[3]||(e[3]=[k(" 确认重置 ",-1)])]),_:1},8,["loading","disabled"]),w("div",W,[s(p,{link:"",type:"primary",onClick:S},{default:a(()=>[...e[4]||(e[4]=[k("返回登录",-1)])]),_:1}),d.value>0?(m(),v("span",X,G(d.value)+" 秒后自动跳转…",1)):x("",!0)])],64)):(m(),v(T,{key:0},[s(c,{type:"error",closable:!1,title:"链接已失效",description:b.value,"show-icon":""},null,8,["description"]),w("div",Q,[s(p,{type:"primary",onClick:S},{default:a(()=>[...e[2]||(e[2]=[k("返回登录",-1)])]),_:1})])],64))]),_:1})])}}},se=L(Y,[["__scopeId","data-v-0bbb511c"]]);export{se as default};

View File

@@ -1 +1 @@
import{_ as U,a as o,c as I,o as E,m as R,b as k,d as i,w as s,e as d,u as W,f as _,g as l,i as B,h as $,k as T,t as v}from"./index-BzB0auqv.js";const j={class:"auth-wrap"},z={class:"actions"},D={key:0,class:"countdown app-muted"},M={__name:"VerifyResultPage",setup(q){const x=W(),p=o(!1),f=o(""),m=o(""),w=o(""),y=o(""),r=o(""),u=o(""),c=o(""),n=o(0);let a=null;function C(){if(typeof window>"u")return null;const e=window.__APP_INITIAL_STATE__;return!e||typeof e!="object"?null:(window.__APP_INITIAL_STATE__=null,e)}function N(e){const t=!!e?.success;p.value=t,f.value=e?.title||(t?"验证成功":"验证失败"),m.value=e?.message||e?.error_message||(t?"操作已完成,现在可以继续使用系统。":"操作失败,请稍后重试。"),w.value=e?.primary_label||(t?"立即登录":"重新注册"),y.value=e?.primary_url||(t?"/login":"/register"),r.value=e?.secondary_label||(t?"":"返回登录"),u.value=e?.secondary_url||(t?"":"/login"),c.value=e?.redirect_url||(t?"/login":""),n.value=Number(e?.redirect_seconds||(t?5:0))||0}const A=I(()=>!!(r.value&&u.value)),b=I(()=>!!(c.value&&n.value>0));async function g(e){if(e){if(e.startsWith("http://")||e.startsWith("https://")){window.location.href=e;return}await x.push(e)}}function P(){b.value&&(a=window.setInterval(()=>{n.value-=1,n.value<=0&&(window.clearInterval(a),a=null,window.location.href=c.value)},1e3))}return E(()=>{const e=C();N(e),P()}),R(()=>{a&&window.clearInterval(a)}),(e,t)=>{const h=d("el-button"),V=d("el-result"),L=d("el-card");return _(),k("div",j,[i(L,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:s(()=>[t[2]||(t[2]=l("div",{class:"brand"},[l("div",{class:"brand-title"},"知识管理平台"),l("div",{class:"brand-sub app-muted"},"验证结果")],-1)),i(V,{icon:p.value?"success":"error",title:f.value,"sub-title":m.value,class:"result"},{extra:s(()=>[l("div",z,[i(h,{type:"primary",onClick:t[0]||(t[0]=S=>g(y.value))},{default:s(()=>[T(v(w.value),1)]),_:1}),A.value?(_(),$(h,{key:0,onClick:t[1]||(t[1]=S=>g(u.value))},{default:s(()=>[T(v(r.value),1)]),_:1})):B("",!0)]),b.value?(_(),k("div",D,v(n.value)+" 秒后自动跳转... ",1)):B("",!0)]),_:1},8,["icon","title","sub-title"])]),_:1})])}}},G=U(M,[["__scopeId","data-v-1fc6b081"]]);export{G as default}; import{_ as U,a as o,c as I,o as E,m as R,b as k,d as i,w as s,e as d,u as W,f as _,g as l,i as B,h as $,k as T,t as v}from"./index-oLrocmqc.js";const j={class:"auth-wrap"},z={class:"actions"},D={key:0,class:"countdown app-muted"},M={__name:"VerifyResultPage",setup(q){const x=W(),p=o(!1),f=o(""),m=o(""),w=o(""),y=o(""),r=o(""),u=o(""),c=o(""),n=o(0);let a=null;function C(){if(typeof window>"u")return null;const e=window.__APP_INITIAL_STATE__;return!e||typeof e!="object"?null:(window.__APP_INITIAL_STATE__=null,e)}function N(e){const t=!!e?.success;p.value=t,f.value=e?.title||(t?"验证成功":"验证失败"),m.value=e?.message||e?.error_message||(t?"操作已完成,现在可以继续使用系统。":"操作失败,请稍后重试。"),w.value=e?.primary_label||(t?"立即登录":"重新注册"),y.value=e?.primary_url||(t?"/login":"/register"),r.value=e?.secondary_label||(t?"":"返回登录"),u.value=e?.secondary_url||(t?"":"/login"),c.value=e?.redirect_url||(t?"/login":""),n.value=Number(e?.redirect_seconds||(t?5:0))||0}const A=I(()=>!!(r.value&&u.value)),b=I(()=>!!(c.value&&n.value>0));async function g(e){if(e){if(e.startsWith("http://")||e.startsWith("https://")){window.location.href=e;return}await x.push(e)}}function P(){b.value&&(a=window.setInterval(()=>{n.value-=1,n.value<=0&&(window.clearInterval(a),a=null,window.location.href=c.value)},1e3))}return E(()=>{const e=C();N(e),P()}),R(()=>{a&&window.clearInterval(a)}),(e,t)=>{const h=d("el-button"),V=d("el-result"),L=d("el-card");return _(),k("div",j,[i(L,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:s(()=>[t[2]||(t[2]=l("div",{class:"brand"},[l("div",{class:"brand-title"},"知识管理平台"),l("div",{class:"brand-sub app-muted"},"验证结果")],-1)),i(V,{icon:p.value?"success":"error",title:f.value,"sub-title":m.value,class:"result"},{extra:s(()=>[l("div",z,[i(h,{type:"primary",onClick:t[0]||(t[0]=S=>g(y.value))},{default:s(()=>[T(v(w.value),1)]),_:1}),A.value?(_(),$(h,{key:0,onClick:t[1]||(t[1]=S=>g(u.value))},{default:s(()=>[T(v(r.value),1)]),_:1})):B("",!0)]),b.value?(_(),k("div",D,v(n.value)+" 秒后自动跳转... ",1)):B("",!0)]),_:1},8,["icon","title","sub-title"])]),_:1})])}}},G=U(M,[["__scopeId","data-v-1fc6b081"]]);export{G as default};

View File

@@ -1 +1 @@
import{p as c}from"./index-BzB0auqv.js";async function o(t={}){const{data:a}=await c.get("/accounts",{params:t});return a}async function u(t){const{data:a}=await c.post("/accounts",t);return a}async function r(t,a){const{data:n}=await c.put(`/accounts/${t}`,a);return n}async function e(t){const{data:a}=await c.delete(`/accounts/${t}`);return a}async function i(t,a){const{data:n}=await c.put(`/accounts/${t}/remark`,a);return n}async function p(t,a){const{data:n}=await c.post(`/accounts/${t}/start`,a);return n}async function d(t){const{data:a}=await c.post(`/accounts/${t}/stop`,{});return a}async function f(t){const{data:a}=await c.post("/accounts/batch/start",t);return a}async function w(t){const{data:a}=await c.post("/accounts/batch/stop",t);return a}async function y(){const{data:t}=await c.post("/accounts/clear",{});return t}async function A(t,a={}){const{data:n}=await c.post(`/accounts/${t}/screenshot`,a);return n}export{w as a,f as b,y as c,d,e,o as f,u as g,i as h,p as s,A as t,r as u}; import{p as c}from"./index-oLrocmqc.js";async function o(t={}){const{data:a}=await c.get("/accounts",{params:t});return a}async function u(t){const{data:a}=await c.post("/accounts",t);return a}async function r(t,a){const{data:n}=await c.put(`/accounts/${t}`,a);return n}async function e(t){const{data:a}=await c.delete(`/accounts/${t}`);return a}async function i(t,a){const{data:n}=await c.put(`/accounts/${t}/remark`,a);return n}async function p(t,a){const{data:n}=await c.post(`/accounts/${t}/start`,a);return n}async function d(t){const{data:a}=await c.post(`/accounts/${t}/stop`,{});return a}async function f(t){const{data:a}=await c.post("/accounts/batch/start",t);return a}async function w(t){const{data:a}=await c.post("/accounts/batch/stop",t);return a}async function y(){const{data:t}=await c.post("/accounts/clear",{});return t}async function A(t,a={}){const{data:n}=await c.post(`/accounts/${t}/screenshot`,a);return n}export{w as a,f as b,y as c,d,e,o as f,u as g,i as h,p as s,A as t,r as u};

View File

@@ -1 +1 @@
import{p as s}from"./index-BzB0auqv.js";async function r(){const{data:t}=await s.get("/email/verify-status");return t}async function e(){const{data:t}=await s.post("/generate_captcha",{});return t}async function o(t){const{data:a}=await s.post("/login",t);return a}async function i(t){const{data:a}=await s.post("/register",t);return a}async function c(t){const{data:a}=await s.post("/resend-verify-email",t);return a}async function u(t){const{data:a}=await s.post("/forgot-password",t);return a}async function f(t){const{data:a}=await s.post("/reset_password_request",t);return a}async function d(t){const{data:a}=await s.post("/reset-password-confirm",t);return a}export{u as a,c as b,i as c,d,r as f,e as g,o as l,f as r}; import{p as s}from"./index-oLrocmqc.js";async function r(){const{data:t}=await s.get("/email/verify-status");return t}async function e(){const{data:t}=await s.post("/generate_captcha",{});return t}async function o(t){const{data:a}=await s.post("/login",t);return a}async function i(t){const{data:a}=await s.post("/register",t);return a}async function c(t){const{data:a}=await s.post("/resend-verify-email",t);return a}async function u(t){const{data:a}=await s.post("/forgot-password",t);return a}async function f(t){const{data:a}=await s.post("/reset_password_request",t);return a}async function d(t){const{data:a}=await s.post("/reset-password-confirm",t);return a}export{u as a,c as b,i as c,d,r as f,e as g,o as l,f as r};

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>知识管理平台</title> <title>知识管理平台</title>
<script type="module" crossorigin src="./assets/index-BzB0auqv.js"></script> <script type="module" crossorigin src="./assets/index-oLrocmqc.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Cvi4RJz4.css"> <link rel="stylesheet" crossorigin href="./assets/index-Cvi4RJz4.css">
</head> </head>
<body> <body>