refactor: 合并 biz_support_unit/biz_execution_unit/biz_service_org 为 biz_org, 删除 2 张孤儿表, 改 biz_person.work_unit → org_id FK

主要改动:
- SQL: biz_support_unit → biz_org, 加 org_type, 加 business_nature; 删 biz_execution_unit, biz_service_org
- SQL: biz_project.support_unit_id/name + service_org_id/name → org_id/name/type
- SQL: biz_meeting.support_unit_name → org_name
- SQL: biz_person.work_unit → org_id (FK)
- 后端: 新 BizOrg entity/mapper/service/controller
- 后端: BizProject/BizMeeting 字段重命名
- 后端: 删 12 个 BizSupportUnit/BizExecutionUnit/BizServiceOrg Java 文件
- 后端: BizPersonImportVO.workUnit → orgName (导入时查 biz_org 取 org_id)
- 后端: BizAuthController.registerExecutor 完整实现 (原 registerSupplier stub)
- 前端: 新 admin/Orgs.vue + manager/Orgs.vue (原 SupportUnits)
- 前端: RegisterExecutor.vue (原 RegisterSupplier, 单页 2 步)
- 前端: sponsor/executor/manager 多个文件 workUnit → orgId/orgName 重命名
- 前端: 统一 supplier → executor, 业务命名 sponsor(赞助方) / executor(执行方=供应商)
- 前端: 全工程 execution → executor (company type / role / person unit_type)
This commit is contained in:
郭庆泰
2026-08-15 12:41:10 +08:00
commit cf5790229a
694 changed files with 102090 additions and 0 deletions
+689
View File
@@ -0,0 +1,689 @@
<template>
<div class="login-page">
<!-- 顶部导航 -->
<header class="header">
<a class="logo" @click.prevent="$router.push('/portal/home')">
<div class="logo-icon"></div>
<div class="logo-text">
<span class="logo-title">北京整合医学学会</span>
<span class="logo-subtitle">Beijing Association of Holistic Integrative Medicine</span>
</div>
</a>
</header>
<!-- 主体 -->
<main class="main-content">
<!-- 左侧 banner -->
<div class="banner-section">
<div class="banner-inner">
<span class="banner-tag">2025-2030</span>
<h2 class="banner-title">协同创新 共建共享<br>整合医学发展新格局</h2>
<div class="banner-divider"></div>
<p class="banner-desc">
聚焦医学整合创新系统推进七大专项计划<br>
全面构建覆盖诊疗科研人才管理公益<br>
政学协作与组织建设的协同发展体系
</p>
</div>
</div>
<!-- 右侧登录卡片 -->
<section class="login-section">
<div class="login-card">
<h1 class="login-title">账号登录</h1>
<p class="login-subtitle">请输入您的账号信息</p>
<form id="loginForm" @submit.prevent="onSubmit">
<div class="form-group">
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<input v-model="form.username" type="text" id="username" class="form-input" placeholder="用户名 / 手机号" autocomplete="username">
</div>
<div class="form-group">
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
<input v-model="form.password" type="password" id="password" class="form-input" placeholder="密码" autocomplete="current-password">
</div>
<div v-if="captchaEnabled" class="captcha-group">
<div class="form-group" style="margin-bottom: 0;">
<input v-model="form.code" type="text" id="captcha" class="form-input captcha-input" placeholder="验证码" maxlength="6">
</div>
<img v-if="captchaImg" :src="captchaImg" class="captcha-image" @click="loadCaptcha" title="点击刷新" alt="验证码">
<div v-else class="captcha-image" @click="loadCaptcha" title="点击刷新">加载中</div>
</div>
<button type="submit" class="login-btn" :disabled="loading">{{ loading ? '登录中...' : '登 录' }}</button>
<div class="form-bottom-links">
<a class="bottom-link" @click="onRegister">新用户注册</a>
<a class="bottom-link" @click="onForgot">忘记密码</a>
</div>
</form>
</div>
</section>
</main>
<!-- 底部 -->
<footer class="footer">
<p>© 北京整合医学学会 BAHIM</p>
<p>京ICP备2020035479号-1 &nbsp;&nbsp; 京公网安备11010802034820</p>
</footer>
<!-- 注册弹窗 (原型: 选择用户类别) -->
<div class="modal-overlay" :class="{ active: showRoleModal }" @click.self="closeModal">
<div class="modal">
<h2 class="modal-title">选择用户类别</h2>
<div class="user-type-list">
<label v-for="t in registerTypes" :key="t.value" class="user-type-item">
<input type="radio" name="registerUserType" :value="t.value" v-model="registerUserType">
<div class="info">
<div class="name">{{ t.name }}</div>
<div class="desc">{{ t.desc }}</div>
</div>
</label>
</div>
<div class="modal-actions">
<button class="modal-btn secondary" type="button" @click="closeModal">取消</button>
<button class="modal-btn primary" type="button" @click="confirmRegister">确定</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { login, getInfo, getCaptcha } from '@/api/auth'
import { useUserStore } from '@/store/user'
const router = useRouter()
const userStore = useUserStore()
const form = reactive({ username: '', password: '', code: '', uuid: '', role: 'leader' })
const captchaImg = ref('')
const captchaEnabled = ref(true)
const loading = ref(false)
const showRoleModal = ref(false)
const registerUserType = ref('')
const registerTypes = [
{ value: 'expert', name: '项目参与专家', desc: '负责项目方案设计与评审' },
{ value: 'executor', name: '项目执行单位(供应商)', desc: '承担项目执行,需走入库流程' },
{ value: 'sponsor', name: '项目支持单位', desc: '注册后默认为监察员角色' }
]
const userTypes = [
{ value: 'admin', name: '系统管理员', desc: '系统配置、用户管理、权限控制' },
{ value: 'leader', name: '学会领导', desc: '查看全局数据、审批重要事项' },
{ value: 'manager', name: '项目经理', desc: '项目管理、方案审核、过程监督' },
{ value: 'doctor', name: '评审专家', desc: '项目评审、评分反馈、查看任务' },
{ value: 'executor', name: '执行单位', desc: '会议组织、人员安排、劳务结算' },
{ value: 'sponsor', name: '支持单位 / 赞助方', desc: '支持函管理、人员管理、合作记录' },
]
async function loadCaptcha() {
try {
const res = await getCaptcha()
captchaEnabled.value = res?.captchaEnabled !== false
if (res?.uuid) {
form.uuid = res.uuid
captchaImg.value = 'data:image/png;base64,' + (res.img || '')
}
} catch (e) {
captchaImg.value = ''
}
}
async function onSubmit() {
if (!form.username || !form.password) {
ElMessage.warning('请输入用户名和密码')
return
}
if (captchaEnabled.value && !form.code) {
ElMessage.warning('请输入验证码')
return
}
loading.value = true
try {
const res = await login({ username: form.username, password: form.password, code: form.code || '', uuid: form.uuid || '' })
const role = autoRole(form.username)
userStore.setToken(res.token)
// /login 接口只返回 token; 调 /getInfo 拿完整 user (含 accountType/parentUserId, 让 isMain/isSub 判断正确)
try {
const info = await getInfo()
const u = info.user || {}
userStore.setUser({
userId: u.userId,
userName: u.userName || form.username,
nickName: u.nickName || form.username,
accountType: u.accountType || 'MAIN',
parentUserId: u.parentUserId || null,
role
})
} catch {
// /getInfo 失败时回退: 只存基本信息
userStore.setUser({
userId: res.userId,
userName: form.username,
nickName: form.username,
accountType: 'MAIN',
parentUserId: null,
role
})
}
ElMessage.success(`欢迎,${form.username}${userTypes.find(u => u.value === role)?.name || role}`)
router.replace(roleHome[role] || '/leader/home')
} catch (e) {
ElMessage.error(e?.msg || '登录失败')
loadCaptcha()
} finally {
loading.value = false
}
}
function autoRole(username) {
const u = (username || '').toLowerCase()
if (u === 'admin' || u === 'ry') return 'admin'
if (u.startsWith('leader')) return 'leader'
if (u.startsWith('manager')) return 'manager'
if (u.startsWith('doctor')) return 'doctor'
if (u.startsWith('executor')) return 'executor'
if (u.startsWith('sponsor')) return 'sponsor'
return 'leader'
}
const roleHome = {
admin: '/admin/workbench',
leader: '/leader/home',
manager: '/manager/workbench',
doctor: '/doctor/home',
executor: '/executor/meetings',
sponsor: '/sponsor/home',
}
function onRegister() {
showRoleModal.value = true
registerUserType.value = ''
}
function closeModal() {
showRoleModal.value = false
}
function confirmRegister() {
if (!registerUserType.value) {
ElMessage.warning('请选择用户类别')
return
}
const routeMap = {
expert: '/register-expert',
executor: '/register-executor',
sponsor: '/register-sponsor'
}
const target = routeMap[registerUserType.value]
if (!target) {
ElMessage.warning('该类别暂未开放注册')
return
}
showRoleModal.value = false
router.push(target)
}
function onForgot() {
ElMessage.info('请联系系统管理员重置密码')
}
onMounted(() => { loadCaptcha() })
</script>
<style>
/* === 原型 CSS 1:1 复制自 /home/john/ry8080/proto/html/登录.html === */
.login-page *{
margin: 0;
padding: 0;
box-sizing: border-box;
}
.login-page{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #333;
background: #f5f6f8;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* 顶部导航 */
.login-page .header{
height: 64px;
padding: 0 60px;
background: #ffffff;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
}
.login-page .logo{
display: flex;
align-items: center;
gap: 12px;
}
.login-page .logo-icon{
width: 36px;
height: 36px;
background: #1e3a8a;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 600;
}
.login-page .logo-text{
display: flex;
flex-direction: column;
line-height: 1.2;
}
.login-page .logo-title{
font-size: 16px;
font-weight: 600;
color: #1f2937;
letter-spacing: 0.5px;
}
.login-page .logo-subtitle{
font-size: 11px;
color: #6b7280;
margin-top: 2px;
letter-spacing: 0.3px;
}
/* 主体 */
.login-page .main-content{
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 60px;
gap: 60px;
}
.login-page .banner-section{
flex: 1;
max-width: 560px;
}
.login-page .banner-inner{
background: #1e3a8a;
color: #fff;
padding: 56px 56px;
height: 440px;
display: flex;
flex-direction: column;
justify-content: center;
}
.login-page .banner-tag{
display: inline-block;
padding: 4px 12px;
border: 1px solid rgba(255, 255, 255, 0.3);
font-size: 12px;
letter-spacing: 2px;
margin-bottom: 24px;
width: fit-content;
}
.login-page .banner-title{
font-size: 30px;
font-weight: 600;
line-height: 1.4;
margin-bottom: 20px;
letter-spacing: 1px;
}
.login-page .banner-divider{
width: 48px;
height: 2px;
background: #fff;
margin-bottom: 24px;
}
.login-page .banner-desc{
font-size: 14px;
line-height: 1.9;
color: rgba(255, 255, 255, 0.85);
letter-spacing: 0.5px;
}
/* 登录卡片 */
.login-page .login-section{
width: 400px;
flex-shrink: 0;
}
.login-page .login-card{
background: #ffffff;
padding: 40px 40px;
border: 1px solid #e5e7eb;
}
.login-page .login-title{
font-size: 22px;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px;
letter-spacing: 1px;
}
.login-page .login-subtitle{
font-size: 13px;
color: #6b7280;
margin-bottom: 28px;
}
.login-page .form-group{
margin-bottom: 18px;
position: relative;
}
.login-page .form-input{
width: 100%;
height: 44px;
padding: 0 14px 0 42px;
border: 1px solid #d1d5db;
background: #fff;
font-size: 14px;
color: #1f2937;
outline: none;
transition: border-color 0.2s;
}
.login-page .form-input:focus{
border-color: #1e3a8a;
}
.login-page .form-input::placeholder{
color: #9ca3af;
}
.login-page .input-icon{
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
color: #9ca3af;
pointer-events: none;
}
.login-page .captcha-group{
display: flex;
gap: 10px;
}
.login-page .captcha-group .form-group{
flex: 1;
margin-bottom: 0;
}
.login-page .captcha-input{
padding-left: 14px;
}
.login-page .captcha-input + .input-icon{
display: none;
}
.login-page .captcha-image{
width: 110px;
height: 44px;
border: 1px solid #d1d5db;
background: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 600;
letter-spacing: 4px;
color: #1e3a8a;
cursor: pointer;
user-select: none;
font-family: 'Courier New', monospace;
}
.login-page .form-bottom-links{
display: flex;
justify-content: space-between;
margin-top: 14px;
font-size: 13px;
}
.login-page .form-bottom-links .bottom-link{
color: #1e3a8a;
cursor: pointer;
}
.login-page .form-bottom-links .bottom-link:hover{
text-decoration: underline;
}
.login-page .login-btn{
width: 100%;
height: 44px;
background: #1e3a8a;
border: none;
color: #fff;
font-size: 15px;
font-weight: 500;
letter-spacing: 4px;
cursor: pointer;
margin-top: 8px;
transition: background 0.2s;
}
.login-page .login-btn:hover{
background: #1e40af;
}
/* 底部 */
.login-page .footer{
padding: 20px 60px;
border-top: 1px solid #e5e7eb;
background: #fff;
text-align: center;
color: #6b7280;
font-size: 12px;
line-height: 1.8;
}
.login-page .footer p{ margin-bottom: 2px; }
/* 弹窗 */
.login-page .modal-overlay{
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.login-page .modal-overlay.active{ display: flex; }
.login-page .modal{
background: #fff;
padding: 32px 36px;
width: 440px;
max-width: 90vw;
border: 1px solid #e5e7eb;
}
.login-page .modal-title{
font-size: 18px;
font-weight: 600;
color: #1f2937;
text-align: center;
margin-bottom: 24px;
letter-spacing: 1px;
}
.login-page .user-type-list{
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.login-page .user-type-item{
display: flex;
align-items: center;
padding: 14px 18px;
border: 1px solid #e5e7eb;
cursor: pointer;
transition: all 0.2s;
}
.login-page .user-type-item:hover{
border-color: #1e3a8a;
background: #f9fafb;
}
.login-page .user-type-item input[type="radio"]{
margin-right: 12px;
cursor: pointer;
accent-color: #1e3a8a;
}
.login-page .user-type-item .info{ flex: 1; }
.login-page .user-type-item .name{
font-size: 14px;
font-weight: 500;
color: #1f2937;
margin-bottom: 2px;
}
.login-page .user-type-item .desc{
font-size: 12px;
color: #6b7280;
}
.login-page .modal-actions{
display: flex;
gap: 12px;
}
.login-page .modal-btn{
flex: 1;
height: 40px;
font-size: 14px;
cursor: pointer;
border: none;
}
.login-page .modal-btn.primary{
background: #1e3a8a;
color: #fff;
}
.login-page .modal-btn.primary:hover{ background: #1e40af; }
.login-page .modal-btn.secondary{
background: #fff;
color: #4b5563;
border: 1px solid #d1d5db;
}
.login-page .modal-btn.secondary:hover{
border-color: #1e3a8a;
color: #1e3a8a;
}
/* 响应式 */
@media (max-width: 1024px) {
.main-content {
flex-direction: column;
padding: 30px 30px;
gap: 30px;
}
.banner-section { width: 100%; max-width: 100%; }
.banner-inner { height: auto; padding: 40px 30px; }
.login-section { width: 100%; max-width: 400px; }
}
@media (max-width: 640px) {
.header { padding: 0 20px; }
.main-content { padding: 20px; }
.login-card { padding: 30px 24px; }
.footer { padding: 16px 20px; }
}
/* === 用户要求: 左侧保持 440px 不动, 右侧登录卡片高度追平左侧, 两栏在 main-content 中居中 === */
.login-page .main-content { align-items: center; }
.login-page .login-card { height: 440px; display: flex; flex-direction: column; justify-content: center; }
/* === 注册选择类别弹窗 (原型 1:1 抄 /home/john/ry8080/proto/html/登录.html) === */
.modal-overlay {
position: fixed; inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex; align-items: center; justify-content: center;
z-index: 1000;
opacity: 0; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
}
.modal-overlay.active { opacity: 1; visibility: visible; }
.modal {
background: #fff;
padding: 32px 36px;
width: 440px;
max-width: 90vw;
border: 1px solid #e5e7eb;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
}
.modal-title { font-size: 18px; font-weight: 600; color: #1a1a1a; margin-bottom: 20px; text-align: center; }
.user-type-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 24px; }
.user-type-item {
display: flex; align-items: center; gap: 12px;
padding: 14px 18px;
border: 1px solid #e5e7eb;
cursor: pointer;
transition: all 0.2s;
}
.user-type-item:hover { border-color: var(--brand-primary, #1890ff); background: #f7faff; }
.user-type-item input[type="radio"] {
width: 18px; height: 18px;
accent-color: var(--brand-primary, #1890ff);
cursor: pointer;
flex-shrink: 0;
}
.user-type-item .info { flex: 1; }
.user-type-item .name { font-size: 15px; color: #1a1a1a; font-weight: 500; margin-bottom: 2px; }
.user-type-item .desc { font-size: 12px; color: #909399; }
.modal-actions { display: flex; gap: 12px; }
.modal-btn {
flex: 1; height: 40px;
border: none; cursor: pointer;
font-size: 14px; font-weight: 500;
transition: all 0.2s;
}
.modal-btn.secondary { background: #f5f7fa; color: #606266; }
.modal-btn.secondary:hover { background: #ebeef5; }
.modal-btn.primary { background: var(--brand-primary, #1890ff); color: #fff; }
.modal-btn.primary:hover { background: var(--brand-primary-deep, #096dd9); }
</style>