feat(app): migrate /app accounts to Vue SPA (stage 3)

This commit is contained in:
2025-12-13 23:56:47 +08:00
parent 324e0d614a
commit 9798ed52c3
26 changed files with 1098 additions and 69 deletions

View File

@@ -0,0 +1,57 @@
import { publicApi } from './http'
export async function fetchAccounts(params = {}) {
const { data } = await publicApi.get('/accounts', { params })
return data
}
export async function addAccount(payload) {
const { data } = await publicApi.post('/accounts', payload)
return data
}
export async function updateAccount(accountId, payload) {
const { data } = await publicApi.put(`/accounts/${accountId}`, payload)
return data
}
export async function deleteAccount(accountId) {
const { data } = await publicApi.delete(`/accounts/${accountId}`)
return data
}
export async function updateAccountRemark(accountId, payload) {
const { data } = await publicApi.put(`/accounts/${accountId}/remark`, payload)
return data
}
export async function startAccount(accountId, payload) {
const { data } = await publicApi.post(`/accounts/${accountId}/start`, payload)
return data
}
export async function stopAccount(accountId) {
const { data } = await publicApi.post(`/accounts/${accountId}/stop`, {})
return data
}
export async function batchStartAccounts(payload) {
const { data } = await publicApi.post('/accounts/batch/start', payload)
return data
}
export async function batchStopAccounts(payload) {
const { data } = await publicApi.post('/accounts/batch/stop', payload)
return data
}
export async function clearAccounts() {
const { data } = await publicApi.post('/accounts/clear', {})
return data
}
export async function takeScreenshot(accountId, payload = {}) {
const { data } = await publicApi.post(`/accounts/${accountId}/screenshot`, payload)
return data
}

View File

@@ -0,0 +1,7 @@
import { publicApi } from './http'
export async function fetchRunStats() {
const { data } = await publicApi.get('/run_stats')
return data
}

View File

@@ -0,0 +1,12 @@
import { publicApi } from './http'
export async function fetchVipInfo() {
const { data } = await publicApi.get('/user/vip')
return data
}
export async function logout() {
const { data } = await publicApi.post('/logout', {})
return data
}

View File

@@ -0,0 +1,15 @@
import { io } from 'socket.io-client'
let socketInstance = null
export function useSocket() {
if (socketInstance) return socketInstance
socketInstance = io({
transports: ['websocket', 'polling'],
withCredentials: true,
})
return socketInstance
}

View File

@@ -4,8 +4,11 @@ import { useRoute, useRouter } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import { Calendar, Camera, User } from '@element-plus/icons-vue'
import { useUserStore } from '../stores/user'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const isMobile = ref(false)
const drawerOpen = ref(false)
@@ -20,6 +23,10 @@ onMounted(() => {
mediaQuery = window.matchMedia('(max-width: 768px)')
mediaQuery.addEventListener?.('change', syncIsMobile)
syncIsMobile()
userStore.refreshVipInfo().catch(() => {
window.location.href = '/login'
})
})
onBeforeUnmount(() => {
@@ -50,6 +57,7 @@ async function logout() {
return
}
await userStore.logout()
window.location.href = '/login'
}
</script>
@@ -80,6 +88,14 @@ async function logout() {
</div>
<div class="header-right">
<div class="user-meta">
<el-tag v-if="userStore.isVip" type="success" size="small" effect="light">VIP</el-tag>
<el-tag v-else type="info" size="small" effect="light">普通</el-tag>
<span class="user-name">{{ userStore.username || '用户' }}</span>
<span v-if="userStore.isVip && userStore.vipDaysLeft <= 7 && userStore.vipDaysLeft > 0" class="vip-warn">
({{ userStore.vipDaysLeft }}天后到期)
</span>
</div>
<el-button type="primary" plain @click="logout">退出</el-button>
</div>
</el-header>
@@ -94,12 +110,20 @@ async function logout() {
<div class="brand-title">知识管理平台</div>
<div class="brand-sub app-muted">用户中心</div>
</div>
<div class="drawer-user">
<el-tag v-if="userStore.isVip" type="success" size="small" effect="light">VIP</el-tag>
<el-tag v-else type="info" size="small" effect="light">普通</el-tag>
<span class="user-name">{{ userStore.username || '用户' }}</span>
</div>
<el-menu :default-active="activeMenu" class="aside-menu" router @select="go">
<el-menu-item v-for="item in menuItems" :key="item.path" :index="item.path">
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span>
</el-menu-item>
</el-menu>
<div class="drawer-actions">
<el-button type="primary" plain style="width: 100%" @click="logout">退出登录</el-button>
</div>
</el-drawer>
</el-container>
</template>
@@ -170,10 +194,44 @@ async function logout() {
gap: 12px;
}
.user-meta {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.user-name {
font-size: 13px;
font-weight: 700;
max-width: 180px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.vip-warn {
font-size: 12px;
color: var(--app-muted);
white-space: nowrap;
}
.layout-main {
padding: 16px;
}
.drawer-user {
padding: 0 16px 10px;
display: flex;
align-items: center;
gap: 8px;
}
.drawer-actions {
padding: 12px 16px 4px;
border-top: 1px solid var(--app-border);
}
@media (max-width: 768px) {
.layout-header {
flex-wrap: wrap;
@@ -190,6 +248,9 @@ async function logout() {
.layout-main {
padding: 12px;
}
.user-name {
max-width: 120px;
}
}
</style>

View File

@@ -1,20 +1,822 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
addAccount,
batchStartAccounts,
batchStopAccounts,
clearAccounts,
deleteAccount as apiDeleteAccount,
fetchAccounts,
startAccount,
stopAccount,
takeScreenshot,
updateAccount,
updateAccountRemark,
} from '../api/accounts'
import { fetchRunStats } from '../api/stats'
import { useSocket } from '../composables/useSocket'
import { useUserStore } from '../stores/user'
const userStore = useUserStore()
const socket = useSocket()
const loading = ref(false)
const statsLoading = ref(false)
const stats = reactive({
today_completed: 0,
today_failed: 0,
current_running: 0,
today_items: 0,
today_attachments: 0,
})
const accountsById = reactive({})
const selectedIds = ref([])
const browseTypeById = reactive({})
const batchBrowseType = ref('应读')
const batchEnableScreenshot = ref(true)
const addOpen = ref(false)
const editOpen = ref(false)
const upgradeOpen = ref(false)
const addForm = reactive({
username: '',
password: '',
remark: '',
})
const editForm = reactive({
id: '',
username: '',
password: '',
remark: '',
originalRemark: '',
})
const browseTypeOptions = [
{ label: '应读', value: '应读' },
{ label: '未读', value: '未读' },
{ label: '注册前未读', value: '注册前未读' },
]
const accounts = computed(() =>
Object.values(accountsById).sort((a, b) => String(a.username || '').localeCompare(String(b.username || ''), 'zh-CN')),
)
const accountCount = computed(() => accounts.value.length)
const accountLimit = computed(() => (userStore.isVip ? 999 : 3))
const selectedCount = computed(() => selectedIds.value.length)
const allSelected = computed(() => accountCount.value > 0 && selectedCount.value === accountCount.value)
const showUpgradeBanner = computed(() => !userStore.isVip)
function normalizeAccountPayload(acc) {
const base = accountsById[acc.id] || {}
accountsById[acc.id] = { ...base, ...acc }
}
function replaceAccounts(list) {
for (const key of Object.keys(accountsById)) delete accountsById[key]
for (const acc of list || []) normalizeAccountPayload(acc)
}
function ensureBrowseTypeDefaults() {
for (const acc of accounts.value) {
if (!browseTypeById[acc.id]) browseTypeById[acc.id] = '应读'
}
}
watch(accounts, ensureBrowseTypeDefaults, { immediate: true })
function toggleSelectAll(value) {
if (value) selectedIds.value = accounts.value.map((a) => a.id)
else selectedIds.value = []
}
function requireVip(featureName) {
if (userStore.isVip) return true
ElMessage.warning(`${featureName}是VIP专属功能`)
upgradeOpen.value = true
return false
}
function toPercent(acc) {
const total = Number(acc.total_items || 0)
const done = Number(acc.progress_items || 0)
if (!total) return 0
return Math.max(0, Math.min(100, Math.round((done / total) * 100)))
}
function statusTagType(status = '') {
const text = String(status)
if (text.includes('已完成') || text.includes('完成')) return 'success'
if (text.includes('失败') || text.includes('错误') || text.includes('异常') || text.includes('登录失败')) return 'danger'
if (text.includes('排队') || text.includes('运行') || text.includes('截图')) return 'warning'
return 'info'
}
async function refreshStats() {
statsLoading.value = true
try {
const data = await fetchRunStats()
stats.today_completed = Number(data?.today_completed || 0)
stats.today_failed = Number(data?.today_failed || 0)
stats.current_running = Number(data?.current_running || 0)
stats.today_items = Number(data?.today_items || 0)
stats.today_attachments = Number(data?.today_attachments || 0)
} catch (e) {
if (e?.response?.status === 401) window.location.href = '/login'
} finally {
statsLoading.value = false
}
}
async function refreshAccounts() {
loading.value = true
try {
const list = await fetchAccounts({ refresh: true })
replaceAccounts(list)
} catch (e) {
if (e?.response?.status === 401) window.location.href = '/login'
} finally {
loading.value = false
}
}
async function onStart(acc) {
try {
await startAccount(acc.id, { browse_type: browseTypeById[acc.id] || '应读', enable_screenshot: true })
} catch (e) {
const data = e?.response?.data
ElMessage.error(data?.error || '启动失败')
}
}
async function onStop(acc) {
try {
await stopAccount(acc.id)
} catch (e) {
const data = e?.response?.data
ElMessage.error(data?.error || '停止失败')
}
}
async function onScreenshot(acc) {
try {
await takeScreenshot(acc.id, { browse_type: browseTypeById[acc.id] || '应读' })
ElMessage.success('已提交截图')
} catch (e) {
const data = e?.response?.data
ElMessage.error(data?.error || '截图失败')
}
}
async function onDelete(acc) {
try {
await ElMessageBox.confirm(`确定要删除账号「${acc.username}」吗?`, '删除账号', {
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
const res = await apiDeleteAccount(acc.id)
if (res?.success) {
delete accountsById[acc.id]
selectedIds.value = selectedIds.value.filter((id) => id !== acc.id)
ElMessage.success('已删除')
await refreshStats()
} else {
ElMessage.error(res?.error || '删除失败')
}
} catch (e) {
const data = e?.response?.data
ElMessage.error(data?.error || '删除失败')
}
}
function openAdd() {
addForm.username = ''
addForm.password = ''
addForm.remark = ''
addOpen.value = true
}
async function submitAdd() {
const username = addForm.username.trim()
if (!username || !addForm.password.trim()) {
ElMessage.error('用户名和密码不能为空')
return
}
try {
await addAccount({
username,
password: addForm.password,
remember: true,
remark: addForm.remark.trim(),
})
ElMessage.success('添加成功')
addOpen.value = false
await refreshStats()
} catch (e) {
const data = e?.response?.data
ElMessage.error(data?.error || '添加失败')
}
}
function openEdit(acc) {
editForm.id = acc.id
editForm.username = acc.username
editForm.password = ''
editForm.remark = String(acc.remark || '')
editForm.originalRemark = String(acc.remark || '')
editOpen.value = true
}
async function submitEdit() {
if (!editForm.id) return
if (!editForm.password.trim()) {
ElMessage.error('请输入新密码')
return
}
try {
const res = await updateAccount(editForm.id, { password: editForm.password, remember: true })
if (res?.account) normalizeAccountPayload(res.account)
const remarkText = editForm.remark.trim()
if (remarkText !== editForm.originalRemark) {
await updateAccountRemark(editForm.id, { remark: remarkText })
normalizeAccountPayload({ id: editForm.id, remark: remarkText })
}
ElMessage.success('已更新')
editOpen.value = false
} catch (e) {
const data = e?.response?.data
ElMessage.error(data?.error || '更新失败')
}
}
async function batchStart() {
if (!requireVip('批量操作')) return
if (selectedIds.value.length === 0) {
ElMessage.warning('请先选择账号')
return
}
try {
const res = await batchStartAccounts({
account_ids: selectedIds.value,
browse_type: batchBrowseType.value,
enable_screenshot: batchEnableScreenshot.value,
})
ElMessage.success(`已启动 ${res?.started_count || 0} 个账号`)
} catch (e) {
const data = e?.response?.data
ElMessage.error(data?.error || '操作失败')
}
}
async function batchStop() {
if (!requireVip('批量操作')) return
if (selectedIds.value.length === 0) {
ElMessage.warning('请先选择账号')
return
}
try {
const res = await batchStopAccounts({ account_ids: selectedIds.value })
ElMessage.success(`已停止 ${res?.stopped_count || 0} 个账号`)
} catch (e) {
const data = e?.response?.data
ElMessage.error(data?.error || '操作失败')
}
}
async function startAll() {
if (!requireVip('全部启动')) return
if (accounts.value.length === 0) {
ElMessage.warning('没有账号')
return
}
try {
await ElMessageBox.confirm('确定要启动全部账号吗?', '全部启动', {
confirmButtonText: '启动',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
const res = await batchStartAccounts({
account_ids: accounts.value.map((a) => a.id),
browse_type: batchBrowseType.value,
enable_screenshot: batchEnableScreenshot.value,
})
ElMessage.success(`已启动 ${res?.started_count || 0} 个账号`)
} catch (e) {
const data = e?.response?.data
ElMessage.error(data?.error || '操作失败')
}
}
async function stopAll() {
if (!requireVip('全部停止')) return
if (accounts.value.length === 0) {
ElMessage.warning('没有账号')
return
}
try {
await ElMessageBox.confirm('确定要停止全部账号吗?', '全部停止', {
confirmButtonText: '停止',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
const res = await batchStopAccounts({ account_ids: accounts.value.map((a) => a.id) })
ElMessage.success(`已停止 ${res?.stopped_count || 0} 个账号`)
} catch (e) {
const data = e?.response?.data
ElMessage.error(data?.error || '操作失败')
}
}
async function clearAll() {
if (accounts.value.length === 0) {
ElMessage.warning('没有账号')
return
}
try {
await ElMessageBox.confirm('确定要清空所有账号吗?此操作不可恢复!', '清空账号', {
confirmButtonText: '继续',
cancelButtonText: '取消',
type: 'warning',
})
await ElMessageBox.confirm('再次确认:真的要删除所有账号吗?', '二次确认', {
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
const res = await clearAccounts()
if (res?.success) {
replaceAccounts([])
selectedIds.value = []
ElMessage.success('已清空所有账号')
await refreshStats()
return
}
ElMessage.error(res?.error || '操作失败')
} catch (e) {
const data = e?.response?.data
ElMessage.error(data?.error || '操作失败')
}
}
function bindSocket() {
const onAccountsList = (list) => {
replaceAccounts(list)
}
const onAccountUpdate = (acc) => {
normalizeAccountPayload(acc)
}
const onTaskProgress = (payload) => {
if (!payload?.account_id) return
normalizeAccountPayload({
id: payload.account_id,
detail_status: payload.stage || '',
total_items: payload.total_items,
progress_items: payload.browsed_items,
total_attachments: payload.total_attachments,
progress_attachments: payload.viewed_attachments,
elapsed_seconds: payload.elapsed_seconds,
elapsed_display: payload.elapsed_display,
})
}
socket.on('accounts_list', onAccountsList)
socket.on('account_update', onAccountUpdate)
socket.on('task_progress', onTaskProgress)
if (!socket.connected) socket.connect()
return () => {
socket.off('accounts_list', onAccountsList)
socket.off('account_update', onAccountUpdate)
socket.off('task_progress', onTaskProgress)
}
}
let unbindSocket = null
let statsTimer = null
onMounted(async () => {
if (!userStore.vipInfo) {
userStore.refreshVipInfo().catch(() => {
window.location.href = '/login'
})
}
unbindSocket = bindSocket()
await refreshAccounts()
await refreshStats()
statsTimer = window.setInterval(refreshStats, 10_000)
})
onBeforeUnmount(() => {
if (unbindSocket) unbindSocket()
if (statsTimer) window.clearInterval(statsTimer)
})
</script>
<template>
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card">
<h2 class="title">账号管理</h2>
<div class="app-muted">阶段1页面壳子已就绪功能将在后续阶段迁移</div>
</el-card>
<div class="page">
<el-row :gutter="12" class="stats-row">
<el-col :xs="12" :sm="8" :md="4">
<el-card shadow="never" class="stat-card" :body-style="{ padding: '14px' }">
<div class="stat-label app-muted">今日完成</div>
<div class="stat-value">{{ stats.today_completed }}</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
<el-card shadow="never" class="stat-card" :body-style="{ padding: '14px' }">
<div class="stat-label app-muted">今日失败</div>
<div class="stat-value">{{ stats.today_failed }}</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
<el-card shadow="never" class="stat-card" :body-style="{ padding: '14px' }">
<div class="stat-label app-muted">运行中</div>
<div class="stat-value">{{ stats.current_running }}</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
<el-card shadow="never" class="stat-card" :body-style="{ padding: '14px' }">
<div class="stat-label app-muted">浏览内容</div>
<div class="stat-value">{{ stats.today_items }}</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
<el-card shadow="never" class="stat-card" :body-style="{ padding: '14px' }">
<div class="stat-label app-muted">查看附件</div>
<div class="stat-value">{{ stats.today_attachments }}</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
<el-card shadow="never" class="stat-card" :body-style="{ padding: '14px' }">
<div class="stat-label app-muted">账号数</div>
<div class="stat-value">
{{ accountCount }}<span class="stat-suffix app-muted">/ {{ userStore.isVip ? '∞' : accountLimit }}</span>
</div>
</el-card>
</el-col>
</el-row>
<el-alert
v-if="showUpgradeBanner"
type="info"
show-icon
:closable="false"
class="upgrade-banner"
title="升级 VIP解锁更多功能无限账号 · 优先排队 · 定时任务 · 批量操作"
>
<template #default>
<div class="upgrade-actions">
<el-button type="primary" plain @click="upgradeOpen = true">了解VIP特权</el-button>
</div>
</template>
</el-alert>
<el-card shadow="never" class="panel" :body-style="{ padding: '14px' }">
<div class="panel-head">
<div class="panel-title">账号管理</div>
<div class="panel-actions">
<el-button :loading="loading" @click="refreshAccounts">刷新</el-button>
<el-button type="primary" @click="openAdd">添加账号</el-button>
</div>
</div>
<div class="toolbar">
<div class="toolbar-left">
<el-checkbox :model-value="allSelected" @change="toggleSelectAll">全选</el-checkbox>
<span class="app-muted">已选 {{ selectedCount }} </span>
</div>
<div class="toolbar-middle">
<el-select v-model="batchBrowseType" size="small" style="width: 120px">
<el-option v-for="opt in browseTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
<el-switch v-model="batchEnableScreenshot" inline-prompt active-text="截图" inactive-text="不截图" />
</div>
<div class="toolbar-right">
<el-button type="primary" @click="batchStart">批量启动</el-button>
<el-button @click="batchStop">批量停止</el-button>
<el-button type="success" plain @click="startAll">全部启动</el-button>
<el-button type="danger" plain @click="stopAll">全部停止</el-button>
<el-button type="danger" text @click="clearAll">清空</el-button>
</div>
</div>
<el-skeleton v-if="loading || statsLoading" :rows="5" animated />
<template v-else>
<el-empty v-if="accounts.length === 0" description="暂无账号,点击右上角添加" />
<div v-else class="grid">
<el-card v-for="acc in accounts" :key="acc.id" shadow="never" class="account-card" :body-style="{ padding: '14px' }">
<div class="card-top">
<el-checkbox-group v-model="selectedIds" class="card-check">
<el-checkbox :value="acc.id" />
</el-checkbox-group>
<div class="card-main">
<div class="card-title">
<span class="card-name">{{ acc.username }}</span>
<el-tag size="small" :type="statusTagType(acc.status)" effect="light">{{ acc.status }}</el-tag>
</div>
<div class="card-sub app-muted">
{{ acc.remark || '—' }}
<span v-if="acc.detail_status"> · {{ acc.detail_status }}</span>
<span v-if="acc.elapsed_display"> · {{ acc.elapsed_display }}</span>
</div>
</div>
</div>
<div class="progress">
<el-progress :percentage="toPercent(acc)" :stroke-width="10" :show-text="false" />
<div class="progress-meta app-muted">
<span>内容 {{ acc.progress_items || 0 }}/{{ acc.total_items || 0 }}</span>
<span>附件 {{ acc.progress_attachments || 0 }}/{{ acc.total_attachments || 0 }}</span>
</div>
</div>
<div class="card-controls">
<el-select v-model="browseTypeById[acc.id]" size="small" style="width: 130px">
<el-option v-for="opt in browseTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
<div class="card-buttons">
<el-button size="small" type="primary" :disabled="acc.is_running" @click="onStart(acc)">启动</el-button>
<el-button size="small" :disabled="!acc.is_running" @click="onStop(acc)">停止</el-button>
<el-button size="small" :disabled="acc.is_running" @click="onScreenshot(acc)">截图</el-button>
<el-button size="small" :disabled="acc.is_running" @click="openEdit(acc)">编辑</el-button>
<el-button size="small" type="danger" text @click="onDelete(acc)">删除</el-button>
</div>
</div>
</el-card>
</div>
</template>
</el-card>
<el-dialog v-model="addOpen" title="添加账号" width="min(560px, 92vw)">
<el-form label-position="top">
<el-form-item label="账号">
<el-input v-model="addForm.username" placeholder="请输入账号" autocomplete="off" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="addForm.password" type="password" show-password placeholder="请输入密码" autocomplete="off" />
</el-form-item>
<el-form-item label="备注可选最多200字">
<el-input v-model="addForm.remark" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="例如:部门/用途" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="addOpen = false">取消</el-button>
<el-button type="primary" @click="submitAdd">添加</el-button>
</template>
</el-dialog>
<el-dialog v-model="editOpen" title="编辑账号" width="min(560px, 92vw)">
<el-form label-position="top">
<el-form-item label="账号">
<el-input v-model="editForm.username" disabled />
</el-form-item>
<el-form-item label="新密码(必填)">
<el-input v-model="editForm.password" type="password" show-password placeholder="请输入新密码" autocomplete="off" />
</el-form-item>
<el-form-item label="备注可选最多200字">
<el-input v-model="editForm.remark" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="例如:部门/用途" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editOpen = false">取消</el-button>
<el-button type="primary" @click="submitEdit">保存</el-button>
</template>
</el-dialog>
<el-dialog v-model="upgradeOpen" title="VIP 特权" width="min(560px, 92vw)">
<el-alert
type="info"
:closable="false"
title="升级 VIP 后可解锁:无限账号、优先排队、定时任务、批量操作。"
show-icon
/>
<div class="vip-body">
<div class="vip-tip app-muted">升级方式请通过反馈联系管理员开通与后台一致</div>
</div>
<template #footer>
<el-button type="primary" @click="upgradeOpen = false">我知道了</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.card {
.page {
display: flex;
flex-direction: column;
gap: 12px;
}
.stat-card,
.panel {
border-radius: var(--app-radius);
border: 1px solid var(--app-border);
}
.title {
margin: 0 0 6px;
.stat-label {
font-size: 12px;
}
.stat-value {
margin-top: 6px;
font-size: 22px;
font-weight: 900;
letter-spacing: 0.2px;
}
.stat-suffix {
margin-left: 6px;
font-size: 12px;
font-weight: 600;
}
.upgrade-banner {
border-radius: var(--app-radius);
border: 1px solid var(--app-border);
}
.upgrade-actions {
margin-top: 10px;
}
.panel-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.panel-title {
font-size: 16px;
font-weight: 800;
font-weight: 900;
}
.panel-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
padding: 10px;
border: 1px dashed rgba(17, 24, 39, 0.14);
border-radius: 12px;
background: rgba(246, 247, 251, 0.6);
}
.toolbar-left,
.toolbar-middle,
.toolbar-right {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.toolbar-right {
margin-left: auto;
justify-content: flex-end;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 12px;
}
.account-card {
border-radius: 14px;
border: 1px solid var(--app-border);
}
.card-top {
display: flex;
gap: 10px;
}
.card-check {
padding-top: 2px;
}
.card-main {
min-width: 0;
flex: 1;
}
.card-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.card-name {
font-size: 14px;
font-weight: 900;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-sub {
margin-top: 6px;
font-size: 12px;
line-height: 1.4;
word-break: break-word;
}
.progress {
margin-top: 12px;
}
.progress-meta {
margin-top: 6px;
display: flex;
justify-content: space-between;
gap: 10px;
font-size: 12px;
}
.card-controls {
margin-top: 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.card-buttons {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
.vip-body {
padding: 12px 0 0;
}
.vip-tip {
margin-top: 10px;
font-size: 13px;
line-height: 1.6;
}
@media (max-width: 480px) {
.grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,34 @@
import { defineStore } from 'pinia'
import { fetchVipInfo as apiFetchVipInfo, logout as apiLogout } from '../api/user'
export const useUserStore = defineStore('user', {
state: () => ({
vipInfo: null,
loading: false,
}),
getters: {
username: (state) => state.vipInfo?.username || '',
isVip: (state) => Boolean(state.vipInfo?.is_vip),
vipDaysLeft: (state) => Number(state.vipInfo?.days_left || 0),
vipExpireTime: (state) => state.vipInfo?.expire_time || '',
},
actions: {
async refreshVipInfo() {
this.loading = true
try {
this.vipInfo = await apiFetchVipInfo()
} finally {
this.loading = false
}
},
async logout() {
try {
await apiLogout()
} catch {
// ignore
}
},
},
})

40
app.py
View File

@@ -1202,7 +1202,14 @@ def register_page():
@login_required
def app_page():
"""主应用页面"""
return render_template('index.html')
return render_app_spa_or_legacy('index.html')
@app.route('/app/<path:subpath>')
@login_required
def app_page_subpath(subpath):
"""SPA 子路由刷新支持History 模式)"""
return render_app_spa_or_legacy('index.html')
@app.route('/yuyx')
@@ -2608,6 +2615,33 @@ def delete_account(account_id):
return jsonify({"success": True})
@app.route('/api/accounts/clear', methods=['POST'])
@login_required
def clear_accounts():
"""清空当前用户的所有账号"""
user_id = current_user.id
accounts = user_accounts.get(user_id, {})
if any(acc.is_running for acc in accounts.values()):
return jsonify({"error": "有任务正在运行,请先停止后再清空"}), 400
account_ids = list(accounts.keys())
deleted = database.delete_user_accounts(user_id)
# 清理内存缓存
if user_id in user_accounts:
user_accounts[user_id] = {}
# 清理任务状态缓存
for account_id in account_ids:
safe_remove_task_status(account_id)
safe_remove_task(account_id)
log_to_client(f"清空账号: {deleted}", user_id)
return jsonify({"success": True, "deleted": deleted})
@app.route('/api/accounts/<account_id>/remark', methods=['PUT'])
@login_required
def update_remark(account_id):
@@ -3380,6 +3414,10 @@ def take_screenshot_for_account(user_id, account_id, browse_type="应读", sourc
)
if not submitted:
screenshot_callback(None, "截图队列已满,请稍后重试")
@app.route('/api/accounts/<account_id>/screenshot', methods=['POST'])
@login_required
def manual_screenshot(account_id):
"""手动为指定账号截图"""
user_id = current_user.id

View File

@@ -1,14 +1,17 @@
{
"_auth-PlCOj1Xe.js": {
"file": "assets/auth-PlCOj1Xe.js",
"name": "auth"
"_auth-yhlOdREj.js": {
"file": "assets/auth-yhlOdREj.js",
"name": "auth",
"imports": [
"index.html"
]
},
"_password-7ryi82gE.js": {
"file": "assets/password-7ryi82gE.js",
"name": "password"
},
"index.html": {
"file": "assets/index-fYGyZipT.js",
"file": "assets/index-DvbGwVAp.js",
"name": "index",
"src": "index.html",
"isEntry": true,
@@ -22,11 +25,11 @@
"src/pages/ScreenshotsPage.vue"
],
"css": [
"assets/index-CZCRHVLY.css"
"assets/index-Baiuy_-z.css"
]
},
"src/pages/AccountsPage.vue": {
"file": "assets/AccountsPage-DFK9bKik.js",
"file": "assets/AccountsPage-C2BSK5Ns.js",
"name": "AccountsPage",
"src": "src/pages/AccountsPage.vue",
"isDynamicEntry": true,
@@ -34,17 +37,17 @@
"index.html"
],
"css": [
"assets/AccountsPage-ByA-Bv17.css"
"assets/AccountsPage-DXTZ7oC0.css"
]
},
"src/pages/LoginPage.vue": {
"file": "assets/LoginPage-1KWN57o2.js",
"file": "assets/LoginPage-DYohZsxn.js",
"name": "LoginPage",
"src": "src/pages/LoginPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html",
"_auth-PlCOj1Xe.js",
"_auth-yhlOdREj.js",
"_password-7ryi82gE.js"
],
"css": [
@@ -52,26 +55,26 @@
]
},
"src/pages/RegisterPage.vue": {
"file": "assets/RegisterPage-CJqvAJkb.js",
"file": "assets/RegisterPage-CGBzvBqd.js",
"name": "RegisterPage",
"src": "src/pages/RegisterPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html",
"_auth-PlCOj1Xe.js"
"_auth-yhlOdREj.js"
],
"css": [
"assets/RegisterPage-CVjBOq6i.css"
]
},
"src/pages/ResetPasswordPage.vue": {
"file": "assets/ResetPasswordPage-zVP1Rm4S.js",
"file": "assets/ResetPasswordPage-ClLk6uyu.js",
"name": "ResetPasswordPage",
"src": "src/pages/ResetPasswordPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html",
"_auth-PlCOj1Xe.js",
"_auth-yhlOdREj.js",
"_password-7ryi82gE.js"
],
"css": [
@@ -79,7 +82,7 @@
]
},
"src/pages/SchedulesPage.vue": {
"file": "assets/SchedulesPage-D4P6kLJv.js",
"file": "assets/SchedulesPage-DHlqgLCv.js",
"name": "SchedulesPage",
"src": "src/pages/SchedulesPage.vue",
"isDynamicEntry": true,
@@ -91,7 +94,7 @@
]
},
"src/pages/ScreenshotsPage.vue": {
"file": "assets/ScreenshotsPage-By1nYVxK.js",
"file": "assets/ScreenshotsPage-jZuEr5af.js",
"name": "ScreenshotsPage",
"src": "src/pages/ScreenshotsPage.vue",
"isDynamicEntry": true,
@@ -103,7 +106,7 @@
]
},
"src/pages/VerifyResultPage.vue": {
"file": "assets/VerifyResultPage-Buu0rko2.js",
"file": "assets/VerifyResultPage-B_i4AM-j.js",
"name": "VerifyResultPage",
"src": "src/pages/VerifyResultPage.vue",
"isDynamicEntry": true,

View File

@@ -1 +0,0 @@
.card[data-v-f8df5656]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.title[data-v-f8df5656]{margin:0 0 6px;font-size:16px;font-weight:800}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{_ as t,h as o,w as c,e as d,f as n,g as s}from"./index-fYGyZipT.js";const r={};function _(l,e){const a=d("el-card");return n(),o(a,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:c(()=>[...e[0]||(e[0]=[s("h2",{class:"title"},"账号管理",-1),s("div",{class:"app-muted"},"阶段1页面壳子已就绪功能将在后续阶段迁移。",-1)])]),_:1})}const f=t(r,[["render",_],["__scopeId","data-v-f8df5656"]]);export{f as default};

View File

@@ -0,0 +1 @@
.page[data-v-0cf21db0]{display:flex;flex-direction:column;gap:12px}.stat-card[data-v-0cf21db0],.panel[data-v-0cf21db0]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.stat-label[data-v-0cf21db0]{font-size:12px}.stat-value[data-v-0cf21db0]{margin-top:6px;font-size:22px;font-weight:900;letter-spacing:.2px}.stat-suffix[data-v-0cf21db0]{margin-left:6px;font-size:12px;font-weight:600}.upgrade-banner[data-v-0cf21db0]{border-radius:var(--app-radius);border:1px solid var(--app-border)}.upgrade-actions[data-v-0cf21db0]{margin-top:10px}.panel-head[data-v-0cf21db0]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px}.panel-title[data-v-0cf21db0]{font-size:16px;font-weight:900}.panel-actions[data-v-0cf21db0]{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.toolbar[data-v-0cf21db0]{display:flex;flex-wrap:wrap;align-items:center;gap:12px;padding:10px;border:1px dashed rgba(17,24,39,.14);border-radius:12px;background:#f6f7fb99}.toolbar-left[data-v-0cf21db0],.toolbar-middle[data-v-0cf21db0],.toolbar-right[data-v-0cf21db0]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.toolbar-right[data-v-0cf21db0]{margin-left:auto;justify-content:flex-end}.grid[data-v-0cf21db0]{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:12px}.account-card[data-v-0cf21db0]{border-radius:14px;border:1px solid var(--app-border)}.card-top[data-v-0cf21db0]{display:flex;gap:10px}.card-check[data-v-0cf21db0]{padding-top:2px}.card-main[data-v-0cf21db0]{min-width:0;flex:1}.card-title[data-v-0cf21db0]{display:flex;align-items:center;justify-content:space-between;gap:10px}.card-name[data-v-0cf21db0]{font-size:14px;font-weight:900;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card-sub[data-v-0cf21db0]{margin-top:6px;font-size:12px;line-height:1.4;word-break:break-word}.progress[data-v-0cf21db0]{margin-top:12px}.progress-meta[data-v-0cf21db0]{margin-top:6px;display:flex;justify-content:space-between;gap:10px;font-size:12px}.card-controls[data-v-0cf21db0]{margin-top:12px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.card-buttons[data-v-0cf21db0]{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}.vip-body[data-v-0cf21db0]{padding:12px 0 0}.vip-tip[data-v-0cf21db0]{margin-top:10px;font-size:13px;line-height:1.6}@media(max-width:480px){.grid[data-v-0cf21db0]{grid-template-columns:1fr}}

File diff suppressed because one or more lines are too long

View File

@@ -1 +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-fYGyZipT.js";import{g as z,f as F,c as G}from"./auth-PlCOj1Xe.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};
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-DvbGwVAp.js";import{g as z,f as F,c as G}from"./auth-yhlOdREj.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 +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-fYGyZipT.js";import{d as H}from"./auth-PlCOj1Xe.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-DvbGwVAp.js";import{d as H}from"./auth-yhlOdREj.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 t,h as o,w as c,e as d,f as r,g as s}from"./index-fYGyZipT.js";const n={};function l(_,e){const a=d("el-card");return r(),o(a,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:c(()=>[...e[0]||(e[0]=[s("h2",{class:"title"},"定时任务",-1),s("div",{class:"app-muted"},"阶段1页面壳子已就绪功能将在后续阶段迁移。",-1)])]),_:1})}const f=t(n,[["render",l],["__scopeId","data-v-b4b9e229"]]);export{f as default};
import{_ as t,h as o,w as c,e as d,f as r,g as s}from"./index-DvbGwVAp.js";const n={};function l(_,e){const a=d("el-card");return r(),o(a,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:c(()=>[...e[0]||(e[0]=[s("h2",{class:"title"},"定时任务",-1),s("div",{class:"app-muted"},"阶段1页面壳子已就绪功能将在后续阶段迁移。",-1)])]),_:1})}const f=t(n,[["render",l],["__scopeId","data-v-b4b9e229"]]);export{f as default};

View File

@@ -1 +1 @@
import{_ as t,h as o,w as c,e as d,f as r,g as s}from"./index-fYGyZipT.js";const n={};function _(l,e){const a=d("el-card");return r(),o(a,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:c(()=>[...e[0]||(e[0]=[s("h2",{class:"title"},"截图管理",-1),s("div",{class:"app-muted"},"阶段1页面壳子已就绪功能将在后续阶段迁移。",-1)])]),_:1})}const f=t(n,[["render",_],["__scopeId","data-v-08f8d2d3"]]);export{f as default};
import{_ as t,h as o,w as c,e as d,f as r,g as s}from"./index-DvbGwVAp.js";const n={};function _(l,e){const a=d("el-card");return r(),o(a,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:c(()=>[...e[0]||(e[0]=[s("h2",{class:"title"},"截图管理",-1),s("div",{class:"app-muted"},"阶段1页面壳子已就绪功能将在后续阶段迁移。",-1)])]),_:1})}const f=t(n,[["render",_],["__scopeId","data-v-08f8d2d3"]]);export{f 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-fYGyZipT.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-DvbGwVAp.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};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{p as s}from"./index-DvbGwVAp.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

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

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<title>知识管理平台</title>
<script type="module" crossorigin src="./assets/index-fYGyZipT.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CZCRHVLY.css">
<script type="module" crossorigin src="./assets/index-DvbGwVAp.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Baiuy_-z.css">
</head>
<body>
<noscript>该页面需要启用 JavaScript 才能使用。</noscript>