Files
jurong_circle_frontdesk/src/views/Register.vue
2025-09-15 15:25:35 +08:00

1016 lines
23 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.

<template>
<div class="register-page">
<div class="register-container">
<div class="register-card">
<div class="register-header">
<h2>用户注册</h2>
<p>创建你的账号开始使用炬融圈</p>
</div>
<el-form
ref="registerFormRef"
:model="registerForm"
:rules="registerRules"
class="register-form"
@submit.prevent="handleRegister"
>
<!-- <el-form-item prop="username">-->
<!-- <el-input-->
<!-- v-model="registerForm.username"-->
<!-- placeholder="请输入用户名"-->
<!-- size="large"-->
<!-- :prefix-icon="User"-->
<!-- clearable-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item prop="phone">
<el-input
v-model="registerForm.phone"
placeholder="请输入手机号"
size="large"
:prefix-icon="Message"
clearable
/>
</el-form-item>
<el-form-item prop="smsCode">
<div class="sms-code-group">
<el-input
v-model="registerForm.smsCode"
placeholder="请输入短信验证码"
size="large"
:prefix-icon="ChatDotRound"
clearable
class="sms-input"
/>
<el-button
type="primary"
size="large"
:disabled="!canSendSMS || smsCountdown > 0"
:loading="sendingSMS"
@click="sendSMSCode"
class="sms-button"
>
{{ smsCountdown > 0 ? `${smsCountdown}s后重发` : '发送验证码' }}
</el-button>
</div>
</el-form-item>
<el-form-item prop="region">
<el-cascader
v-model="registerForm.region"
:options="regionOptions"
placeholder="请选择省市区"
size="large"
style="width: 100%"
:props="{ expandTrigger: 'hover', value: 'code' }"
:show-all-levels="true"
:collapse-tags="true"
:max-collapse-tags="1"
:teleported="false"
popper-class="mobile-cascader-popper"
@change="handleRegionChange"
>
<template #prefix>
<el-icon><Location /></el-icon>
</template>
</el-cascader>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="registerForm.password"
type="password"
placeholder="请输入密码"
size="large"
:prefix-icon="Lock"
show-password
clearable
/>
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input
v-model="registerForm.confirmPassword"
type="password"
placeholder="请确认密码"
size="large"
:prefix-icon="Lock"
show-password
clearable
/>
</el-form-item>
<el-form-item prop="captcha">
<Captcha
ref="captchaRef"
v-model="registerForm.captcha"
placeholder="请输入验证码"
size="large"
/>
</el-form-item>
<el-form-item prop="agreement">
<el-checkbox
v-model="registerForm.agreement"
@click.prevent="showAgreementDialog"
:disabled="true"
>
<span @click="showAgreementDialog" class="agreement-text">
我已阅读并同意用户协议隐私政策
</span>
</el-checkbox>
<div class="agreement-hint">
<el-icon><InfoFilled /></el-icon>
<span>请点击查看用户协议和隐私政策</span>
</div>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
class="register-button"
:loading="userStore.loading"
@click="handleRegister"
>
{{ userStore.loading ? '注册中...' : '立即注册' }}
</el-button>
</el-form-item>
</el-form>
<div class="register-footer">
<p>
已有账号
<el-link type="primary" @click="$router.push('/mylogin')">
立即登录
</el-link>
</p>
</div>
</div>
</div>
<!-- 背景装饰 -->
<div class="background-decoration">
<div class="decoration-shape shape-1"></div>
<div class="decoration-shape shape-2"></div>
<div class="decoration-shape shape-3"></div>
<div class="decoration-shape shape-4"></div>
</div>
<!-- 协议弹窗 -->
<el-dialog
v-model="showAgreementDialogVisible"
title="用户协议和隐私政策"
width="90%"
:style="{ maxWidth: '600px' }"
:show-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
center
class="agreement-dialog"
position="bottom"
>
<div class="agreement-content">
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane name="agreement">
<template #label>
<span class="tab-label">用户协议</span>
<el-icon v-if="agreementViewed" class="check-icon"><Check /></el-icon>
</template>
<div class="agreement-text-content" ref="agreementContent">
<h3>用户协议</h3>
<p>1. 用户应当遵守法律法规不得发布违法违规内容</p>
<p>2. 用户对自己发布的内容承担全部责任</p>
<p>3. 平台有权对违规内容进行删除或限制</p>
<p>4. 用户应当保护好自己的账号安全</p>
<p>5. 平台保留修改本协议的权利</p>
</div>
</el-tab-pane>
<el-tab-pane name="privacy">
<template #label>
<span class="tab-label">隐私政策</span>
<el-icon v-if="privacyViewed" class="check-icon"><Check /></el-icon>
</template>
<div class="privacy-text-content" ref="privacyContent">
<h3>隐私政策</h3>
<p>1. 我们重视用户隐私保护</p>
<p>2. 我们只收集必要的用户信息</p>
<p>3. 用户信息仅用于提供服务</p>
<p>4. 我们不会向第三方泄露用户信息</p>
<p>5. 用户有权查看修改或删除个人信息</p>
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<div class="dialog-footer">
<div class="read-status">
</div>
<div class="dialog-buttons">
<el-button @click="closeAgreementDialog">取消</el-button>
<el-button type="primary" @click="confirmAgreement" :disabled="!(agreementViewed && privacyViewed)">确认已阅读</el-button>
</div>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useUserStore } from '@/stores/user';
import { ElMessage, ElMessageBox } from 'element-plus';
import {
User,
Lock,
Message,
Edit,
ChatDotRound,
CreditCard,
Location,
InfoFilled,
Check,
} from '@element-plus/icons-vue';
import Captcha from '@/components/Captcha.vue';
import api from '@/utils/api'
const router = useRouter();
const route = useRoute();
const userStore = useUserStore();
// 表单引用
const registerFormRef = ref();
const captchaRef = ref();
// 表单数据
const registerForm = reactive({
username: '',
phone: '',
region: [],
province: '',
city: '',
district: '',
district_id: '',
password: '',
confirmPassword: '',
captcha: '',
smsCode: '',
agreement: false,
});
// 协议查看状态
const agreementViewed = ref(false);
const privacyViewed = ref(false);
// 协议弹窗相关状态
const showAgreementDialogVisible = ref(false);
const activeTab = ref('agreement');
const agreementContent = ref();
const privacyContent = ref();
// 短信验证码相关状态
const sendingSMS = ref(false);
const smsCountdown = ref(0);
const canSendSMS = computed(() => {
const phoneRegex = /^1[3-9]\d{9}$/;
return phoneRegex.test(registerForm.phone);
});
// 地区数据
const regions = ref([]);
const regionOptions = ref([]);
const cities = computed(() => {
const citySet = new Set();
regions.value.forEach((region) => {
if (region.city_name) {
citySet.add(region.city_name);
}
});
return Array.from(citySet).sort();
});
const filteredDistricts = computed(() => {
if (!registerForm.city) return [];
return regions.value.filter(
(region) => region.city_name === registerForm.city
);
});
// 自定义验证函数
const validateUsername = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入用户名'));
} else if (value.length < 3) {
callback(new Error('用户名至少3个字符'));
} else if (value.length > 20) {
callback(new Error('用户名不能超过20个字符'));
} else if (!/^[a-zA-Z0-9_\u4e00-\u9fa5]+$/.test(value)) {
callback(new Error('用户名只能包含字母、数字、下划线和中文'));
} else {
callback();
}
};
const validatePassword = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入密码'));
} else if (value.length < 6) {
callback(new Error('密码至少6个字符'));
} else if (value.length > 20) {
callback(new Error('密码不能超过20个字符'));
} else if (!/(?=.*[a-zA-Z])(?=.*\d)/.test(value)) {
callback(new Error('密码必须包含字母和数字'));
} else {
// 如果确认密码已输入,重新验证确认密码
if (registerForm.confirmPassword) {
registerFormRef.value?.validateField('confirmPassword');
}
callback();
}
};
const validateConfirmPassword = (rule, value, callback) => {
if (!value) {
callback(new Error('请确认密码'));
} else if (value !== registerForm.password) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
};
const validateAgreement = (rule, value, callback) => {
if (!value) {
callback(new Error('请阅读并同意用户协议和隐私政策'));
} else {
callback();
}
};
// 表单验证规则
const registerRules = {
username: [{ validator: validateUsername, trigger: 'blur' }],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号',
trigger: 'blur',
},
],
smsCode: [
{ required: true, message: '请输入短信验证码', trigger: 'blur' },
{ pattern: /^\d{6}$/, message: '短信验证码为6位数字', trigger: 'blur' },
],
region: [{ required: true, message: '请选择省市区', trigger: 'change' }],
password: [{ validator: validatePassword, trigger: 'blur' }],
confirmPassword: [{ validator: validateConfirmPassword, trigger: 'blur' }],
captcha: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
{ min: 4, max: 4, message: '验证码长度为4位', trigger: 'blur' },
],
agreement: [{ validator: validateAgreement, trigger: 'change' }],
};
// 发送短信验证码
const sendSMSCode = async () => {
if (!canSendSMS.value || sendingSMS.value || smsCountdown.value > 0) {
return;
}
try {
sendingSMS.value = true;
const response = await fetch('/api/sms/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
phone: registerForm.phone,
}),
});
const result = await response.json();
if (result.success) {
ElMessage.success('验证码发送成功,请查收短信');
// 开始倒计时
startCountdown();
} else {
ElMessage.error(result.message || '发送失败,请重试');
}
} catch (error) {
console.error('发送短信验证码失败:', error);
ElMessage.error('发送失败,请检查网络连接');
} finally {
sendingSMS.value = false;
}
};
// 开始倒计时
const startCountdown = () => {
smsCountdown.value = 60;
const timer = setInterval(() => {
smsCountdown.value--;
if (smsCountdown.value <= 0) {
clearInterval(timer);
}
}, 1000);
};
// 加载省市区级联数据
const loadRegionOptions = async () => {
const provincesResponse = await api.get('/regions/provinces');
regionOptions.value = provincesResponse.data.data || [];
};
// 处理级联选择器变化
const handleRegionChange = (value) => {
if (value && value.length === 3) {
registerForm.province = value[0];
registerForm.city = value[1];
registerForm.district = value[2];
registerForm.district_id = value[2];
}
};
/**
* 城市变化处理
*/
const onCityChange = () => {
// 清空区域选择
registerForm.district_id = '';
};
// 处理注册
const handleRegister = async () => {
if (!registerFormRef.value || !captchaRef.value) return;
try {
// 先验证表单
const valid = await registerFormRef.value.validate();
if (!valid) return;
// 验证验证码
const captchaValid = await captchaRef.value.verifyCaptcha(
registerForm.captcha
);
if (!captchaValid) {
registerForm.captcha = '';
return;
}
// 获取验证码信息
const captchaInfo = captchaRef.value.getCaptchaInfo();
// 提交注册请求(包含验证码信息)
const registerData = {
username: registerForm.phone,
phone: registerForm.phone,
city: registerForm.city,
district_id: registerForm.district_id,
password: registerForm.password,
smsCode: registerForm.smsCode,
captchaId: captchaInfo.captchaId,
captchaText: captchaInfo.captchaText,
province: registerForm.province,
inviter: registerForm.inviter,
};
console.log(registerData,'registerData');
console.log(registerForm,'registerForm')
const result = await userStore.register(registerData);
if (result.success) {
// 检查是否需要支付
if (result.needPayment) {
ElMessage.success('用户信息创建成功,请完成支付以激活账户');
// 跳转到支付页面
router.push({
path: '/payment',
});
} else {
ElMessage.success('注册成功!请登录');
router.push('/mylogin');
}
}
} catch (error) {
console.error('注册失败:', error);
// 注册失败后刷新验证码
if (captchaRef.value) {
await captchaRef.value.refreshCaptcha();
}
registerForm.captcha = '';
}
};
// 显示协议弹窗
const showAgreementDialog = () => {
showAgreementDialogVisible.value = true;
activeTab.value = 'agreement';
// 弹窗打开时默认用户协议已读
agreementViewed.value = true;
};
// 关闭协议弹窗
const closeAgreementDialog = () => {
showAgreementDialogVisible.value = false;
};
// 确认已阅读协议
const confirmAgreement = () => {
if (agreementViewed.value && privacyViewed.value) {
registerForm.agreement = true;
showAgreementDialogVisible.value = false;
ElMessage.success('已确认阅读用户协议和隐私政策');
} else {
ElMessage.warning('请先查看用户协议和隐私政策');
}
};
// 处理选项卡切换
const handleTabChange = (tabName) => {
if (tabName === 'agreement') {
// 切换到用户协议时,标记为已读
agreementViewed.value = true;
} else if (tabName === 'privacy') {
// 切换到隐私政策时,标记为已读
privacyViewed.value = true;
}
};
// 图片上传成功处理
const handleUploadSuccess = (response, field) => {
ElMessage.success('图片上传成功');
};
// 图片上传失败处理
const handleUploadError = (error) => {
ElMessage.error('图片上传失败,请重试');
};
const invite = async () => {
const inviter = route.query.inviter;
console.log('邀请人ID:', inviter);
if (inviter) {
registerForm.inviter = inviter;
// try {
// const response = await api.get(`/user/${inviter}`);
// if (response.data.success) {
// registerForm.inviter = inviter;
// }
// } catch (error) {
// console.error('获取邀请人信息失败:', error);
// }
}
}
// 组件挂载时的处理
onMounted(() => {
// 如果已经登录,直接跳转
if (userStore.isAuthenticated) {
const redirectPath = route.query.redirect || '/';
router.push(redirectPath);
}
// 加载省市区级联数据
loadRegionOptions();
invite();
});
</script>
<style lang="scss" scoped>
.register-page {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.register-container {
width: 100%;
max-width: 450px;
padding: 20px;
position: relative;
z-index: 10;
}
.register-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 40px 30px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.register-header {
text-align: center;
margin-bottom: 30px;
}
.register-header h2 {
color: #303133;
margin-bottom: 8px;
font-weight: 600;
}
.register-header p {
color: #909399;
font-size: 14px;
}
.register-form {
margin-bottom: 20px;
}
.register-button {
width: 100%;
height: 44px;
font-size: 16px;
font-weight: 600;
}
.register-footer {
text-align: center;
margin-bottom: 20px;
}
.register-footer p {
color: #606266;
font-size: 14px;
}
.document-upload-section {
margin: 20px 0;
}
.document-upload-section .el-divider {
margin: 20px 0;
}
.document-upload-section .el-form-item {
margin-bottom: 20px;
}
.document-upload-section .el-form-item .el-form-item__label {
font-weight: 500;
color: #606266;
}
.feature-item {
display: flex;
align-items: center;
gap: 8px;
color: #606266;
font-size: 14px;
}
.feature-item .el-icon {
color: #409eff;
font-size: 16px;
}
.upload-preview {
margin-top: 10px;
}
.upload-preview img {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #dcdfe6;
}
.upload-demo {
margin-bottom: 10px;
}
.background-decoration {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.decoration-shape {
position: absolute;
background: rgba(255, 255, 255, 0.1);
animation: float 8s ease-in-out infinite;
}
.shape-1 {
width: 100px;
height: 100px;
border-radius: 50%;
top: 15%;
left: 15%;
animation-delay: 0s;
}
.shape-2 {
width: 80px;
height: 80px;
border-radius: 20px;
top: 70%;
right: 20%;
animation-delay: 2s;
}
.shape-3 {
width: 60px;
height: 60px;
border-radius: 50%;
bottom: 25%;
left: 25%;
animation-delay: 4s;
}
.shape-4 {
width: 120px;
height: 40px;
border-radius: 20px;
top: 40%;
right: 10%;
animation-delay: 6s;
}
@keyframes float {
0%,
100% {
transform: translateY(0px) rotate(0deg);
opacity: 0.7;
}
50% {
transform: translateY(-15px) rotate(180deg);
opacity: 1;
}
}
/* 响应式设计 */
@media (max-width: 480px) {
.register-container {
padding: 15px;
}
.register-card {
padding: 30px 20px;
}
.features-list {
gap: 8px;
}
}
/* Element Plus 组件样式覆盖 */
:deep(.el-input__wrapper) {
border-radius: 8px;
}
:deep(.el-button) {
border-radius: 8px;
}
:deep(.el-divider__text) {
background-color: rgba(255, 255, 255, 0.95);
color: #909399;
}
:deep(.el-checkbox__label) {
font-size: 14px;
color: #606266;
}
/* 输入框聚焦效果 */
:deep(.el-input__wrapper:hover),
:deep(.el-input__wrapper.is-focus) {
box-shadow: 0 0 0 1px #409eff inset;
}
/* 加载状态样式 */
.register-button.is-loading {
pointer-events: none;
}
/* 动画效果 */
.register-card {
animation: slideInUp 0.6s ease-out;
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 错误状态样式 */
:deep(.el-form-item.is-error .el-input__wrapper) {
box-shadow: 0 0 0 1px #f56c6c inset;
}
/* 成功状态样式 */
:deep(.el-form-item.is-success .el-input__wrapper) {
box-shadow: 0 0 0 1px #67c23a inset;
}
/* 协议对话框样式 */
:deep(.agreement-dialog),
:deep(.privacy-dialog) {
.el-message-box__content {
max-height: 400px;
overflow-y: auto;
}
}
/* 密码强度指示器 */
.password-strength {
margin-top: 5px;
font-size: 12px;
}
.strength-weak {
color: #f56c6c;
}
.strength-medium {
color: #e6a23c;
}
.strength-strong {
color: #67c23a;
}
/* 短信验证码样式 */
.sms-code-group {
display: flex;
gap: 12px;
align-items: center;
}
.sms-input {
flex: 1;
}
.sms-button {
flex-shrink: 0;
min-width: 120px;
height: 40px;
}
.sms-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 协议相关样式 */
.agreement-hint {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
font-size: 12px;
color: #e6a23c;
}
.agreement-hint .el-icon {
font-size: 14px;
}
.agreement-text {
color: #409eff;
cursor: pointer;
text-decoration: underline;
}
.agreement-text:hover {
color: #66b1ff;
}
/* 协议弹窗样式 */
:deep(.agreement-dialog) {
.el-dialog__body {
padding: 20px;
max-height: 60vh;
overflow: hidden;
}
}
.agreement-content {
height: 100%;
}
:deep(.agreement-dialog .el-tabs__nav) {
display: flex;
width: 100%;
}
:deep(.agreement-dialog .el-tabs__item) {
flex: 1;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
}
.tab-label {
margin-right: 4px;
}
.agreement-text-content,
.privacy-text-content {
max-height: 400px;
overflow-y: auto;
padding: 20px;
line-height: 1.6;
font-size: 14px;
color: #606266;
border: 1px solid #e4e7ed;
border-radius: 8px;
background-color: #fafafa;
}
.agreement-text-content h3,
.privacy-text-content h3 {
color: #303133;
margin-bottom: 16px;
font-size: 18px;
font-weight: 600;
}
.agreement-text-content p,
.privacy-text-content p {
margin-bottom: 12px;
}
.agreement-text-content strong,
.privacy-text-content strong {
color: #409eff;
font-weight: 600;
}
.dialog-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20px;
}
.check-icon {
color: #67c23a;
font-size: 16px;
}
/* 滚动条样式 */
.agreement-text-content::-webkit-scrollbar,
.privacy-text-content::-webkit-scrollbar {
width: 6px;
}
.agreement-text-content::-webkit-scrollbar-track,
.privacy-text-content::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.agreement-text-content::-webkit-scrollbar-thumb,
.privacy-text-content::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.agreement-text-content::-webkit-scrollbar-thumb:hover,
.privacy-text-content::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
:deep(.el-link.viewed:hover) {
color: #529b2e !important;
}
</style>