Files
guoju0808/ry-vue3/src/views/sponsor/Projects.vue
T
2026-08-29 17:52:05 +08:00

601 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>