20 Commits

Author SHA1 Message Date
alivender
51b39cee07 add: 添加了HDMI视频流Client 2025-08-04 11:54:58 +08:00
alivender
0bd1ad8a0e add: 添加了960*540分辨率 2025-08-02 21:07:08 +08:00
alivender
f2c7c78b64 feat: JtaggetDR可以一次全部获取到 2025-08-02 16:01:07 +08:00
alivender
2f23ffe482 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab into dpp 2025-08-02 13:15:07 +08:00
alivender
9904fecbee feat: 统一资源管理 2025-08-02 13:14:01 +08:00
cb229c2a30 fix: 修复jtag边界扫描前后端的bug:无法开始停止,无法通过认证,后端崩溃 2025-08-02 13:10:44 +08:00
alivender
e5f2be616c feat:删除刷新保存功能,大幅提升性能 2025-08-01 20:51:50 +08:00
alivender
2e9e378457 feat: 完善部分jtag边界扫描websocket代码 2025-08-01 20:21:32 +08:00
alivender
9fe0ee959f Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-08-01 20:00:00 +08:00
9adc5295f8 feat: 使用SignalR来控制jtag边界扫描 2025-08-01 19:55:55 +08:00
alivender
8047987935 Index界面可以隐藏NavBar 2025-08-01 13:40:21 +08:00
alivender
2d77706013 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-08-01 12:57:33 +08:00
alivender
c564844673 add: 添加实验列表界面,实验增删完全依赖数据库实现 2025-08-01 12:57:30 +08:00
2adeca3b99 feat: 配置板子网络时,更新动态mac 2025-07-31 16:33:19 +08:00
bafd06162c feat: 优化Common函数以提高性能 2025-07-31 15:43:16 +08:00
8c404d4072 fix: 修复Debugger处理数据时,最终转化为字节时出现的转化问题 2025-07-31 14:31:39 +08:00
alivender
d27b5d7737 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-07-31 14:03:05 +08:00
alivender
4df583e74b add: 为前后端添加exam数据库管理 2025-07-31 14:03:00 +08:00
1ca9999f15 fix: 尝试去修复Debugger处理数据时出现错乱的问题 2025-07-31 13:56:48 +08:00
alivender
0cc35ce541 feat: 移除电源控制按钮,在jtag操作时自动开启 2025-07-31 13:20:45 +08:00
58 changed files with 6542 additions and 2216 deletions

1
.gitignore vendored
View File

@@ -28,7 +28,6 @@ DebuggerCmd.md
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln
*.sw? *.sw?
*.tsbuildinfo *.tsbuildinfo

48
FPGAWebLabServer.sln Normal file
View File

@@ -0,0 +1,48 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "server", "server\server.csproj", "{F31D6A0D-0407-41CE-A67E-01B847488EFB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "server.test", "server.test\server.test.csproj", "{CC274582-AC3C-4FD1-977C-96F1BC2760BD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Debug|x64.ActiveCfg = Debug|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Debug|x64.Build.0 = Debug|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Debug|x86.ActiveCfg = Debug|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Debug|x86.Build.0 = Debug|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Release|Any CPU.Build.0 = Release|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Release|x64.ActiveCfg = Release|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Release|x64.Build.0 = Release|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Release|x86.ActiveCfg = Release|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Release|x86.Build.0 = Release|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Debug|x64.ActiveCfg = Debug|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Debug|x64.Build.0 = Debug|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Debug|x86.ActiveCfg = Debug|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Debug|x86.Build.0 = Debug|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Release|Any CPU.Build.0 = Release|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Release|x64.ActiveCfg = Release|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Release|x64.Build.0 = Release|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Release|x86.ActiveCfg = Release|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -7,7 +7,7 @@
let let
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f {
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system; inherit system;
config.permittedInsecurePackages = ["dotnet-sdk-6.0.428"]; config.permittedInsecurePackages = ["dotnet-sdk-6.0.428"];
}; };
@@ -16,7 +16,7 @@
{ {
devShells = forEachSupportedSystem ({ pkgs }: { devShells = forEachSupportedSystem ({ pkgs }: {
default = pkgs.mkShell { default = pkgs.mkShell {
packages = with pkgs; [ packages = with pkgs; [
# Frontend # Frontend
nodejs nodejs
sqlite sqlite
@@ -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}`));
@@ -74,222 +87,211 @@ async function startWeb(): Promise<ChildProcess> {
// 存储进程引用 // 存储进程引用
webProcess = process; webProcess = process;
// 超时处理 // 超时处理
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}`),
);
} }
}); });
// 存储进程引用 // 存储进程引用
serverProcess = process; serverProcess = process;
// 超时处理 // 超时处理
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;
// 只有在进程可能没有正常退出时才执行清理 // 只有在进程可能没有正常退出时才执行清理
// 移除自动清理逻辑,因为正常退出时不需要 // 移除自动清理逻辑,因为正常退出时不需要
} }
} }
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)) {
throw new Error(`API client file not found: ${filePath}`); throw new Error(`API client file not found: ${filePath}`);
} }
// 读取文件内容 // 读取文件内容
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,80 +365,68 @@ 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;
} }
isCleaningUp = true; isCleaningUp = true;
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);
}); });
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

@@ -1,61 +0,0 @@
using System.Buffers.Binary;
using Common;
namespace server.test;
public class CommonTest
{
[Fact]
public void ReverseBytesTest()
{
var rnd = new Random();
var bytesLen = 8;
var bytesArray = new byte[bytesLen];
rnd.NextBytes(bytesArray);
var rev2Bytes = new byte[] {
bytesArray[1],
bytesArray[0],
bytesArray[3],
bytesArray[2],
bytesArray[5],
bytesArray[4],
bytesArray[7],
bytesArray[6],
};
Assert.Equal(Number.ReverseBytes(bytesArray, 2).Value, rev2Bytes);
var rev4Bytes = new byte[] {
bytesArray[3],
bytesArray[2],
bytesArray[1],
bytesArray[0],
bytesArray[7],
bytesArray[6],
bytesArray[5],
bytesArray[4],
};
Assert.Equal(Number.ReverseBytes(bytesArray, 4).Value, rev4Bytes);
}
[Fact]
public void ToBitTest()
{
Assert.Equal(true, Number.ToBit(0xFF, 3).Value);
Assert.Equal(false, Number.ToBit(0x00, 3).Value);
Assert.Equal(true, Number.ToBit(0xAA, 3).Value);
Assert.Equal(false, Number.ToBit(0xAA, 2).Value);
}
[Fact]
public void ReverseBits()
{
Assert.Equal((byte)0x05, Common.Number.ReverseBits((byte)0xA0));
var bytesSrc = new byte[] { 0xAB, 0x00, 0x00, 0x01 };
var bytes = new byte[4];
for (int i = 0; i < 4; i++)
bytes[i] = Common.Number.ReverseBits(bytesSrc[i]);
Assert.Equal(new byte[] { 0xD5, 0x00, 0x00, 0x80 }, bytes);
}
}

286
server.test/NumberTest.cs Normal file
View File

@@ -0,0 +1,286 @@
using System.Collections;
using Common;
namespace CommonTest;
/// <summary>
/// 针对 Common.Number 的单元测试,覆盖所有公开方法
/// </summary>
public class NumberTest
{
/// <summary>
/// 测试 NumberToBytes 的正常与异常情况
/// </summary>
[Fact]
public void Test_NumberToBytes()
{
// 测试大端isLowNumHigh=false
var result1 = Number.NumberToBytes(0x12345678ABCDEF01, 8, false);
Assert.True(result1.IsSuccessful);
Assert.Equal(new byte[] { 0x12, 0x34, 0x56, 0x78, 0xAB, 0xCD, 0xEF, 0x01 }, result1.Value);
// 测试小端isLowNumHigh=true
var result2 = Number.NumberToBytes(0x12345678ABCDEF01, 8, true);
Assert.True(result2.IsSuccessful);
Assert.Equal(new byte[] { 0x01, 0xEF, 0xCD, 0xAB, 0x78, 0x56, 0x34, 0x12 }, result2.Value);
// 测试长度不足4字节
var result3 = Number.NumberToBytes(0x12345678, 4, false);
Assert.True(result3.IsSuccessful);
Assert.Equal(new byte[] { 0x12, 0x34, 0x56, 0x78 }, result3.Value);
// 测试超长
var result4 = Number.NumberToBytes(0x1, 9, false);
Assert.False(result4.IsSuccessful);
}
/// <summary>
/// 测试 BytesToUInt64 的正常与异常情况
/// </summary>
[Fact]
public void Test_BytesToUInt64()
{
// 正常大端
var bytes = new byte[] { 0x12, 0x34, 0x56, 0x78, 0xAB, 0xCD, 0xEF, 0x01 };
var result = Number.BytesToUInt64((byte[])bytes.Clone(), false);
Assert.True(result.IsSuccessful);
Assert.Equal(0x12345678ABCDEF01UL, result.Value);
// 正常小端
var bytes2 = new byte[] { 0x01, 0xEF, 0xCD, 0xAB, 0x78, 0x56, 0x34, 0x12 };
var result2 = Number.BytesToUInt64((byte[])bytes2.Clone(), true);
Assert.True(result2.IsSuccessful);
Assert.Equal(0x12345678ABCDEF01UL, result2.Value);
// 异常:长度超限
var result3 = Number.BytesToUInt64(new byte[9], false);
Assert.False(result3.IsSuccessful);
// 异常不足8字节
var result4 = Number.BytesToUInt64(new byte[] { 0x01, 0x02 }, false);
Assert.False(result4.IsSuccessful); // BitConverter.ToUInt64 需要8字节
}
/// <summary>
/// 测试 BytesToUInt32 的正常与异常情况
/// </summary>
[Fact]
public void Test_BytesToUInt32()
{
// 正常大端
var bytes = new byte[] { 0x12, 0x34, 0x56, 0x78 };
var result = Number.BytesToUInt32((byte[])bytes.Clone(), false);
Assert.True(result.IsSuccessful);
Assert.Equal(0x12345678U, result.Value);
// 正常小端
var bytes2 = new byte[] { 0x78, 0x56, 0x34, 0x12 };
var result2 = Number.BytesToUInt32((byte[])bytes2.Clone(), true);
Assert.True(result2.IsSuccessful);
Assert.Equal(0x12345678U, result2.Value);
// 异常:长度超限
var result3 = Number.BytesToUInt32(new byte[5], false);
Assert.False(result3.IsSuccessful);
// 异常不足4字节
var result4 = Number.BytesToUInt32(new byte[] { 0x01, 0x02 }, false);
Assert.False(result4.IsSuccessful); // BitConverter.ToUInt32 需要4字节
}
/// <summary>
/// 测试 UInt32ArrayToBytes 的正常与异常情况
/// </summary>
[Fact]
public void Test_UInt32ArrayToBytes()
{
// 正常情况
var arr = new UInt32[] { 0x12345678, 0xABCDEF01 };
var result = Number.UInt32ArrayToBytes(arr);
Assert.True(result.IsSuccessful);
// BlockCopy 按小端序
Assert.Equal(new byte[] { 0x78, 0x56, 0x34, 0x12, 0x01, 0xEF, 0xCD, 0xAB }, result.Value);
// 空数组
var result2 = Number.UInt32ArrayToBytes(new UInt32[0]);
Assert.True(result2.IsSuccessful);
Assert.Empty(result2.Value);
}
/// <summary>
/// 测试 MultiBitsToBytes 和 MultiBitsToNumber (ulong)
/// </summary>
[Fact]
public void Test_MultiBitsToBytesAndNumber_Ulong()
{
// 合并两个比特段
var result = Number.MultiBitsToNumber(0b101UL, 3, 0b11UL, 2);
Assert.True(result.IsSuccessful);
Assert.Equal((ulong)0b10111, result.Value);
// 合并为字节数组
var bytesResult = Number.MultiBitsToBytes(0b101UL, 3, 0b11UL, 2);
Assert.True(bytesResult.IsSuccessful);
Assert.Equal(new byte[] { 0b10111 }, bytesResult.Value);
// 超过64位
var failResult = Number.MultiBitsToNumber(0xFFFFFFFFFFFFFFFF, 64, 1, 1);
Assert.False(failResult.IsSuccessful);
}
/// <summary>
/// 测试 MultiBitsToNumber (uint)
/// </summary>
[Fact]
public void Test_MultiBitsToNumber_Uint()
{
var result = Number.MultiBitsToNumber(0b101U, 3, 0b11U, 2);
Assert.True(result.IsSuccessful);
Assert.Equal((uint)0b10111, result.Value);
// 超过64位
var failResult = Number.MultiBitsToNumber(uint.MaxValue, 64, 1, 1);
Assert.False(failResult.IsSuccessful);
}
/// <summary>
/// 测试 BitsCheck (ulong)
/// </summary>
[Fact]
public void Test_BitsCheck_Ulong()
{
// 完全匹配
Assert.True(Number.BitsCheck(0b1101UL, 0b1101UL));
// 不匹配
Assert.False(Number.BitsCheck(0b1101UL, 0b1001UL));
// 掩码
Assert.True(Number.BitsCheck(0b1101UL, 0b1001UL, 0b1001UL));
}
/// <summary>
/// 测试 BitsCheck (uint)
/// </summary>
[Fact]
public void Test_BitsCheck_Uint()
{
Assert.True(Number.BitsCheck(0b1011U, 0b1011U));
Assert.False(Number.BitsCheck(0b1011U, 0b1001U));
Assert.True(Number.BitsCheck(0b1011U, 0b1001U, 0b1001U));
}
/// <summary>
/// 测试 ToBit
/// </summary>
[Fact]
public void Test_ToBit()
{
// 取第0位
var result = Number.ToBit(0b1010U, 0);
Assert.True(result.IsSuccessful);
Assert.False(result.Value);
// 取第1位
var result2 = Number.ToBit(0b1010U, 1);
Assert.True(result2.IsSuccessful);
Assert.True(result2.Value);
// 负数位置
var result3 = Number.ToBit(0b1010U, -1);
Assert.False(result3.IsSuccessful);
}
/// <summary>
/// 测试 BitsToNumber
/// </summary>
[Fact]
public void Test_BitsToNumber()
{
// 5位BitArray
var bits = new BitArray(new bool[] { true, true, false, true, false }); // 0b01011
var result = Number.BitsToNumber(bits);
Assert.True(result.IsSuccessful);
Assert.Equal((uint)0b01011, result.Value);
// 超过32位
var bits2 = new BitArray(33);
Assert.Throws<ArgumentException>(() => Number.BitsToNumber(bits2));
}
/// <summary>
/// 测试 StringToBytes
/// </summary>
[Fact]
public void Test_StringToBytes()
{
// 16进制字符串
var bytes = Number.StringToBytes("1234ABCD");
Assert.Equal(new byte[] { 0x12, 0x34, 0xAB, 0xCD }, bytes);
// 8位字符串
var bytes2 = Number.StringToBytes("01020304");
Assert.Equal(new byte[] { 0x01, 0x02, 0x03, 0x04 }, bytes2);
}
/// <summary>
/// 测试 ReverseBytes
/// </summary>
[Fact]
public void Test_ReverseBytes()
{
// 步长为2
var src = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var result = Number.ReverseBytes(src, 2);
Assert.True(result.IsSuccessful);
Assert.Equal(new byte[] { 0x02, 0x01, 0x04, 0x03 }, result.Value);
// 步长为4
var src2 = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var result2 = Number.ReverseBytes(src2, 4);
Assert.True(result2.IsSuccessful);
Assert.Equal(new byte[] { 0x04, 0x03, 0x02, 0x01 }, result2.Value);
// 步长为1无变化
var src3 = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var result3 = Number.ReverseBytes(src3, 1);
Assert.True(result3.IsSuccessful);
Assert.Equal(src3, result3.Value);
// 步长为0异常
var result4 = Number.ReverseBytes(src3, 0);
Assert.False(result4.IsSuccessful);
// 步长不能整除
var result5 = Number.ReverseBytes(src3, 3);
Assert.False(result5.IsSuccessful);
}
/// <summary>
/// 测试 ReverseBits (byte)
/// </summary>
[Fact]
public void Test_ReverseBits_Byte()
{
// 0b00010010 -> 0b01001000
byte src = 0b00010010;
byte reversed = Number.ReverseBits(src);
Assert.Equal(0b01001000, reversed);
// 0b11110000 -> 0b00001111
Assert.Equal(0b00001111, Number.ReverseBits(0b11110000));
}
/// <summary>
/// 测试 ReverseBits (byte[])
/// </summary>
[Fact]
public void Test_ReverseBits_ByteArray()
{
var src = new byte[] { 0b00010010, 0b11110000 };
var reversed = Number.ReverseBits(src);
Assert.Equal(new byte[] { 0b01001000, 0b00001111 }, reversed);
// 空数组
var reversed2 = Number.ReverseBits(new byte[0]);
Assert.Empty(reversed2);
}
}

View File

@@ -1,138 +0,0 @@
using System.Net;
using System.Text;
using Common;
using Xunit.Abstractions;
namespace server.test;
public class UDPServerTest
{
const string address = "127.0.0.1";
const int port = 33000;
private static readonly UDPServer udpServer = new UDPServer(port);
private readonly ITestOutputHelper output;
public UDPServerTest(ITestOutputHelper output)
{
this.output = output;
udpServer.Start();
}
[Fact]
public void UDPDataDeepClone()
{
var udpData = new UDPData()
{
DateTime = DateTime.Now,
Address = "127.0.0.1",
Port = 1234,
Data = new byte[] { 0xf0, 00, 00, 00 },
HasRead = false
};
var cloneUdpData = udpData.DeepClone();
Assert.Equal(udpData.DateTime, cloneUdpData.DateTime);
Assert.Equal(udpData.Address, cloneUdpData.Address);
Assert.Equal(udpData.Port, cloneUdpData.Port);
Assert.Equal(udpData.Data, cloneUdpData.Data);
Assert.Equal(udpData.HasRead, cloneUdpData.HasRead);
udpData.DateTime = DateTime.Now;
udpData.Address = "192.168.1.1";
udpData.Port = 33000;
udpData.Data = new byte[] { 0xFF, 00, 00, 00 };
udpData.HasRead = true;
Assert.NotNull(cloneUdpData.DateTime);
Assert.NotNull(cloneUdpData.Address);
Assert.NotNull(cloneUdpData.Port);
Assert.NotNull(cloneUdpData.Data);
Assert.NotNull(cloneUdpData.HasRead);
Assert.NotEqual(udpData.DateTime, cloneUdpData.DateTime);
Assert.NotEqual(udpData.Address, cloneUdpData.Address);
Assert.NotEqual(udpData.Port, cloneUdpData.Port);
Assert.NotEqual(udpData.Data, cloneUdpData.Data);
Assert.NotEqual(udpData.HasRead, cloneUdpData.HasRead);
}
[Theory]
[InlineData(new object[] { new string[] { "Hello World!", "Hello Server!", "What is your problem?" } })]
public async Task UDPServerFindString(string[] textArray)
{
Assert.True(udpServer.IsRunning);
var serverEP = new IPEndPoint(IPAddress.Parse(address), port);
foreach (var text in textArray)
{
Assert.True(await UDPClientPool.SendStringAsync(serverEP, [text]));
var ret = await udpServer.FindDataAsync(address);
Assert.True(ret.HasValue);
var data = ret.Value;
Assert.Equal(address, data.Address);
Assert.Equal(text, Encoding.ASCII.GetString(data.Data));
}
}
[Theory]
[InlineData(new object[] { new UInt32[] { 0xF0_00_00_00, 0xFF_00_00_00, 0xFF_FF_FF_FF } })]
public async Task UDPServerFindBytes(UInt32[] bytesArray)
{
Assert.True(udpServer.IsRunning);
var serverEP = new IPEndPoint(IPAddress.Parse(address), port);
foreach (var number in bytesArray)
{
Assert.True(await UDPClientPool.SendBytesAsync(serverEP, Number.NumberToBytes(number, 4).Value));
var ret = await udpServer.FindDataAsync(address);
Assert.True(ret.HasValue);
var data = ret.Value;
Assert.Equal(address, data.Address);
Assert.Equal(number, Number.BytesToUInt32(data.Data).Value);
}
}
[Theory]
[InlineData(new object[] { new UInt32[] { 0xF0_00_00_00, 0xF0_01_00_00 } })]
public async Task UDPServerWaitResp(UInt32[] bytesArray)
{
Assert.True(udpServer.IsRunning);
var serverEP = new IPEndPoint(IPAddress.Parse(address), port);
foreach (var number in bytesArray)
{
Assert.True(await UDPClientPool.SendBytesAsync(serverEP, Number.NumberToBytes(number, 4).Value));
var ret = await udpServer.WaitForAckAsync(address);
Assert.True(ret.IsSuccessful);
var data = ret.Value;
Assert.True(data.IsSuccessful);
Assert.Equal(number, Number.BytesToUInt32(data.ToBytes()).Value);
}
}
[Theory]
[InlineData(new object[] { new UInt64[] { 0x0F_00_00_00_01_02_02_02, 0x0F_01_00_00_FF_FF_FF_FF } })]
public async Task UDPServerWaitData(UInt64[] bytesArray)
{
Assert.True(udpServer.IsRunning);
var serverEP = new IPEndPoint(IPAddress.Parse(address), port);
foreach (var number in bytesArray)
{
Assert.True(await UDPClientPool.SendBytesAsync(serverEP, Number.NumberToBytes(number, 8).Value));
var ret = await udpServer.WaitForDataAsync(address);
Assert.True(ret.IsSuccessful);
var data = ret.Value;
Assert.True(data.IsSuccessful);
Assert.Equal((UInt64)number, Number.BytesToUInt64(data.ToBytes()).Value);
}
}
}

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 =>
{ {
@@ -168,6 +177,17 @@ try
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "log")), FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "log")),
RequestPath = "/log" RequestPath = "/log"
}); });
// Exam Files (实验静态资源)
if (Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "exam")))
{
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "exam")),
RequestPath = "/exam"
});
}
app.MapFallbackToFile("index.html"); app.MapFallbackToFile("index.html");
} }
@@ -188,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();
@@ -220,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();
} }
@@ -241,4 +267,3 @@ finally
// Close Program // Close Program
MsgBus.Exit(); MsgBus.Exit();
} }

View File

@@ -14,6 +14,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ArpLookup" Version="2.0.3" />
<PackageReference Include="DotNext" Version="5.23.0" /> <PackageReference Include="DotNext" Version="5.23.0" />
<PackageReference Include="DotNext.Threading" Version="5.23.0" /> <PackageReference Include="DotNext.Threading" Version="5.23.0" />
<PackageReference Include="Honoo.IO.Hashing.Crc" Version="1.3.3" /> <PackageReference Include="Honoo.IO.Hashing.Crc" Version="1.3.3" />
@@ -31,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

@@ -1,11 +1,14 @@
using System.Diagnostics; using System.Diagnostics;
using System.Net.NetworkInformation;
using ArpLookup;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Net;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
/// <summary> /// <summary>
/// ARP 记录管理静态类(跨平台支持) /// ARP 记录管理静态类(跨平台支持)
/// </summary> /// </summary>
public static class Arp public static class ArpClient
{ {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@@ -44,20 +47,28 @@ public static class Arp
} }
/// <summary> /// <summary>
/// 通过 Ping 动态更新指定 IP 的 ARP 记录 /// 动态更新指定 IP 的 ARP 记录
/// </summary> /// </summary>
/// <param name="ipAddress">要更新的 IP 地址</param> /// <param name="ipAddress">要更新的 IP 地址</param>
/// <returns>是否成功发送 Ping</returns> /// <returns>是否成功发送 Ping</returns>
public static async Task<bool> UpdateArpEntryByPingAsync(string ipAddress) public static async Task<bool> UpdateArpEntryAsync(string ipAddress)
{ {
if (string.IsNullOrWhiteSpace(ipAddress)) if (string.IsNullOrWhiteSpace(ipAddress))
throw new ArgumentException("IP 地址不能为空", nameof(ipAddress)); throw new ArgumentException("IP 地址不能为空", nameof(ipAddress));
try try
{ {
using var ping = new System.Net.NetworkInformation.Ping(); var ret = await ArpClient.DeleteArpEntryAsync(ipAddress);
var reply = await ping.SendPingAsync(ipAddress, 100); if (!ret)
return reply.Status == System.Net.NetworkInformation.IPStatus.Success; {
logger.Error($"删除 ARP 记录失败: {ipAddress}");
}
PhysicalAddress? mac = await Arp.LookupAsync(IPAddress.Parse(ipAddress));
if (mac == null)
return false;
return true;
} }
catch catch
{ {
@@ -228,22 +239,22 @@ public static class Arp
return null; return null;
var lines = result.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries); var lines = result.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines) foreach (var line in lines)
{ {
// 匹配接口行格式: Interface: 172.6.1.5 --- 0xa // 匹配接口行格式: Interface: 172.6.1.5 --- 0xa
var interfacePattern = @"Interface:\s+(\d+\.\d+\.\d+\.\d+)\s+---\s+(0x[a-fA-F0-9]+)"; var interfacePattern = @"Interface:\s+(\d+\.\d+\.\d+\.\d+)\s+---\s+(0x[a-fA-F0-9]+)";
var match = Regex.Match(line, interfacePattern); var match = Regex.Match(line, interfacePattern);
if (match.Success && match.Groups[1].Value == interfaceIp) if (match.Success && match.Groups[1].Value == interfaceIp)
{ {
// 将十六进制索引转换为十进制 // 将十六进制索引转换为十进制
var hexIndex = match.Groups[2].Value; var hexIndex = match.Groups[2].Value;
// 去掉 "0x" 前缀 // 去掉 "0x" 前缀
var hexValue = hexIndex.StartsWith("0x", StringComparison.OrdinalIgnoreCase) var hexValue = hexIndex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
? hexIndex.Substring(2) ? hexIndex.Substring(2)
: hexIndex; : hexIndex;
if (int.TryParse(hexValue, System.Globalization.NumberStyles.HexNumber, null, out int decimalIndex)) if (int.TryParse(hexValue, System.Globalization.NumberStyles.HexNumber, null, out int decimalIndex))
{ {
logger.Debug($"找到接口 {interfaceIp} 的索引: {hexIndex} -> {decimalIndex}"); logger.Debug($"找到接口 {interfaceIp} 的索引: {hexIndex} -> {decimalIndex}");
@@ -251,7 +262,7 @@ public static class Arp
} }
} }
} }
logger.Warn($"未找到接口 {interfaceIp} 的索引"); logger.Warn($"未找到接口 {interfaceIp} 的索引");
return null; return null;
} }
@@ -417,9 +428,9 @@ public static class Arp
private static ArpEntry? ParseWindowsArpEntry(string line) private static ArpEntry? ParseWindowsArpEntry(string line)
{ {
// 跳过空行和标题行 // 跳过空行和标题行
if (string.IsNullOrWhiteSpace(line) || if (string.IsNullOrWhiteSpace(line) ||
line.Contains("Interface:") || line.Contains("Interface:") ||
line.Contains("Internet Address") || line.Contains("Internet Address") ||
line.Contains("Physical Address") || line.Contains("Physical Address") ||
line.Contains("Type")) line.Contains("Type"))
{ {
@@ -518,7 +529,7 @@ public static class Arp
// 格式化 MAC 地址以适配不同操作系统 // 格式化 MAC 地址以适配不同操作系统
string formattedMac = FormatMacAddress(macAddress); string formattedMac = FormatMacAddress(macAddress);
var entry = await GetArpEntryAsync(ipAddress); var entry = await GetArpEntryAsync(ipAddress);
if (entry != null && string.Equals(FormatMacAddress(entry.MacAddress), formattedMac, StringComparison.OrdinalIgnoreCase)) if (entry != null && string.Equals(FormatMacAddress(entry.MacAddress), formattedMac, StringComparison.OrdinalIgnoreCase))
{ {
@@ -534,7 +545,7 @@ public static class Arp
// 新增 ARP 记录 // 新增 ARP 记录
var ret = await AddArpEntryAsync(ipAddress, formattedMac, interfaceName); var ret = await AddArpEntryAsync(ipAddress, formattedMac, interfaceName);
if(!ret) logger.Error($"添加 ARP 记录失败: {ipAddress} -> {formattedMac} on {interfaceName}"); if (!ret) logger.Error($"添加 ARP 记录失败: {ipAddress} -> {formattedMac} on {interfaceName}");
return true; return true;
} }

View File

@@ -65,7 +65,7 @@ public class Number
{ {
for (var i = 0; i < length; i++) for (var i = 0; i < length; i++)
{ {
arr[length - 1 - i] = Convert.ToByte((num >> (i << 3)) & (0xFF)); arr[i] = Convert.ToByte((num >> (i << 3)) & (0xFF));
} }
} }
else else
@@ -99,20 +99,11 @@ public class Number
try try
{ {
if (isLowNumHigh) if (!isLowNumHigh)
{ {
for (var i = 0; i < len; i++) Array.Reverse(bytes);
{
num += Convert.ToUInt64((UInt64)bytes[len - 1 - i] << (i << 3));
}
}
else
{
for (var i = 0; i < len; i++)
{
num += Convert.ToUInt64((UInt64)bytes[i] << ((int)(len - 1 - i) << 3));
}
} }
num = BitConverter.ToUInt64(bytes, 0);
return num; return num;
} }
@@ -143,20 +134,11 @@ public class Number
try try
{ {
if (isLowNumHigh) if (!isLowNumHigh)
{ {
for (var i = 0; i < len; i++) Array.Reverse(bytes);
{
num += Convert.ToUInt32((UInt32)bytes[len - 1 - i] << (i << 3));
}
}
else
{
for (var i = 0; i < len; i++)
{
num += Convert.ToUInt32((UInt32)bytes[i] << ((int)(len - 1 - i) << 3));
}
} }
num = BitConverter.ToUInt32(bytes, 0);
return num; return num;
} }
@@ -333,10 +315,9 @@ public class Number
for (int i = 0; i < srcBytesLen; i += distance) for (int i = 0; i < srcBytesLen; i += distance)
{ {
var end = i + distance; Buffer.BlockCopy(srcBytes, i, buffer, 0, distance);
buffer = srcBytes[i..end];
Array.Reverse(buffer); Array.Reverse(buffer);
Array.Copy(buffer, 0, dstBytes, i, distance); Buffer.BlockCopy(buffer, 0, dstBytes, i, distance);
} }
return dstBytes; return dstBytes;

View File

@@ -51,7 +51,7 @@ public class DataController : ControllerBase
public DateTime? BoardExpireTime { get; set; } public DateTime? BoardExpireTime { get; set; }
} }
/// <summary> /// <summary>
/// 获取本机IP地址优先选择与实验板同网段的IP /// 获取本机IP地址优先选择与实验板同网段的IP
/// </summary> /// </summary>
/// <returns>本机IP地址</returns> /// <returns>本机IP地址</returns>
@@ -60,7 +60,7 @@ public class DataController : ControllerBase
try try
{ {
var boardIpSegments = BOARD_IP.Split('.').Take(3).ToArray(); var boardIpSegments = BOARD_IP.Split('.').Take(3).ToArray();
// 优先选择与实验板IP前三段相同的IP // 优先选择与实验板IP前三段相同的IP
var sameSegmentIP = System.Net.NetworkInformation.NetworkInterface var sameSegmentIP = System.Net.NetworkInformation.NetworkInterface
.GetAllNetworkInterfaces() .GetAllNetworkInterfaces()
@@ -278,7 +278,7 @@ public class DataController : ControllerBase
return NotFound("没有可用的实验板"); return NotFound("没有可用的实验板");
var boardInfo = boardOpt.Value; var boardInfo = boardOpt.Value;
if (!(await Arp.CheckOrAddAsync(boardInfo.IpAddr, boardInfo.MacAddr, GetLocalIPAddress().ToString()))) if (!(await ArpClient.CheckOrAddAsync(boardInfo.IpAddr, boardInfo.MacAddr, GetLocalIPAddress().ToString())))
{ {
logger.Error($"无法配置ARP实验板可能会无法连接"); logger.Error($"无法配置ARP实验板可能会无法连接");
} }
@@ -346,7 +346,7 @@ public class DataController : ControllerBase
return NotFound("未找到对应的实验板"); return NotFound("未找到对应的实验板");
var boardInfo = ret.Value.Value; var boardInfo = ret.Value.Value;
if (!(await Arp.CheckOrAddAsync(boardInfo.IpAddr, boardInfo.MacAddr, GetLocalIPAddress().ToString()))) if (!(await ArpClient.CheckOrAddAsync(boardInfo.IpAddr, boardInfo.MacAddr, GetLocalIPAddress().ToString())))
{ {
logger.Error($"无法配置ARP实验板可能会无法连接"); logger.Error($"无法配置ARP实验板可能会无法连接");
} }
@@ -490,4 +490,3 @@ public class DataController : ControllerBase
} }
} }
} }

View File

@@ -15,74 +15,74 @@ public class DebuggerController : ControllerBase
{ {
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary> /// <summary>
/// 表示单个信号通道的配置信息 /// 表示单个信号通道的配置信息
/// </summary> /// </summary>
public class ChannelConfig public class ChannelConfig
{ {
/// <summary> /// <summary>
/// 通道名称 /// 通道名称
/// </summary> /// </summary>
required public string name; required public string name;
/// <summary> /// <summary>
/// 通道显示颜色(如前端波形显示用) /// 通道显示颜色(如前端波形显示用)
/// </summary> /// </summary>
required public string color; required public string color;
/// <summary> /// <summary>
/// 通道信号线宽度(位数) /// 通道信号线宽度(位数)
/// </summary> /// </summary>
required public UInt32 wireWidth; required public UInt32 wireWidth;
/// <summary> /// <summary>
/// 信号线在父端口中的起始索引bit /// 信号线在父端口中的起始索引bit
/// </summary> /// </summary>
required public UInt32 wireStartIndex; required public UInt32 wireStartIndex;
/// <summary> /// <summary>
/// 父端口编号 /// 父端口编号
/// </summary> /// </summary>
required public UInt32 parentPort; required public UInt32 parentPort;
/// <summary> /// <summary>
/// 捕获模式(如上升沿、下降沿等) /// 捕获模式(如上升沿、下降沿等)
/// </summary> /// </summary>
required public CaptureMode mode; required public CaptureMode mode;
} }
/// <summary> /// <summary>
/// 调试器整体配置信息 /// 调试器整体配置信息
/// </summary> /// </summary>
public class DebuggerConfig public class DebuggerConfig
{ {
/// <summary> /// <summary>
/// 时钟频率 /// 时钟频率
/// </summary> /// </summary>
required public UInt32 clkFreq; required public UInt32 clkFreq;
/// <summary> /// <summary>
/// 总端口数量 /// 总端口数量
/// </summary> /// </summary>
required public UInt32 totalPortNum; required public UInt32 totalPortNum;
/// <summary> /// <summary>
/// 捕获深度(采样点数) /// 捕获深度(采样点数)
/// </summary> /// </summary>
required public UInt32 captureDepth; required public UInt32 captureDepth;
/// <summary> /// <summary>
/// 触发器数量 /// 触发器数量
/// </summary> /// </summary>
required public UInt32 triggerNum; required public UInt32 triggerNum;
/// <summary> /// <summary>
/// 所有信号通道的配置信息 /// 所有信号通道的配置信息
/// </summary> /// </summary>
required public ChannelConfig[] channelConfigs; required public ChannelConfig[] channelConfigs;
} }
/// <summary> /// <summary>
/// 单个通道的捕获数据 /// 单个通道的捕获数据
/// </summary> /// </summary>
public class ChannelCaptureData public class ChannelCaptureData
{ {
/// <summary> /// <summary>
/// 通道名称 /// 通道名称
/// </summary> /// </summary>
required public string name; required public string name;
/// <summary> /// <summary>
/// 通道捕获到的数据Base64编码的UInt32数组 /// 通道捕获到的数据Base64编码的UInt32数组
/// </summary> /// </summary>
required public string data; required public string data;
@@ -380,7 +380,7 @@ public class DebuggerController : ControllerBase
} }
var rawData = dataResult.Value; var rawData = dataResult.Value;
logger.Debug($"rawData: {BitConverter.ToString(rawData)}"); // logger.Debug($"rawData: {BitConverter.ToString(rawData)}");
int depth = (int)config.captureDepth; int depth = (int)config.captureDepth;
int portDataLen = 4 * depth; int portDataLen = 4 * depth;
int portNum = (int)config.totalPortNum; int portNum = (int)config.totalPortNum;
@@ -405,13 +405,16 @@ public class DebuggerController : ControllerBase
logger.Error($"数据越界: port {port}, sample {i}"); logger.Error($"数据越界: port {port}, sample {i}");
return StatusCode(StatusCodes.Status500InternalServerError, "数据越界"); return StatusCode(StatusCodes.Status500InternalServerError, "数据越界");
} }
UInt32 sample = BitConverter.ToUInt32(rawData, sampleOffset); var sampleBytes = rawData[sampleOffset..(sampleOffset + 4)];
UInt32 sample = Common.Number.BytesToUInt32(sampleBytes, true).Value;
// 提取wireWidth位 // 提取wireWidth位
UInt32 mask = (wireWidth == 32) ? 0xFFFFFFFF : ((1u << wireWidth) - 1u); UInt32 mask = (wireWidth == 32) ? 0xFFFFFFFF : ((1u << wireWidth) - 1u);
channelUintArr[i] = (sample >> wireStart) & mask; channelUintArr[i] = (sample >> wireStart) & mask;
} }
logger.Debug($"{channel.name} HexData: {BitConverter.ToString(channelUintArr.SelectMany(BitConverter.GetBytes).ToArray())}"); var channelBytes = new byte[4 * depth];
var base64 = Convert.ToBase64String(channelUintArr.SelectMany(BitConverter.GetBytes).ToArray()); Buffer.BlockCopy(channelUintArr, 0, channelBytes, 0, channelBytes.Length);
// logger.Debug($"{channel.name} HexData: {BitConverter.ToString(channelBytes)}");
var base64 = Convert.ToBase64String(channelBytes);
channelDataList.Add(new ChannelCaptureData { name = channel.name, data = base64 }); channelDataList.Add(new ChannelCaptureData { name = channel.name, data = base64 });
} }

View File

@@ -0,0 +1,291 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using DotNext;
namespace server.Controllers;
/// <summary>
/// 实验控制器
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class ExamController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// 实验信息类
/// </summary>
public class ExamInfo
{
/// <summary>
/// 实验的唯一标识符
/// </summary>
public required string ID { get; set; }
/// <summary>
/// 实验名称
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 实验描述
/// </summary>
public required string Description { get; set; }
/// <summary>
/// 实验创建时间
/// </summary>
public DateTime CreatedTime { get; set; }
/// <summary>
/// 实验最后更新时间
/// </summary>
public DateTime UpdatedTime { get; set; }
/// <summary>
/// 实验标签
/// </summary>
public string[] Tags { get; set; } = Array.Empty<string>();
/// <summary>
/// 实验难度1-5
/// </summary>
public int Difficulty { get; set; } = 1;
/// <summary>
/// 普通用户是否可见
/// </summary>
public bool IsVisibleToUsers { get; set; } = true;
}
/// <summary>
/// 实验简要信息类(用于列表显示)
/// </summary>
public class ExamSummary
{
/// <summary>
/// 实验的唯一标识符
/// </summary>
public required string ID { get; set; }
/// <summary>
/// 实验名称
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 实验创建时间
/// </summary>
public DateTime CreatedTime { get; set; }
/// <summary>
/// 实验最后更新时间
/// </summary>
public DateTime UpdatedTime { get; set; }
/// <summary>
/// 实验标签
/// </summary>
public string[] Tags { get; set; } = Array.Empty<string>();
/// <summary>
/// 实验难度1-5
/// </summary>
public int Difficulty { get; set; } = 1;
/// <summary>
/// 普通用户是否可见
/// </summary>
public bool IsVisibleToUsers { get; set; } = true;
}
/// <summary>
/// 创建实验请求类
/// </summary>
public class CreateExamRequest
{
/// <summary>
/// 实验ID
/// </summary>
public required string ID { get; set; }
/// <summary>
/// 实验名称
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 实验描述
/// </summary>
public required string Description { get; set; }
/// <summary>
/// 实验标签
/// </summary>
public string[] Tags { get; set; } = Array.Empty<string>();
/// <summary>
/// 实验难度1-5
/// </summary>
public int Difficulty { get; set; } = 1;
/// <summary>
/// 普通用户是否可见
/// </summary>
public bool IsVisibleToUsers { get; set; } = true;
}
/// <summary>
/// 获取所有实验列表
/// </summary>
/// <returns>实验列表</returns>
[Authorize]
[HttpGet("list")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ExamSummary[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetExamList()
{
try
{
using var db = new Database.AppDataConnection();
var exams = db.GetAllExams();
var examSummaries = exams.Select(exam => new ExamSummary
{
ID = exam.ID,
Name = exam.Name,
CreatedTime = exam.CreatedTime,
UpdatedTime = exam.UpdatedTime,
Tags = exam.GetTagsList(),
Difficulty = exam.Difficulty,
IsVisibleToUsers = exam.IsVisibleToUsers
}).ToArray();
logger.Info($"成功获取实验列表,共 {examSummaries.Length} 个实验");
return Ok(examSummaries);
}
catch (Exception ex)
{
logger.Error($"获取实验列表时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验列表失败: {ex.Message}");
}
}
/// <summary>
/// 根据实验ID获取实验详细信息
/// </summary>
/// <param name="examId">实验ID</param>
/// <returns>实验详细信息</returns>
[Authorize]
[HttpGet("{examId}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ExamInfo), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetExam(string examId)
{
if (string.IsNullOrWhiteSpace(examId))
return BadRequest("实验ID不能为空");
try
{
using var db = new Database.AppDataConnection();
var result = db.GetExamByID(examId);
if (!result.IsSuccessful)
{
logger.Error($"获取实验时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验失败: {result.Error.Message}");
}
if (!result.Value.HasValue)
{
logger.Warn($"实验不存在: {examId}");
return NotFound($"实验 {examId} 不存在");
}
var exam = result.Value.Value;
var examInfo = new ExamInfo
{
ID = exam.ID,
Name = exam.Name,
Description = exam.Description,
CreatedTime = exam.CreatedTime,
UpdatedTime = exam.UpdatedTime,
Tags = exam.GetTagsList(),
Difficulty = exam.Difficulty,
IsVisibleToUsers = exam.IsVisibleToUsers
};
logger.Info($"成功获取实验信息: {examId}");
return Ok(examInfo);
}
catch (Exception ex)
{
logger.Error($"获取实验 {examId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验失败: {ex.Message}");
}
}
/// <summary>
/// 创建新实验
/// </summary>
/// <param name="request">创建实验请求</param>
/// <returns>创建结果</returns>
[Authorize("Admin")]
[HttpPost]
[EnableCors("Users")]
[ProducesResponseType(typeof(ExamInfo), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult CreateExam([FromBody] CreateExamRequest request)
{
if (string.IsNullOrWhiteSpace(request.ID) || string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Description))
return BadRequest("实验ID、名称和描述不能为空");
try
{
using var db = new Database.AppDataConnection();
var result = db.CreateExam(request.ID, request.Name, request.Description, request.Tags, request.Difficulty, request.IsVisibleToUsers);
if (!result.IsSuccessful)
{
if (result.Error.Message.Contains("已存在"))
return Conflict(result.Error.Message);
logger.Error($"创建实验时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {result.Error.Message}");
}
var exam = result.Value;
var examInfo = new ExamInfo
{
ID = exam.ID,
Name = exam.Name,
Description = exam.Description,
CreatedTime = exam.CreatedTime,
UpdatedTime = exam.UpdatedTime,
Tags = exam.GetTagsList(),
Difficulty = exam.Difficulty,
IsVisibleToUsers = exam.IsVisibleToUsers
};
logger.Info($"成功创建实验: {request.ID}");
return CreatedAtAction(nameof(GetExam), new { examId = request.ID }, examInfo);
}
catch (Exception ex)
{
logger.Error($"创建实验 {request.ID} 时出错: {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

@@ -19,8 +19,7 @@ public class NetConfigController : ControllerBase
// 固定的实验板IP,端口,MAC地址 // 固定的实验板IP,端口,MAC地址
private const string BOARD_IP = "169.254.109.0"; private const string BOARD_IP = "169.254.109.0";
private const int BOARD_PORT = 1234; private const int BOARD_PORT = 1234;
private const string BOARD_MAC = "12:34:56:78:9a:bc";
// 本机网络信息 // 本机网络信息
private readonly IPAddress _localIP; private readonly IPAddress _localIP;
private readonly byte[] _localMAC; private readonly byte[] _localMAC;
@@ -33,14 +32,14 @@ public class NetConfigController : ControllerBase
// 初始化本机IP地址 // 初始化本机IP地址
_localIP = GetLocalIPAddress(); _localIP = GetLocalIPAddress();
_localIPString = _localIP?.ToString() ?? "未知"; _localIPString = _localIP?.ToString() ?? "未知";
// 初始化本机MAC地址 // 初始化本机MAC地址
_localMAC = GetLocalMACAddress(); _localMAC = GetLocalMACAddress();
_localMACString = _localMAC != null ? BitConverter.ToString(_localMAC).Replace("-", ":") : "未知"; _localMACString = _localMAC != null ? BitConverter.ToString(_localMAC).Replace("-", ":") : "未知";
// 获取本机网络接口名称 // 获取本机网络接口名称
_localInterface = GetLocalNetworkInterface(); _localInterface = GetLocalNetworkInterface();
logger.Info($"NetConfigController 初始化完成 - 本机IP: {_localIPString}, 本机MAC: {_localMACString}, 接口: {_localInterface}"); logger.Info($"NetConfigController 初始化完成 - 本机IP: {_localIPString}, 本机MAC: {_localMACString}, 接口: {_localInterface}");
} }
@@ -53,7 +52,7 @@ public class NetConfigController : ControllerBase
try try
{ {
var boardIpSegments = BOARD_IP.Split('.').Take(3).ToArray(); var boardIpSegments = BOARD_IP.Split('.').Take(3).ToArray();
// 优先选择与实验板IP前三段相同的IP // 优先选择与实验板IP前三段相同的IP
var sameSegmentIP = System.Net.NetworkInformation.NetworkInterface var sameSegmentIP = System.Net.NetworkInformation.NetworkInterface
.GetAllNetworkInterfaces() .GetAllNetworkInterfaces()
@@ -130,7 +129,7 @@ public class NetConfigController : ControllerBase
{ {
try try
{ {
return await Arp.CheckOrAddAsync(BOARD_IP, BOARD_MAC, _localInterface); return await ArpClient.UpdateArpEntryAsync(BOARD_IP);
} }
catch (Exception ex) catch (Exception 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

@@ -133,7 +133,7 @@ public class Board
/// </summary> /// </summary>
public enum BoardStatus public enum BoardStatus
{ {
/// <summary> /// <summary>
/// 未启用状态,无法被使用 /// 未启用状态,无法被使用
/// </summary> /// </summary>
Disabled, Disabled,
@@ -150,6 +150,191 @@ public class Board
} }
} }
/// <summary>
/// 实验类,表示实验信息
/// </summary>
public class Exam
{
/// <summary>
/// 实验的唯一标识符
/// </summary>
[PrimaryKey]
public required string ID { get; set; }
/// <summary>
/// 实验名称
/// </summary>
[NotNull]
public required string Name { get; set; }
/// <summary>
/// 实验描述
/// </summary>
[NotNull]
public required string Description { get; set; }
/// <summary>
/// 实验创建时间
/// </summary>
[NotNull]
public DateTime CreatedTime { get; set; } = DateTime.Now;
/// <summary>
/// 实验最后更新时间
/// </summary>
[NotNull]
public DateTime UpdatedTime { get; set; } = DateTime.Now;
/// <summary>
/// 实验标签(以逗号分隔的字符串)
/// </summary>
[NotNull]
public string Tags { get; set; } = "";
/// <summary>
/// 实验难度1-51为最简单
/// </summary>
[NotNull]
public int Difficulty { get; set; } = 1;
/// <summary>
/// 普通用户是否可见
/// </summary>
[NotNull]
public bool IsVisibleToUsers { get; set; } = true;
/// <summary>
/// 获取标签列表
/// </summary>
/// <returns>标签数组</returns>
public string[] GetTagsList()
{
if (string.IsNullOrWhiteSpace(Tags))
return Array.Empty<string>();
return Tags.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(tag => tag.Trim())
.Where(tag => !string.IsNullOrEmpty(tag))
.ToArray();
}
/// <summary>
/// 设置标签列表
/// </summary>
/// <param name="tags">标签数组</param>
public void SetTagsList(string[] tags)
{
Tags = string.Join(",", tags.Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim()));
}
}
/// <summary>
/// 资源类,统一管理实验资源、用户比特流等各类资源
/// </summary>
public class Resource
{
/// <summary>
/// 资源的唯一标识符
/// </summary>
[PrimaryKey, Identity]
public int ID { get; set; }
/// <summary>
/// 上传资源的用户ID
/// </summary>
[NotNull]
public required Guid UserID { get; set; }
/// <summary>
/// 所属实验ID可选如果不属于特定实验则为空
/// </summary>
[Nullable]
public string? ExamID { get; set; }
/// <summary>
/// 资源类型images, markdown, bitstream, diagram, project等
/// </summary>
[NotNull]
public required string ResourceType { get; set; }
/// <summary>
/// 资源用途template模板或 user用户上传
/// </summary>
[NotNull]
public required string ResourcePurpose { get; set; }
/// <summary>
/// 资源名称(包含文件扩展名)
/// </summary>
[NotNull]
public required string ResourceName { get; set; }
/// <summary>
/// 资源的二进制数据
/// </summary>
[NotNull]
public required byte[] Data { get; set; }
/// <summary>
/// 资源创建/上传时间
/// </summary>
[NotNull]
public DateTime UploadTime { get; set; } = DateTime.Now;
/// <summary>
/// 资源的MIME类型
/// </summary>
[NotNull]
public string MimeType { get; set; } = "application/octet-stream";
/// <summary>
/// 资源类型枚举
/// </summary>
public static class ResourceTypes
{
/// <summary>
/// 图片资源类型
/// </summary>
public const string Images = "images";
/// <summary>
/// Markdown文档资源类型
/// </summary>
public const string Markdown = "markdown";
/// <summary>
/// 比特流文件资源类型
/// </summary>
public const string Bitstream = "bitstream";
/// <summary>
/// 原理图资源类型
/// </summary>
public const string Diagram = "diagram";
/// <summary>
/// 项目文件资源类型
/// </summary>
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>
/// 应用程序数据连接类,用于与数据库交互 /// 应用程序数据连接类,用于与数据库交互
/// </summary> /// </summary>
@@ -197,6 +382,8 @@ public class AppDataConnection : DataConnection
logger.Info("正在创建数据库表..."); logger.Info("正在创建数据库表...");
this.CreateTable<User>(); this.CreateTable<User>();
this.CreateTable<Board>(); this.CreateTable<Board>();
this.CreateTable<Exam>();
this.CreateTable<Resource>();
logger.Info("数据库表创建完成"); logger.Info("数据库表创建完成");
} }
@@ -208,6 +395,8 @@ public class AppDataConnection : DataConnection
logger.Warn("正在删除所有数据库表..."); logger.Warn("正在删除所有数据库表...");
this.DropTable<User>(); this.DropTable<User>();
this.DropTable<Board>(); this.DropTable<Board>();
this.DropTable<Exam>();
this.DropTable<Resource>();
logger.Warn("所有数据库表已删除"); logger.Warn("所有数据库表已删除");
} }
@@ -526,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>
@@ -635,4 +849,418 @@ public class AppDataConnection : DataConnection
/// FPGA 板子表 /// FPGA 板子表
/// </summary> /// </summary>
public ITable<Board> BoardTable => this.GetTable<Board>(); public ITable<Board> BoardTable => this.GetTable<Board>();
/// <summary>
/// 实验表
/// </summary>
public ITable<Exam> ExamTable => this.GetTable<Exam>();
/// <summary>
/// 资源表(统一管理实验资源、用户比特流等)
/// </summary>
public ITable<Resource> ResourceTable => this.GetTable<Resource>();
/// <summary>
/// 创建新实验
/// </summary>
/// <param name="id">实验ID</param>
/// <param name="name">实验名称</param>
/// <param name="description">实验描述</param>
/// <param name="tags">实验标签</param>
/// <param name="difficulty">实验难度</param>
/// <param name="isVisibleToUsers">普通用户是否可见</param>
/// <returns>创建的实验</returns>
public Result<Exam> CreateExam(string id, string name, string description, string[]? tags = null, int difficulty = 1, bool isVisibleToUsers = true)
{
try
{
// 检查实验ID是否已存在
var existingExam = this.ExamTable.Where(e => e.ID == id).FirstOrDefault();
if (existingExam != null)
{
logger.Error($"实验ID已存在: {id}");
return new(new Exception($"实验ID已存在: {id}"));
}
var exam = new Exam
{
ID = id,
Name = name,
Description = description,
Difficulty = Math.Max(1, Math.Min(5, difficulty)),
IsVisibleToUsers = isVisibleToUsers,
CreatedTime = DateTime.Now,
UpdatedTime = DateTime.Now
};
if (tags != null)
{
exam.SetTagsList(tags);
}
this.Insert(exam);
logger.Info($"新实验已创建: {id} ({name})");
return new(exam);
}
catch (Exception ex)
{
logger.Error($"创建实验时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 更新实验信息
/// </summary>
/// <param name="id">实验ID</param>
/// <param name="name">实验名称</param>
/// <param name="description">实验描述</param>
/// <param name="tags">实验标签</param>
/// <param name="difficulty">实验难度</param>
/// <param name="isVisibleToUsers">普通用户是否可见</param>
/// <returns>更新的记录数</returns>
public Result<int> UpdateExam(string id, string? name = null, string? description = null, string[]? tags = null, int? difficulty = null, bool? isVisibleToUsers = null)
{
try
{
int result = 0;
if (name != null)
{
result += this.ExamTable.Where(e => e.ID == id).Set(e => e.Name, name).Update();
}
if (description != null)
{
result += this.ExamTable.Where(e => e.ID == id).Set(e => e.Description, description).Update();
}
if (tags != null)
{
var tagsString = string.Join(",", tags.Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim()));
result += this.ExamTable.Where(e => e.ID == id).Set(e => e.Tags, tagsString).Update();
}
if (difficulty.HasValue)
{
result += this.ExamTable.Where(e => e.ID == id).Set(e => e.Difficulty, Math.Max(1, Math.Min(5, difficulty.Value))).Update();
}
if (isVisibleToUsers.HasValue)
{
result += this.ExamTable.Where(e => e.ID == id).Set(e => e.IsVisibleToUsers, isVisibleToUsers.Value).Update();
}
// 更新时间
this.ExamTable.Where(e => e.ID == id).Set(e => e.UpdatedTime, DateTime.Now).Update();
logger.Info($"实验已更新: {id},更新记录数: {result}");
return new(result);
}
catch (Exception ex)
{
logger.Error($"更新实验时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 添加资源
/// </summary>
/// <param name="userId">上传用户ID</param>
/// <param name="resourceType">资源类型</param>
/// <param name="resourcePurpose">资源用途template 或 user</param>
/// <param name="resourceName">资源名称</param>
/// <param name="data">资源二进制数据</param>
/// <param name="examId">所属实验ID可选</param>
/// <param name="mimeType">MIME类型可选将根据文件扩展名自动确定</param>
/// <returns>创建的资源</returns>
public Result<Resource> AddResource(Guid userId, string resourceType, string resourcePurpose, string resourceName, byte[] data, string? examId = null, string? mimeType = null)
{
try
{
// 验证用户是否存在
var user = this.UserTable.Where(u => u.ID == userId).FirstOrDefault();
if (user == null)
{
logger.Error($"用户不存在: {userId}");
return new(new Exception($"用户不存在: {userId}"));
}
// 如果指定了实验ID验证实验是否存在
if (!string.IsNullOrEmpty(examId))
{
var exam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault();
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类型根据文件扩展名自动确定
if (string.IsNullOrEmpty(mimeType))
{
var extension = Path.GetExtension(resourceName).ToLowerInvariant();
mimeType = GetMimeTypeFromExtension(extension, resourceName);
}
var resource = new Resource
{
UserID = userId,
ExamID = examId,
ResourceType = resourceType,
ResourcePurpose = resourcePurpose,
ResourceName = resourceName,
Data = data,
MimeType = mimeType,
UploadTime = DateTime.Now
};
var insertedId = this.InsertWithIdentity(resource);
resource.ID = Convert.ToInt32(insertedId);
logger.Info($"新资源已添加: {userId}/{resourceType}/{resourcePurpose}/{resourceName} ({data.Length} bytes)" +
(examId != null ? $" [实验: {examId}]" : "") + $" [ID: {resource.ID}]");
return new(resource);
}
catch (Exception ex)
{
logger.Error($"添加资源时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 获取资源信息列表返回ID和名称
/// <param name="resourceType">资源类型</param>
/// <param name="examId">实验ID可选</param>
/// <param name="resourcePurpose">资源用途(可选)</param>
/// <param name="userId">用户ID可选</param>
/// </summary>
/// <returns>资源信息列表</returns>
public Result<(int ID, string Name)[]> GetResourceList(string resourceType, string? examId = null, string? resourcePurpose = null, Guid? userId = null)
{
try
{
var query = this.ResourceTable.Where(r => 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 })
.ToArray();
var result = resources.Select(r => (r.ID, r.ResourceName)).ToArray();
logger.Info($"获取资源列表: {resourceType}" +
(examId != null ? $"/{examId}" : "") +
(resourcePurpose != null ? $"/{resourcePurpose}" : "") +
(userId != null ? $"/{userId}" : "") +
$",共 {result.Length} 个资源");
return new(result);
}
catch (Exception ex)
{
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);
}
}
/// <summary>
/// 根据资源ID获取资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>资源数据</returns>
public Result<Optional<Resource>> GetResourceById(int resourceId)
{
try
{
var resource = this.ResourceTable.Where(r => r.ID == resourceId).FirstOrDefault();
if (resource == null)
{
logger.Info($"未找到资源: {resourceId}");
return new(Optional<Resource>.None);
}
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
return new(resource);
}
catch (Exception ex)
{
logger.Error($"获取资源时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 删除资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>删除的记录数</returns>
public Result<int> DeleteResource(int resourceId)
{
try
{
var result = this.ResourceTable.Where(r => r.ID == resourceId).Delete();
logger.Info($"资源已删除: {resourceId},删除记录数: {result}");
return new(result);
}
catch (Exception ex)
{
logger.Error($"删除资源时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 根据文件扩展名获取MIME类型
/// </summary>
/// <param name="extension">文件扩展名</param>
/// <param name="fileName">文件名(可选,用于特殊文件判断)</param>
/// <returns>MIME类型</returns>
private string GetMimeTypeFromExtension(string extension, string fileName = "")
{
// 特殊文件名处理
if (!string.IsNullOrEmpty(fileName) && fileName.Equals("diagram.json", StringComparison.OrdinalIgnoreCase))
{
return "application/json";
}
return extension.ToLowerInvariant() switch
{
".png" => "image/png",
".jpg" or ".jpeg" => "image/jpeg",
".gif" => "image/gif",
".bmp" => "image/bmp",
".svg" => "image/svg+xml",
".sbit" => "application/octet-stream",
".bit" => "application/octet-stream",
".bin" => "application/octet-stream",
".json" => "application/json",
".zip" => "application/zip",
".md" => "text/markdown",
_ => "application/octet-stream"
};
}
/// <summary>
/// 获取所有实验信息
/// </summary>
/// <returns>所有实验的数组</returns>
public Exam[] GetAllExams()
{
var exams = this.ExamTable.OrderBy(e => e.ID).ToArray();
logger.Debug($"获取所有实验,共 {exams.Length} 个");
return exams;
}
/// <summary>
/// 根据实验ID获取实验信息
/// </summary>
/// <param name="examId">实验ID</param>
/// <returns>包含实验信息的结果,如果未找到则返回空</returns>
public Result<Optional<Exam>> GetExamByID(string examId)
{
var exams = this.ExamTable.Where(exam => exam.ID == examId).ToArray();
if (exams.Length > 1)
{
logger.Error($"数据库中存在多个相同ID的实验: {examId}");
return new(new Exception($"数据库中存在多个相同ID的实验: {examId}"));
}
if (exams.Length == 0)
{
logger.Info($"未找到ID对应的实验: {examId}");
return new(Optional<Exam>.None);
}
logger.Debug($"成功获取实验信息: {examId}");
return new(exams[0]);
}
/// <summary>
/// 根据文件扩展名获取比特流MIME类型
/// </summary>
/// <param name="extension">文件扩展名</param>
/// <returns>MIME类型</returns>
private string GetBitstreamMimeType(string extension)
{
return extension.ToLowerInvariant() switch
{
".bit" => "application/octet-stream",
".sbit" => "application/octet-stream",
".bin" => "application/octet-stream",
".mcs" => "application/octet-stream",
".hex" => "text/plain",
_ => "application/octet-stream"
};
}
} }

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

@@ -23,7 +23,7 @@ public static class MsgBus
/// <returns>无</returns> /// <returns>无</returns>
public async static void Init() public async static void Init()
{ {
if (!Arp.IsAdministrator()) if (!ArpClient.IsAdministrator())
{ {
logger.Error($"非管理员运行ARP无法更新请用管理员权限运行"); logger.Error($"非管理员运行ARP无法更新请用管理员权限运行");
// throw new Exception($"非管理员运行ARP无法更新请用管理员权限运行"); // throw new Exception($"非管理员运行ARP无法更新请用管理员权限运行");

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),

File diff suppressed because it is too large Load Diff

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

@@ -1,5 +1,5 @@
<template> <template>
<div class="fixed left-1/2 top-30 z-999 -translate-x-1/2"> <div class="fixed left-1/2 top-30 z-[9999] -translate-x-1/2">
<transition <transition
name="alert" name="alert"
enter-active-class="alert-enter-active" enter-active-class="alert-enter-active"

View File

@@ -119,6 +119,7 @@
componentManager.prepareComponentProps( componentManager.prepareComponentProps(
component.attrs || {}, component.attrs || {},
component.id, component.id,
props.examId,
) )
" "
@update:bindKey=" @update:bindKey="
@@ -175,9 +176,7 @@ import {
ref, ref,
reactive, reactive,
onMounted, onMounted,
onUnmounted,
computed, computed,
watch,
provide, provide,
} from "vue"; } from "vue";
import { useEventListener } from "@vueuse/core"; import { useEventListener } from "@vueuse/core";
@@ -188,7 +187,6 @@ import { useAlertStore } from "@/components/Alert";
// 导入 diagram 管理器 // 导入 diagram 管理器
import { import {
loadDiagramData, loadDiagramData,
saveDiagramData,
updatePartPosition, updatePartPosition,
updatePartAttribute, updatePartAttribute,
parseConnectionPin, parseConnectionPin,
@@ -217,6 +215,7 @@ const emit = defineEmits(["toggle-doc-panel", "open-components"]);
// 定义组件接受的属性 // 定义组件接受的属性
const props = defineProps<{ const props = defineProps<{
showDocPanel?: boolean; // 添加属性接收文档面板的显示状态 showDocPanel?: boolean; // 添加属性接收文档面板的显示状态
examId?: string; // 新增examId属性
}>(); }>();
// 获取componentManager实例 // 获取componentManager实例
@@ -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);
} }
// 更新组件属性 // 更新组件属性
@@ -977,7 +975,8 @@ function exportDiagram() {
onMounted(async () => { onMounted(async () => {
// 加载图表数据 // 加载图表数据
try { try {
diagramData.value = await loadDiagramData(); // 传入examId参数让diagramManager处理动态加载
diagramData.value = await loadDiagramData(props.examId);
// 预加载所有组件模块 // 预加载所有组件模块
const componentTypes = new Set<string>(); const componentTypes = new Set<string>();

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); // 移除自动保存功能
} }
/** /**
@@ -763,11 +762,15 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
function prepareComponentProps( function prepareComponentProps(
attrs: Record<string, any>, attrs: Record<string, any>,
componentId?: string, componentId?: string,
examId?: string,
): Record<string, any> { ): Record<string, any> {
const result: Record<string, any> = { ...attrs }; const result: Record<string, any> = { ...attrs };
if (componentId) { if (componentId) {
result.componentId = componentId; result.componentId = componentId;
} }
if (examId) {
result.examId = examId;
}
return result; return result;
} }

View File

@@ -26,6 +26,8 @@ export interface DiagramPart {
// 连接类型定义 - 使用元组类型表示四元素数组 // 连接类型定义 - 使用元组类型表示四元素数组
export type ConnectionArray = [string, string, number, string[]]; export type ConnectionArray = [string, string, number, string[]];
import { AuthManager } from '@/utils/AuthManager';
// 解析连接字符串为组件ID和引脚ID // 解析连接字符串为组件ID和引脚ID
export function parseConnectionPin(connectionPin: string): { componentId: string; pinId: string } { export function parseConnectionPin(connectionPin: string): { componentId: string; pinId: string } {
const [componentId, pinId] = connectionPin.split(':'); const [componentId, pinId] = connectionPin.split(':');
@@ -80,22 +82,62 @@ export interface WireItem {
showLabel: boolean; showLabel: boolean;
} }
// 从本地存储加载图表数据 // 从本地存储或动态API加载图表数据
export async function loadDiagramData(): Promise<DiagramData> { export async function loadDiagramData(examId?: string): Promise<DiagramData> {
try { try {
// 先尝试从本地存储加载 // 如果提供了examId优先从API加载实验的diagram
const savedData = localStorage.getItem('diagramData'); if (examId) {
if (savedData) { try {
return JSON.parse(savedData); const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 获取diagram类型的资源列表
const resources = await resourceClient.getResourceList(examId, 'canvas', 'template');
if (resources && resources.length > 0) {
// 获取第一个diagram资源
const diagramResource = resources[0];
// 使用动态API获取资源文件内容
const response = await resourceClient.getResourceById(diagramResource.id);
if (response && response.data) {
const text = await response.data.text();
const data = JSON.parse(text);
// 验证数据格式
const validation = validateDiagramData(data);
if (validation.isValid) {
console.log('成功从API加载实验diagram:', examId);
return data;
} else {
console.warn('API返回的diagram数据格式无效:', validation.errors);
}
}
} else {
console.log('未找到实验diagram资源使用默认加载方式');
}
} catch (error) {
console.warn('从API加载实验diagram失败使用默认加载方式:', error);
}
} }
// 如果本地存储没有,从文件加载 // 如果没有examId或API加载失败尝试从静态文件加载不再使用本地存储
// 从静态文件加载(作为备选方案)
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}`);
} }
const data = await response.json(); const data = await response.json();
return data;
// 验证静态文件数据
const validation = validateDiagramData(data);
if (validation.isValid) {
return data;
} else {
console.warn('静态diagram文件数据格式无效:', validation.errors);
throw new Error('所有diagram数据源都无效');
}
} catch (error) { } catch (error) {
console.error('Error loading diagram data:', error); console.error('Error loading diagram data:', error);
// 返回空的默认数据结构 // 返回空的默认数据结构
@@ -114,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

@@ -588,9 +588,9 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
}; };
const forceCapture = async () => { const forceCapture = async () => {
// 检查是否有其他操作正在进行 // 检查是否正在捕获
if (operationMutex.isLocked()) { if (!isCapturing.value) {
alert.warn("有其他操作正在进行中,请稍后再试", 3000); alert.warn("当前没有正在进行的捕获操作", 2000);
return; return;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,12 @@
工程界面 工程界面
</router-link> </router-link>
</li> </li>
<li class="my-1 hover:translate-x-1 transition-all duration-300">
<router-link to="/exam" class="text-base font-medium">
<FileText class="icon" />
实验列表
</router-link>
</li>
<li class="my-1 hover:translate-x-1 transition-all duration-300"> <li class="my-1 hover:translate-x-1 transition-all duration-300">
<router-link to="/test" class="text-base font-medium"> <router-link to="/test" class="text-base font-medium">
<FlaskConical class="icon" /> <FlaskConical class="icon" />

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

@@ -1,305 +1,325 @@
<template> <template>
<div <div
class="tutorial-carousel relative" class="tutorial-carousel relative"
@wheel.prevent="handleWheel" @wheel.prevent="handleWheel"
@mouseenter="pauseAutoRotation" @mouseenter="pauseAutoRotation"
@mouseleave="resumeAutoRotation" @mouseleave="resumeAutoRotation"
> <!-- 例程卡片堆叠 --> > <!-- 例程卡片堆叠 -->
<div class="card-stack relative mx-auto"> <div class="card-stack relative mx-auto">
<div <div
v-for="(tutorial, index) in tutorials" v-for="(tutorial, index) in tutorials"
:key="index" :key="index"
class="tutorial-card absolute transition-all duration-500 ease-in-out rounded-2xl shadow-2xl border-4 border-base-300 overflow-hidden" class="tutorial-card absolute transition-all duration-500 ease-in-out rounded-2xl shadow-2xl border-4 border-base-300 overflow-hidden"
:class="getCardClass(index)" :class="getCardClass(index)"
:style="getCardStyle(index)" :style="getCardStyle(index)"
@click="handleCardClick(index, tutorial.id)" @click="handleCardClick(index, tutorial.id)"
> >
<!-- 卡片内容 --> <!-- 卡片内容 -->
<div class="relative"> <div class="relative">
<!-- 图片 --> <img <!-- 图片 --> <img
:src="tutorial.thumbnail || `https://placehold.co/600x400?text=${tutorial.title}`" :src="tutorial.thumbnail || `https://placehold.co/600x400?text=${tutorial.title}`"
class="w-full object-contain" class="w-full object-contain"
:alt="tutorial.title" :alt="tutorial.title"
style="width: 600px; height: 400px;" style="width: 600px; height: 400px;"
/> />
<!-- 卡片蒙层 --> <!-- 卡片蒙层 -->
<div <div
class="absolute inset-0 bg-primary opacity-20 transition-opacity duration-300" class="absolute inset-0 bg-primary opacity-20 transition-opacity duration-300"
:class="{'opacity-10': index === currentIndex}" :class="{'opacity-10': index === currentIndex}"
></div> ></div>
<!-- 标题覆盖层 --> <!-- 标题覆盖层 -->
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-base-300 to-transparent"> <div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-base-300 to-transparent">
<h3 class="text-lg font-bold text-base-content">{{ tutorial.title }}</h3> <div class="flex flex-col gap-2">
<p class="text-sm opacity-80 truncate">{{ tutorial.description }}</p> <h3 class="text-lg font-bold text-base-content">{{ tutorial.title }}</h3>
</div> <p class="text-sm opacity-80 truncate">{{ tutorial.description }}</p>
</div> <!-- 标签显示 -->
</div> <div v-if="tutorial.tags && tutorial.tags.length > 0" class="flex flex-wrap gap-1">
</div> <span
v-for="tag in tutorial.tags.slice(0, 3)"
<!-- 导航指示器 --> :key="tag"
<div class="indicators flex justify-center gap-2 mt-4"> class="badge badge-outline badge-xs text-xs"
<button >
v-for="(_, index) in tutorials" {{ tag }}
:key="index" </span>
@click="setActiveCard(index)" </div>
class="w-3 h-3 rounded-full transition-all duration-300" </div>
:class="index === currentIndex ? 'bg-primary scale-125' : 'bg-base-300'" </div>
></button> </div>
</div> </div>
</div> </div>
</template>
<!-- 导航指示器 -->
<script setup lang="ts"> <div class="indicators flex justify-center gap-2 mt-4">
import { ref, onMounted, onUnmounted } from 'vue'; <button
import { useRouter } from 'vue-router'; v-for="(_, index) in tutorials"
:key="index"
// 接口定义 @click="setActiveCard(index)"
interface Tutorial { class="w-3 h-3 rounded-full transition-all duration-300"
id: string; :class="index === currentIndex ? 'bg-primary scale-125' : 'bg-base-300'"
title: string; ></button>
description: string; </div>
thumbnail?: string; </div>
docPath: string; </template>
}
<script setup lang="ts">
// Props import { ref, onMounted, onUnmounted } from 'vue';
const props = defineProps<{ import { useRouter } from 'vue-router';
autoRotationInterval?: number; import { AuthManager } from '@/utils/AuthManager';
}>(); import type { ExamSummary } from '@/APIClient';
// 配置默认值 // 接口定义
const autoRotationInterval = props.autoRotationInterval || 5000; // 默认5秒 interface Tutorial {
id: string;
// 状态管理 title: string;
const tutorials = ref<Tutorial[]>([]); description: string;
const currentIndex = ref(0); thumbnail?: string;
const router = useRouter(); tags: string[];
let autoRotationTimer: number | null = null; }
// 处理卡片点击 // Props
const handleCardClick = (index: number, tutorialId: string) => { const props = defineProps<{
if (index === currentIndex.value) { autoRotationInterval?: number;
goToTutorial(tutorialId); }>();
} else {
setActiveCard(index); // 配置默认值
} const autoRotationInterval = props.autoRotationInterval || 5000; // 默认5秒
};
// 状态管理
// 从 public/doc 目录加载例程信息 const tutorials = ref<Tutorial[]>([]);
onMounted(async () => { const currentIndex = ref(0);
try { const router = useRouter();
// 尝试从API获取教程目录 let autoRotationTimer: number | null = null;
let tutorialIds: string[] = [];
try { // 处理卡片点击
const response = await fetch('/api/tutorial'); const handleCardClick = (index: number, tutorialId: string) => {
if (response.ok) { if (index === currentIndex.value) {
const data = await response.json(); goToExam(tutorialId);
tutorialIds = data.tutorials || []; } else {
} setActiveCard(index);
} catch (error) { }
console.warn('无法从API获取教程目录使用默认值:', error); };
}
// 从数据库加载实验数据
// 如果API调用失败或返回空列表使用默认值 onMounted(async () => {
if (tutorialIds.length === 0) { try {
console.log('使用默认教程列表'); console.log('正在从数据库加载实验数据...');
tutorialIds = ['01', '02', '03', '04', '05', '06', '11', '12', '13']; // 默认例程
} else { // 创建认证客户端
console.log('使用API获取的教程列表:', tutorialIds); const client = AuthManager.createAuthenticatedExamClient();
}
// 获取实验列表
// 为每个例程创建对象并尝试获取文档标题 const examList: ExamSummary[] = await client.getExamList();
const tutorialPromises = tutorialIds.map(async (id) => {
// 尝试读取doc.md获取标题 // 筛选可见的实验并转换为Tutorial格式
let title = `例程 ${id}`; const visibleExams = examList
let description = "点击加载此例程"; .filter(exam => exam.isVisibleToUsers)
let thumbnail = `/doc/${id}/cover.png`; // 默认使用第一张图片作为缩略图 .slice(0, 6); // 限制轮播显示最多6个实验
try { if (visibleExams.length === 0) {
// 尝试读取文档内容获取标题 console.warn('没有找到可见的实验');
const response = await fetch(`/doc/${id}/doc.md`); return;
if (response.ok) { }
const text = await response.text();
// 从Markdown提取标题 // 转换数据格式并获取封面图片
const titleMatch = text.match(/^#\s+(.+)$/m); const tutorialPromises = visibleExams.map(async (exam) => {
if (titleMatch && titleMatch[1]) { let thumbnail: string | undefined;
title = titleMatch[1].trim();
} try {
// 获取实验的封面资源(模板资源)
// 提取第一段作为描述 const resourceClient = AuthManager.createAuthenticatedResourceClient();
const descMatch = text.match(/\n\n([^#\n][^\n]+)/); const resourceList = await resourceClient.getResourceList(exam.id, 'cover', 'template');
if (descMatch && descMatch[1]) { if (resourceList && resourceList.length > 0) {
description = descMatch[1].substring(0, 100).trim(); // 使用第一个封面资源
if (description.length === 100) description += '...'; const coverResource = resourceList[0];
} const fileResponse = await resourceClient.getResourceById(coverResource.id);
} // 创建Blob URL作为缩略图
} catch (error) { thumbnail = URL.createObjectURL(fileResponse.data);
console.warn(`无法读取例程${id}的文档内容:`, error); }
} } catch (error) {
console.warn(`无法获取实验${exam.id}的封面图片:`, error);
return { }
id,
title, return {
description, id: exam.id,
thumbnail, title: exam.name,
docPath: `/doc/${id}/doc.md` description: '点击查看实验详情',
}; thumbnail,
}); tags: exam.tags || []
};
tutorials.value = await Promise.all(tutorialPromises); });
// 启动自动旋转 tutorials.value = await Promise.all(tutorialPromises);
startAutoRotation();
} catch (error) { console.log('成功加载实验数据:', tutorials.value.length, '个实验');
console.error('加载例程失败:', error);
} // 启动自动旋转
}); startAutoRotation();
} catch (error) {
// 在组件销毁时清除计时器 console.error('加载实验数据失败:', error);
onUnmounted(() => {
if (autoRotationTimer) { // 如果加载失败,显示默认的占位内容
clearInterval(autoRotationTimer); tutorials.value = [{
} id: 'placeholder',
}); title: '实验数据加载中...',
description: '请稍后或刷新页面重试',
// 鼠标滚轮处理 thumbnail: undefined,
const handleWheel = (event: WheelEvent) => { tags: []
if (event.deltaY > 0) { }];
nextCard(); }
} else { });
prevCard();
} // 在组件销毁时清除计时器和Blob URLs
}; onUnmounted(() => {
if (autoRotationTimer) {
// 下一张卡片 clearInterval(autoRotationTimer);
const nextCard = () => { }
currentIndex.value = (currentIndex.value + 1) % tutorials.value.length;
}; // 清理创建的Blob URLs
tutorials.value.forEach(tutorial => {
// 上一张卡片 if (tutorial.thumbnail && tutorial.thumbnail.startsWith('blob:')) {
const prevCard = () => { URL.revokeObjectURL(tutorial.thumbnail);
currentIndex.value = (currentIndex.value - 1 + tutorials.value.length) % tutorials.value.length; }
}; });
});
// 设置活动卡片
const setActiveCard = (index: number) => { // 鼠标滚轮处理
currentIndex.value = index; const handleWheel = (event: WheelEvent) => {
}; if (event.deltaY > 0) {
nextCard();
// 自动旋转 } else {
const startAutoRotation = () => { prevCard();
autoRotationTimer = window.setInterval(() => { }
nextCard(); };
}, autoRotationInterval);
}; // 下一张卡片
const nextCard = () => {
// 暂停自动旋转 currentIndex.value = (currentIndex.value + 1) % tutorials.value.length;
const pauseAutoRotation = () => { };
if (autoRotationTimer) {
clearInterval(autoRotationTimer); // 上一张卡片
autoRotationTimer = null; const prevCard = () => {
} currentIndex.value = (currentIndex.value - 1 + tutorials.value.length) % tutorials.value.length;
}; };
// 恢复自动旋转 // 设置活动卡片
const resumeAutoRotation = () => { const setActiveCard = (index: number) => {
if (!autoRotationTimer) { currentIndex.value = index;
startAutoRotation(); };
}
}; // 自动旋转
const startAutoRotation = () => {
// 前往例程 autoRotationTimer = window.setInterval(() => {
const goToTutorial = (tutorialId: string) => { nextCard();
// 跳转到工程页面,并通过 query 参数传递文档路径 }, autoRotationInterval);
router.push({ };
path: '/project',
query: { tutorial: tutorialId } // 暂停自动旋转
}); const pauseAutoRotation = () => {
}; if (autoRotationTimer) {
clearInterval(autoRotationTimer);
// 计算卡片类和样式 autoRotationTimer = null;
const getCardClass = (index: number) => { }
const isActive = index === currentIndex.value; };
const isPrev = (index === currentIndex.value - 1) || (currentIndex.value === 0 && index === tutorials.value.length - 1);
const isNext = (index === currentIndex.value + 1) || (currentIndex.value === tutorials.value.length - 1 && index === 0); // 恢复自动旋转
const resumeAutoRotation = () => {
return { if (!autoRotationTimer) {
'z-30': isActive, startAutoRotation();
'z-20': isPrev || isNext, }
'z-10': !isActive && !isPrev && !isNext, };
'hover:scale-105': isActive,
'cursor-pointer': true // 前往实验
}; const goToExam = (examId: string) => {
}; // 跳转到实验列表页面并传递examId参数页面将自动打开对应的实验详情模态框
router.push({
const getCardStyle = (index: number) => { path: '/exam',
const isActive = index === currentIndex.value; query: { examId: examId }
const isPrev = (index === currentIndex.value - 1) || (currentIndex.value === 0 && index === tutorials.value.length - 1); });
const isNext = (index === currentIndex.value + 1) || (currentIndex.value === tutorials.value.length - 1 && index === 0); };
// 基本样式 // 计算卡片类和样式
let style = { const getCardClass = (index: number) => {
transform: 'scale(1) translateY(0) rotate(0deg)', const isActive = index === currentIndex.value;
opacity: '1', const isPrev = (index === currentIndex.value - 1) || (currentIndex.value === 0 && index === tutorials.value.length - 1);
filter: 'blur(0)' const isNext = (index === currentIndex.value + 1) || (currentIndex.value === tutorials.value.length - 1 && index === 0);
};
return {
// 活动卡片 'z-30': isActive,
if (isActive) { 'z-20': isPrev || isNext,
return style; 'z-10': !isActive && !isPrev && !isNext,
} 'hover:scale-105': isActive,
'cursor-pointer': true
// 上一张卡片 };
if (isPrev) { };
style.transform = 'scale(0.85) translateY(-10%) rotate(-5deg)';
style.opacity = '0.7'; const getCardStyle = (index: number) => {
style.filter = 'blur(1px)'; const isActive = index === currentIndex.value;
return style; const isPrev = (index === currentIndex.value - 1) || (currentIndex.value === 0 && index === tutorials.value.length - 1);
} const isNext = (index === currentIndex.value + 1) || (currentIndex.value === tutorials.value.length - 1 && index === 0);
// 下一张卡片 // 基本样式
if (isNext) { let style = {
style.transform = 'scale(0.85) translateY(10%) rotate(5deg)'; transform: 'scale(1) translateY(0) rotate(0deg)',
style.opacity = '0.7'; opacity: '1',
style.filter = 'blur(1px)'; filter: 'blur(0)'
return style; };
}
// 活动卡片
// 其他卡片 if (isActive) {
style.transform = 'scale(0.7) translateY(0) rotate(0deg)'; return style;
style.opacity = '0.4'; }
style.filter = 'blur(2px)';
return style; // 上一张卡片
} if (isPrev) {
</script> style.transform = 'scale(0.85) translateY(-10%) rotate(-5deg)';
style.opacity = '0.7';
<style scoped> style.filter = 'blur(1px)';
.tutorial-carousel { return style;
width: 100%; }
height: 500px;
perspective: 1000px; // 下一张卡片
display: flex; if (isNext) {
flex-direction: column; style.transform = 'scale(0.85) translateY(10%) rotate(5deg)';
align-items: center; style.opacity = '0.7';
} style.filter = 'blur(1px)';
return style;
.card-stack { }
width: 600px;
height: 440px; // 其他卡片
position: relative; style.transform = 'scale(0.7) translateY(0) rotate(0deg)';
transform-style: preserve-3d; style.opacity = '0.4';
} style.filter = 'blur(2px)';
return style;
.tutorial-card { }
width: 600px; </script>
height: 400px;
background-color: hsl(var(--b2)); <style scoped>
will-change: transform, opacity; .tutorial-carousel {
} width: 100%;
height: 500px;
.tutorial-card:hover { perspective: 1000px;
box-shadow: 0 0 15px rgba(var(--p), 0.5); display: flex;
} flex-direction: column;
</style> align-items: center;
}
.card-stack {
width: 600px;
height: 440px;
position: relative;
transform-style: preserve-3d;
}
.tutorial-card {
width: 600px;
height: 400px;
background-color: hsl(var(--b2));
will-change: transform, opacity;
}
.tutorial-card:hover {
box-shadow: 0 0 15px rgba(var(--p), 0.5);
}
</style>

View File

@@ -1,21 +1,64 @@
<template> <template>
<div class="flex flex-col bg-base-100 justify-center items-center"> <div class="flex flex-col bg-base-100 justify-center items-center gap-4">
<!-- Title --> <!-- Title -->
<h1 class="font-bold text-2xl">上传比特流文件</h1> <h1 class="font-bold text-2xl">比特流文件</h1>
<!-- 示例比特流下载区域 (仅在有examId时显示) -->
<div v-if="examId && availableBitstreams.length > 0" class="w-full">
<fieldset class="fieldset w-full">
<legend class="fieldset-legend text-sm">示例比特流文件</legend>
<div class="space-y-2">
<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>
<div class="flex gap-2">
<button
@click="downloadExampleBitstream(bitstream)"
class="btn btn-sm btn-secondary"
:disabled="isDownloading || isProgramming"
>
<div v-if="isDownloading">
<span class="loading loading-spinner loading-xs"></span>
下载中...
</div>
<div v-else>
下载示例
</div>
</button>
<button
@click="programExampleBitstream(bitstream)"
class="btn btn-sm btn-primary"
:disabled="isDownloading || isProgramming || !uploadEvent"
>
<div v-if="isProgramming">
<span class="loading loading-spinner loading-xs"></span>
烧录中...
</div>
<div v-else>
直接烧录
</div>
</button>
</div>
</div>
</div>
</fieldset>
</div>
<!-- 分割线 -->
<div v-if="examId && availableBitstreams.length > 0" class="divider"></div>
<!-- Input File --> <!-- Input File -->
<fieldset class="fieldset w-full"> <fieldset class="fieldset w-full">
<legend class="fieldset-legend text-sm">选择或拖拽上传文件</legend> <legend class="fieldset-legend text-sm">上传自定义比特流文件</legend>
<input type="file" ref="fileInput" class="file-input w-full" @change="handleFileChange" /> <input type="file" ref="fileInput" class="file-input w-full" @change="handleFileChange" />
<label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label> <label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label>
</fieldset> </fieldset>
<!-- Upload Button --> <!-- Upload Button -->
<div class="card-actions w-full"> <div class="card-actions w-full">
<button @click="handleClick" class="btn btn-primary grow" :disabled="isUploading"> <button @click="handleClick" class="btn btn-primary grow" :disabled="isUploading || isProgramming">
<div v-if="isUploading"> <div v-if="isUploading">
<span class="loading loading-spinner"></span> <span class="loading loading-spinner"></span>
下载... 上传...
</div> </div>
<div v-else> <div v-else>
{{ buttonText }} {{ buttonText }}
@@ -27,17 +70,20 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, useTemplateRef, onMounted } from "vue"; import { computed, ref, useTemplateRef, onMounted } from "vue";
import { AuthManager } from "@/utils/AuthManager"; // Adjust the path based on your project structure
import { useDialogStore } from "@/stores/dialog"; 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属性
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
maxMemory: 4, maxMemory: 4,
examId: '',
}); });
const emits = defineEmits<{ const emits = defineEmits<{
@@ -47,6 +93,10 @@ const emits = defineEmits<{
const dialog = useDialogStore(); const dialog = useDialogStore();
const isUploading = ref(false); const isUploading = ref(false);
const isDownloading = ref(false);
const isProgramming = ref(false);
const availableBitstreams = ref<{id: number, name: string}[]>([]);
const buttonText = computed(() => { const buttonText = computed(() => {
return isUndefined(props.downloadEvent) ? "上传" : "上传并下载"; return isUndefined(props.downloadEvent) ? "上传" : "上传并下载";
}); });
@@ -56,14 +106,97 @@ const bitstream = defineModel("bitstreamFile", {
type: File, type: File,
default: undefined, default: undefined,
}); });
onMounted(() => {
// 初始化时加载示例比特流
onMounted(async () => {
if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) { if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) {
let fileList = new DataTransfer(); let fileList = new DataTransfer();
fileList.items.add(bitstream.value); fileList.items.add(bitstream.value);
fileInput.value.files = fileList.files; fileInput.value.files = fileList.files;
} }
await loadAvailableBitstreams();
}); });
// 加载可用的比特流文件列表
async function loadAvailableBitstreams() {
console.log('加载可用比特流文件examId:', props.examId);
if (!props.examId) {
availableBitstreams.value = [];
return;
}
try {
const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 使用新的ResourceClient API获取比特流模板资源列表
const resources = await resourceClient.getResourceList(props.examId, 'bitstream', 'template');
availableBitstreams.value = resources.map(r => ({ id: r.id, name: r.name })) || [];
} catch (error) {
console.error('加载比特流列表失败:', error);
availableBitstreams.value = [];
}
}
// 下载示例比特流
async function downloadExampleBitstream(bitstream: {id: number, name: string}) {
if (isDownloading.value) return;
isDownloading.value = true;
try {
const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 使用新的ResourceClient API获取资源文件
const response = await resourceClient.getResourceById(bitstream.id);
if (response && response.data) {
// 创建下载链接
const url = URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = url;
link.download = response.fileName || bitstream.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
dialog.info("示例比特流下载成功");
} else {
dialog.error("下载失败:响应数据为空");
}
} catch (error) {
console.error('下载示例比特流失败:', error);
dialog.error("下载示例比特流失败");
} finally {
isDownloading.value = false;
}
}
// 直接烧录示例比特流
async function programExampleBitstream(bitstream: {id: number, name: string}) {
if (isProgramming.value) return;
isProgramming.value = true;
try {
const resourceClient = AuthManager.createAuthenticatedResourceClient();
if (props.downloadEvent) {
const downloadSuccess = await props.downloadEvent(bitstream.id);
if (downloadSuccess) {
dialog.info("示例比特流烧录成功");
} else {
dialog.error("烧录失败");
}
} else {
dialog.info("示例比特流props.downloadEvent未定义 无法烧录");
}
} catch (error) {
console.error('烧录示例比特流失败:', error);
dialog.error("烧录示例比特流失败");
} finally {
isProgramming.value = false;
}
}
function handleFileChange(event: Event): void { function handleFileChange(event: Event): void {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
const file = target.files?.[0]; // 获取选中的第一个文件 const file = target.files?.[0]; // 获取选中的第一个文件
@@ -85,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;
@@ -97,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);
@@ -118,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

@@ -27,7 +27,7 @@
to="#ComponentCapabilities" to="#ComponentCapabilities"
v-if="selectecComponentID === props.componentId" v-if="selectecComponentID === props.componentId"
> >
<MotherBoardCaps :jtagFreq="jtagFreq" @change-jtag-freq="changeJtagFreq" /> <MotherBoardCaps :jtagFreq="jtagFreq" :exam-id="examId" @change-jtag-freq="changeJtagFreq" />
</Teleport> </Teleport>
</template> </template>
@@ -41,6 +41,7 @@ import { toNumber } from "lodash";
export interface MotherBoardProps { export interface MotherBoardProps {
size: number; size: number;
componentId?: string; componentId?: string;
examId?: string; // 新增examId属性
} }
const emit = defineEmits<{ const emit = defineEmits<{

View File

@@ -8,20 +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"
@update:bitstream-file="handleBitstreamChange"> :upload-event="eqps.jtagUploadBitstream"
: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>
@@ -30,27 +45,38 @@
<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>
<div class="divider"></div> <div class="divider"></div>
<h1 class="font-bold text-center text-2xl">外设</h1> <h1 class="font-bold text-center text-2xl">外设</h1>
<div class="flex flex-row justify-around"> <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 class="flex flex-row">
<input type="checkbox" class="checkbox" :checked="eqps.enablePower" @change="handlePowerCheckboxChange" />
<p class="mx-2">启用电源</p>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -65,6 +91,7 @@ import { RefreshCcwIcon } from "lucide-vue-next";
interface CapsProps { interface CapsProps {
jtagFreq?: string; jtagFreq?: string;
examId?: string; // 新增examId属性
} }
const emits = defineEmits<{ const emits = defineEmits<{
@@ -101,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);
@@ -120,18 +152,8 @@ async function handleMatrixkeyCheckboxChange(event: Event) {
} }
} }
async function handlePowerCheckboxChange(event: Event) {
const target = event.target as HTMLInputElement;
const ret = await eqps.powerSetOnOff(target.checked);
if (target.checked) {
eqps.enablePower = ret;
} else {
eqps.enablePower = !ret;
}
}
async function toggleJtagBoundaryScan() { async function toggleJtagBoundaryScan() {
eqps.enableJtagBoundaryScan = !eqps.enableJtagBoundaryScan; eqps.jtagBoundaryScanSetOnOff(!eqps.enableJtagBoundaryScan);
} }
const isGettingIDCode = ref(false); const isGettingIDCode = ref(false);

View File

@@ -4,6 +4,7 @@ import AuthView from "../views/AuthView.vue";
import ProjectView from "../views/Project/Index.vue"; import ProjectView from "../views/Project/Index.vue";
import TestView from "../views/TestView.vue"; import TestView from "../views/TestView.vue";
import UserView from "@/views/User/Index.vue"; import UserView from "@/views/User/Index.vue";
import ExamView from "@/views/ExamView.vue";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@@ -13,6 +14,7 @@ const router = createRouter({
{ path: "/project", name: "project", component: ProjectView }, { path: "/project", name: "project", component: ProjectView },
{ path: "/test", name: "test", component: TestView }, { path: "/test", name: "test", component: TestView },
{ path: "/user", name: "user", component: UserView }, { path: "/user", name: "user", component: UserView },
{ path: "/exam", name: "exam", component: ExamView },
], ],
}); });

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,64 +99,76 @@ 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...");
const jtagClient = AuthManager.createAuthenticatedJtagClient(); return;
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 {
const jtagClient = AuthManager.createAuthenticatedJtagClient(); // 自动开启电源
const resp = await jtagClient.uploadBitstream( await powerSetOnOff(true);
boardAddr.value,
const resourceClient = AuthManager.createAuthenticatedResourceClient();
const resp = await resourceClient.addResource(
'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 {
// 自动开启电源
await powerSetOnOff(true);
const jtagClient = AuthManager.createAuthenticatedJtagClient(); const jtagClient = AuthManager.createAuthenticatedJtagClient();
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 {
@@ -173,6 +179,9 @@ export const useEquipments = defineStore("equipments", () => {
async function jtagGetIDCode(isQuiet: boolean = false): Promise<number> { async function jtagGetIDCode(isQuiet: boolean = false): Promise<number> {
const release = await jtagClientMutex.acquire(); const release = await jtagClientMutex.acquire();
try { try {
// 自动开启电源
await powerSetOnOff(true);
const jtagClient = AuthManager.createAuthenticatedJtagClient(); const jtagClient = AuthManager.createAuthenticatedJtagClient();
const resp = await jtagClient.getDeviceIDCode( const resp = await jtagClient.getDeviceIDCode(
boardAddr.value, boardAddr.value,
@@ -190,6 +199,9 @@ export const useEquipments = defineStore("equipments", () => {
async function jtagSetSpeed(speed: number): Promise<boolean> { async function jtagSetSpeed(speed: number): Promise<boolean> {
const release = await jtagClientMutex.acquire(); const release = await jtagClientMutex.acquire();
try { try {
// 自动开启电源
await powerSetOnOff(true);
const jtagClient = AuthManager.createAuthenticatedJtagClient(); const jtagClient = AuthManager.createAuthenticatedJtagClient();
const resp = await jtagClient.setSpeed( const resp = await jtagClient.setSpeed(
boardAddr.value, boardAddr.value,
@@ -209,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,
@@ -228,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,
@@ -236,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,
@@ -275,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

@@ -13,8 +13,13 @@ import {
NetConfigClient, NetConfigClient,
OscilloscopeApiClient, OscilloscopeApiClient,
DebuggerClient, DebuggerClient,
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 =
@@ -31,7 +36,9 @@ type SupportedClient =
| UDPClient | UDPClient
| NetConfigClient | NetConfigClient
| OscilloscopeApiClient | OscilloscopeApiClient
| DebuggerClient; | DebuggerClient
| ExamClient
| ResourceClient;
export class AuthManager { export class AuthManager {
// 存储token到localStorage // 存储token到localStorage
@@ -115,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;
@@ -181,15 +188,38 @@ export class AuthManager {
public static createAuthenticatedNetConfigClient(): NetConfigClient { public static createAuthenticatedNetConfigClient(): NetConfigClient {
return AuthManager.createAuthenticatedClient(NetConfigClient); return AuthManager.createAuthenticatedClient(NetConfigClient);
} }
public static createAuthenticatedOscilloscopeApiClient(): OscilloscopeApiClient { public static createAuthenticatedOscilloscopeApiClient(): OscilloscopeApiClient {
return AuthManager.createAuthenticatedClient(OscilloscopeApiClient); return AuthManager.createAuthenticatedClient(OscilloscopeApiClient);
} }
public static createAuthenticatedDebuggerClient(): DebuggerClient { public static createAuthenticatedDebuggerClient(): DebuggerClient {
return AuthManager.createAuthenticatedClient(DebuggerClient); return AuthManager.createAuthenticatedClient(DebuggerClient);
} }
public static createAuthenticatedExamClient(): 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,

1069
src/views/ExamView.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -25,26 +25,37 @@
</h1> </h1>
<p class="py-6 text-lg opacity-80 leading-relaxed"> <p class="py-6 text-lg opacity-80 leading-relaxed">
Prototype and simulate electronic circuits in your browser with our 在浏览器中进行FPGA原型设计和电路仿真使用现代直观的界面创建测试和分享您的FPGA设计体验从基础学习到高级项目的完整开发流程
modern, intuitive interface. Create, test, and share your FPGA
designs seamlessly.
</p> </p>
<div class="flex flex-wrap gap-4 actions-container"> <div class="flex flex-col sm:flex-row gap-4 actions-container">
<router-link <router-link
to="/project" to="/project"
class="btn btn-primary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1" class="btn btn-primary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1 flex-1 sm:flex-none"
> >
<BookOpen class="h-5 w-5 mr-2" /> <BookOpen class="h-5 w-5 mr-2" />
进入工程界面 进入工程界面
</router-link> </router-link>
<router-link
to="/exam"
class="btn btn-secondary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1 flex-1 sm:flex-none"
>
<GraduationCap class="h-5 w-5 mr-2" />
实验列表
</router-link>
</div> </div>
<div <div
class="mt-8 p-4 bg-base-300 rounded-lg shadow-inner opacity-80 transition-all duration-300 hover:opacity-100 hover:shadow-md" class="mt-8 p-4 bg-base-300 rounded-lg shadow-inner opacity-80 transition-all duration-300 hover:opacity-100 hover:shadow-md"
> >
<p class="text-sm"> <div class="space-y-2">
<span class="font-semibold text-primary">提示</span> <p class="text-sm">
您可以在工程界面中创建编辑和测试您的FPGA项目使用我们简洁直观的界面轻松进行硬件设计 <span class="font-semibold text-primary">工程界面</span>
</p> 自由创建和编辑FPGA项目使用可视化画布进行电路设计和仿真测试
</p>
<p class="text-sm">
<span class="font-semibold text-secondary">实验列表</span>
浏览结构化的学习实验从基础概念到高级应用的系统性学习路径
</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -55,7 +66,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import "@/router"; import "@/router";
import TutorialCarousel from "@/components/TutorialCarousel.vue"; import TutorialCarousel from "@/components/TutorialCarousel.vue";
import { BookOpen } from "lucide-vue-next"; import { BookOpen, GraduationCap } from "lucide-vue-next";
</script> </script>
<style scoped lang="postcss"> <style scoped lang="postcss">

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

@@ -479,10 +479,10 @@ async function startCapture() {
const arr = []; const arr = [];
for (let i = 0; i < bin.length; i += 4) { for (let i = 0; i < bin.length; i += 4) {
arr.push( arr.push(
(bin.charCodeAt(i) << 24) | bin.charCodeAt(i) |
(bin.charCodeAt(i + 1) << 16) | (bin.charCodeAt(i + 1) << 8) |
(bin.charCodeAt(i + 2) << 8) | (bin.charCodeAt(i + 2) << 16) |
bin.charCodeAt(i + 3), (bin.charCodeAt(i + 3) << 24),
); );
} }
// 截取采样深度 // 截取采样深度

View File

@@ -29,6 +29,7 @@
<DiagramCanvas <DiagramCanvas
ref="diagramCanvas" ref="diagramCanvas"
:showDocPanel="showDocPanel" :showDocPanel="showDocPanel"
:exam-id="(route.query.examId as string) || ''"
@open-components="openComponentsMenu" @open-components="openComponentsMenu"
@toggle-doc-panel="toggleDocPanel" @toggle-doc-panel="toggleDocPanel"
/> />
@@ -36,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">
<!-- 使用条件渲染显示不同的面板 --> <!-- 使用条件渲染显示不同的面板 -->
@@ -59,7 +60,10 @@
v-show="showDocPanel" v-show="showDocPanel"
class="doc-panel overflow-y-auto h-full" class="doc-panel overflow-y-auto h-full"
> >
<MarkdownRenderer :content="documentContent" /> <MarkdownRenderer
:content="documentContent"
:examId="(route.query.examId as string) || ''"
/>
</div> </div>
</div> </div>
</SplitterPanel> </SplitterPanel>
@@ -70,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"
/> />
<!-- 功能底栏 --> <!-- 功能底栏 -->
@@ -100,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";
@@ -115,7 +140,6 @@ import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
import BottomBar from "@/views/Project/BottomBar.vue"; import BottomBar from "@/views/Project/BottomBar.vue";
import RequestBoardDialog from "@/views/Project/RequestBoardDialog.vue"; import RequestBoardDialog from "@/views/Project/RequestBoardDialog.vue";
import { useProvideComponentManager } from "@/components/LabCanvas"; import { useProvideComponentManager } from "@/components/LabCanvas";
import type { DiagramData } from "@/components/LabCanvas";
import { useAlertStore } from "@/components/Alert"; import { useAlertStore } from "@/components/Alert";
import { AuthManager } from "@/utils/AuthManager"; import { AuthManager } from "@/utils/AuthManager";
import { useEquipments } from "@/stores/equipments"; import { useEquipments } from "@/stores/equipments";
@@ -133,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);
@@ -182,29 +212,37 @@ async function toggleDocPanel() {
// 加载文档内容 // 加载文档内容
async function loadDocumentContent() { async function loadDocumentContent() {
try { try {
// 从路由参数中获取教程ID // 检查是否有实验ID参数
const tutorialId = (route.query.tutorial as string) || "02"; // 默认加载02例程 const examId = route.query.examId as string;
if (examId) {
// 构建文档路径 // 如果有实验ID从API加载实验文档
let docPath = `/doc/${tutorialId}/doc.md`; console.log('加载实验文档:', examId);
const client = AuthManager.createAuthenticatedResourceClient();
// 检查当前路径是否包含下划线(例如 02_key 格式)
// 如果不包含,那么使用更新的命名格式 // 获取markdown类型的模板资源列表
if (!tutorialId.includes("_")) { const resources = await client.getResourceList(examId, 'doc', 'template');
docPath = `/doc/${tutorialId}/doc.md`;
if (resources && resources.length > 0) {
// 获取第一个markdown资源
const markdownResource = resources[0];
// 使用新的ResourceClient API获取资源文件内容
const response = await client.getResourceById(markdownResource.id);
if (!response || !response.data) {
throw new Error('获取markdown文件失败');
}
const content = await response.data.text();
// 更新文档内容暂时不处理图片路径由MarkdownRenderer处理
documentContent.value = content;
} else {
documentContent.value = "# 暂无实验文档\n\n该实验尚未提供文档内容。";
}
} else {
documentContent.value = "# 无文档";
} }
// 获取文档内容
const response = await fetch(docPath);
if (!response.ok) {
throw new Error(`Failed to load document: ${response.status}`);
}
// 更新文档内容,并替换图片路径
documentContent.value = (await response.text()).replace(
/.\/images/gi,
`/doc/${tutorialId}/images`,
);
} catch (error) { } catch (error) {
console.error("加载文档失败:", error); console.error("加载文档失败:", error);
documentContent.value = "# 文档加载失败\n\n无法加载请求的文档。"; documentContent.value = "# 文档加载失败\n\n无法加载请求的文档。";
@@ -268,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,
@@ -312,8 +350,8 @@ onMounted(async () => {
// 检查并初始化用户实验板 // 检查并初始化用户实验板
await checkAndInitializeBoard(); await checkAndInitializeBoard();
// 检查是否有例程参数,如果有则自动打开文档面板 // 检查是否有例程参数或实验ID参数,如果有则自动打开文档面板
if (route.query.tutorial) { if (route.query.tutorial || route.query.examId) {
showDocPanel.value = true; showDocPanel.value = true;
await loadDocumentContent(); await loadDocumentContent();
} }
@@ -344,7 +382,7 @@ onMounted(async () => {
} }
} }
/* 确保滚动行为仅在需要时出现 */ /* 确保整个页面禁止滚动 */
html, html,
body { body {
overflow: hidden; overflow: hidden;
@@ -376,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,
} },
}) });