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
+76
View File
@@ -0,0 +1,76 @@
<template>
<div class="executor-account">
<div class="breadcrumb">首页 / 账号信息</div>
<div class="page-card">
<el-form :model="form" label-width="120px" style="max-width:520px;">
<el-form-item label="姓名">
<el-input v-model="form.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="原密码">
<el-input v-model="form.oldPwd" type="password" show-password placeholder="请输入原密码" />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="form.newPwd" type="password" show-password placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认密码">
<el-input v-model="form.confirmPwd" type="password" show-password placeholder="请再次输入新密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSave">保存</el-button>
<el-button @click="onCancel">取消</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup>
import { reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/store/user'
const store = useUserStore()
const form = reactive({
name: '',
phone: '',
oldPwd: '',
newPwd: '',
confirmPwd: ''
})
onMounted(() => {
const u = store.user || {}
form.name = u.nickName || u.userName || ''
form.phone = u.phonenumber || ''
})
function onSave() {
if (!form.name) { ElMessage.warning('请输入姓名'); return }
if (!form.phone) { ElMessage.warning('请输入手机号'); return }
if (form.newPwd || form.confirmPwd) {
if (!form.oldPwd) { ElMessage.warning('请输入原密码'); return }
if (form.newPwd !== form.confirmPwd) { ElMessage.error('两次新密码不一致'); return }
}
ElMessage.success('保存成功(占位)')
}
function onCancel() {
const u = store.user || {}
form.name = u.nickName || u.userName || ''
form.phone = u.phonenumber || ''
form.oldPwd = ''
form.newPwd = ''
form.confirmPwd = ''
}
</script>
<style scoped>
.executor-account { padding: 0; }
.breadcrumb { color: #909399; font-size: 13px; margin-bottom: 12px; }
.page-card { background: #fff; border-radius: 4px; padding: 24px; border: 1px solid #ebeef5; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
</style>
+215
View File
@@ -0,0 +1,215 @@
<template>
<div class="executor-labor">
<div class="breadcrumb">首页 / 劳务凭证</div>
<div class="filter-card">
<el-form :inline="true" :model="q" size="default">
<el-form-item label="会议名称:">
<el-input v-model="q.meetingName" clearable placeholder="输入会议名称" style="width:200px" />
</el-form-item>
<el-form-item label="专家姓名:">
<el-input v-model="q.expertName" clearable placeholder="输入专家姓名" style="width:160px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="page-card">
<p class="tip">*结算前执行方和支持方无法看到凭证材料菜单结算后才能看到结算材料</p>
<p class="tip">*结算后已完结状态 执行方不能再进行编辑</p>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column prop="voucherNo" label="凭证编号" width="140" />
<el-table-column prop="meetingName" label="会议名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="expertName" label="专家姓名" width="110" />
<el-table-column prop="idCard" label="身份证号" width="180" />
<el-table-column prop="bankAccount" label="银行账号" width="220" />
<el-table-column label="金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatMoney(row.amount) }}</template>
</el-table-column>
<el-table-column prop="submitDate" label="提交日期" width="160" />
<el-table-column label="付款凭证.pdf" width="180" align="center">
<template #default="{ row }">
<el-link v-if="row.paymentVoucherUrl" type="primary" @click="previewVoucher(row.paymentVoucherUrl)">查看</el-link>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="success" @click="onUploadVoucher(row)">上传付款凭证</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="load" @size-change="load" />
</div>
</div>
<el-dialog v-model="detailOpen" title="劳务凭证详情" width="640px">
<el-descriptions :column="2" border>
<el-descriptions-item label="凭证编号">{{ detail.voucherNo }}</el-descriptions-item>
<el-descriptions-item label="会议名称" :span="2">{{ detail.meetingName }}</el-descriptions-item>
<el-descriptions-item label="专家姓名">{{ detail.expertName }}</el-descriptions-item>
<el-descriptions-item label="金额">¥ {{ formatMoney(detail.amount) }}</el-descriptions-item>
<el-descriptions-item label="身份证号" :span="2">{{ detail.idCard }}</el-descriptions-item>
<el-descriptions-item label="银行账号" :span="2">{{ detail.bankAccount }}</el-descriptions-item>
<el-descriptions-item label="提交日期" :span="2">{{ detail.submitDate }}</el-descriptions-item>
<el-descriptions-item label="付款凭证" :span="2">
<el-link v-if="detail.paymentVoucherUrl" type="primary" @click="previewVoucher(detail.paymentVoucherUrl)">查看</el-link>
<span v-else style="color:#c0c4cc">-</span>
</el-descriptions-item>
</el-descriptions>
<template #footer><el-button @click="detailOpen=false">关闭</el-button></template>
</el-dialog>
<!-- 上传付款凭证 dialog -->
<el-dialog v-model="voucherOpen" title="上传付款凭证" width="560px">
<el-form label-width="100px">
<el-form-item label="凭证编号">{{ voucherRow.voucherNo }}</el-form-item>
<el-form-item label="会议名称">{{ voucherRow.meetingName }}</el-form-item>
<el-form-item label="付款凭证">
<el-upload
drag
action="#"
:before-upload="beforeVoucherUpload"
:http-request="uploadVoucherPdf"
:file-list="voucherFileList"
:on-remove="onVoucherRemove"
accept=".pdf"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将付款凭证 PDF 拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">仅支持 PDF, 20MB</div>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="voucherOpen=false">取消</el-button>
<el-button type="primary" :loading="submitting" :disabled="!voucherForm.paymentVoucherUrl" @click="submitVoucher">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { bizList, bizUpdate } from '@/api/public'
import { uploadToOss } from '@/utils/oss'
const q = ref({ meetingName: '', expertName: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
const detail = ref({})
const detailOpen = ref(false)
// 付款凭证上传相关
const voucherOpen = ref(false)
const voucherRow = ref({})
const voucherForm = reactive({ voucherId: null, paymentVoucherUrl: '' })
const voucherFileList = ref([])
const submitting = ref(false)
function formatMoney(n) {
if (n == null) return '0.00'
return Number(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
async function load() {
loading.value = true
try {
const { data } = await bizList('laborVoucher', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = data?.rows || []
page.total = data?.total || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
q.value = { meetingName: '', expertName: '' }
page.pageNum = 1
load()
}
function onView(row) { detail.value = row; detailOpen.value = true }
// ========== 上传付款凭证 ==========
function onUploadVoucher(row) {
voucherRow.value = row
voucherForm.voucherId = row.voucherId
voucherForm.paymentVoucherUrl = row.paymentVoucherUrl || ''
voucherFileList.value = row.paymentVoucherUrl ? [{ name: row.paymentVoucherUrl.split('/').pop() || '付款凭证.pdf', url: row.paymentVoucherUrl }] : []
voucherOpen.value = true
}
function previewVoucher(url) {
if (!url) return
window.open(url, '_blank')
}
function beforeVoucherUpload(file) {
const max = 20 * 1024 * 1024
if (file.size > max) { ElMessage.error('文件不能超过 20MB'); return false }
if (!file.name.toLowerCase().endsWith('.pdf')) { ElMessage.error('仅支持 PDF 格式'); return false }
return true
}
async function uploadVoucherPdf(opts) {
try {
const url = await uploadToOss(opts.file, 'ry8080/labor/voucher/')
voucherForm.paymentVoucherUrl = url
ElMessage.success('上传成功')
} catch (e) {
ElMessage.error('上传失败: ' + (e?.message || e))
}
}
function onVoucherRemove() {
voucherForm.paymentVoucherUrl = ''
voucherFileList.value = []
}
async function submitVoucher() {
if (!voucherForm.paymentVoucherUrl) return ElMessage.warning('请先上传付款凭证')
submitting.value = true
try {
await bizUpdate('labor', {
id: voucherForm.voucherId,
paymentVoucherUrl: voucherForm.paymentVoucherUrl
})
ElMessage.success('付款凭证已保存')
voucherOpen.value = false
load()
} catch (e) {
ElMessage.error(e?.msg || '保存失败')
} finally {
submitting.value = false
}
}
onMounted(load)
</script>
<style scoped>
.executor-labor { padding: 0; }
.breadcrumb { color: #909399; font-size: 13px; margin-bottom: 12px; }
.filter-card, .page-card { background: #fff; border-radius: 4px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.tip { font-size: 12px; color: #f56c6c; margin: 4px 0; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
:deep(.el-table .el-button) { border-radius: 4px; }
:deep(.el-table .el-button.is-link) { color: var(--brand-primary); background: transparent; border: none; }
:deep(.el-table .el-button.is-link:hover) { color: var(--brand-primary-deep, var(--brand-primary)); background: transparent; }
:deep(.el-table .el-button.is-link.is-disabled) { color: #c0c4cc; background: transparent; }
:deep(.el-button--warning) { background: #e6a23c; border-color: #e6a23c; border-radius: 4px; }
:deep(.el-table .el-button) { border-radius: 4px; }
</style>
+197
View File
@@ -0,0 +1,197 @@
<template>
<div class="executor-meetings">
<div class="breadcrumb">首页 / 会议列表</div>
<div class="filter-card">
<el-form :inline="true" :model="q" size="default">
<el-form-item label="项目编号:">
<el-input v-model="q.projectNo" clearable placeholder="输入项目编号" style="width:160px" />
</el-form-item>
<el-form-item label="会议名称:">
<el-input v-model="q.meetingName" clearable placeholder="输入会议名称" style="width:200px" />
</el-form-item>
<el-form-item label="是否系列会:">
<el-select v-model="q.isSeries" clearable placeholder="请选择" style="width:140px">
<el-option label="是" value="1" />
<el-option label="否" value="0" />
</el-select>
</el-form-item>
<el-form-item label="是否结题:">
<el-select v-model="q.isFinished" clearable placeholder="请选择" style="width:140px">
<el-option label="已结题" value="1" />
<el-option label="未结题" value="0" />
</el-select>
</el-form-item>
<el-form-item label="期数:">
<el-input v-model="q.sessionNo" clearable placeholder="输入期数" style="width:100px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="page-card">
<div class="toolbar">
<span class="hint">已选 {{ selected.length }} </span>
</div>
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelect">
<el-table-column type="selection" width="48" />
<el-table-column prop="projectNo" label="项目编号" width="120" />
<el-table-column prop="meetingName" label="会议名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="isSeries" label="是否系列会" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.isSeries === '1' || row.isSeries === '是' ? 'success' : 'info'" size="small">
{{ row.isSeries === '1' || row.isSeries === '是' ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="seriesTopic" label="系列会主题" min-width="160" show-overflow-tooltip />
<el-table-column label="期数/总期数" width="110" align="center">
<template #default="{ row }">{{ row.periodNo || 0 }}/{{ row.totalPeriods || 0 }}</template>
</el-table-column>
<el-table-column prop="currentStage" label="当前阶段" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.currentStage === '执行中' ? 'success' : 'info'" size="small">{{ row.currentStage || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="isFinished" label="是否结题" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' || row.isFinished === '已结题' ? 'warning' : 'info'" size="small">
{{ row.isFinished === '1' || row.isFinished === '已结题' ? '已结题' : '未结题' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="warning" @click="onSupervision(row)">查看监督意见</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="load" @size-change="load" />
</div>
</div>
<!-- 监督意见 弹窗 -->
<el-dialog v-model="supOpen" title="监督意见" width="560px">
<el-form label-width="100px">
<el-form-item label="会议名称">
<span>{{ supRow.meetingName }}</span>
</el-form-item>
<el-form-item label="监督结论">
<el-radio-group v-model="supForm.rating">
<el-radio value="good">满意</el-radio>
<el-radio value="normal">一般</el-radio>
<el-radio value="bad">存在需整改的问题</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="监督意见">
<el-input v-model="supForm.opinion" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请输入监督意见(0/500" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="supOpen=false">取消</el-button>
<el-button type="primary" @click="onSaveSup">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="viewOpen" title="会议详情" width="640px">
<el-descriptions :column="2" border>
<el-descriptions-item label="项目编号">{{ view.projectNo }}</el-descriptions-item>
<el-descriptions-item label="会议ID">{{ view.meetingId }}</el-descriptions-item>
<el-descriptions-item label="会议名称" :span="2">{{ view.meetingName }}</el-descriptions-item>
<el-descriptions-item label="项目形式">{{ view.projectForm }}</el-descriptions-item>
<el-descriptions-item label="当前阶段">{{ view.currentStage }}</el-descriptions-item>
<el-descriptions-item label="期数/总期数">{{ view.periodNo }}/{{ view.totalPeriods }}</el-descriptions-item>
<el-descriptions-item label="是否系列会">{{ view.isSeries }}</el-descriptions-item>
<el-descriptions-item label="开始时间" :span="2">{{ view.startTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="结束时间" :span="2">{{ view.endTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="监督意见" :span="2">{{ view.supervisionOpinion || '暂无' }}</el-descriptions-item>
</el-descriptions>
<template #footer><el-button @click="viewOpen=false">关闭</el-button></template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { bizList, bizUpdate } from '@/api/public'
const q = ref({ projectNo: '', meetingName: '', isSeries: '', isFinished: '', sessionNo: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
const selected = ref([])
const supOpen = ref(false)
const supRow = ref({})
const supForm = reactive({ rating: 'good', opinion: '' })
const viewOpen = ref(false)
const view = ref({})
async function load() {
loading.value = true
try {
const { data } = await bizList('meeting', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = data?.rows || []
page.total = data?.total || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
q.value = { projectNo: '', meetingName: '', isSeries: '', isFinished: '', sessionNo: '' }
page.pageNum = 1
load()
}
function onSelect(arr) { selected.value = arr }
function onView(row) { view.value = row; viewOpen.value = true }
function onSupervision(row) {
supRow.value = row
supForm.rating = 'good'
supForm.opinion = row.supervisionOpinion || ''
supOpen.value = true
}
async function onSaveSup() {
try {
await bizUpdate('meeting', {
meetingId: supRow.value.meetingId,
supervisionOpinion: supForm.opinion,
supervisionRating: supForm.rating
})
ElMessage.success('已保存监督意见')
supOpen.value = false
load()
} catch (e) {
ElMessage.error(e?.msg || '保存失败')
}
}
onMounted(load)
</script>
<style scoped>
.executor-meetings { padding: 0; }
.breadcrumb { color: #909399; font-size: 13px; margin-bottom: 12px; }
.filter-card, .page-card { background: #fff; border-radius: 4px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.hint { font-size: 13px; color: #909399; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
:deep(.el-table .el-button) { border-radius: 4px; }
:deep(.el-table .el-button.is-link) { color: var(--brand-primary); background: transparent; border: none; }
:deep(.el-table .el-button.is-link:hover) { color: var(--brand-primary-deep); background: transparent; }
:deep(.el-table .el-button.is-link.is-disabled) { color: #c0c4cc; background: transparent; }
</style>
+84
View File
@@ -0,0 +1,84 @@
<template>
<div class="executor-overview">
<h1 class="page-title">首页</h1>
<p class="hint">*点击数字跳转到相应的列表页</p>
<div class="stats-grid">
<router-link class="stat-card" to="/executor/projects">
<div class="stat-label">项目总数量</div>
<div class="stat-value">{{ stats.totalProjects }}</div>
</router-link>
<router-link class="stat-card" to="/executor/projects">
<div class="stat-label">已结算数量</div>
<div class="stat-value">{{ stats.settledProjects }}</div>
</router-link>
<router-link class="stat-card" to="/executor/projects">
<div class="stat-label">已结题数量</div>
<div class="stat-value">{{ stats.finishedProjects }}</div>
</router-link>
</div>
<section class="section">
<h2 class="section-title">消息通知</h2>
<ul class="notice-list">
<li class="notice-item" v-for="n in notices" :key="n.title">
<div class="dot"></div>
<div class="body">
<div class="title">{{ n.title }}</div>
<div class="text">{{ n.text }}</div>
</div>
<span class="time">{{ n.time }}</span>
</li>
<li v-if="notices.length === 0" class="empty">暂无消息</li>
</ul>
</section>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { bizList } from '@/api/public'
const stats = ref({ totalProjects: 0, settledProjects: 0, finishedProjects: 0 })
const notices = ref([
{ title: '结算审核', text: 'ZH-2026-650 结算已通过', time: '2小时前' },
{ title: '会议邀请', text: '您有一场新会议待确认', time: '昨天' },
{ title: '项目阶段', text: '智慧医院建设项目进入实施阶段', time: '2026-08-01' },
{ title: '系统升级公告', text: '系统将于本周日 02:00-04:00 升级维护', time: '2026-07-28' }
])
async function loadStats() {
try {
const ps = await bizList('project', { pageNum: 1, pageSize: 1 })
stats.value.totalProjects = ps.total || 0
const settled = await bizList('project', { isSettled: '1', pageNum: 1, pageSize: 1 })
stats.value.settledProjects = settled.total || 0
const finished = await bizList('project', { isFinished: '1', pageNum: 1, pageSize: 1 })
stats.value.finishedProjects = finished.total || 0
} catch (e) {}
}
onMounted(loadStats)
</script>
<style scoped>
.executor-overview { padding: 0; }
.page-title { font-size: 22px; font-weight: 600; color: #1a1a1a; margin: 0 0 8px; }
.hint { font-size: 12px; color: #f56c6c; margin: 0 0 16px; }
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px; }
.stat-card { background: #fff; border: 1px solid #ebeef5; border-radius: 4px; padding: 32px 24px; text-decoration: none; color: inherit; display: block; text-align: center; }
.stat-card:hover { border-color: var(--brand-primary); }
.stat-label { font-size: 14px; color: #909399; margin-bottom: 16px; }
.stat-value { font-size: 40px; font-weight: 700; color: var(--brand-primary); line-height: 1; }
.section { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.section-title { font-size: 15px; font-weight: 600; color: #1a1a1a; margin: 0 0 16px; }
.notice-list { list-style: none; padding: 0; margin: 0; }
.notice-item { padding: 12px 0; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; gap: 12px; }
.notice-item:last-child { border-bottom: none; }
.notice-item .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--brand-primary); flex-shrink: 0; }
.notice-item .body { flex: 1; min-width: 0; }
.notice-item .title { font-size: 13px; color: #1a1a1a; }
.notice-item .text { font-size: 12px; color: #595959; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; }
.notice-item .time { font-size: 12px; color: #bfbfbf; flex-shrink: 0; }
.empty { padding: 24px; text-align: center; color: #909399; }
</style>
+198
View File
@@ -0,0 +1,198 @@
<template>
<div class="page-card executor-people">
<p class="page-sub">管理执行方人员,分配会务执行角色</p>
<!-- ========== 筛选区 (按原型 人员管理_1.html top=162) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="姓名"><el-input v-model="q.name" placeholder="姓名" clearable style="width:140px" /></el-form-item>
<el-form-item label="手机号"><el-input v-model="q.phone" placeholder="手机号" clearable style="width:140px" /></el-form-item>
<el-form-item label="所属公司"><el-input v-model="q.orgName" placeholder="所属公司" clearable style="width:160px" /></el-form-item>
<el-form-item label="部门"><el-input v-model="q.department" placeholder="部门" clearable style="width:140px" /></el-form-item>
<el-form-item label="职务"><el-input v-model="q.position" placeholder="职务" clearable style="width:140px" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="loadList">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 批量按钮 (top=203: 新建人员 / 批量导入) ========== -->
<div class="batch-bar">
<el-button type="primary" @click="openAdd">新建人员</el-button>
<el-button @click="importOpen = true">批量导入</el-button>
<span class="filter-tip">*批量导入模板与列表表项一致</span>
</div>
<!-- ========== 表格 (按原型 列头 top=270: 姓名/手机号/工作单位/部门/职务/角色/状态/操作) ========== -->
<el-table :data="list" border stripe v-loading="loading">
<el-table-column prop="name" label="姓名" min-width="100" />
<el-table-column prop="phone" label="手机号" min-width="130" />
<el-table-column prop="orgName" label="所属公司" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" min-width="100" />
<el-table-column prop="position" label="职务" min-width="100" />
<el-table-column prop="role" label="角色" width="100">
<template #default="{ row }">
<el-tag size="small" :type="row.role === '管理员' ? 'warning' : 'info'">{{ row.role || '监察员' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag size="small" :type="row.status === '正常' ? 'success' : 'danger'">{{ row.status || '正常' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="onView(row)">查看</el-button>
<el-button size="small" link type="primary" @click="onEdit(row)">编辑</el-button>
<el-button v-if="(row.status || '正常') === '正常'" size="small" link type="danger" @click="onToggleStatus(row, '禁用')">禁用</el-button>
<el-button v-else size="small" link type="success" @click="onToggleStatus(row, '恢复')">恢复</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="loadList" @size-change="loadList" />
</div>
<!-- ========== Dialog: 新增/编辑 ========== -->
<el-dialog v-model="formOpen" :title="form._id ? '编辑人员' : '新增人员'" width="520px">
<el-form :model="form" label-width="100px">
<el-form-item label="姓名"><el-input v-model="form.name" /></el-form-item>
<el-form-item label="手机号"><el-input v-model="form.phone" /></el-form-item>
<el-form-item label="所属公司">
<el-input v-model="form.orgName" placeholder="所属公司 (TODO: 改 biz_org 下拉)" />
</el-form-item>
<el-form-item label="部门"><el-input v-model="form.department" /></el-form-item>
<el-form-item label="职务"><el-input v-model="form.position" /></el-form-item>
<el-form-item label="角色">
<el-select v-model="form.role" style="width:100%" :disabled="userRole === '管理员'">
<el-option label="管理员" value="管理员" />
<el-option label="监察员" value="监察员" />
</el-select>
<span class="filter-tip">*新建时管理员只能新建监察员</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="formOpen = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
<!-- ========== Dialog: 批量导入 ========== -->
<el-dialog v-model="importOpen" title="批量导入" width="520px">
<el-form label-width="100px">
<el-form-item label="选择文件">
<el-upload ref="uploadRef" :auto-upload="false" :limit="1" accept=".xls,.xlsx">
<el-button>选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="">
<el-button link type="primary" @click="downloadTemplate">批量导入模板下载</el-button>
</el-form-item>
<el-form-item><span class="filter-tip">*批量导入模板xls与列表表项一致</span></el-form-item>
</el-form>
<template #footer>
<el-button @click="importOpen = false">取消</el-button>
<el-button type="primary" @click="submitImport">确定</el-button>
</template>
</el-dialog>
<!-- ========== Dialog: 查看 ========== -->
<el-dialog v-model="viewOpen" title="人员详情" width="520px">
<el-descriptions :column="2" border>
<el-descriptions-item label="姓名">{{ view.name }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ view.phone }}</el-descriptions-item>
<el-descriptions-item label="所属公司" :span="2">{{ view.orgName }}</el-descriptions-item>
<el-descriptions-item label="部门">{{ view.department }}</el-descriptions-item>
<el-descriptions-item label="职务">{{ view.position }}</el-descriptions-item>
<el-descriptions-item label="角色">{{ view.role || '监察员' }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ view.status || '正常' }}</el-descriptions-item>
</el-descriptions>
<template #footer><el-button @click="viewOpen = false">关闭</el-button></template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { bizList, bizAdd, bizUpdate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
const userRole = '监察员' // 简化: 默认监察员
const list = ref([])
const loading = ref(false)
const q = reactive({ name: '', phone: '', orgName: '', department: '', position: '' })
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
const formOpen = ref(false)
const form = reactive({ _id: '', name: '', phone: '', orgName: '', department: '', position: '', role: '监察员' })
const importOpen = ref(false)
const view = ref({})
const viewOpen = ref(false)
async function loadList() {
loading.value = true
try {
const r = await bizList('person', { pageNum: page.pageNum, pageSize: page.pageSize, ...q })
list.value = r.rows || []
page.total = r.total || 0
} finally { loading.value = false }
}
function reset() { Object.assign(q, { name:'',phone:'',orgName:'',department:'',position:'' }); page.pageNum = 1; loadList() }
function openAdd() {
Object.assign(form, { _id:'', name:'', phone:'', orgName:'', department:'', position:'', role: '监察员' })
formOpen.value = true
}
function onView(row) { view.value = row; viewOpen.value = true }
function onEdit(row) {
Object.assign(form, { _id: row.personId, name:row.name, phone:row.phone, orgName:row.orgName, department:row.department, position:row.position, role: row.role || '监察员' })
formOpen.value = true
}
async function save() {
if (!form.name) { ElMessage.error('请填写姓名'); return }
try {
if (form._id) await bizUpdate('person', { personId: form._id, ...form })
else await bizAdd('person', { ...form })
ElMessage.success('保存成功')
formOpen.value = false
loadList()
} catch (e) { ElMessage.error(e?.msg || '保存失败') }
}
async function onToggleStatus(row, action) {
try {
await ElMessageBox.confirm(`确定${action}${row.name}」吗?`, action, { type: 'warning' })
const newStatus = action === '禁用' ? '禁用' : '正常'
await bizUpdate('person', { personId: row.personId, status: newStatus })
ElMessage.success(`${action}成功`)
loadList()
} catch (e) { if (e !== 'cancel') ElMessage.error(e?.msg || `${action}失败`) }
}
function downloadTemplate() {
ElMessage.info('模板下载中...')
}
function submitImport() {
ElMessage.success('导入成功')
importOpen.value = false
loadList()
}
onMounted(loadList)
</script>
<style scoped>
.executor-people { padding: 16px; }
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
.batch-bar { margin-bottom: 12px; display: flex; gap: 8px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
.filter-form { margin-bottom: 12px; }
</style>
+275
View File
@@ -0,0 +1,275 @@
<template>
<div class="executor-projects">
<div class="breadcrumb">首页 / 项目列表</div>
<div class="filter-card">
<el-form :inline="true" :model="query" size="default">
<el-form-item label="项目编号:">
<el-input v-model="query.projectNo" clearable placeholder="项目编号" style="width:160px" />
</el-form-item>
<el-form-item label="项目名称:">
<el-input v-model="query.projectName" clearable placeholder="项目名称" style="width:200px" />
</el-form-item>
<el-form-item label="项目时间:">
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px" />
</el-form-item>
<el-form-item label="是否结题:">
<el-select v-model="query.isFinished" clearable placeholder="请选择" style="width:140px">
<el-option label="已结题" value="1" />
<el-option label="未结题" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadList">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="page-card">
<div class="batch-bar">
<el-button type="primary" :disabled="!selected.length" @click="onAssign">项目分配</el-button>
<el-button type="primary" :disabled="!selected.length" @click="onBatchRate">批量评价</el-button>
<span class="hint">已选 {{ selected.length }} </span>
<span class="tip">*支持方管理员在分配项目的时候只能分配监察员</span>
</div>
<el-table :data="list" border stripe v-loading="loading" @selection-change="sel=>selected=sel">
<el-table-column type="selection" width="48" />
<el-table-column prop="projectNo" label="项目编号" width="120" />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column label="总场次/总期数" width="110" align="center">
<template #default="{ row }">{{ row.totalSessions || 0 }}/{{ row.totalPeriods || 0 }}</template>
</el-table-column>
<el-table-column prop="doneSessions" label="已执行" width="80" align="center" />
<el-table-column prop="todoSessions" label="未执行" width="80" align="center" />
<el-table-column label="总金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.totalAmount) }}</template>
</el-table-column>
<el-table-column label="可用金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.availableAmount) }}</template>
</el-table-column>
<el-table-column label="已支付劳务费" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.paidLaborAmount) }}</template>
</el-table-column>
<el-table-column label="已支付会务费" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.paidMeetingAmount) }}</template>
</el-table-column>
<el-table-column label="执行单位评价(支持方)" width="180" align="center">
<template #default="{ row }">
<el-rate v-model="row.ratingScore" disabled show-score :max="5" />
</template>
</el-table-column>
<el-table-column prop="isFinished" label="是否结题" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' || row.isFinished === '已结题' ? 'warning' : 'info'" size="small">
{{ row.isFinished === '1' || row.isFinished === '已结题' ? '已结题' : '未结题' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="startTime" label="项目开始时间" width="150" />
<el-table-column prop="endTime" label="项目结束时间" width="150" />
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="viewDetail(row)">查看</el-button>
<el-button link type="primary" @click="onAssign(row)">分配</el-button>
<el-button link type="primary" @click="onRate(row)">项目评价</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="loadList" @size-change="loadList" />
</div>
<p class="footnote">*点击后可查看具体评价维度评价标准与供应商系统保持一致</p>
</div>
<!-- 项目分配 弹窗 -->
<el-dialog v-model="assignOpen" title="项目分配" width="520px">
<el-form label-width="100px">
<el-form-item label="项目名称">
<span>{{ assignRow.projectName }}</span>
</el-form-item>
<el-form-item label="分配给">
<el-select v-model="assignForm.assignee" placeholder="请选择监察员" filterable style="width:100%">
<el-option v-for="p in people" :key="p.userId" :label="p.name" :value="p.userId" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="assignOpen=false">取消</el-button>
<el-button type="primary" @click="onSaveAssign">确定</el-button>
</template>
</el-dialog>
<!-- 批量评价 弹窗 -->
<el-dialog v-model="rateOpen" :title="rateRow ? '项目评价' : '批量评价'" width="520px">
<el-form label-width="100px">
<el-form-item v-if="rateRow" label="项目名称">
<span>{{ rateRow.projectName }}</span>
</el-form-item>
<el-form-item v-else label="已选项目">
<span>{{ selected.length }} </span>
</el-form-item>
<el-form-item label="评分">
<el-rate v-model="rateForm.score" :max="5" show-text />
</el-form-item>
<el-form-item label="评价说明">
<el-input v-model="rateForm.comment" type="textarea" :rows="3" placeholder="请输入评价说明" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="rateOpen=false">取消</el-button>
<el-button type="primary" @click="onSaveRate">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="detailOpen" title="项目详情" width="720px">
<el-descriptions :column="2" border>
<el-descriptions-item label="项目编号">{{ detail.projectNo }}</el-descriptions-item>
<el-descriptions-item label="项目形式">{{ detail.projectForm }}</el-descriptions-item>
<el-descriptions-item label="项目名称" :span="2">{{ detail.projectName }}</el-descriptions-item>
<el-descriptions-item label="赞助公司" :span="2">{{ detail.orgName || '-' }}</el-descriptions-item>
<el-descriptions-item label="总场次/总期数">{{ detail.totalSessions }}/{{ detail.totalPeriods }}</el-descriptions-item>
<el-descriptions-item label="已执行">{{ detail.doneSessions || 0 }}</el-descriptions-item>
<el-descriptions-item label="未执行">{{ detail.todoSessions || 0 }}</el-descriptions-item>
<el-descriptions-item label="总金额">¥ {{ formatNum(detail.totalAmount) }}</el-descriptions-item>
<el-descriptions-item label="可用金额" :span="2">¥ {{ formatNum(detail.availableAmount) }}</el-descriptions-item>
<el-descriptions-item label="已支付劳务费">¥ {{ formatNum(detail.paidLaborAmount) }}</el-descriptions-item>
<el-descriptions-item label="已支付会务费">¥ {{ formatNum(detail.paidMeetingAmount) }}</el-descriptions-item>
<el-descriptions-item label="开始时间" :span="2">{{ detail.startTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="结束时间" :span="2">{{ detail.endTime || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer><el-button @click="detailOpen=false">关闭</el-button></template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { bizList, bizUpdate } from '@/api/public'
const list = ref([])
const loading = ref(false)
const query = reactive({ projectNo: '', projectName: '', isFinished: '' })
const dateRange = ref([])
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
const selected = ref([])
const people = ref([])
const detail = ref({})
const detailOpen = ref(false)
const assignOpen = ref(false)
const assignRow = ref({})
const assignForm = reactive({ assignee: '' })
const rateOpen = ref(false)
const rateRow = ref(null)
const rateForm = reactive({ score: 5, comment: '' })
function formatNum(n) {
if (n == null) return '0.00'
return Number(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
async function loadList() {
loading.value = true
try {
const params = { pageNum: page.pageNum, pageSize: page.pageSize, ...query }
if (dateRange.value && dateRange.value.length === 2) {
params.startTime = dateRange.value[0]
params.endTime = dateRange.value[1]
}
const r = await bizList('project', params)
list.value = r.rows || []
page.total = r.total || 0
} catch (e) { list.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
query.projectNo = ''
query.projectName = ''
query.isFinished = ''
dateRange.value = []
page.pageNum = 1
loadList()
}
function viewDetail(row) { detail.value = row; detailOpen.value = true }
async function loadPeople() {
try { const r = await bizList('person', { pageNum: 1, pageSize: 200 }); people.value = (r.rows || []).map(p => ({ userId: p.personId, name: p.name })) }
catch { people.value = [] }
}
function onAssign(row) {
const target = row || selected.value[0]
if (!target) return
assignRow.value = target
assignForm.assignee = ''
assignOpen.value = true
}
async function onSaveAssign() {
if (!assignForm.assignee) { ElMessage.warning('请选择分配人'); return }
try {
await bizUpdate('project', { projectId: assignRow.value.projectId, supervisorId: assignForm.assignee })
ElMessage.success('分配成功')
assignOpen.value = false
loadList()
} catch (e) { ElMessage.error(e?.msg || '分配失败') }
}
function onRate(row) {
rateRow.value = row
rateForm.score = row?.ratingScore || 5
rateForm.comment = row?.ratingComment || ''
rateOpen.value = true
}
function onBatchRate() {
if (!selected.value.length) return
rateRow.value = null
rateForm.score = 5
rateForm.comment = ''
rateOpen.value = true
}
async function onSaveRate() {
try {
let ok = 0
const payload = { ratingScore: rateForm.score, ratingComment: rateForm.comment }
if (rateRow.value) {
await bizUpdate('project', { projectId: rateRow.value.projectId, ...payload })
ok = 1
} else {
for (const r of selected.value) {
try { await bizUpdate('project', { projectId: r.projectId, ...payload }); ok++ } catch {}
}
}
ElMessage.success(rateRow.value ? '评价成功' : `已评价 ${ok} 个项目`)
rateOpen.value = false
loadList()
} catch (e) { ElMessage.error(e?.msg || '评价失败') }
}
onMounted(() => { loadList(); loadPeople() })
</script>
<style scoped>
.executor-projects { padding: 0; }
.breadcrumb { color: #909399; font-size: 13px; margin-bottom: 12px; }
.filter-card, .page-card { background: #fff; border-radius: 4px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.batch-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
.hint { font-size: 13px; color: #909399; }
.tip { font-size: 12px; color: #f56c6c; margin-left: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
.footnote { font-size: 12px; color: #f56c6c; margin-top: 8px; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
:deep(.el-table .el-button) { border-radius: 4px; }
:deep(.el-table .el-button.is-link) { color: var(--brand-primary); }
:deep(.el-table .el-button.is-link:hover) { color: var(--brand-primary-deep); }
</style>