From cc22b23d886951963efaee67a3e45a8ced868270 Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Thu, 5 Nov 2026 19:19:35 +0800 Subject: [PATCH 01/10] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E7=BA=A7=E8=81=94=E5=9B=9E=E6=98=BE=E6=97=B6=E9=80=92?= =?UTF-8?q?=E5=BD=92=E5=8F=96=E6=95=B4=E6=A3=B5=E6=A0=91=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E6=A0=88=E6=BA=A2=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mixins/organization-search.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mixins/organization-search.js b/src/mixins/organization-search.js index fd06b59..7a69e9d 100644 --- a/src/mixins/organization-search.js +++ b/src/mixins/organization-search.js @@ -143,7 +143,9 @@ export default { for (const node of tree || []) { const path = [...parents, String(node.id)]; if (String(node.id) === String(target)) return path; - const matched = this.findOrgPath(target, node.children, path); + // 必须显式传 node.children || []:叶节点的 children 是 undefined, + // 传 undefined 会触发默认参数重新拿到整棵树,逐层重复扫描直到爆栈 + const matched = this.findOrgPath(target, node.children || [], path); if (matched) return matched; } return null; From 7842e3f78d55f96aa311aef160e44cbffb800884 Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Thu, 5 Nov 2026 19:19:40 +0800 Subject: [PATCH 02/10] =?UTF-8?q?:sparkles:=20=E6=96=B0=E5=A2=9E=E5=8F=B8?= =?UTF-8?q?=E6=9C=BA=E6=97=B6=E9=A2=84=E5=A1=AB=E6=89=80=E5=B1=9E=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E4=B8=BA=E5=B9=BF=E8=A5=BF=E6=A1=82=E7=89=A9=E7=89=A9?= =?UTF-8?q?=E6=B5=81=E6=9C=8D=E5=8A=A1=E9=9B=86=E5=9B=A2=E6=9C=89=E9=99=90?= =?UTF-8?q?=E5=85=AC=E5=8F=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/transportCapacity/driver.vue | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/views/transportCapacity/driver.vue b/src/views/transportCapacity/driver.vue index ab82766..c6c0ae2 100644 --- a/src/views/transportCapacity/driver.vue +++ b/src/views/transportCapacity/driver.vue @@ -765,6 +765,10 @@ const emptyForm = () => ({ status: 1, }); +// 新增司机时预填的组织。名称必须与组织树里的节点完全一致:级联框按名称回显, +// 保存时后端也按名称反查 dept_id,写成树里没有的名字会两边都落空。 +const DEFAULT_ORGANIZATION_NAME = '广西桂物物流服务集团有限公司'; + export default { components: { ImageUploadField, @@ -985,7 +989,7 @@ export default { ); vehicleColumn.dicData = this.organizationOptions; if (this.driverBox && !this.driverForm.id && !this.driverForm.organizationName) { - this.driverForm.organizationName = this.getCurrentOrganizationName(); + this.driverForm.organizationName = this.getDefaultOrganizationName(); } }); }, @@ -1016,6 +1020,17 @@ export default { }); return result; }, + // 组织树受数据权限裁剪、不含预填节点时退回当前登录人组织, + // 否则级联框会预填一个树里没有的名称,显示为空 + getDefaultOrganizationName() { + const matched = this.organizationTreeFlatOptions.find( + item => item.rawLabel === DEFAULT_ORGANIZATION_NAME + ); + if (matched || !this.organizationTreeFlatOptions.length) { + return DEFAULT_ORGANIZATION_NAME; + } + return this.getCurrentOrganizationName(); + }, getCurrentOrganizationName() { const currentDeptId = this.userInfo.deptId || this.userInfo.dept_id; const currentDept = this.organizationOptions.find( @@ -1203,7 +1218,7 @@ export default { this.submitAttempted = false; if (!row?.id) { this.driverForm = emptyForm(); - this.driverForm.organizationName = this.getCurrentOrganizationName(); + this.driverForm.organizationName = this.getDefaultOrganizationName(); this.driverBox = true; return; } From d3e5ff73a5df013be1010d3d96029936ed5a58ae Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Thu, 5 Nov 2026 20:20:09 +0800 Subject: [PATCH 03/10] =?UTF-8?q?:sparkles:=20=E8=BF=90=E5=8A=9B=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8F=B8=E6=9C=BA=E3=80=81=E8=BD=A6=E8=BE=86=E3=80=81?= =?UTF-8?q?=E8=88=B9=E8=88=B6=E7=9A=84=E6=89=80=E5=B1=9E=E7=BB=84=E7=BB=87?= =?UTF-8?q?=E5=8F=AA=E5=85=81=E8=AE=B8=E9=80=89=E6=8B=A9=E5=B9=B3=E5=8F=B0?= =?UTF-8?q?=E5=85=AC=E5=8F=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mixins/organization-search.js | 110 ++++++++++++++++++++++-- src/views/transportCapacity/driver.vue | 48 ++++++----- src/views/transportCapacity/ship.vue | 34 ++++++-- src/views/transportCapacity/vehicle.vue | 59 ++++++++----- 4 files changed, 195 insertions(+), 56 deletions(-) diff --git a/src/mixins/organization-search.js b/src/mixins/organization-search.js index 7a69e9d..b9c6760 100644 --- a/src/mixins/organization-search.js +++ b/src/mixins/organization-search.js @@ -1,4 +1,4 @@ -import { getDeptTree } from '@/api/system/dept'; +import { getDeptTree, getPlatformCompanySelect } from '@/api/system/dept'; import { ElCascader } from 'element-plus'; import { h } from 'vue'; @@ -14,6 +14,10 @@ import { h } from 'vue'; * 3. 在 created / 初始化时调用 this.loadOrganizationOptions() * 4. 需要「未分配组织」筛选项的页面在 data 里置 enableUnassignedOrgFilter: true; * 该项只追加进搜索树 organizationSearchTreeOptions,表单级联 organizationTreeOptions 不含哨兵 + * 5. 表单「所属组织」只允许选平台公司的页面(运力管理)在 data 里置 + * restrictFormOrgToPlatformCompany: true,表单级联样式改用 + * buildFormOrganizationTree(...) 的结果,并在打开表单时把回显的组织名写进 + * formOrganizationLegacyName;只影响表单,搜索栏仍是全量组织 * * 说明: * - 自动识别 organizationName / deptName 两种 prop,并挂载 column.renderSearch @@ -34,6 +38,11 @@ const ORG_PROPS = ['organizationName', 'deptName']; // 后端换成空串会让整段组织条件不生效,筛选静默失效。前端与后端这几处必须同步修改。 const UNASSIGNED_ORG_FILTER = '__UNASSIGNED_ORG__'; +// 表单「所属组织」限定只能选平台公司时,存量记录里的旧组织名在组织树里已经找不到 +// (节点被删或改名),用这个 id 补一个只读节点,否则编辑老数据时所属组织显示为空、 +// 保存即把原值丢掉。 +const FORM_LEGACY_ORG_ID = '__LEGACY_ORG__'; + export default { data() { return { @@ -44,6 +53,14 @@ export default { organizationLoading: false, // 组织筛选是否追加「未分配组织」一项;页面在 data 里覆盖为 true 才生效 enableUnassignedOrgFilter: false, + // 表单「所属组织」是否只允许选平台公司;页面在 data 里覆盖为 true 才生效 + restrictFormOrgToPlatformCompany: false, + // 平台公司部门 id(字符串),loadOrganizationOptions 时一并拉取 + platformCompanyOrgIds: [], + // 表单当前回显的组织名,树里没有同名节点时用它补一个只读选项 + formOrganizationLegacyName: '', + // 进行中的组织数据请求,并发调用共用(不是响应式状态,只做去重) + organizationOptionsPromise: null, }; }, computed: { @@ -54,9 +71,12 @@ export default { ? [this.unassignedOrgOption(), ...this.organizationTreeOptions] : this.organizationTreeOptions; }, - // 所属组织级联配置:checkStrictly 允许选中任意层级,不必选到末级 + // 所属组织级联配置:checkStrictly 允许选中任意层级,不必选到末级。 + // 限定只能选平台公司时用 disabled 函数只放开平台公司:非平台公司的非末级节点 + // 在 checkStrictly 下仍可展开(el-cascader 的 expandable 判断),末级节点点击无效, + // 两者都选不中。disabled 在节点渲染时求值,平台公司 id 到手后节点会自行重算。 organizationCascaderProps() { - return { + const props = { value: 'id', label: 'label', children: 'children', @@ -64,15 +84,25 @@ export default { checkStrictly: true, expandTrigger: 'click', }; + if (this.restrictFormOrgToPlatformCompany) { + props.disabled = data => this.platformCompanyOrgDisabled(data); + } + return props; }, }, methods: { loadOrganizationOptions() { - if (this.organizationLoading) return; + // 并发调用共用同一次请求,返回的 Promise 在数据就绪后才 resolve + if (this.organizationOptionsPromise) return this.organizationOptionsPromise; this.organizationLoading = true; const tenantId = this.userInfo?.tenantId; - getDeptTree(tenantId) - .then(res => { + // 平台公司下拉失败不能拖垮组织树,单独兜住,失败时保持上一次的可选范围 + const platformCompanyRequest = this.restrictFormOrgToPlatformCompany + ? getPlatformCompanySelect().catch(() => null) + : Promise.resolve(null); + this.organizationOptionsPromise = Promise.all([getDeptTree(tenantId), platformCompanyRequest]) + .then(([res, platformRes]) => { + this.applyPlatformCompanyOptions(platformRes); const source = res.data?.data || res.data || []; const tree = this.excludeExternalOrganization ? this.excludeExternalOrganization(source) @@ -90,7 +120,21 @@ export default { }) .finally(() => { this.organizationLoading = false; + this.organizationOptionsPromise = null; }); + return this.organizationOptionsPromise; + }, + // 组织树与平台公司列表就绪后执行回调;已加载过则立即执行 + whenOrganizationOptionsReady(callback) { + const pending = this.organizationTreeFlatOptions.length + ? Promise.resolve() + : this.loadOrganizationOptions(); + return Promise.resolve(pending).then(callback, () => callback()); + }, + applyPlatformCompanyOptions(res) { + if (!res) return; + const list = res.data?.data || res.data || []; + this.platformCompanyOrgIds = (Array.isArray(list) ? list : []).map(item => String(item.id)); }, // 默认排除「外部组织」节点;页面可覆盖 excludeExternalOrganization(tree = []) { @@ -154,6 +198,52 @@ export default { findOrgName(id) { return this.findOrgById(id)?.rawLabel || ''; }, + // 级联节点的 disabled 判定:只有平台公司可选。页面自建 props 时也复用它 + platformCompanyOrgDisabled(data) { + return !this.platformCompanyOrgIds.includes(String(data?.id ?? '')); + }, + // 组织名对应的节点是否为平台公司;名称在树里不存在时按「不是」处理 + isPlatformCompanyOrgName(name) { + const value = String(name ?? '').trim(); + if (!value) return false; + const node = this.organizationTreeFlatOptions.find(item => item.rawLabel === value); + return !!node && this.platformCompanyOrgIds.includes(String(node.id)); + }, + // 新增表单的默认组织:只有落在平台公司上才预填,否则留空(不能让用户从默认值 + // 直接带出一个选不中的组织名) + defaultFormOrganizationName(name) { + return this.isPlatformCompanyOrgName(name) ? String(name).trim() : ''; + }, + // 表单级联的选项树:限定平台公司时,把回显的原组织名补成一个只读节点, + // 否则存量记录的组织在树里找不到,编辑时显示为空、保存即丢原值 + buildFormOrganizationTree(tree = this.organizationTreeOptions) { + const source = tree || []; + if (!this.restrictFormOrgToPlatformCompany) return source; + const name = String(this.formOrganizationLegacyName || '').trim(); + if (!name) return source; + if (this.organizationTreeFlatOptions.some(item => item.rawLabel === name)) return source; + return [...source, { id: FORM_LEGACY_ORG_ID, label: name, rawLabel: name }]; + }, + // 表单级联回显用的路径:树里有该组织就用真实路径,否则落到原组织兜底节点 + resolveFormOrgPath(name) { + const path = this.resolveOrgPath(name); + if (path.length) return path; + const value = String(name ?? '').trim(); + if (!value) return []; + return value === String(this.formOrganizationLegacyName || '').trim() + ? [FORM_LEGACY_ORG_ID] + : []; + }, + // 记录表单当前回显的组织名:能匹配真实节点(或已清空)就不需要兜底节点, + // 否则记下来,由 buildFormOrganizationTree 补成只读选项 + syncFormOrganizationLegacy(name) { + const value = String(name ?? '').trim(); + if (!value || this.organizationTreeFlatOptions.some(item => item.rawLabel === value)) { + this.formOrganizationLegacyName = ''; + return; + } + this.formOrganizationLegacyName = value; + }, // 组织名称 → 级联路径(表单回显用;部门树异步加载完成后会自动重算) resolveOrgPath(name) { const value = String(name ?? '').trim(); @@ -166,7 +256,13 @@ export default { const value = Array.isArray(path) ? path.filter(item => item !== undefined && item !== null && item !== '') : []; - return value.length ? this.findOrgById(value)?.rawLabel || '' : ''; + if (!value.length) return ''; + const node = this.findOrgById(value); + if (node) return node.rawLabel || ''; + // 原组织兜底节点不在扁平表里,直接取它代表的历史名称 + return String(value[value.length - 1]) === FORM_LEGACY_ORG_ID + ? String(this.formOrganizationLegacyName || '').trim() + : ''; }, // 搜索提交前把级联路径数组就地转成组织名称(后端按名称过滤) normalizeOrganizationSearch(params, prop = 'organizationName') { diff --git a/src/views/transportCapacity/driver.vue b/src/views/transportCapacity/driver.vue index c6c0ae2..c99b1bd 100644 --- a/src/views/transportCapacity/driver.vue +++ b/src/views/transportCapacity/driver.vue @@ -589,7 +589,7 @@ ({ status: 1, }); -// 新增司机时预填的组织。名称必须与组织树里的节点完全一致:级联框按名称回显, -// 保存时后端也按名称反查 dept_id,写成树里没有的名字会两边都落空。 -const DEFAULT_ORGANIZATION_NAME = '广西桂物物流服务集团有限公司'; - export default { components: { ImageUploadField, @@ -801,6 +797,8 @@ export default { driverBox: false, // 搜索栏组织级联追加「未分配组织」一项(哨兵值见 organization-search mixin) enableUnassignedOrgFilter: true, + // 表单「所属组织」只允许选平台公司;搜索栏不受影响 + restrictFormOrgToPlatformCompany: true, vehicleSelectorVisible: false, vehicleSelectorLoading: false, vehicleSelectorOption, @@ -925,13 +923,19 @@ export default { }, computed: { ...mapGetters(['permission', 'userInfo']), + // 表单所属组织级联的选项:限定平台公司时多一个原组织兜底节点 + formOrganizationTreeOptions() { + return this.buildFormOrganizationTree(); + }, // 所属组织级联:表单存组织名称,级联控件用 id 路径,两者在此双向转换 organizationCascaderValue: { get() { - return this.resolveOrgPath(this.driverForm.organizationName); + return this.resolveFormOrgPath(this.driverForm.organizationName); }, set(path) { - this.driverForm.organizationName = this.resolveOrgName(path); + const name = this.resolveOrgName(path); + this.driverForm.organizationName = name; + this.syncFormOrganizationLegacy(name); }, }, isAdmin() { @@ -969,8 +973,8 @@ export default { }, }, created() { + // initDeptTree 末尾会拉组织树(含平台公司 id),这里不再重复请求 this.initDeptTree(); - this.loadOrganizationOptions(); this.initRegionOptions(); this.initQualificationTypeOptions(); }, @@ -988,9 +992,7 @@ export default { 'organizationName' ); vehicleColumn.dicData = this.organizationOptions; - if (this.driverBox && !this.driverForm.id && !this.driverForm.organizationName) { - this.driverForm.organizationName = this.getDefaultOrganizationName(); - } + return this.loadOrganizationOptions(); }); }, initQualificationTypeOptions() { @@ -1020,16 +1022,10 @@ export default { }); return result; }, - // 组织树受数据权限裁剪、不含预填节点时退回当前登录人组织, - // 否则级联框会预填一个树里没有的名称,显示为空 + // 新增时的默认组织:当前登录人组织是平台公司才预填,否则留空, + // 不能让默认值带出一个在表单里选不中的组织名 getDefaultOrganizationName() { - const matched = this.organizationTreeFlatOptions.find( - item => item.rawLabel === DEFAULT_ORGANIZATION_NAME - ); - if (matched || !this.organizationTreeFlatOptions.length) { - return DEFAULT_ORGANIZATION_NAME; - } - return this.getCurrentOrganizationName(); + return this.defaultFormOrganizationName(this.getCurrentOrganizationName()); }, getCurrentOrganizationName() { const currentDeptId = this.userInfo.deptId || this.userInfo.dept_id; @@ -1218,8 +1214,16 @@ export default { this.submitAttempted = false; if (!row?.id) { this.driverForm = emptyForm(); - this.driverForm.organizationName = this.getDefaultOrganizationName(); + // 组织树与平台公司列表异步加载,默认值要等两者都到位才能判定,这里先留空 + this.driverForm.organizationName = ''; + this.formOrganizationLegacyName = ''; this.driverBox = true; + this.whenOrganizationOptionsReady(() => { + if (!this.driverBox || this.driverForm.id || this.driverForm.organizationName) return; + const name = this.getDefaultOrganizationName(); + this.driverForm.organizationName = name; + this.syncFormOrganizationLegacy(name); + }); return; } getDetail(row.id).then(res => { @@ -1233,11 +1237,13 @@ export default { postList: detail.posts ? detail.posts.split(',').filter(Boolean) : [], }; this.syncAddressRegionPath(); + this.syncFormOrganizationLegacy(this.driverForm.organizationName); this.driverBox = true; }); }, resetDriver() { this.driverForm = emptyForm(); + this.formOrganizationLegacyName = ''; this.drivingLicenseUploads = {}; this.readonly = false; this.submitAttempted = false; diff --git a/src/views/transportCapacity/ship.vue b/src/views/transportCapacity/ship.vue index 42895c8..c8acb8e 100644 --- a/src/views/transportCapacity/ship.vue +++ b/src/views/transportCapacity/ship.vue @@ -147,7 +147,7 @@ { + if (!this.shipBox || this.shipForm.id || this.shipForm.organizationName) return; + this.shipForm.organizationName = this.getDefaultOrganizationName(); + }); return; } getDetail(row.id).then(res => { this.shipForm = { ...emptyForm(), ...(res.data.data || {}) }; + this.syncFormOrganizationLegacy(this.shipForm.organizationName); this.shipBox = true; }); }, @@ -890,6 +907,7 @@ export default { }, resetShip() { this.shipForm = emptyForm(); + this.formOrganizationLegacyName = ''; this.readonly = false; this.submitLoading = false; this.$refs.shipForm?.clearValidate(); diff --git a/src/views/transportCapacity/vehicle.vue b/src/views/transportCapacity/vehicle.vue index c83075d..96da88e 100644 --- a/src/views/transportCapacity/vehicle.vue +++ b/src/views/transportCapacity/vehicle.vue @@ -347,7 +347,7 @@ this.platformCompanyOrgDisabled(data) } + : this.formOrgCascaderProps; + }, // 所属组织级联:本地保存 id 路径,名称同步到表单字段供后端使用 organizationCascaderValue: { get() { @@ -769,7 +777,9 @@ export default { }, set(path) { this.organizationCascaderPath = Array.isArray(path) ? [...path] : []; - this.vehicleForm.organizationName = this.resolveOrgName(this.organizationCascaderPath); + const name = this.resolveOrgName(this.organizationCascaderPath); + this.vehicleForm.organizationName = name; + this.syncFormOrganizationLegacy(name); }, }, // 使用部门级联:独立路径状态,避免与所属组织共用计算回写导致选不中 @@ -810,7 +820,6 @@ export default { }, created() { this.initDeptTree(); - this.loadOrganizationOptions(); }, watch: { organizationTreeOptions() { @@ -852,17 +861,15 @@ export default { })); }, syncOrgCascaderPaths() { - this.organizationCascaderPath = this.resolveOrgPath(this.vehicleForm.organizationName); + this.organizationCascaderPath = this.resolveFormOrgPath(this.vehicleForm.organizationName); this.useDepartmentCascaderPath = this.resolveOrgPath(this.vehicleForm.useDepartment); }, initDeptTree() { + this.loadOrganizationOptions(); getDeptTree(this.userInfo.tenantId).then(res => { this.organizationOptions = this.flattenDeptOptions(res.data.data || []); const column = this.findColumn(this.option.column, 'organizationName'); column.dicData = this.organizationOptions; - if (this.vehicleBox && !this.vehicleForm.id && !this.vehicleForm.organizationName) { - this.vehicleForm.organizationName = this.getCurrentOrganizationName(); - } if (this.vehicleBox && !this.vehicleForm.useDepartment) { this.vehicleForm.useDepartment = this.getCurrentOrganizationName(); } @@ -894,6 +901,10 @@ export default { ); return currentDept?.rawLabel || this.userInfo.deptName || this.userInfo.dept_name || ''; }, + // 新增时的默认组织:当前登录人组织是平台公司才预填,否则留空 + getDefaultOrganizationName() { + return this.defaultFormOrganizationName(this.getCurrentOrganizationName()); + }, digitsOnly(value = '') { return String(value).replace(/\D/g, ''); }, @@ -961,11 +972,18 @@ export default { this.readonly = readonly; if (!row?.id) { this.vehicleForm = emptyForm(); - this.vehicleForm.organizationName = this.getCurrentOrganizationName(); + // 默认组织要先有组织树和平台公司列表才能判定,先留空、数据到位再补 + this.vehicleForm.organizationName = ''; + this.formOrganizationLegacyName = ''; this.vehicleForm.useDepartment = this.getCurrentOrganizationName(); this.syncOrgCascaderPaths(); this.syncPlateNo(false); this.vehicleBox = true; + this.whenOrganizationOptionsReady(() => { + if (!this.vehicleBox || this.vehicleForm.id || this.vehicleForm.organizationName) return; + this.vehicleForm.organizationName = this.getDefaultOrganizationName(); + this.syncOrgCascaderPaths(); + }); return; } getDetail(row.id).then(res => { @@ -977,6 +995,7 @@ export default { if (!this.vehicleForm.useDepartment) { this.vehicleForm.useDepartment = this.getCurrentOrganizationName(); } + this.syncFormOrganizationLegacy(this.vehicleForm.organizationName); this.syncOrgCascaderPaths(); this.splitPlateNo(); this.syncPlateNo(false); @@ -989,6 +1008,7 @@ export default { }, resetVehicle() { this.vehicleForm = emptyForm(); + this.formOrganizationLegacyName = ''; this.organizationCascaderPath = []; this.useDepartmentCascaderPath = []; this.vehicleCertificateUploads = {}; @@ -1610,6 +1630,16 @@ export default { } } +// 列表组织列超长时单行省略 +.org-cell-text { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: bottom; +} + // 编辑弹窗:灰底 + section-card 白卡分组 .vehicle-form { :deep(.el-form-item__label) { @@ -1641,18 +1671,7 @@ export default { flex: 0 0 calc(4em + 24px) !important; width: calc(4em + 24px) !important; } -} -.org-cell-text { - display: inline-block; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - vertical-align: bottom; -} - -.vehicle-form { .org-cascader-tooltip { width: 100%; } From c18da721562219b63828a7ea9ac42b272811d54e Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Fri, 6 Nov 2026 07:30:04 +0800 Subject: [PATCH 04/10] =?UTF-8?q?:sparkles:=20=E8=BF=90=E5=8D=95=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E6=96=B0=E5=A2=9E=E4=B8=8E=E5=B7=B2=E6=9C=89=E8=BF=90?= =?UTF-8?q?=E5=8D=95=E9=87=8D=E5=A4=8D=E9=A1=B5=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/business/waybill-manage.js | 2 + .../components/waybill-import-dialog.vue | 83 ++++++++++++++----- 2 files changed, 64 insertions(+), 21 deletions(-) diff --git a/src/api/business/waybill-manage.js b/src/api/business/waybill-manage.js index e90982c..3778653 100644 --- a/src/api/business/waybill-manage.js +++ b/src/api/business/waybill-manage.js @@ -95,3 +95,5 @@ export const exportImportFailures = data => }); export const confirmImport = data => request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data, timeout: 120000 }); +export const checkImportDuplicates = data => + request({ url: `${baseUrl}/import-batch/duplicates`, method: 'post', data, timeout: 120000 }); diff --git a/src/views/business/components/waybill-import-dialog.vue b/src/views/business/components/waybill-import-dialog.vue index e5db21c..33339d6 100644 --- a/src/views/business/components/waybill-import-dialog.vue +++ b/src/views/business/components/waybill-import-dialog.vue @@ -484,20 +484,24 @@ 保存取消 rows.value.filter(row => row._duplicate)); -const previewRows = computed(() => - previewTab.value === 'duplicate' ? duplicateRows.value : rows.value -); +const historyDuplicateRows = computed(() => rows.value.filter(row => row._historyWaybillNo)); +const previewRows = computed(() => { + if (previewTab.value === 'duplicate') return duplicateRows.value; + if (previewTab.value === 'history') return historyDuplicateRows.value; + return rows.value; +}); const carrierContractsLoaded = ref(false), hasCarrierContracts = ref(null); const carrierTypes = ['承运商', '自运']; @@ -795,6 +800,16 @@ const detailColumns = [ { prop: 'remark', label: '备注', editor: 'textarea', minWidth: 200 }, { prop: 'waybillIdentifier', label: '同一运单标识号', editor: 'input', minWidth: 150 }, ]; +// 与已有运单重复页签的列与明细一致,只在运单批次号后多一列已有运单号。 +const previewColumns = computed(() => + previewTab.value === 'history' + ? [ + detailColumns[0], + { prop: '_historyWaybillNo', label: '已有运单号', minWidth: 220 }, + ...detailColumns.slice(1), + ] + : detailColumns +); const requiredDetailFields = [ { prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号' }, { prop: 'transportType', label: '运输方式' }, @@ -1187,6 +1202,7 @@ const loadBatchRows = async batchId => { batchNo: item.batchNo || form.batchNo, })); refreshDuplicateFlags(); + await refreshHistoryDuplicates(); rowSelection.value = []; previewTab.value = 'all'; }; @@ -1509,6 +1525,8 @@ const fileChange = async (file, list) => { if (isValid) { // 校验通过,填充表格数据 rows.value = parsedRows; + // 传 rows.value(响应式代理),否则后续写入不触发重新渲染 + await refreshHistoryDuplicates(rows.value); } else { // 校验失败,清空数据 rows.value = []; @@ -1526,16 +1544,12 @@ const IMPORT_BATCH_SIZE = 100; const importFieldText = value => String(value ?? '').trim(); -/** 同一运单批次号下,全部导入字段一致时标记为疑似重复。已在线确认的行不再标为重复。 */ +/** 同一运单批次号下,全部导入字段一致时标记为 Excel 内重复。 */ const refreshDuplicateFlags = (list = rows.value) => { const groups = new Map(); list.forEach(row => { const signature = detailColumns.map(column => importFieldText(row[column.prop])).join('\u0001'); const batchNo = importFieldText(row.batchNo || form.batchNo); - if (row._duplicateConfirmed && row._duplicateSignature !== signature) { - row._duplicateConfirmed = false; - } - row._duplicateSignature = signature; const groupKey = `${batchNo}\u0002${signature}`; const group = groups.get(groupKey) || []; group.push(row); @@ -1544,19 +1558,39 @@ const refreshDuplicateFlags = (list = rows.value) => { groups.forEach(group => { const duplicated = group.length > 1; group.forEach(row => { - row._duplicate = duplicated && !row._duplicateConfirmed; + row._duplicate = duplicated; }); }); }; -const confirmDuplicateRow = row => { - row._duplicateConfirmed = true; - refreshDuplicateFlags(); - ElMessage.success('已确认,该行将按正常数据导入'); +/** 逐行提交给后端与项目下已有运单比对,命中的行带上已有运单号。比对失败不影响导入。 */ +const refreshHistoryDuplicates = async (list = rows.value) => { + if (!list.length || !form.projectId) { + list.forEach(row => { + row._historyWaybillNo = ''; + }); + return; + } + let duplicates = []; + try { + const res = await api.checkImportDuplicates(buildImportPayload(form.status, list)); + const data = res?.data?.data ?? res?.data ?? res; + duplicates = Array.isArray(data) ? data : []; + } catch (error) { + console.error('与已有运单重复校验失败:', error); + duplicates = []; + } + const numbersByRow = new Map( + duplicates.map(item => [item.rowIndex, (item.waybillNos || []).filter(Boolean)]) + ); + list.forEach((row, index) => { + row._historyWaybillNo = (numbersByRow.get(index) || []).join('、'); + }); }; const stripInternalRowFields = row => { - const { _key, _duplicate, _duplicateKey, _duplicateConfirmed, _duplicateSignature, _editing, _editSnapshot, ...data } = row; + const { _key, _duplicate, _duplicateKey, _editing, _editSnapshot, _historyWaybillNo, ...data } = + row; return data; }; @@ -1771,6 +1805,7 @@ const saveRow = row => { row._editing = false; delete row._editSnapshot; refreshDuplicateFlags(); + refreshHistoryDuplicates(); }; const cancelRow = row => { if (row._editSnapshot) Object.assign(row, row._editSnapshot); @@ -1780,12 +1815,14 @@ const cancelRow = row => { const deleteRow = row => { rows.value = rows.value.filter(item => item !== row); refreshDuplicateFlags(); + refreshHistoryDuplicates(); }; const removeSelectedRows = () => { const keys = new Set(rowSelection.value.map(row => row._key)); rows.value = rows.value.filter(row => !keys.has(row._key)); rowSelection.value = []; refreshDuplicateFlags(); + refreshHistoryDuplicates(); }; const normalizeOptionalPlanId = value => { if (value === undefined || value === null || value === '') return null; @@ -1849,7 +1886,11 @@ const confirmImport = async () => { await formRef.value?.validate(); if (!validateImportRows()) return; const count = rows.value.filter(row => row._duplicate).length; - if (count) await ElMessageBox.confirm(`当前导入有${count}条疑似重复数据,是否确认导入?`, '提示'); + if (count) + await ElMessageBox.confirm(`当前导入有${count}条Excel内重复数据,是否确认导入?`, '提示'); + const historyCount = rows.value.filter(row => row._historyWaybillNo).length; + if (historyCount) + await ElMessageBox.confirm(`当前导入有${historyCount}条与已有运单重复,是否确认导入?`, '提示'); if (form.importType === 'settlement' && form.status === 'completed') await ElMessageBox.confirm('确认后将同时生成运单、应收应付明细、计结算单,是否继续?', '提示'); From f87c0eea27d1c2ae4a2819b845a4971bf72bc096 Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Fri, 6 Nov 2026 08:10:31 +0800 Subject: [PATCH 05/10] =?UTF-8?q?:sparkles:=20=E5=87=AD=E8=AF=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=85=B3=E8=81=94=E8=BF=90=E5=8D=95=E6=89=B9=E6=AC=A1?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E5=A2=9E=E5=8A=A0=E9=A1=B9=E7=9B=AE=E7=AD=9B?= =?UTF-8?q?=E9=80=89=E4=B8=8E=E9=A1=B9=E7=9B=AE=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 搜索表单新增项目下拉筛选,查询参数透传 projectId - 运单批次表格在运单批次号后新增项目列,同一批次跨项目时显示「多个项目」 --- src/views/business/voucher-manage.vue | 32 +++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/views/business/voucher-manage.vue b/src/views/business/voucher-manage.vue index 54fb8b2..fc3dac2 100644 --- a/src/views/business/voucher-manage.vue +++ b/src/views/business/voucher-manage.vue @@ -389,6 +389,18 @@ v-model="batchQuery.batchNo" clearable placeholder="请输入" /> { batchTarget.value = voucher?.id ? voucher : undefined; batchSelection.value = []; batchVisible.value = true; + if (!projectOptions.value.length) await loadProjects(); await loadBatches(); if (voucher?.waybillBatchNo) { const currentBatch = batchRows.value.find(item => item.batchNo === voucher.waybillBatchNo); @@ -1033,6 +1055,7 @@ const openBatchDialog = async row => { const loadBatches = async () => { const res = await api.getWaybillBatches(batchPage.current, batchPage.size, { batchNo: batchQuery.batchNo, + projectId: batchQuery.projectId, createUser: batchQuery.createUser, waybillCount: batchQuery.waybillCount, createTimeStart: batchCreateRange.value?.[0] ? `${batchCreateRange.value[0]} 00:00:00` : '', @@ -1054,7 +1077,12 @@ const handleWaybillCountInput = value => { } }; const resetBatchQuery = () => { - Object.assign(batchQuery, { batchNo: '', createUser: '', waybillCount: undefined }); + Object.assign(batchQuery, { + batchNo: '', + projectId: '', + createUser: '', + waybillCount: undefined, + }); batchCreateRange.value = []; batchPage.current = 1; loadBatches(); From 6b6c9ed6ed0202d43cfc2c09900b1f96eed2a3a0 Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Fri, 6 Nov 2026 08:31:15 +0800 Subject: [PATCH 06/10] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E5=85=B3?= =?UTF-8?q?=E8=81=94=E8=BF=90=E5=8D=95=E6=89=B9=E6=AC=A1=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E9=87=8D=E6=96=B0=E6=89=93=E5=BC=80=E6=97=B6=E6=AE=8B=E7=95=99?= =?UTF-8?q?=E4=B8=8A=E6=AC=A1=E7=AD=9B=E9=80=89=E6=9D=A1=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 打开弹窗时重置搜索条件与分页,并清空上一次的列表数据 - 重置按钮与打开时的重置共用 resetBatchSearch --- src/views/business/voucher-manage.vue | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/views/business/voucher-manage.vue b/src/views/business/voucher-manage.vue index fc3dac2..376915b 100644 --- a/src/views/business/voucher-manage.vue +++ b/src/views/business/voucher-manage.vue @@ -1044,6 +1044,10 @@ const openBatchDialog = async row => { batchDialogMode.value = voucher?.id ? 'change' : 'upload'; batchTarget.value = voucher?.id ? voucher : undefined; batchSelection.value = []; + // 弹窗内的搜索条件是模块级状态,不重置就会带着上一次的筛选条件打开。 + resetBatchSearch(); + batchRows.value = []; + batchPage.total = 0; batchVisible.value = true; if (!projectOptions.value.length) await loadProjects(); await loadBatches(); @@ -1076,7 +1080,7 @@ const handleWaybillCountInput = value => { batchQuery.waybillCount = filtered.replace(/^0+/, ''); } }; -const resetBatchQuery = () => { +const resetBatchSearch = () => { Object.assign(batchQuery, { batchNo: '', projectId: '', @@ -1085,6 +1089,9 @@ const resetBatchQuery = () => { }); batchCreateRange.value = []; batchPage.current = 1; +}; +const resetBatchQuery = () => { + resetBatchSearch(); loadBatches(); }; const updateWaybillBatch = async (target, selected) => { From efa53b2422e83445a0f0e626709d1c0d3af7e97b Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Fri, 6 Nov 2026 09:00:00 +0800 Subject: [PATCH 07/10] =?UTF-8?q?:sparkles:=20=E6=96=B0=E5=BB=BA=E8=BF=90?= =?UTF-8?q?=E8=BE=93=E8=AE=A1=E5=88=92=E9=BB=98=E8=AE=A4=E8=BF=90=E8=BE=93?= =?UTF-8?q?=E6=96=B9=E5=BC=8F=E4=B8=BA=E5=85=AC=E8=B7=AF=E8=BF=90=E8=BE=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/option/business/transport-plan.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/option/business/transport-plan.js b/src/option/business/transport-plan.js index b7d1200..39da9e6 100644 --- a/src/option/business/transport-plan.js +++ b/src/option/business/transport-plan.js @@ -208,6 +208,9 @@ export const config = { transportPlanImportTitle: '导入运输计划', transportPlanTemplateUrl: '/blade-transport/transport-plan/export-template', attachmentTitle: '附件', + defaultForm: { + transportType: 'road', + }, searchRangeMap: { planStartDateRange: ['planStartDateStart', 'planStartDateEnd'], createTimeRange: ['createTimeStart', 'createTimeEnd', '00:00:00', '23:59:59'], From 49d5c304ac97cda52a346de2b27d7d82ff612f33 Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Fri, 6 Nov 2026 14:57:30 +0800 Subject: [PATCH 08/10] =?UTF-8?q?:sparkles:=20=E6=9B=B4=E6=96=B0=E8=A7=84?= =?UTF-8?q?=E6=A0=BC=E5=92=8C=E5=9E=8B=E5=8F=B7=E8=BE=93=E5=85=A5=E6=A1=86?= =?UTF-8?q?=E7=9A=84=E5=8D=A0=E4=BD=8D=E7=AC=A6=E6=96=87=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/business/components/waybill-manage-page.vue | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/views/business/components/waybill-manage-page.vue b/src/views/business/components/waybill-manage-page.vue index d1958d3..8f1416c 100644 --- a/src/views/business/components/waybill-manage-page.vue +++ b/src/views/business/components/waybill-manage-page.vue @@ -1337,7 +1337,7 @@ @@ -1345,7 +1345,7 @@ @@ -8453,7 +8453,6 @@ export default { } if (!this.isTaskFullMode) { requiredFields.push(['cargoName', '货物名称']); - requiredFields.push(['model', '型号']); requiredFields.push(['quantity', '数量']); } const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(row[prop])); From 85dc96e126285357b17757f37b3b07cb8afe0d3e Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Sat, 7 Nov 2026 04:29:43 +0800 Subject: [PATCH 09/10] =?UTF-8?q?:bug:=20=E8=BF=90=E8=BE=93=E8=AE=A1?= =?UTF-8?q?=E5=88=92=E8=B0=83=E5=BA=A6=E7=A1=AE=E8=AE=A4=E7=94=9F=E6=88=90?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E5=BC=BA=E5=88=B6=E5=8F=B8=E6=9C=BA=E4=B8=8E?= =?UTF-8?q?=E6=89=8B=E6=9C=BA=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 司机与手机号对全部承运类型(承运商/自运/平台)均为选填,仅校验已填内容的格式与长度。 --- .../components/transport-plan-page.vue | 21 ++++--------------- .../business/transport-plan-dispatch.vue | 2 +- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/views/business/components/transport-plan-page.vue b/src/views/business/components/transport-plan-page.vue index 374c9e4..799ec3a 100644 --- a/src/views/business/components/transport-plan-page.vue +++ b/src/views/business/components/transport-plan-page.vue @@ -1861,7 +1861,7 @@ /> - + @@ -2104,7 +2103,7 @@ /> - + - + this.isBillingFieldEmpty(row[prop])); if (emptyField) return requiredMessage(emptyField[1]); diff --git a/src/views/business/transport-plan-dispatch.vue b/src/views/business/transport-plan-dispatch.vue index 6acc086..d1738f2 100644 --- a/src/views/business/transport-plan-dispatch.vue +++ b/src/views/business/transport-plan-dispatch.vue @@ -577,7 +577,7 @@ export default { this.$message.warning(`第${index + 1}条调度明细缺少承运商信息`); return; } - // 司机是否必填由调度明细弹窗按承运类型判定(承运商承运时司机选填),此处不再重复校验 + // 司机与手机号对全部承运类型都是选填,由调度明细弹窗校验已填部分的格式,此处不重复校验 if (!row.vehicleNo) { this.$message.warning(`第${index + 1}条调度明细缺少车牌号`); return; From 09f77467b17a35ef32b1bfaf07d64f5c893231ba Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Mon, 9 Nov 2026 20:33:53 +0800 Subject: [PATCH 10/10] =?UTF-8?q?:zap:=20=E8=BF=90=E5=8D=95=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E8=A1=8C=E6=93=8D=E4=BD=9C=E3=80=8C=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E3=80=8D=E5=90=88=E5=B9=B6=E4=B8=BA=E5=8D=95=E4=B8=80=E5=85=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 待回单确认仍走确认回单(校验打卡节点与未了结异常),进行中/处理中二次确认后置完成。 --- .../components/waybill-manage-page.vue | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/views/business/components/waybill-manage-page.vue b/src/views/business/components/waybill-manage-page.vue index 8f1416c..1001517 100644 --- a/src/views/business/components/waybill-manage-page.vue +++ b/src/views/business/components/waybill-manage-page.vue @@ -1599,14 +1599,7 @@ 取消 - - {{ completeActionLabel }} - - + {{ completeActionLabel }} @@ -5178,12 +5171,9 @@ export default { return false; } const status = this.statusValue(row); - // 待回单确认的「完成」由确认回单入口承担(走打卡节点与未了结异常校验); - // 只有确认回单不可用时才退回这里的普通完成 - if (status === 'pending_return_confirm') { - return !this.canConfirmReturn(row); - } - return status === 'processing' || status === 'running'; + // 同一枚「完成」按钮覆盖两种状态:待回单确认走确认回单(后端校验司机打卡节点齐全且无未了结异常), + // 进行中/处理中二次确认后直接置完成 + return status === 'processing' || status === 'running' || status === 'pending_return_confirm'; }, canConfirmReturn(row) { return ( @@ -10252,6 +10242,14 @@ export default { this.mileageForm = { id: '', mileage: '', mileageRemark: '' }; this.$refs.mileageFormRef?.clearValidate(); }, + // 行操作「完成」的唯一入口:待回单确认的运单走确认回单(后端校验司机打卡节点齐全且无未了结异常, + // 配置里没有确认回单动作时退回普通完成);进行中/处理中的运单二次确认后直接置完成。 + handleComplete(row) { + if (this.canConfirmReturn(row)) { + return this.handleAction('confirmReturn', row); + } + return this.handleAction('complete', row); + }, handleAction(action, row) { if (action === 'reassign') { this.openReassignDrawer(row); @@ -10265,11 +10263,13 @@ export default { reassign: '重新派单', confirmReturn: '完成', }[action]; - // 回单确认是终态动作(本轮不做撤销),把后果写在提示里,别让人以为还能退回 - const confirmText = - action === 'confirmReturn' - ? '回单确认后该运单将置为已完成,且不可撤销。是否继续?' - : `确定${actionName}该${this.config.title}?`; + // 回单确认与「完成」都是终态动作(本轮不做撤销),把后果写在提示里,别让人以为还能退回 + let confirmText = `确定${actionName}该${this.config.title}?`; + if (action === 'confirmReturn') { + confirmText = '回单确认后该运单将置为已完成,且不可撤销。是否继续?'; + } else if (action === 'complete') { + confirmText = `确定完成该${this.config.title}?完成后将置为已完成,且不可撤销。`; + } this.$confirm(confirmText, '提示', { confirmButtonText: '确定', cancelButtonText: '取消',