@@ -3,8 +3,8 @@
|
||||
<!-- 面包屑 (按角色显示: sponsor → 我的项目; manager/admin → 项目管理) -->
|
||||
<div class="breadcrumb">
|
||||
首页 /
|
||||
<span v-if="userStore?.user?.roleType === 'sponsor'">我的项目</span>
|
||||
<span v-else-if="userStore?.user?.roleType === 'executor'">项目列表</span>
|
||||
<span v-if="roleType === 'sponsor'">我的项目</span>
|
||||
<span v-else-if="roleType === 'executor'">项目列表</span>
|
||||
<span v-else>项目管理</span>
|
||||
/ <span class="current">项目详情</span>
|
||||
</div>
|
||||
@@ -25,31 +25,41 @@
|
||||
<el-col :span="12"><el-form-item label="项目形式"><span class="info-value">{{ row.projectForm || '-' }}</span></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12"><el-form-item label="项目开始时间"><span class="info-value">{{ fmtDate(row.startTime) }}</span></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="项目结束时间"><span class="info-value">{{ fmtDate(row.endTime) }}</span></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="项目开始时间"><span class="info-value">{{ fmtDateTime(row.startTime) }}</span></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="项目结束时间"><span class="info-value">{{ fmtDateTime(row.endTime) }}</span></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12"><el-form-item label="是否招标项目"><span class="info-value">{{ row.isBidProject === 'Y' ? '是' : '否' }}</span></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="项目负责人"><span class="info-value">{{ row.leadUserName || '-' }}</span></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12"><el-form-item label="支持公司"><span class="info-value">{{ row.sponsorOrgName || '-' }}</span></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="项目评价"><span class="info-value">{{ fmtScore(row.managerScore) }}</span></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="支持单位"><span class="info-value">{{ row.sponsorOrgName || '-' }}</span></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="服务公司">
|
||||
<el-table :data="assigns" border size="small" class="assign-table-el">
|
||||
<el-table-column type="index" label="#" width="48" align="center" />
|
||||
<el-table-column prop="orgName" label="名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="sessions" label="场次" width="90" align="right" />
|
||||
<el-table-column label="金额" width="130" align="right">
|
||||
<template #default="{ row }"><span class="money">¥ {{ fmtMoney(row.amount) }}</span></template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<span style="color:#909399">暂无分配 (到 项目管理 / 项目分配 添加)</span>
|
||||
</template>
|
||||
</el-table>
|
||||
<el-form-item label="执行单位">
|
||||
<table class="info-table role-labor-table">
|
||||
<colgroup>
|
||||
<col style="width:160px">
|
||||
<col style="width:90px">
|
||||
<col style="width:130px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th style="text-align:right">场次</th>
|
||||
<th style="text-align:right">金额</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(a, idx) in assigns" :key="idx">
|
||||
<td>{{ a.orgName || '-' }}</td>
|
||||
<td class="money" style="text-align:right">{{ a.sessions || 0 }}</td>
|
||||
<td class="money" style="text-align:right">¥ {{ fmtMoney(a.amount) }}</td>
|
||||
</tr>
|
||||
<tr v-if="!assigns.length"><td colspan="3" class="empty-row">到 项目管理/分配 添加</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -78,11 +88,14 @@
|
||||
</table>
|
||||
|
||||
<!-- ================= 3. 支持单位监督员 (只读) ================= -->
|
||||
<div class="new-card-title">支持单位监督员</div>
|
||||
<div class="new-card-title monitor-title">
|
||||
<span>支持单位监督员</span>
|
||||
<el-link v-if="roleType === 'manager' && row.sponsorOrgId" :underline="false" type="primary" @click="openMonitorAssign">分配</el-link>
|
||||
</div>
|
||||
<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>
|
||||
<span class="monitor-name">{{ m.name || '-' }}</span>
|
||||
</div>
|
||||
<div v-if="!monitors.length" class="empty-hint">暂无监督员</div>
|
||||
</div>
|
||||
@@ -105,6 +118,26 @@
|
||||
<!-- 公告预览 (只读) -->
|
||||
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
|
||||
|
||||
<!-- ================= 监督员分配 dialog (仅 manager, 需已分配支持方) ================= -->
|
||||
<el-dialog v-model="monitorAssignVisible" title="分配监督员" width="560px" append-to-body destroy-on-close>
|
||||
<div class="assign-project-info">
|
||||
<div><span class="assign-lbl">项目编号</span><span>{{ row.projectNo || '-' }}</span></div>
|
||||
<div><span class="assign-lbl">项目名称</span><span>{{ row.projectName || '-' }}</span></div>
|
||||
</div>
|
||||
<el-form :model="monitorAssignForm" label-width="100px">
|
||||
<el-form-item label="分配给监督员">
|
||||
<el-select v-model="monitorAssignForm.monitorUserIds" multiple collapse-tags collapse-tags-tooltip filterable
|
||||
placeholder="请选择监督员 (姓名 / 手机号, 可多选)" style="width: 100%">
|
||||
<el-option v-for="m in monitorCandidates" :key="m.userId" :label="m.name + ' (' + m.phone + ')'" :value="m.userId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="monitorAssignVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="monitorAssignSubmitting" :disabled="!monitorAssignForm.monitorUserIds.length" @click="saveMonitorAssign">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 底部操作: 返回 (按角色回退) -->
|
||||
<div class="form-actions">
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
@@ -115,9 +148,11 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { bizGet } from '@/api/public'
|
||||
import { bizGet, bizList } from '@/api/public'
|
||||
import { listExecutorOrgs } from '@/api/system'
|
||||
import { sponsorAssignProject } from '@/api/business/project'
|
||||
import request from '@/utils/request'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import Preview from '@/components/Preview.vue'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
@@ -125,6 +160,9 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 角色 (role_type 单一可信源: store.role = user.role = /getInfo 返回的 roleType)
|
||||
const roleType = computed(() => userStore.role)
|
||||
|
||||
// ===================== 状态 =====================
|
||||
const projectId = ref('')
|
||||
const row = ref({})
|
||||
@@ -167,30 +205,29 @@ 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) {
|
||||
function fmtDateTime(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)
|
||||
}
|
||||
// 从 OSS URL 提取并解码文件名 (中文文件名是 URL 编码的, 需 decodeURIComponent 还原)
|
||||
function fileNameFromUrl(url) {
|
||||
if (!url) return ''
|
||||
let last
|
||||
try {
|
||||
const path = String(url).split('?')[0]
|
||||
return decodeURIComponent(path.split('/').pop() || '')
|
||||
last = decodeURIComponent(path.split('/').pop() || '')
|
||||
} catch (e) {
|
||||
// 非标准编码导致解码失败 → 退回原始最后一段
|
||||
return String(url).split('?')[0].split('/').pop() || ''
|
||||
last = String(url).split('?')[0].split('/').pop() || ''
|
||||
}
|
||||
// 去掉 OSS 上传附加的随机串: 原名_时间戳(13位)_随机串(6位).扩展名 → 原名.扩展名
|
||||
const m = last.match(/^(.+)_\d{13}_[a-z0-9]{6}(\.[^.]+)?$/)
|
||||
if (m) return m[1] + (m[2] || '')
|
||||
return last
|
||||
}
|
||||
|
||||
// ===================== 加载 =====================
|
||||
@@ -261,7 +298,7 @@ async function loadMonitors() {
|
||||
const list = (resp && resp.data) || []
|
||||
monitors.value = (Array.isArray(list) ? list : []).map(x => ({
|
||||
userId: x.monitorUserId,
|
||||
userName: x.monitorUserName || ('(userId=' + x.monitorUserId + ')')
|
||||
name: x.monitorUserName || ('(userId=' + x.monitorUserId + ')')
|
||||
}))
|
||||
} catch (e) {
|
||||
console.error('[project-detail] monitors load failed', e)
|
||||
@@ -269,12 +306,65 @@ async function loadMonitors() {
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 监督员分配 (仅 manager, 需已分配支持方) =====================
|
||||
const monitorAssignVisible = ref(false)
|
||||
const monitorAssignSubmitting = ref(false)
|
||||
const monitorCandidates = ref([])
|
||||
const monitorAssignForm = reactive({ monitorUserIds: [] })
|
||||
|
||||
// 候选: 本项目已分配支持方的员工 (按 orgId 圈定, 过滤出子账号=监督员)
|
||||
async function loadMonitorCandidates() {
|
||||
const sponsorOrgId = row.value && row.value.sponsorOrgId
|
||||
if (!sponsorOrgId) { monitorCandidates.value = []; return }
|
||||
try {
|
||||
const { data } = await bizList('person', { orgId: sponsorOrgId, status: '0', pageNum: 1, pageSize: 100 })
|
||||
const rows = (data && data.rows) || []
|
||||
monitorCandidates.value = rows
|
||||
.filter(p => p.accountType === 'SUB')
|
||||
.map(p => ({ userId: p.userId, name: p.name, phone: p.phone }))
|
||||
} catch (e) {
|
||||
monitorCandidates.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function openMonitorAssign() {
|
||||
monitorAssignForm.monitorUserIds = []
|
||||
monitorAssignVisible.value = true
|
||||
loadMonitorCandidates()
|
||||
// 回显当前已分配监督员 (等候选加载完, 让 el-select 找得到 option)
|
||||
request({ url: `/business/project/${projectId.value}/sponsorAssigns`, method: 'get' })
|
||||
.then(resp => {
|
||||
const list = (resp && resp.data) || []
|
||||
monitorAssignForm.monitorUserIds = (Array.isArray(list) ? list : []).map(x => x.monitorUserId).filter(Boolean)
|
||||
})
|
||||
.catch(() => { monitorAssignForm.monitorUserIds = [] })
|
||||
}
|
||||
|
||||
async function saveMonitorAssign() {
|
||||
if (!monitorAssignForm.monitorUserIds.length) return ElMessage.warning('请至少选择一位监督员')
|
||||
monitorAssignSubmitting.value = true
|
||||
try {
|
||||
await sponsorAssignProject({
|
||||
projectId: projectId.value,
|
||||
monitorUserIds: [...monitorAssignForm.monitorUserIds]
|
||||
})
|
||||
ElMessage.success('分配成功')
|
||||
monitorAssignVisible.value = false
|
||||
await loadMonitors()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || e?.message || '分配失败')
|
||||
} finally {
|
||||
monitorAssignSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 导航 =====================
|
||||
// 按角色回退: sponsor → 我的项目, manager → 项目管理, admin → 项目管理
|
||||
function goBack() {
|
||||
const role = userStore?.role || userStore?.user?.roleType
|
||||
const role = roleType.value
|
||||
if (role === 'sponsor') router.push('/sponsor/my-projects')
|
||||
else if (role === 'executor') router.push('/executor/projects')
|
||||
else if (role === 'admin') router.push('/admin/projects')
|
||||
else router.push('/manager/projects')
|
||||
}
|
||||
function goEdit() { router.push(`/manager/projects/edit/${projectId.value}`) }
|
||||
@@ -326,7 +416,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
/* 监督员 */
|
||||
.monitor-list { padding: 4px 0; }
|
||||
.monitor-list { padding: 4px 0; max-width: 400px; }
|
||||
.monitor-row {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 8px 12px; background: #fafafa; border-radius: 4px;
|
||||
@@ -335,19 +425,19 @@ onMounted(async () => {
|
||||
.monitor-num { width: 24px; color: #909399; font-size: 13px; }
|
||||
.monitor-name { flex: 1; font-size: 14px; color: #262626; }
|
||||
.empty-hint { text-align: center; color: #909399; padding: 12px 0; font-size: 13px; }
|
||||
.monitor-title { display: flex; align-items: center; justify-content: space-between; max-width: 400px; }
|
||||
|
||||
/* 监督员分配 dialog 项目信息 */
|
||||
.assign-project-info { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; font-size: 14px; color: #262626; }
|
||||
.assign-lbl { display: inline-block; min-width: 70px; color: #909399; }
|
||||
|
||||
/* 角色劳务表格 + 服务公司表格: 统一 14px, 表头 600/strong, 单元格 strong */
|
||||
.info-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||
.role-labor-table { width: auto; border: 1px solid #ebeef5; } /* 角色劳务表格按内容紧凑显示, 不撑满卡片 */
|
||||
.role-labor-table { width: auto; border: 1px solid #ebeef5; } /* 按内容紧凑显示, 不撑满卡片 */
|
||||
.role-labor-table th,
|
||||
.role-labor-table td { border-right: 1px solid #ebeef5; }
|
||||
.role-labor-table th:last-child,
|
||||
.role-labor-table td:last-child { border-right: none; }
|
||||
/* 服务公司表格 (el-table in form): 紧凑, 不要撑满整张卡片 */
|
||||
.assign-table-el { width: auto; max-width: 560px; font-size: 14px; }
|
||||
.assign-table-el :deep(.el-table__empty-block) { min-height: 48px; }
|
||||
.assign-table-el :deep(th.el-table__cell) { color: #262626; font-weight: 600; background: #fafafa; }
|
||||
.assign-table-el :deep(td.el-table__cell) { color: #262626; }
|
||||
.info-table th { background: #fafafa; padding: 10px 12px; text-align: left; color: #262626; font-weight: 600; border-bottom: 1px solid #f0f0f0; white-space: nowrap; }
|
||||
.info-table td { padding: 10px 12px; border-bottom: 1px solid #f5f5f5; color: #262626; vertical-align: middle; }
|
||||
.empty-row { text-align: center; color: #909399; }
|
||||
@@ -404,15 +494,7 @@ onMounted(async () => {
|
||||
.info-form :deep(.el-row) { margin: 0 !important; }
|
||||
.info-form :deep(.el-col) { padding: 0 !important; margin-bottom: 0 !important; }
|
||||
|
||||
/* 服务公司表格: 横向滚动容器 + 字号缩小 */
|
||||
.assign-table-el {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
.assign-table-el :deep(.el-table__body-wrapper) { overflow-x: auto !important; }
|
||||
|
||||
/* 角色劳务表 */
|
||||
/* 角色劳务表 + 服务公司表 (共用 info-table 原生表格) */
|
||||
.role-labor-table { width: 100% !important; font-size: 13px !important; }
|
||||
.role-labor-table th,
|
||||
.role-labor-table td { padding: 8px 10px !important; font-size: 13px !important; }
|
||||
|
||||
Reference in New Issue
Block a user