diff --git a/index.html b/index.html index 5a4c44b..eae241d 100644 --- a/index.html +++ b/index.html @@ -13,6 +13,8 @@ + + diff --git a/oaLogin.html b/oaLogin.html new file mode 100644 index 0000000..7c8f80f --- /dev/null +++ b/oaLogin.html @@ -0,0 +1,52 @@ + + + + +oa登录 + + + + + + + + \ No newline at end of file diff --git a/src/api/base/measurement-unit.js b/src/api/base/measurement-unit.js new file mode 100644 index 0000000..c1eee7e --- /dev/null +++ b/src/api/base/measurement-unit.js @@ -0,0 +1,52 @@ +import request from '@/axios'; + +export const getList = (current, size, params) => { + return request({ + url: '/blade-system/measurement-unit/list', + method: 'get', + params: { + ...params, + current, + size, + }, + }); +}; + +export const getDetail = id => { + return request({ + url: '/blade-system/measurement-unit/detail', + method: 'get', + params: { + id, + }, + }); +}; + +export const remove = ids => { + return request({ + url: '/blade-system/measurement-unit/remove', + method: 'post', + params: { + ids, + }, + }); +}; + +export const submit = row => { + return request({ + url: '/blade-system/measurement-unit/submit', + method: 'post', + data: row, + }); +}; + +export const changeStatus = (id, status) => { + return request({ + url: '/blade-system/measurement-unit/status', + method: 'post', + params: { + id, + status, + }, + }); +}; diff --git a/src/api/business/common.js b/src/api/business/common.js index c2dd4f3..14cc03b 100644 --- a/src/api/business/common.js +++ b/src/api/business/common.js @@ -92,13 +92,12 @@ export const createCrudApi = baseUrl => ({ data, }); }, - reassign(id) { + reassign(data) { + const payload = data && typeof data === 'object' ? data : { id: data }; return request({ url: `${baseUrl}/reassign`, method: 'post', - params: { - id, - }, + data: payload, }); }, batchComplete(ids) { diff --git a/src/api/business/loading-manage.js b/src/api/business/loading-manage.js index bb6be5c..5d116bc 100644 --- a/src/api/business/loading-manage.js +++ b/src/api/business/loading-manage.js @@ -79,6 +79,15 @@ export const cancel = id => }, }); +export const start = id => + request({ + url: `${baseUrl}/start`, + method: 'post', + params: { + id, + }, + }); + export const complete = id => request({ url: `${baseUrl}/complete`, diff --git a/src/api/business/project-apply.js b/src/api/business/project-apply.js index 826caa6..83ac915 100644 --- a/src/api/business/project-apply.js +++ b/src/api/business/project-apply.js @@ -6,6 +6,12 @@ const api = createCrudApi(baseUrl); export const getList = api.getList; export const getDetail = api.getDetail; +export const getChangeRecordDetail = (id, recordIndex) => + request({ + url: `${baseUrl}/change-record/detail`, + method: 'get', + params: { id, recordIndex }, + }); export const submit = api.submit; export const remove = api.remove; diff --git a/src/api/business/waybill-manage.js b/src/api/business/waybill-manage.js index 78d93e9..316f053 100644 --- a/src/api/business/waybill-manage.js +++ b/src/api/business/waybill-manage.js @@ -26,6 +26,13 @@ export const maintainMileage = data => method: 'post', data, }); +/** 运单打卡记录 + 司机上传凭证图 */ +export const getPunchRecords = waybillId => + request({ + url: `${baseUrl}/punch-records`, + method: 'get', + params: { waybillId }, + }); export const roadLoading = ids => request({ url: `${baseUrl}/road-loading`, diff --git a/src/api/system/dept.js b/src/api/system/dept.js index ba96954..7bf3f68 100644 --- a/src/api/system/dept.js +++ b/src/api/system/dept.js @@ -41,6 +41,13 @@ export const add = row => { }); }; +export const syncIamOrganizations = () => { + return request({ + url: '/blade-system/dept/sync-iam-organizations', + method: 'post', + }); +}; + export const update = row => { return request({ url: '/blade-system/dept/submit', @@ -78,3 +85,10 @@ export const getDeptLazyTree = parentId => { }, }); }; + +export const getPlatformCompanySelect = () => { + return request({ + url: '/blade-system/dept/platform-company-select', + method: 'get', + }); +}; diff --git a/src/api/system/user.js b/src/api/system/user.js index 1334f0c..70d5b20 100644 --- a/src/api/system/user.js +++ b/src/api/system/user.js @@ -31,6 +31,13 @@ export const add = row => { }); }; +export const syncIamAccounts = () => { + return request({ + url: '/blade-system/user/sync-iam-accounts', + method: 'post', + }); +}; + export const update = row => { return request({ url: '/blade-system/user/update', diff --git a/src/api/transportCapacity/vehicle-dispatch.js b/src/api/transportCapacity/vehicle-dispatch.js new file mode 100644 index 0000000..7256578 --- /dev/null +++ b/src/api/transportCapacity/vehicle-dispatch.js @@ -0,0 +1,25 @@ +import request from '@/axios'; + +const baseUrl = '/blade-transport/vehicle-dispatch'; + +export const getList = (current, size, params) => + request({ url: `${baseUrl}/list`, method: 'get', params: { ...params, current, size } }); + +export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } }); + +export const submit = row => request({ url: `${baseUrl}/submit`, method: 'post', data: row }); + +export const remove = ids => request({ url: `${baseUrl}/remove`, method: 'post', params: { ids } }); + +export const submitApproval = id => + request({ url: `${baseUrl}/submit-approval`, method: 'post', params: { id } }); + +export const approve = id => request({ url: `${baseUrl}/approve`, method: 'post', params: { id } }); + +export const exportVehicleDispatch = params => + request({ + url: `${baseUrl}/export-vehicle-dispatch`, + method: 'get', + params, + responseType: 'blob', + }); diff --git a/src/api/vehicle/annual-inspection-record.js b/src/api/vehicle/annual-inspection-record.js index 51cbd4d..e0ee82a 100644 --- a/src/api/vehicle/annual-inspection-record.js +++ b/src/api/vehicle/annual-inspection-record.js @@ -47,3 +47,11 @@ export const update = row => { data: row, }); }; + +export const getExpiryStat = params => { + return request({ + url: '/blade-transport/annual-inspection-record/expiry-stat', + method: 'get', + params, + }); +}; diff --git a/src/docker/Dockerfile b/src/docker/Dockerfile index 88425be..88c648f 100644 --- a/src/docker/Dockerfile +++ b/src/docker/Dockerfile @@ -1,6 +1,7 @@ FROM nginx VOLUME /tmp ENV LANG en_US.UTF-8 +ADD ./src/docker/nginx.conf /etc/nginx/conf.d/default.conf ADD ./dist/ /usr/share/nginx/html/ EXPOSE 80 EXPOSE 443 \ No newline at end of file diff --git a/src/docker/nginx.conf b/src/docker/nginx.conf new file mode 100644 index 0000000..0155f82 --- /dev/null +++ b/src/docker/nginx.conf @@ -0,0 +1,19 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + location /assets/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri =404; + } + + location / { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + try_files $uri $uri/ /index.html; + } +} diff --git a/src/error.js b/src/error.js index d2b5611..1c507e5 100644 --- a/src/error.js +++ b/src/error.js @@ -1,8 +1,12 @@ import store from './store'; +import { isChunkLoadError, reloadForChunkError } from './utils/chunk-reload'; export default { install: app => { app.config.errorHandler = (err, vm, info) => { + if (isChunkLoadError(err) && reloadForChunkError()) { + return; + } store.commit('ADD_LOGS', { type: 'error', message: err.message, diff --git a/src/main.js b/src/main.js index 9e780e5..c53d2e6 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,5 @@ import { createApp } from 'vue'; +import { installChunkReload } from './utils/chunk-reload'; import website from './config/website'; import axios from './axios'; import router from './router/'; @@ -46,6 +47,7 @@ import sectionCard from './components/section-card/main.vue'; import mapSearchResults from './components/map-search-results/main.vue'; window.$crudCommon = crudCommon; +installChunkReload(); debug(); window.axios = axios; const app = createApp(App); diff --git a/src/option/base/measurement-unit.js b/src/option/base/measurement-unit.js new file mode 100644 index 0000000..c135823 --- /dev/null +++ b/src/option/base/measurement-unit.js @@ -0,0 +1,133 @@ +const dimensionOptions = [ + { label: '重量', value: '重量' }, + { label: '体积', value: '体积' }, + { label: '数量', value: '数量' }, +]; + +const statusOptions = [ + { label: '启用', value: 1 }, + { label: '停用', value: 2 }, +]; + +export const createOption = () => ({ + height: 'auto', + calcHeight: 32, + dialogWidth: 680, + labelPosition: 'right', + labelWidth: 'auto', + tip: false, + searchBtnText: '查询', + emptyBtnText: '重置', + saveBtnText: '提交', + updateBtnText: '提交', + searchShow: true, + searchMenuSpan: 24, + searchIcon: true, + searchIndex: 4, + searchMenuPosition: 'right', + border: true, + index: true, + indexLabel: '序号', + indexWidth: 70, + addBtn: false, + viewBtn: false, + editBtn: false, + delBtn: false, + selection: true, + dialogClickModal: false, + menuWidth: 240, + menuFixed: 'right', + column: [ + { + label: '计量单位', + prop: 'unitName', + minWidth: 150, + search: true, + searchOrder: 3, + searchSpan: 6, + maxlength: 50, + showWordLimit: true, + rules: [ + { required: true, message: '请输入计量单位', trigger: 'blur' }, + { max: 50, message: '计量单位不能超过50个字', trigger: 'blur' }, + ], + }, + { + label: '计量维度', + prop: 'dimension', + type: 'select', + minWidth: 130, + search: true, + searchOrder: 2, + searchSpan: 6, + dicData: dimensionOptions, + rules: [{ required: true, message: '请选择计量维度', trigger: 'change' }], + }, + { + label: '备注', + prop: 'remark', + type: 'textarea', + minRows: 2, + span: 24, + minWidth: 200, + maxlength: 200, + showWordLimit: true, + overHidden: true, + rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }], + }, + { + label: '状态', + prop: 'status', + type: 'select', + search: true, + searchOrder: 1, + searchSpan: 6, + slot: true, + dataType: 'number', + dicData: statusOptions, + clearable: true, + value: 1, + addDisplay: false, + editDisplay: false, + minWidth: 100, + }, + { + label: '创建人', + prop: 'createUserName', + addDisplay: false, + editDisplay: false, + viewDisplay: false, + display: false, + }, + { + label: '更新人', + prop: 'updateUserName', + addDisplay: false, + editDisplay: false, + viewDisplay: false, + display: false, + }, + { + label: '创建时间', + prop: 'createTime', + type: 'datetime', + format: 'YYYY-MM-DD HH:mm:ss', + valueFormat: 'YYYY-MM-DD HH:mm:ss', + sortable: true, + addDisplay: false, + editDisplay: false, + display: false, + }, + { + label: '更新时间', + prop: 'updateTime', + type: 'datetime', + format: 'YYYY-MM-DD HH:mm:ss', + valueFormat: 'YYYY-MM-DD HH:mm:ss', + sortable: true, + addDisplay: false, + editDisplay: false, + display: false, + }, + ], +}); diff --git a/src/option/business/common.js b/src/option/business/common.js index 7d07f95..1f6feb2 100644 --- a/src/option/business/common.js +++ b/src/option/business/common.js @@ -41,7 +41,7 @@ export const planStatusOptions = [ export const waybillStatusOptions = [ { label: '草稿', value: 'draft' }, { label: '待执行', value: 'pending' }, - { label: '进行中', value: 'processing' }, + { label: '进行中', value: 'running' }, { label: '已完成', value: 'completed' }, { label: '已取消', value: 'cancelled' }, ]; @@ -130,12 +130,18 @@ export const contractApprovalStatusOptions = [ { label: '草稿', value: 'draft' }, { label: '审批中', value: 'reviewing' }, { label: '已驳回', value: 'rejected' }, + { label: '已撤回', value: 'withdrawn' }, { label: '审批通过', value: 'approved' }, { label: '变更审批中', value: 'change_reviewing' }, { label: '变更驳回', value: 'change_rejected' }, { label: '变更审批通过', value: 'change_approved' }, ]; +export const contractArchiveStatusOptions = [ + { label: '未归档', value: '未归档' }, + { label: '已归档', value: '已归档' }, +]; + export const nodeOptions = [ { label: '接单', value: '接单' }, { label: '到场', value: '到场' }, diff --git a/src/option/business/contract-manage.js b/src/option/business/contract-manage.js index 06342c0..86f25dd 100644 --- a/src/option/business/contract-manage.js +++ b/src/option/business/contract-manage.js @@ -1,6 +1,7 @@ import { auditColumns, contractApprovalStatusOptions, + contractArchiveStatusOptions, contractCategoryOptions, contractStageOptions, createCrudOption, @@ -48,7 +49,7 @@ export const config = { batchDelete: false, enableProjectSelect: true, projectQueryParams: { - approvalStatuses: 'approved,change_approved', + contractSelectable: true, }, actions: ['copy'], statusProp: 'approvalStatus', @@ -71,6 +72,10 @@ export const config = { ['legalSealFlag', '是否需要加盖法人章'], ['copyCount', '一式(份)'], ['paymentDays', '回款账期(天)'], + ['contractAmount', '合同金额'], + ['templateFlag', '是否范本'], + ['originalContractNo', '原件合同编号'], + ['electronicSealFlag', '是否电子章'], ['remark', '备注', 2], ], }, @@ -84,6 +89,7 @@ export const config = { ['effectiveType', '生效类型'], ['contractStage', '合同阶段'], ['approvalStatus', '审核状态'], + ['archiveStatus', '归档状态'], ['currentNode', '当前节点'], ['currentProcessor', '当前处理人'], ], @@ -104,7 +110,7 @@ export const config = { }, deleteStatus: ['draft'], deleteStage: ['draft'], - editStatus: ['draft', 'rejected', 'change_rejected'], + editStatus: ['draft', 'withdrawn', 'rejected', 'change_rejected'], operations: [ { action: 'flow', @@ -271,7 +277,7 @@ export const option = createCrudOption([ formslot: true, order: 165, dicUrl: - '/blade-transport/project-apply/list?current=1&size=9999&approvalStatuses=approved,change_approved', + '/blade-transport/project-apply/list?current=1&size=9999&contractSelectable=true', dicFormatter: projectNameDicFormatter, props: { label: 'projectName', @@ -485,6 +491,47 @@ export const option = createCrudOption([ order: 50, minWidth: 130, }, + { + label: '合同金额', + prop: 'contractAmount', + type: 'number', + min: 0, + precision: 2, + hide: true, + order: 45, + minWidth: 130, + }, + { + label: '是否范本', + prop: 'templateFlag', + type: 'select', + dicData: [ + { label: '否', value: 0 }, + { label: '是', value: 1 }, + ], + hide: true, + order: 40, + minWidth: 100, + }, + { + label: '原件合同编号', + prop: 'originalContractNo', + hide: true, + order: 35, + minWidth: 160, + }, + { + label: '是否电子章', + prop: 'electronicSealFlag', + type: 'select', + dicData: [ + { label: '否', value: 0 }, + { label: '是', value: 1 }, + ], + hide: true, + order: 30, + minWidth: 110, + }, { label: '创建时间', prop: 'createTime', @@ -522,6 +569,19 @@ export const option = createCrudOption([ addDisplay: false, editDisplay: false, }, + { + label: '归档状态', + prop: 'archiveStatus', + type: 'select', + search: true, + searchOrder: 5, + searchPlaceholder: '请选择', + slot: true, + dicData: contractArchiveStatusOptions, + minWidth: 110, + addDisplay: false, + editDisplay: false, + }, { label: '当前节点', prop: 'currentNode', diff --git a/src/option/business/project-apply.js b/src/option/business/project-apply.js index 399287b..ad46f63 100644 --- a/src/option/business/project-apply.js +++ b/src/option/business/project-apply.js @@ -169,16 +169,16 @@ export const option = createCrudOption([ rules: textRule('业务部门', 50, true), }, { - label: '承办部门', + label: '平台公司', prop: 'undertakeDeptName', search: true, searchPlaceholder: '请输入', searchOrder: 8, minWidth: 150, - rules: textRule('承办部门', 50, true), + rules: textRule('平台公司', 50, true), }, { - label: '承办部门ID', + label: '平台公司ID', prop: 'undertakeDeptId', hide: true, }, @@ -192,6 +192,15 @@ export const option = createCrudOption([ formatter: row => formatUserRealName(row, 'principal'), rules: textRule('项目负责人', 50, true), }, + { + label: '资金使用风险', + prop: 'fundUseRisk', + slot: true, + minWidth: 150, + addDisplay: false, + editDisplay: false, + hide: false, + }, { label: '项目负责人ID', prop: 'principalUserId', @@ -433,14 +442,5 @@ export const option = createCrudOption([ span: 24, hide: true, }, - { - label: '资金使用风险', - prop: 'fundUseRisk', - slot: true, - minWidth: 130, - addDisplay: false, - editDisplay: false, - hide: false, - }, ...auditColumns.map(column => ({ ...column, hide: true })), ]); diff --git a/src/option/business/shipping-template.js b/src/option/business/shipping-template.js index eb90940..ad2f735 100644 --- a/src/option/business/shipping-template.js +++ b/src/option/business/shipping-template.js @@ -55,8 +55,8 @@ export const config = { 'templateType', 'transportType', 'createUserName', - 'remark', 'updateTime', + 'remark', 'createTime', ], exportColumns: [ @@ -65,8 +65,8 @@ export const config = { { prop: 'templateType', label: '模板类型' }, { prop: 'transportType', label: '运输方式' }, { prop: 'createUserName', label: '创建人' }, - { prop: 'remark', label: '备注' }, { prop: 'updateTime', label: '更新时间' }, + { prop: 'remark', label: '备注' }, { prop: 'createTime', label: '创建时间' }, ], enableAllDept: false, @@ -82,12 +82,14 @@ export const config = { detailAttachmentDescriptionPlain: true, enableTransportPlanForm: true, enableShippingTemplateFreight: true, - editableRoadAddress: true, + editableRoadAddress: false, fixedTransportAddressType: true, enableTemplateCodePreview: true, attachmentTitle: '附件', defaultForm: { templateType: '运输计划', + transportType: '公路运输', + transportTypeName: '公路运输', }, transportFormRequiredFields: [ ['projectName', '项目'], @@ -119,15 +121,16 @@ export const option = { labelWidth: 0, }, { - label: '模板编号', - prop: 'templateCode', + label: '模板类型', + prop: 'templateType', + type: 'select', search: true, - searchOrder: 4, + searchOrder: 2, span: 8, order: 390, - minWidth: 170, - disabled: true, - placeholder: '系统自动生成', + dicData: templateTypeOptions, + minWidth: 120, + rules: selectRule('模板类型'), display: true, }, { @@ -142,16 +145,15 @@ export const option = { display: true, }, { - label: '模板类型', - prop: 'templateType', - type: 'select', + label: '模板编号', + prop: 'templateCode', search: true, - searchOrder: 2, + searchOrder: 4, span: 8, order: 370, - dicData: templateTypeOptions, - minWidth: 120, - rules: selectRule('模板类型'), + minWidth: 170, + disabled: true, + placeholder: '系统自动生成', display: true, }, { diff --git a/src/option/business/temporary-credit-limit.js b/src/option/business/temporary-credit-limit.js index 2bed4f2..4ad6fb4 100644 --- a/src/option/business/temporary-credit-limit.js +++ b/src/option/business/temporary-credit-limit.js @@ -2,7 +2,6 @@ import { approvalStatusOptions, auditColumns, createCrudOption, - futureDateRule, nonNegativeRule, textRule, withSearchPlaceholders, @@ -58,30 +57,25 @@ export const config = { detailAlignCenter: true, detailSections: [ { - title: '申请信息', + title: '项目额度信息', fields: [ - ['applicationNo', '申请单号', 1], - ['projectName', '项目', 1], + ['projectName', '项目名称', 1], ['projectCode', '项目编号', 1], ['undertakeDeptName', '承办部门', 1], - ['applyDeptName', '申请部门', 1], - ['applicantName', '申请人', 1], - ['approvalStatus', '审批状态', 1], - ['currentNode', '当前节点', 1], - ['currentProcessor', '当前处理人', 1], - ['validUntil', '申请有效期至', 1], - ['remark', '备注', 1], - ], - }, - { - title: '额度信息', - fields: [ ['projectFundLimit', '项目资金使用额度(万元)', 1], ['usedFundLimit', '已使用项目资金使用额度(万元)', 1], ['remainingFundLimit', '剩余项目资金使用额度(万元)', 1], - ['applyLimit', '申请临时额度(万元)', 1], ], }, + { + title: '临时额度信息', + fields: [ + ['applyLimit', '申请临时额度(万元)', 1], + ['validUntil', '申请有效期至', 1], + ['remark', '备注', 4], + ], + showAttachment: true, + }, ], operations: [ { @@ -140,6 +134,14 @@ export const option = { editDisplay: false, editDisabled: true, }, + { + label: '', + prop: 'projectQuotaInfoTitle', + formslot: true, + span: 24, + hide: true, + labelWidth: 0, + }, { label: '项目名称', prop: 'projectName', @@ -216,6 +218,14 @@ export const option = { value: 0, rules: [nonNegativeRule('剩余项目资金使用额度')], }, + { + label: '', + prop: 'temporaryCreditInfoTitle', + formslot: true, + span: 24, + hide: true, + labelWidth: 0, + }, { label: '申请临时额度(万元)', prop: 'applyLimit', @@ -238,9 +248,27 @@ export const option = { minWidth: 140, rules: [ { required: true, message: '请选择申请有效期至', trigger: 'change' }, - futureDateRule('申请有效期至'), ], }, + { + label: '备注', + prop: 'remark', + type: 'textarea', + span: 24, + minRows: 2, + hide: true, + placeholder: '请输入', + maxlength: 200, + showWordLimit: true, + rules: textRule('备注', 200), + }, + { + label: '附件', + prop: 'attachmentsJson', + formslot: true, + span: 24, + hide: true, + }, { label: '申请部门', prop: 'applyDeptName', @@ -304,25 +332,6 @@ export const option = { addDisplay: false, editDisplay: false, }, - { - label: '临时额度信息', - prop: 'remark', - type: 'textarea', - span: 24, - minRows: 2, - hide: true, - placeholder: '请输入', - maxlength: 200, - showWordLimit: true, - rules: textRule('临时额度信息', 200), - }, - { - label: '附件', - prop: 'attachmentsJson', - formslot: true, - span: 24, - hide: true, - }, ...auditColumns.map(column => ({ ...column, hide: true })), ])), dialogWidth: '96%', diff --git a/src/option/business/transport-plan.js b/src/option/business/transport-plan.js index 45b9154..5fb8150 100644 --- a/src/option/business/transport-plan.js +++ b/src/option/business/transport-plan.js @@ -209,12 +209,12 @@ export const config = { attachmentTitle: '附件', searchRangeMap: { planStartDateRange: ['planStartDateStart', 'planStartDateEnd'], - planEndDateRange: ['planEndDateStart', 'planEndDateEnd'], + createTimeRange: ['createTimeStart', 'createTimeEnd', '00:00:00', '23:59:59'], }, actions: ['copy', 'complete'], statusProp: 'businessStatus', statusTextProp: 'businessStatusName', - deleteStatus: ['draft', 'waiting_dispatch'], + deleteStatus: ['draft'], editStatus: ['draft', 'waiting_dispatch', 'dispatching'], detailButton: true, detailSections: [ @@ -341,6 +341,7 @@ export const option = { { label: '发货地址', prop: 'departureAddress', + slot: true, formslot: true, search: true, searchOrder: 7, @@ -352,6 +353,7 @@ export const option = { { label: '到货地址', prop: 'arrivalAddress', + slot: true, formslot: true, search: true, searchOrder: 6, @@ -577,8 +579,8 @@ export const option = { viewDisplay: false, }, { - label: '计划结束日期', - prop: 'planEndDateRange', + label: '创建时间', + prop: 'createTimeRange', type: 'date', format: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD', diff --git a/src/option/business/waybill-manage.js b/src/option/business/waybill-manage.js index e9f2af6..fe51191 100644 --- a/src/option/business/waybill-manage.js +++ b/src/option/business/waybill-manage.js @@ -19,6 +19,12 @@ const transportTypeDict = { const isEmpty = value => value === undefined || value === null || value === ''; const normalizeNumericDisplayValue = value => (Number(value) === -1 ? '' : value); +const formatMoneyDisplay = value => { + if (isEmpty(value) || Number(value) === -1) return ''; + const number = Number(value); + if (!Number.isFinite(number)) return ''; + return number.toFixed(2); +}; const parseJsonArray = value => { if (Array.isArray(value)) return value; @@ -92,14 +98,32 @@ const getProcessConfigNodes = row => { const isAcceptProcessNode = node => node.key === 'accept' || node.name === '接单'; +const isTruthyFlag = value => { + if (value === true || value === 1) return true; + if (value === false || value === 0 || value === null || value === undefined || value === '') { + return false; + } + return ['true', '1', 'yes'].includes(String(value).trim().toLowerCase()); +}; + const requiresDriverAcceptConfirmation = row => - getProcessConfigNodes(row).some( - node => - isAcceptProcessNode(node) && - node.enabled !== false && - node.confirmMode === 'yes' && - node.confirmDriver === true - ); + getProcessConfigNodes(row).some(node => { + if (!isAcceptProcessNode(node) || node.enabled === false) return false; + if (node.confirmMode === 'no_confirm_accept') return false; + // 与后端一致:是否确认=是 即需接单(不强制依赖 confirmDriver) + return node.confirmMode === 'yes' || !node.confirmMode; + }); + +const getDriverRejectReason = row => { + if (!row) return ''; + const reason = + row.driverRejectReason || + row.driverRefuseReason || + row.acceptRejectReason || + row.rejectReason || + ''; + return String(reason || '').trim(); +}; const isDriverRejectedStatus = value => { if (value === null || value === undefined || typeof value === 'boolean') return false; @@ -122,6 +146,7 @@ const isDriverRejectedStatus = value => { }; const hasDriverRejectRecord = row => { + if (String(row.driverAcceptStatus || '').toLowerCase() === 'rejected') return true; const rejectRecordValues = [ row.driverRejectTime, row.driverRejectedTime, @@ -161,9 +186,16 @@ const hasDriverRejectRecord = row => { }; const isDriverRejectedWaybill = row => - requiresDriverAcceptConfirmation(row) && hasDriverRejectRecord(row); + (row.requireAccept === true || + row.requireAccept === 1 || + requiresDriverAcceptConfirmation(row)) && + hasDriverRejectRecord(row); const hasDriverAcceptRecord = row => { + if (String(row.driverAcceptStatus || '').toLowerCase() === 'accepted') return true; + if (['rejected', 'pending'].includes(String(row.driverAcceptStatus || '').toLowerCase())) { + return false; + } const recordValues = [ row.driverAcceptTime, row.driverAcceptedTime, @@ -172,6 +204,7 @@ const hasDriverAcceptRecord = row => { row.driverAcceptedBy, row.acceptUserId, row.acceptUserName, + row.driverAcceptDriverId, ]; if (recordValues.some(value => value !== null && value !== undefined && value !== '')) { return true; @@ -315,10 +348,10 @@ const formatUnitPrice = row => { return billingUnit ? `${billingPrice}(${billingUnit})` : billingPrice; }; -const formatAmount = (row, props) => getFirstValue(row, props); +const formatAmount = (row, props) => formatMoneyDisplay(getFirstValue(row, props)); -const sumAmount = list => - list.reduce((sum, item) => { +const sumAmount = (rows = []) => + rows.reduce((sum, item) => { const value = getFirstValue(item, ['freightAmount', 'amount', 'totalAmount']); return isEmpty(value) ? sum : sum + Number(value || 0); }, 0); @@ -337,7 +370,7 @@ const calculateFreightTotal = rows => { return sum + Number(item.unitPrice || 0) * Number(item.quantity || 0); }, 0); if (!hasAmountField) return ''; - return Number.isFinite(total) ? Number(total.toFixed(2)).toString() : ''; + return Number.isFinite(total) ? total.toFixed(2) : ''; }; const formatFreight = row => { @@ -345,9 +378,9 @@ const formatFreight = row => { if (!isEmpty(value)) return value; const freight = parseJsonObject(row.freightJson); const freightValue = getFirstValue(freight, ['freightAmount', 'transportFee']); - if (!isEmpty(freightValue)) return freightValue; + if (!isEmpty(freightValue)) return formatMoneyDisplay(freightValue); const total = sumAmount(Array.isArray(freight.freightItems) ? freight.freightItems : []); - return total ? total : ''; + return total ? formatMoneyDisplay(total) : ''; }; const formatOtherFeeTotal = row => { @@ -364,10 +397,10 @@ const formatFreightTotal = row => { const otherFeeTotal = !isEmpty(otherFee) ? otherFee : formatOtherFeeTotal(row); if (!isEmpty(calculated)) { const total = Number(calculated || 0) + Number(otherFeeTotal || 0); - return Number.isFinite(total) ? Number(total.toFixed(2)).toString() : calculated; + return Number.isFinite(total) ? total.toFixed(2) : calculated; } const value = getFirstValue(freight, ['totalFreightAmount', 'totalFreight', 'totalAmount']); - if (!isEmpty(value)) return value; + if (!isEmpty(value)) return formatMoneyDisplay(value); return formatAmount(row, ['freightTotal', 'totalFreight', 'totalAmount']); }; @@ -425,26 +458,26 @@ export const config = { statusProp: 'businessStatus', statusTextProp: 'businessStatusName', formatStatus(row, prop, defaultText) { - if ( - prop === 'businessStatus' && - requiresDriverAcceptConfirmation(row) && - isInProgressStatus(row, defaultText) && - !hasDriverAcceptRecord(row) - ) { + if (prop !== 'businessStatus') return defaultText; + const status = String(row.businessStatus || ''); + const terminalStatuses = ['draft', 'completed', 'cancelled', 'waiting_dispatch', 'dispatching']; + const needAccept = + row.requireAccept === true || + row.requireAccept === 1 || + requiresDriverAcceptConfirmation(row); + // 需司机接单且尚未接单 → 待执行(含库中仍为 running 的历史数据展示校正) + if (needAccept && !hasDriverAcceptRecord(row) && !terminalStatuses.includes(status)) { return '待执行'; } - if ( - prop === 'businessStatus' && - row.businessStatus === 'pending' && - !requiresDriverAcceptConfirmation(row) - ) { - return '进行中'; - } + if (status === 'pending') return '待执行'; + if (status === 'running' || status === 'processing') return '进行中'; return defaultText; }, canReassign(row) { return isDriverRejectedWaybill(row); }, + getDriverRejectReason, + isDriverRejectedWaybill, canEdit(row) { // 进行中的运单不允许编辑 if (isInProgressStatus(row, row.businessStatusName)) { diff --git a/src/option/transportCapacity/vehicle-dispatch.js b/src/option/transportCapacity/vehicle-dispatch.js new file mode 100644 index 0000000..0e50813 --- /dev/null +++ b/src/option/transportCapacity/vehicle-dispatch.js @@ -0,0 +1,181 @@ +const approvalStatusOptions = [ + { label: '草稿', value: 'draft' }, + { label: '审批中', value: 'reviewing' }, + { label: '已驳回', value: 'rejected' }, + { label: '审批通过', value: 'approved' }, +]; + +export const statusName = status => + approvalStatusOptions.find(item => item.value === status)?.label || status || '-'; + +export const option = { + height: 'auto', + calcHeight: 32, + dialogWidth: 1200, + labelPosition: 'right', + labelWidth: 'auto', + tip: false, + searchBtnText: '查询', + emptyBtnText: '重置', + saveBtnText: '提交', + updateBtnText: '提交', + searchShow: true, + searchMenuSpan: 24, + searchIndex: 4, + searchMenuPosition: 'right', + border: true, + index: true, + indexLabel: '序号', + indexWidth: 70, + addBtn: false, + editBtn: false, + delBtn: false, + viewBtn: false, + selection: false, + dialogClickModal: false, + menuFixed: 'right', + menuWidth: 320, + column: [ + // ========== 表格列(顺序固定)========== + { + label: '申请单号', + prop: 'applicationNo', + search: true, + searchOrder: 5, + searchPlaceholder: '请输入', + minWidth: 180, + slot: true, + addDisplay: true, + editDisabled: true, + disabled: true, + placeholder: '保存后自动生成', + }, + { + label: '车牌号', + prop: 'plateNo', + search: true, + searchOrder: 4, + searchPlaceholder: '请输入', + minWidth: 120, + formslot: true, + rules: [{ required: true, message: '请选择车牌号', trigger: 'blur' }], + }, + { + label: '所属组织', + prop: 'organizationName', + search: true, + searchOrder: 3, + searchPlaceholder: '请输入', + minWidth: 140, + disabled: true, + placeholder: '选择车牌号后自动填充', + rules: [{ required: true, message: '请输入所属组织', trigger: 'blur' }], + }, + { + label: '使用部门', + prop: 'useDepartment', + search: true, + searchOrder: 2, + searchPlaceholder: '请输入', + minWidth: 140, + formslot: true, + span: 12, + rules: [{ required: true, message: '请选择使用部门', trigger: 'blur' }], + }, + { + label: '车辆类型', + prop: 'vehicleType', + minWidth: 110, + addDisplay: false, + editDisplay: false, + viewDisplay: false, + }, + { + label: '审批状态', + prop: 'approvalStatus', + search: true, + searchOrder: 1, + type: 'select', + dicData: approvalStatusOptions, + slot: true, + minWidth: 120, + addDisplay: false, + editDisplay: false, + }, + { + label: '当前节点', + prop: 'currentNode', + minWidth: 120, + addDisplay: false, + editDisplay: false, + }, + { + label: '当前处理人', + prop: 'currentProcessor', + minWidth: 130, + addDisplay: false, + editDisplay: false, + }, + { + label: '创建人', + prop: 'createUserName', + minWidth: 120, + addDisplay: false, + editDisplay: false, + }, + { + label: '创建时间', + prop: 'createTime', + type: 'datetime', + format: 'YYYY-MM-DD HH:mm:ss', + valueFormat: 'YYYY-MM-DD HH:mm:ss', + minWidth: 170, + addDisplay: false, + editDisplay: false, + }, + // ========== 新增/编辑表单专用(不进表格)========== + { + label: '申请人', + prop: 'applicantName', + hide: true, + display: true, + addDisplay: true, + editDisplay: true, + viewDisplay: true, + disabled: true, + span: 12, + }, + { + label: '申请时间', + prop: 'applyTime', + type: 'datetime', + format: 'YYYY-MM-DD HH:mm:ss', + valueFormat: 'YYYY-MM-DD HH:mm:ss', + hide: true, + display: true, + addDisplay: true, + editDisplay: true, + viewDisplay: true, + disabled: true, + span: 12, + }, + { + label: '备注', + prop: 'remark', + type: 'textarea', + hide: true, + span: 24, + maxlength: 200, + showWordLimit: true, + rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }], + }, + { + label: '附件', + prop: 'attachments', + hide: true, + span: 24, + minWidth: 100, + formslot: true, + }, + ], +}; diff --git a/src/option/vehicle/accident-record.js b/src/option/vehicle/accident-record.js index 458e597..2cb0ab6 100644 --- a/src/option/vehicle/accident-record.js +++ b/src/option/vehicle/accident-record.js @@ -70,14 +70,16 @@ export const option = { { label: '车牌号/船号', prop: 'vehicleNo', + type: 'select', slot: true, formslot: true, search: true, + searchType: 'input', minWidth: 140, span: 12, - placeholder: '输入车牌号/船号查询选择', + placeholder: '请选择车牌号/船号', rules: [ - { required: true, message: '请输入车牌号/船号', trigger: 'blur' }, + { required: true, message: '请选择车牌号/船号', trigger: 'change' }, { max: 30, message: '最多 30 个字符', trigger: 'blur' }, ], }, diff --git a/src/option/vehicle/annual-inspection-record.js b/src/option/vehicle/annual-inspection-record.js index dbf10ff..29c480d 100644 --- a/src/option/vehicle/annual-inspection-record.js +++ b/src/option/vehicle/annual-inspection-record.js @@ -75,14 +75,16 @@ export const option = { { label: '车牌号/船号', prop: 'vehicleNo', + type: 'select', slot: true, formslot: true, search: true, + searchType: 'input', minWidth: 140, span: 12, - placeholder: '输入车牌号/船号查询选择', + placeholder: '请选择车牌号/船号', rules: [ - { required: true, message: '请输入车牌号/船号', trigger: 'blur' }, + { required: true, message: '请选择车牌号/船号', trigger: 'change' }, { max: 30, message: '最多 30 个字符', trigger: 'blur' }, ], }, @@ -105,6 +107,7 @@ export const option = { type: 'date', format: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD', + slot: true, minWidth: 140, span: 12, placeholder: '请输入', @@ -191,23 +194,12 @@ export const option = { formslot: true, }, { - label: '检测评定开始日期', - prop: 'inspectionAssessmentDateStart', - type: 'date', - format: 'YYYY-MM-DD', - valueFormat: 'YYYY-MM-DD', - search: true, - hide: true, - addDisplay: false, - editDisplay: false, - viewDisplay: false, - }, - { - label: '检测评定结束日期', - prop: 'inspectionAssessmentDateEnd', + label: '检测评定日期', + prop: 'inspectionAssessmentDateRange', type: 'date', format: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD', + searchRange: true, search: true, hide: true, addDisplay: false, diff --git a/src/option/vehicle/equipment-ledger.js b/src/option/vehicle/equipment-ledger.js index 3f0e4ab..d2905b6 100644 --- a/src/option/vehicle/equipment-ledger.js +++ b/src/option/vehicle/equipment-ledger.js @@ -54,9 +54,11 @@ export const option = { { label: '车牌号/船号', prop: 'vehicleNo', + type: 'select', slot: true, formslot: true, search: true, + searchType: 'input', searchOrder: 4, span: 12, order: 10, diff --git a/src/option/vehicle/etc-record.js b/src/option/vehicle/etc-record.js index e14b301..196c952 100644 --- a/src/option/vehicle/etc-record.js +++ b/src/option/vehicle/etc-record.js @@ -49,14 +49,16 @@ export const option = { { label: '车牌号', prop: 'vehicleNo', + type: 'select', slot: true, formslot: true, search: true, + searchType: 'input', minWidth: 140, span: 12, - placeholder: '输入车牌号模糊查询选择', + placeholder: '请选择车牌号', rules: [ - { required: true, message: '请输入车牌号', trigger: 'blur' }, + { required: true, message: '请选择车牌号', trigger: 'change' }, { max: 30, message: '最多 30 个字符', trigger: 'blur' }, ], }, diff --git a/src/option/vehicle/insurance-record.js b/src/option/vehicle/insurance-record.js index 62f6f90..9058d43 100644 --- a/src/option/vehicle/insurance-record.js +++ b/src/option/vehicle/insurance-record.js @@ -78,17 +78,20 @@ export const option = { prop: 'policyFile', formslot: true, hide: true, + viewDisplay: false, span: 24, }, { label: '车牌号/船号', prop: 'vehicleNo', + type: 'select', slot: true, formslot: true, search: true, + searchType: 'input', minWidth: 140, rules: [ - { required: true, message: '请输入车牌号/船号', trigger: 'blur' }, + { required: true, message: '请选择车牌号/船号', trigger: 'change' }, { max: 50, message: '最多 50 个字符', trigger: 'blur' }, ], }, @@ -108,6 +111,7 @@ export const option = { rules: [ { required: true, message: '请输入保单号', trigger: 'blur' }, { max: 80, message: '最多 80 个字符', trigger: 'blur' }, + { pattern: /^[a-zA-Z0-9]+$/, message: '保单号只能输入数字、字母', trigger: 'blur' }, ], }, { diff --git a/src/option/vehicle/mileage-record.js b/src/option/vehicle/mileage-record.js index d8cd492..474ba66 100644 --- a/src/option/vehicle/mileage-record.js +++ b/src/option/vehicle/mileage-record.js @@ -40,13 +40,15 @@ export const option = { { label: '车牌号', prop: 'vehicleNo', + type: 'select', slot: true, formslot: true, search: true, + searchType: 'input', searchSpan: 6, minWidth: 130, rules: [ - { required: true, message: '请输入车牌号', trigger: 'blur' }, + { required: true, message: '请选择车牌号', trigger: 'change' }, { max: 30, message: '最多 30 个字符', trigger: 'blur' }, ], }, diff --git a/src/option/vehicle/oil-electric-record.js b/src/option/vehicle/oil-electric-record.js index cd2eca5..a351190 100644 --- a/src/option/vehicle/oil-electric-record.js +++ b/src/option/vehicle/oil-electric-record.js @@ -92,13 +92,15 @@ export const option = { { label: '车牌号/船号', prop: 'vehicleNo', + type: 'select', slot: true, formslot: true, search: true, + searchType: 'input', minWidth: 150, - placeholder: '输入车牌号/船号查询选择', + placeholder: '请选择车牌号/船号', rules: [ - { required: true, message: '请输入车牌号/船号', trigger: 'blur' }, + { required: true, message: '请选择车牌号/船号', trigger: 'change' }, { max: 30, message: '最多 30 个字符', trigger: 'blur' }, ], }, diff --git a/src/option/vehicle/other-expense-record.js b/src/option/vehicle/other-expense-record.js index 115850c..535a4c9 100644 --- a/src/option/vehicle/other-expense-record.js +++ b/src/option/vehicle/other-expense-record.js @@ -17,16 +17,6 @@ export const vehicleTypeDic = [ { label: '船舶', value: '船舶' }, ]; -export const expenseTypeDic = [ - { label: '过路费', value: '过路费' }, - { label: '停车费', value: '停车费' }, - { label: '维修费', value: '维修费' }, - { label: '保险费', value: '保险费' }, - { label: '年检费', value: '年检费' }, - { label: '装卸费', value: '装卸费' }, - { label: '其他', value: '其他' }, -]; - export const option = { height: 'auto', calcHeight: 32, @@ -68,14 +58,16 @@ export const option = { { label: '车牌号/船号', prop: 'vehicleNo', + type: 'select', slot: true, formslot: true, search: true, + searchType: 'input', minWidth: 150, span: 12, - placeholder: '输入车牌号/船号查询选择', + placeholder: '请选择车牌号/船号', rules: [ - { required: true, message: '请输入车牌号/船号', trigger: 'blur' }, + { required: true, message: '请选择车牌号/船号', trigger: 'change' }, { max: 30, message: '最多 30 个字符', trigger: 'blur' }, ], }, @@ -95,7 +87,7 @@ export const option = { label: '费用类型', prop: 'expenseType', type: 'select', - dicData: expenseTypeDic, + dicData: [], search: true, minWidth: 150, span: 12, diff --git a/src/option/vehicle/tire-replacement-record.js b/src/option/vehicle/tire-replacement-record.js index 0154a9f..4ca62fa 100644 --- a/src/option/vehicle/tire-replacement-record.js +++ b/src/option/vehicle/tire-replacement-record.js @@ -49,14 +49,16 @@ export const option = { { label: '车牌号', prop: 'vehicleNo', + type: 'select', slot: true, formslot: true, search: true, + searchType: 'input', minWidth: 130, span: 12, - placeholder: '输入车牌号查询选择', + placeholder: '请选择车牌号', rules: [ - { required: true, message: '请输入车牌号', trigger: 'blur' }, + { required: true, message: '请选择车牌号', trigger: 'change' }, { max: 30, message: '最多 30 个字符', trigger: 'blur' }, ], }, diff --git a/src/option/vehicle/transport-change-record.js b/src/option/vehicle/transport-change-record.js index edc75dd..c90dea6 100644 --- a/src/option/vehicle/transport-change-record.js +++ b/src/option/vehicle/transport-change-record.js @@ -47,11 +47,13 @@ export const option = { { label: '车牌号/船号', prop: 'vehicleNo', + type: 'select', slot: true, formslot: true, search: true, + searchType: 'input', minWidth: 140, - placeholder: '输入车牌号/船号模糊查询选择', + placeholder: '请选择车牌号/船号', rules: [{ max: 30, message: '最多 30 个字符', trigger: 'blur' }], }, { diff --git a/src/option/vehicle/violation-record.js b/src/option/vehicle/violation-record.js index ba18932..cc6fe20 100644 --- a/src/option/vehicle/violation-record.js +++ b/src/option/vehicle/violation-record.js @@ -70,14 +70,16 @@ export const option = { { label: '车牌号/船号', prop: 'vehicleNo', + type: 'select', slot: true, formslot: true, search: true, + searchType: 'input', minWidth: 140, span: 12, - placeholder: '输入车牌号/船号查询选择', + placeholder: '请选择车牌号/船号', rules: [ - { required: true, message: '请输入车牌号/船号', trigger: 'blur' }, + { required: true, message: '请选择车牌号/船号', trigger: 'change' }, { max: 30, message: '最多 30 个字符', trigger: 'blur' }, ], }, @@ -117,13 +119,13 @@ export const option = { { label: '日期', prop: 'violationTime', - type: 'datetime', - format: 'YYYY-MM-DD HH:mm:ss', + type: 'date', + format: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD HH:mm:ss', - minWidth: 170, + minWidth: 140, span: 12, placeholder: '请选择', - rules: [{ required: true, message: '请选择时间', trigger: 'click' }], + rules: [{ required: true, message: '请选择日期', trigger: 'click' }], }, { label: '地点', diff --git a/src/page/index/layout.vue b/src/page/index/layout.vue index 0439730..f0531f9 100644 --- a/src/page/index/layout.vue +++ b/src/page/index/layout.vue @@ -1,7 +1,18 @@ + + diff --git a/src/permission.js b/src/permission.js index 387889a..9051363 100644 --- a/src/permission.js +++ b/src/permission.js @@ -2,11 +2,38 @@ import router from './router/'; import store from './store'; import { tabKeyOf } from '@/router/tab'; import { getToken } from '@/utils/auth'; +import { + consumeReloadQuery, + hasReloadQuery, + isChunkLoadError, + reloadForChunkError, + setPendingRoutePath, +} from '@/utils/chunk-reload'; +import { ElMessage } from 'element-plus'; import NProgress from 'nprogress'; // progress bar import 'nprogress/nprogress.css'; // progress bar style NProgress.configure({ showSpinner: false }); const lockPage = '/lock'; //锁屏页 + +// 懒加载 chunk / CSS 失败时整页刷新,避免菜单点击后卡死 +router.onError(error => { + if (!isChunkLoadError(error)) return; + if (reloadForChunkError()) { + ElMessage.warning('页面资源已更新,正在重新加载…'); + } +}); + router.beforeEach((to, from, next) => { + if (hasReloadQuery(to.query)) { + next({ + path: to.path, + query: consumeReloadQuery(to.query), + hash: to.hash, + replace: true, + }); + return; + } + setPendingRoutePath(to.fullPath); const meta = to.meta || {}; const isMenu = meta.menu === undefined ? to.query.menu : meta.menu; store.commit('SET_IS_MENU', isMenu === undefined); @@ -35,7 +62,7 @@ router.beforeEach((to, from, next) => { fullPath: tabKeyOf(to), params: to.params, query: to.query, - meta: meta, + meta: { ...meta, keepAlive: true }, }); } next(); diff --git a/src/router/avue-router.js b/src/router/avue-router.js index 7c0a38c..3785072 100644 --- a/src/router/avue-router.js +++ b/src/router/avue-router.js @@ -2,6 +2,7 @@ import website from '@/config/website'; import { getToken } from '@/utils/auth'; import store from '@/store'; import { generateIframePath, processUrlForQuery, isURL } from './router'; +import { wrapViewLoader } from '@/utils/chunk-reload'; const modules = import.meta.glob('../**/**/*.vue'); // 将多级路由扁平化为二级路由,支持 keep-alive 跨层级缓存 @@ -96,21 +97,23 @@ RouterPlugin.install = function (option = {}) { component: (() => { // 判断是否为首路由 if (first) { - return modules[ - option.store.getters.isMacOs || !website.setting.menu - ? '../page/index/layout.vue' - : '../page/index/index.vue' - ]; + return wrapViewLoader( + modules[ + option.store.getters.isMacOs || !website.setting.menu + ? '../page/index/layout.vue' + : '../page/index/index.vue' + ] + ); // 判断是否为多层路由 } else if (isChild && !first) { - return modules['../page/index/layout.vue']; + return wrapViewLoader(modules['../page/index/layout.vue']); // 判断是否为最终的页面视图 } else { let result = modules[`../${component}.vue`]; if (!result) { isComponent = false; } - return result; + return wrapViewLoader(result); } })(), name, @@ -127,7 +130,7 @@ RouterPlugin.install = function (option = {}) { if (first) { oMenu[propsDefault.path] = `${path}`; let componentPath = oMenu.component || component; - let result = modules[`../${componentPath}.vue`]; + let result = wrapViewLoader(modules[`../${componentPath}.vue`]); if (!result) { isComponent = false; } @@ -173,7 +176,7 @@ export const formatPath = (ele, first) => { const icon = ele[propsDefault.icon]; ele[propsDefault.icon] = icon || ''; ele.meta = { - keepAlive: ele.isOpen === 2, + keepAlive: true, }; const iframeComponent = 'components/iframe/main'; const iframeSrc = href => { @@ -215,7 +218,7 @@ export const formatPath = (ele, first) => { ele[propsDefault.children].forEach(child => { child.component = 'views' + child[propsDefault.path]; child.meta = { - keepAlive: child.isOpen === 2, + keepAlive: true, }; if (isURL(child[propsDefault.href])) { let href = child[propsDefault.href]; diff --git a/src/router/tab.js b/src/router/tab.js index f715c00..d904ad1 100644 --- a/src/router/tab.js +++ b/src/router/tab.js @@ -128,5 +128,5 @@ export function tabView(route, Component) { }; wrapperMap.set(tabKey, wrapper); } - return h(wrapper); + return wrapper; } diff --git a/src/router/views/index.js b/src/router/views/index.js index 458a44e..8ac0044 100644 --- a/src/router/views/index.js +++ b/src/router/views/index.js @@ -155,6 +155,18 @@ export default [ }, ], }, + { + path: '/business/contract-manage/detail', + component: Layout, + children: [ + { + path: '', + name: '合同详情', + meta: { keepAlive: false, activeMenu: '/business/contract-manage' }, + component: () => import('@/views/business/contract-manage.vue'), + }, + ], + }, { path: '/business/project-apply/form', component: Layout, diff --git a/src/store/getters.js b/src/store/getters.js index 3edda43..fb53825 100644 --- a/src/store/getters.js +++ b/src/store/getters.js @@ -16,11 +16,8 @@ const getters = { lockPasswd: state => state.common.lockPasswd, tagList: state => state.tags.tagList, tagsKeep: (state, getters) => { - return getters.tagList - .filter(ele => { - return (ele.meta || {}).keepAlive; - }) - .map(ele => ele.fullPath); + // 所有已打开标签均纳入 keep-alive,关闭标签后自动从白名单移除并释放实例 + return getters.tagList.map(ele => ele.fullPath).filter(Boolean); }, tagWel: state => state.tags.tagWel, token: state => state.user.token, diff --git a/src/utils/chunk-reload.js b/src/utils/chunk-reload.js new file mode 100644 index 0000000..3c30bb4 --- /dev/null +++ b/src/utils/chunk-reload.js @@ -0,0 +1,168 @@ +/** + * 懒加载 chunk / CSS preload 失败后的整页恢复。 + * 常见于:部署后旧 hash 失效、静态服务空闲断连、代理 CONNECTION_RESET。 + * + * 失败后必须整页刷新:旧入口里的 hashed 资源地址不会自行更新, + * 仅捕获路由错误而不刷新时,未打开过的页面会一直无法进入。 + */ + +const RELOAD_FLAG = 'app:chunk-reload-ts'; +const RELOAD_QUERY = '_chunkreload'; +const RELOAD_COOLDOWN_MS = 15000; + +const CHUNK_ERROR_RE = + /Failed to fetch dynamically imported module|Importing a module script failed|Unable to preload CSS|error loading dynamically imported module|Loading CSS chunk|Loading chunk .+ failed|ChunkLoadError|Unable to preload|error loading module|Load failed/i; + +/** 最近一次路由跳转目标,供 onError 时整页落到正确地址 */ +let pendingFullPath = ''; +let installed = false; +let reloading = false; + +export function setPendingRoutePath(fullPath = '') { + pendingFullPath = fullPath || ''; +} + +function collectErrorText(error, depth = 0) { + if (!error || depth > 3) return ''; + if (typeof error === 'string') return error; + const parts = [ + error.message, + error.msg, + error.name, + error.stack, + error.error && collectErrorText(error.error, depth + 1), + error.reason && collectErrorText(error.reason, depth + 1), + error.cause && collectErrorText(error.cause, depth + 1), + error.payload && collectErrorText(error.payload, depth + 1), + ]; + const target = error.target; + if (target && (target.src || target.href)) { + parts.push(target.src || target.href); + } + try { + parts.push(String(error)); + } catch (e) { + /* ignore */ + } + return parts.filter(Boolean).join(' '); +} + +export function isChunkLoadError(error) { + const text = collectErrorText(error); + if (!text) return false; + if (CHUNK_ERROR_RE.test(text)) return true; + return /Failed to fetch/i.test(text) && /\.m?js(\?|$|:)/i.test(text); +} + +export function hasReloadQuery(query = {}) { + return !!(query && query[RELOAD_QUERY] !== undefined); +} + +export function consumeReloadQuery(query = {}) { + if (!hasReloadQuery(query)) return query; + const next = { ...query }; + delete next[RELOAD_QUERY]; + return next; +} + +function buildReloadUrl(targetPath) { + const url = new URL(targetPath, window.location.origin); + url.searchParams.set(RELOAD_QUERY, String(Date.now())); + return url.pathname + url.search + url.hash; +} + +/** + * @returns {boolean} 是否已触发刷新(冷却期内返回 false,避免死循环) + */ +export function reloadForChunkError(targetPath) { + if (reloading) return false; + const now = Date.now(); + const last = Number(sessionStorage.getItem(RELOAD_FLAG) || 0); + if (now - last < RELOAD_COOLDOWN_MS) { + return false; + } + reloading = true; + sessionStorage.setItem(RELOAD_FLAG, String(now)); + const path = + targetPath || + pendingFullPath || + `${window.location.pathname}${window.location.search}${window.location.hash}`; + const dest = buildReloadUrl(path); + // cache: 'reload' 会回写 HTTP 缓存,降低 index.html 仍指向旧 hash 资源的概率 + fetch(`${window.location.pathname}?${RELOAD_QUERY}=${now}`, { + cache: 'reload', + credentials: 'same-origin', + headers: { + Pragma: 'no-cache', + 'Cache-Control': 'no-cache', + }, + }) + .catch(() => {}) + .finally(() => { + window.location.replace(dest); + }); + return true; +} + +export function wrapViewLoader(loader) { + if (typeof loader !== 'function') return loader; + return () => + Promise.resolve() + .then(() => loader()) + .catch(error => { + if (isChunkLoadError(error)) { + reloadForChunkError(); + } + throw error; + }); +} + +export function installChunkReload() { + if (installed || typeof window === 'undefined') return; + installed = true; + + window.addEventListener('vite:preloadError', event => { + if (reloadForChunkError()) { + event.preventDefault(); + } + }); + + window.addEventListener('unhandledrejection', event => { + if (!isChunkLoadError(event.reason)) return; + if (reloadForChunkError()) { + event.preventDefault(); + } + }); + + window.addEventListener( + 'error', + event => { + const el = event.target; + const isAssetNode = + el && + el !== window && + (el.tagName === 'SCRIPT' || + (el.tagName === 'LINK' && /modulepreload|stylesheet/i.test(el.rel || ''))); + if (isAssetNode || isChunkLoadError(event.error || event.message)) { + if (reloadForChunkError()) { + event.preventDefault(); + } + } + }, + true + ); + + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') checkDeployedAssets(); + }); +} + +function checkDeployedAssets() { + const el = document.querySelector('script[type="module"][src*="/assets/"]'); + if (!el || !el.src) return; + fetch(el.src, { method: 'HEAD', cache: 'no-store', credentials: 'same-origin' }) + .then(res => { + if (res.status === 404) reloadForChunkError(); + }) + .catch(() => {}); +} diff --git a/src/views/base/insurance-ocr-template.vue b/src/views/base/insurance-ocr-template.vue index d1085de..8a015e6 100644 --- a/src/views/base/insurance-ocr-template.vue +++ b/src/views/base/insurance-ocr-template.vue @@ -20,6 +20,16 @@ 批量删除 - @@ -297,9 +303,6 @@ {{ displayValue(matchForm.destination) }} - {{ - transportModeLabel(matchForm.transportMode) - }} {{ displayValue(matchForm.cargoType) }} @@ -334,21 +337,6 @@ @visible-change="v => v && ensureRegions()" @change="v => matchRegionChange('destination', v)" /> - ({ - lowerLimit: item?.lowerLimit ?? '', - upperLimit: item?.upperLimit ?? '', - unitPrice: - item?.unitPrice === undefined || item?.unitPrice === '' - ? row.unitPrice ?? '' - : item.unitPrice, - minimumBillingWeight: - item?.minimumBillingWeight === undefined || item?.minimumBillingWeight === '' - ? this.usesRangeMinimum(row) - ? row.minimumBillingWeight ?? '' - : '' - : item.minimumBillingWeight, - })) - .filter( - item => - item.lowerLimit !== '' || - item.upperLimit !== '' || - item.unitPrice !== '' || - item.minimumBillingWeight !== '' - ) - : []; + const rawRanges = Array.isArray(row.limitRanges) ? row.limitRanges : []; + const ranges = rawRanges + .map((item, index) => ({ + lowerLimit: item?.lowerLimit ?? '', + upperLimit: item?.upperLimit ?? '', + unitPrice: + item?.unitPrice === undefined || item?.unitPrice === '' + ? row.unitPrice ?? '' + : item.unitPrice, + minimumBillingWeight: this.resolveRangeMinimum(row, item, index), + })) + .filter( + item => + item.lowerLimit !== '' || + item.upperLimit !== '' || + item.unitPrice !== '' || + item.minimumBillingWeight !== '' + ); if ( !ranges.length && (row.lowerLimit !== '' || @@ -598,9 +580,23 @@ export default { minimumBillingWeight: this.usesRangeMinimum(row) ? row.minimumBillingWeight ?? '' : '', }); return ranges.length - ? ranges + ? ranges.map((item, index) => ({ + ...item, + minimumBillingWeight: this.usesRangeMinimum(row) + ? index === 0 + ? item.minimumBillingWeight + : '' + : '', + })) : [{ lowerLimit: '', upperLimit: '', unitPrice: '', minimumBillingWeight: '' }]; }, + resolveRangeMinimum(row, item, index) { + if (!this.usesRangeMinimum(row) || index !== 0) return ''; + if (item?.minimumBillingWeight !== undefined && item?.minimumBillingWeight !== '') { + return item.minimumBillingWeight; + } + return row.minimumBillingWeight ?? ''; + }, syncLegacyLimit(row) { const first = row.limitRanges?.[0] || {}; row.lowerLimit = first.lowerLimit ?? ''; @@ -803,11 +799,16 @@ export default { if (index === 0) row.unitPrice = target.value; }, rangeMinimumInput(row, index, value) { + if (index !== 0) return; row.limitRanges = this.getRanges(row); const target = { value }; this.decimalInput(target, 'value', value); - row.limitRanges[index].minimumBillingWeight = target.value; - if (index === 0) row.minimumBillingWeight = target.value; + row.limitRanges[0].minimumBillingWeight = target.value; + row.minimumBillingWeight = target.value; + row.limitRanges = row.limitRanges.map((item, rangeIndex) => ({ + ...item, + minimumBillingWeight: rangeIndex === 0 ? target.value : '', + })); }, addRule() { this.draft.rules.push(defaultRule()); @@ -929,15 +930,22 @@ export default { plan.transportModeLabel = transportMode?.label || plan.transportMode; plan.rules = plan.rules.map(rule => { const ranges = this.canEditLimit(rule) ? this.getRanges(rule) : []; + const normalizedRanges = ranges.map((item, rangeIndex) => ({ + ...item, + minimumBillingWeight: + this.usesRangeMinimum(rule) && rangeIndex === 0 ? item.minimumBillingWeight || '' : '', + })); return { ...rule, - unitPrice: this.usesRangeUnitPrice(rule) ? ranges[0]?.unitPrice || '' : rule.unitPrice, - limitRanges: ranges, - lowerLimit: ranges[0]?.lowerLimit || '', - upperLimit: ranges[0]?.upperLimit || '', + unitPrice: this.usesRangeUnitPrice(rule) + ? normalizedRanges[0]?.unitPrice || '' + : rule.unitPrice, + limitRanges: normalizedRanges, + lowerLimit: normalizedRanges[0]?.lowerLimit || '', + upperLimit: normalizedRanges[0]?.upperLimit || '', minimumBillingWeight: this.canEditMinimum(rule) ? this.usesRangeMinimum(rule) - ? ranges[0]?.minimumBillingWeight || '' + ? normalizedRanges[0]?.minimumBillingWeight || '' : rule.minimumBillingWeight : '', }; @@ -950,9 +958,6 @@ export default { this.matchIndex = index; this.matchForm = { ...defaultRule().matchCondition, ...(row.matchCondition || {}) }; this.matchVisible = true; - this.ensureTransportModes().then(() => { - this.matchForm.transportMode = this.normalizeTransportMode(this.matchForm.transportMode); - }); this.ensureRegions(); this.ensureCargo().then(() => { const path = this.resolveCargoPath(this.matchForm); @@ -977,21 +982,6 @@ export default { }); return this.transportModeRequest; }, - normalizeTransportMode(value) { - const transportMode = String(value || '').trim(); - if (!transportMode) return ''; - const option = this.transportModeOptions.find(item => { - const optionValue = String(item.value || '').trim(); - const optionLabel = String(item.label || '').trim(); - return ( - optionValue === transportMode || - optionLabel === transportMode || - optionLabel.includes(transportMode) || - transportMode.includes(optionLabel) - ); - }); - return option?.value || transportMode; - }, ensureRegions() { if (this.regionOptions.length) return Promise.resolve(this.regionOptions); if (this.regionRequest) return this.regionRequest; @@ -1214,6 +1204,11 @@ export default { cursor: pointer; } +.billing-plan-editor__column-title { + display: inline-flex; + align-items: center; +} + .limit-list { display: flex; flex-direction: column; @@ -1229,4 +1224,8 @@ export default { .limit-row .el-input { flex: 1; } + +.limit-placeholder { + height: 32px; +} diff --git a/src/views/business/components/business-crud-page.vue b/src/views/business/components/business-crud-page.vue index f237a35..a1131fb 100644 --- a/src/views/business/components/business-crud-page.vue +++ b/src/views/business/components/business-crud-page.vue @@ -1927,13 +1927,13 @@ @@ -1986,6 +1986,59 @@ +
+ + + + + + + + + + + + + +
@@ -5412,13 +5465,19 @@ const defaultBillingPlan = () => ({ }); const defaultSettlementRule = () => ({ - autoGenerate: 1, + autoGenerate: '', billStartDate: '', - settlementType: '月结', - billCycleType: '固定截单日', - billCutoffDay: 25, + settlementType: '', + billCycleType: '', + billCutoffDay: '', cycleDays: '', + customPeriods: [], }); +const normalizeCustomPeriods = (periods = []) => + (periods || []).map(item => ({ + startDay: Number(item?.startDay) > 0 ? Number(item.startDay) : '', + endDay: Number(item?.endDay) > 0 ? Number(item.endDay) : '', + })); const defaultReconciliation = () => ({ skipReconciliation: 1, @@ -5837,7 +5896,7 @@ export default { 按数量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], }, settlementTypeOptions: ['月结', '日结', '周结', '半月结', '固定天数周期结算'], - billCycleTypeOptions: ['固定截单日', '自然月'], + billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'], reconciliationModeOptions: ['明细逐行核对', '按账单汇总核对', '跳过自动核对'], packageOptions, quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'], @@ -6448,6 +6507,11 @@ export default { this.showSettlementBillCycleType && this.settlementRuleForm.billCycleType === '固定截单日' ); }, + showSettlementCustomPeriods() { + return ( + this.showSettlementBillCycleType && this.settlementRuleForm.billCycleType === '自定义多周期' + ); + }, showSettlementCycleDays() { return this.settlementRuleForm.settlementType === '固定天数周期结算'; }, @@ -6874,7 +6938,7 @@ export default { const status = this.statusValue(row); return this.config.permission === 'transport_plan' ? ['waiting_dispatch', 'dispatching'].includes(status) - : ['pending', 'processing'].includes(status); + : ['pending', 'processing', 'running'].includes(status); }, canComplete(row) { if ( @@ -6888,7 +6952,7 @@ export default { const status = this.statusValue(row); return this.config.permission === 'transport_plan' ? status === 'dispatching' - : status === 'processing'; + : status === 'processing' || status === 'running'; }, canMaintainMileage(row) { return ( @@ -7025,7 +7089,7 @@ export default { if (!row) return '-'; if (prop === 'businessStatus') return this.displayStatus(row, prop); if (prop === 'feeGenerationMode') { - return row[prop] === 'manual' ? '手动生成' : '系统生成'; + return row[prop] === 'manual' ? '账单导入生成' : '系统生成'; } const column = this.findColumn(this.option.column, prop); if (column?.formatter) return column.formatter(row, {}, row[prop]); @@ -7115,7 +7179,9 @@ export default { waybillAmountWithCurrency(row = {}, prop) { const value = this.formatDetailValue(row, prop); if (value === '-') return { value: '-' }; - return { value: `${this.waybillCurrencyRemark(row)}${value}` }; + const number = Number(value); + const display = Number.isFinite(number) ? number.toFixed(2) : value; + return { value: `${this.waybillCurrencyRemark(row)}${display}` }; }, normalizeTransportPlanWaybillRow(row = {}, index = 0) { const status = row.businessStatus || row.status || row.waybillStatus || ''; @@ -7566,23 +7632,32 @@ export default { isFixedBillCutoffSettlement(rule = this.settlementRuleForm) { return this.isMonthlySettlement(rule) && rule.billCycleType === '固定截单日'; }, + isCustomMultiPeriodSettlement(rule = this.settlementRuleForm) { + return this.isMonthlySettlement(rule) && rule.billCycleType === '自定义多周期'; + }, isFixedCycleSettlement(rule = this.settlementRuleForm) { return rule.settlementType === '固定天数周期结算'; }, handleSettlementTypeChange(value) { this.clearSettlementRuleError('settlementType'); if (value === '月结') { - if (!this.settlementRuleForm.billCycleType) { - this.settlementRuleForm.billCycleType = '固定截单日'; - } if (this.settlementRuleForm.billCycleType === '固定截单日') { - this.settlementRuleForm.billCutoffDay = this.settlementRuleForm.billCutoffDay || 25; + this.settlementRuleForm.customPeriods = []; + } else if (this.settlementRuleForm.billCycleType === '自定义多周期') { + this.settlementRuleForm.billCutoffDay = ''; + this.settlementRuleForm.customPeriods = normalizeCustomPeriods( + this.settlementRuleForm.customPeriods || [] + ); + } else { + this.settlementRuleForm.billCutoffDay = ''; + this.settlementRuleForm.customPeriods = []; } this.settlementRuleForm.cycleDays = ''; return; } this.settlementRuleForm.billCycleType = ''; this.settlementRuleForm.billCutoffDay = ''; + this.settlementRuleForm.customPeriods = []; if (value !== '固定天数周期结算') { this.settlementRuleForm.cycleDays = ''; } @@ -7590,10 +7665,17 @@ export default { handleSettlementBillCycleTypeChange(value) { this.clearSettlementRuleError('billCycleType'); if (value === '固定截单日') { - this.settlementRuleForm.billCutoffDay = this.settlementRuleForm.billCutoffDay || 25; + this.settlementRuleForm.customPeriods = []; return; } this.settlementRuleForm.billCutoffDay = ''; + if (value === '自定义多周期') { + this.settlementRuleForm.customPeriods = normalizeCustomPeriods( + this.settlementRuleForm.customPeriods || [] + ); + return; + } + this.settlementRuleForm.customPeriods = []; }, normalizeSettlementRule(rule = this.settlementRuleForm) { const nextRule = { @@ -7603,14 +7685,94 @@ export default { if (!this.isMonthlySettlement(nextRule)) { nextRule.billCycleType = ''; nextRule.billCutoffDay = ''; + nextRule.customPeriods = []; } else if (!this.isFixedBillCutoffSettlement(nextRule)) { nextRule.billCutoffDay = ''; } + if (this.isCustomMultiPeriodSettlement(nextRule)) { + nextRule.customPeriods = normalizeCustomPeriods( + Array.isArray(nextRule.customPeriods) ? nextRule.customPeriods : [] + ); + } else { + nextRule.customPeriods = []; + } if (!this.isFixedCycleSettlement(nextRule)) { nextRule.cycleDays = ''; } return nextRule; }, + customPeriodEndDayOptions(row) { + const startDay = Number(row?.startDay); + if (!Number.isFinite(startDay) || startDay < 1) return this.billCutoffDayOptions; + return Array.from({ length: 31 - startDay + 1 }, (_, index) => { + const value = startDay + index; + return { label: `${value}日`, value }; + }); + }, + handleCustomPeriodStartDayChange(index, value) { + const periods = normalizeCustomPeriods(this.settlementRuleForm.customPeriods || []); + const row = periods[index]; + if (!row) return; + row.startDay = value || ''; + const startDay = Number(row.startDay); + const endDay = Number(row.endDay); + if ( + Number.isFinite(startDay) && + Number.isFinite(endDay) && + (endDay < startDay || endDay > 31) + ) { + row.endDay = ''; + } + this.settlementRuleForm.customPeriods = periods; + }, + handleCustomPeriodEndDayChange(index, value) { + const periods = normalizeCustomPeriods(this.settlementRuleForm.customPeriods || []); + const row = periods[index]; + if (!row) return; + row.endDay = value || ''; + this.settlementRuleForm.customPeriods = periods; + }, + addCustomPeriodRow() { + const periods = normalizeCustomPeriods(this.settlementRuleForm.customPeriods || []); + periods.push({ startDay: '', endDay: '' }); + this.settlementRuleForm.customPeriods = periods; + }, + removeCustomPeriodRow(index) { + if (index <= 0) return; + const periods = [...(this.settlementRuleForm.customPeriods || [])]; + periods.splice(index, 1); + this.settlementRuleForm.customPeriods = normalizeCustomPeriods(periods); + }, + validateCustomPeriods(periods = [], label = '') { + const rows = normalizeCustomPeriods(periods); + const prefix = label ? `${label}:` : ''; + if (!rows.length) { + this.$message.warning(`${prefix}请至少配置一段自定义周期`); + return false; + } + for (let index = 0; index < rows.length; index += 1) { + const row = rows[index]; + const startDay = Number(row.startDay); + const endDay = Number(row.endDay); + if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { + this.$message.warning(`${prefix}请选择第${index + 1}行运单区间开始日`); + return false; + } + if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { + this.$message.warning(`${prefix}请选择第${index + 1}行运单区间结束日`); + return false; + } + if (endDay < startDay) { + this.$message.warning(`${prefix}第${index + 1}行结束日不能早于开始日`); + return false; + } + if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { + this.$message.warning(`${prefix}自定义多周期区间必须连续,不允许重叠或存在日期缺口`); + return false; + } + } + return true; + }, clearSettlementRuleError(field) { if (!this.settlementRuleErrors[field]) return; this.settlementRuleErrors = { @@ -7632,8 +7794,8 @@ export default { return false; } if (this.isMonthlySettlement(rule) && this.isBillingFieldEmpty(rule.billCycleType)) { - this.settlementRuleErrors = { billCycleType: '请选择账单周期类型' }; - this.$message.warning(`${label}${label ? ':' : ''}请选择账单周期类型`); + this.settlementRuleErrors = { billCycleType: '请选择结算周期' }; + this.$message.warning(`${label}${label ? ':' : ''}请选择结算周期`); return false; } if (this.isFixedBillCutoffSettlement(rule)) { @@ -7649,6 +7811,9 @@ export default { return false; } } + if (this.isCustomMultiPeriodSettlement(rule)) { + return this.validateCustomPeriods(rule.customPeriods, label); + } if (this.isFixedCycleSettlement(rule)) { const cycleDays = Number(rule.cycleDays); if (!Number.isInteger(cycleDays) || cycleDays < 1 || cycleDays > 365) { @@ -11239,8 +11404,10 @@ export default { if (!this.canEditBillingLimit(row)) { continue; } - const key = row.billingElement || ''; - groups[key] = groups[key] || []; + const feeItem = String(row.feeItem || '').trim(); + const billingElement = String(row.billingElement || '').trim(); + const key = JSON.stringify([feeItem, billingElement]); + groups[key] = groups[key] || { feeItem, billingElement, ranges: [] }; const ranges = this.getBillingLimitRanges(row); for (const range of ranges) { const lower = Number(range.lowerLimit); @@ -11258,14 +11425,14 @@ export default { this.$message.warning('计费要素下限不能大于上限'); return false; } - groups[key].push({ lower, upper }); + groups[key].ranges.push({ lower, upper }); } } return this.validateBillingLimitRangeGroups(groups); }, validateBillingLimitRangeGroups(groups) { const precision = 0.000001; - for (const [billingElement, ranges] of Object.entries(groups)) { + for (const { feeItem, billingElement, ranges } of Object.values(groups)) { if (ranges.length <= 1) continue; const sortedRanges = [...ranges].sort((prev, next) => { if (prev.lower !== next.lower) return prev.lower - next.lower; @@ -11275,11 +11442,13 @@ export default { const prevRange = sortedRanges[index - 1]; const currentRange = sortedRanges[index]; if (currentRange.lower < prevRange.upper - precision) { - this.$message.warning(`${billingElement}的计费要素区间不能重叠`); + this.$message.warning(`费用项“${feeItem}”${billingElement}的计费要素区间不能重叠`); return false; } if (currentRange.lower > prevRange.upper + precision) { - this.$message.warning(`${billingElement}的计费要素区间必须连续,不能存在间隙`); + this.$message.warning( + `费用项“${feeItem}”${billingElement}的计费要素区间必须连续,不能存在间隙` + ); return false; } } diff --git a/src/views/business/components/contract-attachment-section.vue b/src/views/business/components/contract-attachment-section.vue new file mode 100644 index 0000000..ef36b9b --- /dev/null +++ b/src/views/business/components/contract-attachment-section.vue @@ -0,0 +1,596 @@ + + + + + diff --git a/src/views/business/components/master-order-detail.vue b/src/views/business/components/master-order-detail.vue index 5a668c4..730fe09 100644 --- a/src/views/business/components/master-order-detail.vue +++ b/src/views/business/components/master-order-detail.vue @@ -1,8 +1,51 @@