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:
@@ -0,0 +1,439 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<!-- 面包屑 (与列表页一致) -->
|
||||
<div class="breadcrumb">
|
||||
首页 / 项目管理 / <span class="current">项目详情</span>
|
||||
</div>
|
||||
|
||||
<!-- ================= 1. 项目信息 (11 字段, 一列竖排) ================= -->
|
||||
<el-form v-if="row && row.projectId" label-position="left" class="info-form">
|
||||
<el-form-item label="项目编号">
|
||||
<span class="info-value">{{ row.projectNo || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目名称">
|
||||
<span class="info-value">{{ row.projectName || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="总场次/总期数">
|
||||
<span class="info-value">{{ row.totalSessions || 0 }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="总金额">
|
||||
<span class="info-value money">¥ {{ fmtMoney(row.totalAmount) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="管理费及税金">
|
||||
<span class="info-value money">¥ {{ fmtMoney(row.manageFee) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目开始时间">
|
||||
<span class="info-value">{{ fmtDate(row.startTime) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目结束时间">
|
||||
<span class="info-value">{{ fmtDate(row.endTime) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="赞助公司">
|
||||
<span class="info-value">{{ row.orgName || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目形式">
|
||||
<span class="info-value">{{ row.projectForm || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目评价">
|
||||
<a class="op-link" href="javascript:void(0)" @click="openScoreDialog">{{ fmtScore(row.ratingScore) }}</a>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- ================= 2. 角色劳务设置 ================= -->
|
||||
<div class="section-title">角色劳务设置</div>
|
||||
<p class="hint-red">*角色劳务设置针对该项目所有会议生效</p>
|
||||
<el-table :data="form.roleRows" border>
|
||||
<el-table-column prop="role" label="角色" width="120" />
|
||||
<el-table-column label="劳务金额" align="right">
|
||||
<template #default="{ row }">¥ {{ fmtMoney(row.amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" :disabled="form.roleRows.length<=1" @click="removeRoleRowAt($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="margin-top:8px">
|
||||
<el-button link type="primary" @click="addRoleRow">+ 增加角色</el-button>
|
||||
</div>
|
||||
|
||||
<!-- ================= 3. 支持单位监督员 ================= -->
|
||||
<div class="section-title">支持单位监督员</div>
|
||||
<p class="hint-red">*增加只显示已点支持意向的</p>
|
||||
<div class="monitor-list">
|
||||
<div v-for="(m, idx) in monitors" :key="idx" class="monitor-row">
|
||||
<span class="monitor-num">{{ idx + 1 }}.</span>
|
||||
<span class="monitor-name">{{ m.userName || m.name || '-' }}</span>
|
||||
<el-button link type="danger" @click="removeMonitorAt(idx)">删除</el-button>
|
||||
</div>
|
||||
<div v-if="!monitors.length" class="empty-hint">暂无监督员</div>
|
||||
</div>
|
||||
<div style="margin-top:8px">
|
||||
<el-button link type="primary" @click="openAddMonitor">+ 增加监督员</el-button>
|
||||
</div>
|
||||
|
||||
<!-- ================= 4. 已发布公告 ================= -->
|
||||
<div class="section-title">已发布公告</div>
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="openUpload">发布公告</el-button>
|
||||
<el-button @click="loadAnnouncements">刷新</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="announcements" border>
|
||||
<el-table-column prop="title" label="公告标题" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="发布时间" width="160" align="center" :formatter="(r)=>fmtDate(r.createTime)" />
|
||||
<el-table-column prop="creatorName" label="发布人" width="120" align="center" />
|
||||
<el-table-column label="操作" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openPreview(row.fileUrl || row.url, row.title)">查看</el-button>
|
||||
<el-button link type="danger" @click="deleteAnnouncement(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- ================= dialogs ================= -->
|
||||
<!-- 发布公告 dialog -->
|
||||
<el-dialog v-model="uploadOpen" title="发布公告" width="560px" @closed="resetUploadForm">
|
||||
<el-form :model="uploadForm" label-width="100px">
|
||||
<el-form-item label="公告类型">
|
||||
<el-select v-model="uploadForm.announceType" placeholder="请选择" style="width:100%">
|
||||
<el-option v-for="t in announceTypes" :key="t.key" :label="t.label" :value="t.key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="公告标题">
|
||||
<el-input v-model="uploadForm.title" placeholder="请输入公告标题" maxlength="80" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="uploadForm.remark" type="textarea" :rows="2" placeholder="选填" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="uploadOpen=false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitAnnouncement">发布</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 评分 dialog -->
|
||||
<el-dialog v-model="scoreOpen" title="项目评分" width="560px">
|
||||
<el-form :model="scoreForm" label-width="120px">
|
||||
<el-form-item v-for="d in scoreDims" :key="d.key" :label="d.label">
|
||||
<el-rate v-model="scoreForm[d.key]" :max="5" show-score />
|
||||
</el-form-item>
|
||||
<el-form-item label="总分">
|
||||
<span class="score-num">{{ totalScore }}</span> / 20
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="scoreOpen=false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitScore">保存评分</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 增加监督员 dialog -->
|
||||
<el-dialog v-model="addMonitorOpen" title="增加监督员" width="420px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="监督员">
|
||||
<el-select v-model="newMonitor.userId" placeholder="请选择监督员" filterable style="width:100%">
|
||||
<el-option v-for="u in monitorCandidates" :key="u.userId"
|
||||
:label="`${u.userName}${u.nickName ? ' (' + u.nickName + ')' : ''}`" :value="u.userId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="addMonitorOpen=false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmAddMonitor">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 公告预览 -->
|
||||
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { bizGet, bizList, bizAdd, bizUpdate, bizDelete } from '@/api/public'
|
||||
import Preview from '@/components/Preview.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// ===================== 状态 =====================
|
||||
const projectId = ref('')
|
||||
const row = ref({})
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
|
||||
const announceTypes = [
|
||||
{ key: 'invitation', label: '邀请函' },
|
||||
{ key: 'support', label: '支持函' },
|
||||
{ key: 'notice', label: '通知' },
|
||||
{ key: 'agenda', label: '日程' }
|
||||
]
|
||||
|
||||
const form = reactive({
|
||||
roleRows: [
|
||||
{ role: '主席', customName: '', amount: 5000 },
|
||||
{ role: '主持', customName: '', amount: 3000 }
|
||||
]
|
||||
})
|
||||
const monitors = ref([])
|
||||
const addMonitorOpen = ref(false)
|
||||
const newMonitor = reactive({ userId: null })
|
||||
const monitorCandidates = ref([])
|
||||
|
||||
const announcements = ref([])
|
||||
const uploadOpen = ref(false)
|
||||
const uploadForm = reactive({ announceType: 'notice', title: '', remark: '' })
|
||||
|
||||
const scoreOpen = ref(false)
|
||||
const scoreForm = reactive({ scoreQuality: 0, scoreSpeed: 0, scoreCoordination: 0, scoreCompliance: 0 })
|
||||
const scoreDims = [
|
||||
{ key: 'scoreQuality', label: '履约质量' },
|
||||
{ key: 'scoreSpeed', label: '时效响应' },
|
||||
{ key: 'scoreCoordination', label: '配合度' },
|
||||
{ key: 'scoreCompliance', label: '合规安全' }
|
||||
]
|
||||
const totalScore = computed(() =>
|
||||
scoreForm.scoreQuality + scoreForm.scoreSpeed + scoreForm.scoreCoordination + scoreForm.scoreCompliance
|
||||
)
|
||||
|
||||
const previewOpen = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewTitle = ref('公告预览')
|
||||
|
||||
// ===================== 工具 =====================
|
||||
function numOrZero(v) {
|
||||
const n = Number(v)
|
||||
return isNaN(n) ? 0 : n
|
||||
}
|
||||
function fmtScore(v) {
|
||||
if (v == null || v === '') return '-'
|
||||
return Number(v).toFixed(1)
|
||||
}
|
||||
function fmtMoney(v) {
|
||||
if (v == null || v === '' || v === 0) return '0.00'
|
||||
return Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
function fmtDate(v) {
|
||||
if (!v) return '-'
|
||||
const s = String(v)
|
||||
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/)
|
||||
return m ? `${m[1]}.${m[2]}.${m[3]}` : s
|
||||
}
|
||||
|
||||
// ===================== 加载 =====================
|
||||
async function loadProject() {
|
||||
projectId.value = route.params.projectId
|
||||
if (!projectId.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const resp = await bizGet('project', projectId.value)
|
||||
// 后端 success(BizProject) 返回 {code, msg, data: BizProject}
|
||||
const data = (resp && resp.data) || resp || {}
|
||||
row.value = data || {}
|
||||
scoreForm.scoreQuality = numOrZero(row.value.scoreQuality)
|
||||
scoreForm.scoreSpeed = numOrZero(row.value.scoreSpeed)
|
||||
scoreForm.scoreCoordination = numOrZero(row.value.scoreCoordination)
|
||||
scoreForm.scoreCompliance = numOrZero(row.value.scoreCompliance)
|
||||
// 角色劳务 (JSON 字段, 缺则用默认)
|
||||
try {
|
||||
const rl = JSON.parse(row.value.roleLabor || '[]')
|
||||
if (Array.isArray(rl) && rl.length) form.roleRows = rl
|
||||
} catch (e) { /* keep default */ }
|
||||
// 监督员 (JSON 字段)
|
||||
try {
|
||||
const mn = JSON.parse(row.value.monitors || '[]')
|
||||
if (Array.isArray(mn)) monitors.value = mn
|
||||
} catch (e) { /* keep empty */ }
|
||||
} catch (e) {
|
||||
console.error('[project-detail] load failed', e)
|
||||
row.value = {}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAnnouncements() {
|
||||
try {
|
||||
const resp = await bizList('publicity', { projectId: projectId.value })
|
||||
const arr = (resp && resp.rows) || ((resp && resp.data) && (resp && resp.data).rows) || (resp && resp.data) || []
|
||||
announcements.value = Array.isArray(arr) ? arr : []
|
||||
} catch (e) {
|
||||
console.error('[project-detail] announcements load failed', e)
|
||||
announcements.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 角色劳务 =====================
|
||||
function addRoleRow() {
|
||||
if (form.roleRows.length >= 20) { ElMessage.warning('最多 20 行角色'); return }
|
||||
form.roleRows.push({ role: '', customName: '', amount: 0 })
|
||||
}
|
||||
function removeRoleRowAt(idx) {
|
||||
if (form.roleRows.length <= 1) { ElMessage.warning('至少保留 1 行角色'); return }
|
||||
form.roleRows.splice(idx, 1)
|
||||
}
|
||||
|
||||
// ===================== 监督员 =====================
|
||||
async function openAddMonitor() {
|
||||
addMonitorOpen.value = true
|
||||
if (!monitorCandidates.value.length) {
|
||||
try {
|
||||
const resp = await bizList('person', { pageNum: 1, pageSize: 100, role: '监察员', unitType: 'sponsor' })
|
||||
monitorCandidates.value = (resp && resp.rows) || ((resp && resp.data) && (resp && resp.data).rows) || (resp && resp.data) || []
|
||||
} catch (e) { monitorCandidates.value = [] }
|
||||
}
|
||||
}
|
||||
function confirmAddMonitor() {
|
||||
if (!newMonitor.userId) { ElMessage.warning('请选择监督员'); return }
|
||||
const u = monitorCandidates.value.find(x => x.userId === newMonitor.userId)
|
||||
if (u) monitors.value.push({ userId: u.userId, userName: u.userName, name: u.nickName || u.userName })
|
||||
addMonitorOpen.value = false
|
||||
newMonitor.userId = null
|
||||
}
|
||||
function removeMonitorAt(idx) {
|
||||
if (monitors.value.length <= 1) { ElMessage.warning('至少保留 1 行监督员'); return }
|
||||
monitors.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
// ===================== 公告 =====================
|
||||
function openUpload() {
|
||||
resetUploadForm()
|
||||
uploadOpen.value = true
|
||||
}
|
||||
function resetUploadForm() {
|
||||
uploadForm.announceType = 'notice'
|
||||
uploadForm.title = ''
|
||||
uploadForm.remark = ''
|
||||
}
|
||||
async function submitAnnouncement() {
|
||||
if (!uploadForm.title) { ElMessage.warning('请输入公告标题'); return }
|
||||
submitting.value = true
|
||||
try {
|
||||
await bizAdd('publicity', {
|
||||
projectId: projectId.value,
|
||||
announceType: uploadForm.announceType,
|
||||
title: uploadForm.title,
|
||||
remark: uploadForm.remark,
|
||||
status: '1'
|
||||
})
|
||||
ElMessage.success('已发布')
|
||||
uploadOpen.value = false
|
||||
await loadAnnouncements()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('发布失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
async function deleteAnnouncement(a) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除公告【${a.title || '未命名'}】?`, '提示', { type: 'warning' })
|
||||
await bizDelete('publicity', a.id || a.publicityId)
|
||||
ElMessage.success('已删除')
|
||||
await loadAnnouncements()
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 评分 =====================
|
||||
function openScoreDialog() {
|
||||
scoreOpen.value = true
|
||||
}
|
||||
async function submitScore() {
|
||||
submitting.value = true
|
||||
try {
|
||||
await bizUpdate('project', {
|
||||
...row.value,
|
||||
scoreQuality: scoreForm.scoreQuality,
|
||||
scoreSpeed: scoreForm.scoreSpeed,
|
||||
scoreCoordination: scoreForm.scoreCoordination,
|
||||
scoreCompliance: scoreForm.scoreCompliance,
|
||||
ratingScore: totalScore.value
|
||||
})
|
||||
ElMessage.success('评分已保存')
|
||||
scoreOpen.value = false
|
||||
await loadProject()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('保存失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 导航 =====================
|
||||
function goBack() { router.push('/manager/projects') }
|
||||
function goEdit() { router.push(`/manager/projects/edit/${projectId.value}`) }
|
||||
|
||||
// ===================== 预览 =====================
|
||||
function openPreview(url, title) {
|
||||
if (!url) { ElMessage.warning('暂无附件'); return }
|
||||
previewUrl.value = url
|
||||
previewTitle.value = title || '公告预览'
|
||||
previewOpen.value = true
|
||||
}
|
||||
|
||||
// ===================== 初始化 =====================
|
||||
onMounted(async () => {
|
||||
await loadProject()
|
||||
await loadAnnouncements()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 1:1 抄列表页 (manager/Projects.vue) 风格 */
|
||||
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||
.breadcrumb .current { color: #262626; font-weight: 500; }
|
||||
|
||||
.section-title {
|
||||
font-size: 14px; font-weight: 600; color: #262626; margin: 16px 0 12px;
|
||||
padding-left: 8px; border-left: 3px solid var(--brand-primary, #1890ff);
|
||||
}
|
||||
|
||||
/* 项目信息: 11 字段, 一列竖排 (el-form + 文本, 无分隔线) */
|
||||
.info-form { padding-top: 4px; }
|
||||
.info-form :deep(.el-form-item) { margin-bottom: 8px; }
|
||||
.info-form :deep(.el-form-item__label) {
|
||||
font-size: 14px; color: #595959; width: 130px; padding-right: 16px;
|
||||
}
|
||||
.info-value { font-size: 15px; color: #1a1a1a; font-weight: 500; }
|
||||
.info-value.money {
|
||||
font-family: ui-monospace, "Courier New", monospace;
|
||||
}
|
||||
.op-link {
|
||||
color: var(--brand-primary, #1890ff); text-decoration: none;
|
||||
font-weight: 600; cursor: pointer; font-size: 15px;
|
||||
}
|
||||
.op-link:hover { text-decoration: underline; }
|
||||
|
||||
/* 监督员 */
|
||||
.monitor-list { padding: 4px 0; }
|
||||
.monitor-row {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 8px 12px; background: #fafafa; border-radius: 4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.monitor-num { width: 24px; color: #8c8c8c; font-size: 13px; }
|
||||
.monitor-name { flex: 1; font-size: 14px; color: #1a1a1a; }
|
||||
.empty-hint { text-align: center; color: #c0c4cc; padding: 12px 0; font-size: 13px; }
|
||||
|
||||
/* 角色劳务表格 */
|
||||
.info-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.info-table th { background: #fafafa; padding: 10px 12px; text-align: left; color: #1a1a1a; font-weight: 600; border-bottom: 1px solid #f0f0f0; white-space: nowrap; }
|
||||
.info-table td { padding: 10px 12px; border-bottom: 1px solid #f5f5f5; color: #595959; vertical-align: middle; }
|
||||
|
||||
/* 红字提示 */
|
||||
.hint-red { font-size: 12px; color: #ff0000; line-height: 1.7; margin: 4px 0 16px; }
|
||||
|
||||
/* 工具栏 */
|
||||
.toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
||||
|
||||
/* 评分 */
|
||||
.score-num { font-size: 18px; color: var(--brand-primary, #1890ff); font-weight: 700; margin-right: 6px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user