feat(meeting): 会议详情页参会人 CRUD + BizProjectPlan audit 字段
- 后端: BizMeetingAttendee 增 list/add/update/delete 4 端点 按手机号定位 sys_user (无则新建, 用户名=密码=phone, role_type='doctor') Mapper: insertWithProfile (一次写完整档案) + deleteByPrimaryKey Service: insertByPhoneWithProfile (重复参会人抛友好异常) - #4 配套: BizProjectPlan.auditOpinion/auditBy/auditTime + 关联项目名 BizProjectPlan.projectName (LEFT JOIN biz_project) 用于 #4 通知 content - 前端 MeetingDetail.vue: 劳务材料 tab 内, 劳务明细表 file-row 下方 直接出表 (无表题), 含姓名/手机/单位/科室/职称/劳务费/签字状态/操作 新增/编辑 dialog 共用, 删除走 ElMessageBox.confirm 二次确认 - 触发 #5: 新参会人 POST 时也发会议邀请通知 (与 BizMeetingController.add 一致)
This commit is contained in:
@@ -70,6 +70,46 @@
|
||||
<div v-if="r.hint" class="file-hint">{{ r.hint }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 参会人管理 (按用户要求, 放在"劳务明细表"那行下面, 无表题直接出表) -->
|
||||
<div class="attendee-section">
|
||||
<div class="attendee-toolbar">
|
||||
<el-button type="primary" size="small" :loading="attendeeLoading" @click="openAttendeeDialog()">+ 新增参会人</el-button>
|
||||
<span class="toolbar-hint">按手机号定位; 手机号未注册将自动建 sys_user (用户名=密码=手机号)</span>
|
||||
</div>
|
||||
<el-table :data="attendeeRows" v-loading="attendeeLoading" border size="small" style="width:100%">
|
||||
<el-table-column prop="name" label="姓名" min-width="100" />
|
||||
<el-table-column prop="phone" label="手机号" width="120" />
|
||||
<el-table-column prop="workUnit" label="工作单位" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="department" label="科室" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="title" label="职称" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="laborForm" label="劳务形式" min-width="80" show-overflow-tooltip />
|
||||
<el-table-column label="税前劳务费" width="110" align="right">
|
||||
<template #default="{ row }">{{ row.feePreTax != null ? '¥ ' + Number(row.feePreTax).toFixed(2) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税金" width="80" align="right">
|
||||
<template #default="{ row }">{{ row.tax != null ? '¥ ' + Number(row.tax).toFixed(2) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实发劳务费" width="110" align="right">
|
||||
<template #default="{ row }">{{ row.fee != null ? '¥ ' + Number(row.fee).toFixed(2) : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="签字" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.handsign" type="success" size="small">已签</el-tag>
|
||||
<el-tag v-else type="info" size="small">未签</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openAttendeeDialog(row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="confirmDeleteAttendee(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<span class="text-muted">暂无参会人, 点击右上"新增参会人"按手机号添加</span>
|
||||
</template>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="劳务凭证" name="laborVoucher">
|
||||
<div class="file-list">
|
||||
@@ -263,6 +303,44 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 参会人 dialog (新增/编辑通用) -->
|
||||
<el-dialog v-model="attendeeDialog.show" :title="attendeeDialog.title" width="520px" @closed="resetAttendeeForm">
|
||||
<el-form :model="attendeeDialog.form" label-width="100px">
|
||||
<el-form-item label="手机号" required>
|
||||
<el-input v-model="attendeeDialog.form.phone" :disabled="attendeeDialog.editing" placeholder="11位手机号" maxlength="11" />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="attendeeDialog.form.name" placeholder="未注册手机号将以此为昵称新建账号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="工作单位">
|
||||
<el-input v-model="attendeeDialog.form.workUnit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="科室">
|
||||
<el-input v-model="attendeeDialog.form.department" />
|
||||
</el-form-item>
|
||||
<el-form-item label="职称">
|
||||
<el-input v-model="attendeeDialog.form.title" />
|
||||
</el-form-item>
|
||||
<el-form-item label="劳务形式">
|
||||
<el-input v-model="attendeeDialog.form.laborForm" placeholder="如: 授课/主持/评审" />
|
||||
</el-form-item>
|
||||
<el-form-item label="税前劳务费">
|
||||
<el-input-number v-model="attendeeDialog.form.feePreTax" :precision="2" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="税金">
|
||||
<el-input-number v-model="attendeeDialog.form.tax" :precision="2" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="实发劳务费">
|
||||
<el-input-number v-model="attendeeDialog.form.fee" :precision="2" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
<div v-if="attendeeDialog.editing" class="form-hint">编辑时不能改手机号; 若需改手机号, 请删除后重新添加</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="attendeeDialog.show = false">取消</el-button>
|
||||
<el-button type="primary" :loading="attendeeDialog.saving" @click="confirmAttendee">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<div class="form-actions">
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
</div>
|
||||
@@ -278,6 +356,7 @@ import { listSupporters, listExecutor } from '@/api/system'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import OssFileUploader from '@/components/OssFileUploader.vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -442,6 +521,131 @@ async function loadMaterials() {
|
||||
})
|
||||
} catch (e) { console.error('[meeting-detail] loadMaterials failed', e) }
|
||||
}
|
||||
|
||||
// ===================== 参会人 CRUD =====================
|
||||
/** 会议下的参会人列表 (放在 劳务材料 tab 表格) */
|
||||
const attendeeRows = ref([])
|
||||
const attendeeLoading = ref(false)
|
||||
|
||||
/** 参会人 dialog 状态 (新增/编辑共用) */
|
||||
const attendeeDialog = ref({
|
||||
show: false,
|
||||
title: '',
|
||||
editing: false, // true=编辑 / false=新增
|
||||
form: emptyAttendeeForm(),
|
||||
saving: false
|
||||
})
|
||||
function emptyAttendeeForm() {
|
||||
return {
|
||||
id: null,
|
||||
meetingId: null,
|
||||
phone: '',
|
||||
name: '',
|
||||
workUnit: '',
|
||||
department: '',
|
||||
title: '',
|
||||
laborForm: '',
|
||||
feePreTax: 0,
|
||||
tax: 0,
|
||||
fee: 0
|
||||
}
|
||||
}
|
||||
function resetAttendeeForm() {
|
||||
attendeeDialog.value = {
|
||||
show: false, title: '', editing: false,
|
||||
form: emptyAttendeeForm(), saving: false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAttendees() {
|
||||
attendeeLoading.value = true
|
||||
try {
|
||||
const resp = await request.get(`/business/meetingAttendee/list/${meetingId.value}`)
|
||||
const list = (resp && (resp.data || resp)) || []
|
||||
attendeeRows.value = Array.isArray(list) ? list : []
|
||||
} catch (e) {
|
||||
console.error('[meeting-detail] loadAttendees failed', e)
|
||||
attendeeRows.value = []
|
||||
} finally {
|
||||
attendeeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开新增/编辑 dialog; row=null → 新增 */
|
||||
function openAttendeeDialog(row) {
|
||||
if (row) {
|
||||
attendeeDialog.value = {
|
||||
show: true,
|
||||
title: '编辑参会人',
|
||||
editing: true,
|
||||
form: {
|
||||
id: row.id,
|
||||
meetingId: row.meetingId,
|
||||
phone: row.phone || '',
|
||||
name: row.name || '',
|
||||
workUnit: row.workUnit || '',
|
||||
department: row.department || '',
|
||||
title: row.title || '',
|
||||
laborForm: row.laborForm || '',
|
||||
feePreTax: row.feePreTax ?? 0,
|
||||
tax: row.tax ?? 0,
|
||||
fee: row.fee ?? 0
|
||||
},
|
||||
saving: false
|
||||
}
|
||||
} else {
|
||||
attendeeDialog.value = {
|
||||
show: true,
|
||||
title: '新增参会人',
|
||||
editing: false,
|
||||
form: { ...emptyAttendeeForm(), meetingId: Number(meetingId.value) },
|
||||
saving: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交 dialog (新增 → POST; 编辑 → PUT) */
|
||||
async function confirmAttendee() {
|
||||
const { editing, form } = attendeeDialog.value
|
||||
if (!form.phone || !form.phone.trim()) {
|
||||
ElMessage.warning('请输入手机号')
|
||||
return
|
||||
}
|
||||
if (!form.phone.trim().match(/^1[3-9]\d{9}$/)) {
|
||||
ElMessage.warning('手机号格式不正确')
|
||||
return
|
||||
}
|
||||
attendeeDialog.value.saving = true
|
||||
try {
|
||||
if (editing) {
|
||||
await request.put('/business/meetingAttendee', form, { __silentError: true })
|
||||
ElMessage.success('已保存')
|
||||
} else {
|
||||
await request.post('/business/meetingAttendee', form, { __silentError: true })
|
||||
ElMessage.success('已添加, 已向参会人发送会议邀请')
|
||||
}
|
||||
attendeeDialog.value.show = false
|
||||
await loadAttendees()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || e?.message || (editing ? '保存失败' : '添加失败'))
|
||||
} finally {
|
||||
attendeeDialog.value.saving = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除参会人 (单条确认) */
|
||||
async function confirmDeleteAttendee(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除参会人「${row.name || row.phone || row.id}」?该操作仅删除参会关系, 不会删除其 sys_user 账号`, '删除确认', { type: 'warning' })
|
||||
} catch { return /* 用户取消 */ }
|
||||
try {
|
||||
await request.delete(`/business/meetingAttendee/${row.id}`, { __silentError: true })
|
||||
ElMessage.success('已删除')
|
||||
await loadAttendees()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || e?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
async function loadStaff() {
|
||||
try {
|
||||
const [sp, ex] = await Promise.all([
|
||||
@@ -465,7 +669,7 @@ async function load() {
|
||||
try {
|
||||
const resp = await bizGet('meeting', meetingId.value)
|
||||
row.value = (resp && (resp.data || resp)) || {}
|
||||
await Promise.all([loadMaterials(), loadStaff(), loadTrail()])
|
||||
await Promise.all([loadMaterials(), loadStaff(), loadTrail(), loadAttendees()])
|
||||
} catch (e) {
|
||||
console.error('[meeting-detail] load failed', e)
|
||||
row.value = {}
|
||||
@@ -757,4 +961,10 @@ onMounted(load)
|
||||
.audit-column .section-title { font-size: 14px; margin-bottom: 12px; padding-left: 8px; }
|
||||
|
||||
.form-actions { display: flex; justify-content: flex-start; padding: 24px 0 0; }
|
||||
|
||||
/* 参会人管理 (放在 劳务材料 tab 底部, 无表题) */
|
||||
.attendee-section { margin-top: 16px; padding-top: 16px; border-top: 1px dashed #e8e8e8; }
|
||||
.attendee-toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; flex-wrap: wrap; }
|
||||
.toolbar-hint { font-size: 12px; color: #909399; line-height: 1.5; }
|
||||
.form-hint { font-size: 12px; color: #909399; margin-top: -8px; margin-bottom: 8px; line-height: 1.5; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user