diff --git a/.env.development b/.env.development index d18552f6..48cb0df2 100644 --- a/.env.development +++ b/.env.development @@ -6,14 +6,12 @@ VITE_APP_TITLE=vue3-element-admin VITE_APP_BASE_API=/dev-api # 接口地址 -VITE_APP_API_URL=https://api.youlai.tech # 线上 +# VITE_APP_API_URL=https://api.youlai.tech # 线上 # VITE_APP_API_URL=https://api.youlai.tech/v2 # 线上(多租户) -# VITE_APP_API_URL=http://localhost:8000 # 本地 +VITE_APP_API_URL=http://localhost:8000 # 本地 -# WebSocket 端点(不配置则关闭) -# 线上: ws://api.youlai.tech/ws -# 本地: ws://localhost:8000/ws -VITE_APP_WS_ENDPOINT= +# SSE 端点(默认 /api/v1/sse/connect) +# VITE_APP_SSE_ENDPOINT=/api/v1/sse/connect # 启用 Mock 服务(true:开启 false:关闭) diff --git a/.env.production b/.env.production index 79be054b..6c07178a 100644 --- a/.env.production +++ b/.env.production @@ -5,8 +5,8 @@ VITE_APP_BASE_API = '/prod-api' # 项目名称 VITE_APP_TITLE=vue3-element-admin -# WebSocket 端点(可选) -#VITE_APP_WS_ENDPOINT=wss://api.youlai.tech/ws +# SSE 端点(使用 VITE_APP_BASE_API 前缀自动拼接) +# 示例:/prod-api/api/v1/sse/connect # ============================================ # 🎛️ 功能开关 diff --git a/.stylelintrc.cjs b/.stylelintrc.cjs index 5eb86831..37009e9a 100644 --- a/.stylelintrc.cjs +++ b/.stylelintrc.cjs @@ -19,6 +19,13 @@ module.exports = { files: ["**/*.{css,scss}"], customSyntax: "postcss-scss", }, + { + // :export 是 CSS Modules 导出语法,禁用属性检查 + files: ["**/variables.module.scss"], + rules: { + "property-no-unknown": null, + }, + }, ], rules: { "prettier/prettier": true, // 强制执行 Prettier 格式化规则(需配合 .prettierrc 配置文件) diff --git a/mock/log.mock.ts b/mock/log.mock.ts index 5943b6e9..de0e6fd8 100644 --- a/mock/log.mock.ts +++ b/mock/log.mock.ts @@ -164,4 +164,43 @@ export default defineMock([ msg: "一切ok", }, }, + { + url: "logs/visits/trend", + method: ["GET"], + body: { + code: "00000", + data: { + dates: [ + "2024-06-30", + "2024-07-01", + "2024-07-02", + "2024-07-03", + "2024-07-04", + "2024-07-05", + "2024-07-06", + "2024-07-07", + ], + pvList: [1751, 5168, 4882, 5301, 4721, 4885, 1901, 1003], + uvList: null, + ipList: [207, 566, 565, 631, 579, 496, 222, 152], + }, + msg: "一切ok", + }, + }, + { + url: "logs/visits/overview", + method: ["GET"], + body: { + code: "00000", + data: { + todayUvCount: 169, + totalUvCount: 19985, + uvGrowthRate: -0.57, + todayPvCount: 1629, + totalPvCount: 286086, + pvGrowthRate: -0.65, + }, + msg: "一切ok", + }, + }, ]); diff --git a/mock/statistics.mock.ts b/mock/statistics.mock.ts deleted file mode 100644 index ab360dde..00000000 --- a/mock/statistics.mock.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { defineMock } from "./base"; - -export default defineMock([ - { - url: "statistics/visits/trend", - method: ["GET"], - body: { - code: "00000", - data: { - dates: [ - "2024-06-30", - "2024-07-01", - "2024-07-02", - "2024-07-03", - "2024-07-04", - "2024-07-05", - "2024-07-06", - "2024-07-07", - ], - pvList: [1751, 5168, 4882, 5301, 4721, 4885, 1901, 1003], - uvList: null, - ipList: [207, 566, 565, 631, 579, 496, 222, 152], - }, - msg: "一切ok", - }, - }, - { - url: "statistics/visits/overview", - method: ["GET"], - body: { - code: "00000", - data: { - todayUvCount: 169, - totalUvCount: 19985, - uvGrowthRate: -0.57, - todayPvCount: 1629, - totalPvCount: 286086, - pvGrowthRate: -0.65, - }, - msg: "一切ok", - }, - }, -]); diff --git a/package.json b/package.json index 7f56bce6..78daa88c 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,6 @@ "preview": "vite preview", "build-only": "vite build", "type-check": "vue-tsc --noEmit", - "test": "vitest", - "test:ui": "vitest --ui", - "test:run": "vitest run", - "test:coverage": "vitest run --coverage", "lint:eslint": "eslint --cache \"src/**/*.{vue,ts,js}\" --fix", "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,css,scss,vue,html,md}\"", "lint:stylelint": "stylelint --cache \"**/*.{css,scss,vue}\" --fix", @@ -51,7 +47,6 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.3.2", - "@stomp/stompjs": "^7.3.0", "@vueuse/core": "^14.2.1", "@wangeditor-next/editor": "^5.6.49", "@wangeditor-next/editor-for-vue": "^5.1.14", @@ -80,8 +75,6 @@ "@commitlint/config-conventional": "^20.5.0", "@eslint/js": "^10.0.1", "@iconify/utils": "^3.1.0", - "@testing-library/user-event": "^14.6.1", - "@testing-library/vue": "^8.1.0", "@types/codemirror": "^5.60.17", "@types/lodash-es": "^4.17.12", "@types/node": "^25.5.0", @@ -92,8 +85,6 @@ "@typescript-eslint/eslint-plugin": "^8.57.0", "@typescript-eslint/parser": "^8.57.0", "@vitejs/plugin-vue": "^6.0.5", - "@vitest/ui": "^4.1.0", - "@vue/test-utils": "^2.4.6", "autoprefixer": "^10.4.27", "commitizen": "^4.3.1", "cz-git": "^1.12.0", @@ -102,7 +93,6 @@ "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-vue": "^10.8.0", "globals": "^17.4.0", - "happy-dom": "^20.8.4", "husky": "^9.1.7", "lint-staged": "^16.4.0", "postcss": "^8.5.8", @@ -125,7 +115,6 @@ "unplugin-vue-components": "^31.0.0", "vite": "^8.0.0", "vite-plugin-mock-dev-server": "^2.1.0", - "vitest": "^4.1.0", "vue-eslint-parser": "^10.4.0", "vue-tsc": "^3.2.5" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 011307c2..14413e68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ dependencies: '@element-plus/icons-vue': specifier: ^2.3.2 version: 2.3.2(vue@3.5.30) - '@stomp/stompjs': - specifier: ^7.3.0 - version: 7.3.0 '@vueuse/core': specifier: ^14.2.1 version: 14.2.1(vue@3.5.30) @@ -91,12 +88,6 @@ devDependencies: '@iconify/utils': specifier: ^3.1.0 version: 3.1.0 - '@testing-library/user-event': - specifier: ^14.6.1 - version: 14.6.1(@testing-library/dom@10.4.1) - '@testing-library/vue': - specifier: ^8.1.0 - version: 8.1.0(vue@3.5.30) '@types/codemirror': specifier: ^5.60.17 version: 5.60.17 @@ -127,12 +118,6 @@ devDependencies: '@vitejs/plugin-vue': specifier: ^6.0.5 version: 6.0.5(vite@8.0.0)(vue@3.5.30) - '@vitest/ui': - specifier: ^4.1.0 - version: 4.1.0(vitest@4.1.0) - '@vue/test-utils': - specifier: ^2.4.6 - version: 2.4.6 autoprefixer: specifier: ^10.4.27 version: 10.4.27(postcss@8.5.8) @@ -157,9 +142,6 @@ devDependencies: globals: specifier: ^17.4.0 version: 17.4.0 - happy-dom: - specifier: ^20.8.4 - version: 20.8.4 husky: specifier: ^9.1.7 version: 9.1.7 @@ -226,9 +208,6 @@ devDependencies: vite-plugin-mock-dev-server: specifier: ^2.1.0 version: 2.1.0(vite@8.0.0) - vitest: - specifier: ^4.1.0 - version: 4.1.0(@types/node@25.5.0)(@vitest/ui@4.1.0)(happy-dom@20.8.4)(vite@8.0.0) vue-eslint-parser: specifier: ^10.4.0 version: 10.4.0(eslint@10.0.3) @@ -283,6 +262,7 @@ packages: /@babel/runtime@7.28.6: resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} + dev: false /@babel/types@7.29.0: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} @@ -767,18 +747,6 @@ packages: engines: {node: '>= 16'} dev: false - /@isaacs/cliui@8.0.2: - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - dependencies: - string-width: 5.1.2 - string-width-cjs: /string-width@4.2.3 - strip-ansi: 7.2.0 - strip-ansi-cjs: /strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: /wrap-ansi@7.0.0 - dev: true - /@jridgewell/gen-mapping@0.3.13: resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} dependencies: @@ -862,10 +830,6 @@ packages: fastq: 1.20.1 dev: true - /@one-ini/wasm@0.1.1: - resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} - dev: true - /@oxc-parser/binding-android-arm-eabi@0.115.0: resolution: {integrity: sha512-VoB2rhgoqgYf64d6Qs5emONQW8ASiTc0xp+aUE4JUhxjX+0pE3gblTYDO0upcN5vt9UlBNmUhAwfSifkfre7nw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1209,13 +1173,6 @@ packages: resolution: {integrity: sha512-rVmmTdeQs+gdk5XboXG7gv4LSLnCceZ9l9Z1v/P+zScOpwPYn6mSVukPtRC22234rXC/13AZV2gZ3ZDvNmP9XA==} dev: true - /@pkgjs/parseargs@0.11.0: - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - requiresBuild: true - dev: true - optional: true - /@pkgr/core@0.2.9: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -1392,71 +1349,10 @@ packages: engines: {node: '>=18'} dev: true - /@standard-schema/spec@1.1.0: - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - dev: true - - /@stomp/stompjs@7.3.0: - resolution: {integrity: sha512-nKMLoFfJhrQAqkvvKd1vLq/cVBGCMwPRCD0LqW7UT1fecRx9C3GoKEIR2CYwVuErGeZu8w0kFkl2rlhPlqHVgQ==} - dev: false - /@sxzz/popperjs-es@2.11.8: resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==} dev: false - /@testing-library/dom@10.4.1: - resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} - engines: {node: '>=18'} - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.28.6 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - picocolors: 1.1.1 - pretty-format: 27.5.1 - dev: true - - /@testing-library/dom@9.3.4: - resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} - engines: {node: '>=14'} - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.28.6 - '@types/aria-query': 5.0.4 - aria-query: 5.1.3 - chalk: 4.1.2 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - pretty-format: 27.5.1 - dev: true - - /@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1): - resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@testing-library/dom': '>=7.21.4' - dependencies: - '@testing-library/dom': 10.4.1 - dev: true - - /@testing-library/vue@8.1.0(vue@3.5.30): - resolution: {integrity: sha512-ls4RiHO1ta4mxqqajWRh8158uFObVrrtAPoxk7cIp4HrnQUj/ScKzqz53HxYpG3X6Zb7H2v+0eTGLSoy8HQ2nA==} - engines: {node: '>=14'} - peerDependencies: - '@vue/compiler-sfc': '>= 3' - vue: '>= 3' - peerDependenciesMeta: - '@vue/compiler-sfc': - optional: true - dependencies: - '@babel/runtime': 7.28.6 - '@testing-library/dom': 9.3.4 - '@vue/test-utils': 2.4.6 - vue: 3.5.30(typescript@5.9.3) - dev: true - /@transloadit/prettier-bytes@0.0.7: resolution: {integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==} dev: false @@ -1469,27 +1365,12 @@ packages: dev: true optional: true - /@types/aria-query@5.0.4: - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - dev: true - - /@types/chai@5.2.3: - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - dev: true - /@types/codemirror@5.60.17: resolution: {integrity: sha512-AZq2FIsUHVMlp7VSe2hTfl5w4pcUkoFkM3zVsRKsn1ca8CXRDYvnin04+HP2REkwsxemuHqvDofdlhUWNpbwfw==} dependencies: '@types/tern': 0.23.9 dev: true - /@types/deep-eql@4.0.2: - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - dev: true - /@types/esrecurse@4.3.1: resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} dev: true @@ -1552,16 +1433,6 @@ packages: /@types/web-bluetooth@0.0.21: resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} - /@types/whatwg-mimetype@3.0.2: - resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} - dev: true - - /@types/ws@8.18.1: - resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - dependencies: - '@types/node': 25.5.0 - dev: true - /@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0)(eslint@10.0.3)(typescript@5.9.3): resolution: {integrity: sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1932,83 +1803,6 @@ packages: vue: 3.5.30(typescript@5.9.3) dev: true - /@vitest/expect@4.1.0: - resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.0 - '@vitest/utils': 4.1.0 - chai: 6.2.2 - tinyrainbow: 3.1.0 - dev: true - - /@vitest/mocker@4.1.0(vite@8.0.0): - resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - dependencies: - '@vitest/spy': 4.1.0 - estree-walker: 3.0.3 - magic-string: 0.30.21 - vite: 8.0.0(@types/node@25.5.0)(sass@1.98.0)(terser@5.46.0) - dev: true - - /@vitest/pretty-format@4.1.0: - resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} - dependencies: - tinyrainbow: 3.1.0 - dev: true - - /@vitest/runner@4.1.0: - resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} - dependencies: - '@vitest/utils': 4.1.0 - pathe: 2.0.3 - dev: true - - /@vitest/snapshot@4.1.0: - resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} - dependencies: - '@vitest/pretty-format': 4.1.0 - '@vitest/utils': 4.1.0 - magic-string: 0.30.21 - pathe: 2.0.3 - dev: true - - /@vitest/spy@4.1.0: - resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} - dev: true - - /@vitest/ui@4.1.0(vitest@4.1.0): - resolution: {integrity: sha512-sTSDtVM1GOevRGsCNhp1mBUHKo9Qlc55+HCreFT4fe99AHxl1QQNXSL3uj4Pkjh5yEuWZIx8E2tVC94nnBZECQ==} - peerDependencies: - vitest: 4.1.0 - dependencies: - '@vitest/utils': 4.1.0 - fflate: 0.8.2 - flatted: 3.4.0 - pathe: 2.0.3 - sirv: 3.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.1.0 - vitest: 4.1.0(@types/node@25.5.0)(@vitest/ui@4.1.0)(happy-dom@20.8.4)(vite@8.0.0) - dev: true - - /@vitest/utils@4.1.0: - resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} - dependencies: - '@vitest/pretty-format': 4.1.0 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 - dev: true - /@volar/language-core@2.4.28: resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} dependencies: @@ -2168,13 +1962,6 @@ packages: /@vue/shared@3.5.30: resolution: {integrity: sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==} - /@vue/test-utils@2.4.6: - resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} - dependencies: - js-beautify: 1.15.4 - vue-component-type-helpers: 2.2.12 - dev: true - /@vueuse/core@12.0.0(typescript@5.9.3): resolution: {integrity: sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==} dependencies: @@ -2401,11 +2188,6 @@ packages: snabbdom: 3.6.3 dev: false - /abbrev@2.0.0: - resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - /acorn-jsx@5.3.2(acorn@8.16.0): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2483,11 +2265,6 @@ packages: color-convert: 2.0.1 dev: true - /ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - dev: true - /ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -2547,26 +2324,6 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true - /aria-query@5.1.3: - resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} - dependencies: - deep-equal: 2.2.3 - dev: true - - /aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - dependencies: - dequal: 2.0.3 - dev: true - - /array-buffer-byte-length@1.0.2: - resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.4 - is-array-buffer: 3.0.5 - dev: true - /array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} dev: true @@ -2575,11 +2332,6 @@ packages: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} dev: true - /assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - dev: true - /ast-kit@2.2.0: resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} engines: {node: '>=20.19.0'} @@ -2633,13 +2385,6 @@ packages: postcss-value-parser: 4.2.0 dev: true - /available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - dependencies: - possible-typed-array-names: 1.1.0 - dev: true - /axios@1.13.6: resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} dependencies: @@ -2708,6 +2453,7 @@ packages: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} dependencies: balanced-match: 1.0.2 + dev: false /brace-expansion@5.0.4: resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} @@ -2791,16 +2537,6 @@ packages: es-errors: 1.3.0 function-bind: 1.1.2 - /call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} - engines: {node: '>= 0.4'} - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - dev: true - /call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -2817,11 +2553,6 @@ packages: resolution: {integrity: sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==} dev: true - /chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} - engines: {node: '>=18'} - dev: true - /chainsaw@0.1.0: resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} dependencies: @@ -2971,11 +2702,6 @@ packages: delayed-stream: 1.0.0 dev: false - /commander@10.0.1: - resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} - engines: {node: '>=14'} - dev: true - /commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -3039,13 +2765,6 @@ packages: /confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} - /config-chain@1.1.13: - resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} - dependencies: - ini: 1.3.8 - proto-list: 1.2.4 - dev: true - /consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -3078,10 +2797,6 @@ packages: meow: 13.2.0 dev: true - /convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - dev: true - /copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} @@ -3226,30 +2941,6 @@ packages: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} dev: true - /deep-equal@2.2.3: - resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} - engines: {node: '>= 0.4'} - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.8 - es-get-iterator: 1.1.3 - get-intrinsic: 1.3.0 - is-arguments: 1.2.0 - is-array-buffer: 3.0.5 - is-date-object: 1.1.0 - is-regex: 1.2.1 - is-shared-array-buffer: 1.0.4 - isarray: 2.0.5 - object-is: 1.1.6 - object-keys: 1.1.1 - object.assign: 4.1.7 - regexp.prototype.flags: 1.5.4 - side-channel: 1.1.0 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.20 - dev: true - /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -3260,24 +2951,6 @@ packages: clone: 1.0.4 dev: true - /define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - dev: true - - /define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - dev: true - /defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} dev: true @@ -3292,11 +2965,6 @@ packages: engines: {node: '>= 0.8'} dev: true - /dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - dev: true - /destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} dev: true @@ -3328,10 +2996,6 @@ packages: resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} dev: false - /dom-accessibility-api@0.5.16: - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - dev: true - /dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} dependencies: @@ -3394,10 +3058,6 @@ packages: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} dev: true - /eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - dev: true - /echarts@6.0.0: resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==} dependencies: @@ -3405,17 +3065,6 @@ packages: zrender: 6.0.0 dev: false - /editorconfig@1.0.7: - resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} - engines: {node: '>=14'} - hasBin: true - dependencies: - '@one-ini/wasm': 0.1.1 - commander: 10.0.1 - minimatch: 9.0.9 - semver: 7.7.4 - dev: true - /electron-to-chromium@1.5.313: resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==} dev: true @@ -3452,10 +3101,6 @@ packages: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} dev: true - /emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - dev: true - /end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} dependencies: @@ -3495,24 +3140,6 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - /es-get-iterator@1.1.3: - resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} - dependencies: - call-bind: 1.0.8 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - is-arguments: 1.2.0 - is-map: 2.0.3 - is-set: 2.0.3 - is-string: 1.1.1 - isarray: 2.0.5 - stop-iteration-iterator: 1.1.0 - dev: true - - /es-module-lexer@2.0.0: - resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} - dev: true - /es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -3780,11 +3407,6 @@ packages: homedir-polyfill: 1.0.3 dev: true - /expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} - engines: {node: '>=12.0.0'} - dev: true - /exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -3864,10 +3486,6 @@ packages: dependencies: picomatch: 4.0.3 - /fflate@0.8.2: - resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - dev: true - /figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -3940,10 +3558,6 @@ packages: hookified: 1.15.1 dev: true - /flatted@3.4.0: - resolution: {integrity: sha512-kC6Bb+ooptOIvWj5B63EQWkF0FEnNjV2ZNkLMLZRDDduIiWeFF4iKnslwhiWxjAdbg4NzTNo6h0qLuvFrcx+Sw==} - dev: true - /flatted@3.4.1: resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==} dev: true @@ -3958,21 +3572,6 @@ packages: optional: true dev: false - /for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} - dependencies: - is-callable: 1.2.7 - dev: true - - /foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - dev: true - /form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} @@ -4036,10 +3635,6 @@ packages: /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - /functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - dev: true - /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -4098,19 +3693,6 @@ packages: is-glob: 4.0.3 dev: true - /glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.9 - minipass: 7.1.3 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - dev: true - /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -4200,26 +3782,6 @@ packages: duplexer: 0.1.2 dev: true - /happy-dom@20.8.4: - resolution: {integrity: sha512-GKhjq4OQCYB4VLFBzv8mmccUadwlAusOZOI7hC1D9xDIT5HhzkJK17c4el2f6R6C715P9xB4uiMxeKUa2nHMwQ==} - engines: {node: '>=20.0.0'} - dependencies: - '@types/node': 25.5.0 - '@types/whatwg-mimetype': 3.0.2 - '@types/ws': 8.18.1 - entities: 7.0.1 - whatwg-mimetype: 3.0.0 - ws: 8.19.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: true - - /has-bigints@1.1.0: - resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} - engines: {node: '>= 0.4'} - dev: true - /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -4235,12 +3797,6 @@ packages: engines: {node: '>=12'} dev: true - /has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - dependencies: - es-define-property: 1.0.1 - dev: true - /has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -4250,6 +3806,7 @@ packages: engines: {node: '>= 0.4'} dependencies: has-symbols: 1.1.0 + dev: false /hashery@1.5.0: resolution: {integrity: sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==} @@ -4419,56 +3976,10 @@ packages: wrap-ansi: 7.0.0 dev: true - /internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 - dev: true - - /is-arguments@1.2.0: - resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - dev: true - - /is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - dev: true - /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: true - /is-bigint@1.1.0: - resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} - engines: {node: '>= 0.4'} - dependencies: - has-bigints: 1.1.0 - dev: true - - /is-boolean-object@1.2.2: - resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - dev: true - - /is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - dev: true - /is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} @@ -4476,14 +3987,6 @@ packages: hasown: 2.0.2 dev: true - /is-date-object@1.1.0: - resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - dev: true - /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -4517,19 +4020,6 @@ packages: engines: {node: '>=8'} dev: true - /is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - dev: true - - /is-number-object@1.1.1: - resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - dev: true - /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -4554,45 +4044,6 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} - /is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - dev: true - - /is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - dev: true - - /is-shared-array-buffer@1.0.4: - resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.4 - dev: true - - /is-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - dev: true - - /is-symbol@1.1.1: - resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - dev: true - /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -4606,19 +4057,6 @@ packages: resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==} dev: true - /is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - dev: true - - /is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - dev: true - /is-what@5.5.0: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} @@ -4633,44 +4071,15 @@ packages: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} dev: false - /isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - dev: true - /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true - /jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - dev: true - /jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true dev: true - /js-beautify@1.15.4: - resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} - engines: {node: '>=14'} - hasBin: true - dependencies: - config-chain: 1.1.13 - editorconfig: 1.0.7 - glob: 10.5.0 - js-cookie: 3.0.5 - nopt: 7.2.1 - dev: true - - /js-cookie@3.0.5: - resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} - engines: {node: '>=14'} - dev: true - /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: true @@ -5094,15 +4503,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - dev: true - - /lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - dev: true - /magic-regexp@0.10.0: resolution: {integrity: sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==} dependencies: @@ -5232,13 +4632,6 @@ packages: brace-expansion: 2.0.2 dev: false - /minimatch@9.0.9: - resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} - engines: {node: '>=16 || 14 >=14.17'} - dependencies: - brace-expansion: 2.0.2 - dev: true - /minimist@1.2.7: resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} dev: true @@ -5246,11 +4639,6 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - /minipass@7.1.3: - resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} - engines: {node: '>=16 || 14 >=14.17'} - dev: true - /mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} dev: false @@ -5323,14 +4711,6 @@ packages: resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} dev: true - /nopt@7.2.1: - resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - hasBin: true - dependencies: - abbrev: 2.0.0 - dev: true - /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -5358,31 +4738,6 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - /object-is@1.1.6: - resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - dev: true - - /object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - dev: true - - /object.assign@4.1.7: - resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - has-symbols: 1.1.0 - object-keys: 1.1.1 - dev: true - /obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} dev: true @@ -5497,10 +4852,6 @@ packages: p-limit: 3.1.0 dev: true - /package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - dev: true - /package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} dev: true @@ -5548,14 +4899,6 @@ packages: engines: {node: '>=8'} dev: true - /path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.3 - dev: true - /path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} @@ -5609,11 +4952,6 @@ packages: exsolve: 1.0.8 pathe: 2.0.3 - /possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - dev: true - /postcss-html@1.8.1: resolution: {integrity: sha512-OLF6P7qctfAWayOhLpcVnTGqVeJzu2W3WpIYelfz2+JV5oGxfkcEvweN9U4XpeqE0P98dcD9ssusGwlF0TK0uQ==} engines: {node: ^12 || >=14} @@ -5709,15 +5047,6 @@ packages: hasBin: true dev: true - /pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - dev: true - /prismjs@1.30.0: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} @@ -5727,10 +5056,6 @@ packages: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: false - /proto-list@1.2.4: - resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - dev: true - /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false @@ -5774,10 +5099,6 @@ packages: unpipe: 1.0.0 dev: true - /react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - dev: true - /readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} dependencies: @@ -5818,18 +5139,6 @@ packages: hasBin: true dev: true - /regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - dev: true - /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -5939,15 +5248,6 @@ packages: /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - /safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - dev: true - /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: true @@ -5986,28 +5286,6 @@ packages: hasBin: true dev: true - /set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - dev: true - - /set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - dev: true - /setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} dev: false @@ -6064,10 +5342,6 @@ packages: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - /siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - dev: true - /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true @@ -6167,27 +5441,11 @@ packages: resolution: {integrity: sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==} dev: false - /stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - dev: true - /statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} dev: true - /std-env@4.0.0: - resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} - dev: true - - /stop-iteration-iterator@1.1.0: - resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - internal-slot: 1.1.0 - dev: true - /string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -6202,15 +5460,6 @@ packages: strip-ansi: 6.0.1 dev: true - /string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.2.0 - dev: true - /string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -6503,10 +5752,6 @@ packages: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} dev: false - /tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - dev: true - /tinyexec@1.0.4: resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} @@ -6519,11 +5764,6 @@ packages: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - /tinyrainbow@3.1.0: - resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} - engines: {node: '>=14.0.0'} - dev: true - /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -6928,76 +6168,10 @@ packages: fsevents: 2.3.3 dev: true - /vitest@4.1.0(@types/node@25.5.0)(@vitest/ui@4.1.0)(happy-dom@20.8.4)(vite@8.0.0): - resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.0 - '@vitest/browser-preview': 4.1.0 - '@vitest/browser-webdriverio': 4.1.0 - '@vitest/ui': 4.1.0 - happy-dom: '*' - jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - dependencies: - '@types/node': 25.5.0 - '@vitest/expect': 4.1.0 - '@vitest/mocker': 4.1.0(vite@8.0.0) - '@vitest/pretty-format': 4.1.0 - '@vitest/runner': 4.1.0 - '@vitest/snapshot': 4.1.0 - '@vitest/spy': 4.1.0 - '@vitest/ui': 4.1.0(vitest@4.1.0) - '@vitest/utils': 4.1.0 - es-module-lexer: 2.0.0 - expect-type: 1.3.0 - happy-dom: 20.8.4 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.0.0 - tinybench: 2.9.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.15 - tinyrainbow: 3.1.0 - vite: 8.0.0(@types/node@25.5.0)(sass@1.98.0)(terser@5.46.0) - why-is-node-running: 2.3.0 - transitivePeerDependencies: - - msw - dev: true - /vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} dev: true - /vue-component-type-helpers@2.2.12: - resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} - dev: true - /vue-draggable-plus@0.6.1(@types/sortablejs@1.15.9): resolution: {integrity: sha512-FbtQ/fuoixiOfTZzG3yoPl4JAo9HJXRHmBQZFB9x2NYCh6pq0TomHf7g5MUmpaDYv+LU2n6BPq2YN9sBO+FbIg==} peerDependencies: @@ -7121,45 +6295,6 @@ packages: /webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - /whatwg-mimetype@3.0.0: - resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} - engines: {node: '>=12'} - dev: true - - /which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - dev: true - - /which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - dev: true - - /which-typed-array@1.1.20: - resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - dev: true - /which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -7175,15 +6310,6 @@ packages: isexe: 2.0.0 dev: true - /why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - dev: true - /wildcard@1.1.2: resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==} dev: false @@ -7202,15 +6328,6 @@ packages: strip-ansi: 6.0.1 dev: true - /wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.2.0 - dev: true - /wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} diff --git a/src/components/NoticeDropdown/useNotice.ts b/src/components/NoticeDropdown/useNotice.ts index 6dd184b2..6f1681e9 100644 --- a/src/components/NoticeDropdown/useNotice.ts +++ b/src/components/NoticeDropdown/useNotice.ts @@ -4,13 +4,16 @@ import { ref, onMounted, onBeforeUnmount } from "vue"; import type { NoticeItem, NoticeDetail, NoticeQueryParams } from "@/types/api"; import NoticeAPI from "@/api/system/notice"; -import { useStomp } from "@/composables"; +import { useSse } from "@/composables"; import router from "@/router"; const PAGE_SIZE = 5; +// SSE 事件名称:通知消息 +const NOTICE_EVENT = "notice"; + export function useNotice() { - const { subscribe, unsubscribe, isConnected } = useStomp(); + const { on, isConnected } = useSse(); // 状态 const list = ref([]); @@ -18,7 +21,7 @@ export function useNotice() { const detail = ref(null); const dialogVisible = ref(false); - let subscribed = false; + let unsubscribe: (() => void) | null = null; // ============================================ // 数据获取 @@ -60,15 +63,15 @@ export function useNotice() { } // ============================================ - // WebSocket 订阅 + // SSE 订阅 // ============================================ function setupSubscription() { - if (subscribed || !isConnected.value) return; + if (unsubscribe || !isConnected.value) return; - subscribe("/user/queue/message", (message: any) => { + // 订阅新通知事件 + unsubscribe = on(NOTICE_EVENT, (data: any) => { try { - const data = JSON.parse(message.body || "{}"); if (!data.id) return; // 避免重复 @@ -98,7 +101,21 @@ export function useNotice() { } }); - subscribed = true; + // 订阅撤回通知事件 + on("notice-revoke", (data: any) => { + try { + if (!data.id) return; + + // 从列表中移除已撤回的通知 + const idx = list.value.findIndex((item: NoticeItem) => item.id === data.id); + if (idx >= 0) { + list.value.splice(idx, 1); + if (unreadTotal.value > 0) unreadTotal.value -= 1; + } + } catch (e) { + console.error("处理撤回通知失败", e); + } + }); } // ============================================ @@ -111,8 +128,10 @@ export function useNotice() { }); onBeforeUnmount(() => { - unsubscribe("/user/queue/message"); - subscribed = false; + if (unsubscribe) { + unsubscribe(); + unsubscribe = null; + } }); return { diff --git a/src/composables/index.ts b/src/composables/index.ts index 1cb27b9e..d563c917 100644 --- a/src/composables/index.ts +++ b/src/composables/index.ts @@ -1,7 +1,7 @@ -// WebSocket 服务 -export { setupWebSocket, cleanupWebSocket } from "./websocket"; -export { useStomp, useDictSync, useOnlineCount } from "./websocket"; -export type { DictMessage, DictChangeMessage, DictChangeCallback } from "./websocket"; +// SSE 服务 +export { setupSse, cleanupSseServices } from "./sse"; +export { useSse, useDictSync, useOnlineCount, SseConnectionState } from "./sse"; +export type { DictMessage, DictChangeMessage, DictChangeCallback } from "./sse"; // 表格相关 export { useTableSelection } from "./useTableSelection"; diff --git a/src/composables/websocket/index.ts b/src/composables/sse/index.ts similarity index 62% rename from src/composables/websocket/index.ts rename to src/composables/sse/index.ts index a87c7792..96ca4327 100644 --- a/src/composables/websocket/index.ts +++ b/src/composables/sse/index.ts @@ -1,8 +1,8 @@ /** - * WebSocket 服务统一管理 + * SSE 服务统一管理 * * @description - * 提供 WebSocket 服务的统一初始化和清理接口 + * 提供 SSE 服务的统一初始化和清理接口 * - 字典同步服务 * - 在线用户统计服务 * @@ -11,19 +11,20 @@ import { useDictSync } from "./useDictSync"; import { useOnlineCount } from "./useOnlineCount"; +import { cleanupSse } from "./useSse"; /** - * 初始化所有 WebSocket 服务 + * 初始化所有 SSE 服务 * - * 应在应用启动时调用,统一初始化所有 WebSocket 连接 + * 应在应用启动时调用,统一初始化所有 SSE 连接 * * @example * ```ts * // 在 main.ts 中调用 - * setupWebSocket(); + * setupSse(); * ``` */ -export function setupWebSocket() { +export function setupSse() { // 初始化字典同步服务 const dictSync = useDictSync(); dictSync.initialize(); @@ -34,17 +35,17 @@ export function setupWebSocket() { } /** - * 清理所有 WebSocket 连接 + * 清理所有 SSE 连接 * - * 应在用户登出时调用,释放所有 WebSocket 资源 + * 应在用户登出时调用,释放所有 SSE 资源 * * @example * ```ts * // 在 user store 的 logout 方法中调用 - * cleanupWebSocket(); + * cleanupSseServices(); * ``` */ -export function cleanupWebSocket() { +export function cleanupSseServices() { // 清理字典同步服务 const dictSync = useDictSync(); dictSync.cleanup(); @@ -52,10 +53,13 @@ export function cleanupWebSocket() { // 清理在线用户统计服务 const onlineCount = useOnlineCount(); onlineCount.cleanup(); + + // 清理全局 SSE 实例 + cleanupSse(); } -// 导出所有 WebSocket 相关的 composables +// 导出所有 SSE 相关的 composables export { useDictSync } from "./useDictSync"; export { useOnlineCount } from "./useOnlineCount"; -export { useStomp } from "./useStomp"; +export { useSse, cleanupSse, SseConnectionState } from "./useSse"; export type { DictMessage, DictChangeMessage, DictChangeCallback } from "./useDictSync"; diff --git a/src/composables/sse/useDictSync.ts b/src/composables/sse/useDictSync.ts new file mode 100644 index 00000000..9cd7afba --- /dev/null +++ b/src/composables/sse/useDictSync.ts @@ -0,0 +1,150 @@ +import { useDictStoreHook } from "@/store/modules/dict"; +import { useSse } from "./useSse"; + +/** + * 字典变更消息结构 + */ +export interface DictChangeMessage { + /** 字典编码 */ + dictCode: string; + /** 时间戳 */ + timestamp: number; +} + +/** + * 字典消息别名(向后兼容) + */ +export type DictMessage = DictChangeMessage; + +/** + * 字典变更事件回调函数类型 + */ +export type DictChangeCallback = (message: DictChangeMessage) => void; + +/** + * 全局单例实例 + */ +let singletonInstance: ReturnType | null = null; + +/** + * 创建字典同步组合式函数(内部工厂函数) + */ +function createDictSyncComposable() { + const dictStore = useDictStoreHook(); + const sse = useSse(); + + // 消息回调函数列表 + const messageCallbacks = ref([]); + + // 取消订阅函数 + let unsubscribe: (() => void) | null = null; + + /** + * 处理字典变更事件 + */ + const handleDictChangeMessage = (data: DictChangeMessage) => { + const { dictCode } = data; + + if (!dictCode) { + console.warn("[DictSync] 收到无效的字典变更消息:缺少 dictCode"); + return; + } + + // 清除缓存,等待按需加载 + dictStore.removeDictItem(dictCode); + + // 执行所有注册的回调函数 + messageCallbacks.value.forEach((callback) => { + try { + callback(data); + } catch (error) { + console.error("[DictSync] 回调函数执行失败:", error); + } + }); + }; + + /** + * 初始化 SSE 连接并订阅字典事件 + */ + const initialize = () => { + // 建立 SSE 连接 + sse.connect(); + + // 订阅字典变更事件 + unsubscribe = sse.on("dict", handleDictChangeMessage); + }; + + /** + * 关闭 SSE 连接并清理资源 + */ + const cleanup = () => { + // 取消订阅 + if (unsubscribe) { + unsubscribe(); + unsubscribe = null; + } + + // 清空回调列表 + messageCallbacks.value = []; + }; + + /** + * 注册字典变更回调函数 + * + * @param callback 回调函数 + * @returns 返回一个取消注册的函数 + */ + const onDictChange = (callback: DictChangeCallback) => { + messageCallbacks.value.push(callback); + + // 返回取消注册的函数 + return () => { + const index = messageCallbacks.value.indexOf(callback); + if (index !== -1) { + messageCallbacks.value.splice(index, 1); + } + }; + }; + + return { + // 状态 + isConnected: sse.isConnected, + connectionState: sse.connectionState, + + // 方法 + initialize, + cleanup, + onDictChange, + }; +} + +/** + * 字典同步组合式函数(单例模式) + * + * 用于监听后端字典变更并自动同步到前端缓存 + * + * @example + * ```ts + * const dictSync = useDictSync(); + * + * // 初始化(通常在应用启动时调用) + * dictSync.initialize(); + * + * // 注册回调 + * const unsubscribe = dictSync.onDictChange((message) => { + * console.log('字典已更新:', message.dictCode); + * }); + * + * // 取消注册 + * unsubscribe(); + * + * // 清理(在应用退出时调用) + * dictSync.cleanup(); + * ``` + */ +export function useDictSync() { + if (!singletonInstance) { + singletonInstance = createDictSyncComposable(); + } + return singletonInstance; +} diff --git a/src/composables/sse/useOnlineCount.ts b/src/composables/sse/useOnlineCount.ts new file mode 100644 index 00000000..16a50e45 --- /dev/null +++ b/src/composables/sse/useOnlineCount.ts @@ -0,0 +1,113 @@ +import { ref, onMounted, onUnmounted, getCurrentInstance } from "vue"; +import { useSse } from "./useSse"; + +/** + * 全局单例实例 + */ +let globalInstance: ReturnType | null = null; + +/** + * 创建在线用户计数组合式函数(内部工厂函数) + */ +function createOnlineCountComposable() { + // 状态管理 + const onlineUserCount = ref(0); + const lastUpdateTime = ref(0); + + // SSE 客户端 + const sse = useSse(); + + // 取消订阅函数 + let unsubscribe: (() => void) | null = null; + + /** + * 处理在线用户数量消息 + */ + const handleOnlineCountMessage = (count: number) => { + if (count !== undefined && !isNaN(count)) { + onlineUserCount.value = count; + lastUpdateTime.value = Date.now(); + } + }; + + /** + * 初始化 SSE 连接并订阅在线用户事件 + */ + const initialize = () => { + // 建立 SSE 连接 + sse.connect(); + + // 订阅在线用户计数事件 + unsubscribe = sse.on("online-count", handleOnlineCountMessage); + }; + + /** + * 关闭 SSE 连接并清理资源 + */ + const cleanup = () => { + // 取消订阅 + if (unsubscribe) { + unsubscribe(); + unsubscribe = null; + } + + // 重置状态 + onlineUserCount.value = 0; + lastUpdateTime.value = 0; + }; + + return { + // 状态 + onlineUserCount: readonly(onlineUserCount), + lastUpdateTime: readonly(lastUpdateTime), + isConnected: sse.isConnected, + connectionState: sse.connectionState, + + // 方法 + initialize, + cleanup, + }; +} + +/** + * 在线用户计数组合式函数(单例模式) + * + * 用于实时显示系统在线用户数量 + * + * @example + * ```ts + * // 在组件中使用(推荐) + * const { onlineUserCount, isConnected } = useOnlineCount(); + * + * // 手动控制初始化(高级用法) + * const { onlineUserCount, initialize, cleanup } = useOnlineCount({ autoInit: false }); + * onMounted(() => initialize()); + * onUnmounted(() => cleanup()); + * ``` + */ +export function useOnlineCount(options: { autoInit?: boolean } = {}) { + const { autoInit = true } = options; + + // 获取或创建单例实例 + if (!globalInstance) { + globalInstance = createOnlineCountComposable(); + } + + // 组件级自动初始化(仅在组件上下文中生效) + const instance = getCurrentInstance(); + if (autoInit && instance) { + onMounted(() => { + // 防止重复初始化:只有在未连接时才尝试初始化 + if (!globalInstance!.isConnected.value) { + globalInstance!.initialize(); + } + }); + + // 注意:组件卸载时不关闭连接,保持全局连接 + onUnmounted(() => { + // 全局连接由 cleanupSse() 统一管理 + }); + } + + return globalInstance; +} diff --git a/src/composables/sse/useSse.ts b/src/composables/sse/useSse.ts new file mode 100644 index 00000000..136b4ae2 --- /dev/null +++ b/src/composables/sse/useSse.ts @@ -0,0 +1,269 @@ +import { AuthStorage } from "@/utils/auth"; + +export interface UseSseOptions { + /** SSE 端点 URL,不传时使用环境变量拼接 */ + url?: string; + /** 是否开启调试日志 */ + debug?: boolean; + /** 连接超时时间,单位毫秒,默认为 10000 */ + connectionTimeout?: number; +} + +type EventHandler = (data: any) => void; + +/** + * SSE 连接状态枚举 + */ +export enum SseConnectionState { + DISCONNECTED = "DISCONNECTED", + CONNECTING = "CONNECTING", + CONNECTED = "CONNECTED", +} + +/** + * 全局 SSE 实例管理 + */ +let globalInstance: ReturnType | null = null; + +/** + * 创建 SSE 连接(内部工厂函数) + */ +function createSseConnection(options: UseSseOptions = {}) { + // 使用环境变量拼接 SSE URL:/dev-api/api/v1/sse/connect 或 /prod-api/api/v1/sse/connect + const baseApi = import.meta.env.VITE_APP_BASE_API || "/dev-api"; + const defaultUrl = `${baseApi}/api/v1/sse/connect`; + const config = { + url: options.url ?? defaultUrl, + debug: options.debug ?? false, + connectionTimeout: options.connectionTimeout ?? 10000, + }; + + // 状态管理 + const connectionState = ref(SseConnectionState.DISCONNECTED); + const isConnected = computed(() => connectionState.value === SseConnectionState.CONNECTED); + + // EventSource 实例 + let eventSource: EventSource | null = null; + let isManualDisconnect = false; + let connectionTimeoutTimer: ReturnType | null = null; + + // 事件处理器注册表 + const eventHandlers = new Map>(); + + // 日志 + const log = config.debug ? (...args: any[]) => console.log("[SSE]", ...args) : () => {}; + const logError = (...args: any[]) => console.error("[SSE]", ...args); + + /** + * 清除连接超时定时器 + */ + const clearConnectionTimeout = () => { + if (connectionTimeoutTimer) { + clearTimeout(connectionTimeoutTimer); + connectionTimeoutTimer = null; + } + }; + + /** + * 处理连接打开事件 + */ + const handleOpen = () => { + clearConnectionTimeout(); + connectionState.value = SseConnectionState.CONNECTED; + log("SSE 连接已建立"); + }; + + /** + * 处理连接错误事件 + */ + const handleError = (event: Event) => { + clearConnectionTimeout(); + connectionState.value = SseConnectionState.DISCONNECTED; + logError("SSE 连接错误:", event); + + // 非手动断开时,浏览器会自动重连 + if (!isManualDisconnect && eventSource) { + log("浏览器将自动重连..."); + } + }; + + /** + * 处理消息事件(默认 message 类型) + */ + const handleMessage = (event: MessageEvent) => { + log("收到消息:", event.data); + const handlers = eventHandlers.get("message"); + if (handlers) { + try { + const data = JSON.parse(event.data); + handlers.forEach((handler) => handler(data)); + } catch { + handlers.forEach((handler) => handler(event.data)); + } + } + }; + + /** + * 处理自定义事件 + */ + const handleCustomEvent = (eventName: string) => (event: MessageEvent) => { + log(`收到事件[${eventName}]:`, event.data); + const handlers = eventHandlers.get(eventName); + if (handlers) { + try { + const data = JSON.parse(event.data); + handlers.forEach((handler) => handler(data)); + } catch { + handlers.forEach((handler) => handler(event.data)); + } + } + }; + + /** + * 建立 SSE 连接 + */ + const connect = () => { + // 重置手动断开标志 + isManualDisconnect = false; + + // 检查是否已连接或正在连接 + if (eventSource && connectionState.value === SseConnectionState.CONNECTED) { + log("SSE 已连接,跳过重复连接"); + return; + } + + if (connectionState.value === SseConnectionState.CONNECTING) { + log("SSE 正在连接中,跳过重复连接"); + return; + } + + // 检查 Token + const token = AuthStorage.getAccessToken(); + if (!token) { + log("未检测到有效令牌,跳过 SSE 连接"); + return; + } + + // 设置连接状态 + connectionState.value = SseConnectionState.CONNECTING; + + // 构建 URL(带 Token) + const separator = config.url.includes("?") ? "&" : "?"; + const fullUrl = `${config.url}${separator}token=${encodeURIComponent(token)}`; + + // 创建 EventSource + eventSource = new EventSource(fullUrl); + + // 设置连接超时 + connectionTimeoutTimer = setTimeout(() => { + if (connectionState.value === SseConnectionState.CONNECTING) { + log("SSE 连接超时"); + disconnect(); + } + }, config.connectionTimeout); + + // 注册事件监听器 + eventSource.onopen = handleOpen; + eventSource.onerror = handleError; + eventSource.onmessage = handleMessage; + + log("正在建立 SSE 连接..."); + }; + + /** + * 订阅事件 + * + * @param eventName 事件名称(如 "dict"、"online-count") + * @param handler 事件处理函数 + * @returns 取消订阅的函数 + */ + const on = (eventName: string, handler: EventHandler): (() => void) => { + if (!eventHandlers.has(eventName)) { + eventHandlers.set(eventName, new Set()); + } + eventHandlers.get(eventName)!.add(handler); + + // 如果是自定义事件(非 message),需要注册监听器 + if (eventName !== "message" && eventSource) { + eventSource.addEventListener(eventName, handleCustomEvent(eventName) as EventListener); + } + + log(`已订阅事件: ${eventName}`); + + // 返回取消订阅函数 + return () => { + const handlers = eventHandlers.get(eventName); + if (handlers) { + handlers.delete(handler); + if (handlers.size === 0) { + eventHandlers.delete(eventName); + } + } + log(`已取消订阅事件: ${eventName}`); + }; + }; + + /** + * 断开 SSE 连接 + */ + const disconnect = () => { + isManualDisconnect = true; + clearConnectionTimeout(); + + if (eventSource) { + eventSource.close(); + eventSource = null; + log("SSE 连接已断开"); + } + + connectionState.value = SseConnectionState.DISCONNECTED; + }; + + /** + * 清理资源 + */ + const cleanup = () => { + disconnect(); + eventHandlers.clear(); + log("SSE 资源已清理"); + }; + + return { + // 状态 + connectionState: readonly(connectionState), + isConnected, + + // 方法 + connect, + disconnect, + cleanup, + on, + }; +} + +/** + * SSE 连接组合式函数(单例模式) + */ +export function useSse(options: UseSseOptions = {}) { + if (!globalInstance) { + globalInstance = createSseConnection(options); + } + return globalInstance; +} + +/** + * 获取或创建 SSE 实例(用于外部访问) + */ +export function getSseInstance() { + return globalInstance; +} + +/** + * 清理全局 SSE 实例 + */ +export function cleanupSse() { + if (globalInstance) { + globalInstance.cleanup(); + globalInstance = null; + } +} diff --git a/src/composables/websocket/useDictSync.ts b/src/composables/websocket/useDictSync.ts deleted file mode 100644 index a6cdc740..00000000 --- a/src/composables/websocket/useDictSync.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { useDictStoreHook } from "@/store/modules/dict"; -import { useStomp } from "./useStomp"; -import type { IMessage } from "@stomp/stompjs"; - -/** - * 字典变更消息结构 - */ -export interface DictChangeMessage { - /** 字典编码 */ - dictCode: string; - /** 时间戳 */ - timestamp: number; -} - -/** - * 字典消息别名(向后兼容) - */ -export type DictMessage = DictChangeMessage; - -/** - * 字典变更事件回调函数类型 - */ -export type DictChangeCallback = (message: DictChangeMessage) => void; - -/** - * 全局单例实例 - */ -let singletonInstance: ReturnType | null = null; - -/** - * 创建字典同步组合式函数(内部工厂函数) - */ -function createDictSyncComposable() { - const dictStore = useDictStoreHook(); - - // 使用优化后的 useStomp - const stomp = useStomp({ - reconnectDelay: 20000, - connectionTimeout: 15000, - useExponentialBackoff: false, - maxReconnectAttempts: 3, - autoRestoreSubscriptions: true, // 自动恢复订阅 - debug: false, - }); - - // 字典主题地址 - const DICT_TOPIC = "/topic/dict"; - - // 消息回调函数列表 - const messageCallbacks = ref([]); - - // 订阅 ID(用于取消订阅) - let subscriptionId: string | null = null; - - /** - * 处理字典变更事件 - */ - const handleDictChangeMessage = (message: IMessage) => { - if (!message.body) { - return; - } - - try { - const data = JSON.parse(message.body) as DictChangeMessage; - const { dictCode } = data; - - if (!dictCode) { - console.warn("[DictSync] 收到无效的字典变更消息:缺少 dictCode"); - return; - } - - // 清除缓存,等待按需加载 - dictStore.removeDictItem(dictCode); - - // 执行所有注册的回调函数 - messageCallbacks.value.forEach((callback) => { - try { - callback(data); - } catch (error) { - console.error("[DictSync] 回调函数执行失败:", error); - } - }); - } catch (error) { - console.error("[DictSync] 解析字典变更消息失败:", error); - } - }; - - /** - * 初始化 WebSocket 连接并订阅字典主题 - */ - const initialize = () => { - // 检查是否配置了 WebSocket 端点 - const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT; - if (!wsEndpoint) { - console.log("[DictSync] 未配置 WebSocket 端点,跳过字典同步功能"); - return; - } - - // console.log("[DictSync] 初始化字典同步服务..."); // 高频日志已禁用 - - // 建立 WebSocket 连接 - stomp.connect(); - - // 订阅字典主题(useStomp 会自动处理重连后的订阅恢复) - subscriptionId = stomp.subscribe(DICT_TOPIC, handleDictChangeMessage); - - // if (subscriptionId) { - // console.log(`[DictSync] 已订阅字典主题: ${DICT_TOPIC}`); - // } else { - // console.log(`[DictSync] 暂存字典主题订阅,等待连接建立后自动订阅`); - // } - }; - - /** - * 关闭 WebSocket 连接并清理资源 - */ - const cleanup = () => { - // 取消订阅(如果有的话) - if (subscriptionId) { - stomp.unsubscribe(subscriptionId); - subscriptionId = null; - } - - // 也可以通过主题地址取消订阅 - stomp.unsubscribeDestination(DICT_TOPIC); - - // 断开连接 - stomp.disconnect(); - - // 清空回调列表 - messageCallbacks.value = []; - }; - - /** - * 注册字典变更回调函数 - * - * @param callback 回调函数 - * @returns 返回一个取消注册的函数 - */ - const onDictChange = (callback: DictChangeCallback) => { - messageCallbacks.value.push(callback); - - // 返回取消注册的函数 - return () => { - const index = messageCallbacks.value.indexOf(callback); - if (index !== -1) { - messageCallbacks.value.splice(index, 1); - } - }; - }; - - return { - // 状态 - isConnected: stomp.isConnected, - connectionState: stomp.connectionState, - - // 方法 - initialize, - cleanup, - onDictChange, - }; -} - -/** - * 字典同步组合式函数(单例模式) - * - * 用于监听后端字典变更并自动同步到前端缓存 - * - * @example - * ```ts - * const dictSync = useDictSync(); - * - * // 初始化(通常在应用启动时调用) - * dictSync.initialize(); - * - * // 注册回调 - * const unsubscribe = dictSync.onDictChange((message) => { - * console.log('字典已更新:', message.dictCode); - * }); - * - * // 取消注册 - * unsubscribe(); - * - * // 清理(在应用退出时调用) - * dictSync.cleanup(); - * ``` - */ -export function useDictSync() { - if (!singletonInstance) { - singletonInstance = createDictSyncComposable(); - } - return singletonInstance; -} diff --git a/src/composables/websocket/useOnlineCount.ts b/src/composables/websocket/useOnlineCount.ts deleted file mode 100644 index 5f5734d0..00000000 --- a/src/composables/websocket/useOnlineCount.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { ref, onMounted, onUnmounted, getCurrentInstance } from "vue"; -import { useStomp } from "./useStomp"; -import { AuthStorage } from "@/utils/auth"; - -/** - * 在线用户数量消息结构 - */ -interface OnlineCountMessage { - count?: number; - timestamp?: number; -} - -/** - * 全局单例实例 - */ -let globalInstance: ReturnType | null = null; - -/** - * 创建在线用户计数组合式函数(内部工厂函数) - */ -function createOnlineCountComposable() { - // ==================== 状态管理 ==================== - const onlineUserCount = ref(0); - const lastUpdateTime = ref(0); - - // ==================== WebSocket 客户端 ==================== - const stomp = useStomp({ - reconnectDelay: 15000, - maxReconnectAttempts: 3, - connectionTimeout: 10000, - useExponentialBackoff: true, - autoRestoreSubscriptions: true, // 自动恢复订阅 - debug: false, - }); - - // 在线用户计数主题 - const ONLINE_COUNT_TOPIC = "/topic/online-count"; - - // 订阅 ID - let subscriptionId: string | null = null; - - /** - * 处理在线用户数量消息 - */ - const handleOnlineCountMessage = (message: any) => { - try { - const data = message.body; - const jsonData = JSON.parse(data) as OnlineCountMessage; - - // 支持两种消息格式 - // 1. 直接是数字: 42 - // 2. 对象格式: { count: 42, timestamp: 1234567890 } - const count = typeof jsonData === "number" ? jsonData : jsonData.count; - - if (count !== undefined && !isNaN(count)) { - onlineUserCount.value = count; - lastUpdateTime.value = Date.now(); - } else { - console.warn("[useOnlineCount] 收到无效的在线用户数:", data); - } - } catch (error) { - console.error("[useOnlineCount] 解析在线用户数失败:", error); - } - }; - - /** - * 订阅在线用户计数主题 - */ - const subscribeToOnlineCount = () => { - if (subscriptionId) { - return; - } - - // 订阅在线用户计数主题(useStomp 会处理重连后的订阅恢复) - subscriptionId = stomp.subscribe(ONLINE_COUNT_TOPIC, handleOnlineCountMessage); - }; - - /** - * 初始化 WebSocket 连接并订阅在线用户主题 - */ - const initialize = () => { - // 检查 WebSocket 端点是否配置 - const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT; - if (!wsEndpoint) { - console.log("[useOnlineCount] 未配置 WebSocket 端点,跳过初始化"); - return; - } - - // 检查令牌有效性 - const accessToken = AuthStorage.getAccessToken(); - if (!accessToken) { - console.log("[useOnlineCount] 未检测到有效令牌,跳过初始化"); - return; - } - - // 建立 WebSocket 连接 - stomp.connect(); - - // 订阅主题 - subscribeToOnlineCount(); - }; - - /** - * 关闭 WebSocket 连接并清理资源 - */ - const cleanup = () => { - // 取消订阅 - if (subscriptionId) { - stomp.unsubscribe(subscriptionId); - subscriptionId = null; - } - - // 也可以通过主题地址取消订阅 - stomp.unsubscribeDestination(ONLINE_COUNT_TOPIC); - - // 断开连接 - stomp.disconnect(); - - // 重置状态 - onlineUserCount.value = 0; - lastUpdateTime.value = 0; - }; - - return { - // 状态 - onlineUserCount: readonly(onlineUserCount), - lastUpdateTime: readonly(lastUpdateTime), - isConnected: stomp.isConnected, - connectionState: stomp.connectionState, - - // 方法 - initialize, - cleanup, - }; -} - -/** - * 在线用户计数组合式函数(单例模式) - * - * 用于实时显示系统在线用户数量 - * - * @example - * ```ts - * // 在组件中使用(推荐) - * const { onlineUserCount, isConnected } = useOnlineCount(); - * - * // 手动控制初始化(高级用法) - * const { onlineUserCount, initialize, cleanup } = useOnlineCount({ autoInit: false }); - * onMounted(() => initialize()); - * onUnmounted(() => cleanup()); - * ``` - */ -export function useOnlineCount(options: { autoInit?: boolean } = {}) { - const { autoInit = true } = options; - - // 获取或创建单例实例 - if (!globalInstance) { - globalInstance = createOnlineCountComposable(); - } - - // 组件级自动初始化(仅在组件上下文中生效) - const instance = getCurrentInstance(); - if (autoInit && instance) { - onMounted(() => { - // 防止重复初始化:只有在未连接时才尝试初始化 - if (!globalInstance!.isConnected.value) { - globalInstance!.initialize(); - } - }); - - // 注意:组件卸载时不关闭连接,保持全局连接 - onUnmounted(() => { - // 全局连接由 cleanupWebSocket() 统一管理 - }); - } - - return globalInstance; -} diff --git a/src/composables/websocket/useStomp.ts b/src/composables/websocket/useStomp.ts deleted file mode 100644 index 740c12a7..00000000 --- a/src/composables/websocket/useStomp.ts +++ /dev/null @@ -1,568 +0,0 @@ -import { Client, type IMessage, type StompSubscription } from "@stomp/stompjs"; -import { AuthStorage } from "@/utils/auth"; - -export interface UseStompOptions { - /** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */ - brokerURL?: string; - /** 用于鉴权的 token,不传时使用 getAccessToken() 的返回值 */ - token?: string; - /** 重连延迟,单位毫秒,默认为 15000 */ - reconnectDelay?: number; - /** 连接超时时间,单位毫秒,默认为 10000 */ - connectionTimeout?: number; - /** 是否开启指数退避重连策略 */ - useExponentialBackoff?: boolean; - /** 最大重连次数,默认为 3 */ - maxReconnectAttempts?: number; - /** 最大重连延迟,单位毫秒,默认为 60000 */ - maxReconnectDelay?: number; - /** 是否开启调试日志 */ - debug?: boolean; - /** 是否在重连时自动恢复订阅,默认为 true */ - autoRestoreSubscriptions?: boolean; - /** - * 心跳接收间隔,单位毫秒,默认为 4000 - * 注意:标签页失活时,浏览器会节流定时器,建议设置较长的间隔(如 10000)以减少失活影响 - */ - heartbeatIncoming?: number; - /** - * 心跳发送间隔,单位毫秒,默认为 4000 - * 注意:标签页失活时,浏览器会节流定时器,建议设置较长的间隔(如 10000)以减少失活影响 - */ - heartbeatOutgoing?: number; -} - -/** - * 订阅配置信息 - */ -interface SubscriptionConfig { - destination: string; - callback: (message: IMessage) => void; -} - -/** - * 连接状态枚举 - */ -enum ConnectionState { - DISCONNECTED = "DISCONNECTED", - CONNECTING = "CONNECTING", - CONNECTED = "CONNECTED", - RECONNECTING = "RECONNECTING", -} - -/** - * STOMP WebSocket 连接管理组合式函数 - * - * 核心功能: - * - 自动连接管理(连接、断开、重连) - * - 订阅管理(订阅、取消订阅、自动恢复) - * - 心跳检测 - * - Token 自动刷新 - * - * @param options 配置选项 - * @returns STOMP 客户端操作接口 - */ -export function useStomp(options: UseStompOptions = {}) { - // ==================== 配置初始化 ==================== - const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || ""; - - const config = { - brokerURL: ref(options.brokerURL ?? defaultBrokerURL), - reconnectDelay: options.reconnectDelay ?? 15000, - connectionTimeout: options.connectionTimeout ?? 10000, - useExponentialBackoff: options.useExponentialBackoff ?? false, - maxReconnectAttempts: options.maxReconnectAttempts ?? 3, - maxReconnectDelay: options.maxReconnectDelay ?? 60000, - autoRestoreSubscriptions: options.autoRestoreSubscriptions ?? true, - debug: options.debug ?? false, - heartbeatIncoming: options.heartbeatIncoming ?? 4000, - heartbeatOutgoing: options.heartbeatOutgoing ?? 4000, - }; - - // ==================== 状态管理 ==================== - const connectionState = ref(ConnectionState.DISCONNECTED); - const isConnected = computed(() => connectionState.value === ConnectionState.CONNECTED); - const reconnectAttempts = ref(0); - - // ==================== 定时器管理 ==================== - let reconnectTimer: ReturnType | null = null; - let connectionTimeoutTimer: ReturnType | null = null; - - // ==================== 订阅管理 ==================== - // 活动订阅:存储当前 STOMP 订阅对象 - const activeSubscriptions = new Map(); - // 订阅配置注册表:用于自动恢复订阅 - const subscriptionRegistry = new Map(); - - // ==================== 客户端实例 ==================== - const stompClient = ref(null); - let isManualDisconnect = false; - - // ==================== 工具函数 ==================== - - /** - * 清理所有定时器 - */ - const clearAllTimers = () => { - if (reconnectTimer) { - clearTimeout(reconnectTimer); - reconnectTimer = null; - } - if (connectionTimeoutTimer) { - clearTimeout(connectionTimeoutTimer); - connectionTimeoutTimer = null; - } - }; - - /** - * 日志输出(支持调试模式控制) - */ - const log = config.debug ? (...args: any[]) => console.log("[useStomp]", ...args) : () => {}; - const logWarn = (...args: any[]) => console.warn("[useStomp]", ...args); - const logError = (...args: any[]) => console.error("[useStomp]", ...args); - - /** - * 恢复所有订阅 - */ - const restoreSubscriptions = () => { - if (!config.autoRestoreSubscriptions || subscriptionRegistry.size === 0) { - return; - } - - log(`开始恢复 ${subscriptionRegistry.size} 个订阅...`); - - for (const [destination, subscriptionConfig] of subscriptionRegistry.entries()) { - try { - performSubscribe(destination, subscriptionConfig.callback); - } catch (error) { - logError(`恢复订阅 ${destination} 失败:`, error); - } - } - }; - - /** - * 初始化 STOMP 客户端 - */ - const initializeClient = () => { - // 如果客户端已存在且处于活动状态,直接返回 - if (stompClient.value && (stompClient.value.active || stompClient.value.connected)) { - log("STOMP 客户端已存在且处于活动状态,跳过初始化"); - return; - } - - // 检查 WebSocket 端点是否配置 - if (!config.brokerURL.value) { - logWarn("WebSocket 连接失败: 未配置 WebSocket 端点 URL"); - return; - } - - // 每次连接前重新获取最新令牌 - const accessToken = AuthStorage.getAccessToken(); - if (!accessToken) { - logWarn("WebSocket 连接失败:授权令牌为空,请先登录"); - return; - } - - // 清理旧客户端 - if (stompClient.value) { - try { - stompClient.value.deactivate(); - } catch (error) { - logWarn("清理旧客户端时出错:", error); - } - stompClient.value = null; - } - - // 创建 STOMP 客户端 - stompClient.value = new Client({ - brokerURL: config.brokerURL.value, - connectHeaders: { - Authorization: `Bearer ${accessToken}`, - }, - debug: config.debug ? (msg) => console.log("[STOMP]", msg) : () => {}, - reconnectDelay: 0, // 禁用内置重连,使用自定义重连逻辑 - heartbeatIncoming: config.heartbeatIncoming, - heartbeatOutgoing: config.heartbeatOutgoing, - }); - - // ==================== 事件监听器 ==================== - - // 连接成功 - stompClient.value.onConnect = () => { - connectionState.value = ConnectionState.CONNECTED; - reconnectAttempts.value = 0; - clearAllTimers(); - - log("✅ WebSocket 连接已建立"); - - // 自动恢复订阅 - restoreSubscriptions(); - }; - - // 连接断开 - stompClient.value.onDisconnect = () => { - connectionState.value = ConnectionState.DISCONNECTED; - log("❌ WebSocket 连接已断开"); - - // 清空活动订阅(但保留订阅配置用于恢复) - activeSubscriptions.clear(); - - // 如果不是手动断开且未达到最大重连次数,则尝试重连 - if (!isManualDisconnect && reconnectAttempts.value < config.maxReconnectAttempts) { - scheduleReconnect(); - } - }; - - // WebSocket 关闭 - stompClient.value.onWebSocketClose = (event) => { - connectionState.value = ConnectionState.DISCONNECTED; - log(`WebSocket 已关闭: code=${event?.code}, reason=${event?.reason}`); - - // 如果是手动断开,不重连 - if (isManualDisconnect) { - log("手动断开连接,不进行重连"); - return; - } - - // 对于异常关闭,尝试重连 - if ( - event?.code && - [1000, 1006, 1008, 1011].includes(event.code) && - reconnectAttempts.value < config.maxReconnectAttempts - ) { - log("检测到连接异常关闭,将尝试重连"); - scheduleReconnect(); - } - }; - - // STOMP 错误 - stompClient.value.onStompError = (frame) => { - logError("STOMP 错误:", frame.headers, frame.body); - connectionState.value = ConnectionState.DISCONNECTED; - - // 检查是否是授权错误 - const isAuthError = - frame.headers?.message?.includes("Unauthorized") || - frame.body?.includes("Unauthorized") || - frame.body?.includes("Token") || - frame.body?.includes("401"); - - if (isAuthError) { - logWarn("WebSocket 授权错误,停止重连"); - isManualDisconnect = true; // 授权错误不进行重连 - } - }; - }; - - /** - * 调度重连任务 - */ - const scheduleReconnect = () => { - // 如果正在连接或手动断开,不重连 - if (connectionState.value === ConnectionState.CONNECTING || isManualDisconnect) { - return; - } - - // 检查是否达到最大重连次数 - if (reconnectAttempts.value >= config.maxReconnectAttempts) { - logError(`已达到最大重连次数 (${config.maxReconnectAttempts}),停止重连`); - return; - } - - reconnectAttempts.value++; - connectionState.value = ConnectionState.RECONNECTING; - - // 计算重连延迟(支持指数退避) - const delay = config.useExponentialBackoff - ? Math.min( - config.reconnectDelay * Math.pow(2, reconnectAttempts.value - 1), - config.maxReconnectDelay - ) - : config.reconnectDelay; - - log(`准备重连 (${reconnectAttempts.value}/${config.maxReconnectAttempts}),延迟 ${delay}ms`); - - // 清除之前的重连计时器 - if (reconnectTimer) { - clearTimeout(reconnectTimer); - } - - // 设置重连计时器 - reconnectTimer = setTimeout(() => { - if (connectionState.value !== ConnectionState.CONNECTED && !isManualDisconnect) { - log(`开始第 ${reconnectAttempts.value} 次重连...`); - connect(); - } - }, delay); - }; - - // 监听 brokerURL 的变化,自动重新初始化 - watch(config.brokerURL, (newURL, oldURL) => { - if (newURL !== oldURL) { - log(`WebSocket 端点已更改: ${oldURL} -> ${newURL}`); - - // 断开当前连接 - if (stompClient.value && stompClient.value.connected) { - stompClient.value.deactivate(); - } - - // 重新初始化客户端 - initializeClient(); - } - }); - - // 初始化客户端 - initializeClient(); - - // ==================== 标签页可见性监听 ==================== - - /** - * 处理标签页可见性变化 - * 当标签页从失活变为激活时,检查连接状态并尝试重连 - */ - const handleVisibilityChange = () => { - if (document.hidden) { - log("标签页已失活"); - } else { - log("标签页已激活,检查WebSocket连接状态..."); - - // 标签页激活时,检查连接状态 - if (stompClient.value && !stompClient.value.connected && !isManualDisconnect) { - logWarn("检测到WebSocket连接已断开,尝试重新连接..."); - // 重置重连次数,给予更多重连机会 - reconnectAttempts.value = 0; - connect(); - } - } - }; - - // 监听标签页可见性变化 - if (typeof document !== "undefined") { - document.addEventListener("visibilitychange", handleVisibilityChange); - } - - // 清理函数:移除事件监听器 - const cleanup = () => { - if (typeof document !== "undefined") { - document.removeEventListener("visibilitychange", handleVisibilityChange); - } - disconnect(); - }; - - // ==================== 公共接口 ==================== - - /** - * 建立 WebSocket 连接 - */ - const connect = () => { - // 重置手动断开标志 - isManualDisconnect = false; - - // 检查是否配置了 WebSocket 端点 - if (!config.brokerURL.value) { - logError("WebSocket 连接失败: 未配置 WebSocket 端点 URL"); - return; - } - - // 防止重复连接 - if (connectionState.value === ConnectionState.CONNECTING) { - log("WebSocket 正在连接中,跳过重复连接请求"); - return; - } - - // 如果客户端不存在,先初始化 - if (!stompClient.value) { - initializeClient(); - } - - if (!stompClient.value) { - logError("STOMP 客户端初始化失败"); - return; - } - - // 避免重复连接:检查是否已连接 - if (stompClient.value.connected) { - log("WebSocket 已连接,跳过重复连接"); - connectionState.value = ConnectionState.CONNECTED; - return; - } - - // 设置连接状态 - connectionState.value = ConnectionState.CONNECTING; - - // 设置连接超时 - if (connectionTimeoutTimer) { - clearTimeout(connectionTimeoutTimer); - } - - connectionTimeoutTimer = setTimeout(() => { - if (connectionState.value === ConnectionState.CONNECTING) { - logWarn("WebSocket 连接超时"); - connectionState.value = ConnectionState.DISCONNECTED; - - // 超时后尝试重连 - if (!isManualDisconnect && reconnectAttempts.value < config.maxReconnectAttempts) { - scheduleReconnect(); - } - } - }, config.connectionTimeout); - - try { - stompClient.value.activate(); - log("正在建立 WebSocket 连接..."); - } catch (error) { - logError("激活 WebSocket 连接失败:", error); - connectionState.value = ConnectionState.DISCONNECTED; - } - }; - - /** - * 执行订阅操作(内部方法) - */ - const performSubscribe = (destination: string, callback: (message: IMessage) => void): string => { - if (!stompClient.value || !stompClient.value.connected) { - logWarn(`尝试订阅 ${destination} 失败: 客户端未连接`); - return ""; - } - - try { - const subscription = stompClient.value.subscribe(destination, callback); - const subscriptionId = subscription.id; - activeSubscriptions.set(subscriptionId, subscription); - log(`✓ 订阅成功: ${destination} (ID: ${subscriptionId})`); - return subscriptionId; - } catch (error) { - logError(`订阅 ${destination} 失败:`, error); - return ""; - } - }; - - /** - * 订阅指定主题 - * - * @param destination 目标主题地址(如:/topic/message) - * @param callback 接收到消息时的回调函数 - * @returns 订阅 ID,用于后续取消订阅 - */ - const subscribe = (destination: string, callback: (message: IMessage) => void): string => { - // 保存订阅配置到注册表,用于断线重连后自动恢复 - subscriptionRegistry.set(destination, { destination, callback }); - - // 如果已连接,立即订阅 - if (stompClient.value?.connected) { - return performSubscribe(destination, callback); - } - - log(`暂存订阅配置: ${destination},将在连接建立后自动订阅`); - return ""; - }; - - /** - * 取消订阅 - * - * @param subscriptionId 订阅 ID(由 subscribe 方法返回) - */ - const unsubscribe = (subscriptionId: string) => { - const subscription = activeSubscriptions.get(subscriptionId); - if (subscription) { - try { - subscription.unsubscribe(); - activeSubscriptions.delete(subscriptionId); - log(`✓ 已取消订阅: ${subscriptionId}`); - } catch (error) { - logWarn(`取消订阅 ${subscriptionId} 时出错:`, error); - } - } - }; - - /** - * 取消指定主题的订阅(从注册表中移除) - * - * @param destination 主题地址 - */ - const unsubscribeDestination = (destination: string) => { - // 从注册表中移除 - subscriptionRegistry.delete(destination); - - // 取消所有匹配该主题的活动订阅 - for (const [id, subscription] of activeSubscriptions.entries()) { - // 注意:STOMP 的 subscription 对象没有直接暴露 destination, - // 这里简化处理,实际使用时可能需要额外维护 id -> destination 的映射 - try { - subscription.unsubscribe(); - activeSubscriptions.delete(id); - } catch (error) { - logWarn(`取消订阅 ${id} 时出错:`, error); - } - } - - log(`✓ 已移除主题订阅配置: ${destination}`); - }; - - /** - * 断开 WebSocket 连接 - * - * @param clearSubscriptions 是否清除订阅注册表(默认为 true) - */ - const disconnect = (clearSubscriptions = true) => { - // 设置手动断开标志 - isManualDisconnect = true; - - // 清除所有定时器 - clearAllTimers(); - - // 取消所有活动订阅 - for (const [id, subscription] of activeSubscriptions.entries()) { - try { - subscription.unsubscribe(); - } catch (error) { - logWarn(`取消订阅 ${id} 时出错:`, error); - } - } - activeSubscriptions.clear(); - - // 可选:清除订阅注册表 - if (clearSubscriptions) { - subscriptionRegistry.clear(); - log("已清除所有订阅配置"); - } - - // 断开连接 - if (stompClient.value) { - try { - if (stompClient.value.connected || stompClient.value.active) { - stompClient.value.deactivate(); - log("✓ WebSocket 连接已主动断开"); - } - } catch (error) { - logError("断开 WebSocket 连接时出错:", error); - } - stompClient.value = null; - } - - connectionState.value = ConnectionState.DISCONNECTED; - reconnectAttempts.value = 0; - }; - - // ==================== 返回公共接口 ==================== - return { - // 状态 - connectionState: readonly(connectionState), - isConnected, - reconnectAttempts: readonly(reconnectAttempts), - - // 连接管理 - connect, - disconnect, - cleanup, // 清理资源(包括移除事件监听器) - - // 订阅管理 - subscribe, - unsubscribe, - unsubscribeDestination, - - // 统计信息 - getActiveSubscriptionCount: () => activeSubscriptions.size, - getRegisteredSubscriptionCount: () => subscriptionRegistry.size, - }; -} diff --git a/src/main.ts b/src/main.ts index 16789dd9..90d7fa45 100644 --- a/src/main.ts +++ b/src/main.ts @@ -33,9 +33,6 @@ import { configureVxeTable } from "@/plugins/vxe-table"; // ===== 路由守卫 ===== import { setupPermissionGuard } from "@/router/guards/permission"; -// ===== 业务服务 ===== -import { setupWebSocket } from "@/composables"; - // 创建 Vue 应用实例 const app = createApp(App); @@ -56,8 +53,5 @@ app.use(InstallCodeMirror); // 4️⃣ 路由守卫 setupPermissionGuard(); -// 5️⃣ WebSocket 初始化 -setupWebSocket(); - -// 6️⃣ 挂载应用 +// 5️⃣ 挂载应用 app.mount("#app"); diff --git a/src/router/guards/permission.ts b/src/router/guards/permission.ts index 376862ce..bfec534a 100644 --- a/src/router/guards/permission.ts +++ b/src/router/guards/permission.ts @@ -5,6 +5,7 @@ import { usePermissionStore, useUserStore } from "@/store"; import { useTenantStoreHook } from "@/store/modules/tenant"; import { isTenantEnabled } from "@/utils/tenant"; import { addRecentMenu } from "@/composables/useRecentMenus"; +import { setupSse } from "@/composables"; /** * 路由权限守卫 @@ -44,6 +45,8 @@ export function setupPermissionGuard() { if (!permissionStore.isRouteGenerated) { if (!userStore.userInfo?.roles?.length) { await userStore.getUserInfo(); + // 用户信息加载完成后初始化 SSE + setupSse(); } // 加载用户租户列表(VITE_APP_TENANT_ENABLED=true 时生效) diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index b92d32dd..2aa40351 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -8,7 +8,7 @@ import { AuthStorage } from "@/utils/auth"; import { usePermissionStoreHook } from "@/store/modules/permission"; import { useDictStoreHook } from "@/store/modules/dict"; import { useTagsViewStore } from "@/store"; -import { cleanupWebSocket } from "@/composables"; +import { cleanupSseServices } from "@/composables"; export const useUserStore = defineStore("user", () => { // 用户信息 @@ -76,8 +76,8 @@ export const useUserStore = defineStore("user", () => { useDictStoreHook().clearDictCache(); useTagsViewStore().delAllViews(); - // 3. 清理 WebSocket 连接 - cleanupWebSocket(); + // 3. 清理 SSE 连接 + cleanupSseServices(); } /** diff --git a/src/styles/common.scss b/src/styles/_layouts.scss similarity index 73% rename from src/styles/common.scss rename to src/styles/_layouts.scss index 322f03d6..c749e0fe 100644 --- a/src/styles/common.scss +++ b/src/styles/_layouts.scss @@ -1,15 +1,27 @@ -/* 全局业务通用样式 */ +/** + * 布局相关样式 + */ + +// ============================================ +// 通用容器 +// ============================================ .app-container { padding: 15px; } -/* 进度条颜色 */ +// ============================================ +// 进度条 +// ============================================ + #nprogress .bar { background-color: var(--el-color-primary); } -/* 弹出菜单统一使用主题变量(简化 hover 色) */ +// ============================================ +// 弹出菜单 +// ============================================ + .el-menu--popup { --el-menu-bg-color: var(--menu-background); --el-menu-text-color: var(--menu-text); @@ -35,9 +47,10 @@ } } -/* ============================================ - 混合布局左侧菜单 hover 样式 - ============================================ */ +// ============================================ +// 混合布局左侧菜单 hover 样式 +// ============================================ + .layout-mix .layout__sidebar--left .el-menu { .el-menu-item { &:hover { @@ -52,7 +65,7 @@ } } -/* 深色或深蓝侧边栏的 hover 样式 */ +// 深色或深蓝侧边栏的 hover 样式 html.dark .layout-mix .layout__sidebar--left .el-menu, html.sidebar-color-blue .layout-mix .layout__sidebar--left .el-menu { .el-menu-item, @@ -63,7 +76,10 @@ html.sidebar-color-blue .layout-mix .layout__sidebar--left .el-menu { } } -/* Top layout: let horizontal menu fill the remaining header space */ +// ============================================ +// 顶部布局菜单 +// ============================================ + .layout-top .layout__header-left .el-menu--horizontal, .layout-mix .layout__header-menu .el-menu--horizontal { display: flex; @@ -74,7 +90,7 @@ html.sidebar-color-blue .layout-mix .layout__sidebar--left .el-menu { overflow: hidden; } -/* 窄屏隐藏菜单文字,仅保留图标 */ +// 窄屏隐藏菜单文字,仅保留图标 .hideSidebar { &.layout-top .layout__header .el-menu--horizontal, &.layout-mix .layout__header .el-menu--horizontal { @@ -98,7 +114,10 @@ html.sidebar-color-blue .layout-mix .layout__sidebar--left .el-menu { } } -/* 全局筛选区域 */ +// ============================================ +// 全局筛选区域 +// ============================================ + .filter-section { padding: 8px 12px 0; margin-bottom: 8px; @@ -115,7 +134,10 @@ html.sidebar-color-blue .layout-mix .layout__sidebar--left .el-menu { } } -/* 表格区域 */ +// ============================================ +// 表格区域 +// ============================================ + .table-section { margin-bottom: 12px; diff --git a/src/styles/variables.scss b/src/styles/_variables.scss similarity index 79% rename from src/styles/variables.scss rename to src/styles/_variables.scss index 0048e684..c4802cb3 100644 --- a/src/styles/variables.scss +++ b/src/styles/_variables.scss @@ -1,15 +1,14 @@ /** - * 项目主题变量 + * 全局变量定义 * * 结构: - * 1. SCSS 变量 - 布局尺寸(供 JS 导出和组件使用) - * 2. CSS 变量 - 侧边栏/菜单主题色 - * 3. 主题模式 - 深蓝侧边栏、暗黑模式 - * 4. 无障碍模式 - 色弱模式 + * 1. 布局尺寸(SCSS 变量) + * 2. 主题变量(CSS 变量) + * 3. 主题模式切换 */ // ============================================ -// 1. SCSS 变量 - 布局尺寸 +// 1. 布局尺寸(SCSS 变量) // ============================================ $sidebar-width: 210px; @@ -18,7 +17,7 @@ $navbar-height: 50px; $tags-view-height: 34px; // ============================================ -// 2. CSS 变量 - 默认主题(浅色 + 白色侧边栏) +// 2. 主题变量(CSS 变量) // ============================================ :root { @@ -70,10 +69,6 @@ html.dark { } } -// ============================================ -// 4. 无障碍模式 -// ============================================ - // 色弱模式 html.color-weak { filter: invert(80%); diff --git a/src/styles/element-plus-vars.scss b/src/styles/element-plus-vars.scss deleted file mode 100644 index caef517e..00000000 --- a/src/styles/element-plus-vars.scss +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Element Plus 变量覆盖 - * - * 此文件用于覆盖 Element Plus 的默认主题变量 - * 需要在 element-plus.scss 中导入,而不是在 variables.scss 中 - */ -@forward "element-plus/theme-chalk/src/common/var.scss" with ( - $colors: ( - "primary": ( - // 默认主题色 - 修改此值时需同步修改 src/settings.ts 中的 themeColor - "base": #4080ff, - ), - "success": ( - "base": #23c343, - ), - "warning": ( - "base": #ff9a2e, - ), - "danger": ( - "base": #f76560, - ), - "info": ( - "base": #a9aeb8, - ), - ), - - $bg-color: ( - "page": #f5f8fd, - ) -); diff --git a/src/styles/element-plus.scss b/src/styles/element-plus.scss deleted file mode 100644 index 6cdc078f..00000000 --- a/src/styles/element-plus.scss +++ /dev/null @@ -1,48 +0,0 @@ -// Element Plus 变量覆盖(必须在最前面) -@use "./element-plus-vars"; - -$border: 1px solid var(--el-border-color-light); - -/* el-dialog */ -.el-dialog { - .el-dialog__header { - padding: 15px 20px; - margin: 0; - border-bottom: $border; - } - - .el-dialog__body { - padding: 20px; - } - - .el-dialog__footer { - padding: 15px; - border-top: $border; - } -} - -/* el-drawer */ -.el-drawer { - .el-drawer__header { - padding: 15px 20px; - margin: 0; - color: inherit; - border-bottom: $border; - } - - .el-drawer__body { - padding: 20px; - } - - .el-drawer__footer { - padding: 15px; - border-top: $border; - } -} - -// 抽屉和对话框底部按钮区域 -.dialog-footer { - display: flex; - gap: 8px; - justify-content: flex-end; -} diff --git a/src/styles/index.scss b/src/styles/index.scss index 0850210e..b945dac2 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -1,13 +1,75 @@ -// 1. 基础重置 -@use "./reset"; +/** + * 样式入口文件 + * + * 编译说明: + * - index.scss:✅ 编译成 index.css + * - _*.scss:❌ 不单独编译,仅被引入使用 + */ + +// ============================================ +// 1. 变量和第三方库(@use 必须在所有规则之前) +// ============================================ -// 2. 主题变量 @use "./variables" as *; +@use "./vendors"; +@use "./layouts"; -// 3. UI 框架适配 -@use "./element-plus"; -@use "./vxe-table"; -@use "./wangeditor"; +// ============================================ +// 2. 基础重置 +// ============================================ -// 4. 业务通用样式 -@use "./common"; +#app, +html { + box-sizing: border-box; + width: 100%; + height: 100%; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +body { + margin: 0; + font-family: + "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", + Arial, sans-serif; + line-height: 1.5; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizelegibility; +} + +ul, +li { + padding: 0; + margin: 0; + list-style: none; +} + +a, +a:focus, +a:hover { + color: inherit; + text-decoration: none; + cursor: pointer; +} + +a:focus, +a:active, +div:focus { + outline: none; +} + +img, +svg { + display: inline-block; +} + +svg { + // 因 icon 大小被设置为和字体大小一致,而 span 等标签的下边缘会和字体的基线对齐, + // 故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 + vertical-align: -0.15em; +} diff --git a/src/styles/reset.scss b/src/styles/reset.scss deleted file mode 100644 index 747a3fa9..00000000 --- a/src/styles/reset.scss +++ /dev/null @@ -1,70 +0,0 @@ -// 全局基础重置:补充 UnoCSS 预设未覆盖的项目级样式 - -#app { - width: 100%; - height: 100%; -} - -html { - box-sizing: border-box; - width: 100%; - height: 100%; - line-height: 1.5; - tab-size: 4; - text-size-adjust: 100%; -} - -#app, -html { - box-sizing: border-box; -} - -*, -*::before, -*::after { - box-sizing: inherit; -} - -body { - width: 100%; - height: 100%; - margin: 0; - font-family: - "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", - Arial, sans-serif; - line-height: inherit; - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - text-rendering: optimizelegibility; -} - -img, -svg { - display: inline-block; -} - -svg { - // 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 - vertical-align: -0.15em; -} - -ul, -li { - padding: 0; - margin: 0; - list-style: none; -} - -a, -a:focus, -a:hover { - color: inherit; - text-decoration: none; - cursor: pointer; -} - -a:focus, -a:active, -div:focus { - outline: none; -} diff --git a/src/styles/variables.module.scss b/src/styles/variables.module.scss index 9dccf0cd..ce8551cb 100644 --- a/src/styles/variables.module.scss +++ b/src/styles/variables.module.scss @@ -1,7 +1,12 @@ -/* stylelint-disable property-no-unknown */ +/** + * 导出变量供 JS/TS 使用 + * + * 使用方式: + * import styles from "@/styles/variables.module.scss" + * console.log(styles['sidebar-width']) // "210px" + */ -// 通过 SCSS 变量导出给 JS/TS 使用的模块文件 -// 注意:依赖 src/styles/variables.scss 中定义的 SCSS 变量 +@use "./variables" as *; :export { sidebar-width: $sidebar-width; @@ -12,5 +17,3 @@ menu-active-text: $menu-active-text; menu-hover: $menu-hover; } - -/* stylelint-enable property-no-unknown */ diff --git a/src/styles/vendors/_element-plus.scss b/src/styles/vendors/_element-plus.scss new file mode 100644 index 00000000..43afda60 --- /dev/null +++ b/src/styles/vendors/_element-plus.scss @@ -0,0 +1,81 @@ +/** + * Element Plus 变量覆盖和样式适配 + */ + +// ============================================ +// 1. 变量覆盖(必须在最前面) +// ============================================ + +@forward "element-plus/theme-chalk/src/common/var.scss" with ( + $colors: ( + "primary": ( + // 默认主题色 - 修改此值时需同步修改 src/settings.ts 中的 themeColor + "base": #4080ff, + ), + "success": ( + "base": #23c343, + ), + "warning": ( + "base": #ff9a2e, + ), + "danger": ( + "base": #f76560, + ), + "info": ( + "base": #a9aeb8, + ), + ), + $bg-color: ( + "page": #f5f8fd, + ) +); + +// ============================================ +// 2. 样式覆盖 +// ============================================ + +$border: 1px solid var(--el-border-color-light); + +/* el-dialog */ +.el-dialog { + .el-dialog__header { + padding: 15px 20px; + margin: 0; + border-bottom: $border; + } + + .el-dialog__body { + padding: 20px; + } + + .el-dialog__footer { + padding: 15px; + border-top: $border; + } +} + +/* el-drawer */ +.el-drawer { + .el-drawer__header { + padding: 15px 20px; + margin: 0; + color: inherit; + border-bottom: $border; + } + + .el-drawer__body { + padding: 20px; + } + + .el-drawer__footer { + padding: 15px; + border-top: $border; + } +} + +/* 抽屉和对话框底部按钮区域 */ +.dialog-footer { + display: flex; + gap: 8px; + justify-content: flex-end; +} diff --git a/src/styles/vendors/_index.scss b/src/styles/vendors/_index.scss new file mode 100644 index 00000000..b0956032 --- /dev/null +++ b/src/styles/vendors/_index.scss @@ -0,0 +1,10 @@ +/** + * 第三方库(vendors)样式适配入口 + * + * vendors = 第三方供应商代码 + * 包含:Element Plus、Vxe Table、WangEditor 等 + */ + +@use "./element-plus"; +@use "./vxe-table"; +@use "./wangeditor"; diff --git a/src/styles/vxe-table.scss b/src/styles/vendors/_vxe-table.scss similarity index 93% rename from src/styles/vxe-table.scss rename to src/styles/vendors/_vxe-table.scss index 0c8bdfa5..8019985a 100644 --- a/src/styles/vxe-table.scss +++ b/src/styles/vendors/_vxe-table.scss @@ -1,10 +1,12 @@ /** - * Vxe Table 主题统一: - * 1) 用 Element Plus 的 CSS 变量覆写 Vxe Table 的 CSS 变量 - * 2) 自定义局部样式 + * Vxe Table 主题适配 + * 使用 Element Plus CSS 变量实现主题统一 */ -/* 变量覆写 */ +// ============================================ +// 变量覆写 +// ============================================ + :root { /* color */ --vxe-font-color: var(--el-text-color-regular); @@ -92,7 +94,10 @@ --vxe-select-panel-background-color: var(--el-bg-color); } -/* 自定义组件样式 */ +// ============================================ +// 自定义组件样式 +// ============================================ + .vxe-grid { /* 表单 */ &--form-wrapper { diff --git a/src/styles/wangeditor.scss b/src/styles/vendors/_wangeditor.scss similarity index 100% rename from src/styles/wangeditor.scss rename to src/styles/vendors/_wangeditor.scss diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue index b9948d5d..de7b5dac 100644 --- a/src/views/dashboard/index.vue +++ b/src/views/dashboard/index.vue @@ -133,20 +133,15 @@
- + - WebSocket - {{ wsStatusText }} + SSE + {{ sseStatusText }}
@@ -413,19 +408,17 @@ const formattedTime = computed(() => { return useDateFormat(lastUpdateTime, "HH:mm:ss").value; }); -const wsStatusText = computed(() => { +const sseStatusText = computed(() => { if (!isConnected.value) { - return connectionState.value === "CONNECTING" || connectionState.value === "RECONNECTING" - ? "连接中" - : "未连接"; + return connectionState.value === "CONNECTING" ? "连接中" : "未连接"; } return "已连接"; }); -const wsStatusClass = computed(() => { +const sseStatusClass = computed(() => { if (isConnected.value) return "text-[--el-color-success] bg-[--el-color-success-light-9] border-[--el-color-success-light-7]"; - return connectionState.value === "CONNECTING" || connectionState.value === "RECONNECTING" + return connectionState.value === "CONNECTING" ? "text-[--el-color-warning] bg-[--el-color-warning-light-9] border-[--el-color-warning-light-7]" : "text-[--el-color-danger] bg-[--el-color-danger-light-9] border-[--el-color-danger-light-7]"; }); diff --git a/src/views/demo/dict-sync.vue b/src/views/demo/dict-sync.vue index 41213f3a..f78c427c 100644 --- a/src/views/demo/dict-sync.vue +++ b/src/views/demo/dict-sync.vue @@ -3,15 +3,16 @@ - 本示例展示WebSocket实时更新字典缓存的效果。您可以编辑"男"性别字典项,保存后后端将通过WebSocket通知所有客户端刷新缓存。 + 本示例展示 SSE 实时更新字典缓存的效果。您可以编辑"男"性别字典项,保存后后端将通过 SSE + 通知所有客户端刷新缓存。 @@ -161,16 +162,16 @@ const dictForm = ref(null); // 选中的性别 const selectedGender = ref(""); -// 初始化WebSocket -const dictWebSocket = useDictSync(); +// 初始化 SSE +const dictSse = useDictSync(); // 获取连接状态 -const wsConnected = computed(() => dictWebSocket.isConnected); +const sseConnected = computed(() => dictSse.isConnected.value); -// WebSocket连接状态显示文本 -const wsStatusText = computed(() => (wsConnected.value ? "已连接" : "未连接")); +// SSE 连接状态显示文本 +const sseStatusText = computed(() => (sseConnected.value ? "已连接" : "未连接")); -// 保存WebSocket清理函数 +// 保存 SSE 清理函数 let unregisterCallback: (() => void) | null = null; // 当前选中字典的缓存状态 @@ -179,13 +180,13 @@ const dictCacheStatus = computed(() => { return dictStore.getDictItems(DICT_CODE).length > 0; }); -// 设置WebSocket -const setupWebSocket = () => { - // 初始化WebSocket连接 - dictWebSocket.initialize(); +// 设置 SSE +const setupSse = () => { + // 初始化 SSE 连接 + dictSse.initialize(); // 注册字典消息回调 - unregisterCallback = dictWebSocket.onDictChange((message: DictMessage) => { + unregisterCallback = dictSse.onDictChange((message: DictMessage) => { // 只有当消息是关于性别字典的更新时才处理 if (message.dictCode === DICT_CODE) { // 更新最后更新时间 @@ -224,7 +225,7 @@ const saveDict = async () => { // 更新时间 lastUpdateTime.value = useDateFormat(new Date(), "YYYY-MM-DD HH:mm:ss").value; - ElMessage.success("保存成功,后端将通过WebSocket通知所有客户端"); + ElMessage.success("保存成功,后端将通过 SSE 通知所有客户端"); saving.value = false; }; @@ -235,11 +236,11 @@ onMounted(async () => { await dictStore.loadDictItems(DICT_CODE); // 初始化选中性别为男 selectedGender.value = "1"; - // 设置WebSocket - setupWebSocket(); + // 设置 SSE + setupSse(); }); -// 组件卸载时清理WebSocket +// 组件卸载时清理 SSE onUnmounted(() => { unregisterCallback?.(); }); diff --git a/tests/setup.ts b/tests/setup.ts deleted file mode 100644 index f1928975..00000000 --- a/tests/setup.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * 测试环境全局配置 - */ - -import { vi } from "vitest"; - -// Mock window.matchMedia -Object.defineProperty(window, "matchMedia", { - writable: true, - value: vi.fn().mockImplementation((query) => ({ - matches: false, - media: query, - onchange: null, - addListener: vi.fn(), - removeListener: vi.fn(), - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - dispatchEvent: vi.fn(), - })), -}); - -// Mock IntersectionObserver -global.IntersectionObserver = class IntersectionObserver { - constructor() {} - disconnect() {} - observe() {} - takeRecords() { - return []; - } - unobserve() {} -} as any; - -// Mock ResizeObserver -global.ResizeObserver = class ResizeObserver { - constructor() {} - disconnect() {} - observe() {} - unobserve() {} -} as any; - -// Mock Element.scrollIntoView -Element.prototype.scrollIntoView = vi.fn(); - -// Mock __APP_INFO__ -(globalThis as any).__APP_INFO__ = { - pkg: { - name: "vue3-element-admin", - version: "4.0.0", - }, -}; - -// Mock console methods to reduce noise in tests -global.console = { - ...console, - log: vi.fn(), - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), -}; diff --git a/tests/unit/components/Pagination.test.ts b/tests/unit/components/Pagination.test.ts deleted file mode 100644 index 440ff485..00000000 --- a/tests/unit/components/Pagination.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { mount } from "@vue/test-utils"; -import Pagination from "@/components/Pagination/index.vue"; -import ElementPlus from "element-plus"; - -describe("Pagination 组件", () => { - const createWrapper = (props = {}) => { - return mount(Pagination, { - props: { - page: 1, - limit: 10, - total: 100, - ...props, - }, - global: { - plugins: [ElementPlus], - }, - }); - }; - - describe("渲染", () => { - it("应该正确渲染分页组件", () => { - const wrapper = createWrapper(); - expect(wrapper.exists()).toBe(true); - expect(wrapper.find(".pagination").exists()).toBe(true); - }); - - it("应该在 hidden 为 true 时隐藏分页", () => { - const wrapper = createWrapper({ hidden: true }); - expect(wrapper.find(".pagination").classes()).toContain("hidden"); - }); - - it("应该在 hidden 为 false 时显示分页", () => { - const wrapper = createWrapper({ hidden: false }); - expect(wrapper.find(".pagination").classes()).not.toContain("hidden"); - }); - }); - - describe("Props", () => { - it("应该接收 total 属性", () => { - const wrapper = createWrapper({ total: 200 }); - expect(wrapper.props("total")).toBe(200); - }); - - it("应该接收 pageSizes 属性", () => { - const pageSizes = [5, 10, 20, 50]; - const wrapper = createWrapper({ pageSizes }); - expect(wrapper.props("pageSizes")).toEqual(pageSizes); - }); - - it("应该使用默认的 pageSizes", () => { - const wrapper = createWrapper(); - expect(wrapper.props("pageSizes")).toEqual([10, 20, 30, 50]); - }); - - it("应该接收 layout 属性", () => { - const layout = "prev, pager, next"; - const wrapper = createWrapper({ layout }); - expect(wrapper.props("layout")).toBe(layout); - }); - - it("应该接收 background 属性", () => { - const wrapper = createWrapper({ background: false }); - expect(wrapper.props("background")).toBe(false); - }); - }); - - describe("事件", () => { - it("应该在页码改变时触发 pagination 事件", async () => { - const wrapper = createWrapper(); - - // 模拟页码改变 - await wrapper.vm.handleCurrentChange(2); - - expect(wrapper.emitted("pagination")).toBeTruthy(); - expect(wrapper.emitted("pagination")?.[0]).toEqual([{ page: 2, limit: 10 }]); - }); - - it("应该在每页条数改变时触发 pagination 事件", async () => { - const wrapper = createWrapper(); - - // 模拟每页条数改变 - await wrapper.vm.handleSizeChange(20); - - expect(wrapper.emitted("pagination")).toBeTruthy(); - expect(wrapper.emitted("pagination")?.[0]).toEqual([{ page: 1, limit: 20 }]); - }); - - it("应该在每页条数改变时重置页码为 1", async () => { - const wrapper = createWrapper({ page: 3 }); - - await wrapper.vm.handleSizeChange(20); - - expect(wrapper.emitted("pagination")?.[0]).toEqual([{ page: 1, limit: 20 }]); - }); - }); - - describe("边界情况", () => { - it("应该在 total 为 0 时正常工作", () => { - const wrapper = createWrapper({ total: 0 }); - expect(wrapper.exists()).toBe(true); - }); - - it("应该在当前页超出范围时自动调整", async () => { - const wrapper = createWrapper({ page: 5, limit: 10, total: 100 }); - - // 修改 total 使当前页超出范围 - await wrapper.setProps({ total: 20 }); - - // 应该触发 pagination 事件,页码调整为最后一页 - expect(wrapper.emitted("pagination")).toBeTruthy(); - }); - }); -}); diff --git a/tests/unit/composables/useDictSync.test.ts b/tests/unit/composables/useDictSync.test.ts deleted file mode 100644 index cd4919ca..00000000 --- a/tests/unit/composables/useDictSync.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { useDictSync } from "@/composables/websocket/useDictSync"; -import { useDictStore } from "@/store/modules/dict"; -import { setActivePinia, createPinia } from "pinia"; - -// Mock useStomp -vi.mock("@/composables/websocket/useStomp", () => ({ - useStomp: vi.fn(() => ({ - isConnected: ref(false), - connect: vi.fn(), - disconnect: vi.fn(), - subscribe: vi.fn(() => "sub-id"), - unsubscribe: vi.fn(), - })), -})); - -describe("useDictSync", () => { - beforeEach(() => { - setActivePinia(createPinia()); - localStorage.clear(); - vi.clearAllMocks(); - }); - - describe("初始化", () => { - it("应该创建字典同步实例", () => { - const dictSync = useDictSync(); - expect(dictSync).toBeDefined(); - expect(dictSync.initialize).toBeDefined(); - expect(dictSync.cleanup).toBeDefined(); - }); - - it("应该初始化 WebSocket 连接", () => { - const dictSync = useDictSync(); - dictSync.initialize(); - // 验证初始化逻辑(具体实现取决于 useDictSync 的内部逻辑) - expect(dictSync).toBeDefined(); - }); - }); - - describe("清理", () => { - it("应该清理 WebSocket 连接", () => { - const dictSync = useDictSync(); - dictSync.initialize(); - dictSync.cleanup(); - // 验证清理逻辑 - expect(dictSync).toBeDefined(); - }); - }); - - describe("字典同步", () => { - it("应该处理字典更新消息", () => { - const dictSync = useDictSync(); - const dictStore = useDictStore(); - - dictSync.initialize(); - - // 模拟接收字典更新消息 - // 注意:这里需要根据实际的消息格式来测试 - expect(dictStore).toBeDefined(); - }); - }); -}); diff --git a/tests/unit/composables/useTableSelection.test.ts b/tests/unit/composables/useTableSelection.test.ts deleted file mode 100644 index d0cef0f0..00000000 --- a/tests/unit/composables/useTableSelection.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { useTableSelection } from "@/composables/table/useTableSelection"; - -describe("useTableSelection", () => { - let selection: ReturnType; - - beforeEach(() => { - selection = useTableSelection(); - }); - - it("应该初始化为空数组", () => { - expect(selection.selectedIds.value).toEqual([]); - expect(selection.selectedRows.value).toEqual([]); - }); - - describe("handleSelectionChange()", () => { - it("应该更新选中的行", () => { - const rows = [ - { id: 1, name: "张三" }, - { id: 2, name: "李四" }, - ]; - - selection.handleSelectionChange(rows); - - expect(selection.selectedRows.value).toEqual(rows); - expect(selection.selectedIds.value).toEqual([1, 2]); - }); - - it("应该处理空数组", () => { - selection.handleSelectionChange([]); - - expect(selection.selectedRows.value).toEqual([]); - expect(selection.selectedIds.value).toEqual([]); - }); - - it("应该支持自定义 ID 字段", () => { - const customSelection = useTableSelection("userId"); - const rows = [ - { userId: "u1", name: "张三" }, - { userId: "u2", name: "李四" }, - ]; - - customSelection.handleSelectionChange(rows); - - expect(customSelection.selectedIds.value).toEqual(["u1", "u2"]); - }); - }); - - describe("clearSelection()", () => { - it("应该清空选中项", () => { - const rows = [{ id: 1, name: "张三" }]; - selection.handleSelectionChange(rows); - - selection.clearSelection(); - - expect(selection.selectedIds.value).toEqual([]); - expect(selection.selectedRows.value).toEqual([]); - }); - }); - - describe("hasSelection", () => { - it("有选中项时应该返回 true", () => { - const rows = [{ id: 1, name: "张三" }]; - selection.handleSelectionChange(rows); - - expect(selection.hasSelection.value).toBe(true); - }); - - it("无选中项时应该返回 false", () => { - expect(selection.hasSelection.value).toBe(false); - }); - }); - - describe("selectionCount", () => { - it("应该返回选中项数量", () => { - expect(selection.selectionCount.value).toBe(0); - - const rows = [ - { id: 1, name: "张三" }, - { id: 2, name: "李四" }, - { id: 3, name: "王五" }, - ]; - selection.handleSelectionChange(rows); - - expect(selection.selectionCount.value).toBe(3); - }); - }); -}); diff --git a/tests/unit/store/app.test.ts b/tests/unit/store/app.test.ts deleted file mode 100644 index 3edd7d04..00000000 --- a/tests/unit/store/app.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { setActivePinia, createPinia } from "pinia"; -import { useAppStore } from "@/store/modules/app"; -import { DeviceEnum, SidebarStatus } from "@/enums"; - -describe("useAppStore", () => { - beforeEach(() => { - setActivePinia(createPinia()); - localStorage.clear(); - }); - - describe("侧边栏状态", () => { - it("应该切换侧边栏状态", () => { - const store = useAppStore(); - const initialState = store.sidebar.opened; - - store.toggleSidebar(); - expect(store.sidebar.opened).toBe(!initialState); - - store.toggleSidebar(); - expect(store.sidebar.opened).toBe(initialState); - }); - - it("应该关闭侧边栏", () => { - const store = useAppStore(); - store.openSideBar(); - expect(store.sidebar.opened).toBe(true); - - store.closeSideBar(); - expect(store.sidebar.opened).toBe(false); - }); - - it("应该打开侧边栏", () => { - const store = useAppStore(); - store.closeSideBar(); - expect(store.sidebar.opened).toBe(false); - - store.openSideBar(); - expect(store.sidebar.opened).toBe(true); - }); - - it("应该持久化侧边栏状态", () => { - const store = useAppStore(); - store.openSideBar(); - - // 创建新的 store 实例模拟页面刷新 - const newStore = useAppStore(); - expect(newStore.sidebar.opened).toBe(true); - }); - }); - - describe("设备类型", () => { - it("应该切换设备类型", () => { - const store = useAppStore(); - - store.toggleDevice(DeviceEnum.MOBILE); - expect(store.device).toBe(DeviceEnum.MOBILE); - - store.toggleDevice(DeviceEnum.DESKTOP); - expect(store.device).toBe(DeviceEnum.DESKTOP); - }); - - it("应该持久化设备类型", () => { - const store = useAppStore(); - store.toggleDevice(DeviceEnum.MOBILE); - - const newStore = useAppStore(); - expect(newStore.device).toBe(DeviceEnum.MOBILE); - }); - }); - - describe("组件尺寸", () => { - it("应该修改组件尺寸", () => { - const store = useAppStore(); - - store.changeSize("large"); - expect(store.size).toBe("large"); - - store.changeSize("small"); - expect(store.size).toBe("small"); - }); - - it("应该持久化组件尺寸", () => { - const store = useAppStore(); - store.changeSize("large"); - - const newStore = useAppStore(); - expect(newStore.size).toBe("large"); - }); - }); - - describe("语言设置", () => { - it("应该修改语言", () => { - const store = useAppStore(); - - store.changeLanguage("en"); - expect(store.language).toBe("en"); - - store.changeLanguage("zh-cn"); - expect(store.language).toBe("zh-cn"); - }); - - it("应该根据语言返回正确的 locale", () => { - const store = useAppStore(); - - store.changeLanguage("en"); - expect(store.locale).toBeDefined(); - - store.changeLanguage("zh-cn"); - expect(store.locale).toBeDefined(); - }); - - it("应该持久化语言设置", () => { - const store = useAppStore(); - store.changeLanguage("en"); - - const newStore = useAppStore(); - expect(newStore.language).toBe("en"); - }); - }); - - describe("顶部菜单", () => { - it("应该激活顶部菜单", () => { - const store = useAppStore(); - - store.activeTopMenu("/dashboard"); - expect(store.activeTopMenuPath).toBe("/dashboard"); - - store.activeTopMenu("/system"); - expect(store.activeTopMenuPath).toBe("/system"); - }); - - it("应该持久化顶部菜单路径", () => { - const store = useAppStore(); - store.activeTopMenu("/dashboard"); - - const newStore = useAppStore(); - expect(newStore.activeTopMenuPath).toBe("/dashboard"); - }); - }); -}); diff --git a/tests/unit/store/dict.test.ts b/tests/unit/store/dict.test.ts deleted file mode 100644 index 1e46610f..00000000 --- a/tests/unit/store/dict.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { setActivePinia, createPinia } from "pinia"; -import { useDictStore } from "@/store/modules/dict"; -import DictAPI from "@/api/system/dict"; -import type { DictItemOption } from "@/types/api"; - -// Mock DictAPI -vi.mock("@/api/system/dict", () => ({ - default: { - getDictItems: vi.fn(), - }, -})); - -describe("useDictStore", () => { - beforeEach(() => { - // 创建新的 Pinia 实例 - setActivePinia(createPinia()); - // 清理 localStorage - localStorage.clear(); - // 重置所有 mock - vi.clearAllMocks(); - }); - - describe("字典缓存", () => { - it("应该缓存字典数据", async () => { - const store = useDictStore(); - const mockData: DictItemOption[] = [ - { value: "1", label: "选项1" }, - { value: "2", label: "选项2" }, - ]; - - vi.mocked(DictAPI.getDictItems).mockResolvedValue(mockData); - - await store.loadDictItems("test_dict"); - - expect(store.getDictItems("test_dict")).toEqual(mockData); - expect(DictAPI.getDictItems).toHaveBeenCalledWith("test_dict"); - expect(DictAPI.getDictItems).toHaveBeenCalledTimes(1); - }); - - it("应该从缓存中获取字典数据,不重复请求", async () => { - const store = useDictStore(); - const mockData: DictItemOption[] = [{ value: "1", label: "选项1" }]; - - vi.mocked(DictAPI.getDictItems).mockResolvedValue(mockData); - - // 第一次加载 - await store.loadDictItems("test_dict"); - // 第二次加载(应该从缓存获取) - await store.loadDictItems("test_dict"); - - expect(DictAPI.getDictItems).toHaveBeenCalledTimes(1); - expect(store.getDictItems("test_dict")).toEqual(mockData); - }); - - it("应该防止并发重复请求", async () => { - const store = useDictStore(); - const mockData: DictItemOption[] = [{ value: "1", label: "选项1" }]; - - vi.mocked(DictAPI.getDictItems).mockResolvedValue(mockData); - - // 同时发起多个请求 - const promises = [ - store.loadDictItems("test_dict"), - store.loadDictItems("test_dict"), - store.loadDictItems("test_dict"), - ]; - - await Promise.all(promises); - - // 应该只请求一次 - expect(DictAPI.getDictItems).toHaveBeenCalledTimes(1); - }); - }); - - describe("字典操作", () => { - it("应该返回空数组当字典不存在时", () => { - const store = useDictStore(); - expect(store.getDictItems("non_exist")).toEqual([]); - }); - - it("应该移除指定字典项", async () => { - const store = useDictStore(); - const mockData: DictItemOption[] = [{ value: "1", label: "选项1" }]; - - vi.mocked(DictAPI.getDictItems).mockResolvedValue(mockData); - - await store.loadDictItems("test_dict"); - expect(store.getDictItems("test_dict")).toEqual(mockData); - - store.removeDictItem("test_dict"); - expect(store.getDictItems("test_dict")).toEqual([]); - }); - - it("应该清空所有字典缓存", async () => { - const store = useDictStore(); - const mockData1: DictItemOption[] = [{ value: "1", label: "选项1" }]; - const mockData2: DictItemOption[] = [{ value: "2", label: "选项2" }]; - - vi.mocked(DictAPI.getDictItems) - .mockResolvedValueOnce(mockData1) - .mockResolvedValueOnce(mockData2); - - await store.loadDictItems("dict1"); - await store.loadDictItems("dict2"); - - expect(store.getDictItems("dict1")).toEqual(mockData1); - expect(store.getDictItems("dict2")).toEqual(mockData2); - - store.clearDictCache(); - - expect(store.getDictItems("dict1")).toEqual([]); - expect(store.getDictItems("dict2")).toEqual([]); - }); - }); - - describe("错误处理", () => { - it("应该处理请求失败的情况", async () => { - const store = useDictStore(); - const error = new Error("Network error"); - - vi.mocked(DictAPI.getDictItems).mockRejectedValue(error); - - await expect(store.loadDictItems("test_dict")).rejects.toThrow("Network error"); - - // 失败后应该允许重试 - const mockData: DictItemOption[] = [{ value: "1", label: "选项1" }]; - vi.mocked(DictAPI.getDictItems).mockResolvedValue(mockData); - - await store.loadDictItems("test_dict"); - expect(store.getDictItems("test_dict")).toEqual(mockData); - }); - }); -}); diff --git a/tests/unit/store/settings.test.ts b/tests/unit/store/settings.test.ts deleted file mode 100644 index 622ebdf8..00000000 --- a/tests/unit/store/settings.test.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { setActivePinia, createPinia } from "pinia"; -import { useSettingsStore } from "@/store/modules/settings"; -import { ThemeMode, SidebarColor } from "@/enums"; - -// Mock theme utils -vi.mock("@/utils/theme", () => ({ - applyTheme: vi.fn(), - generateThemeColors: vi.fn(() => ({})), - toggleDarkMode: vi.fn(), - toggleSidebarColor: vi.fn(), -})); - -describe("useSettingsStore", () => { - beforeEach(() => { - setActivePinia(createPinia()); - localStorage.clear(); - vi.clearAllMocks(); - }); - - describe("界面显示设置", () => { - it("应该切换标签页显示", () => { - const store = useSettingsStore(); - const initial = store.showTagsView; - - store.showTagsView = !initial; - expect(store.showTagsView).toBe(!initial); - }); - - it("应该切换 Logo 显示", () => { - const store = useSettingsStore(); - const initial = store.showAppLogo; - - store.showAppLogo = !initial; - expect(store.showAppLogo).toBe(!initial); - }); - - it("应该切换水印显示", () => { - const store = useSettingsStore(); - const initial = store.showWatermark; - - store.showWatermark = !initial; - expect(store.showWatermark).toBe(!initial); - }); - - it("应该持久化界面显示设置", () => { - const store = useSettingsStore(); - store.showTagsView = false; - store.showAppLogo = false; - store.showWatermark = true; - - const newStore = useSettingsStore(); - expect(newStore.showTagsView).toBe(false); - expect(newStore.showAppLogo).toBe(false); - expect(newStore.showWatermark).toBe(true); - }); - }); - - describe("主题设置", () => { - it("应该切换主题模式", () => { - const store = useSettingsStore(); - - store.theme = ThemeMode.DARK; - expect(store.theme).toBe(ThemeMode.DARK); - - store.theme = ThemeMode.LIGHT; - expect(store.theme).toBe(ThemeMode.LIGHT); - }); - - it("应该修改主题颜色", () => { - const store = useSettingsStore(); - - store.themeColor = "#409EFF"; - expect(store.themeColor).toBe("#409EFF"); - - store.themeColor = "#67C23A"; - expect(store.themeColor).toBe("#67C23A"); - }); - - it("应该持久化主题设置", () => { - const store = useSettingsStore(); - store.theme = ThemeMode.DARK; - store.themeColor = "#409EFF"; - - const newStore = useSettingsStore(); - expect(newStore.theme).toBe(ThemeMode.DARK); - expect(newStore.themeColor).toBe("#409EFF"); - }); - }); - - describe("侧边栏配色", () => { - it("应该切换侧边栏配色方案", () => { - const store = useSettingsStore(); - - store.sidebarColorScheme = SidebarColor.CLASSIC_BLUE; - expect(store.sidebarColorScheme).toBe(SidebarColor.CLASSIC_BLUE); - - store.sidebarColorScheme = SidebarColor.THEME_COLOR; - expect(store.sidebarColorScheme).toBe(SidebarColor.THEME_COLOR); - }); - - it("应该持久化侧边栏配色", () => { - const store = useSettingsStore(); - store.sidebarColorScheme = SidebarColor.CLASSIC_BLUE; - - const newStore = useSettingsStore(); - expect(newStore.sidebarColorScheme).toBe(SidebarColor.CLASSIC_BLUE); - }); - }); - - describe("特殊模式", () => { - it("应该切换灰色模式", () => { - const store = useSettingsStore(); - - store.grayMode = true; - expect(store.grayMode).toBe(true); - - store.grayMode = false; - expect(store.grayMode).toBe(false); - }); - - it("应该切换色弱模式", () => { - const store = useSettingsStore(); - - store.colorWeak = true; - expect(store.colorWeak).toBe(true); - - store.colorWeak = false; - expect(store.colorWeak).toBe(false); - }); - - it("应该持久化特殊模式", () => { - const store = useSettingsStore(); - store.grayMode = true; - store.colorWeak = true; - - const newStore = useSettingsStore(); - expect(newStore.grayMode).toBe(true); - expect(newStore.colorWeak).toBe(true); - }); - }); - - describe("AI 助手", () => { - it("应该切换 AI 助手", () => { - const store = useSettingsStore(); - - store.userEnableAi = true; - expect(store.userEnableAi).toBe(true); - - store.userEnableAi = false; - expect(store.userEnableAi).toBe(false); - }); - - it("应该持久化 AI 助手设置", () => { - const store = useSettingsStore(); - store.userEnableAi = true; - - const newStore = useSettingsStore(); - expect(newStore.userEnableAi).toBe(true); - }); - }); - - describe("页面切换动画", () => { - it("应该修改页面切换动画", () => { - const store = useSettingsStore(); - - store.pageSwitchingAnimation = "fade"; - expect(store.pageSwitchingAnimation).toBe("fade"); - - store.pageSwitchingAnimation = "fade-slide"; - expect(store.pageSwitchingAnimation).toBe("fade-slide"); - - store.pageSwitchingAnimation = "fade-scale"; - expect(store.pageSwitchingAnimation).toBe("fade-scale"); - - store.pageSwitchingAnimation = "none"; - expect(store.pageSwitchingAnimation).toBe("none"); - }); - - it("应该持久化页面切换动画设置", () => { - const store = useSettingsStore(); - store.pageSwitchingAnimation = "fade-scale"; - - const newStore = useSettingsStore(); - expect(newStore.pageSwitchingAnimation).toBe("fade-scale"); - }); - - it("应该使用默认的页面切换动画", () => { - const store = useSettingsStore(); - // 默认值应该是 "fade-slide" - expect(store.pageSwitchingAnimation).toBe("fade-slide"); - }); - }); - - describe("重置设置", () => { - it("应该重置所有设置为默认值", () => { - const store = useSettingsStore(); - - // 修改所有设置 - store.showTagsView = false; - store.showAppLogo = false; - store.showWatermark = false; - store.pageSwitchingAnimation = "fade-slide"; - store.userEnableAi = true; - store.grayMode = true; - store.colorWeak = true; - store.theme = ThemeMode.DARK; - store.themeColor = "#409EFF"; - - // 重置 - store.resetSettings(); - - // 验证已重置(具体值取决于 defaults) - expect(store.userEnableAi).toBe(false); - expect(store.grayMode).toBe(false); - expect(store.colorWeak).toBe(false); - expect(store.pageSwitchingAnimation).toBe("fade-slide"); - }); - }); -}); diff --git a/tests/unit/utils/auth.test.ts b/tests/unit/utils/auth.test.ts deleted file mode 100644 index 6ee9c5fb..00000000 --- a/tests/unit/utils/auth.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { AuthStorage, hasPerm } from "@/utils/auth"; -import { Storage } from "@/utils/storage"; -import { STORAGE_KEYS, ROLE_ROOT } from "@/constants"; - -// Mock Storage -vi.mock("@/utils/storage", () => ({ - Storage: { - get: vi.fn(), - set: vi.fn(), - remove: vi.fn(), - sessionGet: vi.fn(), - sessionSet: vi.fn(), - sessionRemove: vi.fn(), - }, -})); - -// Mock useUserStoreHook -vi.mock("@/store/modules/user", () => ({ - useUserStoreHook: vi.fn(() => ({ - userInfo: { - roles: ["admin"], - perms: ["sys:user:create", "sys:user:update"], - }, - })), -})); - -describe("Auth 工具函数", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe("AuthStorage", () => { - describe("getAccessToken()", () => { - it("记住我为 true 时应该从 localStorage 获取", () => { - vi.mocked(Storage.get).mockReturnValueOnce(true); // rememberMe - vi.mocked(Storage.get).mockReturnValueOnce("token123"); // accessToken - - const token = AuthStorage.getAccessToken(); - - expect(Storage.get).toHaveBeenCalledWith(STORAGE_KEYS.REMEMBER_ME, false); - expect(Storage.get).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN, ""); - expect(token).toBe("token123"); - }); - - it("记住我为 false 时应该从 sessionStorage 获取", () => { - vi.mocked(Storage.get).mockReturnValueOnce(false); // rememberMe - vi.mocked(Storage.sessionGet).mockReturnValueOnce("session-token"); - - const token = AuthStorage.getAccessToken(); - - expect(Storage.sessionGet).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN, ""); - expect(token).toBe("session-token"); - }); - }); - - describe("getRefreshToken()", () => { - it("记住我为 true 时应该从 localStorage 获取", () => { - vi.mocked(Storage.get).mockReturnValueOnce(true); - vi.mocked(Storage.get).mockReturnValueOnce("refresh123"); - - const token = AuthStorage.getRefreshToken(); - - expect(token).toBe("refresh123"); - }); - - it("记住我为 false 时应该从 sessionStorage 获取", () => { - vi.mocked(Storage.get).mockReturnValueOnce(false); - vi.mocked(Storage.sessionGet).mockReturnValueOnce("session-refresh"); - - const token = AuthStorage.getRefreshToken(); - - expect(token).toBe("session-refresh"); - }); - }); - - describe("setTokens()", () => { - it("记住我为 true 时应该存储到 localStorage", () => { - AuthStorage.setTokens("access123", "refresh123", true); - - expect(Storage.set).toHaveBeenCalledWith(STORAGE_KEYS.REMEMBER_ME, true); - expect(Storage.set).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN, "access123"); - expect(Storage.set).toHaveBeenCalledWith(STORAGE_KEYS.REFRESH_TOKEN, "refresh123"); - }); - - it("记住我为 false 时应该存储到 sessionStorage", () => { - AuthStorage.setTokens("access123", "refresh123", false); - - expect(Storage.set).toHaveBeenCalledWith(STORAGE_KEYS.REMEMBER_ME, false); - expect(Storage.sessionSet).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN, "access123"); - expect(Storage.sessionSet).toHaveBeenCalledWith(STORAGE_KEYS.REFRESH_TOKEN, "refresh123"); - expect(Storage.remove).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN); - expect(Storage.remove).toHaveBeenCalledWith(STORAGE_KEYS.REFRESH_TOKEN); - }); - }); - - describe("clearAuth()", () => { - it("应该清理所有认证信息", () => { - AuthStorage.clearAuth(); - - expect(Storage.remove).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN); - expect(Storage.remove).toHaveBeenCalledWith(STORAGE_KEYS.REFRESH_TOKEN); - expect(Storage.sessionRemove).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN); - expect(Storage.sessionRemove).toHaveBeenCalledWith(STORAGE_KEYS.REFRESH_TOKEN); - }); - }); - - describe("getRememberMe()", () => { - it("应该获取记住我状态", () => { - vi.mocked(Storage.get).mockReturnValueOnce(true); - - const rememberMe = AuthStorage.getRememberMe(); - - expect(Storage.get).toHaveBeenCalledWith(STORAGE_KEYS.REMEMBER_ME, false); - expect(rememberMe).toBe(true); - }); - }); - }); - - describe("hasPerm()", () => { - it("超级管理员应该拥有所有按钮权限", () => { - const { useUserStoreHook } = await import("@/store/modules/user"); - vi.mocked(useUserStoreHook).mockReturnValueOnce({ - userInfo: { - roles: [ROLE_ROOT], - perms: [], - }, - } as any); - - expect(hasPerm("any:permission", "button")).toBe(true); - }); - - it("应该验证单个按钮权限", () => { - expect(hasPerm("sys:user:create", "button")).toBe(true); - expect(hasPerm("sys:user:delete", "button")).toBe(false); - }); - - it("应该验证多个按钮权限(或关系)", () => { - expect(hasPerm(["sys:user:create", "sys:user:delete"], "button")).toBe(true); - expect(hasPerm(["sys:user:delete", "sys:user:export"], "button")).toBe(false); - }); - - it("应该验证角色权限", () => { - expect(hasPerm("admin", "role")).toBe(true); - expect(hasPerm("user", "role")).toBe(false); - }); - - it("用户信息不完整时应该返回 false", () => { - const { useUserStoreHook } = await import("@/store/modules/user"); - vi.mocked(useUserStoreHook).mockReturnValueOnce({ - userInfo: {}, - } as any); - - expect(hasPerm("any:permission")).toBe(false); - }); - }); -}); diff --git a/tests/unit/utils/format.test.ts b/tests/unit/utils/format.test.ts deleted file mode 100644 index f3c19a69..00000000 --- a/tests/unit/utils/format.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { formatGrowthRate, formatFileSize, formatNumber, formatCurrency } from "@/utils/format"; - -describe("Format 工具函数", () => { - describe("formatGrowthRate()", () => { - it("应该格式化正增长率", () => { - expect(formatGrowthRate(0.25)).toBe("+25.00%"); - expect(formatGrowthRate(0.5)).toBe("+50.00%"); - expect(formatGrowthRate(1.5)).toBe("+150.00%"); - }); - - it("应该格式化负增长率", () => { - expect(formatGrowthRate(-0.25)).toBe("-25.00%"); - expect(formatGrowthRate(-0.5)).toBe("-50.00%"); - }); - - it("应该格式化零增长率", () => { - expect(formatGrowthRate(0)).toBe("0.00%"); - }); - - it("应该处理小数精度", () => { - expect(formatGrowthRate(0.12345)).toBe("+12.35%"); - expect(formatGrowthRate(0.12344)).toBe("+12.34%"); - }); - - it("应该处理 null 和 undefined", () => { - expect(formatGrowthRate(null as any)).toBe("0.00%"); - expect(formatGrowthRate(undefined as any)).toBe("0.00%"); - }); - }); - - describe("formatFileSize()", () => { - it("应该格式化字节", () => { - expect(formatFileSize(0)).toBe("0 B"); - expect(formatFileSize(100)).toBe("100 B"); - expect(formatFileSize(1023)).toBe("1023 B"); - }); - - it("应该格式化 KB", () => { - expect(formatFileSize(1024)).toBe("1.00 KB"); - expect(formatFileSize(1536)).toBe("1.50 KB"); - expect(formatFileSize(10240)).toBe("10.00 KB"); - }); - - it("应该格式化 MB", () => { - expect(formatFileSize(1048576)).toBe("1.00 MB"); - expect(formatFileSize(5242880)).toBe("5.00 MB"); - }); - - it("应该格式化 GB", () => { - expect(formatFileSize(1073741824)).toBe("1.00 GB"); - expect(formatFileSize(5368709120)).toBe("5.00 GB"); - }); - - it("应该格式化 TB", () => { - expect(formatFileSize(1099511627776)).toBe("1.00 TB"); - }); - - it("应该处理负数", () => { - expect(formatFileSize(-1024)).toBe("0 B"); - }); - - it("应该处理 null 和 undefined", () => { - expect(formatFileSize(null as any)).toBe("0 B"); - expect(formatFileSize(undefined as any)).toBe("0 B"); - }); - }); - - describe("formatNumber()", () => { - it("应该格式化整数", () => { - expect(formatNumber(1000)).toBe("1,000"); - expect(formatNumber(1000000)).toBe("1,000,000"); - expect(formatNumber(123456789)).toBe("123,456,789"); - }); - - it("应该格式化小数", () => { - expect(formatNumber(1000.5)).toBe("1,000.5"); - expect(formatNumber(1234.56)).toBe("1,234.56"); - }); - - it("应该处理小数位数", () => { - expect(formatNumber(1234.5678, 2)).toBe("1,234.57"); - expect(formatNumber(1234.5, 2)).toBe("1,234.50"); - expect(formatNumber(1234, 2)).toBe("1,234.00"); - }); - - it("应该处理零", () => { - expect(formatNumber(0)).toBe("0"); - expect(formatNumber(0, 2)).toBe("0.00"); - }); - - it("应该处理负数", () => { - expect(formatNumber(-1000)).toBe("-1,000"); - expect(formatNumber(-1234.56, 2)).toBe("-1,234.56"); - }); - - it("应该处理 null 和 undefined", () => { - expect(formatNumber(null as any)).toBe("0"); - expect(formatNumber(undefined as any)).toBe("0"); - }); - }); - - describe("formatCurrency()", () => { - it("应该格式化货币(默认人民币)", () => { - expect(formatCurrency(1000)).toBe("¥1,000.00"); - expect(formatCurrency(1234.56)).toBe("¥1,234.56"); - expect(formatCurrency(1000000)).toBe("¥1,000,000.00"); - }); - - it("应该格式化美元", () => { - expect(formatCurrency(1000, "USD")).toBe("$1,000.00"); - expect(formatCurrency(1234.56, "USD")).toBe("$1,234.56"); - }); - - it("应该格式化欧元", () => { - expect(formatCurrency(1000, "EUR")).toBe("€1,000.00"); - }); - - it("应该格式化日元", () => { - expect(formatCurrency(1000, "JPY")).toBe("¥1,000"); - }); - - it("应该处理零", () => { - expect(formatCurrency(0)).toBe("¥0.00"); - }); - - it("应该处理负数", () => { - expect(formatCurrency(-1000)).toBe("-¥1,000.00"); - }); - - it("应该处理 null 和 undefined", () => { - expect(formatCurrency(null as any)).toBe("¥0.00"); - expect(formatCurrency(undefined as any)).toBe("¥0.00"); - }); - }); -}); diff --git a/tests/unit/utils/storage.test.ts b/tests/unit/utils/storage.test.ts deleted file mode 100644 index 040cfc88..00000000 --- a/tests/unit/utils/storage.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { Storage } from "@/utils/storage"; -import { STORAGE_KEYS, APP_PREFIX } from "@/constants"; - -describe("Storage 工具类", () => { - // 每个测试前清理存储 - beforeEach(() => { - localStorage.clear(); - sessionStorage.clear(); - }); - - // 每个测试后清理存储 - afterEach(() => { - localStorage.clear(); - sessionStorage.clear(); - }); - - describe("localStorage 操作", () => { - it("应该能够存储和获取字符串", () => { - Storage.set("test-key", "test-value"); - expect(Storage.get("test-key")).toBe("test-value"); - }); - - it("应该能够存储和获取对象", () => { - const testObj = { name: "张三", age: 25 }; - Storage.set("test-obj", testObj); - expect(Storage.get("test-obj")).toEqual(testObj); - }); - - it("应该能够存储和获取数组", () => { - const testArr = [1, 2, 3, 4, 5]; - Storage.set("test-arr", testArr); - expect(Storage.get("test-arr")).toEqual(testArr); - }); - - it("应该能够存储和获取布尔值", () => { - Storage.set("test-bool", true); - expect(Storage.get("test-bool")).toBe(true); - }); - - it("获取不存在的键应该返回 undefined", () => { - expect(Storage.get("non-existent")).toBeUndefined(); - }); - - it("获取不存在的键应该返回默认值", () => { - expect(Storage.get("non-existent", "default")).toBe("default"); - }); - - it("应该能够删除存储项", () => { - Storage.set("test-key", "test-value"); - Storage.remove("test-key"); - expect(Storage.get("test-key")).toBeUndefined(); - }); - }); - - describe("sessionStorage 操作", () => { - it("应该能够存储和获取字符串", () => { - Storage.sessionSet("test-key", "test-value"); - expect(Storage.sessionGet("test-key")).toBe("test-value"); - }); - - it("应该能够存储和获取对象", () => { - const testObj = { name: "李四", age: 30 }; - Storage.sessionSet("test-obj", testObj); - expect(Storage.sessionGet("test-obj")).toEqual(testObj); - }); - - it("获取不存在的键应该返回默认值", () => { - expect(Storage.sessionGet("non-existent", "default")).toBe("default"); - }); - - it("应该能够删除存储项", () => { - Storage.sessionSet("test-key", "test-value"); - Storage.sessionRemove("test-key"); - expect(Storage.sessionGet("test-key")).toBeUndefined(); - }); - }); - - describe("批量清理操作", () => { - it("clear() 应该同时清理 localStorage 和 sessionStorage", () => { - Storage.set("test-key", "local-value"); - Storage.sessionSet("test-key", "session-value"); - - Storage.clear("test-key"); - - expect(Storage.get("test-key")).toBeUndefined(); - expect(Storage.sessionGet("test-key")).toBeUndefined(); - }); - - it("clearMultiple() 应该批量清理多个键", () => { - Storage.set("key1", "value1"); - Storage.set("key2", "value2"); - Storage.sessionSet("key1", "session1"); - - Storage.clearMultiple(["key1", "key2"]); - - expect(Storage.get("key1")).toBeUndefined(); - expect(Storage.get("key2")).toBeUndefined(); - expect(Storage.sessionGet("key1")).toBeUndefined(); - }); - - it("clearByPrefix() 应该清理指定前缀的所有存储项", () => { - Storage.set(`${APP_PREFIX}:auth:token`, "token123"); - Storage.set(`${APP_PREFIX}:auth:user`, "user123"); - Storage.set(`${APP_PREFIX}:ui:theme`, "dark"); - Storage.set("other:key", "other-value"); - - Storage.clearByPrefix(`${APP_PREFIX}:auth:`); - - expect(Storage.get(`${APP_PREFIX}:auth:token`)).toBeUndefined(); - expect(Storage.get(`${APP_PREFIX}:auth:user`)).toBeUndefined(); - expect(Storage.get(`${APP_PREFIX}:ui:theme`)).toBe("dark"); - expect(Storage.get("other:key")).toBe("other-value"); - }); - - it("clearAllProject() 应该清理所有项目相关的存储", () => { - Storage.set(STORAGE_KEYS.ACCESS_TOKEN, "token123"); - Storage.set(STORAGE_KEYS.THEME, "dark"); - Storage.set("other:key", "other-value"); - - Storage.clearAllProject(); - - expect(Storage.get(STORAGE_KEYS.ACCESS_TOKEN)).toBeUndefined(); - expect(Storage.get(STORAGE_KEYS.THEME)).toBeUndefined(); - expect(Storage.get("other:key")).toBe("other-value"); - }); - }); - - describe("边界情况", () => { - it("应该能够处理 null 值", () => { - Storage.set("test-null", null); - expect(Storage.get("test-null")).toBeNull(); - }); - - it("应该能够处理空字符串", () => { - Storage.set("test-empty", ""); - expect(Storage.get("test-empty")).toBe(""); - }); - - it("应该能够处理数字 0", () => { - Storage.set("test-zero", 0); - expect(Storage.get("test-zero")).toBe(0); - }); - - it("应该能够处理 false", () => { - Storage.set("test-false", false); - expect(Storage.get("test-false")).toBe(false); - }); - }); -}); diff --git a/tests/unit/utils/validate.test.ts b/tests/unit/utils/validate.test.ts deleted file mode 100644 index beb9b5c5..00000000 --- a/tests/unit/utils/validate.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { isExternal, isValidURL, isEmail, isMobile, VALIDATORS } from "@/utils/validate"; - -describe("Validate 工具函数", () => { - describe("isExternal()", () => { - it("应该识别外部链接", () => { - expect(isExternal("https://www.example.com")).toBe(true); - expect(isExternal("http://example.com")).toBe(true); - expect(isExternal("//example.com")).toBe(true); - expect(isExternal("mailto:test@example.com")).toBe(true); - expect(isExternal("tel:1234567890")).toBe(true); - }); - - it("应该识别内部链接", () => { - expect(isExternal("/dashboard")).toBe(false); - expect(isExternal("dashboard")).toBe(false); - expect(isExternal("./dashboard")).toBe(false); - expect(isExternal("../dashboard")).toBe(false); - }); - - it("应该处理空字符串", () => { - expect(isExternal("")).toBe(false); - }); - }); - - describe("isValidURL()", () => { - it("应该验证有效的 URL", () => { - expect(isValidURL("https://www.example.com")).toBe(true); - expect(isValidURL("http://example.com")).toBe(true); - expect(isValidURL("https://example.com/path?query=1")).toBe(true); - expect(isValidURL("http://localhost:3000")).toBe(true); - }); - - it("应该拒绝无效的 URL", () => { - expect(isValidURL("not-a-url")).toBe(false); - expect(isValidURL("//example.com")).toBe(false); - expect(isValidURL("/path")).toBe(false); - expect(isValidURL("")).toBe(false); - }); - }); - - describe("isEmail()", () => { - it("应该验证有效的邮箱", () => { - expect(isEmail("test@example.com")).toBe(true); - expect(isEmail("user.name@example.com")).toBe(true); - expect(isEmail("user+tag@example.co.uk")).toBe(true); - expect(isEmail("123@example.com")).toBe(true); - }); - - it("应该拒绝无效的邮箱", () => { - expect(isEmail("invalid")).toBe(false); - expect(isEmail("@example.com")).toBe(false); - expect(isEmail("user@")).toBe(false); - expect(isEmail("user @example.com")).toBe(false); - expect(isEmail("")).toBe(false); - }); - }); - - describe("isMobile()", () => { - it("应该验证有效的手机号", () => { - expect(isMobile("13800138000")).toBe(true); - expect(isMobile("15912345678")).toBe(true); - expect(isMobile("18612345678")).toBe(true); - expect(isMobile("19912345678")).toBe(true); - }); - - it("应该拒绝无效的手机号", () => { - expect(isMobile("12345678901")).toBe(false); // 不是 1 开头的有效号段 - expect(isMobile("1381234567")).toBe(false); // 少于 11 位 - expect(isMobile("138123456789")).toBe(false); // 多于 11 位 - expect(isMobile("abcdefghijk")).toBe(false); // 非数字 - expect(isMobile("")).toBe(false); - }); - }); - - describe("VALIDATORS 对象", () => { - describe("required 验证器", () => { - it("应该验证必填项", () => { - const callback = vi.fn(); - - VALIDATORS.required({}, "test", callback); - expect(callback).toHaveBeenCalledWith(new Error("此项为必填项")); - - callback.mockClear(); - VALIDATORS.required({}, "", callback); - expect(callback).toHaveBeenCalledWith(new Error("此项为必填项")); - - callback.mockClear(); - VALIDATORS.required({}, null, callback); - expect(callback).toHaveBeenCalledWith(new Error("此项为必填项")); - - callback.mockClear(); - VALIDATORS.required({}, undefined, callback); - expect(callback).toHaveBeenCalledWith(new Error("此项为必填项")); - }); - - it("应该通过有效值", () => { - const callback = vi.fn(); - - VALIDATORS.required({}, "value", callback); - expect(callback).toHaveBeenCalledWith(); - - callback.mockClear(); - VALIDATORS.required({}, 0, callback); - expect(callback).toHaveBeenCalledWith(); - - callback.mockClear(); - VALIDATORS.required({}, false, callback); - expect(callback).toHaveBeenCalledWith(); - }); - }); - - describe("email 验证器", () => { - it("应该验证邮箱格式", () => { - const callback = vi.fn(); - - VALIDATORS.email({}, "invalid", callback); - expect(callback).toHaveBeenCalledWith(new Error("请输入正确的邮箱地址")); - - callback.mockClear(); - VALIDATORS.email({}, "test@example.com", callback); - expect(callback).toHaveBeenCalledWith(); - }); - - it("应该允许空值", () => { - const callback = vi.fn(); - - VALIDATORS.email({}, "", callback); - expect(callback).toHaveBeenCalledWith(); - - callback.mockClear(); - VALIDATORS.email({}, null, callback); - expect(callback).toHaveBeenCalledWith(); - }); - }); - - describe("mobile 验证器", () => { - it("应该验证手机号格式", () => { - const callback = vi.fn(); - - VALIDATORS.mobile({}, "12345678901", callback); - expect(callback).toHaveBeenCalledWith(new Error("请输入正确的手机号码")); - - callback.mockClear(); - VALIDATORS.mobile({}, "13800138000", callback); - expect(callback).toHaveBeenCalledWith(); - }); - - it("应该允许空值", () => { - const callback = vi.fn(); - - VALIDATORS.mobile({}, "", callback); - expect(callback).toHaveBeenCalledWith(); - }); - }); - - describe("url 验证器", () => { - it("应该验证 URL 格式", () => { - const callback = vi.fn(); - - VALIDATORS.url({}, "not-a-url", callback); - expect(callback).toHaveBeenCalledWith(new Error("请输入正确的 URL 地址")); - - callback.mockClear(); - VALIDATORS.url({}, "https://example.com", callback); - expect(callback).toHaveBeenCalledWith(); - }); - - it("应该允许空值", () => { - const callback = vi.fn(); - - VALIDATORS.url({}, "", callback); - expect(callback).toHaveBeenCalledWith(); - }); - }); - }); -}); diff --git a/vite.config.ts b/vite.config.ts index 2a4477fe..4bf2f050 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,7 +8,6 @@ import { ElementPlusResolver } from "unplugin-vue-components/resolvers"; import { mockDevServerPlugin } from "vite-plugin-mock-dev-server"; import UnoCSS from "unocss/vite"; -import { resolve } from "path"; import { name, version, engines, dependencies, devDependencies } from "./package.json"; // 平台的名称、版本、运行所需的 node 版本、依赖、构建时间的类型提示 @@ -17,8 +16,6 @@ const __APP_INFO__ = { buildTimestamp: Date.now(), }; -const pathSrc = resolve(__dirname, "src"); - // Vite配置 https://cn.vitejs.dev/config export default defineConfig(({ mode }: ConfigEnv): UserConfig => { const env = loadEnv(mode, process.cwd()); @@ -26,15 +23,14 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { return { resolve: { - alias: { - "@": pathSrc, - }, + // Vite 8 新特性:自动读取 tsconfig.json 中的 paths 别名 + tsconfigPaths: true, }, css: { preprocessorOptions: { // 定义全局 SCSS 变量 scss: { - additionalData: `@use "@/styles/variables.scss" as *;`, + additionalData: `@use "@/styles/_variables.scss" as *;`, }, }, }, @@ -62,7 +58,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { imports: ["vue", "@vueuse/core", "pinia", "vue-router", "vue-i18n"], resolvers: [ // 导入 Element Plus函数,如:ElMessage, ElMessageBox 等 - ElementPlusResolver({ importStyle: "sass" }), + ElementPlusResolver(), ], eslintrc: { enabled: false, @@ -78,7 +74,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { Components({ resolvers: [ // 导入 Element Plus 组件 - ElementPlusResolver({ importStyle: "sass" }), + ElementPlusResolver(), ], // 指定自定义组件位置(默认:src/components) dirs: ["src/components", "src/**/components"], @@ -108,84 +104,83 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { "sortablejs", "qs", "path-browserify", - "@stomp/stompjs", "@element-plus/icons-vue", "element-plus/es", "element-plus/es/locale/lang/en", "element-plus/es/locale/lang/zh-cn", - "element-plus/es/components/alert/style/index", - "element-plus/es/components/avatar/style/index", - "element-plus/es/components/backtop/style/index", - "element-plus/es/components/badge/style/index", - "element-plus/es/components/base/style/index", - "element-plus/es/components/breadcrumb-item/style/index", - "element-plus/es/components/breadcrumb/style/index", - "element-plus/es/components/button/style/index", - "element-plus/es/components/card/style/index", - "element-plus/es/components/cascader/style/index", - "element-plus/es/components/checkbox-group/style/index", - "element-plus/es/components/checkbox/style/index", - "element-plus/es/components/col/style/index", - "element-plus/es/components/color-picker/style/index", - "element-plus/es/components/config-provider/style/index", - "element-plus/es/components/date-picker/style/index", - "element-plus/es/components/descriptions-item/style/index", - "element-plus/es/components/descriptions/style/index", - "element-plus/es/components/dialog/style/index", - "element-plus/es/components/divider/style/index", - "element-plus/es/components/drawer/style/index", - "element-plus/es/components/dropdown-item/style/index", - "element-plus/es/components/dropdown-menu/style/index", - "element-plus/es/components/dropdown/style/index", - "element-plus/es/components/empty/style/index", - "element-plus/es/components/form-item/style/index", - "element-plus/es/components/form/style/index", - "element-plus/es/components/icon/style/index", - "element-plus/es/components/image-viewer/style/index", - "element-plus/es/components/image/style/index", - "element-plus/es/components/input-number/style/index", - "element-plus/es/components/input-tag/style/index", - "element-plus/es/components/input/style/index", - "element-plus/es/components/link/style/index", - "element-plus/es/components/loading/style/index", - "element-plus/es/components/menu-item/style/index", - "element-plus/es/components/menu/style/index", - "element-plus/es/components/message-box/style/index", - "element-plus/es/components/message/style/index", - "element-plus/es/components/notification/style/index", - "element-plus/es/components/option/style/index", - "element-plus/es/components/pagination/style/index", - "element-plus/es/components/popover/style/index", - "element-plus/es/components/progress/style/index", - "element-plus/es/components/radio-button/style/index", - "element-plus/es/components/radio-group/style/index", - "element-plus/es/components/radio/style/index", - "element-plus/es/components/row/style/index", - "element-plus/es/components/scrollbar/style/index", - "element-plus/es/components/select/style/index", - "element-plus/es/components/skeleton-item/style/index", - "element-plus/es/components/skeleton/style/index", - "element-plus/es/components/step/style/index", - "element-plus/es/components/steps/style/index", - "element-plus/es/components/sub-menu/style/index", - "element-plus/es/components/switch/style/index", - "element-plus/es/components/tab-pane/style/index", - "element-plus/es/components/table-column/style/index", - "element-plus/es/components/table/style/index", - "element-plus/es/components/tabs/style/index", - "element-plus/es/components/tag/style/index", - "element-plus/es/components/text/style/index", - "element-plus/es/components/time-picker/style/index", - "element-plus/es/components/time-select/style/index", - "element-plus/es/components/timeline-item/style/index", - "element-plus/es/components/timeline/style/index", - "element-plus/es/components/tooltip/style/index", - "element-plus/es/components/tree-select/style/index", - "element-plus/es/components/tree/style/index", - "element-plus/es/components/upload/style/index", - "element-plus/es/components/watermark/style/index", - "element-plus/es/components/checkbox-button/style/index", - "element-plus/es/components/space/style/index", + "element-plus/es/components/alert/style/css", + "element-plus/es/components/avatar/style/css", + "element-plus/es/components/backtop/style/css", + "element-plus/es/components/badge/style/css", + "element-plus/es/components/base/style/css", + "element-plus/es/components/breadcrumb-item/style/css", + "element-plus/es/components/breadcrumb/style/css", + "element-plus/es/components/button/style/css", + "element-plus/es/components/card/style/css", + "element-plus/es/components/cascader/style/css", + "element-plus/es/components/checkbox-group/style/css", + "element-plus/es/components/checkbox/style/css", + "element-plus/es/components/col/style/css", + "element-plus/es/components/color-picker/style/css", + "element-plus/es/components/config-provider/style/css", + "element-plus/es/components/date-picker/style/css", + "element-plus/es/components/descriptions-item/style/css", + "element-plus/es/components/descriptions/style/css", + "element-plus/es/components/dialog/style/css", + "element-plus/es/components/divider/style/css", + "element-plus/es/components/drawer/style/css", + "element-plus/es/components/dropdown-item/style/css", + "element-plus/es/components/dropdown-menu/style/css", + "element-plus/es/components/dropdown/style/css", + "element-plus/es/components/empty/style/css", + "element-plus/es/components/form-item/style/css", + "element-plus/es/components/form/style/css", + "element-plus/es/components/icon/style/css", + "element-plus/es/components/image-viewer/style/css", + "element-plus/es/components/image/style/css", + "element-plus/es/components/input-number/style/css", + "element-plus/es/components/input-tag/style/css", + "element-plus/es/components/input/style/css", + "element-plus/es/components/link/style/css", + "element-plus/es/components/loading/style/css", + "element-plus/es/components/menu-item/style/css", + "element-plus/es/components/menu/style/css", + "element-plus/es/components/message-box/style/css", + "element-plus/es/components/message/style/css", + "element-plus/es/components/notification/style/css", + "element-plus/es/components/option/style/css", + "element-plus/es/components/pagination/style/css", + "element-plus/es/components/popover/style/css", + "element-plus/es/components/progress/style/css", + "element-plus/es/components/radio-button/style/css", + "element-plus/es/components/radio-group/style/css", + "element-plus/es/components/radio/style/css", + "element-plus/es/components/row/style/css", + "element-plus/es/components/scrollbar/style/css", + "element-plus/es/components/select/style/css", + "element-plus/es/components/skeleton-item/style/css", + "element-plus/es/components/skeleton/style/css", + "element-plus/es/components/step/style/css", + "element-plus/es/components/steps/style/css", + "element-plus/es/components/sub-menu/style/css", + "element-plus/es/components/switch/style/css", + "element-plus/es/components/tab-pane/style/css", + "element-plus/es/components/table-column/style/css", + "element-plus/es/components/table/style/css", + "element-plus/es/components/tabs/style/css", + "element-plus/es/components/tag/style/css", + "element-plus/es/components/text/style/css", + "element-plus/es/components/time-picker/style/css", + "element-plus/es/components/time-select/style/css", + "element-plus/es/components/timeline-item/style/css", + "element-plus/es/components/timeline/style/css", + "element-plus/es/components/tooltip/style/css", + "element-plus/es/components/tree-select/style/css", + "element-plus/es/components/tree/style/css", + "element-plus/es/components/upload/style/css", + "element-plus/es/components/watermark/style/css", + "element-plus/es/components/checkbox-button/style/css", + "element-plus/es/components/space/style/css", ], }, // 构建配置 diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index 391e3f6f..00000000 --- a/vitest.config.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { defineConfig } from "vitest/config"; -import vue from "@vitejs/plugin-vue"; -import { resolve } from "path"; -import AutoImport from "unplugin-auto-import/vite"; -import Components from "unplugin-vue-components/vite"; -import { ElementPlusResolver } from "unplugin-vue-components/resolvers"; - -export default defineConfig({ - plugins: [ - vue(), - // API 自动导入 - AutoImport({ - imports: ["vue", "@vueuse/core", "pinia", "vue-router", "vue-i18n"], - resolvers: [ElementPlusResolver()], - dts: false, - }), - // 组件自动导入 - Components({ - resolvers: [ElementPlusResolver()], - dts: false, - }), - ], - test: { - // 使用 happy-dom 作为测试环境(比 jsdom 快) - environment: "happy-dom", - - // 全局测试 API(describe, it, expect 等) - globals: true, - - // 测试环境设置文件 - setupFiles: ["./tests/setup.ts"], - - // 覆盖率配置 - coverage: { - provider: "v8", - reporter: ["text", "json", "html"], - exclude: [ - "node_modules/", - "tests/", - "**/*.d.ts", - "**/*.config.*", - "**/mockData", - "**/.{idea,git,cache,output,temp}", - ], - }, - - // 测试文件匹配规则 - include: ["tests/**/*.{test,spec}.{js,ts}"], - - // 测试超时时间 - testTimeout: 10000, - }, - - resolve: { - alias: { - "@": resolve(__dirname, "./src"), - }, - }, -});