登录注册:优化协议弹窗与PM后台落点

授权项目负责人登录后,若当前项目具备项目成员、中心管理或审计导出读取权限,优先进入对应管理后台页面,避免仍停留在我的工作台。

抽取登录协议为共享内容模块,注册页新增服务条款和隐私政策弹窗,并在注册成功后展示待管理员审核提示。

补充路由、登录页和注册页静态回归测试,覆盖 PM 管理后台默认落点和协议弹窗行为。
This commit is contained in:
Cheng Zhou
2026-05-13 11:03:46 +08:00
parent 89cecce6b5
commit ab1975d3c6
8 changed files with 392 additions and 57 deletions
+174 -9
View File
@@ -57,8 +57,19 @@
<p class="form-subtitle">步骤 {{ currentStep }}/3 · {{ stepDescription }}</p>
</div>
<section v-if="registrationSubmitted" class="approval-notice" aria-live="polite">
<div class="approval-icon">
<svg width="28" height="28" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
<h3 class="approval-title">注册申请已提交</h3>
<p class="approval-message">待管理员审核通过后方可登录</p>
<p class="approval-hint">管理员审核通过前请勿重复提交注册申请</p>
</section>
<!-- 注册表单 -->
<form class="register-form" @submit.prevent="handleSubmit">
<form v-else class="register-form" @submit.prevent="handleSubmit">
<!-- 步骤 1: 基本信息 -->
<div v-show="currentStep === 1" class="step-content">
<!-- 邮箱 -->
@@ -205,9 +216,13 @@
<span class="checkbox-custom"></span>
<span class="checkbox-label">
我同意
<button type="button" class="link link-button">服务条款</button>
<button type="button" class="link link-button" @click.stop.prevent="openProtocolDialog('terms')">
服务条款
</button>
<button type="button" class="link link-button">隐私政策</button>
<button type="button" class="link link-button" @click.stop.prevent="openProtocolDialog('privacy')">
隐私政策
</button>
</span>
</label>
<span v-if="errors.agreeTerms" class="error-text">{{ errors.agreeTerms }}</span>
@@ -290,19 +305,40 @@
</div>
</div>
</div>
<el-dialog
v-model="protocolDialogVisible"
:title="activeProtocolTitle"
width="860px"
class="register-protocol-dialog"
append-to-body
align-center
>
<div class="protocol-content">
<section v-for="section in activeProtocolSections" :key="section.title" class="protocol-section">
<h3>{{ section.title }}</h3>
<p v-for="paragraph in section.paragraphs" :key="paragraph">{{ paragraph }}</p>
</section>
</div>
<template #footer>
<el-button type="primary" size="large" class="protocol-confirm-btn" @click="closeProtocolDialog">
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, computed } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { register as apiRegister } from "../api/auth";
import type { RegisterRequest } from "../types/api";
import { TEXT, requiredMessage } from "../locales";
import { privacyPolicySections, serviceTermsSections } from "../content/authProtocol";
const router = useRouter();
const defaultRole: RegisterRequest["role"] = "CRA";
type ProtocolType = "terms" | "privacy";
// 步骤控制
const currentStep = ref(1);
@@ -324,9 +360,13 @@ const errors = reactive<Record<string, string>>({});
const submitting = ref(false);
const showPassword = ref(false);
const showConfirmPassword = ref(false);
const protocolDialogVisible = ref(false);
const activeProtocolType = ref<ProtocolType>("terms");
const registrationSubmitted = ref(false);
// 步骤标题
const stepTitle = computed(() => {
if (registrationSubmitted.value) return "注册申请已提交";
switch (currentStep.value) {
case 1: return "基本信息";
case 2: return "安全设置";
@@ -337,6 +377,7 @@ const stepTitle = computed(() => {
// 步骤描述
const stepDescription = computed(() => {
if (registrationSubmitted.value) return "待管理员审核通过后方可登录";
switch (currentStep.value) {
case 1: return "填写您的基本信息";
case 2: return "设置登录密码";
@@ -352,6 +393,14 @@ const passwordHints = computed(() => ({
hasNumber: /[0-9]/.test(form.password),
}));
const activeProtocolTitle = computed(() => (
activeProtocolType.value === "terms" ? "服务条款" : "隐私政策"
));
const activeProtocolSections = computed(() => (
activeProtocolType.value === "terms" ? serviceTermsSections : privacyPolicySections
));
// 验证步骤1
const validateStep1 = (): boolean => {
const newErrors: Record<string, string> = {};
@@ -413,6 +462,15 @@ const clearError = (field: string) => {
delete errors[field];
};
const openProtocolDialog = (type: ProtocolType) => {
activeProtocolType.value = type;
protocolDialogVisible.value = true;
};
const closeProtocolDialog = () => {
protocolDialogVisible.value = false;
};
// 下一步
const nextStep = () => {
// 清除之前的错误
@@ -454,10 +512,8 @@ const handleSubmit = async () => {
role: defaultRole,
clinical_department: form.clinical_department,
});
registrationSubmitted.value = true;
ElMessage.success(TEXT.modules.auth.registerSuccess);
setTimeout(() => {
router.push("/login");
}, 3000);
// 清空表单
Object.assign(form, {
email: "",
@@ -467,7 +523,7 @@ const handleSubmit = async () => {
clinical_department: "",
agreeTerms: false,
});
currentStep.value = 1;
currentStep.value = 3;
} finally {
submitting.value = false;
}
@@ -749,6 +805,50 @@ const handleSubmit = async () => {
gap: 20px;
}
.approval-notice {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 18px 12px 10px;
text-align: center;
}
.approval-icon {
display: flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
color: #ffffff;
background: linear-gradient(135deg, rgba(52, 211, 153, 0.45), rgba(99, 102, 241, 0.28));
border-radius: 16px;
box-shadow:
inset 1px 1px 1px rgba(255, 255, 255, 0.45),
0 12px 30px rgba(52, 211, 153, 0.16);
}
.approval-title {
margin: 4px 0 0;
color: #ffffff;
font-size: 1.375rem;
font-weight: 600;
}
.approval-message,
.approval-hint {
max-width: 360px;
margin: 0;
color: rgba(255, 255, 255, 0.72);
font-size: 0.9375rem;
line-height: 1.7;
}
.approval-hint {
color: rgba(255, 255, 255, 0.54);
font-size: 0.8125rem;
}
.step-content {
display: flex;
flex-direction: column;
@@ -953,6 +1053,71 @@ const handleSubmit = async () => {
opacity: 0.8;
}
.protocol-content {
max-height: 58vh;
overflow-y: auto;
padding: 2px 8px 2px 0;
}
.protocol-section {
margin-bottom: 24px;
}
.protocol-section:last-child {
margin-bottom: 0;
}
.protocol-section h3 {
margin: 0 0 12px;
color: #2f3946;
font-size: 18px;
font-weight: 700;
line-height: 1.4;
}
.protocol-section p {
margin: 0 0 10px;
color: #5f6b7a;
font-size: 15px;
font-weight: 500;
line-height: 1.8;
}
.protocol-section p:last-child {
margin-bottom: 0;
}
.protocol-confirm-btn {
min-width: 112px;
height: 44px;
border-radius: 22px;
font-size: 16px;
font-weight: 700;
letter-spacing: 2px;
background: linear-gradient(135deg, #3f5d75, #4a6d87);
border: none;
box-shadow: 0 8px 16px -4px rgba(63, 93, 117, 0.3);
}
:global(.register-protocol-dialog .el-dialog__header) {
padding: 24px 28px 12px;
margin-right: 0;
}
:global(.register-protocol-dialog .el-dialog__title) {
color: #2f3946;
font-size: 20px;
font-weight: 800;
}
:global(.register-protocol-dialog .el-dialog__body) {
padding: 8px 28px 20px;
}
:global(.register-protocol-dialog .el-dialog__footer) {
padding: 0 28px 24px;
}
/* 汇总部分 */
.summary-section {
padding: 16px;