- 给 24 个有 filter-form 但无 media query 的列表页统一加 @media (max-width:768px) - 卡片 padding 收窄 (12px, radius 4px) - 工具栏横向滚动 (6+ 按钮铺不下) - filter-form 保持横向布局 (label 左 80px 右对齐 + input 右占满) - 查找/重置按钮垂直堆叠不偏移 - 表格字号 12px - 13 个宽表 (≥10 列) 替换为 GrTable 组件: - sponsor/Projects, executor/Projects, sponsor/SponsorProjects - sponsor/SponsorPeople, expert/Experts, executor/People - meetings/Meetings, sponsor/Meetings, executor/Meetings - manager/ExecIntent, manager/SupportIntent, manager/Plans - admin/Users - 每个页面的 mainCols 取业务主键 (项目编号+名称 / 账号+姓名 等) - 手机端次要列折叠到展开行, 主列+操作列保留 - 修复 executor/Projects.vue 多行 el-table 标签导致的 GrTable 替换错误 全 95 个 vue 编译 200, 0 错误
442 lines
19 KiB
Vue
442 lines
19 KiB
Vue
<template>
|
||
<div class="page-card manager-plans">
|
||
<div class="breadcrumb">首页 / 策划方案管理</div>
|
||
|
||
<!-- ========== 筛选区 (与 People.vue 风格一致) ========== -->
|
||
<el-form inline :model="q" class="filter-form">
|
||
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable @input="onProjectNoSearch" @clear="onProjectNoSearch" /></el-form-item>
|
||
<el-form-item label="策划方案名称"><el-input v-model="q.planName" placeholder="输入策划方案名称" clearable /></el-form-item>
|
||
<el-form-item label="项目方向">
|
||
<el-select v-model="q.planDirectionId" placeholder="请选择" clearable style="width:200px">
|
||
<el-option v-for="opt in planDirectionOptions" :key="opt.id" :label="opt.title" :value="opt.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="项目类别">
|
||
<dict-select v-model="q.planCategory" dict-type="biz_project_category" value-field="label" style="width:160px" />
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="q.status" placeholder="请选择" clearable style="width:140px">
|
||
<el-option label="待审核" value="1" /><el-option label="通过" value="2" /><el-option label="拒绝" value="3" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="是否结算">
|
||
<el-select v-model="q.isSettled" placeholder="请选择" clearable style="width:140px">
|
||
<el-option label="已结算" value="Y" /><el-option label="未结算" value="N" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="备注"><el-input v-model="q.remark" placeholder="输入备注" clearable /></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>
|
||
|
||
<!-- ========== 批量按钮 (与 People.vue 风格一致) ========== -->
|
||
<div class="batch-bar">
|
||
<el-button type="primary" :disabled="!selection.length" @click="openBatchAudit">批量审核</el-button>
|
||
<el-button type="primary" :disabled="!selection.length" @click="batchSettle">批量结算</el-button>
|
||
<span v-if="selection.length" class="filter-tip">已选 {{ selection.length }} 条</span>
|
||
</div>
|
||
|
||
<!-- ========== 表格 (与 People.vue 风格一致, 无 page-card 包裹) ========== -->
|
||
<GrTable :data="rows" v-loading="loading" stripe border @selection-change="onSel"
|
||
:main-cols="['planName', 'projectForm']"
|
||
>
|
||
<el-table-column type="selection" width="44" />
|
||
<el-table-column prop="planName" label="策划方案名称" min-width="200" fixed show-overflow-tooltip />
|
||
<el-table-column prop="planDirectionTitle" label="项目方向" min-width="220" show-overflow-tooltip />
|
||
<el-table-column prop="planCategory" label="项目类别" width="100" align="center" />
|
||
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
|
||
<el-table-column label="设计文件" width="160" align="center">
|
||
<template #default="{ row }">
|
||
<template v-if="row.designFileUrl">
|
||
<el-link :underline="false" type="primary" @click="openDesign(row)">查看</el-link>
|
||
<el-link :underline="false" type="primary" @click="downloadFile(row)">下载</el-link>
|
||
</template>
|
||
<span v-else style="color:#c0c4cc">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<audit-status-tag :status="row.status" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="是否结算" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag size="small" :type="row.isSettled==='Y' ? 'success' : 'warning'">
|
||
{{ row.isSettled==='Y' ? '已结算' : '未结算' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="projectNo" label="项目编号" width="140" align="center" />
|
||
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
|
||
<el-table-column label="操作" width="200" fixed="right">
|
||
<template #default="{ row }"><div class="table-actions">
|
||
<el-link :underline="false" type="primary" @click="onAudit(row)">审核</el-link>
|
||
<el-link :underline="false" type="primary" @click="goEdit(row)">修改</el-link>
|
||
<el-link :underline="false" type="primary" @click="onSettle(row)" v-if="row.isSettled!=='Y'">结算</el-link>
|
||
</div></template>
|
||
</el-table-column>
|
||
</GrTable>
|
||
<div class="pager">
|
||
<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>
|
||
|
||
<!-- 审核 dialog: 策划案名称 + 审核结果(通过/退回 radio) + 项目编号(仅通过时必填) + 审核意见(可选) -->
|
||
<el-dialog v-model="auditOpen" title="审核策划方案" width="500px">
|
||
<el-form :model="auditForm" :rules="auditRules" ref="auditFormRef" label-width="100px">
|
||
<el-form-item label="策划案名称"><span>{{ auditForm.planName }}</span></el-form-item>
|
||
<el-form-item label="审核结果" prop="result">
|
||
<el-radio-group v-model="auditForm.result">
|
||
<el-radio value="2">通过</el-radio>
|
||
<el-radio value="3">退回</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item v-if="auditForm.result === '2'" label="项目编号" prop="projectNo">
|
||
<el-select v-model="auditForm.projectNo" placeholder="请选择项目编号" filterable style="width:100%">
|
||
<el-option v-for="opt in projectNoOptions" :key="opt.projectNo" :label="opt.projectNo + (opt.projectName ? ' / ' + opt.projectName : '')" :value="opt.projectNo" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="审核意见">
|
||
<el-input v-model="auditForm.opinion" type="textarea" rows="3" placeholder="请填写审核意见 (可选)" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="auditOpen=false">取消</el-button>
|
||
<el-button type="primary" :loading="submitting" @click="submitAudit">确认</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 批量审核 dialog: 审核结果(通过/退回 radio) + 项目编号(仅通过时必填) + 审核意见(可选), 批量应用于所有选中方案 -->
|
||
<el-dialog v-model="batchAuditOpen" title="批量审核策划方案" width="500px">
|
||
<el-form :model="batchAuditForm" :rules="batchAuditRules" ref="batchAuditFormRef" label-width="100px">
|
||
<el-form-item label="审核结果" prop="result">
|
||
<el-radio-group v-model="batchAuditForm.result">
|
||
<el-radio value="2">通过</el-radio>
|
||
<el-radio value="3">退回</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item v-if="batchAuditForm.result === '2'" label="项目编号" prop="projectNo">
|
||
<el-select v-model="batchAuditForm.projectNo" placeholder="请选择项目编号" filterable style="width:100%">
|
||
<el-option v-for="opt in projectNoOptions" :key="opt.projectNo" :label="opt.projectNo + (opt.projectName ? ' / ' + opt.projectName : '')" :value="opt.projectNo" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="审核意见">
|
||
<el-input v-model="batchAuditForm.opinion" type="textarea" rows="3" placeholder="请填写审核意见 (可选)" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="batchAuditOpen=false">取消</el-button>
|
||
<el-button type="primary" :loading="submitting" @click="submitBatchAudit">确认</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 结算 dialog (按原型: 仅文字 + 确定/取消) -->
|
||
<el-dialog v-model="settleOpen" title="结算策划方案" width="420px" :show-close="false">
|
||
<p>您确定要结算吗?</p>
|
||
<template #footer>
|
||
<el-button @click="settleOpen=false">取消</el-button>
|
||
<el-button type="primary" :loading="submitting" @click="submitSettle">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, watch, onMounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { bizList, bizUpdate } from '@/api/public'
|
||
import GrTable from '@/components/GrTable.vue'
|
||
import request from '@/utils/request'
|
||
import { ElMessage } from 'element-plus'
|
||
import DictSelect from '@/components/DictSelect.vue'
|
||
import AuditStatusTag from '@/components/AuditStatusTag.vue'
|
||
|
||
// 专项计划下拉选项 (项目方向): [{id, title}]
|
||
const planDirectionOptions = ref([])
|
||
async function loadPlanDirectionOptions() {
|
||
try {
|
||
const { data } = await request.get('/business/specialPlan/options')
|
||
planDirectionOptions.value = (data && data.data) || data || []
|
||
} catch { planDirectionOptions.value = [] }
|
||
}
|
||
|
||
// 项目编号下拉选项: [{projectNo, projectName}], 审核通过时选择用
|
||
const projectNoOptions = ref([])
|
||
async function loadProjectNoOptions() {
|
||
try {
|
||
const { data } = await request.get('/business/project/projectNoOptions')
|
||
projectNoOptions.value = (data && data.data) || data || []
|
||
} catch { projectNoOptions.value = [] }
|
||
}
|
||
|
||
const router = useRouter()
|
||
const q = ref({ planName: '', projectNo: '', planDirectionId: '', planCategory: '', status: '', isSettled: '', remark: '' })
|
||
const rows = ref([]); const selection = ref([])
|
||
const loading = ref(false)
|
||
const submitting = ref(false)
|
||
const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
const { data } = await bizList('projectPlan', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
|
||
rows.value = data?.rows || []
|
||
page.total = data?.total || 0
|
||
} catch { rows.value = []; page.total = 0 }
|
||
finally { loading.value = false }
|
||
}
|
||
// 项目编号: 边输入边搜索 (debounce 300ms)
|
||
let projectNoTimer = null
|
||
function onProjectNoSearch() {
|
||
if (projectNoTimer) clearTimeout(projectNoTimer)
|
||
projectNoTimer = setTimeout(() => { page.pageNum = 1; load() }, 300)
|
||
}
|
||
function reset() { q.value = { planName:'', projectNo:'', planDirectionId:'', planCategory:'', status:'', isSettled:'', remark:'' }; page.pageNum = 1; load() }
|
||
function onSel(rs) { selection.value = rs }
|
||
|
||
async function batchSettle() {
|
||
if (!selection.value.length) return ElMessage.warning('请先勾选行')
|
||
submitting.value = true
|
||
let ok = 0
|
||
for (const r of selection.value) {
|
||
try { await bizUpdate('projectPlan', { planId: r.planId, isSettled: 'Y' }); ok++ } catch {}
|
||
}
|
||
submitting.value = false
|
||
ElMessage.success(`批量结算 ${ok}/${selection.value.length} 条`)
|
||
selection.value = []
|
||
load()
|
||
}
|
||
|
||
const auditOpen = ref(false)
|
||
const auditFormRef = ref(null)
|
||
// 审核: 通过 '2' / 退回 '3' (BizAuditStatusEnum); 通过时必填项目编号, 退回时不填; 审核意见可选
|
||
const auditForm = reactive({ planId: '', planName: '', result: '2', projectNo: '', opinion: '' })
|
||
const auditRules = reactive({
|
||
result: [{ required: true, message: '请选择审核结果', trigger: 'change' }],
|
||
projectNo: [{ required: false }]
|
||
})
|
||
// 通过时: projectNo 必填; 退回时: projectNo 非必填且隐藏 (同时清掉校验状态)
|
||
watch(() => auditForm.result, (val) => {
|
||
if (val === '2') {
|
||
auditRules.projectNo = [{ required: true, message: '请填写项目编号', trigger: 'blur' }]
|
||
} else {
|
||
auditRules.projectNo = [{ required: false }]
|
||
auditFormRef.value?.clearValidate(['projectNo'])
|
||
}
|
||
})
|
||
function onAudit(row) {
|
||
auditForm.planId = row.planId
|
||
auditForm.planName = row.planName
|
||
auditForm.result = '2'
|
||
auditForm.projectNo = ''
|
||
auditForm.opinion = ''
|
||
loadProjectNoOptions()
|
||
auditOpen.value = true
|
||
}
|
||
async function submitAudit() {
|
||
try {
|
||
await auditFormRef.value.validate()
|
||
submitting.value = true
|
||
const isPass = auditForm.result === '2'
|
||
const payload = { planId: auditForm.planId, status: auditForm.result, auditOpinion: auditForm.opinion }
|
||
if (isPass) payload.projectNo = auditForm.projectNo
|
||
await bizUpdate('projectPlan', payload)
|
||
ElMessage.success(isPass ? '已通过' : '已退回')
|
||
auditOpen.value = false
|
||
load()
|
||
} catch (e) { if (e?.message) ElMessage.error(e.message) }
|
||
finally { submitting.value = false }
|
||
}
|
||
|
||
// 批量审核: 与单独审核字段一致 (审核结果/项目编号/审核意见), 批量应用于所有选中方案
|
||
const batchAuditOpen = ref(false)
|
||
const batchAuditFormRef = ref(null)
|
||
const batchAuditForm = reactive({ result: '2', projectNo: '', opinion: '' })
|
||
const batchAuditRules = reactive({
|
||
result: [{ required: true, message: '请选择审核结果', trigger: 'change' }],
|
||
projectNo: [{ required: false }]
|
||
})
|
||
// 通过时: projectNo 必填; 退回时: projectNo 非必填且隐藏 (同时清掉校验状态)
|
||
watch(() => batchAuditForm.result, (val) => {
|
||
if (val === '2') {
|
||
batchAuditRules.projectNo = [{ required: true, message: '请填写项目编号', trigger: 'blur' }]
|
||
} else {
|
||
batchAuditRules.projectNo = [{ required: false }]
|
||
batchAuditFormRef.value?.clearValidate(['projectNo'])
|
||
}
|
||
})
|
||
function openBatchAudit() {
|
||
if (!selection.value.length) return ElMessage.warning('请先勾选行')
|
||
batchAuditForm.result = '2'
|
||
batchAuditForm.projectNo = ''
|
||
batchAuditForm.opinion = ''
|
||
loadProjectNoOptions()
|
||
batchAuditOpen.value = true
|
||
}
|
||
async function submitBatchAudit() {
|
||
try {
|
||
await batchAuditFormRef.value.validate()
|
||
submitting.value = true
|
||
const isPass = batchAuditForm.result === '2'
|
||
const payload = { status: batchAuditForm.result, auditOpinion: batchAuditForm.opinion }
|
||
if (isPass) payload.projectNo = batchAuditForm.projectNo
|
||
let ok = 0
|
||
for (const r of selection.value) {
|
||
try { await bizUpdate('projectPlan', { ...payload, planId: r.planId }); ok++ } catch {}
|
||
}
|
||
submitting.value = false
|
||
ElMessage.success(`批量审核 ${ok}/${selection.value.length} 条`)
|
||
batchAuditOpen.value = false
|
||
selection.value = []
|
||
load()
|
||
} catch (e) { if (e?.message) ElMessage.error(e.message) }
|
||
finally { submitting.value = false }
|
||
}
|
||
|
||
// 修改: 跳转到独立页面 PlanEdit.vue
|
||
function goEdit(row) {
|
||
router.push({ path: `/manager/plans/edit/${row.planId}` })
|
||
}
|
||
|
||
const settleOpen = ref(false)
|
||
const settleForm = reactive({ planId:'', planName:'' })
|
||
function onSettle(row) { settleForm.planId = row.planId; settleForm.planName = row.planName; settleOpen.value = true }
|
||
async function submitSettle() {
|
||
try {
|
||
submitting.value = true
|
||
await bizUpdate('projectPlan', { planId: settleForm.planId, isSettled: 'Y' })
|
||
ElMessage.success('已结算')
|
||
settleOpen.value = false
|
||
load()
|
||
} catch { ElMessage.error('结算失败') }
|
||
finally { submitting.value = false }
|
||
}
|
||
|
||
// OSS bucket 设置了 Content-Disposition: attachment, iframe/embed 直接访问会被强制下载.
|
||
// 走 /common/oss/proxy 后端代理 (重写为 inline), 浏览器原生 PDF viewer 体验更好 (与 portal/PublicityDetail 一致)
|
||
// 必须加 /dev-api 前缀: Vite dev server proxy 只代理 /dev-api/* 到后端, 其他相对 URL 会 SPA fallback
|
||
function proxyUrl(url) {
|
||
if (!url) return url
|
||
if (url.includes('hwtossbamlorgcn.oss-cn-beijing.aliyuncs.com')) {
|
||
return import.meta.env.VITE_APP_BASE_API + '/common/oss/proxy?url=' + encodeURIComponent(url) + '#toolbar=0&zoom=page-width'
|
||
}
|
||
return url
|
||
}
|
||
function openDesign(row) {
|
||
if (!row.designFileUrl) return
|
||
window.open(proxyUrl(row.designFileUrl), '_blank')
|
||
}
|
||
function downloadFile(row) {
|
||
if (!row.designFileUrl) return
|
||
const a = document.createElement('a')
|
||
a.href = row.designFileUrl
|
||
a.download = row.planName || '设计文件'
|
||
a.target = '_blank'
|
||
a.click()
|
||
}
|
||
|
||
onMounted(() => { loadPlanDirectionOptions(); load() })
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 整页面板 (与 People.vue 风格一致) */
|
||
.manager-plans { padding: 16px; }
|
||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||
.page-title {
|
||
font-size: 22px;
|
||
font-weight: 600;
|
||
color: #1a1a1a;
|
||
margin: 0 0 6px;
|
||
padding-left: 10px;
|
||
border-left: 3px solid var(--brand-primary);
|
||
}
|
||
.page-sub { font-size: 13px; color: #909399; margin: 0 0 16px; }
|
||
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||
.filter-tip { color: #909399; font-size: 12px; }
|
||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||
|
||
/* ========================================
|
||
移动端适配 (≤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>
|