@@ -60,7 +60,7 @@
|
||||
<el-option label="已结算" value="Y" /><el-option label="未结算" value="N" />
|
||||
</el-select>
|
||||
</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-item><el-button type="primary" @click="load">查询</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="toolbar">
|
||||
@@ -91,23 +91,27 @@
|
||||
>
|
||||
<el-table-column type="selection" width="44" />
|
||||
<el-table-column prop="projectNo" label="项目编号" width="170" fixed="left" />
|
||||
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="totalSessions" label="总场次/总期数" width="100" align="center" />
|
||||
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link :underline="false" type="primary" @click="viewDetail(row)">{{ row.projectName }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalSessions" label="总场次/总期数" width="130" align="center" />
|
||||
<el-table-column prop="doneSessions" label="已执行" width="80" align="center" />
|
||||
<el-table-column prop="todoSessions" label="未执行" width="80" align="center" />
|
||||
<el-table-column prop="totalAmount" label="总金额" width="130" align="right" :formatter="fmtMoney" />
|
||||
<el-table-column prop="availableAmount" label="可用金额" width="130" align="right" :formatter="fmtMoney" />
|
||||
<el-table-column prop="paidLaborAmount" label="已支付劳务费" width="140" align="right" :formatter="fmtMoney" />
|
||||
<el-table-column prop="paidMeetingAmount" label="已支付会务费" width="140" align="right" :formatter="fmtMoney" />
|
||||
<el-table-column prop="managerScore" label="执行单位评分(合规)" width="140" align="center">
|
||||
<el-table-column prop="managerScore" label="执行单位评分(合规)" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.managerScore != null">{{ row.managerScore }}</span>
|
||||
<el-link v-if="row.managerScore != null" :underline="false" type="primary" @click="openScoreDetail(row, 'manager')">{{ row.managerScore }}</el-link>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sponsorScore" label="执行单位评分(支持)" width="140" align="center">
|
||||
<el-table-column prop="sponsorScore" label="执行单位评分(支持)" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.sponsorScore != null">{{ row.sponsorScore }}</span>
|
||||
<el-link v-if="row.sponsorScore != null" :underline="false" type="primary" @click="openScoreDetail(row, 'sponsor')">{{ row.sponsorScore }}</el-link>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -120,12 +124,12 @@
|
||||
<el-tag v-else type="warning">未结题</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="startTime" label="项目开始时间" width="120" align="center" :formatter="fmtDate" />
|
||||
<el-table-column prop="endTime" label="项目结束时间" width="120" align="center" :formatter="fmtDate" />
|
||||
<el-table-column prop="startTime" label="项目开始时间" width="170" align="center" :formatter="fmtDateTime" />
|
||||
<el-table-column prop="endTime" label="项目结束时间" width="170" align="center" :formatter="fmtDateTime" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }"><div class="table-actions">
|
||||
<div class="op-cell">
|
||||
<el-link :underline="false" type="primary" @click="doCreateMeeting(row)">建会</el-link>
|
||||
<el-link v-if="!isFinishedRow(row)" :underline="false" type="primary" @click="doCreateMeeting(row)">建会</el-link>
|
||||
<el-link :underline="false" type="primary" :disabled="isFinishedRow(row)" @click="doAssign(row)">分配</el-link>
|
||||
<el-dropdown trigger="hover" @command="(cmd) => onAction(cmd, row)">
|
||||
<el-link :underline="false" type="primary" class="op-dropdown">
|
||||
@@ -194,7 +198,8 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- ============= 单行结题 modal ============= -->
|
||||
<el-dialog v-model="closeModalOpen" title="您确定要结题吗?" width="420px" :show-close="false">
|
||||
<el-dialog v-model="closeModalOpen" title="结题确认" width="420px" :show-close="false">
|
||||
<p style="color:#606266">你确定要结题吗?</p>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="confirmClose">确定</el-button>
|
||||
<el-button @click="closeModalOpen=false">取消</el-button>
|
||||
@@ -240,6 +245,29 @@
|
||||
<el-button @click="singleScoreModalOpen=false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ============= 评分详情 modal(只读 4 维度明细, 点列表列查看) ============= -->
|
||||
<el-dialog v-model="scoreDetailOpen" :title="scoreDetailTitle" width="520px" :show-close="false">
|
||||
<table class="score-table" style="width:100%;border-collapse:collapse;font-size:13px">
|
||||
<thead><tr style="background:#fafafa">
|
||||
<th style="width:110px;padding:10px 12px;text-align:left">评价维度</th>
|
||||
<th style="padding:10px 12px;text-align:left">评价内容(简洁版)</th>
|
||||
<th style="width:110px;padding:10px 12px;text-align:center">平均得分</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">履约质量</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">服务/活动效果达标度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center">{{ scoreDetail.qualityScore ?? '-' }}</td></tr>
|
||||
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">时效响应</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">执行 & 售后响应速度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center">{{ scoreDetail.responseScore ?? '-' }}</td></tr>
|
||||
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">配合度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">沟通配合 & 问题处理</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center">{{ scoreDetail.cooperationScore ?? '-' }}</td></tr>
|
||||
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">合规安全</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">流程合规 & 无安全事故</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center">{{ scoreDetail.complianceScore ?? '-' }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="margin-top:12px;text-align:right;font-size:13px;color:#303133">
|
||||
平均分: <b>{{ scoreDetailTotal }}</b>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="scoreDetailOpen=false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -265,8 +293,12 @@ const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize }
|
||||
// 时间范围含两端: 开始日 00:00:00 ~ 结束日 23:59:59
|
||||
if (params.startTime) params.startTime = params.startTime + ' 00:00:00'
|
||||
if (params.endTime) params.endTime = params.endTime + ' 23:59:59'
|
||||
// axios 拦截器已将 TableDataInfo 归一化为 {code, msg, data: {total, rows}}
|
||||
const { data } = await bizList('project', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
|
||||
const { data } = await bizList('project', params)
|
||||
rows.value = data?.rows || []
|
||||
page.total = data?.total || 0
|
||||
} catch (e) {
|
||||
@@ -302,13 +334,9 @@ function doEdit(row) { router.push(`/manager/projects/edit/${row.projectId}`) }
|
||||
async function doDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认删除项目"${row.projectName}"?\n项目编号: ${row.projectNo}\n\n` +
|
||||
`此操作将级联软删除:\n` +
|
||||
`· 项目主表 + 方案 + 执行方分配 + 支持方分配 + 评分 (5 张表)\n` +
|
||||
`· 项目下所有会议 + 会议附件 + 参会人 + 监督员 + 执行人 + 审计日志 (6 张表)\n\n` +
|
||||
`数据保留在库, 不再展示给前台用户, 审计追溯可查。`,
|
||||
`确认删除项目"${row.projectName}"?`,
|
||||
'删除确认',
|
||||
{ type: 'error', confirmButtonText: '确认软删除', cancelButtonText: '取消' }
|
||||
{ type: 'error', confirmButtonText: '确认删除', cancelButtonText: '取消' }
|
||||
)
|
||||
await bizDelete('project', row.projectId)
|
||||
ElMessage.success('已删除')
|
||||
@@ -325,11 +353,7 @@ async function onBatchDelete() {
|
||||
if (!selection.value.length) return ElMessage.warning('请先勾选项目')
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认批量软删除选中的 ${selection.value.length} 个项目?\n\n` +
|
||||
`此操作将级联软删除每个项目下的:\n` +
|
||||
`· 5 张项目表 (主表/方案/分配/支持方/评分)\n` +
|
||||
`· 6 张会议表 (会议本身 + 附件 + 参会人 + 监督员 + 执行人 + 审计日志)\n\n` +
|
||||
`数据保留, 不再展示给前台用户。`,
|
||||
`确认删除选中的 ${selection.value.length} 个项目?`,
|
||||
'批量删除确认',
|
||||
{ type: 'error', confirmButtonText: `确认删除 ${selection.value.length} 个`, cancelButtonText: '取消' }
|
||||
)
|
||||
@@ -337,7 +361,7 @@ async function onBatchDelete() {
|
||||
const ids = selection.value.map(r => r.projectId)
|
||||
try {
|
||||
await bizDelete('project', ids)
|
||||
ElMessage.success(`已批量软删除 ${ids.length} 个项目`)
|
||||
ElMessage.success(`已删除 ${ids.length} 个项目`)
|
||||
selection.value = []
|
||||
load()
|
||||
} catch (e) {
|
||||
@@ -435,9 +459,7 @@ async function confirmSingleScore() {
|
||||
const avg = ((f.qualityScore + f.responseScore + f.cooperationScore + f.complianceScore) / 4).toFixed(1)
|
||||
const projectId = singleScoreTargetRow.value.projectId
|
||||
try {
|
||||
// 1. 写聚合分到 biz_project.manager_score
|
||||
await bizUpdate('project', { projectId, managerScore: Number(avg) })
|
||||
// 2. 写 4 维度明细到 biz_project_rating (rater_role='manager', 后端自动填 raterId)
|
||||
// 写 4 维度明细到 biz_project_rating (rater_role='manager', 后端自动填 raterId + 重算聚合分)
|
||||
await request.post('/business/project/rate', {
|
||||
projectId,
|
||||
raterRole: 'manager',
|
||||
@@ -453,7 +475,32 @@ async function confirmSingleScore() {
|
||||
ElMessage.error(e?.msg || '评分失败')
|
||||
}
|
||||
}
|
||||
function viewDetail(row) { router.push(`/manager/projects/detail/${row.projectId}`) }
|
||||
function viewDetail(row) {
|
||||
// admin 复用本列表页, 详情须跳到 /admin/projects/detail (否则被 beforeEach 角色守卫拦截)
|
||||
const base = userStore.role === 'admin' ? '/admin' : '/manager'
|
||||
router.push(`${base}/projects/detail/${row.projectId}`)
|
||||
}
|
||||
|
||||
// 点击列表「执行单位评分(合规/支持)」列 → 只读显示该角色 4 维度评分明细
|
||||
async function openScoreDetail(row, role) {
|
||||
scoreDetailTitle.value = role === 'sponsor' ? '执行单位评分详情(支持方)' : '执行单位评分详情(合规)'
|
||||
Object.assign(scoreDetail, { qualityScore: null, responseScore: null, cooperationScore: null, complianceScore: null })
|
||||
scoreDetailTotal.value = role === 'sponsor' ? (row.sponsorScore ?? '—') : (row.managerScore ?? '—')
|
||||
scoreDetailOpen.value = true
|
||||
try {
|
||||
const r = await request({ url: '/business/project/ratings', method: 'get', params: { projectId: row.projectId } })
|
||||
const list = (r.data && (Array.isArray(r.data) ? r.data : r.data.rows)) || r.rows || []
|
||||
const items = list.filter(x => String(x.projectId) === String(row.projectId) && x.raterRole === role)
|
||||
if (items.length) {
|
||||
const n = items.length
|
||||
const dimAvg = (key) => (items.reduce((s, x) => s + (Number(x[key]) || 0), 0) / n).toFixed(1)
|
||||
scoreDetail.qualityScore = dimAvg('qualityScore')
|
||||
scoreDetail.responseScore = dimAvg('responseScore')
|
||||
scoreDetail.cooperationScore = dimAvg('cooperationScore')
|
||||
scoreDetail.complianceScore = dimAvg('complianceScore')
|
||||
}
|
||||
} catch (e) { /* 拉取失败保持空, 弹窗显示 - */ }
|
||||
}
|
||||
|
||||
// 金额格式化: 1234567.5 -> "1,234,567.50"
|
||||
function fmtMoney(row, col, v) {
|
||||
@@ -462,12 +509,10 @@ function fmtMoney(row, col, v) {
|
||||
if (isNaN(n)) return String(v)
|
||||
return n.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 2 })
|
||||
}
|
||||
// 日期格式化: "2026-08-09" 或 "2026-08-09 10:30:00" -> "2026.08.09"
|
||||
function fmtDate(row, col, v) {
|
||||
// 日期时间格式化: 展示完整 yyyy-MM-dd HH:mm:ss (后端 @JsonFormat 已序列化, 直接透传)
|
||||
function fmtDateTime(row, col, 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
|
||||
return String(v)
|
||||
}
|
||||
|
||||
const selection = ref([])
|
||||
@@ -491,6 +536,12 @@ const singleScoreModalOpen = ref(false)
|
||||
const singleScoreTargetRow = ref(null)
|
||||
const singleScoreForm = reactive({ qualityScore: 0, responseScore: 0, cooperationScore: 0, complianceScore: 0 })
|
||||
|
||||
// ========== 评分详情 (只读 4 维度明细, 点列表 score 列查看) ==========
|
||||
const scoreDetailOpen = ref(false)
|
||||
const scoreDetailTitle = ref('评分详情')
|
||||
const scoreDetail = reactive({ qualityScore: null, responseScore: null, cooperationScore: null, complianceScore: null })
|
||||
const scoreDetailTotal = ref('—')
|
||||
|
||||
// 新建项目已迁到独立页面 /manager/projects/new (ManagerProjectsNew.vue)
|
||||
function openNewProject() {
|
||||
router.push('/manager/projects/new')
|
||||
@@ -568,10 +619,6 @@ async function confirmBatch() {
|
||||
// 共享评分: 所有项目用同一组 4 维度
|
||||
for (const r of selection.value) {
|
||||
try {
|
||||
await bizUpdate('project', {
|
||||
projectId: r.projectId,
|
||||
managerScore: Number(total)
|
||||
})
|
||||
await request.post('/business/project/rate', {
|
||||
projectId: r.projectId,
|
||||
raterRole: 'manager',
|
||||
|
||||
Reference in New Issue
Block a user