From a657b9d0ef8cb6f1d5ab87fb7e28b6781ee06dfb Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Thu, 22 Oct 2026 10:59:22 +0800 Subject: [PATCH 1/4] =?UTF-8?q?:sparkles:=20=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86=E6=94=AF=E6=8C=81=E6=89=80?= =?UTF-8?q?=E5=B1=9E=E7=BB=84=E7=BB=87=E4=B8=8E=E6=95=B0=E6=8D=AE=E6=9D=83?= =?UTF-8?q?=E9=99=90=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/option/system/mpUser.js | 61 ++++++++++ src/views/system/mp-user.vue | 224 +++++++++++++++++++++++++++++------ 2 files changed, 246 insertions(+), 39 deletions(-) diff --git a/src/option/system/mpUser.js b/src/option/system/mpUser.js index 994f76f..bc06d41 100644 --- a/src/option/system/mpUser.js +++ b/src/option/system/mpUser.js @@ -14,6 +14,11 @@ const validatePhone = (rule, value, callback) => { callback(new Error('请输入手机号')); return; } + // 详情回显为脱敏手机号(含 *)时放行,未修改则提交时剔除该字段 + if (phone.includes('*')) { + callback(); + return; + } if (!/^1\d{10}$/.test(phone)) { callback(new Error('请输入正确的手机号')); return; @@ -121,6 +126,62 @@ export default { order: 60, rules: [{ required: true, message: '请选择小程序身份', trigger: 'change' }], }, + { + label: '所属组织', + prop: 'deptId', + formslot: true, + hide: true, + span: 24, + order: 55, + rules: [{ required: true, message: '请选择所属组织', trigger: 'change' }], + }, + { + label: '所属组织', + prop: 'deptName', + minWidth: 180, + overHidden: true, + addDisplay: false, + editDisplay: false, + viewDisplay: false, + order: 54, + }, + { + label: '数据权限范围', + prop: 'dataScopeRange', + type: 'radio', + span: 24, + hide: true, + order: 53, + value: 1, + dicData: [ + { label: '仅所属组织', value: 1 }, + { label: '全部组织', value: 2 }, + { label: '自定义', value: 3 }, + ], + rules: [{ required: true, message: '请选择数据权限范围', trigger: 'change' }], + }, + { + label: '数据层级范围', + prop: 'dataLevelRange', + type: 'radio', + span: 24, + hide: true, + order: 52, + value: 1, + dicData: [ + { label: '包含下级', value: 1 }, + { label: '仅本级', value: 2 }, + ], + rules: [{ required: true, message: '请选择数据层级范围', trigger: 'change' }], + }, + { + label: '数据权限', + prop: 'dataScopeDeptIds', + formslot: true, + span: 24, + hide: true, + order: 51, + }, { label: '性别', prop: 'sex', diff --git a/src/views/system/mp-user.vue b/src/views/system/mp-user.vue index a67fd36..072f3d8 100644 --- a/src/views/system/mp-user.vue +++ b/src/views/system/mp-user.vue @@ -20,33 +20,32 @@ @on-load="onLoad" > + - @@ -2107,6 +2152,8 @@ export default { routeNodes: [], cargoRows: [], routeDragIndex: -1, + /** 进行中编辑:子运单打卡记录映射 waybillId -> records[] */ + waybillPunchMap: {}, transportTypeOptions: [], projectOptions: [], customerOptions: [], @@ -2170,6 +2217,23 @@ export default { dialogReadonly() { return this.dialogMode === 'view'; }, + /** 进行中配载编辑:可加子单/调未打卡路线,但任务必填字段锁定 */ + isRunningEdit() { + return ( + this.dialogMode === 'edit' && + String(this.dialogForm.businessStatus || '') === 'running' + ); + }, + /** 进行中任务必填字段(已派车)不可改 */ + isRunningTaskRequiredLocked() { + return this.isRunningEdit; + }, + /** 自运进行中:司机/手机号同属必填派车信息,不可改 */ + isRunningSelfDriverLocked() { + return ( + this.isRunningEdit && String(this.dialogForm.carrierType || '').trim() === '自运' + ); + }, // 打卡时间轴:已打卡按时间升序,未打卡保持原节点顺序排在其后 orderedPunchRecords() { const records = Array.isArray(this.punchRecords) ? this.punchRecords : []; @@ -2913,6 +2977,7 @@ export default { if (status === 'running') { return [ { type: 'view', label: '详情' }, + { type: 'edit', label: '编辑' }, { type: 'complete', label: '完成' }, ]; } @@ -3014,7 +3079,12 @@ export default { query: { mode, ...(mode === 'edit' && row?.id ? { id: row.id } : {}), - name: mode === 'edit' ? '编辑配载管理' : '新增配载管理', + name: + mode === 'edit' + ? row?.businessStatus === 'running' + ? '编辑进行中配载' + : '编辑配载管理' + : '新增配载管理', }, }); return; @@ -3037,6 +3107,9 @@ export default { this.dialogForm.loadingSubNos ); this.syncProcessWaybillOptions(); + if (this.isRunningEdit) { + await this.refreshWaybillPunchMap(); + } this.restoreRouteAndCargo(); this.restoreRouteChangeRecords(); this.loadPunchRecords(); @@ -3071,6 +3144,8 @@ export default { this.taskCarrierLoading = false; this.routeNodes = []; this.cargoRows = []; + this.routeDragIndex = -1; + this.waybillPunchMap = {}; this.candidatePage.currentPage = 1; this.candidatePage.total = 0; this.candidateSearchVisible = false; @@ -3435,20 +3510,144 @@ export default { const statusName = String(row.businessStatusName || row.statusName || ''); return statusName.includes('已完成'); }, + isPunchRecordDone(record = {}) { + if (record.punched === false || record.statusName === '未打卡') return false; + return Boolean(record.punched || record.punchTime || record.punchAt || record.punchDateTime); + }, + isLoadPunchRecord(record = {}) { + if (!this.isPunchRecordDone(record)) return false; + const code = String(record.nodeCode || '') + .trim() + .toLowerCase(); + const name = String(record.nodeName || '').trim(); + return code === 'load' || name.includes('装货'); + }, + isUnloadPunchRecord(record = {}) { + if (!this.isPunchRecordDone(record)) return false; + const code = String(record.nodeCode || '') + .trim() + .toLowerCase(); + const name = String(record.nodeName || '').trim(); + return ( + code === 'unload' || + name.includes('卸货') || + code === 'arrival' || + name.includes('到货') || + code === 'sign' || + name.includes('签收') + ); + }, + waybillPunchRecords(row = {}) { + const key = String(row?.id || ''); + if (!key) return []; + const records = this.waybillPunchMap[key]; + return Array.isArray(records) ? records : []; + }, + isWaybillLoadPunched(row = {}) { + return this.waybillPunchRecords(row).some(record => this.isLoadPunchRecord(record)); + }, + isWaybillUnloadPunched(row = {}) { + return this.waybillPunchRecords(row).some(record => this.isUnloadPunchRecord(record)); + }, + isWaybillPunchLocked(row = {}) { + return this.isWaybillLoadPunched(row) || this.isWaybillUnloadPunched(row); + }, + isSelectedWaybillRemoveLocked(row = {}) { + if (this.isWaybillCompleted(row)) return true; + return this.isRunningEdit && this.isWaybillPunchLocked(row); + }, + isRouteNodeTagLocked(tag, rows = this.selectedWaybillRows) { + const match = String(tag || '').match(/^(装|卸)(\d+)$/); + if (!match) return false; + const row = rows[Number(match[2]) - 1]; + if (!row) return false; + if (this.isRunningEdit) { + return match[1] === '装' ? this.isWaybillLoadPunched(row) : this.isWaybillUnloadPunched(row); + } + return this.isWaybillCompleted(row); + }, isRouteChangeNodeLocked(node = {}) { return Boolean(node?.locked); }, syncRouteNodeLockedFlags(nodes = [], rows = this.selectedWaybillRows) { return (nodes || []).map(node => { - const locked = (node.tags || []).some(tag => { - const match = String(tag).match(/^(装|卸)(\d+)$/); - if (!match) return false; - const row = rows[Number(match[2]) - 1]; - return row ? this.isWaybillCompleted(row) : false; - }); + const locked = (node.tags || []).some(tag => this.isRouteNodeTagLocked(tag, rows)); return { ...node, locked }; }); }, + async refreshWaybillPunchMap() { + const rows = (this.selectedWaybillRows || []).filter(row => row?.id); + if (!rows.length) { + this.waybillPunchMap = {}; + return; + } + const entries = await Promise.all( + rows.map(async row => { + try { + const res = await getWaybillPunchRecords(row.id); + const data = res?.data?.data || res?.data || {}; + return [String(row.id), Array.isArray(data.records) ? data.records : []]; + } catch (error) { + return [String(row.id), []]; + } + }) + ); + this.waybillPunchMap = Object.fromEntries(entries); + }, + /** + * 进行中编辑:已打卡地点保持在前且不可插入,新增地址只能排在已打卡节点之后。 + */ + mergeRouteNodesKeepingPunched(prevNodes = [], builtNodes = []) { + const prev = Array.isArray(prevNodes) ? prevNodes : []; + const built = Array.isArray(builtNodes) ? builtNodes : []; + if (!this.isRunningEdit) { + return built; + } + const builtByAddress = new Map(); + built.forEach(node => { + if (node?.address && !builtByAddress.has(node.address)) { + builtByAddress.set(node.address, node); + } + }); + const lockedAddresses = new Set( + built.filter(node => node?.locked && node.address).map(node => node.address) + ); + const used = new Set(); + const lockedResult = []; + prev.forEach(node => { + if (!node?.address || !lockedAddresses.has(node.address) || used.has(node.address)) { + return; + } + const latest = builtByAddress.get(node.address); + if (!latest) return; + lockedResult.push({ ...latest, locked: true }); + used.add(node.address); + }); + built.forEach(node => { + if (!node?.locked || !node.address || used.has(node.address)) return; + lockedResult.push({ ...node, locked: true }); + used.add(node.address); + }); + const unlockedResult = []; + prev.forEach(node => { + if (!node?.address || lockedAddresses.has(node.address) || used.has(node.address)) { + return; + } + const latest = builtByAddress.get(node.address); + if (!latest) return; + unlockedResult.push({ ...latest, locked: false }); + used.add(node.address); + }); + built.forEach(node => { + if (node?.locked || !node?.address || used.has(node.address)) return; + unlockedResult.push({ ...node, locked: false }); + used.add(node.address); + }); + return [...lockedResult, ...unlockedResult].map((node, index) => ({ + ...node, + key: `${node.address}-${index}`, + })); + }, openRouteChangeMapDialog(row, field) { if (this.isWaybillCompleted(row)) return; this.routeChangeMapTarget = { row, field }; @@ -3766,6 +3965,33 @@ export default { } }) ); + if (this.isRunningEdit && String(this.dialogForm.carrierType || '').trim() === '承运商') { + const contractId = this.dialogForm.carrierContractId; + if (contractId) { + const projectIds = [ + ...new Set( + [...this.selectedWaybillRows, ...selectedRows] + .map(row => row.projectId) + .filter(Boolean) + ), + ]; + if (projectIds.length) { + try { + const response = await loadingApi.getCarrierContracts(projectIds); + const contracts = response?.data?.data || response?.data || []; + const stillAllowed = (Array.isArray(contracts) ? contracts : []).some( + item => String(item.id) === String(contractId) + ); + if (!stillAllowed) { + ElMessage.warning('新增子运单须兼容当前配载单已选定的承运商合同'); + return; + } + } catch (error) { + // 合同校验失败时仍允许添加,最终由后端确认拦截 + } + } + } + } const exists = new Set( this.selectedWaybillRows.map(row => String(row.id || row.waybillNo)) ); @@ -3777,8 +4003,12 @@ export default { exists.add(key); } }); + const prevRouteNodes = [...this.routeNodes]; this.selectedWaybillRows = nextRows; - this.rebuildSummaryFromWaybills(); + if (this.isRunningEdit) { + await this.refreshWaybillPunchMap(); + } + this.rebuildSummaryFromWaybills(true, prevRouteNodes); await this.loadTaskCarrierOptions(); this.$refs.candidateTableRef?.clearSelection?.(); this.candidateSelection = []; @@ -3790,11 +4020,23 @@ export default { await this.addWaybillRows(this.candidateSelection); }, removeSelectedWaybill(index) { + const row = this.selectedWaybillRows[index]; + if (this.isSelectedWaybillRemoveLocked(row)) { + ElMessage.warning('已打卡的子运单不可移除'); + return; + } this.selectedWaybillRows.splice(index, 1); + if (this.isRunningEdit) { + this.refreshWaybillPunchMap().then(() => { + this.rebuildSummaryFromWaybills(); + this.loadTaskCarrierOptions(); + }); + return; + } this.rebuildSummaryFromWaybills(); this.loadTaskCarrierOptions(); }, - rebuildSummaryFromWaybills(rebuildRoute = true) { + rebuildSummaryFromWaybills(rebuildRoute = true, prevRouteNodes = null) { const rows = this.selectedWaybillRows; this.dialogForm.loadingSubNos = rows .map(row => row.waybillNo) @@ -3819,7 +4061,13 @@ export default { this.dialogForm.transportType = rows.find(row => row.transportType)?.transportType || ''; } if (rebuildRoute) { - this.routeNodes = this.buildRouteNodes(rows); + const builtNodes = this.buildRouteNodes(rows); + this.routeNodes = this.isRunningEdit + ? this.mergeRouteNodesKeepingPunched( + prevRouteNodes || this.routeNodes, + builtNodes + ) + : builtNodes; } this.cargoRows = this.buildCargoRows(rows); // 编辑回填阶段 routeNodes 尚未恢复时跳过,避免清空已有发到货地 @@ -3839,7 +4087,9 @@ export default { if (locked) node.locked = true; }; rows.forEach((row, rowIndex) => { - const locked = this.isWaybillCompleted(row); + const locked = this.isRunningEdit + ? this.isWaybillLoadPunched(row) + : this.isWaybillCompleted(row); const departure = row.departureAddress || row.departureName; appendNode( departure, @@ -3849,7 +4099,9 @@ export default { ); }); rows.forEach((row, rowIndex) => { - const locked = this.isWaybillCompleted(row); + const locked = this.isRunningEdit + ? this.isWaybillUnloadPunched(row) + : this.isWaybillCompleted(row); const arrival = row.arrivalAddress || row.arrivalName; appendNode( arrival, @@ -3901,7 +4153,9 @@ export default { if ( originalLockedKeys.some((key, lockedIndex) => key && key !== nextLockedKeys[lockedIndex]) ) { - ElMessage.warning('已完成运单对应的路线不可调整'); + ElMessage.warning( + this.isRunningEdit ? '已打卡地点不可调整,新地址只能在未打卡节点间拖拽' : '已完成运单对应的路线不可调整' + ); this.routeDragIndex = -1; return; } @@ -4145,15 +4399,15 @@ export default { try { const payload = { ...this.normalizePayload(), - // 确认生成正式配载单(待执行),覆盖草稿状态 - businessStatus: 'pending', + // 进行中编辑保持 running;其余确认生成正式配载单为待执行 + businessStatus: this.isRunningEdit ? 'running' : 'pending', }; if (this.dialogMode === 'reassign') { await loadingApi.reassign(payload); ElMessage.success('派单成功'); } else { await loadingApi.submit(payload); - ElMessage.success('确认成功'); + ElMessage.success(this.isRunningEdit ? '保存成功' : '确认成功'); } sessionStorage.setItem(loadingManageListRefreshKey, '1'); this.closeLoadingDialog(); @@ -4787,6 +5041,16 @@ export default { color: #909399; } + .loading-manage-dialog__route-hint { + color: #909399; + font-size: 12px; + } + + .loading-manage-dialog__locked-tip { + color: #909399; + font-size: 12px; + } + .loading-manage-dialog__route-type { width: 28px; height: 28px; diff --git a/src/views/business/project-apply.vue b/src/views/business/project-apply.vue index e4b41bb..75dcdfe 100644 --- a/src/views/business/project-apply.vue +++ b/src/views/business/project-apply.vue @@ -921,7 +921,7 @@ import { getList as getCustomerArchiveList, getDetail as getCustomerArchiveDetail, } from '@/api/vehicle/customer-archive'; -import { getDeptTree, getPlatformCompanySelect } from '@/api/system/dept'; +import { getBusinessDeptSelect, getDeptTree, getPlatformCompanySelect } from '@/api/system/dept'; import { getDictionary as getSystemDictionary } from '@/api/system/dict'; import { getDictionary as getBizDictionary } from '@/api/system/dictbiz'; import { getList as getUserList } from '@/api/system/user'; @@ -1559,6 +1559,7 @@ export default { projectType: type === 'majorSupplement' ? '重大项目' : '普通项目', }); this.fillDefaultUsers(); + this.fillDefaultBusinessDept(); return; } if (!id) return; @@ -1891,6 +1892,7 @@ export default { .filter(Boolean); this.syncSelectedCustomerOptions(); this.ensureCurrentPlatformOption(); + this.ensureCurrentBusinessDeptOption(); }, fillDefaultUsers() { const userId = this.userInfo?.userId || this.userInfo?.id || ''; @@ -1909,6 +1911,25 @@ export default { this.form.principalUserName = userName; } }, + /** + * 新增/重大补录:业务部门默认取当前用户所属部门中第一个「是否业务部门=是」的组织。 + */ + fillDefaultBusinessDept() { + if (!['add', 'majorSupplement'].includes(this.dialogType)) return; + if (this.form.businessDeptId) return; + const raw = this.userInfo?.deptId || this.userInfo?.dept_id || ''; + const userDeptIds = String(raw) + .split(/[,,]/) + .map(item => String(item || '').trim()) + .filter(item => item && item !== '-1'); + if (!userDeptIds.length || !this.deptOptions.length) return; + const matched = userDeptIds.find(id => + this.deptOptions.some(item => String(item.value) === String(id)) + ); + if (!matched) return; + this.form.businessDeptId = matched; + this.handleBusinessDeptChange(matched); + }, resolveUserRealName(row = {}, role) { return ( row[`${role}RealName`] || @@ -2491,17 +2512,61 @@ export default { this.$refs.projectForm?.validateField('projectType'); }, loadDeptOptions() { - getDeptTree().then(res => { - const deptTree = res.data.data || []; - // 新增/编辑/重大补录/项目变更均可选择业务部门,排除外部组织 - const businessDeptTree = ['add', 'edit', 'majorSupplement', 'change'].includes(this.dialogType) - ? this.excludeExternalOrganization(deptTree) - : deptTree; - this.deptOptions = this.flattenDept(businessDeptTree); - this.businessDeptTreeOptions = this.toDeptCascaderOptions(businessDeptTree); - }); + Promise.all([getDeptTree(), getBusinessDeptSelect()]) + .then(([treeRes, bizRes]) => { + const deptTree = this.excludeExternalOrganization(treeRes.data.data || []); + const bizList = bizRes.data.data || []; + const bizIds = new Set( + bizList + .map(item => (item.id == null || item.id === '' ? '' : String(item.id))) + .filter(Boolean) + ); + // 仅保留「是否业务部门=是」的节点及其祖先,祖先不可选 + const businessDeptTree = this.filterBusinessDeptTree(deptTree, bizIds); + this.deptOptions = this.flattenDept(businessDeptTree).filter(item => + bizIds.has(String(item.value)) + ); + this.businessDeptTreeOptions = this.toDeptCascaderOptions(businessDeptTree, bizIds); + this.fillDefaultBusinessDept(); + this.ensureCurrentBusinessDeptOption(); + }) + .catch(() => { + this.deptOptions = []; + this.businessDeptTreeOptions = []; + }); this.loadPlatformCompanyOptions(); }, + /** + * 剪枝组织树:只保留业务部门节点及其祖先路径。 + */ + filterBusinessDeptTree(list = [], bizIds = new Set()) { + return (list || []).reduce((result, item) => { + const children = item.children?.length + ? this.filterBusinessDeptTree(item.children, bizIds) + : []; + const isBiz = bizIds.has(String(item.id)); + if (!isBiz && !children.length) { + return result; + } + result.push({ + ...item, + children: children.length ? children : undefined, + }); + return result; + }, []); + }, + ensureCurrentBusinessDeptOption() { + const id = this.form.businessDeptId; + if (!id) return; + const exists = this.deptOptions.some(item => String(item.value) === String(id)); + if (exists) return; + const label = this.form.businessDeptName || String(id); + this.deptOptions.unshift({ label, rawLabel: label, value: id }); + const pathExists = this.findDeptPath(id, this.businessDeptTreeOptions); + if (!pathExists) { + this.businessDeptTreeOptions.unshift({ label, value: id }); + } + }, loadPlatformCompanyOptions() { getPlatformCompanySelect().then(res => { const list = res.data.data || []; @@ -2545,12 +2610,22 @@ export default { return result; }, []); }, - toDeptCascaderOptions(list = []) { - return list.map(item => ({ - label: item.title || item.deptName || item.name, - value: item.id, - children: item.children?.length ? this.toDeptCascaderOptions(item.children) : undefined, - })); + toDeptCascaderOptions(list = [], bizIds = null) { + return list.map(item => { + const value = item.id == null || item.id === '' ? item.value : item.id; + const option = { + label: item.title || item.deptName || item.name || item.label, + value: value == null || value === '' ? '' : String(value), + children: item.children?.length + ? this.toDeptCascaderOptions(item.children, bizIds) + : undefined, + }; + // 仅业务部门可选;祖先节点保留路径但禁用 + if (bizIds && !bizIds.has(String(option.value))) { + option.disabled = true; + } + return option; + }); }, flattenDept(list = [], level = 0) { return list.reduce((result, item) => { diff --git a/src/views/payment/bill-payment-form.vue b/src/views/payment/bill-payment-form.vue index 77297ed..d848ffd 100644 --- a/src/views/payment/bill-payment-form.vue +++ b/src/views/payment/bill-payment-form.vue @@ -271,7 +271,7 @@ export default { this.unwrapData( await billLedgerApi.getAvailableOptions( this.billDialog.keyword, - this.form.deptId, + undefined, this.form.billLedgerId ) ) || []; diff --git a/src/views/payment/payment-application-form.vue b/src/views/payment/payment-application-form.vue index eb13204..2880a43 100644 --- a/src/views/payment/payment-application-form.vue +++ b/src/views/payment/payment-application-form.vue @@ -1229,14 +1229,6 @@ export default { callback(); return; } - if ( - selected.billType && - selected.billType !== 'received' && - !this.billMatchesSettlementParties(selected) - ) { - callback(new Error('汇票付款仅可选择收票类型,或出票单位与收票单位均为当前结算收付款方的汇票')); - return; - } if (selected.maturityDate) { const maturity = this.$dayjs(selected.maturityDate).startOf('day'); const today = this.$dayjs().startOf('day'); @@ -2444,8 +2436,7 @@ export default { } }, payeeReceiptLabel(item) { - const name = item.accountName || item.accountHolderName || '未命名收款方'; - return item.bankAccount ? `${name}(${item.bankAccount})` : name; + return item.accountName || item.accountHolderName || '未命名收款方'; }, receiptAccountLabel(item) { return [item.accountName || item.accountHolderName, item.bankName, item.bankAccount] @@ -2603,7 +2594,7 @@ export default { this.unwrapData( await billLedgerApi.getAvailableOptions( keyword, - this.form.deptId, + undefined, selectedId, this.settlementPayerName, this.settlementPayeeName @@ -2628,7 +2619,7 @@ export default { this.billPage.current, this.billPage.size, String(this.billQuery.keyword || '').trim() || undefined, - this.form.deptId, + undefined, this.form.billLedgerId, this.settlementPayerName, this.settlementPayeeName @@ -2667,14 +2658,6 @@ export default { ); }, selectBill(row) { - if ( - row?.billType && - row.billType !== 'received' && - !this.billMatchesSettlementParties(row) - ) { - ElMessage.warning('汇票付款仅可选择收票类型,或出票单位与收票单位均为当前结算收付款方的汇票'); - return; - } if (row?.maturityDate) { const maturity = this.$dayjs(row.maturityDate).startOf('day'); const today = this.$dayjs().startOf('day'); diff --git a/src/views/system/dept.vue b/src/views/system/dept.vue index a22b6f3..0b4033c 100644 --- a/src/views/system/dept.vue +++ b/src/views/system/dept.vue @@ -535,6 +535,27 @@ export default { }, ], }, + { + label: '是否业务部门', + prop: 'isBusinessDept', + type: 'select', + dicData: [ + { label: '否', value: 0 }, + { label: '是', value: 1 }, + ], + dataType: 'number', + value: 0, + width: 120, + order: 34, + formatter: row => (Number(row.isBusinessDept) === 1 ? '是' : '否'), + rules: [ + { + required: true, + message: '请选择是否业务部门', + trigger: 'change', + }, + ], + }, { label: '排序', prop: 'sort', @@ -1523,6 +1544,9 @@ export default { if (this.form.isPlatformCompany === undefined || this.form.isPlatformCompany === null) { this.form.isPlatformCompany = 0; } + if (this.form.isBusinessDept === undefined || this.form.isBusinessDept === null) { + this.form.isBusinessDept = 0; + } this.clearEmptyContractInitialSerial(this.form); this.initData(this.form.tenantId); this.loadParentDeptCode(this.form.parentId); From 2a5eca83933aa1fdc69a62417dc3724cbb3e4da4 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 23 Oct 2026 04:28:22 +0800 Subject: [PATCH 4/4] =?UTF-8?q?:sparkles:=20=E8=AF=84=E5=88=86=E5=87=8F?= =?UTF-8?q?=E5=88=86=E7=B4=AF=E8=AE=A1=E4=B8=8E=E6=9C=80=E9=AB=98=E6=89=A3?= =?UTF-8?q?=E5=88=86=E3=80=81=E7=94=A8=E6=88=B7=E9=83=A8=E9=97=A8=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E8=81=94=E5=8A=A8=E6=A0=91=EF=BC=8C=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=AF=B9=E8=B4=A6=E5=AF=BC=E5=85=A5=E4=B8=8E=E7=BB=93?= =?UTF-8?q?=E7=AE=97=E8=B0=83=E6=95=B4=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/axios.js | 41 ++- src/utils/import-excel.js | 24 +- .../components/business-crud-page.vue | 4 +- .../components/master-order-editor.vue | 54 ++-- .../components/waybill-manage-page.vue | 4 +- src/views/business/loading-manage.vue | 76 ++++- src/views/business/voucher-manage.vue | 18 +- .../payment/payment-application-form.vue | 273 +++++++----------- .../settlement-adjustment-editor.vue | 8 +- .../components/settlement-import-page.vue | 30 +- .../transport-reconciliation-editor.vue | 92 ++++-- .../settlement/settlement-import-form.vue | 20 +- src/views/settlement/settlement-import.vue | 9 +- src/views/system/user.vue | 107 ++++++- .../vehicle/credit-score-quantification.vue | 32 ++ src/views/vehicle/customer-archive.vue | 176 ++++++++--- vite.config.mjs | 30 +- 17 files changed, 691 insertions(+), 307 deletions(-) diff --git a/src/axios.js b/src/axios.js index a3de4a3..455eb3d 100644 --- a/src/axios.js +++ b/src/axios.js @@ -105,12 +105,27 @@ const isJsonContentType = res => { return contentType.includes('application/json') || contentType.includes('text/json'); }; +/** + * 是否为可按文本读取的 Blob / Blob-like(含跨 realm)。 + * 禁止对普通业务 JSON 对象调用 .text()。 + */ +const isBlobLikeData = value => + value != null && + typeof value === 'object' && + typeof value.size === 'number' && + typeof value.type === 'string' && + typeof value.text === 'function'; + /** * 判断响应体是否已是解析好的对象(无需再解析)。 * 注意 Blob 也是 object,必须排除,否则会把待解析的文件流误判为已完成。 */ const isPlainObject = value => - value !== null && typeof value === 'object' && !(value instanceof Blob); + value !== null && + typeof value === 'object' && + !(value instanceof Blob) && + !isBlobLikeData(value) && + !(value instanceof ArrayBuffer); //跨域请求,允许保存cookie axios.defaults.withCredentials = true; // NProgress Configuration @@ -190,15 +205,20 @@ axios.interceptors.response.use( // 用户就会以为系统崩了——而后端其实返回了明确的业务提示(如「未读取到数据行…」)。 // 这里按 Content-Type 判断:JSON 才解析回对象;真·文件流(Excel 等)保持原样不动。 // 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。 + // 仅对真正的 Blob/Blob-like 调 .text(),避免业务 JSON 已解析后出现 + // 「e.data.text is not a function」。 if (isJsonContentType(res) && !isPlainObject(res.data)) { try { let text = ''; - if (typeof res.data?.text === 'function') { - text = await res.data.text(); - } else if (typeof res.data === 'string') { - text = res.data; - } else if (typeof res.data?.arrayBuffer === 'function') { - text = new TextDecoder().decode(await res.data.arrayBuffer()); + const raw = res.data; + if (isBlobLikeData(raw) || (typeof Blob !== 'undefined' && raw instanceof Blob)) { + text = await raw.text(); + } else if (typeof raw === 'string') { + text = raw; + } else if (raw && typeof raw.arrayBuffer === 'function') { + text = new TextDecoder().decode(await raw.arrayBuffer()); + } else if (raw instanceof ArrayBuffer) { + text = new TextDecoder().decode(raw); } const parsed = typeof text === 'string' && text ? JSON.parse(text) : null; if (parsed && typeof parsed === 'object') { @@ -208,10 +228,11 @@ axios.interceptors.response.use( // 声明是 JSON 却解析失败:保持原样,交给下游按文件处理 } } - //获取状态信息 - const status = res.data.error_code || res.data.code || res.status; + // 获取状态信息:文件流没有 code/msg,必须退回 HTTP status,禁止读 Blob 上的幽灵字段 + const payload = isPlainObject(res.data) ? res.data : {}; + const status = Number(payload.error_code || payload.code || res.status); const statusWhiteList = website.statusWhiteList || []; - const message = res.data.msg || res.data.error_description || '系统错误'; + const message = payload.msg || payload.error_description || '系统错误'; //如果在白名单里则自行catch逻辑处理 if (statusWhiteList.includes(status)) return Promise.reject(res); diff --git a/src/utils/import-excel.js b/src/utils/import-excel.js index b6a9093..1bd5e02 100644 --- a/src/utils/import-excel.js +++ b/src/utils/import-excel.js @@ -88,7 +88,7 @@ export const parseBlobJson = async data => { * * 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。 */ -const resolveErrorMessage = async error => { +export const resolveImportErrorMessage = async error => { const payload = error?.response?.data ?? error?.data; if (isBusinessJson(payload) && payload.msg) { return payload.msg; @@ -109,6 +109,26 @@ const resolveErrorMessage = async error => { return fallback; }; +/** + * 统一消费导入接口响应:成功 JSON / 失败 Excel 流。 + * 兼容 axios 已把 JSON Blob 解析成对象的情况,禁止再对普通对象调 .text()。 + */ +export const consumeImportResponse = async (res, options = {}) => { + const { businessName = '导入', dayjsFormat } = options; + if (isExcelResponse(res)) { + return { + kind: 'excel', + blob: res?.data, + fileName: `${businessName}导入失败明细${dayjsFormat || ''}.xlsx`, + }; + } + const result = isBusinessJson(res?.data) ? res.data : await parseBlobJson(res?.data); + if (Number(result?.code) !== 200) { + throw new Error(result?.msg || '导入失败'); + } + return { kind: 'success', result }; +}; + /** * 加工后端返回的导入失败明细 Excel。 * 若传入 failDetailDecorator(函数),则动态加载 exceljs 读取 blob 流, @@ -235,7 +255,7 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc // 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次, // 只保留一次清晰的中文提示。 if (!error?._notified) { - vm.$message.error(await resolveErrorMessage(error)); + vm.$message.error(await resolveImportErrorMessage(error)); } notifyUploadError(option, error); } finally { diff --git a/src/views/business/components/business-crud-page.vue b/src/views/business/components/business-crud-page.vue index eb23da3..36aa8b5 100644 --- a/src/views/business/components/business-crud-page.vue +++ b/src/views/business/components/business-crud-page.vue @@ -13824,13 +13824,13 @@ export default { } const unavailableWaybills = this.selectionList.filter( row => - this.statusValue(row) !== 'pending' || + !['pending', 'processing', 'running'].includes(this.statusValue(row)) || row.loadingId || row.loadingManageId || row.loadingNo ); if (unavailableWaybills.length) { - this.$message.warning('公路配载仅支持进行中且未配载的运单'); + this.$message.warning('公路配载仅支持待执行/进行中且未关联有效配载单的运单'); return; } try { diff --git a/src/views/business/components/master-order-editor.vue b/src/views/business/components/master-order-editor.vue index 74ce647..6ad4fde 100644 --- a/src/views/business/components/master-order-editor.vue +++ b/src/views/business/components/master-order-editor.vue @@ -94,7 +94,7 @@
@@ -111,7 +111,7 @@
@@ -714,7 +714,7 @@ export default { 'msg', 'zip', ], - transports: ['公路运输', '铁路运输', '水路运输', '航空运输'], + transports: ['铁路运输', '水路运输', '航空运输', '公路运输'], quantityUnitOptions: [], quantityUnitLoading: false, form: { @@ -798,6 +798,7 @@ export default { methods: { formatContractOptionLabel, defaultQuantityUnit() { + if (this.quantityUnitOptions.includes('吨')) return '吨'; return this.quantityUnitOptions[0] || '吨'; }, loadQuantityUnitOptions() { @@ -1164,22 +1165,30 @@ export default { const field = this.addressModel(target); if (!field) return false; const siteCode = String(field.model[field.siteCode] || '').trim(); - // 地址展示模式由实际选择的数据决定:有站点编码表示非公路站点名称, - // 没有站点编码则保留省市区级联,避免切换运输方式时改写已有地址。 + // 展示模式由已选数据决定:有站点编码则显示站点名,否则省市区;暂不按运输方式强制站点限制。 return Boolean(siteCode && siteCode !== '/'); }, + hasAddressValue(target) { + const field = this.addressModel(target); + if (!field) return false; + return Boolean( + String(field.model[field.address] || '').trim() || String(field.model[field.name] || '').trim() + ); + }, + tipEmptyAddressDetail(target) { + // 替代原「请先选择运输方式」placeholder:空地址点击时 toast,已有值不再提示。 + if (!this.hasAddressValue(target)) { + this.$message.warning('请输入详细地址'); + } + }, resolveAddressRegionCode(row = {}) { return row.regionCode || row.districtCode || row.areaCode || row.adcode || ''; }, openCommonAddress(target) { if (this.isBasicRouteReadonly) return; + this.tipEmptyAddressDetail(target); this.addressTarget = target; - if (this.addressTransportMode(target) !== 'road') { - this.stationQuery = {}; - this.stationPage.current = 1; - this.stationDialogVisible = true; - return; - } + // 暂不按运输方式限制站点:统一走常用地址选择。 this.addressQuery = {}; this.addressPage.current = 1; this.addressDialogVisible = true; @@ -1279,18 +1288,11 @@ export default { }, handleAddressDetailClick(target) { if (this.isBasicRouteReadonly) return; - if (this.addressTransportMode(target) !== 'road') { - this.openCommonAddress(target); - return; - } + this.tipEmptyAddressDetail(target); this.openAddressMap(target); }, openAddressMap(target) { if (this.isBasicRouteReadonly) return; - if (this.addressTransportMode(target) !== 'road') { - this.$message.info('非公路运输地址不能使用地图选择'); - return; - } const field = this.addressModel(target); if (!field) return; this.mapTarget = target; @@ -1690,7 +1692,11 @@ export default { '', cargoTypePath, quantity: row.quantity || '', - quantityUnit: row.quantityUnit || this.defaultQuantityUnit(), + // 常用货物单位若为空或「个」,新建总单统一默认「吨」 + quantityUnit: + !row.quantityUnit || String(row.quantityUnit).trim() === '个' + ? this.defaultQuantityUnit() + : row.quantityUnit, packageType: row.packageType || row.package || '', brand: row.brand || '', specification: row.specification || row.spec || '', @@ -1775,17 +1781,21 @@ export default { return formatUpdateUserName(row); }, selectRoute(route) { + // 先写入运输方式(会触发地点清空),再回填线路地址,避免被清空逻辑覆盖。 + this.form.finalTransportType = '公路运输'; + if (!this.form.routes.length) this.addRoute(); + if (this.form.routes[0]) this.form.routes[0].transportType = '公路运输'; this.form.departureName = route.departureName || ''; this.form.departureAddress = route.departureAddress || ''; this.form.departureContact = route.departureContact || ''; this.form.departurePhone = route.departurePhone || ''; + this.form.departureSiteCode = ''; this.form.arrivalName = route.arrivalName || ''; this.form.arrivalAddress = route.arrivalAddress || ''; this.form.arrivalContact = route.arrivalContact || ''; this.form.arrivalPhone = route.arrivalPhone || ''; + this.form.arrivalSiteCode = ''; this.syncAddressRegionPaths(); - this.form.finalTransportType = '公路运输'; - if (!this.form.routes.length) this.addRoute(); this.routeDialogVisible = false; }, createRoute(index = this.form.routes.length) { diff --git a/src/views/business/components/waybill-manage-page.vue b/src/views/business/components/waybill-manage-page.vue index e7b489f..a701aaf 100644 --- a/src/views/business/components/waybill-manage-page.vue +++ b/src/views/business/components/waybill-manage-page.vue @@ -10414,13 +10414,13 @@ export default { } const unavailableWaybills = this.selectionList.filter( row => - !['processing', 'running'].includes(this.statusValue(row)) || + !['pending', 'processing', 'running'].includes(this.statusValue(row)) || row.loadingId || row.loadingManageId || row.loadingNo ); if (unavailableWaybills.length) { - this.$message.warning('公路配载仅支持进行中且未配载的运单'); + this.$message.warning('公路配载仅支持待执行/进行中且未关联有效配载单的运单'); return; } try { diff --git a/src/views/business/loading-manage.vue b/src/views/business/loading-manage.vue index ce1374d..9196fe0 100644 --- a/src/views/business/loading-manage.vue +++ b/src/views/business/loading-manage.vue @@ -635,10 +635,11 @@ min-width="100" />
+
+
可配载条件
+
+ 同时满足以下条件的运单可被配载:运输方式为「公路」;运单状态为「待执行」或「进行中」;未关联其他有效配载单。 +
+
配载后状态说明
+
+ 子运单:按项目过程配置是否需司机接单,无需接单则为进行中,需接单则为待执行。配载单:按所选运单所属项目的过程配置,需接单则为待执行,无需接单则为进行中。 +
+