Files
guoju0808/ry-vue3/src/views/manager/Plans.vue
T
郭庆泰 55f3147463 refactor(manager/plans): 删表格项目ID 列 (不在页面上展示)
planId 仍保留在 auditForm (后端 WHERE 用), 只是表格不渲染
2026-08-18 01:11:09 +08:00

326 lines
15 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="batchSettle">批量结算</el-button>
<span v-if="selection.length" class="filter-tip">已选 {{ selection.length }} </span>
</div>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSel">
<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-button link type="primary" @click="onFile(row)">查看</el-button>
<el-button link type="primary" @click="downloadFile(row)">下载</el-button>
</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="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onAudit(row)">审核</el-button>
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
<el-button link type="primary" @click="onSettle(row)" v-if="row.isSettled!=='Y'">结算</el-button>
<el-button link type="primary" v-if="row.designFileUrl" @click="downloadFile(row)">下载</el-button>
</template>
</el-table-column>
</el-table>
<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 -->
<el-dialog v-model="editOpen" title="修改策划方案" width="640px" @closed="resetEditForm">
<el-form :model="editForm" label-width="120px" :rules="editRules" ref="editFormRef">
<el-form-item label="方案名称" prop="planName"><el-input v-model="editForm.planName" /></el-form-item>
<el-form-item label="项目编号" prop="projectNo"><el-input v-model="editForm.projectNo" /></el-form-item>
<el-form-item label="项目方向">
<el-select v-model="editForm.planDirectionId" placeholder="请选择" style="width:100%">
<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="editForm.planCategory" dict-type="biz_project_category" value-field="label" style="width:100%" />
</el-form-item>
<el-form-item label="项目形式">
<el-select v-model="editForm.projectForm" placeholder="请选择" style="width:100%">
<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 v-model="editForm.designFileUrl" placeholder="文件 URL(暂以文本输入)" />
</el-form-item>
<el-form-item label="备注"><el-input v-model="editForm.remark" type="textarea" rows="2" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="editOpen=false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="submitEdit">保存</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>
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
</div>
</template>
<script setup>
import { ref, reactive, watch, onMounted } from 'vue'
import { bizList, bizUpdate } from '@/api/public'
import request from '@/utils/request'
import { ElMessage } from 'element-plus'
import Preview from '@/components/Preview.vue'
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 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: 10, 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 editOpen = ref(false)
const editFormRef = ref(null)
const editForm = reactive({ planId:'', planName:'', projectNo:'', planDirectionId:'', planCategory:'', projectForm:'', designFileUrl:'', remark:'' })
const editRules = { planName:[{required:true,message:'请输入方案名称',trigger:'blur'}] }
function onEdit(row) {
Object.assign(editForm, {
planId: row.planId, planName: row.planName, projectNo: row.projectNo,
planDirectionId: row.planDirectionId ?? null, planCategory: row.planCategory,
projectForm: row.projectForm, designFileUrl: row.designFileUrl || '', remark: row.remark || ''
})
editOpen.value = true
}
function resetEditForm() { Object.assign(editForm, { planId:'', planName:'', projectNo:'', planDirectionId:'', planCategory:'', projectForm:'', designFileUrl:'', remark:'' }) }
async function submitEdit() {
await editFormRef.value.validate()
try {
submitting.value = true
await bizUpdate('projectPlan', { ...editForm })
ElMessage.success('已保存')
editOpen.value = false
load()
} catch (e) { ElMessage.error(e?.msg || '保存失败') }
finally { submitting.value = false }
}
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 }
}
const previewOpen = ref(false); const previewUrl = ref(''); const previewTitle = ref('策划方案预览')
function onFile(row) { previewUrl.value = row.designFileUrl; previewTitle.value = `策划方案 - ${row.planName || ''}`; previewOpen.value = true }
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; }
.filter-form { margin-bottom: 12px; }
.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; }
</style>