Compare commits
34 Commits
ea7c2d425a
...
refactor
| Author | SHA1 | Date | |
|---|---|---|---|
| de28471f87 | |||
| 3a292c0a98 | |||
| 91b00a977c | |||
| c5ce246caf | |||
| c3bd61ed51 | |||
| 14d8499f77 | |||
| d18cf82813 | |||
| f1e2dbd9d8 | |||
| 262c5e4003 | |||
| fbd13f8f2f | |||
| 6cf7ef02ac | |||
| 8207c37e12 | |||
| db71681bdf | |||
| 2270022bbe | |||
| dcadb97a7f | |||
| 1538bb9d07 | |||
| f340c86a41 | |||
| b6fb7e05fa | |||
| e0db12e0eb | |||
| dc64a65702 | |||
| 46621fdb40 | |||
| 970a537391 | |||
| 3883cd8304 | |||
| 2aa2f1dc37 | |||
| 1bdcb672ab | |||
| 7b1d1a5e87 | |||
| 5bb011e685 | |||
| e3826f0ff6 | |||
| f8163be98b | |||
| e4791b41a8 | |||
| bea1c7e5ae | |||
| b68d8eaf11 | |||
| 390ce8250d | |||
| 07eb606324 |
@@ -24,6 +24,9 @@ update:
|
||||
gen-api:
|
||||
npm run gen-api
|
||||
|
||||
gen-api-from-server:
|
||||
npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts
|
||||
|
||||
# 构建服务器,包含win与linux平台
|
||||
[working-directory: "server"]
|
||||
build-server self-contained=isSelfContained: _show-dir
|
||||
|
||||
56
components.d.ts
vendored
Normal file
56
components.d.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
BaseBoard: typeof import('./src/components/equipments/BaseBoard.vue')['default']
|
||||
Canvas: typeof import('./src/components/Canvas.vue')['default']
|
||||
CollapsibleSection: typeof import('./src/components/CollapsibleSection.vue')['default']
|
||||
ComponentSelector: typeof import('./src/components/LabCanvas/ComponentSelector.vue')['default']
|
||||
DDR: typeof import('./src/components/equipments/DDR.vue')['default']
|
||||
DDS: typeof import('./src/components/equipments/DDS.vue')['default']
|
||||
DDSPropertyEditor: typeof import('./src/components/equipments/DDSPropertyEditor.vue')['default']
|
||||
DiagramCanvas: typeof import('./src/components/LabCanvas/DiagramCanvas.vue')['default']
|
||||
Dialog: typeof import('./src/components/Dialog.vue')['default']
|
||||
ETH: typeof import('./src/components/equipments/ETH.vue')['default']
|
||||
HDMI: typeof import('./src/components/equipments/HDMI.vue')['default']
|
||||
LabCanvas: typeof import('./src/components/LabCanvasNew/LabCanvas.vue')['default']
|
||||
LabCanvasNew: typeof import('./src/components/LabCanvas/LabCanvasNew.vue')['default']
|
||||
LabComponentsDrawer: typeof import('./src/components/LabCanvasNew/LabComponentsDrawer.vue')['default']
|
||||
LabComponentsDrawerNew: typeof import('./src/components/LabCanvas/LabComponentsDrawerNew.vue')['default']
|
||||
LoginCard: typeof import('./src/components/LoginCard.vue')['default']
|
||||
MarkdownRenderer: typeof import('./src/components/MarkdownRenderer.vue')['default']
|
||||
MechanicalButton: typeof import('./src/components/equipments/MechanicalButton.vue')['default']
|
||||
MotherBoard: typeof import('./src/components/equipments/MotherBoard.vue')['default']
|
||||
MotherBoardCaps: typeof import('./src/components/equipments/MotherBoardCaps.vue')['default']
|
||||
Navbar: typeof import('./src/components/Navbar.vue')['default']
|
||||
PG2L100H_FBG676: typeof import('./src/components/equipments/PG2L100H_FBG676.vue')['default']
|
||||
Pin: typeof import('./src/components/equipments/Pin.vue')['default']
|
||||
PopButton: typeof import('./src/components/PopButton.vue')['default']
|
||||
PropertyEditor: typeof import('./src/components/PropertyEditor.vue')['default']
|
||||
PropertyPanel: typeof import('./src/components/PropertyPanel.vue')['default']
|
||||
RekaSplitterGroup: typeof import('reka-ui')['SplitterGroup']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SD: typeof import('./src/components/equipments/SD.vue')['default']
|
||||
SevenSegmentDisplay: typeof import('./src/components/equipments/SevenSegmentDisplay.vue')['default']
|
||||
SFP: typeof import('./src/components/equipments/SFP.vue')['default']
|
||||
Sidebar: typeof import('./src/components/Sidebar.vue')['default']
|
||||
SMA: typeof import('./src/components/equipments/SMA.vue')['default']
|
||||
SMT_LED: typeof import('./src/components/equipments/SMT_LED.vue')['default']
|
||||
SplitterGroup: typeof import('reka-ui')['SplitterGroup']
|
||||
SplitterPanel: typeof import('reka-ui')['SplitterPanel']
|
||||
SplitterResizeHandle: typeof import('reka-ui')['SplitterResizeHandle']
|
||||
Switch: typeof import('./src/components/equipments/Switch.vue')['default']
|
||||
ThemeControlButton: typeof import('./src/components/ThemeControlButton.vue')['default']
|
||||
ThemeControlToggle: typeof import('./src/components/ThemeControlToggle.vue')['default']
|
||||
TutorialCarousel: typeof import('./src/components/TutorialCarousel.vue')['default']
|
||||
UploadCard: typeof import('./src/components/UploadCard.vue')['default']
|
||||
Wire: typeof import('./src/components/equipments/Wire.vue')['default']
|
||||
}
|
||||
}
|
||||
10
flake.lock
generated
10
flake.lock
generated
@@ -2,12 +2,12 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1741246872,
|
||||
"narHash": "sha256-Q6pMP4a9ed636qilcYX8XUguvKl/0/LGXhHcRI91p0U=",
|
||||
"rev": "10069ef4cf863633f57238f179a0297de84bd8d3",
|
||||
"revCount": 763342,
|
||||
"lastModified": 1748929857,
|
||||
"narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=",
|
||||
"rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4",
|
||||
"revCount": 810143,
|
||||
"type": "tarball",
|
||||
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.763342%2Brev-10069ef4cf863633f57238f179a0297de84bd8d3/01956ed4-f66c-7a87-98e4-b7e58f4aa591/source.tar.gz"
|
||||
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.810143%2Brev-c2a03962b8e24e669fb37b7df10e7c79531ff1a4/01973914-8b42-7168-9ee2-4d6ea6946695/source.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
|
||||
673
package-lock.json
generated
673
package-lock.json
generated
@@ -10,18 +10,21 @@
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "^3.2.4",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lodash": "^4.17.21",
|
||||
"log-symbols": "^7.0.0",
|
||||
"lucide-vue-next": "^0.525.0",
|
||||
"marked": "^12.0.0",
|
||||
"mathjs": "^14.4.0",
|
||||
"pinia": "^3.0.1",
|
||||
"tinypool": "^1.0.2",
|
||||
"reka-ui": "^2.3.1",
|
||||
"ts-log": "^2.2.7",
|
||||
"ts-results-es": "^5.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "4",
|
||||
"yocto-queue": "^1.2.1",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -38,6 +41,7 @@
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.0.12",
|
||||
"typescript": "~5.7.3",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-vue-devtools": "^7.7.2",
|
||||
"vue-tsc": "^2.2.2"
|
||||
@@ -959,6 +963,86 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz",
|
||||
"integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz",
|
||||
"integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.2",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@floating-ui/vue": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.7.tgz",
|
||||
"integrity": "sha512-idmAtbAIigGXN2SI5gItiXYBYtNfDTP9yIiObxgu13dgtG7ARCHlNfnR29GxP4LI4o13oiwsJ8wVgghj1lNqcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.2",
|
||||
"@floating-ui/utils": "^0.2.10",
|
||||
"vue-demi": ">=0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/vue/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@internationalized/date": {
|
||||
"version": "3.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.8.2.tgz",
|
||||
"integrity": "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@internationalized/number": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.3.tgz",
|
||||
"integrity": "sha512-p+Zh1sb6EfrfVaS86jlHGQ9HA66fJhV9x5LiE5vCbZtXEHAuhcmUZUdZ4WrFpUBfNalr2OkAJI5AcKEQF+Lebw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
@@ -1351,6 +1435,15 @@
|
||||
"url": "https://github.com/sponsors/Fuzzyma"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.4.tgz",
|
||||
@@ -1619,6 +1712,32 @@
|
||||
"tailwindcss": "4.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
||||
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/vue-virtual": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.12.tgz",
|
||||
"integrity": "sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.13.12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^2.7.0 || ^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node22": {
|
||||
"version": "22.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.1.tgz",
|
||||
@@ -1649,6 +1768,12 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz",
|
||||
@@ -1989,6 +2114,69 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "13.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.5.0.tgz",
|
||||
"integrity": "sha512-wV7z0eUpifKmvmN78UBZX8T7lMW53Nrk6JP5+6hbzrB9+cJ3jr//hUlhl9TZO/03bUkMK6gGkQpqOPWoabr72g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "13.5.0",
|
||||
"@vueuse/shared": "13.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core/node_modules/@vueuse/shared": {
|
||||
"version": "13.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.5.0.tgz",
|
||||
"integrity": "sha512-K7GrQIxJ/ANtucxIXbQlUHdB0TPA8c+q5i+zbrjxuhJCnJ9GtBg75sBSnvmLSxHKPg2Yo8w62PWksl9kwH0Q8g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "13.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.5.0.tgz",
|
||||
"integrity": "sha512-euhItU3b0SqXxSy8u1XHxUCdQ8M++bsRs+TYhOLDU/OykS7KvJnyIFfep0XM5WjIFry9uAPlVSjmVHiqeshmkw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "12.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz",
|
||||
"integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/alien-signals": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
|
||||
@@ -2009,6 +2197,45 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/async-mutex": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
|
||||
@@ -2063,6 +2290,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/birpc": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz",
|
||||
@@ -2073,15 +2313,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
||||
@@ -2152,6 +2405,31 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/complex.js": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz",
|
||||
@@ -2165,6 +2443,13 @@
|
||||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/confbox": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
|
||||
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -2249,9 +2534,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2315,6 +2600,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/defu": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||
@@ -2458,6 +2749,13 @@
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/exsolve": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
|
||||
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||
@@ -2489,6 +2787,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||
@@ -2560,6 +2871,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
@@ -2612,6 +2936,19 @@
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
|
||||
@@ -2628,6 +2965,29 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-inside-container": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
||||
@@ -2647,6 +3007,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-obj": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
||||
@@ -3041,6 +3411,24 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/local-pkg": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz",
|
||||
"integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mlly": "^1.7.4",
|
||||
"pkg-types": "^2.0.1",
|
||||
"quansync": "^0.2.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
@@ -3073,6 +3461,15 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-vue-next": {
|
||||
"version": "0.525.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.525.0.tgz",
|
||||
"integrity": "sha512-Xf8+x8B2DrnGDV/rxylS+KBp2FIe6ljwDn2JsGTZZvXIfhmm/q+nv8RuGO1OyoMjOVkkz7CqtUqJfwtFPRbB2w==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
@@ -3161,6 +3558,38 @@
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mlly": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
|
||||
"integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.0",
|
||||
"pathe": "^2.0.1",
|
||||
"pkg-types": "^1.3.0",
|
||||
"ufo": "^1.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mlly/node_modules/confbox": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
|
||||
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mlly/node_modules/pkg-types": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
|
||||
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.1.8",
|
||||
"mlly": "^1.7.4",
|
||||
"pathe": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mrmime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||
@@ -3210,6 +3639,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-range": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
|
||||
@@ -3300,6 +3739,12 @@
|
||||
"npm": ">=3.10.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ohash": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
|
||||
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/open": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-10.1.1.tgz",
|
||||
@@ -3415,6 +3860,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.1.tgz",
|
||||
"integrity": "sha512-eY0QFb6eSwc9+0d/5D2lFFUq+A3n3QNGSy/X2Nvp+6MfzGw2u6EbA7S80actgjY1lkvvI0pqB+a4hioMh443Ew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.2.2",
|
||||
"exsolve": "^1.0.7",
|
||||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||
@@ -3466,6 +3923,23 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/quansync": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
|
||||
"integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/sxzz"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/read-package-json-fast": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz",
|
||||
@@ -3480,6 +3954,77 @@
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/reka-ui": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.3.1.tgz",
|
||||
"integrity": "sha512-2SjGeybd7jvD8EQUkzjgg7GdOQdf4cTwdVMq/lDNTMqneUFNnryGO43dg8WaM/jaG9QpSCZBvstfBFWlDdb2Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@floating-ui/vue": "^1.1.6",
|
||||
"@internationalized/date": "^3.5.0",
|
||||
"@internationalized/number": "^3.5.0",
|
||||
"@tanstack/vue-virtual": "^3.12.0",
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"@vueuse/shared": "^12.5.0",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"defu": "^6.1.4",
|
||||
"ohash": "^2.0.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": ">= 3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reka-ui/node_modules/@vueuse/core": {
|
||||
"version": "12.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz",
|
||||
"integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "12.8.2",
|
||||
"@vueuse/shared": "12.8.2",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/reka-ui/node_modules/@vueuse/metadata": {
|
||||
"version": "12.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz",
|
||||
"integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/rfdc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||
@@ -3686,9 +4231,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
||||
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3702,13 +4247,17 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinypool": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
|
||||
"integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==",
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
@@ -3762,6 +4311,13 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/ufo": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
|
||||
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
@@ -3792,6 +4348,74 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.5.tgz",
|
||||
"integrity": "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.1",
|
||||
"picomatch": "^4.0.2",
|
||||
"webpack-virtual-modules": "^0.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin-utils": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.2.4.tgz",
|
||||
"integrity": "sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pathe": "^2.0.2",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sxzz"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin-vue-components": {
|
||||
"version": "28.8.0",
|
||||
"resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-28.8.0.tgz",
|
||||
"integrity": "sha512-2Q6ZongpoQzuXDK0ZsVzMoshH0MWZQ1pzVL538G7oIDKRTVzHjppBDS8aB99SADGHN3lpGU7frraCG6yWNoL5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^3.6.0",
|
||||
"debug": "^4.4.1",
|
||||
"local-pkg": "^1.1.1",
|
||||
"magic-string": "^0.30.17",
|
||||
"mlly": "^1.7.4",
|
||||
"tinyglobby": "^0.2.14",
|
||||
"unplugin": "^2.3.5",
|
||||
"unplugin-utils": "^0.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/parser": "^7.15.8",
|
||||
"@nuxt/kit": "^3.2.2 || ^4.0.0",
|
||||
"vue": "2 || 3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@babel/parser": {
|
||||
"optional": true
|
||||
},
|
||||
"@nuxt/kit": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
@@ -4052,6 +4676,13 @@
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-virtual-modules": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
|
||||
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
|
||||
@@ -4075,6 +4706,18 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz",
|
||||
"integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yoctocolors": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz",
|
||||
|
||||
@@ -16,18 +16,21 @@
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "^3.2.4",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lodash": "^4.17.21",
|
||||
"log-symbols": "^7.0.0",
|
||||
"lucide-vue-next": "^0.525.0",
|
||||
"marked": "^12.0.0",
|
||||
"mathjs": "^14.4.0",
|
||||
"pinia": "^3.0.1",
|
||||
"tinypool": "^1.0.2",
|
||||
"reka-ui": "^2.3.1",
|
||||
"ts-log": "^2.2.7",
|
||||
"ts-results-es": "^5.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "4",
|
||||
"yocto-queue": "^1.2.1",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -44,6 +47,7 @@
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.0.12",
|
||||
"typescript": "~5.7.3",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-vue-devtools": "^7.7.2",
|
||||
"vue-tsc": "^2.2.2"
|
||||
|
||||
314
public/EquipmentTemplates/MatrixKey.json
Normal file
314
public/EquipmentTemplates/MatrixKey.json
Normal file
@@ -0,0 +1,314 @@
|
||||
{
|
||||
"version": 1,
|
||||
"author": "template",
|
||||
"editor": "system",
|
||||
"parts": [
|
||||
{
|
||||
"id": "board",
|
||||
"type": "BaseBoard",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"attrs": {
|
||||
"size": 1.2,
|
||||
"width": 400,
|
||||
"height": 400,
|
||||
"roundCorner": 20
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": false,
|
||||
"isOn": true,
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"id": "key_0_0",
|
||||
"type": "MechanicalButton",
|
||||
"x": 50,
|
||||
"y": 50,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "1",
|
||||
"bindMatrixKey": "0",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"id": "key_0_1",
|
||||
"type": "MechanicalButton",
|
||||
"x": 150,
|
||||
"y": 50,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "2",
|
||||
"bindMatrixKey": "1",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 1
|
||||
},
|
||||
{
|
||||
"id": "key_0_2",
|
||||
"type": "MechanicalButton",
|
||||
"x": 250,
|
||||
"y": 50,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "3",
|
||||
"bindMatrixKey": "2",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 2
|
||||
},
|
||||
{
|
||||
"id": "key_0_3",
|
||||
"type": "MechanicalButton",
|
||||
"x": 350,
|
||||
"y": 50,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "A",
|
||||
"bindMatrixKey": "3",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 3
|
||||
},
|
||||
{
|
||||
"id": "key_1_0",
|
||||
"type": "MechanicalButton",
|
||||
"x": 50,
|
||||
"y": 150,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "4",
|
||||
"bindMatrixKey": "4",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 4
|
||||
},
|
||||
{
|
||||
"id": "key_1_1",
|
||||
"type": "MechanicalButton",
|
||||
"x": 150,
|
||||
"y": 150,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "5",
|
||||
"bindMatrixKey": "5",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 5
|
||||
},
|
||||
{
|
||||
"id": "key_1_2",
|
||||
"type": "MechanicalButton",
|
||||
"x": 250,
|
||||
"y": 150,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "6",
|
||||
"bindMatrixKey": "6",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 6
|
||||
},
|
||||
{
|
||||
"id": "key_1_3",
|
||||
"type": "MechanicalButton",
|
||||
"x": 350,
|
||||
"y": 150,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "B",
|
||||
"bindMatrixKey": "7",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 7
|
||||
},
|
||||
{
|
||||
"id": "key_2_0",
|
||||
"type": "MechanicalButton",
|
||||
"x": 50,
|
||||
"y": 250,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "7",
|
||||
"bindMatrixKey": "8",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 8
|
||||
},
|
||||
{
|
||||
"id": "key_2_1",
|
||||
"type": "MechanicalButton",
|
||||
"x": 150,
|
||||
"y": 250,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "8",
|
||||
"bindMatrixKey": "9",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 9
|
||||
},
|
||||
{
|
||||
"id": "key_2_2",
|
||||
"type": "MechanicalButton",
|
||||
"x": 250,
|
||||
"y": 250,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "9",
|
||||
"bindMatrixKey": "10",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 10
|
||||
},
|
||||
{
|
||||
"id": "key_2_3",
|
||||
"type": "MechanicalButton",
|
||||
"x": 350,
|
||||
"y": 250,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "C",
|
||||
"bindMatrixKey": "11",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 11
|
||||
},
|
||||
{
|
||||
"id": "key_3_0",
|
||||
"type": "MechanicalButton",
|
||||
"x": 50,
|
||||
"y": 350,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "*",
|
||||
"bindMatrixKey": "12",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 12
|
||||
},
|
||||
{
|
||||
"id": "key_3_1",
|
||||
"type": "MechanicalButton",
|
||||
"x": 150,
|
||||
"y": 350,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "0",
|
||||
"bindMatrixKey": "13",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 13
|
||||
},
|
||||
{
|
||||
"id": "key_3_2",
|
||||
"type": "MechanicalButton",
|
||||
"x": 250,
|
||||
"y": 350,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "#",
|
||||
"bindMatrixKey": "14",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 14
|
||||
},
|
||||
{
|
||||
"id": "key_3_3",
|
||||
"type": "MechanicalButton",
|
||||
"x": 350,
|
||||
"y": 350,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "D",
|
||||
"bindMatrixKey": "15",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 15
|
||||
}
|
||||
],
|
||||
"connections": []
|
||||
}
|
||||
@@ -52,6 +52,7 @@ try
|
||||
{
|
||||
options.AddPolicy("Users", policy => policy
|
||||
.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public class BoundaryScanRegs
|
||||
/// </summary>
|
||||
[JsonProperty("cell_name")]
|
||||
[JsonRequired]
|
||||
public string CellName { get; set; }
|
||||
public string CellName { get; set; } = "UnknownCellName";
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
@@ -146,7 +146,8 @@ public class Parser
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public Optional<List<BoundaryScanRegs.CellEntry>> GetBoundaryLogicalPorts()
|
||||
{
|
||||
var registers = this.BoundaryRegsDesp["registers"]?.ToList().Where((item)=>{
|
||||
var registers = this.BoundaryRegsDesp["registers"]?.ToList().Where((item) =>
|
||||
{
|
||||
return item["port_id"] is not null;
|
||||
});
|
||||
if (registers is null) return new();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using DotNext;
|
||||
|
||||
namespace Common
|
||||
@@ -273,6 +274,21 @@ namespace Common
|
||||
return ((srcBits >> location) & ((UInt32)0b1)) == 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将BitArray转化为32bits无符号整型
|
||||
/// </summary>
|
||||
/// <param name="bits">BitArray比特数组</param>
|
||||
/// <returns>32bits无符号整型</returns>
|
||||
public static Result<UInt32> BitsToNumber(BitArray bits)
|
||||
{
|
||||
if (bits.Length > 32)
|
||||
throw new ArgumentException("Argument length shall be at most 32 bits.");
|
||||
|
||||
var array = new UInt32[1];
|
||||
bits.CopyTo(array, 0);
|
||||
return array[0];
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 字符串转二进制字节数组
|
||||
|
||||
@@ -1,887 +0,0 @@
|
||||
using System.Net;
|
||||
using Common;
|
||||
using DotNext;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using WebProtocol;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// UDP API
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class UDPController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
private const string LOCALHOST = "127.0.0.1";
|
||||
|
||||
/// <summary>
|
||||
/// 页面
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public string Index()
|
||||
{
|
||||
return "This is UDP Controller";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送字符串
|
||||
/// </summary>
|
||||
/// <param name="address">IPV4 或者 IPV6 地址</param>
|
||||
/// <param name="port">设备端口号</param>
|
||||
/// <param name="text">发送的文本</param>
|
||||
/// <response code="200">发送成功</response>
|
||||
/// <response code="500">发送失败</response>
|
||||
[HttpPost("SendString")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SendString(string address = LOCALHOST, int port = 1234, string text = "Hello Server!")
|
||||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendStringAsync(endPoint, [text]);
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送二进制数据
|
||||
/// </summary>
|
||||
/// <param name="address" example="127.0.0.1">IPV4 或者 IPV6 地址</param>
|
||||
/// <param name="port" example="1234">设备端口号</param>
|
||||
/// <param name="bytes" example="FFFFAAAA">16进制文本</param>
|
||||
[HttpPost("SendBytes")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SendBytes(string address, int port, string bytes)
|
||||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendBytesAsync(endPoint, Number.StringToBytes(bytes));
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 发送地址包
|
||||
/// </summary>
|
||||
/// <param name="address">IP地址</param>
|
||||
/// <param name="port">UDP 端口号</param>
|
||||
/// <param name="opts">地址包选项</param>
|
||||
[HttpPost("SendAddrPackage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SendAddrPackage(
|
||||
string address,
|
||||
int port,
|
||||
[FromBody] SendAddrPackOptions opts)
|
||||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendAddrPackAsync(endPoint, new WebProtocol.SendAddrPackage(opts));
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送数据包
|
||||
/// </summary>
|
||||
/// <param name="address">IP地址</param>
|
||||
/// <param name="port">UDP 端口号</param>
|
||||
/// <param name="data">16进制数据</param>
|
||||
[HttpPost("SendDataPackage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SendDataPackage(string address, int port, string data)
|
||||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendDataPackAsync(endPoint,
|
||||
new WebProtocol.SendDataPackage(Number.StringToBytes(data)));
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定IP地址接受的数据列表
|
||||
/// </summary>
|
||||
/// <param name="address">IP地址</param>
|
||||
[HttpGet("GetRecvDataArray")]
|
||||
[ProducesResponseType(typeof(List<UDPData>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> GetRecvDataArray(string address)
|
||||
{
|
||||
var ret = await MsgBus.UDPServer.GetDataArrayAsync(address);
|
||||
|
||||
if (ret.HasValue)
|
||||
{
|
||||
var dataJson = JsonConvert.SerializeObject(ret.Value);
|
||||
logger.Debug($"Get Receive Successfully: {dataJson}");
|
||||
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Debug("Get Receive Failed");
|
||||
return TypedResults.InternalServerError();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Jtag API
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class JtagController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
private const string BITSTREAM_PATH = "bitstream/Jtag";
|
||||
|
||||
/// <summary>
|
||||
/// 页面
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public string Index()
|
||||
{
|
||||
return "This is Jtag Controller";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取Jtag ID Code
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
[HttpGet("GetDeviceIDCode")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(uint), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> GetDeviceIDCode(string address, int port)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.ReadIDCode();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Get device {address} ID code: 0x{ret.Value:X4}");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取状态寄存器
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
[HttpGet("ReadStatusReg")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> ReadStatusReg(string address, int port)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.ReadStatusReg();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0'));
|
||||
var decodeValue = new JtagClient.JtagStatusReg(ret.Value);
|
||||
logger.Info($"Read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}");
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
original = ret.Value,
|
||||
binaryValue,
|
||||
decodeValue,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传比特流文件
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="file">比特流文件</param>
|
||||
[HttpPost("UploadBitstream")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
|
||||
public async ValueTask<IResult> UploadBitstream(string address, IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
return TypedResults.BadRequest("未选择文件");
|
||||
|
||||
// 生成安全的文件名(避免路径遍历攻击)
|
||||
var fileName = Path.GetRandomFileName();
|
||||
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
|
||||
|
||||
// 如果存在文件,则删除原文件再上传
|
||||
if (Directory.Exists(uploadsFolder))
|
||||
{
|
||||
Directory.Delete(uploadsFolder, true);
|
||||
}
|
||||
Directory.CreateDirectory(uploadsFolder);
|
||||
|
||||
var filePath = Path.Combine(uploadsFolder, fileName);
|
||||
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
logger.Info($"Device {address} Upload Bitstream Successfully");
|
||||
return TypedResults.Ok(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过Jtag下载比特流文件
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
[HttpPost("DownloadBitstream")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> DownloadBitstream(string address, int port)
|
||||
{
|
||||
// 检查文件
|
||||
var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
|
||||
if (!Directory.Exists(fileDir))
|
||||
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
|
||||
|
||||
try
|
||||
{
|
||||
// 读取文件
|
||||
var filePath = Directory.GetFiles(fileDir)[0];
|
||||
|
||||
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
|
||||
{
|
||||
if (fileStream is null || fileStream.Length <= 0)
|
||||
return TypedResults.BadRequest("Wrong bitstream, Please upload it again");
|
||||
|
||||
// 定义缓冲区大小: 32KB
|
||||
byte[] buffer = new byte[32 * 1024];
|
||||
byte[] revBuffer = new byte[32 * 1024];
|
||||
long totalBytesRead = 0;
|
||||
|
||||
// 使用异步流读取文件
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
int bytesRead;
|
||||
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
// 反转 32bits
|
||||
var retBuffer = Common.Number.ReverseBytes(buffer, 4);
|
||||
if (!retBuffer.IsSuccessful)
|
||||
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();
|
||||
|
||||
// 下载比特流
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.DownloadBitstream(fileBytes);
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} dowload bitstream successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return TypedResults.InternalServerError(error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("BoundaryScanAllPorts")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> BoundaryScanAllPorts(string address, int port)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.BoundaryScan();
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
if (ret.Error is ArgumentException)
|
||||
return TypedResults.BadRequest(ret.Error);
|
||||
else return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("BoundaryScanLogicalPorts")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(Dictionary<string, bool>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> BoundaryScanLogicalPorts(string address, int port)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.BoundaryScanLogicalPorts();
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
if (ret.Error is ArgumentException)
|
||||
return TypedResults.BadRequest(ret.Error);
|
||||
else return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="speed">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("SetSpeed")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SetSpeed(string address, int port, UInt32 speed)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.SetSpeed(speed);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
if (ret.Error is ArgumentException)
|
||||
return TypedResults.BadRequest(ret.Error);
|
||||
else return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 远程更新
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class RemoteUpdateController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
private const string BITSTREAM_PATH = "bitstream/RemoteUpdate";
|
||||
|
||||
/// <summary>
|
||||
/// 上传远程更新比特流文件
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="goldenBitream">黄金比特流文件</param>
|
||||
/// <param name="bitstream1">比特流文件1</param>
|
||||
/// <param name="bitstream2">比特流文件2</param>
|
||||
/// <param name="bitstream3">比特流文件3</param>
|
||||
/// <returns>上传结果</returns>
|
||||
[HttpPost("UploadBitstream")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
public async ValueTask<IResult> UploadBitstreams(
|
||||
string address,
|
||||
IFormFile? goldenBitream,
|
||||
IFormFile? bitstream1,
|
||||
IFormFile? bitstream2,
|
||||
IFormFile? bitstream3)
|
||||
{
|
||||
if ((goldenBitream is null || goldenBitream.Length == 0) &&
|
||||
(bitstream1 is null || bitstream1.Length == 0) &&
|
||||
(bitstream2 is null || bitstream2.Length == 0) &&
|
||||
(bitstream3 is null || bitstream3.Length == 0))
|
||||
return TypedResults.BadRequest("未选择文件");
|
||||
|
||||
// 生成安全的文件名(避免路径遍历攻击)
|
||||
var fileName = Path.GetRandomFileName();
|
||||
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
|
||||
|
||||
// 如果存在文件,则删除原文件再上传
|
||||
if (Directory.Exists(uploadsFolder))
|
||||
{
|
||||
Directory.Delete(uploadsFolder, true);
|
||||
}
|
||||
Directory.CreateDirectory(uploadsFolder);
|
||||
|
||||
for (int bitstreamNum = 0; bitstreamNum < 4; bitstreamNum++)
|
||||
{
|
||||
IFormFile file;
|
||||
if (bitstreamNum == 0 && goldenBitream is not null)
|
||||
file = goldenBitream;
|
||||
else if (bitstreamNum == 1 && bitstream1 is not null)
|
||||
file = bitstream1;
|
||||
else if (bitstreamNum == 2 && bitstream2 is not null)
|
||||
file = bitstream2;
|
||||
else if (bitstreamNum == 3 && bitstream3 is not null)
|
||||
file = bitstream3;
|
||||
else continue;
|
||||
|
||||
var fileFolder = Path.Combine(uploadsFolder, bitstreamNum.ToString());
|
||||
Directory.CreateDirectory(fileFolder);
|
||||
|
||||
var filePath = Path.Combine(fileFolder, fileName);
|
||||
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info($"Device {address} Upload Bitstream Successfully");
|
||||
return TypedResults.Ok(true);
|
||||
}
|
||||
|
||||
private async ValueTask<Result<byte[]>> ProcessBitstream(string filePath)
|
||||
{
|
||||
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
|
||||
{
|
||||
if (fileStream is null || fileStream.Length <= 0)
|
||||
return new(new ArgumentException("Wrong bitstream path"));
|
||||
|
||||
// 定义缓冲区大小: 32KB
|
||||
byte[] buffer = new byte[32 * 1024];
|
||||
byte[] revBuffer = new byte[32 * 1024];
|
||||
long totalBytesRead = 0;
|
||||
|
||||
// 使用异步流读取文件
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
int bytesRead;
|
||||
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
// 反转 32bits
|
||||
var retBuffer = Common.Number.ReverseBytes(buffer, 4);
|
||||
if (!retBuffer.IsSuccessful)
|
||||
return new(retBuffer.Error);
|
||||
revBuffer = retBuffer.Value;
|
||||
|
||||
await memoryStream.WriteAsync(revBuffer, 0, bytesRead);
|
||||
totalBytesRead += bytesRead;
|
||||
}
|
||||
|
||||
// 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存)
|
||||
var restStreamLen = memoryStream.Length % (4 * 1024);
|
||||
if (restStreamLen != 0)
|
||||
{
|
||||
var appendLen = ((int)(4 * 1024 - restStreamLen));
|
||||
var bytesAppend = new byte[appendLen];
|
||||
Array.Fill<byte>(bytesAppend, 0xFF);
|
||||
await memoryStream.WriteAsync(bytesAppend, 0, appendLen);
|
||||
}
|
||||
|
||||
return new(memoryStream.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 远程更新单个比特流文件
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
/// <param name="bitstreamNum"> 比特流位号 </param>
|
||||
[HttpPost("DownloadBitstream")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> UpdateBitstream(string address, int port, int bitstreamNum)
|
||||
{
|
||||
// 检查文件
|
||||
var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}/{bitstreamNum}");
|
||||
if (!Directory.Exists(fileDir))
|
||||
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
|
||||
|
||||
try
|
||||
{
|
||||
// 读取文件
|
||||
var filePath = Directory.GetFiles(fileDir)[0];
|
||||
|
||||
var fileBytes = await ProcessBitstream(filePath);
|
||||
if (!fileBytes.IsSuccessful) return TypedResults.InternalServerError(fileBytes.Error);
|
||||
|
||||
// 下载比特流
|
||||
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
var ret = await remoteUpdater.UpdateBitstream(bitstreamNum, fileBytes.Value);
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} Update bitstream successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return TypedResults.InternalServerError(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 下载多个比特流文件
|
||||
/// </summary>
|
||||
/// <param name="address">设备地址</param>
|
||||
/// <param name="port">设备端口</param>
|
||||
/// <param name="bitstreamNum">比特流编号</param>
|
||||
/// <returns>总共上传比特流的数量</returns>
|
||||
[HttpPost("DownloadMultiBitstreams")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> DownloadMultiBitstreams(string address, int port, int? bitstreamNum)
|
||||
{
|
||||
// 检查文件
|
||||
var bitstreamsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
|
||||
if (!Directory.Exists(bitstreamsFolder))
|
||||
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
|
||||
|
||||
try
|
||||
{
|
||||
var bitstreams = new List<byte[]?>() { null, null, null, null };
|
||||
int cnt = 0; // 上传比特流数量
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
var bitstreamDir = Path.Combine(bitstreamsFolder, i.ToString());
|
||||
if (!Directory.Exists(bitstreamDir))
|
||||
continue;
|
||||
cnt++;
|
||||
|
||||
// 读取文件
|
||||
var filePath = Directory.GetFiles(bitstreamDir)[0];
|
||||
var fileBytes = await ProcessBitstream(filePath);
|
||||
if (!fileBytes.IsSuccessful) return TypedResults.InternalServerError(fileBytes.Error);
|
||||
bitstreams[i] = fileBytes.Value;
|
||||
}
|
||||
|
||||
// 下载比特流
|
||||
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
{
|
||||
var ret = await remoteUpdater.UploadBitstreams(bitstreams[0], bitstreams[1], bitstreams[2], bitstreams[3]);
|
||||
if (!ret.IsSuccessful) return TypedResults.InternalServerError(ret.Error);
|
||||
if (!ret.Value) return TypedResults.InternalServerError("Upload MultiBitstreams failed");
|
||||
}
|
||||
|
||||
if (bitstreamNum is not null)
|
||||
{
|
||||
var ret = await remoteUpdater.HotResetBitstream(bitstreamNum ?? 0);
|
||||
if (!ret.IsSuccessful) return TypedResults.InternalServerError(ret.Error);
|
||||
if (!ret.Value) return TypedResults.InternalServerError("Hot reset failed");
|
||||
}
|
||||
return TypedResults.Ok(cnt);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return TypedResults.InternalServerError(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 热复位比特流文件
|
||||
/// </summary>
|
||||
/// <param name="address">设备地址</param>
|
||||
/// <param name="port">设备端口</param>
|
||||
/// <param name="bitstreamNum">比特流编号</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("HotResetBitstream")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> HotResetBitstream(string address, int port, int bitstreamNum)
|
||||
{
|
||||
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
var ret = await remoteUpdater.HotResetBitstream(bitstreamNum);
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} Update bitstream successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("GetFirmwareVersion")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(UInt32), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> GetFirmwareVersion(string address, int port)
|
||||
{
|
||||
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
var ret = await remoteUpdater.GetVersion();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} get firmware version successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class DDSController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="channelNum">[TODO:parameter]</param>
|
||||
/// <param name="waveNum">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("SetWaveNum")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SetWaveNum(string address, int port, int channelNum, int waveNum)
|
||||
{
|
||||
var dds = new DDSClient.DDS(address, port);
|
||||
|
||||
var ret = await dds.SetWaveNum(channelNum, waveNum);
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} set output wave num successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="channelNum">[TODO:parameter]</param>
|
||||
/// <param name="waveNum">[TODO:parameter]</param>
|
||||
/// <param name="step">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("SetFreq")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SetFreq(string address, int port, int channelNum, int waveNum, UInt32 step)
|
||||
{
|
||||
var dds = new DDSClient.DDS(address, port);
|
||||
|
||||
var ret = await dds.SetFreq(channelNum, waveNum, step);
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} set output freqency successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="channelNum">[TODO:parameter]</param>
|
||||
/// <param name="waveNum">[TODO:parameter]</param>
|
||||
/// <param name="phase">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("SetPhase")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SetPhase(string address, int port, int channelNum, int waveNum, int phase)
|
||||
{
|
||||
var dds = new DDSClient.DDS(address, port);
|
||||
|
||||
var ret = await dds.SetPhase(channelNum, waveNum, phase);
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} set output phase successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class BsdlParserController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
[EnableCors("Development")]
|
||||
[HttpGet("GetBoundaryLogicalPorts")]
|
||||
public IResult GetBoundaryLogicalPorts()
|
||||
{
|
||||
var parser = new BsdlParser.Parser();
|
||||
var ret = parser.GetBoundaryLogicalPorts();
|
||||
if (ret.IsNull) return TypedResults.InternalServerError("Get Null");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 数据控制器
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class DataController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 创建数据库表
|
||||
/// </summary>
|
||||
/// <returns>插入的记录数</returns>
|
||||
[EnableCors("Development")]
|
||||
[HttpPost("CreateTable")]
|
||||
public IResult CreateTables()
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
db.CreateAllTables();
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除数据库表
|
||||
/// </summary>
|
||||
/// <returns>插入的记录数</returns>
|
||||
[EnableCors("Development")]
|
||||
[HttpDelete("DropTables")]
|
||||
public IResult DropTables()
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
db.DropAllTables();
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有用户
|
||||
/// </summary>
|
||||
/// <returns>用户列表</returns>
|
||||
[HttpGet("AllUsers")]
|
||||
public IResult AllUsers()
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var ret = db.User.ToList();
|
||||
return TypedResults.Ok(ret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册新用户
|
||||
/// </summary>
|
||||
/// <param name="name">用户名</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("SignUpUser")]
|
||||
public IResult SignUpUser(string name)
|
||||
{
|
||||
if (name.Length > 255)
|
||||
return TypedResults.BadRequest("Name Couln't over 255 characters");
|
||||
|
||||
using var db = new Database.AppDataConnection();
|
||||
var ret = db.AddUser(name);
|
||||
return TypedResults.Ok(ret);
|
||||
}
|
||||
}
|
||||
|
||||
30
server/src/Controllers/BsdlParserController.cs
Normal file
30
server/src/Controllers/BsdlParserController.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class BsdlParserController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[EnableCors("Development")]
|
||||
[HttpGet("GetBoundaryLogicalPorts")]
|
||||
public IResult GetBoundaryLogicalPorts()
|
||||
{
|
||||
var parser = new BsdlParser.Parser();
|
||||
var ret = parser.GetBoundaryLogicalPorts();
|
||||
if (ret.IsNull) return TypedResults.InternalServerError("Get Null");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
108
server/src/Controllers/DDSController.cs
Normal file
108
server/src/Controllers/DDSController.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class DDSController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="channelNum">[TODO:parameter]</param>
|
||||
/// <param name="waveNum">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("SetWaveNum")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SetWaveNum(string address, int port, int channelNum, int waveNum)
|
||||
{
|
||||
var dds = new Peripherals.DDSClient.DDS(address, port);
|
||||
|
||||
var ret = await dds.SetWaveNum(channelNum, waveNum);
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} set output wave num successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="channelNum">[TODO:parameter]</param>
|
||||
/// <param name="waveNum">[TODO:parameter]</param>
|
||||
/// <param name="step">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("SetFreq")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SetFreq(string address, int port, int channelNum, int waveNum, UInt32 step)
|
||||
{
|
||||
var dds = new Peripherals.DDSClient.DDS(address, port);
|
||||
|
||||
var ret = await dds.SetFreq(channelNum, waveNum, step);
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} set output freqency successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="channelNum">[TODO:parameter]</param>
|
||||
/// <param name="waveNum">[TODO:parameter]</param>
|
||||
/// <param name="phase">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("SetPhase")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SetPhase(string address, int port, int channelNum, int waveNum, int phase)
|
||||
{
|
||||
var dds = new Peripherals.DDSClient.DDS(address, port);
|
||||
|
||||
var ret = await dds.SetPhase(channelNum, waveNum, phase);
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} set output phase successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
69
server/src/Controllers/DataController.cs
Normal file
69
server/src/Controllers/DataController.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 数据控制器
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class DataController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 创建数据库表
|
||||
/// </summary>
|
||||
/// <returns>插入的记录数</returns>
|
||||
[EnableCors("Development")]
|
||||
[HttpPost("CreateTable")]
|
||||
public IResult CreateTables()
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
db.CreateAllTables();
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除数据库表
|
||||
/// </summary>
|
||||
/// <returns>插入的记录数</returns>
|
||||
[EnableCors("Development")]
|
||||
[HttpDelete("DropTables")]
|
||||
public IResult DropTables()
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
db.DropAllTables();
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有用户
|
||||
/// </summary>
|
||||
/// <returns>用户列表</returns>
|
||||
[HttpGet("AllUsers")]
|
||||
public IResult AllUsers()
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var ret = db.User.ToList();
|
||||
return TypedResults.Ok(ret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册新用户
|
||||
/// </summary>
|
||||
/// <param name="name">用户名</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("SignUpUser")]
|
||||
public IResult SignUpUser(string name)
|
||||
{
|
||||
if (name.Length > 255)
|
||||
return TypedResults.BadRequest("Name Couln't over 255 characters");
|
||||
|
||||
using var db = new Database.AppDataConnection();
|
||||
var ret = db.AddUser(name);
|
||||
return TypedResults.Ok(ret);
|
||||
}
|
||||
}
|
||||
|
||||
278
server/src/Controllers/JtagController.cs
Normal file
278
server/src/Controllers/JtagController.cs
Normal file
@@ -0,0 +1,278 @@
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Jtag API
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class JtagController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
private const string BITSTREAM_PATH = "bitstream/Jtag";
|
||||
|
||||
/// <summary>
|
||||
/// 页面
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public string Index()
|
||||
{
|
||||
return "This is Jtag Controller";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取Jtag ID Code
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
[HttpGet("GetDeviceIDCode")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(uint), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> GetDeviceIDCode(string address, int port)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.ReadIDCode();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Get device {address} ID code: 0x{ret.Value:X4}");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取状态寄存器
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
[HttpGet("ReadStatusReg")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> ReadStatusReg(string address, int port)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.ReadStatusReg();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0'));
|
||||
var decodeValue = new JtagClient.JtagStatusReg(ret.Value);
|
||||
logger.Info($"Read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}");
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
original = ret.Value,
|
||||
binaryValue,
|
||||
decodeValue,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传比特流文件
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="file">比特流文件</param>
|
||||
[HttpPost("UploadBitstream")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
|
||||
public async ValueTask<IResult> UploadBitstream(string address, IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
return TypedResults.BadRequest("未选择文件");
|
||||
|
||||
// 生成安全的文件名(避免路径遍历攻击)
|
||||
var fileName = Path.GetRandomFileName();
|
||||
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
|
||||
|
||||
// 如果存在文件,则删除原文件再上传
|
||||
if (Directory.Exists(uploadsFolder))
|
||||
{
|
||||
Directory.Delete(uploadsFolder, true);
|
||||
}
|
||||
Directory.CreateDirectory(uploadsFolder);
|
||||
|
||||
var filePath = Path.Combine(uploadsFolder, fileName);
|
||||
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
logger.Info($"Device {address} Upload Bitstream Successfully");
|
||||
return TypedResults.Ok(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过Jtag下载比特流文件
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
[HttpPost("DownloadBitstream")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> DownloadBitstream(string address, int port)
|
||||
{
|
||||
// 检查文件
|
||||
var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
|
||||
if (!Directory.Exists(fileDir))
|
||||
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
|
||||
|
||||
try
|
||||
{
|
||||
// 读取文件
|
||||
var filePath = Directory.GetFiles(fileDir)[0];
|
||||
|
||||
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
|
||||
{
|
||||
if (fileStream is null || fileStream.Length <= 0)
|
||||
return TypedResults.BadRequest("Wrong bitstream, Please upload it again");
|
||||
|
||||
// 定义缓冲区大小: 32KB
|
||||
byte[] buffer = new byte[32 * 1024];
|
||||
byte[] revBuffer = new byte[32 * 1024];
|
||||
long totalBytesRead = 0;
|
||||
|
||||
// 使用异步流读取文件
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
int bytesRead;
|
||||
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
// 反转 32bits
|
||||
var retBuffer = Common.Number.ReverseBytes(buffer, 4);
|
||||
if (!retBuffer.IsSuccessful)
|
||||
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();
|
||||
|
||||
// 下载比特流
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.DownloadBitstream(fileBytes);
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} dowload bitstream successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return TypedResults.InternalServerError(error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("BoundaryScanAllPorts")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> BoundaryScanAllPorts(string address, int port)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.BoundaryScan();
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
if (ret.Error is ArgumentException)
|
||||
return TypedResults.BadRequest(ret.Error);
|
||||
else return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("BoundaryScanLogicalPorts")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(Dictionary<string, bool>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> BoundaryScanLogicalPorts(string address, int port)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.BoundaryScanLogicalPorts();
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
if (ret.Error is ArgumentException)
|
||||
return TypedResults.BadRequest(ret.Error);
|
||||
else return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="speed">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("SetSpeed")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SetSpeed(string address, int port, UInt32 speed)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.SetSpeed(speed);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
if (ret.Error is ArgumentException)
|
||||
return TypedResults.BadRequest(ret.Error);
|
||||
else return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
}
|
||||
101
server/src/Controllers/MatrixKeyController.cs
Normal file
101
server/src/Controllers/MatrixKeyController.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System.Collections;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 矩阵键控制器,用于管理矩阵键的启用、禁用和状态设置
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class MatrixKeyController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 启用矩阵键控制。
|
||||
/// </summary>
|
||||
/// <param name="address">设备的IP地址</param>
|
||||
/// <param name="port">设备的端口号</param>
|
||||
/// <returns>返回操作结果的状态码</returns>
|
||||
[HttpPost("EnabelMatrixKey")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> EnabelMatrixKey(string address, int port)
|
||||
{
|
||||
var matrixKeyCtrl = new Peripherals.MatrixKeyClient.MatrixKey(address, port);
|
||||
var ret = await matrixKeyCtrl.EnableControl();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Enable device {address}:{port.ToString()} matrix key finished: {ret.Value}.");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用矩阵键控制。
|
||||
/// </summary>
|
||||
/// <param name="address">设备的IP地址</param>
|
||||
/// <param name="port">设备的端口号</param>
|
||||
/// <returns>返回操作结果的状态码</returns>
|
||||
[HttpPost("DisableMatrixKey")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> DisableMatrixKey(string address, int port)
|
||||
{
|
||||
var matrixKeyCtrl = new Peripherals.MatrixKeyClient.MatrixKey(address, port);
|
||||
var ret = await matrixKeyCtrl.DisableControl();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Disable device {address}:{port.ToString()} matrix key finished: {ret.Value}.");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置矩阵键的状态。
|
||||
/// </summary>
|
||||
/// <param name="address">设备的IP地址</param>
|
||||
/// <param name="port">设备的端口号</param>
|
||||
/// <param name="keyStates">矩阵键的状态数组,长度应为16</param>
|
||||
/// <returns>返回操作结果的状态码</returns>
|
||||
[HttpPost("SetMatrixKeyStatus")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SetMatrixKeyStatus(string address, int port, [FromBody] bool[] keyStates)
|
||||
{
|
||||
if (keyStates.Length != 16)
|
||||
return TypedResults.BadRequest($"The length of key states should be 16 instead of {keyStates.Length}");
|
||||
|
||||
var matrixKeyCtrl = new Peripherals.MatrixKeyClient.MatrixKey(address, port);
|
||||
var ret = await matrixKeyCtrl.ControlKey(new BitArray(keyStates));
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Set device {address}:{port.ToString()} matrix key finished: {ret.Value}.");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
46
server/src/Controllers/PowerController.cs
Normal file
46
server/src/Controllers/PowerController.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Collections;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 矩阵键控制器,用于管理矩阵键的启用、禁用和状态设置
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class PowerController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="enable">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("SetPowerOnOff")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SetPowerOnOff(string address, int port, bool enable)
|
||||
{
|
||||
var powerCtrl = new Peripherals.PowerClient.Power(address, port);
|
||||
var ret = await powerCtrl.SetPowerOnOff(enable);
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
var powerStatus = enable ? "ON" : "OFF";
|
||||
logger.Info($"Set device {address}:{port.ToString()} power {powerStatus} finished: {ret.Value}.");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
292
server/src/Controllers/RemoteUpdateController.cs
Normal file
292
server/src/Controllers/RemoteUpdateController.cs
Normal file
@@ -0,0 +1,292 @@
|
||||
using DotNext;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 远程更新
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class RemoteUpdateController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
private const string BITSTREAM_PATH = "bitstream/RemoteUpdate";
|
||||
|
||||
/// <summary>
|
||||
/// 上传远程更新比特流文件
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="goldenBitream">黄金比特流文件</param>
|
||||
/// <param name="bitstream1">比特流文件1</param>
|
||||
/// <param name="bitstream2">比特流文件2</param>
|
||||
/// <param name="bitstream3">比特流文件3</param>
|
||||
/// <returns>上传结果</returns>
|
||||
[HttpPost("UploadBitstream")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
public async ValueTask<IResult> UploadBitstreams(
|
||||
string address,
|
||||
IFormFile? goldenBitream,
|
||||
IFormFile? bitstream1,
|
||||
IFormFile? bitstream2,
|
||||
IFormFile? bitstream3)
|
||||
{
|
||||
if ((goldenBitream is null || goldenBitream.Length == 0) &&
|
||||
(bitstream1 is null || bitstream1.Length == 0) &&
|
||||
(bitstream2 is null || bitstream2.Length == 0) &&
|
||||
(bitstream3 is null || bitstream3.Length == 0))
|
||||
return TypedResults.BadRequest("未选择文件");
|
||||
|
||||
// 生成安全的文件名(避免路径遍历攻击)
|
||||
var fileName = Path.GetRandomFileName();
|
||||
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
|
||||
|
||||
// 如果存在文件,则删除原文件再上传
|
||||
if (Directory.Exists(uploadsFolder))
|
||||
{
|
||||
Directory.Delete(uploadsFolder, true);
|
||||
}
|
||||
Directory.CreateDirectory(uploadsFolder);
|
||||
|
||||
for (int bitstreamNum = 0; bitstreamNum < 4; bitstreamNum++)
|
||||
{
|
||||
IFormFile file;
|
||||
if (bitstreamNum == 0 && goldenBitream is not null)
|
||||
file = goldenBitream;
|
||||
else if (bitstreamNum == 1 && bitstream1 is not null)
|
||||
file = bitstream1;
|
||||
else if (bitstreamNum == 2 && bitstream2 is not null)
|
||||
file = bitstream2;
|
||||
else if (bitstreamNum == 3 && bitstream3 is not null)
|
||||
file = bitstream3;
|
||||
else continue;
|
||||
|
||||
var fileFolder = Path.Combine(uploadsFolder, bitstreamNum.ToString());
|
||||
Directory.CreateDirectory(fileFolder);
|
||||
|
||||
var filePath = Path.Combine(fileFolder, fileName);
|
||||
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info($"Device {address} Upload Bitstream Successfully");
|
||||
return TypedResults.Ok(true);
|
||||
}
|
||||
|
||||
private async ValueTask<Result<byte[]>> ProcessBitstream(string filePath)
|
||||
{
|
||||
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
|
||||
{
|
||||
if (fileStream is null || fileStream.Length <= 0)
|
||||
return new(new ArgumentException("Wrong bitstream path"));
|
||||
|
||||
// 定义缓冲区大小: 32KB
|
||||
byte[] buffer = new byte[32 * 1024];
|
||||
byte[] revBuffer = new byte[32 * 1024];
|
||||
long totalBytesRead = 0;
|
||||
|
||||
// 使用异步流读取文件
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
int bytesRead;
|
||||
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
// 反转 32bits
|
||||
var retBuffer = Common.Number.ReverseBytes(buffer, 4);
|
||||
if (!retBuffer.IsSuccessful)
|
||||
return new(retBuffer.Error);
|
||||
revBuffer = retBuffer.Value;
|
||||
|
||||
await memoryStream.WriteAsync(revBuffer, 0, bytesRead);
|
||||
totalBytesRead += bytesRead;
|
||||
}
|
||||
|
||||
// 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存)
|
||||
var restStreamLen = memoryStream.Length % (4 * 1024);
|
||||
if (restStreamLen != 0)
|
||||
{
|
||||
var appendLen = ((int)(4 * 1024 - restStreamLen));
|
||||
var bytesAppend = new byte[appendLen];
|
||||
Array.Fill<byte>(bytesAppend, 0xFF);
|
||||
await memoryStream.WriteAsync(bytesAppend, 0, appendLen);
|
||||
}
|
||||
|
||||
return new(memoryStream.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 远程更新单个比特流文件
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
/// <param name="bitstreamNum"> 比特流位号 </param>
|
||||
[HttpPost("DownloadBitstream")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> UpdateBitstream(string address, int port, int bitstreamNum)
|
||||
{
|
||||
// 检查文件
|
||||
var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}/{bitstreamNum}");
|
||||
if (!Directory.Exists(fileDir))
|
||||
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
|
||||
|
||||
try
|
||||
{
|
||||
// 读取文件
|
||||
var filePath = Directory.GetFiles(fileDir)[0];
|
||||
|
||||
var fileBytes = await ProcessBitstream(filePath);
|
||||
if (!fileBytes.IsSuccessful) return TypedResults.InternalServerError(fileBytes.Error);
|
||||
|
||||
// 下载比特流
|
||||
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
var ret = await remoteUpdater.UpdateBitstream(bitstreamNum, fileBytes.Value);
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} Update bitstream successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return TypedResults.InternalServerError(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 下载多个比特流文件
|
||||
/// </summary>
|
||||
/// <param name="address">设备地址</param>
|
||||
/// <param name="port">设备端口</param>
|
||||
/// <param name="bitstreamNum">比特流编号</param>
|
||||
/// <returns>总共上传比特流的数量</returns>
|
||||
[HttpPost("DownloadMultiBitstreams")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> DownloadMultiBitstreams(string address, int port, int? bitstreamNum)
|
||||
{
|
||||
// 检查文件
|
||||
var bitstreamsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
|
||||
if (!Directory.Exists(bitstreamsFolder))
|
||||
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
|
||||
|
||||
try
|
||||
{
|
||||
var bitstreams = new List<byte[]?>() { null, null, null, null };
|
||||
int cnt = 0; // 上传比特流数量
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
var bitstreamDir = Path.Combine(bitstreamsFolder, i.ToString());
|
||||
if (!Directory.Exists(bitstreamDir))
|
||||
continue;
|
||||
cnt++;
|
||||
|
||||
// 读取文件
|
||||
var filePath = Directory.GetFiles(bitstreamDir)[0];
|
||||
var fileBytes = await ProcessBitstream(filePath);
|
||||
if (!fileBytes.IsSuccessful) return TypedResults.InternalServerError(fileBytes.Error);
|
||||
bitstreams[i] = fileBytes.Value;
|
||||
}
|
||||
|
||||
// 下载比特流
|
||||
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
{
|
||||
var ret = await remoteUpdater.UploadBitstreams(bitstreams[0], bitstreams[1], bitstreams[2], bitstreams[3]);
|
||||
if (!ret.IsSuccessful) return TypedResults.InternalServerError(ret.Error);
|
||||
if (!ret.Value) return TypedResults.InternalServerError("Upload MultiBitstreams failed");
|
||||
}
|
||||
|
||||
if (bitstreamNum is not null)
|
||||
{
|
||||
var ret = await remoteUpdater.HotResetBitstream(bitstreamNum ?? 0);
|
||||
if (!ret.IsSuccessful) return TypedResults.InternalServerError(ret.Error);
|
||||
if (!ret.Value) return TypedResults.InternalServerError("Hot reset failed");
|
||||
}
|
||||
return TypedResults.Ok(cnt);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return TypedResults.InternalServerError(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 热复位比特流文件
|
||||
/// </summary>
|
||||
/// <param name="address">设备地址</param>
|
||||
/// <param name="port">设备端口</param>
|
||||
/// <param name="bitstreamNum">比特流编号</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("HotResetBitstream")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> HotResetBitstream(string address, int port, int bitstreamNum)
|
||||
{
|
||||
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
var ret = await remoteUpdater.HotResetBitstream(bitstreamNum);
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} Update bitstream successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("GetFirmwareVersion")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(UInt32), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> GetFirmwareVersion(string address, int port)
|
||||
{
|
||||
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
var ret = await remoteUpdater.GetVersion();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} get firmware version successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
133
server/src/Controllers/UDPController.cs
Normal file
133
server/src/Controllers/UDPController.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.Net;
|
||||
using Common;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using WebProtocol;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// UDP API
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class UDPController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
private const string LOCALHOST = "127.0.0.1";
|
||||
|
||||
/// <summary>
|
||||
/// 页面
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public string Index()
|
||||
{
|
||||
return "This is UDP Controller";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送字符串
|
||||
/// </summary>
|
||||
/// <param name="address">IPV4 或者 IPV6 地址</param>
|
||||
/// <param name="port">设备端口号</param>
|
||||
/// <param name="text">发送的文本</param>
|
||||
/// <response code="200">发送成功</response>
|
||||
/// <response code="500">发送失败</response>
|
||||
[HttpPost("SendString")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SendString(string address = LOCALHOST, int port = 1234, string text = "Hello Server!")
|
||||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendStringAsync(endPoint, [text]);
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送二进制数据
|
||||
/// </summary>
|
||||
/// <param name="address" example="127.0.0.1">IPV4 或者 IPV6 地址</param>
|
||||
/// <param name="port" example="1234">设备端口号</param>
|
||||
/// <param name="bytes" example="FFFFAAAA">16进制文本</param>
|
||||
[HttpPost("SendBytes")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SendBytes(string address, int port, string bytes)
|
||||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendBytesAsync(endPoint, Number.StringToBytes(bytes));
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 发送地址包
|
||||
/// </summary>
|
||||
/// <param name="address">IP地址</param>
|
||||
/// <param name="port">UDP 端口号</param>
|
||||
/// <param name="opts">地址包选项</param>
|
||||
[HttpPost("SendAddrPackage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SendAddrPackage(
|
||||
string address,
|
||||
int port,
|
||||
[FromBody] SendAddrPackOptions opts)
|
||||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendAddrPackAsync(endPoint, new WebProtocol.SendAddrPackage(opts));
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送数据包
|
||||
/// </summary>
|
||||
/// <param name="address">IP地址</param>
|
||||
/// <param name="port">UDP 端口号</param>
|
||||
/// <param name="data">16进制数据</param>
|
||||
[HttpPost("SendDataPackage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SendDataPackage(string address, int port, string data)
|
||||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendDataPackAsync(endPoint,
|
||||
new WebProtocol.SendDataPackage(Number.StringToBytes(data)));
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定IP地址接受的数据列表
|
||||
/// </summary>
|
||||
/// <param name="address">IP地址</param>
|
||||
[HttpGet("GetRecvDataArray")]
|
||||
[ProducesResponseType(typeof(List<UDPData>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> GetRecvDataArray(string address)
|
||||
{
|
||||
var ret = await MsgBus.UDPServer.GetDataArrayAsync(address);
|
||||
|
||||
if (ret.HasValue)
|
||||
{
|
||||
var dataJson = JsonConvert.SerializeObject(ret.Value);
|
||||
logger.Debug($"Get Receive Successfully: {dataJson}");
|
||||
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Debug("Get Receive Failed");
|
||||
return TypedResults.InternalServerError();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -776,9 +776,9 @@ public class Jtag
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// 边界扫描
|
||||
/// </summary>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
/// <returns>返回所有引脚边界扫描结果</returns>
|
||||
public async ValueTask<Result<BitArray>> BoundaryScan()
|
||||
{
|
||||
var paser = new BsdlParser.Parser();
|
||||
@@ -824,9 +824,9 @@ public class Jtag
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// 执行边界扫描并返回逻辑端口的状态
|
||||
/// </summary>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
/// <returns>包含逻辑端口状态的字典,键为端口ID,值为布尔状态</returns>
|
||||
public async ValueTask<Result<Dictionary<string, bool>>> BoundaryScanLogicalPorts()
|
||||
{
|
||||
var bitArray = await BoundaryScan();
|
||||
@@ -845,18 +845,23 @@ public class Jtag
|
||||
return portStatus;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置JTAG的运行速度。
|
||||
/// </summary>
|
||||
/// <param name="speed">运行速度值。</param>
|
||||
/// <returns>指示操作是否成功的异步结果。</returns>
|
||||
public async ValueTask<Result<bool>> SetSpeed(UInt32 speed)
|
||||
{
|
||||
// Clear Data
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
|
||||
logger.Trace($"Clear up udp server {this.address} receive data");
|
||||
|
||||
|
||||
var ret = await WriteFIFO(
|
||||
JtagAddr.SPEED_CTRL, (speed << 16) | speed,
|
||||
JtagState.CMD_EXEC_FINISH, JtagState.CMD_EXEC_FINISH);
|
||||
|
||||
if (!ret.IsSuccessful) return new (ret.Error);
|
||||
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
return ret.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Net;
|
||||
using DotNext;
|
||||
|
||||
namespace DDSClient;
|
||||
namespace Peripherals.DDSClient;
|
||||
|
||||
static class DDSAddr
|
||||
{
|
||||
89
server/src/Peripherals/MatrixKeyClient.cs
Normal file
89
server/src/Peripherals/MatrixKeyClient.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System.Collections;
|
||||
using System.Net;
|
||||
using DotNext;
|
||||
|
||||
namespace Peripherals.MatrixKeyClient;
|
||||
|
||||
class MatrixKeyAddr
|
||||
{
|
||||
public const UInt32 BASE = 0x10_00_00_00;
|
||||
public const UInt32 KEY_ENABLE = BASE + 5;
|
||||
public const UInt32 KEY_CTRL = BASE + 6;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 矩阵键盘外设类,用于控制和管理矩阵键盘的功能。
|
||||
/// </summary>
|
||||
public class MatrixKey
|
||||
{
|
||||
readonly int timeout;
|
||||
|
||||
readonly int port;
|
||||
readonly string address;
|
||||
private IPEndPoint ep;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数,用于初始化矩阵键盘外设实例。
|
||||
/// </summary>
|
||||
/// <param name="address">设备的IP地址</param>
|
||||
/// <param name="port">设备的端口号</param>
|
||||
/// <param name="timeout">操作的超时时间(毫秒),默认为1000</param>
|
||||
/// <returns>无返回值。</returns>
|
||||
public MatrixKey(string address, int port, int timeout = 1000)
|
||||
{
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用矩阵键盘的控制功能。
|
||||
/// </summary>
|
||||
/// <returns>返回一个包含操作结果的异步任务</returns>
|
||||
public async ValueTask<Result<bool>> EnableControl()
|
||||
{
|
||||
if (MsgBus.IsRunning)
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
else return new(new Exception("Message Bus not work!"));
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, MatrixKeyAddr.KEY_ENABLE, 1, this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
return ret.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用矩阵键盘的控制功能。
|
||||
/// </summary>
|
||||
/// <returns>返回一个包含操作结果的异步任务</returns>
|
||||
public async ValueTask<Result<bool>> DisableControl()
|
||||
{
|
||||
if (MsgBus.IsRunning)
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
else return new(new Exception("Message Bus not work!"));
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, MatrixKeyAddr.KEY_ENABLE, 0, this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
return ret.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 控制矩阵键盘的按键状态。
|
||||
/// </summary>
|
||||
/// <param name="keyStates">表示按键状态的位数组,长度必须为16</param>
|
||||
/// <returns>返回一个包含操作结果的异步任务</returns>
|
||||
public async ValueTask<Result<bool>> ControlKey(BitArray keyStates)
|
||||
{
|
||||
if (MsgBus.IsRunning)
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
else return new(new Exception("Message Bus not work!"));
|
||||
|
||||
if (keyStates.Length != 16) return new(new ArgumentException(
|
||||
$"The number of key should be 16 instead of {keyStates.Length}", nameof(keyStates)));
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, MatrixKeyAddr.KEY_CTRL, Common.Number.BitsToNumber(keyStates).Value, this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
return ret.Value;
|
||||
}
|
||||
}
|
||||
56
server/src/Peripherals/PowerClient.cs
Normal file
56
server/src/Peripherals/PowerClient.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Net;
|
||||
using DotNext;
|
||||
|
||||
namespace Peripherals.PowerClient;
|
||||
|
||||
class PowerAddr
|
||||
{
|
||||
public const UInt32 Base = 0x10_00_00_00;
|
||||
public const UInt32 PowerCtrl = Base + 7;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public class Power
|
||||
{
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
readonly int timeout;
|
||||
|
||||
readonly int port;
|
||||
readonly string address;
|
||||
private IPEndPoint ep;
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="timeout">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public Power(string address, int port, int timeout = 1000)
|
||||
{
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="enable">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public async ValueTask<Result<bool>> SetPowerOnOff(bool enable)
|
||||
{
|
||||
if (MsgBus.IsRunning)
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
else return new(new Exception("Message Bus not work!"));
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, PowerAddr.PowerCtrl, Convert.ToUInt32(enable), this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
return ret.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ using WebProtocol;
|
||||
/// </summary>
|
||||
public class UDPClientPool
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
private static IPAddress localhost = IPAddress.Parse("127.0.0.1");
|
||||
|
||||
/// <summary>
|
||||
@@ -58,6 +60,9 @@ public class UDPClientPool
|
||||
var sendLen = socket.SendTo(buf, endPoint);
|
||||
socket.Close();
|
||||
|
||||
logger.Debug($"UDP socket send bytes to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
|
||||
logger.Debug($" Original Data: {BitConverter.ToString(buf).Replace("-", " ")}");
|
||||
|
||||
if (sendLen == buf.Length) { return true; }
|
||||
else { return false; }
|
||||
}
|
||||
@@ -86,6 +91,10 @@ public class UDPClientPool
|
||||
var sendLen = socket.SendTo(sendBytes, endPoint);
|
||||
socket.Close();
|
||||
|
||||
logger.Debug($"UDP socket send address package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
|
||||
logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
|
||||
logger.Debug($" Decoded Data: {pkg.ToString()}");
|
||||
|
||||
if (sendLen == sendBytes.Length) { return true; }
|
||||
else { return false; }
|
||||
}
|
||||
@@ -115,6 +124,9 @@ public class UDPClientPool
|
||||
var sendLen = socket.SendTo(sendBytes, endPoint);
|
||||
socket.Close();
|
||||
|
||||
logger.Debug($"UDP socket send data package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
|
||||
logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
|
||||
|
||||
if (sendLen == sendBytes.Length) { return true; }
|
||||
else { return false; }
|
||||
}
|
||||
|
||||
1801
src/APIClient.ts
1801
src/APIClient.ts
File diff suppressed because it is too large
Load Diff
10
src/assets/base.css
Normal file
10
src/assets/base.css
Normal file
@@ -0,0 +1,10 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@plugin "daisyui" {
|
||||
themes: winter --default, night --prefersdark;
|
||||
}
|
||||
|
||||
@custom-variant dark (&:where([data-theme=night], [data-theme=night] *));
|
||||
@custom-variant light (&:where([data-theme=winter], [data-theme=winter] *));
|
||||
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@plugin "daisyui" {
|
||||
themes: winter --default, night --prefersdark;
|
||||
}
|
||||
|
||||
@custom-variant dark (&:where([data-theme=night], [data-theme=night] *));
|
||||
@custom-variant light (&:where([data-theme=winter], [data-theme=winter] *));
|
||||
@import "base.css";
|
||||
|
||||
/* 禁止所有图像和SVG选择 */
|
||||
img, svg {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useDialogStore } from "@/stores/dialog";
|
||||
const dialog = useDialogStore();
|
||||
dialog.enableDialog = true;
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
|
||||
@@ -181,7 +181,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, shallowRef, onMounted } from "vue";
|
||||
import motherboardSvg from "../components/equipments/svg/motherboard.svg";
|
||||
import motherboardSvg from "@/components/equipments/svg/motherboard.svg";
|
||||
import buttonSvg from "@/components//equipments/svg/button.svg";
|
||||
|
||||
// Props 定义
|
||||
interface Props {
|
||||
@@ -219,6 +220,7 @@ const availableComponents = [
|
||||
{ type: "SMA", name: "SMA连接器" },
|
||||
{ type: "MotherBoard", name: "主板" },
|
||||
{ type: "PG2L100H_FBG676", name: "PG2L100H FBG676芯片" },
|
||||
{ type: "BaseBoard", name: "通用底板" },
|
||||
];
|
||||
|
||||
// --- 可用虚拟外设列表 ---
|
||||
@@ -233,6 +235,13 @@ const availableTemplates = ref([
|
||||
path: "/EquipmentTemplates/PG2L100H_Pango100pro.json",
|
||||
thumbnailUrl: motherboardSvg,
|
||||
},
|
||||
{
|
||||
name: "矩阵键盘",
|
||||
id: "MatrixKey",
|
||||
description: "包含4x4,共16个按键的矩阵键盘",
|
||||
path: "/EquipmentTemplates/MatrixKey.json",
|
||||
thumbnailUrl: buttonSvg,
|
||||
},
|
||||
]);
|
||||
|
||||
// 显示/隐藏组件菜单
|
||||
@@ -249,7 +258,7 @@ async function loadComponentModule(type: string) {
|
||||
if (!componentModules.value[type]) {
|
||||
try {
|
||||
// 假设组件都在 src/components/equipments/ 目录下,且文件名与 type 相同
|
||||
const module = await import(`../components/equipments/${type}.vue`);
|
||||
const module = await import(`@/components/equipments/${type}.vue`);
|
||||
|
||||
// 将模块添加到缓存中
|
||||
componentModules.value = {
|
||||
@@ -372,6 +381,7 @@ async function addTemplate(template: any) {
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
template: templateData,
|
||||
capsPage: template.capsPage
|
||||
});
|
||||
|
||||
// 关闭菜单
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="flex-1 h-full w-full bg-base-200 relative overflow-hidden diagram-container" ref="canvasContainer"
|
||||
@mousedown="handleCanvasMouseDown" @mousedown.middle.prevent="startMiddleDrag" @wheel.prevent="onZoom"
|
||||
@contextmenu.prevent="handleContextMenu"> <!-- 工具栏 -->
|
||||
@contextmenu.prevent="handleContextMenu">
|
||||
<!-- 工具栏 -->
|
||||
<div class="absolute top-2 right-2 flex gap-2 z-30">
|
||||
<button class="btn btn-sm btn-primary" @click="openDiagramFileSelector">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
@@ -32,7 +33,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
{{ props.showDocPanel ? '属性面板' : '文档' }}
|
||||
{{ props.showDocPanel ? "属性面板" : "文档" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -64,25 +65,33 @@
|
||||
'component-disabled': !component.isOn,
|
||||
'component-hidepins': component.hidepins,
|
||||
}" :style="{
|
||||
top: component.y + 'px',
|
||||
left: component.x + 'px',
|
||||
zIndex: component.index ?? 0,
|
||||
transform: component.rotate
|
||||
? `rotate(${component.rotate}deg)`
|
||||
: 'none',
|
||||
opacity: component.isOn ? 1 : 0.6,
|
||||
display: 'block',
|
||||
}" @mousedown.left.stop="startComponentDrag($event, component)" @mouseover="hoveredComponent = component.id"
|
||||
@mouseleave="hoveredComponent = null">
|
||||
top: component.y + 'px',
|
||||
left: component.x + 'px',
|
||||
zIndex: component.index ?? 0,
|
||||
transform: component.rotate
|
||||
? `rotate(${component.rotate}deg)`
|
||||
: 'none',
|
||||
opacity: component.isOn ? 1 : 0.6,
|
||||
display: 'block',
|
||||
}" @mousedown.left.stop="startComponentDrag($event, component)" @mouseover="
|
||||
(event) => {
|
||||
hoveredComponent = component.id;
|
||||
}
|
||||
" @mouseleave="
|
||||
(event) => {
|
||||
hoveredComponent = null;
|
||||
}
|
||||
">
|
||||
<!-- 动态渲染组件 -->
|
||||
<component :is="getComponentDefinition(component.type)" v-if="props.componentModules[component.type]"
|
||||
v-bind="prepareComponentProps(component.attrs || {}, component.id)" @update:bindKey="
|
||||
<component :is="componentManager.getComponentDefinition(component.type)"
|
||||
v-if="componentManager.componentModules.value[component.type] && componentManager.getComponentDefinition(component.type)"
|
||||
v-bind="componentManager.prepareComponentProps(component.attrs || {}, component.id)" @update:bindKey="
|
||||
(value: string) =>
|
||||
updateComponentProp(component.id, 'bindKey', value)
|
||||
" @pin-click="
|
||||
(pinInfo: any) =>
|
||||
handlePinClick(component.id, pinInfo, pinInfo.originalEvent)
|
||||
" :ref="(el: any) => setComponentRef(component.id, el)" />
|
||||
" :ref="(el: any) => componentManager.setComponentRef(component.id, el)" />
|
||||
|
||||
<!-- Fallback if component module not loaded yet -->
|
||||
<div v-else
|
||||
@@ -90,6 +99,7 @@
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="loading loading-spinner loading-xs mb-1"></div>
|
||||
<span>Loading {{ component.type }}...</span>
|
||||
<small class="mt-1 text-xs">{{ componentManager.componentModules.value[component.type] ? 'Module loaded but invalid' : 'Module not found' }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,10 +108,10 @@
|
||||
<!-- 通知组件 -->
|
||||
<div v-if="showNotification" class="toast toast-top toast-center z-50 w-fit-content">
|
||||
<div :class="`alert ${notificationType === 'success'
|
||||
? 'alert-success'
|
||||
: notificationType === 'error'
|
||||
? 'alert-error'
|
||||
: 'alert-info'
|
||||
? 'alert-success'
|
||||
: notificationType === 'error'
|
||||
? 'alert-error'
|
||||
: 'alert-info'
|
||||
}`">
|
||||
<span>{{ notificationMessage }}</span>
|
||||
</div>
|
||||
@@ -128,7 +138,8 @@ import {
|
||||
watch,
|
||||
provide,
|
||||
} from "vue";
|
||||
import WireComponent from "./equipments/Wire.vue";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import WireComponent from "@/components/equipments/Wire.vue";
|
||||
|
||||
// 导入 diagram 管理器
|
||||
import {
|
||||
@@ -141,16 +152,17 @@ import {
|
||||
parseConnectionPin,
|
||||
connectionArrayToWireItem,
|
||||
validateDiagramData,
|
||||
} from "./diagramManager";
|
||||
} from "./composable/diagramManager";
|
||||
|
||||
import type {
|
||||
DiagramData,
|
||||
DiagramPart,
|
||||
ConnectionArray,
|
||||
WireItem,
|
||||
} from "./diagramManager";
|
||||
} from "./composable/diagramManager";
|
||||
|
||||
import { CanvasCurrentSelectedComponentID } from "./InjectKeys";
|
||||
import { CanvasCurrentSelectedComponentID } from "../InjectKeys";
|
||||
import { useComponentManager } from "./composable/componentManager";
|
||||
|
||||
// 右键菜单处理函数
|
||||
function handleContextMenu(e: MouseEvent) {
|
||||
@@ -160,22 +172,23 @@ function handleContextMenu(e: MouseEvent) {
|
||||
// 定义组件发出的事件
|
||||
const emit = defineEmits([
|
||||
"diagram-updated",
|
||||
"component-selected",
|
||||
"component-moved",
|
||||
"component-delete",
|
||||
"toggle-doc-panel",
|
||||
"wire-created",
|
||||
"wire-deleted",
|
||||
"load-component-module",
|
||||
"open-components",
|
||||
]);
|
||||
|
||||
// 定义组件接受的属性
|
||||
const props = defineProps<{
|
||||
componentModules: Record<string, any>;
|
||||
showDocPanel?: boolean; // 添加属性接收文档面板的显示状态
|
||||
showDocPanel?: boolean; // 添加属性接收文档面板的显示状态
|
||||
}>();
|
||||
|
||||
// 获取componentManager实例
|
||||
const componentManager = useComponentManager();
|
||||
if (!componentManager) {
|
||||
throw new Error("DiagramCanvas must be used within a component manager provider");
|
||||
}
|
||||
|
||||
// --- 画布状态 ---
|
||||
const canvasContainer = ref<HTMLElement | null>(null);
|
||||
const canvas = ref<HTMLElement | null>(null);
|
||||
@@ -201,8 +214,8 @@ const diagramData = ref<DiagramData>({
|
||||
connections: [],
|
||||
});
|
||||
|
||||
// 组件引用跟踪
|
||||
const componentRefs = ref<Record<string, any>>({});
|
||||
// 组件引用跟踪(保留以便向后兼容)
|
||||
const componentRefs = computed(() => componentManager?.componentRefs.value || {});
|
||||
|
||||
// 计算属性:从 diagramData 中提取组件列表,并按index属性排序
|
||||
const diagramParts = computed<DiagramPart[]>(() => {
|
||||
@@ -246,7 +259,7 @@ const wireItems = computed<WireItem[]>(() => {
|
||||
startPos.y = startComp.y;
|
||||
|
||||
// 尝试获取引脚精确位置(如果有实现)
|
||||
const startCompRef = componentRefs.value?.[startCompId];
|
||||
const startCompRef = componentManager?.getComponentRef(startCompId);
|
||||
if (startCompRef && typeof startCompRef.getPinPosition === "function") {
|
||||
try {
|
||||
const pinPos = startCompRef.getPinPosition(startPinId);
|
||||
@@ -272,7 +285,7 @@ const wireItems = computed<WireItem[]>(() => {
|
||||
endPos.y = endComp.y;
|
||||
|
||||
// 尝试获取引脚精确位置
|
||||
const endCompRef = componentRefs.value?.[endCompId];
|
||||
const endCompRef = componentManager?.getComponentRef(endCompId);
|
||||
if (endCompRef && typeof endCompRef.getPinPosition === "function") {
|
||||
try {
|
||||
const pinPos = endCompRef.getPinPosition(endPinId);
|
||||
@@ -317,6 +330,37 @@ const toastTimers: number[] = [];
|
||||
// 文件选择引用
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// VueUse事件监听器状态管理
|
||||
const isDragEventActive = ref(false);
|
||||
const isComponentDragEventActive = ref(false);
|
||||
const isWireCreationEventActive = ref(false);
|
||||
|
||||
// 使用VueUse设置事件监听器
|
||||
// 画布拖拽事件
|
||||
useEventListener(document, 'mousemove', (e: MouseEvent) => {
|
||||
if (isDragEventActive.value) {
|
||||
onDrag(e);
|
||||
}
|
||||
if (isComponentDragEventActive.value) {
|
||||
onComponentDrag(e);
|
||||
}
|
||||
if (isWireCreationEventActive.value) {
|
||||
onCreatingWireMouseMove(e);
|
||||
}
|
||||
});
|
||||
|
||||
useEventListener(document, 'mouseup', () => {
|
||||
if (isDragEventActive.value) {
|
||||
stopDrag();
|
||||
}
|
||||
if (isComponentDragEventActive.value) {
|
||||
stopComponentDrag();
|
||||
}
|
||||
});
|
||||
|
||||
// 键盘事件
|
||||
useEventListener(window, 'keydown', handleKeyDown);
|
||||
|
||||
// --- 缩放功能 ---
|
||||
const MIN_SCALE = 0.2;
|
||||
const MAX_SCALE = 10.0;
|
||||
@@ -354,73 +398,16 @@ function onZoom(e: WheelEvent) {
|
||||
scale.value = newScale;
|
||||
}
|
||||
|
||||
// --- 动态组件渲染 ---
|
||||
const getComponentDefinition = (type: string) => {
|
||||
const module = props.componentModules[type];
|
||||
if (!module) return null;
|
||||
|
||||
// 确保我们返回一个有效的组件定义
|
||||
if (module.default) {
|
||||
return module.default;
|
||||
} else if (module.__esModule && module.default) {
|
||||
// 有时 Vue 的动态导入会将默认导出包装在 __esModule 属性下
|
||||
return module.default;
|
||||
} else {
|
||||
console.warn(
|
||||
`Module for ${type} found but default export is missing`,
|
||||
module,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
function prepareComponentProps(
|
||||
attrs: Record<string, any>,
|
||||
componentId?: string,
|
||||
): Record<string, any> {
|
||||
const result: Record<string, any> = { ...attrs };
|
||||
if (componentId) {
|
||||
result.componentId = componentId;
|
||||
}
|
||||
// console.log(`组件属性 ID: ${componentId}`, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 设置组件引用
|
||||
function setComponentRef(componentId: string, el: any) {
|
||||
if (componentRefs.value) {
|
||||
if (el) {
|
||||
componentRefs.value[componentId] = el;
|
||||
} else {
|
||||
delete componentRefs.value[componentId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重置组件引用缓存
|
||||
function resetComponentRefs() {
|
||||
componentRefs.value = {};
|
||||
}
|
||||
|
||||
// 加载组件模块
|
||||
async function loadComponentModule(type: string) {
|
||||
if (!props.componentModules[type]) {
|
||||
try {
|
||||
// 通知父组件需要加载此类型的组件
|
||||
emit("load-component-module", type);
|
||||
} catch (error) {
|
||||
console.error(`Failed to request component module ${type}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 画布交互逻辑 ---
|
||||
function handleCanvasMouseDown(e: MouseEvent) {
|
||||
// 如果是直接点击画布(而不是元器件),清除选中状态
|
||||
if (e.target === canvasContainer.value || e.target === canvas.value) {
|
||||
if (selectedComponentId.value !== null) {
|
||||
selectedComponentId.value = null;
|
||||
emit("component-selected", null);
|
||||
// 直接通过componentManager选择组件
|
||||
if (componentManager) {
|
||||
componentManager.selectComponent(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,8 +429,7 @@ function startDrag(e: MouseEvent) {
|
||||
dragStart.x = e.clientX - position.x;
|
||||
dragStart.y = e.clientY - position.y;
|
||||
|
||||
document.addEventListener("mousemove", onDrag);
|
||||
document.addEventListener("mouseup", stopDrag);
|
||||
isDragEventActive.value = true;
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
@@ -458,8 +444,7 @@ function startMiddleDrag(e: MouseEvent) {
|
||||
dragStart.x = e.clientX - position.x;
|
||||
dragStart.y = e.clientY - position.y;
|
||||
|
||||
document.addEventListener("mousemove", onDrag);
|
||||
document.addEventListener("mouseup", stopDrag);
|
||||
isDragEventActive.value = true;
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
@@ -475,9 +460,7 @@ function onDrag(e: MouseEvent) {
|
||||
function stopDrag() {
|
||||
isDragging.value = false;
|
||||
isMiddleDragging.value = false;
|
||||
|
||||
document.removeEventListener("mousemove", onDrag);
|
||||
document.removeEventListener("mouseup", stopDrag);
|
||||
isDragEventActive.value = false;
|
||||
}
|
||||
|
||||
// --- 组件拖拽交互 ---
|
||||
@@ -497,7 +480,10 @@ function startComponentDrag(e: MouseEvent, component: DiagramPart) {
|
||||
// 仍然选中组件,无论是否为交互元素
|
||||
if (selectedComponentId.value !== component.id) {
|
||||
selectedComponentId.value = component.id;
|
||||
emit("component-selected", component);
|
||||
// 直接通过componentManager选择组件
|
||||
if (componentManager) {
|
||||
componentManager.selectComponent(component);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果组件锁定位置或是交互元素,则不启动拖拽
|
||||
@@ -529,9 +515,8 @@ function startComponentDrag(e: MouseEvent, component: DiagramPart) {
|
||||
componentDragOffset.x = mouseX_canvas - component.x;
|
||||
componentDragOffset.y = mouseY_canvas - component.y;
|
||||
|
||||
// 添加全局监听器
|
||||
document.addEventListener("mousemove", onComponentDrag);
|
||||
document.addEventListener("mouseup", stopComponentDrag);
|
||||
// 激活组件拖拽事件监听
|
||||
isComponentDragEventActive.value = true;
|
||||
}
|
||||
|
||||
// 拖拽组件过程
|
||||
@@ -560,14 +545,6 @@ function onComponentDrag(e: MouseEvent) {
|
||||
);
|
||||
if (!draggedComponent) return;
|
||||
|
||||
// 更新组件位置
|
||||
// diagramData.value = updatePartPosition(
|
||||
// diagramData.value,
|
||||
// draggingComponentId.value,
|
||||
// Math.round(newX),
|
||||
// Math.round(newY),
|
||||
// );
|
||||
|
||||
// 如果组件属于组,移动组内所有其他组件
|
||||
if (draggedComponent.group) {
|
||||
const deltaX = Math.round(newX) - draggedComponent.x;
|
||||
@@ -592,12 +569,14 @@ function onComponentDrag(e: MouseEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// 通知父组件位置已更新
|
||||
emit("component-moved", {
|
||||
id: draggingComponentId.value,
|
||||
x: Math.round(newX),
|
||||
y: Math.round(newY),
|
||||
});
|
||||
// 通知componentManager位置已更新
|
||||
if (componentManager) {
|
||||
componentManager.moveComponent({
|
||||
id: draggingComponentId.value,
|
||||
x: Math.round(newX),
|
||||
y: Math.round(newY),
|
||||
});
|
||||
}
|
||||
|
||||
// 通知图表已更新
|
||||
emit("diagram-updated", diagramData.value);
|
||||
@@ -616,8 +595,7 @@ function stopComponentDrag() {
|
||||
draggingComponentId.value = null;
|
||||
}
|
||||
|
||||
document.removeEventListener("mousemove", onComponentDrag);
|
||||
document.removeEventListener("mouseup", stopComponentDrag);
|
||||
isComponentDragEventActive.value = false;
|
||||
}
|
||||
|
||||
// 更新组件属性
|
||||
@@ -626,14 +604,20 @@ function updateComponentProp(
|
||||
propName: string,
|
||||
value: any,
|
||||
) {
|
||||
diagramData.value = updatePartAttribute(
|
||||
diagramData.value,
|
||||
componentId,
|
||||
propName,
|
||||
value,
|
||||
);
|
||||
emit("diagram-updated", diagramData.value);
|
||||
saveDiagramData(diagramData.value);
|
||||
// 直接通过componentManager更新组件属性
|
||||
if (componentManager) {
|
||||
componentManager.updateComponentProp(componentId, propName, value);
|
||||
} else {
|
||||
// 后备方案:直接更新数据
|
||||
diagramData.value = updatePartAttribute(
|
||||
diagramData.value,
|
||||
componentId,
|
||||
propName,
|
||||
value,
|
||||
);
|
||||
emit("diagram-updated", diagramData.value);
|
||||
saveDiagramData(diagramData.value);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 连线操作 ---
|
||||
@@ -663,8 +647,6 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
|
||||
console.log("鼠标位置:", mousePosition);
|
||||
|
||||
if (!isCreatingWire.value) {
|
||||
// 开始创建连线
|
||||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
||||
// 获取初始位置信息
|
||||
let pinPosition = pinInfo.position;
|
||||
console.log("Pin信息:", pinInfo);
|
||||
@@ -673,7 +655,7 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
|
||||
console.log("引脚ID:", pinId);
|
||||
|
||||
// 从组件引用中获取组件实例
|
||||
const component = componentRefs.value[componentId];
|
||||
const component = componentManager?.getComponentRef(componentId);
|
||||
console.log("组件引用:", component);
|
||||
|
||||
// 查找组件部件对象以获取组件位置
|
||||
@@ -734,7 +716,7 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
|
||||
pinId,
|
||||
pinInfo.constraint,
|
||||
);
|
||||
document.addEventListener("mousemove", onCreatingWireMouseMove);
|
||||
isWireCreationEventActive.value = true;
|
||||
} else {
|
||||
// 完成连线创建
|
||||
if (
|
||||
@@ -749,7 +731,7 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
|
||||
// 获取终点引脚位置
|
||||
let endPosition = { x: 0, y: 0 };
|
||||
const componentPart = diagramParts.value.find((p) => p.id === componentId);
|
||||
const endComponent = componentRefs.value[componentId];
|
||||
const endComponent = componentManager?.getComponentRef(componentId);
|
||||
|
||||
console.log("终点组件部件:", componentPart);
|
||||
console.log("终点组件引用:", endComponent);
|
||||
@@ -811,7 +793,7 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
|
||||
|
||||
// 重置连线创建状态
|
||||
resetWireCreation();
|
||||
document.removeEventListener("mousemove", onCreatingWireMouseMove);
|
||||
isWireCreationEventActive.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -844,7 +826,7 @@ function resetWireCreation() {
|
||||
// 取消连线创建
|
||||
function cancelWireCreation() {
|
||||
resetWireCreation();
|
||||
document.removeEventListener("mousemove", onCreatingWireMouseMove);
|
||||
isWireCreationEventActive.value = false;
|
||||
}
|
||||
|
||||
// 连线创建过程中的鼠标移动
|
||||
@@ -863,7 +845,10 @@ function deleteWire(wireIndex: number) {
|
||||
// 删除组件
|
||||
function deleteComponent(componentId: string) {
|
||||
diagramData.value = deletePart(diagramData.value, componentId);
|
||||
emit("component-delete", componentId);
|
||||
// 直接通过componentManager删除组件
|
||||
if (componentManager) {
|
||||
componentManager.deleteComponent(componentId);
|
||||
}
|
||||
emit("diagram-updated", diagramData.value);
|
||||
saveDiagramData(diagramData.value);
|
||||
|
||||
@@ -1013,8 +998,23 @@ function showToast(
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(async () => {
|
||||
// 重置组件引用
|
||||
resetComponentRefs();
|
||||
// 设置componentManager的画布引用
|
||||
if (componentManager) {
|
||||
// 创建一个包含必要方法的画布API对象
|
||||
const canvasAPI = {
|
||||
getDiagramData: () => diagramData.value,
|
||||
updateDiagramDataDirectly: (data: DiagramData) => {
|
||||
diagramData.value = data;
|
||||
saveDiagramData(data);
|
||||
emit("diagram-updated", data);
|
||||
},
|
||||
getCanvasPosition: () => ({ x: position.x, y: position.y }),
|
||||
getScale: () => scale.value,
|
||||
$el: canvasContainer.value,
|
||||
showToast
|
||||
};
|
||||
componentManager.setCanvasRef(canvasAPI);
|
||||
}
|
||||
|
||||
// 加载图表数据
|
||||
try {
|
||||
@@ -1031,12 +1031,10 @@ onMounted(async () => {
|
||||
Array.from(componentTypes),
|
||||
);
|
||||
|
||||
// 通知父组件需要加载组件模块
|
||||
componentTypes.forEach((type) => {
|
||||
if (!props.componentModules[type]) {
|
||||
emit("load-component-module", type);
|
||||
}
|
||||
});
|
||||
// 直接通过componentManager预加载组件模块
|
||||
if (componentManager) {
|
||||
await componentManager.preloadComponentModules(Array.from(componentTypes));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载图表数据失败:", error);
|
||||
}
|
||||
@@ -1046,9 +1044,6 @@ onMounted(async () => {
|
||||
position.x = canvasContainer.value.clientWidth / 2 - 5000; // 画布宽度的一半
|
||||
position.y = canvasContainer.value.clientHeight / 2 - 5000; // 画布高度的一半
|
||||
}
|
||||
|
||||
// 添加键盘事件监听器
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
});
|
||||
|
||||
// 处理键盘事件
|
||||
@@ -1067,32 +1062,15 @@ function handleKeyDown(e: KeyboardEvent) {
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理事件监听器
|
||||
document.removeEventListener("mousemove", onComponentDrag);
|
||||
document.removeEventListener("mouseup", stopComponentDrag);
|
||||
document.removeEventListener("mousemove", onDrag);
|
||||
document.removeEventListener("mouseup", stopDrag);
|
||||
document.removeEventListener("mousemove", onCreatingWireMouseMove);
|
||||
|
||||
// 移除键盘事件监听器
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
|
||||
// 清除所有toast定时器
|
||||
toastTimers.forEach((timerId) => clearTimeout(timerId));
|
||||
|
||||
// 重置事件状态
|
||||
isDragEventActive.value = false;
|
||||
isComponentDragEventActive.value = false;
|
||||
isWireCreationEventActive.value = false;
|
||||
});
|
||||
|
||||
// --- 对外API ---
|
||||
// 获取当前图表数据
|
||||
function getDiagramData() {
|
||||
return diagramData.value;
|
||||
}
|
||||
|
||||
// 设置图表数据
|
||||
function setDiagramData(data: DiagramData) {
|
||||
diagramData.value = data;
|
||||
emit("diagram-updated", data);
|
||||
}
|
||||
|
||||
// 无加载动画的数据更新方法
|
||||
function updateDiagramDataDirectly(data: DiagramData) {
|
||||
// 检查组件是否仍然挂载
|
||||
@@ -1107,7 +1085,7 @@ function updateDiagramDataDirectly(data: DiagramData) {
|
||||
emit("diagram-updated", data);
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
// 暴露方法给父组件 - 简化版本,主要用于数据访问
|
||||
defineExpose({
|
||||
// 基本数据操作
|
||||
getDiagramData: () => diagramData.value,
|
||||
@@ -1146,23 +1124,6 @@ defineExpose({
|
||||
});
|
||||
},
|
||||
|
||||
// 文件操作
|
||||
openDiagramFileSelector,
|
||||
exportDiagram,
|
||||
|
||||
// 组件操作
|
||||
getSelectedComponent: () => {
|
||||
if (!selectedComponentId.value) return null;
|
||||
return (
|
||||
diagramParts.value.find((p) => p.id === selectedComponentId.value) || null
|
||||
);
|
||||
},
|
||||
deleteSelectedComponent: () => {
|
||||
if (selectedComponentId.value) {
|
||||
deleteComponent(selectedComponentId.value);
|
||||
}
|
||||
},
|
||||
|
||||
// 画布状态
|
||||
getCanvasPosition: () => ({ x: position.x, y: position.y }),
|
||||
getScale: () => scale.value,
|
||||
@@ -1179,15 +1140,6 @@ watch(
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// 当组件模块加载完成后,确保组件引用正确建立
|
||||
watch(
|
||||
() => props.componentModules,
|
||||
() => {
|
||||
// 这里不需要特别处理,Vue 会自动通过 setComponentRef 函数更新引用
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
646
src/components/LabCanvas/composable/componentManager.ts
Normal file
646
src/components/LabCanvas/composable/componentManager.ts
Normal file
@@ -0,0 +1,646 @@
|
||||
import { ref, shallowRef, computed } from "vue";
|
||||
import { createInjectionState } from "@vueuse/core";
|
||||
import type { DiagramData, DiagramPart } from "./diagramManager";
|
||||
import type { PropertyConfig } from "@/components/equipments/componentConfig";
|
||||
import {
|
||||
generatePropertyConfigs,
|
||||
generatePropsFromDefault,
|
||||
generatePropsFromAttrs,
|
||||
} from "@/components/equipments/componentConfig";
|
||||
|
||||
// 存储动态导入的组件模块
|
||||
interface ComponentModule {
|
||||
default: any;
|
||||
getDefaultProps?: () => Record<string, any>;
|
||||
config?: {
|
||||
props?: Array<PropertyConfig>;
|
||||
};
|
||||
__esModule?: boolean; // 添加 __esModule 属性
|
||||
}
|
||||
|
||||
// 定义组件管理器的状态和方法
|
||||
const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
() => {
|
||||
// --- 状态管理 ---
|
||||
const componentModules = ref<Record<string, ComponentModule>>({});
|
||||
const selectedComponentId = ref<string | null>(null);
|
||||
const selectedComponentConfig = shallowRef<{ props: PropertyConfig[] } | null>(null);
|
||||
const diagramCanvas = ref<any>(null);
|
||||
const componentRefs = ref<Record<string, any>>({});
|
||||
|
||||
// 计算当前选中的组件数据
|
||||
const selectedComponentData = computed(() => {
|
||||
if (!diagramCanvas.value || !selectedComponentId.value) return null;
|
||||
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (canvasInstance && canvasInstance.getDiagramData) {
|
||||
const data = canvasInstance.getDiagramData();
|
||||
return data.parts.find((p: DiagramPart) => p.id === selectedComponentId.value) || null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// --- 组件模块管理 ---
|
||||
|
||||
/**
|
||||
* 动态加载组件模块
|
||||
*/
|
||||
async function loadComponentModule(type: string) {
|
||||
console.log(`尝试加载组件模块: ${type}`);
|
||||
console.log(`当前已加载的模块:`, Object.keys(componentModules.value));
|
||||
|
||||
if (!componentModules.value[type]) {
|
||||
try {
|
||||
console.log(`正在动态导入模块: @/components/equipments/${type}.vue`);
|
||||
const module = await import(`@/components/equipments/${type}.vue`);
|
||||
console.log(`成功导入模块 ${type}:`, module);
|
||||
|
||||
// 直接设置新的对象引用以触发响应性
|
||||
componentModules.value = {
|
||||
...componentModules.value,
|
||||
[type]: module,
|
||||
};
|
||||
console.log(`模块 ${type} 已添加到 componentModules`);
|
||||
console.log(`更新后的模块列表:`, Object.keys(componentModules.value));
|
||||
} catch (error) {
|
||||
console.error(`Failed to load component module ${type}:`, error);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
console.log(`模块 ${type} 已经存在`);
|
||||
}
|
||||
return componentModules.value[type];
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载所有组件模块
|
||||
*/
|
||||
async function preloadComponentModules(componentTypes: string[]) {
|
||||
console.log("Preloading component modules:", componentTypes);
|
||||
await Promise.all(
|
||||
componentTypes.map((type) => loadComponentModule(type))
|
||||
);
|
||||
console.log("All component modules loaded");
|
||||
}
|
||||
|
||||
// --- 组件操作 ---
|
||||
|
||||
/**
|
||||
* 添加新组件到画布
|
||||
*/
|
||||
async function addComponent(componentData: {
|
||||
type: string;
|
||||
name: string;
|
||||
props: Record<string, any>;
|
||||
}) {
|
||||
console.log("=== 开始添加组件 ===");
|
||||
console.log("组件数据:", componentData);
|
||||
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance) {
|
||||
console.error("没有可用的画布实例");
|
||||
return;
|
||||
}
|
||||
|
||||
// 预加载组件模块,确保组件能正常渲染
|
||||
console.log(`预加载组件模块: ${componentData.type}`);
|
||||
const componentModule = await loadComponentModule(componentData.type);
|
||||
if (!componentModule) {
|
||||
console.error(`无法加载组件模块: ${componentData.type}`);
|
||||
return;
|
||||
}
|
||||
console.log(`组件模块加载成功: ${componentData.type}`, componentModule);
|
||||
|
||||
// 获取画布位置信息
|
||||
let position = { x: 100, y: 100 };
|
||||
let scale = 1;
|
||||
|
||||
try {
|
||||
if (canvasInstance.getCanvasPosition && canvasInstance.getScale) {
|
||||
position = canvasInstance.getCanvasPosition();
|
||||
scale = canvasInstance.getScale();
|
||||
|
||||
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||
if (canvasContainer) {
|
||||
const viewportWidth = canvasContainer.clientWidth;
|
||||
const viewportHeight = canvasContainer.clientHeight;
|
||||
|
||||
position.x = (viewportWidth / 2 - position.x) / scale;
|
||||
position.y = (viewportHeight / 2 - position.y) / scale;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取画布位置时出错:", error);
|
||||
}
|
||||
|
||||
// 添加随机偏移
|
||||
const offsetX = Math.floor(Math.random() * 100) - 50;
|
||||
const offsetY = Math.floor(Math.random() * 100) - 50;
|
||||
|
||||
// 获取组件能力页面
|
||||
let capsPage = null;
|
||||
if (
|
||||
componentModule &&
|
||||
componentModule.default &&
|
||||
typeof componentModule.default.getCapabilities === "function"
|
||||
) {
|
||||
try {
|
||||
capsPage = componentModule.default.getCapabilities();
|
||||
console.log(`获取到${componentData.type}组件的能力页面`);
|
||||
} catch (error) {
|
||||
console.error(`获取${componentData.type}组件能力页面失败:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新组件
|
||||
const newComponent: DiagramPart = {
|
||||
id: `component-${Date.now()}`,
|
||||
type: componentData.type,
|
||||
x: Math.round(position.x + offsetX),
|
||||
y: Math.round(position.y + offsetY),
|
||||
attrs: componentData.props,
|
||||
rotate: 0,
|
||||
group: "",
|
||||
positionlock: false,
|
||||
hidepins: true,
|
||||
isOn: true,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
// 通过画布实例添加组件
|
||||
if (canvasInstance.getDiagramData && canvasInstance.updateDiagramDataDirectly) {
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
currentData.parts.push(newComponent);
|
||||
|
||||
// 使用 updateDiagramDataDirectly 避免触发加载状态
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
|
||||
console.log("组件添加完成:", newComponent);
|
||||
|
||||
// 等待Vue的下一个tick,确保组件模块已经更新
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加模板到画布
|
||||
*/
|
||||
async function addTemplate(templateData: {
|
||||
id: string;
|
||||
name: string;
|
||||
template: any;
|
||||
}) {
|
||||
console.log("添加模板:", templateData);
|
||||
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||
console.error("没有可用的画布实例添加模板");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
console.log("=== 当前图表组件数量:", currentData.parts.length);
|
||||
|
||||
// 生成唯一ID前缀
|
||||
const idPrefix = `template-${Date.now()}-`;
|
||||
|
||||
if (templateData.template?.parts) {
|
||||
// 获取视口中心位置
|
||||
let viewportCenter = { x: 300, y: 200 };
|
||||
try {
|
||||
if (canvasInstance.getCanvasPosition && canvasInstance.getScale) {
|
||||
const position = canvasInstance.getCanvasPosition();
|
||||
const scale = canvasInstance.getScale();
|
||||
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||
|
||||
if (canvasContainer) {
|
||||
const viewportWidth = canvasContainer.clientWidth;
|
||||
const viewportHeight = canvasContainer.clientHeight;
|
||||
viewportCenter.x = (viewportWidth / 2 - position.x) / scale;
|
||||
viewportCenter.y = (viewportHeight / 2 - position.y) / scale;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取视口中心位置时出错:", error);
|
||||
}
|
||||
|
||||
const mainPart = templateData.template.parts[0];
|
||||
|
||||
// 创建新组件
|
||||
const newParts = await Promise.all(
|
||||
templateData.template.parts.map(async (part: any) => {
|
||||
const newPart = JSON.parse(JSON.stringify(part));
|
||||
newPart.id = `${idPrefix}${part.id}`;
|
||||
|
||||
// 加载组件模块并获取能力页面
|
||||
try {
|
||||
const componentModule = await loadComponentModule(part.type);
|
||||
if (
|
||||
componentModule?.default &&
|
||||
typeof componentModule.default.getCapabilities === "function"
|
||||
) {
|
||||
newPart.capsPage = componentModule.default.getCapabilities();
|
||||
console.log(`加载模板组件${part.type}组件的能力页面成功`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`加载模板组件${part.type}的能力页面失败:`, error);
|
||||
}
|
||||
|
||||
// 计算新位置
|
||||
if (typeof newPart.x === "number" && typeof newPart.y === "number") {
|
||||
const relativeX = part.x - mainPart.x;
|
||||
const relativeY = part.y - mainPart.y;
|
||||
newPart.x = viewportCenter.x + relativeX;
|
||||
newPart.y = viewportCenter.y + relativeY;
|
||||
}
|
||||
|
||||
return newPart;
|
||||
})
|
||||
);
|
||||
|
||||
currentData.parts.push(...newParts);
|
||||
|
||||
// 处理连接关系
|
||||
if (templateData.template.connections) {
|
||||
const idMap: Record<string, string> = {};
|
||||
templateData.template.parts.forEach((part: any) => {
|
||||
idMap[part.id] = `${idPrefix}${part.id}`;
|
||||
});
|
||||
|
||||
const newConnections = templateData.template.connections.map((conn: any) => {
|
||||
if (Array.isArray(conn)) {
|
||||
const [from, to, type, path] = conn;
|
||||
const fromParts = from.split(":");
|
||||
const toParts = to.split(":");
|
||||
|
||||
if (fromParts.length === 2 && toParts.length === 2) {
|
||||
const fromComponentId = fromParts[0];
|
||||
const fromPinId = fromParts[1];
|
||||
const toComponentId = toParts[0];
|
||||
const toPinId = toParts[1];
|
||||
|
||||
const newFrom = `${idMap[fromComponentId] || fromComponentId}:${fromPinId}`;
|
||||
const newTo = `${idMap[toComponentId] || toComponentId}:${toPinId}`;
|
||||
|
||||
return [newFrom, newTo, type, path];
|
||||
}
|
||||
}
|
||||
return conn;
|
||||
});
|
||||
|
||||
currentData.connections.push(...newConnections);
|
||||
}
|
||||
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
console.log("=== 更新图表数据完成,新组件数量:", currentData.parts.length);
|
||||
|
||||
return { success: true, message: `已添加 ${templateData.name} 模板` };
|
||||
} else {
|
||||
console.error("模板格式错误,缺少parts数组");
|
||||
return { success: false, message: "模板格式错误" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除组件
|
||||
*/
|
||||
function deleteComponent(componentId: string) {
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
const component = currentData.parts.find((p: DiagramPart) => p.id === componentId);
|
||||
|
||||
if (!component) return;
|
||||
|
||||
const componentsToDelete: string[] = [componentId];
|
||||
|
||||
// 处理组件组
|
||||
if (component.group && component.group !== "") {
|
||||
const groupMembers = currentData.parts.filter(
|
||||
(p: DiagramPart) => p.group === component.group && p.id !== componentId
|
||||
);
|
||||
componentsToDelete.push(...groupMembers.map((p: DiagramPart) => p.id));
|
||||
console.log(`删除组件 ${componentId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`);
|
||||
}
|
||||
|
||||
// 删除组件
|
||||
currentData.parts = currentData.parts.filter(
|
||||
(p: DiagramPart) => !componentsToDelete.includes(p.id)
|
||||
);
|
||||
|
||||
// 删除相关连接
|
||||
currentData.connections = currentData.connections.filter((connection: any) => {
|
||||
for (const id of componentsToDelete) {
|
||||
if (connection[0].startsWith(`${id}:`) || connection[1].startsWith(`${id}:`)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// 清除选中状态
|
||||
if (selectedComponentId.value && componentsToDelete.includes(selectedComponentId.value)) {
|
||||
selectedComponentId.value = null;
|
||||
selectedComponentConfig.value = null;
|
||||
}
|
||||
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中组件
|
||||
*/
|
||||
async function selectComponent(componentData: DiagramPart | null) {
|
||||
selectedComponentId.value = componentData ? componentData.id : null;
|
||||
selectedComponentConfig.value = null;
|
||||
|
||||
if (componentData) {
|
||||
const moduleRef = await loadComponentModule(componentData.type);
|
||||
|
||||
if (moduleRef) {
|
||||
try {
|
||||
const propConfigs: PropertyConfig[] = [];
|
||||
const addedProps = new Set<string>();
|
||||
|
||||
// 从 getDefaultProps 方法获取默认配置
|
||||
if (typeof moduleRef.getDefaultProps === "function") {
|
||||
const defaultProps = moduleRef.getDefaultProps();
|
||||
const defaultPropConfigs = generatePropsFromDefault(defaultProps);
|
||||
defaultPropConfigs.forEach((config) => {
|
||||
propConfigs.push(config);
|
||||
addedProps.add(config.name);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加组件直接属性
|
||||
const directPropConfigs = generatePropertyConfigs(componentData);
|
||||
const newDirectProps = directPropConfigs.filter(
|
||||
(config) => !addedProps.has(config.name)
|
||||
);
|
||||
propConfigs.push(...newDirectProps);
|
||||
|
||||
// 添加 attrs 中的属性
|
||||
if (componentData.attrs) {
|
||||
const attrPropConfigs = generatePropsFromAttrs(componentData.attrs);
|
||||
attrPropConfigs.forEach((attrConfig) => {
|
||||
const existingIndex = propConfigs.findIndex(
|
||||
(p) => p.name === attrConfig.name
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
propConfigs[existingIndex] = attrConfig;
|
||||
} else {
|
||||
propConfigs.push(attrConfig);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectedComponentConfig.value = { props: propConfigs };
|
||||
console.log(`Built config for ${componentData.type}:`, selectedComponentConfig.value);
|
||||
} catch (error) {
|
||||
console.error(`Error building config for ${componentData.type}:`, error);
|
||||
selectedComponentConfig.value = { props: [] };
|
||||
}
|
||||
} else {
|
||||
console.warn(`Module for component ${componentData.type} not found.`);
|
||||
selectedComponentConfig.value = { props: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新组件属性
|
||||
*/
|
||||
function updateComponentProp(componentId: string, propName: string, value: any) {
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||
console.error("没有可用的画布实例进行属性更新");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查值格式
|
||||
if (value !== null && typeof value === "object" && "value" in value) {
|
||||
value = value.value;
|
||||
}
|
||||
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
|
||||
|
||||
if (part) {
|
||||
if (propName in part) {
|
||||
(part as any)[propName] = value;
|
||||
} else {
|
||||
if (!part.attrs) {
|
||||
part.attrs = {};
|
||||
}
|
||||
part.attrs[propName] = value;
|
||||
}
|
||||
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
console.log(`更新组件${componentId}的属性${propName}为:`, value, typeof value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新组件直接属性
|
||||
*/
|
||||
function updateComponentDirectProp(componentId: string, propName: string, value: any) {
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||
console.error("没有可用的画布实例进行属性更新");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
|
||||
|
||||
if (part) {
|
||||
(part as any)[propName] = value;
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
console.log(`更新组件${componentId}的直接属性${propName}为:`, value, typeof value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动组件
|
||||
*/
|
||||
function moveComponent(moveData: { id: string; x: number; y: number }) {
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
const part = currentData.parts.find((p: DiagramPart) => p.id === moveData.id);
|
||||
if (part) {
|
||||
part.x = moveData.x;
|
||||
part.y = moveData.y;
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置画布实例引用
|
||||
*/
|
||||
function setCanvasRef(canvasRef: any) {
|
||||
diagramCanvas.value = canvasRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置组件DOM引用
|
||||
*/
|
||||
function setComponentRef(componentId: string, el: any) {
|
||||
if (el) {
|
||||
componentRefs.value[componentId] = el;
|
||||
} else {
|
||||
delete componentRefs.value[componentId];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件DOM引用
|
||||
*/
|
||||
function getComponentRef(componentId: string) {
|
||||
return componentRefs.value[componentId];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前图表数据
|
||||
*/
|
||||
function getDiagramData() {
|
||||
const canvasInstance = diagramCanvas.value;
|
||||
if (canvasInstance && canvasInstance.getDiagramData) {
|
||||
return canvasInstance.getDiagramData();
|
||||
}
|
||||
return { parts: [], connections: [], version: 1, author: "admin", editor: "me" };
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新图表数据
|
||||
*/
|
||||
function updateDiagramData(data: any) {
|
||||
const canvasInstance = diagramCanvas.value;
|
||||
if (canvasInstance && canvasInstance.updateDiagramDataDirectly) {
|
||||
canvasInstance.updateDiagramDataDirectly(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取画布位置和缩放信息
|
||||
*/
|
||||
function getCanvasInfo() {
|
||||
const canvasInstance = diagramCanvas.value;
|
||||
if (!canvasInstance) return { position: { x: 0, y: 0 }, scale: 1 };
|
||||
|
||||
const position = canvasInstance.getCanvasPosition ? canvasInstance.getCanvasPosition() : { x: 0, y: 0 };
|
||||
const scale = canvasInstance.getScale ? canvasInstance.getScale() : 1;
|
||||
|
||||
return { position, scale };
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示通知
|
||||
*/
|
||||
function showToast(message: string, type: "success" | "error" | "info" = "info") {
|
||||
const canvasInstance = diagramCanvas.value;
|
||||
if (canvasInstance && canvasInstance.showToast) {
|
||||
canvasInstance.showToast(message, type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件定义
|
||||
*/
|
||||
function getComponentDefinition(type: string) {
|
||||
const module = componentModules.value[type];
|
||||
|
||||
if (!module) {
|
||||
console.warn(`No module found for component type: ${type}`);
|
||||
// 尝试异步加载组件模块
|
||||
loadComponentModule(type);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 确保我们返回一个有效的组件定义
|
||||
if (module.default) {
|
||||
return module.default;
|
||||
} else if (module.__esModule && module.default) {
|
||||
// 有时 Vue 的动态导入会将默认导出包装在 __esModule 属性下
|
||||
return module.default;
|
||||
} else {
|
||||
console.warn(
|
||||
`Module for ${type} found but default export is missing`,
|
||||
module,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备组件属性
|
||||
*/
|
||||
function prepareComponentProps(
|
||||
attrs: Record<string, any>,
|
||||
componentId?: string,
|
||||
): Record<string, any> {
|
||||
const result: Record<string, any> = { ...attrs };
|
||||
if (componentId) {
|
||||
result.componentId = componentId;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化组件管理器
|
||||
*/
|
||||
async function initialize() {
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (canvasInstance?.getDiagramData) {
|
||||
const diagramData = canvasInstance.getDiagramData();
|
||||
|
||||
// 收集所有组件类型
|
||||
const componentTypes = new Set<string>();
|
||||
diagramData.parts.forEach((part: DiagramPart) => {
|
||||
componentTypes.add(part.type);
|
||||
});
|
||||
|
||||
// 预加载组件模块
|
||||
await preloadComponentModules(Array.from(componentTypes));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
componentModules,
|
||||
selectedComponentId,
|
||||
selectedComponentData,
|
||||
selectedComponentConfig,
|
||||
componentRefs,
|
||||
|
||||
// 方法
|
||||
loadComponentModule,
|
||||
preloadComponentModules,
|
||||
addComponent,
|
||||
addTemplate,
|
||||
deleteComponent,
|
||||
selectComponent,
|
||||
updateComponentProp,
|
||||
updateComponentDirectProp,
|
||||
moveComponent,
|
||||
setCanvasRef,
|
||||
setComponentRef,
|
||||
getComponentRef,
|
||||
getDiagramData,
|
||||
updateDiagramData,
|
||||
getCanvasInfo,
|
||||
showToast,
|
||||
getComponentDefinition,
|
||||
prepareComponentProps,
|
||||
initialize,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export { useProvideComponentManager, useComponentManager };
|
||||
@@ -83,4 +83,3 @@ export function setMousePosition(x: number, y: number) {
|
||||
mousePosition.y = y;
|
||||
}
|
||||
|
||||
// 其它Wire相关操作可继续扩展...
|
||||
8
src/components/LabCanvas/index.ts
Normal file
8
src/components/LabCanvas/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// 导出组件管理器服务
|
||||
export { useProvideComponentManager, useComponentManager } from './composable/componentManager';
|
||||
|
||||
// 导出图表管理器
|
||||
export type { DiagramData, DiagramPart } from './composable/diagramManager';
|
||||
|
||||
// 导出连线管理器
|
||||
export type { WireItem } from './composable/wireManager';
|
||||
@@ -128,7 +128,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import CollapsibleSection from "./CollapsibleSection.vue";
|
||||
import { type DiagramPart } from "@/components/diagramManager";
|
||||
import { type DiagramPart } from "@/components/LabCanvas/composable/diagramManager";
|
||||
import {
|
||||
type PropertyConfig,
|
||||
getPropValue,
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
// 导入所需的类型和组件
|
||||
import { type DiagramPart } from "@/components/diagramManager"; // 图表部件类型定义
|
||||
import { type DiagramPart } from "@/components/LabCanvas/composable/diagramManager"; // 图表部件类型定义
|
||||
import { type PropertyConfig } from "@/components/equipments/componentConfig"; // 属性配置类型定义
|
||||
import CollapsibleSection from "./CollapsibleSection.vue"; // 可折叠区域组件
|
||||
import PropertyEditor from "./PropertyEditor.vue"; // 属性编辑器组件
|
||||
|
||||
42
src/components/equipments/BaseBoard.vue
Normal file
42
src/components/equipments/BaseBoard.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" :class="$attrs">
|
||||
<rect :width="width" :height="height" :rx="props.roundCorner" fill="#222222" />
|
||||
</svg>
|
||||
<Teleport to="#ComponentCapabilities" v-if="selectecComponentID === props.componentId && !!slot.default">
|
||||
<slot></slot>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, inject } from "vue";
|
||||
import { CanvasCurrentSelectedComponentID } from "../InjectKeys";
|
||||
|
||||
export interface Props {
|
||||
size?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
roundCorner?: number;
|
||||
componentId?: string;
|
||||
}
|
||||
|
||||
const slot = defineSlots();
|
||||
const props = withDefaults(defineProps<Props>(), getDefaultProps());
|
||||
const selectecComponentID = inject(CanvasCurrentSelectedComponentID, ref(null));
|
||||
|
||||
// 计算实际宽高
|
||||
const width = computed(() => props.width * props.size);
|
||||
const height = computed(() => props.height * props.size);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export function getDefaultProps(): Props {
|
||||
return {
|
||||
size: 1,
|
||||
width: 200,
|
||||
height: 200,
|
||||
roundCorner: 20,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss"></style>
|
||||
@@ -80,16 +80,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from "vue";
|
||||
import Pin from "./Pin.vue";
|
||||
import { useEquipments } from "@/stores/equipments";
|
||||
import { useDialogStore } from "@/stores/dialog";
|
||||
import { useConstraintsStore } from "../../stores/constraints";
|
||||
const { notifyConstraintChange } = useConstraintsStore();
|
||||
|
||||
// 存储多个Pin引用
|
||||
const pinRefs = ref<Record<string, any>>({});
|
||||
import { isNull, isUndefined } from "mathjs";
|
||||
import z from "zod";
|
||||
import { toNumber } from "lodash";
|
||||
|
||||
// 按钮特有属性
|
||||
interface ButtonProps {
|
||||
size?: number;
|
||||
bindKey?: string;
|
||||
export interface ButtonProps {
|
||||
size: number;
|
||||
componentId?: string;
|
||||
pins?: {
|
||||
pinId: string;
|
||||
@@ -97,21 +97,20 @@ interface ButtonProps {
|
||||
x: number;
|
||||
y: number;
|
||||
}[];
|
||||
|
||||
bindKey?: string;
|
||||
bindMatrixKey?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
size: 1,
|
||||
bindKey: "",
|
||||
componentId: "button-default",
|
||||
pins: () => [
|
||||
{
|
||||
pinId: "BTN",
|
||||
constraint: "",
|
||||
x: 80,
|
||||
y: 140,
|
||||
},
|
||||
],
|
||||
});
|
||||
const props = defineProps<ButtonProps>();
|
||||
|
||||
// Global Stores
|
||||
const constrainsts = useConstraintsStore();
|
||||
const dialog = useDialogStore();
|
||||
const eqps = useEquipments();
|
||||
|
||||
// 存储多个Pin引用
|
||||
const pinRefs = ref<Record<string, any>>({});
|
||||
|
||||
// 计算实际宽高
|
||||
const width = computed(() => 160 * props.size);
|
||||
@@ -153,15 +152,27 @@ function toggleButtonState(isPressed: boolean) {
|
||||
isKeyPressed.value = isPressed;
|
||||
btnHeight.value = isPressed ? 180 : 200;
|
||||
|
||||
// 矩阵键盘
|
||||
if (eqps.enableMatrixKey) {
|
||||
const ret = eqps.setMatrixKey(props.bindMatrixKey, isPressed);
|
||||
|
||||
if (ret) eqps.matrixKeypadSetKeyStates(eqps.matrixKeyStates);
|
||||
else
|
||||
dialog.error(
|
||||
`绑定的矩阵键盘值只能是0 ~ 15,而不是: ${props.bindMatrixKey}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 发出事件通知父组件
|
||||
if (isPressed) {
|
||||
emit("press");
|
||||
// 如果有约束,通知约束状态变化为高电平
|
||||
// 对所有引脚应用相同的状态
|
||||
|
||||
if (props.pins) {
|
||||
// 如果有约束,通知约束状态变化为高电平
|
||||
// 对所有引脚应用相同的状态
|
||||
props.pins.forEach((pin) => {
|
||||
if (pin.constraint) {
|
||||
notifyConstraintChange(pin.constraint, "high");
|
||||
constrainsts.notifyConstraintChange(pin.constraint, "high");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -172,7 +183,7 @@ function toggleButtonState(isPressed: boolean) {
|
||||
if (props.pins) {
|
||||
props.pins.forEach((pin) => {
|
||||
if (pin.constraint) {
|
||||
notifyConstraintChange(pin.constraint, "low");
|
||||
constrainsts.notifyConstraintChange(pin.constraint, "low");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -200,35 +211,37 @@ onUnmounted(() => {
|
||||
defineExpose({
|
||||
toggleButtonState,
|
||||
getInfo: () => ({
|
||||
// 按钮特有属性
|
||||
bindKey: props.bindKey,
|
||||
componentId: props.componentId,
|
||||
pins: props.pins,
|
||||
}),
|
||||
// 获取引脚位置
|
||||
getPinPosition: (pinId: string) => {
|
||||
console.log(`[MechanicalButton] 调用getPinPosition,寻找pinId: ${pinId}`);
|
||||
console.log(
|
||||
console.debug(`[MechanicalButton] 调用getPinPosition,寻找pinId: ${pinId}`);
|
||||
console.debug(
|
||||
`[MechanicalButton] 组件ID: ${props.componentId}, 当前尺寸: ${props.size}, 组件宽高: ${width.value}x${height.value}`,
|
||||
);
|
||||
console.log(`[MechanicalButton] 当前存在的pins:`, props.pins);
|
||||
console.debug(`[MechanicalButton] 当前存在的pins:`, props.pins);
|
||||
|
||||
// 如果是自定义的引脚ID
|
||||
if (props.pins && props.pins.length > 0) {
|
||||
const customPin = props.pins.find((p) => p.pinId === pinId);
|
||||
|
||||
if (customPin) {
|
||||
console.log(`[MechanicalButton] 找到自定义引脚: ${pinId},配置位置:`, {
|
||||
x: customPin.x,
|
||||
y: customPin.y,
|
||||
});
|
||||
console.debug(
|
||||
`[MechanicalButton] 找到自定义引脚: ${pinId},配置位置:`,
|
||||
{
|
||||
x: customPin.x,
|
||||
y: customPin.y,
|
||||
},
|
||||
);
|
||||
|
||||
// 考虑组件尺寸的缩放
|
||||
// 这里的x和y是针对标准尺寸(size=1)的坐标,需要根据实际size调整
|
||||
const scaledX = customPin.x * props.size;
|
||||
const scaledY = customPin.y * props.size;
|
||||
|
||||
console.log(`[MechanicalButton] 返回缩放后的坐标:`, {
|
||||
console.debug(`[MechanicalButton] 返回缩放后的坐标:`, {
|
||||
x: scaledX,
|
||||
y: scaledY,
|
||||
});
|
||||
@@ -237,12 +250,12 @@ defineExpose({
|
||||
y: scaledY,
|
||||
};
|
||||
} else {
|
||||
console.log(`[MechanicalButton] 未找到pinId: ${pinId}的引脚配置`);
|
||||
console.debug(`[MechanicalButton] 未找到pinId: ${pinId}的引脚配置`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[MechanicalButton] 没有配置任何引脚`);
|
||||
console.debug(`[MechanicalButton] 没有配置任何引脚`);
|
||||
}
|
||||
console.log(`[MechanicalButton] 返回null,未找到引脚`);
|
||||
console.debug(`[MechanicalButton] 返回null,未找到引脚`);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
@@ -253,7 +266,6 @@ defineExpose({
|
||||
export function getDefaultProps() {
|
||||
return {
|
||||
size: 1,
|
||||
bindKey: "",
|
||||
pins: [
|
||||
{
|
||||
pinId: "BTN",
|
||||
@@ -262,6 +274,8 @@ export function getDefaultProps() {
|
||||
y: 140,
|
||||
},
|
||||
],
|
||||
bindKey: "",
|
||||
bindMatrixKey: "",
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -23,7 +23,7 @@ import { toNumber } from "lodash";
|
||||
|
||||
// 主板特有属性
|
||||
export interface MotherBoardProps {
|
||||
size?: number;
|
||||
size: number;
|
||||
boardAddr?: string;
|
||||
boardPort?: string;
|
||||
componentId?: string;
|
||||
@@ -63,7 +63,6 @@ export function getDefaultProps(): MotherBoardProps {
|
||||
size: 1,
|
||||
boardAddr: "127.0.0.1",
|
||||
boardPort: "1234",
|
||||
componentId: "DefaultMotherBoardID",
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -44,6 +44,19 @@
|
||||
{{ eqps.enableJtagBoundaryScan ? "关闭边界扫描" : "启动边界扫描" }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<h1 class="font-bold text-center text-2xl">外设</h1>
|
||||
<div class="flex flex-row justify-around">
|
||||
<div class="flex flex-row">
|
||||
<input type="checkbox" class="checkbox" :checked="eqps.enableMatrixKey"
|
||||
@change="handleMatrixkeyCheckboxChange" />
|
||||
<p class="mx-2">启用矩阵键盘</p>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<input type="checkbox" class="checkbox" :checked="eqps.enablePower" @change="handlePowerCheckboxChange" />
|
||||
<p class="mx-2">启用电源</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -52,7 +65,7 @@ import z from "zod";
|
||||
import UploadCard from "@/components/UploadCard.vue";
|
||||
import { useDialogStore } from "@/stores/dialog";
|
||||
import { useEquipments } from "@/stores/equipments";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { computed, ref, watchEffect, watchPostEffect } from "vue";
|
||||
|
||||
interface CapsProps {
|
||||
jtagAddr?: string;
|
||||
@@ -100,6 +113,29 @@ function handleSelectJtagSpeed(event: Event) {
|
||||
emits("changeJtagFreq", target.value);
|
||||
}
|
||||
|
||||
async function handleMatrixkeyCheckboxChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (target.checked) {
|
||||
const ret = await eqps.matrixKeypadEnable(true);
|
||||
if (!ret) {
|
||||
}
|
||||
} else {
|
||||
const ret = await eqps.matrixKeypadEnable(false);
|
||||
if (!ret) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
if (eqps.jtagClientMutex.isLocked()) {
|
||||
dialog.warn("Jtag正在被占用");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// componentConfig.ts 提供通用的组件配置功能
|
||||
import type { DiagramPart } from '../diagramManager';
|
||||
import type { DiagramPart } from '../LabCanvas/composable/diagramManager';
|
||||
|
||||
// 属性配置接口
|
||||
export interface PropertyConfig {
|
||||
|
||||
@@ -2,13 +2,10 @@ import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import VueKonva from "vue-konva"
|
||||
|
||||
import App from '@/App.vue'
|
||||
import router from './router'
|
||||
// import { Client } from './APIClient'
|
||||
|
||||
const app = createApp(App).use(router).use(createPinia()).mount('#app')
|
||||
|
||||
// 初始化约束通信
|
||||
// initConstraintCommunication();
|
||||
const app = createApp(App).use(router).use(createPinia()).use(VueKonva).mount('#app')
|
||||
|
||||
|
||||
@@ -1,40 +1,61 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, reactive, watchPostEffect, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { isUndefined } from 'lodash';
|
||||
import Queue from 'yocto-queue';
|
||||
|
||||
export const useDialogStore = defineStore('dialog', () => {
|
||||
type Title = "Error" | "Info" | "Warn";
|
||||
const enableDialog = ref(false);
|
||||
const isDialogOpen = ref(false);
|
||||
const dialogTitle = ref<Title>("Error");
|
||||
const dialogContent = ref("这是一个错误");
|
||||
const contentQueue = new Queue<{ type: Title; content: string }>()
|
||||
|
||||
function openDialog(title: Title, content?: string) {
|
||||
if (!isUndefined(content) && content.length != 0)
|
||||
dialogContent.value = content;
|
||||
dialogTitle.value = title;
|
||||
isDialogOpen.value = true;
|
||||
function openDialog(title?: Title, content?: string) {
|
||||
if (isUndefined(title)) {
|
||||
if (contentQueue.size != 0) {
|
||||
const dialog = contentQueue.dequeue();
|
||||
if (isUndefined(dialog)) return;
|
||||
openDialog(dialog.type, dialog.content);
|
||||
}
|
||||
} else {
|
||||
if (!isUndefined(content) && content.length != 0)
|
||||
dialogContent.value = content;
|
||||
dialogTitle.value = title;
|
||||
isDialogOpen.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
isDialogOpen.value = false;
|
||||
|
||||
openDialog();
|
||||
}
|
||||
|
||||
function info(content?: string) {
|
||||
openDialog("Info", content);
|
||||
function info(content: string) {
|
||||
contentQueue.enqueue({ type: "Info", content: content });
|
||||
openDialog();
|
||||
// openDialog("Info", content);
|
||||
}
|
||||
|
||||
function error(content?: string) {
|
||||
openDialog("Error", content);
|
||||
function error(content: string) {
|
||||
contentQueue.enqueue({ type: "Error", content: content });
|
||||
openDialog();
|
||||
// openDialog("Error", content);
|
||||
}
|
||||
|
||||
function warn(content?: string) {
|
||||
openDialog("Warn", content);
|
||||
function warn(content: string) {
|
||||
contentQueue.enqueue({ type: "Warn", content: content });
|
||||
openDialog();
|
||||
// openDialog("Warn", content);
|
||||
}
|
||||
|
||||
return {
|
||||
enableDialog,
|
||||
isDialogOpen,
|
||||
dialogTitle,
|
||||
dialogContent,
|
||||
dialogQueue: contentQueue,
|
||||
openDialog,
|
||||
closeDialog,
|
||||
info,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ref, watchEffect } from 'vue'
|
||||
import { ref, reactive, watchPostEffect } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { isString, toNumber, isUndefined } from 'lodash';
|
||||
import { Common } from '@/Common';
|
||||
import { isString, toNumber } from 'lodash';
|
||||
import { Common } from '@/utils/Common';
|
||||
import z from "zod"
|
||||
import { isNumber } from 'mathjs';
|
||||
import { JtagClient } from "@/APIClient";
|
||||
import { JtagClient, MatrixKeyClient, PowerClient } from "@/APIClient";
|
||||
import { Mutex, withTimeout } from 'async-mutex';
|
||||
import { useConstraintsStore } from "@/stores/constraints";
|
||||
import { useDialogStore } from './dialog';
|
||||
@@ -14,15 +14,36 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
const constrainsts = useConstraintsStore();
|
||||
const dialog = useDialogStore();
|
||||
|
||||
// Basic Info
|
||||
const boardAddr = ref("127.0.0.1");
|
||||
const boardPort = ref(1234);
|
||||
|
||||
// Jtag
|
||||
const jtagBitstream = ref<File>();
|
||||
const jtagBoundaryScanFreq = ref(100);
|
||||
const jtagClientMutex = withTimeout(new Mutex(), 1000, new Error("JtagClient Mutex Timeout!"))
|
||||
const jtagClient = new JtagClient();
|
||||
|
||||
const enableJtagBoundaryScan = ref(false);
|
||||
// Matrix Key
|
||||
const matrixKeyStates = reactive(new Array<boolean>(16).fill(false))
|
||||
const matrixKeypadClientMutex = withTimeout(new Mutex(), 1000, new Error("Matrixkeyclient Mutex Timeout!"));
|
||||
const matrixKeypadClient = new MatrixKeyClient();
|
||||
|
||||
// Power
|
||||
const powerClientMutex = withTimeout(new Mutex(), 1000, new Error("Matrixkeyclient Mutex Timeout!"));
|
||||
const powerClient = new PowerClient();
|
||||
|
||||
// Enable Setting
|
||||
const enableJtagBoundaryScan = ref(false);
|
||||
const enableMatrixKey = ref(false);
|
||||
const enablePower = ref(false)
|
||||
|
||||
// Watch
|
||||
watchPostEffect(async () => {
|
||||
if (true === enableJtagBoundaryScan.value) jtagBoundaryScan();
|
||||
});
|
||||
|
||||
// Parse and Set
|
||||
function setAddr(address: string | undefined): boolean {
|
||||
if (isString(address) && z.string().ip("4").safeParse(address).success) {
|
||||
boardAddr.value = address;
|
||||
@@ -49,9 +70,22 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (enableJtagBoundaryScan.value) jtagBoundaryScan();
|
||||
});
|
||||
function setMatrixKey(keyNum: number | string | undefined, keyValue: boolean): boolean {
|
||||
let _keyNum: number;
|
||||
if (isString(keyNum)) {
|
||||
_keyNum = toNumber(keyNum);
|
||||
} else if (isNumber(keyNum)) {
|
||||
_keyNum = keyNum;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (z.number().nonnegative().max(16).safeParse(_keyNum).success) {
|
||||
matrixKeyStates[_keyNum] = keyValue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function jtagBoundaryScan() {
|
||||
const release = await jtagClientMutex.acquire();
|
||||
@@ -138,11 +172,78 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function matrixKeypadSetKeyStates(keyStates: boolean[]) {
|
||||
const release = await matrixKeypadClientMutex.acquire();
|
||||
console.log("set Key !!!!!!!!!!!!");
|
||||
try {
|
||||
const resp = await matrixKeypadClient.setMatrixKeyStatus(
|
||||
boardAddr.value,
|
||||
boardPort.value,
|
||||
keyStates
|
||||
);
|
||||
return resp;
|
||||
} catch (e) {
|
||||
dialog.error("设置矩阵键盘时,服务器发生错误");
|
||||
return false;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
async function matrixKeypadEnable(enable: boolean) {
|
||||
const release = await matrixKeypadClientMutex.acquire();
|
||||
try {
|
||||
if (enable) {
|
||||
const resp = await matrixKeypadClient.enabelMatrixKey(
|
||||
boardAddr.value,
|
||||
boardPort.value,
|
||||
);
|
||||
enableMatrixKey.value = resp;
|
||||
return resp;
|
||||
} else {
|
||||
const resp = await matrixKeypadClient.disableMatrixKey(
|
||||
boardAddr.value,
|
||||
boardPort.value,
|
||||
);
|
||||
enableMatrixKey.value = !resp;
|
||||
return resp;
|
||||
}
|
||||
} catch (e) {
|
||||
enableMatrixKey.value = false;
|
||||
dialog.error("设置矩阵键盘是否启用时,服务器发生错误");
|
||||
return false;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
async function powerSetOnOff(enable: boolean) {
|
||||
const release = await powerClientMutex.acquire();
|
||||
try {
|
||||
const resp = await powerClient.setPowerOnOff(
|
||||
boardAddr.value,
|
||||
boardPort.value,
|
||||
enable
|
||||
);
|
||||
return resp;
|
||||
} catch (e) {
|
||||
dialog.error("无法开关电源");
|
||||
console.error(e);
|
||||
return false;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
boardAddr,
|
||||
boardPort,
|
||||
setAddr,
|
||||
setPort,
|
||||
setMatrixKey,
|
||||
|
||||
// Jtag
|
||||
enableJtagBoundaryScan,
|
||||
jtagBitstream,
|
||||
jtagBoundaryScanFreq,
|
||||
jtagClientMutex,
|
||||
@@ -151,7 +252,20 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
jtagDownloadBitstream,
|
||||
jtagGetIDCode,
|
||||
jtagSetSpeed,
|
||||
enableJtagBoundaryScan,
|
||||
|
||||
// Matrix Key
|
||||
enableMatrixKey,
|
||||
matrixKeyStates,
|
||||
matrixKeypadClientMutex,
|
||||
matrixKeypadClient,
|
||||
matrixKeypadEnable,
|
||||
matrixKeypadSetKeyStates,
|
||||
|
||||
// Power
|
||||
enablePower,
|
||||
powerClient,
|
||||
powerClientMutex,
|
||||
powerSetOnOff,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FileParameter } from "./APIClient";
|
||||
import { type FileParameter } from "@/APIClient";
|
||||
import { isNull, isUndefined } from "lodash";
|
||||
|
||||
export namespace Common {
|
||||
30
src/utils/VueKonvaType.ts
Normal file
30
src/utils/VueKonvaType.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import Konva from "konva";
|
||||
import type { VueElement } from "vue";
|
||||
|
||||
interface VNode extends VueElement {
|
||||
getNode(): Konva.Node
|
||||
}
|
||||
|
||||
interface VLayer extends VueElement {
|
||||
getNode(): Konva.Layer
|
||||
}
|
||||
|
||||
interface VGroup extends VueElement {
|
||||
getNode(): Konva.Group
|
||||
}
|
||||
|
||||
interface VStage extends VueElement {
|
||||
getNode(): Konva.Stage
|
||||
}
|
||||
|
||||
interface VTransformer extends VueElement {
|
||||
getNode(): Konva.Transformer
|
||||
}
|
||||
|
||||
export type {
|
||||
VNode,
|
||||
VLayer,
|
||||
VGroup,
|
||||
VStage,
|
||||
VTransformer,
|
||||
}
|
||||
@@ -115,7 +115,7 @@ import { isNull, isUndefined } from "lodash";
|
||||
import { ref } from "vue";
|
||||
import { RemoteUpdateClient } from "@/APIClient";
|
||||
import { useDialogStore } from "@/stores/dialog";
|
||||
import { Common } from "@/Common";
|
||||
import { Common } from "@/utils/Common";
|
||||
|
||||
const dialog = useDialogStore();
|
||||
|
||||
|
||||
@@ -1,88 +1,65 @@
|
||||
<template>
|
||||
<div class="h-screen flex flex-col overflow-hidden">
|
||||
<div class="flex flex-1 overflow-hidden relative">
|
||||
<!-- 左侧图形化区域 -->
|
||||
<div
|
||||
class="relative bg-base-200 overflow-hidden h-full"
|
||||
:style="{ width: leftPanelWidth + '%' }"
|
||||
>
|
||||
<DiagramCanvas
|
||||
ref="diagramCanvas"
|
||||
:componentModules="componentModules"
|
||||
:showDocPanel="showDocPanel"
|
||||
@component-selected="handleComponentSelected"
|
||||
@component-moved="handleComponentMoved"
|
||||
@component-delete="handleComponentDelete"
|
||||
@wire-created="handleWireCreated"
|
||||
@wire-deleted="handleWireDeleted"
|
||||
@diagram-updated="handleDiagramUpdated"
|
||||
@open-components="openComponentsMenu"
|
||||
@load-component-module="handleLoadComponentModule"
|
||||
@toggle-doc-panel="toggleDocPanel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 拖拽分割线 -->
|
||||
<div
|
||||
class="resizer bg-base-100 hover:bg-primary hover:opacity-70 active:bg-primary active:opacity-90 transition-colors"
|
||||
@mousedown="startResize"
|
||||
></div> <!-- 右侧编辑区域 -->
|
||||
<div
|
||||
class="bg-base-200 h-full overflow-hidden flex flex-col"
|
||||
:style="{ width: 100 - leftPanelWidth + '%' }"
|
||||
>
|
||||
<div class="overflow-y-auto flex-1">
|
||||
<!-- 使用条件渲染显示不同的面板 -->
|
||||
<PropertyPanel
|
||||
v-if="!showDocPanel"
|
||||
:componentData="selectedComponentData"
|
||||
:componentConfig="selectedComponentConfig"
|
||||
@updateProp="updateComponentProp"
|
||||
@updateDirectProp="updateComponentDirectProp"
|
||||
/> <div
|
||||
v-else
|
||||
class="doc-panel overflow-y-auto h-full"
|
||||
>
|
||||
<MarkdownRenderer :content="documentContent" />
|
||||
<SplitterGroup id="splitter-group" direction="horizontal" class="w-full h-full">
|
||||
<!-- 左侧图形化区域 -->
|
||||
<SplitterPanel
|
||||
id="splitter-group-panel-canvas"
|
||||
:default-size="60"
|
||||
:min-size="30"
|
||||
class="relative bg-base-200 overflow-hidden h-full"
|
||||
>
|
||||
<DiagramCanvas ref="diagramCanvas" :showDocPanel="showDocPanel"
|
||||
@diagram-updated="handleDiagramUpdated" @open-components="openComponentsMenu"
|
||||
@toggle-doc-panel="toggleDocPanel" />
|
||||
</SplitterPanel>
|
||||
<!-- 拖拽分割线 -->
|
||||
<SplitterResizeHandle id="splitter-group-resize-handle" class="w-2 bg-base-100 hover:bg-primary hover:opacity-70 transition-colors" />
|
||||
<!-- 右侧编辑区域 -->
|
||||
<SplitterPanel
|
||||
id="splitter-group-panel-properties"
|
||||
:min-size="20"
|
||||
class="bg-base-200 h-full overflow-hidden flex flex-col"
|
||||
>
|
||||
<div class="overflow-y-auto flex-1">
|
||||
<!-- 使用条件渲染显示不同的面板 -->
|
||||
<PropertyPanel v-show="!showDocPanel" :componentData="componentManager.selectedComponentData.value"
|
||||
:componentConfig="componentManager.selectedComponentConfig.value" @updateProp="updateComponentProp"
|
||||
@updateDirectProp="updateComponentDirectProp" />
|
||||
<div v-show="showDocPanel" class="doc-panel overflow-y-auto h-full">
|
||||
<MarkdownRenderer :content="documentContent" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SplitterPanel>
|
||||
</SplitterGroup>
|
||||
</div>
|
||||
<!-- 元器件选择组件 -->
|
||||
<ComponentSelector
|
||||
:open="showComponentsMenu"
|
||||
@update:open="showComponentsMenu = $event"
|
||||
@add-component="handleAddComponent"
|
||||
@add-template="handleAddTemplate"
|
||||
@close="showComponentsMenu = false"
|
||||
/>
|
||||
<ComponentSelector :open="showComponentsMenu" @update:open="showComponentsMenu = $event"
|
||||
@add-component="handleAddComponent" @add-template="handleAddTemplate" @close="showComponentsMenu = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 引入wokwi-elements和组件
|
||||
// import "@wokwi/elements"; // 不再需要全局引入 wokwi
|
||||
import { ref, computed, onMounted, onUnmounted, shallowRef } from "vue"; // 引入 defineAsyncComponent 和 shallowRef
|
||||
import DiagramCanvas from "@/components/DiagramCanvas.vue";
|
||||
import ComponentSelector from "@/components/ComponentSelector.vue";
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui";
|
||||
import DiagramCanvas from "@/components/LabCanvas/DiagramCanvas.vue";
|
||||
import ComponentSelector from "@/components/LabCanvas/ComponentSelector.vue";
|
||||
import PropertyPanel from "@/components/PropertyPanel.vue";
|
||||
import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
|
||||
import type { DiagramData, DiagramPart } from "@/components/diagramManager";
|
||||
import {
|
||||
type PropertyConfig,
|
||||
generatePropertyConfigs,
|
||||
generatePropsFromDefault,
|
||||
generatePropsFromAttrs,
|
||||
} from "@/components/equipments/componentConfig"; // 引入组件配置工具
|
||||
import { useProvideComponentManager } from "@/components/LabCanvas";
|
||||
import type { DiagramData, DiagramPart } from "@/components/LabCanvas";
|
||||
|
||||
// 获取路由参数
|
||||
import { useRoute } from "vue-router";
|
||||
const route = useRoute();
|
||||
|
||||
// 提供组件管理服务
|
||||
const componentManager = useProvideComponentManager();
|
||||
|
||||
// --- 文档面板控制 ---
|
||||
const showDocPanel = ref(false);
|
||||
const documentContent = ref("");
|
||||
|
||||
// 获取路由参数
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
|
||||
// 切换文档面板和属性面板
|
||||
async function toggleDocPanel() {
|
||||
showDocPanel.value = !showDocPanel.value;
|
||||
@@ -97,636 +74,39 @@ async function toggleDocPanel() {
|
||||
async function loadDocumentContent() {
|
||||
try {
|
||||
// 从路由参数中获取教程ID
|
||||
const tutorialId = route.query.tutorial as string || '02'; // 默认加载02例程
|
||||
|
||||
const tutorialId = (route.query.tutorial as string) || "02"; // 默认加载02例程
|
||||
|
||||
// 构建文档路径
|
||||
let docPath = `/doc/${tutorialId}/doc.md`;
|
||||
|
||||
|
||||
// 检查当前路径是否包含下划线(例如 02_key 格式)
|
||||
// 如果不包含,那么使用更新的命名格式
|
||||
if (!tutorialId.includes('_')) {
|
||||
if (!tutorialId.includes("_")) {
|
||||
docPath = `/doc/${tutorialId}/doc.md`;
|
||||
}
|
||||
|
||||
|
||||
// 获取文档内容
|
||||
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`);
|
||||
documentContent.value = (await response.text()).replace(
|
||||
/.\/images/gi,
|
||||
`/doc/${tutorialId}/images`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('加载文档失败:', error);
|
||||
documentContent.value = '# 文档加载失败\n\n无法加载请求的文档。'; }
|
||||
console.error("加载文档失败:", error);
|
||||
documentContent.value = "# 文档加载失败\n\n无法加载请求的文档。";
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有例程参数,如果有则自动打开文档面板
|
||||
onMounted(async () => {
|
||||
if (route.query.tutorial) {
|
||||
showDocPanel.value = true;
|
||||
await loadDocumentContent();
|
||||
}
|
||||
});
|
||||
|
||||
// --- 元器件管理 ---
|
||||
// --- UI 状态管理 ---
|
||||
const showComponentsMenu = ref(false);
|
||||
const diagramData = ref<DiagramData>({
|
||||
version: 1,
|
||||
author: "admin",
|
||||
editor: "me",
|
||||
parts: [],
|
||||
connections: [],
|
||||
});
|
||||
|
||||
const selectedComponentId = ref<string | null>(null);
|
||||
const selectedComponentData = computed(() => {
|
||||
return (
|
||||
diagramData.value.parts.find((p) => p.id === selectedComponentId.value) ||
|
||||
null
|
||||
);
|
||||
});
|
||||
const diagramCanvas = ref(null);
|
||||
|
||||
// 存储动态导入的组件模块
|
||||
interface ComponentModule {
|
||||
default: any;
|
||||
getDefaultProps?: () => Record<string, any>;
|
||||
config?: {
|
||||
props?: Array<PropertyConfig>;
|
||||
};
|
||||
}
|
||||
|
||||
const componentModules = shallowRef<Record<string, ComponentModule>>({});
|
||||
const selectedComponentConfig = shallowRef<{ props: PropertyConfig[] } | null>(
|
||||
null,
|
||||
); // 存储选中组件的配置
|
||||
|
||||
// 动态加载组件定义
|
||||
async function loadComponentModule(type: string) {
|
||||
if (!componentModules.value[type]) {
|
||||
try {
|
||||
// 假设组件都在 src/components/equipments/ 目录下,且文件名与 type 相同
|
||||
const module = await import(`../components/equipments/${type}.vue`);
|
||||
|
||||
// 使用 markRaw 包装模块,避免不必要的响应式处理
|
||||
componentModules.value = {
|
||||
...componentModules.value,
|
||||
[type]: module,
|
||||
};
|
||||
|
||||
console.log(`Loaded module for ${type}:`, module);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load component module ${type}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return componentModules.value[type];
|
||||
}
|
||||
|
||||
// 处理组件模块加载请求
|
||||
async function handleLoadComponentModule(type: string) {
|
||||
console.log("Handling load component module request for:", type);
|
||||
await loadComponentModule(type);
|
||||
}
|
||||
|
||||
// --- 分割面板 ---
|
||||
const leftPanelWidth = ref(60);
|
||||
const isResizing = ref(false);
|
||||
|
||||
// 分割面板拖拽相关函数
|
||||
function startResize(e: MouseEvent) {
|
||||
isResizing.value = true;
|
||||
document.addEventListener("mousemove", onResize);
|
||||
document.addEventListener("mouseup", stopResize);
|
||||
e.preventDefault(); // 防止文本选择
|
||||
}
|
||||
|
||||
function onResize(e: MouseEvent) {
|
||||
if (!isResizing.value) return;
|
||||
|
||||
// 获取容器宽度和鼠标位置
|
||||
const container = document.querySelector(
|
||||
".flex-1.overflow-hidden",
|
||||
) as HTMLElement;
|
||||
if (!container) return;
|
||||
|
||||
const containerWidth = container.clientWidth;
|
||||
const mouseX = e.clientX;
|
||||
|
||||
// 计算左侧面板应占的百分比
|
||||
let newWidth = (mouseX / containerWidth) * 100;
|
||||
|
||||
// 限制最小宽度和最大宽度
|
||||
newWidth = Math.max(20, Math.min(newWidth, 80));
|
||||
|
||||
// 更新宽度
|
||||
leftPanelWidth.value = newWidth;
|
||||
}
|
||||
|
||||
function stopResize() {
|
||||
isResizing.value = false;
|
||||
document.removeEventListener("mousemove", onResize);
|
||||
document.removeEventListener("mouseup", stopResize);
|
||||
}
|
||||
|
||||
// --- 元器件操作 ---
|
||||
function openComponentsMenu() {
|
||||
showComponentsMenu.value = true;
|
||||
}
|
||||
|
||||
// 处理 ComponentSelector 组件添加元器件事件
|
||||
async function handleAddComponent(componentData: {
|
||||
type: string;
|
||||
name: string;
|
||||
props: Record<string, any>;
|
||||
}) {
|
||||
// 加载组件模块以便后续使用
|
||||
const componentModule = await loadComponentModule(componentData.type);
|
||||
|
||||
// 获取画布容器和位置信息
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
|
||||
// 获取当前画布的位置信息
|
||||
let position = { x: 100, y: 100 };
|
||||
let scale = 1;
|
||||
|
||||
try {
|
||||
if (
|
||||
canvasInstance &&
|
||||
canvasInstance.getCanvasPosition &&
|
||||
canvasInstance.getScale
|
||||
) {
|
||||
position = canvasInstance.getCanvasPosition();
|
||||
scale = canvasInstance.getScale();
|
||||
|
||||
// 获取画布容器
|
||||
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||
if (canvasContainer) {
|
||||
// 计算可视区域中心点在画布坐标系中的位置
|
||||
const viewportWidth = canvasContainer.clientWidth;
|
||||
const viewportHeight = canvasContainer.clientHeight;
|
||||
|
||||
// 计算画布中心点的坐标
|
||||
position.x = (viewportWidth / 2 - position.x) / scale;
|
||||
position.y = (viewportHeight / 2 - position.y) / scale;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取画布位置时出错:", error);
|
||||
}
|
||||
|
||||
// 添加一些随机偏移,避免元器件重叠
|
||||
const offsetX = Math.floor(Math.random() * 100) - 50;
|
||||
const offsetY = Math.floor(Math.random() * 100) - 50;
|
||||
|
||||
// 获取组件的能力页面
|
||||
let capsPage = null;
|
||||
if (
|
||||
componentModule &&
|
||||
componentModule.default &&
|
||||
typeof componentModule.default.getCapabilities === "function"
|
||||
) {
|
||||
try {
|
||||
capsPage = componentModule.default.getCapabilities();
|
||||
console.log(`获取到${componentData.type}组件的能力页面`);
|
||||
} catch (error) {
|
||||
console.error(`获取${componentData.type}组件能力页面失败:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新组件,使用diagramManager接口定义
|
||||
const newComponent: DiagramPart = {
|
||||
id: `component-${Date.now()}`,
|
||||
type: componentData.type,
|
||||
x: Math.round(position.x + offsetX),
|
||||
y: Math.round(position.y + offsetY),
|
||||
attrs: componentData.props,
|
||||
rotate: 0,
|
||||
group: "",
|
||||
positionlock: false,
|
||||
hidepins: true,
|
||||
isOn: true,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
console.log("添加新组件:", newComponent);
|
||||
// 通过画布实例添加组件
|
||||
if (
|
||||
canvasInstance &&
|
||||
canvasInstance.getDiagramData &&
|
||||
canvasInstance.updateDiagramDataDirectly
|
||||
) {
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
currentData.parts.push(newComponent);
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理模板添加事件
|
||||
async function handleAddTemplate(templateData: {
|
||||
id: string;
|
||||
name: string;
|
||||
template: any;
|
||||
}) {
|
||||
console.log("添加模板:", templateData);
|
||||
console.log("=== 模板组件数量:", templateData.template?.parts?.length || 0);
|
||||
// 获取画布实例
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (
|
||||
!canvasInstance ||
|
||||
!canvasInstance.getDiagramData ||
|
||||
!canvasInstance.updateDiagramDataDirectly
|
||||
) {
|
||||
console.error("没有可用的画布实例添加模板");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前图表数据
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
console.log("=== 当前图表组件数量:", currentData.parts.length);
|
||||
|
||||
// 生成唯一ID前缀,以确保添加的组件ID不重复
|
||||
const idPrefix = `template-${Date.now()}-`;
|
||||
// 处理模板组件并添加到图表
|
||||
if (templateData.template && templateData.template.parts) {
|
||||
// 获取当前视口中心位置
|
||||
let viewportCenter = { x: 300, y: 200 }; // 默认值
|
||||
try {
|
||||
if (
|
||||
canvasInstance &&
|
||||
canvasInstance.getCanvasPosition &&
|
||||
canvasInstance.getScale
|
||||
) {
|
||||
const position = canvasInstance.getCanvasPosition();
|
||||
const scale = canvasInstance.getScale();
|
||||
|
||||
// 获取画布容器
|
||||
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||
if (canvasContainer) {
|
||||
// 计算可视区域中心点在画布坐标系中的位置
|
||||
const viewportWidth = canvasContainer.clientWidth;
|
||||
const viewportHeight = canvasContainer.clientHeight;
|
||||
|
||||
// 计算视口中心点的坐标 (与handleAddComponent函数中的方法相同)
|
||||
viewportCenter.x = (viewportWidth / 2 - position.x) / scale;
|
||||
viewportCenter.y = (viewportHeight / 2 - position.y) / scale;
|
||||
console.log(
|
||||
`=== 计算的视口中心: x=${viewportCenter.x}, y=${viewportCenter.y}, scale=${scale}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取视口中心位置时出错:", error);
|
||||
}
|
||||
|
||||
console.log("=== 使用视口中心添加模板组件:", viewportCenter);
|
||||
|
||||
// 找到模板中的主要组件(假设是第一个组件)
|
||||
const mainPart = templateData.template.parts[0];
|
||||
|
||||
// 创建带有新位置的组件
|
||||
const newParts = await Promise.all(
|
||||
templateData.template.parts.map(async (part: any) => {
|
||||
// 创建组件副本并分配新ID
|
||||
const newPart = JSON.parse(JSON.stringify(part));
|
||||
newPart.id = `${idPrefix}${part.id}`;
|
||||
|
||||
// 尝试加载组件模块并获取能力页面
|
||||
try {
|
||||
const componentModule = await loadComponentModule(part.type);
|
||||
if (
|
||||
componentModule &&
|
||||
componentModule.default &&
|
||||
typeof componentModule.default.getCapabilities === "function"
|
||||
) {
|
||||
newPart.capsPage = componentModule.default.getCapabilities();
|
||||
console.log(`加载模板组件${part.type}组件的能力页面成功`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`加载模板组件${part.type}的能力页面失败:`, error);
|
||||
}
|
||||
|
||||
// 计算相对于主要组件的偏移量,保持模板内部组件的相对位置关系
|
||||
if (typeof newPart.x === "number" && typeof newPart.y === "number") {
|
||||
const oldX = newPart.x;
|
||||
const oldY = newPart.y;
|
||||
|
||||
// 计算相对位置(相对于主要组件)
|
||||
const relativeX = part.x - mainPart.x;
|
||||
const relativeY = part.y - mainPart.y;
|
||||
|
||||
// 应用到视口中心位置
|
||||
newPart.x = viewportCenter.x + relativeX;
|
||||
newPart.y = viewportCenter.y + relativeY;
|
||||
|
||||
console.log(
|
||||
`=== 组件[${newPart.id}]位置调整: (${oldX},${oldY}) -> (${newPart.x},${newPart.y})`,
|
||||
);
|
||||
}
|
||||
|
||||
return newPart;
|
||||
}),
|
||||
);
|
||||
|
||||
// 向图表添加新组件
|
||||
currentData.parts.push(...newParts);
|
||||
|
||||
// 处理连接关系
|
||||
if (templateData.template.connections) {
|
||||
// 创建一个映射表用于转换旧组件ID到新组件ID
|
||||
const idMap: Record<string, string> = {};
|
||||
templateData.template.parts.forEach((part: any) => {
|
||||
idMap[part.id] = `${idPrefix}${part.id}`;
|
||||
});
|
||||
|
||||
// 添加连接,更新组件ID引用
|
||||
const newConnections = templateData.template.connections.map(
|
||||
(conn: any) => {
|
||||
// 处理连接数据 (格式为 [from, to, type, path])
|
||||
if (Array.isArray(conn)) {
|
||||
const [from, to, type, path] = conn;
|
||||
|
||||
// 从连接字符串中提取组件ID和引脚ID
|
||||
const fromParts = from.split(":");
|
||||
const toParts = to.split(":");
|
||||
|
||||
if (fromParts.length === 2 && toParts.length === 2) {
|
||||
const fromComponentId = fromParts[0];
|
||||
const fromPinId = fromParts[1];
|
||||
const toComponentId = toParts[0];
|
||||
const toPinId = toParts[1];
|
||||
|
||||
// 创建新的连接字符串,使用新的组件ID
|
||||
const newFrom = `${idMap[fromComponentId] || fromComponentId}:${fromPinId}`;
|
||||
const newTo = `${idMap[toComponentId] || toComponentId}:${toPinId}`;
|
||||
|
||||
return [newFrom, newTo, type, path];
|
||||
}
|
||||
}
|
||||
return conn; // 如果格式不匹配,保持原样
|
||||
},
|
||||
);
|
||||
|
||||
// 添加到当前连接列表
|
||||
currentData.connections.push(...newConnections);
|
||||
}
|
||||
// 更新图表数据
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
console.log("=== 更新图表数据完成,新组件数量:", currentData.parts.length);
|
||||
|
||||
// 显示成功消息
|
||||
showToast(`已添加 ${templateData.name} 模板`, "success");
|
||||
} else {
|
||||
console.error("模板格式错误,缺少parts数组");
|
||||
showToast("模板格式错误", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// 处理组件选中事件
|
||||
async function handleComponentSelected(componentData: DiagramPart | null) {
|
||||
selectedComponentId.value = componentData ? componentData.id : null;
|
||||
selectedComponentConfig.value = null; // 重置配置
|
||||
|
||||
if (componentData) {
|
||||
// 先加载组件模块
|
||||
const moduleRef = await loadComponentModule(componentData.type);
|
||||
|
||||
if (moduleRef) {
|
||||
try {
|
||||
// 创建属性配置数组
|
||||
const propConfigs: PropertyConfig[] = [];
|
||||
|
||||
// 创建一个映射来跟踪已添加的属性名
|
||||
const addedProps = new Set<string>();
|
||||
|
||||
// 1. 首先从getDefaultProps方法获取默认配置
|
||||
if (typeof moduleRef.getDefaultProps === "function") {
|
||||
const defaultProps = moduleRef.getDefaultProps();
|
||||
const defaultPropConfigs = generatePropsFromDefault(defaultProps);
|
||||
|
||||
// 添加默认配置并记录属性名
|
||||
defaultPropConfigs.forEach((config) => {
|
||||
propConfigs.push(config);
|
||||
addedProps.add(config.name);
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 添加组件直接属性,这些属性会覆盖默认配置
|
||||
const directPropConfigs = generatePropertyConfigs(componentData);
|
||||
|
||||
// 过滤掉已经添加过的属性名
|
||||
const newDirectProps = directPropConfigs.filter(
|
||||
(config) => !addedProps.has(config.name),
|
||||
);
|
||||
propConfigs.push(...newDirectProps);
|
||||
|
||||
// 3. 最后添加attrs中的属性
|
||||
if (componentData.attrs) {
|
||||
const attrs = componentData.attrs;
|
||||
const attrPropConfigs = generatePropsFromAttrs(attrs);
|
||||
|
||||
// 更新已存在的属性值,或添加新属性
|
||||
attrPropConfigs.forEach((attrConfig) => {
|
||||
const existingIndex = propConfigs.findIndex(
|
||||
(p) => p.name === attrConfig.name,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
// 更新已存在的属性值
|
||||
propConfigs[existingIndex] = attrConfig;
|
||||
} else {
|
||||
// 添加新属性
|
||||
propConfigs.push(attrConfig);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectedComponentConfig.value = { props: propConfigs };
|
||||
console.log(
|
||||
`Built config for ${componentData.type}:`,
|
||||
selectedComponentConfig.value,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error building config for ${componentData.type}:`,
|
||||
error,
|
||||
);
|
||||
selectedComponentConfig.value = { props: [] };
|
||||
}
|
||||
} else {
|
||||
console.warn(`Module for component ${componentData.type} not found.`);
|
||||
selectedComponentConfig.value = { props: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理图表数据更新事件
|
||||
function handleDiagramUpdated(data: DiagramData) {
|
||||
diagramData.value = data;
|
||||
console.log("Diagram data updated:", data);
|
||||
}
|
||||
|
||||
// 处理组件移动事件
|
||||
function handleComponentMoved(moveData: { id: string; x: number; y: number }) {
|
||||
const part = diagramData.value.parts.find((p) => p.id === moveData.id);
|
||||
if (part) {
|
||||
part.x = moveData.x;
|
||||
part.y = moveData.y;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理组件删除事件
|
||||
function handleComponentDelete(componentId: string) {
|
||||
// 查找要删除的组件
|
||||
const component = diagramData.value.parts.find((p) => p.id === componentId);
|
||||
if (!component) return;
|
||||
|
||||
// 收集需要删除的组件ID列表(包括当前组件和同组组件)
|
||||
const componentsToDelete: string[] = [componentId];
|
||||
|
||||
// 如果组件属于一个组,则找出所有同组的组件
|
||||
if (component.group && component.group !== "") {
|
||||
const groupMembers = diagramData.value.parts.filter(
|
||||
(p) => p.group === component.group && p.id !== componentId,
|
||||
);
|
||||
|
||||
// 将同组组件ID添加到删除列表
|
||||
componentsToDelete.push(...groupMembers.map((p) => p.id));
|
||||
console.log(
|
||||
`删除组件 ${componentId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`,
|
||||
);
|
||||
}
|
||||
|
||||
// 删除所有标记的组件
|
||||
diagramData.value.parts = diagramData.value.parts.filter(
|
||||
(p) => !componentsToDelete.includes(p.id),
|
||||
);
|
||||
|
||||
// 同时删除与这些组件相关的所有连接
|
||||
diagramData.value.connections = diagramData.value.connections.filter(
|
||||
(connection) => {
|
||||
for (const id of componentsToDelete) {
|
||||
if (
|
||||
connection[0].startsWith(`${id}:`) ||
|
||||
connection[1].startsWith(`${id}:`)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
// 如果删除的是当前选中的组件,清除选中状态
|
||||
if (
|
||||
selectedComponentId.value &&
|
||||
componentsToDelete.includes(selectedComponentId.value)
|
||||
) {
|
||||
selectedComponentId.value = null;
|
||||
selectedComponentConfig.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新组件属性的方法
|
||||
function updateComponentProp(
|
||||
componentId: string,
|
||||
propName: string,
|
||||
value: any,
|
||||
) {
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (
|
||||
!canvasInstance ||
|
||||
!canvasInstance.getDiagramData ||
|
||||
!canvasInstance.updateDiagramDataDirectly
|
||||
) {
|
||||
console.error("没有可用的画布实例进行属性更新");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查值是否为对象,如果是对象并有value属性,则使用该属性值
|
||||
if (value !== null && typeof value === "object" && "value" in value) {
|
||||
value = value.value;
|
||||
}
|
||||
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
|
||||
|
||||
if (part) {
|
||||
// 检查是否为基本属性
|
||||
if (propName in part) {
|
||||
(part as any)[propName] = value;
|
||||
} else {
|
||||
// 否则当作attrs中的属性处理
|
||||
if (!part.attrs) {
|
||||
part.attrs = {};
|
||||
}
|
||||
part.attrs[propName] = value;
|
||||
}
|
||||
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
console.log(
|
||||
`更新组件${componentId}的属性${propName}为:`,
|
||||
value,
|
||||
typeof value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新组件的直接属性
|
||||
function updateComponentDirectProp(
|
||||
componentId: string,
|
||||
propName: string,
|
||||
value: any,
|
||||
) {
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (
|
||||
!canvasInstance ||
|
||||
!canvasInstance.getDiagramData ||
|
||||
!canvasInstance.updateDiagramDataDirectly
|
||||
) {
|
||||
console.error("没有可用的画布实例进行属性更新");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
|
||||
|
||||
if (part) {
|
||||
// @ts-ignore: 动态属性赋值
|
||||
part[propName] = value;
|
||||
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
console.log(
|
||||
`更新组件${componentId}的直接属性${propName}为:`,
|
||||
value,
|
||||
typeof value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 专门用于更新组件的显示层级 - 这个方法可以删除,直接使用updateComponentProp即可
|
||||
|
||||
// 处理连线创建事件
|
||||
function handleWireCreated(wireData: any) {
|
||||
console.log("Wire created:", wireData);
|
||||
}
|
||||
|
||||
// 处理连线删除事件
|
||||
function handleWireDeleted(wireId: string) {
|
||||
console.log("Wire deleted:", wireId);
|
||||
}
|
||||
|
||||
// 导出当前diagram数据
|
||||
function exportDiagram() {
|
||||
// 直接使用DiagramCanvas组件提供的导出功能
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (canvasInstance && canvasInstance.exportDiagram) {
|
||||
canvasInstance.exportDiagram();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 消息提示 ---
|
||||
// --- 页面动画和通知 ---
|
||||
const showNotification = ref(false);
|
||||
const notificationMessage = ref("");
|
||||
const notificationType = ref<"success" | "error" | "info">("info");
|
||||
@@ -751,41 +131,68 @@ function showToast(
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
// 显示通知
|
||||
|
||||
// --- 组件属性处理辅助函数 ---
|
||||
// 直接使用 componentConfig.ts 中导入的 getPropValue 函数
|
||||
// --- 事件处理器(委托给组件管理器) ---
|
||||
|
||||
function openComponentsMenu() {
|
||||
showComponentsMenu.value = true;
|
||||
}
|
||||
|
||||
// 处理 ComponentSelector 组件添加元器件事件
|
||||
async function handleAddComponent(componentData: {
|
||||
type: string;
|
||||
name: string;
|
||||
props: Record<string, any>;
|
||||
}) {
|
||||
await componentManager.addComponent(componentData);
|
||||
}
|
||||
|
||||
// 处理模板添加事件
|
||||
async function handleAddTemplate(templateData: {
|
||||
id: string;
|
||||
name: string;
|
||||
template: any;
|
||||
}) {
|
||||
const result = await componentManager.addTemplate(templateData);
|
||||
if (result) {
|
||||
showToast(result.message, result.success ? "success" : "error");
|
||||
}
|
||||
}
|
||||
|
||||
// 处理图表数据更新事件
|
||||
function handleDiagramUpdated(data: DiagramData) {
|
||||
console.log("Diagram data updated:", data);
|
||||
}
|
||||
|
||||
// 更新组件属性的方法 - 委托给componentManager
|
||||
function updateComponentProp(
|
||||
componentId: string,
|
||||
propName: string,
|
||||
value: any,
|
||||
) {
|
||||
componentManager.updateComponentProp(componentId, propName, value);
|
||||
}
|
||||
|
||||
// 更新组件的直接属性 - 委托给componentManager
|
||||
function updateComponentDirectProp(
|
||||
componentId: string,
|
||||
propName: string,
|
||||
value: any,
|
||||
) {
|
||||
componentManager.updateComponentDirectProp(componentId, propName, value);
|
||||
}
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(async () => {
|
||||
// 初始化画布设置
|
||||
console.log("ProjectView mounted, diagram canvas ref:", diagramCanvas.value);
|
||||
|
||||
// 获取初始图表数据
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (canvasInstance && canvasInstance.getDiagramData) {
|
||||
diagramData.value = canvasInstance.getDiagramData();
|
||||
|
||||
// 预加载所有使用的组件模块,以确保它们在渲染时可用
|
||||
const componentTypes = new Set<string>();
|
||||
diagramData.value.parts.forEach((part) => {
|
||||
componentTypes.add(part.type);
|
||||
});
|
||||
|
||||
console.log("Preloading component modules:", Array.from(componentTypes));
|
||||
|
||||
// 并行加载所有组件模块
|
||||
await Promise.all(
|
||||
Array.from(componentTypes).map((type) => loadComponentModule(type)),
|
||||
);
|
||||
|
||||
console.log("All component modules loaded");
|
||||
// 检查是否有例程参数,如果有则自动打开文档面板
|
||||
if (route.query.tutorial) {
|
||||
showDocPanel.value = true;
|
||||
await loadDocumentContent();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("mousemove", onResize);
|
||||
document.removeEventListener("mouseup", stopResize);
|
||||
// 设置画布引用并初始化组件管理器
|
||||
componentManager.setCanvasRef(diagramCanvas.value);
|
||||
await componentManager.initialize();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -793,26 +200,6 @@ onUnmounted(() => {
|
||||
/* 样式保持不变 */
|
||||
@import "../assets/main.css";
|
||||
|
||||
/* 分割线样式 */
|
||||
.resizer {
|
||||
width: 6px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
transition: background-color 0.3s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.resizer:hover,
|
||||
.resizer:active {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
/* 调整大小时应用全局样式 */
|
||||
:global(body.resizing) {
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.animate-slideRight {
|
||||
animation: slideRight 0.3s ease-out forwards;
|
||||
}
|
||||
@@ -843,8 +230,10 @@ body {
|
||||
padding: 1.5rem;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
background-color: transparent; /* 使用透明背景 */
|
||||
border: none; /* 确保没有边框 */
|
||||
background-color: transparent;
|
||||
/* 使用透明背景 */
|
||||
border: none;
|
||||
/* 确保没有边框 */
|
||||
}
|
||||
|
||||
/* 文档切换按钮样式 */
|
||||
|
||||
@@ -1,99 +1,5 @@
|
||||
<template>
|
||||
<div class="flex w-screen h-screen justify-center">
|
||||
<div class="flex flex-col w-3/5 h-screen shadow-2xl p-10">
|
||||
<div class="flex justify-center">
|
||||
<h1 class="font-bold text-3xl">Jtag 下载</h1>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="w-full">
|
||||
<div class="collapse bg-primary">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title font-semibold text-lg text-white">
|
||||
自定义开发板参数
|
||||
</div>
|
||||
<div class="collapse-content bg-primary-content text-sm">
|
||||
<div class="form-control w-full my-3">
|
||||
<label class="label">
|
||||
<span class="label-text text-gray-700">开发板IP地址</span>
|
||||
</label>
|
||||
<label class="input w-full">
|
||||
<img class="h-[1em] opacity-50" src="@/assets/pwd.svg" alt="User img" />
|
||||
<input type="text" class="grow" placeholder="IP地址" v-model="boardAddress" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control w-full my-3">
|
||||
<label class="label">
|
||||
<span class="label-text text-gray-700">开发板端口号</span>
|
||||
</label>
|
||||
<label class="input w-full">
|
||||
<img class="h-[1em] opacity-50" src="@/assets/pwd.svg" alt="User img" />
|
||||
<input type="text" class="grow" placeholder="端口号" v-model="boardPort" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<UploadCard :upload-event="uploadBitstream" :download-event="downloadBitstream">
|
||||
</UploadCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { JtagClient, type FileParameter } from "@/APIClient";
|
||||
import UploadCard from "@/components/UploadCard.vue";
|
||||
import { useDialogStore } from "@/stores/dialog";
|
||||
import { Common } from "@/Common";
|
||||
import { toNumber, isUndefined } from "lodash";
|
||||
import { ref } from "vue";
|
||||
|
||||
const jtagController = new JtagClient();
|
||||
const dialog = useDialogStore();
|
||||
|
||||
// Models with default values
|
||||
const boardAddress = ref("127.0.0.1"); // 默认IP地址
|
||||
const boardPort = ref("1234"); // 默认端口号
|
||||
|
||||
async function uploadBitstream(bitstream: File): Promise<boolean> {
|
||||
if (isUndefined(boardAddress.value) || isUndefined(boardPort.value)) {
|
||||
dialog.error("开发板地址或端口空缺");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await jtagController.uploadBitstream(
|
||||
boardAddress.value,
|
||||
Common.toFileParameterOrNull(bitstream),
|
||||
);
|
||||
return resp;
|
||||
} catch (e) {
|
||||
dialog.error("上传错误");
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadBitstream(): Promise<boolean> {
|
||||
if (isUndefined(boardAddress.value) || isUndefined(boardPort.value)) {
|
||||
dialog.error("开发板地址或端口空缺");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await jtagController.downloadBitstream(
|
||||
boardAddress.value,
|
||||
toNumber(boardPort.value),
|
||||
);
|
||||
return resp;
|
||||
} catch (e) {
|
||||
dialog.error("上传错误");
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
@import "../assets/main.css";
|
||||
</style>
|
||||
</script>
|
||||
@@ -6,6 +6,8 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import tailwindcss from '@tailwindcss/postcss'
|
||||
import autoprefixer from 'autoprefixer'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import RekaResolver from 'reka-ui/resolver'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
@@ -20,6 +22,18 @@ export default defineConfig({
|
||||
}),
|
||||
vueJsx(),
|
||||
vueDevTools(),
|
||||
Components(
|
||||
{
|
||||
dts: true,
|
||||
resolvers: [
|
||||
RekaResolver()
|
||||
|
||||
// RekaResolver({
|
||||
// prefix: '' // use the prefix option to add Prefix to the imported components
|
||||
// })
|
||||
],
|
||||
}
|
||||
)
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Reference in New Issue
Block a user