601 lines
30 KiB
Vue
601 lines
30 KiB
Vue
<template>
|
||
<!--
|
||
1:1 抄 doctor/Projects.vue + doctor/Submissions.vue 容器
|
||
SIDEBAR: 项目管理
|
||
列头 (按原型 sponsor-项目管理.html left 顺序):
|
||
复选框 / 项目编号 / 项目名称 / 总场次 / 已执行 / 未执行 /
|
||
总金额 / 可用金额 / 已支付劳务费 / 已支付会务费 /
|
||
执行单位得分 / 执行单位评价 / 支持单位 / 服务机构 / 项目形式 / 是否结题 /
|
||
开始时间 / 结束时间 / 操作
|
||
操作: 建会 / 分配 / 结题 / 查看 / 编辑 / 开通 / 删除公告 / 支持单位 / 执行单位评分
|
||
业务保留: 批量评分/批量结题/批量开通/项目分配 逻辑
|
||
-->
|
||
<div class="page-card">
|
||
<div class="breadcrumb">首页 / 项目管理</div>
|
||
|
||
<el-form inline :model="q" class="filter-form">
|
||
<el-form-item label="项目编号">
|
||
<el-input v-model="q.projectNo" placeholder="输入项目编号" clearable style="width: 160px" />
|
||
</el-form-item>
|
||
<el-form-item label="项目名称">
|
||
<el-input v-model="q.projectName" placeholder="输入项目名称" clearable style="width: 200px" />
|
||
</el-form-item>
|
||
<el-form-item label="项目形式">
|
||
<el-select v-model="q.projectForm" placeholder="全部" clearable style="width: 140px">
|
||
<el-option label="线上" value="线上" />
|
||
<el-option label="线下" value="线下" />
|
||
<el-option label="线上+线下" value="线上+线下" />
|
||
<el-option label="其他" value="其他" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="是否结题">
|
||
<el-select v-model="q.isFinished" placeholder="全部" clearable style="width: 120px">
|
||
<el-option label="已结题" value="1" />
|
||
<el-option label="未结题" value="0" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="是否结算">
|
||
<el-select v-model="q.isSettled" placeholder="全部" clearable style="width: 120px">
|
||
<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>
|
||
|
||
<div class="toolbar">
|
||
<el-button type="primary" @click="openCreate">新建项目</el-button>
|
||
<el-button :disabled="!selected.length" @click="onAssignSelected">项目分配</el-button>
|
||
<el-button :disabled="!selected.length" @click="onBatchRate">批量评分</el-button>
|
||
<el-button :disabled="!selected.length" @click="onBatchFinish">批量结题</el-button>
|
||
<el-button :disabled="!selected.length" @click="onBatchOpen">批量开通</el-button>
|
||
<span class="hint">已选 {{ selected.length }} 项</span>
|
||
</div>
|
||
|
||
<GrTable :data="rows" v-loading="loading" stripe border @selection-change="onSelect"
|
||
:main-cols="['projectNo', 'projectName']"
|
||
>
|
||
<el-table-column type="selection" width="48" />
|
||
<el-table-column prop="projectNo" label="项目编号" min-width="170" fixed>
|
||
<template #default="{ row }">
|
||
<el-link type="primary" :underline="false" @click="openDetail(row)">{{ row.projectNo }}</el-link></template></el-table-column>
|
||
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<el-link type="primary" :underline="false" @click="openDetail(row)">{{ row.projectName }}</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="totalSessions" label="总场次/总期数" width="120" 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 label="总金额" width="120" align="right"><template #default="{ row }">¥{{ formatMoney(row.totalAmount) }}</template></el-table-column>
|
||
<el-table-column label="可用金额" width="120" align="right"><template #default="{ row }">¥{{ formatMoney(row.availableAmount) }}</template></el-table-column>
|
||
<el-table-column label="已支付劳务费" width="120" align="right"><template #default="{ row }">¥{{ formatMoney(row.paidLaborAmount) }}</template></el-table-column>
|
||
<el-table-column label="已支付会务费" width="120" align="right"><template #default="{ row }">¥{{ formatMoney(row.paidMeetingAmount) }}</template></el-table-column>
|
||
<el-table-column prop="managerScore" label="执行单位得分(合规)" width="150" align="center">
|
||
<template #default="{ row }">
|
||
<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="160" align="center">
|
||
<template #default="{ row }">
|
||
<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>
|
||
<el-table-column prop="orgName" label="支持公司" min-width="140" show-overflow-tooltip />
|
||
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
|
||
<el-table-column label="是否结题" width="90" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.isFinished === '1' ? 'success' : 'info'" size="small" disable-transitions>
|
||
{{ row.isFinished === '1' ? '已结题' : '未结题' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="startTime" label="项目开始时间" min-width="160" />
|
||
<el-table-column prop="endTime" label="项目结束时间" min-width="160" />
|
||
<el-table-column label="操作" width="480" fixed="right">
|
||
<template #default="{ row }"><div class="table-actions">
|
||
<el-link :underline="false" type="primary" @click="onCreateMeeting(row)">建会</el-link>
|
||
<el-link :underline="false" type="primary" @click="openAssign(row)">分配</el-link>
|
||
<el-link :underline="false" type="primary" @click="onFinish(row)">结题</el-link>
|
||
<el-link :underline="false" type="primary" @click="openDetail(row)">查看</el-link>
|
||
<el-link :underline="false" type="primary" @click="openEdit(row)">编辑</el-link>
|
||
<el-link :underline="false" type="primary" @click="onOpen(row)">开通</el-link>
|
||
<el-link :underline="false" type="danger" @click="onDeleteNotice(row)">删除公告</el-link>
|
||
<el-link :underline="false" type="primary" @click="onSupportUnit(row)">支持单位</el-link>
|
||
<el-link :underline="false" type="primary" @click="openRate(row)">执行单位评分</el-link>
|
||
</div></template>
|
||
</el-table-column>
|
||
</GrTable>
|
||
|
||
<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"
|
||
style="margin-top: 12px; text-align: right;"
|
||
/>
|
||
|
||
<!-- 新建/编辑项目 dialog (跟 doctor/Projects.vue 风格统一) -->
|
||
<el-dialog v-model="formOpen" :title="formMode === 'create' ? '新建项目' : '编辑项目'" width="600px">
|
||
<el-form :model="form" label-width="100px">
|
||
<el-form-item label="项目编号"><el-input v-model="form.projectNo" /></el-form-item>
|
||
<el-form-item label="项目名称"><el-input v-model="form.projectName" /></el-form-item>
|
||
<el-form-item label="项目形式">
|
||
<el-select v-model="form.projectForm" placeholder="请选择" clearable>
|
||
<el-option label="线上" value="线上" />
|
||
<el-option label="线下" value="线下" />
|
||
<el-option label="线上+线下" value="线上+线下" />
|
||
<el-option label="其他" value="其他" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="总场次"><el-input-number v-model="form.totalSessions" :min="0" :max="9999" /></el-form-item>
|
||
<el-form-item label="管理费"><el-input-number v-model="form.manageFee" :min="0" :precision="2" /></el-form-item>
|
||
<el-form-item label="开始时间"><el-date-picker v-model="form.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" /></el-form-item>
|
||
<el-form-item label="结束时间"><el-date-picker v-model="form.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" /></el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="formOpen = false">取消</el-button>
|
||
<el-button type="primary" @click="submitForm">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 评分 dialog -->
|
||
<el-dialog v-model="rateOpen" title="执行单位评分" width="520px">
|
||
<el-form :model="rateForm" label-width="100px">
|
||
<el-form-item label="项目编号 / 名称">
|
||
<span>{{ rateForm.projectNo }} / {{ rateForm.projectName }}</span>
|
||
</el-form-item>
|
||
<el-form-item label="评价维度">
|
||
<div class="score-row"><span>履约质量:</span><el-rate v-model="rateForm.q1" :max="5" /></div>
|
||
<div class="score-row"><span>时效响应:</span><el-rate v-model="rateForm.q2" :max="5" /></div>
|
||
<div class="score-row"><span>配合度:</span><el-rate v-model="rateForm.q3" :max="5" /></div>
|
||
<div class="score-row"><span>合规安全:</span><el-rate v-model="rateForm.q4" :max="5" /></div>
|
||
</el-form-item>
|
||
<el-form-item label="评价备注"><el-input v-model="rateForm.remark" type="textarea" rows="3" /></el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="rateOpen = false">取消</el-button>
|
||
<el-button type="primary" @click="submitRate">提交评分</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 分配 dialog -->
|
||
<el-dialog v-model="assignOpen" title="项目分配" width="520px">
|
||
<el-form :model="assignForm" label-width="100px">
|
||
<el-form-item label="项目编号 / 名称">
|
||
<span>{{ assignForm.projectNo }} / {{ assignForm.projectName }}</span>
|
||
</el-form-item>
|
||
<el-form-item label="分配给监察员">
|
||
<el-select v-model="assignForm.supervisor" filterable placeholder="请选择监察员" style="width:100%">
|
||
<el-option v-for="p in personList" :key="p.personId" :label="p.name" :value="p.personId" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="监察要点"><el-input v-model="assignForm.supervisionPoint" type="textarea" rows="3" /></el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="assignOpen = false">取消</el-button>
|
||
<el-button type="primary" @click="submitAssign">确认分配</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 详情 dialog (跟 doctor/Projects.vue el-descriptions 同款) -->
|
||
<el-dialog v-model="detailOpen" title="项目详情" width="640px">
|
||
<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.sponsorAdminUserName || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="公司类型" :span="2">支持方</el-descriptions-item>
|
||
<el-descriptions-item label="总场次/总期数">{{ detail.totalSessions || 0 }}</el-descriptions-item>
|
||
<el-descriptions-item label="已执行/未执行">{{ detail.doneSessions || 0 }} / {{ detail.todoSessions || 0 }}</el-descriptions-item>
|
||
<el-descriptions-item label="总金额">¥{{ formatMoney(detail.totalAmount) }}</el-descriptions-item>
|
||
<el-descriptions-item label="可用金额">¥{{ formatMoney(detail.availableAmount) }}</el-descriptions-item>
|
||
<el-descriptions-item label="已支付劳务费">¥{{ formatMoney(detail.paidLaborAmount) }}</el-descriptions-item>
|
||
<el-descriptions-item label="已支付会务费">¥{{ formatMoney(detail.paidMeetingAmount) }}</el-descriptions-item>
|
||
<el-descriptions-item label="是否结题">
|
||
<el-tag :type="detail.isFinished === '1' ? 'success' : 'info'" disable-transitions>
|
||
{{ detail.isFinished === '1' ? '已结题' : '未结题' }}
|
||
</el-tag>
|
||
</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>
|
||
|
||
<!-- ============= 评分详情 modal(只读 4 维度明细, 点列表列查看) ============= -->
|
||
<el-dialog v-model="scoreDetailOpen" :title="scoreDetailTitle" width="520px" :show-close="false">
|
||
<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>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import { bizList, bizAdd, bizUpdate } from '@/api/public'
|
||
import GrTable from '@/components/GrTable.vue'
|
||
import request from '@/utils/request'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
|
||
const rows = ref([])
|
||
const loading = ref(false)
|
||
const q = reactive({ projectNo: '', projectName: '', projectForm: '', isFinished: '', isSettled: '' })
|
||
const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
|
||
const selected = ref([])
|
||
|
||
const formOpen = ref(false)
|
||
const formMode = ref('create')
|
||
const form = reactive({
|
||
projectId: null, projectNo: '', projectName: '', projectForm: '',
|
||
totalSessions: 0, manageFee: 0, startTime: '', endTime: ''
|
||
})
|
||
|
||
const rateOpen = ref(false)
|
||
const rateForm = reactive({ projectId: null, projectNo: '', projectName: '', q1: 0, q2: 0, q3: 0, q4: 0, remark: '' })
|
||
|
||
// ========== 评分详情 (只读 4 维度明细, 点列表 score 列查看) ==========
|
||
const scoreDetailOpen = ref(false)
|
||
const scoreDetailTitle = ref('评分详情')
|
||
const scoreDetail = reactive({ qualityScore: null, responseScore: null, cooperationScore: null, complianceScore: null })
|
||
const scoreDetailTotal = ref('—')
|
||
|
||
const assignOpen = ref(false)
|
||
const assignForm = reactive({ projectId: '', projectNo: '', projectName: '', supervisor: '', supervisionPoint: '' })
|
||
const personList = ref([])
|
||
|
||
const detail = ref({})
|
||
const detailOpen = ref(false)
|
||
|
||
function formatMoney(v) { return Number(v || 0).toFixed(2) }
|
||
// 当前本地时间 "yyyy-MM-dd HH:mm:ss" (发布用, 与后端 GMT+8 一致; 不能用 toISOString 的 UTC)
|
||
function nowLocalDatetime() {
|
||
const d = new Date()
|
||
const p = n => String(n).padStart(2, '0')
|
||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
||
}
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
// sponsor 专属: GET /business/project/sponsorList (后端按 mainUid 团队可见性过滤)
|
||
const { data } = await request.get('/business/project/sponsorList', {
|
||
params: { ...q, pageNum: page.pageNum, pageSize: page.pageSize }
|
||
})
|
||
const payload = data?.data || data
|
||
rows.value = payload?.rows || []
|
||
page.total = payload?.total || 0
|
||
} catch (e) { rows.value = []; page.total = 0 }
|
||
finally { loading.value = false }
|
||
}
|
||
|
||
async function loadPersons() {
|
||
try {
|
||
// sponsor 团队下"监察员" (SUB 子账号), 排除主账号
|
||
const r = await bizList('person', { pageNum: 1, pageSize: 100, unitType: 'sponsor' })
|
||
personList.value = ((r.data && r.data.rows) || r.rows || []).filter(p => p.userId && p.accountType === 'SUB')
|
||
} catch (e) { personList.value = [] }
|
||
}
|
||
|
||
// 读 URL query 写入 q (Home KPI 卡跳转时带 ?isSettled=Y / ?isFinished=1, 让列表页自动应用筛选)
|
||
// 只读不改 URL — Home 是 source of truth, 列表页内 reset()/search 不反向写 URL
|
||
const route = useRoute()
|
||
function readQueryFromRoute() {
|
||
const q2 = route.query
|
||
if (q2.isFinished != null && q2.isFinished !== '') q.isFinished = String(q2.isFinished)
|
||
if (q2.isSettled != null && q2.isSettled !== '') q.isSettled = String(q2.isSettled)
|
||
}
|
||
|
||
function reset() {
|
||
Object.assign(q, { projectNo: '', projectName: '', projectForm: '', isFinished: '', isSettled: '' })
|
||
page.pageNum = 1
|
||
load()
|
||
}
|
||
|
||
function onSelect(arr) { selected.value = arr }
|
||
|
||
function openDetail(row) { detail.value = row; detailOpen.value = true }
|
||
|
||
function openCreate() {
|
||
formMode.value = 'create'
|
||
Object.assign(form, { projectId: null, projectNo: '', projectName: '', projectForm: '', totalSessions: 0, manageFee: 0, startTime: '', endTime: '' })
|
||
formOpen.value = true
|
||
}
|
||
function openEdit(row) {
|
||
formMode.value = 'edit'
|
||
Object.assign(form, {
|
||
projectId: row.projectId, projectNo: row.projectNo, projectName: row.projectName,
|
||
projectForm: row.projectForm, totalSessions: row.totalSessions, manageFee: row.manageFee,
|
||
startTime: row.startTime || '', endTime: row.endTime || ''
|
||
})
|
||
formOpen.value = true
|
||
}
|
||
function onCreateMeeting(row) {
|
||
ElMessage.info(`已为项目「${row.projectName}」启动建会流程`)
|
||
}
|
||
|
||
function onSupportUnit(row) {
|
||
if (!row.sponsorOrgId) { ElMessage.warning('该项目未关联支持方'); return }
|
||
ElMessage.info(`查看支持方: ${row.sponsorAdminUserName || row.sponsorOrgId}`)
|
||
}
|
||
async function onDeleteNotice(row) {
|
||
try { await ElMessageBox.confirm(`确定删除「${row.projectName}」的所有公告吗?删除后无法恢复`, '删除公告', { type: 'warning' }) } catch { return }
|
||
try {
|
||
await bizUpdate('project', { projectId: row.projectId, noticeDeleted: '1' })
|
||
ElMessage.success('公告已删除')
|
||
load()
|
||
} catch (e) { ElMessage.error(e?.msg || '删除失败') }
|
||
}
|
||
async function submitForm() {
|
||
if (!form.projectNo || !form.projectName) { ElMessage.warning('请填写项目编号与项目名称'); return }
|
||
try {
|
||
if (formMode.value === 'create') await bizAdd('project', { ...form })
|
||
else await bizUpdate('project', form)
|
||
ElMessage.success('保存成功')
|
||
formOpen.value = false
|
||
load()
|
||
} catch (e) { ElMessage.error(e?.msg || '保存失败') }
|
||
}
|
||
|
||
function openRate(row) {
|
||
Object.assign(rateForm, { projectId: row.projectId, projectNo: row.projectNo, projectName: row.projectName, q1: 0, q2: 0, q3: 0, q4: 0, remark: '' })
|
||
rateOpen.value = true
|
||
}
|
||
// 点击列表「执行单位得分(合规)/评价(支持方)」列 → 只读显示该角色 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) { /* 拉取失败保持空, 弹窗显示 - */ }
|
||
}
|
||
async function submitRate() {
|
||
if (!rateForm.q1 && !rateForm.q2 && !rateForm.q3 && !rateForm.q4) {
|
||
ElMessage.warning('请至少完成一项评分'); return
|
||
}
|
||
// 计算 4 维平均 (任意维度填了就参与平均)
|
||
const scores = [rateForm.q1, rateForm.q2, rateForm.q3, rateForm.q4].map(v => Number(v))
|
||
const filled = scores.filter(v => v > 0)
|
||
const avg = filled.length ? (filled.reduce((s, v) => s + v, 0) / filled.length).toFixed(1) : null
|
||
try {
|
||
// 1. 写明细到 biz_project_rating (rater_role='sponsor', 后端自动填 raterId)
|
||
await request.post('/business/project/rate', {
|
||
projectId: rateForm.projectId,
|
||
projectNo: rateForm.projectNo,
|
||
projectName: rateForm.projectName,
|
||
raterRole: 'sponsor',
|
||
qualityScore: rateForm.q1,
|
||
responseScore: rateForm.q2,
|
||
cooperationScore: rateForm.q3,
|
||
complianceScore: rateForm.q4,
|
||
remark: rateForm.remark
|
||
})
|
||
// 聚合分由后端 /rate 重算, 前端不再单独写 sponsor_score
|
||
ElMessage.success(`评分已提交, 平均分:${avg ?? '-'}`)
|
||
rateOpen.value = false
|
||
load()
|
||
} catch (e) { ElMessage.error(e?.msg || '提交失败') }
|
||
}
|
||
|
||
async function onBatchRate() {
|
||
if (!selected.value.length) return
|
||
let ok = 0
|
||
for (const r of selected.value) {
|
||
try {
|
||
await bizAdd('projectRating', { projectId: r.projectId, projectNo: r.projectNo, projectName: r.projectName, q1: 3, q2: 3, q3: 3, q4: 3, remark: '批量评分' })
|
||
ok++
|
||
} catch {}
|
||
}
|
||
ElMessage.success(`已批量评分 ${ok} 项`)
|
||
}
|
||
|
||
async function onBatchFinish() {
|
||
if (!selected.value.length) return
|
||
try { await ElMessageBox.confirm(`确定对选中的 ${selected.value.length} 个项目批量结题吗?`, '批量结题', { type: 'warning' }) } catch { return }
|
||
let ok = 0
|
||
for (const r of selected.value) {
|
||
try { await bizUpdate('project', { projectId: r.projectId, isFinished: '1' }); ok++ } catch {}
|
||
}
|
||
ElMessage.success(`已批量结题 ${ok} 项`)
|
||
load()
|
||
}
|
||
|
||
async function onBatchOpen() {
|
||
if (!selected.value.length) return
|
||
try { await ElMessageBox.confirm(`确定对选中的 ${selected.value.length} 个项目批量开通吗?`, '批量开通', { type: 'warning' }) } catch { return }
|
||
let ok = 0
|
||
for (const r of selected.value) {
|
||
try { await bizUpdate('project', { projectId: r.projectId, isPublished: '1', publishTime: nowLocalDatetime() }); ok++ } catch {}
|
||
}
|
||
ElMessage.success(`已批量开通 ${ok} 项`)
|
||
load()
|
||
}
|
||
|
||
async function onFinish(row) {
|
||
try { await ElMessageBox.confirm(`确定将「${row.projectName}」结题吗?`, '结题', { type: 'warning' }) } catch { return }
|
||
try {
|
||
await bizUpdate('project', { projectId: row.projectId, isFinished: '1' })
|
||
ElMessage.success('已结题')
|
||
load()
|
||
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
|
||
}
|
||
|
||
async function onOpen(row) {
|
||
try { await ElMessageBox.confirm(`确定开通「${row.projectName}」吗?`, '开通', { type: 'warning' }) } catch { return }
|
||
try {
|
||
await bizUpdate('project', { projectId: row.projectId, isPublished: '1', publishTime: nowLocalDatetime() })
|
||
ElMessage.success('已开通')
|
||
load()
|
||
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
|
||
}
|
||
|
||
function onAssignSelected() {
|
||
if (!selected.value.length) { ElMessage.warning('请先勾选项目'); return }
|
||
ElMessage.info(`已为 ${selected.value.length} 个项目启动批量分配流程,请逐个确认`)
|
||
}
|
||
|
||
function openAssign(row) {
|
||
assignForm.projectId = row.projectId
|
||
assignForm.projectNo = row.projectNo
|
||
assignForm.projectName = row.projectName
|
||
assignForm.supervisor = ''
|
||
assignForm.supervisionPoint = ''
|
||
assignOpen.value = true
|
||
}
|
||
async function submitAssign() {
|
||
if (!assignForm.supervisor) { ElMessage.warning('请选择监察员'); return }
|
||
try {
|
||
const person = personList.value.find(p => String(p.personId) === String(assignForm.supervisor))
|
||
if (!person || !person.userId) { ElMessage.error('未找到监察员账号'); return }
|
||
// sponsor 专属: POST /business/project/sponsorAssign (BizProjectSponsorAssign 字段)
|
||
await request.post('/business/project/sponsorAssign', {
|
||
projectId: assignForm.projectId,
|
||
projectNo: assignForm.projectNo,
|
||
projectName: assignForm.projectName,
|
||
monitorUserId: person.userId,
|
||
assignPoints: assignForm.supervisionPoint || '',
|
||
assignDesc: ''
|
||
})
|
||
ElMessage.success('分配成功')
|
||
assignOpen.value = false
|
||
} catch (e) { ElMessage.error(e?.msg || '分配失败') }
|
||
}
|
||
|
||
onMounted(() => { readQueryFromRoute(); load(); loadPersons() })
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 1:1 抄 doctor/Projects.vue + doctor/Submissions.vue */
|
||
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||
.toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
|
||
.hint { color: #909399; font-size: 12px; margin-left: 8px; }
|
||
.score-row { display: flex; align-items: center; gap: 8px; margin-bottom: 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); 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; }
|
||
|
||
/* ========================================
|
||
移动端适配 (≤768px)
|
||
- 卡片 padding 收窄
|
||
- 工具栏横向滚动 (6 个按钮铺不下, 用 overflow-x)
|
||
- filter-form: 保持 label 左 + input 右横向布局
|
||
- 表格字号收紧
|
||
======================================== */
|
||
@media (max-width: 768px) {
|
||
/* 卡片 padding */
|
||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||
|
||
/* 工具栏: 横向滚动 (按钮多时不换行) */
|
||
.toolbar {
|
||
flex-wrap: nowrap !important;
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
padding-bottom: 6px;
|
||
margin-bottom: 8px !important;
|
||
scrollbar-width: thin;
|
||
}
|
||
.toolbar::-webkit-scrollbar { height: 4px; }
|
||
.toolbar::-webkit-scrollbar-thumb { background: var(--brand-slate-200); border-radius: 2px; }
|
||
.toolbar :deep(.action-btn),
|
||
.toolbar :deep(.el-button) {
|
||
flex-shrink: 0;
|
||
font-size: 12px !important;
|
||
padding: 0 10px !important;
|
||
height: 30px !important;
|
||
}
|
||
|
||
/* filter-form: label 与输入框横向 (label 左, input 右) */
|
||
.filter-form {
|
||
display: flex !important;
|
||
flex-direction: column !important;
|
||
align-items: stretch !important;
|
||
gap: 10px !important;
|
||
}
|
||
.filter-form :deep(.el-form-item) {
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
margin-right: 0 !important;
|
||
margin-bottom: 0 !important;
|
||
}
|
||
.filter-form :deep(.el-form-item__label) {
|
||
float: none !important;
|
||
width: auto !important;
|
||
min-width: 80px !important;
|
||
text-align: right !important;
|
||
padding: 0 8px 0 0 !important;
|
||
font-size: 13px !important;
|
||
color: var(--el-text-color-regular) !important;
|
||
line-height: 32px !important;
|
||
height: 32px !important;
|
||
}
|
||
.filter-form :deep(.el-form-item__content) {
|
||
margin-left: 0 !important;
|
||
line-height: 32px !important;
|
||
flex: 1 !important;
|
||
min-width: 0 !important;
|
||
}
|
||
|
||
/* 强制所有控件全宽, 覆盖 inline 260px / 140px / 120px */
|
||
.filter-form :deep(.el-select),
|
||
.filter-form :deep(.el-input),
|
||
.filter-form :deep(.el-date-editor),
|
||
.filter-form :deep(.el-button),
|
||
.filter-form :deep(.el-cascader) {
|
||
width: 100% !important;
|
||
min-width: 0 !important;
|
||
margin-left: 0 !important;
|
||
margin-right: 0 !important;
|
||
display: block !important;
|
||
}
|
||
.filter-form :deep(.el-button + .el-button) {
|
||
margin-top: 8px !important;
|
||
}
|
||
|
||
/* 表格字号收紧 */
|
||
:deep(.el-table) { font-size: 12px !important; }
|
||
}
|
||
</style> |