feat(oa/project/dispatch): 新增项目和合同的CRUD功能

重构项目调度页面:
1. 左侧项目列表新增增删改操作按钮
2. 合同模块新增工具栏和增删改功能
3. 实现完整的项目编辑弹窗,包含表单校验和字典数据绑定
4. 实现完整的合同编辑弹窗,支持甲乙双方信息填写
5. 补充对应的API调用和状态管理逻辑
6. 调整页面样式布局优化展示效果
This commit is contained in:
2026-07-07 15:34:34 +08:00
parent fd38ddfb91
commit e1dc6cb7b4

View File

@@ -4,8 +4,15 @@
<!-- 左栏项目列表 -->
<div class="panel-left">
<div class="left-header">
<span class="left-title">项目列表</span>
<span class="left-count">{{ projectList.length }} </span>
<div class="left-header-row">
<span class="left-title">项目管理</span>
<span class="left-count">{{ projectList.length }} </span>
</div>
<div class="left-actions">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAddProject">新增</el-button>
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="!activeProject" @click="handleEditProject">修改</el-button>
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="!activeProject" @click="handleDeleteProject">删除</el-button>
</div>
</div>
<div class="left-search">
<el-input v-model="searchText" placeholder="编号 / 代号 / 名称" prefix-icon="el-icon-search" size="small" clearable @input="filterProjects" />
@@ -103,7 +110,18 @@
<!-- 合同 -->
<template v-else-if="activeTab === 'contract'">
<div class="section-title">合同记录</div>
<el-table :data="moduleData.contract" size="small" border stripe>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAddContract">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="!contractSelectedRow" @click="handleEditContract">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="!contractSelectedRow" @click="handleDeleteContract">删除</el-button>
</el-col>
</el-row>
<el-table :data="moduleData.contract" size="small" border stripe highlight-current-row @current-change="val => { contractSelectedRow = val }">
<el-table-column label="合同编号" prop="contractNum" width="140" />
<el-table-column label="合同名称" prop="contractName" min-width="160" show-overflow-tooltip />
<el-table-column label="合同金额()" prop="contractPrice" width="120" align="right">
@@ -585,12 +603,223 @@
<p>请从左侧选择一个项目</p>
</div>
<!-- ══════════════ 项目编辑弹窗 ══════════════ -->
<el-dialog :title="projectTitle" :visible.sync="projectEditShow" width="70%" append-to-body @close="closeProjectDialog">
<el-form ref="projectFormRef" :model="projectForm" :rules="projectRules" label-width="100px">
<el-divider content-position="center">基本信息</el-divider>
<el-row :gutter="16">
<el-col :span="16">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="projectForm.projectName" placeholder="请输入项目名称" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="项目总金额" prop="funds">
<el-input v-model="projectForm.funds" placeholder="" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="负责人" prop="functionary">
<el-input v-model="projectForm.functionary" placeholder="项目负责人" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="项目编号" prop="projectNum">
<el-input v-model="projectForm.projectNum" placeholder="项目编号" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="项目代号" prop="projectCode">
<el-select v-model="projectForm.projectCodeType" placeholder="代号类型" style="width:45%" filterable>
<el-option v-for="dict in dict.type.sys_project_code" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
<el-input v-model="projectForm.projectCodeNumber" placeholder="序号" style="width:55%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目地址" prop="address">
<el-input v-model="projectForm.address" placeholder="项目地址" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="贸易类型">
<el-select v-model="projectForm.tradeType" placeholder="贸易类型" style="width:100%">
<el-option v-for="dict in dict.type.sys_trade_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="项目类型">
<el-select v-model="projectForm.projectType" placeholder="项目类型" style="width:100%">
<el-option v-for="dict in dict.type.sys_project_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="优先级">
<el-radio-group v-model="projectForm.projectGrade">
<el-radio-button v-for="dict in dict.type.sys_sort_grade" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="项目周期">
<el-date-picker v-model="projectPeriod" value-format="yyyy-MM-dd HH:mm:ss" type="daterange" range-separator="" start-placeholder="开始" end-placeholder="结束" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="交货期" prop="delivery">
<el-input v-model="projectForm.delivery" placeholder="交货期" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="质保期" prop="guarantee">
<el-input v-model="projectForm.guarantee" placeholder="质保期" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="签约公司">
<el-select v-model="projectForm.signingCompany" placeholder="签约公司" style="width:100%" clearable>
<el-option v-for="dict in dict.type.signing_company" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预付款" prop="prePay">
<el-input v-model="projectForm.prePay" placeholder="预付款金额" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="项目介绍">
<el-input v-model="projectForm.introduction" type="textarea" :rows="3" placeholder="项目介绍" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="projectForm.remark" type="textarea" :rows="2" placeholder="备注" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="附件">
<file-upload v-model="projectForm.accessory" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer">
<el-button @click="closeProjectDialog">取 消</el-button>
<el-button type="primary" :loading="projectSubmitLoading" @click="submitProjectForm">保 存</el-button>
</div>
</el-dialog>
<!-- ══════════════ 合同编辑弹窗 ══════════════ -->
<el-dialog :title="contractTitle" :visible.sync="contractEditShow" width="65%" append-to-body @close="closeContractDialog">
<el-form ref="contractFormRef" :model="contractForm" label-width="100px">
<!-- 基本信息 -->
<el-divider content-position="left">基本信息</el-divider>
<el-row :gutter="16">
<el-col :span="16">
<el-form-item label="合同名称" prop="contractName">
<el-input v-model="contractForm.contractName" placeholder="合同名称" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="合同编号" prop="contractNum">
<el-input v-model="contractForm.contractNum" placeholder="合同编号" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="合同金额()" prop="contractPrice">
<el-input v-model="contractForm.contractPrice" placeholder="合同金额" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="合同类型">
<el-select v-model="contractForm.contractType" placeholder="合同类型" style="width:100%">
<el-option label="项目合同" value="2" />
<el-option label="采购合同" value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="合同状态">
<el-input v-model="contractForm.contractStatus" placeholder="合同状态" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="签订日期 / 有效期">
<el-date-picker v-model="contractPeriod" value-format="yyyy-MM-dd" type="daterange" range-separator="" start-placeholder="签订日期" end-placeholder="有效期" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<!-- 甲方 -->
<el-divider content-position="left">甲方信息</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="甲方单位">
<el-input v-model="contractForm.firstName" placeholder="甲方单位名称" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="甲方负责人">
<el-input v-model="contractForm.firstPerson" placeholder="负责人" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="甲方电话">
<el-input v-model="contractForm.firstPhone" placeholder="联系电话" />
</el-form-item>
</el-col>
</el-row>
<!-- 乙方 -->
<el-divider content-position="left">乙方信息</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="乙方单位">
<el-input v-model="contractForm.secondName" placeholder="乙方单位名称" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="乙方负责人">
<el-input v-model="contractForm.secondPerson" placeholder="负责人" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="乙方电话">
<el-input v-model="contractForm.secondPhone" placeholder="联系电话" />
</el-form-item>
</el-col>
</el-row>
<!-- 其他 -->
<el-divider content-position="left">其他信息</el-divider>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="contractForm.remark" type="textarea" :rows="2" placeholder="备注" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="附件">
<file-upload v-model="contractForm.accessory" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer">
<el-button @click="closeContractDialog">取 消</el-button>
<el-button type="primary" :loading="contractSubmitLoading" @click="submitContractForm">保 存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listProject, getProject } from '@/api/oa/project'
import { listOaContract } from '@/api/oa/oaContract'
import { listProject, getProject, addProject, updateProject, delProject } from '@/api/oa/project'
import { listOaContract, addOaContract, updateOaContract, delOaContract } from '@/api/oa/oaContract'
import { listFinancePro } from '@/api/oa/finance'
import request from '@/utils/request'
// /oa/cost/list1 支持 projectId 过滤list 是全项目汇总,不能用)
@@ -611,6 +840,7 @@ import { listReportSummary } from '@/api/oa/reportSummary'
import { listFileOperationRecord } from '@/api/oa/fileOperationRecord'
import { listFeedback } from '@/api/oa/feedback'
import { listOperationLog } from '@/api/oa/projectOperationLog'
import FileUpload from '@/components/FileUpload/index.vue'
const MODULES = [
{ key: 'info', label: '基本信息', icon: 'el-icon-office-building' },
@@ -655,6 +885,8 @@ function makePaging () { return { pageNum: 1, pageSize: 20, total: 0 } }
export default {
name: 'ProjectDispatch',
components: { FileUpload },
dicts: ['sys_project_status', 'sys_project_type', 'sys_trade_type', 'sys_project_code', 'signing_company', 'sys_sort_grade'],
data () {
return {
MODULES,
@@ -667,7 +899,29 @@ export default {
moduleData: {},
moduleLoading: {},
badgeCounts: {},
modulePaging: Object.fromEntries(MODULES.map(m => [m.key, makePaging()]))
modulePaging: Object.fromEntries(MODULES.map(m => [m.key, makePaging()])),
// ── 项目 CRUD ──
projectEditShow: false,
projectForm: {},
projectPeriod: [],
projectTitle: '',
projectSubmitLoading: false,
// ── 合同 CRUD ──
contractEditShow: false,
contractForm: {},
contractTitle: '',
contractSubmitLoading: false,
contractSelectedRow: null,
// 合同表单额外字段
contractPeriod: [],
// ── 表单校验规则 ──
projectRules: {
projectName: [{ required: true, message: '项目名称不能为空', trigger: 'blur' }],
projectNum: [{ required: true, message: '项目编号不能为空', trigger: 'blur' }],
functionary: [{ required: true, message: '负责人不能为空', trigger: 'blur' }],
address: [{ required: true, message: '项目地址不能为空', trigger: 'blur' }],
funds: [{ required: true, message: '项目总金额不能为空', trigger: 'blur' }]
}
}
},
computed: {
@@ -796,6 +1050,126 @@ export default {
listOperationLog(cp).then(r => setCount('oplog', r)).catch(() => {})
},
// ── 项目 CRUD 方法 ──
handleAddProject () {
this.projectForm = {}
this.projectPeriod = []
this.projectTitle = '新增项目'
this.projectEditShow = true
},
handleEditProject () {
const p = this.activeProject
if (!p) { this.$message.warning('请先选择项目'); return }
getProject(p.projectId).then(res => {
this.projectForm = res.data || {}
this.projectPeriod = [this.projectForm.beginTime, this.projectForm.finishTime]
this.projectTitle = '编辑项目: ' + (res.data?.projectName || '')
this.projectEditShow = true
}).catch(() => { this.$message.error('获取项目详情失败') })
},
handleDeleteProject () {
const p = this.activeProject
if (!p) { this.$message.warning('请先选择项目'); return }
this.$confirm(`确定删除项目"${p.projectName}"?删除后不可恢复。`, '警告', {
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return delProject(p.projectId)
}).then(() => {
this.$message.success('删除成功')
this.activeProject = null
this.loadProjects()
}).catch(() => {})
},
submitProjectForm () {
this.projectSubmitLoading = true
const payload = {
...this.projectForm,
beginTime: this.projectPeriod[0] || null,
finishTime: this.projectPeriod[1] || null,
projectCode: (this.projectForm.projectCodeType && this.projectForm.projectCodeNumber)
? this.projectForm.projectCodeType + '-' + this.projectForm.projectCodeNumber
: this.projectForm.projectCode || undefined
}
const isEdit = !!this.projectForm.projectId
const api = isEdit ? updateProject(payload) : addProject(payload)
api.then(() => {
this.$message.success(isEdit ? '修改成功' : '新增成功')
this.projectEditShow = false
this.loadProjects()
// 如果是编辑,刷新右侧详情
if (isEdit && this.activeProject) {
this.activeProject = { ...this.activeProject, ...payload }
}
}).catch(() => {
this.$message.error('操作失败')
}).finally(() => {
this.projectSubmitLoading = false
})
},
closeProjectDialog () {
this.projectEditShow = false
this.projectForm = {}
},
// ── 合同 CRUD 方法 ──
handleAddContract () {
if (!this.activeProject) { this.$message.warning('请先选择项目'); return }
this.contractForm = { projectId: this.activeProject.projectId, contractType: '2' }
this.contractPeriod = []
this.contractTitle = '新增合同'
this.contractEditShow = true
},
handleEditContract () {
const row = this.contractSelectedRow
if (!row) { this.$message.warning('请先选择一条合同记录'); return }
this.contractForm = { ...row }
this.contractPeriod = [row.signTime, row.validity]
this.contractTitle = '编辑合同: ' + (row.contractName || '')
this.contractEditShow = true
},
handleDeleteContract () {
const row = this.contractSelectedRow
if (!row) { this.$message.warning('请先选择一条合同记录'); return }
this.$confirm(`确定删除合同"${row.contractName}"`, '警告', {
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return delOaContract(row.contractId)
}).then(() => {
this.$message.success('删除成功')
this.contractSelectedRow = null
this.fetchModule('contract')
this.loadAllBadges()
}).catch(() => {})
},
submitContractForm () {
this.contractSubmitLoading = true
const payload = {
...this.contractForm,
projectId: this.activeProject?.projectId,
signTime: this.contractPeriod[0] || null,
validity: this.contractPeriod[1] || null
}
const isEdit = !!this.contractForm.contractId
const api = isEdit ? updateOaContract(payload) : addOaContract(payload)
api.then(() => {
this.$message.success(isEdit ? '修改成功' : '新增成功')
this.contractEditShow = false
this.contractSelectedRow = null
this.fetchModule('contract')
this.loadAllBadges()
}).catch(() => {
this.$message.error('操作失败')
}).finally(() => {
this.contractSubmitLoading = false
})
},
closeContractDialog () {
this.contractEditShow = false
this.contractForm = {}
},
// ── 辅助 ──
// sys_project_status 字典: '0'=进行中(primary), '1'=进度完成(success)
statusLabel (s) {
@@ -863,15 +1237,18 @@ export default {
overflow: hidden;
}
.left-header {
padding: 14px 14px 8px;
display: flex;
align-items: baseline;
gap: 6px;
padding: 10px 12px 8px;
flex-shrink: 0;
border-bottom: 1px solid #f0f0f0;
}
.left-header-row {
display: flex;
align-items: baseline;
gap: 6px;
}
.left-title { font-size: 14px; font-weight: 700; color: #303133; }
.left-count { font-size: 12px; color: #909399; }
.left-actions { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; }
.left-search { padding: 10px 10px 6px; flex-shrink: 0; }
.left-list { flex: 1; overflow-y: auto; }
.left-empty { text-align: center; color: #c0c4cc; padding: 40px 0; font-size: 13px; }
@@ -1020,6 +1397,7 @@ export default {
padding-left: 8px;
border-left: 3px solid #409EFF;
}
.mb8 { margin-bottom: 8px; }
/* ══ 统计卡片(基本信息页底部) ══ */
.stat-mini {