feat: 合并 admin/manager 重复页 + 公示意向 + 劳务签署
页合并 (admin/manager 共用, route.path 角色感知): - expert: Experts.vue + ExpertNew.vue (list/new/edit/view 一套) - sponsor-orgs: SponsorOrgs.vue - sponsor-people: SponsorPeople.vue + SponsorPersonNew.vue + SponsorPersonDetail.vue - executor-orgs: ExecutorOrgs.vue - executor-people: ExecutorPeople.vue + ExecutorPersonNew.vue + ExecutorPersonDetail.vue - 给 SponsorPeople / ExecutorPeople op 列补上 查看/编辑 按钮 (潜在 bug 修复) - 详情弹窗统一用 el-descriptions 风格 (替代 admin 旧版 ElMessageBox.alert) 后端: - BizSignController + BizSignServiceImpl + PdfService (劳务签署 PDF 流程) - BizPublicityIntentController (公示意向: 支持/执行) - BizPublicitySupportIntent / BizPublicityExecutionIntent ExportVo - BizMeetingAttendee 字段合并 (bank_region 替代 bankProvince/bankCity) - BizExpert 字段合并 (id_card_attachments CSV, bank_region) - Excel 导入模板精简 (开户行 1 列) 前端: - doctor/SignFill + SignContract + SignSuccess (劳务签署 3 页流程) - ImportResultDialog 通用组件 - IdCardUploader 重构 (单 v-model:idCardAttachments, CSV 格式) - AreaCascader 调整 - Login.vue + AdminLayout.vue + PortalShell.vue + Home.vue 适配 DB: 字段合并 (id_card_front/back → id_card_attachments, bank_province/city → bank_region)
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<div class="sign-fill">
|
||||
<div v-loading="loading">
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-position="top" class="sign-fill-form">
|
||||
<!-- 参会人信息 -->
|
||||
<div class="section-title">参会人信息</div>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入姓名" maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="工作单位" prop="workUnit">
|
||||
<el-input v-model="form.workUnit" placeholder="请输入工作单位" maxlength="200" />
|
||||
</el-form-item>
|
||||
<el-form-item label="科室" prop="department">
|
||||
<el-input v-model="form.department" placeholder="请输入科室" maxlength="100" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 银行账户 -->
|
||||
<div class="section-title">银行账户</div>
|
||||
<el-form-item label="账户名称(持卡人姓名)" prop="accountName">
|
||||
<el-input v-model="form.accountName" placeholder="请输入持卡人姓名" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="身份证号" prop="idCard">
|
||||
<el-input v-model="form.idCard" placeholder="请输入身份证号" maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="银行卡号" prop="bankCard">
|
||||
<el-input v-model="form.bankCard" placeholder="请输入银行卡号" maxlength="30" />
|
||||
</el-form-item>
|
||||
<el-form-item label="银行名称" prop="bankName">
|
||||
<el-input v-model="form.bankName" placeholder="请输入银行名称" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开户银行(支行)" prop="bankBranch">
|
||||
<el-input v-model="form.bankBranch" placeholder="请输入开户行名称" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开户银行地址" prop="bankRegion">
|
||||
<AreaCascader
|
||||
v-model="form.bankRegion"
|
||||
format="string"
|
||||
join-sep="/"
|
||||
:max-level="2"
|
||||
placeholder="请选择省/市"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="银行详细地址" prop="bankAddress">
|
||||
<el-input v-model="form.bankAddress" placeholder="如: 海淀支行 / XX 路 1 号" maxlength="200" />
|
||||
</el-form-item>
|
||||
<el-form-item label="身份证附件">
|
||||
<IdCardUploader
|
||||
v-model="form.idCardAttachments"
|
||||
:dir="'ry8080/idcard/'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 劳务信息 (只读, admin 预填) -->
|
||||
<div class="section-title">本次劳务</div>
|
||||
<el-form-item label="劳务形式">
|
||||
<div class="labor-form-cell">
|
||||
<el-checkbox-group v-model="laborFormSelected">
|
||||
<el-checkbox v-for="opt in laborFormOptions" :key="opt.value" :value="opt.value" :label="opt.label" />
|
||||
</el-checkbox-group>
|
||||
<div v-if="laborFormSelected.includes('__other__')" class="other-input">
|
||||
<el-input v-model="laborFormOther" placeholder="请输入其他劳务形式" maxlength="100" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="医务职称">
|
||||
<el-select v-model="form.title" placeholder="请选择" clearable filterable style="width: 240px">
|
||||
<el-option v-for="t in titleOptions" :key="t" :value="t" :label="t" />
|
||||
<template #footer>
|
||||
<el-input v-if="showTitleOther" v-model="titleOther" placeholder="其他职称" maxlength="50" @blur="form.title = titleOther" />
|
||||
<el-button v-else link type="primary" @click="showTitleOther = true">+ 其他</el-button>
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">下一步</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getSignInfo, saveSignProfile } from '@/api/business/sign'
|
||||
import IdCardUploader from '@/components/IdCardUploader.vue'
|
||||
import AreaCascader from '@/components/AreaCascader.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const attendeeId = computed(() => Number(route.query.attendeeId) || null)
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
// 劳务形式 (checkboxOther 多选)
|
||||
const laborFormOptions = ref([])
|
||||
const laborFormSelected = ref([])
|
||||
const laborFormOther = ref('')
|
||||
|
||||
// 职称 (selectOther, 含 "其他" 输入)
|
||||
const titleOptions = ref([])
|
||||
const showTitleOther = ref(false)
|
||||
const titleOther = ref('')
|
||||
|
||||
const form = reactive({
|
||||
name: '', phone: '', workUnit: '', department: '',
|
||||
accountName: '', idCard: '', bankCard: '', bankName: '', bankBranch: '',
|
||||
bankRegion: '', // AreaCascader v-model (string 格式, "省/市")
|
||||
bankAddress: '', // 银行详细地址
|
||||
idCardAttachments: '', // 身份证正反面 URL CSV (IdCardUploader)
|
||||
title: ''
|
||||
})
|
||||
const feeDisplay = reactive({ preTax: '—', tax: '—', fee: '—' })
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[0-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
|
||||
],
|
||||
workUnit: [{ required: true, message: '请输入工作单位', trigger: 'blur' }],
|
||||
department: [{ required: true, message: '请输入科室', trigger: 'blur' }],
|
||||
accountName: [{ required: true, message: '请输入持卡人姓名', trigger: 'blur' }],
|
||||
idCard: [{ required: true, message: '请输入身份证号', trigger: 'blur' }],
|
||||
bankCard: [{ required: true, message: '请输入银行卡号', trigger: 'blur' }],
|
||||
bankName: [{ required: true, message: '请输入银行名称', trigger: 'blur' }],
|
||||
bankBranch: [{ required: true, message: '请输入开户行支行', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!attendeeId.value) {
|
||||
ElMessage.error('参数缺失, 请从工作台进入')
|
||||
router.push('/doctor/home')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await getSignInfo(attendeeId.value)
|
||||
// 预填 (current 优先, 否则用 defaults)
|
||||
const cur = data.current || {}
|
||||
const def = data.defaults || {}
|
||||
Object.assign(form, {
|
||||
name: cur.name || def.name || '',
|
||||
phone: cur.phone || def.phone || '',
|
||||
workUnit: cur.workUnit || def.workUnit || '',
|
||||
department: cur.department || def.department || '',
|
||||
accountName: cur.accountName || '',
|
||||
idCard: cur.idCard || def.idCard || '',
|
||||
bankCard: cur.bankCard || def.bankCard || '',
|
||||
bankName: cur.bankName || def.bankName || '',
|
||||
bankBranch: cur.bankBranch || '',
|
||||
bankRegion: cur.bankRegion || def.bankRegion || '',
|
||||
bankAddress: cur.bankAddress || def.bankAddress || '',
|
||||
idCardAttachments: cur.idCardAttachments || '',
|
||||
title: cur.title || def.title || ''
|
||||
})
|
||||
if (cur.laborForm) {
|
||||
try {
|
||||
const parsed = JSON.parse(cur.laborForm)
|
||||
if (Array.isArray(parsed)) {
|
||||
laborFormSelected.value = parsed.filter(x => x !== '__other__' && !x.startsWith('__other__:'))
|
||||
const otherItem = parsed.find(x => x === '__other__' || x.startsWith('__other__:'))
|
||||
if (otherItem) {
|
||||
laborFormSelected.value.push('__other__')
|
||||
laborFormOther.value = otherItem.startsWith('__other__:') ? otherItem.substring('__other__:'.length) : ''
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
feeDisplay.preTax = cur.feePreTax || '—'
|
||||
feeDisplay.tax = cur.tax || '—'
|
||||
feeDisplay.fee = cur.fee || '—'
|
||||
|
||||
laborFormOptions.value = (data.laborFormOptions || []).map(o => ({ value: o.value, label: o.label }))
|
||||
titleOptions.value = data.titleOptions || []
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
if (!form.idCardAttachments || form.idCardAttachments.split(',').every(s => !s.trim())) {
|
||||
ElMessage.warning('请上传身份证正反面')
|
||||
return
|
||||
}
|
||||
// 劳务形式序列化 (JSON 数组字符串, 含 __other__ + 自定义内容)
|
||||
let laborForm = ''
|
||||
if (laborFormSelected.value.length) {
|
||||
const arr = laborFormSelected.value.map(x => {
|
||||
if (x === '__other__' && laborFormOther.value) {
|
||||
return '__other__:' + laborFormOther.value
|
||||
}
|
||||
return x
|
||||
})
|
||||
laborForm = JSON.stringify(arr)
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
await saveSignProfile(attendeeId.value, {
|
||||
...form,
|
||||
laborForm
|
||||
})
|
||||
ElMessage.success('已保存, 即将进入签署页')
|
||||
setTimeout(() => {
|
||||
router.push('/doctor/sign-contract?attendeeId=' + attendeeId.value)
|
||||
}, 500)
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '保存失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/doctor/home')
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 手机端全屏表单 (无 navbar / 无 page-card) */
|
||||
.sign-fill { padding: 16px; }
|
||||
|
||||
/* 章节标题: 左边 3px 蓝竖线 */
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin: 16px 0 12px;
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid var(--brand-primary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 开户地址 3 列布局 */
|
||||
.region-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.region-row .el-input { flex: 1; }
|
||||
|
||||
/* 劳务形式多选 + 其他输入 */
|
||||
.labor-form-cell { width: 100%; }
|
||||
.labor-form-cell .other-input { margin-top: 8px; }
|
||||
|
||||
/* mobile 上下结构: label 在 input 上方 */
|
||||
.sign-fill-form :deep(.el-form-item__label) {
|
||||
display: block;
|
||||
float: none;
|
||||
text-align: left;
|
||||
width: auto !important;
|
||||
padding: 0 0 6px 0;
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
}
|
||||
.sign-fill-form :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
.sign-fill-form :deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
/* 身份证附件 2 个框排成 1 行 */
|
||||
.sign-fill-form :deep(.ht-image-upload-content) {
|
||||
display: inline-block;
|
||||
}
|
||||
.sign-fill-form :deep(.ht-image-upload-content + .ht-image-upload-content) {
|
||||
margin-left: 12px;
|
||||
}
|
||||
/* mobile 端地址 3 个输入框 1 行 */
|
||||
.region-row { display: flex; gap: 8px; }
|
||||
.region-row .el-input { flex: 1; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user