515 lines
12 KiB
Vue
515 lines
12 KiB
Vue
<template>
|
||
<div class="register">
|
||
<el-row justify="center" align="middle" class="register-container">
|
||
<el-col :xs="22" :sm="16" :md="12" :lg="8" :xl="6">
|
||
<el-card class="register-card">
|
||
<template #header>
|
||
<div class="register-header">
|
||
<el-icon size="32" color="#67C23A"><User /></el-icon>
|
||
<h2>用户注册</h2>
|
||
</div>
|
||
</template>
|
||
|
||
<el-form
|
||
ref="registerFormRef"
|
||
:model="registerForm"
|
||
:rules="registerRules"
|
||
label-width="80px"
|
||
@submit.prevent="handleRegister"
|
||
>
|
||
<el-form-item label="用户名" prop="username">
|
||
<el-input
|
||
v-model="registerForm.username"
|
||
placeholder="请输入用户名"
|
||
prefix-icon="User"
|
||
clearable
|
||
@blur="checkUsername"
|
||
/>
|
||
<div v-if="usernameChecking" class="checking-text">
|
||
<el-icon class="is-loading"><Loading /></el-icon>
|
||
检查中...
|
||
</div>
|
||
<div v-if="usernameExists" class="error-text">
|
||
<el-icon><CircleCloseFilled /></el-icon>
|
||
用户名已存在
|
||
</div>
|
||
<div v-if="usernameAvailable" class="success-text">
|
||
<el-icon><CircleCheckFilled /></el-icon>
|
||
用户名可用
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="邮箱" prop="email">
|
||
<el-input
|
||
v-model="registerForm.email"
|
||
placeholder="请输入邮箱"
|
||
prefix-icon="Message"
|
||
clearable
|
||
@blur="checkEmail"
|
||
/>
|
||
<div v-if="emailChecking" class="checking-text">
|
||
<el-icon class="is-loading"><Loading /></el-icon>
|
||
检查中...
|
||
</div>
|
||
<div v-if="emailExists" class="error-text">
|
||
<el-icon><CircleCloseFilled /></el-icon>
|
||
邮箱已存在
|
||
</div>
|
||
<div v-if="emailAvailable" class="success-text">
|
||
<el-icon><CircleCheckFilled /></el-icon>
|
||
邮箱可用
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="密码" prop="password">
|
||
<el-input
|
||
v-model="registerForm.password"
|
||
type="password"
|
||
placeholder="请输入密码"
|
||
prefix-icon="Lock"
|
||
show-password
|
||
clearable
|
||
@input="checkPasswordStrength"
|
||
/>
|
||
<div v-if="passwordStrength" class="password-strength">
|
||
<div class="strength-bar">
|
||
<div
|
||
class="strength-fill"
|
||
:class="strengthClass"
|
||
:style="{ width: strengthWidth }"
|
||
></div>
|
||
</div>
|
||
<span class="strength-text">{{ strengthText }}</span>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="确认密码" prop="confirmPassword">
|
||
<el-input
|
||
v-model="registerForm.confirmPassword"
|
||
type="password"
|
||
placeholder="请再次输入密码"
|
||
prefix-icon="Lock"
|
||
show-password
|
||
clearable
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item>
|
||
<el-checkbox v-model="agreeTerms">
|
||
我已阅读并同意
|
||
<a href="#" class="terms-link">《用户协议》</a>
|
||
和
|
||
<a href="#" class="terms-link">《隐私政策》</a>
|
||
</el-checkbox>
|
||
</el-form-item>
|
||
|
||
<el-form-item>
|
||
<el-button
|
||
type="success"
|
||
size="large"
|
||
:loading="userStore.loading"
|
||
:disabled="!canRegister"
|
||
@click="handleRegister"
|
||
class="register-button"
|
||
>
|
||
{{ userStore.loading ? '注册中...' : '注册' }}
|
||
</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<div class="register-footer">
|
||
<p>已有账号?<router-link to="/login" class="login-link">立即登录</router-link></p>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { useUserStore } from '@/stores/user'
|
||
import { checkUsernameExists, checkEmailExists } from '@/api/auth'
|
||
import { ElMessage } from 'element-plus'
|
||
import {
|
||
User,
|
||
Lock,
|
||
Message,
|
||
Phone,
|
||
Calendar,
|
||
Location,
|
||
Check,
|
||
Close,
|
||
ArrowLeft,
|
||
ArrowRight
|
||
} from '@element-plus/icons-vue'
|
||
|
||
const router = useRouter()
|
||
const userStore = useUserStore()
|
||
|
||
const registerFormRef = ref()
|
||
const agreeTerms = ref(false)
|
||
|
||
// 用户名检查状态
|
||
const usernameChecking = ref(false)
|
||
const usernameExists = ref(false)
|
||
const usernameAvailable = ref(false)
|
||
|
||
// 邮箱检查状态
|
||
const emailChecking = ref(false)
|
||
const emailExists = ref(false)
|
||
const emailAvailable = ref(false)
|
||
|
||
// 密码强度
|
||
const passwordStrength = ref(false)
|
||
const strengthLevel = ref(0)
|
||
|
||
const registerForm = reactive({
|
||
username: '',
|
||
email: '',
|
||
password: '',
|
||
confirmPassword: ''
|
||
})
|
||
|
||
const registerRules = {
|
||
username: [
|
||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' },
|
||
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线', trigger: 'blur' }
|
||
],
|
||
email: [
|
||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||
],
|
||
password: [
|
||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
||
],
|
||
confirmPassword: [
|
||
{ required: true, message: '请确认密码', trigger: 'blur' },
|
||
{
|
||
validator: (rule, value, callback) => {
|
||
if (value !== registerForm.password) {
|
||
callback(new Error('两次输入密码不一致'))
|
||
} else {
|
||
callback()
|
||
}
|
||
},
|
||
trigger: 'blur'
|
||
}
|
||
]
|
||
}
|
||
|
||
// 检查用户名是否存在
|
||
const checkUsername = async () => {
|
||
if (!registerForm.username || registerForm.username.length < 3) {
|
||
resetUsernameCheck()
|
||
return
|
||
}
|
||
|
||
try {
|
||
usernameChecking.value = true
|
||
usernameExists.value = false
|
||
usernameAvailable.value = false
|
||
|
||
const response = await checkUsernameExists(registerForm.username)
|
||
|
||
if (response.success) {
|
||
usernameExists.value = response.data.exists
|
||
usernameAvailable.value = !response.data.exists
|
||
}
|
||
} catch (error) {
|
||
console.error('Check username error:', error)
|
||
} finally {
|
||
usernameChecking.value = false
|
||
}
|
||
}
|
||
|
||
// 检查邮箱是否存在
|
||
const checkEmail = async () => {
|
||
if (!registerForm.email || !isValidEmail(registerForm.email)) {
|
||
resetEmailCheck()
|
||
return
|
||
}
|
||
|
||
try {
|
||
emailChecking.value = true
|
||
emailExists.value = false
|
||
emailAvailable.value = false
|
||
|
||
const response = await checkEmailExists(registerForm.email)
|
||
|
||
if (response.success) {
|
||
emailExists.value = response.data.exists
|
||
emailAvailable.value = !response.data.exists
|
||
}
|
||
} catch (error) {
|
||
console.error('Check email error:', error)
|
||
} finally {
|
||
emailChecking.value = false
|
||
}
|
||
}
|
||
|
||
// 检查密码强度
|
||
const checkPasswordStrength = () => {
|
||
const password = registerForm.password
|
||
if (!password) {
|
||
passwordStrength.value = false
|
||
return
|
||
}
|
||
|
||
passwordStrength.value = true
|
||
|
||
let score = 0
|
||
if (password.length >= 6) score++
|
||
if (password.length >= 8) score++
|
||
if (/[a-z]/.test(password)) score++
|
||
if (/[A-Z]/.test(password)) score++
|
||
if (/[0-9]/.test(password)) score++
|
||
if (/[^A-Za-z0-9]/.test(password)) score++
|
||
|
||
strengthLevel.value = Math.min(score, 4)
|
||
}
|
||
|
||
// 重置用户名检查状态
|
||
const resetUsernameCheck = () => {
|
||
usernameChecking.value = false
|
||
usernameExists.value = false
|
||
usernameAvailable.value = false
|
||
}
|
||
|
||
// 重置邮箱检查状态
|
||
const resetEmailCheck = () => {
|
||
emailChecking.value = false
|
||
emailExists.value = false
|
||
emailAvailable.value = false
|
||
}
|
||
|
||
// 验证邮箱格式
|
||
const isValidEmail = (email) => {
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||
return emailRegex.test(email)
|
||
}
|
||
|
||
// 计算属性
|
||
const strengthClass = computed(() => {
|
||
const classes = ['weak', 'fair', 'good', 'strong']
|
||
return classes[strengthLevel.value - 1] || 'weak'
|
||
})
|
||
|
||
const strengthWidth = computed(() => {
|
||
return `${(strengthLevel.value / 4) * 100}%`
|
||
})
|
||
|
||
const strengthText = computed(() => {
|
||
const texts = ['弱', '一般', '良好', '强']
|
||
return texts[strengthLevel.value - 1] || '弱'
|
||
})
|
||
|
||
const canRegister = computed(() => {
|
||
return agreeTerms.value &&
|
||
usernameAvailable.value &&
|
||
emailAvailable.value &&
|
||
registerForm.password &&
|
||
registerForm.confirmPassword &&
|
||
registerForm.password === registerForm.confirmPassword
|
||
})
|
||
|
||
const handleRegister = async () => {
|
||
if (!registerFormRef.value) return
|
||
|
||
try {
|
||
const valid = await registerFormRef.value.validate()
|
||
if (!valid) return
|
||
|
||
if (!agreeTerms.value) {
|
||
ElMessage.warning('请先同意用户协议和隐私政策')
|
||
return
|
||
}
|
||
|
||
const result = await userStore.registerUser(registerForm)
|
||
|
||
if (result.success) {
|
||
ElMessage.success(result.message || '注册成功')
|
||
router.push('/login')
|
||
} else {
|
||
ElMessage.error(result.message || '注册失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('Register error:', error)
|
||
ElMessage.error('注册失败,请重试')
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.register {
|
||
min-height: calc(100vh - 120px);
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
padding: 40px 0;
|
||
position: relative;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
/* 页面特殊效果 */
|
||
.register::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background:
|
||
radial-gradient(circle at 25% 25%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||
radial-gradient(circle at 75% 75%, rgba(255, 255, 255, 0.05) 0%, transparent 50%);
|
||
animation: registerFloat 4s ease-in-out infinite alternate;
|
||
pointer-events: none;
|
||
z-index: 1;
|
||
}
|
||
|
||
@keyframes registerFloat {
|
||
0% { transform: translateY(0px) rotate(0deg); }
|
||
100% { transform: translateY(-10px) rotate(1deg); }
|
||
}
|
||
|
||
/* 内容层级 */
|
||
.register > * {
|
||
position: relative;
|
||
z-index: 2;
|
||
}
|
||
|
||
.register-container {
|
||
min-height: calc(100vh - 200px);
|
||
}
|
||
|
||
.register-card {
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||
border-radius: 12px;
|
||
backdrop-filter: blur(10px);
|
||
background: rgba(255, 255, 255, 0.95);
|
||
}
|
||
|
||
.register-header {
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.register-header h2 {
|
||
margin: 12px 0 0 0;
|
||
color: #303133;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.register-button {
|
||
width: 100%;
|
||
height: 45px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.register-footer {
|
||
text-align: center;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.register-footer p {
|
||
margin: 0;
|
||
color: #606266;
|
||
}
|
||
|
||
.login-link {
|
||
color: #67C23A;
|
||
text-decoration: none;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.login-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.terms-link {
|
||
color: #409EFF;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.terms-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.checking-text, .error-text, .success-text {
|
||
font-size: 12px;
|
||
margin-top: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.checking-text {
|
||
color: #909399;
|
||
}
|
||
|
||
.error-text {
|
||
color: #F56C6C;
|
||
}
|
||
|
||
.success-text {
|
||
color: #67C23A;
|
||
}
|
||
|
||
.password-strength {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.strength-bar {
|
||
height: 4px;
|
||
background-color: #EBEEF5;
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.strength-fill {
|
||
height: 100%;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.strength-fill.weak {
|
||
background-color: #F56C6C;
|
||
}
|
||
|
||
.strength-fill.fair {
|
||
background-color: #E6A23C;
|
||
}
|
||
|
||
.strength-fill.good {
|
||
background-color: #409EFF;
|
||
}
|
||
|
||
.strength-fill.strong {
|
||
background-color: #67C23A;
|
||
}
|
||
|
||
.strength-text {
|
||
font-size: 12px;
|
||
color: #606266;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.register {
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.register-container {
|
||
min-height: calc(100vh - 160px);
|
||
}
|
||
}
|
||
</style>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|