1. 风险分衰减定时任务: - services/scheduler.py: 每天 CST 04:00 自动执行 decay_scores() - 支持 RISK_SCORE_DECAY_TIME_CST 环境变量覆盖 2. 密码长度提示统一为8位: - app-frontend/src/pages/RegisterPage.vue - app-frontend/src/layouts/AppLayout.vue - admin-frontend/src/pages/SettingsPage.vue - templates/register.html 3. 浏览器池统计API: - GET /yuyx/api/browser_pool/stats - 返回 worker 状态、队列等待数等信息 - browser_pool_worker.py: 增强 get_stats() 方法 4. 登录后支持 next 参数回跳: - app-frontend/src/pages/LoginPage.vue: 检查 ?next= 参数 - 仅允许站内路径(防止开放重定向) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
284 lines
6.9 KiB
Vue
284 lines
6.9 KiB
Vue
<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'
|
||
import { validateStrongPassword } from '../utils/password'
|
||
|
||
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
|
||
}
|
||
const passwordCheck = validateStrongPassword(password)
|
||
if (!passwordCheck.ok) {
|
||
errorText.value = passwordCheck.message || '密码格式不正确'
|
||
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>
|
||
<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-alert v-if="errorText" type="error" :closable="false" :title="errorText" show-icon class="alert" />
|
||
<el-alert
|
||
v-if="successTitle"
|
||
type="success"
|
||
:closable="false"
|
||
: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="至少8位且包含字母和数字"
|
||
autocomplete="new-password"
|
||
/>
|
||
<div class="hint app-muted">至少8位且包含字母和数字</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">
|
||
<span class="app-muted">已有账号?</span>
|
||
<el-button link type="primary" @click="goLogin">立即登录</el-button>
|
||
</div>
|
||
</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: 420px;
|
||
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;
|
||
}
|
||
|
||
.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: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
}
|
||
</style>
|