Files
guoju0808/ry-vue3/src/views/manager/Plans.vue
T
guoju0808 bfc801a16a feat: 批量移动端适配 (24 个列表页 + 13 个宽表用 GrTable)
- 给 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 错误
2026-08-26 11:43:08 +00:00

442 lines
19 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>
<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>