diff --git a/src/api/system/user.js b/src/api/system/user.js index 70d5b20..4e1c8de 100644 --- a/src/api/system/user.js +++ b/src/api/system/user.js @@ -31,10 +31,16 @@ export const add = row => { }); }; -export const syncIamAccounts = () => { +export const syncIamAccounts = (current = 1, size = 50, signal) => { return request({ url: '/blade-system/user/sync-iam-accounts', method: 'post', + params: { + current, + size, + }, + timeout: 60000, + signal, }); }; diff --git a/src/router/avue-router.js b/src/router/avue-router.js index 3785072..5538125 100644 --- a/src/router/avue-router.js +++ b/src/router/avue-router.js @@ -3,6 +3,8 @@ import { getToken } from '@/utils/auth'; import store from '@/store'; import { generateIframePath, processUrlForQuery, isURL } from './router'; import { wrapViewLoader } from '@/utils/chunk-reload'; + +// 保持懒加载,避免 eager 与 store 循环依赖(Cannot access 'store' before initialization) const modules = import.meta.glob('../**/**/*.vue'); // 将多级路由扁平化为二级路由,支持 keep-alive 跨层级缓存 diff --git a/src/router/views/index.js b/src/router/views/index.js index 3a380e6..93079e0 100644 --- a/src/router/views/index.js +++ b/src/router/views/index.js @@ -1,5 +1,8 @@ import Layout from '@/page/index/index.vue'; import Store from '@/store/'; +import { wrapViewLoader } from '@/utils/chunk-reload'; + +const loadView = loader => wrapViewLoader(loader); export default [ { @@ -10,7 +13,7 @@ export default [ path: '', name: '汇票付款表单', meta: { keepAlive: false, activeMenu: '/payment/bill-payment' }, - component: () => import('@/views/payment/bill-payment-form.vue'), + component: loadView(() => import('@/views/payment/bill-payment-form.vue')), }, ], }, @@ -22,7 +25,7 @@ export default [ path: '', name: '汇票台账表单', meta: { keepAlive: false, activeMenu: '/payment/bill-ledger' }, - component: () => import('@/views/payment/bill-ledger-form.vue'), + component: loadView(() => import('@/views/payment/bill-ledger-form.vue')), }, ], }, @@ -34,7 +37,7 @@ export default [ path: '', name: '认领记录详情', meta: { keepAlive: false, activeMenu: '/payment/receipt-claim-record' }, - component: () => import('@/views/payment/receipt-claim-record-form.vue'), + component: loadView(() => import('@/views/payment/receipt-claim-record-form.vue')), }, ], }, @@ -46,7 +49,7 @@ export default [ path: '', name: '收款流水认领', meta: { keepAlive: false, activeMenu: '/payment/receipt-flow' }, - component: () => import('@/views/payment/receipt-flow-form.vue'), + component: loadView(() => import('@/views/payment/receipt-flow-form.vue')), }, ], }, @@ -58,7 +61,7 @@ export default [ path: '', name: '收票登记', meta: { keepAlive: false, activeMenu: '/payment/invoice-receipt' }, - component: () => import('@/views/payment/invoice-receipt-form.vue'), + component: loadView(() => import('@/views/payment/invoice-receipt-form.vue')), }, ], }, @@ -70,7 +73,7 @@ export default [ path: '', name: '开票申请', meta: { keepAlive: false, activeMenu: '/payment/invoice-application' }, - component: () => import('@/views/payment/invoice-application-form.vue'), + component: loadView(() => import('@/views/payment/invoice-application-form.vue')), }, ], }, @@ -82,7 +85,7 @@ export default [ path: '', name: '付款申请', meta: { keepAlive: false, activeMenu: '/payment/payment-application' }, - component: () => import('@/views/payment/payment-application-form.vue'), + component: loadView(() => import('@/views/payment/payment-application-form.vue')), }, ], }, @@ -97,7 +100,7 @@ export default [ keepAlive: false, activeMenu: '/settlement/pre-settlement', }, - component: () => import('@/views/settlement/pre-settlement-form.vue'), + component: loadView(() => import('@/views/settlement/pre-settlement-form.vue')), }, ], }, @@ -112,7 +115,7 @@ export default [ keepAlive: false, activeMenu: '/settlement/formal-settlement', }, - component: () => import('@/views/settlement/formal-settlement-form.vue'), + component: loadView(() => import('@/views/settlement/formal-settlement-form.vue')), }, ], }, @@ -127,7 +130,7 @@ export default [ keepAlive: false, activeMenu: '/settlement/transport-reconciliation', }, - component: () => import('@/views/settlement/transport-reconciliation-form.vue'), + component: loadView(() => import('@/views/settlement/transport-reconciliation-form.vue')), }, ], }, @@ -139,7 +142,7 @@ export default [ path: '', name: '执行凭证批次详情', meta: { keepAlive: false, activeMenu: '/business/voucher-manage' }, - component: () => import('@/views/business/voucher-manage-detail.vue'), + component: loadView(() => import('@/views/business/voucher-manage-detail.vue')), }, ], }, @@ -151,7 +154,7 @@ export default [ path: '', name: '新增合同管理', meta: { keepAlive: false }, - component: () => import('@/views/business/contract-manage.vue'), + component: loadView(() => import('@/views/business/contract-manage.vue')), }, ], }, @@ -163,7 +166,7 @@ export default [ path: '', name: '合同详情', meta: { keepAlive: false, activeMenu: '/business/contract-manage' }, - component: () => import('@/views/business/contract-manage.vue'), + component: loadView(() => import('@/views/business/contract-manage.vue')), }, ], }, @@ -175,7 +178,7 @@ export default [ path: '', name: '新增项目申请', meta: { keepAlive: false }, - component: () => import('@/views/business/project-apply.vue'), + component: loadView(() => import('@/views/business/project-apply.vue')), }, ], }, @@ -187,7 +190,7 @@ export default [ path: '', name: '新增编辑运单管理', meta: { keepAlive: false }, - component: () => import('@/views/business/waybill-manage.vue'), + component: loadView(() => import('@/views/business/waybill-manage.vue')), }, ], }, @@ -199,7 +202,7 @@ export default [ path: '', name: '运单管理详情', meta: { keepAlive: false, activeMenu: '/business/waybill-manage' }, - component: () => import('@/views/business/waybill-manage-detail.vue'), + component: loadView(() => import('@/views/business/waybill-manage-detail.vue')), }, ], }, @@ -211,7 +214,7 @@ export default [ path: '', name: '导入运单', meta: { keepAlive: false, activeMenu: '/business/waybill-import' }, - component: () => import('@/views/business/waybill-import.vue'), + component: loadView(() => import('@/views/business/waybill-import.vue')), }, ], }, @@ -223,7 +226,7 @@ export default [ path: '', name: '新建导入运单', meta: { keepAlive: false, activeMenu: '/business/waybill-import' }, - component: () => import('@/views/business/waybill-import-form.vue'), + component: loadView(() => import('@/views/business/waybill-import-form.vue')), }, ], }, @@ -235,7 +238,7 @@ export default [ path: '', name: '新增编辑配载管理', meta: { keepAlive: false }, - component: () => import('@/views/business/loading-manage.vue'), + component: loadView(() => import('@/views/business/loading-manage.vue')), }, ], }, @@ -247,7 +250,7 @@ export default [ path: '', name: '新增编辑运输计划', meta: { keepAlive: false }, - component: () => import('@/views/business/transport-plan.vue'), + component: loadView(() => import('@/views/business/transport-plan.vue')), }, ], }, @@ -259,7 +262,7 @@ export default [ path: '', name: '导入运输计划', meta: { keepAlive: false, activeMenu: '/business/transport-plan' }, - component: () => import('@/views/business/transport-plan-import.vue'), + component: loadView(() => import('@/views/business/transport-plan-import.vue')), }, ], }, @@ -271,7 +274,7 @@ export default [ path: '', name: '计划调度', meta: { keepAlive: false, activeMenu: '/business/transport-plan' }, - component: () => import('@/views/business/transport-plan-dispatch.vue'), + component: loadView(() => import('@/views/business/transport-plan-dispatch.vue')), }, ], }, @@ -283,7 +286,7 @@ export default [ path: '', name: '新增编辑运单模板', meta: { keepAlive: false }, - component: () => import('@/views/business/shipping-template.vue'), + component: loadView(() => import('@/views/business/shipping-template.vue')), }, ], }, @@ -295,7 +298,7 @@ export default [ path: '', name: '新增客商档案', meta: { keepAlive: false }, - component: () => import('@/views/vehicle/customer-archive.vue'), + component: loadView(() => import('@/views/vehicle/customer-archive.vue')), }, ], }, @@ -307,7 +310,7 @@ export default [ path: '', name: '合同变更', meta: { keepAlive: false }, - component: () => import('@/views/business/contract-manage-change.vue'), + component: loadView(() => import('@/views/business/contract-manage-change.vue')), }, ], }, @@ -323,7 +326,7 @@ export default [ meta: { i18n: 'dashboard', }, - component: () => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue'), + component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue')), }, { path: 'dashboard', @@ -332,7 +335,7 @@ export default [ i18n: 'dashboard', menu: false, }, - component: () => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue'), + component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue')), }, ], }, @@ -347,7 +350,7 @@ export default [ meta: { i18n: 'test', }, - component: () => import(/* webpackChunkName: "views" */ '@/views/util/test.vue'), + component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/util/test.vue')), }, ], }, @@ -362,8 +365,9 @@ export default [ meta: { i18n: 'dict', }, - component: () => - import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-horizontal.vue'), + component: loadView(() => + import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-horizontal.vue') + ), }, ], }, @@ -378,8 +382,9 @@ export default [ meta: { i18n: 'dict', }, - component: () => - import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-vertical.vue'), + component: loadView(() => + import(/* webpackChunkName: "views" */ '@/views/util/demo/dict-vertical.vue') + ), }, ], }, @@ -394,7 +399,7 @@ export default [ meta: { i18n: 'info', }, - component: () => import(/* webpackChunkName: "views" */ '@/views/system/userinfo.vue'), + component: loadView(() => import(/* webpackChunkName: "views" */ '@/views/system/userinfo.vue')), }, ], }, @@ -409,8 +414,9 @@ export default [ meta: { i18n: 'work', }, - component: () => - import(/* webpackChunkName: "views" */ '@/views/work/process/leave/form.vue'), + component: loadView(() => + import(/* webpackChunkName: "views" */ '@/views/work/process/leave/form.vue') + ), }, { path: 'handle/:taskId/:processInstanceId/:businessId', @@ -418,8 +424,9 @@ export default [ meta: { i18n: 'work', }, - component: () => - import(/* webpackChunkName: "views" */ '@/views/work/process/leave/handle.vue'), + component: loadView(() => + import(/* webpackChunkName: "views" */ '@/views/work/process/leave/handle.vue') + ), }, { path: 'detail/:processInstanceId/:businessId', @@ -427,8 +434,9 @@ export default [ meta: { i18n: 'work', }, - component: () => - import(/* webpackChunkName: "views" */ '@/views/work/process/leave/detail.vue'), + component: loadView(() => + import(/* webpackChunkName: "views" */ '@/views/work/process/leave/detail.vue') + ), }, ], }, diff --git a/src/views/business/project-apply.vue b/src/views/business/project-apply.vue index 3f62e96..e48ba77 100644 --- a/src/views/business/project-apply.vue +++ b/src/views/business/project-apply.vue @@ -1354,6 +1354,13 @@ export default { .filter(fileType => !uploadedTypes.has(fileType)); }, }, + watch: { + '$route.fullPath'() { + if (this.isProjectFormPage) { + this.openProjectFormPage(); + } + }, + }, created() { this.loadDeptOptions(); this.loadCargoTypeOptions(); @@ -1363,6 +1370,11 @@ export default { this.openProjectFormPage(); } }, + activated() { + if (this.isProjectFormPage && !this.projectBox) { + this.openProjectFormPage(); + } + }, methods: { buildTableOption() { return { diff --git a/src/views/system/dept.vue b/src/views/system/dept.vue index edad1f7..9004b7e 100644 --- a/src/views/system/dept.vue +++ b/src/views/system/dept.vue @@ -22,14 +22,6 @@ @tree-load="treeLoad" > - 同步组织 - { - this.iamSyncLoading = true; - return syncIamOrganizations(); - }) - .then(res => { - const count = res?.data?.data ?? 0; - this.$message.success(`IAM组织同步完成,共处理${count}条`); - this.parentId = 0; - this.onLoad(this.page, this.query); - }) - .finally(() => { - this.iamSyncLoading = false; - }); - }, handleDelete() { if (this.selectionList.length === 0) { this.$message.warning('请选择至少一条数据'); diff --git a/src/views/system/user.vue b/src/views/system/user.vue index 1ed0669..70247da 100644 --- a/src/views/system/user.vue +++ b/src/views/system/user.vue @@ -340,6 +340,45 @@ + + + {{ oaSyncDialogTitle }} + + + + 当前进度:第 {{ oaSyncProgress.current || 0 }} / {{ oaSyncTotalPage }} 页 + + + 同步成功 + {{ oaSyncProgress.synced }} + + + 跳过 + {{ oaSyncProgress.skipped }} + + + + + + 取消 + 关闭 + + + + @@ -403,6 +442,18 @@ export default { query: {}, loading: true, iamSyncLoading: false, + oaSyncVisible: false, + oaSyncRunning: false, + oaSyncCancelled: false, + oaSyncStatus: 'running', + oaSyncController: null, + oaSyncProgress: { + current: 0, + size: 50, + total: 0, + synced: 0, + skipped: 0, + }, page: { pageSize: 10, currentPage: 1, @@ -453,6 +504,46 @@ export default { }, computed: { ...mapGetters(['userInfo', 'permission']), + oaSyncDialogTitle() { + if (this.oaSyncStatus === 'done') { + return '同步完成'; + } + if (this.oaSyncStatus === 'cancelled') { + return '已取消同步'; + } + if (this.oaSyncStatus === 'error') { + return '同步失败'; + } + return '同步人员'; + }, + oaSyncTotalPage() { + const total = Number(this.oaSyncProgress.total) || 0; + const size = Number(this.oaSyncProgress.size) || 50; + if (total <= 0) { + return this.oaSyncProgress.current || 0; + } + return Math.max(1, Math.ceil(total / size)); + }, + oaSyncPercent() { + if (this.oaSyncStatus === 'done') { + return 100; + } + const totalPage = this.oaSyncTotalPage; + const current = Number(this.oaSyncProgress.current) || 0; + if (totalPage <= 0) { + return 0; + } + return Math.min(100, Math.round((current / totalPage) * 100)); + }, + oaSyncProgressStatus() { + if (this.oaSyncStatus === 'done') { + return 'success'; + } + if (this.oaSyncStatus === 'error') { + return 'exception'; + } + return undefined; + }, // 所属组织级联配置:checkStrictly 为 false 时只能选择最后一级(叶子节点) deptCascaderProps() { return { @@ -564,23 +655,87 @@ export default { } }, handleIamSync() { - this.$confirm('确定从IAM同步人员信息?', '提示', { + this.$confirm('确定从OA同步人员及组织信息?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning', - }) - .then(() => { - this.iamSyncLoading = true; - return syncIamAccounts(); - }) - .then(res => { - const count = res?.data?.data ?? 0; - this.$message.success(`IAM人员同步完成,共处理${count}条`); + }).then(() => { + this.startOaSync(); + }); + }, + startOaSync() { + this.iamSyncLoading = true; + this.oaSyncVisible = true; + this.oaSyncRunning = true; + this.oaSyncCancelled = false; + this.oaSyncStatus = 'running'; + this.oaSyncController = new AbortController(); + this.oaSyncProgress = { + current: 0, + size: 50, + total: 0, + synced: 0, + skipped: 0, + }; + this.runOaSyncPages(); + }, + isOaSyncCanceledError(error) { + return ( + this.oaSyncCancelled || + error?.code === 'ERR_CANCELED' || + error?.name === 'CanceledError' || + error?.name === 'AbortError' + ); + }, + async runOaSyncPages() { + const size = 50; + let current = 1; + try { + while (!this.oaSyncCancelled) { + const res = await syncIamAccounts(current, size, this.oaSyncController?.signal); + const page = res?.data?.data || {}; + this.oaSyncProgress.current = page.current || current; + this.oaSyncProgress.size = page.size || size; + this.oaSyncProgress.total = page.total || 0; + this.oaSyncProgress.synced += page.syncedCount || 0; + this.oaSyncProgress.skipped += page.skippedCount || 0; + if (page.finished) { + this.oaSyncStatus = 'done'; + break; + } + current += 1; + } + if (this.oaSyncCancelled && this.oaSyncStatus === 'running') { + this.oaSyncStatus = 'cancelled'; + } + } catch (error) { + if (this.isOaSyncCanceledError(error)) { + this.oaSyncStatus = 'cancelled'; + } else { + this.oaSyncStatus = 'error'; + this.$message.error(error?.message || 'OA人员同步失败'); + } + } finally { + this.oaSyncRunning = false; + this.iamSyncLoading = false; + if (this.oaSyncStatus === 'done' || this.oaSyncStatus === 'cancelled') { this.onLoad(this.page, this.query); - }) - .finally(() => { - this.iamSyncLoading = false; - }); + } + } + }, + cancelOaSync() { + if (!this.oaSyncRunning) { + return; + } + this.oaSyncCancelled = true; + this.oaSyncController?.abort(); + }, + closeOaSyncDialog() { + if (this.oaSyncRunning) { + this.cancelOaSync(); + return; + } + this.oaSyncVisible = false; }, handleSetLeader(row) { const tip = row.isLeader === 1 ? '确定取消用户的主管职务?' : '确定设置用户为主管职务?'; @@ -1150,14 +1305,16 @@ export default { margin-top: 8px; } -.user-edit-dialog .dialog-title { +.user-edit-dialog .dialog-title, +.oa-sync-dialog .dialog-title { display: inline-flex; align-items: center; font-size: 16px; font-weight: 600; } -.user-edit-dialog .dialog-title::before { +.user-edit-dialog .dialog-title::before, +.oa-sync-dialog .dialog-title::before { width: 4px; height: 18px; margin-right: 8px; @@ -1165,6 +1322,52 @@ export default { content: ''; } +.oa-sync-body { + padding: 8px 4px 0; +} + +.oa-sync-meta { + margin-top: 12px; + color: #606266; + font-size: 13px; +} + +.oa-sync-stats { + display: flex; + gap: 16px; + margin-top: 16px; +} + +.oa-sync-stat { + flex: 1; + padding: 12px 16px; + background: #fafafa; + border: 1px solid #eff1f7; + border-radius: 4px; +} + +.oa-sync-stat__label { + display: block; + color: #909399; + font-size: 12px; +} + +.oa-sync-stat__value { + display: block; + margin-top: 6px; + font-size: 22px; + font-weight: 600; + line-height: 1.2; +} + +.oa-sync-stat__value--success { + color: #409eff; +} + +.oa-sync-stat__value--skip { + color: #909399; +} + .user-edit-dialog .el-form-item { margin-bottom: 14px; }