From eaf431819ef6e5bd2429ed16d62ac52dbbfbbddf Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 2 Nov 2026 13:31:33 +0800 Subject: [PATCH 1/4] =?UTF-8?q?:sparkles:=20=E5=AE=A2=E5=95=86=E6=A1=A3?= =?UTF-8?q?=E6=A1=88=E8=90=A5=E4=B8=9A=E6=89=A7=E7=85=A7OCR=E6=94=B9?= =?UTF-8?q?=E6=8E=A5=E6=96=B0=E6=8E=A5=E5=8F=A3=E5=B9=B6=E5=9B=9E=E5=A1=AB?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/api/vehicle/customer-archive.js | 3 +- src/views/vehicle/customer-archive.vue | 57 +++++++++++++++++++++----- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/api/vehicle/customer-archive.js b/src/api/vehicle/customer-archive.js index 0814c2a..1ccf902 100644 --- a/src/api/vehicle/customer-archive.js +++ b/src/api/vehicle/customer-archive.js @@ -176,12 +176,11 @@ export const syncKingdee = id => { export const recognizeBusinessLicenseOcr = imageUrl => { return request({ - url: '/blade-transport/baidu-ocr/recognize-url', + url: '/blade-transport/baidu-ocr/business-license', method: 'post', timeout: 60000, params: { imageUrl, - type: 'business_license', }, }); }; diff --git a/src/views/vehicle/customer-archive.vue b/src/views/vehicle/customer-archive.vue index 8f02324..1eddbc7 100644 --- a/src/views/vehicle/customer-archive.vue +++ b/src/views/vehicle/customer-archive.vue @@ -3938,7 +3938,7 @@ export default { }); recognizeBusinessLicenseOcr(url) .then(res => { - this.applyBusinessLicenseRecognition(res?.data?.data?.result || {}); + this.applyBusinessLicenseRecognition(res?.data?.data || {}); this.$message.success('营业执照识别完成'); }) .catch(() => { @@ -3946,8 +3946,31 @@ export default { }) .finally(() => loading.close()); }, + normalizeBusinessLicenseOcrFields(result = {}) { + if (Array.isArray(result)) { + return Object.fromEntries( + result + .filter(item => item && item.key) + .map(item => [String(item.key).trim(), String(item.value ?? '').trim()]) + .filter(([, value]) => value) + ); + } + if (result.fields && typeof result.fields === 'object' && !Array.isArray(result.fields)) { + return result.fields; + } + if (Array.isArray(result.data)) { + return this.normalizeBusinessLicenseOcrFields(result.data); + } + return result; + }, getBusinessLicenseOcrValue(result, labels) { - const words = result?.words_result || result?.wordsResult || result || {}; + const fields = this.normalizeBusinessLicenseOcrFields(result); + for (const label of labels) { + const value = fields[label]; + const text = value && typeof value === 'object' ? value.words : value; + if (text !== undefined && text !== null && String(text).trim()) return String(text).trim(); + } + const words = result?.words_result || result?.wordsResult || {}; for (const label of labels) { const value = words[label]; const text = value && typeof value === 'object' ? value.words : value; @@ -3959,23 +3982,35 @@ export default { return String(value || '').replace(/\s/g, '') === '无'; }, applyBusinessLicenseRecognition(result = {}) { - const code = this.getBusinessLicenseOcrValue(result, [ + const fields = this.normalizeBusinessLicenseOcrFields(result); + const code = this.getBusinessLicenseOcrValue(fields, [ + 'uscc', '统一社会信用代码', '社会信用代码', '注册号', ]); - const name = this.getBusinessLicenseOcrValue(result, ['单位名称', '企业名称', '名称']); - const legalPerson = this.getBusinessLicenseOcrValue(result, ['法定代表人', '法人']); - const address = this.getBusinessLicenseOcrValue(result, ['地址', '住所']); - const capital = this.getBusinessLicenseOcrValue(result, ['注册资本']); - const paidInCapital = this.getBusinessLicenseOcrValue(result, ['实缴资本', '实收资本']); - const term = this.getBusinessLicenseOcrValue(result, ['营业期限', '有效期']); - const termStartDate = this.getBusinessLicenseOcrValue(result, [ + const name = this.getBusinessLicenseOcrValue(fields, [ + 'company_name', + '单位名称', + '企业名称', + '名称', + ]); + const legalPerson = this.getBusinessLicenseOcrValue(fields, [ + 'legal_name', + '法定代表人', + '法人', + ]); + const address = this.getBusinessLicenseOcrValue(fields, ['add', '地址', '住所']); + const capital = this.getBusinessLicenseOcrValue(fields, ['capital', '注册资本']); + const paidInCapital = this.getBusinessLicenseOcrValue(fields, ['实缴资本', '实收资本']); + const term = this.getBusinessLicenseOcrValue(fields, ['period', '营业期限', '有效期']); + const termStartDate = this.getBusinessLicenseOcrValue(fields, [ + 'establishment_date', '有效期起始日期', '成立日期', '核准日期', ]); - const scope = this.getBusinessLicenseOcrValue(result, ['经营范围']); + const scope = this.getBusinessLicenseOcrValue(fields, ['经营范围']); if (code) this.archiveForm.unifiedCreditCode = code.replace(/\s/g, '').toUpperCase(); if (name) this.archiveForm.fullName = name; if (name) this.checkArchiveNameRisk(); From 9fed3785d995c6a961fa7213395b018c11b62c01 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 2 Nov 2026 13:32:06 +0800 Subject: [PATCH 2/4] =?UTF-8?q?:zap:=20=E6=B5=81=E7=A8=8B=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8F=96=E6=B6=88=E9=BB=98=E8=AE=A4=E6=8C=89=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E7=94=A8=E6=88=B7=E9=83=A8=E9=97=A8=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/views/business/process-config.vue | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/views/business/process-config.vue b/src/views/business/process-config.vue index d7ba2b1..2be992d 100644 --- a/src/views/business/process-config.vue +++ b/src/views/business/process-config.vue @@ -535,7 +535,6 @@ export default { created() { this.resetNodeRows(); this.loadProjectOptions(''); - this.applyDefaultDeptSearch(); this.initDeptOptions(); }, methods: { @@ -566,27 +565,6 @@ export default { } }); }, - defaultDeptId() { - const raw = (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || ''; - return ( - String(raw) - .split(',') - .map(item => item.trim()) - .filter(Boolean)[0] || '' - ); - }, - applyDefaultDeptSearch() { - const deptId = this.defaultDeptId(); - const deptColumn = this.findColumn(this.option.column, 'searchDeptId'); - if (!deptId || !deptColumn) return; - deptColumn.searchValue = deptId; - this.query = { ...this.query, deptId }; - this.$nextTick(() => { - if (this.$refs.crud?.searchForm) { - this.$refs.crud.searchForm.searchDeptId = deptId; - } - }); - }, flattenDept(tree, level = 0) { return tree.flatMap(item => { const id = @@ -1113,7 +1091,6 @@ export default { }, searchReset() { this.query = {}; - this.applyDefaultDeptSearch(); this.page.currentPage = 1; this.onLoad(this.page, this.query); }, From 8e1430491bcdf4e18bbbf11ceb7b9b991e27d2e7 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 2 Nov 2026 14:28:07 +0800 Subject: [PATCH 3/4] =?UTF-8?q?:sparkles:=20=E9=A1=B9=E7=9B=AE=E6=8F=90?= =?UTF-8?q?=E4=BA=A4MK=E4=BC=A0projectType=EF=BC=8C=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=B7=B2=E6=92=A4=E5=9B=9E=E5=86=8D=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/api/system/business-process.js | 1 + src/utils/mk-approval.js | 10 ++++++++-- src/views/business/project-apply.vue | 24 +++++++++++++++++------- src/views/business/project-change.vue | 18 +++++++++++------- 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/api/system/business-process.js b/src/api/system/business-process.js index 0ea48d5..5f78bf6 100644 --- a/src/api/system/business-process.js +++ b/src/api/system/business-process.js @@ -12,6 +12,7 @@ export const MK_SUBMIT_TIMEOUT = 30000; * @param {string} data.bizId 业务主键(后端生成/复用 formInstanceId) * @param {string} [data.subject] 流程标题 * @param {string} [data.bizType] 业务类型 + * @param {Object} [data.tempVarData] MK 临时变量(如 { projectType }) */ export const processSubmit = data => { return request({ diff --git a/src/utils/mk-approval.js b/src/utils/mk-approval.js index ee1b0ff..475dbf0 100644 --- a/src/utils/mk-approval.js +++ b/src/utils/mk-approval.js @@ -154,6 +154,7 @@ export function isMkApprovedStatus(bizType, approvalStatus) { * @param {string} [options.subjectName] 标题后缀 * @param {string} [options.approvalStatus] 提交前的审批状态 * @param {boolean} [options.mkEnabled] 调用方已知开关时传入,false 时立即跳过 + * @param {Object} [options.tempVarData] 传给 MK 的临时变量(如项目类型 projectType) * @returns {Promise} true=已发起/复用 MK 流程,false=未开启或已跳过 */ export async function submitMkApprovalFlow({ @@ -162,6 +163,7 @@ export async function submitMkApprovalFlow({ subjectName = '', approvalStatus = '', mkEnabled, + tempVarData, } = {}) { const conf = MK_BIZ[bizType]; if (!conf) { @@ -202,12 +204,16 @@ export async function submitMkApprovalFlow({ const subject = subjectName ? `${conf.subjectPrefix}:${subjectName}` : `${conf.subjectPrefix}:${bizIdText}`; - await processSubmit({ + const submitPayload = { templateCode, bizId: bizIdText, subject, bizType, forceNewFormInstance, - }); + }; + if (tempVarData != null && typeof tempVarData === 'object') { + submitPayload.tempVarData = tempVarData; + } + await processSubmit(submitPayload); return true; } diff --git a/src/views/business/project-apply.vue b/src/views/business/project-apply.vue index 0242715..19d4af2 100644 --- a/src/views/business/project-apply.vue +++ b/src/views/business/project-apply.vue @@ -2012,15 +2012,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 = @@ -2264,6 +2269,9 @@ export default { subjectName: name, approvalStatus: status, mkEnabled: this.mkEnabled, + tempVarData: { + projectType: this.form.projectType || data.projectType || '', + }, }).then(() => { this.$message.success('提交成功!'); this.closeProjectForm(); @@ -2384,6 +2392,7 @@ export default { subjectName: this.form.projectName || '', approvalStatus, mkEnabled: this.mkEnabled, + tempVarData: { projectType: this.form.projectType || '' }, }).then(() => { this.$message.success('提交成功!'); this.closeProjectForm(); @@ -2409,6 +2418,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(); diff --git a/src/views/business/project-change.vue b/src/views/business/project-change.vue index 7fa0031..ca20b94 100644 --- a/src/views/business/project-change.vue +++ b/src/views/business/project-change.vue @@ -214,15 +214,19 @@ export default { } const run = () => { const request = () => this.api[operation.action](row.id); + // 先本地提交再调 MK,避免 MK 回写 reviewing 后本地 submitApproval 被编辑校验拦住。 const afterMk = operation.action === 'submitApproval' - ? submitMkApprovalFlow({ - bizType: 'project-change', - bizId: row.id, - subjectName: row.projectName || '', - approvalStatus: row.approvalStatus || '', - mkEnabled: this.mkEnabled, - }).then(() => request()) + ? request().then(() => + submitMkApprovalFlow({ + bizType: 'project-change', + bizId: row.id, + subjectName: row.projectName || '', + approvalStatus: row.approvalStatus || '', + mkEnabled: this.mkEnabled, + tempVarData: { projectType: row.projectType || '' }, + }) + ) : request(); const loading = operation.action === 'approve' From 8388a55481046bc369077cdba87af8500aa48fb1 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 2 Nov 2026 21:10:47 +0800 Subject: [PATCH 4/4] =?UTF-8?q?:sparkles:=20=E6=96=B0=E5=A2=9E=E4=B8=9A?= =?UTF-8?q?=E5=8A=A1=E9=83=A8=E9=97=A8=E9=85=8D=E7=BD=AE=E9=A1=B5=E5=B9=B6?= =?UTF-8?q?=E7=AE=80=E5=8C=96=E9=A1=B9=E7=9B=AE/=E5=90=88=E5=90=8C?= =?UTF-8?q?=E9=83=A8=E9=97=A8=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/api/business/contract-manage.js | 6 +- src/api/system/business-dept-config.js | 42 ++ src/option/business/contract-manage.js | 32 +- src/views/business/contract-manage.vue | 651 +++++++++++++++++++--- src/views/business/project-apply.vue | 195 ++----- src/views/system/business-dept-config.vue | 407 ++++++++++++++ src/views/system/dept.vue | 184 ++---- 7 files changed, 1145 insertions(+), 372 deletions(-) create mode 100644 src/api/system/business-dept-config.js create mode 100644 src/views/system/business-dept-config.vue diff --git a/src/api/business/contract-manage.js b/src/api/business/contract-manage.js index b7ce897..10f560c 100644 --- a/src/api/business/contract-manage.js +++ b/src/api/business/contract-manage.js @@ -93,10 +93,10 @@ export const expireStats = params => params, }); -/** 按合同负责部门预览下一合同编号 */ -export const nextContractNo = organizationId => +/** 按平台公司 + 业务部门预览下一合同编号 */ +export const nextContractNo = (organizationId, platformCompanyId) => request({ url: `${baseUrl}/next-contract-no`, method: 'get', - params: { organizationId }, + params: { organizationId, platformCompanyId }, }); diff --git a/src/api/system/business-dept-config.js b/src/api/system/business-dept-config.js new file mode 100644 index 0000000..e70fc9a --- /dev/null +++ b/src/api/system/business-dept-config.js @@ -0,0 +1,42 @@ +import request from '@/axios'; + +const baseUrl = '/blade-system/business-dept-config'; + +export const getList = (current, size, params) => + request({ + url: `${baseUrl}/list`, + method: 'get', + params: { + ...params, + current, + size, + }, + }); + +export const getDetail = id => + request({ + url: `${baseUrl}/detail`, + method: 'get', + params: { id }, + }); + +export const submit = row => + request({ + url: `${baseUrl}/submit`, + method: 'post', + data: row, + }); + +export const remove = ids => + request({ + url: `${baseUrl}/remove`, + method: 'post', + params: { ids }, + }); + +export const getByPlatformDept = (platformCompanyId, businessDeptId) => + request({ + url: `${baseUrl}/by-platform-dept`, + method: 'get', + params: { platformCompanyId, businessDeptId }, + }); diff --git a/src/option/business/contract-manage.js b/src/option/business/contract-manage.js index 9efc345..04a1ab3 100644 --- a/src/option/business/contract-manage.js +++ b/src/option/business/contract-manage.js @@ -65,10 +65,12 @@ export const config = { ['contractName', '合同名称'], ['projectName', '所属项目'], ['organizationName', '合同负责部门'], + ['platformCompanyName', '平台公司'], ['contractCategory', '合同类别'], ['signType', '签约类型'], ['partyA', '甲方'], ['partyB', '乙方'], + ['partyC', '丙方'], ['handlerUserName', '经办人'], ['signDate', '签订日期'], ['applyDate', '申请日期'], @@ -311,6 +313,13 @@ export const option = createCrudOption([ minWidth: 150, rules: selectRule('合同负责部门'), }, + { + label: '平台公司', + prop: 'platformCompanyName', + minWidth: 180, + order: 115, + display: false, + }, { label: '合同类别', prop: 'contractCategory', @@ -350,24 +359,23 @@ export const option = createCrudOption([ { label: '甲方', prop: 'partyA', - type: 'select', - formslot: true, - disabled: true, - order: 160, - filterable: true, minWidth: 160, - rules: selectRule('甲方'), + addDisplay: false, + editDisplay: false, }, { label: '乙方', prop: 'partyB', - type: 'select', - formslot: true, - disabled: true, - order: 150, - filterable: true, minWidth: 160, - rules: selectRule('乙方'), + addDisplay: false, + editDisplay: false, + }, + { + label: '丙方', + prop: 'partyC', + minWidth: 160, + addDisplay: false, + editDisplay: false, }, { label: '合同期限', diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index 709f8c5..b98b545 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -155,10 +155,14 @@ }} {{ detailValue('partyA') }} {{ detailValue('partyB') }} + {{ detailValue('partyC') || '-' }} {{ detailContractPeriod }} {{ detailValue('organizationName') }} + {{ + detailValue('platformCompanyName') + }} {{ detailValue('handlerUserName') }} @@ -424,7 +428,9 @@ maxlength="100" clearable :placeholder=" - formMode === 'add' ? '不填写则按部门规则自动生成' : '请输入合同编号' + formMode === 'add' + ? '不填写则按照部门规则自动生成' + : '请输入合同编号' " @input="handleContractNoInput" /> @@ -466,49 +472,37 @@ - - - - + @@ -664,6 +658,81 @@ +
+
+
{{ party.label }}
+ 添加 +
+ + + + + + + + + + + + + + + + + + +
+ - - + + + + + @@ -1003,6 +1077,7 @@ :value="billingPlanForm" :index="billingPlanIndex" :settlement-currency="form.settlementCurrency" + :party-options="billingPartyOptions" @save="saveBillingPlan" /> @@ -1104,8 +1179,11 @@ import { import { Position } from '@element-plus/icons-vue'; import { getMkPublicDetail } from '@/api/mk-process'; import { getList as getProjectList } from '@/api/business/project-apply'; -import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive'; -import { getDeptTree } from '@/api/system/dept'; +import { + getDetail as getCustomerArchiveDetail, + getList as getCustomerArchiveList, +} from '@/api/vehicle/customer-archive'; +import { getBusinessDeptSelect, getDeptTree } from '@/api/system/dept'; import { getDictionary as getSystemDictionary } from '@/api/system/dict'; import { getDictionary as getBizDictionary } from '@/api/system/dictbiz'; import { config, option } from '@/option/business/contract-manage'; @@ -1195,10 +1273,16 @@ const defaultForm = () => ({ projectName: '', organizationId: '', organizationName: '', + platformCompanyId: '', + platformCompanyName: '', signType: '', settlementMode: '', partyA: '', partyB: '', + partyC: '', + partyAJson: '', + partyBJson: '', + partyCJson: '', handlerUserName: '', signDate: '', applyDate: '', @@ -1243,6 +1327,9 @@ const createCopiedDetail = (detail = {}) => { contractFileJson: stripSpecAttachments(detail.contractFileJson), attachmentsJson: stripSpecAttachments(detail.attachmentsJson), billingPlanJson: detail.billingPlanJson || '', + partyAJson: detail.partyAJson || '', + partyBJson: detail.partyBJson || '', + partyCJson: detail.partyCJson || '', paymentRatioJson: detail.paymentRatioJson || '', settlementRuleJson: detail.settlementRuleJson || '', preSettlementConfigJson: detail.preSettlementConfigJson || '', @@ -1250,6 +1337,35 @@ const createCopiedDetail = (detail = {}) => { changeRecordJson: '', }; }; +const createPartyRow = (row = {}) => ({ + id: row.id || `${Date.now()}${Math.floor(Math.random() * 1000)}`, + customerId: row.customerId || '', + customerName: row.customerName || '', + creditCode: row.creditCode || row.unifiedCreditCode || '', + contactId: row.contactId || '', + contactName: row.contactName || '', + contactPhone: row.contactPhone || '', + contactOptions: row.contactOptions || [], + contactLoading: false, +}); +const serializePartyRows = rows => + JSON.stringify( + (rows || []).map(row => ({ + id: row.id, + customerId: row.customerId || null, + customerName: row.customerName || '', + creditCode: row.creditCode || '', + contactId: row.contactId || null, + contactName: row.contactName || '', + contactPhone: row.contactPhone || '', + })) + ); +const partyRoleLabel = role => { + if (role === 'A') return '甲方'; + if (role === 'B') return '乙方'; + if (role === 'C') return '丙方'; + return role || ''; +}; const parseArray = value => { if (Array.isArray(value)) return value; if (!value) return []; @@ -1350,8 +1466,7 @@ export default { settlementCurrency: [{ required: true, message: '请选择结算币种', trigger: 'change' }], projectName: [{ required: true, message: '请选择所属项目', trigger: 'change' }], organizationName: [{ required: true, message: '请选择合同负责部门', trigger: 'change' }], - partyA: [{ required: true, message: '请选择甲方', trigger: 'change' }], - partyB: [{ required: true, message: '请选择乙方', trigger: 'change' }], + platformCompanyName: [{ required: true, message: '请选择平台公司', trigger: 'change' }], signDate: [{ required: true, message: '请选择签订日期', trigger: 'change' }], settlementMode: [{ required: true, message: '请选择结算方式', trigger: 'change' }], contractFormat: [{ required: true, message: '请选择合同格式', trigger: 'change' }], @@ -1394,9 +1509,11 @@ export default { settlementModeDictOptions: [], contractFormatDictOptions: [], contractCategoryDictOptions: [], - contractPartyAOptions: [], - contractPartyBOptions: [], - // 合规客商风险(C101-1 展示用):甲方/乙方选择后即时查询 + contractPartyOptions: [], + partyARows: [createPartyRow()], + partyBRows: [createPartyRow()], + partyCRows: [], + // 合规客商风险(C101-1 展示用):客商选择后即时查询 contractPartyCodeMap: {}, partyRisk: {}, contractPartyLoading: false, @@ -1405,6 +1522,9 @@ export default { organizationFlatOptions: [], selectedOrganizationId: '', organizationLoading: false, + businessDeptOptions: [], + businessDeptLoading: false, + platformCompanyOptions: [], contractFileRows: [], attachmentRows: [], listUploadContractFileRows: [], @@ -1558,6 +1678,29 @@ export default { ? this.contractFormatDictOptions : this.columnOptions('contractFormat'); }, + partySections() { + return [ + { role: 'A', label: '甲方', rows: this.partyARows }, + { role: 'B', label: '乙方', rows: this.partyBRows }, + { role: 'C', label: '丙方', rows: this.partyCRows }, + ]; + }, + billingPartyOptions() { + return [ + ...this.partyARows.map(row => ({ ...row, partyRole: 'A' })), + ...this.partyBRows.map(row => ({ ...row, partyRole: 'B' })), + ...this.partyCRows.map(row => ({ ...row, partyRole: 'C' })), + ] + .filter(row => row.customerName || row.customerId) + .map(row => ({ + value: row.id, + label: `${partyRoleLabel(row.partyRole)}-${row.customerName || row.customerId}`, + partyRole: row.partyRole, + partyRowId: row.id, + customerId: row.customerId, + customerName: row.customerName, + })); + }, organizationCascaderProps() { return { label: 'label', @@ -1619,14 +1762,10 @@ export default { // 高风险/黑名单客商签约提示(C101-1,签约前提示;强制拦截在后端提交校验与推送链路) partyRiskText() { const parts = []; - ['partyA', 'partyB'].forEach(prop => { - const risk = this.partyRisk[prop]; + Object.keys(this.partyRisk || {}).forEach(key => { + const risk = this.partyRisk[key]; if (!risk?.risky) return; - // riskTip 由后端按风险项拼装(【公司名称】:黑名单;涉诉;被执行人等),缺失时兜底通用提示 - parts.push( - risk.riskTip || - `${prop === 'partyA' ? '甲方' : '乙方'}[${this.form[prop] || ''}]为高风险/黑名单客商` - ); + parts.push(risk.riskTip || `[${risk.customerName || key}]为高风险/黑名单客商`); }); if (!parts.length) return ''; return `${parts.join(';')},按集团规定不可直接签约,提交前须上传「特批附件」类型附件,审批通过后将自动发起合同特批`; @@ -1969,6 +2108,8 @@ export default { }, initFormPage() { this.resetFormState(); + this.loadBusinessDeptOptions(); + this.loadProjectOptions(); if (this.formMode === 'add') { const copyId = this.$route.query.copyId; if (copyId) { @@ -1991,8 +2132,12 @@ export default { }; this.selectedProjectId = ''; this.selectedOrganizationId = ''; - this.contractPartyAOptions = []; - this.contractPartyBOptions = []; + this.platformCompanyOptions = []; + this.contractPartyOptions = []; + this.partyARows = [createPartyRow()]; + this.partyBRows = [createPartyRow()]; + this.partyCRows = []; + this.partyRisk = {}; this.contractFileRows = []; this.attachmentRows = []; this.billingPlanRows = []; @@ -2021,9 +2166,30 @@ export default { detail.feeGenerationMode || (Number(detail.billingEnabled) === 0 ? 'manual' : 'system'), }; this.selectedProjectId = projectId; - this.selectedOrganizationId = detail.organizationId || ''; + this.selectedOrganizationId = detail.organizationId ? String(detail.organizationId) : ''; + this.form.platformCompanyId = detail.platformCompanyId + ? String(detail.platformCompanyId) + : ''; + this.form.platformCompanyName = detail.platformCompanyName || ''; + this.loadBusinessDeptOptions(); + const syncPlatform = () => { + const project = this.projectOptions.find(item => String(item.id) === String(projectId)); + if (project) { + this.applyProjectPlatformCompanies(project, false); + } else { + this.syncCurrentPlatformCompanyOption(); + } + }; + if (!this.projectOptions.length && projectId) { + this.loadProjectOptions().then(syncPlatform); + } else { + syncPlatform(); + } this.contractFileRows = normalizeContractFileRows(parseArray(detail.contractFileJson)); this.attachmentRows = parseArray(detail.attachmentsJson); + this.partyARows = this.normalizePartyRows(detail.partyAJson, detail.partyA, true); + this.partyBRows = this.normalizePartyRows(detail.partyBJson, detail.partyB, true); + this.partyCRows = this.normalizePartyRows(detail.partyCJson, detail.partyC, false); this.billingPlanRows = parseArray(detail.billingPlanJson); this.paymentRatioRows = parseArray(detail.paymentRatioJson); this.changeRecordRows = parseArray(detail.changeRecordJson); @@ -2080,8 +2246,7 @@ export default { ['contractCategory', '合同类型'], ['projectName', '所属项目'], ['organizationName', '合同负责部门'], - ['partyA', '甲方'], - ['partyB', '乙方'], + ['platformCompanyName', '平台公司'], ['contractAmount', '合同金额'], ]; const empty = required.find(([prop]) => { @@ -2100,6 +2265,8 @@ export default { this.$message.warning('合同金额不能小于0'); return false; } + if (!this.validatePartyRows(true)) return false; + if (!this.validateBillingPlanBindings()) return false; return true; } if (!this.form.contractName?.trim()) { @@ -2123,8 +2290,7 @@ export default { const required = [ ['projectName', '所属项目'], ['organizationName', '合同负责部门'], - ['partyA', '甲方'], - ['partyB', '乙方'], + ['platformCompanyName', '平台公司'], ['startDate', '合同开始日期'], ['endDate', '合同结束日期'], ['signDate', '签订日期'], @@ -2146,7 +2312,9 @@ export default { this.$message.warning('合同金额不能小于0'); return false; } + if (!this.validatePartyRows(true)) return false; return ( + this.validateBillingPlanBindings() && this.validateBillingPlanDefault() && this.validateSettlementRules() && this.validatePaymentRatios() @@ -2156,11 +2324,29 @@ export default { this.syncSettlementConfig(); const projectId = normalizeProjectValue(this.form.projectId); const projectName = projectId ? normalizeProjectValue(this.form.projectName) : ''; + const partyAJson = serializePartyRows(this.partyARows); + const partyBJson = serializePartyRows(this.partyBRows); + const partyCJson = this.partyCRows.length ? serializePartyRows(this.partyCRows) : ''; return { ...this.form, contractNo: String(this.form.contractNo || '').trim(), projectId, projectName, + partyAJson, + partyBJson, + partyCJson, + partyA: this.partyARows + .map(row => row.customerName) + .filter(Boolean) + .join('、'), + partyB: this.partyBRows + .map(row => row.customerName) + .filter(Boolean) + .join('、'), + partyC: this.partyCRows + .map(row => row.customerName) + .filter(Boolean) + .join('、'), copyCount: normalizeOptionalInteger(this.form.copyCount), invoiceCycle: normalizeOptionalInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalInteger(this.form.paymentDays), @@ -2346,12 +2532,15 @@ export default { } }, loadProjectOptions() { - if (this.projectLoading) return; + if (this.projectLoading) { + return Promise.resolve(this.projectOptions); + } this.projectLoading = true; - getProjectList(1, 9999, this.config.projectQueryParams || {}) + return getProjectList(1, 9999, this.config.projectQueryParams || {}) .then(res => { this.projectOptions = extractRecords(res); this.syncCurrentProjectOption(); + return this.projectOptions; }) .finally(() => { this.projectLoading = false; @@ -2413,7 +2602,15 @@ export default { handleProjectChange(projectId) { const project = this.projectOptions.find(item => String(item.id) === String(projectId)); if (!project) { - Object.assign(this.form, { projectId: '', projectName: '', settlementMode: '' }); + Object.assign(this.form, { + projectId: '', + projectName: '', + settlementMode: '', + platformCompanyId: '', + platformCompanyName: '', + }); + this.platformCompanyOptions = []; + if (this.formMode === 'add') this.refreshPreviewContractNo(); return; } Object.assign(this.form, { @@ -2421,6 +2618,71 @@ export default { projectName: project.projectName || '', settlementMode: project.settlementMode || this.form.settlementMode || '', }); + this.applyProjectPlatformCompanies(project, true); + }, + parseUndertakeDeptIds(value) { + if (Array.isArray(value)) { + return value.map(item => String(item || '').trim()).filter(Boolean); + } + return String(value || '') + .split(',') + .map(item => item.trim()) + .filter(Boolean); + }, + parseUndertakeDeptNames(value) { + if (Array.isArray(value)) { + return value.map(item => String(item || '').trim()).filter(Boolean); + } + return String(value || '') + .split(/[、,,]/) + .map(item => item.trim()) + .filter(Boolean); + }, + applyProjectPlatformCompanies(project, autoSelect = false) { + const ids = this.parseUndertakeDeptIds(project?.undertakeDeptId); + const names = this.parseUndertakeDeptNames(project?.undertakeDeptName); + this.platformCompanyOptions = ids.map((id, index) => ({ + id: String(id), + label: names[index] || String(id), + })); + if (!autoSelect) { + this.syncCurrentPlatformCompanyOption(); + return; + } + if (this.platformCompanyOptions.length === 1) { + this.form.platformCompanyId = this.platformCompanyOptions[0].id; + this.form.platformCompanyName = this.platformCompanyOptions[0].label; + } else if ( + !this.platformCompanyOptions.some( + item => String(item.id) === String(this.form.platformCompanyId) + ) + ) { + this.form.platformCompanyId = ''; + this.form.platformCompanyName = ''; + } else { + this.handlePlatformCompanyChange(this.form.platformCompanyId); + } + if (this.formMode === 'add') this.refreshPreviewContractNo(); + }, + handlePlatformCompanyChange(value) { + const matched = this.platformCompanyOptions.find(item => String(item.id) === String(value)); + this.form.platformCompanyId = matched ? matched.id : ''; + this.form.platformCompanyName = matched ? matched.label : ''; + if (this.formMode === 'add') this.refreshPreviewContractNo(); + }, + syncCurrentPlatformCompanyOption() { + if (!this.form.platformCompanyId || !this.form.platformCompanyName) return; + if ( + this.platformCompanyOptions.some( + item => String(item.id) === String(this.form.platformCompanyId) + ) + ) { + return; + } + this.platformCompanyOptions.unshift({ + id: String(this.form.platformCompanyId), + label: this.form.platformCompanyName, + }); }, loadContractPartyOptions() { if (this.contractPartyLoading) return; @@ -2434,42 +2696,170 @@ export default { .then(responses => { if (requestId !== this.contractPartyRequestId) return; const records = responses.flatMap(extractRecords); - // 名称→客商编码映射,供 C101-1 风险查询使用(§9-U6 编码体系联调确认) const codeMap = {}; + const options = []; + const optionIds = new Set(); records.forEach(item => { + const id = item.id; + if (!id || optionIds.has(String(id))) return; + optionIds.add(String(id)); + const label = String( + item.fullName || item.shortName || item.customerName || item.customerCode || '' + ).trim(); + if (!label) return; const code = item.customerCode; - if (!code) return; - [item.fullName, item.shortName, item.customerName] - .map(name => String(name || '').trim()) - .filter(Boolean) - .forEach(name => { - codeMap[name] = code; - }); + if (code) { + [item.fullName, item.shortName, item.customerName, label] + .map(name => String(name || '').trim()) + .filter(Boolean) + .forEach(name => { + codeMap[name] = code; + }); + codeMap[String(id)] = code; + } + options.push({ + id, + label, + value: id, + customerName: label, + creditCode: item.unifiedCreditCode || '', + customerCode: code || '', + }); }); this.contractPartyCodeMap = codeMap; - const names = records - .map(item => item.fullName || item.shortName || item.customerName || item.customerCode) - .map(name => String(name || '').trim()) - .filter(Boolean); - [this.form.partyA, this.form.partyB].filter(Boolean).forEach(name => names.unshift(name)); - const options = [...new Set(names)].map(name => ({ label: name, value: name })); - this.contractPartyAOptions = options; - this.contractPartyBOptions = options; - // 复制/编辑回填的甲乙方不会触发下拉 change,加载选项后主动查一次 C101-1 风险 - ['partyA', 'partyB'].forEach(prop => { - if (this.form[prop]) this.queryPartyRisk(prop); + this.contractPartyOptions = options; + this.syncPartyRowsWithOptions(); + [...this.partyARows, ...this.partyBRows, ...this.partyCRows].forEach(row => { + if (row.customerId || row.customerName) this.queryPartyRiskByRow(row); }); }) .finally(() => { if (requestId === this.contractPartyRequestId) this.contractPartyLoading = false; }); }, - // C101-1 客商风险查询(展示用,强校验在后端提交/推送链路):失败静默不阻塞填写 - queryPartyRisk(prop) { - const name = this.form[prop]; - const code = name ? this.contractPartyCodeMap[name] : null; + syncPartyRowsWithOptions() { + const fill = rows => { + rows.forEach(row => { + if (row.customerId) { + const matched = this.contractPartyOptions.find( + item => String(item.id) === String(row.customerId) + ); + if (matched) { + row.customerName = matched.customerName; + row.creditCode = matched.creditCode || row.creditCode || ''; + return; + } + } + if (row.customerName) { + const matched = this.contractPartyOptions.find( + item => item.customerName === row.customerName + ); + if (matched) { + row.customerId = matched.id; + row.creditCode = matched.creditCode || row.creditCode || ''; + } + } + }); + }; + fill(this.partyARows); + fill(this.partyBRows); + fill(this.partyCRows); + }, + normalizePartyRows(partyJson, fallbackName, requireOne) { + const rows = parseArray(partyJson).map(item => createPartyRow(item)); + if (rows.length) return rows; + if (fallbackName) { + return String(fallbackName) + .split(/[、,]/) + .map(name => name.trim()) + .filter(Boolean) + .map(name => createPartyRow({ customerName: name })); + } + return requireOne ? [createPartyRow()] : []; + }, + partyRowsByRole(role) { + if (role === 'A') return this.partyARows; + if (role === 'B') return this.partyBRows; + return this.partyCRows; + }, + setPartyRowsByRole(role, rows) { + if (role === 'A') this.partyARows = rows; + else if (role === 'B') this.partyBRows = rows; + else this.partyCRows = rows; + }, + addPartyRow(role) { + const rows = [...this.partyRowsByRole(role), createPartyRow()]; + this.setPartyRowsByRole(role, rows); + }, + canRemovePartyRow(role) { + if (role === 'C') return true; + return this.partyRowsByRole(role).length > 1; + }, + removePartyRow(role, index) { + if (!this.canRemovePartyRow(role)) return; + const rows = [...this.partyRowsByRole(role)]; + rows.splice(index, 1); + this.setPartyRowsByRole(role, rows); + }, + handlePartyCustomerChange(role, row, customerId) { + const matched = this.contractPartyOptions.find(item => String(item.id) === String(customerId)); + row.customerId = matched?.id || ''; + row.customerName = matched?.customerName || ''; + row.creditCode = matched?.creditCode || ''; + row.contactId = ''; + row.contactName = ''; + row.contactPhone = ''; + row.contactOptions = []; + this.queryPartyRiskByRow(row); + if (row.customerId) this.ensurePartyContacts(row); + }, + ensurePartyContacts(row) { + if (!row.customerId) return Promise.resolve(); + if ((row.contactOptions || []).length) return Promise.resolve(); + row.contactLoading = true; + return getCustomerArchiveDetail(row.customerId) + .then(res => { + const detail = res?.data?.data || res?.data || {}; + const contacts = Array.isArray(detail.contacts) ? detail.contacts : []; + row.contactOptions = contacts + .map(item => ({ + id: item.id, + label: item.contactName || item.name || '', + contactName: item.contactName || item.name || '', + contactPhone: item.contactPhone || item.phone || '', + })) + .filter(item => item.id && item.label); + if (row.contactId) { + const matched = row.contactOptions.find( + item => String(item.id) === String(row.contactId) + ); + if (matched) { + row.contactName = matched.contactName; + row.contactPhone = matched.contactPhone; + } + } + if (!row.creditCode && detail.unifiedCreditCode) { + row.creditCode = detail.unifiedCreditCode; + } + }) + .finally(() => { + row.contactLoading = false; + }); + }, + handlePartyContactChange(row, contactId) { + const matched = (row.contactOptions || []).find(item => String(item.id) === String(contactId)); + row.contactId = matched?.id || ''; + row.contactName = matched?.contactName || ''; + row.contactPhone = matched?.contactPhone || ''; + }, + queryPartyRiskByRow(row) { + const key = String(row.customerId || row.customerName || ''); + if (!key) return; + const code = + this.contractPartyCodeMap[String(row.customerId)] || + this.contractPartyCodeMap[row.customerName]; if (!code) { - this.partyRisk = { ...this.partyRisk, [prop]: null }; + this.partyRisk = { ...this.partyRisk, [key]: null }; return; } getRisk(code) @@ -2477,18 +2867,65 @@ export default { const risk = res?.data?.data ?? res?.data ?? {}; this.partyRisk = { ...this.partyRisk, - [prop]: { + [key]: { + customerName: row.customerName || '', customerKind: risk.customerKind || '', - // risky 由后端统一判定:外部客商高风险(等级=2)或黑名单;内部组织客商默认无风险恒为 false risky: risk.risky === true, riskTip: risk.riskTip || '', }, }; }) .catch(() => { - this.partyRisk = { ...this.partyRisk, [prop]: null }; + this.partyRisk = { ...this.partyRisk, [key]: null }; }); }, + validatePartyRows(requireCustomerId) { + const validate = (label, rows, required) => { + if (required && !rows.length) { + this.$message.warning(`请至少添加一个${label}客商`); + return false; + } + for (let index = 0; index < rows.length; index += 1) { + const row = rows[index]; + if (!row.customerName) { + this.$message.warning(`${label}第${index + 1}行请选择客商`); + return false; + } + if (requireCustomerId && !row.customerId) { + this.$message.warning(`${label}第${index + 1}行请选择有效客商`); + return false; + } + if (row.contactName && !row.contactPhone) { + this.$message.warning(`${label}第${index + 1}行联系人需带出联系电话`); + return false; + } + } + return true; + }; + return ( + validate('甲方', this.partyARows, true) && + validate('乙方', this.partyBRows, true) && + validate('丙方', this.partyCRows, false) + ); + }, + validateBillingPlanBindings() { + if (!this.billingPlanRows.length) return true; + const partyIds = new Set(this.billingPartyOptions.map(item => String(item.partyRowId))); + for (let index = 0; index < this.billingPlanRows.length; index += 1) { + const plan = this.billingPlanRows[index]; + if (!plan.partyRowId || !partyIds.has(String(plan.partyRowId))) { + this.$message.warning(`计费方案第${index + 1}个请选择适用客商`); + return false; + } + } + return true; + }, + billingPlanCustomerLabel(row) { + const matched = this.billingPartyOptions.find( + item => String(item.partyRowId) === String(row.partyRowId) + ); + return matched?.label || row.customerName || ''; + }, loadOrganizationOptions() { if (this.organizationLoading) return; this.organizationLoading = true; @@ -2558,7 +2995,10 @@ export default { return null; }, handleOrganizationChange(id) { - const organization = this.findOrganization(id); + const organizationId = Array.isArray(id) ? id.at(-1) : id; + const organization = + this.businessDeptOptions.find(item => String(item.id) === String(organizationId)) || + this.findOrganization(organizationId); Object.assign( this.form, organization @@ -2568,6 +3008,9 @@ export default { } : { organizationId: '', organizationName: '' } ); + this.selectedOrganizationId = this.form.organizationId + ? String(this.form.organizationId) + : ''; if (this.formMode === 'add') { this.refreshPreviewContractNo(); } @@ -2578,23 +3021,58 @@ export default { refreshPreviewContractNo() { if (this.formMode !== 'add') return; const organizationId = this.form.organizationId; - if (!organizationId) { + const platformCompanyId = this.form.platformCompanyId; + if (!organizationId || !platformCompanyId) { if (!this.contractNoManual) this.form.contractNo = ''; return; } const requestId = (this.contractNoPreviewRequestId = (this.contractNoPreviewRequestId || 0) + 1); api - .nextContractNo(organizationId) + .nextContractNo(organizationId, platformCompanyId) .then(res => { if (requestId !== this.contractNoPreviewRequestId || this.contractNoManual) return; this.form.contractNo = res.data?.data || ''; }) - .catch(() => { + .catch(error => { if (requestId !== this.contractNoPreviewRequestId || this.contractNoManual) return; this.form.contractNo = ''; + const message = + error?.response?.data?.msg || + error?.message || + '部门合同前缀未配置,无法自动生成,请手动输入合同编号'; + this.$message.warning(message); }); }, + loadBusinessDeptOptions() { + if (this.businessDeptLoading) return; + this.businessDeptLoading = true; + getBusinessDeptSelect() + .then(res => { + this.businessDeptOptions = (res.data?.data || []).map(item => ({ + id: String(item.id), + label: item.deptName || item.fullName || String(item.id), + rawLabel: item.deptName || item.fullName || String(item.id), + })); + this.syncCurrentBusinessDeptOption(); + }) + .finally(() => { + this.businessDeptLoading = false; + }); + }, + syncCurrentBusinessDeptOption() { + if (!this.form.organizationId || !this.form.organizationName) return; + if ( + this.businessDeptOptions.some(item => String(item.id) === String(this.form.organizationId)) + ) { + return; + } + this.businessDeptOptions.unshift({ + id: String(this.form.organizationId), + label: this.form.organizationName, + rawLabel: this.form.organizationName, + }); + }, syncCurrentOrganizationOption() { if (!this.form.organizationId || !this.form.organizationName) return; if (this.findOrganization(this.form.organizationId)) return; @@ -3617,6 +4095,7 @@ export default { } &__billing-head, + &__party-head, &__attachment-head { display: flex; align-items: center; diff --git a/src/views/business/project-apply.vue b/src/views/business/project-apply.vue index 19d4af2..35ed640 100644 --- a/src/views/business/project-apply.vue +++ b/src/views/business/project-apply.vue @@ -258,15 +258,21 @@
{{ businessDeptTooltipText }}
- + @change="handleBusinessDeptChange" + > + +
@@ -995,7 +1001,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'; @@ -1339,7 +1345,6 @@ export default { ], }, deptOptions: [], - businessDeptTreeOptions: [], platformCompanyOptions: [], cargoTypeOptions: [], selectedCustomerId: [], @@ -1404,44 +1409,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(); }, @@ -1673,9 +1648,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); @@ -2822,10 +2798,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) { @@ -2863,77 +2839,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 => { @@ -2965,50 +2908,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 => ({ diff --git a/src/views/system/business-dept-config.vue b/src/views/system/business-dept-config.vue new file mode 100644 index 0000000..fe1cda4 --- /dev/null +++ b/src/views/system/business-dept-config.vue @@ -0,0 +1,407 @@ + + + diff --git a/src/views/system/dept.vue b/src/views/system/dept.vue index 0b4033c..b7780fc 100644 --- a/src/views/system/dept.vue +++ b/src/views/system/dept.vue @@ -303,7 +303,7 @@ export default { headerAlign: 'left', order: 80, search: true, - searchOrder: 2, + searchOrder: 3, rules: [ { required: true, @@ -366,105 +366,13 @@ export default { }, ], }, - { - label: '金蝶部门代码', - prop: 'kingdeeDeptCode', - minWidth: 140, - order: 65, - maxlength: 64, - showWordLimit: true, - rules: [ - { - validator: (rule, value, callback) => { - const code = String(value || '').trim(); - if (!code) { - callback(); - return; - } - if (code.length > 64) { - callback(new Error('金蝶部门代码不能超过64个字符')); - return; - } - callback(); - }, - trigger: 'blur', - }, - ], - }, - { - label: '合同编码前缀', - prop: 'contractCodePrefix', - minWidth: 160, - order: 62, - maxlength: 64, - showWordLimit: true, - placeholder: '如 GWZH成都', - tip: '合同编号规则:前缀(年份)三位流水号,流水按本部门独立编号', - rules: [ - { - validator: (rule, value, callback) => { - const prefix = String(value || '').trim(); - if (!prefix) { - callback(); - return; - } - if (prefix.length > 64) { - callback(new Error('合同编码前缀不能超过64个字符')); - return; - } - callback(); - }, - trigger: 'blur', - }, - ], - }, - { - label: '合同初始流水号', - prop: 'contractInitialSerial', - type: 'input', - minWidth: 140, - order: 61, - placeholder: '如 1', - // -1 为空占位,列表展示为空 - formatter: row => - Number(row.contractInitialSerial) === -1 ? '' : row.contractInitialSerial, - tip: '该部门当年首个合同时从此流水号起编,后续递增;范围1-999,不填默认从1开始', - rules: [ - { - validator: (rule, value, callback) => { - // -1 为空占位,不填时按空处理 - if ( - value === undefined || - value === null || - value === '' || - Number(value) === -1 - ) { - callback(); - return; - } - const text = String(value).trim(); - if (!/^\d+$/.test(text)) { - callback(new Error('合同初始流水号只能输入正整数')); - return; - } - const serial = Number(text); - if (serial < 1 || serial > 999) { - callback(new Error('合同初始流水号范围为1到999')); - return; - } - callback(); - }, - trigger: 'blur', - }, - ], - }, { label: '组织简称', prop: 'shortName', minWidth: 140, order: 60, search: true, - searchOrder: 1, + searchOrder: 4, }, { label: '助记码', @@ -525,8 +433,12 @@ export default { dataType: 'number', value: 0, width: 120, - order: 35, + order: 36, + search: true, + searchOrder: 2, + searchPlaceholder: '请选择是否平台公司', formatter: row => (Number(row.isPlatformCompany) === 1 ? '是' : '否'), + change: ({ value }) => this.handlePlatformCompanyChange(value), rules: [ { required: true, @@ -535,6 +447,37 @@ export default { }, ], }, + { + label: '金蝶部门代码', + prop: 'kingdeeDeptCode', + minWidth: 140, + order: 35, + maxlength: 64, + showWordLimit: true, + // 仅平台公司维护/展示金蝶组织代码;非平台公司列表留空 + addDisplay: false, + editDisplay: false, + viewDisplay: false, + formatter: row => + Number(row.isPlatformCompany) === 1 ? String(row.kingdeeDeptCode || '').trim() : '', + rules: [ + { + validator: (rule, value, callback) => { + const code = String(value || '').trim(); + if (!code) { + callback(); + return; + } + if (code.length > 64) { + callback(new Error('金蝶部门代码不能超过64个字符')); + return; + } + callback(); + }, + trigger: 'blur', + }, + ], + }, { label: '是否业务部门', prop: 'isBusinessDept', @@ -547,6 +490,9 @@ export default { value: 0, width: 120, order: 34, + search: true, + searchOrder: 1, + searchPlaceholder: '请选择是否业务部门', formatter: row => (Number(row.isBusinessDept) === 1 ? '是' : '否'), rules: [ { @@ -1217,28 +1163,20 @@ export default { row.parentId = 0; } }, - normalizeContractInitialSerial(value) { - // -1 为空占位,不填时不落库、不回显 - if ( - value === undefined || - value === null || - value === '' || - Number(value) === -1 - ) { - return null; + handlePlatformCompanyChange(value) { + const isPlatform = Number(value) === 1; + this.syncKingdeeDeptCodeDisplay(isPlatform); + if (!isPlatform && this.form) { + this.form.kingdeeDeptCode = ''; } - const text = String(value).trim(); - if (!text) { - return null; - } - const serial = Number(text); - return Number.isInteger(serial) ? serial : value; }, - clearEmptyContractInitialSerial(form = this.form) { - if (!form) return; - if (Number(form.contractInitialSerial) === -1) { - form.contractInitialSerial = ''; - } + syncKingdeeDeptCodeDisplay(isPlatformCompany) { + const column = this.findColumn(this.option.column, 'kingdeeDeptCode'); + if (!column) return; + const visible = Number(isPlatformCompany) === 1; + column.addDisplay = visible; + column.editDisplay = visible; + column.viewDisplay = visible; }, isRootDept(row) { return row && (row.parentId === 0 || String(row.parentId) === '0'); @@ -1384,9 +1322,9 @@ export default { rowSave(row, done, loading) { row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId; row.deptCode = String(row.deptCode || '').trim(); - row.kingdeeDeptCode = String(row.kingdeeDeptCode || '').trim(); - row.contractCodePrefix = String(row.contractCodePrefix || '').trim(); - row.contractInitialSerial = this.normalizeContractInitialSerial(row.contractInitialSerial); + // 合同前缀/流水已迁至业务部门配置;组织管理仅维护平台公司金蝶代码 + row.kingdeeDeptCode = + Number(row.isPlatformCompany) === 1 ? String(row.kingdeeDeptCode || '').trim() : ''; row.leaderId = func.join(row.leaderId); this.normalizeTopParent(row); if (!this.prepareDeptSubmitRow(row)) { @@ -1432,9 +1370,9 @@ export default { rowUpdate(row, index, done, loading) { row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId; row.deptCode = String(row.deptCode || '').trim(); - row.kingdeeDeptCode = String(row.kingdeeDeptCode || '').trim(); - row.contractCodePrefix = String(row.contractCodePrefix || '').trim(); - row.contractInitialSerial = this.normalizeContractInitialSerial(row.contractInitialSerial); + // 合同前缀/流水已迁至业务部门配置;组织管理仅维护平台公司金蝶代码 + row.kingdeeDeptCode = + Number(row.isPlatformCompany) === 1 ? String(row.kingdeeDeptCode || '').trim() : ''; row.leaderId = func.join(row.leaderId); this.normalizeTopParent(row); if (!this.prepareDeptSubmitRow(row)) { @@ -1547,7 +1485,7 @@ export default { if (this.form.isBusinessDept === undefined || this.form.isBusinessDept === null) { this.form.isBusinessDept = 0; } - this.clearEmptyContractInitialSerial(this.form); + this.syncKingdeeDeptCodeDisplay(this.form.isPlatformCompany); this.initData(this.form.tenantId); this.loadParentDeptCode(this.form.parentId); } @@ -1562,7 +1500,7 @@ export default { if (this.form.parentId === '0') { this.form.parentId = ''; } - this.clearEmptyContractInitialSerial(this.form); + this.syncKingdeeDeptCodeDisplay(this.form.isPlatformCompany); this.loadParentDeptCode(this.form.parentId); if (this.form.hasOwnProperty('leaderId')) { const splitResult = func.split(this.form.leaderId);