Merge branch 'master' into feature/20261103-bfi-warehouse-web

# Conflicts:
#	src/option/settlement/formalSettlement.js
This commit is contained in:
刘泉佳
2026-11-04 17:18:51 +08:00
35 changed files with 2022 additions and 665 deletions
+196 -155
View File
@@ -258,15 +258,21 @@
<div class="project-apply-form__field-tooltip">{{ businessDeptTooltipText }}</div>
</template>
<span class="project-apply-form__field-tooltip-trigger">
<el-cascader
v-model="businessDeptCascaderValue"
:options="businessDeptTreeOptions"
:props="deptCascaderProps"
<el-select
v-model="form.businessDeptId"
placeholder="请选择"
filterable
clearable
:disabled="dialogReadonly"
/>
@change="handleBusinessDeptChange"
>
<el-option
v-for="item in deptOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</span>
</el-tooltip>
</el-form-item>
@@ -755,6 +761,96 @@
/>
</div>
<template v-if="showProjectContractSection">
<div class="dialog-section-title project-apply-form__table-title">项目合同</div>
<el-table
v-loading="projectContractLoading"
:data="projectContractRows"
border
class="project-apply-form__table"
>
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column
prop="contractCategory"
label="合同类型"
min-width="120"
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="contractNo"
label="合同编号"
min-width="140"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-link
v-if="row.id && row.contractNo"
type="primary"
@click.stop="openProjectContractDetail(row)"
>
{{ row.contractNo }}
</el-link>
<span v-else>{{ row.contractNo || '' }}</span>
</template>
</el-table-column>
<el-table-column
prop="contractName"
label="合同名称"
min-width="160"
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="partyA"
label="甲方"
min-width="140"
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="partyB"
label="乙方"
min-width="140"
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="partyC"
label="丙方"
min-width="140"
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="startDate"
label="开始日期"
min-width="120"
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="endDate"
label="结束日期"
min-width="120"
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="contractAmount"
label="合同金额"
min-width="120"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
{{ formatAmount(row.contractAmount) }}
</template>
</el-table-column>
</el-table>
</template>
<div
v-if="!isChangeDialog && !isNewProjectDialog"
class="dialog-section-title project-apply-form__table-title"
@@ -984,6 +1080,7 @@
import { exportBlob } from '@/api/common';
import { defineAsyncComponent } from 'vue';
import * as api from '@/api/business/project-apply';
import { getList as getContractManageList } from '@/api/business/contract-manage';
import {
getRisk,
loadHcsImmediateEnabled,
@@ -996,7 +1093,7 @@ import {
getList as getCustomerArchiveList,
getDetail as getCustomerArchiveDetail,
} from '@/api/vehicle/customer-archive';
import { getBusinessDeptSelect, getDeptTree, getPlatformCompanySelect } from '@/api/system/dept';
import { getBusinessDeptSelect, getPlatformCompanySelect } from '@/api/system/dept';
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
import { getList as getUserList } from '@/api/system/user';
@@ -1340,7 +1437,6 @@ export default {
],
},
deptOptions: [],
businessDeptTreeOptions: [],
platformCompanyOptions: [],
cargoTypeOptions: [],
selectedCustomerId: [],
@@ -1383,6 +1479,8 @@ export default {
'zip',
],
changeRows: [],
projectContractRows: [],
projectContractLoading: false,
changeRecordDetailVisible: false,
changeRecordDetail: null,
changeRecordDetailRows: [],
@@ -1405,44 +1503,14 @@ export default {
},
computed: {
...mapGetters(['permission', 'userInfo']),
// 部门级联配置:checkStrictly 允许选中任意层级,不必选到末级
deptCascaderProps() {
return {
label: 'label',
value: 'value',
children: 'children',
emitPath: true,
checkStrictly: true,
expandTrigger: 'click',
};
},
// 业务部门:form 存部门 id,级联控件用根到节点的 id 路径,两者在此双向转换
businessDeptCascaderValue: {
get() {
const id = this.form.businessDeptId;
return id ? this.findDeptPath(id, this.businessDeptTreeOptions) || [id] : [];
},
set(path) {
const value = Array.isArray(path)
? path.filter(item => item !== undefined && item !== null && item !== '')
: [];
this.form.businessDeptId = value.length ? value.at(-1) : '';
this.handleBusinessDeptChange(this.form.businessDeptId);
},
},
// 查看态禁用控件无 hover,用完整路径/名称做 tooltip
// 查看态禁用控件无 hover,用业务部门名称做 tooltip
businessDeptTooltipText() {
const pathIds = this.businessDeptCascaderValue || [];
if (pathIds.length && this.businessDeptTreeOptions?.length) {
const labels = [];
let nodes = this.businessDeptTreeOptions;
for (const id of pathIds) {
const node = (nodes || []).find(item => String(item.value) === String(id));
if (!node) break;
labels.push(node.label);
nodes = node.children;
const id = this.form.businessDeptId;
if (id) {
const matched = this.deptOptions.find(item => String(item.value) === String(id));
if (matched?.rawLabel || matched?.label) {
return matched.rawLabel || matched.label;
}
if (labels.length) return labels.join(' / ');
}
return String(this.form.businessDeptName || '').trim();
},
@@ -1489,6 +1557,9 @@ export default {
isNewProjectDialog() {
return ['add', 'majorSupplement'].includes(this.dialogType);
},
showProjectContractSection() {
return this.dialogReadonly && !this.isChangeDialog && !this.isNewProjectDialog && !this.isPublicViewPage;
},
isProjectFormPage() {
return this.isFormPageInstance;
},
@@ -1674,9 +1745,10 @@ export default {
detail.settlementMode
);
if (detail.businessDeptId) {
const label = detail.businessDeptName || String(detail.businessDeptId);
this.businessDeptTreeOptions = [{ label, value: detail.businessDeptId }];
this.deptOptions = [{ label, rawLabel: label, value: detail.businessDeptId }];
const id = String(detail.businessDeptId);
const label = detail.businessDeptName || id;
this.form.businessDeptId = id;
this.deptOptions = [{ label, rawLabel: label, value: id }];
}
const undertakeDeptIds = this.parseUndertakeDeptIds(detail.undertakeDeptId);
const undertakeDeptNames = this.parseUndertakeDeptNames(detail.undertakeDeptName);
@@ -2013,15 +2085,20 @@ export default {
const run = value => {
const args = operation.prompt ? [row.id, value] : [row.id];
const request = () => this.api[operation.action](...args);
// 先本地提交再调 MK:MK 成功后会把状态同步成 reviewing,
// 若先 MK 再本地,已撤回/已驳回单据会被「当前状态不允许编辑」拦住。
const afterMk =
operation.action === 'submitApproval'
? submitMkApprovalFlow({
bizType: 'project-apply',
bizId: row.id,
subjectName: row.projectName || '',
approvalStatus: row.approvalStatus || '',
mkEnabled: this.mkEnabled,
}).then(() => request())
? request().then(() =>
submitMkApprovalFlow({
bizType: 'project-apply',
bizId: row.id,
subjectName: row.projectName || '',
approvalStatus: row.approvalStatus || '',
mkEnabled: this.mkEnabled,
tempVarData: { projectType: row.projectType || '' },
})
)
: request();
// 项目通过会同步等待合规推送,加遮罩防重复点击
const loading =
@@ -2131,6 +2208,8 @@ export default {
this.attachmentRows = [];
this.selectedAttachmentRows = [];
this.changeRows = [];
this.projectContractRows = [];
this.projectContractLoading = false;
this.selectedCustomerId = [];
this.selectedCarrierIds = [];
},
@@ -2179,6 +2258,40 @@ export default {
this.syncSelectedCustomerOptions();
this.ensureCurrentPlatformOption();
this.ensureCurrentBusinessDeptOption();
this.loadProjectContracts(row?.id);
},
loadProjectContracts(projectId) {
if (!this.showProjectContractSection || !projectId) {
this.projectContractRows = [];
this.projectContractLoading = false;
return;
}
this.projectContractLoading = true;
getContractManageList(1, 1000, { projectId })
.then(res => {
const data = res.data?.data;
const records = Array.isArray(data) ? data : data?.records || [];
this.projectContractRows = records.filter(
item => String(item.projectId) === String(projectId)
);
})
.catch(() => {
this.projectContractRows = [];
this.$message.error('项目合同加载失败');
})
.finally(() => {
this.projectContractLoading = false;
});
},
openProjectContractDetail(row) {
if (!row?.id) {
this.$message.warning('合同信息不完整,无法跳转');
return;
}
this.$router.push({
path: '/business/contract-manage/detail',
query: { id: row.id, name: '合同详情' },
});
},
fillDefaultUsers() {
const userId = this.userInfo?.userId || this.userInfo?.id || '';
@@ -2265,6 +2378,9 @@ export default {
subjectName: name,
approvalStatus: status,
mkEnabled: this.mkEnabled,
tempVarData: {
projectType: this.form.projectType || data.projectType || '',
},
}).then(() => {
this.$message.success('提交成功!');
this.closeProjectForm();
@@ -2385,6 +2501,7 @@ export default {
subjectName: this.form.projectName || '',
approvalStatus,
mkEnabled: this.mkEnabled,
tempVarData: { projectType: this.form.projectType || '' },
}).then(() => {
this.$message.success('提交成功!');
this.closeProjectForm();
@@ -2410,6 +2527,7 @@ export default {
subjectName: this.form.projectName || '',
approvalStatus: this.form.approvalStatus || 'change_rejected',
mkEnabled: this.mkEnabled,
tempVarData: { projectType: this.form.projectType || '' },
}).then(() => {
this.$message.success('提交成功!');
this.closeProjectForm();
@@ -2813,10 +2931,10 @@ export default {
return `${base}-BG${this.$dayjs().format('YYYYMMDD')}`;
},
handleBusinessDeptChange(value) {
// 兼容级联路径数组(取末级)与单个部门 id
const id = Array.isArray(value) ? value.at(-1) : value;
const dept = this.deptOptions.find(item => String(item.value) === String(id));
this.form.businessDeptName = dept ? dept.rawLabel : '';
const id = value == null || value === '' ? '' : String(value);
this.form.businessDeptId = id;
const dept = this.deptOptions.find(item => String(item.value) === id);
this.form.businessDeptName = dept ? dept.rawLabel || dept.label : '';
this.$refs.projectForm?.validateField('businessDeptId');
},
handleUndertakeDeptChange(value) {
@@ -2854,77 +2972,44 @@ export default {
firstUndertakeDeptId(value) {
return this.parseUndertakeDeptIds(value)[0] || '';
},
// 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null)
findDeptPath(id, tree = [], parents = []) {
const target = Array.isArray(id) ? id.at(-1) : id;
if (target === undefined || target === null || target === '') return null;
for (const node of tree || []) {
const path = [...parents, node.value];
if (String(node.value) === String(target)) return path;
const matched = this.findDeptPath(target, node.children, path);
if (matched) return matched;
}
return null;
},
handleProjectTypeChange() {
this.normalizeAttachmentFileTypes();
this.$refs.projectForm?.validateField('projectType');
},
loadDeptOptions() {
Promise.all([getDeptTree(), getBusinessDeptSelect()])
.then(([treeRes, bizRes]) => {
const deptTree = this.excludeExternalOrganization(treeRes.data.data || []);
const bizList = bizRes.data.data || [];
const bizIds = new Set(
bizList
.map(item => (item.id == null || item.id === '' ? '' : String(item.id)))
.filter(Boolean)
);
// 仅保留「是否业务部门=是」的节点及其祖先,祖先不可选
const businessDeptTree = this.filterBusinessDeptTree(deptTree, bizIds);
this.deptOptions = this.flattenDept(businessDeptTree).filter(item =>
bizIds.has(String(item.value))
);
this.businessDeptTreeOptions = this.toDeptCascaderOptions(businessDeptTree, bizIds);
getBusinessDeptSelect()
.then(res => {
const bizList = res.data.data || [];
// 仅展示「是否业务部门=是」的本级部门,扁平下拉
this.deptOptions = bizList
.map(item => {
const value = item.id == null || item.id === '' ? '' : String(item.id);
const label = item.deptName || item.fullName || item.title || item.name || value;
return {
label,
rawLabel: label,
value,
};
})
.filter(item => item.value);
this.fillDefaultBusinessDept();
this.ensureCurrentBusinessDeptOption();
})
.catch(() => {
this.deptOptions = [];
this.businessDeptTreeOptions = [];
});
this.loadPlatformCompanyOptions();
},
/**
* 剪枝组织树:只保留业务部门节点及其祖先路径。
*/
filterBusinessDeptTree(list = [], bizIds = new Set()) {
return (list || []).reduce((result, item) => {
const children = item.children?.length
? this.filterBusinessDeptTree(item.children, bizIds)
: [];
const isBiz = bizIds.has(String(item.id));
if (!isBiz && !children.length) {
return result;
}
result.push({
...item,
children: children.length ? children : undefined,
});
return result;
}, []);
},
ensureCurrentBusinessDeptOption() {
const id = this.form.businessDeptId;
const id = this.form.businessDeptId == null || this.form.businessDeptId === ''
? ''
: String(this.form.businessDeptId);
if (!id) return;
const exists = this.deptOptions.some(item => String(item.value) === String(id));
this.form.businessDeptId = id;
const exists = this.deptOptions.some(item => String(item.value) === id);
if (exists) return;
const label = this.form.businessDeptName || String(id);
const label = this.form.businessDeptName || id;
this.deptOptions.unshift({ label, rawLabel: label, value: id });
const pathExists = this.findDeptPath(id, this.businessDeptTreeOptions);
if (!pathExists) {
this.businessDeptTreeOptions.unshift({ label, value: id });
}
},
loadPlatformCompanyOptions() {
getPlatformCompanySelect().then(res => {
@@ -2956,50 +3041,6 @@ export default {
return matched ? matched.value : id;
});
},
excludeExternalOrganization(list = []) {
return list.reduce((result, item) => {
const deptName = item.title || item.deptName || item.name;
if (String(deptName || '').trim() === '外部组织') return result;
result.push({
...item,
children: item.children?.length
? this.excludeExternalOrganization(item.children)
: item.children,
});
return result;
}, []);
},
toDeptCascaderOptions(list = [], bizIds = null) {
return list.map(item => {
const value = item.id == null || item.id === '' ? item.value : item.id;
const option = {
label: item.title || item.deptName || item.name || item.label,
value: value == null || value === '' ? '' : String(value),
children: item.children?.length
? this.toDeptCascaderOptions(item.children, bizIds)
: undefined,
};
// 仅业务部门可选;祖先节点保留路径但禁用
if (bizIds && !bizIds.has(String(option.value))) {
option.disabled = true;
}
return option;
});
},
flattenDept(list = [], level = 0) {
return list.reduce((result, item) => {
const label = item.title || item.deptName || item.name;
result.push({
label: `${' '.repeat(level)}${label}`,
rawLabel: label,
value: item.id,
});
if (item.children && item.children.length) {
result.push(...this.flattenDept(item.children, level + 1));
}
return result;
}, []);
},
loadCargoTypeOptions() {
getSystemDictionary({ code: 'type_of_goods' }).then(res => {
this.cargoTypeOptions = (res.data.data || []).map(item => ({