841 lines
42 KiB
Vue
841 lines
42 KiB
Vue
<template>
|
||
<div class="page-card">
|
||
<div class="breadcrumb">首页 / 项目管理</div>
|
||
|
||
<el-form inline :model="q" class="filter-form">
|
||
<el-form-item label="支持单位:">
|
||
<el-select
|
||
v-model="q.sponsorOrgIds"
|
||
multiple
|
||
filterable
|
||
remote
|
||
:remote-method="searchSponsorOrgsForFilter"
|
||
:loading="sponsorSearching"
|
||
clearable
|
||
collapse-tags
|
||
collapse-tags-tooltip
|
||
placeholder="输入支持单位名称搜索"
|
||
style="width:260px"
|
||
>
|
||
<el-option v-for="o in sponsorOrgOptions" :key="o.orgId" :label="o.orgName" :value="o.orgId" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="服务机构:">
|
||
<el-select
|
||
v-model="q.execOrgIds"
|
||
multiple
|
||
filterable
|
||
remote
|
||
:remote-method="searchExecutorOrgsForFilter"
|
||
:loading="execSearching"
|
||
clearable
|
||
collapse-tags
|
||
collapse-tags-tooltip
|
||
placeholder="输入服务机构名称搜索"
|
||
style="width:260px"
|
||
>
|
||
<el-option v-for="o in executorOrgOptions" :key="o.orgId" :label="o.orgName" :value="o.orgId" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="项目编号:"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
|
||
<el-form-item label="项目名称:"><el-input v-model="q.projectName" placeholder="输入项目名称" clearable /></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-date-picker v-model="q.startTime" type="date" placeholder="开始日期" value-format="YYYY-MM-DD" />
|
||
<span style="margin:0 4px;">至</span>
|
||
<el-date-picker v-model="q.endTime" type="date" placeholder="结束日期" value-format="YYYY-MM-DD" />
|
||
</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 class="action-btn" @click="openNewProject">新建项目</el-button>
|
||
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openAssign">
|
||
项目分配<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||
</el-button>
|
||
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openBatch('score')">
|
||
批量评分<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||
</el-button>
|
||
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openBatch('close')">
|
||
批量结题<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||
</el-button>
|
||
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openBatch('activate')">
|
||
批量开通<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||
</el-button>
|
||
<el-button v-if="canDelete" class="action-btn secondary" type="danger" :disabled="!selection.length" @click="onBatchDelete">
|
||
批量删除<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||
</el-button>
|
||
</div>
|
||
<GrTable
|
||
:data="rows"
|
||
v-loading="loading"
|
||
stripe
|
||
border
|
||
:main-cols="['projectNo', 'projectName']"
|
||
@selection-change="onSel"
|
||
>
|
||
<el-table-column type="selection" width="44" />
|
||
<el-table-column prop="projectNo" label="项目编号" width="140" 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="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">
|
||
<template #default="{ row }">
|
||
<span v-if="row.managerScore != null">{{ row.managerScore }}</span>
|
||
<span v-else style="color:#c0c4cc">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="sponsorScore" label="执行单位评分(支持)" width="140" align="center">
|
||
<template #default="{ row }">
|
||
<span v-if="row.sponsorScore != null">{{ row.sponsorScore }}</span>
|
||
<span v-else style="color:#c0c4cc">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="sponsorOrgName" label="支持单位" width="160" show-overflow-tooltip />
|
||
<el-table-column prop="execOrgNames" label="服务机构" min-width="180" show-overflow-tooltip />
|
||
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
|
||
<el-table-column prop="isFinished" label="是否结题" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.isFinished === '1' || row.isFinished === 1" type="success">已结题</el-tag>
|
||
<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 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 :underline="false" type="primary" @click="doAssign(row)">分配</el-link>
|
||
<el-dropdown trigger="click" @command="(cmd) => onAction(cmd, row)">
|
||
<el-link :underline="false" type="primary" class="op-dropdown">
|
||
更多<el-icon class="op-caret"><ArrowDown /></el-icon>
|
||
</el-link>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item v-if="row.isFinished !== '1' && row.isFinished !== 1" command="close">结题</el-dropdown-item>
|
||
<el-dropdown-item command="view">查看</el-dropdown-item>
|
||
<el-dropdown-item command="edit">编辑</el-dropdown-item>
|
||
<el-dropdown-item command="activate">开通</el-dropdown-item>
|
||
<el-dropdown-item command="delAnn">删除公告</el-dropdown-item>
|
||
<el-dropdown-item command="rate">执行单位评分</el-dropdown-item>
|
||
<el-dropdown-item command="exportExperts">导出报名专家</el-dropdown-item>
|
||
<el-dropdown-item v-if="canDelete" command="delete">删除项目</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
</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" />
|
||
</div>
|
||
|
||
<!-- 项目详情已迁到独立页面 /manager/projects/detail/:projectId -->
|
||
|
||
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
|
||
|
||
<!-- ============= 新建项目 dialog(已迁移到独立页面 /manager/projects/new) ============= -->
|
||
|
||
<!-- 项目分配 dialog 已迁移到独立子页面 /manager/projects/assign (单条) 和 /assign-batch (批量) -->
|
||
|
||
<!-- ============= 批量评分 dialog(按原型 4 维度评分) ============= -->
|
||
<el-dialog v-model="batchOpen" :title="batchMap[batchKind]" width="626px" :show-close="false">
|
||
<!-- 评分专用字段: 共享一份评分 (与 singleScoreForm 同样的三列 table 布局) -->
|
||
<template v-if="batchKind==='score'">
|
||
<p style="color:#606266;margin-bottom:8px">将对以下 <b>{{ selection.length }}</b> 个项目共用同一份评分:</p>
|
||
<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:130px;padding:10px 12px;text-align:center">评分标准(1~5 分)</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"><el-input-number v-model="batchScoreForm.qualityScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></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"><el-input-number v-model="batchScoreForm.responseScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></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"><el-input-number v-model="batchScoreForm.cooperationScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></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"><el-input-number v-model="batchScoreForm.complianceScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</template>
|
||
<!-- 结题专用字段 -->
|
||
<template v-else-if="batchKind==='close'">
|
||
<p>已选择 <b>{{ selection.length }}</b> 个项目。</p>
|
||
<p style="color:#606266;margin-top:8px">确定要将这些项目标记为结题吗?</p>
|
||
</template>
|
||
<!-- 开通专用字段 -->
|
||
<template v-else-if="batchKind==='activate'">
|
||
<p>已选择 <b>{{ selection.length }}</b> 个项目。</p>
|
||
<el-form-item label="开通截止时间"><el-date-picker v-model="activateDeadline" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
|
||
</template>
|
||
<template #footer>
|
||
<el-button @click="batchOpen=false">取消</el-button>
|
||
<el-button type="primary" :loading="batchSubmitting" @click="confirmBatch">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ============= 单行结题 modal ============= -->
|
||
<el-dialog v-model="closeModalOpen" title="您确定要结题吗?" width="420px" :show-close="false">
|
||
<template #footer>
|
||
<el-button type="primary" @click="confirmClose">确定</el-button>
|
||
<el-button @click="closeModalOpen=false">取消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ============= 单行开通 modal (置 open_status=Y + 开通截止时间 open_deadline) ============= -->
|
||
<el-dialog v-model="activateModalOpen" title="开通项目" width="420px" :show-close="false">
|
||
<el-form-item label="开通截止时间">
|
||
<el-date-picker v-model="activateDeadlineSingle" type="date" value-format="YYYY-MM-DD" style="width:100%" />
|
||
</el-form-item>
|
||
<template #footer>
|
||
<el-button type="primary" @click="confirmActivate">确定</el-button>
|
||
<el-button @click="activateModalOpen=false">取消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ============= 单行删除公告 modal ============= -->
|
||
<el-dialog v-model="delAnnModalOpen" title="您确定要删除公告吗?删除后无法恢复" width="420px" :show-close="false">
|
||
<template #footer>
|
||
<el-button type="primary" @click="confirmDeleteAnn">确定</el-button>
|
||
<el-button @click="delAnnModalOpen=false">取消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ============= 单行执行单位评分 modal(4 维度) ============= -->
|
||
<el-dialog v-model="singleScoreModalOpen" title="执行单位评分" width="626px" :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:130px;padding:10px 12px;text-align:center">评分标准(1~5 分)</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"><el-input-number v-model="singleScoreForm.qualityScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></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"><el-input-number v-model="singleScoreForm.responseScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></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"><el-input-number v-model="singleScoreForm.cooperationScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></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"><el-input-number v-model="singleScoreForm.complianceScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
|
||
</tbody>
|
||
</table>
|
||
<template #footer>
|
||
<el-button type="primary" @click="confirmSingleScore">确定</el-button>
|
||
<el-button @click="singleScoreModalOpen=false">取消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
import { bizList, bizAdd, bizUpdate, bizDelete } from '@/api/public'
|
||
import { listExecutorOrgs, listSponsorOrgs } from '@/api/system'
|
||
import request from '@/utils/request'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { ArrowDown } from '@element-plus/icons-vue'
|
||
import Preview from '@/components/Preview.vue'
|
||
import GrTable from '@/components/GrTable.vue'
|
||
import { useUserStore } from '@/store/user'
|
||
const userStore = useUserStore()
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
|
||
const q = ref({ sponsorOrgIds: [], execOrgIds: [], projectNo: '', projectName: '', projectForm: '', startTime: '', endTime: '', isFinished: '', isSettled: '' })
|
||
const rows = ref([])
|
||
const loading = ref(false)
|
||
const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
// axios 拦截器已将 TableDataInfo 归一化为 {code, msg, data: {total, rows}}
|
||
const { data } = await bizList('project', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
|
||
rows.value = data?.rows || []
|
||
page.total = data?.total || 0
|
||
} catch (e) {
|
||
rows.value = []; page.total = 0
|
||
} finally { loading.value = false }
|
||
}
|
||
// 操作列按钮 handlers
|
||
function onAction(cmd, row) {
|
||
if (cmd === 'close') openClose(row)
|
||
else if (cmd === 'view') viewDetail(row)
|
||
else if (cmd === 'edit') doEdit(row)
|
||
else if (cmd === 'activate') openActivate(row)
|
||
else if (cmd === 'delAnn') openDeleteAnnouncement(row)
|
||
else if (cmd === 'rate') openSingleScore(row)
|
||
else if (cmd === 'exportExperts') exportExperts(row)
|
||
else if (cmd === 'delete') doDelete(row)
|
||
}
|
||
// 仅 admin 可见删除按钮 (前端 v-if, 后端 controller 再兜底 role 校验)
|
||
const canDelete = computed(() => {
|
||
const r = userStore.role
|
||
return r === 'admin'
|
||
})
|
||
function doCreateMeeting(row) { router.push(`/manager/meetings/new?projectId=${row.projectId}`) }
|
||
function doAssign(row) {
|
||
// 跳转独立子页面 /manager/projects/assign?projectId=X
|
||
router.push({ path: '/manager/projects/assign', query: { projectId: row.projectId } })
|
||
}
|
||
function doEdit(row) { router.push(`/manager/projects/edit/${row.projectId}`) }
|
||
|
||
// 删除项目 (软删除 + 级联置 5 张项目表 + 项目下所有会议 is_deleted=1)
|
||
// 后端 BizProjectController.remove 已强制仅 admin, 前端只做可见性 v-if
|
||
async function doDelete(row) {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确认删除项目"${row.projectName}"?\n项目编号: ${row.projectNo}\n\n` +
|
||
`此操作将级联软删除:\n` +
|
||
`· 项目主表 + 方案 + 执行方分配 + 支持方分配 + 评分 (5 张表)\n` +
|
||
`· 项目下所有会议 + 会议附件 + 参会人 + 监督员 + 执行人 + 审计日志 (6 张表)\n\n` +
|
||
`数据保留在库, 不再展示给前台用户, 审计追溯可查。`,
|
||
'删除确认',
|
||
{ type: 'error', confirmButtonText: '确认软删除', cancelButtonText: '取消' }
|
||
)
|
||
await bizDelete('project', row.projectId)
|
||
ElMessage.success('已删除')
|
||
// 从 selection 里移除已删行, 避免批量按钮残留
|
||
selection.value = selection.value.filter(r => r.projectId !== row.projectId)
|
||
load()
|
||
} catch (e) {
|
||
if (e !== 'cancel' && e !== 'close') ElMessage.error(e?.msg || '删除失败')
|
||
}
|
||
}
|
||
|
||
// 批量软删除 (admin/manager)
|
||
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` +
|
||
`数据保留, 不再展示给前台用户。`,
|
||
'批量删除确认',
|
||
{ type: 'error', confirmButtonText: `确认删除 ${selection.value.length} 个`, cancelButtonText: '取消' }
|
||
)
|
||
} catch { return }
|
||
const ids = selection.value.map(r => r.projectId)
|
||
try {
|
||
await bizDelete('project', ids)
|
||
ElMessage.success(`已批量软删除 ${ids.length} 个项目`)
|
||
selection.value = []
|
||
load()
|
||
} catch (e) {
|
||
ElMessage.error(e?.msg || '批量删除失败')
|
||
}
|
||
}
|
||
|
||
function reset() { q.value = { sponsorOrgIds:[], execOrgIds:[], projectNo:'', projectName:'', projectForm:'', startTime:'', endTime:'', isFinished:'', isSettled:'' }; page.pageNum = 1; load() }
|
||
|
||
// 读 URL query 写入 q (Workbench KPI 卡跳转时带 ?isFinished=1 / ?isSettled=Y 等, 让列表页自动应用筛选)
|
||
// 注意: 只读不改 URL — 列表页内 reset()/search 不反向写 URL, Workbench 是 source of truth
|
||
function readQueryFromRoute() {
|
||
const q2 = route.query
|
||
if (q2.isFinished != null && q2.isFinished !== '') q.value.isFinished = String(q2.isFinished)
|
||
if (q2.isSettled != null && q2.isSettled !== '') q.value.isSettled = String(q2.isSettled)
|
||
}
|
||
|
||
// 导出
|
||
function exportProjects() { ElMessage.info('导出项目功能开发中') }
|
||
function exportEval() { ElMessage.info('项目评价导出功能开发中') }
|
||
// 导出某项目的报名专家 (biz_execution_intent)
|
||
// 字段: 专家姓名 科室 医院 职称 报名时间 手机号
|
||
async function exportExperts(row) {
|
||
if (!row || !row.projectNo) { ElMessage.warning('缺少项目编号'); return }
|
||
try {
|
||
// 后端 BizExecutionIntent 参数没 @RequestBody, 必须走 URL query string
|
||
const res = await request({
|
||
url: '/business/executionIntent/export',
|
||
method: 'post',
|
||
params: { projectNo: row.projectNo },
|
||
responseType: 'blob'
|
||
})
|
||
const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
||
const url = window.URL.createObjectURL(blob)
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
a.download = `报名专家_${row.projectNo}.xlsx`
|
||
document.body.appendChild(a)
|
||
a.click()
|
||
document.body.removeChild(a)
|
||
window.URL.revokeObjectURL(url)
|
||
ElMessage.success('导出成功')
|
||
} catch (e) {
|
||
const msg = e?.msg || e?.message || '导出失败'
|
||
ElMessage.error(msg)
|
||
}
|
||
}
|
||
|
||
// ========== 单行操作按钮 handlers(弹 5 个独立 dialog) ==========
|
||
function openClose(row) {
|
||
if (row.isFinished === '1' || row.isFinished === 1) {
|
||
return ElMessage.warning('该项目已结题,无需重复操作')
|
||
}
|
||
closeTargetRow.value = row
|
||
closeModalOpen.value = true
|
||
}
|
||
function confirmClose() {
|
||
if (!closeTargetRow.value) return
|
||
bizUpdate('project', { projectId: closeTargetRow.value.projectId, isFinished: '1' })
|
||
.then(() => { ElMessage.success('结题成功'); closeModalOpen.value = false; load() })
|
||
.catch(() => ElMessage.error('结题失败'))
|
||
}
|
||
|
||
function openActivate(row) {
|
||
activateTargetRow.value = row
|
||
activateDeadlineSingle.value = row.openDeadline || ''
|
||
activateModalOpen.value = true
|
||
}
|
||
function confirmActivate() {
|
||
if (!activateTargetRow.value) return
|
||
if (!activateDeadlineSingle.value) return ElMessage.warning('请选择开通截止时间')
|
||
bizUpdate('project', { projectId: activateTargetRow.value.projectId, openStatus: 'Y', openDeadline: activateDeadlineSingle.value })
|
||
.then(() => { ElMessage.success('已开通'); activateModalOpen.value = false; load() })
|
||
.catch(() => ElMessage.error('开通失败'))
|
||
}
|
||
|
||
function openDeleteAnnouncement(row) {
|
||
delAnnTargetRow.value = row
|
||
delAnnModalOpen.value = true
|
||
}
|
||
function confirmDeleteAnn() {
|
||
if (!delAnnTargetRow.value) return
|
||
// 删除公告: 后端专用端点, 将 3 个公示 URL 置 NULL (未发布由 URL 为空判定, 无独立 is_published 列)
|
||
request.delete(`/business/project/${delAnnTargetRow.value.projectId}/announcement`)
|
||
.then(() => { ElMessage.success(`已删除项目 ${delAnnTargetRow.value.projectNo} 的公告`); delAnnModalOpen.value = false; load() })
|
||
.catch(() => ElMessage.error('删除失败'))
|
||
}
|
||
|
||
async function openSingleScore(row) {
|
||
singleScoreTargetRow.value = row
|
||
singleScoreForm.qualityScore = 0
|
||
singleScoreForm.responseScore = 0
|
||
singleScoreForm.cooperationScore = 0
|
||
singleScoreForm.complianceScore = 0
|
||
singleScoreModalOpen.value = true
|
||
// 反显当前登录管理员的历史明细
|
||
const uid = userStore.user?.userId
|
||
if (!uid) return
|
||
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 mine = list.find(x =>
|
||
String(x.projectId) === String(row.projectId) &&
|
||
String(x.raterId) === String(uid) &&
|
||
x.raterRole === 'manager'
|
||
)
|
||
if (mine) {
|
||
singleScoreForm.qualityScore = Number(mine.qualityScore) || 0
|
||
singleScoreForm.responseScore = Number(mine.responseScore) || 0
|
||
singleScoreForm.cooperationScore = Number(mine.cooperationScore) || 0
|
||
singleScoreForm.complianceScore = Number(mine.complianceScore) || 0
|
||
}
|
||
} catch (e) { /* 拉取失败不影响打开, 维持默认 0 */ }
|
||
}
|
||
async function confirmSingleScore() {
|
||
if (!singleScoreTargetRow.value) return
|
||
const f = singleScoreForm
|
||
if (!f.qualityScore || f.qualityScore < 1 || f.qualityScore > 5) return ElMessage.error('履约质量必须是 1~5 的数字')
|
||
if (!f.responseScore || f.responseScore < 1 || f.responseScore > 5) return ElMessage.error('时效响应必须是 1~5 的数字')
|
||
if (!f.cooperationScore || f.cooperationScore < 1 || f.cooperationScore > 5) return ElMessage.error('配合度必须是 1~5 的数字')
|
||
if (!f.complianceScore || f.complianceScore < 1 || f.complianceScore > 5) return ElMessage.error('合规安全必须是 1~5 的数字')
|
||
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)
|
||
await request.post('/business/project/rate', {
|
||
projectId,
|
||
raterRole: 'manager',
|
||
qualityScore: f.qualityScore,
|
||
responseScore: f.responseScore,
|
||
cooperationScore: f.cooperationScore,
|
||
complianceScore: f.complianceScore
|
||
})
|
||
ElMessage.success(`评分完成,平均分:${avg}`)
|
||
singleScoreModalOpen.value = false
|
||
load()
|
||
} catch (e) {
|
||
ElMessage.error(e?.msg || '评分失败')
|
||
}
|
||
}
|
||
function viewDetail(row) { router.push(`/manager/projects/detail/${row.projectId}`) }
|
||
|
||
// 金额格式化: 1234567.5 -> "1,234,567.50"
|
||
function fmtMoney(row, col, v) {
|
||
if (v === null || v === undefined || v === '') return ''
|
||
const n = Number(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) {
|
||
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
|
||
}
|
||
|
||
const selection = ref([])
|
||
function onSel(rs) { selection.value = rs }
|
||
const batchOpen = ref(false)
|
||
const batchKind = ref('')
|
||
const batchSubmitting = ref(false)
|
||
const batchMap = { score: '批量评分', close: '批量结题', activate: '批量开通' }
|
||
const batchScoreForm = reactive({ qualityScore: 5, responseScore: 5, cooperationScore: 5, complianceScore: 5 }) // 批量评分: 共享一份 4 维度
|
||
const activateDeadline = ref('')
|
||
const activateDeadlineSingle = ref('')
|
||
|
||
// 5 个单行 modal 状态
|
||
const closeModalOpen = ref(false)
|
||
const closeTargetRow = ref(null)
|
||
const activateModalOpen = ref(false)
|
||
const activateTargetRow = ref(null)
|
||
const delAnnModalOpen = ref(false)
|
||
const delAnnTargetRow = ref(null)
|
||
const singleScoreModalOpen = ref(false)
|
||
const singleScoreTargetRow = ref(null)
|
||
const singleScoreForm = reactive({ qualityScore: 0, responseScore: 0, cooperationScore: 0, complianceScore: 0 })
|
||
|
||
// 新建项目已迁到独立页面 /manager/projects/new (ManagerProjectsNew.vue)
|
||
function openNewProject() {
|
||
router.push('/manager/projects/new')
|
||
}
|
||
function openAssign() {
|
||
if (!selection.value.length) {
|
||
ElMessage.warning('请先勾选项目')
|
||
return
|
||
}
|
||
// 跳转独立子页面 /manager/projects/assign, 单条 vs 批量通过 query 区分:
|
||
// 单条: ?projectId=X → 子页面 v-if 单条分支
|
||
// 批量: ?projectIds=1,2,3 → 子页面 v-else 批量分支
|
||
if (selection.value.length === 1) {
|
||
router.push({ path: '/manager/projects/assign', query: { projectId: selection.value[0].projectId } })
|
||
} else {
|
||
const ids = selection.value.map(r => r.projectId).join(',')
|
||
router.push({ path: '/manager/projects/assign', query: { projectIds: ids } })
|
||
}
|
||
}
|
||
function openBatch(kind) {
|
||
if (!selection.value.length) return ElMessage.warning('请先勾选项目')
|
||
batchKind.value = kind
|
||
if (kind === 'score') {
|
||
// 共享评分: 重置 form 为默认值 (5 分)
|
||
Object.assign(batchScoreForm, { qualityScore: 5, responseScore: 5, cooperationScore: 5, complianceScore: 5 })
|
||
}
|
||
if (kind === 'activate') {
|
||
activateDeadline.value = ''
|
||
}
|
||
batchOpen.value = true
|
||
}
|
||
|
||
async function confirmBatch() {
|
||
if (batchKind.value === 'close') {
|
||
// 过滤掉已结题项目, 避免无效请求
|
||
const targets = selection.value.filter(r => r.isFinished !== '1' && r.isFinished !== 1)
|
||
const skipCount = selection.value.length - targets.length
|
||
try {
|
||
batchSubmitting.value = true
|
||
await Promise.all(targets.map(r => bizUpdate('project', { projectId: r.projectId, isFinished: '1' })))
|
||
ElMessage.success(skipCount > 0
|
||
? `已批量结题 ${targets.length} 个项目 (跳过 ${skipCount} 个已结题)`
|
||
: `已批量结题 ${targets.length} 个项目`)
|
||
batchOpen.value = false
|
||
selection.value = []
|
||
load()
|
||
} catch { ElMessage.error('批量结题失败') }
|
||
finally { batchSubmitting.value = false }
|
||
} else if (batchKind.value === 'activate') {
|
||
if (!activateDeadline.value) return ElMessage.warning('请选择开通截止时间')
|
||
try {
|
||
batchSubmitting.value = true
|
||
await Promise.all(selection.value.map(r => bizUpdate('project', { projectId: r.projectId, openStatus: 'Y', openDeadline: activateDeadline.value })))
|
||
ElMessage.success(`已批量开通 ${selection.value.length} 个项目`)
|
||
batchOpen.value = false
|
||
selection.value = []
|
||
load()
|
||
} catch { ElMessage.error('批量开通失败') }
|
||
finally { batchSubmitting.value = false }
|
||
} else if (batchKind.value === 'score') {
|
||
const f = batchScoreForm
|
||
if (!f.qualityScore || f.qualityScore < 1 || f.qualityScore > 5) return ElMessage.error('履约质量必须是 1~5 的数字')
|
||
if (!f.responseScore || f.responseScore < 1 || f.responseScore > 5) return ElMessage.error('时效响应必须是 1~5 的数字')
|
||
if (!f.cooperationScore || f.cooperationScore < 1 || f.cooperationScore > 5) return ElMessage.error('配合度必须是 1~5 的数字')
|
||
if (!f.complianceScore || f.complianceScore < 1 || f.complianceScore > 5) return ElMessage.error('合规安全必须是 1~5 的数字')
|
||
const total = ((f.qualityScore + f.responseScore + f.cooperationScore + f.complianceScore) / 4).toFixed(1)
|
||
try {
|
||
batchSubmitting.value = true
|
||
let ok = 0
|
||
// 共享评分: 所有项目用同一组 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',
|
||
qualityScore: f.qualityScore,
|
||
responseScore: f.responseScore,
|
||
cooperationScore: f.cooperationScore,
|
||
complianceScore: f.complianceScore
|
||
})
|
||
ok++
|
||
} catch {}
|
||
}
|
||
ElMessage.success(`已批量评分 ${ok}/${selection.value.length} 个项目 (共用评分 ${total})`)
|
||
batchOpen.value = false
|
||
selection.value = []
|
||
load()
|
||
} finally { batchSubmitting.value = false }
|
||
}
|
||
}
|
||
|
||
// ========== 新建项目 ==========
|
||
const newOpen = ref(false)
|
||
// 新建项目独立页面: /manager/projects/new (ManagerProjectsNew.vue)
|
||
|
||
// ========== 项目分配 dialog 已迁到独立子页面 /manager/projects/assign (单条) 和 /assign-batch (批量) ==========
|
||
// 仅保留顶部筛选区使用的远程搜索状态 (sponsorOrgOptions / executorOrgOptions), 不再保留 dialog 相关 state 和 functions
|
||
|
||
const sponsorOrgOptions = ref([])
|
||
const sponsorSearching = ref(false)
|
||
let _supporterFilterTimer = null
|
||
// 顶部筛选区专用
|
||
const executorOrgOptions = ref([])
|
||
const execSearching = ref(false)
|
||
let _executorFilterTimer = null
|
||
|
||
// 顶部筛选区远程搜索 (多选 el-select, 复用 sponsorOrgOptions, 加 loading + 已选项重拉回)
|
||
function searchSponsorOrgsForFilter(query) {
|
||
if (_supporterFilterTimer) clearTimeout(_supporterFilterTimer)
|
||
sponsorSearching.value = true
|
||
_supporterFilterTimer = setTimeout(async () => {
|
||
try {
|
||
const params = {}
|
||
if (query) params.orgName = query
|
||
const r = await listSponsorOrgs(params)
|
||
sponsorOrgOptions.value = r.data || []
|
||
// 已选项不在结果里时按 orgId 单独拉回 (multi-select 必须保证每个已选 tag 都有 label)
|
||
const ids = q.value.sponsorOrgIds || []
|
||
for (const id of ids) {
|
||
if (!sponsorOrgOptions.value.find(u => u.orgId === id)) {
|
||
const r2 = await listSponsorOrgs({ orgId: id })
|
||
const sel = (r2.data || []).find(u => u.orgId === id)
|
||
if (sel) sponsorOrgOptions.value = [sel, ...sponsorOrgOptions.value]
|
||
}
|
||
}
|
||
} catch (e) {
|
||
sponsorOrgOptions.value = []
|
||
} finally {
|
||
sponsorSearching.value = false
|
||
}
|
||
}, 200)
|
||
}
|
||
|
||
// 顶部筛选区远程搜索 (服务机构多选 el-select, 加 loading + 已选项重拉回)
|
||
function searchExecutorOrgsForFilter(query) {
|
||
if (_executorFilterTimer) clearTimeout(_executorFilterTimer)
|
||
execSearching.value = true
|
||
_executorFilterTimer = setTimeout(async () => {
|
||
try {
|
||
const params = {}
|
||
if (query) params.orgName = query
|
||
const r = await listExecutorOrgs(params)
|
||
executorOrgOptions.value = r.data || []
|
||
// 已选项不在结果里时按 orgId 单独拉回 (multi-select 必须保证每个已选 tag 都有 label)
|
||
const ids = q.value.execOrgIds || []
|
||
for (const id of ids) {
|
||
if (!executorOrgOptions.value.find(u => u.orgId === id)) {
|
||
const r2 = await listExecutorOrgs({ orgId: id })
|
||
const sel = (r2.data || []).find(u => u.orgId === id)
|
||
if (sel) executorOrgOptions.value = [sel, ...executorOrgOptions.value]
|
||
}
|
||
}
|
||
} catch (e) {
|
||
executorOrgOptions.value = []
|
||
} finally {
|
||
execSearching.value = false
|
||
}
|
||
}, 200)
|
||
}
|
||
|
||
const previewOpen = ref(false)
|
||
const previewUrl = ref('')
|
||
const previewTitle = ref('附件预览')
|
||
function openPreview(url, title) { previewUrl.value = url; previewTitle.value = title || '附件预览'; previewOpen.value = true }
|
||
|
||
onMounted(() => { readQueryFromRoute(); load() })
|
||
// 监听路由变化 (从 /manager/projects/new 跳回时强制重载, query 变了也重新应用筛选)
|
||
watch(() => route.fullPath, (newPath, oldPath) => {
|
||
if (newPath === '/manager/projects') {
|
||
readQueryFromRoute()
|
||
load()
|
||
}
|
||
})
|
||
// 兜底: 即便路由完全相同,从列表跳到详情再回来也触发
|
||
watch(() => route.path + (route.query._t || ''), () => load())
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 1:1 抄 sponsor/SponsorProjects.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; }
|
||
|
||
/* 操作列下拉 */
|
||
.op-cell { display: inline-flex; align-items: center; gap: 12px; }
|
||
.op-cell :deep(.el-button) { margin-left: 0 !important; padding: 0 4px !important; }
|
||
.op-dropdown { padding-right: 0; }
|
||
.op-caret { margin-left: 2px; font-size: 10px; }
|
||
|
||
/* 工具按钮 (与 sponsor 一致) */
|
||
.toolbar :deep(.action-btn) {
|
||
height: 32px; padding: 0 16px; font-size: 13px; border-radius: 4px;
|
||
background: var(--brand-primary); color: #fff; border: 1px solid var(--brand-primary);
|
||
display: inline-flex; align-items: center; justify-content: center; white-space: nowrap;
|
||
transition: opacity .15s;
|
||
}
|
||
.toolbar :deep(.action-btn:hover) { opacity: .85; }
|
||
.toolbar :deep(.action-btn.secondary) {
|
||
background: #fff; color: var(--brand-primary); border: 1px solid var(--brand-primary);
|
||
}
|
||
.toolbar :deep(.action-btn:disabled) {
|
||
background: #f5f7fa; color: #c0c4cc; border-color: #e4e7ed; cursor: not-allowed;
|
||
}
|
||
.toolbar :deep(.badge) {
|
||
margin-left: 4px; padding: 0 6px; background: var(--brand-primary); color: #fff;
|
||
border-radius: 8px; font-size: 11px; line-height: 16px;
|
||
}
|
||
.toolbar :deep(.action-btn.secondary .badge) { background: var(--brand-primary); color: #fff; }
|
||
.page-toolbar :deep(.action-btn.secondary:hover) { background: rgba(30,58,138,.06); }
|
||
.page-toolbar :deep(.action-btn.is-disabled),
|
||
.page-toolbar :deep(.action-btn:disabled) { background: #f5f7fa; color: #c0c4cc; border-color: #e4e7ed; opacity: 1; cursor: not-allowed; }
|
||
.page-toolbar :deep(.action-btn.secondary.is-disabled),
|
||
.page-toolbar :deep(.action-btn.secondary:disabled) { background: #fff; color: #c0c4cc; border-color: #e4e7ed; }
|
||
.page-toolbar :deep(.action-btn .badge) {
|
||
display: inline-block; margin-left: 6px; padding: 0 6px; min-width: 18px; height: 18px;
|
||
line-height: 18px; font-size: 11px; text-align: center;
|
||
background: #fff; color: var(--brand-primary); border-radius: 9px;
|
||
}
|
||
.page-toolbar :deep(.action-btn.secondary .badge) { background: var(--brand-primary); color: #fff; }
|
||
.score-row { padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
|
||
.score-row:last-child { border-bottom: none; }
|
||
.score-title { font-weight: 600; color: #303133; margin-bottom: 6px; }
|
||
.score-no { color: #909399; font-weight: normal; font-size: 12px; }
|
||
.score-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||
.score-item { display: flex; align-items: center; gap: 8px; }
|
||
.score-item span { width: 70px; color: #606266; font-size: 13px; }
|
||
|
||
/* 分配 dialog 总场次条 */
|
||
.assign-sessions-bar {
|
||
display: flex; align-items: center; gap: 18px;
|
||
margin: -4px 0 12px; padding: 10px 14px;
|
||
background: #f5f7fa; border: 1px solid #ebeef5; border-radius: 4px;
|
||
font-size: 13px; color: #606266;
|
||
}
|
||
.assign-sessions-bar b { color: #303133; font-size: 14px; margin: 0 2px; }
|
||
.assign-sessions-bar .is-over b { color: #f56c6c; }
|
||
.assign-sessions-bar .over-warn { color: #f56c6c; font-weight: 600; }
|
||
.assign-sessions-bar .hint { color: #909399; font-size: 12px; }
|
||
.assign-sessions-bar .sep { color: #dcdfe6; margin: 0 4px; }
|
||
|
||
/* ========================================
|
||
移动端适配 (≤768px)
|
||
- 卡片 padding 收窄
|
||
- 工具栏横向滚动 (6 个按钮铺不下, 用 overflow-x:auto 让用户滑动)
|
||
- filter-form 全宽垂直堆叠
|
||
- 表格字号缩小 (横向滚动靠 el-table 自带)
|
||
======================================== */
|
||
@media (max-width: 768px) {
|
||
/* 卡片 padding */
|
||
.page-card { padding: 12px !important; border-radius: 4px !important; }
|
||
.breadcrumb { font-size: 12px !important; margin-bottom: 8px !important; }
|
||
|
||
/* 工具栏: 横向滚动, 不换行 (6 个按钮) */
|
||
.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) {
|
||
flex-shrink: 0;
|
||
font-size: 12px !important;
|
||
padding: 0 10px !important;
|
||
height: 30px !important;
|
||
}
|
||
|
||
/* filter-form: 强制单列堆叠 (覆盖 inline style="width:260px" 等) */
|
||
.filter-form {
|
||
display: flex !important;
|
||
flex-direction: column !important;
|
||
align-items: stretch !important;
|
||
gap: 10px !important;
|
||
}
|
||
.filter-form :deep(.el-form-item) {
|
||
display: block !important;
|
||
margin-right: 0 !important;
|
||
margin-bottom: 0 !important;
|
||
}
|
||
.filter-form :deep(.el-form-item__label) {
|
||
float: none !important;
|
||
width: auto !important;
|
||
text-align: left !important;
|
||
padding: 0 0 4px !important;
|
||
font-size: 13px !important;
|
||
color: var(--el-text-color-secondary) !important;
|
||
line-height: 1.5 !important;
|
||
}
|
||
.filter-form :deep(.el-form-item__content) {
|
||
margin-left: 0 !important;
|
||
line-height: 1.6 !important;
|
||
display: flex !important;
|
||
flex-direction: column !important;
|
||
gap: 8px !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) {
|
||
width: 100% !important;
|
||
min-width: 0 !important;
|
||
}
|
||
/* 项目时间区间: 横向保持 (日期 → 至 → 日期) */
|
||
.filter-form :deep(.el-date-editor) { max-width: none !important; }
|
||
|
||
/* 表格字号缩小, 横向滚动靠 el-table 自带 */
|
||
:deep(.el-table) { font-size: 12px !important; }
|
||
/* 操作列固定到右侧, 加上阴影区隔 */
|
||
:deep(.el-table .fixed-right) {
|
||
box-shadow: -2px 0 6px rgba(0, 0, 0, 0.04);
|
||
}
|
||
}
|
||
</style> |