业务背景 - ry-vue3 为政务/卫健委风合规管理系统 (医疗机构合规系统), 不要可爱配色、不要插画 - 品牌色: 主色 #15803D (低调深绿) + 次色 #0E7490 (青蓝), 不要蓝色系主色 变更概要 1. 全局地基 - theme.scss: Element Plus 主色改为 #15803D, slate 9 档, 联动组件库 - main.scss: 字体/页面骨架/筛选栏/表格/控件/滚动条 全套样式 - AdminLayout.vue: 政务顶栏 + 浅色侧栏 - Workbench.vue: 替换原绿底大字卡为 KPI 白底色条 + 快捷入口 + 进度条 2. 表格操作列改造 - el-button link 全面替换为 el-link (:underline="false" + type="primary/danger") - 36 文件 132 处替换, 操作列外加 .table-actions 容器 (内含分隔线) - 修复 el-link 不支持的属性 (size="small"/text/loading) 3. 筛选栏 - 弹性布局 + gap 14px 16px + align-items: flex-start + min-height 32px - 删除所有 scoped .filter-form 覆盖 (28 处) 让全局样式生效 - 控件宽度 180px 统一, 标签 32px 居中, 折行不紧贴 4. 表格紧凑化 - cell-padding 10px → 6px, 表头高度 36px - 行高 ~46px → ~36px, 字号 13px 5. 每页默认 20 条 - 21 文件 pageSize: 10 → 20 6. meetings 操作列折叠 - 11 操作 → 外置 3 个主操作 + 1 个"更多" el-dropdown (8 个次操作) - width 500px → 260px - 配 .op-dropdown / .op-caret 样式 (hover 旋转) - 新增 onMoreAction dispatch 函数 (复用原 handler) 7. 操作列文字防拆行 - .table-actions flex-wrap: nowrap + white-space: nowrap - el-link padding 0 8px → 0 6px, flex-shrink: 0 8. 环境配置 - .env.development: 直连远端 https://risingdoctor.com/hg-api (绕过 vite proxy 卡顿) - vite.config.js: proxy fallback 保留
387 lines
14 KiB
Vue
387 lines
14 KiB
Vue
<template>
|
||
<div class="sign-fill">
|
||
<div v-if="notInvited" class="not-invited">
|
||
<div class="not-invited-text">您暂未被邀请,请联系执会人员</div>
|
||
<el-button type="primary" @click="goBack">返回</el-button>
|
||
</div>
|
||
<div v-else v-loading="loading">
|
||
<el-form :model="form" :rules="rules" ref="formRef" label-position="top" class="sign-fill-form">
|
||
<!-- 大标题: 会议名称 + 期数 -->
|
||
<div class="sign-header">
|
||
<div class="sign-header-title">{{ meetingName || '劳务协议签署' }}</div>
|
||
<div class="sign-header-period">期数:{{ periodDisplay || '-' }}</div>
|
||
</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>
|
||
<el-form-item label="医务职称">
|
||
<el-select v-model="form.title" placeholder="请选择" clearable filterable style="width: 100%">
|
||
<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-link :underline="false" v-else type="primary" @click="showTitleOther = true">+ 其他</el-link>
|
||
</template>
|
||
</el-select>
|
||
</el-form-item>
|
||
<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>
|
||
|
||
<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>
|
||
<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, resolveSign } from '@/api/business/sign'
|
||
import { getInfo } from '@/api/auth'
|
||
import { useUserStore } from '@/store/user'
|
||
import IdCardUploader from '@/components/IdCardUploader.vue'
|
||
import AreaCascader from '@/components/AreaCascader.vue'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const userStore = useUserStore()
|
||
const attendeeId = ref(null)
|
||
const meetingId = computed(() => route.query.meetingId ? Number(route.query.meetingId) : null)
|
||
const notInvited = ref(false)
|
||
|
||
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: '—' })
|
||
|
||
// 会议大标题 (会议名称 + 期数), 由后端 getSignInfo 返回
|
||
const meetingName = ref('')
|
||
const periodNo = ref(null)
|
||
const totalPeriods = ref(null)
|
||
const periodDisplay = computed(() => {
|
||
const p = periodNo.value
|
||
const t = totalPeriods.value
|
||
if (p == null && t == null) return ''
|
||
if (p == null) return `${t}`
|
||
if (t == null) return `${p}`
|
||
return `${p}/${t}`
|
||
})
|
||
|
||
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' }]
|
||
}
|
||
|
||
/** 扫码带 token 直登 / 无 token 校验登录; 返回 true 表示已就绪 */
|
||
async function ensureLogin() {
|
||
const token = route.query.token
|
||
if (token) {
|
||
// 扫码带 token: 用新 token 重新登录 (覆盖本地已登录态)
|
||
userStore.setToken(token)
|
||
try {
|
||
const info = await getInfo()
|
||
const u = info.user || {}
|
||
const role = u.roleType
|
||
if (!role) {
|
||
ElMessage.error('账号角色未配置, 请联系管理员')
|
||
router.replace({ name: 'login' })
|
||
return false
|
||
}
|
||
userStore.setUser({
|
||
userId: u.userId,
|
||
userName: u.userName || u.nickName || '',
|
||
nickName: u.nickName || u.userName || '',
|
||
phonenumber: u.phonenumber || '',
|
||
accountType: u.accountType || 'MAIN',
|
||
parentUserId: u.parentUserId || null,
|
||
role
|
||
})
|
||
return true
|
||
} catch (e) {
|
||
ElMessage.error('登录已失效, 请重新登录')
|
||
router.replace({ name: 'login' })
|
||
return false
|
||
}
|
||
}
|
||
// 无 token: 必须已登录
|
||
if (userStore.user) return true
|
||
ElMessage.error('请先登录')
|
||
router.replace({ name: 'login', query: { redirect: route.fullPath } })
|
||
return false
|
||
}
|
||
|
||
async function init() {
|
||
loading.value = true
|
||
const ok = await ensureLogin()
|
||
if (!ok) return
|
||
// 解析 attendeeId: 优先 URL 里的 attendeeId; 没有则按 meetingId 查
|
||
let aid = route.query.attendeeId ? Number(route.query.attendeeId) : null
|
||
if (!aid && meetingId.value) {
|
||
try {
|
||
const { data } = await resolveSign(meetingId.value)
|
||
if (!data || !data.attendeeId) {
|
||
notInvited.value = true
|
||
loading.value = false
|
||
return
|
||
}
|
||
aid = data.attendeeId
|
||
// 标题: 有 meetingId → 按 meetingId 查 (resolve 已返回会议名/期数)
|
||
meetingName.value = data.meetingName || ''
|
||
periodNo.value = data.periodNo ?? null
|
||
totalPeriods.value = data.totalPeriods ?? null
|
||
} catch (e) {
|
||
ElMessage.error(e?.msg || '加载失败')
|
||
loading.value = false
|
||
return
|
||
}
|
||
}
|
||
attendeeId.value = aid
|
||
await load()
|
||
}
|
||
|
||
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 || ''
|
||
})
|
||
// 先设置选项 (项目角色 + "其他"), 解析劳务形式需要用它区分"项目角色"和"其他自定义"
|
||
laborFormOptions.value = (data.laborFormOptions || []).map(o => ({ value: o.value, label: o.label }))
|
||
titleOptions.value = data.titleOptions || []
|
||
|
||
// 劳务形式回显: 兼容旧 JSON 数组 (["主席","__other__:xx"]) + 新逗号分隔 ("主席,主持,xx")
|
||
laborFormSelected.value = []
|
||
laborFormOther.value = ''
|
||
if (cur.laborForm) {
|
||
let items = []
|
||
if (typeof cur.laborForm === 'string' && cur.laborForm.trim().startsWith('[')) {
|
||
try { const p = JSON.parse(cur.laborForm); if (Array.isArray(p)) items = p } catch (e) { items = [cur.laborForm] }
|
||
} else {
|
||
items = String(cur.laborForm).split(',').map(s => s.trim()).filter(Boolean)
|
||
}
|
||
const known = new Set(laborFormOptions.value.map(o => o.value))
|
||
items.forEach(it => {
|
||
const s = String(it)
|
||
if (s === '__other__' || s.startsWith('__other__:')) {
|
||
if (!laborFormSelected.value.includes('__other__')) laborFormSelected.value.push('__other__')
|
||
if (s.startsWith('__other__:')) laborFormOther.value = s.substring('__other__:'.length)
|
||
} else if (known.has(s)) {
|
||
if (!laborFormSelected.value.includes(s)) laborFormSelected.value.push(s)
|
||
} else {
|
||
// 不在项目角色里 → 当作"其他"自定义
|
||
if (!laborFormSelected.value.includes('__other__')) laborFormSelected.value.push('__other__')
|
||
laborFormOther.value = s
|
||
}
|
||
})
|
||
}
|
||
|
||
feeDisplay.preTax = cur.feePreTax || '—'
|
||
feeDisplay.tax = cur.tax || '—'
|
||
feeDisplay.fee = cur.fee || '—'
|
||
|
||
meetingName.value = data.meetingName || ''
|
||
periodNo.value = data.periodNo ?? null
|
||
totalPeriods.value = data.totalPeriods ?? null
|
||
} 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
|
||
}
|
||
// 劳务形式序列化: 逗号分隔字符串, "其他"自定义内容作为普通项
|
||
let laborForm = ''
|
||
if (laborFormSelected.value.length) {
|
||
laborForm = laborFormSelected.value
|
||
.map(x => (x === '__other__' ? (laborFormOther.value || '').trim() : x))
|
||
.filter(x => x !== '' && x !== '__other__')
|
||
.join(',')
|
||
}
|
||
|
||
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(init)
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 手机端全屏表单 (无 navbar / 无 page-card) */
|
||
.sign-fill { padding: 16px; }
|
||
|
||
/* 大标题: 会议名称 + 期数 */
|
||
.sign-header { margin: 4px 0 20px; padding-bottom: 14px; border-bottom: 1px solid #f0f0f0; }
|
||
.sign-header-title { font-size: 20px; font-weight: 600; color: #1a1a1a; line-height: 1.4; }
|
||
.sign-header-period { margin-top: 6px; font-size: 14px; color: #595959; }
|
||
|
||
/* 未受邀提示 */
|
||
.not-invited { padding: 60px 20px; text-align: center; }
|
||
.not-invited-text { font-size: 16px; color: #606266; margin-bottom: 20px; }
|
||
|
||
/* 开户地址 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> |