邀请发送 (菜单暂隐藏, 待确认参数见 发送邀请-待确认参数.md): - 后端: BizInvite / BizInviteRecipient + Controller/Service/Mapper/XML - 邮件: InviteMailSender (spring-boot-starter-mail SMTP) + application*.yml 邮件配置 - 上传进度: UploadProgressRegistry + UploadProgressController - 前端: InviteList / InviteNew / InviteDetail / InviteView + api/business/invite.js - 原型: proto/html/components/invite-detail / new-invitation / send-invitation 登录/账号: - 首次设密: /getInfo 返回 isPasswordEmpty, SysProfileController 密码为空时跳过旧密码校验, ForcePasswordDialog 强制弹窗 - 姓名单一可信源 resolveDisplayName: doctor→biz_expert.name, sponsor/executor→biz_person.name, 其余回退 nick_name - OA compliance 门禁改为按手机号查 ecology 视图 (不再限定 manager/leader) 其它: - OSS zip 在线查看 (列清单+取单文件, 公开只读) + SecurityConfig permitAll - doctor 项目详情 ProjectDetail.vue - 数据库/测试/设计文档 (md) 入库
316 lines
11 KiB
Vue
316 lines
11 KiB
Vue
<template>
|
|
<PortalShell>
|
|
<div class="page-wrap">
|
|
<div class="page-card">
|
|
|
|
<h2 class="page-title">项目支持单位注册</h2>
|
|
|
|
<el-steps :active="step" finish-status="success" simple class="steps">
|
|
<el-step title="基本信息" />
|
|
<el-step title="注册成功" />
|
|
</el-steps>
|
|
|
|
<div class="step-body">
|
|
<!-- ============ Step 0: 基本信息 (字段与 register-executor 完全一致, 仅 orgType 不同) ============ -->
|
|
<template v-if="step === 0">
|
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="130px" label-position="right">
|
|
<el-form-item label="用户名" prop="username">
|
|
<el-input v-model="form.username" placeholder="请输入登录用户名(4-20位字母数字)" maxlength="20" autocomplete="off" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="企业名称" prop="orgId">
|
|
<el-select
|
|
v-model="form.orgId"
|
|
filterable
|
|
remote
|
|
:remote-method="searchOrg"
|
|
:loading="orgLoading"
|
|
placeholder="输入企业名称搜索并选择"
|
|
style="width:100%"
|
|
>
|
|
<el-option
|
|
v-for="o in orgOptions"
|
|
:key="o.orgId"
|
|
:label="o.orgName"
|
|
:value="o.orgId"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="联系人姓名" prop="realName">
|
|
<el-input v-model="form.realName" placeholder="请输入联系人姓名" maxlength="30" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="手机号码" prop="phone">
|
|
<el-input v-model="form.phone" placeholder="请输入手机号码" maxlength="11" autocomplete="off" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="短信验证码" prop="smsCode">
|
|
<div class="sms-row">
|
|
<el-input v-model="form.smsCode" placeholder="请输入收到的验证码" maxlength="6" autocomplete="one-time-code" />
|
|
<el-button class="sms-btn" :disabled="smsLocked || smsCountdown > 0 || !form.phone" @click="sendSms">
|
|
{{ smsLocked ? '发送中...' : smsCountdown > 0 ? `${smsCountdown}s 后重试` : '获取验证码' }}
|
|
</el-button>
|
|
</div>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="登录密码" prop="password">
|
|
<el-input v-model="form.password" type="password" show-password placeholder="请输入登录密码(6-20位)" autocomplete="new-password" />
|
|
</el-form-item>
|
|
|
|
<el-form-item label="确认密码" prop="confirmPassword">
|
|
<el-input v-model="form.confirmPassword" type="password" show-password placeholder="请再次输入登录密码" autocomplete="new-password" />
|
|
</el-form-item>
|
|
|
|
<el-form-item>
|
|
<el-checkbox v-model="agreed">我已阅读并同意
|
|
<a href="#/article/agreement" target="_blank">《用户协议》</a>
|
|
和
|
|
<a href="#/article/privacy" target="_blank">《隐私政策》</a>
|
|
</el-checkbox>
|
|
</el-form-item>
|
|
|
|
<el-form-item>
|
|
<el-button type="primary" :disabled="!agreed" :loading="submitting" @click="onSubmit" style="width:100%;">
|
|
同意协议并注册
|
|
</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
</template>
|
|
|
|
<!-- ============ Step 1: 注册成功 ============ -->
|
|
<template v-else>
|
|
<el-result icon="success" title="注册成功">
|
|
<template #extra>
|
|
<el-button type="primary" @click="$router.push('/login')">前往登录</el-button>
|
|
</template>
|
|
</el-result>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="login-link">
|
|
已有账号? <a href="javascript:void(0)" @click="$router.push('/login')">立即登录</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</PortalShell>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { reactive, ref, onUnmounted, onMounted } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import request from '@/utils/request'
|
|
import { registerSponsor, sponsorOrgOptions } from '@/api/public'
|
|
import PortalShell from '@/components/PortalShell.vue'
|
|
import { useAsyncLock } from '@/utils/useAsyncLock'
|
|
|
|
const { locked: smsLocked, run: sendSmsOnce } = useAsyncLock()
|
|
|
|
const step = ref(0)
|
|
const formRef = ref()
|
|
const submitting = ref(false)
|
|
const agreed = ref(false)
|
|
|
|
const form = reactive({
|
|
username: '',
|
|
orgId: null,
|
|
realName: '',
|
|
phone: '',
|
|
smsCode: '',
|
|
password: '',
|
|
confirmPassword: ''
|
|
})
|
|
|
|
const rules = {
|
|
username: [
|
|
{ required: true, message: '请输入登录用户名', trigger: 'blur' },
|
|
{ min: 4, max: 20, message: '用户名长度 4-20 位', trigger: 'blur' },
|
|
{ pattern: /^[A-Za-z0-9_]+$/, message: '只能包含字母/数字/下划线', trigger: 'blur' }
|
|
],
|
|
orgId: [{ required: true, message: '请选择企业', trigger: 'change' }],
|
|
realName: [{ required: true, message: '请输入联系人姓名', trigger: 'blur' }],
|
|
phone: [
|
|
{ required: true, message: '请输入手机号码', trigger: 'blur' },
|
|
{ pattern: /^1\d{10}$/, message: '手机号格式错误', trigger: 'blur' }
|
|
],
|
|
smsCode: [{ required: true, message: '请输入短信验证码', trigger: 'blur' }],
|
|
password: [
|
|
{ required: true, message: '请输入登录密码', trigger: 'blur' },
|
|
{ min: 6, max: 20, message: '密码长度 6-20 位', trigger: 'blur' }
|
|
],
|
|
confirmPassword: [
|
|
{ required: true, message: '请再次输入登录密码', trigger: 'blur' },
|
|
{
|
|
validator: (rule, value, cb) => {
|
|
if (value !== form.password) cb(new Error('两次密码输入不一致'))
|
|
else cb()
|
|
},
|
|
trigger: 'blur'
|
|
}
|
|
]
|
|
}
|
|
|
|
// ===== 企业远程搜索 (biz_org sponsor 类型) =====
|
|
const orgOptions = ref([])
|
|
const orgLoading = ref(false)
|
|
function searchOrg(query) {
|
|
orgLoading.value = true
|
|
sponsorOrgOptions({ orgName: query || '' })
|
|
.then(res => { orgOptions.value = res.data || [] })
|
|
.catch(() => { orgOptions.value = [] })
|
|
.finally(() => { orgLoading.value = false })
|
|
}
|
|
onMounted(() => searchOrg(''))
|
|
|
|
// ===== 短信验证码 =====
|
|
const smsCountdown = ref(0)
|
|
let smsTimer = null
|
|
const smsUuid = ref('')
|
|
async function sendSms() {
|
|
if (!/^1\d{10}$/.test(form.phone)) {
|
|
return ElMessage.warning('请先输入正确的手机号')
|
|
}
|
|
await sendSmsOnce(async () => {
|
|
try {
|
|
const resp = await request({
|
|
url: '/business/auth/registerSendSms',
|
|
method: 'post',
|
|
data: { phone: form.phone }
|
|
})
|
|
smsUuid.value = resp?.data || ''
|
|
ElMessage.success('验证码已发送')
|
|
smsCountdown.value = 60
|
|
smsTimer = setInterval(() => {
|
|
smsCountdown.value--
|
|
if (smsCountdown.value <= 0) {
|
|
clearInterval(smsTimer)
|
|
smsTimer = null
|
|
}
|
|
}, 1000)
|
|
} catch (e) {
|
|
ElMessage.warning(e?.msg || '验证码发送失败')
|
|
}
|
|
})
|
|
}
|
|
|
|
// ===== 提交 =====
|
|
async function onSubmit() {
|
|
try {
|
|
await formRef.value.validate()
|
|
} catch {
|
|
return
|
|
}
|
|
submitting.value = true
|
|
try {
|
|
await registerSponsor({ ...form, uuid: smsUuid.value })
|
|
ElMessage.success('注册成功')
|
|
step.value = 1
|
|
} catch (e) {
|
|
// 错误提示已由 utils/request.js 拦截器统一弹 (ElMessage.error), 这里只 log
|
|
console.warn('registerSponsor failed', e?.message || e)
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
onUnmounted(() => { if (smsTimer) clearInterval(smsTimer) })
|
|
</script>
|
|
|
|
<style scoped>
|
|
.page-card { max-width: 1200px; margin: 0 auto; }
|
|
.page-title { text-align: center; font-size: 24px; font-weight: 600; color: #303133; margin: 24px 0 0; border-left: none; padding-left: 0; }
|
|
.steps { max-width: 720px; margin: 24px auto 0; }
|
|
.step-body { max-width: 640px; margin: 32px auto 0; }
|
|
.sms-row { display: flex; gap: 12px; width: 100%; }
|
|
.sms-row .el-input { flex: 1; }
|
|
.sms-btn { flex-shrink: 0; }
|
|
.agreement-content { line-height: 1.8; color: #606266; }
|
|
.agreement-content p { margin-bottom: 12px; }
|
|
.login-link { text-align: center; margin-top: 16px; font-size: 14px; color: #606266; }
|
|
.login-link a { color: var(--brand-primary); text-decoration: none; margin-left: 4px; }
|
|
.login-link a:hover { text-decoration: underline; }
|
|
|
|
|
|
/* ========================================
|
|
移动端适配 (≤768px)
|
|
- 卡片 padding 收窄
|
|
- 工具栏横向滚动
|
|
- filter-form: label 与输入框横向
|
|
- 表格字号收紧
|
|
======================================== */
|
|
@media (max-width: 768px) {
|
|
/* 卡片 padding */
|
|
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
|
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
|
|
|
/* 工具栏: 横向滚动 */
|
|
.toolbar {
|
|
flex-wrap: nowrap !important;
|
|
overflow-x: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
padding-bottom: 6px;
|
|
margin-bottom: 8px !important;
|
|
scrollbar-width: thin;
|
|
}
|
|
.toolbar::-webkit-scrollbar { height: 4px; }
|
|
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
|
.toolbar :deep(.action-btn),
|
|
.toolbar :deep(.el-button) {
|
|
flex-shrink: 0;
|
|
font-size: 12px !important;
|
|
padding: 0 10px !important;
|
|
height: 30px !important;
|
|
}
|
|
|
|
/* filter-form: 横向 (label 左 + input 右) */
|
|
.filter-form {
|
|
display: flex !important;
|
|
flex-direction: column !important;
|
|
align-items: stretch !important;
|
|
gap: 10px !important;
|
|
}
|
|
.filter-form :deep(.el-form-item) {
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
margin-right: 0 !important;
|
|
margin-bottom: 0 !important;
|
|
}
|
|
.filter-form :deep(.el-form-item__label) {
|
|
float: none !important;
|
|
width: auto !important;
|
|
min-width: 80px !important;
|
|
text-align: right !important;
|
|
padding: 0 8px 0 0 !important;
|
|
font-size: 13px !important;
|
|
color: var(--el-text-color-regular) !important;
|
|
line-height: 32px !important;
|
|
height: 32px !important;
|
|
}
|
|
.filter-form :deep(.el-form-item__content) {
|
|
margin-left: 0 !important;
|
|
line-height: 32px !important;
|
|
flex: 1 !important;
|
|
min-width: 0 !important;
|
|
}
|
|
|
|
/* 强制所有控件全宽 */
|
|
.filter-form :deep(.el-select),
|
|
.filter-form :deep(.el-input),
|
|
.filter-form :deep(.el-date-editor),
|
|
.filter-form :deep(.el-button),
|
|
.filter-form :deep(.el-cascader) {
|
|
width: 100% !important;
|
|
min-width: 0 !important;
|
|
margin-left: 0 !important;
|
|
margin-right: 0 !important;
|
|
display: block !important;
|
|
}
|
|
.filter-form :deep(.el-button + .el-button) {
|
|
margin-top: 8px !important;
|
|
}
|
|
|
|
/* 表格字号收紧 */
|
|
:deep(.el-table) { font-size: 12px !important; }
|
|
}
|
|
</style>
|