11 Commits

40 changed files with 2466 additions and 1410 deletions

View File

@@ -39,10 +39,10 @@
typescript-language-server typescript-language-server
]; ];
shellHook = '' shellHook = ''
export PATH=$PATH: export PATH=$PATH:/home/sikongjueluo/.dotnet/tools
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${pkgs.zlib}/lib export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${pkgs.zlib}/lib
export DOTNET_ROOT=${pkgs.dotnetCorePackages.sdk_9_0}/share/dotnet
''; '';
}; };
}); });
}; };

215
package-lock.json generated
View File

@@ -8,9 +8,11 @@
"name": "fpga-weblab", "name": "fpga-weblab",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@microsoft/signalr": "^9.0.6",
"@svgdotjs/svg.js": "^3.2.4", "@svgdotjs/svg.js": "^3.2.4",
"@tanstack/vue-table": "^8.21.3", "@tanstack/vue-table": "^8.21.3",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@types/signalr": "^2.4.3",
"@vueuse/core": "^13.5.0", "@vueuse/core": "^13.5.0",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
"axios": "^1.11.0", "axios": "^1.11.0",
@@ -1128,6 +1130,39 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@microsoft/signalr": {
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-9.0.6.tgz",
"integrity": "sha512-DrhgzFWI9JE4RPTsHYRxh4yr+OhnwKz8bnJe7eIi7mLLjqhJpEb62CiUy/YbFvLqLzcGzlzz1QWgVAW0zyipMQ==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"eventsource": "^2.0.2",
"fetch-cookie": "^2.0.3",
"node-fetch": "^2.6.7",
"ws": "^7.5.10"
}
},
"node_modules/@microsoft/signalr/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/@polka/url": { "node_modules/@polka/url": {
"version": "1.0.0-next.29", "version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
@@ -1845,6 +1880,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/jquery": {
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.32.tgz",
"integrity": "sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==",
"license": "MIT",
"dependencies": {
"@types/sizzle": "*"
}
},
"node_modules/@types/lodash": { "node_modules/@types/lodash": {
"version": "4.17.16", "version": "4.17.16",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz",
@@ -1861,6 +1905,21 @@
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/signalr": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@types/signalr/-/signalr-2.4.3.tgz",
"integrity": "sha512-W6C1wMRIIhJV9nsw19yhw4h9zlkLnJzsu9dYlH35aHUQblPsDF6UpCcAVu4Ljy4RS3c3uJyV88wf2M2SOWqqZg==",
"license": "MIT",
"dependencies": {
"@types/jquery": "*"
}
},
"node_modules/@types/sizzle": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz",
"integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==",
"license": "MIT"
},
"node_modules/@types/web-bluetooth": { "node_modules/@types/web-bluetooth": {
"version": "0.0.21", "version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
@@ -2257,6 +2316,18 @@
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
} }
}, },
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -2988,6 +3059,24 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/eventsource": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
"integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/execa": { "node_modules/execa": {
"version": "9.5.2", "version": "9.5.2",
"resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz",
@@ -3061,6 +3150,16 @@
"node": "^12.20 || >= 14.13" "node": "^12.20 || >= 14.13"
} }
}, },
"node_modules/fetch-cookie": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz",
"integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==",
"license": "Unlicense",
"dependencies": {
"set-cookie-parser": "^2.4.8",
"tough-cookie": "^4.0.0"
}
},
"node_modules/figures": { "node_modules/figures": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
@@ -4475,6 +4574,27 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"funding": {
"url": "https://github.com/sponsors/lupomontero"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/quansync": { "node_modules/quansync": {
"version": "0.2.10", "version": "0.2.10",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
@@ -4492,6 +4612,12 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"license": "MIT"
},
"node_modules/read-package-json-fast": { "node_modules/read-package-json-fast": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz",
@@ -4577,6 +4703,12 @@
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
} }
}, },
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
"node_modules/resolve-pkg-maps": { "node_modules/resolve-pkg-maps": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@@ -4662,6 +4794,12 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -4832,6 +4970,36 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"license": "BSD-3-Clause",
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tough-cookie/node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/ts-log": { "node_modules/ts-log": {
"version": "2.2.7", "version": "2.2.7",
"resolved": "https://registry.npmjs.org/ts-log/-/ts-log-2.2.7.tgz", "resolved": "https://registry.npmjs.org/ts-log/-/ts-log-2.2.7.tgz",
@@ -5073,6 +5241,16 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"license": "MIT",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/v8-compile-cache-lib": { "node_modules/v8-compile-cache-lib": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
@@ -5392,6 +5570,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/webpack-virtual-modules": { "node_modules/webpack-virtual-modules": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
@@ -5399,6 +5583,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
@@ -5415,6 +5609,27 @@
"node": "^18.17.0 || >=20.5.0" "node": "^18.17.0 || >=20.5.0"
} }
}, },
"node_modules/ws": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"license": "MIT",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@@ -12,9 +12,11 @@
"gen-api": "npx tsx scripts/GenerateWebAPI.ts" "gen-api": "npx tsx scripts/GenerateWebAPI.ts"
}, },
"dependencies": { "dependencies": {
"@microsoft/signalr": "^9.0.6",
"@svgdotjs/svg.js": "^3.2.4", "@svgdotjs/svg.js": "^3.2.4",
"@tanstack/vue-table": "^8.21.3", "@tanstack/vue-table": "^8.21.3",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@types/signalr": "^2.4.3",
"@vueuse/core": "^13.5.0", "@vueuse/core": "^13.5.0",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
"axios": "^1.11.0", "axios": "^1.11.0",

View File

@@ -1,36 +1,42 @@
import { spawn, exec, ChildProcess } from 'child_process'; import { spawn, exec, ChildProcess } from "child_process";
import { promisify } from 'util'; import { promisify } from "util";
import fetch from 'node-fetch'; import fetch from "node-fetch";
import * as fs from 'fs'; import * as fs from "fs";
const execAsync = promisify(exec); const execAsync = promisify(exec);
// Windows 支持函数 // Windows 支持函数
function getCommand(command: string): string { function getCommand(command: string): string {
// dotnet 在 Windows 上不需要 .cmd 后缀 // dotnet 在 Windows 上不需要 .cmd 后缀
if (command === 'dotnet') { if (command === "dotnet") {
return 'dotnet'; return "dotnet";
} }
return process.platform === 'win32' ? `${command}.cmd` : command; return process.platform === "win32" ? `${command}.cmd` : command;
} }
function getSpawnOptions() { function getSpawnOptions() {
return process.platform === 'win32' ? { stdio: 'pipe', shell: true } : { stdio: 'pipe' }; return process.platform === "win32"
? { stdio: "pipe", shell: true }
: { stdio: "pipe" };
} }
async function waitForServer(url: string, maxRetries: number = 30, interval: number = 1000): Promise<boolean> { async function waitForServer(
url: string,
maxRetries: number = 30,
interval: number = 1000,
): Promise<boolean> {
for (let i = 0; i < maxRetries; i++) { for (let i = 0; i < maxRetries; i++) {
try { try {
const response = await fetch(url); const response = await fetch(url);
if (response.ok) { if (response.ok) {
console.log('✓ Server is ready'); console.log("✓ Server is ready");
return true; return true;
} }
} catch (error) { } catch (error) {
// Server not ready yet // Server not ready yet
} }
console.log(`Waiting for server... (${i + 1}/${maxRetries})`); console.log(`Waiting for server... (${i + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, interval)); await new Promise((resolve) => setTimeout(resolve, interval));
} }
return false; return false;
} }
@@ -40,32 +46,39 @@ let serverProcess: ChildProcess | null = null;
let webProcess: ChildProcess | null = null; let webProcess: ChildProcess | null = null;
async function startWeb(): Promise<ChildProcess> { async function startWeb(): Promise<ChildProcess> {
console.log('Starting Vite frontend...'); console.log("Starting Vite frontend...");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const process = spawn(getCommand('npm'), ['run', 'dev'], getSpawnOptions() as any); const process = spawn(
getCommand("npm"),
["run", "dev"],
getSpawnOptions() as any,
);
let webStarted = false; let webStarted = false;
process.stdout?.on('data', (data) => { process.stdout?.on("data", (data) => {
const output = data.toString(); const output = data.toString();
console.log(`Web: ${output}`); console.log(`Web: ${output}`);
// 检查 Vite 是否已启动 // 检查 Vite 是否已启动
if ((output.includes('Local:') || output.includes('ready in')) && !webStarted) { if (
(output.includes("Local:") || output.includes("ready in")) &&
!webStarted
) {
webStarted = true; webStarted = true;
resolve(process); resolve(process);
} }
}); });
process.stderr?.on('data', (data) => { process.stderr?.on("data", (data) => {
console.error(`Web Error: ${data}`); console.error(`Web Error: ${data}`);
}); });
process.on('error', (error) => { process.on("error", (error) => {
reject(error); reject(error);
}); });
process.on('exit', (code, signal) => { process.on("exit", (code, signal) => {
console.log(`Web process exited with code ${code} and signal ${signal}`); console.log(`Web process exited with code ${code} and signal ${signal}`);
if (!webStarted) { if (!webStarted) {
reject(new Error(`Web process exited unexpectedly with code ${code}`)); reject(new Error(`Web process exited unexpectedly with code ${code}`));
@@ -78,45 +91,53 @@ async function startWeb(): Promise<ChildProcess> {
// 超时处理 // 超时处理
setTimeout(() => { setTimeout(() => {
if (!webStarted) { if (!webStarted) {
reject(new Error('Web server failed to start within timeout')); reject(new Error("Web server failed to start within timeout"));
} }
}, 30000); // 30秒超时 }, 10000); // 10秒超时
}); });
} }
async function startServer(): Promise<ChildProcess> { async function startServer(): Promise<ChildProcess> {
console.log('Starting .NET server...'); console.log("Starting .NET server...");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const process = spawn(getCommand('dotnet'), ['run', '--property:Configuration=Release'], { const process = spawn(
cwd: 'server', getCommand("dotnet"),
...getSpawnOptions() ["run", "--property:Configuration=Release"],
} as any); {
cwd: "server",
...getSpawnOptions(),
} as any,
);
let serverStarted = false; let serverStarted = false;
process.stdout?.on('data', (data) => { process.stdout?.on("data", (data) => {
const output = data.toString(); const output = data.toString();
console.log(`Server: ${output}`); console.log(`Server: ${output}`);
// 检查服务器是否已启动 // 检查服务器是否已启动
if (output.includes('Now listening on:') && !serverStarted) { if (output.includes("Now listening on:") && !serverStarted) {
serverStarted = true; serverStarted = true;
resolve(process); resolve(process);
} }
}); });
process.stderr?.on('data', (data) => { process.stderr?.on("data", (data) => {
console.error(`Server Error: ${data}`); console.error(`Server Error: ${data}`);
}); });
process.on('error', (error) => { process.on("error", (error) => {
reject(error); reject(error);
}); });
process.on('exit', (code, signal) => { process.on("exit", (code, signal) => {
console.log(`Server process exited with code ${code} and signal ${signal}`); console.log(
`Server process exited with code ${code} and signal ${signal}`,
);
if (!serverStarted) { if (!serverStarted) {
reject(new Error(`Server process exited unexpectedly with code ${code}`)); reject(
new Error(`Server process exited unexpectedly with code ${code}`),
);
} }
}); });
@@ -126,62 +147,53 @@ async function startServer(): Promise<ChildProcess> {
// 超时处理 // 超时处理
setTimeout(() => { setTimeout(() => {
if (!serverStarted) { if (!serverStarted) {
reject(new Error('Server failed to start within timeout')); reject(new Error("Server failed to start within timeout"));
} }
}, 30000); // 30秒超时 }, 10000); // 10秒超时
}); });
} }
async function stopServer(): Promise<void> { async function stopServer(): Promise<void> {
console.log('Stopping server...'); console.log("Stopping server...");
if (!serverProcess) { if (!serverProcess) {
console.log('No server process to stop'); console.log("No server process to stop");
return; return;
} }
try { try {
// 检查进程是否还存在 // 检查进程是否还存在
if (serverProcess.killed || serverProcess.exitCode !== null) { if (serverProcess.killed || serverProcess.exitCode !== null) {
console.log('✓ Server process already terminated'); console.log("✓ Server process already terminated");
serverProcess = null; serverProcess = null;
return; return;
} }
// 发送 SIGTERM 信号 // 发送 SIGTERM 信号
const killed = serverProcess.kill('SIGTERM'); const killed = serverProcess.kill("SIGTERM");
if (!killed) { if (!killed) {
console.warn('Failed to send SIGTERM to server process'); console.warn("Failed to send SIGTERM to server process");
return; return;
} }
// 等待进程优雅退出
const exitPromise = new Promise<void>((resolve) => {
if (serverProcess) {
serverProcess.on('exit', () => {
console.log('✓ Server stopped gracefully');
resolve();
});
} else {
resolve();
}
});
// 设置超时,如果 3 秒内没有退出则强制终止 // 设置超时,如果 3 秒内没有退出则强制终止
const timeoutPromise = new Promise<void>((resolve) => { const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => { setTimeout(() => {
if (serverProcess && !serverProcess.killed && serverProcess.exitCode === null) { if (
console.log('Force killing server process...'); serverProcess &&
serverProcess.kill('SIGKILL'); !serverProcess.killed &&
serverProcess.exitCode === null
) {
console.log("Force killing server process...");
serverProcess.kill("SIGKILL");
} }
resolve(); resolve();
}, 3000); // 减少超时时间到3秒 }, 3000); // 减少超时时间到3秒
}); });
await Promise.race([exitPromise, timeoutPromise]); await Promise.race([timeoutPromise]);
} catch (error) { } catch (error) {
console.warn('Warning: Could not stop server process:', error); console.warn("Warning: Could not stop server process:", error);
} finally { } finally {
serverProcess = null; serverProcess = null;
@@ -191,67 +203,51 @@ async function stopServer(): Promise<void> {
} }
async function stopWeb(): Promise<void> { async function stopWeb(): Promise<void> {
console.log('Stopping web server...'); console.log("Stopping web server...");
if (!webProcess) { if (!webProcess) {
console.log('No web process to stop'); console.log("No web process to stop");
return; return;
} }
try { try {
// 检查进程是否还存在 // 检查进程是否还存在
if (webProcess.killed || webProcess.exitCode !== null) { if (webProcess.killed || webProcess.exitCode !== null) {
console.log('✓ Web process already terminated'); console.log("✓ Web process already terminated");
webProcess = null; webProcess = null;
return; return;
} }
// 发送 SIGTERM 信号 // 发送 SIGTERM 信号
const killed = webProcess.kill('SIGTERM'); const killed = webProcess.kill("SIGTERM");
if (!killed) { if (!killed) {
console.warn('Failed to send SIGTERM to web process'); console.warn("Failed to send SIGTERM to web process");
return; return;
} }
// 等待进程优雅退出
const exitPromise = new Promise<void>((resolve) => {
if (webProcess) {
webProcess.on('exit', () => {
console.log('✓ Web server stopped gracefully');
resolve();
});
} else {
resolve();
}
});
// 设置超时,如果 3 秒内没有退出则强制终止 // 设置超时,如果 3 秒内没有退出则强制终止
const timeoutPromise = new Promise<void>((resolve) => { const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => { setTimeout(() => {
if (webProcess && !webProcess.killed && webProcess.exitCode === null) { if (webProcess && !webProcess.killed && webProcess.exitCode === null) {
console.log('Force killing web process...'); console.log("Force killing web process...");
webProcess.kill('SIGKILL'); webProcess.kill("SIGKILL");
} }
resolve(); resolve();
}, 3000); // 减少超时时间到3秒 }, 3000);
}); });
await Promise.race([exitPromise, timeoutPromise]); await Promise.race([timeoutPromise]);
} catch (error) { } catch (error) {
console.warn('Warning: Could not stop web process:', error); console.warn("Warning: Could not stop web process:", error);
} finally { } finally {
webProcess = null; webProcess = null;
// 只有在进程可能没有正常退出时才执行清理
// 移除自动清理逻辑,因为正常退出时不需要
} }
} }
async function postProcessApiClient(): Promise<void> { async function postProcessApiClient(): Promise<void> {
console.log('Post-processing API client...'); console.log("Post-processing API client...");
try { try {
const filePath = 'src/APIClient.ts'; const filePath = "src/APIClient.ts";
// 检查文件是否存在 // 检查文件是否存在
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
@@ -259,37 +255,43 @@ async function postProcessApiClient(): Promise<void> {
} }
// 读取文件内容 // 读取文件内容
let content = fs.readFileSync(filePath, 'utf8'); let content = fs.readFileSync(filePath, "utf8");
// 替换 ArgumentException 中的 message 属性声明 // 替换 ArgumentException 中的 message 属性声明
content = content.replace( content = content.replace(
/(\s+)message!:\s*string;/g, /(\s+)message!:\s*string;/g,
'$1declare message: string;' "$1declare message: string;",
);
content = content.replace(
"{ AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, CancelToken }",
"{ AxiosError, type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type CancelToken }",
); );
// 写回文件 // 写回文件
fs.writeFileSync(filePath, content, 'utf8'); fs.writeFileSync(filePath, content, "utf8");
console.log('✓ API client post-processing completed'); console.log("✓ API client post-processing completed");
} catch (error) { } catch (error) {
throw new Error(`Failed to post-process API client: ${error}`); throw new Error(`Failed to post-process API client: ${error}`);
} }
} }
async function generateApiClient(): Promise<void> { async function generateApiClient(): Promise<void> {
console.log('Generating API client...'); console.log("Generating API client...");
try { try {
const url = 'http://127.0.0.1:5000/GetAPIClientCode'; const url = "http://127.0.0.1:5000/GetAPIClientCode";
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch API client code: ${response.status} ${response.statusText}`); throw new Error(
`Failed to fetch API client code: ${response.status} ${response.statusText}`,
);
} }
const code = await response.text(); const code = await response.text();
// 写入 APIClient.ts // 写入 APIClient.ts
const filePath = 'src/APIClient.ts'; const filePath = "src/APIClient.ts";
fs.writeFileSync(filePath, code, 'utf8'); fs.writeFileSync(filePath, code, "utf8");
console.log('✓ API client code fetched and written successfully'); console.log("✓ API client code fetched and written successfully");
// 添加后处理步骤 // 添加后处理步骤
await postProcessApiClient(); await postProcessApiClient();
@@ -298,35 +300,58 @@ async function generateApiClient(): Promise<void> {
} }
} }
async function generateSignalRClient(): Promise<void> {
console.log("Generating SignalR TypeScript client...");
try {
// TypedSignalR.Client.TypeScript.Analyzer 会在编译时自动生成客户端
// 我们只需要确保服务器项目构建一次即可生成 TypeScript 客户端
const { stdout, stderr } = await execAsync(
"dotnet build --configuration Release",
{ cwd: "./server" }
);
if (stdout) console.log(stdout);
if (stderr) console.error(stderr);
console.log("✓ SignalR TypeScript client generated successfully");
} catch (error) {
throw new Error(`Failed to generate SignalR client: ${error}`);
}
}
async function main(): Promise<void> { async function main(): Promise<void> {
try { try {
// Generate SignalR client
await generateSignalRClient();
console.log("✓ SignalR TypeScript client generated successfully");
// Start web frontend first // Start web frontend first
await startWeb(); await startWeb();
console.log('✓ Frontend started'); console.log("✓ Frontend started");
// Wait a bit for frontend to fully initialize // Wait a bit for frontend to fully initialize
await new Promise(resolve => setTimeout(resolve, 3000)); await new Promise((resolve) => setTimeout(resolve, 3000));
// Start server // Start server
await startServer(); await startServer();
console.log('✓ Backend started'); console.log("✓ Backend started");
// Wait for server to be ready (给服务器额外时间完全启动) // Wait for server to be ready (给服务器额外时间完全启动)
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
// Check if swagger endpoint is available // Check if swagger endpoint is available
const serverReady = await waitForServer('http://localhost:5000/swagger/v1/swagger.json'); const serverReady = await waitForServer(
"http://localhost:5000/swagger/v1/swagger.json",
);
if (!serverReady) { if (!serverReady) {
throw new Error('Server failed to start within the expected time'); throw new Error("Server failed to start within the expected time");
} }
// Generate API client // Generate API client
await generateApiClient(); await generateApiClient();
console.log('✓ API generation completed successfully'); console.log("✓ API generation completed successfully");
} catch (error) { } catch (error) {
console.error('❌ Error:', error); console.error("❌ Error:", error);
process.exit(1); process.exit(1);
} finally { } finally {
// Always try to stop processes in order: server first, then web // Always try to stop processes in order: server first, then web
@@ -340,7 +365,7 @@ let isCleaningUp = false;
const cleanup = async (signal: string) => { const cleanup = async (signal: string) => {
if (isCleaningUp) { if (isCleaningUp) {
console.log('Cleanup already in progress, ignoring signal'); console.log("Cleanup already in progress, ignoring signal");
return; return;
} }
@@ -348,53 +373,44 @@ const cleanup = async (signal: string) => {
console.log(`\nReceived ${signal}, cleaning up...`); console.log(`\nReceived ${signal}, cleaning up...`);
try { try {
await Promise.all([ await Promise.all([stopServer(), stopWeb()]);
stopServer(),
stopWeb()
]);
} catch (error) { } catch (error) {
console.error('Error during cleanup:', error); console.error("Error during cleanup:", error);
} }
// 立即退出,不等待 // 立即退出,不等待
process.exit(0); process.exit(0);
}; };
process.on('SIGINT', () => cleanup('SIGINT')); process.on("SIGINT", () => cleanup("SIGINT"));
process.on('SIGTERM', () => cleanup('SIGTERM')); process.on("SIGTERM", () => cleanup("SIGTERM"));
// 处理未捕获的异常 // 处理未捕获的异常
process.on('uncaughtException', async (error) => { process.on("uncaughtException", async (error) => {
if (isCleaningUp) return; if (isCleaningUp) return;
console.error('❌ Uncaught exception:', error); console.error("❌ Uncaught exception:", error);
isCleaningUp = true; isCleaningUp = true;
try { try {
await Promise.all([ await Promise.all([stopServer(), stopWeb()]);
stopServer(),
stopWeb()
]);
} catch (cleanupError) { } catch (cleanupError) {
console.error('Error during cleanup:', cleanupError); console.error("Error during cleanup:", cleanupError);
} }
process.exit(1); process.exit(1);
}); });
process.on('unhandledRejection', async (reason, promise) => { process.on("unhandledRejection", async (reason, promise) => {
if (isCleaningUp) return; if (isCleaningUp) return;
console.error('❌ Unhandled rejection at:', promise, 'reason:', reason); console.error("❌ Unhandled rejection at:", promise, "reason:", reason);
isCleaningUp = true; isCleaningUp = true;
try { try {
await Promise.all([ await Promise.all([stopServer(), stopWeb()]);
stopServer(),
stopWeb()
]);
} catch (cleanupError) { } catch (cleanupError) {
console.error('Error during cleanup:', cleanupError); console.error("Error during cleanup:", cleanupError);
} }
process.exit(1); process.exit(1);
@@ -403,16 +419,13 @@ process.on('unhandledRejection', async (reason, promise) => {
main().catch(async (error) => { main().catch(async (error) => {
if (isCleaningUp) return; if (isCleaningUp) return;
console.error('❌ Unhandled error:', error); console.error("❌ Unhandled error:", error);
isCleaningUp = true; isCleaningUp = true;
try { try {
await Promise.all([ await Promise.all([stopServer(), stopWeb()]);
stopServer(),
stopWeb()
]);
} catch (cleanupError) { } catch (cleanupError) {
console.error('Error during cleanup:', cleanupError); console.error("Error during cleanup:", cleanupError);
} }
process.exit(1); process.exit(1);

View File

@@ -5,13 +5,13 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json; using Newtonsoft.Json;
using NJsonSchema.CodeGeneration.TypeScript;
using NLog; using NLog;
using NLog.Web; using NLog.Web;
using NSwag; using NSwag;
using NSwag.CodeGeneration.TypeScript; using NSwag.CodeGeneration.TypeScript;
using NSwag.Generation.Processors.Security; using NSwag.Generation.Processors.Security;
using server.Services; using server.Services;
using TypedSignalR.Client.DevTools;
// Early init of NLog to allow startup and exception logging, before host is built // Early init of NLog to allow startup and exception logging, before host is built
var logger = NLog.LogManager.Setup() var logger = NLog.LogManager.Setup()
@@ -95,8 +95,17 @@ try
.AllowAnyMethod() .AllowAnyMethod()
.AllowAnyHeader() .AllowAnyHeader()
); );
options.AddPolicy("SignalR", policy => policy
.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
);
}); });
// Use SignalR
builder.Services.AddSignalR();
// Add Swagger // Add Swagger
builder.Services.AddSwaggerDocument(options => builder.Services.AddSwaggerDocument(options =>
{ {
@@ -199,8 +208,14 @@ try
}); });
app.UseSwaggerUi(); app.UseSwaggerUi();
// SignalR
app.UseWebSockets();
app.UseSignalRHubSpecification();
app.UseSignalRHubDevelopmentUI();
// Router // Router
app.MapControllers(); app.MapControllers();
app.MapHub<server.Hubs.JtagHub.JtagHub>("hubs/JtagHub");
// Setup Program // Setup Program
MsgBus.Init(); MsgBus.Init();
@@ -231,7 +246,7 @@ try
logger.Error(err); logger.Error(err);
return Results.Problem(err.ToString()); return Results.Problem(err.ToString());
} }
}); }).RequireCors("Development");
app.Run(); app.Run();
} }
@@ -252,4 +267,3 @@ finally
// Close Program // Close Program
MsgBus.Exit(); MsgBus.Exit();
} }

View File

@@ -32,6 +32,16 @@
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" /> <PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" /> <PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
<PackageReference Include="Tapper.Analyzer" Version="1.13.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="TypedSignalR.Client.DevTools" Version="1.2.4" />
<PackageReference Include="TypedSignalR.Client.TypeScript.Analyzer" Version="1.15.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="TypedSignalR.Client.TypeScript.Attributes" Version="1.15.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -101,22 +101,6 @@ public class ExamController : ControllerBase
public bool IsVisibleToUsers { get; set; } = true; public bool IsVisibleToUsers { get; set; } = true;
} }
/// <summary>
/// 资源信息类
/// </summary>
public class ResourceInfo
{
/// <summary>
/// 资源ID
/// </summary>
public int ID { get; set; }
/// <summary>
/// 资源名称
/// </summary>
public required string Name { get; set; }
}
/// <summary> /// <summary>
/// 创建实验请求类 /// 创建实验请求类
/// </summary> /// </summary>
@@ -304,151 +288,4 @@ public class ExamController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {ex.Message}"); return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {ex.Message}");
} }
} }
/// <summary>
/// 添加实验资源
/// </summary>
/// <param name="examId">实验ID</param>
/// <param name="resourceType">资源类型</param>
/// <param name="file">资源文件</param>
/// <returns>添加结果</returns>
[Authorize("Admin")]
[HttpPost("{examId}/resources/{resourceType}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> AddExamResource(string examId, string resourceType, IFormFile file)
{
if (string.IsNullOrWhiteSpace(examId) || string.IsNullOrWhiteSpace(resourceType) || file == null)
return BadRequest("实验ID、资源类型和文件不能为空");
try
{
using var db = new Database.AppDataConnection();
// 读取文件数据
using var memoryStream = new MemoryStream();
await file.CopyToAsync(memoryStream);
var fileData = memoryStream.ToArray();
var result = db.AddExamResource(examId, resourceType, file.FileName, fileData);
if (!result.IsSuccessful)
{
if (result.Error.Message.Contains("不存在"))
return NotFound(result.Error.Message);
if (result.Error.Message.Contains("已存在"))
return Conflict(result.Error.Message);
logger.Error($"添加实验资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加实验资源失败: {result.Error.Message}");
}
var resource = result.Value;
var resourceInfo = new ResourceInfo
{
ID = resource.ID,
Name = resource.ResourceName
};
logger.Info($"成功添加实验资源: {examId}/{resourceType}/{file.FileName}");
return CreatedAtAction(nameof(GetExamResourceById), new { resourceId = resource.ID }, resourceInfo);
}
catch (Exception ex)
{
logger.Error($"添加实验资源 {examId}/{resourceType}/{file.FileName} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加实验资源失败: {ex.Message}");
}
}
/// <summary>
/// 获取指定实验ID的指定资源类型的所有资源的ID和名称
/// </summary>
/// <param name="examId">实验ID</param>
/// <param name="resourceType">资源类型</param>
/// <returns>资源列表</returns>
[Authorize]
[HttpGet("{examId}/resources/{resourceType}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetExamResourceList(string examId, string resourceType)
{
if (string.IsNullOrWhiteSpace(examId) || string.IsNullOrWhiteSpace(resourceType))
return BadRequest("实验ID和资源类型不能为空");
try
{
using var db = new Database.AppDataConnection();
var result = db.GetExamResourceList(examId, resourceType);
if (!result.IsSuccessful)
{
logger.Error($"获取实验资源列表时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验资源列表失败: {result.Error.Message}");
}
var resources = result.Value.Select(r => new ResourceInfo
{
ID = r.ID,
Name = r.Name
}).ToArray();
logger.Info($"成功获取实验资源列表: {examId}/{resourceType},共 {resources.Length} 个资源");
return Ok(resources);
}
catch (Exception ex)
{
logger.Error($"获取实验资源列表 {examId}/{resourceType} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验资源列表失败: {ex.Message}");
}
}
/// <summary>
/// 根据资源ID下载资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>资源文件</returns>
[HttpGet("resources/{resourceId}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetExamResourceById(int resourceId)
{
try
{
using var db = new Database.AppDataConnection();
var result = db.GetExamResourceById(resourceId);
if (!result.IsSuccessful)
{
logger.Error($"获取资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {result.Error.Message}");
}
if (!result.Value.HasValue)
{
logger.Warn($"资源不存在: {resourceId}");
return NotFound($"资源 {resourceId} 不存在");
}
var resource = result.Value.Value;
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
return File(resource.Data, resource.MimeType, resource.ResourceName);
}
catch (Exception ex)
{
logger.Error($"获取资源 {resourceId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {ex.Message}");
}
}
} }

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Database;
namespace server.Controllers; namespace server.Controllers;
@@ -14,8 +15,6 @@ public class JtagController : ControllerBase
{ {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private const string BITSTREAM_PATH = "bitstream/Jtag";
/// <summary> /// <summary>
/// 控制器首页信息 /// 控制器首页信息
/// </summary> /// </summary>
@@ -112,64 +111,12 @@ public class JtagController : ControllerBase
} }
} }
/// <summary>
/// 上传比特流文件到服务器
/// </summary>
/// <param name="address">目标设备地址</param>
/// <param name="file">比特流文件</param>
/// <returns>上传结果</returns>
[HttpPost("UploadBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> UploadBitstream(string address, IFormFile file)
{
logger.Info($"User {User.Identity?.Name} uploading bitstream for device {address}");
if (file == null || file.Length == 0)
{
logger.Warn($"User {User.Identity?.Name} attempted to upload empty file for device {address}");
return TypedResults.BadRequest("未选择文件");
}
try
{
// 生成安全的文件名(避免路径遍历攻击)
var fileName = Path.GetRandomFileName();
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
// 如果存在文件,则删除原文件再上传
if (Directory.Exists(uploadsFolder))
{
Directory.Delete(uploadsFolder, true);
logger.Info($"User {User.Identity?.Name} removed existing bitstream folder for device {address}");
}
Directory.CreateDirectory(uploadsFolder);
var filePath = Path.Combine(uploadsFolder, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
logger.Info($"User {User.Identity?.Name} successfully uploaded bitstream for device {address}, file size: {file.Length} bytes");
return TypedResults.Ok(true);
}
catch (Exception ex)
{
logger.Error(ex, $"User {User.Identity?.Name} failed to upload bitstream for device {address}");
return TypedResults.InternalServerError(ex);
}
}
/// <summary> /// <summary>
/// 通过 JTAG 下载比特流文件到 FPGA 设备 /// 通过 JTAG 下载比特流文件到 FPGA 设备
/// </summary> /// </summary>
/// <param name="address">JTAG 设备地址</param> /// <param name="address">JTAG 设备地址</param>
/// <param name="port">JTAG 设备端口</param> /// <param name="port">JTAG 设备端口</param>
/// <param name="bitstreamId">比特流ID</param>
/// <returns>下载结果</returns> /// <returns>下载结果</returns>
[HttpPost("DownloadBitstream")] [HttpPost("DownloadBitstream")]
[EnableCors("Users")] [EnableCors("Users")]
@@ -177,87 +124,111 @@ public class JtagController : ControllerBase
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> DownloadBitstream(string address, int port) public async ValueTask<IResult> DownloadBitstream(string address, int port, int bitstreamId)
{ {
logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port}"); logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}");
// 检查文件
var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
if (!Directory.Exists(fileDir))
{
logger.Warn($"User {User.Identity?.Name} attempted to download non-existent bitstream for device {address}");
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
}
try try
{ {
// 读取文件 // 获取当前用户名
var filePath = Directory.GetFiles(fileDir)[0]; var username = User.Identity?.Name;
logger.Info($"User {User.Identity?.Name} reading bitstream file: {filePath}"); if (string.IsNullOrEmpty(username))
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
{ {
if (fileStream is null || fileStream.Length <= 0) logger.Warn("Anonymous user attempted to download bitstream");
return TypedResults.Unauthorized();
}
// 从数据库获取用户信息
using var db = new Database.AppDataConnection();
var userResult = db.GetUserByName(username);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
{
logger.Error($"User {username} not found in database");
return TypedResults.BadRequest("用户不存在");
}
var user = userResult.Value.Value;
// 从数据库获取比特流
var bitstreamResult = db.GetResourceById(bitstreamId);
if (!bitstreamResult.IsSuccessful)
{
logger.Error($"User {username} failed to get bitstream from database: {bitstreamResult.Error}");
return TypedResults.InternalServerError($"数据库查询失败: {bitstreamResult.Error?.Message}");
}
if (!bitstreamResult.Value.HasValue)
{
logger.Warn($"User {username} attempted to download non-existent bitstream ID: {bitstreamId}");
return TypedResults.BadRequest("比特流不存在");
}
var bitstream = bitstreamResult.Value.Value;
// 处理比特流数据
var fileBytes = bitstream.Data;
if (fileBytes == null || fileBytes.Length == 0)
{
logger.Warn($"User {username} found empty bitstream data for ID: {bitstreamId}");
return TypedResults.BadRequest("比特流数据为空,请重新上传");
}
logger.Info($"User {username} processing bitstream file of size: {fileBytes.Length} bytes");
// 定义缓冲区大小: 32KB
byte[] buffer = new byte[32 * 1024];
byte[] revBuffer = new byte[32 * 1024];
long totalBytesProcessed = 0;
// 使用内存流处理文件
using (var inputStream = new MemoryStream(fileBytes))
using (var outputStream = new MemoryStream())
{
int bytesRead;
while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{ {
logger.Warn($"User {User.Identity?.Name} found invalid bitstream file for device {address}"); // 反转 32bits
return TypedResults.BadRequest("Wrong bitstream, Please upload it again"); var retBuffer = Common.Number.ReverseBytes(buffer, 4);
if (!retBuffer.IsSuccessful)
{
logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}");
return TypedResults.InternalServerError(retBuffer.Error);
}
revBuffer = retBuffer.Value;
for (int i = 0; i < revBuffer.Length; i++)
{
revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]);
}
await outputStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesProcessed += bytesRead;
} }
logger.Info($"User {User.Identity?.Name} processing bitstream file of size: {fileStream.Length} bytes"); // 获取处理后的数据
var processedBytes = outputStream.ToArray();
logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}");
// 定义缓冲区大小: 32KB // 下载比特流
byte[] buffer = new byte[32 * 1024]; var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
byte[] revBuffer = new byte[32 * 1024]; var ret = await jtagCtrl.DownloadBitstream(processedBytes);
long totalBytesRead = 0;
// 使用异步流读取文件 if (ret.IsSuccessful)
using (var memoryStream = new MemoryStream())
{ {
int bytesRead; logger.Info($"User {username} successfully downloaded bitstream '{bitstream.ResourceName}' to device {address}");
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0) return TypedResults.Ok(ret.Value);
{ }
// 反转 32bits else
var retBuffer = Common.Number.ReverseBytes(buffer, 4); {
if (!retBuffer.IsSuccessful) logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}");
{ return TypedResults.InternalServerError(ret.Error);
logger.Error($"User {User.Identity?.Name} failed to reverse bytes: {retBuffer.Error}");
return TypedResults.InternalServerError(retBuffer.Error);
}
revBuffer = retBuffer.Value;
for (int i = 0; i < revBuffer.Length; i++)
{
revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]);
}
await memoryStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesRead += bytesRead;
}
// 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存)
var fileBytes = memoryStream.ToArray();
logger.Info($"User {User.Identity?.Name} processed {totalBytesRead} bytes for device {address}");
// 下载比特流
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.DownloadBitstream(fileBytes);
if (ret.IsSuccessful)
{
logger.Info($"User {User.Identity?.Name} successfully downloaded bitstream to device {address}");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error($"User {User.Identity?.Name} failed to download bitstream to device {address}: {ret.Error}");
return TypedResults.InternalServerError(ret.Error);
}
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while downloading bitstream to device {address}"); logger.Error(ex, $"User encountered exception while downloading bitstream to device {address}");
return TypedResults.InternalServerError(ex); return TypedResults.InternalServerError(ex);
} }
} }

View File

@@ -0,0 +1,377 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using DotNext;
using Database;
namespace server.Controllers;
/// <summary>
/// 资源控制器 - 提供统一的资源管理API
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class ResourceController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// 资源信息类
/// </summary>
public class ResourceInfo
{
/// <summary>
/// 资源ID
/// </summary>
public int ID { get; set; }
/// <summary>
/// 资源名称
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 资源类型
/// </summary>
public required string Type { get; set; }
/// <summary>
/// 资源用途template/user
/// </summary>
public required string Purpose { get; set; }
/// <summary>
/// 上传时间
/// </summary>
public DateTime UploadTime { get; set; }
/// <summary>
/// 所属实验ID可选
/// </summary>
public string? ExamID { get; set; }
/// <summary>
/// MIME类型
/// </summary>
public string? MimeType { get; set; }
}
/// <summary>
/// 添加资源请求类
/// </summary>
public class AddResourceRequest
{
/// <summary>
/// 资源类型
/// </summary>
public required string ResourceType { get; set; }
/// <summary>
/// 资源用途template/user
/// </summary>
public required string ResourcePurpose { get; set; }
/// <summary>
/// 所属实验ID可选
/// </summary>
public string? ExamID { get; set; }
}
/// <summary>
/// 添加资源(文件上传)
/// </summary>
/// <param name="request">添加资源请求</param>
/// <param name="file">资源文件</param>
/// <returns>添加结果</returns>
[Authorize]
[HttpPost]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> AddResource([FromForm] AddResourceRequest request, IFormFile file)
{
if (string.IsNullOrWhiteSpace(request.ResourceType) || string.IsNullOrWhiteSpace(request.ResourcePurpose) || file == null)
return BadRequest("资源类型、资源用途和文件不能为空");
// 验证资源用途
if (request.ResourcePurpose != Resource.ResourcePurposes.Template && request.ResourcePurpose != Resource.ResourcePurposes.User)
return BadRequest($"无效的资源用途: {request.ResourcePurpose}");
// 模板资源需要管理员权限
if (request.ResourcePurpose == Resource.ResourcePurposes.Template && !User.IsInRole("Admin"))
return Forbid("只有管理员可以添加模板资源");
try
{
using var db = new Database.AppDataConnection();
// 获取当前用户ID
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = db.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 读取文件数据
using var memoryStream = new MemoryStream();
await file.CopyToAsync(memoryStream);
var fileData = memoryStream.ToArray();
var result = db.AddResource(user.ID, request.ResourceType, request.ResourcePurpose, file.FileName, fileData, request.ExamID);
if (!result.IsSuccessful)
{
if (result.Error.Message.Contains("不存在"))
return NotFound(result.Error.Message);
logger.Error($"添加资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {result.Error.Message}");
}
var resource = result.Value;
var resourceInfo = new ResourceInfo
{
ID = resource.ID,
Name = resource.ResourceName,
Type = resource.ResourceType,
Purpose = resource.ResourcePurpose,
UploadTime = resource.UploadTime,
ExamID = resource.ExamID,
MimeType = resource.MimeType
};
logger.Info($"成功添加资源: {request.ResourceType}/{request.ResourcePurpose}/{file.FileName}");
return CreatedAtAction(nameof(GetResourceById), new { resourceId = resource.ID }, resourceInfo);
}
catch (Exception ex)
{
logger.Error($"添加资源 {request.ResourceType}/{request.ResourcePurpose}/{file.FileName} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {ex.Message}");
}
}
/// <summary>
/// 获取资源列表
/// </summary>
/// <param name="examId">实验ID可选</param>
/// <param name="resourceType">资源类型(可选)</param>
/// <param name="resourcePurpose">资源用途(可选)</param>
/// <returns>资源列表</returns>
[Authorize]
[HttpGet]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetResourceList([FromQuery] string? examId = null, [FromQuery] string? resourceType = null, [FromQuery] string? resourcePurpose = null)
{
try
{
using var db = new Database.AppDataConnection();
// 获取当前用户ID
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = db.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 普通用户只能查看自己的资源和模板资源
Guid? userId = null;
if (!User.IsInRole("Admin"))
{
// 如果指定了用户资源用途,则只查看自己的资源
if (resourcePurpose == Resource.ResourcePurposes.User)
{
userId = user.ID;
}
// 如果指定了模板资源用途则不限制用户ID
else if (resourcePurpose == Resource.ResourcePurposes.Template)
{
userId = null;
}
// 如果没有指定用途,则查看自己的用户资源和所有模板资源
else
{
// 这种情况下需要分别查询并合并结果
var userResourcesResult = db.GetFullResourceList(examId, resourceType, Resource.ResourcePurposes.User, user.ID);
var templateResourcesResult = db.GetFullResourceList(examId, resourceType, Resource.ResourcePurposes.Template, null);
if (!userResourcesResult.IsSuccessful || !templateResourcesResult.IsSuccessful)
{
logger.Error($"获取资源列表时出错");
return StatusCode(StatusCodes.Status500InternalServerError, "获取资源列表失败");
}
var allResources = userResourcesResult.Value.Concat(templateResourcesResult.Value)
.OrderByDescending(r => r.UploadTime);
var mergedResourceInfos = allResources.Select(r => new ResourceInfo
{
ID = r.ID,
Name = r.ResourceName,
Type = r.ResourceType,
Purpose = r.ResourcePurpose,
UploadTime = r.UploadTime,
ExamID = r.ExamID,
MimeType = r.MimeType
}).ToArray();
logger.Info($"成功获取资源列表,共 {mergedResourceInfos.Length} 个资源");
return Ok(mergedResourceInfos);
}
}
var result = db.GetFullResourceList(examId, resourceType, resourcePurpose, userId);
if (!result.IsSuccessful)
{
logger.Error($"获取资源列表时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {result.Error.Message}");
}
var resources = result.Value.Select(r => new ResourceInfo
{
ID = r.ID,
Name = r.ResourceName,
Type = r.ResourceType,
Purpose = r.ResourcePurpose,
UploadTime = r.UploadTime,
ExamID = r.ExamID,
MimeType = r.MimeType
}).ToArray();
logger.Info($"成功获取资源列表,共 {resources.Length} 个资源");
return Ok(resources);
}
catch (Exception ex)
{
logger.Error($"获取资源列表时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {ex.Message}");
}
}
/// <summary>
/// 根据资源ID下载资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>资源文件</returns>
[HttpGet("{resourceId}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetResourceById(int resourceId)
{
try
{
using var db = new Database.AppDataConnection();
var result = db.GetResourceById(resourceId);
if (!result.IsSuccessful)
{
logger.Error($"获取资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {result.Error.Message}");
}
if (!result.Value.HasValue)
{
logger.Warn($"资源不存在: {resourceId}");
return NotFound($"资源 {resourceId} 不存在");
}
var resource = result.Value.Value;
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
return File(resource.Data, resource.MimeType ?? "application/octet-stream", resource.ResourceName);
}
catch (Exception ex)
{
logger.Error($"获取资源 {resourceId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {ex.Message}");
}
}
/// <summary>
/// 删除资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>删除结果</returns>
[Authorize]
[HttpDelete("{resourceId}")]
[EnableCors("Users")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult DeleteResource(int resourceId)
{
try
{
using var db = new Database.AppDataConnection();
// 获取当前用户信息
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = db.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 先获取资源信息以验证权限
var resourceResult = db.GetResourceById(resourceId);
if (!resourceResult.IsSuccessful)
{
logger.Error($"获取资源时出错: {resourceResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {resourceResult.Error.Message}");
}
if (!resourceResult.Value.HasValue)
{
logger.Warn($"资源不存在: {resourceId}");
return NotFound($"资源 {resourceId} 不存在");
}
var resource = resourceResult.Value.Value;
// 权限检查:管理员可以删除所有资源,普通用户只能删除自己的用户资源
if (!User.IsInRole("Admin"))
{
if (resource.ResourcePurpose == Resource.ResourcePurposes.Template)
return Forbid("普通用户不能删除模板资源");
if (resource.UserID != user.ID)
return Forbid("只能删除自己的资源");
}
var deleteResult = db.DeleteResource(resourceId);
if (!deleteResult.IsSuccessful)
{
logger.Error($"删除资源时出错: {deleteResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"删除资源失败: {deleteResult.Error.Message}");
}
logger.Info($"成功删除资源: {resourceId} ({resource.ResourceName})");
return NoContent();
}
catch (Exception ex)
{
logger.Error($"删除资源 {resourceId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"删除资源失败: {ex.Message}");
}
}
}

View File

@@ -229,9 +229,9 @@ public class Exam
} }
/// <summary> /// <summary>
/// 实验资源表(图片等) /// 资源类,统一管理实验资源、用户比特流等各类资源
/// </summary> /// </summary>
public class ExamResource public class Resource
{ {
/// <summary> /// <summary>
/// 资源的唯一标识符 /// 资源的唯一标识符
@@ -240,17 +240,29 @@ public class ExamResource
public int ID { get; set; } public int ID { get; set; }
/// <summary> /// <summary>
/// 所属实验ID /// 上传资源的用户ID
/// </summary> /// </summary>
[NotNull] [NotNull]
public required string ExamID { get; set; } public required Guid UserID { get; set; }
/// <summary> /// <summary>
/// 资源类型images, markdown, bitstream, diagram, project /// 所属实验ID可选如果不属于特定实验则为空
/// </summary>
[Nullable]
public string? ExamID { get; set; }
/// <summary>
/// 资源类型images, markdown, bitstream, diagram, project等
/// </summary> /// </summary>
[NotNull] [NotNull]
public required string ResourceType { get; set; } public required string ResourceType { get; set; }
/// <summary>
/// 资源用途template模板或 user用户上传
/// </summary>
[NotNull]
public required string ResourcePurpose { get; set; }
/// <summary> /// <summary>
/// 资源名称(包含文件扩展名) /// 资源名称(包含文件扩展名)
/// </summary> /// </summary>
@@ -264,10 +276,10 @@ public class ExamResource
public required byte[] Data { get; set; } public required byte[] Data { get; set; }
/// <summary> /// <summary>
/// 资源创建时间 /// 资源创建/上传时间
/// </summary> /// </summary>
[NotNull] [NotNull]
public DateTime CreatedTime { get; set; } = DateTime.Now; public DateTime UploadTime { get; set; } = DateTime.Now;
/// <summary> /// <summary>
/// 资源的MIME类型 /// 资源的MIME类型
@@ -305,6 +317,22 @@ public class ExamResource
/// </summary> /// </summary>
public const string Project = "project"; public const string Project = "project";
} }
/// <summary>
/// 资源用途枚举
/// </summary>
public static class ResourcePurposes
{
/// <summary>
/// 模板资源,通常由管理员上传,供用户参考
/// </summary>
public const string Template = "template";
/// <summary>
/// 用户上传的资源
/// </summary>
public const string User = "user";
}
} }
/// <summary> /// <summary>
@@ -355,7 +383,7 @@ public class AppDataConnection : DataConnection
this.CreateTable<User>(); this.CreateTable<User>();
this.CreateTable<Board>(); this.CreateTable<Board>();
this.CreateTable<Exam>(); this.CreateTable<Exam>();
this.CreateTable<ExamResource>(); this.CreateTable<Resource>();
logger.Info("数据库表创建完成"); logger.Info("数据库表创建完成");
} }
@@ -368,7 +396,7 @@ public class AppDataConnection : DataConnection
this.DropTable<User>(); this.DropTable<User>();
this.DropTable<Board>(); this.DropTable<Board>();
this.DropTable<Exam>(); this.DropTable<Exam>();
this.DropTable<ExamResource>(); this.DropTable<Resource>();
logger.Warn("所有数据库表已删除"); logger.Warn("所有数据库表已删除");
} }
@@ -687,6 +715,31 @@ public class AppDataConnection : DataConnection
return new(boards[0]); return new(boards[0]);
} }
/// <summary>
/// 根据用户名获取实验板信息
/// </summary>
/// <param name="userName">用户名</param>
/// <returns>包含实验板信息的结果,如果未找到则返回空</returns>
public Result<Optional<Board>> GetBoardByUserName(string userName)
{
var boards = this.BoardTable.Where(board => board.OccupiedUserName == userName).ToArray();
if (boards.Length > 1)
{
logger.Error($"数据库中存在多个相同用户名的实验板: {userName}");
return new(new Exception($"数据库中存在多个相同用户名的实验板: {userName}"));
}
if (boards.Length == 0)
{
logger.Info($"未找到用户名对应的实验板: {userName}");
return new(Optional<Board>.None);
}
logger.Debug($"成功获取实验板信息: {userName}");
return new(boards[0]);
}
/// <summary> /// <summary>
/// 获取所有实验板信息 /// 获取所有实验板信息
/// </summary> /// </summary>
@@ -803,9 +856,9 @@ public class AppDataConnection : DataConnection
public ITable<Exam> ExamTable => this.GetTable<Exam>(); public ITable<Exam> ExamTable => this.GetTable<Exam>();
/// <summary> /// <summary>
/// 实验资源表 /// 资源表(统一管理实验资源、用户比特流等)
/// </summary> /// </summary>
public ITable<ExamResource> ExamResourceTable => this.GetTable<ExamResource>(); public ITable<Resource> ResourceTable => this.GetTable<Resource>();
/// <summary> /// <summary>
/// 创建新实验 /// 创建新实验
@@ -908,34 +961,44 @@ public class AppDataConnection : DataConnection
} }
/// <summary> /// <summary>
/// 添加实验资源 /// 添加资源
/// </summary> /// </summary>
/// <param name="examId">所属实验ID</param> /// <param name="userId">上传用户ID</param>
/// <param name="resourceType">资源类型</param> /// <param name="resourceType">资源类型</param>
/// <param name="resourcePurpose">资源用途template 或 user</param>
/// <param name="resourceName">资源名称</param> /// <param name="resourceName">资源名称</param>
/// <param name="data">资源二进制数据</param> /// <param name="data">资源二进制数据</param>
/// <param name="examId">所属实验ID可选</param>
/// <param name="mimeType">MIME类型可选将根据文件扩展名自动确定</param> /// <param name="mimeType">MIME类型可选将根据文件扩展名自动确定</param>
/// <returns>创建的资源</returns> /// <returns>创建的资源</returns>
public Result<ExamResource> AddExamResource(string examId, string resourceType, string resourceName, byte[] data, string? mimeType = null) public Result<Resource> AddResource(Guid userId, string resourceType, string resourcePurpose, string resourceName, byte[] data, string? examId = null, string? mimeType = null)
{ {
try try
{ {
// 验证实验是否存在 // 验证用户是否存在
var exam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault(); var user = this.UserTable.Where(u => u.ID == userId).FirstOrDefault();
if (exam == null) if (user == null)
{ {
logger.Error($"实验不存在: {examId}"); logger.Error($"用户不存在: {userId}");
return new(new Exception($"实验不存在: {examId}")); return new(new Exception($"用户不存在: {userId}"));
} }
// 检查资源是否存在 // 如果指定了实验ID验证实验是否存在
var existingResource = this.ExamResourceTable if (!string.IsNullOrEmpty(examId))
.Where(r => r.ExamID == examId && r.ResourceType == resourceType && r.ResourceName == resourceName)
.FirstOrDefault();
if (existingResource != null)
{ {
logger.Error($"资源已存在: {examId}/{resourceType}/{resourceName}"); var exam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault();
return new(new Exception($"资源已存在: {examId}/{resourceType}/{resourceName}")); if (exam == null)
{
logger.Error($"实验不存在: {examId}");
return new(new Exception($"实验不存在: {examId}"));
}
}
// 验证资源用途
if (resourcePurpose != Resource.ResourcePurposes.Template && resourcePurpose != Resource.ResourcePurposes.User)
{
logger.Error($"无效的资源用途: {resourcePurpose}");
return new(new Exception($"无效的资源用途: {resourcePurpose}"));
} }
// 如果未指定MIME类型根据文件扩展名自动确定 // 如果未指定MIME类型根据文件扩展名自动确定
@@ -945,49 +1008,126 @@ public class AppDataConnection : DataConnection
mimeType = GetMimeTypeFromExtension(extension, resourceName); mimeType = GetMimeTypeFromExtension(extension, resourceName);
} }
var resource = new ExamResource var resource = new Resource
{ {
UserID = userId,
ExamID = examId, ExamID = examId,
ResourceType = resourceType, ResourceType = resourceType,
ResourcePurpose = resourcePurpose,
ResourceName = resourceName, ResourceName = resourceName,
Data = data, Data = data,
MimeType = mimeType, MimeType = mimeType,
CreatedTime = DateTime.Now UploadTime = DateTime.Now
}; };
this.Insert(resource); var insertedId = this.InsertWithIdentity(resource);
logger.Info($"新资源已添加: {examId}/{resourceType}/{resourceName} ({data.Length} bytes)"); resource.ID = Convert.ToInt32(insertedId);
logger.Info($"新资源已添加: {userId}/{resourceType}/{resourcePurpose}/{resourceName} ({data.Length} bytes)" +
(examId != null ? $" [实验: {examId}]" : "") + $" [ID: {resource.ID}]");
return new(resource); return new(resource);
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.Error($"添加实验资源时出错: {ex.Message}"); logger.Error($"添加资源时出错: {ex.Message}");
return new(ex); return new(ex);
} }
} }
/// <summary> /// <summary>
/// 获取指定实验ID的指定资源类型的所有资源的ID和名称 /// 获取资源信息列表(返回ID和名称
/// </summary>
/// <param name="examId">实验ID</param>
/// <param name="resourceType">资源类型</param> /// <param name="resourceType">资源类型</param>
/// <param name="examId">实验ID可选</param>
/// <param name="resourcePurpose">资源用途(可选)</param>
/// <param name="userId">用户ID可选</param>
/// </summary>
/// <returns>资源信息列表</returns> /// <returns>资源信息列表</returns>
public Result<(int ID, string Name)[]> GetExamResourceList(string examId, string resourceType) public Result<(int ID, string Name)[]> GetResourceList(string resourceType, string? examId = null, string? resourcePurpose = null, Guid? userId = null)
{ {
try try
{ {
var resources = this.ExamResourceTable var query = this.ResourceTable.Where(r => r.ResourceType == resourceType);
.Where(r => r.ExamID == examId && r.ResourceType == resourceType)
if (examId != null)
{
query = query.Where(r => r.ExamID == examId);
}
if (resourcePurpose != null)
{
query = query.Where(r => r.ResourcePurpose == resourcePurpose);
}
if (userId != null)
{
query = query.Where(r => r.UserID == userId);
}
var resources = query
.Select(r => new { r.ID, r.ResourceName }) .Select(r => new { r.ID, r.ResourceName })
.ToArray(); .ToArray();
var result = resources.Select(r => (r.ID, r.ResourceName)).ToArray(); var result = resources.Select(r => (r.ID, r.ResourceName)).ToArray();
logger.Info($"获取实验资源列表: {examId}/{resourceType},共 {result.Length} 个资源"); logger.Info($"获取资源列表: {resourceType}" +
(examId != null ? $"/{examId}" : "") +
(resourcePurpose != null ? $"/{resourcePurpose}" : "") +
(userId != null ? $"/{userId}" : "") +
$",共 {result.Length} 个资源");
return new(result); return new(result);
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.Error($"获取实验资源列表时出错: {ex.Message}"); logger.Error($"获取资源列表时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 获取完整的资源列表
/// </summary>
/// <param name="examId">实验ID可选</param>
/// <param name="resourceType">资源类型(可选)</param>
/// <param name="resourcePurpose">资源用途(可选)</param>
/// <param name="userId">用户ID可选</param>
/// <returns>完整的资源对象列表</returns>
public Result<List<Resource>> GetFullResourceList(string? examId = null, string? resourceType = null, string? resourcePurpose = null, Guid? userId = null)
{
try
{
var query = this.ResourceTable.AsQueryable();
if (examId != null)
{
query = query.Where(r => r.ExamID == examId);
}
if (resourceType != null)
{
query = query.Where(r => r.ResourceType == resourceType);
}
if (resourcePurpose != null)
{
query = query.Where(r => r.ResourcePurpose == resourcePurpose);
}
if (userId != null)
{
query = query.Where(r => r.UserID == userId);
}
var resources = query.OrderByDescending(r => r.UploadTime).ToList();
logger.Info($"获取完整资源列表" +
(examId != null ? $" [实验: {examId}]" : "") +
(resourceType != null ? $" [类型: {resourceType}]" : "") +
(resourcePurpose != null ? $" [用途: {resourcePurpose}]" : "") +
(userId != null ? $" [用户: {userId}]" : "") +
$",共 {resources.Count} 个资源");
return new(resources);
}
catch (Exception ex)
{
logger.Error($"获取完整资源列表时出错: {ex.Message}");
return new(ex); return new(ex);
} }
} }
@@ -997,16 +1137,16 @@ public class AppDataConnection : DataConnection
/// </summary> /// </summary>
/// <param name="resourceId">资源ID</param> /// <param name="resourceId">资源ID</param>
/// <returns>资源数据</returns> /// <returns>资源数据</returns>
public Result<Optional<ExamResource>> GetExamResourceById(int resourceId) public Result<Optional<Resource>> GetResourceById(int resourceId)
{ {
try try
{ {
var resource = this.ExamResourceTable.Where(r => r.ID == resourceId).FirstOrDefault(); var resource = this.ResourceTable.Where(r => r.ID == resourceId).FirstOrDefault();
if (resource == null) if (resource == null)
{ {
logger.Info($"未找到资源: {resourceId}"); logger.Info($"未找到资源: {resourceId}");
return new(Optional<ExamResource>.None); return new(Optional<Resource>.None);
} }
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})"); logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
@@ -1020,15 +1160,15 @@ public class AppDataConnection : DataConnection
} }
/// <summary> /// <summary>
/// 删除实验资源 /// 删除资源
/// </summary> /// </summary>
/// <param name="resourceId">资源ID</param> /// <param name="resourceId">资源ID</param>
/// <returns>删除的记录数</returns> /// <returns>删除的记录数</returns>
public Result<int> DeleteExamResource(int resourceId) public Result<int> DeleteResource(int resourceId)
{ {
try try
{ {
var result = this.ExamResourceTable.Where(r => r.ID == resourceId).Delete(); var result = this.ResourceTable.Where(r => r.ID == resourceId).Delete();
logger.Info($"资源已删除: {resourceId},删除记录数: {result}"); logger.Info($"资源已删除: {resourceId},删除记录数: {result}");
return new(result); return new(result);
} }
@@ -1107,29 +1247,20 @@ public class AppDataConnection : DataConnection
} }
/// <summary> /// <summary>
/// 删除所有实验 /// 根据文件扩展名获取比特流MIME类型
/// </summary> /// </summary>
/// <returns>删除的实验数量</returns> /// <param name="extension">文件扩展名</param>
public int DeleteAllExams() /// <returns>MIME类型</returns>
private string GetBitstreamMimeType(string extension)
{ {
// 先删除所有实验资源 return extension.ToLowerInvariant() switch
var resourceDeleteCount = this.DeleteAllExamResources(); {
logger.Info($"已删除所有实验资源,共删除 {resourceDeleteCount} 个资源"); ".bit" => "application/octet-stream",
".sbit" => "application/octet-stream",
// 再删除所有实验 ".bin" => "application/octet-stream",
var examDeleteCount = this.ExamTable.Delete(); ".mcs" => "application/octet-stream",
logger.Info($"已删除所有实验,共删除 {examDeleteCount} 个实验"); ".hex" => "text/plain",
return examDeleteCount; _ => "application/octet-stream"
} };
/// <summary>
/// 删除所有实验资源
/// </summary>
/// <returns>删除的资源数量</returns>
public int DeleteAllExamResources()
{
var deleteCount = this.ExamResourceTable.Delete();
logger.Info($"已删除所有实验资源,共删除 {deleteCount} 个资源");
return deleteCount;
} }
} }

196
server/src/Hubs/JtagHub.cs Normal file
View File

@@ -0,0 +1,196 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
using DotNext;
using System.Collections.Concurrent;
using TypedSignalR.Client;
using Tapper;
namespace server.Hubs.JtagHub;
[Hub]
public interface IJtagHub
{
Task<bool> SetBoundaryScanFreq(int freq);
Task<bool> StartBoundaryScan(int freq = 100);
Task<bool> StopBoundaryScan();
}
[Receiver]
public interface IJtagReceiver
{
Task OnReceiveBoundaryScanData(Dictionary<string, bool> msg);
}
[Authorize]
[EnableCors("SignalR")]
public class JtagHub : Hub<IJtagReceiver>, IJtagHub
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private static ConcurrentDictionary<string, int> FreqTable = new();
private static ConcurrentDictionary<string, CancellationTokenSource> CancellationTokenSourceTable = new();
private readonly IHubContext<JtagHub, IJtagReceiver> _hubContext;
public JtagHub(IHubContext<JtagHub, IJtagReceiver> hubContext)
{
_hubContext = hubContext;
}
private Optional<Peripherals.JtagClient.Jtag> GetJtagClient(string userName)
{
try
{
using var db = new Database.AppDataConnection();
var board = db.GetBoardByUserName(userName);
if (!board.IsSuccessful)
{
logger.Error($"Find Board {board.Value.Value.ID} failed because {board.Error}");
return new(null);
}
if (!board.Value.HasValue)
{
logger.Error($"Board {board.Value.Value.ID} not found");
return new(null);
}
var jtag = new Peripherals.JtagClient.Jtag(board.Value.Value.IpAddr, board.Value.Value.Port);
return new(jtag);
}
catch (Exception error)
{
logger.Error(error);
return new(null);
}
}
public async Task<bool> SetBoundaryScanFreq(int freq)
{
try
{
var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
if (userName is null)
{
logger.Error("Can't get user info");
return false;
}
FreqTable.AddOrUpdate(userName, freq, (key, value) => freq);
return true;
}
catch (Exception error)
{
logger.Error(error);
return false;
}
}
public async Task<bool> StartBoundaryScan(int freq = 100)
{
try
{
var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
if (userName is null)
{
logger.Error("No Such User");
return false;
}
await SetBoundaryScanFreq(freq);
var cts = new CancellationTokenSource();
CancellationTokenSourceTable.AddOrUpdate(userName, cts, (key, value) => cts);
_ = Task.Run(
() => BoundaryScanLogicPorts(Context.ConnectionId, userName, cts.Token),
cts.Token)
.ContinueWith((task) =>
{
if (task.IsFaulted)
{
// 遍历所有异常
foreach (var ex in task.Exception.InnerExceptions)
{
if (ex is OperationCanceledException)
{
logger.Info($"Boundary scan operation cancelled for user {userName}");
}
else
{
logger.Error($"Boundary scan operation failed for user {userName}: {ex}");
}
}
}
else if (task.IsCanceled)
{
logger.Info($"Boundary scan operation cancelled for user {userName}");
}
else
{
logger.Info($"Boundary scan completed successfully for user {userName}");
}
});
logger.Info($"Boundary scan started for user {userName}");
return true;
}
catch (Exception error)
{
logger.Error(error);
return false;
}
}
public async Task<bool> StopBoundaryScan()
{
var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
if (userName is null)
{
logger.Error("No Such User");
return false;
}
if (!CancellationTokenSourceTable.TryGetValue(userName, out var cts))
{
return false;
}
cts.Cancel();
cts.Token.WaitHandle.WaitOne();
logger.Info($"Boundary scan stopped for user {userName}");
return true;
}
private async Task BoundaryScanLogicPorts(string connectionID, string userName, CancellationToken cancellationToken)
{
var jtagCtrl = GetJtagClient(userName).OrThrow(() => new InvalidOperationException("JTAG client not found"));
var cntFail = 0;
while (true && cntFail < 5)
{
cancellationToken.ThrowIfCancellationRequested();
var ret = await jtagCtrl.BoundaryScanLogicalPorts();
if (!ret.IsSuccessful)
{
logger.Error($"User {userName} boundary scan failed for device {jtagCtrl.address}: {ret.Error}");
cntFail++;
continue;
}
await _hubContext.Clients.Client(connectionID).OnReceiveBoundaryScanData(ret.Value);
// logger.Info($"User {userName} successfully completed boundary scan for device {jtagCtrl.address}");
await Task.Delay(FreqTable.TryGetValue(userName, out var freq) ? 1000 / freq : 1000 / 100, cancellationToken);
}
if (cntFail >= 5)
{
logger.Error($"User {userName} boundary scan failed for device {jtagCtrl.address} after 5 attempts");
throw new InvalidOperationException("Boundary scan failed");
}
}
}

View File

@@ -1,6 +1,7 @@
using System.Net; using System.Net;
using DotNext; using DotNext;
using Peripherals.PowerClient; using Peripherals.PowerClient;
using WebProtocol;
namespace Peripherals.CameraClient; namespace Peripherals.CameraClient;
@@ -19,7 +20,7 @@ class Camera
{ {
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 2000; readonly int timeout = 500;
readonly int taskID; readonly int taskID;
readonly int port; readonly int port;
readonly string address; readonly string address;
@@ -43,7 +44,7 @@ class Camera
/// <param name="address">摄像头设备IP地址</param> /// <param name="address">摄像头设备IP地址</param>
/// <param name="port">摄像头设备端口</param> /// <param name="port">摄像头设备端口</param>
/// <param name="timeout">超时时间(毫秒)</param> /// <param name="timeout">超时时间(毫秒)</param>
public Camera(string address, int port, int timeout = 2000) public Camera(string address, int port, int timeout = 500)
{ {
if (timeout < 0) if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout)); throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
@@ -225,6 +226,7 @@ class Camera
this.taskID, // taskID this.taskID, // taskID
FrameAddr, FrameAddr,
(int)_currentFrameLength, // 使用当前分辨率的动态大小 (int)_currentFrameLength, // 使用当前分辨率的动态大小
BurstType.ExtendBurst,
this.timeout); this.timeout);
if (!result.IsSuccessful) if (!result.IsSuccessful)
@@ -462,6 +464,20 @@ class Camera
); );
} }
/// <summary>
/// 配置为960x540分辨率
/// </summary>
/// <returns>配置结果</returns>
public async ValueTask<Result<bool>> ConfigureResolution960x540()
{
return await ConfigureResolution(
hStart: 0, vStart: 0,
dvpHo: 960, dvpVo: 540,
hts: 1700, vts: 1500,
hOffset: 16, vOffset: 4
);
}
/// <summary> /// <summary>
/// 配置为320x240分辨率 /// 配置为320x240分辨率
/// </summary> /// </summary>
@@ -543,6 +559,9 @@ class Camera
case "640x480": case "640x480":
result = await ConfigureResolution640x480(); result = await ConfigureResolution640x480();
break; break;
case "960x540":
result = await ConfigureResolution960x540();
break;
case "1280x720": case "1280x720":
result = await ConfigureResolution1280x720(); result = await ConfigureResolution1280x720();
break; break;

View File

@@ -0,0 +1,118 @@
using System.Net;
using DotNext;
using Peripherals.PowerClient;
using WebProtocol;
namespace Peripherals.HdmiInClient;
static class HdmiInAddr
{
public const UInt32 BASE = 0xA000_0000;
public const UInt32 HdmiIn_CTRL = BASE + 0x0; //[0]: rstn, 0 is reset.
public const UInt32 HdmiIn_READFIFO = BASE + 0x1;
}
class HdmiIn
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 500;
readonly int taskID;
readonly int port;
readonly string address;
private IPEndPoint ep;
// 动态分辨率参数
private UInt16 _currentWidth = 960;
private UInt16 _currentHeight = 540;
private UInt32 _currentFrameLength = 960 * 540 * 2 / 4; // RGB565格式2字节/像素按4字节对齐
/// <summary>
/// 初始化HDMI输入客户端
/// </summary>
/// <param name="address">HDMI输入设备IP地址</param>
/// <param name="port">HDMI输入设备端口</param>
/// <param name="timeout">超时时间(毫秒)</param>
public HdmiIn(string address, int port, int timeout = 500)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.timeout = timeout;
}
public async ValueTask<Result<bool>> EnableTrans(bool isEnable)
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, HdmiInAddr.HdmiIn_CTRL, (isEnable ? 0x00000001u : 0x00000000u));
if (!ret.IsSuccessful)
{
logger.Error($"Failed to write HdmiIn_CTRL to HdmiIn at {this.address}:{this.port}, error: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"HdmiIn_CTRL write returned false for HdmiIn at {this.address}:{this.port}");
return false;
}
return true;
}
/// <summary>
/// 读取一帧图像数据
/// </summary>
/// <returns>包含图像数据的字节数组</returns>
public async ValueTask<Result<byte[]>> ReadFrame()
{
// 只在第一次或出错时清除UDP缓冲区避免每帧都清除造成延迟
// MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
logger.Trace($"Reading frame from HdmiIn {this.address}");
// 使用UDPClientPool读取图像帧数据
var result = await UDPClientPool.ReadAddr4BytesAsync(
this.ep,
this.taskID, // taskID
HdmiInAddr.HdmiIn_READFIFO,
(int)_currentFrameLength, // 使用当前分辨率的动态大小
BurstType.FixedBurst,
this.timeout);
if (!result.IsSuccessful)
{
logger.Error($"Failed to read frame from HdmiIn {this.address}:{this.port}, error: {result.Error}");
// 读取失败时清除缓冲区,为下次读取做准备
try
{
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
}
catch (Exception ex)
{
logger.Warn($"Failed to clear UDP data after read error: {ex.Message}");
}
return new(result.Error);
}
logger.Trace($"Successfully read frame from HdmiIn {this.address}:{this.port}, data length: {result.Value.Length} bytes");
return result.Value;
}
/// <summary>
/// 获取当前分辨率
/// </summary>
/// <returns>当前分辨率(宽度, 高度)</returns>
public (int Width, int Height) GetCurrentResolution()
{
return (_currentWidth, _currentHeight);
}
/// <summary>
/// 获取当前帧长度
/// </summary>
/// <returns>当前帧长度</returns>
public UInt32 GetCurrentFrameLength()
{
return _currentFrameLength;
}
}

View File

@@ -386,7 +386,10 @@ public class Jtag
readonly int timeout; readonly int timeout;
readonly int port; readonly int port;
readonly string address; /// <summary>
/// Jtag控制器IP地址
/// </summary>
public readonly string address;
private IPEndPoint ep; private IPEndPoint ep;
/// <summary> /// <summary>
@@ -436,7 +439,7 @@ public class Jtag
if (retPackLen != 4) if (retPackLen != 4)
return new(new Exception($"RecvDataPackage BodyData Length not Equal to 4: Total {retPackLen} bytes")); return new(new Exception($"RecvDataPackage BodyData Length not Equal to 4: Total {retPackLen} bytes"));
return Convert.ToUInt32(Common.Number.BytesToUInt64(retPackOpts.Data).Value); return Convert.ToUInt32(Common.Number.BytesToUInt32(retPackOpts.Data).Value);
} }
async ValueTask<Result<bool>> WriteFIFO async ValueTask<Result<bool>> WriteFIFO
@@ -609,13 +612,10 @@ public class Jtag
if (ret.Value) if (ret.Value)
{ {
var array = new UInt32[UInt32Num]; var array = new UInt32[UInt32Num];
for (int i = 0; i < UInt32Num; i++) var retData = await UDPClientPool.ReadAddr4Bytes(ep, 0, JtagAddr.READ_DATA, (int)UInt32Num);
{ if (!retData.IsSuccessful)
var retData = await ReadFIFO(JtagAddr.READ_DATA); return new(new Exception("Read FIFO failed when Load DR"));
if (!retData.IsSuccessful) Buffer.BlockCopy(retData.Value, 0, array, 0, (int)UInt32Num * 4);
return new(new Exception("Read FIFO failed when Load DR"));
array[i] = retData.Value;
}
return array; return array;
} }
else else
@@ -785,7 +785,7 @@ public class Jtag
{ {
var paser = new BsdlParser.Parser(); var paser = new BsdlParser.Parser();
var portNum = paser.GetBoundaryRegsNum().Value; var portNum = paser.GetBoundaryRegsNum().Value;
logger.Debug($"Get boundar scan registers number: {portNum}"); logger.Debug($"Get boundary scan registers number: {portNum}");
// Clear Data // Clear Data
MsgBus.UDPServer.ClearUDPData(this.address, 0); MsgBus.UDPServer.ClearUDPData(this.address, 0);

View File

@@ -2,6 +2,7 @@ using System.Collections;
using System.Net; using System.Net;
using Common; using Common;
using DotNext; using DotNext;
using WebProtocol;
namespace Peripherals.LogicAnalyzerClient; namespace Peripherals.LogicAnalyzerClient;
@@ -475,6 +476,7 @@ public class Analyzer
this.taskID, this.taskID,
AnalyzerAddr.STORE_OFFSET_ADDR, AnalyzerAddr.STORE_OFFSET_ADDR,
capture_length, capture_length,
BurstType.ExtendBurst, // 使用扩展突发读取
this.timeout this.timeout
); );
if (!ret.IsSuccessful) if (!ret.IsSuccessful)

View File

@@ -1,6 +1,7 @@
using System.Net; using System.Net;
using Common; using Common;
using DotNext; using DotNext;
using WebProtocol;
namespace Peripherals.OscilloscopeClient; namespace Peripherals.OscilloscopeClient;
@@ -319,6 +320,7 @@ class Oscilloscope
this.taskID, this.taskID,
OscilloscopeAddr.RD_DATA_ADDR, OscilloscopeAddr.RD_DATA_ADDR,
(int)OscilloscopeAddr.RD_DATA_LENGTH / 32, (int)OscilloscopeAddr.RD_DATA_LENGTH / 32,
BurstType.ExtendBurst, // 使用扩展突发读取
this.timeout this.timeout
); );
if (!ret.IsSuccessful) if (!ret.IsSuccessful)

View File

@@ -1109,6 +1109,7 @@ public class HttpVideoStreamService : BackgroundService
return new List<(int, int, string)> return new List<(int, int, string)>
{ {
(640, 480, "640x480 (VGA)"), (640, 480, "640x480 (VGA)"),
(960, 540, "960x540 (qHD)"),
(1280, 720, "1280x720 (HD)"), (1280, 720, "1280x720 (HD)"),
(1280, 960, "1280x960 (SXGA)"), (1280, 960, "1280x960 (SXGA)"),
(1920, 1080, "1920x1080 (Full HD)") (1920, 1080, "1920x1080 (Full HD)")

View File

@@ -336,7 +336,7 @@ public class UDPClientPool
$"Device {address} receive data is {retData.Length} bytes instead of 4 bytes")); $"Device {address} receive data is {retData.Length} bytes instead of 4 bytes"));
// Check result // Check result
var retCode = Convert.ToUInt32(Common.Number.BytesToUInt64(retData).Value); var retCode = Convert.ToUInt32(Common.Number.BytesToUInt32(retData).Value);
if (Common.Number.BitsCheck(retCode, result, resultMask)) return true; if (Common.Number.BitsCheck(retCode, result, resultMask)) return true;
} }
catch (Exception error) catch (Exception error)
@@ -433,11 +433,12 @@ public class UDPClientPool
/// <param name="endPoint">IP端点IP地址与端口</param> /// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="taskID">任务ID</param> /// <param name="taskID">任务ID</param>
/// <param name="devAddr">设备地址</param> /// <param name="devAddr">设备地址</param>
/// <param name="burstType">突发类型</param>
/// <param name="dataLength">要读取的数据长度4字节</param> /// <param name="dataLength">要读取的数据长度4字节</param>
/// <param name="timeout">超时时间(毫秒)</param> /// <param name="timeout">超时时间(毫秒)</param>
/// <returns>读取结果,包含接收到的字节数组</returns> /// <returns>读取结果,包含接收到的字节数组</returns>
public static async ValueTask<Result<byte[]>> ReadAddr4BytesAsync( public static async ValueTask<Result<byte[]>> ReadAddr4BytesAsync(
IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, int timeout = 1000) IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, BurstType burstType, int timeout = 1000)
{ {
var pkgList = new List<SendAddrPackage>(); var pkgList = new List<SendAddrPackage>();
var resultData = new List<byte>(); var resultData = new List<byte>();
@@ -460,7 +461,7 @@ public class UDPClientPool
var opts = new SendAddrPackOptions var opts = new SendAddrPackOptions
{ {
BurstType = BurstType.FixedBurst, BurstType = burstType,
CommandID = Convert.ToByte(taskID), CommandID = Convert.ToByte(taskID),
IsWrite = false, IsWrite = false,
BurstLength = (byte)(currentSegmentSize - 1), BurstLength = (byte)(currentSegmentSize - 1),

View File

@@ -23,6 +23,50 @@ export class Client {
} }
getSignalrDevSpec_json( cancelToken?: CancelToken): Promise<void> {
let url_ = this.baseUrl + "/signalr-dev/spec.json";
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
method: "GET",
url: url_,
headers: {
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processGetSignalrDevSpec_json(_response);
});
}
protected processGetSignalrDevSpec_json(response: AxiosResponse): Promise<void> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200) {
const _responseText = response.data;
return Promise.resolve<void>(null as any);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<void>(null as any);
}
getGetAPIClientCode( cancelToken?: CancelToken): Promise<void> { getGetAPIClientCode( cancelToken?: CancelToken): Promise<void> {
let url_ = this.baseUrl + "/GetAPIClientCode"; let url_ = this.baseUrl + "/GetAPIClientCode";
url_ = url_.replace(/[?&]$/, ""); url_ = url_.replace(/[?&]$/, "");
@@ -2898,277 +2942,6 @@ export class ExamClient {
} }
return Promise.resolve<ExamInfo>(null as any); return Promise.resolve<ExamInfo>(null as any);
} }
/**
* 添加实验资源
* @param examId 实验ID
* @param resourceType 资源类型
* @param file (optional) 资源文件
* @return 添加结果
*/
addExamResource(examId: string, resourceType: string, file: FileParameter | undefined, cancelToken?: CancelToken): Promise<ResourceInfo> {
let url_ = this.baseUrl + "/api/Exam/{examId}/resources/{resourceType}";
if (examId === undefined || examId === null)
throw new Error("The parameter 'examId' must be defined.");
url_ = url_.replace("{examId}", encodeURIComponent("" + examId));
if (resourceType === undefined || resourceType === null)
throw new Error("The parameter 'resourceType' must be defined.");
url_ = url_.replace("{resourceType}", encodeURIComponent("" + resourceType));
url_ = url_.replace(/[?&]$/, "");
const content_ = new FormData();
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_: AxiosRequestConfig = {
data: content_,
method: "POST",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processAddExamResource(_response);
});
}
protected processAddExamResource(response: AxiosResponse): Promise<ResourceInfo> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 201) {
const _responseText = response.data;
let result201: any = null;
let resultData201 = _responseText;
result201 = ResourceInfo.fromJS(resultData201);
return Promise.resolve<ResourceInfo>(result201);
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = ProblemDetails.fromJS(resultData400);
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status === 403) {
const _responseText = response.data;
let result403: any = null;
let resultData403 = _responseText;
result403 = ProblemDetails.fromJS(resultData403);
return throwException("A server side error occurred.", status, _responseText, _headers, result403);
} else if (status === 404) {
const _responseText = response.data;
let result404: any = null;
let resultData404 = _responseText;
result404 = ProblemDetails.fromJS(resultData404);
return throwException("A server side error occurred.", status, _responseText, _headers, result404);
} else if (status === 409) {
const _responseText = response.data;
let result409: any = null;
let resultData409 = _responseText;
result409 = ProblemDetails.fromJS(resultData409);
return throwException("A server side error occurred.", status, _responseText, _headers, result409);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<ResourceInfo>(null as any);
}
/**
* 获取指定实验ID的指定资源类型的所有资源的ID和名称
* @param examId 实验ID
* @param resourceType 资源类型
* @return 资源列表
*/
getExamResourceList(examId: string, resourceType: string, cancelToken?: CancelToken): Promise<ResourceInfo[]> {
let url_ = this.baseUrl + "/api/Exam/{examId}/resources/{resourceType}";
if (examId === undefined || examId === null)
throw new Error("The parameter 'examId' must be defined.");
url_ = url_.replace("{examId}", encodeURIComponent("" + examId));
if (resourceType === undefined || resourceType === null)
throw new Error("The parameter 'resourceType' must be defined.");
url_ = url_.replace("{resourceType}", encodeURIComponent("" + resourceType));
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
method: "GET",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processGetExamResourceList(_response);
});
}
protected processGetExamResourceList(response: AxiosResponse): Promise<ResourceInfo[]> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200) {
const _responseText = response.data;
let result200: any = null;
let resultData200 = _responseText;
if (Array.isArray(resultData200)) {
result200 = [] as any;
for (let item of resultData200)
result200!.push(ResourceInfo.fromJS(item));
}
else {
result200 = <any>null;
}
return Promise.resolve<ResourceInfo[]>(result200);
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = ProblemDetails.fromJS(resultData400);
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<ResourceInfo[]>(null as any);
}
/**
* 根据资源ID下载资源
* @param resourceId 资源ID
* @return 资源文件
*/
getExamResourceById(resourceId: number, cancelToken?: CancelToken): Promise<FileResponse> {
let url_ = this.baseUrl + "/api/Exam/resources/{resourceId}";
if (resourceId === undefined || resourceId === null)
throw new Error("The parameter 'resourceId' must be defined.");
url_ = url_.replace("{resourceId}", encodeURIComponent("" + resourceId));
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
responseType: "blob",
method: "GET",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processGetExamResourceById(_response);
});
}
protected processGetExamResourceById(response: AxiosResponse): Promise<FileResponse> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200 || status === 206) {
const contentDisposition = response.headers ? response.headers["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 Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers });
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = ProblemDetails.fromJS(resultData400);
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 404) {
const _responseText = response.data;
let result404: any = null;
let resultData404 = _responseText;
result404 = ProblemDetails.fromJS(resultData404);
return throwException("A server side error occurred.", status, _responseText, _headers, result404);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<FileResponse>(null as any);
}
} }
export class JtagClient { export class JtagClient {
@@ -3383,98 +3156,14 @@ export class JtagClient {
return Promise.resolve<void>(null as any); return Promise.resolve<void>(null as any);
} }
/**
* 上传比特流文件到服务器
* @param address (optional) 目标设备地址
* @param file (optional) 比特流文件
* @return 上传结果
*/
uploadBitstream(address: string | undefined, file: FileParameter | undefined, cancelToken?: CancelToken): Promise<boolean> {
let url_ = this.baseUrl + "/api/Jtag/UploadBitstream?";
if (address === null)
throw new Error("The parameter 'address' cannot be null.");
else if (address !== undefined)
url_ += "address=" + encodeURIComponent("" + address) + "&";
url_ = url_.replace(/[?&]$/, "");
const content_ = new FormData();
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_: AxiosRequestConfig = {
data: content_,
method: "POST",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processUploadBitstream(_response);
});
}
protected processUploadBitstream(response: AxiosResponse): Promise<boolean> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200) {
const _responseText = response.data;
let result200: any = null;
let resultData200 = _responseText;
result200 = resultData200 !== undefined ? resultData200 : <any>null;
return Promise.resolve<boolean>(result200);
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = resultData400 !== undefined ? resultData400 : <any>null;
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<boolean>(null as any);
}
/** /**
* 通过 JTAG 下载比特流文件到 FPGA 设备 * 通过 JTAG 下载比特流文件到 FPGA 设备
* @param address (optional) JTAG 设备地址 * @param address (optional) JTAG 设备地址
* @param port (optional) JTAG 设备端口 * @param port (optional) JTAG 设备端口
* @param bitstreamId (optional) 比特流ID
* @return 下载结果 * @return 下载结果
*/ */
downloadBitstream(address: string | undefined, port: number | undefined, cancelToken?: CancelToken): Promise<boolean> { downloadBitstream(address: string | undefined, port: number | undefined, bitstreamId: number | undefined, cancelToken?: CancelToken): Promise<boolean> {
let url_ = this.baseUrl + "/api/Jtag/DownloadBitstream?"; let url_ = this.baseUrl + "/api/Jtag/DownloadBitstream?";
if (address === null) if (address === null)
throw new Error("The parameter 'address' cannot be null."); throw new Error("The parameter 'address' cannot be null.");
@@ -3484,6 +3173,10 @@ export class JtagClient {
throw new Error("The parameter 'port' cannot be null."); throw new Error("The parameter 'port' cannot be null.");
else if (port !== undefined) else if (port !== undefined)
url_ += "port=" + encodeURIComponent("" + port) + "&"; url_ += "port=" + encodeURIComponent("" + port) + "&";
if (bitstreamId === null)
throw new Error("The parameter 'bitstreamId' cannot be null.");
else if (bitstreamId !== undefined)
url_ += "bitstreamId=" + encodeURIComponent("" + bitstreamId) + "&";
url_ = url_.replace(/[?&]$/, ""); url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = { let options_: AxiosRequestConfig = {
@@ -6521,6 +6214,353 @@ export class RemoteUpdateClient {
} }
} }
export class ResourceClient {
protected instance: AxiosInstance;
protected baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(baseUrl?: string, instance?: AxiosInstance) {
this.instance = instance || axios.create();
this.baseUrl = baseUrl ?? "http://127.0.0.1:5000";
}
/**
* 添加资源(文件上传)
* @param resourceType (optional) 资源类型
* @param resourcePurpose (optional) 资源用途template/user
* @param examID (optional) 所属实验ID可选
* @param file (optional) 资源文件
* @return 添加结果
*/
addResource(resourceType: string | undefined, resourcePurpose: string | undefined, examID: string | null | undefined, file: FileParameter | undefined, cancelToken?: CancelToken): Promise<ResourceInfo> {
let url_ = this.baseUrl + "/api/Resource";
url_ = url_.replace(/[?&]$/, "");
const content_ = new FormData();
if (resourceType === null || resourceType === undefined)
throw new Error("The parameter 'resourceType' cannot be null.");
else
content_.append("ResourceType", resourceType.toString());
if (resourcePurpose === null || resourcePurpose === undefined)
throw new Error("The parameter 'resourcePurpose' cannot be null.");
else
content_.append("ResourcePurpose", resourcePurpose.toString());
if (examID !== null && examID !== undefined)
content_.append("ExamID", examID.toString());
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_: AxiosRequestConfig = {
data: content_,
method: "POST",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processAddResource(_response);
});
}
protected processAddResource(response: AxiosResponse): Promise<ResourceInfo> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 201) {
const _responseText = response.data;
let result201: any = null;
let resultData201 = _responseText;
result201 = ResourceInfo.fromJS(resultData201);
return Promise.resolve<ResourceInfo>(result201);
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = ProblemDetails.fromJS(resultData400);
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status === 404) {
const _responseText = response.data;
let result404: any = null;
let resultData404 = _responseText;
result404 = ProblemDetails.fromJS(resultData404);
return throwException("A server side error occurred.", status, _responseText, _headers, result404);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<ResourceInfo>(null as any);
}
/**
* 获取资源列表
* @param examId (optional) 实验ID可选
* @param resourceType (optional) 资源类型(可选)
* @param resourcePurpose (optional) 资源用途(可选)
* @return 资源列表
*/
getResourceList(examId: string | null | undefined, resourceType: string | null | undefined, resourcePurpose: string | null | undefined, cancelToken?: CancelToken): Promise<ResourceInfo[]> {
let url_ = this.baseUrl + "/api/Resource?";
if (examId !== undefined && examId !== null)
url_ += "examId=" + encodeURIComponent("" + examId) + "&";
if (resourceType !== undefined && resourceType !== null)
url_ += "resourceType=" + encodeURIComponent("" + resourceType) + "&";
if (resourcePurpose !== undefined && resourcePurpose !== null)
url_ += "resourcePurpose=" + encodeURIComponent("" + resourcePurpose) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
method: "GET",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processGetResourceList(_response);
});
}
protected processGetResourceList(response: AxiosResponse): Promise<ResourceInfo[]> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200) {
const _responseText = response.data;
let result200: any = null;
let resultData200 = _responseText;
if (Array.isArray(resultData200)) {
result200 = [] as any;
for (let item of resultData200)
result200!.push(ResourceInfo.fromJS(item));
}
else {
result200 = <any>null;
}
return Promise.resolve<ResourceInfo[]>(result200);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<ResourceInfo[]>(null as any);
}
/**
* 根据资源ID下载资源
* @param resourceId 资源ID
* @return 资源文件
*/
getResourceById(resourceId: number, cancelToken?: CancelToken): Promise<FileResponse> {
let url_ = this.baseUrl + "/api/Resource/{resourceId}";
if (resourceId === undefined || resourceId === null)
throw new Error("The parameter 'resourceId' must be defined.");
url_ = url_.replace("{resourceId}", encodeURIComponent("" + resourceId));
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
responseType: "blob",
method: "GET",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processGetResourceById(_response);
});
}
protected processGetResourceById(response: AxiosResponse): Promise<FileResponse> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200 || status === 206) {
const contentDisposition = response.headers ? response.headers["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 Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers });
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = ProblemDetails.fromJS(resultData400);
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 404) {
const _responseText = response.data;
let result404: any = null;
let resultData404 = _responseText;
result404 = ProblemDetails.fromJS(resultData404);
return throwException("A server side error occurred.", status, _responseText, _headers, result404);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<FileResponse>(null as any);
}
/**
* 删除资源
* @param resourceId 资源ID
* @return 删除结果
*/
deleteResource(resourceId: number, cancelToken?: CancelToken): Promise<void> {
let url_ = this.baseUrl + "/api/Resource/{resourceId}";
if (resourceId === undefined || resourceId === null)
throw new Error("The parameter 'resourceId' must be defined.");
url_ = url_.replace("{resourceId}", encodeURIComponent("" + resourceId));
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
method: "DELETE",
url: url_,
headers: {
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processDeleteResource(_response);
});
}
protected processDeleteResource(response: AxiosResponse): Promise<void> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 204) {
const _responseText = response.data;
return Promise.resolve<void>(null as any);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status === 403) {
const _responseText = response.data;
let result403: any = null;
let resultData403 = _responseText;
result403 = ProblemDetails.fromJS(resultData403);
return throwException("A server side error occurred.", status, _responseText, _headers, result403);
} else if (status === 404) {
const _responseText = response.data;
let result404: any = null;
let resultData404 = _responseText;
result404 = ProblemDetails.fromJS(resultData404);
return throwException("A server side error occurred.", status, _responseText, _headers, result404);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<void>(null as any);
}
}
export class TutorialClient { export class TutorialClient {
protected instance: AxiosInstance; protected instance: AxiosInstance;
protected baseUrl: string; protected baseUrl: string;
@@ -7979,52 +8019,6 @@ export interface ICreateExamRequest {
isVisibleToUsers: boolean; isVisibleToUsers: boolean;
} }
/** 资源信息类 */
export class ResourceInfo implements IResourceInfo {
/** 资源ID */
id!: number;
/** 资源名称 */
name!: string;
constructor(data?: IResourceInfo) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.id = _data["id"];
this.name = _data["name"];
}
}
static fromJS(data: any): ResourceInfo {
data = typeof data === 'object' ? data : {};
let result = new ResourceInfo();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["id"] = this.id;
data["name"] = this.name;
return data;
}
}
/** 资源信息类 */
export interface IResourceInfo {
/** 资源ID */
id: number;
/** 资源名称 */
name: string;
}
/** 逻辑分析仪运行状态枚举 */ /** 逻辑分析仪运行状态枚举 */
export enum CaptureStatus { export enum CaptureStatus {
None = 0, None = 0,
@@ -8474,6 +8468,82 @@ export interface IOscilloscopeDataResponse {
waveformData: string; waveformData: string;
} }
/** 资源信息类 */
export class ResourceInfo implements IResourceInfo {
/** 资源ID */
id!: number;
/** 资源名称 */
name!: string;
/** 资源类型 */
type!: string;
/** 资源用途template/user */
purpose!: string;
/** 上传时间 */
uploadTime!: Date;
/** 所属实验ID可选 */
examID?: string | undefined;
/** MIME类型 */
mimeType?: string | undefined;
constructor(data?: IResourceInfo) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.id = _data["id"];
this.name = _data["name"];
this.type = _data["type"];
this.purpose = _data["purpose"];
this.uploadTime = _data["uploadTime"] ? new Date(_data["uploadTime"].toString()) : <any>undefined;
this.examID = _data["examID"];
this.mimeType = _data["mimeType"];
}
}
static fromJS(data: any): ResourceInfo {
data = typeof data === 'object' ? data : {};
let result = new ResourceInfo();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["id"] = this.id;
data["name"] = this.name;
data["type"] = this.type;
data["purpose"] = this.purpose;
data["uploadTime"] = this.uploadTime ? this.uploadTime.toISOString() : <any>undefined;
data["examID"] = this.examID;
data["mimeType"] = this.mimeType;
return data;
}
}
/** 资源信息类 */
export interface IResourceInfo {
/** 资源ID */
id: number;
/** 资源名称 */
name: string;
/** 资源类型 */
type: string;
/** 资源用途template/user */
purpose: string;
/** 上传时间 */
uploadTime: Date;
/** 所属实验ID可选 */
examID?: string | undefined;
/** MIME类型 */
mimeType?: string | undefined;
}
/** Package options which to send address to read or write */ /** Package options which to send address to read or write */
export class SendAddrPackOptions implements ISendAddrPackOptions { export class SendAddrPackOptions implements ISendAddrPackOptions {
/** 突发类型 */ /** 突发类型 */

View File

@@ -12,6 +12,14 @@ const isDarkMode = ref(
window.matchMedia("(prefers-color-scheme: dark)").matches, window.matchMedia("(prefers-color-scheme: dark)").matches,
); );
// Navbar显示状态管理
const showNavbar = ref(true);
// 切换Navbar显示状态
const toggleNavbar = () => {
showNavbar.value = !showNavbar.value;
};
// 初始化主题设置 // 初始化主题设置
onMounted(() => { onMounted(() => {
// 应用初始主题 // 应用初始主题
@@ -47,6 +55,12 @@ provide("theme", {
toggleTheme, toggleTheme,
}); });
// 提供Navbar控制给子组件
provide("navbar", {
showNavbar,
toggleNavbar,
});
const currentRoutePath = computed(() => { const currentRoutePath = computed(() => {
return router.currentRoute.value.path; return router.currentRoute.value.path;
}); });
@@ -56,8 +70,8 @@ useAlertProvider();
<template> <template>
<div> <div>
<header class="relative"> <header class="relative" :class="{ 'navbar-hidden': !showNavbar }">
<Navbar /> <Navbar v-show="showNavbar" />
<Dialog /> <Dialog />
<Alert /> <Alert />
</header> </header>
@@ -79,4 +93,25 @@ useAlertProvider();
<style scoped> <style scoped>
/* 特定于App.vue的样式 */ /* 特定于App.vue的样式 */
header {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: top;
}
.navbar-hidden {
transform: scaleY(0);
height: 0;
overflow: hidden;
}
/* Navbar显示/隐藏动画 */
header .navbar {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: top;
}
/* 当header被隐藏时确保navbar也相应变化 */
.navbar-hidden .navbar {
opacity: 0;
}
</style> </style>

View File

@@ -0,0 +1,118 @@
/* THIS (.ts) FILE IS GENERATED BY TypedSignalR.Client.TypeScript */
/* eslint-disable */
/* tslint:disable */
// @ts-nocheck
import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr';
import type { IJtagHub, IJtagReceiver } from './server.Hubs.JtagHub';
// components
export type Disposable = {
dispose(): void;
}
export type HubProxyFactory<T> = {
createHubProxy(connection: HubConnection): T;
}
export type ReceiverRegister<T> = {
register(connection: HubConnection, receiver: T): Disposable;
}
type ReceiverMethod = {
methodName: string,
method: (...args: any[]) => void
}
class ReceiverMethodSubscription implements Disposable {
public constructor(
private connection: HubConnection,
private receiverMethod: ReceiverMethod[]) {
}
public readonly dispose = () => {
for (const it of this.receiverMethod) {
this.connection.off(it.methodName, it.method);
}
}
}
// API
export type HubProxyFactoryProvider = {
(hubType: "IJtagHub"): HubProxyFactory<IJtagHub>;
}
export const getHubProxyFactory = ((hubType: string) => {
if(hubType === "IJtagHub") {
return IJtagHub_HubProxyFactory.Instance;
}
}) as HubProxyFactoryProvider;
export type ReceiverRegisterProvider = {
(receiverType: "IJtagReceiver"): ReceiverRegister<IJtagReceiver>;
}
export const getReceiverRegister = ((receiverType: string) => {
if(receiverType === "IJtagReceiver") {
return IJtagReceiver_Binder.Instance;
}
}) as ReceiverRegisterProvider;
// HubProxy
class IJtagHub_HubProxyFactory implements HubProxyFactory<IJtagHub> {
public static Instance = new IJtagHub_HubProxyFactory();
private constructor() {
}
public readonly createHubProxy = (connection: HubConnection): IJtagHub => {
return new IJtagHub_HubProxy(connection);
}
}
class IJtagHub_HubProxy implements IJtagHub {
public constructor(private connection: HubConnection) {
}
public readonly setBoundaryScanFreq = async (freq: number): Promise<boolean> => {
return await this.connection.invoke("SetBoundaryScanFreq", freq);
}
public readonly startBoundaryScan = async (freq: number): Promise<boolean> => {
return await this.connection.invoke("StartBoundaryScan", freq);
}
public readonly stopBoundaryScan = async (): Promise<boolean> => {
return await this.connection.invoke("StopBoundaryScan");
}
}
// Receiver
class IJtagReceiver_Binder implements ReceiverRegister<IJtagReceiver> {
public static Instance = new IJtagReceiver_Binder();
private constructor() {
}
public readonly register = (connection: HubConnection, receiver: IJtagReceiver): Disposable => {
const __onReceiveBoundaryScanData = (...args: [Partial<Record<string, boolean>>]) => receiver.onReceiveBoundaryScanData(...args);
connection.on("OnReceiveBoundaryScanData", __onReceiveBoundaryScanData);
const methodList: ReceiverMethod[] = [
{ methodName: "OnReceiveBoundaryScanData", method: __onReceiveBoundaryScanData }
]
return new ReceiverMethodSubscription(connection, methodList);
}
}

View File

@@ -0,0 +1,31 @@
/* THIS (.ts) FILE IS GENERATED BY TypedSignalR.Client.TypeScript */
/* eslint-disable */
/* tslint:disable */
// @ts-nocheck
import type { IStreamResult, Subject } from '@microsoft/signalr';
export type IJtagHub = {
/**
* @param freq Transpiled from int
* @returns Transpiled from System.Threading.Tasks.Task<bool>
*/
setBoundaryScanFreq(freq: number): Promise<boolean>;
/**
* @param freq Transpiled from int
* @returns Transpiled from System.Threading.Tasks.Task<bool>
*/
startBoundaryScan(freq: number): Promise<boolean>;
/**
* @returns Transpiled from System.Threading.Tasks.Task<bool>
*/
stopBoundaryScan(): Promise<boolean>;
}
export type IJtagReceiver = {
/**
* @param msg Transpiled from System.Collections.Generic.Dictionary<string, bool>
* @returns Transpiled from System.Threading.Tasks.Task
*/
onReceiveBoundaryScanData(msg: Partial<Record<string, boolean>>): Promise<void>;
}

View File

@@ -187,7 +187,6 @@ import { useAlertStore } from "@/components/Alert";
// 导入 diagram 管理器 // 导入 diagram 管理器
import { import {
loadDiagramData, loadDiagramData,
saveDiagramData,
updatePartPosition, updatePartPosition,
updatePartAttribute, updatePartAttribute,
parseConnectionPin, parseConnectionPin,
@@ -606,14 +605,13 @@ function onComponentDrag(e: MouseEvent) {
// 停止拖拽组件 // 停止拖拽组件
function stopComponentDrag() { function stopComponentDrag() {
// 如果有组件被拖拽,保存当前状态 // 如果有组件被拖拽,仅清除拖拽状态(不保存)
if (draggingComponentId.value) { if (draggingComponentId.value) {
draggingComponentId.value = null; draggingComponentId.value = null;
} }
isComponentDragEventActive.value = false; isComponentDragEventActive.value = false;
// 移除自动保存功能 - 不再自动保存到localStorage
saveDiagramData(diagramData.value);
} }
// 更新组件属性 // 更新组件属性

View File

@@ -1,7 +1,6 @@
import { ref, shallowRef, computed, reactive } from "vue"; import { ref, shallowRef, computed, reactive } from "vue";
import { createInjectionState } from "@vueuse/core"; import { createInjectionState } from "@vueuse/core";
import { import {
saveDiagramData,
type DiagramData, type DiagramData,
type DiagramPart, type DiagramPart,
} from "./diagramManager"; } from "./diagramManager";
@@ -302,7 +301,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
// 使用 updateDiagramDataDirectly 避免触发加载状态 // 使用 updateDiagramDataDirectly 避免触发加载状态
canvasInstance.updateDiagramDataDirectly(currentData); canvasInstance.updateDiagramDataDirectly(currentData);
saveDiagramData(currentData); // 移除自动保存功能
console.log("组件添加完成:", newComponent); console.log("组件添加完成:", newComponent);
@@ -431,7 +430,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
"=== 更新图表数据完成,新组件数量:", "=== 更新图表数据完成,新组件数量:",
currentData.parts.length, currentData.parts.length,
); );
saveDiagramData(currentData); // 移除自动保存功能
return { success: true, message: `已添加 ${templateData.name} 模板` }; return { success: true, message: `已添加 ${templateData.name} 模板` };
} else { } else {
@@ -504,7 +503,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
canvasInstance.updateDiagramDataDirectly(currentData); canvasInstance.updateDiagramDataDirectly(currentData);
saveDiagramData(currentData); // 移除自动保存功能
} }
/** /**

View File

@@ -88,17 +88,17 @@ export async function loadDiagramData(examId?: string): Promise<DiagramData> {
// 如果提供了examId优先从API加载实验的diagram // 如果提供了examId优先从API加载实验的diagram
if (examId) { if (examId) {
try { try {
const examClient = AuthManager.createAuthenticatedExamClient(); const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 获取diagram类型的资源列表 // 获取diagram类型的资源列表
const resources = await examClient.getExamResourceList(examId, 'canvas'); const resources = await resourceClient.getResourceList(examId, 'canvas', 'template');
if (resources && resources.length > 0) { if (resources && resources.length > 0) {
// 获取第一个diagram资源 // 获取第一个diagram资源
const diagramResource = resources[0]; const diagramResource = resources[0];
// 使用动态API获取资源文件内容 // 使用动态API获取资源文件内容
const response = await examClient.getExamResourceById(diagramResource.id); const response = await resourceClient.getResourceById(diagramResource.id);
if (response && response.data) { if (response && response.data) {
const text = await response.data.text(); const text = await response.data.text();
@@ -121,19 +121,9 @@ export async function loadDiagramData(examId?: string): Promise<DiagramData> {
} }
} }
// 如果没有examId或API加载失败尝试从本地存储加载 // 如果没有examId或API加载失败尝试从静态文件加载(不再使用本地存储
const savedData = localStorage.getItem('diagramData');
if (savedData) {
const data = JSON.parse(savedData);
const validation = validateDiagramData(data);
if (validation.isValid) {
return data;
} else {
console.warn('本地存储的diagram数据格式无效:', validation.errors);
}
}
// 如果本地存储也没有,从静态文件加载(作为最后的备选) // 从静态文件加载(作为备选方案
const response = await fetch('/src/components/diagram.json'); const response = await fetch('/src/components/diagram.json');
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to load diagram.json: ${response.statusText}`); throw new Error(`Failed to load diagram.json: ${response.statusText}`);
@@ -166,13 +156,10 @@ export function createEmptyDiagram(): DiagramData {
}; };
} }
// 保存图表数据本地存储 // 保存图表数据(已禁用本地存储
export function saveDiagramData(data: DiagramData): void { export function saveDiagramData(data: DiagramData): void {
try { // 本地存储功能已禁用 - 不再保存到localStorage
localStorage.setItem('diagramData', JSON.stringify(data)); console.debug('saveDiagramData called but localStorage saving is disabled');
} catch (error) {
console.error('Error saving diagram data:', error);
}
} }
// 更新组件位置 // 更新组件位置

View File

@@ -6,8 +6,6 @@ import hljs from 'highlight.js';
import 'highlight.js/styles/github.css'; // 亮色主题 import 'highlight.js/styles/github.css'; // 亮色主题
// 导入主题存储 // 导入主题存储
import { useThemeStore } from '@/stores/theme'; import { useThemeStore } from '@/stores/theme';
// 导入ExamClient用于获取图片资源
import { ExamClient } from '@/APIClient';
import { AuthManager } from '@/utils/AuthManager'; import { AuthManager } from '@/utils/AuthManager';
const props = defineProps({ const props = defineProps({
@@ -36,8 +34,8 @@ const imageResourceCache = ref<Map<string, string>>(new Map());
// 获取图片资源ID的函数 // 获取图片资源ID的函数
async function getImageResourceId(examId: string, imagePath: string): Promise<string | null> { async function getImageResourceId(examId: string, imagePath: string): Promise<string | null> {
try { try {
const client = AuthManager.createAuthenticatedExamClient(); const client = AuthManager.createAuthenticatedResourceClient();
const resources = await client.getExamResourceList(examId, 'images'); const resources = await client.getResourceList(examId, 'images', 'template');
// 查找匹配的图片资源 // 查找匹配的图片资源
const imageResource = resources.find(r => r.name === imagePath || r.name.endsWith(imagePath)); const imageResource = resources.find(r => r.name === imagePath || r.name.endsWith(imagePath));
@@ -52,8 +50,8 @@ async function getImageResourceId(examId: string, imagePath: string): Promise<st
// 通过资源ID获取图片数据URL // 通过资源ID获取图片数据URL
async function getImageDataUrl(resourceId: string): Promise<string | null> { async function getImageDataUrl(resourceId: string): Promise<string | null> {
try { try {
const client = AuthManager.createAuthenticatedExamClient(); const client = AuthManager.createAuthenticatedResourceClient();
const response = await client.getExamResourceById(parseInt(resourceId)); const response = await client.getResourceById(parseInt(resourceId));
if (response && response.data) { if (response && response.data) {
return URL.createObjectURL(response.data); return URL.createObjectURL(response.data);

View File

@@ -1,11 +0,0 @@
<template>
<button class="btn transition-transform duration-150 ease-in-out hover:scale-120">
Button A
</button>
</template>
<script lang="ts" setup></script>
<style scoped lang="postcss">
@import "@/assets/main.css";
</style>

View File

@@ -1,162 +0,0 @@
<template>
<div
class="card card-dash shadow-xl h-screen"
:class="[sidebar.isClose ? 'w-31' : 'w-80']"
>
<div
class="card-body flex relative transition-all duration-500 ease-in-out"
>
<!-- Avatar and Name -->
<div class="relative" :class="sidebar.isClose ? 'h-50' : 'h-20'">
<!-- Img -->
<div
class="avatar h-10 fixed top-10"
:class="sidebar.isClose ? 'left-10' : 'left-7'"
>
<div class="rounded-full">
<img src="../assets/user.svg" alt="User" />
</div>
</div>
<!-- Text -->
<Transition>
<div v-if="!sidebar.isClose" class="mx-5 grow fixed left-20 top-11">
<label class="text-2xl">用户名</label>
</div>
</Transition>
</div>
<!-- Toggle Button -->
<button
class="btn btn-square rounded-lg p-2 m-3 fixed"
:class="sidebar.isClose ? 'left-7 top-23' : 'left-60 top-7'"
@click="sidebar.toggleSidebar"
>
<svg
t="1741694970690"
:class="sidebar.isClose ? 'rotate-0' : 'rotate-540'"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4546"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="200"
height="200"
>
<path
d="M803.758 514.017c-0.001-0.311-0.013-0.622-0.018-0.933-0.162-23.974-9.386-47.811-27.743-65.903-0.084-0.082-0.172-0.157-0.256-0.239-0.154-0.154-0.296-0.315-0.451-0.468L417.861 94.096c-37.685-37.153-99.034-37.476-136.331-0.718-37.297 36.758-36.979 97.231 0.707 134.384l290.361 286.257-290.362 286.257c-37.685 37.153-38.004 97.625-0.707 134.383 37.297 36.758 98.646 36.435 136.331-0.718l357.43-352.378c0.155-0.153 0.297-0.314 0.451-0.468 0.084-0.082 0.172-0.157 0.256-0.239 18.354-18.089 27.578-41.922 27.743-65.892 0.004-0.315 0.017-0.631 0.018-0.947z"
:fill="theme.isLightTheme() ? '#828282' : '#C0C3C8'"
p-id="4547"
></path>
</svg>
</button>
<div class="divider"></div>
<ul class="menu h-full w-full">
<li v-for="item in props.items" class="text-xl my-1">
<a @click="router.push(item.page)">
<svg
t="1741694797806"
class="icon h-[1.5em] w-[1.5em] opacity-50 mx-1"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="2622"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="200"
height="200"
>
<path
d="M192 192m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z"
p-id="2623"
></path>
<path
d="M192 512m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z"
p-id="2624"
></path>
<path
d="M192 832m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z"
p-id="2625"
></path>
<path
d="M864 160H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 480H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 800H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32z"
p-id="2626"
></path>
</svg>
<Transition>
<p class="break-keep" v-if="!sidebar.isClose">{{ item.text }}</p>
</Transition>
</a>
</li>
</ul>
<div class="divider"></div>
<ul class="menu w-full">
<li>
<a @click="theme.toggleTheme" class="text-xl">
<ThemeControlButton />
<Transition>
<p v-if="!sidebar.isClose" class="break-keep">改变主题</p>
</Transition>
<Transition>
<ThemeControlToggle v-if="!sidebar.isClose" />
</Transition>
</a>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import iconMenu from "../assets/menu.svg";
import "../router";
import { useThemeStore } from "@/stores/theme";
import { useSidebarStore } from "@/stores/sidebar";
import ThemeControlButton from "./ThemeControlButton.vue";
import ThemeControlToggle from "./ThemeControlToggle.vue";
import router from "../router";
const theme = useThemeStore();
const sidebar = useSidebarStore();
type Item = {
id: number;
icon: string;
text: string;
page: string;
};
interface Props {
items?: Array<Item>;
}
const props = withDefaults(defineProps<Props>(), {
items: () => [
{ id: 1, icon: iconMenu, text: "btn1", page: "/" },
{ id: 2, icon: iconMenu, text: "btn2", page: "/" },
{ id: 3, icon: iconMenu, text: "btn3", page: "/" },
{ id: 4, icon: iconMenu, text: "btn4", page: "/" },
],
});
</script>
<style scoped lang="postcss">
@reference "../assets/main.css";
* {
@apply transition-all duration-500 ease-in-out;
}
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>

View File

@@ -127,12 +127,13 @@ onMounted(async () => {
let thumbnail: string | undefined; let thumbnail: string | undefined;
try { try {
// 获取实验的封面资源 // 获取实验的封面资源(模板资源)
const resourceList = await client.getExamResourceList(exam.id, 'cover'); const resourceClient = AuthManager.createAuthenticatedResourceClient();
const resourceList = await resourceClient.getResourceList(exam.id, 'cover', 'template');
if (resourceList && resourceList.length > 0) { if (resourceList && resourceList.length > 0) {
// 使用第一个封面资源 // 使用第一个封面资源
const coverResource = resourceList[0]; const coverResource = resourceList[0];
const fileResponse = await client.getExamResourceById(coverResource.id); const fileResponse = await resourceClient.getResourceById(coverResource.id);
// 创建Blob URL作为缩略图 // 创建Blob URL作为缩略图
thumbnail = URL.createObjectURL(fileResponse.data); thumbnail = URL.createObjectURL(fileResponse.data);
} }

View File

@@ -8,7 +8,7 @@
<fieldset class="fieldset w-full"> <fieldset class="fieldset w-full">
<legend class="fieldset-legend text-sm">示例比特流文件</legend> <legend class="fieldset-legend text-sm">示例比特流文件</legend>
<div class="space-y-2"> <div class="space-y-2">
<div v-for="bitstream in availableBitstreams" :key="bitstream.id" class="flex items-center justify-between p-2 bg-base-200 rounded"> <div v-for="bitstream in availableBitstreams" :key="bitstream.id" class="flex items-center justify-between p-2 border-2 border-base-300 rounded-lg">
<span class="text-sm">{{ bitstream.name }}</span> <span class="text-sm">{{ bitstream.name }}</span>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
@@ -75,8 +75,8 @@ import { useDialogStore } from "@/stores/dialog";
import { isNull, isUndefined } from "lodash"; import { isNull, isUndefined } from "lodash";
interface Props { interface Props {
uploadEvent?: (file: File) => Promise<boolean>; uploadEvent?: (file: File, examId: string) => Promise<number | null>;
downloadEvent?: () => Promise<boolean>; downloadEvent?: (bitstreamId: number) => Promise<boolean>;
maxMemory?: number; maxMemory?: number;
examId?: string; // 新增examId属性 examId?: string; // 新增examId属性
} }
@@ -127,9 +127,9 @@ async function loadAvailableBitstreams() {
} }
try { try {
const examClient = AuthManager.createAuthenticatedExamClient(); const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 使用新的API获取比特流资源列表 // 使用新的ResourceClient API获取比特流模板资源列表
const resources = await examClient.getExamResourceList(props.examId, 'bitstream'); const resources = await resourceClient.getResourceList(props.examId, 'bitstream', 'template');
availableBitstreams.value = resources.map(r => ({ id: r.id, name: r.name })) || []; availableBitstreams.value = resources.map(r => ({ id: r.id, name: r.name })) || [];
} catch (error) { } catch (error) {
console.error('加载比特流列表失败:', error); console.error('加载比特流列表失败:', error);
@@ -143,10 +143,10 @@ async function downloadExampleBitstream(bitstream: {id: number, name: string}) {
isDownloading.value = true; isDownloading.value = true;
try { try {
const examClient = AuthManager.createAuthenticatedExamClient(); const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 使用动态API获取资源文件 // 使用新的ResourceClient API获取资源文件
const response = await examClient.getExamResourceById(bitstream.id); const response = await resourceClient.getResourceById(bitstream.id);
if (response && response.data) { if (response && response.data) {
// 创建下载链接 // 创建下载链接
@@ -173,37 +173,21 @@ async function downloadExampleBitstream(bitstream: {id: number, name: string}) {
// 直接烧录示例比特流 // 直接烧录示例比特流
async function programExampleBitstream(bitstream: {id: number, name: string}) { async function programExampleBitstream(bitstream: {id: number, name: string}) {
if (isProgramming.value || !props.uploadEvent) return; if (isProgramming.value) return;
isProgramming.value = true; isProgramming.value = true;
try { try {
const examClient = AuthManager.createAuthenticatedExamClient(); const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 使用动态API获取比特流文件数据 if (props.downloadEvent) {
const response = await examClient.getExamResourceById(bitstream.id); const downloadSuccess = await props.downloadEvent(bitstream.id);
if (downloadSuccess) {
if (!response || !response.data) { dialog.info("示例比特流烧录成功");
throw new Error('获取比特流文件失败');
}
const file = new File([response.data], response.fileName || bitstream.name, { type: response.data.type });
// 调用上传事件
const uploadSuccess = await props.uploadEvent(file);
if (uploadSuccess) {
// 如果有下载事件(烧录),则执行
if (props.downloadEvent) {
const downloadSuccess = await props.downloadEvent();
if (downloadSuccess) {
dialog.info("示例比特流烧录成功");
} else {
dialog.error("烧录失败");
}
} else { } else {
dialog.info("示例比特流上传成功"); dialog.error("烧录失败");
} }
} else { } else {
dialog.error("上传失败"); dialog.info("示例比特流props.downloadEvent未定义 无法烧录");
} }
} catch (error) { } catch (error) {
console.error('烧录示例比特流失败:', error); console.error('烧录示例比特流失败:', error);
@@ -234,6 +218,7 @@ function checkFile(file: File): boolean {
} }
async function handleClick(event: Event): Promise<void> { async function handleClick(event: Event): Promise<void> {
console.log("上传按钮被点击");
if (isNull(bitstream.value) || isUndefined(bitstream.value)) { if (isNull(bitstream.value) || isUndefined(bitstream.value)) {
dialog.error(`未选择文件`); dialog.error(`未选择文件`);
return; return;
@@ -246,19 +231,21 @@ async function handleClick(event: Event): Promise<void> {
} }
isUploading.value = true; isUploading.value = true;
let uploadedBitstreamId: number | null = null;
try { try {
const ret = await props.uploadEvent(bitstream.value); console.log("开始上传比特流文件:", bitstream.value.name);
const bitstreamId = await props.uploadEvent(bitstream.value, props.examId || '');
console.log("上传结果ID:", bitstreamId);
if (isUndefined(props.downloadEvent)) { if (isUndefined(props.downloadEvent)) {
if (ret) { console.log("上传成功,下载未定义");
dialog.info("上传成功");
emits("finishedUpload", bitstream.value);
} else dialog.error("上传失败");
return;
}
if (!ret) {
isUploading.value = false; isUploading.value = false;
return; return;
} }
if (bitstreamId === null || bitstreamId === undefined) {
isUploading.value = false;
return;
}
uploadedBitstreamId = bitstreamId;
} catch (e) { } catch (e) {
dialog.error("上传失败"); dialog.error("上传失败");
console.error(e); console.error(e);
@@ -267,9 +254,14 @@ async function handleClick(event: Event): Promise<void> {
// Download // Download
try { try {
const ret = await props.downloadEvent(); console.log("开始下载比特流ID:", uploadedBitstreamId);
if (ret) dialog.info("下载成功"); if (uploadedBitstreamId === null || uploadedBitstreamId === undefined) {
else dialog.error("下载失败"); dialog.error("uploadedBitstreamId is null or undefined");
} else {
const ret = await props.downloadEvent(uploadedBitstreamId);
if (ret) dialog.info("下载成功");
else dialog.error("下载失败");
}
} catch (e) { } catch (e) {
dialog.error("下载失败"); dialog.error("下载失败");
console.error(e); console.error(e);

View File

@@ -8,21 +8,35 @@
<p> <p>
IDCode: 0x{{ jtagIDCode.toString(16).padStart(8, "0").toUpperCase() }} IDCode: 0x{{ jtagIDCode.toString(16).padStart(8, "0").toUpperCase() }}
</p> </p>
<button class="btn btn-circle w-6 h-6" :disabled="isGettingIDCode" :onclick="getIDCode"> <button
<RefreshCcwIcon class="icon" :class="{ 'animate-spin': isGettingIDCode }" /> class="btn btn-circle w-6 h-6"
:disabled="isGettingIDCode"
:onclick="getIDCode"
>
<RefreshCcwIcon
class="icon"
:class="{ 'animate-spin': isGettingIDCode }"
/>
</button> </button>
</div> </div>
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<UploadCard class="bg-base-200" :upload-event="eqps.jtagUploadBitstream" <UploadCard
:download-event="eqps.jtagDownloadBitstream" :bitstream-file="eqps.jtagBitstream" :exam-id="props.examId"
:exam-id="examId" :upload-event="eqps.jtagUploadBitstream"
@update:bitstream-file="handleBitstreamChange"> :download-event="handleDownloadBitstream"
:bitstream-file="eqps.jtagBitstream"
@update:bitstream-file="handleBitstreamChange"
>
</UploadCard> </UploadCard>
<div class="divider"></div> <div class="divider"></div>
<div class="w-full"> <div class="w-full">
<legend class="fieldset-legend text-sm mb-0.3">Jtag运行频率</legend> <legend class="fieldset-legend text-sm mb-0.3">Jtag运行频率</legend>
<select class="select w-full" @change="handleSelectJtagSpeed" :value="props.jtagFreq"> <select
class="select w-full"
@change="handleSelectJtagSpeed"
:value="props.jtagFreq"
>
<option v-for="option in selectJtagSpeedOptions" :value="option.id"> <option v-for="option in selectJtagSpeedOptions" :value="option.id">
{{ option.text }} {{ option.text }}
</option> </option>
@@ -31,12 +45,23 @@
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<fieldset class="fieldset w-70"> <fieldset class="fieldset w-70">
<legend class="fieldset-legend text-sm">边界扫描刷新率 / Hz</legend> <legend class="fieldset-legend text-sm">边界扫描刷新率 / Hz</legend>
<input type="number" class="input validator" required placeholder="Type a number between 1 to 1000" min="1" <input
max="1000" v-model="jtagBoundaryScanFreq" title="Type a number between 1 to 1000" /> type="number"
class="input validator"
required
placeholder="Type a number between 1 to 1000"
min="1"
max="1000"
v-model="jtagBoundaryScanFreq"
title="Type a number between 1 to 1000"
/>
<p class="validator-hint">输入一个1 ~ 1000的数</p> <p class="validator-hint">输入一个1 ~ 1000的数</p>
</fieldset> </fieldset>
<button class="btn btn-primary grow mx-4" :class="eqps.enableJtagBoundaryScan ? '' : 'btn-soft'" <button
:onclick="toggleJtagBoundaryScan"> class="btn btn-primary grow mx-4"
:class="eqps.enableJtagBoundaryScan ? '' : 'btn-soft'"
:onclick="toggleJtagBoundaryScan"
>
{{ eqps.enableJtagBoundaryScan ? "关闭边界扫描" : "启动边界扫描" }} {{ eqps.enableJtagBoundaryScan ? "关闭边界扫描" : "启动边界扫描" }}
</button> </button>
</div> </div>
@@ -44,8 +69,12 @@
<h1 class="font-bold text-center text-2xl">外设</h1> <h1 class="font-bold text-center text-2xl">外设</h1>
<div class="flex flex-row justify-center"> <div class="flex flex-row justify-center">
<div class="flex flex-row"> <div class="flex flex-row">
<input type="checkbox" class="checkbox" :checked="eqps.enableMatrixKey" <input
@change="handleMatrixkeyCheckboxChange" /> type="checkbox"
class="checkbox"
:checked="eqps.enableMatrixKey"
@change="handleMatrixkeyCheckboxChange"
/>
<p class="mx-2">启用矩阵键盘</p> <p class="mx-2">启用矩阵键盘</p>
</div> </div>
</div> </div>
@@ -99,6 +128,11 @@ function handleBitstreamChange(file: File | undefined) {
eqps.jtagBitstream = file; eqps.jtagBitstream = file;
} }
async function handleDownloadBitstream(bitstreamId: number): Promise<boolean> {
console.log("开始下载比特流ID:", bitstreamId);
return await eqps.jtagDownloadBitstream(bitstreamId);
}
function handleSelectJtagSpeed(event: Event) { function handleSelectJtagSpeed(event: Event) {
const target = event.target as HTMLSelectElement; const target = event.target as HTMLSelectElement;
eqps.jtagSetSpeed(target.selectedIndex); eqps.jtagSetSpeed(target.selectedIndex);
@@ -119,7 +153,7 @@ async function handleMatrixkeyCheckboxChange(event: Event) {
} }
async function toggleJtagBoundaryScan() { async function toggleJtagBoundaryScan() {
eqps.enableJtagBoundaryScan = !eqps.enableJtagBoundaryScan; eqps.jtagBoundaryScanSetOnOff(!eqps.enableJtagBoundaryScan);
} }
const isGettingIDCode = ref(false); const isGettingIDCode = ref(false);

View File

@@ -1,26 +1,25 @@
import { ref, computed, reactive } from 'vue' import { ref, computed, reactive } from "vue";
import { defineStore } from 'pinia' import { defineStore } from "pinia";
import { isBoolean } from 'lodash'; import { isBoolean } from "lodash";
// 约束电平状态类型 // 约束电平状态类型
export type ConstraintLevel = 'high' | 'low' | 'undefined'; export type ConstraintLevel = "high" | "low" | "undefined";
export const useConstraintsStore = defineStore('constraints', () => {
export const useConstraintsStore = defineStore("constraints", () => {
// 约束状态存储 // 约束状态存储
const constraintStates = reactive<Record<string, ConstraintLevel>>({}); const constraintStates = reactive<Record<string, ConstraintLevel>>({});
// 约束颜色映射 // 约束颜色映射
const constraintColors = { const constraintColors = {
high: '#ff3333', // 高电平为红色 high: "#ff3333", // 高电平为红色
low: '#3333ff', // 低电平为蓝色 low: "#3333ff", // 低电平为蓝色
undefined: '#999999' // 未定义为灰色 undefined: "#999999", // 未定义为灰色
}; };
// 获取约束状态 // 获取约束状态
function getConstraintState(constraint: string): ConstraintLevel { function getConstraintState(constraint: string): ConstraintLevel {
if (!constraint) return 'undefined'; if (!constraint) return "undefined";
return constraintStates[constraint] || 'undefined'; return constraintStates[constraint] || "undefined";
} }
// 设置约束状态 // 设置约束状态
@@ -30,7 +29,9 @@ export const useConstraintsStore = defineStore('constraints', () => {
} }
// 批量设置约束状态 // 批量设置约束状态
function batchSetConstraintStates(states: Record<string, ConstraintLevel> | Record<string, boolean>) { function batchSetConstraintStates(
states: Record<string, ConstraintLevel> | Partial<Record<string, boolean>>,
) {
// 收集发生变化的约束 // 收集发生变化的约束
const changedConstraints: [string, ConstraintLevel][] = []; const changedConstraints: [string, ConstraintLevel][] = [];
@@ -38,6 +39,8 @@ export const useConstraintsStore = defineStore('constraints', () => {
Object.entries(states).forEach(([constraint, level]) => { Object.entries(states).forEach(([constraint, level]) => {
if (isBoolean(level)) { if (isBoolean(level)) {
level = level ? "high" : "low"; level = level ? "high" : "low";
} else {
level = "low";
} }
if (constraintStates[constraint] !== level) { if (constraintStates[constraint] !== level) {
@@ -48,7 +51,7 @@ export const useConstraintsStore = defineStore('constraints', () => {
// 通知所有变化 // 通知所有变化
changedConstraints.forEach(([constraint, level]) => { changedConstraints.forEach(([constraint, level]) => {
stateChangeCallbacks.forEach(callback => callback(constraint, level)); stateChangeCallbacks.forEach((callback) => callback(constraint, level));
}); });
} }
@@ -60,7 +63,7 @@ export const useConstraintsStore = defineStore('constraints', () => {
// 清除所有约束状态 // 清除所有约束状态
function clearAllConstraintStates() { function clearAllConstraintStates() {
Object.keys(constraintStates).forEach(key => { Object.keys(constraintStates).forEach((key) => {
delete constraintStates[key]; delete constraintStates[key];
}); });
} }
@@ -71,9 +74,14 @@ export const useConstraintsStore = defineStore('constraints', () => {
} }
// 注册约束状态变化回调 // 注册约束状态变化回调
const stateChangeCallbacks: ((constraint: string, level: ConstraintLevel) => void)[] = []; const stateChangeCallbacks: ((
constraint: string,
level: ConstraintLevel,
) => void)[] = [];
function onConstraintStateChange(callback: (constraint: string, level: ConstraintLevel) => void) { function onConstraintStateChange(
callback: (constraint: string, level: ConstraintLevel) => void,
) {
stateChangeCallbacks.push(callback); stateChangeCallbacks.push(callback);
return () => { return () => {
const index = stateChangeCallbacks.indexOf(callback); const index = stateChangeCallbacks.indexOf(callback);
@@ -86,7 +94,7 @@ export const useConstraintsStore = defineStore('constraints', () => {
// 触发约束变化 // 触发约束变化
function notifyConstraintChange(constraint: string, level: ConstraintLevel) { function notifyConstraintChange(constraint: string, level: ConstraintLevel) {
setConstraintState(constraint, level); setConstraintState(constraint, level);
stateChangeCallbacks.forEach(callback => callback(constraint, level)); stateChangeCallbacks.forEach((callback) => callback(constraint, level));
} }
return { return {
@@ -98,6 +106,5 @@ export const useConstraintsStore = defineStore('constraints', () => {
getAllConstraintStates, getAllConstraintStates,
onConstraintStateChange, onConstraintStateChange,
notifyConstraintChange, notifyConstraintChange,
} };
}) });

View File

@@ -1,15 +1,18 @@
import { ref, reactive, watchPostEffect } from "vue"; import { ref, reactive, watchPostEffect, onMounted, onUnmounted } from "vue";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { useLocalStorage } from "@vueuse/core"; import { useLocalStorage } from "@vueuse/core";
import { isString, toNumber } from "lodash"; import { isString, toNumber, isUndefined, type Dictionary } from "lodash";
import z from "zod"; import z from "zod";
import { isNumber } from "mathjs"; import { isNumber } from "mathjs";
import { JtagClient, MatrixKeyClient, PowerClient } from "@/APIClient";
import { Mutex, withTimeout } from "async-mutex"; import { Mutex, withTimeout } from "async-mutex";
import { useConstraintsStore } from "@/stores/constraints"; import { useConstraintsStore } from "@/stores/constraints";
import { useDialogStore } from "./dialog"; import { useDialogStore } from "./dialog";
import { toFileParameterOrUndefined } from "@/utils/Common"; import { toFileParameterOrUndefined } from "@/utils/Common";
import { AuthManager } from "@/utils/AuthManager"; import { AuthManager } from "@/utils/AuthManager";
import { HubConnection, HubConnectionBuilder } from "@microsoft/signalr";
import { getHubProxyFactory, getReceiverRegister } from "@/TypedSignalR.Client";
import type { ResourceInfo } from "@/APIClient";
import type { IJtagHub } from "@/TypedSignalR.Client/server.Hubs.JtagHub";
export const useEquipments = defineStore("equipments", () => { export const useEquipments = defineStore("equipments", () => {
// Global Stores // Global Stores
@@ -22,13 +25,39 @@ export const useEquipments = defineStore("equipments", () => {
// Jtag // Jtag
const jtagBitstream = ref<File>(); const jtagBitstream = ref<File>();
const jtagBoundaryScanFreq = ref(100); const jtagBoundaryScanFreq = ref(100);
const jtagBoundaryScanErrorCount = ref(0); // 边界扫描连续错误计数 const jtagUserBitstreams = ref<ResourceInfo[]>([]);
const maxJtagBoundaryScanErrors = 5; // 最大允许连续错误次数
const jtagClientMutex = withTimeout( const jtagClientMutex = withTimeout(
new Mutex(), new Mutex(),
1000, 1000,
new Error("JtagClient Mutex Timeout!"), new Error("JtagClient Mutex Timeout!"),
); );
const jtagHubConnection = ref<HubConnection>();
const jtagHubProxy = ref<IJtagHub>();
onMounted(async () => {
// 每次挂载都重新创建连接
jtagHubConnection.value =
AuthManager.createAuthenticatedJtagHubConnection();
jtagHubProxy.value = getHubProxyFactory("IJtagHub").createHubProxy(
jtagHubConnection.value,
);
getReceiverRegister("IJtagReceiver").register(jtagHubConnection.value, {
onReceiveBoundaryScanData: async (msg) => {
constrainsts.batchSetConstraintStates(msg);
},
});
await jtagHubConnection.value.start();
});
onUnmounted(() => {
// 断开连接,清理资源
if (jtagHubConnection.value) {
jtagHubConnection.value.stop();
jtagHubConnection.value = undefined;
jtagHubProxy.value = undefined;
}
});
// Matrix Key // Matrix Key
const matrixKeyStates = reactive(new Array<boolean>(16).fill(false)); const matrixKeyStates = reactive(new Array<boolean>(16).fill(false));
@@ -50,41 +79,6 @@ export const useEquipments = defineStore("equipments", () => {
const enableMatrixKey = ref(false); const enableMatrixKey = ref(false);
const enablePower = ref(false); const enablePower = ref(false);
// Watch
watchPostEffect(async () => {
if (true === enableJtagBoundaryScan.value) {
// 重新启用时重置错误计数器
jtagBoundaryScanErrorCount.value = 0;
jtagBoundaryScan();
}
});
// Parse and Set
function setAddr(address: string | undefined): boolean {
if (isString(address) && z.string().ip("4").safeParse(address).success) {
boardAddr.value = address;
return true;
}
return false;
}
function setPort(port: string | number | undefined): boolean {
if (isString(port) && port.length != 0) {
const portNumber = toNumber(port);
if (z.number().nonnegative().max(65535).safeParse(portNumber).success) {
boardPort.value = portNumber;
return true;
}
} else if (isNumber(port)) {
if (z.number().nonnegative().max(65535).safeParse(port).success) {
boardPort.value = port;
return true;
}
}
return false;
}
function setMatrixKey( function setMatrixKey(
keyNum: number | string | undefined, keyNum: number | string | undefined,
keyValue: boolean, keyValue: boolean,
@@ -105,60 +99,62 @@ export const useEquipments = defineStore("equipments", () => {
return false; return false;
} }
async function jtagBoundaryScan() { async function jtagBoundaryScanSetOnOff(enable: boolean) {
const release = await jtagClientMutex.acquire(); if (isUndefined(jtagHubProxy.value)) {
try { console.error("JtagHub Not Initialize...");
// 自动开启电源 return;
await powerSetOnOff(true);
const jtagClient = AuthManager.createAuthenticatedJtagClient();
const portStates = await jtagClient.boundaryScanLogicalPorts(
boardAddr.value,
boardPort.value,
);
constrainsts.batchSetConstraintStates(portStates);
// 扫描成功,重置错误计数器
jtagBoundaryScanErrorCount.value = 0;
} catch (error) {
jtagBoundaryScanErrorCount.value++;
console.error(`边界扫描错误 (${jtagBoundaryScanErrorCount.value}/${maxJtagBoundaryScanErrors}):`, error);
// 如果错误次数超过最大允许次数,才停止扫描并显示错误
if (jtagBoundaryScanErrorCount.value >= maxJtagBoundaryScanErrors) {
dialog.error("边界扫描发生连续错误,已自动停止");
enableJtagBoundaryScan.value = false;
jtagBoundaryScanErrorCount.value = 0; // 重置错误计数器
}
} finally {
release();
if (enableJtagBoundaryScan.value)
setTimeout(jtagBoundaryScan, 1000 / jtagBoundaryScanFreq.value);
} }
if (enable) {
const ret = await jtagHubProxy.value.startBoundaryScan(
jtagBoundaryScanFreq.value,
);
if (!ret) {
console.error("Failed to start boundary scan");
return;
}
} else {
const ret = await jtagHubProxy.value.stopBoundaryScan();
if (!ret) {
console.error("Failed to stop boundary scan");
return;
}
}
enableJtagBoundaryScan.value = enable;
} }
async function jtagUploadBitstream(bitstream: File): Promise<boolean> { async function jtagUploadBitstream(bitstream: File, examId?: string): Promise<number | null> {
try { try {
// 自动开启电源 // 自动开启电源
await powerSetOnOff(true); await powerSetOnOff(true);
const jtagClient = AuthManager.createAuthenticatedJtagClient(); const resourceClient = AuthManager.createAuthenticatedResourceClient();
const resp = await jtagClient.uploadBitstream( const resp = await resourceClient.addResource(
boardAddr.value, 'bitstream',
'user',
examId || null,
toFileParameterOrUndefined(bitstream), toFileParameterOrUndefined(bitstream),
); );
return resp;
// 如果上传成功,设置为当前选中的比特流
if (resp && resp.id !== undefined && resp.id !== null) {
return resp.id;
}
return null;
} catch (e) { } catch (e) {
dialog.error("上传错误"); dialog.error("上传错误");
console.error(e); console.error(e);
return false; return null;
} }
} }
async function jtagDownloadBitstream(): Promise<boolean> { async function jtagDownloadBitstream(bitstreamId?: number): Promise<boolean> {
if (bitstreamId === null || bitstreamId === undefined) {
dialog.error("请先选择要下载的比特流");
return false;
}
const release = await jtagClientMutex.acquire(); const release = await jtagClientMutex.acquire();
try { try {
// 自动开启电源 // 自动开启电源
@@ -168,10 +164,11 @@ export const useEquipments = defineStore("equipments", () => {
const resp = await jtagClient.downloadBitstream( const resp = await jtagClient.downloadBitstream(
boardAddr.value, boardAddr.value,
boardPort.value, boardPort.value,
bitstreamId,
); );
return resp; return resp;
} catch (e) { } catch (e) {
dialog.error("上传错误"); dialog.error("下载错误");
console.error(e); console.error(e);
return false; return false;
} finally { } finally {
@@ -224,7 +221,8 @@ export const useEquipments = defineStore("equipments", () => {
const release = await matrixKeypadClientMutex.acquire(); const release = await matrixKeypadClientMutex.acquire();
console.log("set Key !!!!!!!!!!!!"); console.log("set Key !!!!!!!!!!!!");
try { try {
const matrixKeypadClient = AuthManager.createAuthenticatedMatrixKeyClient(); const matrixKeypadClient =
AuthManager.createAuthenticatedMatrixKeyClient();
const resp = await matrixKeypadClient.setMatrixKeyStatus( const resp = await matrixKeypadClient.setMatrixKeyStatus(
boardAddr.value, boardAddr.value,
boardPort.value, boardPort.value,
@@ -243,7 +241,8 @@ export const useEquipments = defineStore("equipments", () => {
const release = await matrixKeypadClientMutex.acquire(); const release = await matrixKeypadClientMutex.acquire();
try { try {
if (enable) { if (enable) {
const matrixKeypadClient = AuthManager.createAuthenticatedMatrixKeyClient(); const matrixKeypadClient =
AuthManager.createAuthenticatedMatrixKeyClient();
const resp = await matrixKeypadClient.enabelMatrixKey( const resp = await matrixKeypadClient.enabelMatrixKey(
boardAddr.value, boardAddr.value,
boardPort.value, boardPort.value,
@@ -251,7 +250,8 @@ export const useEquipments = defineStore("equipments", () => {
enableMatrixKey.value = resp; enableMatrixKey.value = resp;
return resp; return resp;
} else { } else {
const matrixKeypadClient = AuthManager.createAuthenticatedMatrixKeyClient(); const matrixKeypadClient =
AuthManager.createAuthenticatedMatrixKeyClient();
const resp = await matrixKeypadClient.disableMatrixKey( const resp = await matrixKeypadClient.disableMatrixKey(
boardAddr.value, boardAddr.value,
boardPort.value, boardPort.value,
@@ -290,16 +290,14 @@ export const useEquipments = defineStore("equipments", () => {
return { return {
boardAddr, boardAddr,
boardPort, boardPort,
setAddr,
setPort,
setMatrixKey, setMatrixKey,
// Jtag // Jtag
enableJtagBoundaryScan, enableJtagBoundaryScan,
jtagBoundaryScanSetOnOff,
jtagBitstream, jtagBitstream,
jtagBoundaryScanFreq, jtagBoundaryScanFreq,
jtagBoundaryScanErrorCount, jtagUserBitstreams,
jtagClientMutex,
jtagUploadBitstream, jtagUploadBitstream,
jtagDownloadBitstream, jtagDownloadBitstream,
jtagGetIDCode, jtagGetIDCode,

View File

@@ -1,29 +0,0 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useSidebarStore = defineStore('sidebar', () => {
const isClose = ref(false);
function closeSidebar() {
isClose.value = true;
console.info("Close sidebar");
}
function openSidebar() {
isClose.value = false;
console.info("Open sidebar");
}
function toggleSidebar() {
if (isClose.value) {
openSidebar();
// themeSidebar.value = "card-dash sidebar-base sidebar-open"
} else {
closeSidebar();
// themeSidebar.value = "card-dash sidebar-base sidebar-close"
}
}
return { isClose, closeSidebar, openSidebar, toggleSidebar }
})

View File

@@ -14,8 +14,12 @@ import {
OscilloscopeApiClient, OscilloscopeApiClient,
DebuggerClient, DebuggerClient,
ExamClient, ExamClient,
ResourceClient,
} from "@/APIClient"; } from "@/APIClient";
import router from "@/router";
import { HubConnectionBuilder } from "@microsoft/signalr";
import axios, { type AxiosInstance } from "axios"; import axios, { type AxiosInstance } from "axios";
import { isNull } from "lodash";
// 支持的客户端类型联合类型 // 支持的客户端类型联合类型
type SupportedClient = type SupportedClient =
@@ -33,7 +37,8 @@ type SupportedClient =
| NetConfigClient | NetConfigClient
| OscilloscopeApiClient | OscilloscopeApiClient
| DebuggerClient | DebuggerClient
| ExamClient; | ExamClient
| ResourceClient;
export class AuthManager { export class AuthManager {
// 存储token到localStorage // 存储token到localStorage
@@ -117,7 +122,7 @@ export class AuthManager {
if (!token) return null; if (!token) return null;
const instance = axios.create(); const instance = axios.create();
instance.interceptors.request.use(config => { instance.interceptors.request.use((config) => {
config.headers = config.headers || {}; config.headers = config.headers || {};
(config.headers as any)["Authorization"] = `Bearer ${token}`; (config.headers as any)["Authorization"] = `Bearer ${token}`;
return config; return config;
@@ -196,6 +201,25 @@ export class AuthManager {
return AuthManager.createAuthenticatedClient(ExamClient); return AuthManager.createAuthenticatedClient(ExamClient);
} }
public static createAuthenticatedResourceClient(): ResourceClient {
return AuthManager.createAuthenticatedClient(ResourceClient);
}
public static createAuthenticatedJtagHubConnection() {
const token = this.getToken();
if (isNull(token)) {
router.push("/login");
throw Error("Token Null!");
}
return new HubConnectionBuilder()
.withUrl("http://127.0.0.1:5000/hubs/JtagHub", {
accessTokenFactory: () => token,
})
.withAutomaticReconnect()
.build();
}
// 登录函数 // 登录函数
public static async login( public static async login(
username: string, username: string,

View File

@@ -679,15 +679,15 @@ const downloadResources = async () => {
downloadingResources.value = true downloadingResources.value = true
try { try {
const client = AuthManager.createAuthenticatedExamClient() const resourceClient = AuthManager.createAuthenticatedResourceClient()
// 获取资源包列表 // 获取资源包列表(模板资源)
const resourceList = await client.getExamResourceList(selectedExam.value.id, 'resource') const resourceList = await resourceClient.getResourceList(selectedExam.value.id, 'resource', 'template')
if (resourceList && resourceList.length > 0) { if (resourceList && resourceList.length > 0) {
// 使用动态API获取第一个资源包 // 使用新的ResourceClient API获取第一个资源包
const resourceId = resourceList[0].id const resourceId = resourceList[0].id
const fileResponse = await client.getExamResourceById(resourceId) const fileResponse = await resourceClient.getResourceById(resourceId)
// 创建Blob URL // 创建Blob URL
const blobUrl = URL.createObjectURL(fileResponse.data) const blobUrl = URL.createObjectURL(fileResponse.data)
@@ -925,7 +925,7 @@ const submitCreateExam = async () => {
// 上传实验资源 // 上传实验资源
const uploadExamResources = async (examId: string) => { const uploadExamResources = async (examId: string) => {
const client = AuthManager.createAuthenticatedExamClient() const client = AuthManager.createAuthenticatedResourceClient()
try { try {
// 上传MD文档 // 上传MD文档
@@ -934,7 +934,7 @@ const uploadExamResources = async (examId: string) => {
data: uploadFiles.value.mdFile, data: uploadFiles.value.mdFile,
fileName: uploadFiles.value.mdFile.name fileName: uploadFiles.value.mdFile.name
} }
await client.addExamResource(examId, 'doc', mdFileParam) await client.addResource('doc', 'template', examId, mdFileParam)
console.log('MD文档上传成功') console.log('MD文档上传成功')
} }
@@ -944,7 +944,7 @@ const uploadExamResources = async (examId: string) => {
data: imageFile, data: imageFile,
fileName: imageFile.name fileName: imageFile.name
} }
await client.addExamResource(examId, 'image', imageFileParam) await client.addResource('image', 'template', examId, imageFileParam)
console.log('图片上传成功:', imageFile.name) console.log('图片上传成功:', imageFile.name)
} }
@@ -954,7 +954,7 @@ const uploadExamResources = async (examId: string) => {
data: bitstreamFile, data: bitstreamFile,
fileName: bitstreamFile.name fileName: bitstreamFile.name
} }
await client.addExamResource(examId, 'bitstream', bitstreamFileParam) await client.addResource('bitstream', 'template', examId, bitstreamFileParam)
console.log('比特流文件上传成功:', bitstreamFile.name) console.log('比特流文件上传成功:', bitstreamFile.name)
} }
@@ -964,7 +964,7 @@ const uploadExamResources = async (examId: string) => {
data: canvasFile, data: canvasFile,
fileName: canvasFile.name fileName: canvasFile.name
} }
await client.addExamResource(examId, 'canvas', canvasFileParam) await client.addResource('canvas', 'template', examId, canvasFileParam)
console.log('画布模板上传成功:', canvasFile.name) console.log('画布模板上传成功:', canvasFile.name)
} }
@@ -974,7 +974,7 @@ const uploadExamResources = async (examId: string) => {
data: uploadFiles.value.resourceFile, data: uploadFiles.value.resourceFile,
fileName: uploadFiles.value.resourceFile.name fileName: uploadFiles.value.resourceFile.name
} }
await client.addExamResource(examId, 'resource', resourceFileParam) await client.addResource('resource', 'template', examId, resourceFileParam)
console.log('资源包上传成功') console.log('资源包上传成功')
} }

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="h-full flex flex-col gap-7"> <div class="h-full flex flex-col gap-7">
<div class="tabs tabs-box flex-shrink-0 shadow-xl mx-5"> <div class="tabs tabs-lift flex-shrink-0 mx-5">
<label class="tab"> <label class="tab">
<input <input
type="radio" type="radio"

View File

@@ -37,13 +37,13 @@
<!-- 拖拽分割线 --> <!-- 拖拽分割线 -->
<SplitterResizeHandle <SplitterResizeHandle
id="splitter-group-h-resize-handle" id="splitter-group-h-resize-handle"
class="w-2 bg-base-100 hover:bg-primary hover:opacity-70 transition-colors" class="w-1 bg-base-300"
/> />
<!-- 右侧编辑区域 --> <!-- 右侧编辑区域 -->
<SplitterPanel <SplitterPanel
id="splitter-group-h-panel-properties" id="splitter-group-h-panel-properties"
:min-size="20" :min-size="20"
class="bg-base-200 h-full overflow-hidden flex flex-col" class="bg-base-100 h-full overflow-hidden flex flex-col"
> >
<div class="overflow-y-auto flex-1"> <div class="overflow-y-auto flex-1">
<!-- 使用条件渲染显示不同的面板 --> <!-- 使用条件渲染显示不同的面板 -->
@@ -74,7 +74,7 @@
<SplitterResizeHandle <SplitterResizeHandle
v-show="!isBottomBarFullscreen" v-show="!isBottomBarFullscreen"
id="splitter-group-v-resize-handle" id="splitter-group-v-resize-handle"
class="h-2 bg-base-100 hover:bg-primary hover:opacity-70 transition-colors" class="h-1 bg-base-300"
/> />
<!-- 功能底栏 --> <!-- 功能底栏 -->
@@ -104,11 +104,32 @@
@close="handleRequestBoardClose" @close="handleRequestBoardClose"
@success="handleRequestBoardSuccess" @success="handleRequestBoardSuccess"
/> />
<!-- Navbar切换浮动按钮 -->
<div
class="navbar-toggle-btn"
:class="{ 'with-navbar': navbarControl.showNavbar.value }"
>
<button
@click="navbarControl.toggleNavbar"
class="btn btn-circle btn-primary shadow-lg hover:shadow-xl transition-all duration-300"
:class="{ 'btn-outline': navbarControl.showNavbar.value }"
:title="navbarControl.showNavbar.value ? '隐藏顶部导航栏' : '显示顶部导航栏'"
>
<!-- 使用SVG图标表示菜单/关闭状态 -->
<svg v-if="navbarControl.showNavbar.value" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch } from "vue"; import { ref, onMounted, watch, inject, type Ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useLocalStorage } from '@vueuse/core'; // 添加VueUse导入 import { useLocalStorage } from '@vueuse/core'; // 添加VueUse导入
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui"; import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui";
@@ -136,6 +157,12 @@ const equipments = useEquipments();
const alert = useAlertStore(); const alert = useAlertStore();
// --- Navbar控制 ---
const navbarControl = inject('navbar') as {
showNavbar: Ref<boolean>;
toggleNavbar: () => void;
};
// --- 使用VueUse保存分栏状态 --- // --- 使用VueUse保存分栏状态 ---
// 左右分栏比例默认60% // 左右分栏比例默认60%
const horizontalSplitterSize = useLocalStorage('project-horizontal-splitter-size', 60); const horizontalSplitterSize = useLocalStorage('project-horizontal-splitter-size', 60);
@@ -190,17 +217,17 @@ async function loadDocumentContent() {
if (examId) { if (examId) {
// 如果有实验ID从API加载实验文档 // 如果有实验ID从API加载实验文档
console.log('加载实验文档:', examId); console.log('加载实验文档:', examId);
const client = AuthManager.createAuthenticatedExamClient(); const client = AuthManager.createAuthenticatedResourceClient();
// 获取markdown类型的资源列表 // 获取markdown类型的模板资源列表
const resources = await client.getExamResourceList(examId, 'doc'); const resources = await client.getResourceList(examId, 'doc', 'template');
if (resources && resources.length > 0) { if (resources && resources.length > 0) {
// 获取第一个markdown资源 // 获取第一个markdown资源
const markdownResource = resources[0]; const markdownResource = resources[0];
// 使用动态API获取资源文件内容 // 使用新的ResourceClient API获取资源文件内容
const response = await client.getExamResourceById(markdownResource.id); const response = await client.getResourceById(markdownResource.id);
if (!response || !response.data) { if (!response || !response.data) {
throw new Error('获取markdown文件失败'); throw new Error('获取markdown文件失败');
@@ -279,8 +306,8 @@ async function checkAndInitializeBoard() {
// 根据实验板信息更新equipment store // 根据实验板信息更新equipment store
function updateEquipmentFromBoard(board: Board) { function updateEquipmentFromBoard(board: Board) {
equipments.setAddr(board.ipAddr); equipments.boardAddr = board.ipAddr;
equipments.setPort(board.port); equipments.boardPort = board.port;
console.log(`实验板信息已更新到equipment store:`, { console.log(`实验板信息已更新到equipment store:`, {
address: board.ipAddr, address: board.ipAddr,
@@ -355,7 +382,7 @@ onMounted(async () => {
} }
} }
/* 确保滚动行为仅在需要时出现 */ /* 确保整个页面禁止滚动 */
html, html,
body { body {
overflow: hidden; overflow: hidden;
@@ -387,7 +414,42 @@ body {
:deep(.markdown-content) { :deep(.markdown-content) {
padding: 1rem; padding: 1rem;
background-color: hsl(var(--b1)); background-color: hsl(var(--b1));
border-radius: 0.5rem; }
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
/* Navbar切换浮动按钮样式 */
.navbar-toggle-btn {
position: fixed;
top: 20px;
left: 20px;
z-index: 1000;
transition: all 0.3s ease-in-out;
}
/* 当Navbar显示时调整按钮位置 */
.navbar-toggle-btn.with-navbar {
top: 80px; /* 调整到Navbar下方 */
}
.navbar-toggle-btn button {
backdrop-filter: blur(10px);
background: rgba(var(--p), 0.9);
border: 2px solid rgba(var(--p), 0.3);
transition: all 0.3s ease-in-out;
}
.navbar-toggle-btn button:hover {
transform: scale(1.1);
background: rgba(var(--p), 1);
}
.navbar-toggle-btn button.btn-outline {
background: rgba(var(--b1), 0.9);
color: hsl(var(--p));
border: 2px solid rgba(var(--p), 0.5);
}
.navbar-toggle-btn button.btn-outline:hover {
background: rgba(var(--p), 0.1);
border: 2px solid rgba(var(--p), 0.8);
} }
</style> </style>

View File

@@ -1,13 +1,13 @@
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from "node:url";
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import vue from '@vitejs/plugin-vue' import vue from "@vitejs/plugin-vue";
import vueJsx from '@vitejs/plugin-vue-jsx' import vueJsx from "@vitejs/plugin-vue-jsx";
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from "vite-plugin-vue-devtools";
import tailwindcss from '@tailwindcss/postcss' import tailwindcss from "@tailwindcss/postcss";
import autoprefixer from 'autoprefixer' import autoprefixer from "autoprefixer";
import Components from 'unplugin-vue-components/vite' import Components from "unplugin-vue-components/vite";
import RekaResolver from 'reka-ui/resolver' import RekaResolver from "reka-ui/resolver";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
@@ -16,49 +16,44 @@ export default defineConfig({
template: { template: {
compilerOptions: { compilerOptions: {
// 将所有 wokwi- 开头的标签视为自定义元素 // 将所有 wokwi- 开头的标签视为自定义元素
isCustomElement: (tag) => tag.startsWith('wokwi-') isCustomElement: (tag) => tag.startsWith("wokwi-"),
} },
} },
}), }),
vueJsx(), vueJsx(),
vueDevTools(), vueDevTools(),
Components( Components({
{
dts: true, dts: true,
resolvers: [ resolvers: [
RekaResolver() RekaResolver(),
// RekaResolver({ // RekaResolver({
// prefix: '' // use the prefix option to add Prefix to the imported components // prefix: '' // use the prefix option to add Prefix to the imported components
// }) // })
], ],
} }),
)
], ],
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) "@": fileURLToPath(new URL("./src", import.meta.url)),
}, },
}, },
css: { css: {
postcss: { postcss: {
plugins: [ plugins: [tailwindcss(), autoprefixer()],
tailwindcss(), },
autoprefixer()
]
}
}, },
build: { build: {
outDir: 'wwwroot', outDir: "wwwroot",
emptyOutDir: true, // also necessary emptyOutDir: true, // also necessary
}, },
server: { server: {
proxy: { proxy: {
"/swagger": { "/swagger": {
target: 'http://localhost:5000', target: "http://localhost:5000",
changeOrigin: true changeOrigin: true,
} },
}, },
port: 5173, port: 5173,
} },
}) });