✨ 合同结算币种联动计费与运单计价单位,并完善合同与派车交互

结算币种驱动计费单价单位及运单/计划/总单计价单位;同步合同编号预览、部门前缀配置、派车备注与查看弹窗等交互优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-10 00:53:17 +08:00
parent a1da644c49
commit eaece0209d
16 changed files with 867 additions and 158 deletions
+8
View File
@@ -90,3 +90,11 @@ export const expireStats = params =>
method: 'get', method: 'get',
params, params,
}); });
/** 按合同负责部门预览下一合同编号 */
export const nextContractNo = organizationId =>
request({
url: `${baseUrl}/next-contract-no`,
method: 'get',
params: { organizationId },
});
+1
View File
@@ -117,6 +117,7 @@ export const contractCategoryOptions = [
export const signTypeOptions = [ export const signTypeOptions = [
{ label: '新签', value: '新签' }, { label: '新签', value: '新签' },
{ label: '续签', value: '续签' }, { label: '续签', value: '续签' },
{ label: '补充协议', value: '补充协议' },
]; ];
export const contractStageOptions = [ export const contractStageOptions = [
+38 -9
View File
@@ -6,6 +6,7 @@ import {
contractStageOptions, contractStageOptions,
createCrudOption, createCrudOption,
effectiveTypeOptions, effectiveTypeOptions,
nonNegativeRule,
selectRule, selectRule,
signTypeOptions, signTypeOptions,
textRule, textRule,
@@ -61,13 +62,14 @@ export const config = {
['contractNo', '合同编号'], ['contractNo', '合同编号'],
['contractName', '合同名称'], ['contractName', '合同名称'],
['projectName', '所属项目'], ['projectName', '所属项目'],
['organizationName', '所属组织'], ['organizationName', '合同负责部门'],
['contractCategory', '合同类别'], ['contractCategory', '合同类别'],
['signType', '签约类型'], ['signType', '签约类型'],
['partyA', '甲方'], ['partyA', '甲方'],
['partyB', '乙方'], ['partyB', '乙方'],
['handlerUserName', '经办人'], ['handlerUserName', '经办人'],
['signDate', '签订日期'], ['signDate', '签订日期'],
['applyDate', '申请日期'],
['contractFormat', '合同格式'], ['contractFormat', '合同格式'],
['legalSealFlag', '是否需要加盖法人章'], ['legalSealFlag', '是否需要加盖法人章'],
['copyCount', '一式(份)'], ['copyCount', '一式(份)'],
@@ -76,7 +78,8 @@ export const config = {
['templateFlag', '是否范本'], ['templateFlag', '是否范本'],
['originalContractNo', '原件合同编号'], ['originalContractNo', '原件合同编号'],
['electronicSealFlag', '是否电子章'], ['electronicSealFlag', '是否电子章'],
['remark', '备注', 2], ['mainContent', '合同主要内容', 2],
['remark', '其他事项说明', 2],
], ],
}, },
{ {
@@ -294,17 +297,17 @@ export const option = createCrudOption([
display: false, display: false,
}, },
{ {
label: '所属组织', label: '合同负责部门',
prop: 'organizationName', prop: 'organizationName',
search: true, search: true,
searchOrder: 3, searchOrder: 3,
searchslot: true, searchslot: true,
searchPlaceholder: '请选择 所属组织', searchPlaceholder: '请选择 合同负责部门',
formslot: true, formslot: true,
order: 120, order: 120,
dicData: [], dicData: [],
minWidth: 150, minWidth: 150,
rules: selectRule('所属组织'), rules: selectRule('合同负责部门'),
}, },
{ {
label: '合同类别', label: '合同类别',
@@ -435,6 +438,17 @@ export const option = createCrudOption([
minWidth: 130, minWidth: 130,
rules: [{ required: true, message: '请选择签订日期', trigger: 'change' }], rules: [{ required: true, message: '请选择签订日期', trigger: 'change' }],
}, },
{
label: '申请日期',
prop: 'applyDate',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
sortable: true,
minWidth: 130,
addDisplay: false,
editDisplay: false,
},
{ {
label: '结算类型', label: '结算类型',
prop: 'settlementMode', prop: 'settlementMode',
@@ -468,11 +482,10 @@ export const option = createCrudOption([
{ label: '否', value: 0 }, { label: '否', value: 0 },
{ label: '是', value: 1 }, { label: '是', value: 1 },
], ],
value: 0, clearable: true,
hide: true, hide: true,
order: 70, order: 70,
minWidth: 100, minWidth: 100,
rules: selectRule('是否需要加盖法人章'),
}, },
{ {
label: '一式(份)', label: '一式(份)',
@@ -500,6 +513,10 @@ export const option = createCrudOption([
hide: true, hide: true,
order: 45, order: 45,
minWidth: 130, minWidth: 130,
rules: [
{ required: true, message: '请输入合同金额', trigger: 'blur' },
nonNegativeRule('合同金额'),
],
}, },
{ {
label: '是否范本', label: '是否范本',
@@ -617,7 +634,19 @@ export const option = createCrudOption([
minWidth: 110, minWidth: 110,
}, },
{ {
label: '备注', label: '合同主要内容',
prop: 'mainContent',
type: 'textarea',
span: 24,
minRows: 4,
hide: true,
order: 41,
maxlength: 1000,
showWordLimit: true,
rules: textRule('合同主要内容', 1000),
},
{
label: '其他事项说明',
prop: 'remark', prop: 'remark',
type: 'textarea', type: 'textarea',
span: 24, span: 24,
@@ -626,7 +655,7 @@ export const option = createCrudOption([
order: 40, order: 40,
maxlength: 2000, maxlength: 2000,
showWordLimit: true, showWordLimit: true,
rules: textRule('备注', 2000), rules: textRule('其他事项说明', 2000),
}, },
{ {
label: '', label: '',
@@ -101,6 +101,7 @@ export const option = {
minWidth: 120, minWidth: 120,
addDisplay: false, addDisplay: false,
editDisplay: false, editDisplay: false,
viewDisplay: false,
}, },
{ {
label: '当前节点', label: '当前节点',
@@ -108,6 +109,7 @@ export const option = {
minWidth: 120, minWidth: 120,
addDisplay: false, addDisplay: false,
editDisplay: false, editDisplay: false,
viewDisplay: false,
}, },
{ {
label: '当前处理人', label: '当前处理人',
@@ -115,6 +117,7 @@ export const option = {
minWidth: 130, minWidth: 130,
addDisplay: false, addDisplay: false,
editDisplay: false, editDisplay: false,
viewDisplay: false,
}, },
{ {
label: '创建人', label: '创建人',
@@ -122,6 +125,7 @@ export const option = {
minWidth: 120, minWidth: 120,
addDisplay: false, addDisplay: false,
editDisplay: false, editDisplay: false,
viewDisplay: false,
}, },
{ {
label: '创建时间', label: '创建时间',
@@ -132,6 +136,7 @@ export const option = {
minWidth: 170, minWidth: 170,
addDisplay: false, addDisplay: false,
editDisplay: false, editDisplay: false,
viewDisplay: false,
}, },
// ========== 新增/编辑表单专用(不进表格)========== // ========== 新增/编辑表单专用(不进表格)==========
{ {
@@ -165,6 +170,7 @@ export const option = {
type: 'textarea', type: 'textarea',
hide: true, hide: true,
span: 24, span: 24,
minRows: 2,
maxlength: 200, maxlength: 200,
showWordLimit: true, showWordLimit: true,
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }], rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
+109
View File
@@ -0,0 +1,109 @@
const CURRENCY_NAME_MAP = {
CNY: '人民币',
RMB: '人民币',
USD: '美元',
EUR: '欧元',
JPY: '日元',
GBP: '英镑',
};
const CURRENCY_UNIT_MAP = {
CNY: '元',
RMB: '元',
USD: '美元',
EUR: '欧元',
JPY: '日元',
GBP: '英镑',
};
const CURRENCY_NAME_TO_UNIT = {
人民币: '元',
美元: '美元',
欧元: '欧元',
日元: '日元',
英镑: '英镑',
};
/**
* 归一化币种代码(CNY → RMB)
*/
export function normalizeCurrencyCode(value, fallback = 'RMB') {
const raw =
typeof value === 'object'
? value?.dictKey || value?.value || value?.code || value?.dictValue || ''
: value;
const normalized = String(raw || fallback)
.trim()
.toUpperCase();
const code = normalized.includes(' - ') ? normalized.split(' - ')[0].trim() : normalized;
if (!code) return fallback;
return code === 'CNY' ? 'RMB' : code;
}
/**
* 币种中文名称
*/
export function currencyName(value) {
const text = String(value || '');
if (text.includes(' - ')) return text.slice(text.indexOf(' - ') + 3);
return CURRENCY_NAME_MAP[text.toUpperCase()] || text;
}
/**
* 金额单位文案:RMB/CNY → 元,USD → 美元
*/
export function currencyUnit(value) {
const text = String(value || '');
let code = text;
if (text.includes(' - ')) {
code = text.split(' - ')[0].trim();
}
const upperCode = code.toUpperCase();
if (CURRENCY_UNIT_MAP[upperCode]) {
return CURRENCY_UNIT_MAP[upperCode];
}
if (CURRENCY_NAME_TO_UNIT[code]) {
return CURRENCY_NAME_TO_UNIT[code];
}
return text || '元';
}
/**
* 将计价单位前缀替换为当前币种单位,如 元/吨 → 美元/吨
*/
export function remapPriceUnitCurrency(priceUnit, currency) {
const text = String(priceUnit || '').trim();
if (!text || !text.includes('/')) return text;
const suffix = text.slice(text.indexOf('/'));
return `${currencyUnit(currency)}${suffix}`;
}
/**
* 按数量单位生成默认计价单位
*/
export function resolvePriceUnitByQuantityUnit(quantityUnit, currency = 'RMB') {
const unit = currencyUnit(currency);
return (
{
吨: `${unit}/吨`,
方: `${unit}/立方`,
件: `${unit}/件`,
}[String(quantityUnit || '').trim()] || ''
);
}
/**
* 将字典中的计价单位选项映射到当前币种
*/
export function mapFeeUnitOptions(options = [], currency = 'RMB') {
const unit = currencyUnit(currency);
return (options || []).map(item => {
const label = String(item.label || item.value || '');
const value = String(item.value || item.label || '');
return {
...item,
label: label.replace(/^元/, unit).replace(/^人民币/, unit),
value: value.replace(/^元/, unit).replace(/^人民币/, unit),
};
});
}
@@ -171,7 +171,7 @@
:label="item.label" :label="item.label"
:value="item.value" /></el-select></template :value="item.value" /></el-select></template
></el-table-column> ></el-table-column>
<el-table-column label="单价(元)" width="180" <el-table-column :label="unitPriceColumnLabel" width="180"
><template #default="{ row }" ><template #default="{ row }"
><div v-if="readonly && usesRangeUnitPrice(row)" class="limit-list"> ><div v-if="readonly && usesRangeUnitPrice(row)" class="limit-list">
<span v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex">{{ <span v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex">{{
@@ -379,6 +379,7 @@ import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
import { InfoFilled } from '@element-plus/icons-vue'; import { InfoFilled } from '@element-plus/icons-vue';
import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary } from '@/api/system/dictbiz';
import SectionCard from '@/components/section-card/main.vue'; import SectionCard from '@/components/section-card/main.vue';
import { currencyUnit } from '@/utils/currency';
const clone = value => JSON.parse(JSON.stringify(value)); const clone = value => JSON.parse(JSON.stringify(value));
/** 计费要素与计量单位维度的对应关系(这些要素可选计费单位) */ /** 计费要素与计量单位维度的对应关系(这些要素可选计费单位) */
@@ -431,6 +432,7 @@ export default {
value: { type: Object, default: () => ({}) }, value: { type: Object, default: () => ({}) },
index: { type: Number, default: -1 }, index: { type: Number, default: -1 },
readonly: Boolean, readonly: Boolean,
settlementCurrency: { type: String, default: 'RMB' },
}, },
emits: ['update:modelValue', 'save'], emits: ['update:modelValue', 'save'],
data() { data() {
@@ -500,6 +502,9 @@ export default {
emitPath: true, emitPath: true,
}; };
}, },
unitPriceColumnLabel() {
return `单价(${currencyUnit(this.settlementCurrency)})`;
},
}, },
mounted() { mounted() {
this.loadDictionaries(); this.loadDictionaries();
@@ -173,13 +173,41 @@ import { downloadFileByUrl } from '@/utils/util';
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz'; import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
const CONTRACT_ATTACHMENT_TYPE_OTHER = '其他文件'; const CONTRACT_ATTACHMENT_TYPE_OTHER = '其他文件';
/** 用印材料历史取值兼容别名 */
export const CONTRACT_SEAL_MATERIAL_LOCATION = '用印材料';
export const CONTRACT_SEAL_MATERIAL_TYPE_ALIASES = {
合同文件: CONTRACT_SEAL_MATERIAL_LOCATION,
};
/** 佐证材料附件位置(历史:其它附件) */
export const CONTRACT_SUPPORTING_ATTACHMENT_LOCATION = '附件(佐证材料等)';
export const CONTRACT_ATTACHMENT_TYPE_DICT = 'contract_attachment_type';
export const CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT = 'contract_supporting_attachment_type';
export const isContractSupportingAttachmentLocation = location => {
const text = String(location || '');
return (
text === CONTRACT_SUPPORTING_ATTACHMENT_LOCATION ||
text === '其它附件' ||
text === '其他附件'
);
};
export const resolveAttachmentTypeDictCode = (location, fallback = CONTRACT_ATTACHMENT_TYPE_DICT) =>
isContractSupportingAttachmentLocation(location)
? CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT
: fallback || CONTRACT_ATTACHMENT_TYPE_DICT;
export const defaultContractAttachmentTypeOptions = [ export const defaultContractAttachmentTypeOptions = [
{ label: '合同文件', value: '合同文件' }, { label: '用印材料', value: '用印材料' },
{ label: '双章归档文件', value: '双章归档文件' }, { label: '双章归档文件', value: '双章归档文件' },
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER }, { label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
]; ];
/** @deprecated 兼容旧引用,实际选项由业务字典 contract_attachment_type 动态加载 */ export const defaultContractSupportingAttachmentTypeOptions = [
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
];
/** @deprecated 兼容旧引用,实际选项由业务字典动态加载 */
export const contractAttachmentTypeOptions = defaultContractAttachmentTypeOptions; export const contractAttachmentTypeOptions = defaultContractAttachmentTypeOptions;
export const isContractSealMaterialLocation = location => {
const text = String(location || '');
return text === CONTRACT_SEAL_MATERIAL_LOCATION || text === '合同文件';
};
export const attachmentFileTypes = [ export const attachmentFileTypes = [
'pdf', 'pdf',
'bmp', 'bmp',
@@ -234,8 +262,11 @@ const isOtherAttachmentTypeOption = item => {
const attachmentTypeKeywords = fileType => { const attachmentTypeKeywords = fileType => {
const text = String(fileType || ''); const text = String(fileType || '');
if (text.includes('双章')) return ['双章归档文件', '双章归档', '双章']; if (text.includes('双章')) return ['双章归档文件', '双章归档', '双章'];
if (text.includes('合同') && !text.includes('其他') && !text.includes('其它')) { if (
return ['合同文件', '合同']; text.includes('用印') ||
(text.includes('合同') && !text.includes('其他') && !text.includes('其它'))
) {
return ['用印材料', '合同文件', '用印', '合同'];
} }
if (isOtherAttachmentTypeOption({ label: text, value: text })) return []; if (isOtherAttachmentTypeOption({ label: text, value: text })) return [];
return text ? [text] : []; return text ? [text] : [];
@@ -254,8 +285,13 @@ export const resolveContractAttachmentType = (
const list = options.length ? options : defaultContractAttachmentTypeOptions; const list = options.length ? options : defaultContractAttachmentTypeOptions;
const otherType = resolveOtherAttachmentType(list); const otherType = resolveOtherAttachmentType(list);
const values = list.map(item => item.value); const values = list.map(item => item.value);
if (values.includes(row.fileType)) return row.fileType; const rawFileType = String(row.fileType || '');
const matchedByLabel = list.find(item => String(item.label) === String(row.fileType)); const aliasedFileType = CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawFileType] || rawFileType;
if (values.includes(aliasedFileType)) return aliasedFileType;
if (values.includes(rawFileType)) return rawFileType;
const matchedByLabel = list.find(
item => String(item.label) === aliasedFileType || String(item.label) === rawFileType
);
if (matchedByLabel) return matchedByLabel.value; if (matchedByLabel) return matchedByLabel.value;
const fileName = normalizeAttachmentTypeText(attachmentName(row)); const fileName = normalizeAttachmentTypeText(attachmentName(row));
if (!fileName) return otherType; if (!fileName) return otherType;
@@ -283,17 +319,32 @@ export const normalizeContractFileRows = (rows, options = defaultContractAttachm
description: row.description || '', description: row.description || '',
})); }));
const mapDictOptions = response => { const mapDictOptions = (response, dictCode = CONTRACT_ATTACHMENT_TYPE_DICT) => {
const data = response?.data?.data || response?.data || []; const data = response?.data?.data || response?.data || [];
const records = Array.isArray(data) ? data : data.records || []; const records = Array.isArray(data) ? data : data.records || [];
const applySealAlias = dictCode === CONTRACT_ATTACHMENT_TYPE_DICT;
return records return records
.map(item => ({ .map(item => {
label: item.dictValue, const rawLabel = String(item.dictValue || '');
value: item.dictKey, const rawValue = String(item.dictKey || '');
})) const label = applySealAlias
? CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawLabel] ||
(rawLabel === '合同文件' ? CONTRACT_SEAL_MATERIAL_LOCATION : rawLabel)
: rawLabel;
const value = applySealAlias
? CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawValue] ||
(rawValue === '合同文件' ? CONTRACT_SEAL_MATERIAL_LOCATION : rawValue)
: rawValue;
return { label, value };
})
.filter(item => item.label && item.value); .filter(item => item.label && item.value);
}; };
const defaultOptionsByDictCode = dictCode =>
dictCode === CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT
? [...defaultContractSupportingAttachmentTypeOptions]
: [...defaultContractAttachmentTypeOptions];
export default { export default {
name: 'ContractAttachmentSection', name: 'ContractAttachmentSection',
components: { UploadFilled }, components: { UploadFilled },
@@ -307,8 +358,13 @@ export default {
markApprovedOnUpload: Boolean, markApprovedOnUpload: Boolean,
useUploadDialog: Boolean, useUploadDialog: Boolean,
dialogOnly: Boolean, dialogOnly: Boolean,
attachmentLocation: { type: String, default: '合同文件' }, attachmentLocation: { type: String, default: CONTRACT_SEAL_MATERIAL_LOCATION },
attachmentLocationOptions: { type: Array, default: null }, attachmentLocationOptions: { type: Array, default: null },
/** 附件类型业务字典 code,默认用印材料字典;佐证材料传 contract_supporting_attachment_type */
attachmentTypeDictCode: {
type: String,
default: CONTRACT_ATTACHMENT_TYPE_DICT,
},
preview: { type: Function, default: null }, preview: { type: Function, default: null },
}, },
emits: ['update:rows', 'upload-to-location', 'upload-confirmed'], emits: ['update:rows', 'upload-to-location', 'upload-confirmed'],
@@ -317,8 +373,9 @@ export default {
selected: [], selected: [],
attachmentFileTypes, attachmentFileTypes,
contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions], contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions],
activeAttachmentTypeDictCode: CONTRACT_ATTACHMENT_TYPE_DICT,
uploadDialogVisible: false, uploadDialogVisible: false,
uploadDialogLocation: '合同文件', uploadDialogLocation: CONTRACT_SEAL_MATERIAL_LOCATION,
uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions), uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions),
uploadDialogFiles: [], uploadDialogFiles: [],
uploadDialogFileList: [], uploadDialogFileList: [],
@@ -337,48 +394,91 @@ export default {
defaultAttachmentType() { defaultAttachmentType() {
return resolveOtherAttachmentType(this.contractAttachmentTypeOptions); return resolveOtherAttachmentType(this.contractAttachmentTypeOptions);
}, },
resolvedAttachmentTypeDictCode() {
return this.attachmentTypeDictCode || CONTRACT_ATTACHMENT_TYPE_DICT;
},
resolvedLocationOptions() { resolvedLocationOptions() {
const options = (this.attachmentLocationOptions || []).filter(Boolean); const options = (this.attachmentLocationOptions || []).filter(Boolean);
if (options.length) return options; if (options.length) {
return ['合同文件', '其它附件']; return options.map(item => {
if (item === '合同文件') return CONTRACT_SEAL_MATERIAL_LOCATION;
if (item === '其它附件' || item === '其他附件') {
return CONTRACT_SUPPORTING_ATTACHMENT_LOCATION;
}
return item;
});
}
return [CONTRACT_SEAL_MATERIAL_LOCATION, CONTRACT_SUPPORTING_ATTACHMENT_LOCATION];
},
},
watch: {
attachmentTypeDictCode: {
immediate: false,
handler() {
this.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
},
}, },
}, },
created() { created() {
this.loadAttachmentTypeOptions(); this.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
}, },
methods: { methods: {
attachmentName, attachmentName,
loadAttachmentTypeOptions() { currentUploadUserName() {
return getBizDictionary({ code: 'contract_attachment_type' }) const raw = this.$store.getters.userInfo;
const userInfo = raw && !Array.isArray(raw) ? raw : {};
// 与顶部栏一致优先 real_name;用印材料 / 佐证材料共用
return (
userInfo.real_name ||
userInfo.realName ||
userInfo.nick_name ||
userInfo.nickName ||
userInfo.user_name ||
userInfo.userName ||
userInfo.account ||
''
);
},
loadAttachmentTypeOptions(dictCode = this.resolvedAttachmentTypeDictCode, alignRows = false) {
const code = dictCode || CONTRACT_ATTACHMENT_TYPE_DICT;
this.activeAttachmentTypeDictCode = code;
const fallbackOptions = defaultOptionsByDictCode(code);
return getBizDictionary({ code })
.then(res => { .then(res => {
const options = mapDictOptions(res); // 切换字典期间若已发起更新请求,忽略过期响应
if (this.activeAttachmentTypeDictCode !== code) return;
const options = mapDictOptions(res, code);
if (options.length) { if (options.length) {
// 字典缺少「其他文件」时补上,保证未匹配文件名可默认选中 // 字典缺少「其他文件」时补上,保证未匹配文件名可默认选中
this.contractAttachmentTypeOptions = options.some(isOtherAttachmentTypeOption) this.contractAttachmentTypeOptions = options.some(isOtherAttachmentTypeOption)
? options ? options
: [...options, { label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER }]; : [...options, { label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER }];
if ( } else {
!this.contractAttachmentTypeOptions.some( this.contractAttachmentTypeOptions = fallbackOptions;
item => String(item.value) === String(this.uploadDialogType) }
) if (
) { !this.contractAttachmentTypeOptions.some(
this.uploadDialogType = this.defaultAttachmentType; item => String(item.value) === String(this.uploadDialogType)
} )
// 字典加载后,把已有行的中文类型名对齐到 dictKey,保证下拉可选中 ) {
if (this.attachmentType && (this.rows || []).length) { this.uploadDialogType = this.defaultAttachmentType;
const aligned = (this.rows || []).map(row => ({ }
...row, // 字典加载后,把已有行的中文类型名对齐到 dictKey,保证下拉可选中
fileType: resolveContractAttachmentType(row, this.contractAttachmentTypeOptions), if (alignRows && this.attachmentType && (this.rows || []).length) {
})); const aligned = (this.rows || []).map(row => ({
const changed = aligned.some( ...row,
(row, index) => row.fileType !== this.rows[index]?.fileType fileType: resolveContractAttachmentType(row, this.contractAttachmentTypeOptions),
); }));
if (changed) this.update(aligned); const changed = aligned.some(
} (row, index) => row.fileType !== this.rows[index]?.fileType
);
if (changed) this.update(aligned);
} }
}) })
.catch(() => { .catch(() => {
// 字典加载失败时保留默认选项 if (this.activeAttachmentTypeDictCode !== code) return;
this.contractAttachmentTypeOptions = fallbackOptions;
this.uploadDialogType = this.defaultAttachmentType;
}); });
}, },
isRowLocked(row) { isRowLocked(row) {
@@ -388,13 +488,14 @@ export default {
this.$emit('update:rows', Array.isArray(rows) ? rows.map(item => ({ ...item })) : []); this.$emit('update:rows', Array.isArray(rows) ? rows.map(item => ({ ...item })) : []);
}, },
enrichUploadedRow(row, existing) { enrichUploadedRow(row, existing) {
const uploadUserName = this.$store.getters.userInfo?.realName || ''; const uploadUserName = this.currentUploadUserName();
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss'); const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
const next = { const next = {
...existing, ...existing,
...row, ...row,
description: row.description || existing?.description || '', description: row.description || existing?.description || '',
uploadUserName: existing?.uploadUserName || row.uploadUserName || uploadUserName, // 新上传行始终写入当前用户;已有行保留原上传人
uploadUserName: existing?.uploadUserName || uploadUserName || row.uploadUserName || '',
uploadTime: existing?.uploadTime || row.uploadTime || uploadTime, uploadTime: existing?.uploadTime || row.uploadTime || uploadTime,
approved: existing?.approved || row.approved || false, approved: existing?.approved || row.approved || false,
}; };
@@ -434,20 +535,38 @@ export default {
resolveTypeByLocation(location) { resolveTypeByLocation(location) {
const options = this.contractAttachmentTypeOptions || []; const options = this.contractAttachmentTypeOptions || [];
const locationText = String(location || ''); const locationText = String(location || '');
if (locationText.includes('其它') || locationText.includes('其他')) { if (
isContractSupportingAttachmentLocation(locationText) ||
locationText.includes('佐证') ||
locationText.includes('其它') ||
locationText.includes('其他')
) {
return this.defaultAttachmentType; return this.defaultAttachmentType;
} }
const matched = options.find( const matched = options.find(
item => item =>
String(item.value) === locationText || String(item.value) === locationText ||
String(item.label) === locationText || String(item.label) === locationText ||
String(item.value).includes('用印') ||
String(item.label).includes('用印') ||
String(item.value).includes('合同') || String(item.value).includes('合同') ||
String(item.label).includes('合同') String(item.label).includes('合同')
); );
return matched?.value || options[0]?.value || this.defaultAttachmentType; return matched?.value || options[0]?.value || this.defaultAttachmentType;
}, },
syncUploadDialogTypeByLocation() { syncUploadDialogTypeByLocation() {
this.uploadDialogType = this.resolveTypeByLocation(this.uploadDialogLocation); const dictCode = resolveAttachmentTypeDictCode(
this.uploadDialogLocation,
this.resolvedAttachmentTypeDictCode
);
const applyType = () => {
this.uploadDialogType = this.resolveTypeByLocation(this.uploadDialogLocation);
};
if (dictCode !== this.activeAttachmentTypeDictCode) {
this.loadAttachmentTypeOptions(dictCode, false).finally(applyType);
return;
}
applyType();
}, },
openUploadDialog() { openUploadDialog() {
const options = this.resolvedLocationOptions; const options = this.resolvedLocationOptions;
@@ -544,13 +663,13 @@ export default {
this.$message.warning('请选择附件类型'); this.$message.warning('请选择附件类型');
return; return;
} }
const uploadUserName = this.$store.getters.userInfo?.realName || ''; const uploadUserName = this.currentUploadUserName();
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss'); const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
const appended = this.uploadDialogFiles.map(row => ({ const appended = this.uploadDialogFiles.map(row => ({
...row, ...row,
fileType: this.uploadDialogType, fileType: this.uploadDialogType,
description: row.description || '', description: row.description || '',
uploadUserName: row.uploadUserName || uploadUserName, uploadUserName: uploadUserName || row.uploadUserName || '',
uploadTime: row.uploadTime || uploadTime, uploadTime: row.uploadTime || uploadTime,
...(this.markApprovedOnUpload ? { approved: true } : {}), ...(this.markApprovedOnUpload ? { approved: true } : {}),
})); }));
@@ -266,7 +266,7 @@
<el-col :span="6"> <el-col :span="6">
<el-form-item label="单价"> <el-form-item label="单价">
<el-input :model-value="freightGroupUnitPrice(group)" inputmode="decimal" placeholder="请输入" @input="value => handleFreightGroupUnitPriceInput(group, value)" @clear="() => handleFreightGroupUnitPriceInput(group, '')"> <el-input :model-value="freightGroupUnitPrice(group)" inputmode="decimal" placeholder="请输入" @input="value => handleFreightGroupUnitPriceInput(group, value)" @clear="() => handleFreightGroupUnitPriceInput(group, '')">
<template #append><el-select :model-value="freightGroupPriceUnit(group)" class="freight-unit-select" @change="value => handleFreightGroupPriceUnitChange(group, value)"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /><el-option label="元/方" value="元/方" /></el-select></template> <template #append><el-select :model-value="freightGroupPriceUnit(group)" class="freight-unit-select" @change="value => handleFreightGroupPriceUnitChange(group, value)"><el-option v-for="item in priceUnitOptions(route)" :key="item.value" :label="item.label" :value="item.value" /></el-select></template>
</el-input> </el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -318,6 +318,11 @@
import * as api from '@/api/business/master-order'; import * as api from '@/api/business/master-order';
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource'; import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
import { formatContractOptionLabel } from '@/utils/contract'; import { formatContractOptionLabel } from '@/utils/contract';
import {
currencyUnit,
remapPriceUnitCurrency,
resolvePriceUnitByQuantityUnit,
} from '@/utils/currency';
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit'; import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
import { isMobile } from '@/utils/validate'; import { isMobile } from '@/utils/validate';
@@ -429,8 +434,10 @@ export default {
}, },
async loadContractCurrency() { async loadContractCurrency() {
const directCurrency = this.master?.settlementCurrency || this.master?.settlementCurrencyCode || this.master?.currency; const directCurrency = this.master?.settlementCurrency || this.master?.settlementCurrencyCode || this.master?.currency;
const previousCurrency = this.master?.settlementCurrency;
if (!this.master?.projectId || !this.master?.contractId) { if (!this.master?.projectId || !this.master?.contractId) {
this.master.settlementCurrency = this.normalizeCurrencyValue(directCurrency || 'RMB'); this.master.settlementCurrency = this.normalizeCurrencyValue(directCurrency || 'RMB');
this.syncRouteCurrency(previousCurrency, this.master.settlementCurrency);
return; return;
} }
try { try {
@@ -444,6 +451,20 @@ export default {
this.master.customerContractPartyB = ''; this.master.customerContractPartyB = '';
this.master.settlementCurrency = 'RMB'; this.master.settlementCurrency = 'RMB';
} }
this.syncRouteCurrency(previousCurrency, this.master.settlementCurrency);
},
syncRouteCurrency(_previousCurrency, nextCurrency) {
const currency = this.normalizeCurrencyValue(nextCurrency || 'RMB');
(this.routes || []).forEach(route => {
route.currency = currency;
(route.freightItems || []).forEach(item => {
if (item.priceUnit) {
item.priceUnit = remapPriceUnitCurrency(item.priceUnit, currency);
} else if (item.quantityUnit) {
item.priceUnit = resolvePriceUnitByQuantityUnit(item.quantityUnit, currency);
}
});
});
}, },
dispatchRouteNodes() { dispatchRouteNodes() {
const routes = Array.isArray(this.master?.routes) ? this.master.routes : []; const routes = Array.isArray(this.master?.routes) ? this.master.routes : [];
@@ -618,8 +639,23 @@ export default {
const prices = (group.rows || []).map(item => String(item.unitPrice ?? '').trim()).filter(Boolean); const prices = (group.rows || []).map(item => String(item.unitPrice ?? '').trim()).filter(Boolean);
return !prices.length || prices.some(price => price !== prices[0]) ? '' : prices[0]; return !prices.length || prices.some(price => price !== prices[0]) ? '' : prices[0];
}, },
freightGroupPriceUnit(group = {}) { return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : ''); }, freightGroupPriceUnit(group = {}) {
if (group.rows?.[0]?.priceUnit) return group.rows[0].priceUnit;
if (!group.quantityUnit) return '';
return (
resolvePriceUnitByQuantityUnit(group.quantityUnit, this.master?.settlementCurrency || 'RMB') ||
`${currencyUnit(this.master?.settlementCurrency || 'RMB')}/${group.quantityUnit}`
);
},
freightGroupAmount(group = {}) { return (group.rows || []).reduce((sum, item) => sum + this.freightAmount(item), 0); }, freightGroupAmount(group = {}) { return (group.rows || []).reduce((sum, item) => sum + this.freightAmount(item), 0); },
priceUnitOptions(route = {}) {
const unit = currencyUnit(route.currency || this.master?.settlementCurrency || 'RMB');
return [
{ label: `${unit}/吨`, value: `${unit}/吨` },
{ label: `${unit}/件`, value: `${unit}/件` },
{ label: `${unit}/方`, value: `${unit}/方` },
];
},
handleFreightGroupUnitPriceInput(group, value) { handleFreightGroupUnitPriceInput(group, value) {
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.'); const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
const normalized = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer; const normalized = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
@@ -629,6 +665,10 @@ export default {
handleFreightGroupQuantityUnitChange(route, group, value) { handleFreightGroupQuantityUnitChange(route, group, value) {
(group.rows || []).forEach(item => { (group.rows || []).forEach(item => {
item.quantityUnit = value || ''; item.quantityUnit = value || '';
item.priceUnit = resolvePriceUnitByQuantityUnit(
value,
route.currency || this.master?.settlementCurrency || 'RMB'
) || item.priceUnit;
const goods = (route.goods || []).find(row => String(row.sourceIndex) === String(item.sourceIndex)); const goods = (route.goods || []).find(row => String(row.sourceIndex) === String(item.sourceIndex));
if (goods) goods.quantityUnit = value || ''; if (goods) goods.quantityUnit = value || '';
}); });
@@ -651,7 +691,7 @@ export default {
})), })),
}); });
}, },
currencyLabel(value) { return ({ CNY: '元', RMB: '元', USD: '美元', EUR: '欧元' })[value] || value || '元'; }, currencyLabel(value) { return currencyUnit(value); },
quantityNumber(value) { return Number(value || 0); }, quantityNumber(value) { return Number(value || 0); },
goodsKey(goods = {}, includeUnit = true) { goodsKey(goods = {}, includeUnit = true) {
return [ return [
@@ -841,6 +881,7 @@ export default {
removeGoods(route, index) { route.goods.splice(index, 1); this.syncFreightItems(route); }, removeGoods(route, index) { route.goods.splice(index, 1); this.syncFreightItems(route); },
syncFreightItems(route) { syncFreightItems(route) {
const oldItems = route.freightItems || []; const oldItems = route.freightItems || [];
const currency = route.currency || this.master?.settlementCurrency || 'RMB';
route.freightItems = (route.goods || []) route.freightItems = (route.goods || [])
.filter(item => item.sourceIndex !== undefined && item.sourceIndex !== '') .filter(item => item.sourceIndex !== undefined && item.sourceIndex !== '')
.map(item => { .map(item => {
@@ -852,7 +893,10 @@ export default {
quantity: item.dispatchQuantity || '', quantity: item.dispatchQuantity || '',
quantityUnit: item.quantityUnit || '', quantityUnit: item.quantityUnit || '',
unitPrice: old.unitPrice || '', unitPrice: old.unitPrice || '',
priceUnit: old.priceUnit || `元/${item.quantityUnit || '吨'}`, priceUnit:
old.priceUnit ||
resolvePriceUnitByQuantityUnit(item.quantityUnit || '吨', currency) ||
`${currencyUnit(currency)}/${item.quantityUnit || '吨'}`,
}; };
}); });
}, },
@@ -1744,11 +1744,11 @@
><el-select ><el-select
:model-value="dispatchFreightGroupPriceUnit(group)" :model-value="dispatchFreightGroupPriceUnit(group)"
class="transport-plan-page__dispatch-unit-select" class="transport-plan-page__dispatch-unit-select"
placeholder="元/吨" :placeholder="defaultPriceUnit"
@change="value => handleDispatchFreightGroupPriceUnitChange(group, value)" @change="value => handleDispatchFreightGroupPriceUnitChange(group, value)"
@visible-change="visible => visible && loadTaskFeeUnitOptions()" @visible-change="visible => visible && loadTaskFeeUnitOptions()"
><el-option ><el-option
v-for="item in taskFeeUnitOptions" v-for="item in taskFeeUnitSelectOptions"
:key="item.value" :key="item.value"
:label="item.label" :label="item.label"
:value="item.value" /></el-select :value="item.value" /></el-select
@@ -1969,17 +1969,17 @@
<el-select <el-select
v-model="dispatchItemForm.priceUnit" v-model="dispatchItemForm.priceUnit"
class="transport-plan-page__dispatch-unit-select" class="transport-plan-page__dispatch-unit-select"
placeholder="元/吨" :placeholder="defaultPriceUnit"
:loading="taskFeeUnitLoading" :loading="taskFeeUnitLoading"
@visible-change="visible => visible && loadTaskFeeUnitOptions()" @visible-change="visible => visible && loadTaskFeeUnitOptions()"
> >
<el-option <el-option
v-if="!taskFeeUnitOptions.some(item => item.value === '元/吨')" v-if="!taskFeeUnitSelectOptions.some(item => item.value === defaultPriceUnit)"
label="元/吨" :label="defaultPriceUnit"
value="元/吨" :value="defaultPriceUnit"
/> />
<el-option <el-option
v-for="item in taskFeeUnitOptions" v-for="item in taskFeeUnitSelectOptions"
:key="item.value" :key="item.value"
:label="item.label" :label="item.label"
:value="item.value" :value="item.value"
@@ -2741,6 +2741,12 @@ import { addressTypeOptions } from '@/option/base/common-address';
import { packageOptions } from '@/option/business/common'; import { packageOptions } from '@/option/business/common';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { getToken, isAdministrator } from '@/utils/auth'; import { getToken, isAdministrator } from '@/utils/auth';
import {
currencyUnit,
mapFeeUnitOptions,
remapPriceUnitCurrency,
resolvePriceUnitByQuantityUnit,
} from '@/utils/currency';
import { formatContractOptionLabel } from '@/utils/contract'; import { formatContractOptionLabel } from '@/utils/contract';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search'; import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit'; import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
@@ -3612,6 +3618,22 @@ export default {
const value = this.dispatchItemForm.freightCurrency || 'RMB'; const value = this.dispatchItemForm.freightCurrency || 'RMB';
return this.currencyRemark(value); return this.currencyRemark(value);
}, },
taskFeeUnitSelectOptions() {
const currency =
this.dispatchItemForm.freightCurrency ||
this.dispatchContractCurrency ||
this.form.settlementCurrency ||
'RMB';
return mapFeeUnitOptions(this.taskFeeUnitOptions, currency);
},
defaultPriceUnit() {
return `${currencyUnit(
this.dispatchItemForm.freightCurrency ||
this.dispatchContractCurrency ||
this.form.settlementCurrency ||
'RMB'
)}/吨`;
},
isAdmin() { isAdmin() {
return isAdministrator(this.userInfo); return isAdministrator(this.userInfo);
}, },
@@ -4806,19 +4828,49 @@ export default {
contract.customerNames || contract.customerNames ||
this.form.customerName || this.form.customerName ||
''; '';
const nextCurrency = this.getContractCurrency(contract);
this.remapTransportPlanPriceUnits(nextCurrency);
this.$refs.crud?.validateField?.('contractName'); this.$refs.crud?.validateField?.('contractName');
}, },
remapTransportPlanPriceUnits(currency) {
if (this.form.priceUnit) {
this.form.priceUnit = remapPriceUnitCurrency(this.form.priceUnit, currency);
}
if (this.dispatchItemForm.priceUnit) {
this.dispatchItemForm.priceUnit = remapPriceUnitCurrency(
this.dispatchItemForm.priceUnit,
currency
);
}
this.dispatchItemForm.freightCurrency = currency;
(this.dispatchItemCargoRows || []).forEach(cargo => {
if (cargo.priceUnit) {
cargo.priceUnit = remapPriceUnitCurrency(cargo.priceUnit, currency);
}
});
(this.transportCargoRows || []).forEach(cargo => {
if (cargo.priceUnit) {
cargo.priceUnit = remapPriceUnitCurrency(cargo.priceUnit, currency);
}
});
},
loadDispatchContractCurrency(plan = {}) { loadDispatchContractCurrency(plan = {}) {
const findContract = () => const findContract = () =>
this.contractOptions.find(item => String(item.id) === String(plan.contractId)) || this.contractOptions.find(item => String(item.id) === String(plan.contractId)) ||
this.contractOptions.find(item => item.contractName === plan.contractName); this.contractOptions.find(item => item.contractName === plan.contractName);
const applyCurrency = contract => { const applyCurrency = contract => {
const previousCurrency = this.dispatchContractCurrency;
this.dispatchContractCurrency = this.getContractCurrency( this.dispatchContractCurrency = this.getContractCurrency(
contract && contract &&
(contract.settlementCurrency || contract.settlementCurrencyCode || contract.currency) (contract.settlementCurrency || contract.settlementCurrencyCode || contract.currency)
? contract ? contract
: { settlementCurrency: plan.settlementCurrency } : { settlementCurrency: plan.settlementCurrency }
); );
if (previousCurrency && previousCurrency !== this.dispatchContractCurrency) {
this.remapTransportPlanPriceUnits(this.dispatchContractCurrency);
} else {
this.dispatchItemForm.freightCurrency = this.dispatchContractCurrency;
}
return this.dispatchContractCurrency; return this.dispatchContractCurrency;
}; };
const current = findContract(); const current = findContract();
@@ -5217,7 +5269,7 @@ export default {
}, },
addTransportCargoRow(index = -1, row = {}) { addTransportCargoRow(index = -1, row = {}) {
const nextRow = this.normalizeTransportCargoRow({ const nextRow = this.normalizeTransportCargoRow({
priceUnit: this.form.priceUnit || '元/吨', priceUnit: this.form.priceUnit || this.defaultPriceUnit,
...row, ...row,
}); });
if ( if (
@@ -7680,7 +7732,10 @@ export default {
this.dispatchItemCargoRows = this.dispatchItemCargoRows.map((cargo, itemIndex) => ({ this.dispatchItemCargoRows = this.dispatchItemCargoRows.map((cargo, itemIndex) => ({
...cargo, ...cargo,
unitPrice: cargo.unitPrice || freightInfo.freightItems[itemIndex]?.unitPrice || '', unitPrice: cargo.unitPrice || freightInfo.freightItems[itemIndex]?.unitPrice || '',
priceUnit: cargo.priceUnit || freightInfo.freightItems[itemIndex]?.priceUnit || '元/吨', priceUnit:
cargo.priceUnit ||
freightInfo.freightItems[itemIndex]?.priceUnit ||
this.defaultPriceUnit,
freightAmount: freightAmount:
cargo.freightAmount || freightInfo.freightItems[itemIndex]?.freightAmount || '', cargo.freightAmount || freightInfo.freightItems[itemIndex]?.freightAmount || '',
})); }));
@@ -7929,7 +7984,7 @@ export default {
return value; return value;
}, },
addDispatchCargoRow(index = -1) { addDispatchCargoRow(index = -1) {
const nextRow = this.normalizeTransportCargoRow({ priceUnit: '元/吨' }); const nextRow = this.normalizeTransportCargoRow({ priceUnit: this.defaultPriceUnit });
if (index > -1) this.dispatchItemCargoRows.splice(index + 1, 0, nextRow); if (index > -1) this.dispatchItemCargoRows.splice(index + 1, 0, nextRow);
else this.dispatchItemCargoRows.push(nextRow); else this.dispatchItemCargoRows.push(nextRow);
}, },
@@ -7973,7 +8028,14 @@ export default {
return prices[0]; return prices[0];
}, },
dispatchFreightGroupPriceUnit(group = {}) { dispatchFreightGroupPriceUnit(group = {}) {
return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : ''); if (group.rows?.[0]?.priceUnit) return group.rows[0].priceUnit;
if (!group.quantityUnit) return '';
return (
resolvePriceUnitByQuantityUnit(
group.quantityUnit,
this.dispatchItemForm.freightCurrency || this.dispatchContractCurrency || 'RMB'
) || `${currencyUnit(this.dispatchContractCurrency || 'RMB')}/${group.quantityUnit}`
);
}, },
dispatchFreightGroupAmount(group = {}) { dispatchFreightGroupAmount(group = {}) {
const amount = (group.rows || []).reduce( const amount = (group.rows || []).reduce(
@@ -270,15 +270,22 @@
prop="mileage" prop="mileage"
label="里程(km)" label="里程(km)"
width="100" width="100"
/><el-table-column prop="unitPrice" label="单价" width="90" /><el-table-column >
prop="freight" <template #default="{ row }">{{ formatOptionalNumber(row.mileage) }}</template>
label="运费" </el-table-column>
width="100" <el-table-column prop="unitPrice" label="单价" width="90">
/><el-table-column prop="otherFeeTotal" label="其他费用合计" width="120" /><el-table-column <template #default="{ row }">{{ formatOptionalNumber(row.unitPrice) }}</template>
prop="freightTotal" </el-table-column>
label="运费合计" <el-table-column prop="freight" label="运费" width="100">
width="100" <template #default="{ row }">{{ formatOptionalNumber(row.freight) }}</template>
/><el-table-column </el-table-column>
<el-table-column prop="otherFeeTotal" label="其他费用合计" width="120">
<template #default="{ row }">{{ formatOptionalNumber(row.otherFeeTotal) }}</template>
</el-table-column>
<el-table-column prop="freightTotal" label="运费合计" width="100">
<template #default="{ row }">{{ formatOptionalNumber(row.freightTotal) }}</template>
</el-table-column>
<el-table-column
prop="actualStartDate" prop="actualStartDate"
label="实际发货时间" label="实际发货时间"
width="160" width="160"
@@ -1107,7 +1114,7 @@ const loadDetails = async () => {
current: detailPage.current, current: detailPage.current,
size: detailPage.size, size: detailPage.size,
}); });
details.value = res.data?.data?.records || []; details.value = (res.data?.data?.records || []).map(normalizeDetailRow);
detailPage.total = res.data?.data?.total || 0; detailPage.total = res.data?.data?.total || 0;
}; };
const openCreate = () => prepareCreateForm(true); const openCreate = () => prepareCreateForm(true);
@@ -1156,7 +1163,7 @@ const loadBatchRows = async batchId => {
if (!batchId) return; if (!batchId) return;
const res = await api.getImportDetails({ batchId, current: 1, size: 9999 }); const res = await api.getImportDetails({ batchId, current: 1, size: 9999 });
rows.value = extractRecords(res).map(({ id, ...item }, index) => ({ rows.value = extractRecords(res).map(({ id, ...item }, index) => ({
...item, ...normalizeDetailRow(item),
_key: `${id || 'row'}-${index}`, _key: `${id || 'row'}-${index}`,
batchNo: item.batchNo || form.batchNo, batchNo: item.batchNo || form.batchNo,
})); }));
@@ -1300,10 +1307,22 @@ const carrierChange = value => {
}; };
const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? ''; const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? '';
const OPTIONAL_NUMBER_EMPTY_SENTINELS = [-1]; const OPTIONAL_NUMBER_EMPTY_SENTINELS = [-1];
const OPTIONAL_NUMBER_PROPS = ['mileage', 'unitPrice', 'freight', 'otherFeeTotal', 'freightTotal'];
const normalizeOptionalImportNumber = (value, emptySentinels = OPTIONAL_NUMBER_EMPTY_SENTINELS) => { const normalizeOptionalImportNumber = (value, emptySentinels = OPTIONAL_NUMBER_EMPTY_SENTINELS) => {
if (!String(value ?? '').trim()) return null; if (!String(value ?? '').trim()) return null;
return emptySentinels.some(sentinel => Number(value) === sentinel) ? null : value; return emptySentinels.some(sentinel => Number(value) === sentinel) ? null : value;
}; };
const formatOptionalNumber = value => {
const normalized = normalizeOptionalImportNumber(value);
return normalized === null || normalized === undefined ? '' : normalized;
};
const normalizeDetailRow = row => {
const next = { ...(row || {}) };
OPTIONAL_NUMBER_PROPS.forEach(prop => {
next[prop] = formatOptionalNumber(next[prop]);
});
return next;
};
const normalizeImportRow = row => ({ const normalizeImportRow = row => ({
...row, ...row,
mileage: normalizeOptionalImportNumber( mileage: normalizeOptionalImportNumber(
@@ -1083,7 +1083,7 @@
@visible-change="visible => visible && loadTaskFeeUnitOptions()" @visible-change="visible => visible && loadTaskFeeUnitOptions()"
> >
<el-option <el-option
v-for="item in taskFeeUnitOptions" v-for="item in taskFeeUnitSelectOptions"
:key="item.value" :key="item.value"
:label="item.label" :label="item.label"
:value="item.value" :value="item.value"
@@ -1416,7 +1416,7 @@
@visible-change="visible => visible && loadTaskFeeUnitOptions()" @visible-change="visible => visible && loadTaskFeeUnitOptions()"
> >
<el-option <el-option
v-for="item in taskFeeUnitOptions" v-for="item in taskFeeUnitSelectOptions"
:key="item.value" :key="item.value"
:label="item.label" :label="item.label"
:value="item.value" :value="item.value"
@@ -3642,6 +3642,12 @@ import { addressTypeOptions } from '@/option/base/common-address';
import { packageOptions } from '@/option/business/common'; import { packageOptions } from '@/option/business/common';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { getToken, isAdministrator } from '@/utils/auth'; import { getToken, isAdministrator } from '@/utils/auth';
import {
currencyUnit,
mapFeeUnitOptions,
remapPriceUnitCurrency,
resolvePriceUnitByQuantityUnit,
} from '@/utils/currency';
import { formatContractOptionLabel } from '@/utils/contract'; import { formatContractOptionLabel } from '@/utils/contract';
import { wgs84ToGcj02 } from '@/utils/coordinate'; import { wgs84ToGcj02 } from '@/utils/coordinate';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
@@ -4185,6 +4191,8 @@ export default {
}, },
selectionList: [], selectionList: [],
attachmentUploadMode: false, attachmentUploadMode: false,
/** 复制来源运单业务状态(表单字段 addDisplay:false,提交时需显式带回) */
copySourceBusinessStatus: '',
standaloneFormKey: '', standaloneFormKey: '',
// 实例所属路由的路径快照:用于判断当前 $route 是否还是「本实例自己的路由」。 // 实例所属路由的路径快照:用于判断当前 $route 是否还是「本实例自己的路由」。
// 必须在 data 里初始化,不能放 created —— Vue 的 Options API 执行顺序是 // 必须在 data 里初始化,不能放 created —— Vue 的 Options API 执行顺序是
@@ -4581,6 +4589,12 @@ export default {
if (!currency) return ''; if (!currency) return '';
return this.currencyRemark(currency); return this.currencyRemark(currency);
}, },
taskFeeUnitSelectOptions() {
return mapFeeUnitOptions(this.taskFeeUnitOptions, this.taskFreightCurrency || 'RMB');
},
defaultPriceUnit() {
return `${currencyUnit(this.taskFreightCurrency || 'RMB')}/吨`;
},
isAdmin() { isAdmin() {
return isAdministrator(this.userInfo); return isAdministrator(this.userInfo);
}, },
@@ -5175,7 +5189,7 @@ export default {
quantity: goods.quantity ?? goods.cargoQuantity ?? goods.goodsQuantity ?? '', quantity: goods.quantity ?? goods.cargoQuantity ?? goods.goodsQuantity ?? '',
quantityUnit: goods.quantityUnit || goods.goodsQuantityUnit || goods.unit || '吨', quantityUnit: goods.quantityUnit || goods.goodsQuantityUnit || goods.unit || '吨',
unitPrice: goods.unitPrice, unitPrice: goods.unitPrice,
priceUnit: goods.priceUnit || '元/吨', priceUnit: goods.priceUnit || this.defaultPriceUnit,
}); });
}, },
canOperation(operation, row) { canOperation(operation, row) {
@@ -6477,6 +6491,18 @@ export default {
if (isAddMode) { if (isAddMode) {
delete submitRow.id; delete submitRow.id;
delete submitRow.waybillNo; delete submitRow.waybillNo;
// 复制提交:状态列未进表单,Avue 可能不带回;显式保留原运单业务状态(含已完成)
const copyId = this.$route.query.copyId || '';
if (copyId && !saveAsDraft) {
const copiedStatus =
this.form?.businessStatus ||
this.copySourceBusinessStatus ||
submitRow.businessStatus ||
'';
if (copiedStatus) {
submitRow.businessStatus = copiedStatus;
}
}
} }
const skipValidation = saveAsDraft && this.config.skipDraftValidation === true; const skipValidation = saveAsDraft && this.config.skipDraftValidation === true;
if ( if (
@@ -6596,7 +6622,29 @@ export default {
selected?.currency || selected?.currency ||
this.form.settlementCurrency || this.form.settlementCurrency ||
'RMB'; 'RMB';
this.taskFreightCurrency = this.normalizeFreightCurrency(value); const nextCurrency = this.normalizeFreightCurrency(value);
const previousCurrency = this.taskFreightCurrency;
this.taskFreightCurrency = nextCurrency;
if (previousCurrency && previousCurrency !== nextCurrency) {
this.remapWaybillPriceUnits(nextCurrency);
}
},
remapWaybillPriceUnits(currency) {
if (this.form.priceUnit) {
this.form.priceUnit = remapPriceUnitCurrency(this.form.priceUnit, currency);
}
(this.transportCargoRows || []).forEach(cargo => {
if (cargo.priceUnit) {
cargo.priceUnit = remapPriceUnitCurrency(cargo.priceUnit, currency);
}
});
(this.taskFreightGroups || []).forEach(group => {
(group.rows || []).forEach(row => {
if (row.priceUnit) {
row.priceUnit = remapPriceUnitCurrency(row.priceUnit, currency);
}
});
});
}, },
loadFreightCurrencyOptions() { loadFreightCurrencyOptions() {
if (this.freightCurrencyOptions.length || this.freightCurrencyLoading) { if (this.freightCurrencyOptions.length || this.freightCurrencyLoading) {
@@ -6880,6 +6928,8 @@ export default {
if (type === 'add' && copyId) { if (type === 'add' && copyId) {
this.api.getDetail(copyId).then(res => { this.api.getDetail(copyId).then(res => {
const sourceData = res.data.data || {}; const sourceData = res.data.data || {};
// 原运单业务状态(如 completed)提交时需原样带回并生成应收应付
this.copySourceBusinessStatus = sourceData.businessStatus || '';
// 清除不应该复制的字段(业务状态与原运单保持一致) // 清除不应该复制的字段(业务状态与原运单保持一致)
delete sourceData.id; delete sourceData.id;
delete sourceData.code; delete sourceData.code;
@@ -6901,14 +6951,18 @@ export default {
delete sourceData.updateUserName; delete sourceData.updateUserName;
this.applyFormDetail(sourceData); this.applyFormDetail(sourceData);
// 标记为手工创建;业务状态沿用原运单 // 标记为手工创建;业务状态沿用原运单(含已完成)
this.form.dataSource = '手工创建'; this.form.dataSource = '手工创建';
if (this.copySourceBusinessStatus) {
this.form.businessStatus = this.copySourceBusinessStatus;
}
done(); done();
}); });
return; return;
} }
if (type === 'add') { if (type === 'add') {
this.copySourceBusinessStatus = '';
this.form = { this.form = {
...(this.config.defaultForm || {}), ...(this.config.defaultForm || {}),
}; };
@@ -7382,7 +7436,7 @@ export default {
if (this.isSentinelMinusOne(this.form[prop])) this.form[prop] = ''; if (this.isSentinelMinusOne(this.form[prop])) this.form[prop] = '';
}); });
this.form.priceUnit = this.form.priceUnit =
this.form.priceUnit || taskInfo.priceUnit || goodsInfo.priceUnit || '元/吨'; this.form.priceUnit || taskInfo.priceUnit || goodsInfo.priceUnit || this.defaultPriceUnit;
this.form.otherFeeTotal = this.normalizeOptionalAmount( this.form.otherFeeTotal = this.normalizeOptionalAmount(
this.form.otherFeeTotal || taskInfo.otherFeeTotal || goodsInfo.otherFeeTotal || '' this.form.otherFeeTotal || taskInfo.otherFeeTotal || goodsInfo.otherFeeTotal || ''
); );
@@ -8095,7 +8149,7 @@ export default {
if (index === 0 && !cargo.unitPrice && this.form.unitPrice) { if (index === 0 && !cargo.unitPrice && this.form.unitPrice) {
cargo.unitPrice = this.form.unitPrice; cargo.unitPrice = this.form.unitPrice;
} }
cargo.priceUnit = cargo.priceUnit || this.form.priceUnit || '元/吨'; cargo.priceUnit = cargo.priceUnit || this.form.priceUnit || this.defaultPriceUnit;
if (this.isBillingFieldEmpty(cargo.freightAmount)) { if (this.isBillingFieldEmpty(cargo.freightAmount)) {
cargo.freightAmount = this.normalizeOptionalAmount( cargo.freightAmount = this.normalizeOptionalAmount(
freightItems[index]?.freightAmount ?? '' freightItems[index]?.freightAmount ?? ''
@@ -8477,7 +8531,7 @@ export default {
}, },
addTransportCargoRow(index = -1, row = {}) { addTransportCargoRow(index = -1, row = {}) {
const nextRow = this.normalizeTransportCargoRow({ const nextRow = this.normalizeTransportCargoRow({
priceUnit: this.form.priceUnit || '元/吨', priceUnit: this.form.priceUnit || this.defaultPriceUnit,
...row, ...row,
}); });
if ( if (
@@ -8524,12 +8578,9 @@ export default {
this.syncTransportCargoJson(); this.syncTransportCargoJson();
}, },
resolvePriceUnitByQuantityUnit(quantityUnit) { resolvePriceUnitByQuantityUnit(quantityUnit) {
return ( return resolvePriceUnitByQuantityUnit(
{ quantityUnit,
吨: '元/吨', this.taskFreightCurrency || this.form.settlementCurrency || 'RMB'
方: '元/立方',
件: '元/件',
}[String(quantityUnit || '').trim()] || ''
); );
}, },
handleTransportCargoQuantityUnitChange(quantityUnit) { handleTransportCargoQuantityUnitChange(quantityUnit) {
+56 -13
View File
@@ -45,7 +45,7 @@
<el-form-item label="项目"> <el-form-item label="项目">
<el-input v-model="form.projectName" disabled /> <el-input v-model="form.projectName" disabled />
</el-form-item> </el-form-item>
<el-form-item label="所属组织"> <el-form-item label="合同负责部门">
<el-input v-model="form.organizationName" disabled /> <el-input v-model="form.organizationName" disabled />
</el-form-item> </el-form-item>
<el-form-item label="甲方"> <el-form-item label="甲方">
@@ -63,6 +63,15 @@
disabled disabled
/> />
</el-form-item> </el-form-item>
<el-form-item label="申请日期">
<el-date-picker
v-model="form.applyDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
disabled
/>
</el-form-item>
<el-form-item label="合同期限"> <el-form-item label="合同期限">
<div class="contract-basic-section__date-range"> <div class="contract-basic-section__date-range">
<el-date-picker <el-date-picker
@@ -91,7 +100,7 @@
<template #suffix>天</template> <template #suffix>天</template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item label="合同金额"> <el-form-item label="合同金额" prop="contractAmount">
<el-input <el-input
:model-value="contractAmountDisplay" :model-value="contractAmountDisplay"
placeholder="请输入" placeholder="请输入"
@@ -154,7 +163,7 @@
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item label="是否需要加盖法人章"> <el-form-item label="是否需要加盖法人章">
<el-select v-model="form.legalSealFlag" placeholder="请选择"> <el-select v-model="form.legalSealFlag" placeholder="请选择" clearable>
<el-option label="否" :value="0" /> <el-option label="否" :value="0" />
<el-option label="是" :value="1" /> <el-option label="是" :value="1" />
</el-select> </el-select>
@@ -173,20 +182,30 @@
<el-input v-model="form.handlerUserName" disabled /> <el-input v-model="form.handlerUserName" disabled />
</el-form-item> </el-form-item>
</div> </div>
<el-form-item label="备注" class="contract-basic-section__remark"> <el-form-item label="合同主要内容" class="contract-basic-section__remark">
<el-input
v-model="form.mainContent"
type="textarea"
:rows="4"
maxlength="1000"
show-word-limit
placeholder="请输入合同主要内容"
/>
</el-form-item>
<el-form-item label="其他事项说明" class="contract-basic-section__remark">
<el-input <el-input
v-model="form.remark" v-model="form.remark"
type="textarea" type="textarea"
:rows="2" :rows="2"
maxlength="200" maxlength="200"
show-word-limit show-word-limit
placeholder="请输入备注" placeholder="请输入其他事项说明"
/> />
</el-form-item> </el-form-item>
</section> </section>
<contract-attachment-section <contract-attachment-section
title="合同文件" title="用印材料"
description description
attachment-type attachment-type
:attachment-location-options="changeAttachmentLocationOptions" :attachment-location-options="changeAttachmentLocationOptions"
@@ -251,13 +270,20 @@
<el-button type="primary" plain @click="addPaymentRatioRow">添加</el-button> <el-button type="primary" plain @click="addPaymentRatioRow">添加</el-button>
</section> </section>
<billing-plan-editor v-model="planDialogVisible" :value="planEditor" :index="planEditorIndex" @save="savePlan" /> <billing-plan-editor
v-model="planDialogVisible"
:value="planEditor"
:index="planEditorIndex"
:settlement-currency="form.settlementCurrency"
@save="savePlan"
/>
<contract-attachment-section <contract-attachment-section
title="其它附件" title="附件(佐证材料等)"
description description
attachment-type attachment-type
attachment-location="其它附件" attachment-location="附件(佐证材料等)"
attachment-type-dict-code="contract_supporting_attachment_type"
:attachment-location-options="changeAttachmentLocationOptions" :attachment-location-options="changeAttachmentLocationOptions"
:rows="attachments" :rows="attachments"
:preview="previewAttachment" :preview="previewAttachment"
@@ -308,6 +334,8 @@ import * as api from '@/api/business/contract-manage';
import { submitMkApprovalFlow } from '@/utils/mk-approval'; import { submitMkApprovalFlow } from '@/utils/mk-approval';
import BillingPlanEditor from './components/billing-plan-editor.vue'; import BillingPlanEditor from './components/billing-plan-editor.vue';
import ContractAttachmentSection, { import ContractAttachmentSection, {
isContractSealMaterialLocation,
isContractSupportingAttachmentLocation,
normalizeContractFileRows, normalizeContractFileRows,
} from './components/contract-attachment-section.vue'; } from './components/contract-attachment-section.vue';
import { getDictionary as getSystemDictionary } from '@/api/system/dict'; import { getDictionary as getSystemDictionary } from '@/api/system/dict';
@@ -424,6 +452,21 @@ export default {
], ],
rules: { rules: {
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }], contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
contractAmount: [
{ required: true, message: '请输入合同金额', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
return callback(new Error('请输入合同金额'));
}
if (Number(value) < 0) {
return callback(new Error('合同金额不能小于0'));
}
callback();
},
trigger: 'blur',
},
],
changeReason: [{ max: 500, message: '变更原因不能超过500个字符', trigger: 'blur' }], changeReason: [{ max: 500, message: '变更原因不能超过500个字符', trigger: 'blur' }],
}, },
}; };
@@ -433,7 +476,7 @@ export default {
return this.$route.query.name || '合同变更'; return this.$route.query.name || '合同变更';
}, },
changeAttachmentLocationOptions() { changeAttachmentLocationOptions() {
return ['合同文件', '其它附件', '变更材料']; return ['用印材料', '附件(佐证材料等)', '变更材料'];
}, },
contractAmountDisplay() { contractAmountDisplay() {
const value = this.form.contractAmount; const value = this.form.contractAmount;
@@ -601,7 +644,7 @@ export default {
return []; return [];
} }
}, },
// 审核通过后上一次的变更材料归集到「其它附件」,按文件地址/文件名去重,避免重复展示 // 审核通过后上一次的变更材料归集到「附件(佐证材料等)」,按文件地址/文件名去重,避免重复展示
mergeChangeAttachments(attachments = [], changeAttachments = []) { mergeChangeAttachments(attachments = [], changeAttachments = []) {
const key = item => this.attachmentUrl(item) || this.attachmentName(item); const key = item => this.attachmentUrl(item) || this.attachmentName(item);
const exists = new Set(attachments.map(key)); const exists = new Set(attachments.map(key));
@@ -655,11 +698,11 @@ export default {
previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; }, previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; },
handleAttachmentUploadToLocation({ location, rows = [] }) { handleAttachmentUploadToLocation({ location, rows = [] }) {
if (!rows.length) return; if (!rows.length) return;
if (location === '合同文件') { if (isContractSealMaterialLocation(location)) {
this.contractFileRows = [...(this.contractFileRows || []), ...rows]; this.contractFileRows = [...(this.contractFileRows || []), ...rows];
return; return;
} }
if (location === '其它附件') { if (isContractSupportingAttachmentLocation(location)) {
this.attachments = [...(this.attachments || []), ...rows]; this.attachments = [...(this.attachments || []), ...rows];
return; return;
} }
+136 -36
View File
@@ -128,7 +128,7 @@
<el-descriptions-item label="甲方">{{ detailValue('partyA') }}</el-descriptions-item> <el-descriptions-item label="甲方">{{ detailValue('partyA') }}</el-descriptions-item>
<el-descriptions-item label="乙方">{{ detailValue('partyB') }}</el-descriptions-item> <el-descriptions-item label="乙方">{{ detailValue('partyB') }}</el-descriptions-item>
<el-descriptions-item label="合同期限">{{ detailContractPeriod }}</el-descriptions-item> <el-descriptions-item label="合同期限">{{ detailContractPeriod }}</el-descriptions-item>
<el-descriptions-item label="所属组织">{{ <el-descriptions-item label="合同负责部门">{{
detailValue('organizationName') detailValue('organizationName')
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item label="经办人">{{ <el-descriptions-item label="经办人">{{
@@ -140,6 +140,9 @@
<el-descriptions-item label="签订日期">{{ <el-descriptions-item label="签订日期">{{
detailValue('signDate') detailValue('signDate')
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item label="申请日期">{{
detailValue('applyDate')
}}</el-descriptions-item>
<el-descriptions-item label="合同格式">{{ <el-descriptions-item label="合同格式">{{
detailValue('contractFormat') detailValue('contractFormat')
}}</el-descriptions-item> }}</el-descriptions-item>
@@ -171,14 +174,17 @@
<el-descriptions-item label="是否电子章"> <el-descriptions-item label="是否电子章">
{{ detailElectronicSealText }} {{ detailElectronicSealText }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="备注" :span="3">{{ <el-descriptions-item label="合同主要内容" :span="3">{{
detailValue('mainContent')
}}</el-descriptions-item>
<el-descriptions-item label="其他事项说明" :span="3">{{
detailValue('remark') detailValue('remark')
}}</el-descriptions-item> }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</section> </section>
<contract-attachment-section <contract-attachment-section
title="合同文件" title="用印材料"
description description
attachment-type attachment-type
readonly readonly
@@ -294,7 +300,7 @@
</section> </section>
<contract-attachment-section <contract-attachment-section
title="其它附件" title="附件(佐证材料等)"
description description
readonly readonly
:rows="detailAttachmentRows" :rows="detailAttachmentRows"
@@ -377,8 +383,14 @@
<el-form-item label="合同编号"> <el-form-item label="合同编号">
<el-input <el-input
v-model="form.contractNo" v-model="form.contractNo"
:disabled="formMode === 'edit'" disabled
placeholder="不填写,则系统自动生成" :placeholder="
formMode === 'add'
? form.organizationId
? '系统按部门规则自动生成'
: '请先选择合同负责部门'
: ''
"
/> />
</el-form-item> </el-form-item>
<el-form-item label="合同名称" prop="contractName"> <el-form-item label="合同名称" prop="contractName">
@@ -417,7 +429,7 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="所属组织" prop="organizationName"> <el-form-item label="合同负责部门" prop="organizationName">
<el-cascader <el-cascader
v-model="organizationCascaderValue" v-model="organizationCascaderValue"
:options="organizationOptions" :options="organizationOptions"
@@ -471,6 +483,16 @@
placeholder="请选择签订日期" placeholder="请选择签订日期"
/> />
</el-form-item> </el-form-item>
<el-form-item label="申请日期">
<el-date-picker
v-model="form.applyDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="提交正式合同时自动填充"
disabled
/>
</el-form-item>
<el-form-item label="合同期限" prop="startDate"> <el-form-item label="合同期限" prop="startDate">
<div class="contract-manage-form__date-range"> <div class="contract-manage-form__date-range">
<el-date-picker <el-date-picker
@@ -497,7 +519,7 @@
@input="value => integerInput('paymentDays', value)" @input="value => integerInput('paymentDays', value)"
><template #suffix>天</template></el-input> ><template #suffix>天</template></el-input>
</el-form-item> </el-form-item>
<el-form-item label="合同金额"> <el-form-item label="合同金额" prop="contractAmount">
<el-input <el-input
:model-value="contractAmountDisplay" :model-value="contractAmountDisplay"
placeholder="请输入" placeholder="请输入"
@@ -557,7 +579,7 @@
> >
</el-form-item> </el-form-item>
<el-form-item label="是否需要加盖法人章" prop="legalSealFlag"> <el-form-item label="是否需要加盖法人章" prop="legalSealFlag">
<el-select v-model="form.legalSealFlag" placeholder="请选择"> <el-select v-model="form.legalSealFlag" placeholder="请选择" clearable>
<el-option label="否" :value="0" /> <el-option label="否" :value="0" />
<el-option label="是" :value="1" /> <el-option label="是" :value="1" />
</el-select> </el-select>
@@ -576,20 +598,30 @@
<el-input v-model="form.handlerUserName" disabled /> <el-input v-model="form.handlerUserName" disabled />
</el-form-item> </el-form-item>
</div> </div>
<el-form-item label="备注" prop="remark" class="contract-manage-form__remark"> <el-form-item label="合同主要内容" prop="mainContent" class="contract-manage-form__remark">
<el-input
v-model="form.mainContent"
type="textarea"
:rows="4"
maxlength="1000"
show-word-limit
placeholder="请输入合同主要内容"
/>
</el-form-item>
<el-form-item label="其他事项说明" prop="remark" class="contract-manage-form__remark">
<el-input <el-input
v-model="form.remark" v-model="form.remark"
type="textarea" type="textarea"
:rows="2" :rows="2"
maxlength="200" maxlength="200"
show-word-limit show-word-limit
placeholder="请输入备注" placeholder="请输入其他事项说明"
/> />
</el-form-item> </el-form-item>
</section> </section>
<contract-attachment-section <contract-attachment-section
title="合同文件" title="用印材料"
description description
attachment-type attachment-type
:attachment-location-options="contractAttachmentLocationOptions" :attachment-location-options="contractAttachmentLocationOptions"
@@ -809,10 +841,11 @@
</section> </section>
<contract-attachment-section <contract-attachment-section
title="其它附件" title="附件(佐证材料等)"
description description
attachment-type attachment-type
attachment-location="其它附件" attachment-location="附件(佐证材料等)"
attachment-type-dict-code="contract_supporting_attachment_type"
:attachment-location-options="contractAttachmentLocationOptions" :attachment-location-options="contractAttachmentLocationOptions"
:readonly="isAttachmentUploadMode" :readonly="isAttachmentUploadMode"
:rows="attachmentRows" :rows="attachmentRows"
@@ -920,18 +953,19 @@
v-model="billingPlanBox" v-model="billingPlanBox"
:value="billingPlanForm" :value="billingPlanForm"
:index="billingPlanIndex" :index="billingPlanIndex"
:settlement-currency="form.settlementCurrency"
@save="saveBillingPlan" @save="saveBillingPlan"
/> />
<contract-attachment-section <contract-attachment-section
ref="listAttachmentUploader" ref="listAttachmentUploader"
dialog-only dialog-only
title="合同文件" title="用印材料"
description description
attachment-type attachment-type
use-upload-dialog use-upload-dialog
mark-approved-on-upload mark-approved-on-upload
:attachment-location-options="['合同文件', '其它附件']" :attachment-location-options="['用印材料', '附件(佐证材料等)']"
:rows="listUploadContractFileRows" :rows="listUploadContractFileRows"
@update:rows="listUploadContractFileRows = $event" @update:rows="listUploadContractFileRows = $event"
@upload-to-location="handleListAttachmentUploadToLocation" @upload-to-location="handleListAttachmentUploadToLocation"
@@ -988,6 +1022,7 @@
v-model="detailBillingPlanBox" v-model="detailBillingPlanBox"
:value="detailBillingPlanForm" :value="detailBillingPlanForm"
:index="detailBillingPlanIndex" :index="detailBillingPlanIndex"
:settlement-currency="detail.settlementCurrency || form.settlementCurrency"
readonly readonly
/> />
@@ -1026,6 +1061,8 @@ import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/m
import ContractAttachmentSection, { import ContractAttachmentSection, {
attachmentName as sharedAttachmentName, attachmentName as sharedAttachmentName,
attachmentUrl as sharedAttachmentUrl, attachmentUrl as sharedAttachmentUrl,
isContractSealMaterialLocation,
isContractSupportingAttachmentLocation,
normalizeContractFileRows, normalizeContractFileRows,
} from './components/contract-attachment-section.vue'; } from './components/contract-attachment-section.vue';
import PdfPreview from '@/components/pdf-preview/main.vue'; import PdfPreview from '@/components/pdf-preview/main.vue';
@@ -1093,8 +1130,9 @@ const defaultForm = () => ({
partyB: '', partyB: '',
handlerUserName: '', handlerUserName: '',
signDate: '', signDate: '',
applyDate: '',
contractFormat: '', contractFormat: '',
legalSealFlag: 0, legalSealFlag: null,
copyCount: '', copyCount: '',
settlementCurrency: '', settlementCurrency: '',
invoiceCycle: '', invoiceCycle: '',
@@ -1110,6 +1148,7 @@ const defaultForm = () => ({
effectiveType: '', effectiveType: '',
feeGenerationMode: 'system', feeGenerationMode: 'system',
archiveStatus: '未归档', archiveStatus: '未归档',
mainContent: '',
remark: '', remark: '',
}); });
// 复制新增:仅保留可编辑字段与明细,剔除主键、编号、审批与流程信息 // 复制新增:仅保留可编辑字段与明细,剔除主键、编号、审批与流程信息
@@ -1121,6 +1160,7 @@ const createCopiedDetail = (detail = {}) => {
return { return {
...form, ...form,
contractNo: '', contractNo: '',
applyDate: '',
archiveStatus: '未归档', archiveStatus: '未归档',
feeGenerationMode: detail.feeGenerationMode || '', feeGenerationMode: detail.feeGenerationMode || '',
billingEnabled: detail.billingEnabled, billingEnabled: detail.billingEnabled,
@@ -1215,13 +1255,12 @@ export default {
signType: [{ required: true, message: '请选择签约类型', trigger: 'change' }], signType: [{ required: true, message: '请选择签约类型', trigger: 'change' }],
settlementCurrency: [{ required: true, message: '请选择结算币种', trigger: 'change' }], settlementCurrency: [{ required: true, message: '请选择结算币种', trigger: 'change' }],
projectName: [{ required: true, message: '请选择所属项目', trigger: 'change' }], projectName: [{ required: true, message: '请选择所属项目', trigger: 'change' }],
organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }], organizationName: [{ required: true, message: '请选择合同负责部门', trigger: 'change' }],
partyA: [{ required: true, message: '请选择甲方', trigger: 'change' }], partyA: [{ required: true, message: '请选择甲方', trigger: 'change' }],
partyB: [{ required: true, message: '请选择乙方', trigger: 'change' }], partyB: [{ required: true, message: '请选择乙方', trigger: 'change' }],
signDate: [{ required: true, message: '请选择签订日期', trigger: 'change' }], signDate: [{ required: true, message: '请选择签订日期', trigger: 'change' }],
settlementMode: [{ required: true, message: '请选择结算方式', trigger: 'change' }], settlementMode: [{ required: true, message: '请选择结算方式', trigger: 'change' }],
contractFormat: [{ required: true, message: '请选择合同格式', trigger: 'change' }], contractFormat: [{ required: true, message: '请选择合同格式', trigger: 'change' }],
legalSealFlag: [{ required: true, message: '请选择是否需要加盖法人章', trigger: 'change' }],
invoiceCycle: [ invoiceCycle: [
{ {
validator: (rule, value, callback) => { validator: (rule, value, callback) => {
@@ -1236,6 +1275,22 @@ export default {
], ],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }], startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }], endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }],
contractAmount: [
{ required: true, message: '请输入合同金额', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
return callback(new Error('请输入合同金额'));
}
if (Number(value) < 0) {
return callback(new Error('合同金额不能小于0'));
}
callback();
},
trigger: 'blur',
},
],
mainContent: [{ max: 1000, message: '合同主要内容不能超过1000个字符', trigger: 'blur' }],
}, },
submitAction: '', submitAction: '',
projectOptions: [], projectOptions: [],
@@ -1345,8 +1400,8 @@ export default {
return this.$route.query.attachmentUpload === '1'; return this.$route.query.attachmentUpload === '1';
}, },
contractAttachmentLocationOptions() { contractAttachmentLocationOptions() {
if (this.isAttachmentUploadMode) return ['合同文件']; if (this.isAttachmentUploadMode) return ['用印材料'];
return ['合同文件', '其它附件']; return ['用印材料', '附件(佐证材料等)'];
}, },
contractAmountDisplay() { contractAmountDisplay() {
const value = this.form.contractAmount; const value = this.form.contractAmount;
@@ -1413,7 +1468,7 @@ export default {
expandTrigger: 'click', expandTrigger: 'click',
}; };
}, },
// 所属组织级联:selectedOrganizationId 存部门 id,级联控件用根到节点的 id 路径, // 合同负责部门级联:selectedOrganizationId 存部门 id,级联控件用根到节点的 id 路径,
// 两者在此双向转换;依赖 organizationOptions,树加载完成后回显自动刷新 // 两者在此双向转换;依赖 organizationOptions,树加载完成后回显自动刷新
organizationCascaderValue: { organizationCascaderValue: {
get() { get() {
@@ -1775,6 +1830,7 @@ export default {
this.applyDetail(createCopiedDetail(detail)); this.applyDetail(createCopiedDetail(detail));
this.form.handlerUserName = this.form.handlerUserName =
this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || ''; this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || '';
this.refreshPreviewContractNo();
this.$nextTick(() => this.$refs.contractForm?.clearValidate()); this.$nextTick(() => this.$refs.contractForm?.clearValidate());
}, },
closeForm() { closeForm() {
@@ -1789,9 +1845,10 @@ export default {
['contractName', '合同名称'], ['contractName', '合同名称'],
['contractCategory', '合同类型'], ['contractCategory', '合同类型'],
['projectName', '所属项目'], ['projectName', '所属项目'],
['organizationName', '所属组织'], ['organizationName', '合同负责部门'],
['partyA', '甲方'], ['partyA', '甲方'],
['partyB', '乙方'], ['partyB', '乙方'],
['contractAmount', '合同金额'],
]; ];
const empty = required.find(([prop]) => { const empty = required.find(([prop]) => {
const value = this.form[prop]; const value = this.form[prop];
@@ -1799,10 +1856,16 @@ export default {
}); });
if (empty) { if (empty) {
this.$message.warning( this.$message.warning(
empty[0] === 'contractName' ? `请输入${empty[1]}` : `请选择${empty[1]}` ['contractName', 'contractAmount'].includes(empty[0])
? `请输入${empty[1]}`
: `请选择${empty[1]}`
); );
return false; return false;
} }
if (Number(this.form.contractAmount) < 0) {
this.$message.warning('合同金额不能小于0');
return false;
}
return true; return true;
} }
if (!this.form.contractName?.trim()) { if (!this.form.contractName?.trim()) {
@@ -1825,7 +1888,7 @@ export default {
if (action !== 'formal' && action !== 'edit') return true; if (action !== 'formal' && action !== 'edit') return true;
const required = [ const required = [
['projectName', '所属项目'], ['projectName', '所属项目'],
['organizationName', '所属组织'], ['organizationName', '合同负责部门'],
['partyA', '甲方'], ['partyA', '甲方'],
['partyB', '乙方'], ['partyB', '乙方'],
['startDate', '合同开始日期'], ['startDate', '合同开始日期'],
@@ -1833,10 +1896,20 @@ export default {
['signDate', '签订日期'], ['signDate', '签订日期'],
['settlementMode', '结算方式'], ['settlementMode', '结算方式'],
['contractFormat', '合同格式'], ['contractFormat', '合同格式'],
['contractAmount', '合同金额'],
]; ];
const empty = required.find(([prop]) => !this.form[prop]); const empty = required.find(([prop]) => {
const value = this.form[prop];
return value === undefined || value === null || String(value).trim() === '';
});
if (empty) { if (empty) {
this.$message.warning(`请选择或输入${empty[1]}`); this.$message.warning(
empty[0] === 'contractAmount' ? `请输入${empty[1]}` : `请选择或输入${empty[1]}`
);
return false;
}
if (Number(this.form.contractAmount) < 0) {
this.$message.warning('合同金额不能小于0');
return false; return false;
} }
return ( return (
@@ -2026,11 +2099,11 @@ export default {
}, },
handleListAttachmentUploadToLocation({ location, rows = [] }) { handleListAttachmentUploadToLocation({ location, rows = [] }) {
if (!rows.length) return; if (!rows.length) return;
if (location === '合同文件') { if (isContractSealMaterialLocation(location)) {
this.listUploadContractFileRows = [...(this.listUploadContractFileRows || []), ...rows]; this.listUploadContractFileRows = [...(this.listUploadContractFileRows || []), ...rows];
return; return;
} }
if (location === '其它附件') { if (isContractSupportingAttachmentLocation(location)) {
this.listUploadAttachmentRows = [...(this.listUploadAttachmentRows || []), ...rows]; this.listUploadAttachmentRows = [...(this.listUploadAttachmentRows || []), ...rows];
} }
}, },
@@ -2150,6 +2223,9 @@ export default {
column.renderSearch = scope => this.renderOrganizationSearch(scope); column.renderSearch = scope => this.renderOrganizationSearch(scope);
} }
this.syncCurrentOrganizationOption(); this.syncCurrentOrganizationOption();
if (this.isFormPage && this.formMode === 'add' && this.form.organizationId) {
this.refreshPreviewContractNo();
}
}) })
.finally(() => { .finally(() => {
this.organizationLoading = false; this.organizationLoading = false;
@@ -2212,6 +2288,29 @@ export default {
} }
: { organizationId: '', organizationName: '' } : { organizationId: '', organizationName: '' }
); );
if (this.formMode === 'add') {
this.refreshPreviewContractNo();
}
},
refreshPreviewContractNo() {
if (this.formMode !== 'add') return;
const organizationId = this.form.organizationId;
if (!organizationId) {
this.form.contractNo = '';
return;
}
const requestId = (this.contractNoPreviewRequestId =
(this.contractNoPreviewRequestId || 0) + 1);
api
.nextContractNo(organizationId)
.then(res => {
if (requestId !== this.contractNoPreviewRequestId) return;
this.form.contractNo = res.data?.data || '';
})
.catch(() => {
if (requestId !== this.contractNoPreviewRequestId) return;
this.form.contractNo = '';
});
}, },
syncCurrentOrganizationOption() { syncCurrentOrganizationOption() {
if (!this.form.organizationId || !this.form.organizationName) return; if (!this.form.organizationId || !this.form.organizationName) return;
@@ -2225,7 +2324,7 @@ export default {
this.organizationFlatOptions.unshift(item); this.organizationFlatOptions.unshift(item);
}, },
renderOrganizationSearch(scope) { renderOrganizationSearch(scope) {
// 搜索区所属组织:单选级联(与新增/编辑表单一致) // 搜索区合同负责部门:单选级联(与新增/编辑表单一致)
// 级联返回 id 路径数组,提交时由 normalizeSearch 取末级 id 转组织名称 // 级联返回 id 路径数组,提交时由 normalizeSearch 取末级 id 转组织名称
return h(ElCascader, { return h(ElCascader, {
modelValue: Array.isArray(scope.row?.organizationName) ? scope.row.organizationName : [], modelValue: Array.isArray(scope.row?.organizationName) ? scope.row.organizationName : [],
@@ -2244,7 +2343,7 @@ export default {
filterable: true, filterable: true,
clearable: true, clearable: true,
style: 'width: 100%', style: 'width: 100%',
placeholder: '请选择 所属组织', placeholder: '请选择 合同负责部门',
}); });
}, },
integerInput(prop, value) { integerInput(prop, value) {
@@ -2289,11 +2388,11 @@ export default {
}, },
handleAttachmentUploadToLocation({ location, rows = [] }) { handleAttachmentUploadToLocation({ location, rows = [] }) {
if (!rows.length) return; if (!rows.length) return;
if (location === '合同文件') { if (isContractSealMaterialLocation(location)) {
this.contractFileRows = [...(this.contractFileRows || []), ...rows]; this.contractFileRows = [...(this.contractFileRows || []), ...rows];
return; return;
} }
if (location === '其它附件') { if (isContractSupportingAttachmentLocation(location)) {
this.attachmentRows = [...(this.attachmentRows || []), ...rows]; this.attachmentRows = [...(this.attachmentRows || []), ...rows];
} }
}, },
@@ -2654,15 +2753,16 @@ export default {
templateFlag: '是否范本', templateFlag: '是否范本',
originalContractNo: '原件合同编号', originalContractNo: '原件合同编号',
electronicSealFlag: '是否电子章', electronicSealFlag: '是否电子章',
remark: '备注', mainContent: '合同主要内容',
remark: '其他事项说明',
feeGenerationMode: '费用生成模式', feeGenerationMode: '费用生成模式',
billingPlanJson: '计费方案', billingPlanJson: '计费方案',
settlementRuleJson: '结算单规则', settlementRuleJson: '结算单规则',
preSettlementConfigJson: '预结算配置', preSettlementConfigJson: '预结算配置',
formalSettlementConfigJson: '正式结算配置', formalSettlementConfigJson: '正式结算配置',
paymentRatioJson: '付款比例设置', paymentRatioJson: '付款比例设置',
contractFileJson: '合同文件', contractFileJson: '用印材料',
attachmentsJson: '其它附件', attachmentsJson: '附件(佐证材料等)',
changeAttachmentsJson: '变更材料', changeAttachmentsJson: '变更材料',
}; };
return labels[field] || field; return labels[field] || field;
+29
View File
@@ -357,6 +357,33 @@ export default {
}, },
], ],
}, },
{
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: '组织简称', label: '组织简称',
prop: 'shortName', prop: 'shortName',
@@ -995,6 +1022,7 @@ export default {
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId; row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
row.deptCode = String(row.deptCode || '').trim(); row.deptCode = String(row.deptCode || '').trim();
row.kingdeeDeptCode = String(row.kingdeeDeptCode || '').trim(); row.kingdeeDeptCode = String(row.kingdeeDeptCode || '').trim();
row.contractCodePrefix = String(row.contractCodePrefix || '').trim();
row.leaderId = func.join(row.leaderId); row.leaderId = func.join(row.leaderId);
this.normalizeTopParent(row); this.normalizeTopParent(row);
if (!this.prepareDeptSubmitRow(row)) { if (!this.prepareDeptSubmitRow(row)) {
@@ -1030,6 +1058,7 @@ export default {
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId; row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
row.deptCode = String(row.deptCode || '').trim(); row.deptCode = String(row.deptCode || '').trim();
row.kingdeeDeptCode = String(row.kingdeeDeptCode || '').trim(); row.kingdeeDeptCode = String(row.kingdeeDeptCode || '').trim();
row.contractCodePrefix = String(row.contractCodePrefix || '').trim();
row.leaderId = func.join(row.leaderId); row.leaderId = func.join(row.leaderId);
this.normalizeTopParent(row); this.normalizeTopParent(row);
if (!this.prepareDeptSubmitRow(row)) { if (!this.prepareDeptSubmitRow(row)) {
@@ -428,4 +428,43 @@ export default {
.vehicle-dispatch-page :deep(.vehicle-dispatch-form-control) { .vehicle-dispatch-page :deep(.vehicle-dispatch-form-control) {
width: 100%; width: 100%;
} }
/* 查看弹窗保持与新增/编辑相同的表单样式,仅禁用控件 */
.vehicle-dispatch-page :deep(.avue--detail .el-form-item) {
background-color: transparent;
}
.vehicle-dispatch-page :deep(.avue--detail .el-input.is-disabled .el-input__wrapper) {
box-shadow: 0 0 0 1px var(--el-disabled-border-color) inset;
background-color: var(--el-disabled-bg-color);
}
.vehicle-dispatch-page :deep(.avue--detail .el-input.is-disabled .el-input__inner),
.vehicle-dispatch-page :deep(.avue--detail .el-textarea.is-disabled .el-textarea__inner),
.vehicle-dispatch-page :deep(.avue--detail .el-range-editor.is-disabled),
.vehicle-dispatch-page :deep(.avue--detail .el-range-editor.is-disabled input) {
color: var(--el-disabled-text-color);
background-color: var(--el-disabled-bg-color);
padding-left: 11px;
box-shadow: 0 0 0 1px var(--el-disabled-border-color) inset;
-webkit-text-fill-color: var(--el-disabled-text-color);
}
.vehicle-dispatch-page :deep(.avue--detail .el-select__wrapper.is-disabled) {
background-color: var(--el-disabled-bg-color);
box-shadow: 0 0 0 1px var(--el-disabled-border-color) inset !important;
}
.vehicle-dispatch-page :deep(.avue--detail .el-select__suffix),
.vehicle-dispatch-page :deep(.avue--detail .el-input__suffix),
.vehicle-dispatch-page :deep(.avue--detail .el-input__prefix) {
display: inline-flex;
}
.vehicle-dispatch-page :deep(.avue--detail .el-input__inner),
.vehicle-dispatch-page :deep(.avue--detail .el-textarea__inner) {
border: none;
}
.vehicle-dispatch-page :deep(.avue--detail .el-cascader) {
width: 100%;
}
.vehicle-dispatch-page :deep(.avue--detail .el-cascader .el-input.is-disabled .el-input__wrapper),
.vehicle-dispatch-page :deep(.avue--detail .el-autocomplete .el-input.is-disabled .el-input__wrapper) {
box-shadow: 0 0 0 1px var(--el-disabled-border-color) inset;
background-color: var(--el-disabled-bg-color);
}
</style> </style>
+60 -15
View File
@@ -985,7 +985,7 @@ export default {
} }
this.applyDrivingLicenseValidity(inspectionValidity); this.applyDrivingLicenseValidity(inspectionValidity);
if (registrationDate) { if (registrationDate) {
this.vehicleForm.registrationDate = this.normalizeDate(registrationDate); this.vehicleForm.registrationDate = this.normalizeDate(registrationDate, 'first');
} }
if (registrationNo) { if (registrationNo) {
this.vehicleForm.registrationNo = String(registrationNo).replace(/\s/g, ''); this.vehicleForm.registrationNo = String(registrationNo).replace(/\s/g, '');
@@ -1000,6 +1000,7 @@ export default {
'vehicleType', 'vehicleType',
'drivingLicenseNo', 'drivingLicenseNo',
'drivingLicenseEndDate', 'drivingLicenseEndDate',
'registrationDate',
'registrationNo', 'registrationNo',
'approvedLoadKg', 'approvedLoadKg',
'outerLength', 'outerLength',
@@ -1057,9 +1058,9 @@ export default {
this.vehicleForm.roadTransportCertEndDate = ''; this.vehicleForm.roadTransportCertEndDate = '';
return; return;
} }
const dateList = value.match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || []; const endDate = this.normalizeDate(value, 'last');
if (dateList.length) { if (endDate) {
this.vehicleForm.roadTransportCertEndDate = this.normalizeDate(dateList[dateList.length - 1]); this.vehicleForm.roadTransportCertEndDate = endDate;
this.vehicleForm.roadTransportCertLongTerm = 0; this.vehicleForm.roadTransportCertLongTerm = 0;
} }
}, },
@@ -1071,9 +1072,9 @@ export default {
this.vehicleForm.drivingLicenseEndDate = ''; this.vehicleForm.drivingLicenseEndDate = '';
return; return;
} }
const dateList = value.match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || []; const endDate = this.normalizeDate(value, 'last');
if (dateList.length) { if (endDate) {
this.vehicleForm.drivingLicenseEndDate = this.normalizeDate(dateList[dateList.length - 1]); this.vehicleForm.drivingLicenseEndDate = endDate;
this.vehicleForm.drivingLicenseLongTerm = 0; this.vehicleForm.drivingLicenseLongTerm = 0;
} }
}, },
@@ -1150,11 +1151,11 @@ export default {
this.vehicleForm.drivingLicenseNo = drivingLicenseNo; this.vehicleForm.drivingLicenseNo = drivingLicenseNo;
} }
if (drivingLicenseEndDate) { if (drivingLicenseEndDate) {
this.vehicleForm.drivingLicenseEndDate = this.normalizeDate(drivingLicenseEndDate); this.vehicleForm.drivingLicenseEndDate = this.normalizeDate(drivingLicenseEndDate, 'last');
this.vehicleForm.drivingLicenseLongTerm = 0; this.vehicleForm.drivingLicenseLongTerm = 0;
} }
if (registrationDate) { if (registrationDate) {
this.vehicleForm.registrationDate = this.normalizeDate(registrationDate); this.vehicleForm.registrationDate = this.normalizeDate(registrationDate, 'first');
} }
if (registrationNo) { if (registrationNo) {
this.vehicleForm.registrationNo = registrationNo; this.vehicleForm.registrationNo = registrationNo;
@@ -1165,18 +1166,60 @@ export default {
'vehicleType', 'vehicleType',
'drivingLicenseNo', 'drivingLicenseNo',
'drivingLicenseEndDate', 'drivingLicenseEndDate',
'registrationDate',
'registrationNo', 'registrationNo',
].forEach(prop => { ].forEach(prop => {
this.$refs.vehicleForm?.validateField(prop); this.$refs.vehicleForm?.validateField(prop);
}); });
}); });
}, },
normalizeDate(value = '') { /**
* OCR 日期常为区间(如 2022-08-17至2024-01-05)或含无效日(如 01-65)。
* 提取可解析的合法日期;prefer=first 取登记类起始日,prefer=last 取有效期截止日。
*/
normalizeDate(value = '', prefer = 'first') {
const dateValue = String(value || '').trim(); const dateValue = String(value || '').trim();
const match = dateValue.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/); if (!dateValue) return '';
if (!match) return dateValue; const segments = dateValue.match(/\d{4}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/g) || [];
const [, year, month, day] = match; const sourceList = segments.length ? segments : [dateValue];
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; const candidates = sourceList
.map(item => {
const match = String(item)
.trim()
.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/);
if (!match) return '';
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const date = new Date(year, month - 1, day);
if (
date.getFullYear() !== year ||
date.getMonth() !== month - 1 ||
date.getDate() !== day
) {
return '';
}
return `${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
})
.filter(Boolean);
if (!candidates.length) return '';
return prefer === 'last' ? candidates[candidates.length - 1] : candidates[0];
},
normalizeVehicleDateFields(form = {}) {
const dateFields = [
['compulsoryScrapDate', 'first'],
['drivingLicenseStartDate', 'first'],
['drivingLicenseEndDate', 'last'],
['roadTransportCertStartDate', 'first'],
['roadTransportCertEndDate', 'last'],
['annualReviewEndDate', 'last'],
['registrationDate', 'first'],
];
dateFields.forEach(([prop, prefer]) => {
if (form[prop] === undefined || form[prop] === null || form[prop] === '') return;
form[prop] = this.normalizeDate(form[prop], prefer);
});
return form;
}, },
normalizeVehicleType(value = '') { normalizeVehicleType(value = '') {
const vehicleType = String(value || '').trim(); const vehicleType = String(value || '').trim();
@@ -1207,12 +1250,14 @@ export default {
}, },
handleSubmit() { handleSubmit() {
this.syncPlateNo(); this.syncPlateNo();
this.normalizeVehicleDateFields(this.vehicleForm);
this.$refs.vehicleForm.validate(valid => { this.$refs.vehicleForm.validate(valid => {
if (!valid) { if (!valid) {
return; return;
} }
this.submitLoading = true; this.submitLoading = true;
submit(this.vehicleForm) const payload = this.normalizeVehicleDateFields({ ...this.vehicleForm });
submit(payload)
.then(() => { .then(() => {
this.$message.success('操作成功'); this.$message.success('操作成功');
this.vehicleBox = false; this.vehicleBox = false;