feat(app): migrate /app accounts to Vue SPA (stage 3)
This commit is contained in:
57
app-frontend/src/api/accounts.js
Normal file
57
app-frontend/src/api/accounts.js
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
7
app-frontend/src/api/stats.js
Normal file
7
app-frontend/src/api/stats.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { publicApi } from './http'
|
||||||
|
|
||||||
|
export async function fetchRunStats() {
|
||||||
|
const { data } = await publicApi.get('/run_stats')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
12
app-frontend/src/api/user.js
Normal file
12
app-frontend/src/api/user.js
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
15
app-frontend/src/composables/useSocket.js
Normal file
15
app-frontend/src/composables/useSocket.js
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
@@ -4,8 +4,11 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { ElMessageBox } from 'element-plus'
|
import { ElMessageBox } from 'element-plus'
|
||||||
import { Calendar, Camera, User } from '@element-plus/icons-vue'
|
import { Calendar, Camera, User } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const isMobile = ref(false)
|
const isMobile = ref(false)
|
||||||
const drawerOpen = ref(false)
|
const drawerOpen = ref(false)
|
||||||
@@ -20,6 +23,10 @@ onMounted(() => {
|
|||||||
mediaQuery = window.matchMedia('(max-width: 768px)')
|
mediaQuery = window.matchMedia('(max-width: 768px)')
|
||||||
mediaQuery.addEventListener?.('change', syncIsMobile)
|
mediaQuery.addEventListener?.('change', syncIsMobile)
|
||||||
syncIsMobile()
|
syncIsMobile()
|
||||||
|
|
||||||
|
userStore.refreshVipInfo().catch(() => {
|
||||||
|
window.location.href = '/login'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -50,6 +57,7 @@ async function logout() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await userStore.logout()
|
||||||
window.location.href = '/login'
|
window.location.href = '/login'
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -80,6 +88,14 @@ async function logout() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-right">
|
<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>
|
<el-button type="primary" plain @click="logout">退出</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
@@ -94,12 +110,20 @@ async function logout() {
|
|||||||
<div class="brand-title">知识管理平台</div>
|
<div class="brand-title">知识管理平台</div>
|
||||||
<div class="brand-sub app-muted">用户中心</div>
|
<div class="brand-sub app-muted">用户中心</div>
|
||||||
</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 :default-active="activeMenu" class="aside-menu" router @select="go">
|
||||||
<el-menu-item v-for="item in menuItems" :key="item.path" :index="item.path">
|
<el-menu-item v-for="item in menuItems" :key="item.path" :index="item.path">
|
||||||
<el-icon><component :is="item.icon" /></el-icon>
|
<el-icon><component :is="item.icon" /></el-icon>
|
||||||
<span>{{ item.label }}</span>
|
<span>{{ item.label }}</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
|
<div class="drawer-actions">
|
||||||
|
<el-button type="primary" plain style="width: 100%" @click="logout">退出登录</el-button>
|
||||||
|
</div>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</el-container>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
@@ -170,10 +194,44 @@ async function logout() {
|
|||||||
gap: 12px;
|
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 {
|
.layout-main {
|
||||||
padding: 16px;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.layout-header {
|
.layout-header {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -190,6 +248,9 @@ async function logout() {
|
|||||||
.layout-main {
|
.layout-main {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
<template>
|
||||||
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card">
|
<div class="page">
|
||||||
<h2 class="title">账号管理</h2>
|
<el-row :gutter="12" class="stats-row">
|
||||||
<div class="app-muted">阶段1:页面壳子已就绪,功能将在后续阶段迁移。</div>
|
<el-col :xs="12" :sm="8" :md="4">
|
||||||
</el-card>
|
<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>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.card {
|
.page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card,
|
||||||
|
.panel {
|
||||||
border-radius: var(--app-radius);
|
border-radius: var(--app-radius);
|
||||||
border: 1px solid var(--app-border);
|
border: 1px solid var(--app-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.stat-label {
|
||||||
margin: 0 0 6px;
|
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-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>
|
</style>
|
||||||
|
|
||||||
|
|||||||
34
app-frontend/src/stores/user.js
Normal file
34
app-frontend/src/stores/user.js
Normal 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
40
app.py
@@ -1202,7 +1202,14 @@ def register_page():
|
|||||||
@login_required
|
@login_required
|
||||||
def app_page():
|
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')
|
@app.route('/yuyx')
|
||||||
@@ -2608,6 +2615,33 @@ def delete_account(account_id):
|
|||||||
return jsonify({"success": True})
|
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'])
|
@app.route('/api/accounts/<account_id>/remark', methods=['PUT'])
|
||||||
@login_required
|
@login_required
|
||||||
def update_remark(account_id):
|
def update_remark(account_id):
|
||||||
@@ -3380,6 +3414,10 @@ def take_screenshot_for_account(user_id, account_id, browse_type="应读", sourc
|
|||||||
)
|
)
|
||||||
if not submitted:
|
if not submitted:
|
||||||
screenshot_callback(None, "截图队列已满,请稍后重试")
|
screenshot_callback(None, "截图队列已满,请稍后重试")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/accounts/<account_id>/screenshot', methods=['POST'])
|
||||||
|
@login_required
|
||||||
def manual_screenshot(account_id):
|
def manual_screenshot(account_id):
|
||||||
"""手动为指定账号截图"""
|
"""手动为指定账号截图"""
|
||||||
user_id = current_user.id
|
user_id = current_user.id
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
{
|
{
|
||||||
"_auth-PlCOj1Xe.js": {
|
"_auth-yhlOdREj.js": {
|
||||||
"file": "assets/auth-PlCOj1Xe.js",
|
"file": "assets/auth-yhlOdREj.js",
|
||||||
"name": "auth"
|
"name": "auth",
|
||||||
|
"imports": [
|
||||||
|
"index.html"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"_password-7ryi82gE.js": {
|
"_password-7ryi82gE.js": {
|
||||||
"file": "assets/password-7ryi82gE.js",
|
"file": "assets/password-7ryi82gE.js",
|
||||||
"name": "password"
|
"name": "password"
|
||||||
},
|
},
|
||||||
"index.html": {
|
"index.html": {
|
||||||
"file": "assets/index-fYGyZipT.js",
|
"file": "assets/index-DvbGwVAp.js",
|
||||||
"name": "index",
|
"name": "index",
|
||||||
"src": "index.html",
|
"src": "index.html",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
@@ -22,11 +25,11 @@
|
|||||||
"src/pages/ScreenshotsPage.vue"
|
"src/pages/ScreenshotsPage.vue"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/index-CZCRHVLY.css"
|
"assets/index-Baiuy_-z.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/AccountsPage.vue": {
|
"src/pages/AccountsPage.vue": {
|
||||||
"file": "assets/AccountsPage-DFK9bKik.js",
|
"file": "assets/AccountsPage-C2BSK5Ns.js",
|
||||||
"name": "AccountsPage",
|
"name": "AccountsPage",
|
||||||
"src": "src/pages/AccountsPage.vue",
|
"src": "src/pages/AccountsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -34,17 +37,17 @@
|
|||||||
"index.html"
|
"index.html"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/AccountsPage-ByA-Bv17.css"
|
"assets/AccountsPage-DXTZ7oC0.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/LoginPage.vue": {
|
"src/pages/LoginPage.vue": {
|
||||||
"file": "assets/LoginPage-1KWN57o2.js",
|
"file": "assets/LoginPage-DYohZsxn.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-PlCOj1Xe.js",
|
"_auth-yhlOdREj.js",
|
||||||
"_password-7ryi82gE.js"
|
"_password-7ryi82gE.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
@@ -52,26 +55,26 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/RegisterPage.vue": {
|
"src/pages/RegisterPage.vue": {
|
||||||
"file": "assets/RegisterPage-CJqvAJkb.js",
|
"file": "assets/RegisterPage-CGBzvBqd.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-PlCOj1Xe.js"
|
"_auth-yhlOdREj.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"assets/RegisterPage-CVjBOq6i.css"
|
"assets/RegisterPage-CVjBOq6i.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/ResetPasswordPage.vue": {
|
"src/pages/ResetPasswordPage.vue": {
|
||||||
"file": "assets/ResetPasswordPage-zVP1Rm4S.js",
|
"file": "assets/ResetPasswordPage-ClLk6uyu.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-PlCOj1Xe.js",
|
"_auth-yhlOdREj.js",
|
||||||
"_password-7ryi82gE.js"
|
"_password-7ryi82gE.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
@@ -79,7 +82,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/SchedulesPage.vue": {
|
"src/pages/SchedulesPage.vue": {
|
||||||
"file": "assets/SchedulesPage-D4P6kLJv.js",
|
"file": "assets/SchedulesPage-DHlqgLCv.js",
|
||||||
"name": "SchedulesPage",
|
"name": "SchedulesPage",
|
||||||
"src": "src/pages/SchedulesPage.vue",
|
"src": "src/pages/SchedulesPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -91,7 +94,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/ScreenshotsPage.vue": {
|
"src/pages/ScreenshotsPage.vue": {
|
||||||
"file": "assets/ScreenshotsPage-By1nYVxK.js",
|
"file": "assets/ScreenshotsPage-jZuEr5af.js",
|
||||||
"name": "ScreenshotsPage",
|
"name": "ScreenshotsPage",
|
||||||
"src": "src/pages/ScreenshotsPage.vue",
|
"src": "src/pages/ScreenshotsPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -103,7 +106,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/pages/VerifyResultPage.vue": {
|
"src/pages/VerifyResultPage.vue": {
|
||||||
"file": "assets/VerifyResultPage-Buu0rko2.js",
|
"file": "assets/VerifyResultPage-B_i4AM-j.js",
|
||||||
"name": "VerifyResultPage",
|
"name": "VerifyResultPage",
|
||||||
"src": "src/pages/VerifyResultPage.vue",
|
"src": "src/pages/VerifyResultPage.vue",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
|
|||||||
@@ -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}
|
|
||||||
1
static/app/assets/AccountsPage-C2BSK5Ns.js
Normal file
1
static/app/assets/AccountsPage-C2BSK5Ns.js
Normal file
File diff suppressed because one or more lines are too long
@@ -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};
|
|
||||||
1
static/app/assets/AccountsPage-DXTZ7oC0.css
Normal file
1
static/app/assets/AccountsPage-DXTZ7oC0.css
Normal 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
@@ -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};
|
||||||
@@ -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};
|
||||||
@@ -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};
|
||||||
@@ -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};
|
||||||
@@ -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
1
static/app/assets/auth-yhlOdREj.js
Normal file
1
static/app/assets/auth-yhlOdREj.js
Normal 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};
|
||||||
1
static/app/assets/index-Baiuy_-z.css
Normal file
1
static/app/assets/index-Baiuy_-z.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
30
static/app/assets/index-DvbGwVAp.js
Normal file
30
static/app/assets/index-DvbGwVAp.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,8 +4,8 @@
|
|||||||
<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-fYGyZipT.js"></script>
|
<script type="module" crossorigin src="./assets/index-DvbGwVAp.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-CZCRHVLY.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-Baiuy_-z.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>该页面需要启用 JavaScript 才能使用。</noscript>
|
<noscript>该页面需要启用 JavaScript 才能使用。</noscript>
|
||||||
|
|||||||
Reference in New Issue
Block a user