feat(app): migrate /app accounts to Vue SPA (stage 3)
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { publicApi } from './http'
|
||||||
|
|
||||||
|
export async function fetchRunStats() {
|
||||||
|
const { data } = await publicApi.get('/run_stats')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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 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-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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
@@ -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}
|
|
||||||
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};
|
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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