feat(erp): 初始化新增项目变更编辑页面

- 项目计划变更(erp_project_change、erp_project_change_budget、erp_project_change_progress)
- 实现项目变更表单的与初始化、数据加载保存功能
- 添加预算行的增删改操作及表格展示
- 集成项目里程碑变更的时间调整与完成度设置
- 支持暂存与提交两种操作模式,并校验用户权限
- 实现根据项目ID自动加载项目计划信息功能
- 添加返回按钮与表单重置逻辑,优化用户体验
dev
zangch@mesnac.com 3 months ago
parent 2f37ecdf29
commit fa01567268

@ -0,0 +1,99 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { ErpProjectChangeVO, ErpProjectChangeForm, ErpProjectChangeQuery } from '@/api/oa/erp/erpProjectChange/types';
/**
*
* @param query
* @returns {*}
*/
export const listErpProjectChange = (query?: ErpProjectChangeQuery): AxiosPromise<ErpProjectChangeVO[]> => {
return request({
url: '/oa/erp/erpProjectChange/list',
method: 'get',
params: query
});
};
/**
*
* @param projectChangeId
*/
export const getErpProjectChange = (projectChangeId: string | number): AxiosPromise<ErpProjectChangeVO> => {
return request({
url: '/oa/erp/erpProjectChange/' + projectChangeId,
method: 'get'
});
};
/**
*
* @param data
*/
export const addErpProjectChange = (data: ErpProjectChangeForm) => {
return request({
url: '/oa/erp/erpProjectChange',
method: 'post',
data: data
});
};
/**
*
* @param data
*/
export const updateErpProjectChange = (data: ErpProjectChangeForm) => {
return request({
url: '/oa/erp/erpProjectChange',
method: 'put',
data: data
});
};
/**
*
* @param projectChangeId
*/
export const delErpProjectChange = (projectChangeId: string | number | Array<string | number>) => {
return request({
url: '/oa/erp/erpProjectChange/' + projectChangeId,
method: 'delete'
});
};
/**
*
* @param query
* @returns {*}
*/
export function getErpProjectChangeList (query) {
return request({
url: '/oa/erp/erpProjectChange/getErpProjectChangeList',
method: 'get',
params: query
});
};
/**
* ID
* @param projectId
*/
export const prepareProjectChangeWithInfo = (projectId: string | number): AxiosPromise<ErpProjectChangeVO> => {
return request({
url: '/oa/erp/erpProjectChange/prepareWithInfo/' + projectId,
method: 'get'
});
};
/**
*
* @param data
*/
export const submitProjectChangeAndFlowStart = (data: ErpProjectChangeForm) => {
return request({
url: '/oa/erp/erpProjectChange/submitAndFlowStart',
method: 'post',
data: data
});
};

@ -0,0 +1,521 @@
/**
*
*/
export interface ErpProjectChangeBudget {
/**
* ID
*/
changeBudgetId?: string | number;
/**
* ID
*/
projectChangeId?: string | number;
/**
* ID
*/
budgetDetailId?: string | number;
/**
*
*/
subjectName?: string;
/**
*
*/
budgetBefore?: number;
/**
*
*/
budgetAfter?: number;
/**
* 使
*/
amountUsed?: number;
/**
*
*/
adjustmentReason?: string;
/**
*
*/
sortOrder?: number;
/**
*
*/
remark?: string;
}
/**
*
*/
export interface ErpProjectChangeProgress {
/**
* ID
*/
changeProgressId?: string | number;
/**
* ID
*/
projectChangeId?: string | number;
/**
* ID
*/
planStageId?: string | number;
/**
* project_phases
*/
projectPhases?: string;
/**
*
*/
milestoneName?: string;
/**
*
*/
originalStart?: string;
/**
*
*/
originalEnd?: string;
/**
*
*/
changedStart?: string;
/**
*
*/
changedEnd?: string;
/**
*
*/
completionDegree?: number;
/**
*
*/
sortOrder?: number;
/**
*
*/
remark?: string;
}
export interface ErpProjectChangeVO {
/**
* ID
*/
projectChangeId: string | number;
/**
* ID
*/
projectId: string | number;
/**
*
*/
projectCode: string;
/**
*
*/
projectName: string;
/**
* 1 2 3 4
*/
projectCategory: string;
/**
* 1 2 3 4 5
*/
changeType: string;
/**
*
*/
changeNumber: number;
/**
* ID
*/
projectManagerId: string | number;
/**
*
*/
projectManagerName: string;
/**
* ID
*/
deptHeadId: string | number;
/**
*
*/
deptHeadName: string;
/**
* ID
*/
responsibleVpId: string | number;
/**
*
*/
responsibleVpName: string;
/**
*
*/
applyChangeDate: string;
/**
*
*/
contractAmount: number;
/**
*
*/
contractNetAmount: number;
/**
*
*/
currentStatus: string;
/**
*
*/
changeReason: string;
/**
*
*/
followUpWork: string;
/**
* (1 2 3)
*/
projectChangeStatus: string;
/**
*
*/
flowStatus: string;
/**
*
*/
remark: string;
/**
* 1 0
*/
activeFlag: string;
/**
*
*/
createBy: number;
/**
*
*/
createTime: string;
/**
*
*/
budgetList?: ErpProjectChangeBudget[];
/**
*
*/
progressList?: ErpProjectChangeProgress[];
}
export interface ErpProjectChangeForm extends BaseEntity {
/**
* ID
*/
projectChangeId?: string | number;
/**
* ID
*/
projectId?: string | number;
/**
*
*/
projectCode?: string;
/**
*
*/
projectName?: string;
/**
* 1 2 3 4
*/
projectCategory?: string;
/**
* 1 2 3 4 5
*/
changeType?: string;
/**
*
*/
changeNumber?: number;
/**
* ID
*/
projectManagerId?: string | number;
/**
*
*/
projectManagerName?: string;
/**
* ID
*/
deptHeadId?: string | number;
/**
*
*/
deptHeadName?: string;
/**
* ID
*/
responsibleVpId?: string | number;
/**
*
*/
responsibleVpName?: string;
/**
*
*/
applyChangeDate?: string;
/**
*
*/
contractAmount?: number;
/**
*
*/
contractNetAmount?: number;
/**
*
*/
currentStatus?: string;
/**
*
*/
changeReason?: string;
/**
*
*/
followUpWork?: string;
/**
* (1 2 3)
*/
projectChangeStatus?: string;
/**
*
*/
flowStatus?: string;
/**
*
*/
remark?: string;
/**
* 1 0
*/
activeFlag?: string;
/**
*
*/
budgetList?: ErpProjectChangeBudget[];
/**
*
*/
progressList?: ErpProjectChangeProgress[];
/**
*
*/
flowCode?: string;
/**
*
*/
handler?: string;
/**
*
*/
variables?: any;
/**
*
*/
bizExt?: any;
}
export interface ErpProjectChangeQuery extends PageQuery {
/**
* ID
*/
projectId?: string | number;
/**
*
*/
projectCode?: string;
/**
*
*/
projectName?: string;
/**
* 1 2 3 4
*/
projectCategory?: string;
/**
* 1 2 3 4 5
*/
changeType?: string;
/**
*
*/
changeNumber?: number;
/**
* ID
*/
projectManagerId?: string | number;
/**
*
*/
projectManagerName?: string;
/**
* ID
*/
deptHeadId?: string | number;
/**
*
*/
deptHeadName?: string;
/**
* ID
*/
responsibleVpId?: string | number;
/**
*
*/
responsibleVpName?: string;
/**
*
*/
applyChangeDate?: string;
/**
*
*/
contractAmount?: number;
/**
*
*/
contractNetAmount?: number;
/**
*
*/
currentStatus?: string;
/**
*
*/
changeReason?: string;
/**
*
*/
followUpWork?: string;
/**
* (1 2 3)
*/
projectChangeStatus?: string;
/**
*
*/
flowStatus?: string;
/**
* 1 0
*/
activeFlag?: string;
/**
*
*/
params?: any;
}

@ -0,0 +1,76 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { ErpProjectChangeBudgetVO, ErpProjectChangeBudgetForm, ErpProjectChangeBudgetQuery } from '@/api/oa/erp/erpProjectChangeBudget/types';
/**
*
* @param query
* @returns {*}
*/
export const listErpProjectChangeBudget = (query?: ErpProjectChangeBudgetQuery): AxiosPromise<ErpProjectChangeBudgetVO[]> => {
return request({
url: '/oa/erp/erpProjectChangeBudget/list',
method: 'get',
params: query
});
};
/**
*
* @param changeBudgetId
*/
export const getErpProjectChangeBudget = (changeBudgetId: string | number): AxiosPromise<ErpProjectChangeBudgetVO> => {
return request({
url: '/oa/erp/erpProjectChangeBudget/' + changeBudgetId,
method: 'get'
});
};
/**
*
* @param data
*/
export const addErpProjectChangeBudget = (data: ErpProjectChangeBudgetForm) => {
return request({
url: '/oa/erp/erpProjectChangeBudget',
method: 'post',
data: data
});
};
/**
*
* @param data
*/
export const updateErpProjectChangeBudget = (data: ErpProjectChangeBudgetForm) => {
return request({
url: '/oa/erp/erpProjectChangeBudget',
method: 'put',
data: data
});
};
/**
*
* @param changeBudgetId
*/
export const delErpProjectChangeBudget = (changeBudgetId: string | number | Array<string | number>) => {
return request({
url: '/oa/erp/erpProjectChangeBudget/' + changeBudgetId,
method: 'delete'
});
};
/**
*
* @param query
* @returns {*}
*/
export function getErpProjectChangeBudgetList (query) {
return request({
url: '/oa/erp/erpProjectChangeBudget/getErpProjectChangeBudgetList',
method: 'get',
params: query
});
};

@ -0,0 +1,156 @@
export interface ErpProjectChangeBudgetVO {
/**
* ID
*/
changeBudgetId: string | number;
/**
* ID
*/
projectChangeId: string | number;
/**
* ID
*/
budgetDetailId: string | number;
/**
*
*/
subjectName: string;
/**
*
*/
budgetBefore: number;
/**
*
*/
budgetAfter: number;
/**
* 使
*/
amountUsed: number;
/**
*
*/
adjustmentReason: string;
/**
*
*/
sortOrder: number;
/**
*
*/
remark: string;
}
export interface ErpProjectChangeBudgetForm extends BaseEntity {
/**
* ID
*/
changeBudgetId?: string | number;
/**
* ID
*/
projectChangeId?: string | number;
/**
* ID
*/
budgetDetailId?: string | number;
/**
*
*/
subjectName?: string;
/**
*
*/
budgetBefore?: number;
/**
*
*/
budgetAfter?: number;
/**
* 使
*/
amountUsed?: number;
/**
*
*/
adjustmentReason?: string;
/**
*
*/
sortOrder?: number;
/**
*
*/
remark?: string;
}
export interface ErpProjectChangeBudgetQuery extends PageQuery {
/**
* ID
*/
projectChangeId?: string | number;
/**
* ID
*/
budgetDetailId?: string | number;
/**
*
*/
subjectName?: string;
/**
*
*/
budgetBefore?: number;
/**
*
*/
budgetAfter?: number;
/**
* 使
*/
amountUsed?: number;
/**
*
*/
adjustmentReason?: string;
/**
*
*/
sortOrder?: number;
/**
*
*/
params?: any;
}

@ -0,0 +1,76 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { ErpProjectChangeProgressVO, ErpProjectChangeProgressForm, ErpProjectChangeProgressQuery } from '@/api/oa/erp/erpProjectChangeProgress/types';
/**
*
* @param query
* @returns {*}
*/
export const listErpProjectChangeProgress = (query?: ErpProjectChangeProgressQuery): AxiosPromise<ErpProjectChangeProgressVO[]> => {
return request({
url: '/oa/erp/erpProjectChangeProgress/list',
method: 'get',
params: query
});
};
/**
*
* @param changeProgressId
*/
export const getErpProjectChangeProgress = (changeProgressId: string | number): AxiosPromise<ErpProjectChangeProgressVO> => {
return request({
url: '/oa/erp/erpProjectChangeProgress/' + changeProgressId,
method: 'get'
});
};
/**
*
* @param data
*/
export const addErpProjectChangeProgress = (data: ErpProjectChangeProgressForm) => {
return request({
url: '/oa/erp/erpProjectChangeProgress',
method: 'post',
data: data
});
};
/**
*
* @param data
*/
export const updateErpProjectChangeProgress = (data: ErpProjectChangeProgressForm) => {
return request({
url: '/oa/erp/erpProjectChangeProgress',
method: 'put',
data: data
});
};
/**
*
* @param changeProgressId
*/
export const delErpProjectChangeProgress = (changeProgressId: string | number | Array<string | number>) => {
return request({
url: '/oa/erp/erpProjectChangeProgress/' + changeProgressId,
method: 'delete'
});
};
/**
*
* @param query
* @returns {*}
*/
export function getErpProjectChangeProgressList (query) {
return request({
url: '/oa/erp/erpProjectChangeProgress/getErpProjectChangeProgressList',
method: 'get',
params: query
});
};

@ -0,0 +1,171 @@
export interface ErpProjectChangeProgressVO {
/**
* ID
*/
changeProgressId: string | number;
/**
* ID
*/
projectChangeId: string | number;
/**
* ID
*/
planStageId: string | number;
/**
*
*/
milestoneName: string;
/**
*
*/
originalStart: string;
/**
*
*/
originalEnd: string;
/**
*
*/
changedStart: string;
/**
*
*/
changedEnd: string;
/**
*
*/
completionDegree: number;
/**
*
*/
sortOrder: number;
/**
*
*/
remark: string;
}
export interface ErpProjectChangeProgressForm extends BaseEntity {
/**
* ID
*/
changeProgressId?: string | number;
/**
* ID
*/
projectChangeId?: string | number;
/**
* ID
*/
planStageId?: string | number;
/**
*
*/
milestoneName?: string;
/**
*
*/
originalStart?: string;
/**
*
*/
originalEnd?: string;
/**
*
*/
changedStart?: string;
/**
*
*/
changedEnd?: string;
/**
*
*/
completionDegree?: number;
/**
*
*/
sortOrder?: number;
/**
*
*/
remark?: string;
}
export interface ErpProjectChangeProgressQuery extends PageQuery {
/**
* ID
*/
projectChangeId?: string | number;
/**
* ID
*/
planStageId?: string | number;
/**
*
*/
milestoneName?: string;
/**
*
*/
originalStart?: string;
/**
*
*/
originalEnd?: string;
/**
*
*/
changedStart?: string;
/**
*
*/
changedEnd?: string;
/**
*
*/
completionDegree?: number;
/**
*
*/
sortOrder?: number;
/**
*
*/
params?: any;
}

@ -0,0 +1,839 @@
<template>
<div class="p-2">
<!-- 统一表单包裹所有分区,便于整体验证 -->
<el-form ref="quoteFormRef" :model="form" :rules="rules" label-width="120px" :disabled="isView">
<el-card shadow="never">
<div style="margin-bottom: 12px; text-align: center; font-weight: bold; font-size: 18px">添加报价单基本信息</div>
<div class="basic-center">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="报价单编号" prop="quoteCode">
<el-input v-model="form.quoteCode" placeholder="自动生成">
<template #append>
<el-button type="primary" @click="generateQuoteCode" :disabled="isView || isCodeGenerated">生成报价单编号</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="报价单名称" prop="quoteName">
<el-input v-model="form.quoteName" placeholder="请输入报价单名称" />
</el-form-item>
</el-col>
<!-- 与截图不符的字段按要求暂时注释,保留原位置方便后续恢复 -->
<!--
<el-col :span="12">
<el-form-item label="报价大类" prop="quoteCategory">
<el-select v-model="form.quoteCategory" placeholder="请选择报价大类">
<el-option v-for="dict in quote_category" :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="quoteType">
<el-select v-model="form.quoteType" placeholder="请选择报价类型">
<el-option v-for="dict in contract_type" :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="businessDirection">
<el-select v-model="form.businessDirection" placeholder="请选择业务方向">
<el-option v-for="dict in business_direction" :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="quoteDate">
<el-date-picker clearable v-model="form.quoteDate" type="date" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择报价日期" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="有效期起" prop="validFrom">
<el-date-picker clearable v-model="form.validFrom" type="date" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择有效期起" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="有效期止" prop="validTo">
<el-date-picker clearable v-model="form.validTo" type="date" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择有效期止" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="交货期(天)" prop="deliveryPeriod">
<!-- 使用数字输入框,禁用随表单统一控制 -->
<el-input-number v-model="form.deliveryPeriod" :min="0" :precision="0" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="交货方式" prop="deliveryMethod">
<el-input v-model="form.deliveryMethod" placeholder="请输入交货方式" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="付款方式" prop="paymentMethod">
<el-input v-model="form.paymentMethod" placeholder="请输入付款方式" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="币种" prop="currencyType">
<el-select v-model="form.currencyType" placeholder="请选择币种">
<el-option v-for="dict in currency_type" :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="taxIncludedInfo">
<el-input v-model="form.taxIncludedInfo" placeholder="如含13%增值税" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="税率(%)" prop="taxRate">
<!-- 使用数字输入框两位小数步长0.01 -->
<el-input-number v-model="form.taxRate" :min="0" :precision="2" :step="0.01" style="width: 100%" />
</el-form-item>
</el-col>
<!-- 原客户方信息块已迁移到“客户方信息”分区,保留以便后续恢复 -->
<el-col :span="12" v-if="false">
<el-form-item label="客户联系人" prop="customerContactId">
<el-select v-model="form.customerContactId" filterable placeholder="请选择客户联系人" @change="onCustomerContactChanged">
<el-option v-for="c in customerContactList" :key="c.contactId" :label="c.contactName + ' - ' + (c.phoneNumber||'')" :value="c.contactId" />
</el-select>
</el-form-item>
<!-- 客户方联系电话与邮箱(随选择带出,可编辑) -->
<el-form-item label="客户联系电话" prop="customerContactPhone">
<el-input v-model="form.customerContactPhone" placeholder="客户联系电话" />
</el-form-item>
<el-form-item label="客户邮箱" prop="customerContactEmail">
<el-input v-model="form.customerContactEmail" placeholder="客户邮箱" />
</el-form-item>
</el-col>
<!-- 原供货方信息块已迁移到“供货方信息”分区,保留以便后续恢复 -->
<el-col :span="12" v-if="false">
<el-form-item label="供货方联系人" prop="supplierContactId">
<el-select v-model="form.supplierContactId" filterable placeholder="请选择供货方联系人" @change="onSupplierContactChanged">
<el-option v-for="c in supplierContactList" :key="c.contactId" :label="c.contactName + ' - ' + (c.phoneNumber||'')" :value="c.contactId" />
</el-select>
</el-form-item>
<!-- 供货方联系电话与邮箱(随选择带出,可编辑) -->
<el-form-item label="供货方联系电话" prop="supplierContactPhone">
<el-input v-model="form.supplierContactPhone" placeholder="供货方联系电话" />
</el-form-item>
<el-form-item label="供货方邮箱" prop="supplierContactEmail">
<el-input v-model="form.supplierContactEmail" placeholder="供货方邮箱" />
</el-form-item>
</el-col>
<!-- 原备注块已迁移到“备注”分区,保留以便后续恢复 -->
<el-col :span="12" v-if="false">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-col>
<!-- 原附件上传块已迁移到“附件”分区,保留以便后续恢复 -->
<el-col :span="12" v-if="false">
<el-form-item label="附件上传">
<el-button type="primary" plain icon="Upload" @click="handleFile">上传附件</el-button>
<div style="margin-top: 8px; color: #999">支持格式:.rar .zip .doc .docx .pdf单个文件不超过20MB</div>
</el-form-item>
</el-col>
</el-row>
</div>
</el-card>
<!-- 报价物料编辑对话框 -->
<el-dialog :title="materialDialog.title" v-model="materialDialog.visible" width="800px" append-to-body>
<el-form ref="materialFormRef" :model="materialForm" :rules="materialRules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="计划标识" prop="planFlag">
<el-radio-group v-model="materialForm.planFlag">
<el-radio v-for="dict in plan_flag" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="产品名称" prop="productName">
<el-input v-model="materialForm.productName" placeholder="请输入产品名称">
<template #suffix>
<el-icon style="cursor: pointer" v-if="materialForm.planFlag === '1'" @click="openSaleMaterialSelect">
<Search />
</el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="规格描述" prop="specificationDescription">
<el-input v-model="materialForm.specificationDescription" placeholder="请输入规格描述" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="数量" prop="amount">
<el-input-number v-model="materialForm.amount" placeholder="请输入数量" style="width: 100%" @change="calculateSubtotal" :precision="2" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="物料单位" prop="unitId">
<el-select v-model="materialForm.unitId" placeholder="请选择物料单位">
<el-option v-for="item in unitInfoList" :key="item.unitId" :label="item.unitName" :value="item.unitId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="税率(%)" prop="taxRate">
<el-input-number v-model="materialForm.taxRate" placeholder="请输入税率" style="width: 100%" @change="calculateBeforePrice" :precision="2" :min="0" :max="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="未税单价" prop="beforePrice">
<el-input-number v-model="materialForm.beforePrice" placeholder="自动计算" style="width: 100%" :precision="2" :controls="false" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="含税单价" prop="includingPrice">
<el-input-number v-model="materialForm.includingPrice" placeholder="请输入含税单价" style="width: 100%" :precision="2" @change="calculateBeforePrice" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="小计" prop="subtotal">
<el-input-number v-model="materialForm.subtotal" placeholder="自动计算" style="width: 100%" readonly :precision="2" :controls="false" disabled />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="materialForm.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitMaterialForm">确 定</el-button>
<el-button @click="cancelMaterial">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 销售物料选择 -->
<SaleMaterialSelect ref="saleMaterialSelectRef" :multiple="false" @confirm-call-back="saleMaterialSelectCallBack" />
<!-- 客户方/供货方信息(左右并列对称布局) -->
<el-row :gutter="20" style="margin-top: 20px">
<el-col :span="12">
<el-card shadow="never">
<template #header>
<div style="text-align: left; font-weight: bold; font-size: 18px">客户方信息</div>
</template>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="客户联系人" prop="customerContactId">
<el-select v-model="form.customerContactId" filterable placeholder="请选择客户联系人" @change="onCustomerContactChanged">
<el-option v-for="c in customerContactList" :key="c.contactId" :label="c.contactName + ' - ' + (c.phoneNumber||'')" :value="c.contactId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="客户联系电话" prop="customerContactPhone">
<el-input v-model="form.customerContactPhone" placeholder="客户联系电话" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="客户邮箱" prop="customerContactEmail">
<el-input v-model="form.customerContactEmail" placeholder="客户邮箱" />
</el-form-item>
</el-col>
</el-row>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header>
<div style="text-align: left; font-weight: bold; font-size: 18px">供货方信息</div>
</template>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="计划标识" prop="supplierPlanFlag">
<el-radio-group v-model="supplierPlanFlag" @change="onSupplierPlanFlagChanged">
<el-radio value="1">计划内</el-radio>
<el-radio value="2">计划外</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24" v-if="supplierPlanFlag === '1'">
<el-form-item label="供应商" prop="supplierContactId">
<el-select v-model="form.supplierContactId" filterable placeholder="请选择供应商" @change="onSupplierChanged">
<el-option v-for="s in supplierList" :key="s.supplierId" :label="s.supplierName" :value="s.supplierId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="供货方联系人" prop="supplierContactName">
<el-input v-model="form.supplierContactName" placeholder="供货方联系人" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="供货方联系电话" prop="supplierContactPhone">
<el-input v-model="form.supplierContactPhone" placeholder="供货方联系电话" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="供货方邮箱" prop="supplierContactEmail">
<el-input v-model="form.supplierContactEmail" placeholder="供货方邮箱" />
</el-form-item>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
<!-- 附件(独立分区,样式参考合同编辑页) -->
<el-card shadow="never" style="margin-top: 20px">
<template #header>
<div style="text-align: left; font-weight: bold; font-size: 18px">附件</div>
</template>
<el-row :gutter="20">
<el-col :span="24">
<!-- 与合同页对齐,采用“附件”表单项与上传按钮 -->
<el-form-item label="附件">
<el-button type="primary" plain icon="Upload" @click="handleFile" :disabled="isView">上传附件</el-button>
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 备注(独立分区) -->
<el-card shadow="never" style="margin-top: 20px">
<template #header>
<div style="text-align: left; font-weight: bold; font-size: 18px">备注</div>
</template>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 统一表单结束 -->
</el-form>
<!-- 报价物料管理 -->
<el-card shadow="never" style="margin-top: 20px">
<template #header>
<div style="text-align: left; font-weight: bold; font-size: 18px">报价明细表格</div>
</template>
<div style="margin-bottom: 12px">
<el-button type="primary" icon="Plus" @click="handleAddMaterial" :disabled="isView">新增物料</el-button>
</div>
<el-table :data="materialRows" border show-summary :summary-method="getSummary">
<el-table-column label="产品名称" align="center" prop="productName" min-width="160" />
<el-table-column label="规格描述" align="center" prop="specificationDescription" min-width="160" />
<el-table-column label="物料ID" align="center" prop="materialId" width="120" />
<el-table-column label="销售物料ID" align="center" prop="relationMaterialId" width="140" />
<el-table-column label="数量" align="center" prop="amount" width="120">
<template #default="scope">
{{ scope.row.amount ? Number(scope.row.amount).toFixed(2) : '0.00' }}
</template>
</el-table-column>
<el-table-column label="单位ID" align="center" prop="unitId" width="120" />
<el-table-column label="未税单价" align="center" prop="beforePrice" width="140">
<template #default="scope">
{{ scope.row.beforePrice ? Number(scope.row.beforePrice).toFixed(2) : '0.00' }}
</template>
</el-table-column>
<el-table-column label="税率(%)" align="center" prop="taxRate" width="120">
<template #default="scope">
{{ scope.row.taxRate ? Number(scope.row.taxRate).toFixed(2) : '0.00' }}
</template>
</el-table-column>
<el-table-column label="含税单价" align="center" prop="includingPrice" width="140">
<template #default="scope">
{{ scope.row.includingPrice ? Number(scope.row.includingPrice).toFixed(2) : '0.00' }}
</template>
</el-table-column>
<el-table-column label="小计" align="center" prop="subtotal" width="140">
<template #default="scope">
{{ scope.row.subtotal ? Number(scope.row.subtotal).toFixed(2) : '0.00' }}
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" min-width="140" />
<el-table-column label="操作" align="center" fixed="right" width="150" v-if="!isView">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleEditMaterial(scope.row)">编辑</el-button>
<el-button link type="danger" icon="Delete" @click="handleDeleteMaterial(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 12px; text-align: right">
<span style="margin-right: 16px">含税总价:{{ totalIncludingTax.toFixed(2) }}</span>
<el-button v-if="!isView" type="primary" :loading="buttonLoading" @click="submitForm">保 存</el-button>
<el-button @click="goBack">返 回</el-button>
</div>
</el-card>
<!-- 附件上传对话框 -->
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
<el-form label-width="80px">
<el-form-item label="文件名">
<fileUpload v-if="type === 0" v-model="ossFileModel" />
<imageUpload v-if="type === 1" v-model="ossFileModel" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitOss">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import { getCrmQuoteInfo, addCrmQuoteInfo, updateCrmQuoteInfo, recalcQuoteTotals } from '@/api/oa/crm/crmQuoteInfo';
import { CrmQuoteInfoForm } from '@/api/oa/crm/crmQuoteInfo/types';
import type { CrmQuoteMaterialForm } from '@/api/oa/crm/crmQuoteMaterial/types';
import { getBaseMaterialInfoList } from '@/api/oa/base/materialInfo';
import { getCrmCustomerContactList } from '@/api/oa/crm/customerContact';
import { getCrmSupplierInfoList } from '@/api/oa/crm/crmSupplierInfo';
import { getRuleGenerateCode } from '@/api/system/codeRule';
import { getBaseUnitInfoList } from '@/api/oa/base/unitInfo';
import SaleMaterialSelect from '@/components/SaleMaterialSelect/index.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { business_direction, currency_type, quote_category, contract_type, plan_flag } = toRefs<any>(proxy?.useDict('business_direction', 'currency_type', 'quote_category', 'contract_type', 'plan_flag'));
const router = useRouter();
const route = useRoute();
// 查看模式根据路由type=view禁用输入与保存
const isView = computed(() => route.query.type === 'view');
const buttonLoading = ref(false);
const quoteFormRef = ref<ElFormInstance>();
// 编号生成状态(生成后禁用按钮)
const isCodeGenerated = ref(false);
const form = reactive<CrmQuoteInfoForm>({
quoteId: undefined,
quoteCode: undefined,
quoteName: undefined,
quoteCategory: undefined,
quoteType: undefined,
businessDirection: undefined,
quoteDate: undefined,
validFrom: undefined,
validTo: undefined,
deliveryPeriod: undefined,
deliveryMethod: undefined,
paymentMethod: undefined,
currencyType: undefined,
taxIncludedInfo: undefined,
taxRate: undefined,
customerContactId: undefined,
customerContactName: undefined,
customerContactPhone: undefined,
customerContactEmail: undefined,
supplierContactId: undefined,
supplierContactName: undefined,
supplierContactPhone: undefined,
supplierContactEmail: undefined,
remark: undefined
});
const rules = {
quoteName: [{ required: true, message: '报价单名称不能为空', trigger: 'blur' }],
customerContactId: [{ required: true, message: '请选择客户联系人', trigger: 'change' }]
};
// 下拉数据
const materialList = ref<any[]>([]);
const customerContactList = ref<any[]>([]);
const supplierList = ref<any[]>([]);
// 供应商计划标识1计划内2计划外
const supplierPlanFlag = ref<string>('1');
// 单位下拉数据
const unitInfoList = ref<any[]>([]);
const getUnitInfoListSelect = async () => {
const res = await getBaseUnitInfoList(null);
unitInfoList.value = res.data || [];
};
// 物料编辑弹窗与表单
const materialDialog = reactive({ visible: false, title: '' });
const materialFormRef = ref<ElFormInstance>();
const initMaterialFormData: CrmQuoteMaterialForm & { planFlag?: string } = {
quoteMaterialId: undefined,
planFlag: '2', // 1计划内2计划外
quoteId: undefined,
productName: undefined,
specificationDescription: undefined,
materialId: undefined,
relationMaterialId: undefined,
amount: undefined,
unitId: undefined,
unitName: undefined,
beforePrice: undefined,
taxRate: undefined,
includingPrice: undefined,
subtotal: undefined,
remark: undefined,
activeFlag: '1'
};
const materialForm = ref<CrmQuoteMaterialForm & { planFlag?: string }>({ ...initMaterialFormData });
const materialRules = {
productName: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }],
amount: [{ required: true, message: '数量不能为空', trigger: 'blur' }],
taxRate: [{ required: true, message: '税率不能为空', trigger: 'blur' }]
};
const saleMaterialSelectRef = ref<InstanceType<typeof SaleMaterialSelect>>();
const openSaleMaterialSelect = () => saleMaterialSelectRef.value?.open();
const saleMaterialSelectCallBack = (data: any) => {
const list = data || [];
if (list.length) {
const m = list[0];
materialForm.value.materialId = m.materialId;
materialForm.value.relationMaterialId = m.relationMaterialId;
materialForm.value.productName = m.saleMaterialName || m.materialName;
if (m.unitName) materialForm.value.unitName = m.unitName;
}
};
const resetMaterialForm = () => {
materialForm.value = { ...initMaterialFormData };
materialFormRef.value?.resetFields();
};
const cancelMaterial = () => {
resetMaterialForm();
materialDialog.visible = false;
};
const calculateBeforePrice = () => {
const tax = Number(materialForm.value.taxRate);
const including = Number(materialForm.value.includingPrice);
if (!isNaN(tax) && !isNaN(including)) {
const divisor = 1 + tax / 100;
if (divisor > 0) {
materialForm.value.beforePrice = Number((including / divisor).toFixed(2));
}
}
calculateSubtotal();
};
const calculateSubtotal = () => {
const amount = Number(materialForm.value.amount);
const including = Number(materialForm.value.includingPrice);
if (!isNaN(amount) && !isNaN(including)) {
materialForm.value.subtotal = Number((amount * including).toFixed(2));
}
};
const handleAddMaterial = () => {
resetMaterialForm();
materialForm.value.quoteId = form.quoteId as any;
materialDialog.visible = true;
materialDialog.title = '新增报价物料';
};
const handleEditMaterial = (row: CrmQuoteMaterialForm) => {
resetMaterialForm();
materialForm.value = { ...(row as any), planFlag: (row as any).materialId ? '1' : '2' };
materialDialog.visible = true;
materialDialog.title = '编辑报价物料';
};
const handleDeleteMaterial = async (row: CrmQuoteMaterialForm) => {
await proxy?.$modal.confirm('是否确认删除该报价物料?');
const idx = materialRows.value.findIndex((x: any) => x.quoteMaterialId === (row as any).quoteMaterialId);
if (idx !== -1) materialRows.value.splice(idx, 1);
else {
const i2 = materialRows.value.indexOf(row);
if (i2 !== -1) materialRows.value.splice(i2, 1);
}
proxy?.$modal.msgSuccess('删除成功');
};
const submitMaterialForm = () => {
materialFormRef.value?.validate((valid: boolean) => {
if (!valid) return;
if ((materialForm.value as any).quoteMaterialId) {
const idx = materialRows.value.findIndex((x: any) => x.quoteMaterialId === (materialForm.value as any).quoteMaterialId);
if (idx !== -1) materialRows.value[idx] = { ...materialForm.value } as any;
} else {
const newItem = { ...materialForm.value, quoteMaterialId: Date.now() } as any;
materialRows.value.push(newItem);
}
proxy?.$modal.msgSuccess('操作成功');
materialDialog.visible = false;
});
};
// 物料行
const materialRows = ref<CrmQuoteMaterialForm[]>([]);
const totalIncludingTax = computed(() => {
return materialRows.value.reduce((sum, r) => sum + (Number(r.subtotal || 0)), 0);
});
// 表格合计行:仅统计小计列
const getSummary = (param: any) => {
const { columns, data } = param;
const sums: string[] = [];
columns.forEach((column: any, index: number) => {
if (index === 0) {
sums[index] = '合计';
return;
}
if (column.property === 'subtotal') {
const total = data.reduce((sum: number, row: any) => sum + Number(row.subtotal || 0), 0);
sums[index] = total.toFixed(2);
} else {
sums[index] = '';
}
});
return sums;
};
const addMaterialRow = () => {
// 默认支持非标:预留自填名称与规格;单位可手填
materialRows.value.push({
materialId: undefined,
unitId: undefined,
unitName: '',
productName: '',
specificationDescription: '',
amount: 0,
beforePrice: 0,
taxRate: Number(form.taxRate || 0),
includingPrice: 0,
subtotal: 0,
remark: ''
} as CrmQuoteMaterialForm);
};
const removeRow = (idx: number) => materialRows.value.splice(idx, 1);
// 工具:数值化与保留两位小数
const toNum = (v: any) => (v === '' || v === null || v === undefined) ? undefined : (Number.isFinite(Number(v)) ? Number(v) : undefined);
const round2 = (n: number) => Math.round(n * 100) / 100;
// 计算器
const calcIncluding = (before: number, rate: number) => round2(before * (1 + rate / 100));
const calcBefore = (including: number, rate: number) => round2(including / (1 + rate / 100));
const calcRate = (including: number, before: number) => round2(((including / before) - 1) * 100);
// 统一小计:始终以含税单价×数量;含税缺失则以未税+税率推导
const updateSubtotal = (row: any) => {
const amount = toNum(row.amount) ?? 0;
const before = toNum(row.beforePrice);
const including = toNum(row.includingPrice);
const rate = toNum(row.taxRate) ?? toNum(form.taxRate);
let inc = including;
if (inc === undefined && before !== undefined && rate !== undefined) {
inc = calcIncluding(before, rate);
row.includingPrice = inc; // 推导同时回填,保持一致
}
row.subtotal = (inc !== undefined) ? round2((inc as number) * amount) : 0;
};
// 基于“最近编辑字段”的确定性互算:任意两项给出,自动算第三项
const onAmountChange = (row: any, val: number) => {
row.amount = val;
updateSubtotal(row);
};
const onBeforePriceChange = (row: any, val: number) => {
row.beforePrice = val;
const rate = toNum(row.taxRate) ?? toNum(form.taxRate);
const including = toNum(row.includingPrice);
if (rate !== undefined) {
row.includingPrice = calcIncluding(val, rate as number);
} else if (including !== undefined) {
row.taxRate = calcRate(including as number, val);
}
updateSubtotal(row);
};
const onTaxRateChange = (row: any, val: number) => {
row.taxRate = val;
const before = toNum(row.beforePrice);
const including = toNum(row.includingPrice);
if (before !== undefined) {
row.includingPrice = calcIncluding(before as number, val);
} else if (including !== undefined) {
row.beforePrice = calcBefore(including as number, val);
}
updateSubtotal(row);
};
const onIncludingPriceChange = (row: any, val: number) => {
row.includingPrice = val;
const rate = toNum(row.taxRate) ?? toNum(form.taxRate);
const before = toNum(row.beforePrice);
if (rate !== undefined) {
row.beforePrice = calcBefore(val, rate as number);
} else if (before !== undefined) {
row.taxRate = calcRate(val, before as number);
}
updateSubtotal(row);
};
// 选中联系人后,自动带出姓名、电话、邮箱(可编辑,不影响再次选择)
const onCustomerContactChanged = (id: any) => {
const c = customerContactList.value.find((x: any) => x.contactId === id);
form.customerContactName = c?.contactName || '';
form.customerContactPhone = c?.phoneNumber || '';
form.customerContactEmail = c?.email || '';
};
// 供应商计划标识变化处理
const onSupplierPlanFlagChanged = (flag: string) => {
supplierPlanFlag.value = flag;
// 切换计划标识时清空供应商选择
form.supplierContactId = undefined;
form.supplierContactName = '';
form.supplierContactPhone = '';
form.supplierContactEmail = '';
};
// 供应商选择变化处理(计划内供应商)
const onSupplierChanged = (supplierId: any) => {
const supplier = supplierList.value.find((x: any) => x.supplierId === supplierId);
if (supplier) {
// 从供应商信息中带出联系人、电话、邮箱(可编辑)
form.supplierContactName = supplier.contactPerson || '';
form.supplierContactPhone = supplier.contactPhone || supplier.contactMobile || '';
form.supplierContactEmail = supplier.contactEmail || '';
}
};
const onMaterialChange = (row: any) => {
// 根据选中物料带出单位与名称(若有),提升录入效率
const m = materialList.value.find((x: any) => x.materialId === row.materialId);
if (m) {
if (m.unitName) row.unitName = m.unitName;
if (!row.productName && m.materialName) row.productName = m.materialName;
}
};
// 生成报价单编号codeRuleCode=1004
const generateQuoteCode = async () => {
if (isCodeGenerated.value) return;
try {
const params = { codeRuleCode: '1004' } as any;
const res = await getRuleGenerateCode(params);
form.quoteCode = res.msg;
isCodeGenerated.value = true;
proxy?.$modal.msgSuccess('报价单编号生成成功');
} catch (error) {
proxy?.$modal.msgError('报价单编号生成失败');
}
};
// 附件上传对话框UI占位复用通用上传组件
const type = ref(0);
const dialog = reactive({ visible: false, title: '' });
const ossFileModel = ref<string | string[] | undefined>(undefined);
const handleFile = () => {
type.value = 0;
dialog.visible = true;
dialog.title = '上传报价附件';
};
const submitOss = () => {
dialog.visible = false;
proxy?.$modal.msgSuccess('附件已更新');
};
const cancel = () => {
dialog.visible = false;
};
const submitForm = () => {
quoteFormRef.value?.validate(async (valid: boolean) => {
if (!valid) return;
buttonLoading.value = true;
try {
// 携带itemsBo一次性保存由后端主子表事务处理
const payload: any = {
...form,
itemsBo: materialRows.value.map((r, idx) => ({
...r,
itemNo: idx + 1,
subtotal: Number(r.subtotal || 0)
}))
};
if (!form.quoteId) {
await addCrmQuoteInfo(payload);
} else {
await updateCrmQuoteInfo(payload);
}
proxy?.$modal.msgSuccess('保存成功');
router.back();
} finally {
buttonLoading.value = false;
}
});
};
const goBack = () => router.back();
onMounted(async () => {
// 下拉数据初始化
const materialRes = await getBaseMaterialInfoList({});
materialList.value = materialRes.data || [];
const contactRes = await getCrmCustomerContactList({});
customerContactList.value = contactRes.data || [];
// 加载供应商列表(计划内供应商)
const supplierRes = await getCrmSupplierInfoList({});
supplierList.value = supplierRes.data || [];
await getUnitInfoListSelect();
// 编辑场景
const id = route.query.id || route.params.id;
if (id) {
// 先回填主键,防止数据尚未加载完用户就点击保存而误走新增
// 前端保持 quoteId 为字符串,避免长整型精度丢失
form.quoteId = id as any;
const res = await getCrmQuoteInfo(id as any);
Object.assign(form, res.data);
// 编辑模式:已存在编号则禁用生成
isCodeGenerated.value = !!form.quoteCode;
// 根据已有联系人ID回填姓名、电话、邮箱若后端已返回则保持
if (!form.customerContactPhone || !form.customerContactEmail || !form.customerContactName) {
onCustomerContactChanged(form.customerContactId);
}
// 供应商信息回填:判断是否为计划内供应商
if (form.supplierContactId) {
const supplier = supplierList.value.find((x: any) => x.supplierId === form.supplierContactId);
if (supplier) {
// 计划内供应商
supplierPlanFlag.value = '1';
if (!form.supplierContactPhone || !form.supplierContactEmail || !form.supplierContactName) {
onSupplierChanged(form.supplierContactId);
}
} else {
// 计划外供应商
supplierPlanFlag.value = '2';
}
}
// 仅使用后端VO内联返回的 itemsVo前端不再单独请求子表接口
const inlineItems = (res.data as any)?.itemsVo;
if (inlineItems && inlineItems.length) {
// 后端 BigDecimal/字符串数值需在前端转为 number确保 el-input-number 与 toFixed 正常工作
materialRows.value = inlineItems.map((r: any) => ({
quoteMaterialId: r.quoteMaterialId,
quoteId: r.quoteId,
itemNo: r.itemNo,
materialId: r.materialId,
relationMaterialId: r.relationMaterialId,
unitId: r.unitId,
unitName: r.unitName,
productName: r.productName,
specificationDescription: r.specificationDescription,
amount: Number(r.amount ?? 0),
beforePrice: Number(r.beforePrice ?? 0),
taxRate: Number(r.taxRate ?? (form.taxRate ?? 0)),
includingPrice: Number(r.includingPrice ?? 0),
subtotal: Number(r.subtotal ?? 0),
remark: r.remark
}));
} else {
materialRows.value = [];
}
}
// 新增模式:如已有编号则禁用生成按钮
if (!id) {
isCodeGenerated.value = !!form.quoteCode;
}
});
</script>
<style scoped>
/* 基础信息分区整体居中显示 */
.basic-center {
max-width: 860px;
margin: 0 auto;
}
</style>

@ -0,0 +1,423 @@
<template>
<div class="p-2">
<el-card shadow="never">
<div class="mb-3 flex items-center justify-between">
<div>
<el-button type="info" :loading="draftLoading" :disabled="submitLoading" @click="handleSave('draft')"></el-button>
<el-button type="primary" :loading="submitLoading" :disabled="draftLoading" @click="handleSave('submit')"></el-button>
</div>
<div>
<el-button @click="goBack"></el-button>
</div>
</div>
<el-form ref="projectChangeFormRef" :model="form" :rules="rules" label-width="140px">
<el-divider content-position="left">基本信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目名称" prop="projectId">
<el-input v-model="form.projectName" disabled placeholder="项目名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目编号">
<el-input v-model="form.projectCode" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目类别">
<el-select v-model="form.projectCategory" disabled style="width: 100%">
<el-option v-for="dict in project_category" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目经理">
<el-input v-model="form.projectManagerName" disabled placeholder="项目经理" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="部门负责人">
<el-input v-model="form.deptHeadName" disabled placeholder="部门负责人" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="分管副总">
<el-input v-model="form.responsibleVpName" disabled placeholder="分管副总" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="变更次数">
<el-input-number v-model="form.changeNumber" disabled :controls="false" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="申请变更日期" prop="applyChangeDate">
<el-date-picker v-model="form.applyChangeDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择申请变更日期" style="width: 100%"/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="变更类型" prop="changeType">
<el-checkbox-group v-model="changeTypeList">
<el-checkbox v-for="dict in change_type" :key="dict.value" :label="dict.value">{{ dict.label }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">预算变更</el-divider>
<el-row :gutter="20" class="mb-2">
<el-col :span="8">
<el-form-item label="当前合同额">
<el-input-number v-model="form.contractAmount" :precision="2" disabled style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="当前合同净额">
<el-input-number v-model="form.contractNetAmount" :precision="2" disabled style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" icon="Plus" @click="handleAddBudget"></el-button>
</el-col>
</el-row>
<el-table :data="form.budgetList" border stripe max-height="400">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="科目名称" width="200" align="center">
<template #default="scope">
<el-input v-model="scope.row.subjectName" placeholder="请输入科目名称" />
</template>
</el-table-column>
<el-table-column label="变更前预算" width="150" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.budgetBefore" :min="0" :precision="2" controls-position="right" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="变更后预算" width="150" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.budgetAfter" :min="0" :precision="2" controls-position="right" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="已使用金额" width="150" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.amountUsed" :min="0" :precision="2" controls-position="right" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="费用调整原因" min-width="200" align="center">
<template #default="scope">
<el-input v-model="scope.row.adjustmentReason" placeholder="请输入费用调整原因" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="150" align="center">
<template #default="scope">
<el-input v-model="scope.row.remark" placeholder="请输入备注" />
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="scope">
<el-button type="danger" link icon="Delete" @click="handleDeleteBudget(scope.$index)"></el-button>
</template>
</el-table-column>
</el-table>
<el-divider content-position="left">进度变更项目里程碑/关键节点</el-divider>
<el-table :data="form.progressList" border stripe max-height="400">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="项目阶段" width="120" align="center">
<template #default="scope">
<dict-tag :options="project_phases" :value="scope.row.projectPhases" />
</template>
</el-table-column>
<el-table-column label="里程碑名称" width="150" align="center">
<template #default="scope">
<el-input v-model="scope.row.milestoneName" placeholder="请输入里程碑名称" />
</template>
</el-table-column>
<el-table-column label="原计划时间起" width="160" align="center">
<template #default="scope">
<el-date-picker v-model="scope.row.originalStart" type="date" value-format="YYYY-MM-DD" disabled style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="原计划时间止" width="160" align="center">
<template #default="scope">
<el-date-picker v-model="scope.row.originalEnd" type="date" value-format="YYYY-MM-DD" disabled style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="变更后时间起" width="160" align="center">
<template #default="scope">
<el-date-picker v-model="scope.row.changedStart" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="变更后时间止" width="160" align="center">
<template #default="scope">
<el-date-picker v-model="scope.row.changedEnd" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="里程碑完成程度(%)" width="180" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.completionDegree" :min="0" :max="100" :precision="2" controls-position="right" style="width: 100%" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="200" align="center">
<template #default="scope">
<el-input v-model="scope.row.remark" placeholder="请输入备注" />
</template>
</el-table-column>
</el-table>
<el-divider content-position="left">项目当前情况说明</el-divider>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="当前情况说明" prop="currentStatus">
<el-input v-model="form.currentStatus" type="textarea" :rows="3" placeholder="请输入当前情况说明" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="变更原因" prop="changeReason">
<el-input v-model="form.changeReason" type="textarea" :rows="3" placeholder="请输入变更原因" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="后续工作" prop="followUpWork">
<el-input v-model="form.followUpWork" type="textarea" :rows="3" placeholder="请输入后续工作" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts" name="ErpProjectChangeEdit">
import { ref, reactive, onMounted, getCurrentInstance, toRefs, watch, onActivated, nextTick } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { getErpProjectChange, addErpProjectChange, updateErpProjectChange, prepareProjectChangeWithInfo, submitProjectChangeAndFlowStart } from '@/api/oa/erp/erpProjectChange';
import { ErpProjectChangeForm, ErpProjectChangeBudget, ErpProjectChangeProgress } from '@/api/oa/erp/erpProjectChange/types';
import { useUserStore } from '@/store/modules/user';
import type { FormInstance as ElFormInstance } from 'element-plus';
import type { ComponentInternalInstance } from 'vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { project_category, change_type, project_phases } = toRefs<any>(proxy?.useDict('project_category', 'change_type', 'project_phases'));
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const projectChangeFormRef = ref<ElFormInstance>();
const draftLoading = ref(false);
const submitLoading = ref(false);
const changeTypeList = ref<string[]>([]);
const formatToday = () => (proxy?.parseTime ? proxy.parseTime(new Date(), '{y}-{m}-{d}') : new Date().toISOString().slice(0, 10));
const toDateOnly = (time?: string) => {
if (!time) {
return undefined;
}
const formatted = proxy?.parseTime ? proxy.parseTime(time, '{y}-{m}-{d}') : new Date(time).toISOString().slice(0, 10);
return formatted === null ? undefined : formatted;
};
const getDefaultForm = (): ErpProjectChangeForm => ({
projectChangeId: undefined,
projectId: undefined,
projectCode: '',
projectName: '',
projectCategory: '',
changeType: '',
changeNumber: undefined,
projectManagerId: undefined,
projectManagerName: '',
deptHeadId: undefined,
deptHeadName: '',
responsibleVpId: undefined,
responsibleVpName: '',
applyChangeDate: formatToday(),
contractAmount: 0,
contractNetAmount: 0,
currentStatus: '',
changeReason: '',
followUpWork: '',
projectChangeStatus: '1',
activeFlag: '1',
budgetList: [],
progressList: []
});
watch(
changeTypeList,
(val) => {
form.value.changeType = val.join(',');
},
{ deep: true }
);
const form = ref<ErpProjectChangeForm>({
...getDefaultForm()
});
const resetFormState = async () => {
form.value = { ...getDefaultForm() };
changeTypeList.value = [];
await nextTick();
projectChangeFormRef.value?.clearValidate?.();
};
const rules = reactive({
projectId: [{ required: true, message: '项目不能为空', trigger: 'change' }],
applyChangeDate: [{ required: true, message: '申请变更时间不能为空', trigger: 'change' }],
changeType: [{ required: true, message: '变更类型不能为空', trigger: 'change' }]
});
const loadProjectDataByProjectId = async (projectId: string | number) => {
if (!projectId) return;
try {
const res = await prepareProjectChangeWithInfo(projectId);
if (res.data) {
Object.assign(form.value, res.data);
const dateOnly = toDateOnly(form.value.applyChangeDate);
form.value.applyChangeDate = dateOnly ?? formatToday();
form.value.budgetList = res.data.budgetList ?? [];
form.value.progressList = res.data.progressList ?? [];
//
if (form.value.changeType) {
changeTypeList.value = form.value.changeType.split(',');
}
}
} catch (error) {
proxy?.$modal.msgError('加载项目信息失败');
console.error(error);
}
};
const handleAddBudget = () => {
const list = form.value.budgetList ?? (form.value.budgetList = []);
const budget: ErpProjectChangeBudget = {
subjectName: '',
budgetBefore: 0,
budgetAfter: 0,
amountUsed: 0,
adjustmentReason: '',
sortOrder: list.length + 1,
remark: ''
};
list.push(budget);
};
const handleDeleteBudget = (index: number) => {
form.value.budgetList.splice(index, 1);
};
const handleSave = async (action: 'draft' | 'submit') => {
await projectChangeFormRef.value?.validate();
if (!form.value.projectManagerId) {
proxy?.$modal.msgError('请先选择项目');
return;
}
if (action === 'submit' && userStore.userId !== form.value.projectManagerId) {
proxy?.$modal.msgError('只有项目经理才能提交项目变更申请');
return;
}
form.value.changeType = changeTypeList.value.join(',');
if (action === 'draft') {
draftLoading.value = true;
try {
if (form.value.projectChangeId) {
await updateErpProjectChange(form.value);
} else {
await addErpProjectChange(form.value);
}
proxy?.$modal.msgSuccess('暂存成功');
goBack();
} catch (error) {
proxy?.$modal.msgError('暂存失败');
} finally {
draftLoading.value = false;
}
} else {
submitLoading.value = true;
try {
//
form.value.flowCode = 'OAEP';
//
form.value.variables = {
projectId: form.value.projectId,
projectName: form.value.projectName,
projectCode: form.value.projectCode,
projectManagerId: form.value.projectManagerId,
deptHeadId: form.value.deptHeadId,
changeType: form.value.changeType
};
//
form.value.bizExt = {
businessTitle: '项目计划变更申请',
businessCode: form.value.projectCode
};
await submitProjectChangeAndFlowStart(form.value);
proxy?.$modal.msgSuccess('提交成功');
goBack();
} catch (error) {
proxy?.$modal.msgError('提交失败');
} finally {
submitLoading.value = false;
}
}
};
const goBack = () => {
router.back();
};
const loadFormData = async () => {
await resetFormState();
const projectChangeId = route.params.projectChangeId as string;
const projectId = route.query.projectId as string;
if (projectChangeId && projectChangeId !== '0') {
//
try {
const res = await getErpProjectChange(projectChangeId);
if (res.data) {
Object.assign(form.value, res.data);
const dateOnly = toDateOnly(form.value.applyChangeDate);
form.value.applyChangeDate = dateOnly ?? formatToday();
form.value.budgetList = res.data.budgetList ?? [];
form.value.progressList = res.data.progressList ?? [];
if (res.data.changeType) {
changeTypeList.value = res.data.changeType.split(',');
}
}
} catch (error) {
proxy?.$modal.msgError('加载变更申请数据失败');
console.error(error);
}
} else if (projectId) {
// ID
await loadProjectDataByProjectId(projectId);
} else {
proxy?.$modal.msgError('缺少必要的参数项目ID');
}
};
let skipNextActivated = false;
onMounted(async () => {
skipNextActivated = true;
await loadFormData();
});
onActivated(async () => {
if (skipNextActivated) {
skipNextActivated = false;
return;
}
await loadFormData();
});
</script>

@ -0,0 +1,492 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width='100px'>
<el-form-item label="项目ID" prop="projectId">
<el-input v-model="queryParams.projectId" placeholder="请输入项目ID" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="项目编号" prop="projectCode">
<el-input v-model="queryParams.projectCode" placeholder="请输入项目编号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="项目类别" prop="projectCategory">
<el-select v-model="queryParams.projectCategory" placeholder="请选择项目类别" clearable >
<el-option v-for="dict in project_category" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item label="变更次数" prop="changeNumber">
<el-input v-model="queryParams.changeNumber" placeholder="请输入变更次数" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="项目经理ID" prop="projectManagerId">
<el-input v-model="queryParams.projectManagerId" placeholder="请输入项目经理ID" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="项目经理姓名" prop="projectManagerName">
<el-input v-model="queryParams.projectManagerName" placeholder="请输入项目经理姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="部门负责人ID" prop="deptHeadId">
<el-input v-model="queryParams.deptHeadId" placeholder="请输入部门负责人ID" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="部门负责人姓名" prop="deptHeadName">
<el-input v-model="queryParams.deptHeadName" placeholder="请输入部门负责人姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="分管副总ID" prop="responsibleVpId">
<el-input v-model="queryParams.responsibleVpId" placeholder="请输入分管副总ID" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="分管副总姓名" prop="responsibleVpName">
<el-input v-model="queryParams.responsibleVpName" placeholder="请输入分管副总姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="申请变更时间" prop="applyChangeDate">
<el-date-picker clearable
v-model="queryParams.applyChangeDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择申请变更时间"
/>
</el-form-item>
<el-form-item label="项目合同额" prop="contractAmount">
<el-input v-model="queryParams.contractAmount" placeholder="请输入项目合同额" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="项目合同净额" prop="contractNetAmount">
<el-input v-model="queryParams.contractNetAmount" placeholder="请输入项目合同净额" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="变更原因" prop="changeReason">
<el-input v-model="queryParams.changeReason" placeholder="请输入变更原因" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="后续工作" prop="followUpWork">
<el-input v-model="queryParams.followUpWork" placeholder="请输入后续工作" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="项目变更状态(1暂存 2审批中 3可用)" prop="projectChangeStatus">
<el-select v-model="queryParams.projectChangeStatus" placeholder="请选择项目变更状态(1暂存 2审批中 3可用)" clearable >
<el-option v-for="dict in project_change_status" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item label="激活标识" prop="activeFlag">
<el-select v-model="queryParams.activeFlag" placeholder="请选择激活标识" clearable >
<el-option v-for="dict in active_flag" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</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-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['oa/erp:erpProjectChange:add']"></el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['oa/erp:erpProjectChange:edit']"></el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['oa/erp:erpProjectChange:remove']"></el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['oa/erp:erpProjectChange:export']"></el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" :columns="columns" :search="true" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="erpProjectChangeList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="项目变更ID" align="center" prop="projectChangeId" v-if="columns[0].visible"/>
<el-table-column label="项目ID" align="center" prop="projectId" v-if="columns[2].visible"/>
<el-table-column label="项目编号" align="center" prop="projectCode" v-if="columns[3].visible"/>
<el-table-column label="项目名称" align="center" prop="projectName" v-if="columns[4].visible"/>
<el-table-column label="项目类别" align="center" prop="projectCategory" v-if="columns[5].visible">
<template #default="scope">
<dict-tag :options="project_category" :value="scope.row.projectCategory"/>
</template>
</el-table-column>
<el-table-column label="变更类型" align="center" prop="changeType" v-if="columns[6].visible">
<template #default="scope">
<dict-tag :options="change_type" :value="scope.row.changeType ? scope.row.changeType.split(',') : []"/>
</template>
</el-table-column>
<el-table-column label="变更次数" align="center" prop="changeNumber" v-if="columns[7].visible"/>
<el-table-column label="项目经理ID" align="center" prop="projectManagerId" v-if="columns[8].visible"/>
<el-table-column label="项目经理姓名" align="center" prop="projectManagerName" v-if="columns[9].visible"/>
<el-table-column label="部门负责人ID" align="center" prop="deptHeadId" v-if="columns[10].visible"/>
<el-table-column label="部门负责人姓名" align="center" prop="deptHeadName" v-if="columns[11].visible"/>
<el-table-column label="分管副总ID" align="center" prop="responsibleVpId" v-if="columns[12].visible"/>
<el-table-column label="分管副总姓名" align="center" prop="responsibleVpName" v-if="columns[13].visible"/>
<el-table-column label="申请变更时间" align="center" prop="applyChangeDate" width="180" v-if="columns[14].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.applyChangeDate, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="项目合同额" align="center" prop="contractAmount" v-if="columns[15].visible"/>
<el-table-column label="项目合同净额" align="center" prop="contractNetAmount" v-if="columns[16].visible"/>
<el-table-column label="项目当前情况" align="center" prop="currentStatus" v-if="columns[17].visible"/>
<el-table-column label="变更原因" align="center" prop="changeReason" v-if="columns[18].visible"/>
<el-table-column label="后续工作" align="center" prop="followUpWork" v-if="columns[19].visible"/>
<el-table-column label="项目变更状态(1暂存 2审批中 3可用)" align="center" prop="projectChangeStatus" v-if="columns[20].visible">
<template #default="scope">
<dict-tag :options="project_change_status" :value="scope.row.projectChangeStatus"/>
</template>
</el-table-column>
<el-table-column label="流程状态" align="center" prop="flowStatus" v-if="columns[21].visible"/>
<el-table-column label="备注" align="center" prop="remark" v-if="columns[22].visible"/>
<el-table-column label="激活标识" align="center" prop="activeFlag" v-if="columns[23].visible">
<template #default="scope">
<dict-tag :options="active_flag" :value="scope.row.activeFlag"/>
</template>
</el-table-column>
<el-table-column label="创建人" align="center" prop="createBy" v-if="columns[26].visible"/>
<el-table-column label="创建时间" align="center" prop="createTime" width="180" v-if="columns[27].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['oa/erp:erpProjectChange:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['oa/erp:erpProjectChange:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改项目变更申请对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="erpProjectChangeFormRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="项目ID" prop="projectId">
<el-input v-model="form.projectId" placeholder="请输入项目ID" />
</el-form-item>
<el-form-item label="项目编号" prop="projectCode">
<el-input v-model="form.projectCode" placeholder="请输入项目编号" />
</el-form-item>
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入项目名称" />
</el-form-item>
<el-form-item label="项目类别" prop="projectCategory">
<el-select v-model="form.projectCategory" placeholder="请选择项目类别">
<el-option
v-for="dict in project_category"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="变更类型" prop="changeType">
<el-checkbox-group v-model="form.changeType">
<el-checkbox
v-for="dict in change_type"
:key="dict.value"
:label="dict.value">
{{dict.label}}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="变更次数" prop="changeNumber">
<el-input v-model="form.changeNumber" placeholder="请输入变更次数" />
</el-form-item>
<el-form-item label="项目经理ID" prop="projectManagerId">
<el-input v-model="form.projectManagerId" placeholder="请输入项目经理ID" />
</el-form-item>
<el-form-item label="项目经理姓名" prop="projectManagerName">
<el-input v-model="form.projectManagerName" placeholder="请输入项目经理姓名" />
</el-form-item>
<el-form-item label="部门负责人ID" prop="deptHeadId">
<el-input v-model="form.deptHeadId" placeholder="请输入部门负责人ID" />
</el-form-item>
<el-form-item label="部门负责人姓名" prop="deptHeadName">
<el-input v-model="form.deptHeadName" placeholder="请输入部门负责人姓名" />
</el-form-item>
<el-form-item label="分管副总ID" prop="responsibleVpId">
<el-input v-model="form.responsibleVpId" placeholder="请输入分管副总ID" />
</el-form-item>
<el-form-item label="分管副总姓名" prop="responsibleVpName">
<el-input v-model="form.responsibleVpName" placeholder="请输入分管副总姓名" />
</el-form-item>
<el-form-item label="申请变更时间" prop="applyChangeDate">
<el-date-picker clearable
v-model="form.applyChangeDate"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择申请变更时间">
</el-date-picker>
</el-form-item>
<el-form-item label="项目合同额" prop="contractAmount">
<el-input v-model="form.contractAmount" placeholder="请输入项目合同额" />
</el-form-item>
<el-form-item label="项目合同净额" prop="contractNetAmount">
<el-input v-model="form.contractNetAmount" placeholder="请输入项目合同净额" />
</el-form-item>
<el-form-item label="变更原因" prop="changeReason">
<el-input v-model="form.changeReason" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="后续工作" prop="followUpWork">
<el-input v-model="form.followUpWork" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="项目变更状态(1暂存 2审批中 3可用)" prop="projectChangeStatus">
<el-radio-group v-model="form.projectChangeStatus">
<el-radio
v-for="dict in project_change_status"
:key="dict.value"
:value="dict.value"
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="激活标识" prop="activeFlag">
<el-radio-group v-model="form.activeFlag">
<el-radio
v-for="dict in active_flag"
:key="dict.value"
:value="dict.value"
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="ErpProjectChange" lang="ts">
import { listErpProjectChange, getErpProjectChange, delErpProjectChange, addErpProjectChange, updateErpProjectChange } from '@/api/oa/erp/erpProjectChange';
import { ErpProjectChangeVO, ErpProjectChangeQuery, ErpProjectChangeForm } from '@/api/oa/erp/erpProjectChange/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { active_flag, change_type, project_change_status, project_category } = toRefs<any>(proxy?.useDict('active_flag', 'change_type', 'project_change_status', 'project_category'));
const erpProjectChangeList = ref<ErpProjectChangeVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const erpProjectChangeFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
//
const columns = ref<FieldOption[]>([
{ key: 0, label: `项目变更ID`, visible: false },
{ key: 1, label: `租户编号`, visible: false },
{ key: 2, label: `项目ID`, visible: false },
{ key: 3, label: `项目编号`, visible: true },
{ key: 4, label: `项目名称`, visible: true },
{ key: 5, label: `项目类别`, visible: true },
{ key: 6, label: `变更类型`, visible: true },
{ key: 7, label: `变更次数`, visible: true },
{ key: 8, label: `项目经理ID`, visible: false },
{ key: 9, label: `项目经理姓名`, visible: true },
{ key: 10, label: `部门负责人ID`, visible: false },
{ key: 11, label: `部门负责人姓名`, visible: true },
{ key: 12, label: `分管副总ID`, visible: false },
{ key: 13, label: `分管副总姓名`, visible: false },
{ key: 14, label: `申请变更时间`, visible: true },
{ key: 15, label: `项目合同额`, visible: false },
{ key: 16, label: `项目合同净额`, visible: false },
{ key: 17, label: `项目当前情况`, visible: false },
{ key: 18, label: `变更原因`, visible: false },
{ key: 19, label: `后续工作`, visible: false },
{ key: 20, label: `项目变更状态`, visible: true },
{ key: 21, label: `流程状态`, visible: false },
{ key: 22, label: `备注`, visible: false },
{ key: 23, label: `激活标识`, visible: false },
{ key: 24, label: `删除标志`, visible: false },
{ key: 25, label: `创建部门`, visible: false },
{ key: 26, label: `创建人`, visible: true },
{ key: 27, label: `创建时间`, visible: true },
{ key: 28, label: `更新人`, visible: false },
{ key: 29, label: `更新时间`, visible: false },
]);
const initFormData: ErpProjectChangeForm = {
projectChangeId: undefined,
projectId: undefined,
projectCode: undefined,
projectName: undefined,
projectCategory: undefined,
changeType: [],
changeNumber: undefined,
projectManagerId: undefined,
projectManagerName: undefined,
deptHeadId: undefined,
deptHeadName: undefined,
responsibleVpId: undefined,
responsibleVpName: undefined,
applyChangeDate: undefined,
contractAmount: undefined,
contractNetAmount: undefined,
currentStatus: undefined,
changeReason: undefined,
followUpWork: undefined,
projectChangeStatus: undefined,
flowStatus: undefined,
remark: undefined,
activeFlag: undefined,
}
const data = reactive<PageData<ErpProjectChangeForm, ErpProjectChangeQuery>>({
form: {...initFormData},
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: undefined,
projectCode: undefined,
projectName: undefined,
projectCategory: undefined,
changeType: undefined,
changeNumber: undefined,
projectManagerId: undefined,
projectManagerName: undefined,
deptHeadId: undefined,
deptHeadName: undefined,
responsibleVpId: undefined,
responsibleVpName: undefined,
applyChangeDate: undefined,
contractAmount: undefined,
contractNetAmount: undefined,
currentStatus: undefined,
changeReason: undefined,
followUpWork: undefined,
projectChangeStatus: undefined,
flowStatus: undefined,
activeFlag: undefined,
params: {
}
},
rules: {
projectChangeId: [
{ required: true, message: "项目变更ID不能为空", trigger: "blur" }
],
projectId: [
{ required: true, message: "项目ID不能为空", trigger: "blur" }
],
applyChangeDate: [
{ required: true, message: "申请变更时间不能为空", trigger: "blur" }
],
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询项目变更申请列表 */
const getList = async () => {
loading.value = true;
const res = await listErpProjectChange(queryParams.value);
erpProjectChangeList.value = res.rows;
total.value = res.total;
loading.value = false;
}
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
}
/** 表单重置 */
const reset = () => {
form.value = {...initFormData};
erpProjectChangeFormRef.value?.resetFields();
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: ErpProjectChangeVO[]) => {
ids.value = selection.map(item => item.projectChangeId);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = "添加项目变更申请";
}
/** 修改按钮操作 */
const handleUpdate = async (row?: ErpProjectChangeVO) => {
reset();
const _projectChangeId = row?.projectChangeId || ids.value[0]
const res = await getErpProjectChange(_projectChangeId);
Object.assign(form.value, res.data);
form.value.changeType = form.value.changeType.split(",");
dialog.visible = true;
dialog.title = "修改项目变更申请";
}
/** 提交按钮 */
const submitForm = () => {
erpProjectChangeFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
form.value.changeType = form.value.changeType.join(",");
if (form.value.projectChangeId) {
await updateErpProjectChange(form.value).finally(() => buttonLoading.value = false);
} else {
await addErpProjectChange(form.value).finally(() => buttonLoading.value = false);
}
proxy?.$modal.msgSuccess("操作成功");
dialog.visible = false;
await getList();
}
});
}
/** 删除按钮操作 */
const handleDelete = async (row?: ErpProjectChangeVO) => {
const _projectChangeIds = row?.projectChangeId || ids.value;
await proxy?.$modal.confirm('是否确认删除项目变更申请编号为"' + _projectChangeIds + '"的数据项?').finally(() => loading.value = false);
await delErpProjectChange(_projectChangeIds);
proxy?.$modal.msgSuccess("删除成功");
await getList();
}
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download('oa/erp/erpProjectChange/export', {
...queryParams.value
}, `erpProjectChange_${new Date().getTime()}.xlsx`)
}
onMounted(() => {
getList();
});
</script>

@ -0,0 +1,305 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width='100px'>
<el-form-item label="变更申请ID" prop="projectChangeId">
<el-input v-model="queryParams.projectChangeId" placeholder="请输入变更申请ID" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="预算详情ID" prop="budgetDetailId">
<el-input v-model="queryParams.budgetDetailId" placeholder="请输入预算详情ID" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="科目名称" prop="subjectName">
<el-input v-model="queryParams.subjectName" placeholder="请输入科目名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="变更前预算" prop="budgetBefore">
<el-input v-model="queryParams.budgetBefore" placeholder="请输入变更前预算" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="变更后预算" prop="budgetAfter">
<el-input v-model="queryParams.budgetAfter" placeholder="请输入变更后预算" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="已使用金额" prop="amountUsed">
<el-input v-model="queryParams.amountUsed" placeholder="请输入已使用金额" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="费用调整原因" prop="adjustmentReason">
<el-input v-model="queryParams.adjustmentReason" placeholder="请输入费用调整原因" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="排序顺序" prop="sortOrder">
<el-input v-model="queryParams.sortOrder" placeholder="请输入排序顺序" clearable @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-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['oa/erp:erpProjectChangeBudget:add']"></el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['oa/erp:erpProjectChangeBudget:edit']"></el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['oa/erp:erpProjectChangeBudget:remove']"></el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['oa/erp:erpProjectChangeBudget:export']"></el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" :columns="columns" :search="true" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="erpProjectChangeBudgetList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="预算变更ID" align="center" prop="changeBudgetId" v-if="columns[0].visible"/>
<el-table-column label="变更申请ID" align="center" prop="projectChangeId" v-if="columns[1].visible"/>
<el-table-column label="预算详情ID" align="center" prop="budgetDetailId" v-if="columns[2].visible"/>
<el-table-column label="科目名称" align="center" prop="subjectName" v-if="columns[3].visible"/>
<el-table-column label="变更前预算" align="center" prop="budgetBefore" v-if="columns[4].visible"/>
<el-table-column label="变更后预算" align="center" prop="budgetAfter" v-if="columns[5].visible"/>
<el-table-column label="已使用金额" align="center" prop="amountUsed" v-if="columns[6].visible"/>
<el-table-column label="费用调整原因" align="center" prop="adjustmentReason" v-if="columns[7].visible"/>
<el-table-column label="排序顺序" align="center" prop="sortOrder" v-if="columns[8].visible"/>
<el-table-column label="备注" align="center" prop="remark" v-if="columns[9].visible"/>
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['oa/erp:erpProjectChangeBudget:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['oa/erp:erpProjectChangeBudget:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改项目预算变更明细对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="erpProjectChangeBudgetFormRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="变更申请ID" prop="projectChangeId">
<el-input v-model="form.projectChangeId" placeholder="请输入变更申请ID" />
</el-form-item>
<el-form-item label="预算详情ID" prop="budgetDetailId">
<el-input v-model="form.budgetDetailId" placeholder="请输入预算详情ID" />
</el-form-item>
<el-form-item label="科目名称" prop="subjectName">
<el-input v-model="form.subjectName" placeholder="请输入科目名称" />
</el-form-item>
<el-form-item label="变更前预算" prop="budgetBefore">
<el-input v-model="form.budgetBefore" placeholder="请输入变更前预算" />
</el-form-item>
<el-form-item label="变更后预算" prop="budgetAfter">
<el-input v-model="form.budgetAfter" placeholder="请输入变更后预算" />
</el-form-item>
<el-form-item label="已使用金额" prop="amountUsed">
<el-input v-model="form.amountUsed" placeholder="请输入已使用金额" />
</el-form-item>
<el-form-item label="费用调整原因" prop="adjustmentReason">
<el-input v-model="form.adjustmentReason" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="排序顺序" prop="sortOrder">
<el-input v-model="form.sortOrder" placeholder="请输入排序顺序" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="ErpProjectChangeBudget" lang="ts">
import { listErpProjectChangeBudget, getErpProjectChangeBudget, delErpProjectChangeBudget, addErpProjectChangeBudget, updateErpProjectChangeBudget } from '@/api/oa/erp/erpProjectChangeBudget';
import { ErpProjectChangeBudgetVO, ErpProjectChangeBudgetQuery, ErpProjectChangeBudgetForm } from '@/api/oa/erp/erpProjectChangeBudget/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const erpProjectChangeBudgetList = ref<ErpProjectChangeBudgetVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const erpProjectChangeBudgetFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
//
const columns = ref<FieldOption[]>([
{ key: 0, label: `预算变更ID`, visible: true },
{ key: 1, label: `变更申请ID`, visible: true },
{ key: 2, label: `预算详情ID`, visible: true },
{ key: 3, label: `科目名称`, visible: true },
{ key: 4, label: `变更前预算`, visible: true },
{ key: 5, label: `变更后预算`, visible: true },
{ key: 6, label: `已使用金额`, visible: true },
{ key: 7, label: `费用调整原因`, visible: true },
{ key: 8, label: `排序顺序`, visible: true },
{ key: 9, label: `备注`, visible: true },
{ key: 10, label: `删除标志`, visible: true },
{ key: 11, label: `创建部门`, visible: true },
{ key: 12, label: `创建人`, visible: true },
{ key: 13, label: `创建时间`, visible: true },
{ key: 14, label: `更新人`, visible: true },
{ key: 15, label: `更新时间`, visible: true },
]);
const initFormData: ErpProjectChangeBudgetForm = {
changeBudgetId: undefined,
projectChangeId: undefined,
budgetDetailId: undefined,
subjectName: undefined,
budgetBefore: undefined,
budgetAfter: undefined,
amountUsed: undefined,
adjustmentReason: undefined,
sortOrder: undefined,
remark: undefined,
}
const data = reactive<PageData<ErpProjectChangeBudgetForm, ErpProjectChangeBudgetQuery>>({
form: {...initFormData},
queryParams: {
pageNum: 1,
pageSize: 10,
projectChangeId: undefined,
budgetDetailId: undefined,
subjectName: undefined,
budgetBefore: undefined,
budgetAfter: undefined,
amountUsed: undefined,
adjustmentReason: undefined,
sortOrder: undefined,
params: {
}
},
rules: {
changeBudgetId: [
{ required: true, message: "预算变更ID不能为空", trigger: "blur" }
],
projectChangeId: [
{ required: true, message: "变更申请ID不能为空", trigger: "blur" }
],
subjectName: [
{ required: true, message: "科目名称不能为空", trigger: "blur" }
],
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询项目预算变更明细列表 */
const getList = async () => {
loading.value = true;
const res = await listErpProjectChangeBudget(queryParams.value);
erpProjectChangeBudgetList.value = res.rows;
total.value = res.total;
loading.value = false;
}
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
}
/** 表单重置 */
const reset = () => {
form.value = {...initFormData};
erpProjectChangeBudgetFormRef.value?.resetFields();
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: ErpProjectChangeBudgetVO[]) => {
ids.value = selection.map(item => item.changeBudgetId);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = "添加项目预算变更明细";
}
/** 修改按钮操作 */
const handleUpdate = async (row?: ErpProjectChangeBudgetVO) => {
reset();
const _changeBudgetId = row?.changeBudgetId || ids.value[0]
const res = await getErpProjectChangeBudget(_changeBudgetId);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = "修改项目预算变更明细";
}
/** 提交按钮 */
const submitForm = () => {
erpProjectChangeBudgetFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.changeBudgetId) {
await updateErpProjectChangeBudget(form.value).finally(() => buttonLoading.value = false);
} else {
await addErpProjectChangeBudget(form.value).finally(() => buttonLoading.value = false);
}
proxy?.$modal.msgSuccess("操作成功");
dialog.visible = false;
await getList();
}
});
}
/** 删除按钮操作 */
const handleDelete = async (row?: ErpProjectChangeBudgetVO) => {
const _changeBudgetIds = row?.changeBudgetId || ids.value;
await proxy?.$modal.confirm('是否确认删除项目预算变更明细编号为"' + _changeBudgetIds + '"的数据项?').finally(() => loading.value = false);
await delErpProjectChangeBudget(_changeBudgetIds);
proxy?.$modal.msgSuccess("删除成功");
await getList();
}
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download('oa/erp/erpProjectChangeBudget/export', {
...queryParams.value
}, `erpProjectChangeBudget_${new Date().getTime()}.xlsx`)
}
onMounted(() => {
getList();
});
</script>

@ -0,0 +1,374 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width='100px'>
<el-form-item label="变更申请ID" prop="projectChangeId">
<el-input v-model="queryParams.projectChangeId" placeholder="请输入变更申请ID" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="计划阶段ID" prop="planStageId">
<el-input v-model="queryParams.planStageId" placeholder="请输入计划阶段ID" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="项目里程碑" prop="milestoneName">
<el-input v-model="queryParams.milestoneName" placeholder="请输入项目里程碑" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="原计划时间起" prop="originalStart">
<el-date-picker clearable
v-model="queryParams.originalStart"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择原计划时间起"
/>
</el-form-item>
<el-form-item label="原计划时间止" prop="originalEnd">
<el-date-picker clearable
v-model="queryParams.originalEnd"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择原计划时间止"
/>
</el-form-item>
<el-form-item label="变更后时间起" prop="changedStart">
<el-date-picker clearable
v-model="queryParams.changedStart"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择变更后时间起"
/>
</el-form-item>
<el-form-item label="变更后时间止" prop="changedEnd">
<el-date-picker clearable
v-model="queryParams.changedEnd"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择变更后时间止"
/>
</el-form-item>
<el-form-item label="里程碑完成程度" prop="completionDegree">
<el-input v-model="queryParams.completionDegree" placeholder="请输入里程碑完成程度" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="排序顺序" prop="sortOrder">
<el-input v-model="queryParams.sortOrder" placeholder="请输入排序顺序" clearable @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-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['oa/erp:erpProjectChangeProgress:add']"></el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['oa/erp:erpProjectChangeProgress:edit']"></el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['oa/erp:erpProjectChangeProgress:remove']"></el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['oa/erp:erpProjectChangeProgress:export']"></el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" :columns="columns" :search="true" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="erpProjectChangeProgressList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="进度变更ID" align="center" prop="changeProgressId" v-if="columns[0].visible"/>
<el-table-column label="变更申请ID" align="center" prop="projectChangeId" v-if="columns[1].visible"/>
<el-table-column label="计划阶段ID" align="center" prop="planStageId" v-if="columns[2].visible"/>
<el-table-column label="项目里程碑" align="center" prop="milestoneName" v-if="columns[3].visible"/>
<el-table-column label="原计划时间起" align="center" prop="originalStart" width="180" v-if="columns[4].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.originalStart, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="原计划时间止" align="center" prop="originalEnd" width="180" v-if="columns[5].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.originalEnd, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="变更后时间起" align="center" prop="changedStart" width="180" v-if="columns[6].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.changedStart, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="变更后时间止" align="center" prop="changedEnd" width="180" v-if="columns[7].visible">
<template #default="scope">
<span>{{ parseTime(scope.row.changedEnd, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="里程碑完成程度" align="center" prop="completionDegree" v-if="columns[8].visible"/>
<el-table-column label="排序顺序" align="center" prop="sortOrder" v-if="columns[9].visible"/>
<el-table-column label="备注" align="center" prop="remark" v-if="columns[10].visible"/>
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['oa/erp:erpProjectChangeProgress:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['oa/erp:erpProjectChangeProgress:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改项目进度变更明细对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="erpProjectChangeProgressFormRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="变更申请ID" prop="projectChangeId">
<el-input v-model="form.projectChangeId" placeholder="请输入变更申请ID" />
</el-form-item>
<el-form-item label="计划阶段ID" prop="planStageId">
<el-input v-model="form.planStageId" placeholder="请输入计划阶段ID" />
</el-form-item>
<el-form-item label="项目里程碑" prop="milestoneName">
<el-input v-model="form.milestoneName" placeholder="请输入项目里程碑" />
</el-form-item>
<el-form-item label="原计划时间起" prop="originalStart">
<el-date-picker clearable
v-model="form.originalStart"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择原计划时间起">
</el-date-picker>
</el-form-item>
<el-form-item label="原计划时间止" prop="originalEnd">
<el-date-picker clearable
v-model="form.originalEnd"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择原计划时间止">
</el-date-picker>
</el-form-item>
<el-form-item label="变更后时间起" prop="changedStart">
<el-date-picker clearable
v-model="form.changedStart"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择变更后时间起">
</el-date-picker>
</el-form-item>
<el-form-item label="变更后时间止" prop="changedEnd">
<el-date-picker clearable
v-model="form.changedEnd"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择变更后时间止">
</el-date-picker>
</el-form-item>
<el-form-item label="里程碑完成程度" prop="completionDegree">
<el-input v-model="form.completionDegree" placeholder="请输入里程碑完成程度" />
</el-form-item>
<el-form-item label="排序顺序" prop="sortOrder">
<el-input v-model="form.sortOrder" placeholder="请输入排序顺序" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="ErpProjectChangeProgress" lang="ts">
import { listErpProjectChangeProgress, getErpProjectChangeProgress, delErpProjectChangeProgress, addErpProjectChangeProgress, updateErpProjectChangeProgress } from '@/api/oa/erp/erpProjectChangeProgress';
import { ErpProjectChangeProgressVO, ErpProjectChangeProgressQuery, ErpProjectChangeProgressForm } from '@/api/oa/erp/erpProjectChangeProgress/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const erpProjectChangeProgressList = ref<ErpProjectChangeProgressVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const erpProjectChangeProgressFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
//
const columns = ref<FieldOption[]>([
{ key: 0, label: `进度变更ID`, visible: true },
{ key: 1, label: `变更申请ID`, visible: true },
{ key: 2, label: `计划阶段ID`, visible: true },
{ key: 3, label: `项目里程碑`, visible: true },
{ key: 4, label: `原计划时间起`, visible: true },
{ key: 5, label: `原计划时间止`, visible: true },
{ key: 6, label: `变更后时间起`, visible: true },
{ key: 7, label: `变更后时间止`, visible: true },
{ key: 8, label: `里程碑完成程度`, visible: true },
{ key: 9, label: `排序顺序`, visible: true },
{ key: 10, label: `备注`, visible: true },
{ key: 11, label: `删除标志`, visible: true },
{ key: 12, label: `创建部门`, visible: true },
{ key: 13, label: `创建人`, visible: true },
{ key: 14, label: `创建时间`, visible: true },
{ key: 15, label: `更新人`, visible: true },
{ key: 16, label: `更新时间`, visible: true },
]);
const initFormData: ErpProjectChangeProgressForm = {
changeProgressId: undefined,
projectChangeId: undefined,
planStageId: undefined,
milestoneName: undefined,
originalStart: undefined,
originalEnd: undefined,
changedStart: undefined,
changedEnd: undefined,
completionDegree: undefined,
sortOrder: undefined,
remark: undefined,
}
const data = reactive<PageData<ErpProjectChangeProgressForm, ErpProjectChangeProgressQuery>>({
form: {...initFormData},
queryParams: {
pageNum: 1,
pageSize: 10,
projectChangeId: undefined,
planStageId: undefined,
milestoneName: undefined,
originalStart: undefined,
originalEnd: undefined,
changedStart: undefined,
changedEnd: undefined,
completionDegree: undefined,
sortOrder: undefined,
params: {
}
},
rules: {
changeProgressId: [
{ required: true, message: "进度变更ID不能为空", trigger: "blur" }
],
projectChangeId: [
{ required: true, message: "变更申请ID不能为空", trigger: "blur" }
],
planStageId: [
{ required: true, message: "计划阶段ID不能为空", trigger: "blur" }
],
milestoneName: [
{ required: true, message: "项目里程碑不能为空", trigger: "blur" }
],
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询项目进度变更明细列表 */
const getList = async () => {
loading.value = true;
const res = await listErpProjectChangeProgress(queryParams.value);
erpProjectChangeProgressList.value = res.rows;
total.value = res.total;
loading.value = false;
}
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
}
/** 表单重置 */
const reset = () => {
form.value = {...initFormData};
erpProjectChangeProgressFormRef.value?.resetFields();
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: ErpProjectChangeProgressVO[]) => {
ids.value = selection.map(item => item.changeProgressId);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = "添加项目进度变更明细";
}
/** 修改按钮操作 */
const handleUpdate = async (row?: ErpProjectChangeProgressVO) => {
reset();
const _changeProgressId = row?.changeProgressId || ids.value[0]
const res = await getErpProjectChangeProgress(_changeProgressId);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = "修改项目进度变更明细";
}
/** 提交按钮 */
const submitForm = () => {
erpProjectChangeProgressFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.changeProgressId) {
await updateErpProjectChangeProgress(form.value).finally(() => buttonLoading.value = false);
} else {
await addErpProjectChangeProgress(form.value).finally(() => buttonLoading.value = false);
}
proxy?.$modal.msgSuccess("操作成功");
dialog.visible = false;
await getList();
}
});
}
/** 删除按钮操作 */
const handleDelete = async (row?: ErpProjectChangeProgressVO) => {
const _changeProgressIds = row?.changeProgressId || ids.value;
await proxy?.$modal.confirm('是否确认删除项目进度变更明细编号为"' + _changeProgressIds + '"的数据项?').finally(() => loading.value = false);
await delErpProjectChangeProgress(_changeProgressIds);
proxy?.$modal.msgSuccess("删除成功");
await getList();
}
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download('oa/erp/erpProjectChangeProgress/export', {
...queryParams.value
}, `erpProjectChangeProgress_${new Date().getTime()}.xlsx`)
}
onMounted(() => {
getList();
});
</script>

@ -0,0 +1,411 @@
<template>
<div class="p-2">
<el-card shadow="never">
<div class="mb-3 flex items-center justify-between">
<div class="flex flex-wrap items-center gap-2">
<el-switch
v-model="showActual"
active-text="显示实际进度"
inactive-text="隐藏实际进度"
/>
</div>
<div>
<el-button @click="goBack"></el-button>
</div>
</div>
<el-descriptions v-loading="loading" :column="3" border size="small" class="mb-3">
<el-descriptions-item label="项目名称">
{{ planDetail?.projectName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="项目经理">
{{ planDetail?.managerName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="部门负责人">
{{ planDetail?.chargeName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="项目计划状态">
{{ planStatusLabel }}
</el-descriptions-item>
<el-descriptions-item label="任务数量">
{{ taskList.length }}
</el-descriptions-item>
<el-descriptions-item label="最后同步时间">
{{ formatDisplayDate(new Date()) }}
</el-descriptions-item>
</el-descriptions>
<el-alert
title="任务数据来源于项目阶段,仅供查看。"
type="info"
show-icon
:closable="false"
class="mb-3"
/>
<el-divider content-position="left">项目甘特图</el-divider>
<div ref="chartContainerRef" class="gantt-chart" />
<el-empty v-if="!taskList.length" description="暂无任务数据" />
<el-divider content-position="left">任务列表</el-divider>
<el-table :data="taskList" border stripe max-height="420" size="small">
<el-table-column type="index" width="60" label="序号" align="center" />
<el-table-column prop="taskName" label="任务名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="phaseName" label="所属阶段" min-width="120" show-overflow-tooltip />
<el-table-column prop="ownerName" label="负责人" min-width="120" show-overflow-tooltip />
<el-table-column prop="planStart" label="计划开始" min-width="120" show-overflow-tooltip />
<el-table-column prop="planEnd" label="计划结束" min-width="120" show-overflow-tooltip />
<el-table-column prop="realStart" label="实际开始" min-width="120" show-overflow-tooltip />
<el-table-column prop="realEnd" label="实际结束" min-width="120" show-overflow-tooltip />
<el-table-column
prop="dependencyLabels"
label="依赖任务"
min-width="160"
show-overflow-tooltip
/>
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts" name="ErpProjectPlanGantt">
import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import * as echarts from 'echarts';
import { useResizeObserver } from '@vueuse/core';
import { getErpProjectPlan } from '@/api/oa/erp/erpProjectPlan';
import type { ErpProjectPlanVO } from '@/api/oa/erp/erpProjectPlan/types';
interface GanttTask {
taskId: string;
taskName: string;
phaseName?: string;
ownerName?: string;
planStart?: string;
planEnd?: string;
realStart?: string;
realEnd?: string;
dependencyLabels?: string;
remark?: string;
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const router = useRouter();
const { project_plan_status, project_phases } = toRefs<any>(
proxy?.useDict('project_plan_status', 'project_phases')
);
const loading = ref(false);
const showActual = ref(true);
const planDetail = ref<ErpProjectPlanVO | null>(null);
const taskList = ref<GanttTask[]>([]);
const chartContainerRef = ref<HTMLDivElement | null>(null);
const chartInstance = ref<echarts.ECharts | null>(null);
// 便
const phaseMap = computed<Record<string, string>>(() => {
const map: Record<string, string> = {};
(project_phases?.value || []).forEach((item: any) => {
map[item.value] = item.label;
});
return map;
});
const statusMap = computed<Record<string, string>>(() => {
const map: Record<string, string> = {};
(project_plan_status?.value || []).forEach((item: any) => {
map[item.value] = item.label;
});
return map;
});
const planStatusLabel = computed(() => {
if (!planDetail.value?.projectPlanStatus) {
return '-';
}
return statusMap.value[planDetail.value.projectPlanStatus] || planDetail.value.projectPlanStatus;
});
const projectPlanId = computed(() => route.params.projectPlanId as string);
const renderGanttItem = (params: any, api: any) => {
const categoryIndex = api.value(0);
const start = api.coord([api.value(1), categoryIndex]);
const end = api.coord([api.value(2), categoryIndex]);
const height = api.size([0, 1])[1] * 0.55;
const rectShape = echarts.graphic.clipRectByRect(
{
x: Math.min(start[0], end[0]),
y: start[1] - height / 2,
width: Math.abs(end[0] - start[0]),
height
},
{
x: params.coordSys.x,
y: params.coordSys.y,
width: params.coordSys.width,
height: params.coordSys.height
}
);
if (!rectShape) {
return null;
}
return {
type: 'rect',
transition: ['shape'],
shape: rectShape,
style: api.style()
};
};
const updateChart = () => {
if (!chartInstance.value) {
return;
}
if (!taskList.value.length) {
chartInstance.value.clear();
chartInstance.value.setOption({
tooltip: { trigger: 'axis' },
xAxis: { type: 'time' },
yAxis: { type: 'category', data: [] },
series: []
});
return;
}
const categories = taskList.value.map(task => task.taskName || '未命名任务');
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
const planData = taskList.value
.map((task, index) => {
const start = toTimestamp(task.planStart);
const end = toTimestamp(task.planEnd);
if (start === null || end === null || end < start) {
return null;
}
min = Math.min(min, start);
max = Math.max(max, end);
return {
name: task.taskName,
value: [index, start, end],
task
};
})
.filter(Boolean) as Array<{ name: string; value: [number, number, number]; task: GanttTask }>;
const actualData = showActual.value
? (taskList.value
.map((task, index) => {
const start = toTimestamp(task.realStart);
const end = toTimestamp(task.realEnd);
if (start === null || end === null || end < start) {
return null;
}
min = Math.min(min, start);
max = Math.max(max, end);
return {
name: `${task.taskName}-实际`,
value: [index, start, end],
task
};
})
.filter(Boolean) as Array<{ name: string; value: [number, number, number]; task: GanttTask }>)
: [];
if (min === Number.POSITIVE_INFINITY || max === Number.NEGATIVE_INFINITY) {
const now = Date.now();
min = now - 7 * 24 * 3600 * 1000;
max = now + 7 * 24 * 3600 * 1000;
}
const series: any[] = [];
if (planData.length) {
series.push({
name: '计划进度',
type: 'custom',
renderItem: renderGanttItem,
itemStyle: { color: '#91cc75', opacity: 0.9 },
encode: { x: [1, 2], y: 0 },
data: planData
});
}
if (actualData.length) {
series.push({
name: '实际进度',
type: 'custom',
renderItem: renderGanttItem,
itemStyle: { color: '#5470c6', opacity: 0.75 },
encode: { x: [1, 2], y: 0 },
data: actualData
});
}
chartInstance.value.setOption(
{
tooltip: {
trigger: 'item',
formatter: (params: any) => {
const task: GanttTask | undefined = params.data?.task;
if (!task) {
return params.name;
}
const planText = `计划:${task.planStart || '-'}${task.planEnd || '-'}`;
const actualText = `实际:${task.realStart || '-'}${task.realEnd || '-'}`;
return [params.marker + (task.taskName || '未命名任务'), planText, actualText].join('<br/>');
}
},
legend: {
data: series.map(s => s.name),
top: 0
},
grid: {
left: 80,
right: 40,
top: 60,
bottom: 60
},
xAxis: {
type: 'time',
min: min - 24 * 3600 * 1000,
max: max + 24 * 3600 * 1000,
axisLabel: {
formatter: (value: number) => formatDisplayDate(new Date(value))
}
},
yAxis: {
type: 'category',
data: categories,
axisTick: { alignWithLabel: true }
},
dataZoom: [
{ type: 'inside', xAxisIndex: 0, filterMode: 'weakFilter' },
{ type: 'slider', xAxisIndex: 0, filterMode: 'weakFilter' }
],
series
},
true
);
};
watch(
() => taskList.value,
() => {
updateChart();
}
);
watch(showActual, () => {
updateChart();
});
const buildTaskListFromStages = (stages: any[] = []): GanttTask[] =>
stages.map(stage => ({
taskId: String(stage.planStageId ?? generateTaskId()),
taskName:
phaseMap.value[stage.projectPhases as string] ||
stage.projectPhases ||
stage.scheduleRemark ||
'未命名阶段',
phaseName: phaseMap.value[stage.projectPhases as string] || '-',
ownerName: planDetail.value?.managerName,
planStart: normalizeDate(stage.planStartTime),
planEnd: normalizeDate(stage.planEndTime),
realStart: normalizeDate(stage.realStartTime),
realEnd: normalizeDate(stage.realEndTime),
dependencyLabels: Array.isArray(stage.dependencyNames)
? stage.dependencyNames.join('、')
: stage.dependencyNames || '-',
remark: stage.scheduleRemark || '-'
}));
const loadPlanDetail = async () => {
if (!projectPlanId.value || projectPlanId.value === '0') {
proxy?.$modal.msgWarning('请先保存项目计划,再查看甘特图');
return;
}
loading.value = true;
try {
const res = await getErpProjectPlan(projectPlanId.value);
planDetail.value = res.data;
await nextTick();
taskList.value = buildTaskListFromStages(res.data.planStageList || []);
updateChart();
} catch (error) {
proxy?.$modal.msgError('获取项目计划失败');
} finally {
loading.value = false;
}
};
const generateTaskId = () => {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
return `task_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
};
const goBack = () => {
router.back();
};
const normalizeDate = (value?: string | null) => {
if (!value) {
return undefined;
}
const trimmed = value.trim();
if (!trimmed) {
return undefined;
}
return trimmed.length > 10 ? trimmed.slice(0, 10) : trimmed;
};
const toTimestamp = (value?: string) => {
const dateText = normalizeDate(value);
if (!dateText) {
return null;
}
const time = new Date(`${dateText}T00:00:00`).getTime();
if (Number.isNaN(time)) {
return null;
}
return time;
};
const formatDisplayDate = (date: Date) => {
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
return '-';
}
const year = date.getFullYear();
const month = `${date.getMonth() + 1}`.padStart(2, '0');
const day = `${date.getDate()}`.padStart(2, '0');
return `${year}-${month}-${day}`;
};
onMounted(async () => {
if (chartContainerRef.value) {
chartInstance.value = echarts.init(chartContainerRef.value);
}
if (chartContainerRef.value) {
useResizeObserver(chartContainerRef, () => {
chartInstance.value?.resize();
});
}
await loadPlanDetail();
updateChart();
});
onBeforeUnmount(() => {
chartInstance.value?.dispose();
});
</script>
<style scoped>
.gantt-chart {
width: 100%;
min-height: 420px;
}
</style>
Loading…
Cancel
Save