Files
zsglpt/app-frontend/src/pages/RegisterPage.vue
yuyx 1b20478a08 feat: 风险分定时衰减 + 密码提示修复 + 浏览器池API + next回跳
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>
2025-12-27 18:28:21 +08:00

284 lines
6.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>