From c1c4fa18c915b1e4191c760e9d088780b14a4bc3 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 16 Sep 2026 06:30:30 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=20iam?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 2 + src/api/business/loading-manage.js | 9 + src/api/system/user.js | 7 + src/docker/Dockerfile | 1 + src/error.js | 4 + src/main.js | 2 + src/option/business/shipping-template.js | 34 +- src/option/business/transport-plan.js | 10 +- src/page/index/layout.vue | 13 +- src/permission.js | 23 +- src/router/avue-router.js | 23 +- src/router/tab.js | 2 +- src/store/getters.js | 7 +- src/utils/chunk-reload.js | 144 ++++- .../components/master-order-detail.vue | 131 +++- .../components/master-order-dispatch.vue | 195 +++--- .../components/shipping-template-page.vue | 108 +++- .../components/transport-plan-page.vue | 400 ++++++++---- .../components/waybill-manage-page.vue | 15 +- src/views/business/loading-manage.vue | 43 +- src/views/business/master-order.vue | 326 ++++++---- .../business/transport-plan-dispatch.vue | 609 +++++++++++------- src/views/business/voucher-manage.vue | 32 +- .../settlement/receivable-payable-detail.vue | 12 - src/views/system/user.vue | 29 + src/views/vehicle/customer-archive.vue | 2 +- 26 files changed, 1517 insertions(+), 666 deletions(-) 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/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/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/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/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/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/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/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 27b2028..9051363 100644 --- a/src/permission.js +++ b/src/permission.js @@ -3,6 +3,8 @@ import store from './store'; import { tabKeyOf } from '@/router/tab'; import { getToken } from '@/utils/auth'; import { + consumeReloadQuery, + hasReloadQuery, isChunkLoadError, reloadForChunkError, setPendingRoutePath, @@ -17,19 +19,20 @@ const lockPage = '/lock'; //锁屏页 router.onError(error => { if (!isChunkLoadError(error)) return; if (reloadForChunkError()) { - ElMessage.warning('页面资源加载失败,正在重新加载…'); - } -}); - -window.addEventListener('unhandledrejection', event => { - if (!isChunkLoadError(event.reason)) return; - if (reloadForChunkError()) { - event.preventDefault(); - ElMessage.warning('页面资源加载失败,正在重新加载…'); + 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; @@ -59,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/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 index 2d42f54..3c30bb4 100644 --- a/src/utils/chunk-reload.js +++ b/src/utils/chunk-reload.js @@ -1,38 +1,168 @@ /** * 懒加载 chunk / CSS preload 失败后的整页恢复。 * 常见于:部署后旧 hash 失效、静态服务空闲断连、代理 CONNECTION_RESET。 + * + * 失败后必须整页刷新:旧入口里的 hashed 资源地址不会自行更新, + * 仅捕获路由错误而不刷新时,未打开过的页面会一直无法进入。 */ const RELOAD_FLAG = 'app:chunk-reload-ts'; -const RELOAD_COOLDOWN_MS = 10000; +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/i; + /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) { - if (!error) return false; - const message = error.message || String(error); - return CHUNK_ERROR_RE.test(message); + 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; - window.location.assign(path); + 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/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 @@ -
- 暂无变更记录 -
@@ -2734,12 +2728,6 @@ export default { padding: 16px 0; } -.settlement-detail-page__empty { - padding: 24px 0; - color: #909399; - text-align: center; -} - .settlement-detail-page__dialog-form { padding: 16px 20px; background: #fff; diff --git a/src/views/system/user.vue b/src/views/system/user.vue index bb4c73d..1ed0669 100644 --- a/src/views/system/user.vue +++ b/src/views/system/user.vue @@ -56,6 +56,14 @@ @click="handleAudit" >审 核 + 同步人员 + { + this.iamSyncLoading = true; + return syncIamAccounts(); + }) + .then(res => { + const count = res?.data?.data ?? 0; + this.$message.success(`IAM人员同步完成,共处理${count}条`); + this.onLoad(this.page, this.query); + }) + .finally(() => { + this.iamSyncLoading = false; + }); + }, handleSetLeader(row) { const tip = row.isLeader === 1 ? '确定取消用户的主管职务?' : '确定设置用户为主管职务?'; const message = row.isLeader === 1 ? '取消主管成功!' : '设置主管成功!'; diff --git a/src/views/vehicle/customer-archive.vue b/src/views/vehicle/customer-archive.vue index 9c36d1a..1325abc 100644 --- a/src/views/vehicle/customer-archive.vue +++ b/src/views/vehicle/customer-archive.vue @@ -4758,7 +4758,7 @@ export default { }, selectionClear() { this.selectionList = []; - this.$refs.crud.toggleSelection(); + this.$refs.crud?.toggleSelection(); }, currentChange(currentPage) { this.page.currentPage = currentPage;