登录注册:优化协议弹窗与PM后台落点
授权项目负责人登录后,若当前项目具备项目成员、中心管理或审计导出读取权限,优先进入对应管理后台页面,避免仍停留在我的工作台。 抽取登录协议为共享内容模块,注册页新增服务条款和隐私政策弹窗,并在注册成功后展示待管理员审核提示。 补充路由、登录页和注册页静态回归测试,覆盖 PM 管理后台默认落点和协议弹窗行为。
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user