diff --git a/ClientApp/package-lock.json b/ClientApp/package-lock.json index fb440cd..afd8b4f 100644 --- a/ClientApp/package-lock.json +++ b/ClientApp/package-lock.json @@ -20,6 +20,7 @@ "@types/node": "^22.15.3", "@typescript-eslint/eslint-plugin": "^8.31.1", "@typescript-eslint/parser": "^8.31.1", + "@unocss/preset-mini": "^66.1.2", "@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue-jsx": "^4.1.2", "@vitest/eslint-plugin": "^1.1.39", @@ -36,6 +37,7 @@ "npm-run-all2": "^7.0.2", "prettier": "3.5.3", "typescript": "~5.8.0", + "unocss": "^66.1.2", "unplugin-auto-import": "^19.2.0", "unplugin-vue-components": "^28.5.0", "vite": "^6.2.4", @@ -58,6 +60,27 @@ "node": ">=6.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/install-pkg/node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "dev": true, + "license": "MIT" + }, "node_modules/@antfu/utils": { "version": "0.7.10", "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz", @@ -1399,6 +1422,53 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/@iconify/utils/-/utils-2.3.0.tgz", + "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.0.0", + "@antfu/utils": "^8.1.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.0", + "globals": "^15.14.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.0.0", + "mlly": "^1.7.4" + } + }, + "node_modules/@iconify/utils/node_modules/@antfu/utils": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-8.1.1.tgz", + "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@iconify/utils/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1902,6 +1972,22 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@quansync/fs": { + "version": "0.1.3", + "resolved": "https://registry.npmmirror.com/@quansync/fs/-/fs-0.1.3.tgz", + "integrity": "sha512-G0OnZbMWEs5LhDyqy2UL17vGhSVHkQIfVojMtEWVenvj0V5S84VBgy86kJIuNsGDp2p7sTKlpSIpBUWdC35OKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^0.2.10" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/@rollup/pluginutils": { "version": "5.1.4", "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", @@ -2502,6 +2588,486 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@unocss/astro": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/astro/-/astro-66.1.2.tgz", + "integrity": "sha512-QBcvrPp0F2jqe2Y/S/FQDmEmNlAhGjeWN5fkUGj02N7mXRrg0/VJxSpOJH6XHRWkMoFPoNNyEjHk563ODbjtHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2", + "@unocss/reset": "66.1.2", + "@unocss/vite": "66.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/@unocss/cli": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/cli/-/cli-66.1.2.tgz", + "integrity": "sha512-bYCRpkGMu0QwC6Ktq3S/HwtcIW8Famy0dXOu1RIAM1IT60lq+4S5UTEBPdwryoFgDBoVMB7KLUhPYiGQ3pmSTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@unocss/config": "66.1.2", + "@unocss/core": "66.1.2", + "@unocss/preset-uno": "66.1.2", + "cac": "^6.7.14", + "chokidar": "^3.6.0", + "colorette": "^2.0.20", + "consola": "^3.4.2", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "tinyglobby": "^0.2.13", + "unplugin-utils": "^0.2.4" + }, + "bin": { + "unocss": "bin/unocss.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/cli/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@unocss/cli/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@unocss/cli/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@unocss/config": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/config/-/config-66.1.2.tgz", + "integrity": "sha512-2sQXj+Qaq4RVDELVTPoXMggZ30g1WKHeCuur396I12Ab0HgAR6bTc/DIrNtqKVHFI3mmlvP1oM1ynhKWSKPsTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2", + "unconfig": "^7.3.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/core": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-66.1.2.tgz", + "integrity": "sha512-mN9h1hHEuhDcdbI4z74o7UnxlBZYVsJpYcdC1YLWBKROcLYTkuyZ7hgBzpo1FBNox2Bt3JnrSinVDmc44Bxjow==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/extractor-arbitrary-variants": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/extractor-arbitrary-variants/-/extractor-arbitrary-variants-66.1.2.tgz", + "integrity": "sha512-F570wH9VYeFTb4r8qgcbN5QpEVIAvFC1zOnrAPUr6B6kbU2YChMXxHP7PHK0AzLHnEr458Pwpzl6hmP6bzxZ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/inspector": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/inspector/-/inspector-66.1.2.tgz", + "integrity": "sha512-ftdZzFP5DAKDzgBI078xDDZbNNVq1RV/yhpNkviBvWCUsgRWc6o3G8swqJPIvFaphmUms0RIYH9shmXilVXFtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2", + "@unocss/rule-utils": "66.1.2", + "colorette": "^2.0.20", + "gzip-size": "^6.0.0", + "sirv": "^3.0.1", + "vue-flow-layout": "^0.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/postcss": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/postcss/-/postcss-66.1.2.tgz", + "integrity": "sha512-RCA3or1qBdRVduNW73xdeiFDCEb8cvcGKsHSN66rL66RrlzNnunE4NE55vbI+yoArTRZ7RdUnxq1KuXKjrJbYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/config": "66.1.2", + "@unocss/core": "66.1.2", + "@unocss/rule-utils": "66.1.2", + "css-tree": "^3.1.0", + "postcss": "^8.5.3", + "tinyglobby": "^0.2.13" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/@unocss/preset-attributify": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/preset-attributify/-/preset-attributify-66.1.2.tgz", + "integrity": "sha512-i7+LRtpxbtSzS+gHdc+aW99mGLYeR8hUnEWqFNnr+MiiyzbD8yFimye/u8TySSBLzPKGbLCb4YWVV684BuZgxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-icons": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/preset-icons/-/preset-icons-66.1.2.tgz", + "integrity": "sha512-14390jFBJ2anuKvjX9TeRCm7adNjR/mey0bh0+S/k/5W3VugIY2y0E+OH3m+sx5d/5ZUYbYkUGsmtuKbVNwwxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iconify/utils": "^2.3.0", + "@unocss/core": "66.1.2", + "ofetch": "^1.4.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-mini": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/preset-mini/-/preset-mini-66.1.2.tgz", + "integrity": "sha512-oiDe+VhwZ8B5Z0UGfggtOwgpRZMLtH1RTDFvmJmJEXYYX5BPWknS6wYcQzxy0i/y9ym0xp2QnEaTpGmR7LKdkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2", + "@unocss/extractor-arbitrary-variants": "66.1.2", + "@unocss/rule-utils": "66.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-tagify": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/preset-tagify/-/preset-tagify-66.1.2.tgz", + "integrity": "sha512-Xw5sFJGuzmGnfAXMI0kAiWDBh4DT3cOyphcyY9grBxbmxgqQDxRFHOV3Eg85lWK6X5cScOv3DhO0ndGv5ND8YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-typography": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/preset-typography/-/preset-typography-66.1.2.tgz", + "integrity": "sha512-+k9zp27Ak8rB6LPFDwq9fcwd3+ivFeSvXFQ2d4fBCwGGOAKHIA7qHLg3etxRaMhGd3YUPv/6d7FWpBbQgUVYZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2", + "@unocss/preset-mini": "66.1.2", + "@unocss/rule-utils": "66.1.2" + } + }, + "node_modules/@unocss/preset-uno": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/preset-uno/-/preset-uno-66.1.2.tgz", + "integrity": "sha512-JL9YkDwluu1YGhzBaxO60XkKtZBagL13z3K6dsjsghbs+dKVlh35rhlIm5TZ+NdLAzcLM8PHhXm2ausjSd54Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2", + "@unocss/preset-wind3": "66.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-web-fonts": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/preset-web-fonts/-/preset-web-fonts-66.1.2.tgz", + "integrity": "sha512-2ru+6jaac72oUx0kOBgNzbbkVe6oWKjqGmx24uK94fAcrP9eQyd+r7xiFpqXegrQ8+kONI66+HxAClvF2JHqdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2", + "ofetch": "^1.4.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-wind": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/preset-wind/-/preset-wind-66.1.2.tgz", + "integrity": "sha512-O3nIfbTbX/YRMFj7jNb7nHBDV47G79qOmyid4WPFZrPV3BbFAo94d/54kSoDVuc8jAt06YYQH9XC4ZeD59Sr3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2", + "@unocss/preset-wind3": "66.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-wind3": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/preset-wind3/-/preset-wind3-66.1.2.tgz", + "integrity": "sha512-S09imGOngAAOXCBCHb3JAtxD1/L7nDWrgEeX6NT0ElDp3X1T6XxUXYJlpjCfcqV/klMoXyYouKvp0YuG9QSgVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2", + "@unocss/preset-mini": "66.1.2", + "@unocss/rule-utils": "66.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-wind4": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/preset-wind4/-/preset-wind4-66.1.2.tgz", + "integrity": "sha512-03p4rpBAWzz58BzAiKsUuG+6YO7IG6mJMGQAtPzuhd+nVBJLIRa3eBIVXOPmAVz1rNx5XPRTAr6PMC7ycdMFRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2", + "@unocss/extractor-arbitrary-variants": "66.1.2", + "@unocss/rule-utils": "66.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/reset": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/reset/-/reset-66.1.2.tgz", + "integrity": "sha512-njNy/QCpuPKBFeEvhYGwwCe3t8R8JTxONsyUB9NsFOamkF13DSlEB4Yy/QLQfIinbbmx0F/wiej/JGOJk1ecDg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/rule-utils": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/rule-utils/-/rule-utils-66.1.2.tgz", + "integrity": "sha512-nn0ehvDh7yyWq2mcBDLVpmMAivjRATUroZ8ETinyN1rmfsGesm71R0d1gV3K+Z6YC7a3+dMLc+/qzI7VK3AG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "^66.1.2", + "magic-string": "^0.30.17" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/transformer-attributify-jsx": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/transformer-attributify-jsx/-/transformer-attributify-jsx-66.1.2.tgz", + "integrity": "sha512-PNwxpsQlBlTAyw1apIMyioeAKrLAf7axLDjZ4BW20WH7ql0GUwvMhuO/qzsWDpYWdtSlFnnAdWI2aCxyvhzdCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/transformer-compile-class": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/transformer-compile-class/-/transformer-compile-class-66.1.2.tgz", + "integrity": "sha512-viJetYFncLf9llxYQ7DKf5PuSJw08B7qhp0IXv/7ZG7agU09J1mlussC6ff+00iRoMxvG+5uXiYlTzL2vfikwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/transformer-directives": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/transformer-directives/-/transformer-directives-66.1.2.tgz", + "integrity": "sha512-A41/cPMB+BUEgnhz5kFiTYgSuCAziJy6hSlLYBDcrFbARUsvmhZFou0P2fRr3wDOFxD3BuApHjsefybKTh1UeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2", + "@unocss/rule-utils": "66.1.2", + "css-tree": "^3.1.0" + } + }, + "node_modules/@unocss/transformer-variant-group": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/transformer-variant-group/-/transformer-variant-group-66.1.2.tgz", + "integrity": "sha512-RfqJmeic4kAwS5OhSk/D00hqla+xXIw8AJH93jYqHfyDhJR5vddEAJi5RBMOL7y6vDQqRlUCEDQvfp3zSmi6iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/vite": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/@unocss/vite/-/vite-66.1.2.tgz", + "integrity": "sha512-ZJHN8+HKSrclVjT/+S7Vh2t59DK8J44d5nLZPG1Goua7uNK8yYJeOLK2sCGX7aackRer1ZynmglFFzxNFVt+IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@unocss/config": "66.1.2", + "@unocss/core": "66.1.2", + "@unocss/inspector": "66.1.2", + "chokidar": "^3.6.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "tinyglobby": "^0.2.13", + "unplugin-utils": "^0.2.4" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0" + } + }, + "node_modules/@unocss/vite/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@unocss/vite/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@unocss/vite/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/@vitejs/plugin-vue": { "version": "5.2.3", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz", @@ -3566,6 +4132,13 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz", @@ -3601,6 +4174,16 @@ "proto-list": "~1.2.1" } }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.0.0.tgz", @@ -3695,6 +4278,20 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", @@ -3840,6 +4437,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", @@ -3850,6 +4454,13 @@ "node": ">= 0.8" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "dev": true, + "license": "MIT" + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz", @@ -3880,6 +4491,13 @@ "node": ">= 0.4" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4912,6 +5530,22 @@ "integrity": "sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==", "license": "Standard 'no charge' license: https://gsap.com/standard-license." }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", @@ -5623,6 +6257,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz", @@ -5840,6 +6481,13 @@ "optional": true, "peer": true }, + "node_modules/node-fetch-native": { + "version": "1.6.6", + "resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.6.tgz", + "integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.19.tgz", @@ -6028,6 +6676,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ofetch": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/ofetch/-/ofetch-1.4.1.tgz", + "integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==", + "dev": true, + "license": "MIT", + "dependencies": { + "destr": "^2.0.3", + "node-fetch-native": "^1.6.4", + "ufo": "^1.5.4" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", @@ -6127,6 +6787,13 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/package-manager-detector": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.3.0.tgz", + "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", @@ -7492,6 +8159,22 @@ "dev": true, "license": "MIT" }, + "node_modules/unconfig": { + "version": "7.3.2", + "resolved": "https://registry.npmmirror.com/unconfig/-/unconfig-7.3.2.tgz", + "integrity": "sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^0.1.1", + "defu": "^6.1.4", + "jiti": "^2.4.2", + "quansync": "^0.2.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", @@ -7584,6 +8267,52 @@ "node": ">= 10.0.0" } }, + "node_modules/unocss": { + "version": "66.1.2", + "resolved": "https://registry.npmmirror.com/unocss/-/unocss-66.1.2.tgz", + "integrity": "sha512-mVwuXzIZ5Ex83F4w3XVJyp9DSbh5KhDzglyvMLktX8oU0QxQtaSpa5lE1twl3wgM0pVL9gmzD4a0FoYWZuJIDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/astro": "66.1.2", + "@unocss/cli": "66.1.2", + "@unocss/core": "66.1.2", + "@unocss/postcss": "66.1.2", + "@unocss/preset-attributify": "66.1.2", + "@unocss/preset-icons": "66.1.2", + "@unocss/preset-mini": "66.1.2", + "@unocss/preset-tagify": "66.1.2", + "@unocss/preset-typography": "66.1.2", + "@unocss/preset-uno": "66.1.2", + "@unocss/preset-web-fonts": "66.1.2", + "@unocss/preset-wind": "66.1.2", + "@unocss/preset-wind3": "66.1.2", + "@unocss/preset-wind4": "66.1.2", + "@unocss/transformer-attributify-jsx": "66.1.2", + "@unocss/transformer-compile-class": "66.1.2", + "@unocss/transformer-directives": "66.1.2", + "@unocss/transformer-variant-group": "66.1.2", + "@unocss/vite": "66.1.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@unocss/webpack": "66.1.2", + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0" + }, + "peerDependenciesMeta": { + "@unocss/webpack": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", @@ -8201,6 +8930,16 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/vue-flow-layout": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/vue-flow-layout/-/vue-flow-layout-0.1.1.tgz", + "integrity": "sha512-JdgRRUVrN0Y2GosA0M68DEbKlXMqJ7FQgsK8CjQD2vxvNSqAU6PZEpi4cfcTVtfM2GVOMjHo7GKKLbXxOBqDqA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vue": "^3.4.37" + } + }, "node_modules/vue-router": { "version": "4.5.1", "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz", diff --git a/ClientApp/src/composables/useFirewallRules.ts b/ClientApp/src/composables/useFirewallRules.ts new file mode 100644 index 0000000..00d8202 --- /dev/null +++ b/ClientApp/src/composables/useFirewallRules.ts @@ -0,0 +1,116 @@ +/** + * Time:2025/5/25 19:16 09 + * Name:useFirewallRules.ts + * Path:src/composables + * ProjectName:ClientApp + * Author:charlatans + * + * Il n'ya qu'un héroïsme au monde : + * c'est de voir le monde tel qu'il est et de l'aimer. + */ +// composables/useFirewallRules.ts - 防火墙规则管理 +import { ref, onMounted, onUnmounted } from 'vue' +import { useWebSocket } from './useWebSocket' +import type { FirewallRuleMessage } from '@/types/websocket' + +interface FirewallRule { + name: string + action: string + protocol: string + port: string + enabled: boolean +} + +export function useFirewallRules() { + const { sendCommand, onMessage, isConnected } = useWebSocket() + + const firewallRules = ref([]) + const loading = ref(false) + const cleanupFunctions: (() => void)[] = [] + + // 获取防火墙规则 + const fetchFirewallRules = async () => { + if (!isConnected.value) return + + loading.value = true + try { + await sendCommand('getFirewallRules') + } catch (error) { + console.error('获取防火墙规则失败:', error) + } finally { + loading.value = false + } + } + + // 添加防火墙规则 + const addFirewallRule = async (rule: Omit) => { + if (!isConnected.value) return false + + const ruleData: FirewallRuleMessage = { + Type: 'firewallRule', + RuleName: rule.name, + Action: rule.action, + Protocol: rule.protocol, + Port: rule.port, + Enabled: true, + Timestamp: new Date().toISOString() + } + + return await sendCommand('addFirewallRule', ruleData) + } + + // 处理防火墙规则响应 + const handleFirewallRulesResponse = (message: any) => { + if (message.Success && message.Data && Array.isArray(message.Data)) { + firewallRules.value = message.Data.map((item: FirewallRuleMessage) => ({ + name: item.RuleName, + action: item.Action, + protocol: item.Protocol, + port: item.Port, + enabled: item.Enabled + })) + } + } + + onMounted(() => { + // 监听防火墙规则响应 + cleanupFunctions.push( + onMessage('firewallRules', handleFirewallRulesResponse) + ) + + // 监听防火墙规则更新 + cleanupFunctions.push( + onMessage('firewallRulesUpdate', handleFirewallRulesResponse) + ) + + // 监听添加规则响应 + cleanupFunctions.push( + onMessage('addFirewallRuleResponse', (message) => { + if (message.Success) { + console.log('防火墙规则添加成功') + // 规则会通过广播更新自动刷新 + } else { + console.error('防火墙规则添加失败:', message.Message) + } + }) + ) + + // 连接成功后获取规则 + cleanupFunctions.push( + onMessage('welcome', () => { + fetchFirewallRules() + }) + ) + }) + + onUnmounted(() => { + cleanupFunctions.forEach(cleanup => cleanup()) + }) + + return { + firewallRules, + loading, + fetchFirewallRules, + addFirewallRule + } +} diff --git a/ClientApp/src/composables/useWebSocket.ts b/ClientApp/src/composables/useWebSocket.ts new file mode 100644 index 0000000..f6af114 --- /dev/null +++ b/ClientApp/src/composables/useWebSocket.ts @@ -0,0 +1,140 @@ + +// composables/useWebSocket.ts +import { onMounted, onUnmounted, watch } from 'vue' +import { useWebSocketStore } from '@/stores/websocket' +import type { ConnectionState } from '@/types/websocket' + +export interface UseWebSocketOptions { + url: string + autoConnect?: boolean + autoRefresh?: boolean + refreshInterval?: number + onConnected?: () => void + onDisconnected?: () => void + onError?: (error: string) => void + onStateChange?: (state: ConnectionState) => void +} + +export const useWebSocket = (options: UseWebSocketOptions) => { + const store = useWebSocketStore() + let refreshTimer: NodeJS.Timeout | null = null + + const { + url, + autoConnect = true, + autoRefresh = true, + refreshInterval = 5000, + onConnected, + onDisconnected, + onError, + onStateChange + } = options + + // 启动自动刷新 + const startAutoRefresh = (): void => { + if (!autoRefresh || refreshTimer) return + + refreshTimer = setInterval(async () => { + if (store.isConnected) { + try { + await store.fetchSystemInfo() + } catch (error) { + console.error('Auto refresh failed:', error) + } + } + }, refreshInterval) + } + + // 停止自动刷新 + const stopAutoRefresh = (): void => { + if (refreshTimer) { + clearInterval(refreshTimer) + refreshTimer = null + } + } + + // 手动连接 + const connect = async (): Promise => { + try { + await store.initialize(url) + if (autoRefresh) { + startAutoRefresh() + } + } catch (error) { + console.error('Connection failed:', error) + throw error + } + } + + // 手动断开连接 + const disconnect = (): void => { + stopAutoRefresh() + store.disconnect() + } + + // 监听连接状态变化 + watch( + () => store.connectionState, + (newState, oldState) => { + onStateChange?.(newState) + + if (newState === 'connected' && oldState !== 'connected') { + onConnected?.() + if (autoRefresh) { + startAutoRefresh() + } + } else if (newState === 'disconnected' && oldState === 'connected') { + stopAutoRefresh() + onDisconnected?.() + } + }, + { immediate: true } + ) + + // 监听错误 + watch( + () => store.lastError, + (error) => { + if (error) { + onError?.(error) + } + } + ) + + onMounted(() => { + if (autoConnect) { + connect().catch(console.error) + } + }) + + onUnmounted(() => { + disconnect() + }) + + return { + // 状态 + connectionState: store.connectionState, + isConnected: store.isConnected, + systemInfo: store.systemInfo, + networkStatus: store.networkStatus, + firewallRules: store.firewallRules, + lastError: store.lastError, + isLoading: store.isLoading, + connectionCount: store.connectionCount, + activeNetworkInterfaces: store.activeNetworkInterfaces, + + // 方法 + connect, + disconnect, + fetchSystemInfo: store.fetchSystemInfo, + fetchNetworkStatus: store.fetchNetworkStatus, + fetchFirewallRules: store.fetchFirewallRules, + addFirewallRule: store.addFirewallRule, + updateFirewallRule: store.updateFirewallRule, + deleteFirewallRule: store.deleteFirewallRule, + toggleFirewallRule: store.toggleFirewallRule, + clearError: store.clearError, + startAutoRefresh, + stopAutoRefresh + } +} diff --git a/ClientApp/src/composables/useWebSocketEnhanced.ts b/ClientApp/src/composables/useWebSocketEnhanced.ts new file mode 100644 index 0000000..00b8a3b --- /dev/null +++ b/ClientApp/src/composables/useWebSocketEnhanced.ts @@ -0,0 +1,381 @@ +/** + * Time:2025/5/25 20:42 29 + * Name:useWebSocketEnhanced.ts + * Path:src/composables + * ProjectName:ClientApp + * Author:charlatans + * + * Il n'ya qu'un héroïsme au monde : + * c'est de voir le monde tel qu'il est et de l'aimer. + */ + +// composables/useWebSocketEnhanced.ts - 增强的WebSocket管理 +import { ref, onUnmounted, watch } from 'vue'; +import { useWebSocketStore } from '@/stores/websocketStore'; +import { WebSocketRetryManager } from '@/utils/websocketHelpers'; +import type { NetworkStatus, ConnectionInfo, FirewallRule, WebSocketMessage, WebSocketCommand } from '@/types/websocket'; + +export class EnhancedWebSocketManager { + private ws: WebSocket | null = null; + private retryManager = new WebSocketRetryManager(); + private pingTimer: number | null = null; + private dataRefreshTimer: number | null = null; + private messageHandlers = new Map(); + private pendingCommands = new Map(); + private store = useWebSocketStore(); + + constructor(private url: string = 'ws://localhost:8080') { + this.setupEventHandlers(); + } + + async connect(): Promise { + if (this.ws?.readyState === WebSocket.OPEN) return; + + this.store.updateConnectionState(false, true); + + return new Promise((resolve, reject) => { + try { + this.ws = new WebSocket(this.url); + + this.ws.onopen = () => { + console.log('WebSocket connected successfully'); + this.store.updateConnectionState(true, false, 'excellent'); + this.retryManager.reset(); + this.startPing(); + this.startDataRefresh(); + resolve(); + }; + + this.ws.onmessage = (event) => { + this.handleMessage(event.data); + }; + + this.ws.onclose = (event) => { + console.log('WebSocket closed:', event.code, event.reason); + this.store.updateConnectionState(false, false); + this.stopPing(); + this.stopDataRefresh(); + + if (event.code !== 1000 && this.retryManager.shouldRetry()) { + this.scheduleReconnect(); + } + }; + + this.ws.onerror = (error) => { + console.error('WebSocket error:', error); + this.store.setError('WebSocket连接错误'); + this.store.updateConnectionState(false, false); + reject(error); + }; + + } catch (error) { + this.store.updateConnectionState(false, false); + this.store.setError('无法创建WebSocket连接'); + reject(error); + } + }); + } + + disconnect() { + this.stopPing(); + this.stopDataRefresh(); + this.retryManager.reset(); + + // 清理待处理的命令 + this.pendingCommands.forEach(({ reject, timeout }) => { + clearTimeout(timeout); + reject(new Error('Connection closed')); + }); + this.pendingCommands.clear(); + + if (this.ws) { + this.ws.close(1000, 'User initiated disconnect'); + this.ws = null; + } + + this.store.updateConnectionState(false, false, 'disconnected'); + } + + async sendCommand(action: WebSocketCommand['action'], data?: unknown): Promise { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error('WebSocket not connected'); + } + + const commandId = this.generateId(); + const command: WebSocketCommand = { action, data, id: commandId }; + const message: WebSocketMessage = { + type: 'command', + data: command, + timestamp: Date.now(), + id: commandId + }; + + return new Promise((resolve, reject) => { + // 设置超时 + const timeout = setTimeout(() => { + this.pendingCommands.delete(commandId); + reject(new Error(`Command timeout: ${action}`)); + }, 15000); + + // 存储待处理命令 + this.pendingCommands.set(commandId, { resolve, reject, timeout }); + + // 发送命令 + try { + this.ws!.send(JSON.stringify(message)); + } catch (error) { + this.pendingCommands.delete(commandId); + clearTimeout(timeout); + reject(error); + } + }); + } + + private handleMessage(data: string) { + try { + const message: WebSocketMessage = JSON.parse(data); + + switch (message.type) { + case 'network_status': + this.store.updateNetworkStatus(message.data); + this.emit('network_status', message.data); + break; + + case 'connection_info': + this.store.updateConnections(message.data); + this.emit('connection_info', message.data); + break; + + case 'firewall_rules': + this.store.updateFirewallRules(message.data); + this.emit('firewall_rules', message.data); + break; + + case 'response': + this.handleCommandResponse(message); + break; + + case 'error': + this.store.setError(message.data.message || '服务器错误'); + this.emit('error', message.data); + break; + + default: + this.emit('message', message); + } + } catch (error) { + console.error('Failed to parse WebSocket message:', error); + } + } + + private handleCommandResponse(message: WebSocketMessage) { + if (!message.id) return; + + const pending = this.pendingCommands.get(message.id); + if (pending) { + clearTimeout(pending.timeout); + this.pendingCommands.delete(message.id); + + if (message.data.success) { + pending.resolve(message.data.data); + } else { + pending.reject(new Error(message.data.error || 'Command failed')); + } + } + } + + private scheduleReconnect() { + const delay = this.retryManager.getNextDelay(); + console.log(`Reconnecting in ${delay}ms (attempt ${this.retryManager.getCurrentAttempt()})`); + + setTimeout(() => { + this.connect().catch(console.error); + }, delay); + } + + private startPing() { + this.pingTimer = setInterval(() => { + if (this.ws?.readyState === WebSocket.OPEN) { + this.sendPing().catch(() => { + this.store.updateConnectionState(true, false, 'poor'); + }); + } + }, 30000); + } + + private stopPing() { + if (this.pingTimer) { + clearInterval(this.pingTimer); + this.pingTimer = null; + } + } + + private startDataRefresh() { + // 定期刷新数据 + this.dataRefreshTimer = setInterval(() => { + this.refreshAllData().catch(console.error); + }, 5000); // 每5秒刷新一次 + } + + private stopDataRefresh() { + if (this.dataRefreshTimer) { + clearInterval(this.dataRefreshTimer); + this.dataRefreshTimer = null; + } + } + + private async sendPing() { + const message: WebSocketMessage = { + type: 'command', + data: { action: 'ping' }, + timestamp: Date.now() + }; + + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(message)); + } + } + + private async refreshAllData() { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return; + + try { + await Promise.all([ + this.sendCommand('get_network_status'), + this.sendCommand('get_connections'), + this.sendCommand('get_firewall_rules') + ]); + } catch (error) { + console.error('Failed to refresh data:', error); + } + } + + private setupEventHandlers() { + // 监听store状态变化 + watch(() => this.store.isConnected, (connected) => { + if (connected) { + this.refreshAllData(); + } + }); + } + + // 事件系统 + on(event: string, handler: Function) { + if (!this.messageHandlers.has(event)) { + this.messageHandlers.set(event, []); + } + this.messageHandlers.get(event)!.push(handler); + } + + off(event: string, handler: Function) { + const handlers = this.messageHandlers.get(event); + if (handlers) { + const index = handlers.indexOf(handler); + if (index > -1) { + handlers.splice(index, 1); + } + } + } + + private emit(event: string, data?: unknown) { + const handlers = this.messageHandlers.get(event); + if (handlers) { + handlers.forEach(handler => handler(data)); + } + } + + private generateId(): string { + return Date.now().toString(36) + Math.random().toString(36).substr(2); + } + + // API方法 + async getNetworkStatus(): Promise { + return this.sendCommand('get_network_status'); + } + + async getConnections(): Promise { + return this.sendCommand('get_connections'); + } + + async getFirewallRules(): Promise { + return this.sendCommand('get_firewall_rules'); + } + + async addFirewallRule(rule: Omit): Promise { + return this.sendCommand('add_firewall_rule', rule); + } + + async removeFirewallRule(ruleId: string): Promise { + return this.sendCommand('remove_firewall_rule', { id: ruleId }); + } + + async toggleFirewallRule(ruleId: string): Promise { + return this.sendCommand('toggle_firewall_rule', { id: ruleId }); + } +} + +// 全局WebSocket管理器实例 +let wsManager: EnhancedWebSocketManager | null = null; + +export function useWebSocketEnhanced(url?: string) { + if (!wsManager) { + wsManager = new EnhancedWebSocketManager(url); + } + + const store = useWebSocketStore(); + + // 自动连接 + if (!store.isConnected && !store.isConnecting) { + wsManager.connect().catch(console.error); + } + + onUnmounted(() => { + // 组件卸载时不断开连接,因为是单例 + }); + + return { + // 从store获取状态 + ...store, + + // WebSocketManager方法 + connect: () => wsManager!.connect(), + disconnect: () => wsManager!.disconnect(), + sendCommand: (action: WebSocketCommand['action'], data?: unknown) => wsManager!.sendCommand(action, data), + + // API方法 + getNetworkStatus: () => wsManager!.getNetworkStatus(), + getConnections: () => wsManager!.getConnections(), + getFirewallRules: () => wsManager!.getFirewallRules(), + addFirewallRule: (rule: Omit) => wsManager!.addFirewallRule(rule), + removeFirewallRule: (ruleId: string) => wsManager!.removeFirewallRule(ruleId), + toggleFirewallRule: (ruleId: string) => wsManager!.toggleFirewallRule(ruleId), + + // 事件监听 + on: (event: string, handler: Function) => wsManager!.on(event, handler), + off: (event: string, handler: Function) => wsManager!.off(event, handler), + }; +} + +// plugins/websocket.ts - Vue插件 +import type { App } from 'vue'; +import { EnhancedWebSocketManager } from '@/composables/useWebSocketEnhanced'; + +declare module '@vue/runtime-core' { + interface ComponentCustomProperties { + $ws: EnhancedWebSocketManager; + } +} + +export default { + install(app: App, options: { url?: string } = {}) { + const wsManager = new EnhancedWebSocketManager(options.url); + + app.config.globalProperties.$ws = wsManager; + app.provide('websocket', wsManager); + + // 应用关闭时断开连接 + window.addEventListener('beforeunload', () => { + wsManager.disconnect(); + }); + } +}; diff --git a/ClientApp/src/services/WebSocketManager.ts b/ClientApp/src/services/WebSocketManager.ts new file mode 100644 index 0000000..5ebcfe8 --- /dev/null +++ b/ClientApp/src/services/WebSocketManager.ts @@ -0,0 +1,232 @@ + +// services/WebSocketManager.ts +import { EventEmitter } from 'events' +import type { + WebSocketMessage, + WebSocketConfig, + ConnectionState, + MessageType +} from '@/types/websocket' + +export class WebSocketManager extends EventEmitter { + private ws: WebSocket | null = null + private config: WebSocketConfig + private reconnectTimer: NodeJS.Timeout | null = null + private heartbeatTimer: NodeJS.Timeout | null = null + /** + * 重新连接尝试 + * @private + */ + private reconnectAttempts = 0 + private state: ConnectionState = ConnectionState.DISCONNECTED + /** + * 待处理请求 + * @private + */ + private pendingRequests = new Map void + reject: (reason: unknown) => void + timeout: NodeJS.Timeout + }>() + + constructor(config: WebSocketConfig) { + super() + this.config = config + } + + public connect(): Promise { + return new Promise((resolve, reject) => { + if (this.state === ConnectionState.CONNECTED) { + resolve() + return + } + + this.setState(ConnectionState.CONNECTING) + + try { + this.ws = new WebSocket(this.config.url) + this.setupEventListeners(resolve, reject) + } catch (error) { + this.setState(ConnectionState.ERROR) + reject(error) + } + }) + } + + public disconnect(): void { + this.clearTimers() + this.setState(ConnectionState.DISCONNECTED) + + if (this.ws) { + this.ws.close() + this.ws = null + } + + this.rejectPendingRequests('Connection closed') + } + + public send(type: MessageType, data?: T): void { + if (!this.isConnected()) { + throw new Error('WebSocket is not connected') + } + + const message: WebSocketMessage = { + type, + data, + timestamp: Date.now() + } + + this.ws!.send(JSON.stringify(message)) + } + + public sendRequest(type: MessageType, data?: T): Promise { + return new Promise((resolve, reject) => { + if (!this.isConnected()) { + reject(new Error('WebSocket is not connected')) + return + } + + const requestId = this.generateRequestId() + const message: WebSocketMessage = { + type, + data, + timestamp: Date.now(), + requestId + } + + const timeout = setTimeout(() => { + this.pendingRequests.delete(requestId) + reject(new Error('Request timeout')) + }, this.config.timeout) + + this.pendingRequests.set(requestId, { resolve, reject, timeout }) + this.ws!.send(JSON.stringify(message)) + }) + } + + public getState(): ConnectionState { + return this.state + } + + public isConnected(): boolean { + return this.state === ConnectionState.CONNECTED && this.ws?.readyState === WebSocket.OPEN + } + + private setupEventListeners( + connectResolve: () => void, + connectReject: (error: unknown) => void + ): void { + if (!this.ws) return + + this.ws.onopen = () => { + this.setState(ConnectionState.CONNECTED) + this.reconnectAttempts = 0 + this.startHeartbeat() + connectResolve() + this.emit('connected') + } + + this.ws.onclose = (event) => { + this.setState(ConnectionState.DISCONNECTED) + this.stopHeartbeat() + this.emit('disconnected', event) + + if (!event.wasClean && this.reconnectAttempts < this.config.maxReconnectAttempts) { + this.scheduleReconnect() + } + } + + this.ws.onerror = (error) => { + this.setState(ConnectionState.ERROR) + this.emit('error', error) + connectReject(error) + } + + this.ws.onmessage = (event) => { + try { + const message: WebSocketMessage = JSON.parse(event.data) + this.handleMessage(message) + } catch (error) { + console.error('Failed to parse WebSocket message:', error) + } + } + } + + private handleMessage(message: WebSocketMessage): void { + // 处理响应消息 + if (message.requestId && this.pendingRequests.has(message.requestId)) { + const request = this.pendingRequests.get(message.requestId)! + clearTimeout(request.timeout) + this.pendingRequests.delete(message.requestId) + + if (message.type === MessageType.ERROR) { + request.reject(new Error(message.data)) + } else { + request.resolve(message.data) + } + return + } + + // 触发消息事件 + this.emit('message', message) + this.emit(message.type, message.data) + } + + private scheduleReconnect(): void { + if (this.reconnectTimer) return + + this.setState(ConnectionState.RECONNECTING) + this.reconnectAttempts++ + + const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 30000) + + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = null + this.connect().catch(() => { + // 重连失败,会自动触发下一次重连 + }) + }, delay) + } + + private startHeartbeat(): void { + this.heartbeatTimer = setInterval(() => { + if (this.isConnected()) { + this.send(MessageType.GET_SYSTEM_INFO) + } + }, this.config.heartbeatInterval) + } + + private stopHeartbeat(): void { + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer) + this.heartbeatTimer = null + } + } + + private clearTimers(): void { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer) + this.reconnectTimer = null + } + this.stopHeartbeat() + } + + private setState(state: ConnectionState): void { + if (this.state !== state) { + this.state = state + this.emit('stateChange', state) + } + } + + private generateRequestId(): string { + return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + } + + private rejectPendingRequests(reason: string): void { + this.pendingRequests.forEach(({ reject, timeout }) => { + clearTimeout(timeout) + reject(new Error(reason)) + }) + this.pendingRequests.clear() + } +} diff --git a/ClientApp/src/stores/websocketStore.ts b/ClientApp/src/stores/websocketStore.ts new file mode 100644 index 0000000..46c564c --- /dev/null +++ b/ClientApp/src/stores/websocketStore.ts @@ -0,0 +1,233 @@ + +// stores/websocket.ts +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { WebSocketManager } from '@/services/WebSocketManager' +import type { + SystemInfo, + NetworkStatus, + FirewallRule, + ConnectionState, + MessageType +} from '@/types/websocket' + +export const useWebSocketStore = defineStore('websocket', () => { + // 状态 + const wsManager = ref(null) + const connectionState = ref('disconnected' as ConnectionState) + const systemInfo = ref(null) + const networkStatus = ref([]) + const firewallRules = ref([]) + const lastError = ref(null) + const isLoading = ref(false) + + // 计算属性 + const isConnected = computed(() => connectionState.value === 'connected') + const connectionCount = computed(() => systemInfo.value?.connectionCount ?? 0) + const activeNetworkInterfaces = computed(() => + networkStatus.value.filter(status => status.isConnected) + ) + + // 初始化WebSocket连接 + const initialize = async (url: string): Promise => { + if (wsManager.value) { + wsManager.value.disconnect() + } + + wsManager.value = new WebSocketManager({ + url, + reconnectInterval: 5000, + maxReconnectAttempts: 10, + heartbeatInterval: 30000, + timeout: 10000 + }) + + setupEventListeners() + + try { + await wsManager.value.connect() + } catch (error) { + lastError.value = error instanceof Error ? error.message : 'Connection failed' + throw error + } + } + + // 设置事件监听器 + const setupEventListeners = (): void => { + if (!wsManager.value) return + + wsManager.value.on('stateChange', (state: ConnectionState) => { + connectionState.value = state + }) + + wsManager.value.on('error', (error: Error) => { + lastError.value = error.message + }) + + // 监听数据更新事件 + wsManager.value.on('network_status_response', (data: NetworkStatus[]) => { + networkStatus.value = data + }) + + wsManager.value.on('firewall_rules_response', (data: FirewallRule[]) => { + firewallRules.value = data + }) + + wsManager.value.on('system_info_response', (data: SystemInfo) => { + systemInfo.value = data + networkStatus.value = data.networkInterfaces + firewallRules.value = data.firewallRules + }) + + // 监听实时更新事件 + wsManager.value.on('network_status_changed', (data: NetworkStatus[]) => { + networkStatus.value = data + }) + + wsManager.value.on('firewall_rule_changed', (data: FirewallRule[]) => { + firewallRules.value = data + }) + + wsManager.value.on('system_alert', (data: { message: string; level: string }) => { + console.warn('System Alert:', data) + }) + } + + // 数据获取方法 + const fetchSystemInfo = async (): Promise => { + if (!wsManager.value || !isConnected.value) { + throw new Error('WebSocket not connected') + } + + isLoading.value = true + try { + const data = await wsManager.value.sendRequest( + 'get_system_info' as MessageType + ) + systemInfo.value = data + return data + } finally { + isLoading.value = false + } + } + + const fetchNetworkStatus = async (): Promise => { + if (!wsManager.value || !isConnected.value) { + throw new Error('WebSocket not connected') + } + + isLoading.value = true + try { + const data = await wsManager.value.sendRequest( + 'get_network_status' as MessageType + ) + networkStatus.value = data + return data + } finally { + isLoading.value = false + } + } + + const fetchFirewallRules = async (): Promise => { + if (!wsManager.value || !isConnected.value) { + throw new Error('WebSocket not connected') + } + + isLoading.value = true + try { + const data = await wsManager.value.sendRequest( + 'get_firewall_rules' as MessageType + ) + firewallRules.value = data + return data + } finally { + isLoading.value = false + } + } + + // 防火墙操作方法 + const addFirewallRule = async (rule: Omit): Promise => { + if (!wsManager.value || !isConnected.value) { + throw new Error('WebSocket not connected') + } + + await wsManager.value.sendRequest, void>( + 'add_firewall_rule' as MessageType, + rule + ) + } + + const updateFirewallRule = async (rule: FirewallRule): Promise => { + if (!wsManager.value || !isConnected.value) { + throw new Error('WebSocket not connected') + } + + await wsManager.value.sendRequest( + 'update_firewall_rule' as MessageType, + rule + ) + } + + const deleteFirewallRule = async (ruleId: string): Promise => { + if (!wsManager.value || !isConnected.value) { + throw new Error('WebSocket not connected') + } + + await wsManager.value.sendRequest<{ id: string }, void>( + 'delete_firewall_rule' as MessageType, + { id: ruleId } + ) + } + + const toggleFirewallRule = async (ruleId: string): Promise => { + if (!wsManager.value || !isConnected.value) { + throw new Error('WebSocket not connected') + } + + await wsManager.value.sendRequest<{ id: string }, void>( + 'toggle_firewall_rule' as MessageType, + { id: ruleId } + ) + } + + // 断开连接 + const disconnect = (): void => { + if (wsManager.value) { + wsManager.value.disconnect() + wsManager.value = null + } + connectionState.value = 'disconnected' as ConnectionState + } + + // 清除错误 + const clearError = (): void => { + lastError.value = null + } + + return { + // 状态 + connectionState, + systemInfo, + networkStatus, + firewallRules, + lastError, + isLoading, + + // 计算属性 + isConnected, + connectionCount, + activeNetworkInterfaces, + + // 方法 + initialize, + fetchSystemInfo, + fetchNetworkStatus, + fetchFirewallRules, + addFirewallRule, + updateFirewallRule, + deleteFirewallRule, + toggleFirewallRule, + disconnect, + clearError + } +}) diff --git a/ClientApp/src/types/websocket.ts b/ClientApp/src/types/websocket.ts new file mode 100644 index 0000000..306a8be --- /dev/null +++ b/ClientApp/src/types/websocket.ts @@ -0,0 +1,88 @@ +// types/websocket.ts +export interface WebSocketMessage { + type: string + data: T + timestamp: number + requestId?: string +} + +export interface NetworkStatus { + interfaceName: string + isConnected: boolean + ipAddress: string + macAddress: string + speed: number + bytesReceived: number + bytesSent: number +} + +export interface FirewallRule { + id: string + name: string + direction: 'inbound' | 'outbound' + action: 'allow' | 'block' + protocol: 'tcp' | 'udp' | 'icmp' | 'any' + localPort?: string + remotePort?: string + localAddress?: string + remoteAddress?: string + enabled: boolean +} + +export interface SystemInfo { + cpuUsage: number + memoryUsage: number + networkInterfaces: NetworkStatus[] + firewallRules: FirewallRule[] + connectionCount: number +} + +export enum MessageType { + // 数据获取相关 + GET_NETWORK_STATUS = 'get_network_status', + GET_FIREWALL_RULES = 'get_firewall_rules', + GET_SYSTEM_INFO = 'get_system_info', + + // 数据响应相关 + NETWORK_STATUS_RESPONSE = 'network_status_response', + FIREWALL_RULES_RESPONSE = 'firewall_rules_response', + SYSTEM_INFO_RESPONSE = 'system_info_response', + + // 操作相关 + ADD_FIREWALL_RULE = 'add_firewall_rule', + UPDATE_FIREWALL_RULE = 'update_firewall_rule', + DELETE_FIREWALL_RULE = 'delete_firewall_rule', + TOGGLE_FIREWALL_RULE = 'toggle_firewall_rule', + + // 通知相关 + NETWORK_STATUS_CHANGED = 'network_status_changed', + FIREWALL_RULE_CHANGED = 'firewall_rule_changed', + SYSTEM_ALERT = 'system_alert', + ERROR = 'error', + SUCCESS = 'success' +} + +export interface WebSocketConfig { + url: string + /** + * 重新连接间隔 + */ + reconnectInterval: number + /** + * 最大重新连接尝试次数 + */ + maxReconnectAttempts: number + /** + * 心跳间隔 + */ + heartbeatInterval: number + timeout: number +} + +export enum ConnectionState { + CONNECTING = 'connecting', + CONNECTED = 'connected', + DISCONNECTED = 'disconnected', + RECONNECTING = 'reconnecting', + ERROR = 'error' +} diff --git a/ClientApp/src/utils/websocketHelpers.ts b/ClientApp/src/utils/websocketHelpers.ts new file mode 100644 index 0000000..6b05eb6 --- /dev/null +++ b/ClientApp/src/utils/websocketHelpers.ts @@ -0,0 +1,86 @@ +/** + * Time:2025/5/25 20:41 47 + * Name:websocketHelpers.ts + * Path:src/utils + * ProjectName:ClientApp + * Author:charlatans + * + * Il n'ya qu'un héroïsme au monde : + * c'est de voir le monde tel qu'il est et de l'aimer. + */ + + +// utils/websocketHelpers.ts - WebSocket辅助工具 +export class WebSocketRetryManager { + private retryAttempts = 0; + private maxRetries: number; + private baseDelay: number; + private maxDelay: number; + + constructor(maxRetries = 5, baseDelay = 1000, maxDelay = 30000) { + this.maxRetries = maxRetries; + this.baseDelay = baseDelay; + this.maxDelay = maxDelay; + } + + shouldRetry(): boolean { + return this.retryAttempts < this.maxRetries; + } + + getNextDelay(): number { + const delay = Math.min(this.baseDelay * Math.pow(2, this.retryAttempts), this.maxDelay); + this.retryAttempts++; + return delay; + } + + reset() { + this.retryAttempts = 0; + } + + getCurrentAttempt(): number { + return this.retryAttempts; + } +} + +export class WebSocketMessageQueue { + private queue: Array<{ message: unknown; resolve: Function; reject: Function }> = []; + private isProcessing = false; + + async enqueue(message: unknown): Promise { + return new Promise((resolve, reject) => { + this.queue.push({ message, resolve, reject }); + this.processQueue(); + }); + } + + private async processQueue() { + if (this.isProcessing || this.queue.length === 0) return; + + this.isProcessing = true; + + while (this.queue.length > 0) { + const item = this.queue.shift(); + if (item) { + try { + // 这里应该调用实际的发送方法 + await this.sendMessage(item.message); + item.resolve(); + } catch (error) { + item.reject(error); + } + } + } + + this.isProcessing = false; + } + + private async sendMessage(message: unknown): Promise { + // 由WebSocketManager实现 + throw new Error('sendMessage method should be implemented by WebSocketManager'); + } + + clear() { + this.queue.forEach(item => item.reject(new Error('Queue cleared'))); + this.queue = []; + } +}