diff --git a/.husky/commit-msg b/.husky/commit-msg index fd2bf708..f6f2ce3f 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1 +1,2 @@ -npx --no-install commitlint --edit $1 +#!/usr/bin/env sh +npx --no-install commitlint --edit "$1" diff --git a/.husky/pre-commit b/.husky/pre-commit index beabb122..e77b172c 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,2 @@ -npm run lint:lint-staged +#!/usr/bin/env sh +npm run lint:lint-staged diff --git a/README.md b/README.md index f024ca69..94cf9cb4 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ ![](https://foruda.gitee.com/images/1708618984641188532/a7cca095_716974.png "rainbow.png")
- 🖥️ 在线预览 | 📲 移动端预览 | 📑 阅读文档| 🌐 官网 | 💬 English + 🖥️ 在线预览 | 📲 移动端预览 | 📑 阅读文档| 🌐 官网 | 💬 English
## 项目简介 @@ -100,7 +100,7 @@ | -------- | ---------------------------------------------------------------------------------------------------------------------------------- | | 视频教程 | [https://www.bilibili.com/video/BV1eFUuYyEFj](https://www.bilibili.com/video/BV1eFUuYyEFj) | | 项目搭建 | [基于 Vue3 + Vite + TypeScript + Element-Plus 从0到1搭建后台管理系统](https://blog.csdn.net/u013737132/article/details/130191394) | -| 官方文档 | [https://www.youlai.tech/vue3-element-admin](https://www.youlai.tech/vue3-element-admin/) | +| 官方文档 | [https://www.youlai.tech/docs/web/](https://www.youlai.tech/docs/web/) | | 代码规范 | [ESLint V9 + Prettier + Stylelint + EditorConfig 约束和统一前端代码规范](https://youlai.blog.csdn.net/article/details/145608723) | | 提交规范 | [Husky + Lint-staged + Commitlint + Commitizen + cz-git 配置 Git 提交规范](https://youlai.blog.csdn.net/article/details/145615236) | | 接口文档 | [https://www.apifox.cn](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5) | @@ -177,7 +177,7 @@ server { 1. 获取基于 `Java` 和 `SpringBoot` 开发的后端 [youlai-boot](https://gitee.com/youlaiorg/youlai-boot.git) 源码。 2. 根据后端工程的说明文档 [README.md](https://gitee.com/youlaiorg/youlai-boot#%E9%A1%B9%E7%9B%AE%E8%BF%90%E8%A1%8C) 完成本地启动。 -3. 修改 `.env.development` 文件中的 `VITE_APP_API_URL` 的值,将其从 https://api.youlai.tech 更改为 http://localhost:8989 即可。 +3. 修改 `.env.development` 文件中的 `VITE_APP_API_URL` 的值,将其从 https://api.youlai.tech 更改为 http://localhost:8000 即可。 ## 注意事项 @@ -223,7 +223,7 @@ Thanks to all the contributors! diff --git a/eslint.config.ts b/eslint.config.ts index a8753377..4c32ca69 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -107,8 +107,8 @@ export default [ }, rules: { // 基础规则 - "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", - "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", + "no-console": ["error", { allow: ["warn", "error", "debug"] }], + "no-debugger": "error", // ES6+ 规则 "prefer-const": "error", @@ -116,7 +116,7 @@ export default [ "object-shorthand": "error", // 最佳实践 - eqeqeq: "off", + eqeqeq: ["error", "always", { null: "ignore" }], "no-multi-spaces": "error", "no-multiple-empty-lines": ["error", { max: 1, maxBOF: 0, maxEOF: 0 }], @@ -171,7 +171,7 @@ export default [ }, ], "vue/component-name-in-template-casing": ["error", "PascalCase"], - "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-explicit-any": "warn", }, }, @@ -189,7 +189,7 @@ export default [ }, rules: { // TypeScript 规则 - "@typescript-eslint/no-explicit-any": "off", // 允许使用any类型,方便开发 + "@typescript-eslint/no-explicit-any": "warn", // 逐步收敛 any 类型 "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-object-type": "off", "@typescript-eslint/ban-ts-comment": "off", diff --git a/mock/dict.mock.ts b/mock/dict.mock.ts index 131709b4..5452f658 100644 --- a/mock/dict.mock.ts +++ b/mock/dict.mock.ts @@ -145,7 +145,7 @@ export default defineMock([ let list = null; - if (dictCode == "gender") { + if (dictCode === "gender") { list = [ { value: "1", @@ -160,7 +160,7 @@ export default defineMock([ label: "保密", }, ]; - } else if (dictCode == "notice_level") { + } else if (dictCode === "notice_level") { list = [ { value: "L", @@ -178,7 +178,7 @@ export default defineMock([ tagType: "D", }, ]; - } else if (dictCode == "notice_type") { + } else if (dictCode === "notice_type") { list = [ { value: "1", diff --git a/src/App.vue b/src/App.vue index dca8b3a9..bc6d8b0f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,5 @@ diff --git a/src/composables/sse/useSse.ts b/src/composables/sse/useSse.ts index 700b4592..9fa5ae47 100644 --- a/src/composables/sse/useSse.ts +++ b/src/composables/sse/useSse.ts @@ -10,7 +10,13 @@ export interface UseSseOptions { maxReconnectAttempts?: number; // 最大重试次数,超过后停止重连 } -type EventHandler = (data: any) => void; +type EventHandler = (data: unknown) => void; + +type SseParseState = { + currentEvent: string; + currentData: string; + buffer: string; +}; export enum SseConnectionState { DISCONNECTED = "DISCONNECTED", // 未连接 @@ -46,9 +52,14 @@ function createSseConnection(options: UseSseOptions = {}) { const eventHandlers = new Map>(); - const log = (...args: any[]) => console.log("[SSE]", ...args); - const logError = (...args: any[]) => console.error("[SSE]", ...args); + const log = (...args: unknown[]) => { + if (config.debug) { + console.debug("[SSE]", ...args); + } + }; + const logError = (...args: unknown[]) => console.error("[SSE]", ...args); + // 清理定时器并返回空值 const clearTimer = (timer: typeof connectionTimeoutTimer) => { if (timer) { clearTimeout(timer); @@ -57,6 +68,74 @@ function createSseConnection(options: UseSseOptions = {}) { return timer; }; + // 重置重连计数和间隔 + const resetReconnectState = () => { + reconnectAttempts = 0; + currentReconnectInterval = config.reconnectInterval; + }; + + // 更新下一次重连间隔 + const advanceReconnectState = () => { + currentReconnectInterval = Math.min(currentReconnectInterval * 2, config.maxReconnectInterval); + }; + + // 分发一条完整的 SSE 事件 + const flushSseEvent = (eventName: string, data: string) => { + if (!data) return; + const handlers = eventHandlers.get(eventName); + if (handlers) { + try { + const parsed = JSON.parse(data); + handlers.forEach((handler) => handler(parsed)); + } catch { + handlers.forEach((handler) => handler(data)); + } + } + log(`收到事件[${eventName}]:`, data); + }; + + // 解析单行 SSE 文本并更新当前事件状态 + const handleSseLine = (line: string, state: SseParseState) => { + if (line.startsWith(":")) return; + if (line.startsWith("event:")) { + state.currentEvent = line.slice(6).trim() || "message"; + return; + } + if (line.startsWith("data:")) { + const dataLine = line.slice(5).trim(); + state.currentData = state.currentData ? `${state.currentData}\n${dataLine}` : dataLine; + return; + } + if (line === "") { + flushSseEvent(state.currentEvent, state.currentData); + state.currentEvent = "message"; + state.currentData = ""; + } + }; + + // 持续读取 SSE 流并按行解析 + const consumeSseStream = async (streamReader: ReadableStreamDefaultReader) => { + const decoder = new TextDecoder(); + const state: SseParseState = { currentEvent: "message", currentData: "", buffer: "" }; + + while (true) { + const { done, value } = await streamReader.read(); + if (done) { + connectionState.value = SseConnectionState.DISCONNECTED; + log("SSE 连接已关闭"); + return; + } + + state.buffer += decoder.decode(value, { stream: true }); + const lines = state.buffer.split("\n"); + state.buffer = lines.pop() || ""; + + for (const line of lines) { + handleSseLine(line, state); + } + } + }; + // 指数退避重连 const scheduleReconnect = () => { if (isManualDisconnect) return; @@ -70,13 +149,11 @@ function createSseConnection(options: UseSseOptions = {}) { reconnectTimer = setTimeout(() => { connect(); - currentReconnectInterval = Math.min( - currentReconnectInterval * 2, - config.maxReconnectInterval - ); + advanceReconnectState(); }, currentReconnectInterval); }; + // 建立 SSE 连接 const connect = () => { isManualDisconnect = false; @@ -122,66 +199,17 @@ function createSseConnection(options: UseSseOptions = {}) { } connectionTimeoutTimer = clearTimer(connectionTimeoutTimer); connectionState.value = SseConnectionState.CONNECTED; - reconnectAttempts = 0; - currentReconnectInterval = config.reconnectInterval; + resetReconnectState(); log("SSE 连接已建立"); return response.body?.getReader(); }) - .then((r) => { - if (!r) return; - reader = r; - const decoder = new TextDecoder(); - let buffer = ""; - let currentEvent = "message"; - let currentData = ""; - - // SSE 文本协议解析:event / data / 空行分隔 - const processChunk = ({ - done, - value, - }: ReadableStreamReadResult): Promise | void => { - if (done) { - connectionState.value = SseConnectionState.DISCONNECTED; - log("SSE 连接已关闭"); - return; - } - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - - for (const line of lines) { - if (line.startsWith(":")) continue; - if (line.startsWith("event:")) { - currentEvent = line.slice(6).trim(); - } else if (line.startsWith("data:")) { - const dataLine = line.slice(5).trim(); - currentData = currentData ? `${currentData}\n${dataLine}` : dataLine; - } else if (line === "") { - if (currentData) { - const handlers = eventHandlers.get(currentEvent); - if (handlers) { - try { - const data = JSON.parse(currentData); - handlers.forEach((h) => h(data)); - } catch { - handlers.forEach((h) => h(currentData)); - } - } - log(`收到事件[${currentEvent}]:`, currentData); - } - currentEvent = "message"; - currentData = ""; - } - } - - return reader!.read().then(processChunk); - }; - - return reader.read().then(processChunk); + .then((streamReader) => { + if (!streamReader) return; + reader = streamReader; + return consumeSseStream(streamReader); }) - .catch((err) => { - if (err.name === "AbortError") { + .catch((err: unknown) => { + if (err instanceof Error && err.name === "AbortError") { log("SSE 连接已主动断开"); } else { logError("SSE 连接错误:", err); @@ -192,17 +220,18 @@ function createSseConnection(options: UseSseOptions = {}) { }; // 订阅事件,返回取消函数 - const on = (eventName: string, handler: EventHandler): (() => void) => { + const on = (eventName: string, handler: (data: T) => void): (() => void) => { if (!eventHandlers.has(eventName)) { eventHandlers.set(eventName, new Set()); } - eventHandlers.get(eventName)!.add(handler); + const wrappedHandler: EventHandler = (data) => handler(data as T); + eventHandlers.get(eventName)!.add(wrappedHandler); log(`已订阅事件: ${eventName}`); return () => { const handlers = eventHandlers.get(eventName); if (handlers) { - handlers.delete(handler); + handlers.delete(wrappedHandler); if (handlers.size === 0) { eventHandlers.delete(eventName); } diff --git a/src/main.ts b/src/main.ts index 4e19947c..ee2144eb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,6 +20,7 @@ import "animate.css"; import { setupDirective } from "@/directives"; import { setupRouter } from "@/router"; import { setupStore } from "@/stores"; +import { setupI18n } from "@/lang"; // ===== 全局组件 ===== import * as ElementPlusIcons from "@element-plus/icons-vue"; @@ -40,6 +41,7 @@ const app = createApp(App); // 1️⃣ 核心配置 setupDirective(app); +setupI18n(app); setupRouter(app); setupStore(app); diff --git a/src/types/global.d.ts b/src/types/global.d.ts index d97cfaf4..cfe67152 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -4,7 +4,7 @@ * @deprecated 请使用 @/types 下的具名导出 */ declare global { - type ApiResponse = import("@/types/api").ApiResponse; + type ApiResponse = import("@/types/api").ApiResponse; type BaseQueryParams = import("@/types/api").BaseQueryParams; type PageResult = import("@/types/api").PageResult; type OptionItem = import("@/types/api").OptionItem; diff --git a/src/types/ui/tagsview.ts b/src/types/ui/tagsview.ts index 5cc68ce8..e511bb57 100644 --- a/src/types/ui/tagsview.ts +++ b/src/types/ui/tagsview.ts @@ -1,3 +1,5 @@ +import type { LocationQuery } from "vue-router"; + /** * 标签页相关类型定义 */ @@ -10,5 +12,5 @@ export interface TagView { icon?: string; affix?: boolean; keepAlive?: boolean; - query?: any; + query?: LocationQuery; } diff --git a/src/utils/storage.ts b/src/utils/storage.ts index acb3c9a4..511a7cf0 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -15,7 +15,7 @@ export class Storage { /** * 存储数据到 localStorage */ - static set(key: string, value: any): void { + static set(key: string, value: unknown): void { localStorage.setItem(key, JSON.stringify(value)); } @@ -46,7 +46,7 @@ export class Storage { /** * 存储数据到 sessionStorage */ - static sessionSet(key: string, value: any): void { + static sessionSet(key: string, value: unknown): void { sessionStorage.setItem(key, JSON.stringify(value)); } diff --git a/src/views/codegen/components/FieldConfigStep.vue b/src/views/codegen/components/FieldConfigStep.vue index 14efe035..61e2df52 100644 --- a/src/views/codegen/components/FieldConfigStep.vue +++ b/src/views/codegen/components/FieldConfigStep.vue @@ -111,7 +111,7 @@
- 公众号「有来技术」
+ 公众号「有来技术」
公众号「有来技术」