feat(app): migrate auth pages to Vue SPA (stage 2)
This commit is contained in:
41
app-frontend/src/api/auth.js
Normal file
41
app-frontend/src/api/auth.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { publicApi } from './http'
|
||||
|
||||
export async function fetchEmailVerifyStatus() {
|
||||
const { data } = await publicApi.get('/email/verify-status')
|
||||
return data
|
||||
}
|
||||
|
||||
export async function generateCaptcha() {
|
||||
const { data } = await publicApi.post('/generate_captcha', {})
|
||||
return data
|
||||
}
|
||||
|
||||
export async function login(payload) {
|
||||
const { data } = await publicApi.post('/login', payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function register(payload) {
|
||||
const { data } = await publicApi.post('/register', payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function resendVerifyEmail(payload) {
|
||||
const { data } = await publicApi.post('/resend-verify-email', payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function forgotPassword(payload) {
|
||||
const { data } = await publicApi.post('/forgot-password', payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function requestPasswordReset(payload) {
|
||||
const { data } = await publicApi.post('/reset_password_request', payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function confirmPasswordReset(payload) {
|
||||
const { data } = await publicApi.post('/reset-password-confirm', payload)
|
||||
return data
|
||||
}
|
||||
8
app-frontend/src/api/http.js
Normal file
8
app-frontend/src/api/http.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export const publicApi = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 30_000,
|
||||
withCredentials: true,
|
||||
})
|
||||
|
||||
@@ -1,11 +1,270 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import {
|
||||
fetchEmailVerifyStatus,
|
||||
forgotPassword,
|
||||
generateCaptcha,
|
||||
login,
|
||||
requestPasswordReset,
|
||||
resendVerifyEmail,
|
||||
} from '../api/auth'
|
||||
import { validateStrongPassword } from '../utils/password'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
captcha: '',
|
||||
})
|
||||
|
||||
const needCaptcha = ref(false)
|
||||
const captchaImage = ref('')
|
||||
const captchaSession = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const emailEnabled = ref(false)
|
||||
const registerVerifyEnabled = ref(false)
|
||||
|
||||
const forgotOpen = ref(false)
|
||||
const resendOpen = ref(false)
|
||||
|
||||
const emailResetForm = reactive({
|
||||
email: '',
|
||||
captcha: '',
|
||||
})
|
||||
const emailResetCaptchaImage = ref('')
|
||||
const emailResetCaptchaSession = ref('')
|
||||
const emailResetLoading = ref(false)
|
||||
|
||||
const manualResetForm = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
new_password: '',
|
||||
})
|
||||
const manualResetLoading = ref(false)
|
||||
|
||||
const resendForm = reactive({
|
||||
email: '',
|
||||
captcha: '',
|
||||
})
|
||||
const resendCaptchaImage = ref('')
|
||||
const resendCaptchaSession = ref('')
|
||||
const resendLoading = ref(false)
|
||||
|
||||
const showResendLink = computed(() => Boolean(registerVerifyEnabled.value))
|
||||
|
||||
async function refreshLoginCaptcha() {
|
||||
try {
|
||||
const data = await generateCaptcha()
|
||||
captchaSession.value = data?.session_id || ''
|
||||
captchaImage.value = data?.captcha_image || ''
|
||||
form.captcha = ''
|
||||
} catch {
|
||||
captchaSession.value = ''
|
||||
captchaImage.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshEmailResetCaptcha() {
|
||||
try {
|
||||
const data = await generateCaptcha()
|
||||
emailResetCaptchaSession.value = data?.session_id || ''
|
||||
emailResetCaptchaImage.value = data?.captcha_image || ''
|
||||
emailResetForm.captcha = ''
|
||||
} catch {
|
||||
emailResetCaptchaSession.value = ''
|
||||
emailResetCaptchaImage.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshResendCaptcha() {
|
||||
try {
|
||||
const data = await generateCaptcha()
|
||||
resendCaptchaSession.value = data?.session_id || ''
|
||||
resendCaptchaImage.value = data?.captcha_image || ''
|
||||
resendForm.captcha = ''
|
||||
} catch {
|
||||
resendCaptchaSession.value = ''
|
||||
resendCaptchaImage.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!form.username.trim() || !form.password.trim()) {
|
||||
ElMessage.error('用户名和密码不能为空')
|
||||
return
|
||||
}
|
||||
if (needCaptcha.value && !form.captcha.trim()) {
|
||||
ElMessage.error('请输入验证码')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await login({
|
||||
username: form.username.trim(),
|
||||
password: form.password,
|
||||
captcha_session: captchaSession.value,
|
||||
captcha: form.captcha.trim(),
|
||||
need_captcha: needCaptcha.value,
|
||||
})
|
||||
ElMessage.success('登录成功,正在跳转...')
|
||||
setTimeout(() => {
|
||||
window.location.href = '/app'
|
||||
}, 300)
|
||||
} catch (e) {
|
||||
const status = e?.response?.status
|
||||
const data = e?.response?.data
|
||||
const message = data?.error || data?.message || '登录失败'
|
||||
|
||||
ElMessage.error(message)
|
||||
|
||||
if (data?.need_captcha) {
|
||||
needCaptcha.value = true
|
||||
await refreshLoginCaptcha()
|
||||
} else if (needCaptcha.value && status === 400) {
|
||||
await refreshLoginCaptcha()
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openForgot() {
|
||||
forgotOpen.value = true
|
||||
|
||||
if (emailEnabled.value) {
|
||||
emailResetForm.email = ''
|
||||
emailResetForm.captcha = ''
|
||||
await refreshEmailResetCaptcha()
|
||||
} else {
|
||||
manualResetForm.username = ''
|
||||
manualResetForm.email = ''
|
||||
manualResetForm.new_password = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function submitForgot() {
|
||||
if (emailEnabled.value) {
|
||||
const email = emailResetForm.email.trim()
|
||||
if (!email) {
|
||||
ElMessage.error('请输入邮箱')
|
||||
return
|
||||
}
|
||||
if (!emailResetForm.captcha.trim()) {
|
||||
ElMessage.error('请输入验证码')
|
||||
return
|
||||
}
|
||||
|
||||
emailResetLoading.value = true
|
||||
try {
|
||||
const res = await forgotPassword({
|
||||
email,
|
||||
captcha_session: emailResetCaptchaSession.value,
|
||||
captcha: emailResetForm.captcha.trim(),
|
||||
})
|
||||
ElMessage.success(res?.message || '已发送重置邮件')
|
||||
setTimeout(() => {
|
||||
forgotOpen.value = false
|
||||
}, 800)
|
||||
} catch (e) {
|
||||
const data = e?.response?.data
|
||||
ElMessage.error(data?.error || '发送失败')
|
||||
await refreshEmailResetCaptcha()
|
||||
} finally {
|
||||
emailResetLoading.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const username = manualResetForm.username.trim()
|
||||
const newPassword = manualResetForm.new_password
|
||||
if (!username || !newPassword) {
|
||||
ElMessage.error('用户名和新密码不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
const check = validateStrongPassword(newPassword)
|
||||
if (!check.ok) {
|
||||
ElMessage.error(check.message)
|
||||
return
|
||||
}
|
||||
|
||||
manualResetLoading.value = true
|
||||
try {
|
||||
await requestPasswordReset({
|
||||
username,
|
||||
email: manualResetForm.email.trim(),
|
||||
new_password: newPassword,
|
||||
})
|
||||
ElMessage.success('申请已提交,请等待审核')
|
||||
setTimeout(() => {
|
||||
forgotOpen.value = false
|
||||
}, 800)
|
||||
} catch (e) {
|
||||
const data = e?.response?.data
|
||||
ElMessage.error(data?.error || '提交失败')
|
||||
} finally {
|
||||
manualResetLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openResend() {
|
||||
resendOpen.value = true
|
||||
resendForm.email = ''
|
||||
resendForm.captcha = ''
|
||||
await refreshResendCaptcha()
|
||||
}
|
||||
|
||||
async function submitResend() {
|
||||
const email = resendForm.email.trim()
|
||||
if (!email) {
|
||||
ElMessage.error('请输入邮箱')
|
||||
return
|
||||
}
|
||||
if (!resendForm.captcha.trim()) {
|
||||
ElMessage.error('请输入验证码')
|
||||
return
|
||||
}
|
||||
|
||||
resendLoading.value = true
|
||||
try {
|
||||
const res = await resendVerifyEmail({
|
||||
email,
|
||||
captcha_session: resendCaptchaSession.value,
|
||||
captcha: resendForm.captcha.trim(),
|
||||
})
|
||||
ElMessage.success(res?.message || '验证邮件已发送,请查收')
|
||||
setTimeout(() => {
|
||||
resendOpen.value = false
|
||||
}, 800)
|
||||
} catch (e) {
|
||||
const data = e?.response?.data
|
||||
ElMessage.error(data?.error || '发送失败')
|
||||
await refreshResendCaptcha()
|
||||
} finally {
|
||||
resendLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goRegister() {
|
||||
router.push('/register')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const status = await fetchEmailVerifyStatus()
|
||||
emailEnabled.value = Boolean(status?.email_enabled)
|
||||
registerVerifyEnabled.value = Boolean(status?.register_verify_enabled)
|
||||
} catch {
|
||||
emailEnabled.value = false
|
||||
registerVerifyEnabled.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -16,17 +275,127 @@ function goRegister() {
|
||||
<div class="brand-sub app-muted">用户登录</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
title="阶段1:仅完成前台工程与布局搭建。登录/验证码/找回密码等功能将在后续阶段迁移。"
|
||||
show-icon
|
||||
/>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="form.username" placeholder="请输入用户名" autocomplete="username" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入密码"
|
||||
autocomplete="current-password"
|
||||
@keyup.enter="onSubmit"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goRegister">前往注册</el-button>
|
||||
<el-form-item v-if="needCaptcha" label="验证码">
|
||||
<div class="captcha-row">
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" @keyup.enter="onSubmit" />
|
||||
<img
|
||||
v-if="captchaImage"
|
||||
class="captcha-img"
|
||||
:src="captchaImage"
|
||||
alt="验证码"
|
||||
title="点击刷新"
|
||||
@click="refreshLoginCaptcha"
|
||||
/>
|
||||
<el-button @click="refreshLoginCaptcha">刷新</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="links">
|
||||
<el-button text type="primary" @click="openForgot">忘记密码?</el-button>
|
||||
<el-button v-if="showResendLink" text type="primary" @click="openResend">重发验证邮件</el-button>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" class="submit-btn" :loading="loading" @click="onSubmit">登录</el-button>
|
||||
|
||||
<div class="foot">
|
||||
<span class="app-muted">还没有账号?</span>
|
||||
<el-button link type="primary" @click="goRegister">立即注册</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="forgotOpen" title="找回密码" width="min(560px, 92vw)">
|
||||
<template v-if="emailEnabled">
|
||||
<el-alert type="info" :closable="false" title="输入注册邮箱,我们将发送重置链接。" show-icon />
|
||||
<el-form label-position="top" class="dialog-form">
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="emailResetForm.email" placeholder="name@example.com" />
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码">
|
||||
<div class="captcha-row">
|
||||
<el-input v-model="emailResetForm.captcha" placeholder="请输入验证码" />
|
||||
<img
|
||||
v-if="emailResetCaptchaImage"
|
||||
class="captcha-img"
|
||||
:src="emailResetCaptchaImage"
|
||||
alt="验证码"
|
||||
title="点击刷新"
|
||||
@click="refreshEmailResetCaptcha"
|
||||
/>
|
||||
<el-button @click="refreshEmailResetCaptcha">刷新</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-alert type="warning" :closable="false" title="邮件功能未启用:提交申请后等待管理员审核。" show-icon />
|
||||
<el-form label-position="top" class="dialog-form">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="manualResetForm.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱(可选)">
|
||||
<el-input v-model="manualResetForm.email" placeholder="可选填写邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码(至少8位且包含字母和数字)">
|
||||
<el-input v-model="manualResetForm.new_password" type="password" show-password placeholder="请输入新密码" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="forgotOpen = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="emailEnabled ? emailResetLoading : manualResetLoading"
|
||||
@click="submitForgot"
|
||||
>
|
||||
{{ emailEnabled ? '发送重置邮件' : '提交申请' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="resendOpen" title="重发验证邮件" width="min(520px, 92vw)">
|
||||
<el-alert type="info" :closable="false" title="用于注册邮箱验证:请输入邮箱并完成验证码。" show-icon />
|
||||
<el-form label-position="top" class="dialog-form">
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="resendForm.email" placeholder="name@example.com" />
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码">
|
||||
<div class="captcha-row">
|
||||
<el-input v-model="resendForm.captcha" placeholder="请输入验证码" />
|
||||
<img
|
||||
v-if="resendCaptchaImage"
|
||||
class="captcha-img"
|
||||
:src="resendCaptchaImage"
|
||||
alt="验证码"
|
||||
title="点击刷新"
|
||||
@click="refreshResendCaptcha"
|
||||
/>
|
||||
<el-button @click="refreshResendCaptcha">刷新</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="resendOpen = false">取消</el-button>
|
||||
<el-button type="primary" :loading="resendLoading" @click="submitResend">发送</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -61,8 +430,49 @@ function goRegister() {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 16px;
|
||||
.links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin: 2px 0 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.foot {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dialog-form {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.captcha-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.captcha-img {
|
||||
height: 40px;
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.captcha-img {
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,11 +1,140 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import { fetchEmailVerifyStatus, generateCaptcha, register } from '../api/auth'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
email: '',
|
||||
captcha: '',
|
||||
})
|
||||
|
||||
const emailVerifyEnabled = ref(false)
|
||||
const captchaImage = ref('')
|
||||
const captchaSession = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const errorText = ref('')
|
||||
const successTitle = ref('')
|
||||
const successDesc = ref('')
|
||||
|
||||
const emailLabel = computed(() => (emailVerifyEnabled.value ? '邮箱 *' : '邮箱(可选)'))
|
||||
const emailHint = computed(() => (emailVerifyEnabled.value ? '必填,用于账号验证' : '选填,用于接收审核通知'))
|
||||
|
||||
async function refreshCaptcha() {
|
||||
try {
|
||||
const data = await generateCaptcha()
|
||||
captchaSession.value = data?.session_id || ''
|
||||
captchaImage.value = data?.captcha_image || ''
|
||||
form.captcha = ''
|
||||
} catch {
|
||||
captchaSession.value = ''
|
||||
captchaImage.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEmailVerifyStatus() {
|
||||
try {
|
||||
const data = await fetchEmailVerifyStatus()
|
||||
emailVerifyEnabled.value = Boolean(data?.register_verify_enabled)
|
||||
} catch {
|
||||
emailVerifyEnabled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clearAlerts() {
|
||||
errorText.value = ''
|
||||
successTitle.value = ''
|
||||
successDesc.value = ''
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
clearAlerts()
|
||||
|
||||
const username = form.username.trim()
|
||||
const password = form.password
|
||||
const confirmPassword = form.confirm_password
|
||||
const email = form.email.trim()
|
||||
const captcha = form.captcha.trim()
|
||||
|
||||
if (username.length < 3) {
|
||||
errorText.value = '用户名至少3个字符'
|
||||
ElMessage.error(errorText.value)
|
||||
return
|
||||
}
|
||||
if (password.length < 6) {
|
||||
errorText.value = '密码至少6个字符'
|
||||
ElMessage.error(errorText.value)
|
||||
return
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
errorText.value = '两次输入的密码不一致'
|
||||
ElMessage.error(errorText.value)
|
||||
return
|
||||
}
|
||||
if (emailVerifyEnabled.value && !email) {
|
||||
errorText.value = '请填写邮箱地址用于账号验证'
|
||||
ElMessage.error(errorText.value)
|
||||
return
|
||||
}
|
||||
if (email && !email.includes('@')) {
|
||||
errorText.value = '邮箱格式不正确'
|
||||
ElMessage.error(errorText.value)
|
||||
return
|
||||
}
|
||||
if (!captcha) {
|
||||
errorText.value = '请输入验证码'
|
||||
ElMessage.error(errorText.value)
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await register({
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
captcha_session: captchaSession.value,
|
||||
captcha,
|
||||
})
|
||||
|
||||
successTitle.value = res?.message || '注册成功'
|
||||
successDesc.value = res?.need_verify ? '请检查您的邮箱(包括垃圾邮件文件夹)' : ''
|
||||
ElMessage.success('注册成功')
|
||||
|
||||
form.username = ''
|
||||
form.password = ''
|
||||
form.confirm_password = ''
|
||||
form.email = ''
|
||||
form.captcha = ''
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login'
|
||||
}, 3000)
|
||||
} catch (e) {
|
||||
const data = e?.response?.data
|
||||
errorText.value = data?.error || '注册失败'
|
||||
ElMessage.error(errorText.value)
|
||||
await refreshCaptcha()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goLogin() {
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshCaptcha()
|
||||
await loadEmailVerifyStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -16,15 +145,67 @@ function goLogin() {
|
||||
<div class="brand-sub app-muted">用户注册</div>
|
||||
</div>
|
||||
|
||||
<el-alert v-if="errorText" type="error" :closable="false" :title="errorText" show-icon class="alert" />
|
||||
<el-alert
|
||||
type="info"
|
||||
v-if="successTitle"
|
||||
type="success"
|
||||
:closable="false"
|
||||
title="阶段1:仅完成前台工程与布局搭建。注册/邮箱验证等功能将在后续阶段迁移。"
|
||||
:title="successTitle"
|
||||
:description="successDesc"
|
||||
show-icon
|
||||
class="alert"
|
||||
/>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="用户名 *">
|
||||
<el-input v-model="form.username" placeholder="至少3个字符" autocomplete="username" />
|
||||
<div class="hint app-muted">至少3个字符</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码 *">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="至少6个字符"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<div class="hint app-muted">至少6个字符</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码 *">
|
||||
<el-input
|
||||
v-model="form.confirm_password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请再次输入密码"
|
||||
autocomplete="new-password"
|
||||
@keyup.enter="onSubmit"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="emailLabel">
|
||||
<el-input v-model="form.email" placeholder="name@example.com" autocomplete="email" />
|
||||
<div class="hint app-muted">{{ emailHint }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码 *">
|
||||
<div class="captcha-row">
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" @keyup.enter="onSubmit" />
|
||||
<img
|
||||
v-if="captchaImage"
|
||||
class="captcha-img"
|
||||
:src="captchaImage"
|
||||
alt="验证码"
|
||||
title="点击刷新"
|
||||
@click="refreshCaptcha"
|
||||
/>
|
||||
<el-button @click="refreshCaptcha">刷新</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-button type="primary" class="submit-btn" :loading="loading" @click="onSubmit">注册</el-button>
|
||||
|
||||
<div class="actions">
|
||||
<el-button @click="goLogin">返回登录</el-button>
|
||||
<span class="app-muted">已有账号?</span>
|
||||
<el-button link type="primary" @click="goLogin">立即登录</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -61,8 +242,40 @@ function goLogin() {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.captcha-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.captcha-img {
|
||||
height: 40px;
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 16px;
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,11 +1,101 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import { confirmPasswordReset } from '../api/auth'
|
||||
import { validateStrongPassword } from '../utils/password'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const token = ref(String(route.params.token || ''))
|
||||
const valid = ref(true)
|
||||
const invalidMessage = ref('')
|
||||
|
||||
const form = reactive({
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const successText = ref('')
|
||||
|
||||
const redirectSeconds = ref(0)
|
||||
let redirectTimer = null
|
||||
|
||||
function loadInitialState() {
|
||||
if (typeof window === 'undefined') return null
|
||||
const state = window.__APP_INITIAL_STATE__
|
||||
if (!state || typeof state !== 'object') return null
|
||||
window.__APP_INITIAL_STATE__ = null
|
||||
return state
|
||||
}
|
||||
|
||||
const canSubmit = computed(() => Boolean(valid.value && token.value && !successText.value))
|
||||
|
||||
function goLogin() {
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
function startRedirect() {
|
||||
redirectSeconds.value = 3
|
||||
redirectTimer = window.setInterval(() => {
|
||||
redirectSeconds.value -= 1
|
||||
if (redirectSeconds.value <= 0) {
|
||||
window.clearInterval(redirectTimer)
|
||||
redirectTimer = null
|
||||
window.location.href = '/login'
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!canSubmit.value) return
|
||||
|
||||
const newPassword = form.newPassword
|
||||
const confirmPassword = form.confirmPassword
|
||||
|
||||
const check = validateStrongPassword(newPassword)
|
||||
if (!check.ok) {
|
||||
ElMessage.error(check.message)
|
||||
return
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
ElMessage.error('两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await confirmPasswordReset({ token: token.value, new_password: newPassword })
|
||||
successText.value = '密码重置成功!3秒后跳转到登录页面...'
|
||||
ElMessage.success('密码重置成功')
|
||||
startRedirect()
|
||||
} catch (e) {
|
||||
const data = e?.response?.data
|
||||
ElMessage.error(data?.error || '重置失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const init = loadInitialState()
|
||||
if (init?.page === 'reset_password') {
|
||||
token.value = String(init?.token || token.value || '')
|
||||
valid.value = Boolean(init?.valid)
|
||||
invalidMessage.value =
|
||||
init?.error_message || (valid.value ? '' : '重置链接无效或已过期,请重新申请密码重置')
|
||||
} else if (!token.value) {
|
||||
valid.value = false
|
||||
invalidMessage.value = '重置链接无效或已过期,请重新申请密码重置'
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (redirectTimer) window.clearInterval(redirectTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -16,16 +106,55 @@ function goLogin() {
|
||||
<div class="brand-sub app-muted">重置密码</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
title="阶段1:仅完成前台工程与布局搭建。重置密码功能将在后续阶段迁移。"
|
||||
show-icon
|
||||
/>
|
||||
<template v-if="!valid">
|
||||
<el-alert type="error" :closable="false" title="链接已失效" :description="invalidMessage" show-icon />
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="goLogin">返回登录</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="actions">
|
||||
<el-button @click="goLogin">返回登录</el-button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<el-alert
|
||||
v-if="successText"
|
||||
type="success"
|
||||
:closable="false"
|
||||
title="重置成功"
|
||||
:description="successText"
|
||||
show-icon
|
||||
class="alert"
|
||||
/>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="新密码(至少8位且包含字母和数字)">
|
||||
<el-input
|
||||
v-model="form.newPassword"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入新密码"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码">
|
||||
<el-input
|
||||
v-model="form.confirmPassword"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请再次输入新密码"
|
||||
autocomplete="new-password"
|
||||
@keyup.enter="onSubmit"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-button type="primary" class="submit-btn" :loading="loading" :disabled="!canSubmit" @click="onSubmit">
|
||||
确认重置
|
||||
</el-button>
|
||||
|
||||
<div class="actions">
|
||||
<el-button link type="primary" @click="goLogin">返回登录</el-button>
|
||||
<span v-if="redirectSeconds > 0" class="app-muted">{{ redirectSeconds }} 秒后自动跳转…</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -61,8 +190,21 @@ function goLogin() {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
157
app-frontend/src/pages/VerifyResultPage.vue
Normal file
157
app-frontend/src/pages/VerifyResultPage.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const success = ref(false)
|
||||
const title = ref('')
|
||||
const message = ref('')
|
||||
|
||||
const primaryLabel = ref('')
|
||||
const primaryUrl = ref('')
|
||||
const secondaryLabel = ref('')
|
||||
const secondaryUrl = ref('')
|
||||
|
||||
const redirectUrl = ref('')
|
||||
const secondsLeft = ref(0)
|
||||
let countdownTimer = null
|
||||
|
||||
function loadInitialState() {
|
||||
if (typeof window === 'undefined') return null
|
||||
const state = window.__APP_INITIAL_STATE__
|
||||
if (!state || typeof state !== 'object') return null
|
||||
window.__APP_INITIAL_STATE__ = null
|
||||
return state
|
||||
}
|
||||
|
||||
function normalize(state) {
|
||||
const ok = Boolean(state?.success)
|
||||
success.value = ok
|
||||
|
||||
title.value = state?.title || (ok ? '验证成功' : '验证失败')
|
||||
message.value =
|
||||
state?.message || state?.error_message || (ok ? '操作已完成,现在可以继续使用系统。' : '操作失败,请稍后重试。')
|
||||
|
||||
primaryLabel.value = state?.primary_label || (ok ? '立即登录' : '重新注册')
|
||||
primaryUrl.value = state?.primary_url || (ok ? '/login' : '/register')
|
||||
secondaryLabel.value = state?.secondary_label || (ok ? '' : '返回登录')
|
||||
secondaryUrl.value = state?.secondary_url || (ok ? '' : '/login')
|
||||
|
||||
redirectUrl.value = state?.redirect_url || (ok ? '/login' : '')
|
||||
secondsLeft.value = Number(state?.redirect_seconds || (ok ? 5 : 0)) || 0
|
||||
}
|
||||
|
||||
const hasSecondary = computed(() => Boolean(secondaryLabel.value && secondaryUrl.value))
|
||||
const hasCountdown = computed(() => Boolean(redirectUrl.value && secondsLeft.value > 0))
|
||||
|
||||
async function go(url) {
|
||||
if (!url) return
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
window.location.href = url
|
||||
return
|
||||
}
|
||||
await router.push(url)
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
if (!hasCountdown.value) return
|
||||
countdownTimer = window.setInterval(() => {
|
||||
secondsLeft.value -= 1
|
||||
if (secondsLeft.value <= 0) {
|
||||
window.clearInterval(countdownTimer)
|
||||
countdownTimer = null
|
||||
window.location.href = redirectUrl.value
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const state = loadInitialState()
|
||||
normalize(state)
|
||||
startCountdown()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (countdownTimer) window.clearInterval(countdownTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="auth-wrap">
|
||||
<el-card shadow="never" class="auth-card" :body-style="{ padding: '22px' }">
|
||||
<div class="brand">
|
||||
<div class="brand-title">知识管理平台</div>
|
||||
<div class="brand-sub app-muted">验证结果</div>
|
||||
</div>
|
||||
|
||||
<el-result
|
||||
:icon="success ? 'success' : 'error'"
|
||||
:title="title"
|
||||
:sub-title="message"
|
||||
class="result"
|
||||
>
|
||||
<template #extra>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="go(primaryUrl)">{{ primaryLabel }}</el-button>
|
||||
<el-button v-if="hasSecondary" @click="go(secondaryUrl)">{{ secondaryLabel }}</el-button>
|
||||
</div>
|
||||
<div v-if="hasCountdown" class="countdown app-muted">
|
||||
{{ secondsLeft }} 秒后自动跳转...
|
||||
</div>
|
||||
</template>
|
||||
</el-result>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.auth-wrap {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
border-radius: var(--app-radius);
|
||||
border: 1px solid var(--app-border);
|
||||
box-shadow: var(--app-shadow);
|
||||
}
|
||||
|
||||
.brand {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.brand-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.result {
|
||||
padding: 8px 0 2px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,7 @@ import AppLayout from '../layouts/AppLayout.vue'
|
||||
const LoginPage = () => import('../pages/LoginPage.vue')
|
||||
const RegisterPage = () => import('../pages/RegisterPage.vue')
|
||||
const ResetPasswordPage = () => import('../pages/ResetPasswordPage.vue')
|
||||
const VerifyResultPage = () => import('../pages/VerifyResultPage.vue')
|
||||
|
||||
const AccountsPage = () => import('../pages/AccountsPage.vue')
|
||||
const SchedulesPage = () => import('../pages/SchedulesPage.vue')
|
||||
@@ -14,7 +15,9 @@ const routes = [
|
||||
{ path: '/', redirect: '/login' },
|
||||
{ path: '/login', name: 'login', component: LoginPage },
|
||||
{ path: '/register', name: 'register', component: RegisterPage },
|
||||
{ path: '/reset_password', name: 'reset_password', component: ResetPasswordPage },
|
||||
{ path: '/reset-password/:token', name: 'reset_password', component: ResetPasswordPage },
|
||||
{ path: '/api/verify-email/:token', name: 'verify_email', component: VerifyResultPage },
|
||||
{ path: '/api/verify-bind-email/:token', name: 'verify_bind_email', component: VerifyResultPage },
|
||||
{
|
||||
path: '/app',
|
||||
component: AppLayout,
|
||||
@@ -34,4 +37,3 @@ const router = createRouter({
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
|
||||
7
app-frontend/src/utils/password.js
Normal file
7
app-frontend/src/utils/password.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export function validateStrongPassword(value) {
|
||||
const text = String(value || '')
|
||||
if (text.length < 8) return { ok: false, message: '密码长度至少8位' }
|
||||
if (!/[a-zA-Z]/.test(text) || !/\d/.test(text)) return { ok: false, message: '密码必须包含字母和数字' }
|
||||
return { ok: true, message: '' }
|
||||
}
|
||||
|
||||
121
app.py
121
app.py
@@ -1189,13 +1189,13 @@ def index():
|
||||
@app.route('/login')
|
||||
def login_page():
|
||||
"""登录页面"""
|
||||
return render_template('login.html')
|
||||
return render_app_spa_or_legacy('login.html')
|
||||
|
||||
|
||||
@app.route('/register')
|
||||
def register_page():
|
||||
"""注册页面"""
|
||||
return render_template('register.html')
|
||||
return render_app_spa_or_legacy('register.html')
|
||||
|
||||
|
||||
@app.route('/app')
|
||||
@@ -1243,11 +1243,16 @@ def admin_page():
|
||||
return render_template('admin_legacy.html')
|
||||
|
||||
|
||||
def render_app_spa_or_legacy(legacy_template_name: str):
|
||||
def render_app_spa_or_legacy(
|
||||
legacy_template_name: str,
|
||||
legacy_context: dict | None = None,
|
||||
spa_initial_state: dict | None = None,
|
||||
):
|
||||
"""渲染前台 Vue SPA(构建产物位于 static/app),失败则回退旧模板。
|
||||
|
||||
说明:该函数仅负责“资源注入/回退逻辑”,不改变任何接口与业务流程。
|
||||
"""
|
||||
legacy_context = legacy_context or {}
|
||||
manifest_path = os.path.join(app.root_path, 'static', 'app', '.vite', 'manifest.json')
|
||||
try:
|
||||
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||
@@ -1259,19 +1264,20 @@ def render_app_spa_or_legacy(legacy_template_name: str):
|
||||
|
||||
if not js_file:
|
||||
logger.warning(f"[app_spa] manifest缺少入口文件: {manifest_path}")
|
||||
return render_template(legacy_template_name)
|
||||
return render_template(legacy_template_name, **legacy_context)
|
||||
|
||||
return render_template(
|
||||
'app.html',
|
||||
app_spa_js_file=f'app/{js_file}',
|
||||
app_spa_css_files=[f'app/{p}' for p in css_files],
|
||||
app_spa_initial_state=spa_initial_state,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
logger.info(f"[app_spa] 未找到manifest: {manifest_path},回退旧模板: {legacy_template_name}")
|
||||
return render_template(legacy_template_name)
|
||||
return render_template(legacy_template_name, **legacy_context)
|
||||
except Exception as e:
|
||||
logger.error(f"[app_spa] 加载manifest失败: {e}")
|
||||
return render_template(legacy_template_name)
|
||||
return render_template(legacy_template_name, **legacy_context)
|
||||
# ==================== 用户认证API ====================
|
||||
|
||||
@app.route('/api/register', methods=['POST'])
|
||||
@@ -1387,10 +1393,35 @@ def verify_email(token):
|
||||
database.set_user_vip(user_id, auto_approve_vip_days)
|
||||
|
||||
logger.info(f"用户邮箱验<EFBFBD><EFBFBD><EFBFBD>成功: user_id={user_id}, email={email}")
|
||||
return render_template('verify_success.html')
|
||||
spa_initial_state = {
|
||||
'page': 'verify_result',
|
||||
'success': True,
|
||||
'title': '验证成功',
|
||||
'message': '您的邮箱已验证成功!账号已激活,现在可以登录使用了。',
|
||||
'primary_label': '立即登录',
|
||||
'primary_url': '/login',
|
||||
'redirect_url': '/login',
|
||||
'redirect_seconds': 5,
|
||||
}
|
||||
return render_app_spa_or_legacy('verify_success.html', spa_initial_state=spa_initial_state)
|
||||
else:
|
||||
logger.warning(f"邮箱验证失败: token={token[:20]}...")
|
||||
return render_template('verify_failed.html', error_message="验证链接无效或已过期,请重新注册或申请重发验证邮件")
|
||||
error_message = "验证链接无效或已过期,请重新注册或申请重发验证邮件"
|
||||
spa_initial_state = {
|
||||
'page': 'verify_result',
|
||||
'success': False,
|
||||
'title': '验证失败',
|
||||
'error_message': error_message,
|
||||
'primary_label': '重新注册',
|
||||
'primary_url': '/register',
|
||||
'secondary_label': '返回登录',
|
||||
'secondary_url': '/login',
|
||||
}
|
||||
return render_app_spa_or_legacy(
|
||||
'verify_failed.html',
|
||||
legacy_context={'error_message': error_message},
|
||||
spa_initial_state=spa_initial_state,
|
||||
)
|
||||
|
||||
|
||||
@app.route('/api/resend-verify-email', methods=['POST'])
|
||||
@@ -1517,11 +1548,26 @@ def forgot_password():
|
||||
def reset_password_page(token):
|
||||
"""密码重置页面"""
|
||||
result = email_service.verify_password_reset_token(token)
|
||||
if result:
|
||||
return render_template('reset_password.html', token=token, valid=True, error_message='')
|
||||
else:
|
||||
return render_template('reset_password.html', token=token, valid=False,
|
||||
error_message='重置链接无效或已过期,请重新申请密码重置')
|
||||
valid = bool(result)
|
||||
error_message = '' if valid else '重置链接无效或已过期,请重新申请密码重置'
|
||||
|
||||
legacy_context = {
|
||||
'token': token,
|
||||
'valid': valid,
|
||||
'error_message': error_message,
|
||||
}
|
||||
spa_initial_state = {
|
||||
'page': 'reset_password',
|
||||
'token': token,
|
||||
'valid': valid,
|
||||
'error_message': error_message,
|
||||
}
|
||||
|
||||
return render_app_spa_or_legacy(
|
||||
'reset_password.html',
|
||||
legacy_context=legacy_context,
|
||||
spa_initial_state=spa_initial_state,
|
||||
)
|
||||
|
||||
|
||||
@app.route('/api/reset-password-confirm', methods=['POST'])
|
||||
@@ -3697,21 +3743,46 @@ def verify_bind_email(token):
|
||||
|
||||
# 更新用户邮箱
|
||||
if database.update_user_email(user_id, email, verified=True):
|
||||
# 返回成功页面
|
||||
return render_template('verify_success.html',
|
||||
title='邮箱绑定成功',
|
||||
message=f'邮箱 {email} 已成功绑定到您的账号!',
|
||||
redirect_url='/'
|
||||
)
|
||||
spa_initial_state = {
|
||||
'page': 'verify_result',
|
||||
'success': True,
|
||||
'title': '邮箱绑定成功',
|
||||
'message': f'邮箱 {email} 已成功绑定到您的账号!',
|
||||
'primary_label': '返回登录',
|
||||
'primary_url': '/login',
|
||||
'redirect_url': '/login',
|
||||
'redirect_seconds': 5,
|
||||
}
|
||||
return render_app_spa_or_legacy('verify_success.html', spa_initial_state=spa_initial_state)
|
||||
else:
|
||||
return render_template('verify_failed.html',
|
||||
title='绑定失败',
|
||||
message='邮箱绑定失败,请重试'
|
||||
error_message = '邮箱绑定失败,请重试'
|
||||
spa_initial_state = {
|
||||
'page': 'verify_result',
|
||||
'success': False,
|
||||
'title': '绑定失败',
|
||||
'error_message': error_message,
|
||||
'primary_label': '返回登录',
|
||||
'primary_url': '/login',
|
||||
}
|
||||
return render_app_spa_or_legacy(
|
||||
'verify_failed.html',
|
||||
legacy_context={'error_message': error_message},
|
||||
spa_initial_state=spa_initial_state,
|
||||
)
|
||||
else:
|
||||
return render_template('verify_failed.html',
|
||||
title='链接无效',
|
||||
message='验证链接已过期或无效,请重新发送验证邮件'
|
||||
error_message = '验证链接已过期或无效,请重新发送验证邮件'
|
||||
spa_initial_state = {
|
||||
'page': 'verify_result',
|
||||
'success': False,
|
||||
'title': '链接无效',
|
||||
'error_message': error_message,
|
||||
'primary_label': '返回登录',
|
||||
'primary_url': '/login',
|
||||
}
|
||||
return render_app_spa_or_legacy(
|
||||
'verify_failed.html',
|
||||
legacy_context={'error_message': error_message},
|
||||
spa_initial_state=spa_initial_state,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
{
|
||||
"_auth-PlCOj1Xe.js": {
|
||||
"file": "assets/auth-PlCOj1Xe.js",
|
||||
"name": "auth"
|
||||
},
|
||||
"_password-7ryi82gE.js": {
|
||||
"file": "assets/password-7ryi82gE.js",
|
||||
"name": "password"
|
||||
},
|
||||
"index.html": {
|
||||
"file": "assets/index-BDLnyqR1.js",
|
||||
"file": "assets/index-fYGyZipT.js",
|
||||
"name": "index",
|
||||
"src": "index.html",
|
||||
"isEntry": true,
|
||||
@@ -8,6 +16,7 @@
|
||||
"src/pages/LoginPage.vue",
|
||||
"src/pages/RegisterPage.vue",
|
||||
"src/pages/ResetPasswordPage.vue",
|
||||
"src/pages/VerifyResultPage.vue",
|
||||
"src/pages/AccountsPage.vue",
|
||||
"src/pages/SchedulesPage.vue",
|
||||
"src/pages/ScreenshotsPage.vue"
|
||||
@@ -17,7 +26,7 @@
|
||||
]
|
||||
},
|
||||
"src/pages/AccountsPage.vue": {
|
||||
"file": "assets/AccountsPage-CDp_6M3v.js",
|
||||
"file": "assets/AccountsPage-DFK9bKik.js",
|
||||
"name": "AccountsPage",
|
||||
"src": "src/pages/AccountsPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
@@ -29,43 +38,48 @@
|
||||
]
|
||||
},
|
||||
"src/pages/LoginPage.vue": {
|
||||
"file": "assets/LoginPage-CUFPnwuZ.js",
|
||||
"file": "assets/LoginPage-1KWN57o2.js",
|
||||
"name": "LoginPage",
|
||||
"src": "src/pages/LoginPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"index.html"
|
||||
"index.html",
|
||||
"_auth-PlCOj1Xe.js",
|
||||
"_password-7ryi82gE.js"
|
||||
],
|
||||
"css": [
|
||||
"assets/LoginPage-B-WqAKk4.css"
|
||||
"assets/LoginPage-8DI6Rf67.css"
|
||||
]
|
||||
},
|
||||
"src/pages/RegisterPage.vue": {
|
||||
"file": "assets/RegisterPage-BYIu9Dvh.js",
|
||||
"file": "assets/RegisterPage-CJqvAJkb.js",
|
||||
"name": "RegisterPage",
|
||||
"src": "src/pages/RegisterPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"index.html"
|
||||
"index.html",
|
||||
"_auth-PlCOj1Xe.js"
|
||||
],
|
||||
"css": [
|
||||
"assets/RegisterPage-CPyuLOs6.css"
|
||||
"assets/RegisterPage-CVjBOq6i.css"
|
||||
]
|
||||
},
|
||||
"src/pages/ResetPasswordPage.vue": {
|
||||
"file": "assets/ResetPasswordPage-Ditr0QEq.js",
|
||||
"file": "assets/ResetPasswordPage-zVP1Rm4S.js",
|
||||
"name": "ResetPasswordPage",
|
||||
"src": "src/pages/ResetPasswordPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"index.html"
|
||||
"index.html",
|
||||
"_auth-PlCOj1Xe.js",
|
||||
"_password-7ryi82gE.js"
|
||||
],
|
||||
"css": [
|
||||
"assets/ResetPasswordPage-CErwB9tI.css"
|
||||
"assets/ResetPasswordPage-DybfLMAw.css"
|
||||
]
|
||||
},
|
||||
"src/pages/SchedulesPage.vue": {
|
||||
"file": "assets/SchedulesPage-B7bCm2b3.js",
|
||||
"file": "assets/SchedulesPage-D4P6kLJv.js",
|
||||
"name": "SchedulesPage",
|
||||
"src": "src/pages/SchedulesPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
@@ -77,7 +91,7 @@
|
||||
]
|
||||
},
|
||||
"src/pages/ScreenshotsPage.vue": {
|
||||
"file": "assets/ScreenshotsPage-DPphuiaz.js",
|
||||
"file": "assets/ScreenshotsPage-By1nYVxK.js",
|
||||
"name": "ScreenshotsPage",
|
||||
"src": "src/pages/ScreenshotsPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
@@ -87,5 +101,17 @@
|
||||
"css": [
|
||||
"assets/ScreenshotsPage-CmPGicmh.css"
|
||||
]
|
||||
},
|
||||
"src/pages/VerifyResultPage.vue": {
|
||||
"file": "assets/VerifyResultPage-Buu0rko2.js",
|
||||
"name": "VerifyResultPage",
|
||||
"src": "src/pages/VerifyResultPage.vue",
|
||||
"isDynamicEntry": true,
|
||||
"imports": [
|
||||
"index.html"
|
||||
],
|
||||
"css": [
|
||||
"assets/VerifyResultPage-CG6ZYNrm.css"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as t,e as o,w as c,r,o as d,b as s}from"./index-BDLnyqR1.js";const n={};function _(l,e){const a=r("el-card");return d(),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-f8df5656"]]);export{f as default};
|
||||
1
static/app/assets/AccountsPage-DFK9bKik.js
Normal file
1
static/app/assets/AccountsPage-DFK9bKik.js
Normal file
@@ -0,0 +1 @@
|
||||
import{_ as t,h as o,w as c,e as d,f as n,g as s}from"./index-fYGyZipT.js";const r={};function _(l,e){const a=d("el-card");return n(),o(a,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:c(()=>[...e[0]||(e[0]=[s("h2",{class:"title"},"账号管理",-1),s("div",{class:"app-muted"},"阶段1:页面壳子已就绪,功能将在后续阶段迁移。",-1)])]),_:1})}const f=t(r,[["render",_],["__scopeId","data-v-f8df5656"]]);export{f as default};
|
||||
1
static/app/assets/LoginPage-1KWN57o2.js
Normal file
1
static/app/assets/LoginPage-1KWN57o2.js
Normal file
File diff suppressed because one or more lines are too long
1
static/app/assets/LoginPage-8DI6Rf67.css
Normal file
1
static/app/assets/LoginPage-8DI6Rf67.css
Normal file
@@ -0,0 +1 @@
|
||||
.auth-wrap[data-v-50df591d]{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}.auth-card[data-v-50df591d]{width:100%;max-width:420px;border-radius:var(--app-radius);border:1px solid var(--app-border);box-shadow:var(--app-shadow)}.brand[data-v-50df591d]{margin-bottom:14px}.brand-title[data-v-50df591d]{font-size:18px;font-weight:900}.brand-sub[data-v-50df591d]{margin-top:4px;font-size:12px}.links[data-v-50df591d]{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:2px 0 10px;flex-wrap:wrap}.submit-btn[data-v-50df591d]{width:100%}.foot[data-v-50df591d]{margin-top:14px;display:flex;align-items:center;justify-content:center;gap:6px}.dialog-form[data-v-50df591d]{margin-top:10px}.captcha-row[data-v-50df591d]{display:flex;align-items:center;gap:10px;width:100%}.captcha-img[data-v-50df591d]{height:40px;border:1px solid var(--app-border);border-radius:8px;cursor:pointer;-webkit-user-select:none;user-select:none}@media(max-width:480px){.captcha-img[data-v-50df591d]{height:38px}}
|
||||
@@ -1 +0,0 @@
|
||||
.auth-wrap[data-v-b02cd436]{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}.auth-card[data-v-b02cd436]{width:100%;max-width:420px;border-radius:var(--app-radius);border:1px solid var(--app-border);box-shadow:var(--app-shadow)}.brand[data-v-b02cd436]{margin-bottom:14px}.brand-title[data-v-b02cd436]{font-size:18px;font-weight:900}.brand-sub[data-v-b02cd436]{margin-top:4px;font-size:12px}.actions[data-v-b02cd436]{margin-top:16px}
|
||||
@@ -1 +0,0 @@
|
||||
import{_,c as i,a as s,w as a,r as o,u as p,o as u,b as t,d as m}from"./index-BDLnyqR1.js";const b={class:"auth-wrap"},f={class:"actions"},g={__name:"LoginPage",setup(v){const n=p();function r(){n.push("/register")}return(x,e)=>{const c=o("el-alert"),d=o("el-button"),l=o("el-card");return u(),i("div",b,[s(l,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:a(()=>[e[1]||(e[1]=t("div",{class:"brand"},[t("div",{class:"brand-title"},"知识管理平台"),t("div",{class:"brand-sub app-muted"},"用户登录")],-1)),s(c,{type:"info",closable:!1,title:"阶段1:仅完成前台工程与布局搭建。登录/验证码/找回密码等功能将在后续阶段迁移。","show-icon":""}),t("div",f,[s(d,{type:"primary",onClick:r},{default:a(()=>[...e[0]||(e[0]=[m("前往注册",-1)])]),_:1})])]),_:1})])}}},w=_(g,[["__scopeId","data-v-b02cd436"]]);export{w as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_,c as i,a as s,w as a,r as o,u as p,o as u,b as t,d as m}from"./index-BDLnyqR1.js";const b={class:"auth-wrap"},f={class:"actions"},g={__name:"RegisterPage",setup(v){const n=p();function c(){n.push("/login")}return(x,e)=>{const r=o("el-alert"),l=o("el-button"),d=o("el-card");return u(),i("div",b,[s(d,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:a(()=>[e[1]||(e[1]=t("div",{class:"brand"},[t("div",{class:"brand-title"},"知识管理平台"),t("div",{class:"brand-sub app-muted"},"用户注册")],-1)),s(r,{type:"info",closable:!1,title:"阶段1:仅完成前台工程与布局搭建。注册/邮箱验证等功能将在后续阶段迁移。","show-icon":""}),t("div",f,[s(l,{onClick:c},{default:a(()=>[...e[0]||(e[0]=[m("返回登录",-1)])]),_:1})])]),_:1})])}}},w=_(g,[["__scopeId","data-v-6a731624"]]);export{w as default};
|
||||
1
static/app/assets/RegisterPage-CJqvAJkb.js
Normal file
1
static/app/assets/RegisterPage-CJqvAJkb.js
Normal file
@@ -0,0 +1 @@
|
||||
import{_ as M,r as j,a as p,c as B,o as A,b as S,d as t,w as o,e as m,u as H,f as g,g as n,h as U,i as x,j as N,t as q,k as E,E as d}from"./index-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};
|
||||
@@ -1 +0,0 @@
|
||||
.auth-wrap[data-v-6a731624]{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}.auth-card[data-v-6a731624]{width:100%;max-width:420px;border-radius:var(--app-radius);border:1px solid var(--app-border);box-shadow:var(--app-shadow)}.brand[data-v-6a731624]{margin-bottom:14px}.brand-title[data-v-6a731624]{font-size:18px;font-weight:900}.brand-sub[data-v-6a731624]{margin-top:4px;font-size:12px}.actions[data-v-6a731624]{margin-top:16px}
|
||||
1
static/app/assets/RegisterPage-CVjBOq6i.css
Normal file
1
static/app/assets/RegisterPage-CVjBOq6i.css
Normal file
@@ -0,0 +1 @@
|
||||
.auth-wrap[data-v-32684b4d]{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}.auth-card[data-v-32684b4d]{width:100%;max-width:420px;border-radius:var(--app-radius);border:1px solid var(--app-border);box-shadow:var(--app-shadow)}.brand[data-v-32684b4d]{margin-bottom:14px}.brand-title[data-v-32684b4d]{font-size:18px;font-weight:900}.brand-sub[data-v-32684b4d]{margin-top:4px;font-size:12px}.alert[data-v-32684b4d]{margin-bottom:12px}.hint[data-v-32684b4d]{margin-top:6px;font-size:12px}.captcha-row[data-v-32684b4d]{display:flex;align-items:center;gap:10px;width:100%}.captcha-img[data-v-32684b4d]{height:40px;border:1px solid var(--app-border);border-radius:8px;cursor:pointer;-webkit-user-select:none;user-select:none}.submit-btn[data-v-32684b4d]{width:100%;margin-top:4px}.actions[data-v-32684b4d]{margin-top:14px;display:flex;align-items:center;justify-content:center;gap:6px}
|
||||
@@ -1 +0,0 @@
|
||||
.auth-wrap[data-v-8f60ffad]{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}.auth-card[data-v-8f60ffad]{width:100%;max-width:420px;border-radius:var(--app-radius);border:1px solid var(--app-border);box-shadow:var(--app-shadow)}.brand[data-v-8f60ffad]{margin-bottom:14px}.brand-title[data-v-8f60ffad]{font-size:18px;font-weight:900}.brand-sub[data-v-8f60ffad]{margin-top:4px;font-size:12px}.actions[data-v-8f60ffad]{margin-top:16px}
|
||||
@@ -1 +0,0 @@
|
||||
import{_,c as i,a as t,w as a,r as o,u as p,o as u,b as s,d as f}from"./index-BDLnyqR1.js";const m={class:"auth-wrap"},b={class:"actions"},v={__name:"ResetPasswordPage",setup(w){const n=p();function c(){n.push("/login")}return(g,e)=>{const d=o("el-alert"),r=o("el-button"),l=o("el-card");return u(),i("div",m,[t(l,{shadow:"never",class:"auth-card","body-style":{padding:"22px"}},{default:a(()=>[e[1]||(e[1]=s("div",{class:"brand"},[s("div",{class:"brand-title"},"知识管理平台"),s("div",{class:"brand-sub app-muted"},"重置密码")],-1)),t(d,{type:"info",closable:!1,title:"阶段1:仅完成前台工程与布局搭建。重置密码功能将在后续阶段迁移。","show-icon":""}),s("div",b,[t(r,{onClick:c},{default:a(()=>[...e[0]||(e[0]=[f("返回登录",-1)])]),_:1})])]),_:1})])}}},h=_(v,[["__scopeId","data-v-8f60ffad"]]);export{h as default};
|
||||
1
static/app/assets/ResetPasswordPage-DybfLMAw.css
Normal file
1
static/app/assets/ResetPasswordPage-DybfLMAw.css
Normal file
@@ -0,0 +1 @@
|
||||
.auth-wrap[data-v-0bbb511c]{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}.auth-card[data-v-0bbb511c]{width:100%;max-width:420px;border-radius:var(--app-radius);border:1px solid var(--app-border);box-shadow:var(--app-shadow)}.brand[data-v-0bbb511c]{margin-bottom:14px}.brand-title[data-v-0bbb511c]{font-size:18px;font-weight:900}.brand-sub[data-v-0bbb511c]{margin-top:4px;font-size:12px}.alert[data-v-0bbb511c]{margin-bottom:12px}.submit-btn[data-v-0bbb511c]{width:100%;margin-top:4px}.actions[data-v-0bbb511c]{margin-top:16px;display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap}
|
||||
1
static/app/assets/ResetPasswordPage-zVP1Rm4S.js
Normal file
1
static/app/assets/ResetPasswordPage-zVP1Rm4S.js
Normal file
@@ -0,0 +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};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as t,e as o,w as c,r,o as d,b as s}from"./index-BDLnyqR1.js";const n={};function l(_,e){const a=r("el-card");return d(),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
static/app/assets/SchedulesPage-D4P6kLJv.js
Normal file
1
static/app/assets/SchedulesPage-D4P6kLJv.js
Normal file
@@ -0,0 +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};
|
||||
1
static/app/assets/ScreenshotsPage-By1nYVxK.js
Normal file
1
static/app/assets/ScreenshotsPage-By1nYVxK.js
Normal file
@@ -0,0 +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};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as t,e as o,w as c,r,o as d,b as s}from"./index-BDLnyqR1.js";const n={};function _(l,e){const a=r("el-card");return d(),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
static/app/assets/VerifyResultPage-Buu0rko2.js
Normal file
1
static/app/assets/VerifyResultPage-Buu0rko2.js
Normal file
@@ -0,0 +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};
|
||||
1
static/app/assets/VerifyResultPage-CG6ZYNrm.css
Normal file
1
static/app/assets/VerifyResultPage-CG6ZYNrm.css
Normal file
@@ -0,0 +1 @@
|
||||
.auth-wrap[data-v-1fc6b081]{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}.auth-card[data-v-1fc6b081]{width:100%;max-width:520px;border-radius:var(--app-radius);border:1px solid var(--app-border);box-shadow:var(--app-shadow)}.brand[data-v-1fc6b081]{margin-bottom:14px}.brand-title[data-v-1fc6b081]{font-size:18px;font-weight:900}.brand-sub[data-v-1fc6b081]{margin-top:4px;font-size:12px}.result[data-v-1fc6b081]{padding:8px 0 2px}.actions[data-v-1fc6b081]{display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap}.countdown[data-v-1fc6b081]{margin-top:10px;text-align:center;font-size:13px}
|
||||
6
static/app/assets/auth-PlCOj1Xe.js
Normal file
6
static/app/assets/auth-PlCOj1Xe.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
static/app/assets/password-7ryi82gE.js
Normal file
1
static/app/assets/password-7ryi82gE.js
Normal file
@@ -0,0 +1 @@
|
||||
function s(t){const e=String(t||"");return e.length<8?{ok:!1,message:"密码长度至少8位"}:!/[a-zA-Z]/.test(e)||!/\d/.test(e)?{ok:!1,message:"密码必须包含字母和数字"}:{ok:!0,message:""}}export{s as v};
|
||||
@@ -4,7 +4,7 @@
|
||||
<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-BDLnyqR1.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-fYGyZipT.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CZCRHVLY.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
<body>
|
||||
<noscript>该页面需要启用 JavaScript 才能使用。</noscript>
|
||||
<div id="app"></div>
|
||||
{% if app_spa_initial_state is defined and app_spa_initial_state %}
|
||||
<script>
|
||||
window.__APP_INITIAL_STATE__ = {{ app_spa_initial_state | tojson }};
|
||||
</script>
|
||||
{% endif %}
|
||||
<script type="module" src="{{ url_for('serve_static', filename=app_spa_js_file) }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user