From d88c710606b72569d6e59e8a4227be577344a1fc Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Fri, 11 Jul 2025 14:32:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=B9=E8=BF=9Bapi=E7=94=9F=E6=88=90?= =?UTF-8?q?=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 300 ++++++++++++++++++++++++++++++++++ package.json | 7 +- scripts/GenerateWebAPI.ts | 335 ++++++++++++++++++++++++++++++++++++++ src/APIClient.ts | 177 +++++++++----------- 4 files changed, 720 insertions(+), 99 deletions(-) create mode 100644 scripts/GenerateWebAPI.ts diff --git a/package-lock.json b/package-lock.json index 61712d5..7d331b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,10 +40,13 @@ "@vue/tsconfig": "^0.7.0", "autoprefixer": "^10.4.20", "daisyui": "^5.0.0", + "node-fetch": "^3.3.2", "npm-run-all2": "^7.0.2", "nswag": "^14.3.0", "postcss": "^8.5.3", "tailwindcss": "^4.0.12", + "ts-node": "^10.9.2", + "tsx": "^4.20.3", "typescript": "~5.7.3", "unplugin-vue-components": "^28.8.0", "vite": "^6.1.0", @@ -542,6 +545,30 @@ "node": ">=6.9.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", @@ -1742,6 +1769,34 @@ "vue": "^2.7.0 || ^3.0.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tsconfig/node22": { "version": "22.0.1", "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.1.tgz", @@ -2181,6 +2236,19 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/alien-signals": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", @@ -2228,6 +2296,13 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", @@ -2476,6 +2551,13 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2530,6 +2612,16 @@ "url": "https://github.com/saadeghi/daisyui?sponsor=1" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -2620,6 +2712,16 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/echarts": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", @@ -2791,6 +2893,30 @@ } } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", @@ -2820,6 +2946,19 @@ "node": ">=8" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -2891,6 +3030,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -3519,6 +3671,13 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/marked": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", @@ -3672,6 +3831,46 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -4065,6 +4264,16 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -4316,6 +4525,50 @@ "integrity": "sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==", "license": "MIT" }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/ts-results-es": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ts-results-es/-/ts-results-es-5.0.1.tgz", @@ -4328,6 +4581,26 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", + "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/typed-function": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz", @@ -4487,6 +4760,13 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", @@ -4789,6 +5069,16 @@ "typescript": ">=5.0.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -4819,6 +5109,16 @@ "dev": true, "license": "ISC" }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", diff --git a/package.json b/package.json index c989293..06d1c34 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,7 @@ "preview": "vite preview", "build-only": "vite build", "type-check": "vue-tsc --build", - "pregen-api": "cd server && dotnet run --property:Configuration=Release &", - "gen-api": "npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts", - "postgen-api": "pkill server" + "gen-api": "npx tsx scripts/GenerateWebAPI.ts" }, "dependencies": { "@svgdotjs/svg.js": "^3.2.4", @@ -46,10 +44,13 @@ "@vue/tsconfig": "^0.7.0", "autoprefixer": "^10.4.20", "daisyui": "^5.0.0", + "node-fetch": "^3.3.2", "npm-run-all2": "^7.0.2", "nswag": "^14.3.0", "postcss": "^8.5.3", "tailwindcss": "^4.0.12", + "ts-node": "^10.9.2", + "tsx": "^4.20.3", "typescript": "~5.7.3", "unplugin-vue-components": "^28.8.0", "vite": "^6.1.0", diff --git a/scripts/GenerateWebAPI.ts b/scripts/GenerateWebAPI.ts new file mode 100644 index 0000000..b7f2889 --- /dev/null +++ b/scripts/GenerateWebAPI.ts @@ -0,0 +1,335 @@ +import { spawn, exec, ChildProcess } from 'child_process'; +import { promisify } from 'util'; +import fetch from 'node-fetch'; + +const execAsync = promisify(exec); + +async function waitForServer(url: string, maxRetries: number = 30, interval: number = 1000): Promise { + for (let i = 0; i < maxRetries; i++) { + try { + const response = await fetch(url); + if (response.ok) { + console.log('✓ Server is ready'); + return true; + } + } catch (error) { + // Server not ready yet + } + console.log(`Waiting for server... (${i + 1}/${maxRetries})`); + await new Promise(resolve => setTimeout(resolve, interval)); + } + return false; +} + +// 改进全局变量类型 +let serverProcess: ChildProcess | null = null; +let webProcess: ChildProcess | null = null; + +async function startWeb(): Promise { + console.log('Starting Vite frontend...'); + return new Promise((resolve, reject) => { + const process = spawn('npm', ['run', 'dev'], { + stdio: 'pipe' + }); + + let webStarted = false; + + process.stdout?.on('data', (data) => { + const output = data.toString(); + console.log(`Web: ${output}`); + + // 检查 Vite 是否已启动 + if ((output.includes('Local:') || output.includes('ready in')) && !webStarted) { + webStarted = true; + resolve(process); + } + }); + + process.stderr?.on('data', (data) => { + console.error(`Web Error: ${data}`); + }); + + process.on('error', (error) => { + reject(error); + }); + + process.on('exit', (code, signal) => { + console.log(`Web process exited with code ${code} and signal ${signal}`); + if (!webStarted) { + reject(new Error(`Web process exited unexpectedly with code ${code}`)); + } + }); + + // 存储进程引用 + webProcess = process; + + // 超时处理 + setTimeout(() => { + if (!webStarted) { + reject(new Error('Web server failed to start within timeout')); + } + }, 30000); // 30秒超时 + }); +} + +async function startServer(): Promise { + console.log('Starting .NET server...'); + return new Promise((resolve, reject) => { + const process = spawn('dotnet', ['run', '--property:Configuration=Release'], { + cwd: 'server', + stdio: 'pipe' + }); + + let serverStarted = false; + + process.stdout?.on('data', (data) => { + const output = data.toString(); + console.log(`Server: ${output}`); + + // 检查服务器是否已启动 + if (output.includes('Now listening on:') && !serverStarted) { + serverStarted = true; + resolve(process); + } + }); + + process.stderr?.on('data', (data) => { + console.error(`Server Error: ${data}`); + }); + + process.on('error', (error) => { + reject(error); + }); + + process.on('exit', (code, signal) => { + console.log(`Server process exited with code ${code} and signal ${signal}`); + if (!serverStarted) { + reject(new Error(`Server process exited unexpectedly with code ${code}`)); + } + }); + + // 存储进程引用 + serverProcess = process; + + // 超时处理 + setTimeout(() => { + if (!serverStarted) { + reject(new Error('Server failed to start within timeout')); + } + }, 30000); // 30秒超时 + }); +} + +async function stopServer(): Promise { + console.log('Stopping server...'); + + if (!serverProcess) { + console.log('No server process to stop'); + return; + } + + try { + // 检查进程是否还存在 + if (serverProcess.killed || serverProcess.exitCode !== null) { + console.log('✓ Server process already terminated'); + serverProcess = null; + return; + } + + // 发送 SIGTERM 信号 + const killed = serverProcess.kill('SIGTERM'); + if (!killed) { + console.warn('Failed to send SIGTERM to server process'); + return; + } + + // 等待进程优雅退出 + const exitPromise = new Promise((resolve) => { + if (serverProcess) { + serverProcess.on('exit', () => { + console.log('✓ Server stopped gracefully'); + resolve(); + }); + } else { + resolve(); + } + }); + + // 设置超时,如果 5 秒内没有退出则强制终止 + const timeoutPromise = new Promise((resolve) => { + setTimeout(() => { + if (serverProcess && !serverProcess.killed && serverProcess.exitCode === null) { + console.log('Force killing server process...'); + serverProcess.kill('SIGKILL'); + } + resolve(); + }, 5000); + }); + + await Promise.race([exitPromise, timeoutPromise]); + + } catch (error) { + console.warn('Warning: Could not stop server process:', error); + } finally { + serverProcess = null; + + // 额外清理:确保没有遗留的 dotnet 进程 + try { + if (process.platform !== 'win32') { + // 只清理与我们项目相关的进程 + await execAsync('pkill -f "dotnet.*run.*--property:Configuration=Release"').catch(() => { + // 忽略错误,可能没有匹配的进程 + }); + } + } catch (cleanupError) { + // 忽略清理错误 + } + } +} + +async function stopWeb(): Promise { + console.log('Stopping web server...'); + + if (!webProcess) { + console.log('No web process to stop'); + return; + } + + try { + // 检查进程是否还存在 + if (webProcess.killed || webProcess.exitCode !== null) { + console.log('✓ Web process already terminated'); + webProcess = null; + return; + } + + // 发送 SIGTERM 信号 + const killed = webProcess.kill('SIGTERM'); + if (!killed) { + console.warn('Failed to send SIGTERM to web process'); + return; + } + + // 等待进程优雅退出 + const exitPromise = new Promise((resolve) => { + if (webProcess) { + webProcess.on('exit', () => { + console.log('✓ Web server stopped gracefully'); + resolve(); + }); + } else { + resolve(); + } + }); + + // 设置超时,如果 5 秒内没有退出则强制终止 + const timeoutPromise = new Promise((resolve) => { + setTimeout(() => { + if (webProcess && !webProcess.killed && webProcess.exitCode === null) { + console.log('Force killing web process...'); + webProcess.kill('SIGKILL'); + } + resolve(); + }, 5000); + }); + + await Promise.race([exitPromise, timeoutPromise]); + + } catch (error) { + console.warn('Warning: Could not stop web process:', error); + } finally { + webProcess = null; + + // 额外清理:确保没有遗留的 npm/node 进程 + try { + if (process.platform !== 'win32') { + // 清理可能的 vite 进程 + await execAsync('pkill -f "vite"').catch(() => { + // 忽略错误,可能没有匹配的进程 + }); + } + } catch (cleanupError) { + // 忽略清理错误 + } + } +} + +async function generateApiClient(): Promise { + console.log('Generating API client...'); + try { + await execAsync('npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts'); + console.log('✓ API client generated successfully'); + } catch (error) { + throw new Error(`Failed to generate API client: ${error}`); + } +} + +async function main(): Promise { + try { + // Start web frontend first + await startWeb(); + console.log('✓ Frontend started'); + + // Wait a bit for frontend to fully initialize + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Start server + await startServer(); + console.log('✓ Backend started'); + + // Wait for server to be ready (给服务器额外时间完全启动) + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Check if swagger endpoint is available + const serverReady = await waitForServer('http://localhost:5000/swagger/v1/swagger.json'); + + if (!serverReady) { + throw new Error('Server failed to start within the expected time'); + } + + // Generate API client + await generateApiClient(); + + console.log('✓ API generation completed successfully'); + } catch (error) { + console.error('❌ Error:', error); + process.exit(1); + } finally { + // Always try to stop processes in order: server first, then web + await stopServer(); + await stopWeb(); + } +} + +// 改进的进程终止处理 +const cleanup = async (signal: string) => { + console.log(`\nReceived ${signal}, cleaning up...`); + await stopServer(); + await stopWeb(); + process.exit(0); +}; + +process.on('SIGINT', () => cleanup('SIGINT')); +process.on('SIGTERM', () => cleanup('SIGTERM')); + +// 处理未捕获的异常 +process.on('uncaughtException', async (error) => { + console.error('❌ Uncaught exception:', error); + await stopServer(); + await stopWeb(); + process.exit(1); +}); + +process.on('unhandledRejection', async (reason, promise) => { + console.error('❌ Unhandled rejection at:', promise, 'reason:', reason); + await stopServer(); + await stopWeb(); + process.exit(1); +}); + +main().catch(async (error) => { + console.error('❌ Unhandled error:', error); + await stopServer(); + await stopWeb(); + process.exit(1); +}); \ No newline at end of file diff --git a/src/APIClient.ts b/src/APIClient.ts index 115b1eb..d696954 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -327,7 +327,7 @@ export class BsdlParserClient { * [TODO:description] * @return [TODO:return] */ - getBoundaryLogicalPorts(): Promise { + getBoundaryLogicalPorts(): Promise { let url_ = this.baseUrl + "/api/BsdlParser/GetBoundaryLogicalPorts"; url_ = url_.replace(/[?&]$/, ""); @@ -343,7 +343,7 @@ export class BsdlParserClient { }); } - protected processGetBoundaryLogicalPorts(response: Response): Promise { + protected processGetBoundaryLogicalPorts(response: Response): Promise { const status = response.status; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; if (status === 200 || status === 206) { @@ -362,7 +362,7 @@ export class BsdlParserClient { return throwException("An unexpected server error occurred.", status, _responseText, _headers); }); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } } @@ -377,11 +377,21 @@ export class DataClient { } /** - * 创建数据库表 - * @return 插入的记录数 + * [TODO:description] + * @param name (optional) [TODO:parameter] + * @param password (optional) [TODO:parameter] + * @return [TODO:return] */ - createTables(): Promise { - let url_ = this.baseUrl + "/api/Data/CreateTable"; + login(name: string | undefined, password: string | undefined): Promise { + let url_ = this.baseUrl + "/api/Data/login?"; + if (name === null) + throw new Error("The parameter 'name' cannot be null."); + else if (name !== undefined) + url_ += "name=" + encodeURIComponent("" + name) + "&"; + if (password === null) + throw new Error("The parameter 'password' cannot be null."); + else if (password !== undefined) + url_ += "password=" + encodeURIComponent("" + password) + "&"; url_ = url_.replace(/[?&]$/, ""); let options_: RequestInit = { @@ -392,11 +402,11 @@ export class DataClient { }; return this.http.fetch(url_, options_).then((_response: Response) => { - return this.processCreateTables(_response); + return this.processLogin(_response); }); } - protected processCreateTables(response: Response): Promise { + protected processLogin(response: Response): Promise { const status = response.status; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; if (status === 200 || status === 206) { @@ -415,57 +425,15 @@ export class DataClient { return throwException("An unexpected server error occurred.", status, _responseText, _headers); }); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } /** - * 删除数据库表 - * @return 插入的记录数 + * [TODO:description] + * @return [TODO:return] */ - dropTables(): Promise { - let url_ = this.baseUrl + "/api/Data/DropTables"; - url_ = url_.replace(/[?&]$/, ""); - - let options_: RequestInit = { - method: "DELETE", - headers: { - "Accept": "application/octet-stream" - } - }; - - return this.http.fetch(url_, options_).then((_response: Response) => { - return this.processDropTables(_response); - }); - } - - protected processDropTables(response: Response): Promise { - const status = response.status; - let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; - if (status === 200 || status === 206) { - const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined; - let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; - let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; - if (fileName) { - fileName = decodeURIComponent(fileName); - } else { - fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; - fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; - } - return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; }); - } else if (status !== 200 && status !== 204) { - return response.text().then((_responseText) => { - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - }); - } - return Promise.resolve(null as any); - } - - /** - * 获取所有用户 - * @return 用户列表 - */ - allUsers(): Promise { - let url_ = this.baseUrl + "/api/Data/AllUsers"; + testAuth(): Promise { + let url_ = this.baseUrl + "/api/Data/TestAuth"; url_ = url_.replace(/[?&]$/, ""); let options_: RequestInit = { @@ -476,11 +444,11 @@ export class DataClient { }; return this.http.fetch(url_, options_).then((_response: Response) => { - return this.processAllUsers(_response); + return this.processTestAuth(_response); }); } - protected processAllUsers(response: Response): Promise { + protected processTestAuth(response: Response): Promise { const status = response.status; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; if (status === 200 || status === 206) { @@ -499,20 +467,30 @@ export class DataClient { return throwException("An unexpected server error occurred.", status, _responseText, _headers); }); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } /** * 注册新用户 * @param name (optional) 用户名 + * @param email (optional) [TODO:parameter] + * @param password (optional) [TODO:parameter] * @return 操作结果 */ - signUpUser(name: string | undefined): Promise { + signUpUser(name: string | undefined, email: string | undefined, password: string | undefined): Promise { let url_ = this.baseUrl + "/api/Data/SignUpUser?"; if (name === null) throw new Error("The parameter 'name' cannot be null."); else if (name !== undefined) url_ += "name=" + encodeURIComponent("" + name) + "&"; + if (email === null) + throw new Error("The parameter 'email' cannot be null."); + else if (email !== undefined) + url_ += "email=" + encodeURIComponent("" + email) + "&"; + if (password === null) + throw new Error("The parameter 'password' cannot be null."); + else if (password !== undefined) + url_ += "password=" + encodeURIComponent("" + password) + "&"; url_ = url_.replace(/[?&]$/, ""); let options_: RequestInit = { @@ -527,7 +505,7 @@ export class DataClient { }); } - protected processSignUpUser(response: Response): Promise { + protected processSignUpUser(response: Response): Promise { const status = response.status; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; if (status === 200 || status === 206) { @@ -546,7 +524,7 @@ export class DataClient { return throwException("An unexpected server error occurred.", status, _responseText, _headers); }); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } } @@ -943,9 +921,9 @@ export class JtagClient { /** * 上传比特流文件 * @param address (optional) 设备地址 - * @param file (optional) + * @param file (optional) 比特流文件 */ - uploadBitstream(address: string | undefined, file: FileParameter | null | undefined): Promise { + uploadBitstream(address: string | undefined, file: FileParameter | undefined): Promise { let url_ = this.baseUrl + "/api/Jtag/UploadBitstream?"; if (address === null) throw new Error("The parameter 'address' cannot be null."); @@ -954,7 +932,9 @@ export class JtagClient { url_ = url_.replace(/[?&]$/, ""); const content_ = new FormData(); - if (file !== null && file !== undefined) + if (file === null || file === undefined) + throw new Error("The parameter 'file' cannot be null."); + else content_.append("file", file.data, file.fileName ? file.fileName : "file"); let options_: RequestInit = { @@ -1519,10 +1499,10 @@ export class RemoteUpdateClient { /** * 上传远程更新比特流文件 * @param address (optional) 设备地址 - * @param goldenBitream (optional) - * @param bitstream1 (optional) - * @param bitstream2 (optional) - * @param bitstream3 (optional) + * @param goldenBitream (optional) 黄金比特流文件 + * @param bitstream1 (optional) 比特流文件1 + * @param bitstream2 (optional) 比特流文件2 + * @param bitstream3 (optional) 比特流文件3 * @return 上传结果 */ uploadBitstreams(address: string | undefined, goldenBitream: FileParameter | null | undefined, bitstream1: FileParameter | null | undefined, bitstream2: FileParameter | null | undefined, bitstream3: FileParameter | null | undefined): Promise { @@ -2156,7 +2136,7 @@ export class UDPClient { /** * 获取指定IP地址接收的数据列表 * @param address (optional) IP地址 - * @param taskID (optional) + * @param taskID (optional) 任务ID */ getRecvDataArray(address: string | undefined, taskID: number | undefined): Promise { let url_ = this.baseUrl + "/api/UDP/GetRecvDataArray?"; @@ -2213,7 +2193,7 @@ export class UDPClient { } export class Exception implements IException { - message?: string; + message!: string; innerException?: Exception | undefined; source?: string | undefined; stackTrace?: string | undefined; @@ -2254,7 +2234,7 @@ export class Exception implements IException { } export interface IException { - message?: string; + message: string; innerException?: Exception | undefined; source?: string | undefined; stackTrace?: string | undefined; @@ -2262,7 +2242,9 @@ export interface IException { /** 摄像头配置请求模型 */ export class CameraConfigRequest implements ICameraConfigRequest { + /** 摄像头地址 */ address!: string; + /** 摄像头端口 */ port!: number; constructor(data?: ICameraConfigRequest) { @@ -2298,7 +2280,9 @@ export class CameraConfigRequest implements ICameraConfigRequest { /** 摄像头配置请求模型 */ export interface ICameraConfigRequest { + /** 摄像头地址 */ address: string; + /** 摄像头端口 */ port: number; } @@ -2330,6 +2314,7 @@ export interface ISystemException extends IException { } export class ArgumentException extends SystemException implements IArgumentException { + declare message: string; paramName?: string | undefined; constructor(data?: IArgumentException) { @@ -2361,22 +2346,22 @@ export class ArgumentException extends SystemException implements IArgumentExcep } export interface IArgumentException extends ISystemException { - message?: string; + message: string; paramName?: string | undefined; } /** Package options which to send address to read or write */ export class SendAddrPackOptions implements ISendAddrPackOptions { /** 突发类型 */ - burstType?: BurstType; + burstType!: BurstType; /** 任务ID */ - commandID?: number; + commandID!: number; /** 标识写入还是读取 */ - isWrite?: boolean; + isWrite!: boolean; /** 突发长度:0是32bits,255是32bits x 256 */ - burstLength?: number; + burstLength!: number; /** 目标地址 */ - address?: number; + address!: number; constructor(data?: ISendAddrPackOptions) { if (data) { @@ -2418,15 +2403,15 @@ export class SendAddrPackOptions implements ISendAddrPackOptions { /** Package options which to send address to read or write */ export interface ISendAddrPackOptions { /** 突发类型 */ - burstType?: BurstType; + burstType: BurstType; /** 任务ID */ - commandID?: number; + commandID: number; /** 标识写入还是读取 */ - isWrite?: boolean; + isWrite: boolean; /** 突发长度:0是32bits,255是32bits x 256 */ - burstLength?: number; + burstLength: number; /** 目标地址 */ - address?: number; + address: number; } /** Package Burst Type */ @@ -2438,17 +2423,17 @@ export enum BurstType { /** UDP接受数据包格式 */ export class UDPData implements IUDPData { /** 接受到的时间 */ - dateTime?: Date; + dateTime!: Date; /** 发送来源的IP地址 */ - address?: string; + address!: string; /** 发送来源的端口号 */ - port?: number; + port!: number; /** 任务ID */ - taskID?: number; + taskID!: number; /** 接受到的数据 */ - data?: string; + data!: string; /** 是否被读取过 */ - hasRead?: boolean; + hasRead!: boolean; constructor(data?: IUDPData) { if (data) { @@ -2492,17 +2477,17 @@ export class UDPData implements IUDPData { /** UDP接受数据包格式 */ export interface IUDPData { /** 接受到的时间 */ - dateTime?: Date; + dateTime: Date; /** 发送来源的IP地址 */ - address?: string; + address: string; /** 发送来源的端口号 */ - port?: number; + port: number; /** 任务ID */ - taskID?: number; + taskID: number; /** 接受到的数据 */ - data?: string; + data: string; /** 是否被读取过 */ - hasRead?: boolean; + hasRead: boolean; } export interface FileParameter { @@ -2546,4 +2531,4 @@ function throwException(message: string, status: number, response: string, heade throw result; else throw new ApiException(message, status, response, headers, null); -} +} \ No newline at end of file