Merge remote-tracking branch 'origin/master'

master
汤全昆 2025-08-28 11:26:23 +08:00
commit 5ec93514ad
34 changed files with 2463 additions and 1197 deletions

207
pc-fe/package-lock.json generated
View File

@ -1,26 +1,23 @@
{ {
"name": "vdi-manager", "name": "vdi-manager",
"version": "1.0.5", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "vdi-manager", "name": "vdi-manager",
"version": "1.0.5", "version": "1.0.0",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@ant-design/icons": "^6.0.0", "@ant-design/icons": "^6.0.0",
"@grpc/grpc-js": "^1.13.4",
"@grpc/proto-loader": "^0.8.0",
"antd": "^5.26.6", "antd": "^5.26.6",
"axios": "^1.11.0", "axios": "^1.11.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"google-protobuf": "^4.0.0", "electron-updater": "^6.6.2",
"umi": "^4.0.42" "umi": "^4.0.42"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node14": "^1.0.3", "@tsconfig/node14": "^1.0.3",
"@types/google-protobuf": "^3.15.12",
"@types/react": "^18.0.0", "@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0", "@types/react-dom": "^18.0.0",
"@umijs/plugin-electron": "^0.2.0", "@umijs/plugin-electron": "^0.2.0",
@ -3211,55 +3208,6 @@
"use-isomorphic-layout-effect": "^1.1.1" "use-isomorphic-layout-effect": "^1.1.1"
} }
}, },
"node_modules/@grpc/grpc-js": {
"version": "1.13.4",
"resolved": "https://registry.npmmirror.com/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
"integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.7.13",
"@js-sdsl/ordered-map": "^4.4.2"
},
"engines": {
"node": ">=12.10.0"
}
},
"node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": {
"version": "0.7.15",
"resolved": "https://registry.npmmirror.com/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
"license": "Apache-2.0",
"dependencies": {
"lodash.camelcase": "^4.3.0",
"long": "^5.0.0",
"protobufjs": "^7.2.5",
"yargs": "^17.7.2"
},
"bin": {
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@grpc/proto-loader": {
"version": "0.8.0",
"resolved": "https://registry.npmmirror.com/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
"integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
"license": "Apache-2.0",
"dependencies": {
"lodash.camelcase": "^4.3.0",
"long": "^5.0.0",
"protobufjs": "^7.5.3",
"yargs": "^17.7.2"
},
"bin": {
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.13.0", "version": "0.13.0",
"resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@ -3653,16 +3601,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@js-sdsl/ordered-map": {
"version": "4.4.2",
"resolved": "https://registry.npmmirror.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/@loadable/component": { "node_modules/@loadable/component": {
"version": "5.15.2", "version": "5.15.2",
"resolved": "https://registry.npmmirror.com/@loadable/component/-/component-5.15.2.tgz", "resolved": "https://registry.npmmirror.com/@loadable/component/-/component-5.15.2.tgz",
@ -4216,70 +4154,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@rc-component/async-validator": { "node_modules/@rc-component/async-validator": {
"version": "5.0.4", "version": "5.0.4",
"resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.0.4.tgz", "resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.0.4.tgz",
@ -4918,13 +4792,6 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/google-protobuf": {
"version": "3.15.12",
"resolved": "https://registry.npmmirror.com/@types/google-protobuf/-/google-protobuf-3.15.12.tgz",
"integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/graceful-fs": { "node_modules/@types/graceful-fs": {
"version": "4.1.9", "version": "4.1.9",
"resolved": "https://registry.npmmirror.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "resolved": "https://registry.npmmirror.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@ -10251,9 +10118,7 @@
"version": "6.6.2", "version": "6.6.2",
"resolved": "https://registry.npmmirror.com/electron-updater/-/electron-updater-6.6.2.tgz", "resolved": "https://registry.npmmirror.com/electron-updater/-/electron-updater-6.6.2.tgz",
"integrity": "sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==", "integrity": "sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==",
"dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"builder-util-runtime": "9.3.1", "builder-util-runtime": "9.3.1",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
@ -10269,9 +10134,7 @@
"version": "9.3.1", "version": "9.3.1",
"resolved": "https://registry.npmmirror.com/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz", "resolved": "https://registry.npmmirror.com/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz",
"integrity": "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==", "integrity": "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"debug": "^4.3.4", "debug": "^4.3.4",
"sax": "^1.2.4" "sax": "^1.2.4"
@ -10284,9 +10147,7 @@
"version": "10.1.0", "version": "10.1.0",
"resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1", "jsonfile": "^6.0.1",
@ -10300,9 +10161,7 @@
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz", "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"universalify": "^2.0.0" "universalify": "^2.0.0"
}, },
@ -10314,9 +10173,7 @@
"version": "7.7.2", "version": "7.7.2",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
}, },
@ -10328,9 +10185,7 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
@ -12167,12 +12022,6 @@
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
"node_modules/google-protobuf": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/google-protobuf/-/google-protobuf-4.0.0.tgz",
"integrity": "sha512-b8wmenhUMf2WNL+xIJ/slvD/hEE6V3nRnG86O2bzkBrMweM9gnqZE1dfXlDjibY3aXJXDNbAHepevYyQ7qWKsQ==",
"license": "(BSD-3-Clause AND Apache-2.0)"
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
@ -13874,7 +13723,6 @@
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmmirror.com/lazy-val/-/lazy-val-1.0.5.tgz", "resolved": "https://registry.npmmirror.com/lazy-val/-/lazy-val-1.0.5.tgz",
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/less": { "node_modules/less": {
@ -14242,12 +14090,6 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/lodash.debounce": { "node_modules/lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@ -14259,18 +14101,14 @@
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmmirror.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "resolved": "https://registry.npmmirror.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
"dev": true, "license": "MIT"
"license": "MIT",
"peer": true
}, },
"node_modules/lodash.isequal": { "node_modules/lodash.isequal": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"dev": true, "license": "MIT"
"license": "MIT",
"peer": true
}, },
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
@ -14286,12 +14124,6 @@
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/loose-envify": { "node_modules/loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
@ -16816,30 +16648,6 @@
"license": "ISC", "license": "ISC",
"optional": true "optional": true
}, },
"node_modules/protobufjs": {
"version": "7.5.4",
"resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-7.5.4.tgz",
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -18554,7 +18362,6 @@
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.1.tgz", "resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
"devOptional": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/scheduler": { "node_modules/scheduler": {
@ -20062,9 +19869,7 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmmirror.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", "resolved": "https://registry.npmmirror.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
"dev": true, "license": "MIT"
"license": "MIT",
"peer": true
}, },
"node_modules/titleize": { "node_modules/titleize": {
"version": "3.0.0", "version": "3.0.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "vdi-manager", "name": "vdi-manager",
"version": "1.0.5", "version": "1.0.0",
"scripts": { "scripts": {
"dev": "umi dev", "dev": "umi dev",
"build": "umi build", "build": "umi build",
@ -17,17 +17,14 @@
"homepage": "http://10.209.8.11/users/sign_in", "homepage": "http://10.209.8.11/users/sign_in",
"dependencies": { "dependencies": {
"@ant-design/icons": "^6.0.0", "@ant-design/icons": "^6.0.0",
"@grpc/grpc-js": "^1.13.4",
"@grpc/proto-loader": "^0.8.0",
"antd": "^5.26.6", "antd": "^5.26.6",
"axios": "^1.11.0", "axios": "^1.11.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"google-protobuf": "^4.0.0", "electron-updater": "^6.6.2",
"umi": "^4.0.42" "umi": "^4.0.42"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node14": "^1.0.3", "@tsconfig/node14": "^1.0.3",
"@types/google-protobuf": "^3.15.12",
"@types/react": "^18.0.0", "@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0", "@types/react-dom": "^18.0.0",
"@umijs/plugin-electron": "^0.2.0", "@umijs/plugin-electron": "^0.2.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -0,0 +1,113 @@
// src/contexts/ConfigStepContext.tsx
/**
* Tab
*
* tab
* 便tab
*
* @example
* // 1. 在顶层组件包装Provider
* import { ConfigStepProvider } from '@/contexts/ConfigStepContext';
*
* const tabs = [
* { key: "networkConfig", label: '网络配置', component: <NetworkConfig /> },
* { key: "systemConfig", label: '系统配置', component: <SystemConfig /> },
* ];
*
* <ConfigStepProvider tabs={tabs}>
* <YourComponent />
* </ConfigStepProvider>
*
* @example
* // 2. 在子组件中使用hook访问和控制tab状态
* import { useConfigStep } from '@/contexts/ConfigStepContext';
*
* const MyComponent = () => {
* const {
* activeTab, // 当前激活的tab key
* setActiveTab, // 设置特定tab为激活状态
* goToNextTab, // 切换到下一个tab
* goToPreviousTab // 切换到上一个tab
* } = useConfigStep();
*
* const handleNext = () => {
* // 执行某些操作后切换到下一个tab
* setTimeout(() => {
* goToNextTab();
* }, 300);
* };
*
* const handleJump = () => {
* // 直接跳转到指定tab
* setActiveTab('systemConfig');
* };
*
* return (
* <div>
* <p>Tab: {activeTab}</p>
* <button onClick={handleNext}></button>
* <button onClick={handleJump}></button>
* </div>
* );
* };
*/
import React, { createContext, useContext, useState, ReactNode } from 'react';
interface ConfigStepContextType {
activeTab: string;
setActiveTab: (tab: string) => void;
goToNextTab: () => void;
goToPreviousTab: () => void;
tabs: { key: string; label: string; component: React.ReactNode }[];
}
const ConfigStepContext = createContext<ConfigStepContextType | undefined>(undefined);
interface ConfigStepProviderProps {
children: ReactNode;
initialTab?: string;
tabs: { key: string; label: string; component: React.ReactNode }[];
}
export const ConfigStepProvider: React.FC<ConfigStepProviderProps> = ({
children,
initialTab = "networkConfig",
tabs
}) => {
const [activeTab, setActiveTab] = useState<string>(initialTab);
const goToNextTab = () => {
const currentIndex = tabs.findIndex(tab => tab.key === activeTab);
const nextIndex = (currentIndex + 1) % tabs.length;
setActiveTab(tabs[nextIndex].key);
};
const goToPreviousTab = () => {
const currentIndex = tabs.findIndex(tab => tab.key === activeTab);
const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length;
setActiveTab(tabs[prevIndex].key);
};
return (
<ConfigStepContext.Provider
value={{
activeTab,
setActiveTab,
goToNextTab,
goToPreviousTab,
tabs
}}
>
{children}
</ConfigStepContext.Provider>
);
};
export const useConfigStep = () => {
const context = useContext(ConfigStepContext);
if (context === undefined) {
throw new Error('useConfigStep must be used within a ConfigStepProvider');
}
return context;
};

View File

@ -1,15 +1,15 @@
import { BrowserWindowConstructorOptions } from "electron"; import { BrowserWindowConstructorOptions } from "electron";
export default{ export default{
browserWindow:{ browserWindow:{
kiosk: true, kiosk: true, // 全屏
frame: false, frame: false, // 无边框
titleBarStyle: 'hidden', titleBarStyle: 'hidden', // 隐藏标题栏
autoHideMenuBar: true, autoHideMenuBar: true, // 隐藏菜单栏
// 禁止调整窗口大小 // 禁止调整窗口大小
resizable: false, resizable: false,
// 禁止最大化和最小化按钮 // 禁止最大化和最小化按钮
maximizable: false, maximizable: false,
minimizable: false, minimizable: false,
closable: false, // closable: false, // 禁止关闭窗口
} as BrowserWindowConstructorOptions, } as BrowserWindowConstructorOptions,
} }

View File

@ -1,194 +1,194 @@
// src/grpc/BTGrpcClient.ts 调用后端服务 // // src/grpc/BTGrpcClient.ts 调用后端服务
import * as grpc from '@grpc/grpc-js'; // import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader'; // import * as protoLoader from '@grpc/proto-loader';
import path from 'path'; // import path from 'path';
import { app } from 'electron'; // import { app } from 'electron';
export interface DownloadRequest { // export interface DownloadRequest {
torrent_url: string; // torrent_url: string;
item_name: string; // item_name: string;
item_id?: string; // item_id?: string;
} // }
export interface ProgressCallback { // export interface ProgressCallback {
(progress: ProgressUpdate): void; // (progress: ProgressUpdate): void;
} // }
export interface ProgressUpdate { // export interface ProgressUpdate {
download_id: string; // download_id: string;
progress: number; // progress: number;
download_speed: number; // download_speed: number;
upload_speed: number; // upload_speed: number;
eta: number; // eta: number;
total_size: number; // total_size: number;
downloaded_size: number; // downloaded_size: number;
state: string; // state: string;
} // }
export class BTGrpcClient { // export class BTGrpcClient {
private client: any; // private client: any;
private progressCallbacks: Map<string, ProgressCallback[]> = new Map(); // private progressCallbacks: Map<string, ProgressCallback[]> = new Map();
private progressStream: any = null; // private progressStream: any = null;
constructor() { // constructor() {
this.initializeClient(); // this.initializeClient();
} // }
private initializeClient() { // private initializeClient() {
try { // try {
const PROTO_PATH = path.join(__dirname, 'protos', 'bittorrent.proto'); // const PROTO_PATH = path.join(__dirname, 'protos', 'bittorrent.proto');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, { // const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true, // keepCase: true,
longs: String, // longs: String,
enums: String, // enums: String,
defaults: true, // defaults: true,
oneofs: true, // oneofs: true,
}); // });
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition); // const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
const bittorrent = protoDescriptor.bittorrent as any; // const bittorrent = protoDescriptor.bittorrent as any;
// 连接到后端 Agent假设运行在 localhost:50051 // // 连接到后端 Agent假设运行在 localhost:50051
this.client = new bittorrent.BTDownloadService( // this.client = new bittorrent.BTDownloadService(
'localhost:50051', // 'localhost:50051',
grpc.credentials.createInsecure() // grpc.credentials.createInsecure()
); // );
console.log('gRPC客户端初始化成功'); // console.log('gRPC客户端初始化成功');
this.setupProgressStream(); // this.setupProgressStream();
} catch (error) { // } catch (error) {
console.error('gRPC客户端初始化失败:', error); // console.error('gRPC客户端初始化失败:', error);
} // }
} // }
// 设置进度流监听 // // 设置进度流监听
private setupProgressStream() { // private setupProgressStream() {
try { // try {
this.progressStream = this.client.SubscribeProgress({}); // this.progressStream = this.client.SubscribeProgress({});
// 注册数据事件监听器,接收进度更新 // // 注册数据事件监听器,接收进度更新
this.progressStream.on('data', (progress: ProgressUpdate) => { // this.progressStream.on('data', (progress: ProgressUpdate) => {
console.log('收到进度数据:', progress); // 添加调试日志 // console.log('收到进度数据:', progress); // 添加调试日志
// BTGrpcClient 通过IPC将进度发送给主进程 // // BTGrpcClient 通过IPC将进度发送给主进程
this.progressCallbacks.forEach((callbacks, downloadId) => { // this.progressCallbacks.forEach((callbacks, downloadId) => {
if (downloadId === 'all' || downloadId === progress.download_id) { // if (downloadId === 'all' || downloadId === progress.download_id) {
callbacks.forEach(callback => callback(progress)); // callbacks.forEach(callback => callback(progress));
} // }
}); // });
// 如果有针对特定下载ID的回调也要通知 // // 如果有针对特定下载ID的回调也要通知
const specificCallbacks = this.progressCallbacks.get(progress.download_id); // const specificCallbacks = this.progressCallbacks.get(progress.download_id);
if (specificCallbacks) { // if (specificCallbacks) {
specificCallbacks.forEach(callback => callback(progress)); // specificCallbacks.forEach(callback => callback(progress));
} // }
}); // });
this.progressStream.on('error', (error: Error) => { // this.progressStream.on('error', (error: Error) => {
console.error('进度流错误:', error); // console.error('进度流错误:', error);
}); // });
this.progressStream.on('end', () => { // this.progressStream.on('end', () => {
console.log('进度流结束'); // console.log('进度流结束');
}); // });
} catch (error) { // } catch (error) {
console.error('设置进度流失败:', error); // console.error('设置进度流失败:', error);
} // }
} // }
// 开始下载 // // 开始下载
startDownload(request: DownloadRequest): Promise<any> { // startDownload(request: DownloadRequest): Promise<any> {
return new Promise((resolve, reject) => { // return new Promise((resolve, reject) => {
// MockBTService 接收请求,创建下载任务 // // MockBTService 接收请求,创建下载任务
this.client.StartDownload(request, (error: grpc.ServiceError, response: any) => { // this.client.StartDownload(request, (error: grpc.ServiceError, response: any) => {
if (error) { // if (error) {
reject(error); // reject(error);
} else { // } else {
resolve(response); // resolve(response);
} // }
}); // });
}); // });
} // }
// 停止下载 // // 停止下载
stopDownload(downloadId: string): Promise<any> { // stopDownload(downloadId: string): Promise<any> {
return new Promise((resolve, reject) => { // return new Promise((resolve, reject) => {
this.client.StopDownload({ download_id: downloadId }, (error: grpc.ServiceError, response: any) => { // this.client.StopDownload({ download_id: downloadId }, (error: grpc.ServiceError, response: any) => {
if (error) { // if (error) {
reject(error); // reject(error);
} else { // } else {
resolve(response); // resolve(response);
} // }
}); // });
}); // });
} // }
// 注册进度回调 // // 注册进度回调
registerProgressCallback(downloadId: string, callback: ProgressCallback) { // registerProgressCallback(downloadId: string, callback: ProgressCallback) {
if (!this.progressCallbacks.has(downloadId)) { // if (!this.progressCallbacks.has(downloadId)) {
this.progressCallbacks.set(downloadId, []); // this.progressCallbacks.set(downloadId, []);
} // }
this.progressCallbacks.get(downloadId)!.push(callback); // this.progressCallbacks.get(downloadId)!.push(callback);
} // }
// 移除进度回调 // // 移除进度回调
removeProgressCallback(downloadId: string, callback: ProgressCallback) { // removeProgressCallback(downloadId: string, callback: ProgressCallback) {
const callbacks = this.progressCallbacks.get(downloadId); // const callbacks = this.progressCallbacks.get(downloadId);
if (callbacks) { // if (callbacks) {
const index = callbacks.indexOf(callback); // const index = callbacks.indexOf(callback);
if (index > -1) { // if (index > -1) {
callbacks.splice(index, 1); // callbacks.splice(index, 1);
} // }
} // }
} // }
// 检查连接状态 // // 检查连接状态
isConnected(): boolean { // isConnected(): boolean {
return this.client && this.client.getChannel().getConnectivityState(true) === grpc.connectivityState.READY; // return this.client && this.client.getChannel().getConnectivityState(true) === grpc.connectivityState.READY;
} // }
// 重新连接 // // 重新连接
reconnect() { // reconnect() {
try { // try {
this.client.close(); // this.client.close();
this.initializeClient(); // this.initializeClient();
} catch (error) { // } catch (error) {
console.error('重新连接失败:', error); // console.error('重新连接失败:', error);
} // }
} // }
getConnectionState(): string { // getConnectionState(): string {
if (!this.client) return 'NOT_CREATED'; // if (!this.client) return 'NOT_CREATED';
const state = this.client.getChannel().getConnectivityState(false); // const state = this.client.getChannel().getConnectivityState(false);
const stateNames = { // const stateNames = {
[grpc.connectivityState.IDLE]: 'IDLE', // [grpc.connectivityState.IDLE]: 'IDLE',
[grpc.connectivityState.CONNECTING]: 'CONNECTING', // [grpc.connectivityState.CONNECTING]: 'CONNECTING',
[grpc.connectivityState.READY]: 'READY', // [grpc.connectivityState.READY]: 'READY',
[grpc.connectivityState.TRANSIENT_FAILURE]: 'TRANSIENT_FAILURE', // [grpc.connectivityState.TRANSIENT_FAILURE]: 'TRANSIENT_FAILURE',
[grpc.connectivityState.SHUTDOWN]: 'SHUTDOWN' // [grpc.connectivityState.SHUTDOWN]: 'SHUTDOWN'
} as const; // } as const;
// 使用类型断言确保 state 是合法的 key // // 使用类型断言确保 state 是合法的 key
return stateNames[state as keyof typeof stateNames] || 'UNKNOWN'; // return stateNames[state as keyof typeof stateNames] || 'UNKNOWN';
} // }
// 添加测试方法 // // 添加测试方法
async testConnection(): Promise<boolean> { // async testConnection(): Promise<boolean> {
try { // try {
// 尝试调用一个简单的方法来测试连接 // // 尝试调用一个简单的方法来测试连接
await new Promise((resolve, reject) => { // await new Promise((resolve, reject) => {
this.client.ListDownloads({}, (error: any, response: any) => { // this.client.ListDownloads({}, (error: any, response: any) => {
if (error && error.code !== grpc.status.UNIMPLEMENTED) { // if (error && error.code !== grpc.status.UNIMPLEMENTED) {
reject(error); // reject(error);
} else { // } else {
resolve(response); // resolve(response);
} // }
}); // });
}); // });
return true; // return true;
} catch (error) { // } catch (error) {
console.error('连接测试失败:', error); // console.error('连接测试失败:', error);
return false; // return false;
} // }
} // }
} // }

View File

@ -1,323 +1,323 @@
// 本地测试用的 gRPC 服务器,用于模拟 BT 下载 // // 本地测试用的 gRPC 服务器,用于模拟 BT 下载
// src/grpc/MockBTService.ts // // src/grpc/MockBTService.ts
import * as grpc from '@grpc/grpc-js'; // import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader'; // import * as protoLoader from '@grpc/proto-loader';
import path from 'path'; // import path from 'path';
// 进度更新接口 // // 进度更新接口
interface ProgressUpdate { // interface ProgressUpdate {
download_id: string; // download_id: string;
progress: number; // progress: number;
download_speed: number; // download_speed: number;
item_name?: string; // 可选的文件名字段 // item_name?: string; // 可选的文件名字段
upload_speed: number; // upload_speed: number;
eta: number; // eta: number;
total_size: number; // total_size: number;
downloaded_size: number; // downloaded_size: number;
state: string; // state: string;
} // }
// 模拟的下载任务 // // 模拟的下载任务
interface MockDownloadTask { // interface MockDownloadTask {
id: string; // id: string;
itemName: string; // itemName: string;
torrentUrl: string; // torrentUrl: string;
progress: number; // progress: number;
totalSize: number; // totalSize: number;
downloadedSize: number; // downloadedSize: number;
downloadSpeed: number; // downloadSpeed: number;
state: 'downloading' | 'completed' | 'error' | 'paused'; // state: 'downloading' | 'completed' | 'error' | 'paused';
startTime: number; // startTime: number;
} // }
export class MockBTService { // export class MockBTService {
private server: grpc.Server; // private server: grpc.Server;
private activeDownloads: Map<string, MockDownloadTask> = new Map(); // private activeDownloads: Map<string, MockDownloadTask> = new Map();
private progressIntervals: Map<string, NodeJS.Timeout> = new Map(); // private progressIntervals: Map<string, NodeJS.Timeout> = new Map();
// 存储所有的进度回调函数,发送信息 // // 存储所有的进度回调函数,发送信息
private progressCallbacks: ((progress: ProgressUpdate) => void)[] = []; // private progressCallbacks: ((progress: ProgressUpdate) => void)[] = [];
constructor() { // constructor() {
this.server = new grpc.Server(); // this.server = new grpc.Server();
this.setupService(); // this.setupService();
} // }
private setupService() { // private setupService() {
// 设置gRPC服务,加载 bittorrent.proto 定义,实现所有gRPC服务方法 // // 设置gRPC服务,加载 bittorrent.proto 定义,实现所有gRPC服务方法
const PROTO_PATH = path.join(__dirname, 'protos', 'bittorrent.proto'); // const PROTO_PATH = path.join(__dirname, 'protos', 'bittorrent.proto');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, { // const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true, // keepCase: true,
longs: String, // longs: String,
enums: String, // enums: String,
defaults: true, // defaults: true,
oneofs: true, // oneofs: true,
}); // });
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition); // const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
const bittorrent = protoDescriptor.bittorrent as any; // const bittorrent = protoDescriptor.bittorrent as any;
// 实现 gRPC 服务方法 // // 实现 gRPC 服务方法
this.server.addService(bittorrent.BTDownloadService.service, { // this.server.addService(bittorrent.BTDownloadService.service, {
StartDownload: this.startDownload.bind(this), // StartDownload: this.startDownload.bind(this),
StopDownload: this.stopDownload.bind(this), // StopDownload: this.stopDownload.bind(this),
GetDownloadStatus: this.getDownloadStatus.bind(this), // GetDownloadStatus: this.getDownloadStatus.bind(this),
ListDownloads: this.listDownloads.bind(this), // ListDownloads: this.listDownloads.bind(this),
SubscribeProgress: this.subscribeProgress.bind(this), // SubscribeProgress: this.subscribeProgress.bind(this),
}); // });
} // }
// 开始下载 // // 开始下载
// 在 startDownload 方法中,可以添加根据文件名设置不同大小的逻辑 // // 在 startDownload 方法中,可以添加根据文件名设置不同大小的逻辑
private startDownload(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) { // private startDownload(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
const { torrent_url, item_name, item_id } = call.request; // const { torrent_url, item_name, item_id } = call.request;
const downloadId = item_id || `download-${Date.now()}`; // const downloadId = item_id || `download-${Date.now()}`;
console.log(`模拟开始下载: ${item_name}, ID: ${downloadId}`); // console.log(`模拟开始下载: ${item_name}, ID: ${downloadId}`);
// 根据文件名或ID设置不同大小的文件 // // 根据文件名或ID设置不同大小的文件
let fileSize = 1024 * 1024 * 100; // 默认100MB // let fileSize = 1024 * 1024 * 100; // 默认100MB
if (item_name.includes('large') || item_name.includes('大文件')) { // if (item_name.includes('large') || item_name.includes('大文件')) {
fileSize = 4 * 1024 * 1024 * 1024; // 4GB // fileSize = 4 * 1024 * 1024 * 1024; // 4GB
} else if (item_name.includes('medium') || item_name.includes('中等')) { // } else if (item_name.includes('medium') || item_name.includes('中等')) {
fileSize = 1024 * 1024 * 1024; // 1GB // fileSize = 1024 * 1024 * 1024; // 1GB
} else if (item_name.includes('small') || item_name.includes('小文件')) { // } else if (item_name.includes('small') || item_name.includes('小文件')) {
fileSize = 100 * 1024 * 1024; // 100MB // fileSize = 100 * 1024 * 1024; // 100MB
} // }
// 创建模拟下载任务 // // 创建模拟下载任务
const task: MockDownloadTask = { // const task: MockDownloadTask = {
id: downloadId, // id: downloadId,
itemName: item_name, // itemName: item_name,
torrentUrl: torrent_url, // torrentUrl: torrent_url,
progress: 0, // progress: 0,
totalSize: fileSize, // totalSize: fileSize,
downloadedSize: 0, // downloadedSize: 0,
downloadSpeed: 1024 * 1024 * 2, // 2MB/s // downloadSpeed: 1024 * 1024 * 2, // 2MB/s
state: 'downloading', // state: 'downloading',
startTime: Date.now(), // startTime: Date.now(),
}; // };
this.activeDownloads.set(downloadId, task); // this.activeDownloads.set(downloadId, task);
console.log(`已创建下载任务: ${downloadId}, 大小: ${fileSize} bytes`); // console.log(`已创建下载任务: ${downloadId}, 大小: ${fileSize} bytes`);
// 启动进度模拟 // // 启动进度模拟
this.startProgressSimulation(downloadId); // this.startProgressSimulation(downloadId);
callback(null, { // callback(null, {
success: true, // success: true,
message: '下载已开始', // message: '下载已开始',
download_id: downloadId, // download_id: downloadId,
}); // });
} // }
// 停止下载 // // 停止下载
private stopDownload(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) { // private stopDownload(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
const { download_id } = call.request; // const { download_id } = call.request;
if (this.activeDownloads.has(download_id)) { // if (this.activeDownloads.has(download_id)) {
const task = this.activeDownloads.get(download_id)!; // const task = this.activeDownloads.get(download_id)!;
task.state = 'paused'; // task.state = 'paused';
// 清除进度定时器 // // 清除进度定时器
if (this.progressIntervals.has(download_id)) { // if (this.progressIntervals.has(download_id)) {
clearInterval(this.progressIntervals.get(download_id)); // clearInterval(this.progressIntervals.get(download_id));
this.progressIntervals.delete(download_id); // this.progressIntervals.delete(download_id);
} // }
console.log(`模拟停止下载: ${download_id}`); // console.log(`模拟停止下载: ${download_id}`);
callback(null, { success: true, message: '下载已停止' }); // callback(null, { success: true, message: '下载已停止' });
} else { // } else {
callback({ // callback({
code: grpc.status.NOT_FOUND, // code: grpc.status.NOT_FOUND,
message: `下载任务不存在: ${download_id}` // message: `下载任务不存在: ${download_id}`
}); // });
} // }
} // }
// 获取下载状态 // // 获取下载状态
private getDownloadStatus(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) { // private getDownloadStatus(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
const { download_id } = call.request; // const { download_id } = call.request;
if (this.activeDownloads.has(download_id)) { // if (this.activeDownloads.has(download_id)) {
const task = this.activeDownloads.get(download_id)!; // const task = this.activeDownloads.get(download_id)!;
callback(null, { // callback(null, {
download_id: task.id, // download_id: task.id,
progress: task.progress, // progress: task.progress,
download_speed: task.downloadSpeed, // download_speed: task.downloadSpeed,
state: task.state, // state: task.state,
total_size: task.totalSize, // total_size: task.totalSize,
downloaded_size: task.downloadedSize, // downloaded_size: task.downloadedSize,
}); // });
} else { // } else {
callback({ // callback({
code: grpc.status.NOT_FOUND, // code: grpc.status.NOT_FOUND,
message: `下载任务不存在: ${download_id}` // message: `下载任务不存在: ${download_id}`
}); // });
} // }
} // }
// 列出所有下载 // // 列出所有下载
private listDownloads(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) { // private listDownloads(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
const downloads = Array.from(this.activeDownloads.values()).map(task => ({ // const downloads = Array.from(this.activeDownloads.values()).map(task => ({
download_id: task.id, // download_id: task.id,
item_name: task.itemName, // item_name: task.itemName,
progress: task.progress, // progress: task.progress,
download_speed: task.downloadSpeed, // download_speed: task.downloadSpeed,
state: task.state, // state: task.state,
total_size: task.totalSize, // total_size: task.totalSize,
downloaded_size: task.downloadedSize, // downloaded_size: task.downloadedSize,
})); // }));
callback(null, { downloads }); // callback(null, { downloads });
} // }
// 订阅进度更新(流式响应) // // 订阅进度更新(流式响应)
private subscribeProgress(call: grpc.ServerWritableStream<any, any>) { // private subscribeProgress(call: grpc.ServerWritableStream<any, any>) {
console.log('客户端订阅了进度更新'); // console.log('客户端订阅了进度更新');
// 存储回调以便发送进度更新 // // 存储回调以便发送进度更新
const callback = (progress: ProgressUpdate) => { // const callback = (progress: ProgressUpdate) => {
try { // try {
call.write(progress); // call.write(progress);
} catch (error) { // } catch (error) {
console.error('发送进度更新失败:', error); // console.error('发送进度更新失败:', error);
} // }
}; // };
this.progressCallbacks.push(callback); // this.progressCallbacks.push(callback);
// 当客户端断开连接时清理 // // 当客户端断开连接时清理
call.on('cancelled', () => { // call.on('cancelled', () => {
console.log('客户端取消了进度订阅'); // console.log('客户端取消了进度订阅');
const index = this.progressCallbacks.indexOf(callback); // const index = this.progressCallbacks.indexOf(callback);
if (index > -1) { // if (index > -1) {
this.progressCallbacks.splice(index, 1); // this.progressCallbacks.splice(index, 1);
} // }
}); // });
call.on('error', (error) => { // call.on('error', (error) => {
console.error('进度流错误:', error); // console.error('进度流错误:', error);
const index = this.progressCallbacks.indexOf(callback); // const index = this.progressCallbacks.indexOf(callback);
if (index > -1) { // if (index > -1) {
this.progressCallbacks.splice(index, 1); // this.progressCallbacks.splice(index, 1);
} // }
}); // });
// 处理客户端断开连接 // // 处理客户端断开连接
call.on('end', () => { // call.on('end', () => {
console.log('客户端断开连接'); // console.log('客户端断开连接');
const index = this.progressCallbacks.indexOf(callback); // const index = this.progressCallbacks.indexOf(callback);
if (index > -1) { // if (index > -1) {
this.progressCallbacks.splice(index, 1); // this.progressCallbacks.splice(index, 1);
} // }
}); // });
} // }
// 启动进度模拟 // // 启动进度模拟
// 在 MockBTService.ts 中修改 startProgressSimulation 方法 // // 在 MockBTService.ts 中修改 startProgressSimulation 方法
private startProgressSimulation(downloadId: string) { // private startProgressSimulation(downloadId: string) {
const interval = setInterval(() => { // const interval = setInterval(() => {
if (this.activeDownloads.has(downloadId)) { // if (this.activeDownloads.has(downloadId)) {
const task = this.activeDownloads.get(downloadId)!; // const task = this.activeDownloads.get(downloadId)!;
if (task.state === 'downloading' && task.progress < 100) { // if (task.state === 'downloading' && task.progress < 100) {
// // 更新进度但确保不超过100% // // // 更新进度但确保不超过100%
// task.progress += Math.random() * 2; // // task.progress += Math.random() * 2;
// 使用固定的进度增加而不是随机值,使下载更加稳定 // // 使用固定的进度增加而不是随机值,使下载更加稳定
task.progress += 1.5; // 每秒增加1.5%的进度 // task.progress += 1.5; // 每秒增加1.5%的进度
// 确保进度不超过100% // // 确保进度不超过100%
if (task.progress >= 100) { // if (task.progress >= 100) {
task.progress = 100; // task.progress = 100;
task.state = 'completed'; // task.state = 'completed';
task.downloadedSize = task.totalSize; // 确保已完成时已下载大小等于总大小 // task.downloadedSize = task.totalSize; // 确保已完成时已下载大小等于总大小
console.log(`下载完成: ${downloadId}`); // console.log(`下载完成: ${downloadId}`);
// 清除定时器 // // 清除定时器
clearInterval(interval); // clearInterval(interval);
this.progressIntervals.delete(downloadId); // this.progressIntervals.delete(downloadId);
} else { // } else {
// 根据进度计算已下载大小 // // 根据进度计算已下载大小
task.downloadedSize = (task.totalSize * task.progress) / 100; // task.downloadedSize = (task.totalSize * task.progress) / 100;
} // }
// 计算剩余时间(秒) // // 计算剩余时间(秒)
let eta = 0; // let eta = 0;
if (task.progress < 100) { // if (task.progress < 100) {
// 基于当前速度计算剩余时间 // // 基于当前速度计算剩余时间
const progressPerSecond = 1.5; // 每秒进度百分比 // const progressPerSecond = 1.5; // 每秒进度百分比
eta = Math.round(((100 - task.progress) / progressPerSecond)); // eta = Math.round(((100 - task.progress) / progressPerSecond));
} // }
// 发送进度更新 // // 发送进度更新
this.sendProgressUpdate({ // this.sendProgressUpdate({
download_id: task.id, // download_id: task.id,
item_name: task.itemName, // item_name: task.itemName,
progress: task.progress, // progress: task.progress,
download_speed: task.downloadSpeed, // download_speed: task.downloadSpeed,
upload_speed: 1024 * 512, // 512KB/s 上传速度 // upload_speed: 1024 * 512, // 512KB/s 上传速度
eta: task.progress >= 100 ? 0 : Math.round(((100 - task.progress) / 2) * 1000), // eta: task.progress >= 100 ? 0 : Math.round(((100 - task.progress) / 2) * 1000),
total_size: task.totalSize, // total_size: task.totalSize,
downloaded_size: task.downloadedSize, // downloaded_size: task.downloadedSize,
state: task.state, // state: task.state,
}); // });
} // }
} else { // } else {
clearInterval(interval); // clearInterval(interval);
this.progressIntervals.delete(downloadId); // this.progressIntervals.delete(downloadId);
} // }
}, 1000); // 每秒更新一次进度 // }, 1000); // 每秒更新一次进度
this.progressIntervals.set(downloadId, interval); // this.progressIntervals.set(downloadId, interval);
} // }
// 发送进度更新给所有订阅者 // // 发送进度更新给所有订阅者
private sendProgressUpdate(progress: ProgressUpdate) { // private sendProgressUpdate(progress: ProgressUpdate) {
console.log('发送进度更新:', progress); // console.log('发送进度更新:', progress);
// 通过已注册的回调函数传递给 BTGrpcClient // // 通过已注册的回调函数传递给 BTGrpcClient
this.progressCallbacks.forEach((callback, index) => { // this.progressCallbacks.forEach((callback, index) => {
try { // try {
callback(progress); // callback(progress);
} catch (error) { // } catch (error) {
console.error(`发送进度更新给回调 ${index} 失败:`, error); // console.error(`发送进度更新给回调 ${index} 失败:`, error);
} // }
}); // });
} // }
// 绑定端口并启动服务器 // // 绑定端口并启动服务器
start(port: number = 50051): Promise<void> { // start(port: number = 50051): Promise<void> {
return new Promise((resolve, reject) => { // return new Promise((resolve, reject) => {
this.server.bindAsync( // this.server.bindAsync(
`0.0.0.0:${port}`, // `0.0.0.0:${port}`,
grpc.ServerCredentials.createInsecure(), // grpc.ServerCredentials.createInsecure(),
(error, port) => { // (error, port) => {
if (error) { // if (error) {
reject(error); // reject(error);
} else { // } else {
this.server.start(); // this.server.start();
console.log(`Mock gRPC 服务器运行在端口 ${port}`); // console.log(`Mock gRPC 服务器运行在端口 ${port}`);
resolve(); // resolve();
} // }
} // }
); // );
}); // });
} // }
// 停止服务器 // // 停止服务器
stop(): Promise<void> { // stop(): Promise<void> {
return new Promise((resolve) => { // return new Promise((resolve) => {
// 清理所有定时器 // // 清理所有定时器
this.progressIntervals.forEach(interval => clearInterval(interval)); // this.progressIntervals.forEach(interval => clearInterval(interval));
this.progressIntervals.clear(); // this.progressIntervals.clear();
this.activeDownloads.clear(); // this.activeDownloads.clear();
this.progressCallbacks = []; // this.progressCallbacks = [];
this.server.tryShutdown(() => { // this.server.tryShutdown(() => {
console.log('Mock gRPC 服务器已停止'); // console.log('Mock gRPC 服务器已停止');
resolve(); // resolve();
}); // });
}); // });
} // }
} // }

View File

@ -0,0 +1,181 @@
// import { ipcMain,app,BrowserWindow } from 'electron';
// // 模拟grpc服务端
// import { BTGrpcClient } from '../grpc/BTGrpcClient';
// import { MockBTService } from '../grpc/MockBTService';
// // 客户端和服务端 通信
// const IS_TEST_MODE = true; // 设置为 false 时连接真实后端
// const GRPC_SERVER_PORT = 50051;
// // 先声明变量,但不立即初始化
// let btGrpcClient: BTGrpcClient | null = null;
// let mockServer: MockBTService | null = null;
// let healthCheckInterval: NodeJS.Timeout | null = null; // 声明一个变量来存储定时器
// // 初始化 gRPC 客户端
// async function initializeGrpc() {
// try {
// if (IS_TEST_MODE) {
// console.log('启动模拟 gRPC 服务器...');
// mockServer = new MockBTService();
// await mockServer.start(GRPC_SERVER_PORT);
// // 给一点时间让服务器启动
// await new Promise(resolve => setTimeout(resolve, 1000));
// }
// // 创建 BTGrpcClient 实例连接到gRPC服务
// btGrpcClient = new BTGrpcClient();
// // 注册进度回调函数,用于接收下载进度更新
// btGrpcClient.registerProgressCallback('all', (progress) => {
// const mainWindow = BrowserWindow.getFocusedWindow();
// if (mainWindow) {
// mainWindow.webContents.send('grpc-progress-update', progress);
// }
// });
// // 测试连接
// setTimeout(async () => {
// try {
// const connected = await btGrpcClient!.testConnection();
// console.log('gRPC 连接状态:', connected ? '已连接' : '未连接');
// const mainWindow = BrowserWindow.getFocusedWindow();
// if (mainWindow) {
// mainWindow.webContents.send('grpc-connection-status', {
// connected,
// isMock: IS_TEST_MODE
// });
// }
// } catch (error) {
// console.error('连接测试失败:', error);
// }
// }, 2000);
// // 启动健康检查
// startHealthCheck();
// } catch (error) {
// console.error('gRPC 初始化失败:', error);
// }
// }
// // 启动健康检查
// function startHealthCheck() {
// if (healthCheckInterval) {
// clearInterval(healthCheckInterval);
// }
// healthCheckInterval = setInterval(async () => {
// try {
// if (!btGrpcClient) return;
// const isConnected = await btGrpcClient.testConnection();
// const mainWindow = BrowserWindow.getFocusedWindow();
// if (mainWindow) {
// mainWindow.webContents.send('grpc-connection-status', {
// connected: isConnected,
// state: btGrpcClient.getConnectionState(),
// isMock: IS_TEST_MODE
// });
// }
// } catch (error) {
// console.error('健康检查失败:', error);
// }
// }, 5000); // 每5秒检查一次
// }
// // 应用启动时初始化
// app.whenReady().then(() => {
// initializeGrpc();
// });
// // 应用退出时清理
// app.on('before-quit', async () => {
// if (healthCheckInterval) {
// clearInterval(healthCheckInterval);
// }
// if (mockServer) {
// await mockServer.stop();
// }
// });
// // 添加 gRPC 相关的 IPC 处理程序
// ipcMain.handle('grpc-start-download', async (event, config) => {
// try {
// if (!btGrpcClient) {
// return {
// success: false,
// error: 'gRPC客户端未初始化'
// };
// }
// console.log('开始gRPC下载:', config);
// // gRPC客户端向服务端发起 StartDownload 调用
// const result = await btGrpcClient.startDownload({
// torrent_url: config.torrentUrl,
// item_name: config.itemName,
// item_id: config.itemId
// });
// return { success: true, data: result };
// } catch (error: any) {
// console.error('gRPC下载失败:', error);
// return {
// success: false,
// error: error.message || '未知错误',
// details: error.details
// };
// }
// });
// ipcMain.handle('grpc-stop-download', async (event, downloadId) => {
// try {
// if (!btGrpcClient) {
// return {
// success: false,
// error: 'gRPC客户端未初始化'
// };
// }
// const result = await btGrpcClient.stopDownload(downloadId);
// return { success: true, data: result };
// } catch (error: any) {
// return {
// success: false,
// error: error.message || '未知错误'
// };
// }
// });
// ipcMain.handle('grpc-check-connection', async () => {
// if (!btGrpcClient) {
// return { connected: false, error: 'gRPC客户端未初始化' };
// }
// return {
// connected: btGrpcClient.isConnected(),
// state: btGrpcClient.getConnectionState()
// };
// });
// // 重新连接 gRPC
// ipcMain.handle('grpc-reconnect', async () => {
// try {
// if (btGrpcClient) {
// btGrpcClient.reconnect();
// return { success: true, message: '正在重新连接' };
// } else {
// await initializeGrpc();
// return { success: true, message: '正在初始化连接' };
// }
// } catch (error: any) {
// return {
// success: false,
// error: error.message || '重新连接失败'
// };
// }
// });

View File

@ -1,190 +1,16 @@
import { ipcMain,app,BrowserWindow } from 'electron'; import { ipcMain,app } from 'electron';
import { getDeviceId, getWiredConnectionName,netmaskToCidr } from '../utils/utils'; import { getDeviceId, getWiredConnectionName, netmaskToCidr,simulateUpdate,performRealUpdate } from '../utils/utils';
import { BrowserWindow } from 'electron';
import { autoUpdater } from 'electron-updater';
import request from '../utils/request';
const { exec } = require('child_process'); const { exec } = require('child_process');
const { promisify } = require('util'); const { promisify } = require('util');
const execAsync = promisify(exec); const execAsync = promisify(exec);
// 模拟grpc服务端
import { BTGrpcClient } from '../grpc/BTGrpcClient';
import { MockBTService } from '../grpc/MockBTService';
// 客户端和服务端 通信
const IS_TEST_MODE = true; // 设置为 false 时连接真实后端
const GRPC_SERVER_PORT = 50051;
// 先声明变量,但不立即初始化
let btGrpcClient: BTGrpcClient | null = null;
let mockServer: MockBTService | null = null;
let healthCheckInterval: NodeJS.Timeout | null = null; // 声明一个变量来存储定时器
const window = getBrowserWindowRuntime(); const window = getBrowserWindowRuntime();
// 初始化 gRPC 客户端 let currentServerIp: string | undefined;
async function initializeGrpc() {
try {
if (IS_TEST_MODE) {
console.log('启动模拟 gRPC 服务器...');
mockServer = new MockBTService();
await mockServer.start(GRPC_SERVER_PORT);
// 给一点时间让服务器启动
await new Promise(resolve => setTimeout(resolve, 1000));
}
// 创建 BTGrpcClient 实例连接到gRPC服务
btGrpcClient = new BTGrpcClient();
// 注册进度回调函数,用于接收下载进度更新
btGrpcClient.registerProgressCallback('all', (progress) => {
const mainWindow = BrowserWindow.getFocusedWindow();
if (mainWindow) {
mainWindow.webContents.send('grpc-progress-update', progress);
}
});
// 测试连接
setTimeout(async () => {
try {
const connected = await btGrpcClient!.testConnection();
console.log('gRPC 连接状态:', connected ? '已连接' : '未连接');
const mainWindow = BrowserWindow.getFocusedWindow();
if (mainWindow) {
mainWindow.webContents.send('grpc-connection-status', {
connected,
isMock: IS_TEST_MODE
});
}
} catch (error) {
console.error('连接测试失败:', error);
}
}, 2000);
// 启动健康检查
startHealthCheck();
} catch (error) {
console.error('gRPC 初始化失败:', error);
}
}
// 启动健康检查
function startHealthCheck() {
if (healthCheckInterval) {
clearInterval(healthCheckInterval);
}
healthCheckInterval = setInterval(async () => {
try {
if (!btGrpcClient) return;
const isConnected = await btGrpcClient.testConnection();
const mainWindow = BrowserWindow.getFocusedWindow();
if (mainWindow) {
mainWindow.webContents.send('grpc-connection-status', {
connected: isConnected,
state: btGrpcClient.getConnectionState(),
isMock: IS_TEST_MODE
});
}
} catch (error) {
console.error('健康检查失败:', error);
}
}, 5000); // 每5秒检查一次
}
// 应用启动时初始化
app.whenReady().then(() => {
initializeGrpc();
});
// 应用退出时清理
app.on('before-quit', async () => {
if (healthCheckInterval) {
clearInterval(healthCheckInterval);
}
if (mockServer) {
await mockServer.stop();
}
});
// 添加 gRPC 相关的 IPC 处理程序
ipcMain.handle('grpc-start-download', async (event, config) => {
try {
if (!btGrpcClient) {
return {
success: false,
error: 'gRPC客户端未初始化'
};
}
console.log('开始gRPC下载:', config);
// gRPC客户端向服务端发起 StartDownload 调用
const result = await btGrpcClient.startDownload({
torrent_url: config.torrentUrl,
item_name: config.itemName,
item_id: config.itemId
});
return { success: true, data: result };
} catch (error: any) {
console.error('gRPC下载失败:', error);
return {
success: false,
error: error.message || '未知错误',
details: error.details
};
}
});
ipcMain.handle('grpc-stop-download', async (event, downloadId) => {
try {
if (!btGrpcClient) {
return {
success: false,
error: 'gRPC客户端未初始化'
};
}
const result = await btGrpcClient.stopDownload(downloadId);
return { success: true, data: result };
} catch (error: any) {
return {
success: false,
error: error.message || '未知错误'
};
}
});
ipcMain.handle('grpc-check-connection', async () => {
if (!btGrpcClient) {
return { connected: false, error: 'gRPC客户端未初始化' };
}
return {
connected: btGrpcClient.isConnected(),
state: btGrpcClient.getConnectionState()
};
});
// 重新连接 gRPC
ipcMain.handle('grpc-reconnect', async () => {
try {
if (btGrpcClient) {
btGrpcClient.reconnect();
return { success: true, message: '正在重新连接' };
} else {
await initializeGrpc();
return { success: true, message: '正在初始化连接' };
}
} catch (error: any) {
return {
success: false,
error: error.message || '重新连接失败'
};
}
});
// 监听渲染进程发送的消息 // 监听渲染进程发送的消息
ipcMain.handle('getPlatform', () => { ipcMain.handle('getPlatform', () => {
@ -197,23 +23,31 @@ ipcMain.on('close-app', () => {
}); });
ipcMain.on('minimize-app', () => { ipcMain.on('minimize-app', () => {
window?.minimize(); const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
focusedWindow.minimize();
}
// window?.minimize();
}); });
ipcMain.on('exit-kiosk', () => { ipcMain.on('exit-kiosk', () => {
if (window) { const focusedWindow = BrowserWindow.getFocusedWindow();
window.setFullScreen(false); if (focusedWindow) {
focusedWindow.setFullScreen(false);
} }
// if (window) {
// window.setFullScreen(false);
// }
}); });
ipcMain.handle('get-deviceid',async()=>{ ipcMain.handle('get-device-id',async()=>{
const deviceId = await getDeviceId(); const deviceId = await getDeviceId();
console.log(`Using device ID: ${deviceId}`); console.log(`Using device ID: ${deviceId}`);
// TODO:传给后端 // TODO:传给后端
}) })
/* IPC 处理应用有线网络配置 */ /* 1. 平台网络配置:IPC 处理应用有线网络配置 */
ipcMain.handle('apply-wired-config',async(event,config)=>{ ipcMain.handle('apply-wired-config',async(event,config)=>{
// return { // return {
// success: true, // success: true,
@ -234,7 +68,13 @@ ipcMain.handle('apply-wired-config',async(event,config)=>{
// 添加 IPv6 配置(如果存在 ipv6Gateway????ipv6和长度需要吗ui只写了ipv6网关 // 添加 IPv6 配置(如果存在 ipv6Gateway????ipv6和长度需要吗ui只写了ipv6网关
// ipv6PrefixLength 是 IPv6 地址的前缀长度,类似于 IPv4 中的子网掩码。???? // ipv6PrefixLength 是 IPv6 地址的前缀长度,类似于 IPv4 中的子网掩码。????
if (config.ipv6 && config.ipv6Gateway) { if (config.ipv6 && config.ipv6Gateway) {
modifyCmd += ` ipv6.method manual ipv6.addresses "${config.ipv6}/${config.ipv6PrefixLength || 64}" ipv6.gateway "${config.ipv6Gateway}"`; const ipv6PrefixLength = config.ipv6PrefixLength &&
config.ipv6PrefixLength >= 0 &&
config.ipv6PrefixLength <= 128 ?
config.ipv6PrefixLength : 64; // 默认使用64
modifyCmd += ` ipv6.method manual ipv6.addresses "${config.ipv6}/${ipv6PrefixLength}" ipv6.gateway "${config.ipv6Gateway}"`;
}else if (config.ipv6 || config.ipv6Gateway) {
console.warn('IPv6配置不完整需要同时提供IPv6地址和网关');
} }
// 执行配置命令 // 执行配置命令
@ -267,3 +107,161 @@ ipcMain.handle('apply-wired-config',async(event,config)=>{
}; };
} }
}) })
/**2. 服务器配置 */
ipcMain.handle('connect-server', async (event, { serverIp }) => {
console.log(`Connecting to server: ${serverIp}`);
try {
// 获取设备ID
const deviceId = await getDeviceId();
console.log(`Using device ID: ${deviceId}`);
// 构建新的API地址使用POST请求
const apiUrl = `http://${serverIp}:8113/api/nex/v1/client/authentication`;
console.log(`Testing API endpoint: ${apiUrl}`);
// 调用Authentication接口进行连接验证POST请求
const response = await request(apiUrl, {
method: 'POST',
timeout: 10000, // 10秒超时
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({ device_id: deviceId })
});
console.log('API response received:', response);
// 解析响应数据
let responseData;
try {
responseData = JSON.parse((response as any).data);
} catch (parseError) {
console.error('Failed to parse authentication response:', parseError);
return { success: false, message: '服务器响应格式错误' };
}
console.log('Parsed authentication response:', responseData);
// 检查认证结果 - 只有code等于200时才算成功
if (responseData.code === '200' || responseData.code === 200) {
// 认证成功存储服务器IP
currentServerIp = serverIp;
console.log('Authentication successful, server IP stored:', serverIp);
return { success: true, message: `已连接到服务器 ${serverIp}`, serverIp: serverIp };
} else {
// 认证失败,返回服务器提供的错误信息
const errorMessage = responseData.message || responseData.msg || responseData.error || '服务器认证失败';
console.log('Authentication failed:', errorMessage);
return { success: false, message: errorMessage };
}
} catch (error:any) {
console.error('Server connection error details:', {
message: error.message,
code: error.code,
errno: error.errno,
syscall: error.syscall,
address: error.address,
port: error.port
});
// 根据具体错误提供更准确的提示
let errorMessage = '请输入正确服务器ip或者联系管理员';
if (error.message.includes('ECONNREFUSED')) {
errorMessage = `无法连接到服务器 ${serverIp}:8113请检查服务器是否运行`;
} else if (error.message.includes('ENOTFOUND')) {
errorMessage = `无法解析服务器地址 ${serverIp}请检查IP地址是否正确`;
} else if (error.message.includes('请求超时')) {
errorMessage = `连接服务器 ${serverIp}:8113 超时,请检查网络连接`;
} else if (error.message.includes('ENETUNREACH')) {
errorMessage = `网络不可达,无法连接到服务器 ${serverIp}`;
} else if (error.message.includes('HTTP 404')) {
errorMessage = `服务器返回404错误Authentication接口路径可能不正确`;
} else if (error.message.includes('HTTP')) {
errorMessage = `服务器返回错误: ${error.message}`;
}
return {
success: false,
message: errorMessage
};
}
});
// 服务器IP获取
ipcMain.handle('get-current-server-ip', () => {
return currentServerIp;
});
/**3. 侦测管理平台 */
// 下载并更新客户端
let updateDownloadedInfo: any = null;
// 修改 download-and-update 处理器中的事件监听器
ipcMain.handle('download-and-update', async (event, url) => {
console.log('下载并更新客户端:', url);
// 重置更新状态
updateDownloadedInfo = null;
if (process.env.NODE_ENV === 'development') {
// 开发环境下使用模拟更新
return simulateUpdate(event, url);
} else {
// 生产环境下使用真实更新
// 清除之前的事件监听器,避免重复注册
autoUpdater.removeAllListeners('update-downloaded');
// 监听更新下载完成事件
autoUpdater.on('update-downloaded', (info) => {
console.log('更新已下载=====>', info);
updateDownloadedInfo = info;
});
return performRealUpdate(event, url);
}
});
// 安装更新并重启应用
ipcMain.on('install-update-and-restart', () => {
console.log('安装更新并重启应用...');
if (process.env.NODE_ENV === 'development') {
// 开发环境下使用模拟更新
console.log('开发环境:模拟重启应用');
// 直接调用,添加小延迟
app.quit();
} else {
// 生产环境下使用真实更新
if (!updateDownloadedInfo) {
console.warn('没有已完成下载的更新');
// 可以选择不执行重启或提示用户
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
focusedWindow.webContents.send('update-not-available', {
message: '没有已完成下载的更新,请先下载更新'
});
}
return;
}
console.log('生产环境:安装更新并重启');
try {
autoUpdater.quitAndInstall(false, true);
} catch (installError) {
console.error('自动更新安装失败,降级到手动重启:', installError);
// app.relaunch();
// app.quit();
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
focusedWindow.webContents.send('update-install-error', {
message: '更新安装失败: ' + (installError instanceof Error ? installError.message : String(installError))
});
}
}
}
});

View File

@ -8,19 +8,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
closeApp: () => ipcRenderer.send('close-app'), closeApp: () => ipcRenderer.send('close-app'),
minimizeApp: () => ipcRenderer.send('minimize-app'), minimizeApp: () => ipcRenderer.send('minimize-app'),
exitKiosk: () => ipcRenderer.send('exit-kiosk'), exitKiosk: () => ipcRenderer.send('exit-kiosk'),
// 版本更新相关API
// 新增的 gRPC API downloadAndUpdate: (url: string) => ipcRenderer.invoke('download-and-update', url),
grpcStartDownload: (config: any) => ipcRenderer.invoke('grpc-start-download', config), // 服务器IP获取
grpcStopDownload: (downloadId: string) => ipcRenderer.invoke('grpc-stop-download', downloadId), getCurrentServerIp: () => ipcRenderer.invoke('get-current-server-ip'),
grpcCheckConnection: () => ipcRenderer.invoke('grpc-check-connection'),
// gRPC 进度监听
onGrpcProgress: (callback: (progress: any) => void) => {
ipcRenderer.on('grpc-progress-update', (_, progress) => callback(progress));
},
// 移除监听器
removeAllGrpcProgressListeners: () => {
ipcRenderer.removeAllListeners('grpc-progress-update');
},
// 事件监听 // 事件监听
onMainProcessMessage: (callback: (data: string) => void) => { onMainProcessMessage: (callback: (data: string) => void) => {

View File

@ -0,0 +1,132 @@
// src/main/utils/request.ts
/**
* // 使用方式1: 直接调用,类似 axios
const queryAlarmHandleReq = async (params = {}) => {
const res = await request({
url: `/api/biz-scene/v1/alarm/base/info/statistics`,
data: params,
method: 'POST',
});
return res;
};
// 使用方式2: 使用便捷方法
const queryAlarmHandleReq2 = async (params = {}) => {
const res = await post(`/api/biz-scene/v1/alarm/base/info/statistics`, params);
return res;
};
// 使用方式3: 使用默认导出
import axiosLikeRequest from './request';
const queryAlarmHandleReq3 = async (params = {}) => {
const res = await axiosLikeRequest({
url: `/api/biz-scene/v1/alarm/base/info/statistics`,
data: params,
method: 'POST',
});
return res;
};
*/
const https = require('https');
const http = require('http');
interface RequestOptions {
method?: string;
headers?: Record<string, string>;
data?: any;
timeout?: number;
}
export default function request(url:string, options: RequestOptions = {}) {
return new Promise((resolve, reject) => {
console.log(`\n=== HTTP REQUEST START ===`);
console.log(`URL: ${url}`);
console.log(`Method: ${options.method || 'GET'}`);
const urlObj = new URL(url);
const isHttps = urlObj.protocol === 'https:';
const httpModule = isHttps ? https : http;
// 确保端口号正确解析
let port: number;
if (!urlObj.port) {
port = isHttps ? 443 : 80;
} else {
port = parseInt(urlObj.port, 10);
}
const requestOptions = {
hostname: urlObj.hostname,
port: port,
path: urlObj.pathname + urlObj.search,
method: options.method || 'GET',
timeout: options.timeout || 5000,
headers: {
'User-Agent': 'NexOS/1.0',
'Accept': 'application/json, text/plain, */*',
...options.headers
}
};
console.log('Request Headers:', JSON.stringify(requestOptions.headers, null, 2));
if (options.data) {
console.log('Request Body:', options.data);
}
console.log('Request Options:', {
hostname: requestOptions.hostname,
port: requestOptions.port,
path: requestOptions.path,
method: requestOptions.method,
timeout: requestOptions.timeout
});
const req = httpModule.request(requestOptions, (res:any) => {
let data = '';
res.on('data', (chunk:any) => {
data += chunk;
});
res.on('end', () => {
try {
const result = {
statusCode: res.statusCode,
headers: res.headers,
data: data
};
console.log(`\n=== HTTP RESPONSE ===`);
console.log(`Status Code: ${res.statusCode}`);
console.log(`Response Headers:`, JSON.stringify(res.headers, null, 2));
console.log(`Response Body:`, data);
console.log(`=== HTTP REQUEST END ===\n`);
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(result);
} else {
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
}
} catch (parseError) {
reject(parseError);
}
});
});
req.on('timeout', () => {
req.destroy();
reject(new Error('请求超时'));
});
req.on('error', (error:any) => {
console.error('HTTP request error:', error);
reject(error);
});
if (options.data) {
req.write(options.data);
}
req.end();
});
}

View File

@ -1,4 +1,6 @@
import { BrowserWindow } from 'electron';
import { autoUpdater } from 'electron-updater';
const { exec } = require('child_process'); const { exec } = require('child_process');
const os = require('os'); const os = require('os');
const { promisify } = require('util'); const { promisify } = require('util');
@ -100,3 +102,99 @@ export function netmaskToCidr(netmask:string) {
}; };
return netmaskMap[netmask] || '24'; return netmaskMap[netmask] || '24';
} }
/**模拟更新客户端 */
export async function simulateUpdate(event: any, url: any) {
const focusedWindow = BrowserWindow.getFocusedWindow();
return new Promise((resolve) => {
let progress = 0;
const interval = setInterval(() => {
progress += 10;
if (focusedWindow) {
focusedWindow.webContents.send('update-progress', {
percent: progress,
transferred: progress,
total: 100,
bytesPerSecond: 1024
});
}
if (progress >= 100) {
clearInterval(interval);
// 关键:发送更新下载完成的通知
if (focusedWindow) {
focusedWindow.webContents.send('update-downloaded', {
message: '模拟更新已下载完成'
});
}
resolve({ success: true, message: '模拟更新完成' });
}
}, 500);
});
}
/**真实更新 */
export async function performRealUpdate(event: any, url: string) {
try {
console.log('开始检查更新...');
// 设置更新URL
if (url) {
autoUpdater.setFeedURL(url);
}
// 获取当前窗口用于发送进度更新
const focusedWindow = BrowserWindow.getFocusedWindow();
return new Promise((resolve, reject) => {
// 创建一次性监听器函数
const handleDownloadProgress = (progress: any) => {
console.log('下载进度:', progress);
if (focusedWindow) {
focusedWindow.webContents.send('update-progress', progress);
}
};
const handleUpdateDownloaded = (info: any) => {
console.log('更新下载完成:', info);
// 通知前端更新已下载完成
if (focusedWindow) {
focusedWindow.webContents.send('update-downloaded', info);
}
// 清理监听器
autoUpdater.removeListener('download-progress', handleDownloadProgress);
autoUpdater.removeListener('update-downloaded', handleUpdateDownloaded);
autoUpdater.removeListener('error', handleError);
resolve({ success: true, message: '更新已下载完成' });
};
const handleError = (error: Error) => {
console.error('更新错误:', error);
// 清理监听器
autoUpdater.removeListener('download-progress', handleDownloadProgress);
autoUpdater.removeListener('update-downloaded', handleUpdateDownloaded);
autoUpdater.removeListener('error', handleError);
reject(new Error(`更新失败: ${error.message}`));
};
// 注册监听器
autoUpdater.on('download-progress', handleDownloadProgress);
autoUpdater.on('update-downloaded', handleUpdateDownloaded);
autoUpdater.on('error', handleError);
// 开始检查更新
autoUpdater.checkForUpdates();
});
} catch (error) {
console.error('更新过程出错:', error);
return {
success: false,
error: `更新失败: ${error instanceof Error ? error.message : String(error)}`
};
}
}

View File

@ -1,20 +1,21 @@
.button-container { .button-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 15px 0;
flex-shrink: 0; flex-shrink: 0;
gap: 40px; gap: 40px;
margin-top: 60px;
margin-bottom: 80px;
.cancel-button { .cancel-button {
width: 140px; width: 140px;
height: 64px; height: 64px;
border-radius: 32px; border-radius: 32px;
background: transparent; background: rgba(1, 90, 255, 0.1);
border: 1px solid rgba(134, 133, 158, 1);
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: none;
span { span {
font-family: PingFang SC; font-family: PingFang SC;
@ -23,23 +24,23 @@
font-size: 20px; font-size: 20px;
line-height: 32px; line-height: 32px;
letter-spacing: 0%; letter-spacing: 0%;
color: rgba(229, 229, 229, 1); color: rgba(1, 90, 255, 1);
} }
&:hover { &:hover {
background: rgba(255, 255, 255, 0.05); background: rgba(1, 90, 255, 0.2);
} }
} }
.confirm-button { .confirm-button {
height: 64px; height: 64px;
border-radius: 32px; border-radius: 32px;
background: rgba(255, 255, 255, 0.3); background: rgba(1, 90, 255, 1);
border: none; border: none;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 30px; padding: 0 67px;
span { span {
font-family: PingFang SC; font-family: PingFang SC;
@ -60,7 +61,7 @@
} }
&:hover { &:hover {
background: rgba(255, 255, 255, 0.4); background: rgba(1, 90, 255, 1);
} }
} }
} }

View File

@ -1,11 +1,11 @@
.main-layout { .main-layout {
background-color: rgba(0, 9, 51, 0.9); background-image: url("../../../assets/bg.png");
background-size: 100% 100%;
} }
.main-content { .main-content {
// background-size: 100% 100%; background-color: transparent;
background-color: rgba(0, 9, 51, 0.9);
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
} }

View File

@ -26,11 +26,12 @@ const MainLayout: React.FC = () => {
useEffect(() => { useEffect(() => {
// TODO: 第一次来判断是否配置ip/DHCP、服务ip绑定终端 绑定:直接到版本更新页面 未绑定到配置ip/DHCP页面 // TODO: 第一次来判断是否配置ip/DHCP、服务ip绑定终端 绑定:直接到版本更新页面 未绑定到配置ip/DHCP页面
setTimeout(() => { setTimeout(() => {
history.push('/grpc'); // history.push('/login');
history.push('/configSteps?tab=terminalGetImage');
},1000) },1000)
// const fetchDeviceId = async () => { // const fetchDeviceId = async () => {
// try { // try {
// const res = await window.electronAPI.invoke('get-deviceid'); // const res = await window.electronAPI.invoke('get-device-id');
// console.log('获取设备ID:', res); // console.log('获取设备ID:', res);
// } catch (error) { // } catch (error) {
// console.error('获取设备ID失败:', error); // console.error('获取设备ID失败:', error);

View File

@ -0,0 +1,55 @@
// src/pages/components/ProgressBar/index.less
.progressBarContainer {
width: 620px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
.progressLine{
display: flex;
align-items: center;
width: 100%;
margin-bottom: 20px;
.progressContainer {
width: 560px;
height: 10px;
border-radius: 5px;
background: rgba(255, 255, 255, 0.1);
overflow: hidden;
color: #BBBBBB;
.progressBar {
height: 100%;
border-radius: 5px;
background: #E5E5E5;
transition: width 0.3s ease;
}
}
.progressPercent {
color: #BBBBBB;
margin-left: 10px;
min-width: 40px;
text-align: right;
font-family: PingFang SC;
font-weight: 500;
font-style: Medium;
font-size: 22px;
leading-trim: NONE;
line-height: 100%;
letter-spacing: -1.17px;
vertical-align: middle;
}
}
.progressText {
color: #BBBBBB;
font-family: PingFang SC;
font-weight: 500;
font-style: Medium;
font-size: 22px;
line-height: 100%;
letter-spacing: -1.17px;
vertical-align: middle;
}
}

View File

@ -0,0 +1,28 @@
// src/pages/components/ProgressBar/index.tsx
import React from 'react';
import styles from './index.less';
interface ProgressBarProps {
percent: number;
text?: string;
}
const ProgressBar: React.FC<ProgressBarProps> = ({ percent, text = '处理中...' }) => {
const clampedPercent = Math.min(100, Math.max(0, percent));
return (
<div className={styles.progressBarContainer}>
<div className={styles.progressLine}>
<div className={styles.progressContainer}>
<div
className={styles.progressBar}
style={{ width: `${clampedPercent}%` }}
/>
</div>
<div className={styles.progressPercent}>{clampedPercent}%</div>
</div>
<div className={styles.progressText}>{text}</div>
</div>
);
};
export default ProgressBar;

View File

@ -1,113 +1,171 @@
// src/pages/configSteps/components/networkConfig/index.less
.network-config { .network-config {
width: 100%;
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
box-sizing: border-box;
.tab-content-container-wrapper {
display: flex;
flex-direction: column;
flex: 1;
box-sizing: border-box;
// background: rgba(255, 255, 255, 1);
border-radius: 20px;
overflow: hidden;
z-index: 1;
position: relative;
.tab-container { .tab-container {
height: 70px;
flex-shrink: 0;
background: linear-gradient(180deg, rgba(229, 229, 229, 0.05) 0%, rgba(229, 229, 229, 0.05) 100%);
display: flex; display: flex;
justify-content: center; z-index: 2; // 提高tab容器的层级
align-items: center; position: relative;
flex-shrink: 0;
.special-tab-bg-con{
position: absolute;
width: 100%;
height: 100%;
z-index: 0;
.tab-bg{
height: 50%;
width: 100%;
}
.tab-grey-bg{
background: rgba(231, 235, 242, 1);
}
.tab-white-bg{
background: rgba(255, 255, 255, 1);
top: 50%;
}
}
.tab-item { .tab-item {
height: 80px;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
font-family: PingFang SC; font-family: PingFang SC;
font-weight: 400; font-weight: 400;
font-style: Heavy; font-style: Heavy;
top: -5px;
font-size: 20px; font-size: 20px;
line-height: 30px; line-height: 30px;
letter-spacing: 0%; letter-spacing: 0%;
padding: 0 30px; padding: 0 40px;
cursor: pointer; z-index: 1;
position: relative;
color: rgba(229, 229, 229, 0.5); &.dhcp-tab {
// 默认状态 - 左上角和左下角圆角
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
&.active { &.active {
color: rgba(229, 229, 229, 1); background: rgba(255, 255, 255, 1);
color: rgba(51, 51, 51, 1);
height: 80px;
// 激活状态 - 左上角和右上角圆角
border-radius: 20px 20px 0 0;
margin-bottom: -20px;
position: relative;
z-index: 3;
// 移除底部圆角
border-bottom-left-radius: 0;
} }
.indicator { &:not(.active) {
position: absolute; background: rgba(231, 235, 242, 1);
bottom: -15px; color: rgba(153, 153, 153, 1);
left: 50%; // 未激活状态 - 左下角和右下角圆角
transform: translateX(-50%); border-radius: 0 0 20px 0;
width: 8px; // 确保顶部没有圆角
height: 8px; border-top-left-radius: 0;
background: rgba(229, 229, 229, 1); }
border-radius: 50%; }
&.static-tab {
// 默认状态 - 右上角和右下角圆角
border-top-right-radius: 20px;
border-bottom-right-radius: 20px;
&.active {
background: rgba(255, 255, 255, 1);
color: rgba(51, 51, 51, 1);
height: 80px;
// 激活状态 - 左上角和右上角圆角
border-radius: 20px 20px 0 0;
margin-bottom: -20px;
position: relative;
z-index: 3;
// 移除底部圆角
border-bottom-right-radius: 0;
}
&:not(.active) {
background: rgba(231, 235, 242, 1);
color: rgba(153, 153, 153, 1);
// 未激活状态 - 左下角和右下角圆角
border-radius: 0 0 0 20px;
// 确保顶部没有圆角
border-top-right-radius: 0;
}
} }
} }
} }
.content-container { .content-container {
flex: 1; flex: 1;
overflow: hidden; padding: 40px 0;
box-sizing: border-box;
overflow-y: hidden;
min-height: 0;
display: flex;
background: rgba(255, 255, 255, 1);
justify-content: center;
z-index: 1;
.dhcp-content,
.static-ip-container {
height: 100%;
overflow-y: auto;
padding-right: 60px;
width: 568px;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: rgba(245, 245, 245, 1);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: rgba(224, 229, 236, 1);
border-radius: 4px;
}
} }
.dhcp-content { .dhcp-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
h2 { h2 {
font-family: PingFang SC; font-family: PingFang SC;
font-weight: 400; font-weight: 400;
font-style: Heavy; font-style: Heavy;
font-size: 24px; font-size: 18px;
color: rgba(229, 229, 229, 1); line-height: 32px;
margin-bottom: 20px; margin-bottom: 10px;
} }
p { p {
font-family: PingFang SC; color: #666;
font-size: 18px;
color: rgba(229, 229, 229, 0.8);
} }
} }
.static-ip-container { .static-ip-container {
display: flex;
flex-direction: column;
height: 100%;
.form-container { .form-container {
width: 500px; width: 500px;
margin: 0 auto; margin: 0 auto;
flex: 1;
overflow-y: auto;
padding: 20px 0px;
padding-right: 60px;
// 滚动条样式
&::-webkit-scrollbar {
width: 10px;
}
&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 28px;
margin: 10px 0;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 28px;
min-height: 30px;
}
// 表单项样式
.ant-form-item {
margin-bottom: 24px;
}
.ant-form-item-label {
padding: 0 0 12px 0;
}
.label { .label {
font-family: PingFang SC; font-family: PingFang SC;
@ -116,63 +174,29 @@
font-size: 18px; font-size: 18px;
line-height: 32px; line-height: 32px;
letter-spacing: 0%; letter-spacing: 0%;
color: rgba(229, 229, 229, 1); color: rgba(102, 102, 102, 1);
margin-bottom: 12px;
} }
.input-field { .input-field {
width: 100%; background: rgba(245, 245, 245, 1);
height: 56px; height: 56px;
background: rgba(255, 255, 255, 0.1);
border-radius: 28px; border-radius: 28px;
padding-left: 20px;
border: none; border: none;
padding: 0 24px;
box-sizing: border-box; &::placeholder {
color: rgba(187, 187, 187, 1);
font-family: PingFang SC; font-family: PingFang SC;
font-weight: 400; font-weight: 400;
font-style: Heavy; font-style: Heavy;
font-size: 18px; font-size: 18px;
leading-trim: NONE;
line-height: 32px; line-height: 32px;
letter-spacing: 0%; letter-spacing: 0%;
color: rgba(229, 229, 229, 1);
&::placeholder {
color: rgba(229, 229, 229, 0.5);
}
&:focus {
outline: none;
background: rgba(255, 255, 255, 0.15);
}
}
// 覆盖 Ant Design 的默认样式
.ant-input {
background: rgba(255, 255, 255, 0.1);
border-radius: 28px;
border: none;
padding: 0 24px;
box-sizing: border-box;
font-family: PingFang SC;
font-weight: 400;
font-style: Heavy;
font-size: 18px;
line-height: 32px;
letter-spacing: 0%;
color: rgba(229, 229, 229, 1);
height: 56px;
&::placeholder {
color: rgba(229, 229, 229, 0.5);
}
&:focus {
outline: none;
background: rgba(255, 255, 255, 0.15);
box-shadow: none;
} }
} }
} }
}
}
} }
} }

View File

@ -3,8 +3,9 @@ import styles from './index.less';
import cs from 'classnames'; import cs from 'classnames';
import { Form, Input, message } from 'antd'; import { Form, Input, message } from 'antd';
import ButtonCom from '../../../components/ButtonCom'; import ButtonCom from '../../../components/ButtonCom';
import { useConfigStep } from '@/contexts/ConfigStepContext';
const staticIpFormFields: CONFIG_STEPS.StaticFormFieldConfig[] = [ const staticIpFormFields: CONFIG_STEPS.FormFieldConfig[] = [
{ {
name: "ipv4", name: "ipv4",
label: "IPv4", label: "IPv4",
@ -44,6 +45,30 @@ const staticIpFormFields: CONFIG_STEPS.StaticFormFieldConfig[] = [
} }
] ]
}, },
{
name: "ipv6",
label: "IPv6地址",
type: "input",
placeholder: "请输入",
rules: [
{
pattern: /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)*::([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$|^::$/,
message: '请输入正确的IPv6地址格式'
}
]
},
{
name: "ipv6PrefixLength",
label: "IPv6前缀长度",
type: "input",
placeholder: "请输入前缀长度默认64",
rules: [
{
pattern: /^(?:[0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$/,
message: '请输入0-128之间的数字默认为64'
}
]
},
{ {
name: "ipv6Gateway", name: "ipv6Gateway",
label: "IPv6网关", label: "IPv6网关",
@ -86,6 +111,30 @@ const staticIpFormFields: CONFIG_STEPS.StaticFormFieldConfig[] = [
const NetworkConfig: React.FC = () => { const NetworkConfig: React.FC = () => {
const [activeTab, setActiveTab] = useState<'dhcp' | 'static'>('dhcp'); const [activeTab, setActiveTab] = useState<'dhcp' | 'static'>('dhcp');
const [form] = Form.useForm(); const [form] = Form.useForm();
const { goToNextTab } = useConfigStep();
const validateIPv6Config = (values: any) => {
// 如果提供了IPv6网关但没有IPv6地址给出提示
if (values.ipv6Gateway && !values.ipv6) {
message.warning('提供了IPv6网关但未提供IPv6地址IPv6配置可能不完整');
}
// 如果提供了IPv6地址但没有前缀长度使用默认值
if (values.ipv6 && !values.ipv6PrefixLength) {
message.info('未提供IPv6前缀长度将使用默认值64');
}
// 验证前缀长度范围
if (values.ipv6PrefixLength) {
const prefixLength = parseInt(values.ipv6PrefixLength, 10);
if (prefixLength < 0 || prefixLength > 128) {
message.error('IPv6前缀长度必须在0-128之间');
return false;
}
}
return true;
};
const handleSubmit = async () => { const handleSubmit = async () => {
if (activeTab === 'dhcp') { if (activeTab === 'dhcp') {
@ -95,6 +144,9 @@ const NetworkConfig: React.FC = () => {
console.log('网络配置返回信息成功:', res); console.log('网络配置返回信息成功:', res);
if(res.success){ if(res.success){
message.success('网络配置成功'); message.success('网络配置成功');
setTimeout(() => {
goToNextTab();
}, 300);
}else{ }else{
message.error(res.message || '网络配置失败'); message.error(res.message || '网络配置失败');
} }
@ -106,10 +158,26 @@ const NetworkConfig: React.FC = () => {
try { try {
const values = await form.validateFields(); const values = await form.validateFields();
console.log('表单提交数据:', values); console.log('表单提交数据:', values);
// 验证IPv6配置
if (!validateIPv6Config(values)) {
return;
}
// 处理IPv6前缀长度默认值
if (values.ipv6 && !values.ipv6PrefixLength) {
values.ipv6PrefixLength = 64; // 默认前缀长度
} else if (values.ipv6PrefixLength) {
values.ipv6PrefixLength = parseInt(values.ipv6PrefixLength, 10);
}
const res = await window.electronAPI.invoke('apply-wired-config',{ method: 'static', ...values }); const res = await window.electronAPI.invoke('apply-wired-config',{ method: 'static', ...values });
console.log('网络配置返回信息成功:', res); console.log('网络配置返回信息成功:', res);
if(res.success){ if(res.success){
message.success('网络配置成功'); message.success('网络配置成功');
setTimeout(() => {
goToNextTab();
}, 300);
}else{ }else{
message.error(res.message || '网络配置失败'); message.error(res.message || '网络配置失败');
} }
@ -154,26 +222,35 @@ const NetworkConfig: React.FC = () => {
return ( return (
<div className={styles["network-config"]}> <div className={styles["network-config"]}>
<div className={styles['tab-content-container-wrapper']}>
<div className={styles["tab-container"]}> <div className={styles["tab-container"]}>
<div className={styles["special-tab-bg-con"]}>
<div className={cs(styles["tab-bg"],styles["tab-grey-bg"])}></div>
<div className={cs(styles["tab-bg"],styles["tab-white-bg"])}></div>
</div>
<div <div
className={cs(styles["tab-item"], { [styles.active]: activeTab === 'dhcp' })} className={cs(styles["tab-item"], styles["dhcp-tab"], {
[styles.active]: activeTab === 'dhcp'
})}
onClick={() => setActiveTab('dhcp')} onClick={() => setActiveTab('dhcp')}
> >
<span>DHCP</span> <span>DHCP</span>
{activeTab === 'dhcp' && <div className={styles["indicator"]}></div>}
</div> </div>
<div <div
className={cs(styles["tab-item"], { [styles.active]: activeTab === 'static' })} className={cs(styles["tab-item"], styles["static-tab"], {
[styles.active]: activeTab === 'static'
})}
onClick={() => setActiveTab('static')} onClick={() => setActiveTab('static')}
> >
<span>IP</span> <span>IP</span>
{activeTab === 'static' && <div className={styles["indicator"]}></div>}
</div> </div>
</div> </div>
<div className={styles["content-container"]}> <div className={styles["content-container"]}>
{activeTab === 'dhcp' ? <DhcpComponent /> : <StaticIpComponent />} {activeTab === 'dhcp' ? <DhcpComponent /> : <StaticIpComponent />}
</div> </div>
</div>
<ButtonCom confirmText="确认并进入下一步" onConfirm={handleSubmit}/> <ButtonCom confirmText="确认并进入下一步" onConfirm={handleSubmit}/>
</div> </div>
); );

View File

@ -0,0 +1,54 @@
// src/pages/configSteps/components/networkConfig/index.less
.server-config {
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
.content-container {
flex: 1;
padding: 40px 0;
box-sizing: border-box;
overflow-y: hidden;
min-height: 0;
display: flex;
background: rgba(255, 255, 255, 1);
justify-content: center;
z-index: 1;
.server-container {
.form-container {
width: 500px;
margin: 0 auto;
.label {
font-family: PingFang SC;
font-weight: 400;
font-style: Heavy;
font-size: 18px;
line-height: 32px;
letter-spacing: 0%;
color: rgba(102, 102, 102, 1);
}
.input-field {
background: rgba(245, 245, 245, 1);
height: 56px;
border-radius: 28px;
padding-left: 20px;
border: none;
&::placeholder {
color: rgba(187, 187, 187, 1);
font-family: PingFang SC;
font-weight: 400;
font-style: Heavy;
font-size: 18px;
leading-trim: NONE;
line-height: 32px;
letter-spacing: 0%;
}
}
}
}
}
}

View File

@ -0,0 +1,83 @@
import React from 'react';
import ButtonCom from '../../../components/ButtonCom';
import { useConfigStep } from '@/contexts/ConfigStepContext';
import styles from './index.less';
import { Form, Input, message } from 'antd';
const serverIpFormFields: CONFIG_STEPS.FormFieldConfig[] = [
{
name: "serverIp",
label: "服务器IP地址",
type: "input",
placeholder: "请输入",
rules: [
{ required: true, message: '请输入服务器IP地址' },
{
pattern: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
message: '请输入正确的服务器IP地址格式'
}
]
}
];
const Index = () => {
const [form] = Form.useForm();
const { goToNextTab,setActiveTab } = useConfigStep();
const ServerComponent = () => (
<div className={styles["server-container"]}>
<Form
form={form}
layout="vertical"
requiredMark={false}
className={styles["form-container"]}
>
{serverIpFormFields.map(field => (
<Form.Item
key={field.name}
name={field.name}
label={<div className={styles["label"]}>{field.label}</div>}
rules={field.rules}
>
<Input
className={styles["input-field"]}
placeholder={field.placeholder}
/>
</Form.Item>
))}
</Form>
</div>
);
const handleSubmit= async () => {
const values = await form.validateFields();
console.log('表单提交数据:', values);
const { serverIp } = values;
const result = await window.electronAPI.invoke('connect-server', { serverIp });
console.log('连接结果:', result);
if (result.success) {
// 连接成功保存服务器IP到本地存储
localStorage.setItem('connected-server-ip', serverIp);
console.log('Connected server IP saved to localStorage:', serverIp);
// TODO:跳转到版本更新
// setActiveTab("watchManagement")
setActiveTab("terminalGetImage")
// await ipcRenderer.invoke('show-login-window');
} else {
message.error(result.message || '连接服务器失败');
}
}
return (
<div className={styles["server-config"]}>
<div className={styles["content-container"]}>
<ServerComponent />
</div>
<ButtonCom confirmText="确认并进入下一步" onConfirm={handleSubmit}/>
</div>
);
}
export default Index;

View File

@ -0,0 +1,115 @@
// src/pages/configSteps/components/networkConfig/index.less
.terminal-config {
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
.image-loading-container {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
width: 100%;
background: white;
border-radius: 20px;
.loading-wrapper {
display: flex;
flex-direction: column;
align-items: center;
.spinner {
width: 141px;
height: 140px;
position: relative;
// 动画1旋转
&.rotating {
animation: rotate 1.5s linear infinite;
}
.spinner-item {
position: absolute;
left: 50%;
top: 50%;
background: rgba(171, 184, 204, 1);
border-radius: 50%;
transform-origin: 0 0;
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loading-text {
font-family: "Microsoft YaHei UI";
font-weight: 700;
font-size: 34px;
line-height: 200%;
text-align: center;
color: rgba(51, 51, 51, 1);
margin-top: 70px;
}
.loading-subtext {
font-family: "PingFang SC";
font-weight: 500;
font-size: 22px;
line-height: 100%;
letter-spacing: -1.17px;
color: rgba(153, 153, 153, 1);
margin-top: 30px;
}
}
}
.success-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
flex: 1;
background: white;
.success-box {
width: 381px;
height: 260px;
border: 1px solid #ccc;
display: flex;
justify-content: center;
align-items: center;
margin-top: 100px;
}
.ready-icon {
display: flex;
align-items: center;
margin-top: 50px;
gap: 9px;
.ready-text {
font-family: "Microsoft YaHei UI";
font-weight: 700;
font-size: 34px;
line-height: 200%;
text-align: center;
color: rgba(51, 51, 51, 1);
}
}
.ready-subtext {
font-family: "PingFang SC";
font-weight: 500;
font-size: 22px;
line-height: 100%;
letter-spacing: -1.17px;
color: rgba(153, 153, 153, 1);
margin-top: 30px;
}
}
}

View File

@ -1,9 +1,89 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import ButtonCom from '../../../components/ButtonCom';
import { useConfigStep } from '@/contexts/ConfigStepContext';
import styles from './index.less';
import { message } from 'antd';
import { history } from 'umi';
import TrueIcon from '@assets/true.png'
const Index = () => { const Index = () => {
const { goToNextTab,setActiveTab } = useConfigStep();
const [imageStatus, setImageStatus] = useState<'loading' | 'success' | 'error'>('loading');
// 模拟获取镜像数据的API调用
useEffect(() => {
const fetchImageData = async () => {
try {
// 模拟API请求
await new Promise(resolve => setTimeout(resolve, 3000));
// 模拟随机成功或失败
const isSuccess = Math.random() > 0.3;
setImageStatus(isSuccess ? 'success' : 'loading');
// 如果失败,继续轮询检查状态
if (!isSuccess) {
const checkStatus = async () => {
// 模拟WebSocket或轮询检查
await new Promise(resolve => setTimeout(resolve, 5000));
setImageStatus('success'); // 模拟收到通知
};
checkStatus();
}
} catch (error) {
message.error('获取镜像数据失败');
}
};
fetchImageData();
}, []);
const ImageLoadingComponent = () => (
<div className={styles["image-loading-container"]}>
<div className={styles["loading-wrapper"]}>
<div className={styles["spinner"] + ' ' + styles["rotating"]}>
{[...Array(8)].map((_, index) => (
<div
key={index}
className={styles["spinner-item"]}
style={{
transform: `rotate(${-45 * index - 90}deg) translate(60px)`, // 添加 -90 度偏移
width: `${12 - index * 1.5}px`,
height: `${12 - index * 1.5}px`
}}
/>
))}
</div>
<div className={styles["loading-text"]}></div>
<div className={styles["loading-subtext"]}>...</div>
</div>
</div>
);
const SuccessComponent = () => (
<div className={styles["success-container"]}>
<div className={styles["success-box"]}>
{/* 这里可以显示实际的镜像信息 */}
<div></div>
</div>
<div className={styles["ready-icon"]}>
<img src={TrueIcon} />
<div className={styles["ready-text"]}></div>
</div>
<div className={styles["ready-subtext"]}>使</div>
</div>
);
const handleSubmit = () => {
history.push('/login');
}
return ( return (
<div> <div className={styles["terminal-config"]}>
{imageStatus === 'loading' && <ImageLoadingComponent />}
{imageStatus === 'success' && <SuccessComponent />}
{imageStatus === 'loading' && <div style={{height:'204px'}}></div>}
{imageStatus === 'success' && <ButtonCom confirmText="确认并立即使用" onConfirm={handleSubmit} showCancel={false} />}
</div> </div>
); );
} }

View File

@ -0,0 +1,122 @@
// src/pages/configSteps/components/watchManagement/index.less
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.content {
flex: 1;
width: 80%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.buttonWrapper {
align-self: center;
}
.latestContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
.iconText {
display: flex;
align-items: center;
justify-content: center;
border-radius: 28px;
background: #FFFFFF1A;
backdrop-filter: blur(100px);
font-family: PingFang SC;
font-weight: 400;
font-style: Heavy;
font-size: 22px;
line-height: 32px;
letter-spacing: 0%;
padding: 20px 40px;
margin-bottom: 30px;
.icon {
margin-right: 10px;
font-size: 24px;
font-weight: bold;
}
}
.description {
font-family: PingFang SC;
font-weight: 500;
font-style: Medium;
font-size: 22px;
line-height: 100%;
letter-spacing: -1.17px;
vertical-align: middle;
color: #BBBBBB;
text-align: center;
}
}
.outdatedContainer {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
.warningBox {
width: 200px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 28px;
background: #FFFFFF1A;
backdrop-filter: blur(100px);
font-family: PingFang SC;
font-weight: 400;
font-style: Heavy;
font-size: 22px;
line-height: 32px;
letter-spacing: 0%;
padding: 20px 20px;
margin-bottom: 8px;
color: #E5E5E5;
.icon {
margin-right: 10px;
}
}
.title {
font-family: Microsoft YaHei UI;
font-weight: 700;
font-style: Bold;
font-size: 40px;
line-height: 200%;
letter-spacing: 0%;
text-align: center;
color: #fff;
margin: 20px 0;
}
.description {
font-family: PingFang SC;
font-weight: 500;
font-style: Medium;
font-size: 22px;
line-height: 100%;
letter-spacing: -1.17px;
vertical-align: middle;
color: #BBBBBB;
text-align: center;
}
}
}
}

View File

@ -1,11 +1,261 @@
import React from 'react'; // src/pages/configSteps/components/watchManagement/index.tsx
import React, { useState, useEffect } from 'react';
import styles from './index.less';
import ProgressBar from '@/pages/components/ProgressBar';
import ButtonCom from '@/pages/components/ButtonCom';
import { useConfigStep } from '@/contexts/ConfigStepContext';
import WaringIcon from '@/assets/waringIcon.png';
import { message } from 'antd';
const Index = () => { const WatchManagement: React.FC = () => {
return ( const { goToNextTab } = useConfigStep();
<div> // 添加 'downloaded' 状态
const [status, setStatus] = useState<'checking' | 'latest' | 'outdated' | 'updating' | 'downloaded' | 'updated'>('checking');
</div> const [progress, setProgress] = useState(0);
); const [countdown, setCountdown] = useState(6);
// 模拟当前版本
const currentVersion:string = '1.0.0';
// 模拟最新版本
const latestVersion:string = '1.2.0';
// 模拟更新URL实际应该从后端获取
const updateUrl = 'http://localhost:3000/updates/linux';
useEffect(() => {
// 模拟检测版本
// const timer = setTimeout(() => {
// if (currentVersion === latestVersion) {
// setStatus('latest');
// } else {
// setStatus('outdated');
// }
// }, 2000);
// return () => clearTimeout(timer);
}, []);
// 监听更新进度和下载完成事件
useEffect(() => {
const handleUpdateProgress = (event: any, progressData: any) => {
console.log('收到更新进度:', progressData);
if (status === 'updating') {
setProgress(progressData.percent);
}
};
const handleUpdateDownloaded = (event: any, info: any) => {
console.log('更新已下载完成:', info);
setStatus('downloaded');
setProgress(100);
};
const handleUpdateError = (event: any, error: any) => {
console.error('更新出错:', error);
message.error('更新出错,请稍后重试');
setStatus('outdated'); // 出错时回到初始状态
};
const handleUpdateNotAvailable = (event:any, info:any) => {
// 显示提示信息给用户
console.log(info.message || '没有可安装的更新');
message.info('没有可安装的更新');
setStatus('latest');
} }
export default Index; // 监听更新进度事件
if (window.electronAPI) {
window.electronAPI.on('update-progress', handleUpdateProgress);
window.electronAPI.on('update-downloaded', handleUpdateDownloaded);
window.electronAPI.on('update-not-available', handleUpdateNotAvailable);
window.electronAPI.on('update-install-error', handleUpdateError);
}
// 清理监听器
return () => {
if (window.electronAPI) {
window.electronAPI.off('update-progress', handleUpdateProgress);
window.electronAPI.off('update-downloaded', handleUpdateDownloaded);
window.electronAPI.off('update-not-available', handleUpdateNotAvailable);
window.electronAPI.off('update-install-error', handleUpdateError);
}
};
}, [status]);
// useEffect(() => {
// let timer: NodeJS.Timeout;
// if (status === 'updated') {
// // 倒计时6秒后自动重启
// timer = setInterval(() => {
// setCountdown(prev => {
// if (prev <= 1) {
// clearInterval(timer);
// handleRestart();
// return 0;
// }
// return prev - 1;
// });
// }, 1000);
// }
// return () => {
// if (timer) clearInterval(timer);
// };
// }, [status]);
const handleCancel = () => {
console.log('取消操作');
};
const handleConfirmUpdate = () => {
// 确认更新客户端
setStatus('updating');
startUpdateProcess(updateUrl);
};
const handleGetImage = () => {
goToNextTab();
};
const handleRestart = () => {
// 安装更新并重启客户端
if (window.electronAPI) {
window.electronAPI.send('install-update-and-restart');
}
};
const startUpdateProcess = (url: string) => {
// 调用electron API下载并安装更新
if (window.electronAPI) {
window.electronAPI.downloadAndUpdate(url)
.then((result:any) => {
console.log('更新完成:', result);
if (result.success) {
// 不再直接设置为 updated等待 update-downloaded 事件
// setStatus('updated');
// setProgress(100);
}
})
.catch((error: any) => {
console.error('更新失败:', error);
setStatus('outdated'); // 回到初始状态
});
}
};
const renderContent = () => {
switch (status) {
case 'checking':
return (
<div className={styles.content}>
<ProgressBar percent={30} text="稍等片刻,正在侦测管理平台" />
<div className={styles.buttonWrapper}>
<ButtonCom
cancelText="取消"
onCancel={handleCancel}
showConfirm={false}
/>
</div>
</div>
);
case 'latest':
return (
<div className={styles.content}>
<div className={styles.latestContainer}>
<div className={styles.iconText}>
<span className={styles.icon}></span>
<span></span>
</div>
<p className={styles.description}></p>
</div>
<div className={styles.buttonWrapper}>
<ButtonCom
confirmText="确认并进入下一步"
onConfirm={goToNextTab}
showCancel={false}
/>
</div>
</div>
);
case 'outdated':
return (
<div className={styles.content}>
<div className={styles.outdatedContainer}>
<div className={styles.warningBox}>
<img src={WaringIcon} className={styles.icon}></img>
<span></span>
</div>
<p className={styles.title}></p>
<p className={styles.description}></p>
</div>
<div className={styles.buttonWrapper}>
<ButtonCom
cancelText="取消"
onCancel={handleCancel}
confirmText="确认并更新客户端"
onConfirm={handleConfirmUpdate}
/>
</div>
</div>
);
case 'updating':
return (
<div className={styles.content}>
<ProgressBar
percent={progress}
text={progress < 100 ? "更新中..." : "更新成功"}
/>
<div className={styles.buttonWrapper}>
<ButtonCom
cancelText="取消"
onCancel={handleCancel}
showConfirm={false}
/>
</div>
</div>
);
case 'downloaded':
return (
<div className={styles.content}>
<ProgressBar percent={100} text="下载完成,准备安装更新" />
<div className={styles.buttonWrapper}>
<ButtonCom
confirmText="立即安装"
onConfirm={handleRestart}
cancelText="稍后安装"
onCancel={handleCancel}
/>
</div>
</div>
);
case 'updated':
return (
<div className={styles.content}>
<ProgressBar percent={100} text={`更新成功,${countdown}秒后自动重启`} />
<div className={styles.buttonWrapper}>
<ButtonCom
confirmText="确认重启"
onConfirm={handleRestart}
showCancel={false}
/>
</div>
</div>
);
default:
return null;
}
};
return (
<div className={styles.container}>
{renderContent()}
</div>
);
};
export default WatchManagement;

View File

@ -1,70 +1,97 @@
// src/pages/configSteps/index.less
.config-step-container { .config-step-container {
width: 100%; display: flex;
flex-direction: column;
height: 100%; height: 100%;
padding-top: 24px; align-items: center;
display: flex;
flex-direction: column;
.tabs-container { .steps-container {
display: flex; display: flex;
width: 100%;
height: 60px;
flex-shrink: 0;
// background: rgba(229, 229, 229, 0.2);
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 48px;
margin-top: 80px;
margin-bottom: 60px;
.step-item {
display: flex;
align-items: center;
cursor: pointer; cursor: pointer;
position: relative;
.tab-label { .step-number {
width: 48px;
height: 48px;
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
font-family: PingFang SC;
font-weight: 400;
font-style: Heavy;
font-size: 26px;
line-height: 36px;
letter-spacing: 0%;
}
.step-number-active {
background: rgba(1, 90, 255, 1);
color: rgba(255, 255, 255, 1);
}
.step-number-inactive {
background: rgba(171, 184, 204, 1);
color: rgba(255, 255, 255, 1);
}
.step-label {
margin-left: 10px;
font-family: PingFang SC;
font-weight: 400;
font-style: Heavy;
font-size: 22px; font-size: 22px;
color: rgba(255, 255, 255, 0.6); // 未激活tab文字透明 line-height: 32px;
transition: color 0.3s ease; letter-spacing: 0%;
} }
.tab-indicator { .step-label-active {
position: absolute; color: rgba(1, 90, 255, 1);
bottom: 0;
width: 100%;
height: 4px;
background: rgba(229, 229, 229, 0.2);
transition: background-color 0.3s ease;
} }
&.active { .step-label-inactive {
.tab-label { color: rgba(139, 153, 173, 1);
color: white; // 激活tab文字为白色 }
} }
.tab-indicator { .step-divider {
background: white; // 激活tab指示器为白色 margin: 0 20px;
font-family: PingFang SC;
font-weight: 400;
font-style: Heavy;
font-size: 22px;
line-height: 32px;
letter-spacing: 0%;
} }
.step-divider-active {
color: rgba(1, 90, 255, 1);
} }
.step-divider-inactive {
color: rgba(139, 153, 173, 1);
} }
} }
.tab-content-container { .tab-content-container {
flex: 1; flex: 1;
overflow: hidden; overflow: auto;
background: rgba(255, 255, 255, 0.1); width: 1400px;
.tab-content { .tab-content {
height: 100%; height: 100%;
max-height: 100%;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
padding: 20px;
box-sizing: border-box;
} }
} }
.emptyBox { .emptyBox {
height: 60px;
flex-shrink: 0; flex-shrink: 0;
} }
} }

View File

@ -1,45 +1,94 @@
// src/pages/configSteps/index.tsx // src/pages/configSteps/index.tsx
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useLocation } from 'umi';
import styles from './index.less'; import styles from './index.less';
import cs from 'classnames'; import cs from 'classnames';
import NetworkConfig from './components/networkConfig'; import NetworkConfig from './components/networkConfig';
import ServerConfig from './components/serverConfig';
import WatchManagement from './components/watchManagement'; import WatchManagement from './components/watchManagement';
import TerminalGetImage from './components/terminalGetImage'; import TerminalGetImage from './components/terminalGetImage';
import { ConfigStepProvider, useConfigStep } from '@/contexts/ConfigStepContext';
const Index: React.FC = () => {
const [activeTab, setActiveTab] = useState<string>("networkConfig");
const tabs = [ const tabs = [
{ key: "networkConfig", label: '平台网络配置', component: <NetworkConfig /> }, { key: "networkConfig", label: '平台网络配置', component: <NetworkConfig /> },
{ key: "watchManagement", label: '侦测管理平台', component: <WatchManagement /> }, { key: "serverConfig", label: '服务器配置', component: <ServerConfig /> },
{ key: "watchManagement", label: '侦测平台管理', component: <WatchManagement /> },
{ key: "terminalGetImage", label: '终端获取镜像信息', component: <TerminalGetImage /> }, { key: "terminalGetImage", label: '终端获取镜像信息', component: <TerminalGetImage /> },
]; ];
const activeTabItem = tabs.find(tab => tab.key === activeTab); const ConfigStepsContent: React.FC = () => {
const { activeTab, setActiveTab, tabs: contextTabs } = useConfigStep();
const location = useLocation();
const [contentOpacity, setContentOpacity] = useState(1);
// 根据路由参数设置初始tab
useEffect(() => {
const params = new URLSearchParams(location.search);
const tabParam = params.get('tab');
const hash = location.hash.replace('#', '');
const targetTab = tabParam || hash;
if (targetTab && contextTabs.some(tab => tab.key === targetTab)) {
setActiveTab(targetTab);
}
}, [location.search, location.hash, setActiveTab, contextTabs]);
const activeTabItem = contextTabs.find(tab => tab.key === activeTab);
const activeTabIndex = contextTabs.findIndex(tab => tab.key === activeTab);
return ( return (
<div className={styles["config-step-container"]}> <div className={styles["config-step-container"]}>
<div className={styles["tabs-container"]}> <div className={styles["steps-container"]}>
{tabs.map((tab) => ( {contextTabs.map((tab, index) => (
<React.Fragment key={tab.key}>
<div <div
key={tab.key} className={cs(styles["step-item"], {
className={cs(styles["tab-item"], {
[styles.active]: activeTab === tab.key [styles.active]: activeTab === tab.key
})} })}
onClick={() => setActiveTab(tab.key)} onClick={() => setActiveTab(tab.key)}
> >
<span className={styles["tab-label"]}>{tab.label}</span> <div className={cs(styles["step-number"], {
<div className={styles["tab-indicator"]} /> [styles["step-number-active"]]: index <= activeTabIndex,
[styles["step-number-inactive"]]: index > activeTabIndex
})}>
{index + 1}
</div> </div>
<div className={cs(styles["step-label"], {
[styles["step-label-active"]]: index <= activeTabIndex,
[styles["step-label-inactive"]]: index > activeTabIndex
})}>
{tab.label}
</div>
</div>
{index < contextTabs.length - 1 && (
<div className={cs(styles["step-divider"], {
[styles["step-divider-active"]]: index < activeTabIndex,
[styles["step-divider-inactive"]]: index >= activeTabIndex
})}>
&gt;
</div>
)}
</React.Fragment>
))} ))}
</div> </div>
<div className={styles["tab-content-container"]}> <div className={styles["tab-content-container"]}>
<div className={styles["tab-content"]} style={{ opacity: contentOpacity }}>
{activeTabItem?.component || <div></div>} {activeTabItem?.component || <div></div>}
</div> </div>
</div>
<div className={styles.emptyBox}></div> <div className={styles.emptyBox}></div>
</div> </div>
); );
}; };
const Index: React.FC = () => {
return (
<ConfigStepProvider tabs={tabs}>
<ConfigStepsContent />
</ConfigStepProvider>
);
};
export default Index; export default Index;

View File

@ -8,29 +8,16 @@
.showTextCon{ .showTextCon{
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-self: center; // justify-self: center;
align-items: center; align-items: center;
.name{ height: 155px;
height: 80px;
margin-bottom: 20px;
font-size: 36px;
color: #e5e5e5;
display: flex;
.welcomeIcon{
height: 66px;
width: 70px;
margin-right: 20px;
}
.textCon{
height: 120px;
display: flex;
flex-direction: column;
justify-content: space-between; justify-content: space-between;
.text{ .imgIcon{
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.nameText{ .welcomeIcon{
font-size: 28px; height: 90px;
width: 350px;
} }
} }
.loadingCon{ .loadingCon{
@ -43,7 +30,7 @@
width: 10px; width: 10px;
height: 10px; height: 10px;
border-radius: 50%; border-radius: 50%;
background-color: rgba(255, 255, 255, 0.3); background: rgba(1, 90, 255, 0.2);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
animation: loading 1.5s infinite ease-in-out; animation: loading 1.5s infinite ease-in-out;
@ -59,22 +46,20 @@
@keyframes loading { @keyframes loading {
0%, 100% { 0%, 100% {
background-color: rgba(255, 255, 255, 0.3); background: rgba(1, 90, 255, 0.2);
transform: scale(0.6); // transform: scale(0.6);
} }
25% { 25% {
background-color: rgba(255, 255, 255, 0.6); background: rgba(1, 90, 255, 0.5);
transform: scale(0.8); // transform: scale(0.8);
} }
50% { 50% {
background-color: rgba(255, 255, 255, 1); background: rgba(1, 90, 255, 1);
transform: scale(1); // transform: scale(1);
} }
75% { 75% {
background-color: rgba(255, 255, 255, 0.6); background: rgba(1, 90, 255, 0.5);
transform: scale(0.8); // transform: scale(0.8);
}
}
} }
} }
} }

View File

@ -1,17 +1,13 @@
import React from 'react'; import React from 'react';
import styles from './index.less'; import styles from './index.less';
import WelcomeIcon from '../../assets/welcome-icon.jpg' import WelcomeIcon from '../../assets/welcome-icon.png'
const Welcome = () => { const Welcome = () => {
return ( return (
<div className={styles.welcomeCon} > <div className={styles.welcomeCon} >
<div className={styles.showTextCon}> <div className={styles.showTextCon}>
<div className={styles.name}> <div className={styles.imgIcon}>
<img src={WelcomeIcon} className={styles.welcomeIcon} /> <img src={WelcomeIcon} className={styles.welcomeIcon} />
<div className={styles.textCon}>
<div className={styles.text}>
<span></span>
<span className={styles.nameText}>UNISSENSE</span>
</div> </div>
<div className={styles.loadingCon}> <div className={styles.loadingCon}>
<div className={styles.dot}></div> <div className={styles.dot}></div>
@ -20,9 +16,6 @@ const Welcome = () => {
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
); );
} }

View File

@ -1,5 +1,6 @@
declare namespace CONFIG_STEPS { declare namespace CONFIG_STEPS {
interface StaticFormFieldConfig { // 平台网络配置>静态IP字段
interface FormFieldConfig {
name: string; name: string;
label: string; label: string;
type: 'input' | 'select'; // 可扩展更多类型 type: 'input' | 'select'; // 可扩展更多类型
@ -8,4 +9,10 @@ declare namespace CONFIG_STEPS {
options?: { label: string; value: string }[]; // select 专用 options?: { label: string; value: string }[]; // select 专用
required?: boolean; required?: boolean;
} }
interface UpdateProgress {
percent: number;
transferred: number;
total: number;
bytesPerSecond: number;
}
} }