结算和合同管理模块开发

This commit is contained in:
BianLzhaoMin 2026-01-26 13:50:42 +08:00
parent 872fc0e443
commit 519a395fc7
10 changed files with 2096 additions and 0 deletions

View File

@ -0,0 +1,38 @@
import request from '@/utils/request'
// 合同管理 - 查询列表
export function listContractAPI(query) {
return request({
url: '/contract/getContractList',
method: 'GET',
params: query,
})
}
// 合同管理 - 新增
export function addContractAPI(data) {
return request({
url: '/contract/addContract',
method: 'POST',
data,
})
}
// 合同管理 - 修改
export function updateContractAPI(data) {
return request({
url: '/contract/updateContract',
method: 'POST',
data,
})
}
// 合同管理 - 删除
export function delContractAPI(data) {
return request({
url: '/contract/delContract',
method: 'POST',
data,
})
}

View File

@ -0,0 +1,77 @@
import request from '@/utils/request'
// 结算管理 - 查询列表
export function listSettlementAPI(query) {
return request({
url: '/settlement/getSettlementList',
method: 'GET',
params: query,
})
}
// 结算管理 - 查询详情
export function getSettlementDetailAPI(settlementId) {
return request({
url: `/settlement/getSettlementDetail`,
method: 'GET',
params: {
settlementId,
},
})
}
// 结算管理 - 新增
export function addSettlementAPI(data) {
return request({
url: '/settlement/addSettlement',
method: 'POST',
data,
})
}
// 结算管理 - 修改
export function updateSettlementAPI(data) {
return request({
url: '/settlement/updateSettlement',
method: 'POST',
data,
})
}
// 结算管理 - 删除
export function delSettlementAPI(data) {
return request({
url: '/settlement/delSettlement',
method: 'POST',
data,
})
}
// 结算管理 - 获取合同下拉列表
export function getContractSelectAPI(query) {
return request({
url: '/contract/getContractListAll',
method: 'GET',
params: query,
})
}
// 结算管理 - 获取计划列表(用于选择计划弹框)
export function getPlanListForSelectAPI(query) {
return request({
url: '/settlement/getMonthPlanSelect',
method: 'GET',
params: query,
})
}
// 结算管理 - 下载统计表
export function downloadSettlementStatisticAPI(query) {
return request({
url: '/settlement/downloadStatistic',
method: 'GET',
params: query,
responseType: 'blob',
})
}

View File

@ -117,6 +117,22 @@ export const constantRoutes = [
},
],
},
{
path: '/settlement/settlementEdit',
component: Layout,
hidden: true,
children: [
{
path: 'index',
component: () => import('@/views/settlementManage/edit.vue'),
name: 'SettlementEdit',
meta: {
title: '结算管理',
activeMenu: '/settlement', // 保持左侧高亮在列表菜单
},
},
],
},
]
// 动态路由,基于用户权限动态去加载

View File

@ -0,0 +1,179 @@
<template>
<el-form
size="large"
label-width="auto"
:model="formData"
ref="formRef"
:rules="rules"
class="add-and-edit-form"
>
<!-- 运检站 -->
<el-form-item label="运检站" prop="inspectionStationId">
<el-select
clearable
filterable
style="width: 100%"
placeholder="请选择运检站"
@change="handleInspectionStationChange"
v-model="formData.inspectionStationId"
>
<el-option
v-for="item in inspectionStationOptions"
:key="item.id"
:value="item.id"
:label="item.value"
/>
</el-select>
</el-form-item>
<!-- 合同承揽时间 -->
<el-form-item label="合同承揽时间" prop="contractPeriod">
<el-date-picker
type="year"
style="width: 100%"
value-format="YYYY"
placeholder="请选择合同承揽时间"
v-model="formData.contractPeriod"
/>
</el-form-item>
<!-- 合同名称 -->
<el-form-item label="合同名称" prop="contractName">
<el-input
clearable
maxlength="100"
show-word-limit
placeholder="请输入合同名称"
v-model.trim="formData.contractName"
/>
</el-form-item>
<!-- 合同编号 -->
<el-form-item label="合同编号" prop="contractCode">
<el-input
clearable
maxlength="50"
show-word-limit
placeholder="请输入合同编号"
v-model.trim="formData.contractCode"
/>
</el-form-item>
<!-- 合同状态 -->
<el-form-item label="合同状态" prop="contractStatus">
<el-select
clearable
style="width: 100%"
placeholder="请选择合同状态"
v-model="formData.contractStatus"
>
<el-option
v-for="item in dictOptions"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</el-select>
</el-form-item>
<!-- 备注 -->
<el-form-item label="备注" prop="remark">
<el-input
type="textarea"
maxlength="200"
show-word-limit
placeholder="请输入备注"
v-model="formData.remark"
:autosize="{ minRows: 4, maxRows: 8 }"
/>
</el-form-item>
</el-form>
<el-row class="common-btn-row">
<ComButton plain type="info" @click="handleCancel">取消</ComButton>
<ComButton @click="handleSave">保存</ComButton>
</el-row>
</template>
<script setup name="AddAndEditForm">
import { ref } from 'vue'
import ComButton from '@/components/ComButton/index.vue'
const props = defineProps({
formData: {
type: Object,
default: () => ({}),
},
dictOptions: {
type: Array,
default: () => [],
},
inspectionStationOptions: {
type: Array,
default: () => [],
},
})
const emit = defineEmits(['save', 'cancel'])
const formRef = ref(null)
const rules = {
inspectionStationId: [{ required: true, message: '请选择运检站', trigger: 'change' }],
contractPeriod: [{ required: true, message: '请选择合同承揽时间', trigger: 'change' }],
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
contractCode: [{ required: true, message: '请输入合同编号', trigger: 'blur' }],
contractStatus: [{ required: true, message: '请选择合同状态', trigger: 'change' }],
}
//
const handleInspectionStationChange = (value) => {
if (value) {
// id
const selectedStation = props.inspectionStationOptions.find((item) => item.id === value)
if (selectedStation) {
props.formData.inspectionStationName = selectedStation.value
}
} else {
//
props.formData.inspectionStationName = ''
}
}
//
const handleSave = async () => {
try {
await formRef.value.validate()
emit('save')
} catch (error) {
console.error('表单校验失败:', error)
return Promise.reject(error)
}
}
//
const handleCancel = () => {
emit('cancel')
}
//
defineExpose({
validate: () => formRef.value.validate(),
resetFields: () => formRef.value.resetFields(),
clearValidate: () => formRef.value.clearValidate(),
})
</script>
<style lang="scss" scoped>
.add-and-edit-form {
padding: 10px 20px;
}
.common-btn-row {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 20px;
}
</style>

View File

@ -0,0 +1,54 @@
import { reactive } from 'vue'
export default {
formColumns: [
{
type: 'input',
prop: 'inspectionStationName',
placeholder: '请输入运检站名称',
},
{
type: 'input',
prop: 'contractName',
placeholder: '请输入合同名称',
},
],
tableColumns: [
{
prop: 'inspectionStationName',
label: '运检站',
},
{
prop: 'contractPeriod',
label: '合同承揽时间',
},
{
prop: 'contractName',
label: '合同名称',
},
{
prop: 'contractCode',
label: '合同编号',
},
{
prop: 'contractStatus',
label: '合同状态',
slot: 'contractStatus', // 使用插槽显示字典值
},
{
prop: 'remark',
label: '备注',
width: '200',
showOverflowTooltip: true,
},
],
dialogConfig: reactive({
outerVisible: false,
outerTitle: '新建合同',
outerWidth: '720px',
minHeight: '400px',
maxHeight: '80vh',
}),
}

View File

@ -0,0 +1,191 @@
<template>
<div class="app-container">
<!-- 合同管理 -->
<ComTable
ref="comTableRef"
:form-columns="formColumns"
:table-columns="tableColumns"
:load-data="listContractAPI"
:show-toolbar="true"
:show-action="true"
:action-columns="actionColumns"
>
<!-- 工具栏插槽 -->
<template #toolbar>
<ComButton
type="primary"
v-hasPermi="['contract:add']"
icon="Plus"
@click="onHandleAdd"
>新建</ComButton
>
</template>
<!-- 合同状态列插槽 -->
<template #contractStatus="{ row }">
<dict-tag :options="contract_satus" :value="row.contractStatus" />
</template>
</ComTable>
<ComDialog :dialog-config="dialogConfig" @closeDialogOuter="onCloseDialogOuter">
<template #outerContent>
<AddAndEditForm
ref="addAndEditFormRef"
:form-data="addAndEditForm"
:dict-options="contract_satus"
:inspection-station-options="inspectionStationOptions"
@save="onHandleSave"
@cancel="onHandleCancel"
/>
</template>
</ComDialog>
</div>
</template>
<script setup name="contractManage">
import { ref, nextTick } from 'vue'
import {
listContractAPI,
addContractAPI,
delContractAPI,
updateContractAPI,
} from '@/api/contractManage/contract'
import { getCurrentInstance } from 'vue'
import { useOptions } from '@/hooks/useOptions'
import { getInspectionStationSelectAPI } from '@/api/common'
import config from './config'
import ComTable from '@/components/ComTable/index.vue'
import ComButton from '@/components/ComButton/index.vue'
import ComDialog from '@/components/ComDialog/index.vue'
import AddAndEditForm from './components/addAndEditForm.vue'
const { formColumns, tableColumns, dialogConfig } = config
const { proxy } = getCurrentInstance()
const addAndEditFormRef = ref(null)
const comTableRef = ref(null)
const editId = ref(null)
//
const { contract_satus } = proxy.useDict('contract_satus')
//
const { options: inspectionStationOptions } = useOptions(
'inspectionStationOptions',
getInspectionStationSelectAPI,
{},
)
//
const getInitFormData = () => ({
inspectionStationId: '', // id
inspectionStationName: '', //
contractPeriod: '', //
contractName: '', //
contractCode: '', //
contractStatus: '', //
remark: '', //
})
const addAndEditForm = ref(getInitFormData())
const actionColumns = [
{
label: '编辑',
type: 'primary',
link: true,
permission: ['contract:edit'],
handler: (row) => {
const {
contractId,
inspectionStationId,
inspectionStationName,
contractPeriod,
contractName,
contractCode,
contractStatus,
remark,
} = row
editId.value = contractId
dialogConfig.outerTitle = '编辑合同'
dialogConfig.outerVisible = true
// 使 nextTick
nextTick(() => {
addAndEditForm.value.inspectionStationId = inspectionStationId + '' || ''
addAndEditForm.value.inspectionStationName = inspectionStationName || ''
addAndEditForm.value.contractPeriod = contractPeriod || ''
addAndEditForm.value.contractName = contractName || ''
addAndEditForm.value.contractCode = contractCode || ''
addAndEditForm.value.contractStatus = contractStatus || ''
addAndEditForm.value.remark = remark || ''
})
},
},
{
label: '删除',
type: 'danger',
link: true,
permission: ['contract:remove'],
handler: (row) => {
proxy.$modal.confirm('是否确认删除该合同?').then(async () => {
const result = await delContractAPI({
contractId: row.contractId,
})
if (result.code === 200) {
proxy.$modal.msgSuccess('删除成功')
comTableRef.value?.refresh() //
}
})
},
},
]
//
const onHandleAdd = () => {
editId.value = null
dialogConfig.outerTitle = '新建合同'
dialogConfig.outerVisible = true
//
addAndEditForm.value = getInitFormData()
//
nextTick(() => {
addAndEditFormRef.value?.clearValidate()
})
}
//
const onHandleCancel = () => {
addAndEditFormRef.value?.resetFields() //
dialogConfig.outerVisible = false
}
//
const onHandleSave = async () => {
try {
await addAndEditFormRef.value.validate()
const API = editId.value ? updateContractAPI : addContractAPI
const params = JSON.parse(JSON.stringify(addAndEditForm.value))
editId.value ? (params.contractId = editId.value) : null
const result = await API(params)
if (result.code === 200) {
proxy.$modal.msgSuccess(editId.value ? '编辑成功' : '新增成功')
addAndEditFormRef.value.resetFields() //
dialogConfig.outerVisible = false
comTableRef.value?.refresh() //
}
} catch (error) {
console.error('表单校验失败或请求错误:', error)
// Promise.reject()ComButton loading
return Promise.reject(error)
}
}
const onCloseDialogOuter = (visible) => {
dialogConfig.outerVisible = visible
if (!visible) {
addAndEditFormRef.value?.resetFields()
}
}
</script>

View File

@ -0,0 +1,199 @@
<template>
<ComDialog :dialog-config="dialogConfig" @closeDialogOuter="onCloseDialogOuter">
<template #outerContent>
<div class="plan-select-dialog">
<!-- 搜索栏 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" class="search-form">
<el-form-item label="综合查询:">
<el-input
v-model="queryParams.keyWord"
placeholder="输入内容"
clearable
style="width: 300px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery"
>查询</el-button
>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 表格 -->
<el-table
ref="tableRef"
v-loading="loading"
:data="planList"
@selection-change="handleSelectionChange"
border
height="400px"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column prop="inspectionStationName" label="运检站" align="center" />
<el-table-column
prop="projectName"
label="项目名称"
align="center"
:show-overflow-tooltip="true"
/>
<el-table-column
prop="workContent"
label="工作任务"
align="center"
:show-overflow-tooltip="true"
/>
</el-table>
</div>
<!-- 操作按钮 -->
<el-row class="common-btn-row">
<ComButton plain type="info" @click="handleCancel">取消</ComButton>
<ComButton type="primary" @click="handleConfirm">确定</ComButton>
</el-row>
</template>
</ComDialog>
</template>
<script setup name="PlanSelectDialog">
import { ref, reactive, watch, nextTick } from 'vue'
import { getPlanListForSelectAPI } from '@/api/settlementManage/settlement'
import ComDialog from '@/components/ComDialog/index.vue'
import ComButton from '@/components/ComButton/index.vue'
const props = defineProps({
dialogConfig: {
type: Object,
required: true,
},
selectedPlanIds: {
type: Array,
default: () => [],
},
})
const emit = defineEmits(['confirm', 'close'])
const queryRef = ref(null)
const tableRef = ref(null)
const loading = ref(false)
const planList = ref([])
const selectedPlans = ref([])
const queryParams = reactive({
keyWord: '',
})
//
watch(
() => props.dialogConfig.outerVisible,
(visible) => {
if (visible) {
queryParams.keyWord = ''
selectedPlans.value = []
//
if (tableRef.value) {
tableRef.value.clearSelection()
}
getList()
}
},
)
//
const getList = async () => {
loading.value = true
try {
const response = await getPlanListForSelectAPI(queryParams)
if (response.code === 200) {
planList.value = response?.data || []
//
nextTick(() => {
setSelectedRows()
})
}
} catch (error) {
console.error('获取计划列表失败:', error)
} finally {
loading.value = false
}
}
//
const setSelectedRows = () => {
if (!tableRef.value || !props.selectedPlanIds || props.selectedPlanIds.length === 0) {
return
}
planList.value.forEach((row) => {
// monthPlanIdplanId
const isSelected = props.selectedPlanIds.some((id) => id === row.monthlyPlanId)
if (isSelected) {
tableRef.value.toggleRowSelection(row, true)
}
})
}
//
const handleQuery = () => {
getList()
}
//
const resetQuery = () => {
queryParams.keyWord = ''
selectedPlans.value = []
//
if (tableRef.value) {
tableRef.value.clearSelection()
}
getList()
}
//
const handleSelectionChange = (selection) => {
selectedPlans.value = selection
}
//
const handleConfirm = () => {
emit('confirm', selectedPlans.value)
props.dialogConfig.outerVisible = false
}
//
const handleCancel = () => {
emit('close')
props.dialogConfig.outerVisible = false
}
//
const onCloseDialogOuter = (visible) => {
props.dialogConfig.outerVisible = visible
if (!visible) {
resetQuery()
}
}
//
defineExpose({
getList,
})
</script>
<style lang="scss" scoped>
.plan-select-dialog {
.search-form {
margin-bottom: 20px;
}
.common-btn-row {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
}
</style>

View File

@ -0,0 +1,124 @@
import { reactive } from 'vue'
export default {
formColumns: [
{
type: 'select',
prop: 'contractId',
placeholder: '请输入所属合同',
options: [], // 动态从字典获取
},
{
type: 'input',
prop: 'contractCode',
placeholder: '请输入合同编号',
},
{
type: 'input',
prop: 'projectSettlementName',
placeholder: '请输入项目结算名称',
},
{
type: 'select',
prop: 'settlementStatus',
placeholder: '请选择合同或项目状态',
options: [], // 动态从字典获取
},
],
tableColumns: [
{
prop: 'inspectionStationName',
label: '运检站',
width: '140',
},
{
prop: 'contractCode',
label: '合同编号',
width: '140',
},
{
prop: 'contractName',
label: '所属合同',
width: '140',
},
{
prop: 'projectSettlementName',
label: '项目结算名称',
width: '140',
},
{
prop: 'planCount',
label: '执行计划数量',
formatter: (row) => {
return row.executionPlanList.length || ''
},
},
{
prop: 'estimatedAmount',
label: '合同预估金额(万元)',
width: '140',
formatter: (row) => {
return row.estimatedAmount ? (row.estimatedAmount / 10000 / 100).toFixed(2) : '0.00'
},
width: '140',
},
{
prop: 'contractStatus',
label: '合同或项目状态',
slot: 'contractStatus', // 使用插槽显示字典值
width: '140',
},
{
prop: 'settlementBatch',
label: '结算批次',
width: '140',
},
{
prop: 'settlementPayment',
label: '结算款-价',
formatter: (row) => {
// 金额从分转换为元保留2位小数
return row.settlementPayment ? (row.settlementPayment / 100).toFixed(2) : '0.00'
},
width: '140',
},
{
prop: 'settlementTax',
label: '结算款-税',
formatter: (row) => {
return row.settlementTax ? (row.settlementTax / 100).toFixed(2) : '0.00'
},
width: '140',
},
{
prop: 'approvedSettlement',
label: '审定结算-价',
formatter: (row) => {
return row.approvedSettlement ? (row.approvedSettlement / 100).toFixed(2) : '0.00'
},
width: '140',
},
{
prop: 'approvedSettlementTax',
label: '审定结算-税',
formatter: (row) => {
return row.approvedSettlementTax ? (row.approvedSettlementTax / 100).toFixed(2) : '0.00'
},
width: '140',
},
{
prop: 'settlementDataNumber',
label: '结算资料编号',
width: '140',
},
],
dialogConfig: reactive({
outerVisible: false,
outerTitle: '新建结算',
outerWidth: '720px',
minHeight: '400px',
maxHeight: '80vh',
}),
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,185 @@
<template>
<div class="app-container">
<!-- 结算管理 -->
<ComTable
ref="comTableRef"
:form-columns="formColumns"
:table-columns="tableColumns"
:load-data="listSettlementAPI"
:show-toolbar="true"
:show-action="true"
:action-columns="actionColumns"
>
<!-- 工具栏插槽 -->
<template #toolbar>
<ComButton
type="primary"
v-hasPermi="['settlement:add']"
icon="Plus"
@click="onHandleAdd"
>新建</ComButton
>
<ComButton
plain
type="info"
icon="Download"
@click="onDownloadStatistic"
v-hasPermi="['settlement:download']"
>下载统计表</ComButton
>
</template>
<!-- 合同状态列插槽 -->
<template #contractStatus="{ row }">
<dict-tag :options="contract_satus" :value="row.settlementStatus" />
</template>
</ComTable>
</div>
</template>
<script setup name="settlementManage">
import { ref, computed, getCurrentInstance, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import {
listSettlementAPI,
delSettlementAPI,
downloadSettlementStatisticAPI,
getContractSelectAPI,
} from '@/api/settlementManage/settlement'
import { download } from '@/utils/request'
import config from './config'
import ComTable from '@/components/ComTable/index.vue'
import ComButton from '@/components/ComButton/index.vue'
const router = useRouter()
const { proxy } = getCurrentInstance()
const comTableRef = ref(null)
const contractList = ref([])
//
const { contract_satus } = proxy.useDict('contract_satus')
//
const formColumns = computed(() => {
return config.formColumns.map((column) => {
if (column.prop === 'settlementStatus') {
return {
...column,
options: contract_satus.value || [],
}
}
if (column.prop === 'contractId') {
return {
...column,
options: contractList.value || [],
}
}
return column
})
})
const { tableColumns } = config
const actionColumns = [
{
label: '详情',
type: 'primary',
link: true,
permission: ['settlement:detail'],
handler: (row) => {
router.push({
path: '/settlement/settlementEdit/index',
query: {
id: row.settlementId,
mode: 'detail',
},
})
},
},
{
label: '编辑',
type: 'primary',
link: true,
permission: ['settlement:edit'],
handler: (row) => {
router.push({
path: '/settlement/settlementEdit/index',
query: {
id: row.settlementId,
mode: 'edit',
},
})
},
},
{
label: '删除',
type: 'danger',
link: true,
permission: ['settlement:remove'],
handler: (row) => {
proxy.$modal.confirm('是否确认删除该结算?').then(async () => {
const result = await delSettlementAPI({
settlementId: row.settlementId,
})
if (result.code === 200) {
proxy.$modal.msgSuccess('删除成功')
comTableRef.value?.refresh() //
}
})
},
},
]
//
const onHandleAdd = () => {
router.push({
path: '/settlement/settlementEdit/index',
query: {
mode: 'add',
contractId: null,
},
})
}
//
const onDownloadStatistic = async () => {
try {
const formData = comTableRef.value?.getFormData() || {}
const response = await downloadSettlementStatisticAPI(formData)
//
const blob = new Blob([response], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `结算统计表_${new Date().getTime()}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
proxy.$modal.msgSuccess('下载成功')
} catch (error) {
console.error('下载失败:', error)
proxy.$modal.msgError('下载失败')
}
}
//
const getContractList = async () => {
const response = await getContractSelectAPI({})
if (response.code === 200) {
contractList.value = response.rows.map((item) => ({
label: item.contractName,
value: item.contractId,
}))
}
}
onMounted(() => {
getContractList()
})
</script>