WholeProcessPlatform/frontend/src/views/register/index.vue
2026-05-07 08:35:52 +08:00

849 lines
30 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-container">
<div class="register-wrapper">
<!-- 左侧背景图区域 -->
<div class="left-section">
<div class="slogan">
<p>{{ $t("login.titleSjtb") }}</p>
</div>
</div>
<!-- 右侧注册表单区域 -->
<div class="right-section">
<a-tabs v-model:activeKey="activeTab" class="register-tabs">
<a-tab-pane key="register" tab="用户注册">
<a-form :model="registerData" :rules="registerRules" layout="horizontal"
:label-col="{ span: 6 }" :wrapper-col="{ span: 18 }" class="form-container"
@finish="handleFormFinish">
<!-- 账号信息 -->
<div class="form-section">
<div class="section-title">账号信息</div>
<!-- 账号 -->
<a-form-item name="username" label="账&nbsp;&nbsp;号">
<a-input v-model:value="registerData.username" placeholder="请输入登录账号4-20个字符" />
</a-form-item>
<!-- 密码 -->
<a-form-item name="password" label="密&nbsp;&nbsp;码" :dependencies="['username']">
<a-input-password v-model:value="registerData.password"
placeholder="请设置密码6-20个字符" />
</a-form-item>
<!-- 确认密码 -->
<a-form-item name="confirmPassword" label="确认密码">
<a-input-password v-model:value="registerData.confirmPassword"
placeholder="请再次输入密码" />
</a-form-item>
</div>
<!-- 个人信息 -->
<div class="form-section">
<div class="section-title">个人信息</div>
<!-- 真实姓名 -->
<a-form-item name="realName" label="真实姓名">
<a-input v-model:value="registerData.realName" placeholder="请输入真实姓名" />
</a-form-item>
<!-- 所属单位 -->
<a-form-item name="belongingUnit" label="所属单位">
<a-input v-model:value="registerData.belongingUnit" placeholder="请输入所属单位(选填)" />
</a-form-item>
<!-- 手机号 -->
<a-form-item name="phone" label="手 机 号">
<a-row :gutter="8">
<a-col :span="16">
<a-input v-model:value="registerData.phone" placeholder="请输入11位手机号" />
</a-col>
<a-col :span="8">
<a-button type="primary" :disabled="smsCountdown > 0" @click="handleSendSms"
style="width: 100%">
{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
</a-button>
</a-col>
</a-row>
</a-form-item>
<!-- 短信验证码 -->
<a-form-item name="smsCode" label="验证码">
<a-input v-model:value="registerData.smsCode" placeholder="请输入短信验证码" />
</a-form-item>
</div>
<!-- 注册按钮 -->
<a-form-item style="display: flex;align-items: center;justify-content: center;width: 100%;">
<a-button type="primary" size="large" block htmlType="submit" :loading="loading"
style="margin-left: 20px;">
<span>立即注册</span>
</a-button>
</a-form-item>
<!-- 返回登录 -->
<a-form-item style="display: flex;align-items: center;justify-content: center;width: 100%;">
<a-button type="link" size="small" block @click="backToLogin">
已有账号返回登录
</a-button>
<a-button type="link" size="small" block >
|
</a-button>
<div></div>
<a href="/file/注册操作手册.docx" download="注册用户操作手册.docx">
<a-button type="link" size="small" block >
注册用户操作手册
</a-button>
</a>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
<!-- 注册确认弹框 -->
<a-modal v-model:open="modalVisible" title="选择所属组织" width="600px" :confirm-loading="loading"
@ok="onRegister" @cancel="handleModalCancel" :maskClosable="false">
<a-form :model="organizationData" :rules="organizationRules" layout="horizontal"
ref="organizationFormRef" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<!-- 集团单选字符串 -->
<a-form-item name="groupCode" label="集 团">
<a-select v-model:value="organizationData.groupCode" placeholder="请选择集团" style="width: 100%"
show-search :filter-option="filterOption" @change="onGroupChange">
<a-select-option v-for="item in groupList" :key="item.hycd" :value="item.hycd"
:label="item.hynm">
{{ item.hynm }}
</a-select-option>
</a-select>
</a-form-item>
<!-- 公司(单选,字符串) -->
<a-form-item name="companyCode" label="公 司">
<a-select v-model:value="organizationData.companyCode" placeholder="请选择公司"
style="width: 100%" show-search :filter-option="filterOption" allow-clear>
<a-select-option v-for="item in companyList" :key="item.hycd" :value="item.hycd"
:label="item.hynm">
{{ item.hynm }}
</a-select-option>
</a-select>
</a-form-item>
<!-- 流域(多选,数组,必填) -->
<a-form-item name="hbrvcdCode" label="流 域" required>
<a-select v-model:value="organizationData.hbrvcdCode" mode="multiple" placeholder="请选择流域"
style="width: 100%" show-search :filter-option="filterOption" @change="onRvcdChange">
<a-select-option v-for="item in basinList" :key="item.hbrvcd" :value="item.hbrvcd"
:label="item.hbrvnm">
{{ item.hbrvnm }}
</a-select-option>
</a-select>
</a-form-item>
<!-- 电站(多选,数组,必填,依赖流域) -->
<a-form-item name="stationCode" label="电 站" required>
<a-select v-model:value="organizationData.stationCode" mode="multiple" placeholder="请先选择流域"
:disabled="organizationData.hbrvcdCode.length === 0" style="width: 100%" show-search
:filter-option="filterOption">
<a-select-option v-for="item in stationList" :key="item.stcd" :value="item.stcd"
:label="item.ennm">
{{ item.ennm }}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, onUnmounted } from "vue";
import {
getBasinList,
getGroupList,
getCompanyList,
getStationList,
sendSmsCode,
verifySmsCode,
registerUser
} from "@/api/auth";
import { message } from "ant-design-vue";
import router from "@/router";
import { encrypt } from "@/utils/rsaEncrypt";
// 注册表单数据
const registerData = reactive({
// 用户信息
username: "",
password: "",
confirmPassword: "",
realName: "",
belongingUnit: "",
phone: "",
smsCode: "",
});
// 下拉选项数据
const basinList = ref<any[]>([]);
const groupList = ref<any[]>([]);
const companyList = ref<any[]>([]);
const stationList = ref<any[]>([]);
// 短信验证码倒计时
const smsCountdown = ref(0);
let smsTimer: any = null;
// ==================== 组织表单数据 ====================
const organizationData = reactive({
groupCode: '', // 集团(单选,字符串)
companyCode: '', // 公司(单选,字符串)
hbrvcdCode: [], // 流域(多选,数组)
stationCode: [] // 电站(多选,数组)
});
// 组织表单引用
const organizationFormRef = ref();
// ==================== 组织表单验证规则 ====================
const organizationRules = {
hbrvcdCode: [
{
validator: (rule: any, value: any[]) => {
if (!value || value.length === 0) {
return Promise.reject("请至少选择一个流域");
}
return Promise.resolve();
},
trigger: "change"
}
],
stationCode: [
{
validator: (rule: any, value: any[]) => {
if (!value || value.length === 0) {
return Promise.reject("请至少选择一个电站");
}
return Promise.resolve();
},
trigger: "change"
}
]
};
// 表单验证规则
const registerRules = {
// 登录账号
username: [
{ required: true, message: "请输入登录账号", trigger: "blur" },
{ min: 4, max: 20, message: "账号长度4-20个字符", trigger: "blur" },
{ pattern: /^[a-zA-Z0-9_]+$/, message: "只能包含字母、数字和下划线", trigger: "blur" }
],
// 密码
password: [
{
validator: (rule: any, value: string) => {
if (!value) {
return Promise.reject("请输入密码");
}
// 1. 长度检查不低于10位
if (value.length < 10) {
return Promise.reject("密码长度不能少于10位");
}
// 2. 复杂度检查:四类字符中至少包含三类
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasNumber = /[0-9]/.test(value);
const hasSpecial = /[^a-zA-Z0-9]/.test(value);
const typeCount = [hasUpperCase, hasLowerCase, hasNumber, hasSpecial].filter(Boolean).length;
if (typeCount < 3) {
return Promise.reject("密码必须包含大写字母、小写字母、数字、特殊字符中的至少三类");
}
// 3. 禁止连续重复序列检查
// 3.1 检查是否有2位及以上相同字符重复
for (let i = 0; i <= value.length - 2; i++) {
if (value[i] === value[i + 1]) {
return Promise.reject("密码不能包含2位及以上相同字符的连续重复如11、aa");
}
}
// 3.2 检查是否有2位及以上连续递增/递减的数字
for (let i = 0; i <= value.length - 2; i++) {
const char1 = value.charCodeAt(i);
const char2 = value.charCodeAt(i + 1);
// 检查是否都是数字
if (/\d/.test(value[i]) && /\d/.test(value[i + 1])) {
// 检查递增或递减
if (Math.abs(char2 - char1) === 1) {
return Promise.reject("密码不能包含2位及以上连续递增或递减的数字如12、21");
}
}
}
// 3.3 检查是否有2位及以上连续递增/递减的字母
for (let i = 0; i <= value.length - 2; i++) {
const char1 = value.charCodeAt(i);
const char2 = value.charCodeAt(i + 1);
// 检查是否都是字母
if (/[a-zA-Z]/.test(value[i]) && /[a-zA-Z]/.test(value[i + 1])) {
// 检查递增或递减
if (Math.abs(char2 - char1) === 1) {
return Promise.reject("密码不能包含2位及以上连续递增或递减的字母如ab、ba");
}
}
}
// 4. 用户名关联检查(严格模式,忽略大小写)
// 4.0 先判断用户名是否为空,若为空则跳过该检查
const username = registerData.username;
if (!username || username.trim() === '') {
// 用户名为空,跳过用户名关联检查
return Promise.resolve();
}
const usernameLower = username.toLowerCase();
const passwordLower = value.toLowerCase();
// 4.1 检查密码是否完整包含用户名
if (passwordLower.includes(usernameLower)) {
return Promise.reject("密码不能包含用户名");
}
// 4.2 提取密码中的字母部分
const passwordLetters = passwordLower.replace(/[^a-z]/g, '');
// 4.3 如果密码字母部分长度 >= 3检查是否是用户名的子串
if (passwordLetters.length >= 3 && usernameLower.includes(passwordLetters)) {
return Promise.reject("密码的字母部分不能是用户名的子串");
}
// 4.4 检查用户名中是否包含密码的任意连续3位字母
for (let i = 0; i <= passwordLetters.length - 3; i++) {
const substring = passwordLetters.substring(i, i + 3);
if (usernameLower.includes(substring)) {
return Promise.reject("密码不能与用户名存在明显关联");
}
}
return Promise.resolve();
},
trigger: "blur"
}
],
// 确认密码
confirmPassword: [
{ required: true, message: "请再次输入密码", trigger: "blur" },
{
validator: (rule: any, value: string) => {
if (value && value !== registerData.password) {
return Promise.reject("两次输入的密码不一致");
}
return Promise.resolve();
},
trigger: "blur"
}
],
// 真实姓名
realName: [
{ required: true, message: "请输入真实姓名", trigger: "blur" },
{ min: 2, max: 20, message: "姓名长度2-20个字符", trigger: "blur" }
],
// 所属单位(选填,无验证规则)
// 手机号
phone: [
{ required: true, message: "请输入手机号", trigger: "blur" },
{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的11位手机号", trigger: "blur" }
],
// 短信验证码
smsCode: [
{ required: true, message: "请输入短信验证码", trigger: "blur" },
{ len: 6, message: "验证码为6位数字", trigger: "blur" }
]
};
const loading = ref(false);
const activeTab = ref("register");
const modalVisible = ref(false);
// 初始化加载集团列表
const loadGroupList = async () => {
try {
const res = await getGroupList();
groupList.value = res.data || [];
} catch (error) {
// console.error("加载集团列表失败", error);
}
};
// 集团变化
const onGroupChange = async () => {
// 加载公司列表
// if (value) {
try {
// const res = await getCompanyList(organizationData.groupCode ? organizationData.groupCode : '');
const res = await getCompanyList();
companyList.value = res.data || [];
} catch (error) {
// message.error("加载公司列表失败");
}
// }
};
// 公司变化
const onCompanyChange = async () => {
// 加载流域列表
// if (value) {
try {
const res = await getBasinList();
basinList.value = res.data || [];
} catch (error) {
// message.error("加载流域列表失败");
}
// }
};
// 流域变化
const onBasinChange = async (ids: any) => {
// 加载电站列表
// if (value) {
try {
const res = await getStationList(ids);
stationList.value = res.data || [];
} catch (error) {
// message.error("加载电站列表失败");
}
// }
};
// ==================== 流域变化处理(弹框内) ====================
const onRvcdChange = () => {
// 清空电站选择
organizationData.stationCode = [];
// 获取当前选中的所有流域ID加载对应的电站列表
if (organizationData.hbrvcdCode && organizationData.hbrvcdCode.length > 0) {
onBasinChange({ hbrvcds: organizationData.hbrvcdCode });
} else {
// 如果没有选择流域,清空电站列表
stationList.value = [];
}
};
// 发送短信验证码
const handleSendSms = async () => {
// 验证手机号格式
if (!/^1[3-9]\d{9}$/.test(registerData.phone)) {
message.error("请输入正确的手机号");
return;
}
try {
const res: any = await sendSmsCode(registerData.phone, 1); // type=1 表示注册
if (res.code == 1) {
return;
}
message.success("验证码已发送");
// 启动60秒倒计时
smsCountdown.value = 60;
smsTimer = setInterval(() => {
smsCountdown.value--;
if (smsCountdown.value <= 0) {
clearInterval(smsTimer);
smsTimer = null;
}
}, 1000);
} catch (error: any) {
// message.error(error.message || "发送失败,请重试");
}
};
// 表单验证通过后的处理
const handleFormFinish = async () => {
// 第一步:验证短信验证码
// const res: any = await verifySmsCode(registerData.phone, registerData.smsCode, 1);
// if (res.code == 1) {
// message.error('短信验证码错误,请重试')
// return
// }
// 打开确认弹框
modalVisible.value = true;
};
// 弹框取消
const handleModalCancel = () => {
// 关闭弹框,不做任何操作
modalVisible.value = false;
// 重置组织表单数据
organizationData.groupCode = '';
organizationData.companyCode = '';
organizationData.hbrvcdCode = [];
organizationData.stationCode = [];
// 清除表单验证状态
organizationFormRef.value?.clearValidate();
};
// 注册提交
const onRegister = async () => {
// 先验证组织表单
try {
await organizationFormRef.value.validate();
} catch (error) {
// message.error("请完善组织信息");
return; // 验证失败,阻止提交
}
loading.value = true;
try {
// 第二步:密码加密
const encryptedPassword = encrypt(registerData.password);
// 第三步:构造注册数据(包含组织信息)
const registerParams: any = {
type: 1, // 固定值:注册类型
phone: registerData.phone, // 手机号(必填)
code: registerData.smsCode, // 短信验证码(必填,字段改名)
username: registerData.username, // 用户名(必填)
password: encryptedPassword, // 加密后的密码(必填)
realName: registerData.realName, // 真实姓名(必填)
};
// 可选字段:所属单位(如果有值才传)
if (registerData.belongingUnit) {
registerParams.belongingUnit = registerData.belongingUnit;
}
// 可选字段:集团(如果有值才传)
if (organizationData.groupCode) {
registerParams.groupCode = organizationData.groupCode;
}
// 可选字段:公司(如果有值才传)
if (organizationData.companyCode) {
registerParams.companyCode = organizationData.companyCode;
}
// 可选字段:流域(如果有值才传,数组转逗号分隔字符串)
if (organizationData.hbrvcdCode && organizationData.hbrvcdCode.length > 0) {
registerParams.hbrvcdCode = organizationData.hbrvcdCode.join(',');
}
// 可选字段:电站(如果有值才传,数组转逗号分隔字符串)
if (organizationData.stationCode && organizationData.stationCode.length > 0) {
registerParams.stationCode = organizationData.stationCode.join(',');
}
// 第四步:调用注册接口
const res1: any = await registerUser(registerParams);
if (res1.code == 1) {
message.error('注册失败,请重试')
return
}
message.success("注册成功,等待管理员审核");
// 关闭弹框
modalVisible.value = false;
// 延迟跳转到登录页
setTimeout(() => {
router.push({ path: "/login" });
}, 1500);
} catch (error: any) {
// 区分验证码错误和注册错误
// if (error.message && error.message.includes('验证码')) {
// message.error("验证码错误,请重试");
// } else {
// message.error(error.message || "注册失败,请重试");
// }
} finally {
loading.value = false;
}
};
// 返回登录
const backToLogin = () => {
router.push({ path: "/login" });
};
// 组件挂载时加载集团列表
onMounted(() => {
loadGroupList()
onGroupChange()
onCompanyChange()
// modalVisible.value = true
});
// 组件卸载时清除定时器
onUnmounted(() => {
if (smsTimer) {
clearInterval(smsTimer);
smsTimer = null;
}
});
const filterOption = (inputValue: string, option: any) => {
if (!option.label) return false;
return option.label.indexOf(inputValue) !== -1;
};
</script>
<style scoped lang="scss">
.register-container {
margin: 0 auto;
position: relative;
width: 100%;
height: 100%;
min-width: 1200px; // 降低最小宽度要求
background-color: #fff;
.register-wrapper {
position: relative;
width: 100%;
height: 100%;
min-height: 600px;
background: url("@/assets/images/bg_sjtb.png");
background-repeat: no-repeat;
background-size: cover; // 改为cover以更好地适配不同尺寸
background-position: center; // 居中显示背景图
}
// 左侧背景区域
.left-section {
.slogan {
position: absolute;
top: 20%;
left: 5%; // 减小左边距更靠左
width: 40vw; // 使用视口宽度百分比
max-width: 700px; // 保持最大宽度限制
min-width: 300px; // 设置最小宽度
color: #040504;
font-size: clamp(24px, 3vw, 40px); // 响应式字体大小
line-height: 1.5;
p {
margin: 0;
word-wrap: break-word; // 允许长文本换行
}
}
}
// 右侧注册卡片区域
.right-section {
position: absolute;
right: 5%; // 改用right定位更合理
top: 50%; // 垂直居中
transform: translateY(-50%); // 垂直居中对齐
width: clamp(380px, 30vw, 500px); // 响应式宽度
min-height: 600px; // 降低最小高度
max-height: 90vh; // 限制最大高度为视口的90%
border-radius: 8px; // 稍微增大圆角
padding: clamp(15px, 2vw, 24px) clamp(15px, 2vw, 24px); // 响应式内边距
background-color: rgba(255, 255, 255, 0.98); // 轻微透明效果
overflow-y: auto;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); // 添加阴影提升层次感
// 自定义滚动条样式
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
:deep(.ant-tabs-nav) {
height: auto; // 自动高度
margin-bottom: 16px;
}
// 表单分区样式
.form-section {
margin-bottom: clamp(12px, 1.5vh, 20px); // 响应式间距
.section-title {
font-size: clamp(13px, 1.2vw, 15px); // 响应式字体
font-weight: 600;
color: #333;
margin-bottom: clamp(8px, 1vh, 12px);
padding-bottom: clamp(6px, 0.8vh, 10px);
border-bottom: 1px solid #e8e8e8;
}
}
:deep(.ant-form-item) {
margin-bottom: clamp(10px, 1.2vh, 16px); // 响应式表单项间距
}
:deep(.ant-form-item-label) {
text-align: right !important;
>label {
font-size: clamp(12px, 1vw, 14px); // 响应式标签字体
color: #333;
white-space: nowrap;
display: inline-block;
&::after {
content: ':';
}
}
}
:deep(.ant-input),
:deep(.ant-input-password) {
font-size: clamp(12px, 1vw, 14px); // 输入框字体响应式
}
:deep(.ant-btn) {
font-size: clamp(12px, 1vw, 14px); // 按钮字体响应式
}
:deep(.ant-radio-group) {
display: flex;
gap: clamp(12px, 1.5vw, 20px); // 响应式间距
}
:deep(.ant-form-item-control-input-content) {
display: flex;
align-items: center;
}
}
}
// 响应式媒体查询 - 小屏幕适配
@media (max-width: 1400px) {
.register-container {
.left-section {
.slogan {
left: 3%;
width: 45vw;
font-size: clamp(20px, 2.5vw, 32px);
}
}
.right-section {
right: 3%;
width: clamp(350px, 35vw, 450px);
}
}
}
// 中等屏幕适配
@media (min-width: 1401px) and (max-width: 1920px) {
.register-container {
.left-section {
.slogan {
left: 5%;
width: 40vw;
}
}
.right-section {
right: 5%;
width: clamp(380px, 30vw, 480px);
}
}
}
// 大屏幕适配
@media (min-width: 1921px) {
.register-container {
.left-section {
.slogan {
left: 8%;
width: 35vw;
font-size: clamp(32px, 3.5vw, 48px);
}
}
.right-section {
right: 8%;
width: clamp(420px, 28vw, 550px);
padding: 24px 32px;
}
}
}
// 超小屏幕适配
@media (max-width: 1200px) {
.register-container {
.left-section {
.slogan {
display: none; // 小屏幕隐藏标语,聚焦表单
}
}
.right-section {
left: 50%;
right: auto;
transform: translate(-50%, -50%); // 水平垂直都居中
width: clamp(320px, 90vw, 420px);
}
}
}
// 高缩放比例适配125%、150%等)
@media (-webkit-min-device-pixel-ratio: 1.25),
(min-resolution: 120dpi) {
.register-container {
.right-section {
padding: clamp(12px, 1.5vw, 20px);
.form-section {
margin-bottom: clamp(10px, 1.2vh, 16px);
}
:deep(.ant-form-item) {
margin-bottom: 8px;
}
}
}
}
// 横屏模式优化
@media (orientation: landscape) and (max-height: 700px) {
.register-container {
.right-section {
max-height: 95vh;
padding: 10px 15px;
.form-section {
margin-bottom: 8px;
.section-title {
margin-bottom: 6px;
padding-bottom: 6px;
font-size: 13px;
}
}
:deep(.ant-form-item) {
margin-bottom: 8px;
}
}
}
}
</style>