feat(app): add announcements, feedback, settings (stage 5)

This commit is contained in:
2025-12-14 00:27:05 +08:00
parent 54cf6fe538
commit 69443c2de6
20 changed files with 715 additions and 64 deletions

View File

@@ -0,0 +1,12 @@
import { publicApi } from './http'
export async function fetchActiveAnnouncement() {
const { data } = await publicApi.get('/announcements/active')
return data
}
export async function dismissAnnouncement(announcementId) {
const { data } = await publicApi.post(`/announcements/${announcementId}/dismiss`, {})
return data
}

View File

@@ -0,0 +1,12 @@
import { publicApi } from './http'
export async function submitFeedback(payload) {
const { data } = await publicApi.post('/feedback', payload)
return data
}
export async function fetchMyFeedbacks() {
const { data } = await publicApi.get('/feedback')
return data
}

View File

@@ -0,0 +1,32 @@
import { publicApi } from './http'
export async function fetchUserEmail() {
const { data } = await publicApi.get('/user/email')
return data
}
export async function bindEmail(payload) {
const { data } = await publicApi.post('/user/bind-email', payload)
return data
}
export async function unbindEmail() {
const { data } = await publicApi.post('/user/unbind-email', {})
return data
}
export async function fetchEmailNotify() {
const { data } = await publicApi.get('/user/email-notify')
return data
}
export async function updateEmailNotify(payload) {
const { data } = await publicApi.post('/user/email-notify', payload)
return data
}
export async function changePassword(payload) {
const { data } = await publicApi.post('/user/password', payload)
return data
}

View File

@@ -1,9 +1,19 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Calendar, Camera, User } from '@element-plus/icons-vue'
import { fetchActiveAnnouncement, dismissAnnouncement } from '../api/announcements'
import { fetchMyFeedbacks, submitFeedback } from '../api/feedback'
import {
bindEmail,
changePassword,
fetchEmailNotify,
fetchUserEmail,
unbindEmail,
updateEmailNotify,
} from '../api/settings'
import { useUserStore } from '../stores/user'
const route = useRoute()
@@ -14,6 +24,42 @@ const isMobile = ref(false)
const drawerOpen = ref(false)
let mediaQuery
const announcementOpen = ref(false)
const announcement = ref(null)
const announcementLoading = ref(false)
const feedbackOpen = ref(false)
const feedbackTab = ref('new')
const feedbackSubmitting = ref(false)
const feedbackLoading = ref(false)
const myFeedbacks = ref([])
const feedbackForm = reactive({
title: '',
description: '',
contact: '',
})
const settingsOpen = ref(false)
const settingsTab = ref('email')
const emailLoading = ref(false)
const bindEmailLoading = ref(false)
const emailInfo = reactive({
email: '',
email_verified: false,
})
const bindEmailValue = ref('')
const emailNotifyLoading = ref(false)
const emailNotifyEnabled = ref(true)
const passwordLoading = ref(false)
const passwordForm = reactive({
current_password: '',
new_password: '',
confirm_password: '',
})
function syncIsMobile() {
isMobile.value = Boolean(mediaQuery?.matches)
if (!isMobile.value) drawerOpen.value = false
@@ -27,6 +73,8 @@ onMounted(() => {
userStore.refreshVipInfo().catch(() => {
window.location.href = '/login'
})
loadAnnouncement()
})
onBeforeUnmount(() => {
@@ -60,6 +108,258 @@ async function logout() {
await userStore.logout()
window.location.href = '/login'
}
function openFeedbackForm() {
feedbackTab.value = 'new'
feedbackForm.title = ''
feedbackForm.description = ''
feedbackForm.contact = ''
feedbackOpen.value = true
}
async function openMyFeedbacks() {
feedbackTab.value = 'list'
feedbackOpen.value = true
await loadMyFeedbacks()
}
async function loadMyFeedbacks() {
feedbackLoading.value = true
try {
const list = await fetchMyFeedbacks()
myFeedbacks.value = Array.isArray(list) ? list : []
} catch {
myFeedbacks.value = []
} finally {
feedbackLoading.value = false
}
}
function feedbackStatusLabel(status) {
if (status === 'replied') return '已回复'
if (status === 'closed') return '已关闭'
return '待处理'
}
function feedbackStatusTagType(status) {
if (status === 'replied') return 'success'
if (status === 'closed') return 'info'
return 'warning'
}
async function submitFeedbackForm() {
const title = feedbackForm.title.trim()
const description = feedbackForm.description.trim()
const contact = feedbackForm.contact.trim()
if (!title || !description) {
ElMessage.error('标题和描述不能为空')
return
}
feedbackSubmitting.value = true
try {
const res = await submitFeedback({ title, description, contact })
ElMessage.success(res?.message || '反馈提交成功')
feedbackOpen.value = false
feedbackForm.title = ''
feedbackForm.description = ''
feedbackForm.contact = ''
} catch (e) {
const data = e?.response?.data
ElMessage.error(data?.error || '提交失败')
} finally {
feedbackSubmitting.value = false
}
}
async function openSettings() {
settingsOpen.value = true
settingsTab.value = 'email'
await loadSettings()
}
async function loadSettings() {
await Promise.all([loadEmailInfo(), loadEmailNotify()])
}
async function loadEmailInfo() {
emailLoading.value = true
try {
const data = await fetchUserEmail()
emailInfo.email = data?.email || ''
emailInfo.email_verified = Boolean(data?.email_verified)
bindEmailValue.value = emailInfo.email || ''
} catch {
emailInfo.email = ''
emailInfo.email_verified = false
bindEmailValue.value = ''
} finally {
emailLoading.value = false
}
}
async function loadEmailNotify() {
emailNotifyLoading.value = true
try {
const data = await fetchEmailNotify()
emailNotifyEnabled.value = Boolean(data?.enabled)
} catch {
emailNotifyEnabled.value = true
} finally {
emailNotifyLoading.value = false
}
}
async function onBindEmail() {
const email = bindEmailValue.value.trim().toLowerCase()
if (!email) {
ElMessage.error('请输入邮箱地址')
return
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
ElMessage.error('邮箱格式不正确')
return
}
bindEmailLoading.value = true
try {
const res = await bindEmail({ email })
ElMessage.success(res?.message || '验证邮件已发送')
emailInfo.email = email
emailInfo.email_verified = false
} catch (e) {
const data = e?.response?.data
ElMessage.error(data?.error || '绑定失败')
} finally {
bindEmailLoading.value = false
}
}
async function onUnbindEmail() {
try {
await ElMessageBox.confirm('确定要解绑当前邮箱吗?', '解绑邮箱', {
confirmButtonText: '解绑',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
const res = await unbindEmail()
if (res?.success) {
ElMessage.success(res?.message || '邮箱已解绑')
await loadEmailInfo()
return
}
ElMessage.error(res?.error || '解绑失败')
} catch (e) {
const data = e?.response?.data
ElMessage.error(data?.error || '解绑失败')
}
}
async function onToggleEmailNotify(value) {
const previous = emailNotifyEnabled.value
emailNotifyEnabled.value = Boolean(value)
emailNotifyLoading.value = true
try {
const res = await updateEmailNotify({ enabled: Boolean(value) })
if (res?.success) {
ElMessage.success('已更新')
return
}
emailNotifyEnabled.value = previous
ElMessage.error(res?.error || '更新失败')
} catch (e) {
emailNotifyEnabled.value = previous
const data = e?.response?.data
ElMessage.error(data?.error || '更新失败')
} finally {
emailNotifyLoading.value = false
}
}
async function onChangePassword() {
const currentPassword = passwordForm.current_password
const newPassword = passwordForm.new_password
const confirmPassword = passwordForm.confirm_password
if (!currentPassword || !newPassword || !confirmPassword) {
ElMessage.error('请填写完整信息')
return
}
if (String(newPassword).length < 6) {
ElMessage.error('新密码至少6位')
return
}
if (newPassword !== confirmPassword) {
ElMessage.error('两次输入的新密码不一致')
return
}
passwordLoading.value = true
try {
const res = await changePassword({ current_password: currentPassword, new_password: newPassword })
if (res?.success) {
ElMessage.success('密码修改成功')
passwordForm.current_password = ''
passwordForm.new_password = ''
passwordForm.confirm_password = ''
return
}
ElMessage.error(res?.error || '修改失败')
} catch (e) {
const data = e?.response?.data
ElMessage.error(data?.error || '修改失败')
} finally {
passwordLoading.value = false
}
}
async function loadAnnouncement() {
announcementLoading.value = true
try {
const data = await fetchActiveAnnouncement()
const ann = data?.announcement
if (!ann?.id) return
const sessionKey = `announcement_closed_${ann.id}`
if (window.sessionStorage.getItem(sessionKey) === '1') return
announcement.value = ann
announcementOpen.value = true
} catch {
// ignore
} finally {
announcementLoading.value = false
}
}
function closeAnnouncementOnce() {
const ann = announcement.value
if (ann?.id) window.sessionStorage.setItem(`announcement_closed_${ann.id}`, '1')
announcementOpen.value = false
}
async function dismissAnnouncementPermanently() {
const ann = announcement.value
if (!ann?.id) {
announcementOpen.value = false
return
}
try {
const res = await dismissAnnouncement(ann.id)
if (res?.success) ElMessage.success('已永久关闭')
} catch {
// ignore
} finally {
announcementOpen.value = false
}
}
</script>
<template>
@@ -96,6 +396,8 @@ async function logout() {
({{ userStore.vipDaysLeft }}天后到期)
</span>
</div>
<el-button text type="primary" @click="openFeedbackForm">反馈</el-button>
<el-button text @click="openSettings">设置</el-button>
<el-button type="primary" plain @click="logout">退出</el-button>
</div>
</el-header>
@@ -122,9 +424,191 @@ async function logout() {
</el-menu-item>
</el-menu>
<div class="drawer-actions">
<el-button text type="primary" style="width: 100%" @click="openFeedbackForm">问题反馈</el-button>
<el-button text style="width: 100%" @click="openSettings">个人设置</el-button>
<el-button type="primary" plain style="width: 100%" @click="logout">退出登录</el-button>
</div>
</el-drawer>
<el-dialog v-model="announcementOpen" width="min(560px, 92vw)" :title="announcement?.title || '系统公告'">
<div class="announcement-body" v-loading="announcementLoading">
<div class="announcement-content">{{ announcement?.content || '' }}</div>
</div>
<template #footer>
<el-button @click="closeAnnouncementOnce">当次关闭</el-button>
<el-button type="primary" @click="dismissAnnouncementPermanently">永久关闭</el-button>
</template>
</el-dialog>
<el-dialog v-model="feedbackOpen" title="问题反馈" width="min(720px, 92vw)">
<el-tabs v-model="feedbackTab" @tab-change="(name) => name === 'list' && loadMyFeedbacks()">
<el-tab-pane label="提交反馈" name="new">
<el-form label-position="top">
<el-form-item label="标题">
<el-input v-model="feedbackForm.title" placeholder="简要描述问题" maxlength="100" show-word-limit />
</el-form-item>
<el-form-item label="详细描述">
<el-input v-model="feedbackForm.description" type="textarea" :rows="5" placeholder="请详细描述您遇到的问题" maxlength="2000" show-word-limit />
</el-form-item>
<el-form-item label="联系方式(可选)">
<el-input v-model="feedbackForm.contact" placeholder="方便我们联系您" />
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="我的反馈" name="list">
<el-skeleton v-if="feedbackLoading" :rows="6" animated />
<template v-else>
<el-empty v-if="myFeedbacks.length === 0" description="暂无反馈" />
<el-collapse v-else accordion>
<el-collapse-item v-for="item in myFeedbacks" :key="item.id" :name="String(item.id)">
<template #title>
<div class="feedback-title">
<span class="feedback-title-text">{{ item.title }}</span>
<el-tag size="small" effect="light" :type="feedbackStatusTagType(item.status)">
{{ feedbackStatusLabel(item.status) }}
</el-tag>
<span class="feedback-time app-muted">{{ item.created_at || '' }}</span>
</div>
</template>
<div class="feedback-body">
<div class="feedback-section">
<div class="feedback-label app-muted">描述</div>
<div class="feedback-text">{{ item.description }}</div>
</div>
<div v-if="item.admin_reply" class="feedback-section">
<div class="feedback-label app-muted">管理员回复</div>
<div class="feedback-text">{{ item.admin_reply }}</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
</template>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-button @click="feedbackOpen = false">关闭</el-button>
<el-button v-if="feedbackTab === 'list'" @click="loadMyFeedbacks">刷新</el-button>
<el-button v-if="feedbackTab === 'new'" type="primary" :loading="feedbackSubmitting" @click="submitFeedbackForm">提交</el-button>
</template>
</el-dialog>
<el-dialog v-model="settingsOpen" title="个人设置" width="min(720px, 92vw)">
<el-tabs v-model="settingsTab">
<el-tab-pane label="邮箱绑定" name="email">
<div v-loading="emailLoading" class="settings-section">
<el-alert
v-if="emailInfo.email && emailInfo.email_verified"
type="success"
:closable="false"
title="邮箱已绑定并验证"
show-icon
class="settings-alert"
>
<template #default>
<div class="email-row">
<div class="email-value">{{ emailInfo.email }}</div>
<el-button type="danger" text @click="onUnbindEmail">解绑</el-button>
</div>
</template>
</el-alert>
<el-alert
v-else-if="emailInfo.email"
type="warning"
:closable="false"
title="邮箱待验证:请查收验证邮件(含垃圾箱)"
show-icon
class="settings-alert"
/>
<el-form label-position="top">
<el-form-item label="邮箱地址">
<el-input v-model="bindEmailValue" placeholder="name@example.com" />
</el-form-item>
<el-button type="primary" :loading="bindEmailLoading" @click="onBindEmail">发送验证邮件</el-button>
</el-form>
<el-divider />
<div class="notify-row">
<div>
<div class="notify-title">任务完成通知</div>
<div class="app-muted notify-desc">定时任务完成后发送邮件</div>
</div>
<el-switch
:model-value="emailNotifyEnabled"
:disabled="!emailInfo.email_verified || emailNotifyLoading"
inline-prompt
active-text=""
inactive-text=""
@change="onToggleEmailNotify"
/>
</div>
<el-alert
v-if="!emailInfo.email_verified"
type="info"
:closable="false"
title="绑定并验证邮箱后可开启邮件通知。"
show-icon
class="settings-hint"
/>
</div>
</el-tab-pane>
<el-tab-pane label="修改密码" name="password">
<div class="settings-section">
<el-form label-position="top">
<el-form-item label="当前密码">
<el-input v-model="passwordForm.current_password" type="password" show-password autocomplete="current-password" />
</el-form-item>
<el-form-item label="新密码至少6位">
<el-input v-model="passwordForm.new_password" type="password" show-password autocomplete="new-password" />
</el-form-item>
<el-form-item label="确认新密码">
<el-input
v-model="passwordForm.confirm_password"
type="password"
show-password
autocomplete="new-password"
@keyup.enter="onChangePassword"
/>
</el-form-item>
<el-button type="primary" :loading="passwordLoading" @click="onChangePassword">确认修改</el-button>
</el-form>
</div>
</el-tab-pane>
<el-tab-pane label="VIP信息" name="vip">
<div class="settings-section">
<el-alert
:type="userStore.isVip ? 'success' : 'info'"
:closable="false"
:title="userStore.isVip ? '当前为 VIP 会员' : '当前为普通用户'"
show-icon
class="settings-alert"
/>
<div v-if="userStore.isVip" class="vip-info">
<div class="vip-line">
<span class="app-muted">到期时间</span>
<span>{{ userStore.vipExpireTime || '未知' }}</span>
</div>
<div class="vip-line">
<span class="app-muted">剩余天数</span>
<span>{{ userStore.vipDaysLeft }}</span>
</div>
</div>
<div v-else class="vip-info">
<div class="app-muted">升级方式请通过反馈联系管理员开通</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-button @click="settingsOpen = false">关闭</el-button>
</template>
</el-dialog>
</el-container>
</template>
@@ -232,6 +716,113 @@ async function logout() {
border-top: 1px solid var(--app-border);
}
.announcement-body {
min-height: 80px;
}
.announcement-content {
white-space: pre-wrap;
line-height: 1.6;
font-size: 14px;
}
.feedback-title {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
min-width: 0;
}
.feedback-title-text {
font-weight: 800;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.feedback-time {
margin-left: auto;
font-size: 12px;
white-space: nowrap;
}
.feedback-body {
padding: 6px 0 2px;
}
.feedback-section + .feedback-section {
margin-top: 12px;
}
.feedback-label {
font-size: 12px;
margin-bottom: 6px;
}
.feedback-text {
white-space: pre-wrap;
line-height: 1.6;
font-size: 13px;
}
.settings-section {
padding: 6px 2px 2px;
}
.settings-alert {
margin-bottom: 12px;
}
.email-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.email-value {
font-weight: 800;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.notify-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.notify-title {
font-weight: 800;
}
.notify-desc {
margin-top: 4px;
font-size: 12px;
}
.settings-hint {
margin-top: 10px;
}
.vip-info {
margin-top: 12px;
display: grid;
gap: 10px;
}
.vip-line {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
@media (max-width: 768px) {
.layout-header {
flex-wrap: wrap;

8
app.py
View File

@@ -3697,9 +3697,13 @@ def change_user_password():
if len(new_password) < 6:
return jsonify({"error": "新密码至少6位"}), 400
# 验证当前密码
# 验证当前密码(使用现有 bcrypt/SHA256 兼容逻辑)
user = database.get_user_by_id(current_user.id)
if not user or not check_password_hash(user['password_hash'], current_password):
if not user:
return jsonify({"error": "用户不存在"}), 404
username = user.get('username', '')
if not username or not database.verify_user(username, current_password):
return jsonify({"error": "当前密码错误"}), 400
# 更新密码(使用管理员重置密码的函数,因为已经验证过当前密码了)

View File

@@ -1,13 +1,13 @@
{
"_accounts-DI7tHfNj.js": {
"file": "assets/accounts-DI7tHfNj.js",
"_accounts-L94-op7C.js": {
"file": "assets/accounts-L94-op7C.js",
"name": "accounts",
"imports": [
"index.html"
]
},
"_auth-CgGRYkq1.js": {
"file": "assets/auth-CgGRYkq1.js",
"_auth-BTF1DcMg.js": {
"file": "assets/auth-BTF1DcMg.js",
"name": "auth",
"imports": [
"index.html"
@@ -18,7 +18,7 @@
"name": "password"
},
"index.html": {
"file": "assets/index-BstQMnWL.js",
"file": "assets/index-iA-vJJPI.js",
"name": "index",
"src": "index.html",
"isEntry": true,
@@ -32,16 +32,16 @@
"src/pages/ScreenshotsPage.vue"
],
"css": [
"assets/index-Baiuy_-z.css"
"assets/index-Cvi4RJz4.css"
]
},
"src/pages/AccountsPage.vue": {
"file": "assets/AccountsPage-Bcis23qR.js",
"file": "assets/AccountsPage-C6vp-hsE.js",
"name": "AccountsPage",
"src": "src/pages/AccountsPage.vue",
"isDynamicEntry": true,
"imports": [
"_accounts-DI7tHfNj.js",
"_accounts-L94-op7C.js",
"index.html"
],
"css": [
@@ -49,13 +49,13 @@
]
},
"src/pages/LoginPage.vue": {
"file": "assets/LoginPage-BNb9NBzk.js",
"file": "assets/LoginPage-B3UYJO5m.js",
"name": "LoginPage",
"src": "src/pages/LoginPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html",
"_auth-CgGRYkq1.js",
"_auth-BTF1DcMg.js",
"_password-7ryi82gE.js"
],
"css": [
@@ -63,26 +63,26 @@
]
},
"src/pages/RegisterPage.vue": {
"file": "assets/RegisterPage-CFiuiL-s.js",
"file": "assets/RegisterPage-DZG6_P91.js",
"name": "RegisterPage",
"src": "src/pages/RegisterPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html",
"_auth-CgGRYkq1.js"
"_auth-BTF1DcMg.js"
],
"css": [
"assets/RegisterPage-CVjBOq6i.css"
]
},
"src/pages/ResetPasswordPage.vue": {
"file": "assets/ResetPasswordPage-DeWXyaHc.js",
"file": "assets/ResetPasswordPage-BSHAEmwh.js",
"name": "ResetPasswordPage",
"src": "src/pages/ResetPasswordPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html",
"_auth-CgGRYkq1.js",
"_auth-BTF1DcMg.js",
"_password-7ryi82gE.js"
],
"css": [
@@ -90,12 +90,12 @@
]
},
"src/pages/SchedulesPage.vue": {
"file": "assets/SchedulesPage-BXyRqadY.js",
"file": "assets/SchedulesPage-BEe8nXPf.js",
"name": "SchedulesPage",
"src": "src/pages/SchedulesPage.vue",
"isDynamicEntry": true,
"imports": [
"_accounts-DI7tHfNj.js",
"_accounts-L94-op7C.js",
"index.html"
],
"css": [
@@ -103,7 +103,7 @@
]
},
"src/pages/ScreenshotsPage.vue": {
"file": "assets/ScreenshotsPage-BwyU04c1.js",
"file": "assets/ScreenshotsPage-B2uEhK_5.js",
"name": "ScreenshotsPage",
"src": "src/pages/ScreenshotsPage.vue",
"isDynamicEntry": true,
@@ -115,7 +115,7 @@
]
},
"src/pages/VerifyResultPage.vue": {
"file": "assets/VerifyResultPage-DgCNTQ4L.js",
"file": "assets/VerifyResultPage-s3b-_h4m.js",
"name": "VerifyResultPage",
"src": "src/pages/VerifyResultPage.vue",
"isDynamicEntry": true,

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -1 +1 @@
import{p as C,_ as P,a as y,o as R,h as w,w as s,e as _,f as c,g as o,b,d as i,k as f,F as B,v as D,t as T,x as I,E as l}from"./index-BstQMnWL.js";async function F(){const{data:d}=await C.get("/screenshots");return d}async function L(d){const{data:u}=await C.delete(`/screenshots/${encodeURIComponent(d)}`);return u}async function O(){const{data:d}=await C.post("/screenshots/clear",{});return d}const j={class:"panel-head"},q={class:"panel-actions"},G={key:1,class:"grid"},H=["src","alt","onClick"],J={class:"shot-body"},K=["title"],Q={class:"shot-meta app-muted"},W={class:"shot-actions"},X={class:"preview"},Y=["src","alt"],Z={__name:"ScreenshotsPage",setup(d){const u=y(!1),r=y([]),p=y(!1),g=y(""),h=y("");function v(a){return`/screenshots/${encodeURIComponent(a)}`}async function x(){u.value=!0;try{const a=await F();r.value=Array.isArray(a)?a:[]}catch(a){a?.response?.status===401&&(window.location.href="/login"),r.value=[]}finally{u.value=!1}}function S(a){h.value=a.display_name||a.filename||"截图预览",g.value=v(a.filename),p.value=!0}async function U(){try{await I.confirm("确定要清空全部截图吗?","清空截图",{confirmButtonText:"清空",cancelButtonText:"取消",type:"warning"})}catch{return}try{const a=await O();if(a?.success){l.success(`已清空(删除 ${a?.deleted||0} 张)`),r.value=[],p.value=!1;return}l.error(a?.error||"操作失败")}catch(a){const e=a?.response?.data;l.error(e?.error||"操作失败")}}async function V(a){try{await I.confirm(`确定要删除截图「${a.display_name||a.filename}」吗?`,"删除截图",{confirmButtonText:"删除",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await L(a.filename);if(e?.success){r.value=r.value.filter(n=>n.filename!==a.filename),g.value.includes(encodeURIComponent(a.filename))&&(p.value=!1),l.success("已删除");return}l.error(e?.error||"删除失败")}catch(e){const n=e?.response?.data;l.error(n?.error||"删除失败")}}async function E(a){const e=`${window.location.origin}${v(a.filename)}`;try{await navigator.clipboard.writeText(e),l.success("链接已复制")}catch{l.warning("复制失败,请手动复制链接")}}async function z(a){const e=v(a.filename);try{const m=await(await fetch(e,{credentials:"include"})).blob();await navigator.clipboard.write([new ClipboardItem({[m.type]:m})]),l.success("图片已复制")}catch{await E(a)}}function A(a){const e=document.createElement("a");e.href=v(a.filename),e.download=a.display_name||a.filename,document.body.appendChild(e),e.click(),e.remove()}return R(x),(a,e)=>{const n=_("el-button"),m=_("el-skeleton"),M=_("el-empty"),$=_("el-card"),N=_("el-dialog");return c(),w($,{shadow:"never",class:"panel","body-style":{padding:"14px"}},{default:s(()=>[o("div",j,[e[4]||(e[4]=o("div",{class:"panel-title"},"截图管理",-1)),o("div",q,[i(n,{loading:u.value,onClick:x},{default:s(()=>[...e[2]||(e[2]=[f("刷新",-1)])]),_:1},8,["loading"]),i(n,{type:"danger",plain:"",disabled:r.value.length===0,onClick:U},{default:s(()=>[...e[3]||(e[3]=[f("清空全部",-1)])]),_:1},8,["disabled"])])]),u.value?(c(),w(m,{key:0,rows:6,animated:""})):(c(),b(B,{key:1},[r.value.length===0?(c(),w(M,{key:0,description:"暂无截图"})):(c(),b("div",G,[(c(!0),b(B,null,D(r.value,t=>(c(),w($,{key:t.filename,shadow:"never",class:"shot-card","body-style":{padding:"0"}},{default:s(()=>[o("img",{class:"shot-img",src:v(t.filename),alt:t.display_name||t.filename,loading:"lazy",onClick:k=>S(t)},null,8,H),o("div",J,[o("div",{class:"shot-name",title:t.display_name||t.filename},T(t.display_name||t.filename),9,K),o("div",Q,T(t.created||""),1),o("div",W,[i(n,{size:"small",text:"",type:"primary",onClick:k=>z(t)},{default:s(()=>[...e[5]||(e[5]=[f("复制图片",-1)])]),_:1},8,["onClick"]),i(n,{size:"small",text:"",onClick:k=>A(t)},{default:s(()=>[...e[6]||(e[6]=[f("下载",-1)])]),_:1},8,["onClick"]),i(n,{size:"small",text:"",type:"danger",onClick:k=>V(t)},{default:s(()=>[...e[7]||(e[7]=[f("删除",-1)])]),_:1},8,["onClick"])])])]),_:2},1024))),128))]))],64)),i(N,{modelValue:p.value,"onUpdate:modelValue":e[1]||(e[1]=t=>p.value=t),title:h.value,width:"min(920px, 94vw)"},{footer:s(()=>[i(n,{onClick:e[0]||(e[0]=t=>p.value=!1)},{default:s(()=>[...e[8]||(e[8]=[f("关闭",-1)])]),_:1})]),default:s(()=>[o("div",X,[o("img",{src:g.value,alt:h.value,class:"preview-img"},null,8,Y)])]),_:1},8,["modelValue","title"])]),_:1})}}},ae=P(Z,[["__scopeId","data-v-ff08abf8"]]);export{ae as default};
import{p as C,_ as P,a as y,o as R,h as w,w as s,e as _,f as c,g as o,b,d as i,k as f,F as B,v as D,t as T,x as I,E as l}from"./index-iA-vJJPI.js";async function F(){const{data:d}=await C.get("/screenshots");return d}async function L(d){const{data:u}=await C.delete(`/screenshots/${encodeURIComponent(d)}`);return u}async function O(){const{data:d}=await C.post("/screenshots/clear",{});return d}const j={class:"panel-head"},q={class:"panel-actions"},G={key:1,class:"grid"},H=["src","alt","onClick"],J={class:"shot-body"},K=["title"],Q={class:"shot-meta app-muted"},W={class:"shot-actions"},X={class:"preview"},Y=["src","alt"],Z={__name:"ScreenshotsPage",setup(d){const u=y(!1),r=y([]),p=y(!1),g=y(""),h=y("");function v(a){return`/screenshots/${encodeURIComponent(a)}`}async function x(){u.value=!0;try{const a=await F();r.value=Array.isArray(a)?a:[]}catch(a){a?.response?.status===401&&(window.location.href="/login"),r.value=[]}finally{u.value=!1}}function S(a){h.value=a.display_name||a.filename||"截图预览",g.value=v(a.filename),p.value=!0}async function U(){try{await I.confirm("确定要清空全部截图吗?","清空截图",{confirmButtonText:"清空",cancelButtonText:"取消",type:"warning"})}catch{return}try{const a=await O();if(a?.success){l.success(`已清空(删除 ${a?.deleted||0} 张)`),r.value=[],p.value=!1;return}l.error(a?.error||"操作失败")}catch(a){const e=a?.response?.data;l.error(e?.error||"操作失败")}}async function V(a){try{await I.confirm(`确定要删除截图「${a.display_name||a.filename}」吗?`,"删除截图",{confirmButtonText:"删除",cancelButtonText:"取消",type:"warning"})}catch{return}try{const e=await L(a.filename);if(e?.success){r.value=r.value.filter(n=>n.filename!==a.filename),g.value.includes(encodeURIComponent(a.filename))&&(p.value=!1),l.success("已删除");return}l.error(e?.error||"删除失败")}catch(e){const n=e?.response?.data;l.error(n?.error||"删除失败")}}async function E(a){const e=`${window.location.origin}${v(a.filename)}`;try{await navigator.clipboard.writeText(e),l.success("链接已复制")}catch{l.warning("复制失败,请手动复制链接")}}async function z(a){const e=v(a.filename);try{const m=await(await fetch(e,{credentials:"include"})).blob();await navigator.clipboard.write([new ClipboardItem({[m.type]:m})]),l.success("图片已复制")}catch{await E(a)}}function A(a){const e=document.createElement("a");e.href=v(a.filename),e.download=a.display_name||a.filename,document.body.appendChild(e),e.click(),e.remove()}return R(x),(a,e)=>{const n=_("el-button"),m=_("el-skeleton"),M=_("el-empty"),$=_("el-card"),N=_("el-dialog");return c(),w($,{shadow:"never",class:"panel","body-style":{padding:"14px"}},{default:s(()=>[o("div",j,[e[4]||(e[4]=o("div",{class:"panel-title"},"截图管理",-1)),o("div",q,[i(n,{loading:u.value,onClick:x},{default:s(()=>[...e[2]||(e[2]=[f("刷新",-1)])]),_:1},8,["loading"]),i(n,{type:"danger",plain:"",disabled:r.value.length===0,onClick:U},{default:s(()=>[...e[3]||(e[3]=[f("清空全部",-1)])]),_:1},8,["disabled"])])]),u.value?(c(),w(m,{key:0,rows:6,animated:""})):(c(),b(B,{key:1},[r.value.length===0?(c(),w(M,{key:0,description:"暂无截图"})):(c(),b("div",G,[(c(!0),b(B,null,D(r.value,t=>(c(),w($,{key:t.filename,shadow:"never",class:"shot-card","body-style":{padding:"0"}},{default:s(()=>[o("img",{class:"shot-img",src:v(t.filename),alt:t.display_name||t.filename,loading:"lazy",onClick:k=>S(t)},null,8,H),o("div",J,[o("div",{class:"shot-name",title:t.display_name||t.filename},T(t.display_name||t.filename),9,K),o("div",Q,T(t.created||""),1),o("div",W,[i(n,{size:"small",text:"",type:"primary",onClick:k=>z(t)},{default:s(()=>[...e[5]||(e[5]=[f("复制图片",-1)])]),_:1},8,["onClick"]),i(n,{size:"small",text:"",onClick:k=>A(t)},{default:s(()=>[...e[6]||(e[6]=[f("下载",-1)])]),_:1},8,["onClick"]),i(n,{size:"small",text:"",type:"danger",onClick:k=>V(t)},{default:s(()=>[...e[7]||(e[7]=[f("删除",-1)])]),_:1},8,["onClick"])])])]),_:2},1024))),128))]))],64)),i(N,{modelValue:p.value,"onUpdate:modelValue":e[1]||(e[1]=t=>p.value=t),title:h.value,width:"min(920px, 94vw)"},{footer:s(()=>[i(n,{onClick:e[0]||(e[0]=t=>p.value=!1)},{default:s(()=>[...e[8]||(e[8]=[f("关闭",-1)])]),_:1})]),default:s(()=>[o("div",X,[o("img",{src:g.value,alt:h.value,class:"preview-img"},null,8,Y)])]),_:1},8,["modelValue","title"])]),_:1})}}},ae=P(Z,[["__scopeId","data-v-ff08abf8"]]);export{ae as default};

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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