@@ -491,7 +493,9 @@
v-model="planDialogVisible"
:value="planEditor"
:index="planEditorIndex"
+ :readonly="pageReadonly"
:settlement-currency="form.settlementCurrency"
+ :party-options="billingPartyOptions"
@save="savePlan"
/>
@@ -630,6 +634,45 @@ const normalizeCustomPeriods = (periods = []) =>
endDay: Number(item?.endDay) > 0 ? Number(item.endDay) : '',
}));
+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 || '',
+});
+
+const partyRoleLabel = role => {
+ if (role === 'A') return '甲方';
+ if (role === 'B') return '乙方';
+ if (role === 'C') return '丙方';
+ return role || '';
+};
+
+const normalizePartyRows = (partyJson, fallbackName, requireOne) => {
+ const rows = (() => {
+ if (Array.isArray(partyJson)) return partyJson;
+ if (!partyJson) return [];
+ try {
+ const data = JSON.parse(partyJson);
+ return Array.isArray(data) ? data : [];
+ } catch (error) {
+ return [];
+ }
+ })().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()] : [];
+};
+
export default {
components: { BillingPlanEditor, ContractAttachmentSection, ElImageViewer, OpenFileViewer },
data() {
@@ -637,6 +680,9 @@ export default {
form: {},
period: [],
plans: [],
+ partyARows: [],
+ partyBRows: [],
+ partyCRows: [],
attachments: [],
contractFiles: [],
contractFileRows: [],
@@ -675,7 +721,7 @@ export default {
settlementModeDictOptions: [],
contractFormatDictOptions: [],
planDialogVisible: false,
- planEditor: { planName: '', defaultPlan: false, remark: '', rules: [] },
+ planEditor: { planName: '', remark: '', rules: [] },
planEditorIndex: -1,
billingElements: [
'按重量',
@@ -756,6 +802,29 @@ export default {
isSettlementRuleChange() {
return this.form.changeType === '结算规则变更';
},
+ /**
+ * 计费方案适用客商:客户合同仅甲方;承运商合同仅乙方、丙方
+ */
+ billingPartyOptions() {
+ const category = String(this.form.contractCategory || '').trim();
+ const allowedRoles =
+ category === '承运商合同' ? ['B', 'C'] : category === '客户合同' ? ['A'] : ['A', 'B', 'C'];
+ return [
+ ...this.partyARows.map(row => ({ ...row, partyRole: 'A' })),
+ ...this.partyBRows.map(row => ({ ...row, partyRole: 'B' })),
+ ...this.partyCRows.map(row => ({ ...row, partyRole: 'C' })),
+ ]
+ .filter(row => allowedRoles.includes(row.partyRole))
+ .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,
+ }));
+ },
changeAttachmentLocationOptions() {
if (this.isSettlementRuleChange) {
return ['变更材料'];
@@ -890,6 +959,9 @@ export default {
};
this.changeMaterials = this.parse(data.changeAttachmentsJson);
this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : [];
+ this.partyARows = normalizePartyRows(data.partyAJson, data.partyA, true);
+ this.partyBRows = normalizePartyRows(data.partyBJson, data.partyB, true);
+ this.partyCRows = normalizePartyRows(data.partyCJson, data.partyC, false);
this.plans = this.parse(data.billingPlanJson);
this.attachments = this.mergeChangeAttachments(
this.parse(data.attachmentsJson),
@@ -1048,10 +1120,32 @@ export default {
.replace(/\D/g, '')
.replace(/^0+/, '');
},
- addPlan() { this.planEditorIndex = -1; this.planEditor = { planName: `计费方案${this.plans.length + 1}`, defaultPlan: !this.plans.length, remark: '', rules: [{}] }; this.planDialogVisible = true; },
- editPlan(row, index) { this.planEditorIndex = index; this.planEditor = JSON.parse(JSON.stringify(row)); this.planDialogVisible = true; },
- toggleDefaultPlan(value) { if (value) this.plans.forEach(item => { item.defaultPlan = false; }); },
- savePlan(value, index) { if (index < 0) this.plans.push(value); else this.plans.splice(index, 1, value); if (value.defaultPlan) this.plans.forEach((item, current) => { if (current !== (index < 0 ? this.plans.length - 1 : index)) item.defaultPlan = false; }); },
+ addPlan() {
+ this.planEditorIndex = -1;
+ this.planEditor = {
+ planName: `计费方案${this.plans.length + 1}`,
+ remark: '',
+ rules: [{}],
+ };
+ this.planDialogVisible = true;
+ },
+ editPlan(row, index) {
+ this.planEditorIndex = index;
+ this.planEditor = JSON.parse(JSON.stringify(row));
+ this.planDialogVisible = true;
+ },
+ billingPlanCustomerLabel(row) {
+ const matched = this.billingPartyOptions.find(
+ item => String(item.partyRowId) === String(row.partyRowId)
+ );
+ return matched?.label || row.customerName || '';
+ },
+ savePlan(value, index) {
+ const nextPlan = { ...value };
+ delete nextPlan.defaultPlan;
+ if (index < 0) this.plans.push(nextPlan);
+ else this.plans.splice(index, 1, nextPlan);
+ },
handleSettlementTypeChange(value) { if (value !== '月结') { this.settlementRule.billCycleType = ''; this.settlementRule.billCutoffDay = ''; this.settlementRule.customPeriods = []; } else if (this.settlementRule.billCycleType === '自定义多周期') this.settlementRule.customPeriods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); else this.settlementRule.customPeriods = []; if (value !== '固定天数周期结算') this.settlementRule.cycleDays = ''; },
handleCycleTypeChange(value) { if (value !== '固定截单日') this.settlementRule.billCutoffDay = ''; if (value === '自定义多周期') this.settlementRule.customPeriods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); else this.settlementRule.customPeriods = []; },
attachmentUrl(row = {}) { return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || ''; },
diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue
index b98b545..3972ef3 100644
--- a/src/views/business/contract-manage.vue
+++ b/src/views/business/contract-manage.vue
@@ -1685,12 +1685,21 @@ export default {
{ role: 'C', label: '丙方', rows: this.partyCRows },
];
},
+ /**
+ * 计费方案适用客商:
+ * - 客户合同:仅甲方(仅 1 个甲方时编辑器自动选中)
+ * - 承运商合同:仅乙方、丙方(应付明细按这些方案生成)
+ */
billingPartyOptions() {
+ const category = String(this.form.contractCategory || '').trim();
+ const allowedRoles =
+ category === '承运商合同' ? ['B', 'C'] : category === '客户合同' ? ['A'] : ['A', 'B', 'C'];
return [
...this.partyARows.map(row => ({ ...row, partyRole: 'A' })),
...this.partyBRows.map(row => ({ ...row, partyRole: 'B' })),
...this.partyCRows.map(row => ({ ...row, partyRole: 'C' })),
]
+ .filter(row => allowedRoles.includes(row.partyRole))
.filter(row => row.customerName || row.customerId)
.map(row => ({
value: row.id,
@@ -2315,7 +2324,6 @@ export default {
if (!this.validatePartyRows(true)) return false;
return (
this.validateBillingPlanBindings() &&
- this.validateBillingPlanDefault() &&
this.validateSettlementRules() &&
this.validatePaymentRatios()
);
@@ -2911,10 +2919,17 @@ export default {
validateBillingPlanBindings() {
if (!this.billingPlanRows.length) return true;
const partyIds = new Set(this.billingPartyOptions.map(item => String(item.partyRowId)));
+ const category = String(this.form.contractCategory || '').trim();
+ const roleHint =
+ category === '承运商合同'
+ ? '请选择乙方或丙方客商'
+ : category === '客户合同'
+ ? '请选择甲方客商'
+ : '请选择适用客商';
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}个请选择适用客商`);
+ this.$message.warning(`计费方案第${index + 1}个${roleHint}`);
return false;
}
}
@@ -3169,21 +3184,13 @@ export default {
openBillingPlan(row = {}, index = -1) {
this.billingPlanIndex = index;
this.billingPlanForm =
- index < 0
- ? { planName: '', transportMode: '', defaultPlan: false, remark: '', rules: [] }
- : clone(row);
+ index < 0 ? { planName: '', transportMode: '', remark: '', rules: [] } : clone(row);
this.billingPlanBox = true;
this.$nextTick(() => this.$refs.billingPlanEditor?.ensureTransportModes?.());
},
saveBillingPlan(plan, index) {
const nextPlan = clone(plan);
- if (this.isDefaultBillingPlan(nextPlan)) {
- this.billingPlanRows.forEach((item, itemIndex) => {
- if (itemIndex !== index && item.transportMode === nextPlan.transportMode) {
- item.defaultPlan = false;
- }
- });
- }
+ delete nextPlan.defaultPlan;
if (index < 0) this.billingPlanRows.push(nextPlan);
else this.billingPlanRows.splice(index, 1, nextPlan);
this.billingPlanBox = false;
@@ -3193,31 +3200,6 @@ export default {
this.billingPlanRows.splice(index, 1);
});
},
- isDefaultBillingPlan(plan) {
- return (
- plan?.defaultPlan === true ||
- plan?.defaultPlan === 1 ||
- ['true', '1'].includes(String(plan?.defaultPlan).toLowerCase())
- );
- },
- validateBillingPlanDefault() {
- if (!this.billingPlanRows.length) return true;
- const defaultCountByTransportMode = this.billingPlanRows.reduce((result, plan) => {
- const transportMode = plan.transportMode || '';
- result[transportMode] =
- (result[transportMode] || 0) + Number(this.isDefaultBillingPlan(plan));
- return result;
- }, {});
- const invalidTransportMode = Object.entries(defaultCountByTransportMode).find(
- ([, count]) => count > 1
- );
- if (invalidTransportMode) {
- const [transportMode, count] = invalidTransportMode;
- this.$message.warning(`运输方式“${transportMode}”仅支持配置一个默认计费方案`);
- return false;
- }
- return true;
- },
isFixedCycleSettlementType(value) {
return value === '按固定天数周期' || value === '固定天数周期结算';
},
@@ -3551,8 +3533,7 @@ export default {
return list
.map(item => {
if (!item || typeof item !== 'object') return String(item ?? '');
- const name = item.planName || item.name || '未命名方案';
- return item.defaultPlan ? `${name}(默认)` : name;
+ return item.planName || item.name || '未命名方案';
})
.filter(Boolean)
.join('、');
diff --git a/src/views/business/project-apply.vue b/src/views/business/project-apply.vue
index 35ed640..2dfa40b 100644
--- a/src/views/business/project-apply.vue
+++ b/src/views/business/project-apply.vue
@@ -761,6 +761,96 @@
/>
+
+ 项目合同
+
+
+
+
+
+
+ {{ row.contractNo }}
+
+ {{ row.contractNo || '' }}
+
+
+
+
+
+
+
+
+
+
+ {{ formatAmount(row.contractAmount) }}
+
+
+
+
+
{
+ 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 || '';
diff --git a/src/views/system/business-dept-config.vue b/src/views/system/business-dept-config.vue
index fe1cda4..1e87a6b 100644
--- a/src/views/system/business-dept-config.vue
+++ b/src/views/system/business-dept-config.vue
@@ -20,6 +20,13 @@
@on-load="onLoad"
>
+
+ 查看
+
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const ORGANIZATION_TYPE_DICT_CODE = 'organization_type';
/**
* 组织类型名称 → 字典数字键(兼容 dictKey 配成中文的情况)
@@ -323,7 +322,7 @@ export default {
{
validator: (rule, value, callback) => {
const deptCode = String(value || '').trim();
- // 组织编码非必填,为空直接通过
+ // 组织编码非必填,为空直接通过;不再校验 GWWL/上级编码+三位数字格式
if (!deptCode) {
callback();
return;
@@ -332,34 +331,6 @@ export default {
callback(new Error('组织编码不能超过30个字符'));
return;
}
- const parentId = this.normalizeParentId(this.form?.parentId);
- // 顶级组织不选择上级时,编码不依赖上级编码
- if (!parentId) {
- callback();
- return;
- }
- if (!this.parentDept || String(this.parentDept.id) !== String(parentId)) {
- this.loadParentDeptCode(parentId);
- callback(new Error('正在加载上级组织编码,请稍后重试'));
- return;
- }
- if (!this.parentDeptCode) {
- callback(new Error('上级组织未配置组织编码,请先完善上级组织编码'));
- return;
- }
- // 公司:GWWL+三位数字;部门:所属公司编码+三位数字;其他:上级编码+三位数字(中间不加-)
- let expectedPrefix = this.parentDeptCode;
- const category = Number(this.form?.deptCategory);
- if (category === 2) {
- expectedPrefix = 'GWWL';
- } else if (category === 3 && this.form?.belongCompanyCode) {
- expectedPrefix = String(this.form.belongCompanyCode).trim();
- }
- const pattern = new RegExp(`^${escapeRegExp(expectedPrefix)}\\d{3}$`);
- if (!pattern.test(deptCode)) {
- callback(new Error(`组织编码格式应为:${expectedPrefix}三位数字(不足补零,中间不加-)`));
- return;
- }
callback();
},
trigger: 'blur',
@@ -431,50 +402,22 @@ export default {
{ label: '是', value: 1 },
],
dataType: 'number',
- value: 0,
width: 120,
order: 36,
search: true,
searchOrder: 2,
searchPlaceholder: '请选择是否平台公司',
formatter: row => (Number(row.isPlatformCompany) === 1 ? '是' : '否'),
- change: ({ value }) => this.handlePlatformCompanyChange(value),
- rules: [
- {
- required: true,
- message: '请选择是否平台公司',
- trigger: 'change',
- },
- ],
- },
- {
- 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个字符'));
+ if (value === undefined || value === null || value === '') {
+ callback(new Error('请选择是否平台公司'));
return;
}
callback();
},
- trigger: 'blur',
+ trigger: 'change',
},
],
},
@@ -487,7 +430,6 @@ export default {
{ label: '是', value: 1 },
],
dataType: 'number',
- value: 0,
width: 120,
order: 34,
search: true,
@@ -496,8 +438,14 @@ export default {
formatter: row => (Number(row.isBusinessDept) === 1 ? '是' : '否'),
rules: [
{
- required: true,
- message: '请选择是否业务部门',
+ validator: (rule, value, callback) => {
+ // 不能用 required:true:值为 0(否)时会被当成空
+ if (value === undefined || value === null || value === '') {
+ callback(new Error('请选择是否业务部门'));
+ return;
+ }
+ callback();
+ },
trigger: 'change',
},
],
@@ -988,6 +936,7 @@ export default {
/**
* 提交前将组织类型规范为 Integer;空状态/排序字段去掉,避免 "" 反序列化失败。
* 排序为空时由后端默认填充 1。
+ * 是否平台公司 / 是否业务部门强制规范为数字 0/1,避免下拉值类型导致漏更新。
*/
prepareDeptSubmitRow(row) {
const category = this.resolveDeptCategoryValue(row.deptCategory);
@@ -995,6 +944,8 @@ export default {
return false;
}
row.deptCategory = category;
+ row.isPlatformCompany = Number(row.isPlatformCompany) === 1 ? 1 : 0;
+ row.isBusinessDept = Number(row.isBusinessDept) === 1 ? 1 : 0;
if (row.status === '' || row.status === null || row.status === undefined) {
delete row.status;
} else {
@@ -1007,6 +958,9 @@ export default {
}
return true;
},
+ normalizeYesNoFlag(value) {
+ return Number(value) === 1 ? 1 : 0;
+ },
resolveDeptCategoryValue(value) {
if (value === null || value === undefined || value === '') {
this.$message.warning('请选择组织类型');
@@ -1163,21 +1117,6 @@ export default {
row.parentId = 0;
}
},
- handlePlatformCompanyChange(value) {
- const isPlatform = Number(value) === 1;
- this.syncKingdeeDeptCodeDisplay(isPlatform);
- if (!isPlatform && this.form) {
- this.form.kingdeeDeptCode = '';
- }
- },
- 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');
},
@@ -1322,9 +1261,6 @@ export default {
rowSave(row, done, loading) {
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
row.deptCode = String(row.deptCode || '').trim();
- // 合同前缀/流水已迁至业务部门配置;组织管理仅维护平台公司金蝶代码
- row.kingdeeDeptCode =
- Number(row.isPlatformCompany) === 1 ? String(row.kingdeeDeptCode || '').trim() : '';
row.leaderId = func.join(row.leaderId);
this.normalizeTopParent(row);
if (!this.prepareDeptSubmitRow(row)) {
@@ -1370,9 +1306,6 @@ export default {
rowUpdate(row, index, done, loading) {
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
row.deptCode = String(row.deptCode || '').trim();
- // 合同前缀/流水已迁至业务部门配置;组织管理仅维护平台公司金蝶代码
- row.kingdeeDeptCode =
- Number(row.isPlatformCompany) === 1 ? String(row.kingdeeDeptCode || '').trim() : '';
row.leaderId = func.join(row.leaderId);
this.normalizeTopParent(row);
if (!this.prepareDeptSubmitRow(row)) {
@@ -1479,34 +1412,40 @@ export default {
beforeOpen(done, type) {
if (type === 'add') {
this.form.tenantId = this.form.tenantId || this.userInfo.tenantId || website.tenantId;
- if (this.form.isPlatformCompany === undefined || this.form.isPlatformCompany === null) {
- this.form.isPlatformCompany = 0;
- }
- if (this.form.isBusinessDept === undefined || this.form.isBusinessDept === null) {
- this.form.isBusinessDept = 0;
- }
- this.syncKingdeeDeptCodeDisplay(this.form.isPlatformCompany);
+ this.form.isPlatformCompany = this.normalizeYesNoFlag(this.form.isPlatformCompany);
+ this.form.isBusinessDept = this.normalizeYesNoFlag(this.form.isBusinessDept);
this.initData(this.form.tenantId);
this.loadParentDeptCode(this.form.parentId);
+ done();
+ return;
}
if (type === 'edit') {
this.initData(this.form.tenantId || this.userInfo.tenantId || website.tenantId);
}
if (['edit', 'view'].includes(type)) {
- getDept(this.form.id).then(res => {
- this.form = Object.assign(res.data.data, {
- hasChildren: this.form.hasChildren,
+ // 等详情返回后再打开弹窗,避免异步回填覆盖用户已改值;就地赋值保持 Avue 表单引用
+ getDept(this.form.id)
+ .then(res => {
+ const detail = res.data.data || {};
+ Object.assign(this.form, detail, {
+ hasChildren: this.form.hasChildren,
+ isPlatformCompany: this.normalizeYesNoFlag(detail.isPlatformCompany),
+ isBusinessDept: this.normalizeYesNoFlag(detail.isBusinessDept),
+ });
+ if (this.form.parentId === '0') {
+ this.form.parentId = '';
+ }
+ this.loadParentDeptCode(this.form.parentId);
+ if (Object.prototype.hasOwnProperty.call(this.form, 'leaderId')) {
+ const splitResult = func.split(this.form.leaderId);
+ this.form.leaderId = Array.isArray(splitResult) ? splitResult : [];
+ }
+ done();
+ })
+ .catch(() => {
+ done();
});
- if (this.form.parentId === '0') {
- this.form.parentId = '';
- }
- this.syncKingdeeDeptCodeDisplay(this.form.isPlatformCompany);
- this.loadParentDeptCode(this.form.parentId);
- if (this.form.hasOwnProperty('leaderId')) {
- const splitResult = func.split(this.form.leaderId);
- this.form.leaderId = Array.isArray(splitResult) ? splitResult : [];
- }
- });
+ return;
}
done();
},