Compare commits
30 Commits
f7cc0b675a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4b207df29 | ||
|
|
fefa836ee7 | ||
|
|
50a3022e9d | ||
|
|
8aeaacac42 | ||
|
|
4cdf6bade8 | ||
|
|
ab43a9a140 | ||
|
|
bd00e54acd | ||
|
|
c1138cd568 | ||
|
|
87892951f6 | ||
|
|
f973284140 | ||
|
|
e2374571d7 | ||
|
|
0a0e6df66b | ||
|
|
2caa8f26a3 | ||
|
|
2a14389daf | ||
|
|
7ce645704e | ||
|
|
e09129cab6 | ||
|
|
7d627a45fb | ||
|
|
81c61f433d | ||
|
|
4ec8e88e58 | ||
|
|
8f3f2d63a0 | ||
|
|
877a47807c | ||
|
|
f9d92e6cc9 | ||
|
|
d4ace86fb3 | ||
|
|
ed542f108c | ||
|
|
3515faa814 | ||
|
|
71f19b658c | ||
|
|
a71622f797 | ||
|
|
8c041c1740 | ||
|
|
65cf80fb51 | ||
|
|
26fef2fd7a |
6
.idea/compiler.xml
generated
Normal file
6
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="TypeScriptCompiler">
|
||||||
|
<option name="useTypesFromServer" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
15
.idea/dataSources.xml
generated
Normal file
15
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="chaos@10.91.3.253" uuid="1f27a48f-618d-4971-b39e-c88644e7d55d">
|
||||||
|
<driver-ref>mariadb</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.mariadb.jdbc.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:mariadb://10.91.3.253:3306/chaos</jdbc-url>
|
||||||
|
<jdbc-additional-properties>
|
||||||
|
<property name="database.introspection.mysql.dbe5060" value="true" />
|
||||||
|
</jdbc-additional-properties>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
1
.idea/inspectionProfiles/Project_Default.xml
generated
1
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -2,5 +2,6 @@
|
|||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<option name="myName" value="Project Default" />
|
||||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
||||||
827
package-lock.json
generated
827
package-lock.json
generated
@@ -8,7 +8,8 @@
|
|||||||
"name": "chaos-it",
|
"name": "chaos-it",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"daisyui": "^5.5.5"
|
"daisyui": "^5.5.5",
|
||||||
|
"ofetch": "^1.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.4.0",
|
"@eslint/compat": "^1.4.0",
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-svelte": "^3.4.0",
|
"prettier-plugin-svelte": "^3.4.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
"prettier-plugin-tailwindcss": "^0.7.1",
|
||||||
|
"sass-embedded": "^1.93.3",
|
||||||
"svelte": "^5.41.0",
|
"svelte": "^5.41.0",
|
||||||
"svelte-check": "^4.3.3",
|
"svelte-check": "^4.3.3",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
@@ -33,6 +35,13 @@
|
|||||||
"vite": "^7.1.10"
|
"vite": "^7.1.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@bufbuild/protobuf": {
|
||||||
|
"version": "2.10.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@bufbuild/protobuf/-/protobuf-2.10.1.tgz",
|
||||||
|
"integrity": "sha512-ckS3+vyJb5qGpEYv/s1OebUHDi/xSNtfgw1wqKZo7MR9F2z+qXr0q5XagafAG/9O0QPVIUfST0smluYSTpYFkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||||
@@ -793,6 +802,330 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@parcel/watcher": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^1.0.3",
|
||||||
|
"is-glob": "^4.0.3",
|
||||||
|
"micromatch": "^4.0.5",
|
||||||
|
"node-addon-api": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@parcel/watcher-android-arm64": "2.5.1",
|
||||||
|
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||||
|
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||||
|
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||||
|
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||||
|
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||||
|
"@parcel/watcher-win32-x64": "2.5.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-android-arm64": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-darwin-x64": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-win32-arm64": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-win32-ia32": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-win32-x64": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher/node_modules/detect-libc": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"bin": {
|
||||||
|
"detect-libc": "bin/detect-libc.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@polka/url": {
|
"node_modules/@polka/url": {
|
||||||
"version": "1.0.0-next.29",
|
"version": "1.0.0-next.29",
|
||||||
"resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz",
|
"resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||||
@@ -1888,6 +2221,13 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-builder": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/buffer-builder/-/buffer-builder-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT/X11"
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
|
||||||
@@ -1961,6 +2301,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/colorjs.io": {
|
||||||
|
"version": "0.5.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/colorjs.io/-/colorjs.io-0.5.2.tgz",
|
||||||
|
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -2050,6 +2397,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/destr": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
@@ -2575,6 +2928,13 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immutable": {
|
||||||
|
"version": "5.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.4.tgz",
|
||||||
|
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
@@ -3151,6 +3511,31 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-addon-api": {
|
||||||
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/node-fetch-native": {
|
||||||
|
"version": "1.6.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||||
|
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/ofetch": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ofetch/-/ofetch-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"destr": "^2.0.5",
|
||||||
|
"node-fetch-native": "^1.6.7",
|
||||||
|
"ufo": "^1.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -3639,6 +4024,16 @@
|
|||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rxjs": {
|
||||||
|
"version": "7.8.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz",
|
||||||
|
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sade": {
|
"node_modules/sade": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmmirror.com/sade/-/sade-1.8.1.tgz",
|
"resolved": "https://registry.npmmirror.com/sade/-/sade-1.8.1.tgz",
|
||||||
@@ -3652,6 +4047,393 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sass": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"chokidar": "^4.0.0",
|
||||||
|
"immutable": "^5.0.2",
|
||||||
|
"source-map-js": ">=0.6.2 <2.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"sass": "sass.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@parcel/watcher": "^2.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded/-/sass-embedded-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-+VUy01yfDqNmIVMd/LLKl2TTtY0ovZN0rTonh+FhKr65mFwIYgU9WzgIZKS7U9/SPCQvWTsTGx9jyt+qRm/XFw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@bufbuild/protobuf": "^2.5.0",
|
||||||
|
"buffer-builder": "^0.2.0",
|
||||||
|
"colorjs.io": "^0.5.0",
|
||||||
|
"immutable": "^5.0.2",
|
||||||
|
"rxjs": "^7.4.0",
|
||||||
|
"supports-color": "^8.1.1",
|
||||||
|
"sync-child-process": "^1.0.2",
|
||||||
|
"varint": "^6.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"sass": "dist/bin/sass.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"sass-embedded-all-unknown": "1.93.3",
|
||||||
|
"sass-embedded-android-arm": "1.93.3",
|
||||||
|
"sass-embedded-android-arm64": "1.93.3",
|
||||||
|
"sass-embedded-android-riscv64": "1.93.3",
|
||||||
|
"sass-embedded-android-x64": "1.93.3",
|
||||||
|
"sass-embedded-darwin-arm64": "1.93.3",
|
||||||
|
"sass-embedded-darwin-x64": "1.93.3",
|
||||||
|
"sass-embedded-linux-arm": "1.93.3",
|
||||||
|
"sass-embedded-linux-arm64": "1.93.3",
|
||||||
|
"sass-embedded-linux-musl-arm": "1.93.3",
|
||||||
|
"sass-embedded-linux-musl-arm64": "1.93.3",
|
||||||
|
"sass-embedded-linux-musl-riscv64": "1.93.3",
|
||||||
|
"sass-embedded-linux-musl-x64": "1.93.3",
|
||||||
|
"sass-embedded-linux-riscv64": "1.93.3",
|
||||||
|
"sass-embedded-linux-x64": "1.93.3",
|
||||||
|
"sass-embedded-unknown-all": "1.93.3",
|
||||||
|
"sass-embedded-win32-arm64": "1.93.3",
|
||||||
|
"sass-embedded-win32-x64": "1.93.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-all-unknown": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-3okGgnE41eg+CPLtAPletu6nQ4N0ij7AeW+Sl5Km4j29XcmqZQeFwYjHe1AlKTEgLi/UAONk1O8i8/lupeKMbw==",
|
||||||
|
"cpu": [
|
||||||
|
"!arm",
|
||||||
|
"!arm64",
|
||||||
|
"!riscv64",
|
||||||
|
"!x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"sass": "1.93.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-android-arm": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-android-arm/-/sass-embedded-android-arm-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-8xOw9bywfOD6Wv24BgCmgjkk6tMrsOTTHcb28KDxeJtFtoxiUyMbxo0vChpPAfp2Hyg2tFFKS60s0s4JYk+Raw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-android-arm64": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-uqUl3Kt1IqdGVAcAdbmC+NwuUJy8tM+2ZnB7/zrt6WxWVShVCRdFnWR9LT8HJr7eJN7AU8kSXxaVX/gedanPsg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-android-riscv64": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-2jNJDmo+3qLocjWqYbXiBDnfgwrUeZgZFHJIwAefU7Fn66Ot7rsXl+XPwlokaCbTpj7eMFIqsRAZ/uDueXNCJg==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-android-x64": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-android-x64/-/sass-embedded-android-x64-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-y0RoAU6ZenQFcjM9PjQd3cRqRTjqwSbtWLL/p68y2oFyh0QGN0+LQ826fc0ZvU/AbqCsAizkqjzOn6cRZJxTTQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-darwin-arm64": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-7zb/hpdMOdKteK17BOyyypemglVURd1Hdz6QGsggy60aUFfptTLQftLRg8r/xh1RbQAUKWFbYTNaM47J9yPxYg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-darwin-x64": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-Ek1Vp8ZDQEe327Lz0b7h3hjvWH3u9XjJiQzveq74RPpJQ2q6d9LfWpjiRRohM4qK6o4XOHw1X10OMWPXJtdtWg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-linux-arm": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-yeiv2y+dp8B4wNpd3+JsHYD0mvpXSfov7IGyQ1tMIR40qv+ROkRqYiqQvAOXf76Qwh4Y9OaYZtLpnsPjfeq6mA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-linux-arm64": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-RBrHWgfd8Dd8w4fbmdRVXRrhh8oBAPyeWDTKAWw8ZEmuXfVl4ytjDuyxaVilh6rR1xTRTNpbaA/YWApBlLrrNw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-linux-musl-arm": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-fU0fwAwbp7sBE3h5DVU5UPzvaLg7a4yONfFWkkcCp6ZrOiPuGRHXXYriWQ0TUnWy4wE+svsVuWhwWgvlb/tkKg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-linux-musl-arm64": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-PS829l+eUng+9W4PFclXGb4uA2+965NHV3/Sa5U7qTywjeeUUYTZg70dJHSqvhrBEfCc2XJABeW3adLJbyQYkw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-linux-musl-riscv64": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-cK1oBY+FWQquaIGEeQ5H74KTO8cWsSWwXb/WaildOO9U6wmUypTgUYKQ0o5o/29nZbWWlM1PHuwVYTSnT23Jjg==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-linux-musl-x64": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-A7wkrsHu2/I4Zpa0NMuPGkWDVV7QGGytxGyUq3opSXgAexHo/vBPlGoDXoRlSdex0cV+aTMRPjoGIfdmNlHwyg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-linux-riscv64": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-vWkW1+HTF5qcaHa6hO80gx/QfB6GGjJUP0xLbnAoY4pwEnw5ulGv6RM8qYr8IDhWfVt/KH+lhJ2ZFxnJareisQ==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-linux-x64": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-k6uFxs+e5jSuk1Y0niCwuq42F9ZC5UEP7P+RIOurIm8w/5QFa0+YqeW+BPWEW5M1FqVOsNZH3qGn4ahqvAEjPA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-unknown-all": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-o5wj2rLpXH0C+GJKt/VpWp6AnMsCCbfFmnMAttcrsa+U3yrs/guhZ3x55KAqqUsE8F47e3frbsDL+1OuQM5DAA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"!android",
|
||||||
|
"!darwin",
|
||||||
|
"!linux",
|
||||||
|
"!win32"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"sass": "1.93.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-win32-arm64": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-0dOfT9moy9YmBolodwYYXtLwNr4jL4HQC9rBfv6mVrD7ud8ue2kDbn+GVzj1hEJxvEexVSmDCf7MHUTLcGs9xQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded-win32-x64": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-wHFVfxiS9hU/sNk7KReD+lJWRp3R0SLQEX4zfOnRP2zlvI2X4IQR5aZr9GNcuMP6TmNpX0nQPZTegS8+h9RrEg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sass-embedded/node_modules/supports-color": {
|
||||||
|
"version": "8.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz",
|
||||||
|
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
|
||||||
@@ -3826,6 +4608,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sync-child-process": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sync-child-process/-/sync-child-process-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sync-message-port": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sync-message-port": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/sync-message-port/-/sync-message-port-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.17",
|
||||||
"resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
"resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
||||||
@@ -3900,6 +4705,13 @@
|
|||||||
"typescript": ">=4.8.4"
|
"typescript": ">=4.8.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
|
||||||
@@ -3951,6 +4763,12 @@
|
|||||||
"typescript": ">=4.8.4 <6.0.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ufo": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
@@ -3975,6 +4793,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/varint": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/varint/-/varint-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.2.2",
|
"version": "7.2.2",
|
||||||
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.2.2.tgz",
|
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.2.2.tgz",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-svelte": "^3.4.0",
|
"prettier-plugin-svelte": "^3.4.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
"prettier-plugin-tailwindcss": "^0.7.1",
|
||||||
|
"sass-embedded": "^1.93.3",
|
||||||
"svelte": "^5.41.0",
|
"svelte": "^5.41.0",
|
||||||
"svelte-check": "^4.3.3",
|
"svelte-check": "^4.3.3",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
"vite": "^7.1.10"
|
"vite": "^7.1.10"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"daisyui": "^5.5.5"
|
"daisyui": "^5.5.5",
|
||||||
|
"ofetch": "^1.5.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/app.d.ts
vendored
24
src/app.d.ts
vendored
@@ -1,12 +1,26 @@
|
|||||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
|
import type { ApiClient } from '$lib/api/httpClient.ts';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
interface User {
|
||||||
// interface Locals {}
|
id: string;
|
||||||
// interface PageData {}
|
username: string;
|
||||||
// interface PageState {}
|
nickname: string;
|
||||||
// interface Platform {}
|
avatar?: string;
|
||||||
|
roles: string[];
|
||||||
|
}
|
||||||
|
interface Locals {
|
||||||
|
user: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface pageData {
|
||||||
|
user: User | null;
|
||||||
|
}
|
||||||
|
interface Locals {
|
||||||
|
api: ApiClient;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|||||||
34
src/hooks.server.ts
Normal file
34
src/hooks.server.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { type Handle, redirect } from '@sveltejs/kit';
|
||||||
|
import { parseJwt } from '$lib/utils/tokenUtils.ts';
|
||||||
|
import type { JwtPayload } from '$lib/types/auth.ts';
|
||||||
|
import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts';
|
||||||
|
import { createApi } from '$lib/api/httpClient.ts';
|
||||||
|
|
||||||
|
export const handle: Handle = async ({ event, resolve}) =>{
|
||||||
|
const authorization = event.cookies.get(COOKIE_TOKEN_KEY);
|
||||||
|
|
||||||
|
event.locals.api = createApi(authorization);
|
||||||
|
|
||||||
|
if (authorization){
|
||||||
|
const split = authorization?.split(' ');
|
||||||
|
const token = split[1];
|
||||||
|
const jwt = parseJwt<JwtPayload>(token);
|
||||||
|
|
||||||
|
if (jwt){
|
||||||
|
|
||||||
|
event.locals.user = {
|
||||||
|
id: jwt.userId,
|
||||||
|
username: jwt.sub,
|
||||||
|
nickname: jwt.nickname,
|
||||||
|
avatar: jwt.avatar,
|
||||||
|
roles: jwt.authorities
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else if(event.url.pathname.startsWith('/app')){
|
||||||
|
throw redirect(303, '/auth/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(event);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,132 +1,73 @@
|
|||||||
// src/lib/api/httpClient.ts
|
import { ofetch, type FetchOptions, type SearchParameters } from 'ofetch';
|
||||||
|
import { log } from '$lib/log';
|
||||||
|
|
||||||
import { browser } from '$app/environment';
|
type QueryParams = SearchParameters;
|
||||||
import type { HttpMethod, JsonObject, JsonValue } from '$lib/types/http.ts';
|
type RequestBody = Record<string, unknown> | FormData | unknown[] | object;
|
||||||
import { authStore } from '$lib/stores/authStore.ts';
|
type AppFetchOptions = Omit<FetchOptions<'json'>, 'method' | 'body' | 'query'>;
|
||||||
import type { ApiResult } from '$lib/types/api.ts';
|
|
||||||
|
|
||||||
|
export interface ApiResult<T> {
|
||||||
interface RequestOptions extends Omit<RequestInit, 'method' | 'body'> {
|
code: number;
|
||||||
body?: JsonObject | FormData;
|
msg: string;
|
||||||
|
data: T;
|
||||||
}
|
}
|
||||||
const API_BASE_URL = import.meta.env.VITE_PUBLIC_API_URL || 'http://localhost:18888/api';
|
|
||||||
|
|
||||||
let currentToken: string | null = null;
|
const BASE_URL = import.meta.env.VITE_PUBLIC_API_URL || 'http://localhost:18888/api';
|
||||||
let currentTokenHead: string | null = null;
|
|
||||||
|
|
||||||
if (browser) {
|
export type ApiClient = ReturnType<typeof createApi>;
|
||||||
// 只有在浏览器环境下才订阅,防止 SSR 内存泄漏
|
|
||||||
authStore.subscribe(state => {
|
export const createApi = (token?: string) => {
|
||||||
currentToken = state.token;
|
const client = ofetch.create({
|
||||||
currentTokenHead = state.tokenHead;
|
baseURL: BASE_URL,
|
||||||
|
// 建议:通常 Token 前面需要加 Bearer
|
||||||
|
headers: token ? { Authorization: token } : {},
|
||||||
|
onRequest({ options, request }) {
|
||||||
|
log.debug(`[API] ${options.method} ${request}`
|
||||||
|
|
||||||
|
,{
|
||||||
|
body: options.body as unknown,
|
||||||
|
headers: options.headers,
|
||||||
|
query: options.query
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onResponseError({ request, response }) {
|
||||||
|
log.error(`[API] Error ${request}`, {
|
||||||
|
status: response.status,
|
||||||
|
headers: response.headers,
|
||||||
|
data: response._data as unknown
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
const normalizeHeaders = (headers?: HeadersInit):Record<string, string> =>{
|
|
||||||
const result:Record<string,string> = {};
|
|
||||||
|
|
||||||
if (!headers){
|
return {
|
||||||
return result;
|
get: <T>(url: string, query?: QueryParams, options?: AppFetchOptions) =>
|
||||||
}
|
client<ApiResult<T>>(url, { ...options, method: 'GET', query }),
|
||||||
|
|
||||||
if (headers instanceof Headers){
|
// 关键修复点:
|
||||||
headers.forEach((value, key) => {
|
// 1. 使用 <T, B = RequestBody> 保持泛型灵活性
|
||||||
result[key.toLowerCase()] = value;
|
// 2. 使用 `as unknown as Record<string, unknown>` 替代 `as any`
|
||||||
});
|
// 这告诉编译器:"先把 B 当作未知类型,再把它视为一个通用的键值对对象",完美绕过 ESLint 和 TS 检查
|
||||||
}else if (Array.isArray(headers)){
|
post: <T, B = RequestBody>(url: string, body?: B, options?: AppFetchOptions) =>
|
||||||
headers.forEach(([key, value]) => {
|
client<ApiResult<T>>(url, {
|
||||||
result[key.toLowerCase()] = value;
|
...options,
|
||||||
})
|
method: 'POST',
|
||||||
}else {
|
body: body as unknown as Record<string, unknown>
|
||||||
Object.keys(headers).forEach(key => {
|
}),
|
||||||
result[key.toLowerCase()] = headers[key.toLowerCase()] as string;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
export class HttpError extends Error {
|
|
||||||
public status: number;
|
|
||||||
public details: JsonValue | string;
|
|
||||||
|
|
||||||
constructor(message: string, status: number, details: JsonValue | string) {
|
put: <T, B = RequestBody>(url: string, body?: B, options?: AppFetchOptions) =>
|
||||||
super(message);
|
client<ApiResult<T>>(url, {
|
||||||
this.name = 'HttpError';
|
...options,
|
||||||
this.status = status;
|
method: 'PUT',
|
||||||
this.details = details;
|
body: body as unknown as Record<string, unknown>
|
||||||
|
}),
|
||||||
|
|
||||||
// 保持正确的原型链
|
patch: <T, B = RequestBody>(url: string, body?: B, options?: AppFetchOptions) =>
|
||||||
if (Error.captureStackTrace) {
|
client<ApiResult<T>>(url, {
|
||||||
Error.captureStackTrace(this, HttpError);
|
...options,
|
||||||
}
|
method: 'PATCH',
|
||||||
}
|
body: body as unknown as Record<string, unknown>
|
||||||
}
|
}),
|
||||||
|
|
||||||
|
delete: <T>(url: string, query?: QueryParams, options?: AppFetchOptions) =>
|
||||||
const httpRequest= async <T>(
|
client<ApiResult<T>>(url, { ...options, method: 'DELETE', query })
|
||||||
url:string,
|
};
|
||||||
method: HttpMethod,
|
|
||||||
options: RequestOptions = {}
|
|
||||||
):Promise<ApiResult<T>> =>{
|
|
||||||
const fullUrl = `${API_BASE_URL}${url}`;
|
|
||||||
const { body, headers, ...rest} = options;
|
|
||||||
|
|
||||||
const requestHeaders: Record<string, string> = normalizeHeaders(headers);
|
|
||||||
let requestBody:BodyInit | undefined;
|
|
||||||
|
|
||||||
if (body instanceof FormData){
|
|
||||||
requestBody = body;
|
|
||||||
}else if (body){
|
|
||||||
requestHeaders['content-type'] = 'application/json';
|
|
||||||
requestBody = JSON.stringify(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentToken && currentTokenHead) {
|
|
||||||
requestHeaders['authorization'] = `${currentTokenHead} ${currentToken}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(fullUrl,{
|
|
||||||
method,
|
|
||||||
headers: requestHeaders,
|
|
||||||
body: requestBody,
|
|
||||||
...rest
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
|
|
||||||
let errorDetail;
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
errorDetail = await response.json()
|
|
||||||
|
|
||||||
}catch (e){
|
|
||||||
console.error('Error parsing JSON:', e);
|
|
||||||
errorDetail = await response.text()
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = `HTTP Error ${response.status} (${response.statusText})`;
|
|
||||||
throw new HttpError(message, response.status, errorDetail);
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentType = response.headers.get('Content-Type');
|
|
||||||
if (contentType && contentType.includes('application/json')){
|
|
||||||
return (await response.json() ) as ApiResult<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {code:200, msg:'OK', data:null} ;
|
|
||||||
|
|
||||||
}catch (error){
|
|
||||||
console.error(`API Request Failed to ${fullUrl}:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const api = {
|
|
||||||
get: <T>(url: string, options?: RequestOptions) => httpRequest<T>(url, 'GET', options),
|
|
||||||
post: <T>(url: string, body: JsonObject, options?: RequestOptions) => httpRequest<T>(url, 'POST', { ...options, body }),
|
|
||||||
put: <T>(url: string, body: JsonObject, options?: RequestOptions) => httpRequest<T>(url, 'PUT', { ...options, body }),
|
|
||||||
delete: <T>(url: string, options?: RequestOptions) => httpRequest<T>(url, 'DELETE', options),
|
|
||||||
patch: <T>(url: string, body: JsonObject, options?: RequestOptions) => httpRequest<T>(url, 'PATCH', { ...options, body }),
|
|
||||||
};
|
};
|
||||||
@@ -1,48 +1,31 @@
|
|||||||
|
|
||||||
import {api} from '$lib/api/httpClient.ts'
|
import type { AuthResponse, LoginPayload } from '$lib/types/auth';
|
||||||
import type { AuthResponse, LoginPayload } from '$lib/types/auth.ts';
|
import { ApiError } from '$lib/types/api.ts';
|
||||||
import { browser } from '$app/environment';
|
import type { ApiClient } from '$lib/api/httpClient.ts';
|
||||||
import { authStore } from '$lib/stores/authStore.ts';
|
|
||||||
import { userService } from '$lib/api/services/userService.ts';
|
|
||||||
import { userStore } from '$lib/stores/userStore.ts';
|
|
||||||
|
|
||||||
export const authService = {
|
export const authService = {
|
||||||
login: async (payload: LoginPayload): Promise<AuthResponse> => {
|
/**
|
||||||
|
* 登录流程
|
||||||
|
*/
|
||||||
|
login: async (api: ApiClient,payload: LoginPayload): Promise<AuthResponse> => {
|
||||||
const response = await api.post<AuthResponse>('/auth/login', payload);
|
const response = await api.post<AuthResponse>('/auth/login', payload);
|
||||||
|
|
||||||
if (response.code != 200 || !response.data){
|
if (response.code !== 200 || !response.data) {
|
||||||
throw new Error(response.msg);
|
throw new ApiError(response);
|
||||||
}
|
}
|
||||||
if (browser){
|
|
||||||
authService._setToken(response.data.token, response.data.tokenHead)
|
|
||||||
}
|
|
||||||
|
|
||||||
const userProfile = await userService.getUserProfile();
|
|
||||||
|
|
||||||
if (browser){
|
|
||||||
userStore.set(userProfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
logout: async () => {
|
|
||||||
|
|
||||||
if (browser){
|
/**
|
||||||
|
* 登出流程
|
||||||
authStore.clear();
|
*/
|
||||||
userStore.clear();
|
logout: async (api: ApiClient) => {
|
||||||
localStorage.removeItem('auth_token');
|
try {
|
||||||
localStorage.removeItem('auth_token_head');
|
await api.post('/auth/logout', {});
|
||||||
|
} catch (error) {
|
||||||
return true;
|
console.warn('Logout API call failed:', error);
|
||||||
}else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
_setToken: (token:string ,tokenHead: string)=> {
|
|
||||||
authStore.set({ token, tokenHead, isAuthenticated: true });
|
|
||||||
localStorage.setItem('auth_token', token);
|
|
||||||
localStorage.setItem('auth_token_head', tokenHead);
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
37
src/lib/api/services/deviceService.ts
Normal file
37
src/lib/api/services/deviceService.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { type ApiClient } from '$lib/api/httpClient.ts';
|
||||||
|
import type { PageResult } from '$lib/types/dataTable.ts';
|
||||||
|
import type { CreateDeviceRequest, DeviceResponse } from '$lib/types/api.ts';
|
||||||
|
|
||||||
|
|
||||||
|
export const deviceService = {
|
||||||
|
getAllDevices: async (api:ApiClient,{ page, size,type,keyword}:{
|
||||||
|
page: number,
|
||||||
|
size: number,
|
||||||
|
type?: number,
|
||||||
|
keyword?: string,
|
||||||
|
|
||||||
|
}) => {
|
||||||
|
const queryParams: Record<string, string | number> = {
|
||||||
|
pageNum: page,
|
||||||
|
pageSize: size
|
||||||
|
};
|
||||||
|
if (type) queryParams.type = type;
|
||||||
|
if (keyword) queryParams.keyword = keyword;
|
||||||
|
|
||||||
|
const result = await api.get<PageResult<DeviceResponse[]>>('/devices',queryParams);
|
||||||
|
|
||||||
|
if (result.code != 200 || !result.data){
|
||||||
|
throw new Error(result.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
},
|
||||||
|
createDevice: async (api: ApiClient,device: CreateDeviceRequest) => {
|
||||||
|
|
||||||
|
const result = await api.post<DeviceResponse>('/devices', device);
|
||||||
|
if (result.code != 200 || !result.data){
|
||||||
|
throw new Error(result.msg);
|
||||||
|
}
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/lib/api/services/deviceTypesService.ts
Normal file
15
src/lib/api/services/deviceTypesService.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
import type { Options } from '$lib/types/api.ts';
|
||||||
|
import type { ApiClient } from '$lib/api/httpClient.ts';
|
||||||
|
|
||||||
|
export const deviceTypesService = {
|
||||||
|
getDeviceTypesOptions: async (api:ApiClient) => {
|
||||||
|
const result = await api.get<Options[]>('/device-types/options',undefined);
|
||||||
|
|
||||||
|
if (result.code != 200 || !result.data){
|
||||||
|
throw new Error(result.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/lib/api/services/roleService.ts
Normal file
15
src/lib/api/services/roleService.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import {type ApiClient } from '$lib/api/httpClient.ts';
|
||||||
|
import type { Options } from '$lib/types/api.ts';
|
||||||
|
import { log } from '$lib/log.ts';
|
||||||
|
|
||||||
|
|
||||||
|
export const roleService = {
|
||||||
|
getRolesOptions: async (api:ApiClient) => {
|
||||||
|
const response = await api.get<Options[]>('/roles/options',undefined);
|
||||||
|
if (response.code != 200 || !response.data){
|
||||||
|
log.error(response.msg);
|
||||||
|
throw new Error(response.msg);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
}
|
||||||
45
src/lib/api/services/tokenService.ts
Normal file
45
src/lib/api/services/tokenService.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { api } from '$lib/api/httpClient';
|
||||||
|
import type { ApiResult } from '$lib/types/api';
|
||||||
|
import { authStore } from '$lib/stores/authStore';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
export const tokenService = {
|
||||||
|
/**
|
||||||
|
* Check if the current token is valid
|
||||||
|
*/
|
||||||
|
validateToken: async (): Promise<boolean> => {
|
||||||
|
if (!browser) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.get<null>('/auth/validate');
|
||||||
|
return response.code === 200;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Token validation failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the current token
|
||||||
|
*/
|
||||||
|
refreshToken: async (): Promise<boolean> => {
|
||||||
|
if (!browser) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.post<{token: string, tokenHead: string}>('/auth/refresh', {});
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
// Update the auth store with new token
|
||||||
|
authStore.update(state => ({
|
||||||
|
...state,
|
||||||
|
token: response.data!.token,
|
||||||
|
tokenHead: response.data!.tokenHead
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Token refresh failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,12 +1,33 @@
|
|||||||
import { api } from '$lib/api/httpClient.ts';
|
import { type ApiClient } from '$lib/api/httpClient.ts';
|
||||||
import type { UserProfile } from '$lib/types/user.ts';
|
import type { UserProfile } from '$lib/types/user.ts';
|
||||||
|
import type { PageResult } from '$lib/types/dataTable.ts';
|
||||||
|
import { type SearchParameters } from 'ofetch';
|
||||||
|
|
||||||
|
|
||||||
|
// 1. 定义更安全的类型,替代 any
|
||||||
|
type QueryParams = SearchParameters;
|
||||||
export const userService = {
|
export const userService = {
|
||||||
getUserProfile: async () => {
|
getUserProfile: async (api:ApiClient) => {
|
||||||
const response = await api.get<UserProfile>('/user/profile');
|
const response = await api.get<UserProfile>('/users/me',undefined);
|
||||||
if (response.code != 200 || !response.data){
|
if (response.code != 200 || !response.data){
|
||||||
throw new Error(response.msg);
|
throw new Error(response.msg);
|
||||||
}
|
}
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
},
|
||||||
|
getAllUsers: async (api:ApiClient,{ page, size , keyword, roleId}: { page: number, size: number , keyword?: string, roleId?: number}) => {
|
||||||
|
|
||||||
|
const params: QueryParams= {
|
||||||
|
pageNum: page,
|
||||||
|
pageSize: size,
|
||||||
|
...(keyword && { keyword }),
|
||||||
|
...(roleId && { roleId })
|
||||||
|
} ;
|
||||||
|
const response = await api.get<PageResult<UserProfile[]>>(
|
||||||
|
'/users',
|
||||||
|
params);
|
||||||
|
if (response.code != 200 || !response.data){
|
||||||
|
throw new Error(response.msg);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<svg>
|
|
||||||
<symbol id="panel-right-close" viewBox="0 0 24 24">
|
|
||||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
|
|
||||||
<path d="M15 3.5v17M8 9l3 3l-3 3" />
|
|
||||||
<path d="M3 9.4c0-2.24 0-3.36.436-4.216a4 4 0 0 1 1.748-1.748C6.04 3 7.16 3 9.4 3h5.2c2.24 0 3.36 0 4.216.436a4 4 0 0 1 1.748 1.748C21 6.04 21 7.16 21 9.4v5.2c0 2.24 0 3.36-.436 4.216a4 4 0 0 1-1.748 1.748C17.96 21 16.84 21 14.6 21H9.4c-2.24 0-3.36 0-4.216-.436a4 4 0 0 1-1.748-1.748C3 17.96 3 16.84 3 14.6z" />
|
|
||||||
</g>
|
|
||||||
</symbol>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 580 B |
148
src/lib/components/DataTable.svelte
Normal file
148
src/lib/components/DataTable.svelte
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<script lang="ts" generics="T extends import('$lib/types/dataTable').BaseRecord">
|
||||||
|
|
||||||
|
|
||||||
|
// --- Props ---
|
||||||
|
import type { PageResult, TableColumn } from '$lib/types/dataTable.ts';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
export let data: PageResult<T>;
|
||||||
|
|
||||||
|
// 这里的 columns 被严格约束,传入错误的 key 会报错
|
||||||
|
export let columns: TableColumn<T>[];
|
||||||
|
|
||||||
|
export let loading: boolean = false;
|
||||||
|
|
||||||
|
// --- State ---
|
||||||
|
let selectedIds: Set<number | string> = new Set();
|
||||||
|
|
||||||
|
// 响应式计算
|
||||||
|
$: allSelected = data.records.length > 0 && data.records.every(item => selectedIds.has(item.id));
|
||||||
|
$: indeterminate = data.records.some(item => selectedIds.has(item.id)) && !allSelected;
|
||||||
|
|
||||||
|
// 定义事件,为了严格起见,我们明确 Payload 类型
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
pageChange: number;
|
||||||
|
delete: T;
|
||||||
|
edit: T;
|
||||||
|
batchDelete: (number | string)[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// --- Logic ---
|
||||||
|
function toggleAll() {
|
||||||
|
if (allSelected) {
|
||||||
|
data.records.forEach(item => selectedIds.delete(item.id));
|
||||||
|
} else {
|
||||||
|
data.records.forEach(item => selectedIds.add(item.id));
|
||||||
|
}
|
||||||
|
selectedIds = selectedIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOne(id: number | string) {
|
||||||
|
if (selectedIds.has(id)) {
|
||||||
|
selectedIds.delete(id);
|
||||||
|
} else {
|
||||||
|
selectedIds.add(id);
|
||||||
|
}
|
||||||
|
selectedIds = selectedIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBatchDelete() {
|
||||||
|
dispatch('batchDelete', Array.from(selectedIds));
|
||||||
|
selectedIds = new Set();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bg-base-100 rounded-box shadow-md w-full border border-base-200">
|
||||||
|
<div class="p-4 border-b border-base-200 flex justify-between items-center bg-base-100 rounded-t-box">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
{#if selectedIds.size > 0}
|
||||||
|
<div class="badge badge-neutral">已选 {selectedIds.size} 项</div>
|
||||||
|
<button class="btn btn-error btn-sm text-white" on:click={handleBatchDelete}>
|
||||||
|
批量删除
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<slot name="toolbar"></slot>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div><slot name="toolbar-right"></slot></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table w-full">
|
||||||
|
<thead class="bg-base-200/50">
|
||||||
|
<tr>
|
||||||
|
<th class="w-12">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" class="checkbox checkbox-sm"
|
||||||
|
checked={allSelected}
|
||||||
|
indeterminate={indeterminate}
|
||||||
|
on:change={toggleAll} />
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
{#each columns as col(col.key)}
|
||||||
|
<th class="{col.align === 'right' ? 'text-right' : col.align === 'center' ? 'text-center' : 'text-left'} font-semibold">
|
||||||
|
{col.label}
|
||||||
|
</th>
|
||||||
|
{/each}
|
||||||
|
<th class="text-right">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{#if loading}
|
||||||
|
{#each Array(5) as _}
|
||||||
|
<tr><td colspan={columns.length + 2} class="skeleton h-12 w-full rounded-none opacity-50"></td></tr>
|
||||||
|
{/each}
|
||||||
|
{:else if data.records.length === 0}
|
||||||
|
<tr>
|
||||||
|
<td colspan={columns.length + 2} class="text-center py-10 text-base-content/50">
|
||||||
|
暂无数据
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{:else}
|
||||||
|
{#each data.records as row (row.id)}
|
||||||
|
<tr class="hover group {selectedIds.has(row.id) ? 'bg-base-200/30' : ''}">
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" class="checkbox checkbox-sm"
|
||||||
|
checked={selectedIds.has(row.id)}
|
||||||
|
on:change={() => toggleOne(row.id)} />
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{#each columns as col}
|
||||||
|
<td class="{col.align === 'right' ? 'text-right' : col.align === 'center' ? 'text-center' : 'text-left'}">
|
||||||
|
<slot name="cell" row={row} key={col.key} value={row[col.key]}>
|
||||||
|
{String(row[col.key] ?? '-')}
|
||||||
|
</slot>
|
||||||
|
</td>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<td class="text-right">
|
||||||
|
<div class="join opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<button class="btn btn-xs btn-ghost" on:click={() => dispatch('edit', row)}>编辑</button>
|
||||||
|
<button class="btn btn-xs btn-ghost text-error" on:click={() => dispatch('delete', row)}>删除</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if data.total > 0}
|
||||||
|
<div class="p-4 flex justify-between items-center border-t border-base-200">
|
||||||
|
<span class="text-sm opacity-60">第 {data.current} / {data.pages} 页</span>
|
||||||
|
<div class="join">
|
||||||
|
<button class="join-item btn btn-sm" disabled={data.current === 1}
|
||||||
|
on:click={() => dispatch('pageChange', data.current - 1)}>«</button>
|
||||||
|
<button class="join-item btn btn-sm pointer-events-none bg-base-100">
|
||||||
|
{data.current}
|
||||||
|
</button>
|
||||||
|
<button class="join-item btn btn-sm" disabled={data.current === data.pages}
|
||||||
|
on:click={() => dispatch('pageChange', data.current + 1)}>»</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
171
src/lib/components/Modal.svelte
Normal file
171
src/lib/components/Modal.svelte
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
open?: boolean;
|
||||||
|
title?: string;
|
||||||
|
width?: string | number;
|
||||||
|
centered?: boolean;
|
||||||
|
confirmLoading?: boolean;
|
||||||
|
footer?: import('svelte').Snippet | null | undefined;
|
||||||
|
okText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
maskClosable?: boolean;
|
||||||
|
destroyOnHidden?: boolean;
|
||||||
|
children?: import('svelte').Snippet;
|
||||||
|
titleSlot?: import('svelte').Snippet;
|
||||||
|
footerSlot?: import('svelte').Snippet ;
|
||||||
|
onOk?: () => Promise<void> | void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
open = $bindable(false),
|
||||||
|
title = '',
|
||||||
|
width = 520,
|
||||||
|
footer = undefined,
|
||||||
|
centered = true,
|
||||||
|
confirmLoading = false,
|
||||||
|
okText = '确定',
|
||||||
|
cancelText = '取消',
|
||||||
|
maskClosable = true,
|
||||||
|
destroyOnHidden = false,
|
||||||
|
children,
|
||||||
|
titleSlot,
|
||||||
|
footerSlot,
|
||||||
|
onOk,
|
||||||
|
onCancel
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let dialog: HTMLDialogElement;
|
||||||
|
let internalLoading = $state(false);
|
||||||
|
|
||||||
|
// 1. 唯一的 DOM 操作入口:$effect
|
||||||
|
// 所有的开关逻辑都通过改变 open 变量来触发这里
|
||||||
|
$effect(() => {
|
||||||
|
if (!dialog) return;
|
||||||
|
|
||||||
|
if (open && !dialog.open) {
|
||||||
|
dialog.showModal();
|
||||||
|
}
|
||||||
|
else if (!open && dialog.open) {
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 处理原生关闭(仅用于处理 ESC 键等浏览器原生行为)
|
||||||
|
function handleNativeClose() {
|
||||||
|
// 只有当状态认为它是“开”,但 DOM 变成了“关”时,才需要同步
|
||||||
|
if (open) {
|
||||||
|
open = false;
|
||||||
|
onCancel?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 按钮点击:只修改状态
|
||||||
|
function handleCancel() {
|
||||||
|
if (internalLoading) return;
|
||||||
|
// 先触发回调,再关闭
|
||||||
|
onCancel?.();
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleOk() {
|
||||||
|
if (internalLoading) return;
|
||||||
|
|
||||||
|
if (onOk) {
|
||||||
|
const result = onOk();
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
internalLoading = true;
|
||||||
|
try {
|
||||||
|
await result;
|
||||||
|
open = false; // 成功后,修改状态来关闭
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Modal ok error', e);
|
||||||
|
} finally {
|
||||||
|
internalLoading = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
open = false; // 修改状态来关闭
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
open = false; // 修改状态来关闭
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBackdropClick(e: MouseEvent) {
|
||||||
|
if (maskClosable && e.target === dialog) {
|
||||||
|
// 同样,只修改状态
|
||||||
|
onCancel?.();
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let widthStyle = $derived(typeof width === 'number' ? `max-width: ${width}px` : `max-width: ${width}`);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
bind:this={dialog}
|
||||||
|
class="modal"
|
||||||
|
class:modal-bottom={!centered}
|
||||||
|
class:modal-middle={centered}
|
||||||
|
onclose={handleNativeClose}
|
||||||
|
onclick={handleBackdropClick}
|
||||||
|
>
|
||||||
|
<div class="modal-box" style={widthStyle}>
|
||||||
|
<!-- 移除 stopPropagation,改用新的 handleCancel -->
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
||||||
|
onclick={handleCancel}
|
||||||
|
>✕</button>
|
||||||
|
|
||||||
|
{#if title || titleSlot}
|
||||||
|
<h3 class="font-bold text-lg mb-4">
|
||||||
|
{#if titleSlot}
|
||||||
|
{@render titleSlot()}
|
||||||
|
{:else}
|
||||||
|
{title}
|
||||||
|
{/if}
|
||||||
|
</h3>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="py-4">
|
||||||
|
{#if destroyOnHidden && !open}
|
||||||
|
<!-- Destroyed -->
|
||||||
|
{:else if children}
|
||||||
|
{@render children()}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-action">
|
||||||
|
{#if footer === undefined}
|
||||||
|
{#if footerSlot}
|
||||||
|
{@render footerSlot()}
|
||||||
|
{:else}
|
||||||
|
|
||||||
|
<button class="btn" disabled={internalLoading || confirmLoading}
|
||||||
|
onclick={handleCancel}
|
||||||
|
>
|
||||||
|
{cancelText}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
class:loading={internalLoading || confirmLoading}
|
||||||
|
disabled={internalLoading || confirmLoading}
|
||||||
|
onclick={handleOk}
|
||||||
|
>
|
||||||
|
{#if internalLoading || confirmLoading}
|
||||||
|
<span class="loading loading-spinner"></span>
|
||||||
|
{/if}
|
||||||
|
{okText}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{:else if footer === null}
|
||||||
|
<!-- No footer -->
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button>close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
29
src/lib/components/ToastContainer.svelte
Normal file
29
src/lib/components/ToastContainer.svelte
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { fly } from 'svelte/transition';
|
||||||
|
import { flip } from 'svelte/animate';
|
||||||
|
import Icon from '$lib/components/icon/Icon.svelte';
|
||||||
|
import type { IconId } from '$lib/types/icon-ids.ts';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import { TOAST_KEY, type ToastState, type ToastType } from '$lib/stores/toast.svelte.ts';
|
||||||
|
const toastState = getContext<ToastState>(TOAST_KEY);
|
||||||
|
|
||||||
|
const toastIconMap: Record<ToastType, IconId> = {
|
||||||
|
success: 'success',
|
||||||
|
error: 'error',
|
||||||
|
warning: 'warning',
|
||||||
|
info: 'info'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="toast toast-top toast-center z-50">
|
||||||
|
{#each toastState.toasts as t (t.id)}
|
||||||
|
<div
|
||||||
|
animate:flip={{ duration: 300 }}
|
||||||
|
transition:fly={{ x: 100, duration: 300 }}
|
||||||
|
class="alert bg-base-100 text-base-content border-0 shadow-base-300/50 shadow-lg min-w-[200px] flex justify-start"
|
||||||
|
>
|
||||||
|
<span><Icon Cid={toastIconMap[t.type]} size="24"></Icon></span>
|
||||||
|
<span>{t.message}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
8
src/lib/components/button/LogoutButton.svelte
Normal file
8
src/lib/components/button/LogoutButton.svelte
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import { TOAST_KEY, type ToastState } from '$lib/stores/toast.svelte.ts';
|
||||||
|
import Icon from '$lib/components/icon/Icon.svelte'; // 假设你的路径
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
2
src/lib/components/constants/cookiesConstants.ts
Normal file
2
src/lib/components/constants/cookiesConstants.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const COOKIE_TOKEN_KEY = 'authorization';
|
||||||
|
export const COOKIE_THEME_KEY = 'theme';
|
||||||
22
src/lib/components/error/TableLoadingError.svelte
Normal file
22
src/lib/components/error/TableLoadingError.svelte
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import Icon from '$lib/components/icon/Icon.svelte';
|
||||||
|
|
||||||
|
const { error } = $props();
|
||||||
|
|
||||||
|
|
||||||
|
let message = $state("");
|
||||||
|
|
||||||
|
if (error){
|
||||||
|
message = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex-1 inset-0 flex flex-col items-center justify-center text-error ">
|
||||||
|
<Icon id="error" size="56" />
|
||||||
|
<p class="font-bold">加载失败</p>
|
||||||
|
<p class="text-sm opacity-80">{message}</p>
|
||||||
|
<button class="btn btn-sm btn-outline btn-error mt-4" onclick={() => location.reload()}>重新加载</button>
|
||||||
|
</div>
|
||||||
265
src/lib/components/form/AddDevice.svelte
Normal file
265
src/lib/components/form/AddDevice.svelte
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import type { CreateDeviceRequest, Options } from '$lib/types/api.ts';
|
||||||
|
import { log } from '$lib/log.ts';
|
||||||
|
|
||||||
|
let {
|
||||||
|
deviceTypeOptions = [],
|
||||||
|
open = $bindable(false)
|
||||||
|
} = $props<{ deviceTypeOptions: Options[] , open: boolean}>();
|
||||||
|
|
||||||
|
|
||||||
|
log.info('device type options',deviceTypeOptions);
|
||||||
|
let formData = $state<CreateDeviceRequest>({
|
||||||
|
name: '',
|
||||||
|
typeId: null,
|
||||||
|
model: '',
|
||||||
|
manufacturer: '',
|
||||||
|
purchaseDate: '',
|
||||||
|
interfaces: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// 错误信息状态
|
||||||
|
let errors = $state<Record<string, string>>({});
|
||||||
|
|
||||||
|
|
||||||
|
function addInterface() {
|
||||||
|
formData.interfaces.push({
|
||||||
|
name: '',
|
||||||
|
type: 1,
|
||||||
|
addressConfigs: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeInterface(index: number) {
|
||||||
|
formData.interfaces.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAddressConfig(interfaceIndex: number) {
|
||||||
|
formData.interfaces[interfaceIndex].addressConfigs.push({
|
||||||
|
isPrimary: false,
|
||||||
|
isDhcp: false,
|
||||||
|
ipAddress: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAddressConfig(interfaceIndex: number, configIndex: number) {
|
||||||
|
formData.interfaces[interfaceIndex].addressConfigs.splice(configIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function validate(): boolean {
|
||||||
|
let newErrors: Record<string, string> = {};
|
||||||
|
let isValid = true;
|
||||||
|
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
||||||
|
const ipRegex = /^((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]?)$/;
|
||||||
|
|
||||||
|
// 基础字段验证
|
||||||
|
if (!formData.name) newErrors['name'] = '设备名称不能为空';
|
||||||
|
if (!formData.typeId) newErrors['typeId'] = '设备类型ID不能为空';
|
||||||
|
if (!formData.model) newErrors['model'] = '设备型号不能为空';
|
||||||
|
if (!formData.manufacturer) newErrors['manufacturer'] = '厂商不能为空';
|
||||||
|
|
||||||
|
// 嵌套验证
|
||||||
|
formData.interfaces.forEach((iface, i) => {
|
||||||
|
if (!iface.name) newErrors[`iface_${i}_name`] = '接口名称不能为空';
|
||||||
|
if (iface.macAddress && !macRegex.test(iface.macAddress)) {
|
||||||
|
newErrors[`iface_${i}_mac`] = 'MAC地址格式错误';
|
||||||
|
}
|
||||||
|
|
||||||
|
iface.addressConfigs.forEach((addr, j) => {
|
||||||
|
if (addr.ipAddress && !ipRegex.test(addr.ipAddress)) {
|
||||||
|
newErrors[`iface_${i}_addr_${j}_ip`] = 'IP地址格式错误';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(newErrors).length > 0) {
|
||||||
|
errors = newErrors;
|
||||||
|
isValid = false;
|
||||||
|
} else {
|
||||||
|
errors = {};
|
||||||
|
}
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 【关键】导出方法供父组件调用 ---
|
||||||
|
|
||||||
|
export function submitAndGetPayload(): CreateDeviceRequest | null {
|
||||||
|
if (validate()) {
|
||||||
|
const snapshot = $state.snapshot(formData);
|
||||||
|
|
||||||
|
fetch('/api/devices', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {},
|
||||||
|
body: JSON.stringify(snapshot)
|
||||||
|
}).then(res => res.json())
|
||||||
|
.then(res => {
|
||||||
|
if (res.ok) {
|
||||||
|
log.info('设备创建成功', res);
|
||||||
|
open = false;
|
||||||
|
} else {
|
||||||
|
log.error('设备创建失败', res);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
log.error('设备创建失败', err);
|
||||||
|
});
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // 验证失败
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const payload = submitAndGetPayload();
|
||||||
|
if (payload) {
|
||||||
|
log.info('设备创建成功', payload);
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="space-y-8">
|
||||||
|
<div class="card bg-base-100 shadow-md border border-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-primary">基础信息</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label"><span class="label-text">设备名称 *</span></div>
|
||||||
|
<input type="text" bind:value={formData.name} class="input input-bordered w-full {errors.name ? 'input-error' : ''}" />
|
||||||
|
{#if errors.name}<div class="label"><span class="label-text-alt text-error">{errors.name}</span></div>{/if}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label"><span class="label-text">厂商 *</span></div>
|
||||||
|
<input type="text" bind:value={formData.manufacturer} class="input input-bordered w-full {errors.manufacturer ? 'input-error' : ''}" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label"><span class="label-text">型号 *</span></div>
|
||||||
|
<input type="text" bind:value={formData.model} class="input input-bordered w-full {errors.model ? 'input-error' : ''}" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label"><span class="label-text">设备类型 *</span></div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
bind:value={formData.typeId}
|
||||||
|
class="select select-bordered w-full {errors.typeId ? 'select-error' : ''}"
|
||||||
|
>
|
||||||
|
<option disabled selected value={null}>请选择设备类型</option>
|
||||||
|
|
||||||
|
{#each deviceTypeOptions as opt (opt.value)}
|
||||||
|
<option value={Number(opt.value)}>{opt.label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{#if errors.typeId}
|
||||||
|
<div class="label"><span class="label-text-alt text-error">{errors.typeId}</span></div>
|
||||||
|
{/if}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label"><span class="label-text">采购日期</span></div>
|
||||||
|
<input type="date" bind:value={formData.purchaseDate} class="input input-bordered w-full" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow-md border border-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h2 class="card-title text-secondary">网络接口 (Interfaces)</h2>
|
||||||
|
<button class="btn btn-sm btn-outline btn-secondary" onclick={addInterface}>+ 添加接口</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each formData.interfaces as iface, i (i)}
|
||||||
|
<div class="collapse collapse-arrow border border-base-300 bg-base-100 mb-2">
|
||||||
|
<input type="checkbox" checked={true} />
|
||||||
|
<div class="collapse-title text-lg font-medium flex justify-between pr-12">
|
||||||
|
<span>接口 #{i + 1}: {iface.name || '(未命名)'}</span>
|
||||||
|
<button class="btn btn-xs btn-error z-10" onclick={() => removeInterface(i)}>删除</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse-content space-y-4">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-2">
|
||||||
|
<label class="form-control">
|
||||||
|
<span class="label-text mb-1">接口名称 *</span>
|
||||||
|
<input type="text" bind:value={iface.name} class="input input-bordered input-sm {errors[`iface_${i}_name`] ? 'input-error' : ''}" />
|
||||||
|
{#if errors[`iface_${i}_name`]}<span class="text-xs text-error mt-1">{errors[`iface_${i}_name`]}</span>{/if}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-control">
|
||||||
|
<span class="label-text mb-1">接口类型</span>
|
||||||
|
<select bind:value={iface.type} class="select select-bordered select-sm">
|
||||||
|
<option value={1}>物理口</option>
|
||||||
|
<option value={2}>聚合口</option>
|
||||||
|
<option value={3}>虚拟口</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-control">
|
||||||
|
<span class="label-text mb-1">MAC 地址</span>
|
||||||
|
<input type="text" bind:value={iface.macAddress} placeholder="XX:XX:XX..." class="input input-bordered input-sm {errors[`iface_${i}_mac`] ? 'input-error' : ''}" />
|
||||||
|
{#if errors[`iface_${i}_mac`]}<span class="text-xs text-error mt-1">{errors[`iface_${i}_mac`]}</span>{/if}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-base-200 p-4 rounded-lg">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<h4 class="text-sm font-bold opacity-70">IP / VLAN 配置</h4>
|
||||||
|
<button class="btn btn-xs btn-neutral" onclick={() => addAddressConfig(i)}>+ 添加 IP</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if iface.addressConfigs.length === 0}
|
||||||
|
<div class="text-xs text-center opacity-50 py-2">暂无 IP 配置</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each iface.addressConfigs as addr, j (j)}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-12 gap-2 items-end mb-2 border-b border-base-300 pb-2 last:border-0">
|
||||||
|
<div class="md:col-span-1">
|
||||||
|
<label class="label-text text-xs">VLAN</label>
|
||||||
|
<input type="number" bind:value={addr.vlanId} class="input input-bordered input-xs w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-3">
|
||||||
|
<label class="label-text text-xs">IP地址</label>
|
||||||
|
<input type="text" bind:value={addr.ipAddress} class="input input-bordered input-xs w-full {errors[`iface_${i}_addr_${j}_ip`] ? 'input-error' : ''}" />
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-3">
|
||||||
|
<label class="label-text text-xs">子网掩码</label>
|
||||||
|
<input type="text" bind:value={addr.subnetMask} class="input input-bordered input-xs w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2 flex flex-col gap-1">
|
||||||
|
<label class="label cursor-pointer justify-start gap-2 p-0 h-6">
|
||||||
|
<input type="checkbox" bind:checked={addr.isPrimary} class="checkbox checkbox-xs" />
|
||||||
|
<span class="label-text text-xs">主IP</span>
|
||||||
|
</label>
|
||||||
|
<label class="label cursor-pointer justify-start gap-2 p-0 h-6">
|
||||||
|
<input type="checkbox" bind:checked={addr.isDhcp} class="checkbox checkbox-xs" />
|
||||||
|
<span class="label-text text-xs">DHCP</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-1 flex justify-end">
|
||||||
|
<button class="btn btn-square btn-xs btn-ghost text-error" onclick={() => removeAddressConfig(i, j)}>✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="join flex justify-center" >
|
||||||
|
<button class="btn btn-error join-item" onclick={onreset}>重置</button>
|
||||||
|
<button class="btn join-item" onclick={draft}>保存草稿</button>
|
||||||
|
<button class="btn btn-primary btn-wide join-item" onclick={handleSubmit}>提交</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,9 +1,101 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SpriteSvg from '$lib/assets/sprite.svg'
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;" >
|
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;" >
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
<!-- <!– eslint-disable-next-line svelte/no-at-html-tags –>-->
|
||||||
{@html SpriteSvg}
|
<!-- {@html SpriteSvg}-->
|
||||||
|
<symbol id="panel-right-close" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
|
||||||
|
<path d="M15 3.5v17M8 9l3 3l-3 3" />
|
||||||
|
<path d="M3 9.4c0-2.24 0-3.36.436-4.216a4 4 0 0 1 1.748-1.748C6.04 3 7.16 3 9.4 3h5.2c2.24 0 3.36 0 4.216.436a4 4 0 0 1 1.748 1.748C21 6.04 21 7.16 21 9.4v5.2c0 2.24 0 3.36-.436 4.216a4 4 0 0 1-1.748 1.748C17.96 21 16.84 21 14.6 21H9.4c-2.24 0-3.36 0-4.216-.436a4 4 0 0 1-1.748-1.748C3 17.96 3 16.84 3 14.6z" />
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="panel-right-close-solid" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M9.367 2.25h5.266c1.092 0 1.958 0 2.655.057c.714.058 1.317.18 1.869.46a4.75 4.75 0 0 1 2.075 2.077c.281.55.403 1.154.461 1.868c.057.697.057 1.563.057 2.655v5.266c0 1.092 0 1.958-.057 2.655c-.058.714-.18 1.317-.46 1.869a4.75 4.75 0 0 1-2.076 2.075c-.552.281-1.155.403-1.869.461c-.697.057-1.563.057-2.655.057H9.367c-1.092 0-1.958 0-2.655-.057c-.714-.058-1.317-.18-1.868-.46a4.75 4.75 0 0 1-2.076-2.076c-.281-.552-.403-1.155-.461-1.869c-.057-.697-.057-1.563-.057-2.655V9.367c0-1.092 0-1.958.057-2.655c.058-.714.18-1.317.46-1.868a4.75 4.75 0 0 1 2.077-2.076c.55-.281 1.154-.403 1.868-.461c.697-.057 1.563-.057 2.655-.057m6.383 17.997a20 20 0 0 0 1.416-.049c.62-.05 1.005-.147 1.31-.302a3.25 3.25 0 0 0 1.42-1.42c.155-.305.251-.69.302-1.31c.051-.63.052-1.434.052-2.566V9.4c0-1.132 0-1.937-.052-2.566c-.05-.62-.147-1.005-.302-1.31a3.25 3.25 0 0 0-1.42-1.42c-.305-.155-.69-.251-1.31-.302a20 20 0 0 0-1.416-.05zM7.47 8.47a.75.75 0 0 0 0 1.06L9.94 12l-2.47 2.47a.75.75 0 1 0 1.06 1.06l3-3a.75.75 0 0 0 0-1.06l-3-3a.75.75 0 0 0-1.06 0" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="panel-left-close" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
|
||||||
|
<path d="M9 3.5v17m7-5.5l-3-3l3-3" />
|
||||||
|
<path d="M3 9.4c0-2.24 0-3.36.436-4.216a4 4 0 0 1 1.748-1.748C6.04 3 7.16 3 9.4 3h5.2c2.24 0 3.36 0 4.216.436a4 4 0 0 1 1.748 1.748C21 6.04 21 7.16 21 9.4v5.2c0 2.24 0 3.36-.436 4.216a4 4 0 0 1-1.748 1.748C17.96 21 16.84 21 14.6 21H9.4c-2.24 0-3.36 0-4.216-.436a4 4 0 0 1-1.748-1.748C3 17.96 3 16.84 3 14.6z" />
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="panel-left-close-solid" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M9.367 2.25h5.266c1.092 0 1.958 0 2.655.057c.714.058 1.317.18 1.869.46a4.75 4.75 0 0 1 2.075 2.077c.281.55.403 1.154.461 1.868c.057.697.057 1.563.057 2.655v5.266c0 1.092 0 1.958-.057 2.655c-.058.714-.18 1.317-.46 1.869a4.75 4.75 0 0 1-2.076 2.075c-.552.281-1.155.403-1.869.461c-.697.057-1.563.057-2.655.057H9.367c-1.092 0-1.958 0-2.655-.057c-.714-.058-1.317-.18-1.868-.46a4.75 4.75 0 0 1-2.076-2.076c-.281-.552-.403-1.155-.461-1.869c-.057-.697-.057-1.563-.057-2.655V9.367c0-1.092 0-1.958.057-2.655c.058-.714.18-1.317.46-1.868a4.75 4.75 0 0 1 2.077-2.076c.55-.281 1.154-.403 1.868-.461c.697-.057 1.563-.057 2.655-.057M6.834 3.802c-.62.05-1.005.147-1.31.302a3.25 3.25 0 0 0-1.42 1.42c-.155.305-.251.69-.302 1.31c-.051.63-.052 1.434-.052 2.566v5.2c0 1.133 0 1.937.052 2.566c.05.62.147 1.005.302 1.31a3.25 3.25 0 0 0 1.42 1.42c.305.155.69.251 1.31.302c.392.032.851.044 1.416.05V3.752c-.565.005-1.024.017-1.416.049M16.53 8.47a.75.75 0 0 0-1.06 0l-3 3a.75.75 0 0 0 0 1.06l3 3a.75.75 0 1 0 1.06-1.06L14.06 12l2.47-2.47a.75.75 0 0 0 0-1.06" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="starburst" viewBox="0 0 48 48">
|
||||||
|
<g fill="none">
|
||||||
|
<path fill="url(#SVGvr5ORdWH)" d="M25.183 2.58a1.5 1.5 0 0 0-2.368 0l-3.388 4.356l-5.112-2.078a1.5 1.5 0 0 0-2.051 1.184l-.756 5.467l-5.467.756a1.5 1.5 0 0 0-1.184 2.05l2.078 5.113l-4.356 3.388a1.5 1.5 0 0 0 0 2.368l4.356 3.388l-2.078 5.113a1.5 1.5 0 0 0 1.184 2.05l5.467.757l.756 5.466a1.5 1.5 0 0 0 2.05 1.184l5.113-2.078l3.388 4.356a1.5 1.5 0 0 0 2.368 0l3.388-4.356l5.113 2.078a1.5 1.5 0 0 0 2.05-1.184l.756-5.466l5.467-.757a1.5 1.5 0 0 0 1.184-2.05l-2.078-5.113l4.356-3.388a1.5 1.5 0 0 0 0-2.368l-4.356-3.388l2.078-5.113a1.5 1.5 0 0 0-1.184-2.05l-5.467-.756l-.756-5.467a1.5 1.5 0 0 0-2.05-1.184L28.57 6.936z" />
|
||||||
|
<path fill="url(#SVGRWDvEe1n)" fill-opacity="0.95" d="M24 14c.69 0 1.25.56 1.25 1.25v7.5h7.5a1.25 1.25 0 1 1 0 2.5h-7.5v7.5a1.25 1.25 0 1 1-2.5 0v-7.5h-7.5a1.25 1.25 0 1 1 0-2.5h7.5v-7.5c0-.69.56-1.25 1.25-1.25" />
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="SVGvr5ORdWH" cx="0" cy="0" r="1" gradientTransform="rotate(-119.49 41.522 10.903)scale(97.2587 93.1572)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#ffc470" />
|
||||||
|
<stop offset=".251" stop-color="#ff835c" />
|
||||||
|
<stop offset=".55" stop-color="#f24a9d" />
|
||||||
|
<stop offset=".814" stop-color="#b339f0" />
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="SVGRWDvEe1n" x1="32.611" x2="11.626" y1="39.646" y2="26.053" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".024" stop-color="#ffc8d7" />
|
||||||
|
<stop offset=".807" stop-color="#fff" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="data" viewBox="0 0 16 16">
|
||||||
|
<path fill="currentColor" d="M10 4a2 2 0 1 0-4 0v10h4zM5 7H4a2 2 0 0 0-2 2v4.5a.5.5 0 0 0 .5.5H5zm6 7h2.5a.5.5 0 0 0 .5-.5V7a2 2 0 0 0-2-2h-1z" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="home" viewBox="0 0 16 16">
|
||||||
|
<path fill="currentColor" d="M8.687 1.262a1 1 0 0 0-1.374 0L2.469 5.84A1.5 1.5 0 0 0 2 6.931v5.57A1.5 1.5 0 0 0 3.5 14H5a1.5 1.5 0 0 0 1.5-1.5V10a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2.5A1.5 1.5 0 0 0 11 14h1.5a1.5 1.5 0 0 0 1.5-1.5V6.93a1.5 1.5 0 0 0-.47-1.09z" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="menu" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M3.75 6.5a.75.75 0 0 1 .75-.75h15a.75.75 0 0 1 0 1.5h-15a.75.75 0 0 1-.75-.75m0 5.5a.75.75 0 0 1 .75-.75h15a.75.75 0 0 1 0 1.5h-15a.75.75 0 0 1-.75-.75m0 5.5a.75.75 0 0 1 .75-.75h15a.75.75 0 0 1 0 1.5h-15a.75.75 0 0 1-.75-.75" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
|
||||||
|
<symbol id="info" viewBox="0 0 24 24">
|
||||||
|
<path fill="#3B82F6" d="M12 1.999c5.524 0 10.002 4.478 10.002 10.002c0 5.523-4.478 10.001-10.002 10.001S2 17.524 2 12.001C1.999 6.477 6.476 1.999 12 1.999" class="duoicon-secondary-layer" opacity="0.3" />
|
||||||
|
<path fill="#3B82F6" d="M12.001 6.5a1.252 1.252 0 1 0 .002 2.503A1.252 1.252 0 0 0 12 6.5zm-.005 3.749a1 1 0 0 0-.992.885l-.007.116l.004 5.502l.006.117a1 1 0 0 0 1.987-.002L13 16.75l-.004-5.501l-.007-.117a1 1 0 0 0-.994-.882z" class="duoicon-primary-layer" />
|
||||||
|
</symbol>
|
||||||
|
<symbol id="success" viewBox="0 0 24 24">
|
||||||
|
<path fill="#10B981" fill-rule="evenodd" d="M10.586 2.1a2 2 0 0 1 2.7-.116l.128.117L15.314 4H18a2 2 0 0 1 1.994 1.85L20 6v2.686l1.9 1.9a2 2 0 0 1 .116 2.701l-.117.127l-1.9 1.9V18a2 2 0 0 1-1.85 1.995L18 20h-2.685l-1.9 1.9a2 2 0 0 1-2.701.116l-.127-.116l-1.9-1.9H6a2 2 0 0 1-1.995-1.85L4 18v-2.686l-1.9-1.9a2 2 0 0 1-.116-2.701l.116-.127l1.9-1.9V6a2 2 0 0 1 1.85-1.994L6 4h2.686z" class="duoicon-secondary-layer" opacity="0.3" />
|
||||||
|
<path fill="#10B981" fill-rule="evenodd" d="m15.079 8.983l-4.244 4.244l-1.768-1.768a1 1 0 1 0-1.414 1.415l2.404 2.404a1.1 1.1 0 0 0 1.556 0l4.88-4.881a1 1 0 0 0-1.414-1.414" class="duoicon-primary-layer" />
|
||||||
|
</symbol>
|
||||||
|
<symbol id="warning" viewBox="0 0 24 24">
|
||||||
|
<path fill="#F59E0B" fill-rule="evenodd" d="M15.314 2a2 2 0 0 1 1.414.586l4.686 4.686A2 2 0 0 1 22 8.686v6.628a2 2 0 0 1-.586 1.414l-4.686 4.686a2 2 0 0 1-1.414.586H8.686a2 2 0 0 1-1.414-.586l-4.686-4.686A2 2 0 0 1 2 15.314V8.686a2 2 0 0 1 .586-1.414l4.686-4.686A2 2 0 0 1 8.686 2z" class="duoicon-secondary-layer" opacity="0.3" />
|
||||||
|
<path fill="#F59E0B" fill-rule="evenodd" d="M12 6a1 1 0 0 0-.993.883L11 7v6a1 1 0 0 0 1.993.117L13 13V7a1 1 0 0 0-1-1m0 9a1 1 0 1 0 0 2a1 1 0 0 0 0-2" class="duoicon-primary-layer" />
|
||||||
|
</symbol>
|
||||||
|
<symbol id="error" viewBox="0 0 24 24">
|
||||||
|
<path fill="#EF4444" d="m13.299 3.148l8.634 14.954a1.5 1.5 0 0 1-1.299 2.25H3.366a1.5 1.5 0 0 1-1.299-2.25l8.634-14.954c.577-1 2.02-1 2.598 0" class="duoicon-secondary-layer" opacity="0.3" />
|
||||||
|
<path fill="#EF4444" d="M12 8a1 1 0 0 0-.993.883L11 9v4a1 1 0 0 0 1.993.117L13 13V9a1 1 0 0 0-1-1m0 7a1 1 0 1 0 0 2a1 1 0 0 0 0-2" class="duoicon-primary-layer" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
|
||||||
|
<symbol id="settings" viewBox="0 0 16 16">
|
||||||
|
<path fill="currentColor" d="M2.267 6.153A6 6 0 0 1 3.53 3.98a.36.36 0 0 1 .382-.095l1.36.484a.71.71 0 0 0 .935-.538l.26-1.416a.35.35 0 0 1 .274-.282a6.1 6.1 0 0 1 2.52 0c.14.03.248.141.274.282l.26 1.416a.708.708 0 0 0 .935.538l1.36-.484a.36.36 0 0 1 .382.095a6 6 0 0 1 1.262 2.173a.35.35 0 0 1-.108.378l-1.102.931a.703.703 0 0 0 0 1.076l1.102.931c.11.093.152.242.108.378a6 6 0 0 1-1.262 2.173a.36.36 0 0 1-.382.095l-1.36-.484a.71.71 0 0 0-.935.538l-.26 1.416a.35.35 0 0 1-.275.282a6.1 6.1 0 0 1-2.519 0a.35.35 0 0 1-.275-.282l-.259-1.416a.708.708 0 0 0-.935-.538l-1.36.484a.36.36 0 0 1-.382-.095a6 6 0 0 1-1.262-2.173a.35.35 0 0 1 .108-.378l1.102-.931a.704.704 0 0 0 0-1.076l-1.102-.931a.35.35 0 0 1-.108-.378M6.25 8a1.75 1.75 0 1 0 3.5 0a1.75 1.75 0 0 0-3.5 0" />
|
||||||
|
</symbol>
|
||||||
|
<symbol id="user-settings" viewBox="0 0 32 32">
|
||||||
|
<path fill="currentColor" d="M25.303 16.86a7.5 7.5 0 0 1 2.749 1.596l-.495 1.725a1.52 1.52 0 0 0 1.095 1.892l1.698.423a7.5 7.5 0 0 1-.04 3.189l-1.536.351a1.52 1.52 0 0 0-1.117 1.927l.467 1.514a7.5 7.5 0 0 1-2.737 1.635L24.15 29.84a1.53 1.53 0 0 0-2.192 0l-1.26 1.3a7.5 7.5 0 0 1-2.75-1.597l.495-1.724a1.52 1.52 0 0 0-1.095-1.892l-1.698-.424a7.5 7.5 0 0 1 .04-3.189l1.536-.35a1.52 1.52 0 0 0 1.117-1.928l-.467-1.513a7.5 7.5 0 0 1 2.737-1.635l1.237 1.272a1.53 1.53 0 0 0 2.192 0zM16 17c.387 0 .757.075 1.097.209a8.98 8.98 0 0 0-2.962 8.342c-.995.28-2.192.449-3.635.449C2.04 26 2 20.205 2 20.15V20a3 3 0 0 1 3-3zm7 5a2 2 0 1 0 0 4a2 2 0 0 0 0-4M10.5 4a5.5 5.5 0 1 1 0 11a5.5 5.5 0 0 1 0-11M23 7a4 4 0 1 1 0 8a4 4 0 0 1 0-8" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
|
||||||
|
<symbol id="user-profile" viewBox="0 0 16 16">
|
||||||
|
<path fill="currentColor" d="M1 4.75C1 3.784 1.784 3 2.75 3h10.5c.966 0 1.75.784 1.75 1.75v6.5A1.75 1.75 0 0 1 13.25 13H2.75A1.75 1.75 0 0 1 1 11.25zM2.75 4a.75.75 0 0 0-.75.75v6.5c0 .414.336.75.75.75h10.5a.75.75 0 0 0 .75-.75v-6.5a.75.75 0 0 0-.75-.75zM9.5 6a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zM5.261 7.714a1.357 1.357 0 1 0 0-2.714a1.357 1.357 0 0 0 0 2.714m-1.403.678A.86.86 0 0 0 3 9.25a1.67 1.67 0 0 0 1.265 1.62l.053.014c.62.155 1.267.155 1.886 0l.054-.013a1.67 1.67 0 0 0 1.265-1.62a.86.86 0 0 0-.858-.859z" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="sign-out" viewBox="0 0 20 20"><path fill="currentColor" d="M8.5 11.25a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5M11 3.5a.5.5 0 0 0-.576-.494l-7 1.07A.5.5 0 0 0 3 4.57v10.86a.5.5 0 0 0 .424.494l7 1.071a.5.5 0 0 0 .576-.494V10h5.172l-.997.874a.5.5 0 0 0 .658.752l1.996-1.75a.5.5 0 0 0 0-.752l-1.996-1.75a.499.499 0 1 0-.658.752l.997.874H11zm-1 .582V15.92L4 15V4.999zM12.5 16H12v-5h1v4.5a.5.5 0 0 1-.5.5M12 8V4h.5a.5.5 0 0 1 .5.5V8z" /></symbol>
|
||||||
|
<symbol id="auth" viewBox="0 0 16 16"><path fill="currentColor" d="M9.07 6.746a2 2 0 0 0 2.91.001l.324-.344c.297.14.577.316.835.519l-.126.422a2 2 0 0 0 1.456 2.518l.348.083a4.7 4.7 0 0 1 .011 1.017l-.46.117a2 2 0 0 0-1.431 2.479l.156.556q-.383.294-.822.497l-.337-.357a2 2 0 0 0-2.91-.002l-.325.345a4.3 4.3 0 0 1-.835-.519l.126-.423a2 2 0 0 0-1.456-2.518l-.35-.083a4.7 4.7 0 0 1-.01-1.016l.462-.118a2 2 0 0 0 1.43-2.478l-.156-.557q.383-.294.822-.497zm-1.423-4.6a.5.5 0 0 1 .707 0C9.594 3.39 10.97 4 12.5 4a.5.5 0 0 1 .5.5v1.1a5.5 5.5 0 0 0-7.494 7.204C3.846 11.59 3 9.812 3 7.502V4.5a.5.5 0 0 1 .5-.5c1.53 0 2.904-.61 4.147-1.854M10.501 9.5a1 1 0 1 0 0 2a1 1 0 0 0 0-2"/></symbol>
|
||||||
|
<symbol id="chevron-up-down" viewBox="0 0 16 16"><path fill="currentColor" d="M4.22 6.53a.75.75 0 0 0 1.06 0L8 3.81l2.72 2.72a.75.75 0 1 0 1.06-1.06L8.53 2.22a.75.75 0 0 0-1.06 0L4.22 5.47a.75.75 0 0 0 0 1.06m0 2.94a.75.75 0 0 1 1.06 0L8 12.19l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0l-3.25-3.25a.75.75 0 0 1 0-1.06"/></symbol>
|
||||||
|
<symbol id="laptop-settings" viewBox="0 0 20 20"><path fill="currentColor" d="M4.5 5A1.5 1.5 0 0 0 3 6.5v6A1.5 1.5 0 0 0 4.5 14h4.522A5.5 5.5 0 0 1 17 9.6V6.5A1.5 1.5 0 0 0 15.5 5zm-2 10h6.522q.047.516.185 1H2.5a.5.5 0 0 1 0-1m9.565-3.558a2 2 0 0 1-1.43 2.478l-.462.118a4.7 4.7 0 0 0 .01 1.016l.35.083a2 2 0 0 1 1.456 2.519l-.127.423q.388.306.835.517l.325-.344a2 2 0 0 1 2.91.002l.337.358q.44-.203.822-.498l-.156-.556a2 2 0 0 1 1.43-2.478l.46-.118a4.7 4.7 0 0 0-.01-1.017l-.348-.082a2 2 0 0 1-1.456-2.52l.126-.421a4.3 4.3 0 0 0-.835-.519l-.325.344a2 2 0 0 1-2.91-.001l-.337-.358a4.3 4.3 0 0 0-.821.497zm2.434 4.058a1 1 0 1 1 0-2a1 1 0 0 1 0 2"/></symbol>
|
||||||
|
<symbol id="people-search" viewBox="0 0 20 20"><path fill="currentColor" d="M10 2a4 4 0 1 0 0 8a4 4 0 0 0 0-8m4.865 14.797c-1.071.683-2.454 1.064-3.962 1.171a1.5 1.5 0 0 0-.342-.529l-2-1.999A4.5 4.5 0 0 0 9 13.5a4.5 4.5 0 0 0-.758-2.5H15a2 2 0 0 1 2 2c0 1.691-.833 2.966-2.135 3.797M4.5 17c.786 0 1.512-.26 2.096-.697l2.55 2.55a.5.5 0 1 0 .708-.707l-2.55-2.55A3.5 3.5 0 1 0 4.5 17m0-1a2.5 2.5 0 1 1 0-5a2.5 2.5 0 0 1 0 5"/></symbol>
|
||||||
|
<symbol id="search-12" viewBox="0 0 12 12"><path fill="currentColor" d="M5 1a4 4 0 1 0 2.248 7.31l2.472 2.47a.75.75 0 1 0 1.06-1.06L8.31 7.248A4 4 0 0 0 5 1M2.5 5a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0"/></symbol>
|
||||||
|
<symbol id="delete-12" viewBox="0 0 12 12"><path fill="currentColor" d="M5 3h2a1 1 0 0 0-2 0M4 3a2 2 0 1 1 4 0h2.5a.5.5 0 0 1 0 1h-.441l-.443 5.17A2 2 0 0 1 7.623 11H4.377a2 2 0 0 1-1.993-1.83L1.941 4H1.5a.5.5 0 0 1 0-1zm3.5 3a.5.5 0 0 0-1 0v2a.5.5 0 0 0 1 0zM5 5.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5M3.38 9.085a1 1 0 0 0 .997.915h3.246a1 1 0 0 0 .996-.915L9.055 4h-6.11z"/></symbol>
|
||||||
|
<symbol id="person-add" viewBox="0 0 16 16"><path fill="currentColor" d="M9.626 5.07a5.5 5.5 0 0 0-3.299 1.847A2.751 2.751 0 1 1 9.626 5.07M5.6 8c-.384.75-.6 1.6-.6 2.5c0 1.31.458 2.512 1.222 3.457C3.555 13.653 2 11.803 2 10v-.5A1.5 1.5 0 0 1 3.5 8zm4.9 7a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9m0-7a.5.5 0 0 1 .5.5V10h1.5a.5.5 0 0 1 0 1H11v1.5a.5.5 0 0 1-1 0V11H8.5a.5.5 0 0 1 0-1H10V8.5a.5.5 0 0 1 .5-.5"/></symbol>
|
||||||
|
|
||||||
|
<symbol id="logo" viewBox="0 0 1028 1024"><path d="M550.68864 672c25.6-54.4 76.8-96 134.4-115.2l41.6-89.6c-3.2-6.4-6.4-12.8-6.4-19.2-3.2-6.4-3.2-16-6.4-22.4-25.6-6.4-41.6-19.2-51.2-38.4-16-32 3.2-76.8 41.6-115.2-25.6-35.2-57.6-64-92.8-89.6-38.4 41.6-80 60.8-112 48-35.2-12.8-51.2-57.6-51.2-112-41.6-6.4-86.4-3.2-128 3.2 3.2 60.8-12.8 105.6-44.8 121.6-35.2 12.8-76.8-3.2-118.4-41.6-35.2 25.6-64 57.6-89.6 92.8 41.6 38.4 60.8 80 48 112-12.8 35.2-57.6 51.2-112 51.2-6.4 41.6-3.2 86.4 3.2 128 54.4-3.2 99.2 12.8 115.2 44.8 16 32-3.2 76.8-41.6 115.2 25.6 35.2 57.6 64 92.8 89.6 38.4-41.6 80-60.8 112-48 35.2 12.8 51.2 57.6 51.2 112 41.6 6.4 86.4 3.2 128-3.2-3.2-54.4 12.8-99.2 44.8-115.2 3.2-3.2 9.6-3.2 12.8-3.2 6.4-35.2 12.8-73.6 28.8-105.6z m-156.8 6.4C304.28864 678.4 227.48864 604.8 227.48864 512c0-92.8 73.6-166.4 166.4-166.4s166.4 73.6 166.4 166.4c3.2 92.8-73.6 166.4-166.4 166.4z" fill="currentColor" ></path><path d="M1001.88864 288l-54.4 96-99.2-48 41.6-102.4c-48 3.2-96 28.8-118.4 76.8-22.4 48-16 105.6 16 144L707.48864 620.8c-48 3.2-92.8 28.8-115.2 76.8-22.4 48-16 99.2 12.8 140.8l60.8-102.4 99.2 48-44.8 112c48-3.2 96-28.8 118.4-76.8 22.4-48 16-102.4-16-144l80-166.4c48-3.2 92.8-28.8 115.2-76.8 19.2-51.2 12.8-105.6-16-144z" fill="currentColor" ></path></symbol>
|
||||||
|
|
||||||
</svg>
|
</svg>
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
header
|
|
||||||
</div>
|
|
||||||
61
src/lib/components/layout/app/AppHeader.svelte
Normal file
61
src/lib/components/layout/app/AppHeader.svelte
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import ThemeSelector from '$lib/widget/ThemeSelector.svelte';
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="w-full h-18 flex justify-between items-center px-4 bg-base-300 flex-shrink-0 z-10">
|
||||||
|
<div>
|
||||||
|
<!-- <button-->
|
||||||
|
<!-- class="btn btn-square btn-ghost"-->
|
||||||
|
<!-- aria-label="Toggle Sidebar"-->
|
||||||
|
<!-- onclick={sidebarState.toggleSidebar}-->
|
||||||
|
<!-- >-->
|
||||||
|
<!-- <Icon Cid="menu" size="24" />-->
|
||||||
|
<!-- </button>-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex justify-center items-center gap-4 select-none">
|
||||||
|
<ThemeSelector/>
|
||||||
|
|
||||||
|
{#if page.data.user }
|
||||||
|
<div class="dropdown dropdown-end ">
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
class="rounded-full cursor-pointer shadow-base-content bg-base-100/50 p-0.5 flex items-center justify-center text-primary-content font-bold"
|
||||||
|
>
|
||||||
|
{#if page.data.user.avatar}
|
||||||
|
<img
|
||||||
|
class="w-8 h-8 rounded-full "
|
||||||
|
src="{page.data.user.avatar}"
|
||||||
|
alt="Avatar"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<span>{page.data.user.nickname.slice(0, 1)}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-content mt-2 w-64 shadow-base-300 p-12 shadow-2xl bg-base-200 border border-base-content/10 rounded-box ">
|
||||||
|
<div class="text-center ">
|
||||||
|
<p class="font-bold">{page.data.user.nickname}</p>
|
||||||
|
<p class="text-xs mt-2">{page.data.user.username}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="">
|
||||||
|
<button class="btn btn-primary btn-sm" onclick={() => goto(resolve("/auth/login"))}>
|
||||||
|
登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
235
src/lib/components/layout/app/AppSidebar.svelte
Normal file
235
src/lib/components/layout/app/AppSidebar.svelte
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import {resolve as _resolve} from '$app/paths'
|
||||||
|
import type { NavItem } from '$lib/types/layout.ts';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import Icon from '$lib/components/icon/Icon.svelte';
|
||||||
|
import { TOAST_KEY, type ToastState } from '$lib/stores/toast.svelte.ts';
|
||||||
|
import type { RouteId } from '$app/types';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const rawNavItems: NavItem[] = [
|
||||||
|
{
|
||||||
|
id: 'dashboard',
|
||||||
|
label: '仪表盘',
|
||||||
|
icon: 'home',
|
||||||
|
href: '/app/dashboard'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'statistics',
|
||||||
|
label: '数据看板',
|
||||||
|
icon: 'data',
|
||||||
|
href: '/app/statistics'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'settings',
|
||||||
|
label: '系统设置',
|
||||||
|
icon: 'settings',
|
||||||
|
href: '/app/settings', // 父级带链接
|
||||||
|
subItems: [
|
||||||
|
{
|
||||||
|
id: 'auth',
|
||||||
|
label: '认证管理',
|
||||||
|
href: '/app/settings/auth',
|
||||||
|
icon: 'auth',
|
||||||
|
subItems: [
|
||||||
|
{
|
||||||
|
id: 'users',
|
||||||
|
label: '用户管理',
|
||||||
|
href: '/app/settings/auth/users'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'roles',
|
||||||
|
label: '角色权限',
|
||||||
|
href: '/app/settings/auth/roles'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'permissions',
|
||||||
|
label: '权限管理',
|
||||||
|
href: '/app/settings/auth/permissions'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'device',
|
||||||
|
label: '设备管理',
|
||||||
|
icon: 'laptop-settings',
|
||||||
|
href: '/app/settings/devices',
|
||||||
|
subItems: [
|
||||||
|
{
|
||||||
|
id: 'device-type',
|
||||||
|
label: '类型管理',
|
||||||
|
href: '/app/settings/devices/type'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const toast = getContext<ToastState>(TOAST_KEY);
|
||||||
|
// @ts-expect-error : ES + TS 混合报错手动忽略
|
||||||
|
const resolve = (href: RouteId) => _resolve(href);
|
||||||
|
|
||||||
|
let expandedIds = $state<string[]>([]);
|
||||||
|
|
||||||
|
const toggleExpand = (id: string) => {
|
||||||
|
if (expandedIds.includes(id)) {
|
||||||
|
expandedIds = expandedIds.filter(item => item !== id);
|
||||||
|
} else {
|
||||||
|
expandedIds = [...expandedIds, id];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleClick = (item: NavItem) => {
|
||||||
|
|
||||||
|
|
||||||
|
if (item.subItems && item.subItems.length > 0) {
|
||||||
|
toggleExpand(item.id);
|
||||||
|
} else {
|
||||||
|
// 叶子节点逻辑
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const computeNavState = (items: NavItem[], currentPath: string, openIds: string[]): NavItem[] => {
|
||||||
|
return items.map((item) => {
|
||||||
|
const newItem = { ...item };
|
||||||
|
|
||||||
|
newItem.isActive = currentPath === newItem.href;
|
||||||
|
|
||||||
|
// 递归处理子项
|
||||||
|
let hasActiveChild = false;
|
||||||
|
if (newItem.subItems && newItem.subItems.length > 0) {
|
||||||
|
newItem.subItems = computeNavState(newItem.subItems, currentPath, openIds);
|
||||||
|
hasActiveChild = newItem.subItems.some(child => child.isActive || child.isOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isManuallyOpen = openIds.includes(newItem.id);
|
||||||
|
|
||||||
|
// 如果你希望“进入父级页面自动展开子菜单”,请保留 `|| newItem.isActive`
|
||||||
|
newItem.isOpen = isManuallyOpen || hasActiveChild || newItem.isActive;
|
||||||
|
|
||||||
|
return newItem;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
let menuItems = $derived(computeNavState(rawNavItems, page.url.pathname, expandedIds));
|
||||||
|
|
||||||
|
let userProfileOpen = $state(false);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<aside class="custom-scrollbar h-screen bg-base-200 flex flex-col rounded-r-box overflow-hidden w-64">
|
||||||
|
<div class="flex items-center h-18 w-full bg-base-200">
|
||||||
|
<div class="space-x-4 pl-6 w-full">
|
||||||
|
<Icon id="logo" className="inline" size="36"/><h1 class="font-mono font-bold text-[0.85rem] inline align-bottom">IT Management System</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-y-auto flex-1 ">
|
||||||
|
<ul class="menu bg-base-200 w-full px-4 pb-4 pt-0 text-base-content flex-nowrap ">
|
||||||
|
{#each menuItems as item (item.id)}
|
||||||
|
<li class="{item.isActive ? 'menu-active' : ''} w-full rounded-box ">
|
||||||
|
{#if item.subItems && item.subItems.length > 0}
|
||||||
|
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||||
|
<a href={resolve(item.href)} class="p-2" onclick="{() => handleClick(item)}">
|
||||||
|
{#if item.icon}
|
||||||
|
<Icon id={item.icon} size="24"/>
|
||||||
|
{/if}
|
||||||
|
<span class="menu-dropdown-toggle {item.isOpen ? 'menu-dropdown-show ' : ''}">{item.label}</span>
|
||||||
|
</a>
|
||||||
|
<ul class="menu-dropdown rounded-box {item.isOpen ? 'menu-dropdown-show' : ''}">
|
||||||
|
{#each item.subItems as subItem (subItem.id)}
|
||||||
|
<li class="{subItem.isActive ? 'menu-active' : ''} rounded-box ">
|
||||||
|
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||||
|
<a href={resolve(subItem.href)} class="p-2 " onclick={() => handleClick(subItem)}>
|
||||||
|
{#if subItem.icon}
|
||||||
|
<Icon id={subItem.icon} size="24"/>
|
||||||
|
{/if}
|
||||||
|
<span class="menu-dropdown-toggle">
|
||||||
|
{subItem.label}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{#if subItem.subItems && subItem.subItems.length > 0}
|
||||||
|
<ul class="menu-dropdown {subItem.isOpen ? 'menu-dropdown-show' : ''}" >
|
||||||
|
{#each subItem.subItems as childItem (childItem.id)}
|
||||||
|
<li class="{childItem.isActive ? 'menu-active' : ''} rounded-box">
|
||||||
|
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||||
|
<a href={resolve(childItem.href)} class="p-2">
|
||||||
|
{#if childItem.icon}
|
||||||
|
<Icon id={childItem.icon} size="24"/>
|
||||||
|
{:else}
|
||||||
|
<div class="w-0.5/2 h-1">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{childItem.label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{:else }
|
||||||
|
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||||
|
<a href={resolve(item.href)} class="p-2">
|
||||||
|
{#if item.icon}
|
||||||
|
<Icon id={item.icon} size="24"/>
|
||||||
|
{/if}
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="h-24 w-full bg-base-200 ">
|
||||||
|
{@render UserCard( )}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(156, 163, 175, 0.3);
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{#snippet UserCard()}
|
||||||
|
{#if page.data.user}
|
||||||
|
<div class="flex items-center px-4 h-full select-none w-full overflow-hidden hover:bg-base-100 cursor-pointer transition-all dropdown dropdown-top dropdown-end">
|
||||||
|
<div class="flex gap-4 w-full" tabindex="0" role="button" >
|
||||||
|
<div class="w-12 h-12 rounded-box overflow-hidden flex-shrink-0"> <img src={page.data.user.avatar} alt="avatar" class="w-full h-full object-cover">
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="truncate font-medium">{page.data.user.nickname}</p>
|
||||||
|
<p class="text-xs mt-2 truncate text-base-content/60">@{page.data.user.username}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul tabindex="-1" class="dropdown-contents menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm">
|
||||||
|
<li><a>Item 1</a></li>
|
||||||
|
<li><a>Item 2</a></li>
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
4
src/lib/components/loading/TableLoadingState.svelte
Normal file
4
src/lib/components/loading/TableLoadingState.svelte
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="flex-1 inset-0 flex flex-col items-center justify-center bg-base-100/50 backdrop-blur-sm z-10">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
|
<p class="text-base-content/70 mt-4 font-medium animate-pulse">正在加载数据...</p>
|
||||||
|
</div>
|
||||||
147
src/lib/components/table/DevicesTable.svelte
Normal file
147
src/lib/components/table/DevicesTable.svelte
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
|
||||||
|
import type { PageResult } from '$lib/types/dataTable.ts';
|
||||||
|
import Icon from '$lib/components/icon/Icon.svelte';
|
||||||
|
import type { DeviceResponse, } from '$lib/types/api.ts';
|
||||||
|
|
||||||
|
let { devices } = $props<{
|
||||||
|
devices: PageResult<DeviceResponse[]>,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const newRowTitles = [
|
||||||
|
{ title: 'ID', width: 5}
|
||||||
|
, { title: '用户名', width: 15 }
|
||||||
|
, { title: '昵称', width: 20 }
|
||||||
|
, { title: '头像', width: 10 }
|
||||||
|
, { title: '用户组', width: 45 }
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="h-full">
|
||||||
|
<div class="overflow-x-auto rounded-box shadow bg-base-100 mt-1 min-h-1/2">
|
||||||
|
<div class="flex items-center justify-between px-4 pt-4 pb-2 ">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<label class="input">
|
||||||
|
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-width="2.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<path d="m21 21-4.3-4.3"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<input type="search" required placeholder="Search" />
|
||||||
|
<button class="btn btn-xs btn-primary">搜索</button>
|
||||||
|
</label>
|
||||||
|
<!--{#if rolesOptions}-->
|
||||||
|
<!-- <div class="filter w-64">-->
|
||||||
|
<!-- <input class="btn filter-reset " type="radio" value='' name="metaframeworks" aria-label="All" onchange={handleRoleChange} />-->
|
||||||
|
<!-- {#each rolesOptions as role(role.value)}-->
|
||||||
|
<!-- <input class="btn" type="radio" name="metaframeworks" aria-label="{role.label}" value={role.value} onchange={handleRoleChange} />-->
|
||||||
|
<!-- {/each}-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!--{/if}-->
|
||||||
|
</div>
|
||||||
|
<div class=" flex items-center justify-center gap-4">
|
||||||
|
<button class="btn btn-primary">添加设备</button>
|
||||||
|
|
||||||
|
<div class="dropdown dropdown-bottom dropdown-end">
|
||||||
|
<div tabindex="0" role="button" class="btn" ><Icon Cid="menu" size="24" /></div>
|
||||||
|
<ul tabindex="-1" class="dropdown-content menu bg-base-200 rounded-box z-1 w-52 p-2 mt-2 shadow-sm" >
|
||||||
|
<li><div>删除</div></li>
|
||||||
|
<li><div>封禁</div></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{#if devices.total > 0}
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 5%">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" class="checkbox" />
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
{#each newRowTitles as item,index(index)}
|
||||||
|
<th style="width: {item.width}%" >{item.title}</th>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!--{#if users.records}-->
|
||||||
|
<!-- <tbody>-->
|
||||||
|
<!-- {#each users.records as record(record.id)}-->
|
||||||
|
<!-- <tr>-->
|
||||||
|
<!-- <th>-->
|
||||||
|
<!-- <label>-->
|
||||||
|
<!-- <input type="checkbox" class="checkbox" />-->
|
||||||
|
<!-- </label>-->
|
||||||
|
<!-- </th>-->
|
||||||
|
<!-- <td>{record.id}</td>-->
|
||||||
|
<!-- <td>{record.username}</td>-->
|
||||||
|
<!-- <td>{record.nickname}</td>-->
|
||||||
|
<!-- <td>-->
|
||||||
|
<!-- <div class="w-8 h-8 rounded-box bg-primary-content/10 border-0">-->
|
||||||
|
<!-- {#if record.avatar}-->
|
||||||
|
<!-- <img class="w-8 h-8 rounded-box bg-primary-content/10 border-0" src="{record.avatar}" alt=" ">-->
|
||||||
|
<!-- {/if}-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </td>-->
|
||||||
|
<!-- <td class="">-->
|
||||||
|
<!-- {#each record.roles as role (role.id)}-->
|
||||||
|
<!-- <span class="badge select-none mr-2 last:mr-0 {role.id === 1 ? 'badge-primary' : 'badge-secondary'}">{role.name}</span>-->
|
||||||
|
<!-- {/each}-->
|
||||||
|
<!-- </td>-->
|
||||||
|
<!-- </tr>-->
|
||||||
|
<!-- {/each}-->
|
||||||
|
|
||||||
|
<!-- </tbody>-->
|
||||||
|
<!-- <tfoot>-->
|
||||||
|
<!-- <tr>-->
|
||||||
|
<!-- <th colspan={newRowTitles.length + 1} class="text-center py-4 ">-->
|
||||||
|
<!-- <div class=" flex items-center justify-between">-->
|
||||||
|
<!-- <div>-->
|
||||||
|
<!-- page {users.current} of {users.pages}-->
|
||||||
|
<!-- </div>-->
|
||||||
|
|
||||||
|
<!-- <div class="join">-->
|
||||||
|
<!-- <button class="join-item btn">1</button>-->
|
||||||
|
<!-- <button class="join-item btn">2</button>-->
|
||||||
|
<!-- <button class="join-item btn btn-disabled">...</button>-->
|
||||||
|
<!-- <button class="join-item btn">99</button>-->
|
||||||
|
<!-- <button class="join-item btn">100</button>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
|
||||||
|
<!-- <div>-->
|
||||||
|
<!-- <button class="btn btn-primary">下一页</button>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </th>-->
|
||||||
|
<!-- </tr>-->
|
||||||
|
<!-- </tfoot>-->
|
||||||
|
|
||||||
|
<!--{/if}-->
|
||||||
|
|
||||||
|
</table>
|
||||||
|
{:else }
|
||||||
|
<p>No users found</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
201
src/lib/components/table/UserTableOld.svelte
Normal file
201
src/lib/components/table/UserTableOld.svelte
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import Icon from '$lib/components/icon/Icon.svelte';
|
||||||
|
import type { UserProfile } from '$lib/types/user.ts';
|
||||||
|
|
||||||
|
const { data } = $props();
|
||||||
|
|
||||||
|
console.log("data", data);
|
||||||
|
const newRowTitles = [
|
||||||
|
{ title: 'ID', width: 5}
|
||||||
|
, { title: '用户名', width: 15 }
|
||||||
|
, { title: '昵称', width: 20 }
|
||||||
|
, { title: '头像', width: 10 }
|
||||||
|
, { title: '用户组', width: 45 }
|
||||||
|
];
|
||||||
|
|
||||||
|
let x ;
|
||||||
|
|
||||||
|
const handleRoleChange = (e) => {
|
||||||
|
console.log(e.target.value);
|
||||||
|
x = e.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let users: UserProfile[] ;
|
||||||
|
|
||||||
|
if (data.streamed.userList){
|
||||||
|
users = data.streamed.userList.records;
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class=" ">
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center ">
|
||||||
|
<p class="font-bold">用户管理</p>
|
||||||
|
<div class="breadcrumbs ">
|
||||||
|
<ul>
|
||||||
|
<li><a href={resolve('/app/dashboard')}>仪表盘</a></li>
|
||||||
|
<li><a href={resolve('/app/settings')}>系统设置</a></li>
|
||||||
|
<li><a href={resolve('/app/settings/auth')}>认证管理</a></li>
|
||||||
|
<li><a href={resolve('/app/settings/auth/users')}>用户管理</a></li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="overflow-x-auto rounded-box shadow bg-base-100 mt-1 ">
|
||||||
|
<div class="flex items-center justify-between px-4 pt-4 pb-2">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<label class="input">
|
||||||
|
<svg class="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-width="2.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<path d="m21 21-4.3-4.3"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<input type="search" required placeholder="Search" />
|
||||||
|
<button class="btn btn-xs btn-primary">搜索</button>
|
||||||
|
</label>
|
||||||
|
{#await data.streamed.roles}
|
||||||
|
<div></div>
|
||||||
|
{:then roles }
|
||||||
|
<div class="filter w-64">
|
||||||
|
<input class="btn filter-reset " type="radio" value='' name="metaframeworks" aria-label="All" onchange={handleRoleChange} />
|
||||||
|
{#each roles as role(role.id)}
|
||||||
|
<input class="btn " type="radio" name="metaframeworks" aria-label="{role.name}" value={role.id} onchange={handleRoleChange} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
<div class=" flex items-center justify-center gap-4">
|
||||||
|
<button class="btn btn-primary">添加用户</button>
|
||||||
|
|
||||||
|
<div class="dropdown dropdown-bottom dropdown-end">
|
||||||
|
<div tabindex="0" role="button" class="btn" ><Icon Cid="menu" size="24" /></div>
|
||||||
|
<ul tabindex="-1" class="dropdown-content menu bg-base-200 rounded-box z-1 w-52 p-2 mt-2 shadow-sm" >
|
||||||
|
<li><div>删除</div></li>
|
||||||
|
<li><div>封禁</div></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 5%">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" class="checkbox" />
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
{#each newRowTitles as item,index(index)}
|
||||||
|
<th style="width: {item.width}%" >{item.title}</th>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
{#await data.streamed.userList}
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colspan={newRowTitles.length + 1} class="text-center py-4 ">
|
||||||
|
<div class="min-h-96 flex items-center justify-center">
|
||||||
|
<div class="loading text-base-content"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
{:then userList}
|
||||||
|
<tbody>
|
||||||
|
{#each userList.records as record(record.id)}
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" class="checkbox" />
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
<td>{record.id}</td>
|
||||||
|
<td>{record.username}</td>
|
||||||
|
<td>{record.nickname}</td>
|
||||||
|
<td>
|
||||||
|
<div class="w-8 h-8 rounded-box bg-primary-content/10 border-0">
|
||||||
|
{#if record.avatar}
|
||||||
|
<img class="w-8 h-8 rounded-box bg-primary-content/10 border-0" src="{record.avatar}" alt=" ">
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="">
|
||||||
|
{#each record.roles as role (role.id)}
|
||||||
|
<span class="badge mr-2 last:mr-0 {role.id === 1 ? 'badge-primary' : 'badge-secondary'}">{role.name}</span>
|
||||||
|
{/each}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th colspan={newRowTitles.length + 1} class="text-center py-4 ">
|
||||||
|
<div class=" flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
page {userList.current} of {userList.pages}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="join">
|
||||||
|
<button class="join-item btn">1</button>
|
||||||
|
<button class="join-item btn">2</button>
|
||||||
|
<button class="join-item btn btn-disabled">...</button>
|
||||||
|
<button class="join-item btn">99</button>
|
||||||
|
<button class="join-item btn">100</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-primary">下一页</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
{:catch error}
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colspan={newRowTitles.length + 1} class="text-center py-4 ">
|
||||||
|
<div class="min-h-96 flex items-center justify-center">
|
||||||
|
<p class="error">组件加载失败: {error.message}</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
{/await}
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<style lang="scss">
|
||||||
|
.loading {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
196
src/lib/components/table/UsersTable.svelte
Normal file
196
src/lib/components/table/UsersTable.svelte
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PageResult } from '$lib/types/dataTable';
|
||||||
|
import type { UserProfile } from '$lib/types/user';
|
||||||
|
import Icon from '$lib/components/icon/Icon.svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
users,
|
||||||
|
selectedIds = $bindable([]),
|
||||||
|
onPageChange
|
||||||
|
} = $props<{
|
||||||
|
users: PageResult<UserProfile[]>;
|
||||||
|
selectedIds: number[];
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// --- 内部状态逻辑 (与UI展示紧密相关) ---
|
||||||
|
|
||||||
|
// 计算属性:是否全选
|
||||||
|
let isAllSelected = $derived(
|
||||||
|
users.records.length > 0 && selectedIds.length === users.records.length
|
||||||
|
);
|
||||||
|
|
||||||
|
// 计算属性:是否部分选中 (indeterminate)
|
||||||
|
let isIndeterminate = $derived(
|
||||||
|
selectedIds.length > 0 && selectedIds.length < users.records.length
|
||||||
|
);
|
||||||
|
|
||||||
|
function toggleAll() {
|
||||||
|
if (isAllSelected) {
|
||||||
|
selectedIds = [];
|
||||||
|
} else {
|
||||||
|
selectedIds = users.records.map((u) => u.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOne(id: number) {
|
||||||
|
if (selectedIds.includes(id)) {
|
||||||
|
selectedIds = selectedIds.filter((itemId) => itemId !== id);
|
||||||
|
} else {
|
||||||
|
selectedIds = [...selectedIds, id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action: 处理 checkbox 的 indeterminate 视觉状态
|
||||||
|
function indeterminate(node: HTMLInputElement, isIndeterminate: boolean) {
|
||||||
|
$effect(() => {
|
||||||
|
node.indeterminate = isIndeterminate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页逻辑辅助函数
|
||||||
|
function getPaginationRange(current: number, total: number) {
|
||||||
|
const delta = 2;
|
||||||
|
const range = [];
|
||||||
|
const rangeWithDots: (number | string)[] = [];
|
||||||
|
let l: number | undefined;
|
||||||
|
|
||||||
|
for (let i = 1; i <= total; i++) {
|
||||||
|
if (i === 1 || i === total || (i >= current - delta && i <= current + delta)) {
|
||||||
|
range.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i of range) {
|
||||||
|
if (l) {
|
||||||
|
if (i - l === 2) {
|
||||||
|
rangeWithDots.push(l + 1);
|
||||||
|
} else if (i - l !== 1) {
|
||||||
|
rangeWithDots.push('...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rangeWithDots.push(i);
|
||||||
|
l = i;
|
||||||
|
}
|
||||||
|
return rangeWithDots;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRowTitles = [
|
||||||
|
{ title: 'ID', width: 5 },
|
||||||
|
{ title: '用户名', width: 15 },
|
||||||
|
{ title: '昵称', width: 20 },
|
||||||
|
{ title: '头像', width: 10 },
|
||||||
|
{ title: '用户组', width: 45 }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
<div class="flex-1 overflow-y-auto">
|
||||||
|
<div class="bg-base-100">
|
||||||
|
<table class="table table-pin-rows">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-12 bg-base-100"> <label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
checked={isAllSelected}
|
||||||
|
use:indeterminate={isIndeterminate}
|
||||||
|
onchange={toggleAll}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
{#each newRowTitles as item (item.title)}
|
||||||
|
<th style="width: {item.width}%" class="bg-base-100">{item.title}</th>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody class="w-full">
|
||||||
|
{#each users.records as record (record.id)}
|
||||||
|
<tr class="hover">
|
||||||
|
<th>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
checked={selectedIds.includes(record.id)}
|
||||||
|
onchange={() => toggleOne(record.id)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
<td class="font-mono text-xs opacity-70">{record.id}</td>
|
||||||
|
<td class="font-bold">{record.username}</td>
|
||||||
|
<td>{record.nickname || '-'}</td>
|
||||||
|
<td>
|
||||||
|
<div class="avatar">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-base-300 ring ring-base-200 ring-offset-base-100 ring-offset-2">
|
||||||
|
{#if record.avatar}
|
||||||
|
<img src={record.avatar} alt={record.username} />
|
||||||
|
{:else}
|
||||||
|
<span class="text-xs flex items-center justify-center h-full w-full uppercase">
|
||||||
|
{record.username.slice(0, 2)}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
{#each record.roles as role (role.id)}
|
||||||
|
<span class="badge badge-sm {role.id === 1 ? 'badge-primary' : 'badge-ghost'}">
|
||||||
|
{role.name}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{#if users.records.length === 0}
|
||||||
|
<div class="flex flex-col items-center justify-center p-10 text-base-content/50">
|
||||||
|
<Icon id="search-off" size="48" />
|
||||||
|
<p class="mt-2">未找到匹配的用户</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{#if users.total > 0}
|
||||||
|
<div class="border-t border-base-200 p-4 flex items-center justify-between bg-base-100 ">
|
||||||
|
<div class="text-sm text-base-content/70">
|
||||||
|
显示 {(users.current - 1) * users.size + 1} 到 {Math.min(users.current * users.size, users.total)} 条,共 {users.total} 条
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="join">
|
||||||
|
<button
|
||||||
|
class="join-item btn btn-sm"
|
||||||
|
disabled={users.current === 1}
|
||||||
|
onclick={() => onPageChange(users.current - 1)}
|
||||||
|
>
|
||||||
|
上一页
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#each getPaginationRange(users.current, users.pages) as pageNum (pageNum)}
|
||||||
|
{#if pageNum === '...'}
|
||||||
|
<button class="join-item btn btn-sm btn-disabled">...</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="join-item btn btn-sm {users.current === pageNum ? 'btn-active btn-primary' : ''}"
|
||||||
|
onclick={() => onPageChange(Number(pageNum))}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="join-item btn btn-sm"
|
||||||
|
disabled={users.current === users.pages}
|
||||||
|
onclick={() => onPageChange(users.current + 1)}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
41
src/lib/hooks/useAuth.ts
Normal file
41
src/lib/hooks/useAuth.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { authStore } from '$lib/stores/authStore';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { isAuthenticated } from '$lib/utils/authUtils';
|
||||||
|
import type { UserProfile } from '$lib/types/user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to protect routes and provide auth utilities
|
||||||
|
*/
|
||||||
|
export const useAuth = () => {
|
||||||
|
/**
|
||||||
|
* Protect a route by checking authentication
|
||||||
|
*/
|
||||||
|
const protectRoute = () => {
|
||||||
|
onMount(async () => {
|
||||||
|
const authenticated = await isAuthenticated();
|
||||||
|
if (!authenticated) {
|
||||||
|
goto('/auth/login');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user
|
||||||
|
*/
|
||||||
|
const getUser = (): UserProfile | null => {
|
||||||
|
let user: UserProfile | null = null;
|
||||||
|
const unsubscribe = authStore.subscribe(state => {
|
||||||
|
user = state.user;
|
||||||
|
});
|
||||||
|
unsubscribe(); // Immediately unsubscribe
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
protectRoute,
|
||||||
|
getUser,
|
||||||
|
logout: authStore.logout,
|
||||||
|
isAuthenticated
|
||||||
|
};
|
||||||
|
};
|
||||||
43
src/lib/log.ts
Normal file
43
src/lib/log.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
|
||||||
|
type LogArgs = unknown[];
|
||||||
|
|
||||||
|
const getTime = () => new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
||||||
|
|
||||||
|
|
||||||
|
function print(level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR', message: string, args: LogArgs) {
|
||||||
|
if (browser) {
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
DEBUG: 'color: #999; font-weight: bold;',
|
||||||
|
INFO: 'color: #2196f3; font-weight: bold;',
|
||||||
|
WARN: 'color: #ff9800; font-weight: bold;',
|
||||||
|
ERROR: 'color: #f44336; font-weight: bold;',
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`%c[${level}] ${message}`, styles[level], ...args);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
DEBUG: '\x1b[90m',
|
||||||
|
INFO: '\x1b[34m',
|
||||||
|
WARN: '\x1b[33m',
|
||||||
|
ERROR: '\x1b[31m',
|
||||||
|
};
|
||||||
|
const reset = '\x1b[0m';
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`${colors[level]}[${getTime()}] [${level}] ${message}${reset}`,
|
||||||
|
...args
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const log = {
|
||||||
|
debug: (message: string, ...args: LogArgs) => print('DEBUG', message, args),
|
||||||
|
info: (message: string, ...args: LogArgs) => print('INFO', message, args),
|
||||||
|
warn: (message: string, ...args: LogArgs) => print('WARN', message, args),
|
||||||
|
error: (message: string, ...args: LogArgs) => print('ERROR', message, args),
|
||||||
|
};
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import {writable} from 'svelte/store';
|
|
||||||
import { browser } from '$app/environment';
|
|
||||||
|
|
||||||
export interface AuthStore {
|
|
||||||
token: string | null;
|
|
||||||
tokenHead: string | null;
|
|
||||||
isAuthenticated: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
let initialToken: string | null = null;
|
|
||||||
let initialTokenHead: string | null = null;
|
|
||||||
|
|
||||||
|
|
||||||
if (browser) {
|
|
||||||
initialToken = localStorage.getItem('auth_token');
|
|
||||||
initialTokenHead = localStorage.getItem('auth_token_head');
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialAuthStore: AuthStore = {
|
|
||||||
token: initialToken,
|
|
||||||
tokenHead: initialTokenHead,
|
|
||||||
isAuthenticated: initialToken !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
const authStatusStore = writable<AuthStore>({
|
|
||||||
token: initialToken,
|
|
||||||
tokenHead: initialTokenHead,
|
|
||||||
isAuthenticated: initialToken !== null
|
|
||||||
})
|
|
||||||
|
|
||||||
export const authStore = {
|
|
||||||
subscribe: authStatusStore.subscribe,
|
|
||||||
set: authStatusStore.set,
|
|
||||||
update: authStatusStore.update,
|
|
||||||
clear: () => {
|
|
||||||
authStatusStore.set(initialAuthStore);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
20
src/lib/stores/sidebar.svelte.ts
Normal file
20
src/lib/stores/sidebar.svelte.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export class SidebarState {
|
||||||
|
isSidebarExpanded = $state(true);
|
||||||
|
|
||||||
|
constructor(initialIsSidebarExpanded = true) {
|
||||||
|
this.isSidebarExpanded = initialIsSidebarExpanded;
|
||||||
|
}
|
||||||
|
toggleSidebar = ()=> {
|
||||||
|
this.isSidebarExpanded = !this.isSidebarExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeSidebar() {
|
||||||
|
this.isSidebarExpanded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
openSidebar() {
|
||||||
|
this.isSidebarExpanded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SIDEBAR_KEY = Symbol('SIDEBAR');
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { writable } from 'svelte/store';
|
|
||||||
|
|
||||||
|
|
||||||
interface SidebarState {
|
|
||||||
isOpen: boolean;
|
|
||||||
isExpanded: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const sidebarStore = writable<SidebarState>({
|
|
||||||
isOpen: false,
|
|
||||||
isExpanded: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换侧边栏打开、隐藏(偏移隐藏)状态
|
|
||||||
*/
|
|
||||||
export const toggleSidebar = () => {
|
|
||||||
sidebarStore.update(state => ({
|
|
||||||
...state,
|
|
||||||
isOpen: !state.isOpen,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换侧边栏展开状态
|
|
||||||
*/
|
|
||||||
export const toggleSidebarOpen = () => {
|
|
||||||
sidebarStore.update(state => ({
|
|
||||||
...state,
|
|
||||||
isExpanded: !state.isExpanded,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
16
src/lib/stores/theme.svelte.ts
Normal file
16
src/lib/stores/theme.svelte.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { DaisyUIThemeID } from '$lib/types/theme.ts';
|
||||||
|
|
||||||
|
export class ThemeState {
|
||||||
|
theme: DaisyUIThemeID = $state('dark');
|
||||||
|
|
||||||
|
constructor(initialTheme = 'dark' as DaisyUIThemeID) {
|
||||||
|
this.theme = initialTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(theme: DaisyUIThemeID) {
|
||||||
|
this.theme = theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const THEME_KEY = Symbol('THEME');
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { writable } from 'svelte/store';
|
|
||||||
import type { DaisyUIThemeID } from '$lib/types/theme.ts';
|
|
||||||
import { browser } from '$app/environment';
|
|
||||||
|
|
||||||
|
|
||||||
let initialTheme: DaisyUIThemeID = 'light';
|
|
||||||
|
|
||||||
if (browser){
|
|
||||||
initialTheme = localStorage.getItem('theme') as DaisyUIThemeID || 'light';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const themeStatusStore = writable<DaisyUIThemeID>(initialTheme);
|
|
||||||
|
|
||||||
export const themeStore = {
|
|
||||||
subscribe: themeStatusStore.subscribe,
|
|
||||||
set: (theme: DaisyUIThemeID) => {
|
|
||||||
if (browser){
|
|
||||||
localStorage.setItem('theme', theme);
|
|
||||||
}
|
|
||||||
themeStatusStore.set(theme);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
33
src/lib/stores/toast.svelte.ts
Normal file
33
src/lib/stores/toast.svelte.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
||||||
|
|
||||||
|
export interface ToastMessage {
|
||||||
|
id: string;
|
||||||
|
type: ToastType;
|
||||||
|
message: string;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ToastState {
|
||||||
|
toasts = $state<ToastMessage[]>([]);
|
||||||
|
add(message:string, type:ToastType = 'info', duration = 3000){
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
|
||||||
|
this.toasts.push({id,type,message,duration});
|
||||||
|
if (duration > 0){
|
||||||
|
setTimeout(()=>{
|
||||||
|
this.remove(id);
|
||||||
|
},duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(id:string) {
|
||||||
|
this.toasts = this.toasts.filter(toast => toast.id !== id)
|
||||||
|
}
|
||||||
|
success(msg: string, duration = 3000) { this.add(msg, 'success', duration); }
|
||||||
|
error(msg: string, duration = 3000) { this.add(msg, 'error', duration); }
|
||||||
|
warning(msg: string, duration = 3000) { this.add(msg, 'warning', duration); }
|
||||||
|
info(msg: string, duration = 3000) { this.add(msg, 'info', duration); }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TOAST_KEY = Symbol('TOAST');
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { type Writable, writable} from 'svelte/store';
|
|
||||||
import type { UserProfile } from '$lib/types/user.ts';
|
|
||||||
|
|
||||||
export const userStateStore:Writable<UserProfile > = writable<UserProfile>( {
|
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
nickname: '',
|
|
||||||
roles: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const initialUserProfile: UserProfile = {
|
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
nickname: '',
|
|
||||||
roles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const clearUserProfile = () => {
|
|
||||||
userStore.set(initialUserProfile);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const userStore = {
|
|
||||||
// 导出 subscribe 方法供组件订阅
|
|
||||||
subscribe: userStateStore.subscribe,
|
|
||||||
|
|
||||||
// 导出 set 方法
|
|
||||||
set: userStateStore.set,
|
|
||||||
|
|
||||||
// 导出 update 方法
|
|
||||||
update: userStateStore.update,
|
|
||||||
|
|
||||||
// 导出清晰的 'clear' 方法
|
|
||||||
clear: clearUserProfile
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,88 @@
|
|||||||
|
import type { AuthResponse } from '$lib/types/auth.ts';
|
||||||
|
|
||||||
export interface ApiResult<T> {
|
export interface ApiResult<T> {
|
||||||
code: number,
|
code: number;
|
||||||
msg: string,
|
msg: string;
|
||||||
data: T | null;
|
data: T | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class HttpError extends Error{
|
||||||
|
constructor(
|
||||||
|
public message: string,
|
||||||
|
public code: number,
|
||||||
|
public data: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiError<T> extends HttpError {
|
||||||
|
constructor(ApiResult: ApiResult<T>) {
|
||||||
|
super(ApiResult.msg, ApiResult.code, JSON.stringify(ApiResult.data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginFailure {
|
||||||
|
message: string;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginSuccess {
|
||||||
|
message: string;
|
||||||
|
data: AuthResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface DeviceResponse {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
model: string;
|
||||||
|
typeId: number;
|
||||||
|
locationId: number;
|
||||||
|
snmpCommunity: string;
|
||||||
|
manufacturer: string;
|
||||||
|
purchaseDate: Date;
|
||||||
|
status: number;
|
||||||
|
remark: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface Options {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface InterfaceAddressConfigRequest {
|
||||||
|
vlanId?: number | null;
|
||||||
|
ipAddress?: string;
|
||||||
|
subnetMask?: string;
|
||||||
|
gatewayIp?: string;
|
||||||
|
broadcastAddress?: string;
|
||||||
|
isPrimary: boolean;
|
||||||
|
isDhcp: boolean;
|
||||||
|
mtu?: number | null;
|
||||||
|
dnsServers?: string[]; // 简化处理,前端可用逗号分隔字符串转换
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkInterfaceRequest {
|
||||||
|
name: string;
|
||||||
|
type: number | null; // 1:物理口, 2:聚合口, 3:虚拟口
|
||||||
|
macAddress?: string;
|
||||||
|
portSpeed?: number | null;
|
||||||
|
duplex?: number | null;
|
||||||
|
remark?: string;
|
||||||
|
addressConfigs: InterfaceAddressConfigRequest[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateDeviceRequest {
|
||||||
|
name: string;
|
||||||
|
typeId: number | null;
|
||||||
|
locationId?: number | null;
|
||||||
|
model: string;
|
||||||
|
manufacturer: string;
|
||||||
|
snmpCommunity?: string;
|
||||||
|
purchaseDate?: string; // 对应 Java LocalDate (YYYY-MM-DD)
|
||||||
|
remark?: string;
|
||||||
|
interfaces: NetworkInterfaceRequest[];
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { JsonObject } from '$lib/types/http.ts';
|
import type { JsonObject } from '$lib/types/http.ts';
|
||||||
|
import type { UserProfile } from '$lib/types/user.ts';
|
||||||
|
|
||||||
|
|
||||||
export interface LoginPayload extends JsonObject {
|
export interface LoginPayload extends JsonObject {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -8,4 +10,15 @@ export interface LoginPayload extends JsonObject {
|
|||||||
export interface AuthResponse {
|
export interface AuthResponse {
|
||||||
token: string;
|
token: string;
|
||||||
tokenHead: string;
|
tokenHead: string;
|
||||||
|
userProfile: UserProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JwtPayload {
|
||||||
|
sub: string; // 用户标识
|
||||||
|
iat: number; // 签发时间(Unix 时间戳)
|
||||||
|
exp: number; // 过期时间(Unix 时间戳)
|
||||||
|
authorities: string[]; // 权限列表
|
||||||
|
userId: string; // 用户ID
|
||||||
|
nickname: string; // 昵称
|
||||||
|
avatar: string; // 头像URL
|
||||||
}
|
}
|
||||||
21
src/lib/types/dataTable.ts
Normal file
21
src/lib/types/dataTable.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
export interface BaseRecord {
|
||||||
|
id: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface TableColumn<T> {
|
||||||
|
key: keyof T; // 核心修改:强制 key 必须存在于数据模型中
|
||||||
|
label: string;
|
||||||
|
width?: string;
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface PageResult<T> {
|
||||||
|
records: T[];
|
||||||
|
total: number;
|
||||||
|
size: number;
|
||||||
|
current: number;
|
||||||
|
pages: number;
|
||||||
|
}
|
||||||
@@ -4,4 +4,4 @@ export interface JsonObject {
|
|||||||
[key:string] : JsonValue;
|
[key:string] : JsonValue;
|
||||||
}
|
}
|
||||||
export type JsonArray = JsonValue[];
|
export type JsonArray = JsonValue[];
|
||||||
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
|
export type JsonValue = JsonPrimitive | JsonObject | JsonArray | object;
|
||||||
|
|||||||
@@ -1,3 +1,25 @@
|
|||||||
export type IconId = {
|
export type IconId =
|
||||||
[key: string]: string;
|
"panel-right-close" |
|
||||||
};
|
"panel-right-close-solid"|
|
||||||
|
"panel-left-close"|
|
||||||
|
"panel-left-close-solid"|
|
||||||
|
"data"|
|
||||||
|
"starburst"|
|
||||||
|
"home"|
|
||||||
|
"menu"|
|
||||||
|
"logo"|
|
||||||
|
"success"|
|
||||||
|
"error"|
|
||||||
|
"warning"|
|
||||||
|
"info"|
|
||||||
|
"settings"|
|
||||||
|
"user-settings" |
|
||||||
|
"user-profile"|
|
||||||
|
"auth"|
|
||||||
|
"chevron-up-down"|
|
||||||
|
"laptop-settings"|
|
||||||
|
"people-search"|
|
||||||
|
"search-12"|
|
||||||
|
"delete-12" |
|
||||||
|
"person-add"
|
||||||
|
;
|
||||||
21
src/lib/types/layout.ts
Normal file
21
src/lib/types/layout.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { IconId } from '$lib/types/icon-ids.ts';
|
||||||
|
import type { RouteId } from '$app/types';
|
||||||
|
|
||||||
|
export interface NavItem {
|
||||||
|
id: string;
|
||||||
|
icon?: IconId;
|
||||||
|
label: string;
|
||||||
|
href: RouteId ;
|
||||||
|
isActive?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isHidden?: boolean;
|
||||||
|
isOpen?: boolean;
|
||||||
|
subItems?: NavItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessedNavItem extends Omit<NavItem, 'subItems'> {
|
||||||
|
isActive: boolean;
|
||||||
|
isChildActive: boolean;
|
||||||
|
// 递归定义:子项也是 ProcessedNavItem
|
||||||
|
subItems?: ProcessedNavItem[];
|
||||||
|
}
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
export interface UserProfile{
|
export interface UserProfile{
|
||||||
id: string;
|
id: number;
|
||||||
name : string;
|
username : string;
|
||||||
nickname : string;
|
nickname : string;
|
||||||
roles : string[];
|
roles : RoleResponse[];
|
||||||
|
avatar? : string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export interface RoleResponse {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import {user} from '$lib/stores/userStore';
|
|
||||||
import {get} from 'svelte/store';
|
|
||||||
|
|
||||||
export const hasRole = (role: string[]) => {
|
|
||||||
const userProfile = get(user);
|
|
||||||
|
|
||||||
if (!userProfile){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return role.some(r => userProfile.roles.includes(r))
|
|
||||||
};
|
|
||||||
60
src/lib/utils/authUtils.ts
Normal file
60
src/lib/utils/authUtils.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { authStore } from '$lib/stores/authStore';
|
||||||
|
import { tokenService } from '$lib/api/services/tokenService';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is authenticated
|
||||||
|
* This function will validate the token if needed
|
||||||
|
*/
|
||||||
|
export const isAuthenticated = async (): Promise<boolean> => {
|
||||||
|
if (!browser) return false;
|
||||||
|
|
||||||
|
const state = authStore.isAuthenticated();
|
||||||
|
if (!state) return false;
|
||||||
|
|
||||||
|
// Optionally validate token with server
|
||||||
|
const isValid = await tokenService.validateToken();
|
||||||
|
if (!isValid) {
|
||||||
|
// If token is invalid, logout user
|
||||||
|
authStore.logout();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require authentication for a page
|
||||||
|
* Redirects to login if not authenticated
|
||||||
|
*/
|
||||||
|
export const requireAuth = async (): Promise<void> => {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
const authenticated = await isAuthenticated();
|
||||||
|
if (!authenticated) {
|
||||||
|
// Redirect to login page
|
||||||
|
goto('/auth/login?redirect=' + encodeURIComponent(window.location.pathname));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout user and redirect to login
|
||||||
|
*/
|
||||||
|
export const logout = async (): Promise<void> => {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
// Call logout API
|
||||||
|
await fetch('/api/auth/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include'
|
||||||
|
}).catch(() => {
|
||||||
|
// Ignore errors during logout
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear local state
|
||||||
|
authStore.logout();
|
||||||
|
|
||||||
|
// Redirect to login
|
||||||
|
goto('/auth/login');
|
||||||
|
};
|
||||||
14
src/lib/utils/errorUtils.ts
Normal file
14
src/lib/utils/errorUtils.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { HttpError } from '$lib/api/httpClient.ts';
|
||||||
|
import { fail } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const handleError = (error: Error) => {
|
||||||
|
if (error instanceof HttpError) {
|
||||||
|
return fail(error.s, {
|
||||||
|
message: error.details?.msg || '认证失败,请检查凭证'
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (error instanceof TypeError || (error instanceof Error && error.message.includes('fetch'))) {
|
||||||
|
return { message: '无法连接服务器,请检查网络' };
|
||||||
|
}
|
||||||
|
};
|
||||||
20
src/lib/utils/tokenUtils.ts
Normal file
20
src/lib/utils/tokenUtils.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
export const parseJwt = <T>(token:string):T | null => {
|
||||||
|
try {
|
||||||
|
const base64Url = token.split('.')[1];
|
||||||
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const jsonPayload = decodeURIComponent(
|
||||||
|
atob(base64)
|
||||||
|
.split('')
|
||||||
|
.map(function(c) {
|
||||||
|
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
);
|
||||||
|
|
||||||
|
return JSON.parse(jsonPayload) as T;
|
||||||
|
}catch (e){
|
||||||
|
console.error('parseJwt error', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { themeStore } from '$lib/stores/themeStore.ts';
|
import { DAISYUI_THEME_OPTIONS } from '$lib/types/theme.ts';
|
||||||
import { DAISYUI_THEME_OPTIONS, type DaisyUIThemeID } from '$lib/types/theme.ts';
|
|
||||||
import ThemePreview from '$lib/widget/ThemePreview.svelte';
|
import ThemePreview from '$lib/widget/ThemePreview.svelte';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import { THEME_KEY, ThemeState } from '$lib/stores/theme.svelte.ts';
|
||||||
|
const themeState = getContext<ThemeState>(THEME_KEY);
|
||||||
|
|
||||||
// ... 逻辑保持不变 ...
|
|
||||||
const handleThemeChange = (themeValue: DaisyUIThemeID) => {
|
|
||||||
themeStore.set(themeValue);
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="dropdown dropdown-center md:dropdown-end ">
|
<div class="dropdown dropdown-center md:dropdown-end ">
|
||||||
<div tabindex="0" role="button" class="rounded hover:bg-base-100 active:bg-base-200 p-2 overflow-hidden flex items-center gap-2">
|
<div tabindex="0" role="button" class="rounded hover:bg-base-100 active:bg-base-200 p-2 overflow-hidden flex items-center gap-2">
|
||||||
<ThemePreview themeId={$themeStore} />
|
<ThemePreview themeId={themeState.theme} />
|
||||||
<svg width="12px" height="12px" class="mt-px text-base-content size-2 fill-current opacity-60 sm:inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z"></path></svg>
|
<svg width="12px" height="12px" class="mt-px text-base-content size-2 fill-current opacity-60 sm:inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z"></path></svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="dropdown-content shadow bg-base-200 border border-base-content/10 z-[1] rounded-box w-64 max-h-80 overflow-x-hidden overflow-y-auto flex flex-col ">
|
<ul
|
||||||
|
|
||||||
|
class="dropdown-content shadow bg-base-200 border border-base-content/10 z-[1] rounded-box w-64 max-h-80 overflow-x-hidden overflow-y-auto ">
|
||||||
|
|
||||||
{#each DAISYUI_THEME_OPTIONS as theme (theme.value)}
|
{#each DAISYUI_THEME_OPTIONS as theme (theme.value)}
|
||||||
|
|
||||||
@@ -23,14 +23,14 @@
|
|||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
on:click={() => handleThemeChange(theme.value)}
|
onclick={() => themeState.setTheme(theme.value)}
|
||||||
on:keydown={(e) => {
|
onkeydown={(e) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleThemeChange(theme.value);
|
themeState.setTheme(theme.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
class="gap-3 w-full flex hover:bg-base-300 active:bg-primary p-2 items-center {theme.value === $themeStore ? 'active' : ''}"
|
class="gap-3 w-full flex hover:bg-base-300 active:bg-primary p-2 items-center {theme.value === themeState.theme ? 'active' : ''}"
|
||||||
>
|
>
|
||||||
<ThemePreview themeId={theme.value} />
|
<ThemePreview themeId={theme.value} />
|
||||||
<div class=" ">{theme.name}</div>
|
<div class=" ">{theme.name}</div>
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import type { LayoutServerLoad } from '../../.svelte-kit/types/src/routes/$types';
|
import type { LayoutServerLoad } from '../../.svelte-kit/types/src/routes/$types';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import type { RouteId } from '$app/types';
|
||||||
|
import { COOKIE_THEME_KEY } from '$lib/components/constants/cookiesConstants.ts';
|
||||||
|
|
||||||
export const load: LayoutServerLoad = async ({url}) => {
|
|
||||||
|
|
||||||
const targetPath = '/app/dashboard';
|
export const load: LayoutServerLoad = async ({url,cookies,locals}) => {
|
||||||
|
|
||||||
|
const targetPath: RouteId = '/app/dashboard';
|
||||||
|
|
||||||
// 2. 检查当前访问的路径是否为根路径 '/'
|
|
||||||
// 并且确保当前路径不是目标路径本身,以避免无限循环
|
|
||||||
if (url.pathname === '/') {
|
if (url.pathname === '/') {
|
||||||
// 如果是根路径,则执行重定向到目标路径
|
// 如果是根路径,则执行重定向到目标路径
|
||||||
throw redirect(302, targetPath);
|
throw redirect(302, resolve(targetPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return{
|
||||||
|
theme: cookies.get(COOKIE_THEME_KEY) || 'dark',
|
||||||
|
user: locals.user
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -1,55 +1,38 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import './layout.css';
|
import './layout.css';
|
||||||
import favicon from '$lib/assets/favicon.svg?url';
|
import favicon from '$lib/assets/favicon.svg?url';
|
||||||
|
import { getContext, setContext } from 'svelte';
|
||||||
import { themeStore } from '$lib/stores/themeStore.ts';
|
|
||||||
let { children } = $props();
|
|
||||||
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { sidebarStore } from '$lib/stores/sidebarStore.ts';
|
|
||||||
import Sprite from '$lib/components/icon/Sprite.svelte';
|
import Sprite from '$lib/components/icon/Sprite.svelte';
|
||||||
|
import ToastContainer from '$lib/components/ToastContainer.svelte';
|
||||||
|
import {ThemeState,THEME_KEY} from '$lib/stores/theme.svelte.ts';
|
||||||
|
import type { DaisyUIThemeID } from '$lib/types/theme.ts';
|
||||||
|
import { COOKIE_THEME_KEY } from '$lib/components/constants/cookiesConstants.ts';
|
||||||
|
import { TOAST_KEY, ToastState } from '$lib/stores/toast.svelte.ts';
|
||||||
|
import { SIDEBAR_KEY, SidebarState } from '$lib/stores/sidebar.svelte.ts';
|
||||||
|
let { data ,children} = $props();
|
||||||
|
|
||||||
const MD_BREAKPOINT = '(min-width: 768px)';
|
setContext(THEME_KEY, new ThemeState(data.theme as DaisyUIThemeID ?? 'dark'))
|
||||||
|
setContext(TOAST_KEY,new ToastState())
|
||||||
|
setContext(SIDEBAR_KEY,new SidebarState())
|
||||||
|
const themeState = getContext<ThemeState>(THEME_KEY);
|
||||||
|
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
const handleMediaQueryChange = (event: MediaQueryListEvent) => {
|
document.documentElement.setAttribute('data-theme', themeState.theme);
|
||||||
const isCurrentlyDesktop = event.matches;
|
document.cookie = `${COOKIE_THEME_KEY}=${themeState.theme}; path=/; max-age=31536000; SameSite=Lax`;
|
||||||
console.log(isCurrentlyDesktop);
|
|
||||||
sidebarStore.update((store) => ({
|
|
||||||
...store,
|
|
||||||
isOpen: isCurrentlyDesktop
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let isMounted = $state(false); // 客户端渲染标志
|
|
||||||
onMount(()=>{
|
|
||||||
isMounted = true;
|
|
||||||
const isDesktop = window.matchMedia(MD_BREAKPOINT).matches;
|
|
||||||
console.log(isDesktop);
|
|
||||||
sidebarStore.update((store) => ({
|
|
||||||
...store,
|
|
||||||
isOpen: isDesktop,
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
const mediaQuery = window.matchMedia(MD_BREAKPOINT);
|
|
||||||
|
|
||||||
mediaQuery.addEventListener('change', handleMediaQueryChange);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
mediaQuery.removeEventListener('change', handleMediaQueryChange);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<link rel="icon" href={favicon} />
|
<link rel="icon" href={favicon} />
|
||||||
{#if isMounted}
|
|
||||||
<Sprite />
|
|
||||||
{/if}
|
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div data-theme={$themeStore} class="text-base-content">
|
<Sprite />
|
||||||
|
<div class="text-base-content">
|
||||||
|
<ToastContainer />
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { hasRole } from '$lib/utils/auth.ts';
|
|
||||||
|
|
||||||
const isAdmin = hasRole(['admin'])
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<div class="h-screen w-screen">
|
<div class="h-screen w-screen">
|
||||||
|
|
||||||
{#if isAdmin}
|
|
||||||
<div>
|
|
||||||
是管理员
|
|
||||||
</div>
|
|
||||||
{:else }
|
|
||||||
<div>
|
|
||||||
没有权限
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
29
src/routes/api/devices/+server.ts
Normal file
29
src/routes/api/devices/+server.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { log } from '$lib/log.ts';
|
||||||
|
import type { CreateDeviceRequest } from '$lib/types/api.ts';
|
||||||
|
import { deviceService } from '$lib/api/services/deviceService.ts';
|
||||||
|
import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
|
||||||
|
export async function POST({ request ,cookies }) {
|
||||||
|
const data = await request.json() as CreateDeviceRequest;
|
||||||
|
const token = cookies.get(COOKIE_TOKEN_KEY);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw redirect(302, '/auth/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 实际应用中:将 data 存入数据库
|
||||||
|
|
||||||
|
log.info('client request data', data)
|
||||||
|
|
||||||
|
const device = await deviceService.createDevice( data, token );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ message: 'Device created successfully', device: device }),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import AppHeader from '$lib/components/layout/app/AppHeader.svelte';
|
||||||
|
import AppSidebar from '$lib/components/layout/app/AppSidebar.svelte';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="">
|
<div class="flex h-screen bg-base-300 overflow-hidden relative">
|
||||||
{@render children()}
|
<AppSidebar />
|
||||||
</main>
|
<div class="flex-1 flex flex-col min-w-0 overflow-hidden relative h-full">
|
||||||
|
<AppHeader />
|
||||||
|
<main class="flex-1 flex flex-col min-h-0 overflow-hidden px-4 pb-4 ">
|
||||||
|
{@render children()}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -1,64 +1,11 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { getContext } from 'svelte';
|
||||||
import { resolve } from '$app/paths';
|
import { THEME_KEY, ThemeState } from '$lib/stores/theme.svelte.ts';
|
||||||
import { authStore } from '$lib/stores/authStore.ts';
|
|
||||||
import { authService } from '$lib/api/services/authService.ts';
|
|
||||||
import ThemeSelector from '$lib/widget/ThemeSelector.svelte';
|
|
||||||
import { sidebarStore, toggleSidebarOpen } from '$lib/stores/sidebarStore';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const themeState = getContext<ThemeState>(THEME_KEY);
|
||||||
</script>
|
</script>
|
||||||
<div class="flex h-screen bg-base-300 font-sans overflow-hidden">
|
|
||||||
<aside class=" opacity-0 md:opacity-100 flex-shrink-0 flex flex-col bg-base-200 border-r border-gray-700/30
|
|
||||||
transition-all duration-500 ease-in-out relative
|
|
||||||
{$sidebarStore.isOpen ? 'translate-x-0' : '-translate-x-full'}
|
|
||||||
{$sidebarStore.isExpanded ? 'w-[280px]' : 'w-[72px]'}
|
|
||||||
|
|
||||||
|
<div>
|
||||||
">
|
你好
|
||||||
|
{themeState.theme}
|
||||||
<div class="h-16 flex items-center px-4 justify-start">
|
</div>
|
||||||
<button
|
|
||||||
on:click={toggleSidebarOpen}
|
|
||||||
class="p-2 hover:bg-gray-700/50 rounded-full transition-colors"
|
|
||||||
aria-label="Toggle Menu"
|
|
||||||
>
|
|
||||||
123
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>1</div>
|
|
||||||
<div>1</div>
|
|
||||||
<div>1</div>
|
|
||||||
<div>1</div>
|
|
||||||
<div>1</div>
|
|
||||||
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<div class="w-full">
|
|
||||||
<header class="w-full h-18 flex justify-end items-center px-4 bg-base-300 gap-4 ">
|
|
||||||
<ThemeSelector/>
|
|
||||||
{#if $authStore.isAuthenticated}
|
|
||||||
<button
|
|
||||||
tabindex="0"
|
|
||||||
class="rounded-full bg-primary h-12 w-12 "
|
|
||||||
on:click={()=>{
|
|
||||||
console.log("退出登录")
|
|
||||||
authService.logout()
|
|
||||||
}}
|
|
||||||
aria-label="Logout"
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
{:else }
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="w-24">
|
|
||||||
<button class="btn btn-primary btn-wide" on:click={()=>{goto(resolve("/auth/login"))}}>登录</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</header>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
3
src/routes/app/settings/+page.svelte
Normal file
3
src/routes/app/settings/+page.svelte
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<div>
|
||||||
|
123
|
||||||
|
</div>
|
||||||
3
src/routes/app/settings/auth/+page.svelte
Normal file
3
src/routes/app/settings/auth/+page.svelte
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<div>
|
||||||
|
234
|
||||||
|
</div>
|
||||||
0
src/routes/app/settings/auth/roles/+page.svelte
Normal file
0
src/routes/app/settings/auth/roles/+page.svelte
Normal file
36
src/routes/app/settings/auth/users/+page.server.ts
Normal file
36
src/routes/app/settings/auth/users/+page.server.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { userService } from '$lib/api/services/userService.ts';
|
||||||
|
|
||||||
|
import { roleService } from '$lib/api/services/roleService.ts';
|
||||||
|
import { log } from '$lib/log.ts';
|
||||||
|
|
||||||
|
export const load:PageServerLoad = async ({ locals ,url }) => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const page = Number(url.searchParams.get('page')) || 1;
|
||||||
|
const size = Number(url.searchParams.get('size')) || 12;
|
||||||
|
const keyword = url.searchParams.get('q') || undefined;
|
||||||
|
const role = Number(url.searchParams.get('role')) || undefined;
|
||||||
|
|
||||||
|
log.debug('getAllUsers', { page, size, keyword, role });
|
||||||
|
|
||||||
|
|
||||||
|
const getRoles = async() => {
|
||||||
|
return await roleService.getRolesOptions(locals.api);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const getUserList = async() => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
return await userService.getAllUsers(locals.api,{ page: page, size: size , keyword:keyword, roleId:role});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
streamed:{
|
||||||
|
userList: getUserList(),
|
||||||
|
rolesOptions: getRoles(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
136
src/routes/app/settings/auth/users/+page.svelte
Normal file
136
src/routes/app/settings/auth/users/+page.svelte
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import Icon from '$lib/components/icon/Icon.svelte';
|
||||||
|
|
||||||
|
|
||||||
|
import TableLoadingState from '$lib/components/loading/TableLoadingState.svelte';
|
||||||
|
import TableLoadingError from '$lib/components/error/TableLoadingError.svelte';
|
||||||
|
import UsersTable from '$lib/components/table/UsersTable.svelte';
|
||||||
|
|
||||||
|
const { data } = $props();
|
||||||
|
|
||||||
|
// --- 1. 状态管理 ---
|
||||||
|
// selectedIds 需要在父组件,因为"批量操作按钮"在 Toolbar 里
|
||||||
|
let selectedIds = $state<number[]>([]);
|
||||||
|
|
||||||
|
let searchQuery = $state(page.url.searchParams.get('q') || '');
|
||||||
|
let currentRole = $derived(page.url.searchParams.get('role') || '');
|
||||||
|
|
||||||
|
// --- 2. URL 参数更新逻辑 ---
|
||||||
|
function updateParams(key: string, value: string | number | null) {
|
||||||
|
const url = new URL(page.url);
|
||||||
|
if (value === null || value === '') {
|
||||||
|
url.searchParams.delete(key);
|
||||||
|
} else {
|
||||||
|
url.searchParams.set(key, String(value));
|
||||||
|
}
|
||||||
|
if (key !== 'page') {
|
||||||
|
url.searchParams.set('page', '1');
|
||||||
|
}
|
||||||
|
// 切换筛选条件时清空选中状态,避免误操作
|
||||||
|
selectedIds = [];
|
||||||
|
goto(url, { keepFocus: true, noScroll: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
updateParams('q', searchQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRoleChange(e: Event) {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
updateParams('role', target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageChange(newPage: number) {
|
||||||
|
updateParams('page', newPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBatchAction(action: 'delete' | 'ban') {
|
||||||
|
if (selectedIds.length === 0) return alert('请先选择用户');
|
||||||
|
console.log(`执行批量操作: ${action}`, selectedIds);
|
||||||
|
// TODO: 调用 API...
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>用户管理 | 系统设置</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class=" h-full flex flex-col rounded-box overflow-hidden ">
|
||||||
|
<div class="flex justify-between items-center select-none pb-2">
|
||||||
|
<p class="font-bold text-lg">用户管理</p>
|
||||||
|
<div class="breadcrumbs text-sm text-base-content/70">
|
||||||
|
<ul>
|
||||||
|
<li><a href={resolve('/app/dashboard')}>仪表盘</a></li>
|
||||||
|
<li>系统设置</li>
|
||||||
|
<li>认证管理</li>
|
||||||
|
<li><a href={resolve('/app/settings/auth/users')}>用户管理</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center justify-between p-4 border-b border-base-200 bg-base-100 relative rounded-t-box">
|
||||||
|
<div class="flex flex-wrap items-center ">
|
||||||
|
<label class="input input-bordered input-sm flex items-center gap-2">
|
||||||
|
<Icon id="search-12" size="16" class="opacity-50" />
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
bind:value={searchQuery}
|
||||||
|
onkeydown={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
|
placeholder="搜索用户..."
|
||||||
|
class="grow"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-xs btn-ghost" onclick={handleSearch}>搜索</button>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button class="btn btn-primary btn-sm">
|
||||||
|
<Icon id="person-add" size="16" /> 添加用户
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="dropdown dropdown-bottom dropdown-end">
|
||||||
|
<div tabindex="0" role="button" class="btn btn-sm btn-square btn-ghost">
|
||||||
|
<Icon id="menu" size="20" />
|
||||||
|
</div>
|
||||||
|
<div tabindex="-1" class="dropdown-content w-48 bg-base-100 rounded-box z-[1] mt-2 p-2 shadow-lg border border-base-200 join join-vertical">
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={() => handleBatchAction('delete')}
|
||||||
|
class:btn-disabled={selectedIds.length === 0}
|
||||||
|
class=" join-item btn btn-error"
|
||||||
|
>
|
||||||
|
<span>批量删除 ({selectedIds.length})</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={() => handleBatchAction('ban')}
|
||||||
|
class:btn-disabled={selectedIds.length === 0}
|
||||||
|
class="join-item btn btn-neutral"
|
||||||
|
>
|
||||||
|
<span>批量封禁 ({selectedIds.length})</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 bg-base-100 flex flex-col min-h-0 overflow-hidden ">
|
||||||
|
{#await data.streamed.userList}
|
||||||
|
<TableLoadingState />
|
||||||
|
{:then users}
|
||||||
|
<UsersTable
|
||||||
|
{users}
|
||||||
|
bind:selectedIds
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
{:catch error}
|
||||||
|
<TableLoadingError error={error} />
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
24
src/routes/app/settings/devices/+page.server.ts
Normal file
24
src/routes/app/settings/devices/+page.server.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
import { deviceService } from '$lib/api/services/deviceService.ts';
|
||||||
|
import { deviceTypesService } from '$lib/api/services/deviceTypesService.ts';
|
||||||
|
import { log } from '$lib/log.ts';
|
||||||
|
|
||||||
|
export const load:PageServerLoad = async ({ locals }) => {
|
||||||
|
|
||||||
|
|
||||||
|
const result = deviceService.getAllDevices(locals.api,{ page: 1, size: 10 });
|
||||||
|
const options = deviceTypesService.getDeviceTypesOptions(locals.api);
|
||||||
|
|
||||||
|
const handle = () => {
|
||||||
|
return {
|
||||||
|
list: result,
|
||||||
|
options: options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
streamed:{
|
||||||
|
result: handle()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
66
src/routes/app/settings/devices/+page.svelte
Normal file
66
src/routes/app/settings/devices/+page.svelte
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<script>
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import DevicesTable from '$lib/components/table/DevicesTable.svelte';
|
||||||
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
|
import AddDevice from '$lib/components/form/AddDevice.svelte';
|
||||||
|
import { log } from '$lib/log.ts';
|
||||||
|
|
||||||
|
const {data} = $props();
|
||||||
|
let isOpen = $state(false)
|
||||||
|
|
||||||
|
let formRef = $state();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex-1 h-full flex flex-col">
|
||||||
|
<div class="flex justify-between items-center ">
|
||||||
|
<p class="font-bold">设备管理</p>
|
||||||
|
<div class="breadcrumbs ">
|
||||||
|
<ul>
|
||||||
|
<li><a href={resolve('/app/dashboard')}>仪表盘</a></li>
|
||||||
|
<li><a href={resolve('/app/settings')}>系统设置</a></li>
|
||||||
|
<li><a href={resolve('/app/settings/devices')}>设备管理</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#await data.streamed.result.list}
|
||||||
|
<div class="">
|
||||||
|
<p class="text-center">正在加载设备列表...</p>
|
||||||
|
<p class="text-center">请稍后...</p>
|
||||||
|
</div>
|
||||||
|
{:then list}
|
||||||
|
{#if list.total > 0 }
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<DevicesTable
|
||||||
|
bind:open={isOpen}
|
||||||
|
devices={list} />
|
||||||
|
</div>
|
||||||
|
{:else }
|
||||||
|
<div class="flex-1 w-full flex justify-center items-center 需要占满高度">
|
||||||
|
<div class="select-none text-center">
|
||||||
|
<p class="mb-10">暂无数据</p>
|
||||||
|
<button class="btn btn-primary" onclick={()=>{isOpen = true}}>添加设备</button>
|
||||||
|
<Modal bind:open={isOpen}
|
||||||
|
title="添加设备"
|
||||||
|
width="100%"
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
{#await data.streamed.result.options}
|
||||||
|
<div class="">
|
||||||
|
<p class="text-center">正在加载设备列表...</p>
|
||||||
|
<p class="text-center">请稍后...</p>
|
||||||
|
</div>
|
||||||
|
{:then options}
|
||||||
|
<AddDevice deviceTypeOptions={options} />
|
||||||
|
{:catch error}
|
||||||
|
{log.error(error)}
|
||||||
|
{/await}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:catch error}
|
||||||
|
<p class="text-center">{error}</p>
|
||||||
|
<p class="text-center">请稍后...</p>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
0
src/routes/app/settings/devices/type/+page.svelte
Normal file
0
src/routes/app/settings/devices/type/+page.svelte
Normal file
1
src/routes/app/statistics/+page.svelte
Normal file
1
src/routes/app/statistics/+page.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
这是数据
|
||||||
16
src/routes/app/user/+page.server.ts
Normal file
16
src/routes/app/user/+page.server.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { userService } from '$lib/api/services/userService.ts';
|
||||||
|
|
||||||
|
export async function load({cookies}) {
|
||||||
|
const token = cookies.get(COOKIE_TOKEN_KEY);
|
||||||
|
if (!token) {
|
||||||
|
throw redirect(302, '/auth/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = await userService.getUserProfile(token);
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/routes/app/user/+page.svelte
Normal file
9
src/routes/app/user/+page.svelte
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
这里展示个人信息
|
||||||
|
{JSON.stringify(data.profile)}
|
||||||
|
</div>
|
||||||
8
src/routes/app/user/[id]/+page.server.ts
Normal file
8
src/routes/app/user/[id]/+page.server.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export function load({ params }) {
|
||||||
|
|
||||||
|
console.log('params:', params);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
13
src/routes/app/user/[id]/+page.svelte
Normal file
13
src/routes/app/user/[id]/+page.svelte
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import {page} from '$app/state';
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<div>
|
||||||
|
{JSON.stringify(data)}
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{JSON.stringify(page.params)}
|
||||||
|
</div>
|
||||||
7
src/routes/auth/forgetPassword/+page.svelte
Normal file
7
src/routes/auth/forgetPassword/+page.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="h-screen w-screen bg-base-300">
|
||||||
|
123
|
||||||
|
</div>
|
||||||
91
src/routes/auth/login/+page.server.ts
Normal file
91
src/routes/auth/login/+page.server.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import type { Actions } from './$types';
|
||||||
|
import { fail } from '@sveltejs/kit';
|
||||||
|
import { authService } from '$lib/api/services/authService.ts';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import { ApiError } from '$lib/types/api.ts';
|
||||||
|
import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const actions:Actions = {
|
||||||
|
default: async ({ request,cookies ,url ,locals}) => {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const username = formData.get('username');
|
||||||
|
const password = formData.get('password');
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof username !== 'string' ||
|
||||||
|
typeof password !== 'string' ||
|
||||||
|
!username.trim() ||
|
||||||
|
!password.trim()
|
||||||
|
){
|
||||||
|
return fail(400,{
|
||||||
|
missing: true,
|
||||||
|
message: '请填写用户名和密码',
|
||||||
|
username: username?.toString() ?? ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const regexp = /^[a-zA-Z0-9_-]{5,16}$/
|
||||||
|
|
||||||
|
if (!regexp.test(username)){
|
||||||
|
return fail(400,{
|
||||||
|
username: username.toString(),
|
||||||
|
message: '用户名格式错误,请输入5-16位字符,只能包含字母、数字、下划线、减号'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length<8 || password.length>16){
|
||||||
|
return fail(400,{
|
||||||
|
message: '密码格式错误,请输入8-16位字符'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try{
|
||||||
|
const response = await authService.login(locals.api,{username,password});
|
||||||
|
|
||||||
|
cookies.set(COOKIE_TOKEN_KEY,`${response.tokenHead} ${response.token}`,{
|
||||||
|
path: '/',
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
maxAge: 60 * 60 * 24 * 7
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: '登录成功',
|
||||||
|
data: response,// 这个传入的data 在前端页面怎么使用?
|
||||||
|
redirectTo: url.searchParams.get('redirectTo') ?? resolve("/app/dashboard")
|
||||||
|
};
|
||||||
|
|
||||||
|
}catch (error){
|
||||||
|
if (error instanceof ApiError) {
|
||||||
|
return fail(400, {
|
||||||
|
incorrect: true,
|
||||||
|
message: error.message,
|
||||||
|
username
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof TypeError || (error instanceof Error && error.message.includes('fetch'))) {
|
||||||
|
return fail(503, {
|
||||||
|
message: '网络连接失败,无法连接到服务器',
|
||||||
|
username
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 4. 兜底的未知错误处理
|
||||||
|
console.error('Login unexpected error:', error);
|
||||||
|
return fail(500, {
|
||||||
|
message: '系统内部错误,请稍后再试',
|
||||||
|
username
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +1,128 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import Icon from '$lib/components/icon/Icon.svelte';
|
||||||
|
|
||||||
import { authService } from '$lib/api/services/authService.ts';
|
import { enhance } from '$app/forms';
|
||||||
import type { LoginPayload } from '$lib/types/auth.ts';
|
import { goto } from '$app/navigation';
|
||||||
import { goto } from '$app/navigation';
|
import type { AuthResponse } from '$lib/types/auth';
|
||||||
import { resolve } from '$app/paths';
|
import type { SubmitFunction } from '@sveltejs/kit';
|
||||||
import { get } from 'svelte/store';
|
import type { RouteId } from '$app/types';
|
||||||
import { authStore } from '$lib/stores/authStore.ts';
|
import { getContext } from 'svelte';
|
||||||
|
import { TOAST_KEY, ToastState } from '$lib/stores/toast.svelte.ts';
|
||||||
|
|
||||||
const loginPayload:LoginPayload = {
|
|
||||||
username: '',
|
const toast = getContext<ToastState>(TOAST_KEY);
|
||||||
password: ''
|
|
||||||
}
|
let loading = false;
|
||||||
const handleSubmit = async (e: Event) => {
|
|
||||||
e.preventDefault();
|
const handleLogin:SubmitFunction = () => {
|
||||||
try{
|
loading = true;
|
||||||
await authService.login(loginPayload);
|
return async ({ result , update }) => {
|
||||||
if(get(authStore).isAuthenticated){
|
loading = false;
|
||||||
await goto(resolve('/'));
|
if (result.type === 'failure') {
|
||||||
}
|
|
||||||
}catch (e){
|
if (typeof result.data?.message === 'string' ){
|
||||||
console.error(e);
|
toast.error(result.data?.message)
|
||||||
|
}
|
||||||
|
await update();
|
||||||
|
|
||||||
|
}else if (result.type === 'success') {
|
||||||
|
|
||||||
|
console.log('result', result)
|
||||||
|
if (typeof result.data?.message === 'string' ){
|
||||||
|
toast.success(result.data?.message || '登录成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.data?.redirectTo && typeof result.data?.redirectTo === 'string') {
|
||||||
|
// @ts-expect-error : 设计
|
||||||
|
await goto(resolve(result.data.redirectTo as RouteId ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const handleChange = (e: Event) => {
|
|
||||||
const target = e.target as HTMLInputElement;
|
|
||||||
loginPayload[target.name] = target.value;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="h-screen w-screen">
|
<div class="min-h-screen bg-base-200 flex items-center justify-center p-4">
|
||||||
<form id="loginForm">
|
|
||||||
<div>
|
<div class="card w-full max-w-sm bg-base-100 shadow-2xl">
|
||||||
<label class="" for="username" > username </label>
|
<div class="card-body">
|
||||||
<input type="text" name="username" on:change={handleChange} placeholder="username">
|
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h2 class="text-2xl font-bold flex justify-center items-center gap-2">
|
||||||
|
<Icon id="logo" size="40" className="inline-block"></Icon>
|
||||||
|
<span>IT DTMS登录</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<form method="post"
|
||||||
|
use:enhance={handleLogin}
|
||||||
|
class="space-y-4">
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="username">
|
||||||
|
<span class="label-text">用户名</span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
placeholder="username"
|
||||||
|
class="input input-bordered w-full pl-10"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-base-content/50">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor"
|
||||||
|
class="w-4 h-4 opacity-70"><path
|
||||||
|
d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM12.735 14c.618 0 1.093-.561.872-1.139a6.002 6.002 0 0 0-11.215 0c-.22.578.254 1.139.872 1.139h9.47Z" /></svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="password">
|
||||||
|
<span class="label-text">密码</span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="password"
|
||||||
|
class="input input-bordered w-full pl-10"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-base-content/50">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor"
|
||||||
|
class="w-4 h-4 opacity-70"><path fill-rule="evenodd"
|
||||||
|
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
|
||||||
|
clip-rule="evenodd" /></svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mt-6 flex justify-between">
|
||||||
|
<label class="label cursor-pointer justify-start gap-2">
|
||||||
|
<input type="checkbox" class="checkbox checkbox-sm checkbox-primary" />
|
||||||
|
<span class="label-text">记住我</span>
|
||||||
|
</label>
|
||||||
|
<div class="label">
|
||||||
|
<a href={resolve('/auth/forgetPassword')} class="label-text-alt link link-hover">忘记密码?</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mt-2">
|
||||||
|
<button class="btn btn-primary w-full {loading?'btn-disabled ':''}" >
|
||||||
|
{#if loading}
|
||||||
|
<span class="loading loading-spinner"></span>
|
||||||
|
{/if}
|
||||||
|
<span>登录</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<label class="" for="password" > password </label>
|
|
||||||
<input type="password" name="password" on:change={handleChange} placeholder="password">
|
|
||||||
</div>
|
|
||||||
<button class="" type="submit" on:click="{handleSubmit}" > 登录</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
17
src/routes/auth/logout/+page.server.ts
Normal file
17
src/routes/auth/logout/+page.server.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Actions } from './$types';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
|
||||||
|
import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const actions:Actions = {
|
||||||
|
default: async ({ cookies,locals}) => {
|
||||||
|
cookies.delete(COOKIE_TOKEN_KEY,{path:'/'});
|
||||||
|
locals.user = null;
|
||||||
|
|
||||||
|
throw redirect(302, resolve('/'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
@import 'tailwindcss';
|
@import "tailwindcss";
|
||||||
@plugin "daisyui"{
|
@plugin "daisyui"{
|
||||||
themes: all;
|
themes: light , dark , cupcake , bumblebee , emerald , corporate
|
||||||
|
, synthwave , retro , cyberpunk , valentine , halloween , garden
|
||||||
|
, forest , aqua , lofi , pastel , fantasy , wireframe --default , black --prefersdark
|
||||||
|
, luxury , dracula , cmyk , autumn , business , acid , lemonade
|
||||||
|
, night , coffee , winter , dim , nord , sunset , caramellatte
|
||||||
|
, abyss , silk;;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user