From 1383b58e803755fe28e82e52ee150da4a1a42119 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Fri, 30 Oct 2026 17:39:50 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E5=AE=9E=E6=97=B6=E8=BF=9E=E6=8E=A5=E6=8F=A1?= =?UTF-8?q?=E6=89=8B=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 开发代理转发线上接口时,WebSocket 握手补 changeOrigin 与 Origin 改写 - 实时连接握手连续失败两次后停止重连 - 版本号升至 4.11.1 --- .env.development | 7 +- .env.production | 6 +- package.json | 3 +- src/plugins/vxe-table.ts | 2 +- src/stores/tags-view.ts | 1 - src/utils/realtime/client.ts | 8 ++ uno.config.ts | 2 +- vite.config.ts | 208 +++++++++++------------------------ 8 files changed, 83 insertions(+), 154 deletions(-) diff --git a/.env.development b/.env.development index 34c56c6d..c417803b 100644 --- a/.env.development +++ b/.env.development @@ -8,9 +8,8 @@ VITE_APP_TITLE=vue3-element-admin VITE_API_BASE=/dev-api # 接口地址(开发代理转发目标,默认走线上) -# API_PROXY_TARGET=https://www.youlai.tech # 线上 -API_PROXY_TARGET=http://localhost:8000 # 本地 -# API_PROXY_TARGET=https://api.youlai.tech/v2 # 多租户线上 +API_PROXY_TARGET=https://www.youlai.tech # 线上 +# API_PROXY_TARGET=http://localhost:8000 # 本地 # Mock 开关 VITE_MOCK_ENABLED=false @@ -21,5 +20,5 @@ VITE_TENANT_ENABLED=false # AI 开关 VITE_AI_ENABLED=false -# OIDC 登录入口,后端需启用相应配置 +# 统一认证登录开关 VITE_OIDC_ENABLED=false diff --git a/.env.production b/.env.production index 3998e74d..fdbf983b 100644 --- a/.env.production +++ b/.env.production @@ -4,11 +4,11 @@ VITE_API_BASE=/prod-api # 项目名称 VITE_APP_TITLE=vue3-element-admin -# 多租户开关(true:开启 false:关闭) +# 多租户开关 VITE_TENANT_ENABLED=false -# AI 辅助功能开关(true:开启 false:关闭,缺省关闭) +# AI 辅助功能开关 VITE_AI_ENABLED=true -# OIDC 登录入口,后端需启用相应配置 +# 统一认证登录开关 VITE_OIDC_ENABLED=false diff --git a/package.json b/package.json index 2493f829..c4acba25 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vue3-element-admin", "description": "Vue3 + Vite + TypeScript + Element-Plus 的后台管理模板,vue-element-admin 的 Vue3 版本", - "version": "4.11.0", + "version": "4.11.1", "private": true, "type": "module", "scripts": { @@ -14,7 +14,6 @@ "lint:eslint": "eslint --cache \"src/**/*.{vue,ts,js}\" --fix", "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,css,scss,vue,html}\"", "lint:stylelint": "stylelint --cache \"**/*.{css,scss,vue}\" --fix", - "lint:lint-staged": "lint-staged", "lint": "pnpm run lint:eslint && pnpm run lint:prettier && pnpm run lint:stylelint", "lint:check": "eslint src mock vite.config.ts uno.config.ts eslint.config.ts", "format:check": "prettier --check --no-error-on-unmatched-pattern \"src/**/*.{ts,vue,scss,css,json}\" \"mock/**/*.ts\" \"*.{ts,cjs,mjs,json,yaml}\" \".github/workflows/*.{yml,yaml}\"", diff --git a/src/plugins/vxe-table.ts b/src/plugins/vxe-table.ts index b5d303e1..aefd3f40 100644 --- a/src/plugins/vxe-table.ts +++ b/src/plugins/vxe-table.ts @@ -36,7 +36,7 @@ export function configureVxeUI() { VxeUI.setConfig({ // 全局尺寸 size: "medium", - // 全局 zIndex 起始值,默认是 999,如果项目的的 z-index 样式值过大时就需要跟随设置更大,避免被遮挡 + // 全局 zIndex 起始值,默认是 999,如果项目的 z-index 样式值过大时就需要跟随设置更大,避免被遮挡 zIndex: 9999, // 版本号,对于某些带数据缓存的功能有用到,上升版本号可以用于重置数据 version: 0, diff --git a/src/stores/tags-view.ts b/src/stores/tags-view.ts index cb89f576..9ee50906 100644 --- a/src/stores/tags-view.ts +++ b/src/stores/tags-view.ts @@ -115,7 +115,6 @@ export const useTagsViewStore = defineStore("tagsView", () => { if (index > -1) { cachedViews.value = cachedViews.value.slice(index, index + 1); } else { - // if index = -1, there is no cached tags cachedViews.value = []; } resolve([...cachedViews.value]); diff --git a/src/utils/realtime/client.ts b/src/utils/realtime/client.ts index 12bc501c..a62bbfb2 100644 --- a/src/utils/realtime/client.ts +++ b/src/utils/realtime/client.ts @@ -39,6 +39,8 @@ interface ConnectionAttempt { type MessageHandler = (message: RealtimeServerMessage) => void; const MAX_RECONNECT_ATTEMPTS = 10; +// 从未握手成功时只重试两次,服务端不可用时直接停止 +const MAX_HANDSHAKE_ATTEMPTS = 2; const MAX_BUFFERED_BYTES = 16_384; const STOP_RECONNECT_CODES = new Set([1000, 1008, 1009, 4003, 4009]); @@ -129,6 +131,12 @@ export function createRealtimeClient(options: RealtimeClientOptions) { } } if (!retry && enabled) blocked = true; + // 从未连上时快速放弃,避免服务端不可用时反复重连 + if (retry && !attempt.connectedAt && retryCount >= MAX_HANDSHAKE_ATTEMPTS) { + enabled = false; + blocked = true; + return; + } if (retry) scheduleRetry(minimumDelay); } diff --git a/uno.config.ts b/uno.config.ts index 90425d5b..e5f994d6 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -64,7 +64,7 @@ export default defineConfig({ width: "1em", height: "1em", }, - // 图表集合 + // 图标集合 collections: { // svg 是图标集合名称,使用 `i-svg:图标名` 调用 svg: FileSystemIconLoader(iconsDir, (svg) => { diff --git a/vite.config.ts b/vite.config.ts index 551b055f..83d36769 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,6 +8,7 @@ import { ElementPlusResolver } from "unplugin-vue-components/resolvers"; import { mockDevServerPlugin } from "vite-plugin-mock-dev-server"; import UnoCSS from "unocss/vite"; +import { existsSync, readdirSync } from "node:fs"; import { resolve } from "node:path"; import packageInfo from "./package.json" with { type: "json" }; @@ -19,21 +20,13 @@ const __APP_INFO__ = { const pathSrc = resolve(import.meta.dirname, "src"); -export default defineConfig(({ mode, command, isPreview }: ConfigEnv): UserConfig => { +export default defineConfig(({ mode }: ConfigEnv): UserConfig => { const env = loadEnv(mode, import.meta.dirname, ""); - const isDevServer = command === "serve" && !isPreview; - const port = Number(env.APP_PORT ?? "3000"); - if (!Number.isInteger(port) || port < 1 || port > 65535) - throw new Error("APP_PORT 必须是 1–65535 的整数"); + const port = Number(env.APP_PORT) || undefined; const proxyTarget = env.API_PROXY_TARGET; - if (isDevServer) { - if (!proxyTarget || !/^https?:\/\//.test(proxyTarget)) - throw new Error("API_PROXY_TARGET 必须是 HTTP(S) 地址"); - new URL(proxyTarget); - } return { - // 部署在子路径时由 .env.youlai 指定 VITE_BASE_URL,缺省用站点根路径 + // 部署在子路径时由环境变量 VITE_BASE_URL 指定,缺省用站点根路径 base: env.VITE_BASE_URL || "/", resolve: { alias: { @@ -48,76 +41,58 @@ export default defineConfig(({ mode, command, isPreview }: ConfigEnv): UserConfi }, }, }, - server: isDevServer - ? { - host: "0.0.0.0", - port, - open: true, - proxy: { - "/api/v1/realtime/ws": { - target: proxyTarget, - ws: true, - }, - [env.VITE_API_BASE]: { - changeOrigin: true, - target: proxyTarget, - rewrite: (path: string) => path.slice(env.VITE_API_BASE.length), - }, - // 保留浏览器访问地址,OIDC 回调经开发代理返回。 - "/oauth2": { - changeOrigin: true, - xfwd: true, - target: proxyTarget, - }, - "/login/oauth2": { - changeOrigin: true, - xfwd: true, - target: proxyTarget, - }, - }, - } - : undefined, + server: { + host: "0.0.0.0", + port, + open: true, + proxy: { + [env.VITE_API_BASE]: { + changeOrigin: true, + target: proxyTarget, + rewrite: (path: string) => path.slice(env.VITE_API_BASE.length), + }, + // 通知中心 WebSocket,单独代理以完成 ws 升级 + "/api/v1/realtime/ws": { + changeOrigin: true, + rewriteWsOrigin: true, + target: proxyTarget, + ws: true, + }, + // 统一认证登录的授权跳转与回调,路径不改写,跳转回当前开发地址 + "^/(oauth2|login/oauth2)": { + changeOrigin: true, + xfwd: true, + target: proxyTarget, + }, + }, + }, plugins: [ vue(), ...(env.VITE_MOCK_ENABLED === "true" ? [mockDevServerPlugin()] : []), UnoCSS(), - // API 自动导入 + // 函数自动导入 AutoImport({ - // 导入 Vue 函数,如:ref, reactive, toRef 等 imports: ["vue", "@vueuse/core", "pinia", "vue-router", "vue-i18n"], - resolvers: [ - // 导入 Element Plus函数,如:ElMessage, ElMessageBox 等 - ElementPlusResolver({ importStyle: "sass" }), - ], + // Element Plus 函数按需导入 + resolvers: [ElementPlusResolver({ importStyle: "sass" })], vueTemplate: true, - // 类型声明默认不生成,新增自动导入的函数后手动打开并提交一次声明文件 + // 默认不生成类型声明,需要时改用下面的 dts 路径 dts: false, // dts: "types/auto-imports.d.ts", }), // 组件自动导入 Components({ resolvers: [ - // 导入 Element Plus 组件 + // Element Plus 组件按需导入 ElementPlusResolver({ importStyle: "sass" }), ], - // 指定自定义组件位置(默认:src/components) + // 全局组件与页面内组件目录 dirs: ["src/components", "src/**/components"], - // 类型声明默认不生成,新增自动导入的组件后手动打开并提交一次声明文件 + // 默认不生成类型声明,需要时改用下面的 dts 路径 dts: false, // dts: "types/components.d.ts", }), - // 生成版本号文件 - { - name: "dist-version", - apply: "build", - generateBundle() { - this.emitFile({ - type: "asset", - fileName: "version.json", - source: JSON.stringify({ version: __APP_INFO__.buildTimestamp }), - }); - }, - }, + distVersionPlugin, ], // 预加载项目必需的依赖 optimizeDeps: { @@ -142,90 +117,14 @@ export default defineConfig(({ mode, command, isPreview }: ConfigEnv): UserConfi "vxe-table", "path-browserify", "lodash-es", + "dompurify", "@element-plus/icons-vue", "element-plus/es", "element-plus/es/locale/lang/en", "element-plus/es/locale/lang/zh-cn", - // Element Plus 组件样式预构建:按 src 实际用到的组件清单硬编码,首启即预加载, - // 避免首次使用某组件时触发依赖重优化导致页面刷新 - ...[ - "alert", - "avatar", - "backtop", - "badge", - "base", - "breadcrumb", - "breadcrumb-item", - "button", - "card", - "cascader", - "checkbox", - "checkbox-group", - "checkbox-button", - "col", - "color-picker", - "config-provider", - "collapse-transition", - "date-picker", - "descriptions", - "descriptions-item", - "dialog", - "divider", - "drawer", - "dropdown", - "dropdown-item", - "dropdown-menu", - "empty", - "form", - "form-item", - "icon", - "image", - "image-viewer", - "input", - "input-number", - "input-tag", - "link", - "loading", - "menu", - "menu-item", - "message", - "message-box", - "notification", - "option", - "pagination", - "popover", - "progress", - "radio", - "radio-button", - "radio-group", - "result", - "row", - "scrollbar", - "select", - "skeleton", - "skeleton-item", - "space", - "step", - "steps", - "sub-menu", - "switch", - "tab-pane", - "table", - "table-column", - "tabs", - "tag", - "text", - "time-picker", - "time-select", - "timeline", - "timeline-item", - "tooltip", - "tree", - "tree-select", - "upload", - "watermark", - ].map((c) => `element-plus/es/components/${c}/style/index`), - // 流程设计器与预览器为路由懒加载,bpmn-js 体积大,首启预构建避免进入页面时重优化刷新 + // Element Plus 组件样式:按组件目录枚举,新增组件时不会在运行期补优化并刷新页面 + ...getElementPlusStyleDeps(), + // 流程设计器与预览器为路由懒加载,首启预构建,避免进入页面时重优化刷新 "bpmn-js/lib/Modeler", "bpmn-js/lib/NavigatedViewer", "bpmn-js/lib/util/ModelUtil", @@ -285,3 +184,28 @@ export default defineConfig(({ mode, command, isPreview }: ConfigEnv): UserConfi }, }; }); + +// 按组件目录枚举 Element Plus 样式入口,避免手写清单漏项后运行期补优化并刷新页面 +function getElementPlusStyleDeps(): string[] { + const componentsDir = resolve(import.meta.dirname, "node_modules/element-plus/es/components"); + if (!existsSync(componentsDir)) return []; + return readdirSync(componentsDir, { withFileTypes: true }) + .filter( + (entry) => + entry.isDirectory() && existsSync(resolve(componentsDir, entry.name, "style/index.mjs")) + ) + .map((entry) => `element-plus/es/components/${entry.name}/style/index`); +} + +// 生成 version.json,供前端检测版本更新 +const distVersionPlugin: Plugin = { + name: "dist-version", + apply: "build", + generateBundle() { + this.emitFile({ + type: "asset", + fileName: "version.json", + source: JSON.stringify({ version: __APP_INFO__.buildTimestamp }), + }); + }, +};