modify: 更新日志配置

This commit is contained in:
2026-04-26 10:20:32 +08:00
parent 55463a9d07
commit 00b9e0ebc0
83 changed files with 4050 additions and 1755 deletions
+2
View File
@@ -44,6 +44,7 @@ debug = true
host = "127.0.0.1"
port = 8000
reload = true
access_log = false
[development.logging]
level = "DEBUG"
@@ -65,6 +66,7 @@ host = "0.0.0.0"
port = 8000
reload = false
workers = 4
access_log = false
[production.logging]
level = "INFO"
+2
View File
@@ -44,6 +44,7 @@ debug = true
host = "127.0.0.1"
port = 8001
reload = true
access_log = false
[development.logging]
level = "DEBUG"
@@ -65,6 +66,7 @@ host = "0.0.0.0"
port = 8000
reload = false
workers = 4
access_log = false
[production.logging]
level = "INFO"
+1 -1
View File
@@ -54,7 +54,7 @@ def app_run(app_config: dict, default_config: dict):
reload=reload,
workers=workers,
log_level="debug" if app_config.get("debug", False) else "info",
access_log=True,
access_log=False,
timeout_keep_alive=timeout_keep_alive,
)
+512 -179
View File
@@ -8,17 +8,19 @@
"name": "chongming-web",
"version": "0.0.0",
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"element-plus": "^2.13.7",
"pinia": "^3.0.4",
"vue": "^3.5.31",
"vue-router": "^5.0.4"
},
"devDependencies": {
"@hey-api/openapi-ts": "^0.96.1",
"@tsconfig/node24": "^24.0.4",
"@types/node": "^24.12.0",
"@vitejs/plugin-vue": "^6.0.5",
"@vue/tsconfig": "^0.9.1",
"npm-run-all2": "^8.0.4",
"openapi-typescript-codegen": "^0.29.0",
"prettier": "3.8.1",
"typescript": "~6.0.0",
"vite": "^8.0.3",
@@ -29,24 +31,6 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "11.9.3",
"resolved": "https://registry.npmmirror.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz",
"integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.15",
"js-yaml": "^4.1.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/philsturgeon"
}
},
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -501,6 +485,24 @@
"node": ">=6.9.0"
}
},
"node_modules/@ctrl/tinycolor": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
"integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/@element-plus/icons-vue": {
"version": "2.3.2",
"resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
"integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.9.2",
"resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.9.2.tgz",
@@ -535,6 +537,202 @@
"tslib": "^2.4.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.5",
"resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz",
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.6",
"resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz",
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.5",
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.11",
"resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz",
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT"
},
"node_modules/@hey-api/codegen-core": {
"version": "0.8.0",
"resolved": "https://registry.npmmirror.com/@hey-api/codegen-core/-/codegen-core-0.8.0.tgz",
"integrity": "sha512-OuF/jenX9wz7AWHRBfb37v+jLkrfCt0FJXQuALNH2UsW6+bdZBmoibHl0K778SiHwneotJbAaEvX2S05wEqUQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@hey-api/types": "0.1.4",
"ansi-colors": "4.1.3",
"c12": "3.3.4",
"color-support": "1.1.3"
},
"engines": {
"node": ">=22.13.0"
},
"funding": {
"url": "https://github.com/sponsors/hey-api"
}
},
"node_modules/@hey-api/json-schema-ref-parser": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.4.1.tgz",
"integrity": "sha512-DoPJGxVApDlktP1yYLjmOrF0YBEqb32ieCbx1S1i09n8TyCgdoh4yQaQ3kp0sMTauH+bwNKPsFh7S8qiWCoKZA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jsdevtools/ono": "7.1.3",
"@types/json-schema": "7.0.15",
"yaml": "2.8.3"
},
"engines": {
"node": ">=22.13.0"
},
"funding": {
"url": "https://github.com/sponsors/hey-api"
}
},
"node_modules/@hey-api/openapi-ts": {
"version": "0.96.1",
"resolved": "https://registry.npmmirror.com/@hey-api/openapi-ts/-/openapi-ts-0.96.1.tgz",
"integrity": "sha512-EnH+6ncaVC1yNvYWcsO7MoeBSqCvYZaKvRDRqh+S7dsfb9lhe2mKUR2+7sDacGUBm7VDZZ7hg4q4egxlpZaf0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@hey-api/codegen-core": "0.8.0",
"@hey-api/json-schema-ref-parser": "1.4.1",
"@hey-api/shared": "0.4.1",
"@hey-api/spec-types": "0.2.0",
"@hey-api/types": "0.1.4",
"ansi-colors": "4.1.3",
"color-support": "1.1.3",
"commander": "14.0.3",
"get-tsconfig": "4.14.0"
},
"bin": {
"openapi-ts": "bin/run.js"
},
"engines": {
"node": ">=22.13.0"
},
"funding": {
"url": "https://github.com/sponsors/hey-api"
},
"peerDependencies": {
"typescript": ">=5.5.3 || >=6.0.0 || 6.0.1-rc"
}
},
"node_modules/@hey-api/openapi-ts/node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmmirror.com/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/@hey-api/shared": {
"version": "0.4.1",
"resolved": "https://registry.npmmirror.com/@hey-api/shared/-/shared-0.4.1.tgz",
"integrity": "sha512-EDAjvGpEamn2fOB7W87ZStyDSv5mEpnQCciVbiZ+Ll4EfawwIjHMMtDtRQOYol0YjTkAeEm274GFPAtg3xfPGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@hey-api/codegen-core": "0.8.0",
"@hey-api/json-schema-ref-parser": "1.4.1",
"@hey-api/spec-types": "0.2.0",
"@hey-api/types": "0.1.4",
"ansi-colors": "4.1.3",
"cross-spawn": "7.0.6",
"open": "11.0.0",
"semver": "7.7.4"
},
"engines": {
"node": ">=22.13.0"
},
"funding": {
"url": "https://github.com/sponsors/hey-api"
}
},
"node_modules/@hey-api/shared/node_modules/open": {
"version": "11.0.0",
"resolved": "https://registry.npmmirror.com/open/-/open-11.0.0.tgz",
"integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==",
"dev": true,
"license": "MIT",
"dependencies": {
"default-browser": "^5.4.0",
"define-lazy-prop": "^3.0.0",
"is-in-ssh": "^1.0.0",
"is-inside-container": "^1.0.0",
"powershell-utils": "^0.1.0",
"wsl-utils": "^0.3.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@hey-api/shared/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@hey-api/shared/node_modules/wsl-utils": {
"version": "0.3.1",
"resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.3.1.tgz",
"integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-wsl": "^3.1.0",
"powershell-utils": "^0.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@hey-api/spec-types": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/@hey-api/spec-types/-/spec-types-0.2.0.tgz",
"integrity": "sha512-ibQ8Is7evMavzr8GNyJCcTg975d8DpaMUyLmOrQ85UBdy1l6t1KuRAwgChAbesJsIlNV6gjmlXruWyegDX18Fg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@hey-api/types": "0.1.4"
},
"funding": {
"url": "https://github.com/sponsors/hey-api"
}
},
"node_modules/@hey-api/types": {
"version": "0.1.4",
"resolved": "https://registry.npmmirror.com/@hey-api/types/-/types-0.1.4.tgz",
"integrity": "sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -623,6 +821,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/@popperjs/core": {
"name": "@sxzz/popperjs-es",
"version": "2.11.8",
"resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz",
"integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
@@ -912,6 +1121,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.24",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz",
"integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
"license": "MIT"
},
"node_modules/@types/lodash-es": {
"version": "4.17.12",
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/node": {
"version": "24.12.2",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-24.12.2.tgz",
@@ -922,6 +1146,12 @@
"undici-types": "~7.16.0"
}
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.20",
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": {
"version": "6.0.6",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz",
@@ -1257,6 +1487,42 @@
}
}
},
"node_modules/@vueuse/core": {
"version": "12.0.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-12.0.0.tgz",
"integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
"@vueuse/metadata": "12.0.0",
"@vueuse/shared": "12.0.0",
"vue": "^3.5.13"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/metadata": {
"version": "12.0.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-12.0.0.tgz",
"integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "12.0.0",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-12.0.0.tgz",
"integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==",
"license": "MIT",
"dependencies": {
"vue": "^3.5.13"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz",
@@ -1276,6 +1542,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmmirror.com/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz",
@@ -1299,13 +1575,6 @@
"node": ">=14"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/ast-kit": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/ast-kit/-/ast-kit-2.2.0.tgz",
@@ -1338,6 +1607,12 @@
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.18",
"resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz",
@@ -1410,19 +1685,42 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"node_modules/c12": {
"version": "3.3.4",
"resolved": "https://registry.npmmirror.com/c12/-/c12-3.3.4.tgz",
"integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
"dependencies": {
"chokidar": "^5.0.0",
"confbox": "^0.2.4",
"defu": "^6.1.6",
"dotenv": "^17.3.1",
"exsolve": "^1.0.8",
"giget": "^3.2.0",
"jiti": "^2.6.1",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"perfect-debounce": "^2.1.0",
"pkg-types": "^2.3.0",
"rc9": "^3.0.1"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"peerDependencies": {
"magicast": "*"
},
"peerDependenciesMeta": {
"magicast": {
"optional": true
}
}
},
"node_modules/c12/node_modules/perfect-debounce": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
"integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
"dev": true,
"license": "MIT"
},
"node_modules/caniuse-lite": {
"version": "1.0.30001788",
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
@@ -1459,14 +1757,14 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmmirror.com/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmmirror.com/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
"license": "ISC",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/confbox": {
@@ -1541,6 +1839,12 @@
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/dayjs": {
"version": "1.11.20",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz",
"integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
@@ -1602,6 +1906,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/defu": {
"version": "6.1.7",
"resolved": "https://registry.npmmirror.com/defu/-/defu-6.1.7.tgz",
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
"dev": true,
"license": "MIT"
},
"node_modules/destr": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz",
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
"dev": true,
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1612,6 +1930,19 @@
"node": ">=8"
}
},
"node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.336",
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz",
@@ -1619,6 +1950,32 @@
"dev": true,
"license": "ISC"
},
"node_modules/element-plus": {
"version": "2.13.7",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.13.7.tgz",
"integrity": "sha512-XdHATFZOyzVFL1DaHQ90IOJQSg9UnSAV+bhDW+YB5UoZ0Hxs50mwqjqfwXkuwpSag+VXXizVcErBR6Movo5daw==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^4.2.0",
"@element-plus/icons-vue": "^2.3.2",
"@floating-ui/dom": "^1.0.1",
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
"@types/lodash": "^4.17.20",
"@types/lodash-es": "^4.17.12",
"@vueuse/core": "12.0.0",
"async-validator": "^4.2.5",
"dayjs": "^1.11.19",
"lodash": "^4.17.23",
"lodash-es": "^4.17.23",
"lodash-unified": "^1.0.3",
"memoize-one": "^6.0.0",
"normalize-wheel-es": "^1.2.0",
"vue-component-type-helpers": "^3.2.4"
},
"peerDependencies": {
"vue": "^3.3.0"
}
},
"node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
@@ -1680,21 +2037,6 @@
}
}
},
"node_modules/fs-extra": {
"version": "11.3.4",
"resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.4.tgz",
"integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
@@ -1720,33 +2062,27 @@
"node": ">=6.9.0"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/handlebars": {
"version": "4.7.9",
"resolved": "https://registry.npmmirror.com/handlebars/-/handlebars-4.7.9.tgz",
"integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
"node_modules/get-tsconfig": {
"version": "4.14.0",
"resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
"integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/giget": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/giget/-/giget-3.2.0.tgz",
"integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==",
"dev": true,
"license": "MIT",
"bin": {
"handlebars": "bin/handlebars"
},
"engines": {
"node": ">=0.4.7"
},
"optionalDependencies": {
"uglify-js": "^3.1.4"
"giget": "dist/cli.mjs"
}
},
"node_modules/hookable": {
@@ -1771,6 +2107,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-in-ssh": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
"integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-inside-container": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz",
@@ -1828,6 +2177,16 @@
"node": ">=18"
}
},
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -1835,19 +2194,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
@@ -1882,19 +2228,6 @@
"node": ">=6"
}
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/kolorist": {
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz",
@@ -2180,6 +2513,29 @@
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.18.1",
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"license": "MIT"
},
"node_modules/lodash-unified": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz",
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
"license": "MIT",
"peerDependencies": {
"@types/lodash-es": "*",
"lodash": "*",
"lodash-es": "*"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -2214,6 +2570,12 @@
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/memorystream": {
"version": "0.3.1",
"resolved": "https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz",
@@ -2223,16 +2585,6 @@
"node": ">= 0.10.0"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
@@ -2309,13 +2661,6 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true,
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.37",
"resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.37.tgz",
@@ -2323,6 +2668,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/normalize-wheel-es": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
"license": "BSD-3-Clause"
},
"node_modules/npm-normalize-package-bin": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz",
@@ -2386,23 +2737,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openapi-typescript-codegen": {
"version": "0.29.0",
"resolved": "https://registry.npmmirror.com/openapi-typescript-codegen/-/openapi-typescript-codegen-0.29.0.tgz",
"integrity": "sha512-/wC42PkD0LGjDTEULa/XiWQbv4E9NwLjwLjsaJ/62yOsoYhwvmBR31kPttn1DzQ2OlGe5stACcF/EIkZk43M6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.5.4",
"camelcase": "^6.3.0",
"commander": "^12.0.0",
"fs-extra": "^11.2.0",
"handlebars": "^4.7.8"
},
"bin": {
"openapi": "bin/index.js"
}
},
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -2523,6 +2857,19 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/powershell-utils": {
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/powershell-utils/-/powershell-utils-0.1.0.tgz",
"integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/prettier": {
"version": "3.8.1",
"resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.8.1.tgz",
@@ -2555,6 +2902,17 @@
],
"license": "MIT"
},
"node_modules/rc9": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/rc9/-/rc9-3.0.1.tgz",
"integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"defu": "^6.1.6",
"destr": "^2.0.5"
}
},
"node_modules/read-package-json-fast": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz",
@@ -2582,6 +2940,16 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
@@ -2709,16 +3077,6 @@
"node": ">=18"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2803,20 +3161,6 @@
"integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
"license": "MIT"
},
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmmirror.com/uglify-js/-/uglify-js-3.19.3.tgz",
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"bin": {
"uglifyjs": "bin/uglifyjs"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
@@ -2824,16 +3168,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/unplugin": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-3.0.0.tgz",
@@ -3132,6 +3466,12 @@
}
}
},
"node_modules/vue-component-type-helpers": {
"version": "3.2.7",
"resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.2.7.tgz",
"integrity": "sha512-+gPp5YGmhfsj1IN+xUo7y0fb4clfnOiiUA39y07yW1VzCRjzVgwLbtmdWlghh7mXrPsEaYc7rrIir/HT6C8vYQ==",
"license": "MIT"
},
"node_modules/vue-router": {
"version": "5.0.4",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-5.0.4.tgz",
@@ -3249,13 +3589,6 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
"dev": true,
"license": "MIT"
},
"node_modules/wsl-utils": {
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz",
+4 -2
View File
@@ -10,20 +10,22 @@
"build-only": "vite build",
"type-check": "vue-tsc --build",
"format": "prettier --write --experimental-cli src/",
"openapi": "npx openapi-typescript-codegen --input http://127.0.0.1:8000/openapi.json --output ./src/api/generated/"
"openapi": "npx @hey-api/openapi-ts --input http://127.0.0.1:8000/openapi.json --output ./src/api/generated/"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"element-plus": "^2.13.7",
"pinia": "^3.0.4",
"vue": "^3.5.31",
"vue-router": "^5.0.4"
},
"devDependencies": {
"@hey-api/openapi-ts": "^0.96.1",
"@tsconfig/node24": "^24.0.4",
"@types/node": "^24.12.0",
"@vitejs/plugin-vue": "^6.0.5",
"@vue/tsconfig": "^0.9.1",
"npm-run-all2": "^8.0.4",
"openapi-typescript-codegen": "^0.29.0",
"prettier": "3.8.1",
"typescript": "~6.0.0",
"vite": "^8.0.3",
+7 -102
View File
@@ -1,111 +1,16 @@
<!-- src/App.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
import { DefaultService } from './api/generated'
const result = ref<any>(null)
const loading = ref(true)
const error = ref<string | null>(null)
onMounted(async () => {
try {
const data = await DefaultService.rootGet()
result.value = data
console.log('API 请求成功:', data)
} catch (error: any) {
error.value = error.message || '请求失败'
} finally {
loading.value = false
}
})
import { RouterView } from 'vue-router'
</script>
<template>
<header>
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
<div class="wrapper">
<HelloWorld msg="You did it!" />
<!-- 显示加载状态或错误信息 -->
<div v-if="loading">加载中...</div>
<div v-else-if="error" style="color: red;">{{ error }}</div>
<div v-else>
<p>请求结果: {{ result }}</p>
</div>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</div>
</header>
<RouterView />
</template>
<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
<style>
html, body, #app {
margin: 0;
padding: 0;
height: 100%;
}
</style>
@@ -0,0 +1,16 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type ClientOptions, type Config, createClient, createConfig } from './client';
import type { ClientOptions as ClientOptions2 } from './types.gen';
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: 'http://127.0.0.1:8000' }));
@@ -0,0 +1,298 @@
// This file is auto-generated by @hey-api/openapi-ts
import { createSseClient } from '../core/serverSentEvents.gen';
import type { HttpMethod } from '../core/types.gen';
import { getValidRequestBody } from '../core/utils.gen';
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen';
import {
buildUrl,
createConfig,
createInterceptors,
getParseAs,
mergeConfigs,
mergeHeaders,
setAuthParams,
} from './utils.gen';
type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
body?: any;
headers: ReturnType<typeof mergeHeaders>;
};
export const createClient = (config: Config = {}): Client => {
let _config = mergeConfigs(createConfig(), config);
const getConfig = (): Config => ({ ..._config });
const setConfig = (config: Config): Config => {
_config = mergeConfigs(_config, config);
return getConfig();
};
const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>();
const beforeRequest = async <
TData = unknown,
TResponseStyle extends 'data' | 'fields' = 'fields',
ThrowOnError extends boolean = boolean,
Url extends string = string,
>(
options: RequestOptions<TData, TResponseStyle, ThrowOnError, Url>,
) => {
const opts = {
..._config,
...options,
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
headers: mergeHeaders(_config.headers, options.headers),
serializedBody: undefined as string | undefined,
};
if (opts.security) {
await setAuthParams({
...opts,
security: opts.security,
});
}
if (opts.requestValidator) {
await opts.requestValidator(opts);
}
if (opts.body !== undefined && opts.bodySerializer) {
opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined;
}
// remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.body === undefined || opts.serializedBody === '') {
opts.headers.delete('Content-Type');
}
const resolvedOpts = opts as typeof opts &
ResolvedRequestOptions<TResponseStyle, ThrowOnError, Url>;
const url = buildUrl(resolvedOpts);
return { opts: resolvedOpts, url };
};
const request: Client['request'] = async (options) => {
const { opts, url } = await beforeRequest(options);
const requestInit: ReqInit = {
redirect: 'follow',
...opts,
body: getValidRequestBody(opts),
};
let request = new Request(url, requestInit);
for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts);
}
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!;
let response: Response;
try {
response = await _fetch(request);
} catch (error) {
// Handle fetch exceptions (AbortError, network errors, etc.)
let finalError = error;
for (const fn of interceptors.error.fns) {
if (fn) {
finalError = (await fn(error, undefined as any, request, opts)) as unknown;
}
}
finalError = finalError || ({} as unknown);
if (opts.throwOnError) {
throw finalError;
}
// Return error response
return opts.responseStyle === 'data'
? undefined
: {
error: finalError,
request,
response: undefined as any,
};
}
for (const fn of interceptors.response.fns) {
if (fn) {
response = await fn(response, request, opts);
}
}
const result = {
request,
response,
};
if (response.ok) {
const parseAs =
(opts.parseAs === 'auto'
? getParseAs(response.headers.get('Content-Type'))
: opts.parseAs) ?? 'json';
if (response.status === 204 || response.headers.get('Content-Length') === '0') {
let emptyData: any;
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'text':
emptyData = await response[parseAs]();
break;
case 'formData':
emptyData = new FormData();
break;
case 'stream':
emptyData = response.body;
break;
case 'json':
default:
emptyData = {};
break;
}
return opts.responseStyle === 'data'
? emptyData
: {
data: emptyData,
...result,
};
}
let data: any;
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'formData':
case 'text':
data = await response[parseAs]();
break;
case 'json': {
// Some servers return 200 with no Content-Length and empty body.
// response.json() would throw; read as text and parse if non-empty.
const text = await response.text();
data = text ? JSON.parse(text) : {};
break;
}
case 'stream':
return opts.responseStyle === 'data'
? response.body
: {
data: response.body,
...result,
};
}
if (parseAs === 'json') {
if (opts.responseValidator) {
await opts.responseValidator(data);
}
if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
}
}
return opts.responseStyle === 'data'
? data
: {
data,
...result,
};
}
const textError = await response.text();
let jsonError: unknown;
try {
jsonError = JSON.parse(textError);
} catch {
// noop
}
const error = jsonError ?? textError;
let finalError = error;
for (const fn of interceptors.error.fns) {
if (fn) {
finalError = (await fn(error, response, request, opts)) as string;
}
}
finalError = finalError || ({} as string);
if (opts.throwOnError) {
throw finalError;
}
// TODO: we probably want to return error and improve types
return opts.responseStyle === 'data'
? undefined
: {
error: finalError,
...result,
};
};
const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
request({ ...options, method });
const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
const { opts, url } = await beforeRequest(options);
return createSseClient({
...opts,
body: opts.body as BodyInit | null | undefined,
headers: opts.headers as unknown as Record<string, string>,
method,
onRequest: async (url, init) => {
let request = new Request(url, init);
for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts);
}
}
return request;
},
serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined,
url,
});
};
const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options });
return {
buildUrl: _buildUrl,
connect: makeMethodFn('CONNECT'),
delete: makeMethodFn('DELETE'),
get: makeMethodFn('GET'),
getConfig,
head: makeMethodFn('HEAD'),
interceptors,
options: makeMethodFn('OPTIONS'),
patch: makeMethodFn('PATCH'),
post: makeMethodFn('POST'),
put: makeMethodFn('PUT'),
request,
setConfig,
sse: {
connect: makeSseFn('CONNECT'),
delete: makeSseFn('DELETE'),
get: makeSseFn('GET'),
head: makeSseFn('HEAD'),
options: makeSseFn('OPTIONS'),
patch: makeSseFn('PATCH'),
post: makeSseFn('POST'),
put: makeSseFn('PUT'),
trace: makeSseFn('TRACE'),
},
trace: makeMethodFn('TRACE'),
} as Client;
};
@@ -0,0 +1,25 @@
// This file is auto-generated by @hey-api/openapi-ts
export type { Auth } from '../core/auth.gen';
export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
ClientOptions,
Config,
CreateClientConfig,
Options,
RequestOptions,
RequestResult,
ResolvedRequestOptions,
ResponseStyle,
TDataShape,
} from './types.gen';
export { createConfig, mergeHeaders } from './utils.gen';
@@ -0,0 +1,215 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth } from '../core/auth.gen';
import type {
ServerSentEventsOptions,
ServerSentEventsResult,
} from '../core/serverSentEvents.gen';
import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen';
import type { Middleware } from './utils.gen';
export type ResponseStyle = 'data' | 'fields';
export interface Config<T extends ClientOptions = ClientOptions>
extends Omit<RequestInit, 'body' | 'headers' | 'method'>, CoreConfig {
/**
* Base URL for all requests made by this client.
*/
baseUrl?: T['baseUrl'];
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: typeof fetch;
/**
* Please don't use the Fetch client for Next.js applications. The `next`
* options won't have any effect.
*
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
*/
next?: never;
/**
* Return the response data parsed in a specified format. By default, `auto`
* will infer the appropriate method from the `Content-Type` response header.
* You can override this behavior with any of the {@link Body} methods.
* Select `stream` if you don't want to parse response data at all.
*
* @default 'auto'
*/
parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text';
/**
* Should we return only data or multiple fields (data, error, response, etc.)?
*
* @default 'fields'
*/
responseStyle?: ResponseStyle;
/**
* Throw an error instead of returning it in the response?
*
* @default false
*/
throwOnError?: T['throwOnError'];
}
export interface RequestOptions<
TData = unknown,
TResponseStyle extends ResponseStyle = 'fields',
ThrowOnError extends boolean = boolean,
Url extends string = string,
>
extends
Config<{
responseStyle: TResponseStyle;
throwOnError: ThrowOnError;
}>,
Pick<
ServerSentEventsOptions<TData>,
| 'onRequest'
| 'onSseError'
| 'onSseEvent'
| 'sseDefaultRetryDelay'
| 'sseMaxRetryAttempts'
| 'sseMaxRetryDelay'
> {
/**
* Any body that you want to add to your request.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
*/
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
/**
* Security mechanism(s) to use for the request.
*/
security?: ReadonlyArray<Auth>;
url: Url;
}
export interface ResolvedRequestOptions<
TResponseStyle extends ResponseStyle = 'fields',
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
headers: Headers;
serializedBody?: string;
}
export type RequestResult<
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = 'fields',
> = ThrowOnError extends true
? Promise<
TResponseStyle extends 'data'
? TData extends Record<string, unknown>
? TData[keyof TData]
: TData
: {
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
request: Request;
response: Response;
}
>
: Promise<
TResponseStyle extends 'data'
? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined
: (
| {
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
error: undefined;
}
| {
data: undefined;
error: TError extends Record<string, unknown> ? TError[keyof TError] : TError;
}
) & {
request: Request;
response: Response;
}
>;
export interface ClientOptions {
baseUrl?: string;
responseStyle?: ResponseStyle;
throwOnError?: boolean;
}
type MethodFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type SseFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<never, TResponseStyle, ThrowOnError>, 'method'>,
) => Promise<ServerSentEventsResult<TData, TError>>;
type RequestFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, 'method'>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type BuildUrlFn = <
TData extends {
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
url: string;
},
>(
options: TData & Options<TData>,
) => string;
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & {
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
};
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>;
export interface TDataShape {
body?: unknown;
headers?: unknown;
path?: unknown;
query?: unknown;
url: string;
}
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Options<
TData extends TDataShape = TDataShape,
ThrowOnError extends boolean = boolean,
TResponse = unknown,
TResponseStyle extends ResponseStyle = 'fields',
> = OmitKeys<
RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
'body' | 'path' | 'query' | 'url'
> &
([TData] extends [never] ? unknown : Omit<TData, 'url'>);
@@ -0,0 +1,316 @@
// This file is auto-generated by @hey-api/openapi-ts
import { getAuthToken } from '../core/auth.gen';
import type { QuerySerializerOptions } from '../core/bodySerializer.gen';
import { jsonBodySerializer } from '../core/bodySerializer.gen';
import {
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from '../core/pathSerializer.gen';
import { getUrl } from '../core/utils.gen';
import type { Client, ClientOptions, Config, RequestOptions } from './types.gen';
export const createQuerySerializer = <T = unknown>({
parameters = {},
...args
}: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => {
const search: string[] = [];
if (queryParams && typeof queryParams === 'object') {
for (const name in queryParams) {
const value = queryParams[name];
if (value === undefined || value === null) {
continue;
}
const options = parameters[name] || args;
if (Array.isArray(value)) {
const serializedArray = serializeArrayParam({
allowReserved: options.allowReserved,
explode: true,
name,
style: 'form',
value,
...options.array,
});
if (serializedArray) search.push(serializedArray);
} else if (typeof value === 'object') {
const serializedObject = serializeObjectParam({
allowReserved: options.allowReserved,
explode: true,
name,
style: 'deepObject',
value: value as Record<string, unknown>,
...options.object,
});
if (serializedObject) search.push(serializedObject);
} else {
const serializedPrimitive = serializePrimitiveParam({
allowReserved: options.allowReserved,
name,
value: value as string,
});
if (serializedPrimitive) search.push(serializedPrimitive);
}
}
}
return search.join('&');
};
return querySerializer;
};
/**
* Infers parseAs value from provided Content-Type header.
*/
export const getParseAs = (contentType: string | null): Exclude<Config['parseAs'], 'auto'> => {
if (!contentType) {
// If no Content-Type header is provided, the best we can do is return the raw response body,
// which is effectively the same as the 'stream' option.
return 'stream';
}
const cleanContent = contentType.split(';')[0]?.trim();
if (!cleanContent) {
return;
}
if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) {
return 'json';
}
if (cleanContent === 'multipart/form-data') {
return 'formData';
}
if (
['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type))
) {
return 'blob';
}
if (cleanContent.startsWith('text/')) {
return 'text';
}
return;
};
const checkForExistence = (
options: Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
},
name?: string,
): boolean => {
if (!name) {
return false;
}
if (
options.headers.has(name) ||
options.query?.[name] ||
options.headers.get('Cookie')?.includes(`${name}=`)
) {
return true;
}
return false;
};
export const setAuthParams = async ({
security,
...options
}: Pick<Required<RequestOptions>, 'security'> &
Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
}) => {
for (const auth of security) {
if (checkForExistence(options, auth.name)) {
continue;
}
const token = await getAuthToken(auth, options.auth);
if (!token) {
continue;
}
const name = auth.name ?? 'Authorization';
switch (auth.in) {
case 'query':
if (!options.query) {
options.query = {};
}
options.query[name] = token;
break;
case 'cookie':
options.headers.append('Cookie', `${name}=${token}`);
break;
case 'header':
default:
options.headers.set(name, token);
break;
}
}
};
export const buildUrl: Client['buildUrl'] = (options) =>
getUrl({
baseUrl: options.baseUrl as string,
path: options.path,
query: options.query,
querySerializer:
typeof options.querySerializer === 'function'
? options.querySerializer
: createQuerySerializer(options.querySerializer),
url: options.url,
});
export const mergeConfigs = (a: Config, b: Config): Config => {
const config = { ...a, ...b };
if (config.baseUrl?.endsWith('/')) {
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
}
config.headers = mergeHeaders(a.headers, b.headers);
return config;
};
const headersEntries = (headers: Headers): Array<[string, string]> => {
const entries: Array<[string, string]> = [];
headers.forEach((value, key) => {
entries.push([key, value]);
});
return entries;
};
export const mergeHeaders = (
...headers: Array<Required<Config>['headers'] | undefined>
): Headers => {
const mergedHeaders = new Headers();
for (const header of headers) {
if (!header) {
continue;
}
const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
for (const [key, value] of iterator) {
if (value === null) {
mergedHeaders.delete(key);
} else if (Array.isArray(value)) {
for (const v of value) {
mergedHeaders.append(key, v as string);
}
} else if (value !== undefined) {
// assume object headers are meant to be JSON stringified, i.e., their
// content value in OpenAPI specification is 'application/json'
mergedHeaders.set(
key,
typeof value === 'object' ? JSON.stringify(value) : (value as string),
);
}
}
}
return mergedHeaders;
};
type ErrInterceptor<Err, Res, Req, Options> = (
error: Err,
response: Res,
request: Req,
options: Options,
) => Err | Promise<Err>;
type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>;
type ResInterceptor<Res, Req, Options> = (
response: Res,
request: Req,
options: Options,
) => Res | Promise<Res>;
class Interceptors<Interceptor> {
fns: Array<Interceptor | null> = [];
clear(): void {
this.fns = [];
}
eject(id: number | Interceptor): void {
const index = this.getInterceptorIndex(id);
if (this.fns[index]) {
this.fns[index] = null;
}
}
exists(id: number | Interceptor): boolean {
const index = this.getInterceptorIndex(id);
return Boolean(this.fns[index]);
}
getInterceptorIndex(id: number | Interceptor): number {
if (typeof id === 'number') {
return this.fns[id] ? id : -1;
}
return this.fns.indexOf(id);
}
update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false {
const index = this.getInterceptorIndex(id);
if (this.fns[index]) {
this.fns[index] = fn;
return id;
}
return false;
}
use(fn: Interceptor): number {
this.fns.push(fn);
return this.fns.length - 1;
}
}
export interface Middleware<Req, Res, Err, Options> {
error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>;
request: Interceptors<ReqInterceptor<Req, Options>>;
response: Interceptors<ResInterceptor<Res, Req, Options>>;
}
export const createInterceptors = <Req, Res, Err, Options>(): Middleware<
Req,
Res,
Err,
Options
> => ({
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
request: new Interceptors<ReqInterceptor<Req, Options>>(),
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
});
const defaultQuerySerializer = createQuerySerializer({
allowReserved: false,
array: {
explode: true,
style: 'form',
},
object: {
explode: true,
style: 'deepObject',
},
});
const defaultHeaders = {
'Content-Type': 'application/json',
};
export const createConfig = <T extends ClientOptions = ClientOptions>(
override: Config<Omit<ClientOptions, keyof T> & T> = {},
): Config<Omit<ClientOptions, keyof T> & T> => ({
...jsonBodySerializer,
headers: defaultHeaders,
parseAs: 'auto',
querySerializer: defaultQuerySerializer,
...override,
});
@@ -1,25 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: any;
public readonly request: ApiRequestOptions;
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
super(message);
this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}
@@ -1,17 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ApiRequestOptions = {
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, any>;
readonly cookies?: Record<string, any>;
readonly headers?: Record<string, any>;
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
};
@@ -1,11 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ApiResult = {
readonly url: string;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly body: any;
};
@@ -1,131 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export class CancelError extends Error {
constructor(message: string) {
super(message);
this.name = 'CancelError';
}
public get isCancelled(): boolean {
return true;
}
}
export interface OnCancel {
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;
(cancelHandler: () => void): void;
}
export class CancelablePromise<T> implements Promise<T> {
#isResolved: boolean;
#isRejected: boolean;
#isCancelled: boolean;
readonly #cancelHandlers: (() => void)[];
readonly #promise: Promise<T>;
#resolve?: (value: T | PromiseLike<T>) => void;
#reject?: (reason?: any) => void;
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: OnCancel
) => void
) {
this.#isResolved = false;
this.#isRejected = false;
this.#isCancelled = false;
this.#cancelHandlers = [];
this.#promise = new Promise<T>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isResolved = true;
if (this.#resolve) this.#resolve(value);
};
const onReject = (reason?: any): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isRejected = true;
if (this.#reject) this.#reject(reason);
};
const onCancel = (cancelHandler: () => void): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#cancelHandlers.push(cancelHandler);
};
Object.defineProperty(onCancel, 'isResolved', {
get: (): boolean => this.#isResolved,
});
Object.defineProperty(onCancel, 'isRejected', {
get: (): boolean => this.#isRejected,
});
Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this.#isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
get [Symbol.toStringTag]() {
return "Cancellable Promise";
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
return this.#promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult> {
return this.#promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this.#promise.finally(onFinally);
}
public cancel(): void {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isCancelled = true;
if (this.#cancelHandlers.length) {
try {
for (const cancelHandler of this.#cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn('Cancellation threw an error', error);
return;
}
}
this.#cancelHandlers.length = 0;
if (this.#reject) this.#reject(new CancelError('Request aborted'));
}
public get isCancelled(): boolean {
return this.#isCancelled;
}
}
@@ -1,32 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
type Headers = Record<string, string>;
export type OpenAPIConfig = {
BASE: string;
VERSION: string;
WITH_CREDENTIALS: boolean;
CREDENTIALS: 'include' | 'omit' | 'same-origin';
TOKEN?: string | Resolver<string> | undefined;
USERNAME?: string | Resolver<string> | undefined;
PASSWORD?: string | Resolver<string> | undefined;
HEADERS?: Headers | Resolver<Headers> | undefined;
ENCODE_PATH?: ((path: string) => string) | undefined;
};
export const OpenAPI: OpenAPIConfig = {
BASE: '',
VERSION: 'chongming',
WITH_CREDENTIALS: false,
CREDENTIALS: 'include',
TOKEN: undefined,
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};
@@ -0,0 +1,41 @@
// This file is auto-generated by @hey-api/openapi-ts
export type AuthToken = string | undefined;
export interface Auth {
/**
* Which part of the request do we use to send the auth?
*
* @default 'header'
*/
in?: 'header' | 'query' | 'cookie';
/**
* Header or query parameter name.
*
* @default 'Authorization'
*/
name?: string;
scheme?: 'basic' | 'bearer';
type: 'apiKey' | 'http';
}
export const getAuthToken = async (
auth: Auth,
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
): Promise<string | undefined> => {
const token = typeof callback === 'function' ? await callback(auth) : callback;
if (!token) {
return;
}
if (auth.scheme === 'bearer') {
return `Bearer ${token}`;
}
if (auth.scheme === 'basic') {
return `Basic ${btoa(token)}`;
}
return token;
};
@@ -0,0 +1,82 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen';
export type QuerySerializer = (query: Record<string, unknown>) => string;
export type BodySerializer = (body: unknown) => unknown;
type QuerySerializerOptionsObject = {
allowReserved?: boolean;
array?: Partial<SerializerOptions<ArrayStyle>>;
object?: Partial<SerializerOptions<ObjectStyle>>;
};
export type QuerySerializerOptions = QuerySerializerOptionsObject & {
/**
* Per-parameter serialization overrides. When provided, these settings
* override the global array/object settings for specific parameter names.
*/
parameters?: Record<string, QuerySerializerOptionsObject>;
};
const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => {
if (typeof value === 'string' || value instanceof Blob) {
data.append(key, value);
} else if (value instanceof Date) {
data.append(key, value.toISOString());
} else {
data.append(key, JSON.stringify(value));
}
};
const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => {
if (typeof value === 'string') {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value));
}
};
export const formDataBodySerializer = {
bodySerializer: (body: unknown): FormData => {
const data = new FormData();
Object.entries(body as Record<string, unknown>).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeFormDataPair(data, key, v));
} else {
serializeFormDataPair(data, key, value);
}
});
return data;
},
};
export const jsonBodySerializer = {
bodySerializer: (body: unknown): string =>
JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)),
};
export const urlSearchParamsBodySerializer = {
bodySerializer: (body: unknown): string => {
const data = new URLSearchParams();
Object.entries(body as Record<string, unknown>).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
} else {
serializeUrlSearchParamsPair(data, key, value);
}
});
return data.toString();
},
};
@@ -0,0 +1,169 @@
// This file is auto-generated by @hey-api/openapi-ts
type Slot = 'body' | 'headers' | 'path' | 'query';
export type Field =
| {
in: Exclude<Slot, 'body'>;
/**
* Field name. This is the name we want the user to see and use.
*/
key: string;
/**
* Field mapped name. This is the name we want to use in the request.
* If omitted, we use the same value as `key`.
*/
map?: string;
}
| {
in: Extract<Slot, 'body'>;
/**
* Key isn't required for bodies.
*/
key?: string;
map?: string;
}
| {
/**
* Field name. This is the name we want the user to see and use.
*/
key: string;
/**
* Field mapped name. This is the name we want to use in the request.
* If `in` is omitted, `map` aliases `key` to the transport layer.
*/
map: Slot;
};
export interface Fields {
allowExtra?: Partial<Record<Slot, boolean>>;
args?: ReadonlyArray<Field>;
}
export type FieldsConfig = ReadonlyArray<Field | Fields>;
const extraPrefixesMap: Record<string, Slot> = {
$body_: 'body',
$headers_: 'headers',
$path_: 'path',
$query_: 'query',
};
const extraPrefixes = Object.entries(extraPrefixesMap);
type KeyMap = Map<
string,
| {
in: Slot;
map?: string;
}
| {
in?: never;
map: Slot;
}
>;
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
if (!map) {
map = new Map();
}
for (const config of fields) {
if ('in' in config) {
if (config.key) {
map.set(config.key, {
in: config.in,
map: config.map,
});
}
} else if ('key' in config) {
map.set(config.key, {
map: config.map,
});
} else if (config.args) {
buildKeyMap(config.args, map);
}
}
return map;
};
interface Params {
body: unknown;
headers: Record<string, unknown>;
path: Record<string, unknown>;
query: Record<string, unknown>;
}
const stripEmptySlots = (params: Params) => {
for (const [slot, value] of Object.entries(params)) {
if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) {
delete params[slot as Slot];
}
}
};
export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => {
const params: Params = {
body: {},
headers: {},
path: {},
query: {},
};
const map = buildKeyMap(fields);
let config: FieldsConfig[number] | undefined;
for (const [index, arg] of args.entries()) {
if (fields[index]) {
config = fields[index];
}
if (!config) {
continue;
}
if ('in' in config) {
if (config.key) {
const field = map.get(config.key)!;
const name = field.map || config.key;
if (field.in) {
(params[field.in] as Record<string, unknown>)[name] = arg;
}
} else {
params.body = arg;
}
} else {
for (const [key, value] of Object.entries(arg ?? {})) {
const field = map.get(key);
if (field) {
if (field.in) {
const name = field.map || key;
(params[field.in] as Record<string, unknown>)[name] = value;
} else {
params[field.map] = value;
}
} else {
const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix));
if (extra) {
const [prefix, slot] = extra;
(params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value;
} else if ('allowExtra' in config && config.allowExtra) {
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
if (allowed) {
(params[slot as Slot] as Record<string, unknown>)[key] = value;
break;
}
}
}
}
}
}
}
stripEmptySlots(params);
return params;
};
@@ -0,0 +1,171 @@
// This file is auto-generated by @hey-api/openapi-ts
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
interface SerializePrimitiveOptions {
allowReserved?: boolean;
name: string;
}
export interface SerializerOptions<T> {
/**
* @default true
*/
explode: boolean;
style: T;
}
export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
type MatrixStyle = 'label' | 'matrix' | 'simple';
export type ObjectStyle = 'form' | 'deepObject';
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
value: string;
}
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
default:
return '&';
}
};
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case 'form':
return ',';
case 'pipeDelimited':
return '|';
case 'spaceDelimited':
return '%20';
default:
return ',';
}
};
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
switch (style) {
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
default:
return '&';
}
};
export const serializeArrayParam = ({
allowReserved,
explode,
name,
style,
value,
}: SerializeOptions<ArraySeparatorStyle> & {
value: unknown[];
}) => {
if (!explode) {
const joinedValues = (
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
).join(separatorArrayNoExplode(style));
switch (style) {
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
case 'simple':
return joinedValues;
default:
return `${name}=${joinedValues}`;
}
}
const separator = separatorArrayExplode(style);
const joinedValues = value
.map((v) => {
if (style === 'label' || style === 'simple') {
return allowReserved ? v : encodeURIComponent(v as string);
}
return serializePrimitiveParam({
allowReserved,
name,
value: v as string,
});
})
.join(separator);
return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues;
};
export const serializePrimitiveParam = ({
allowReserved,
name,
value,
}: SerializePrimitiveParam) => {
if (value === undefined || value === null) {
return '';
}
if (typeof value === 'object') {
throw new Error(
'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.',
);
}
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
};
export const serializeObjectParam = ({
allowReserved,
explode,
name,
style,
value,
valueOnly,
}: SerializeOptions<ObjectSeparatorStyle> & {
value: Record<string, unknown> | Date;
valueOnly?: boolean;
}) => {
if (value instanceof Date) {
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
}
if (style !== 'deepObject' && !explode) {
let values: string[] = [];
Object.entries(value).forEach(([key, v]) => {
values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)];
});
const joinedValues = values.join(',');
switch (style) {
case 'form':
return `${name}=${joinedValues}`;
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
default:
return joinedValues;
}
}
const separator = separatorObjectExplode(style);
const joinedValues = Object.entries(value)
.map(([key, v]) =>
serializePrimitiveParam({
allowReserved,
name: style === 'deepObject' ? `${name}[${key}]` : key,
value: v as string,
}),
)
.join(separator);
return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues;
};
@@ -0,0 +1,117 @@
// This file is auto-generated by @hey-api/openapi-ts
/**
* JSON-friendly union that mirrors what Pinia Colada can hash.
*/
export type JsonValue =
| null
| string
| number
| boolean
| JsonValue[]
| { [key: string]: JsonValue };
/**
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
*/
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
return undefined;
}
if (typeof value === 'bigint') {
return value.toString();
}
if (value instanceof Date) {
return value.toISOString();
}
return value;
};
/**
* Safely stringifies a value and parses it back into a JsonValue.
*/
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
try {
const json = JSON.stringify(input, queryKeyJsonReplacer);
if (json === undefined) {
return undefined;
}
return JSON.parse(json) as JsonValue;
} catch {
return undefined;
}
};
/**
* Detects plain objects (including objects with a null prototype).
*/
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
if (value === null || typeof value !== 'object') {
return false;
}
const prototype = Object.getPrototypeOf(value as object);
return prototype === Object.prototype || prototype === null;
};
/**
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
*/
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b));
const result: Record<string, JsonValue> = {};
for (const [key, value] of entries) {
const existing = result[key];
if (existing === undefined) {
result[key] = value;
continue;
}
if (Array.isArray(existing)) {
(existing as string[]).push(value);
} else {
result[key] = [existing, value];
}
}
return result;
};
/**
* Normalizes any accepted value into a JSON-friendly shape for query keys.
*/
export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => {
if (value === null) {
return null;
}
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value;
}
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
return undefined;
}
if (typeof value === 'bigint') {
return value.toString();
}
if (value instanceof Date) {
return value.toISOString();
}
if (Array.isArray(value)) {
return stringifyToJsonValue(value);
}
if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) {
return serializeSearchParams(value);
}
if (isPlainObject(value)) {
return stringifyToJsonValue(value);
}
return undefined;
};
@@ -1,322 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import { CancelablePromise } from './CancelablePromise';
import type { OnCancel } from './CancelablePromise';
import type { OpenAPIConfig } from './OpenAPI';
export const isDefined = <T>(value: T | null | undefined): value is Exclude<T, null | undefined> => {
return value !== undefined && value !== null;
};
export const isString = (value: any): value is string => {
return typeof value === 'string';
};
export const isStringWithValue = (value: any): value is string => {
return isString(value) && value !== '';
};
export const isBlob = (value: any): value is Blob => {
return (
typeof value === 'object' &&
typeof value.type === 'string' &&
typeof value.stream === 'function' &&
typeof value.arrayBuffer === 'function' &&
typeof value.constructor === 'function' &&
typeof value.constructor.name === 'string' &&
/^(Blob|File)$/.test(value.constructor.name) &&
/^(Blob|File)$/.test(value[Symbol.toStringTag])
);
};
export const isFormData = (value: any): value is FormData => {
return value instanceof FormData;
};
export const base64 = (str: string): string => {
try {
return btoa(str);
} catch (err) {
// @ts-ignore
return Buffer.from(str).toString('base64');
}
};
export const getQueryString = (params: Record<string, any>): string => {
const qs: string[] = [];
const append = (key: string, value: any) => {
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
};
const process = (key: string, value: any) => {
if (isDefined(value)) {
if (Array.isArray(value)) {
value.forEach(v => {
process(key, v);
});
} else if (typeof value === 'object') {
Object.entries(value).forEach(([k, v]) => {
process(`${key}[${k}]`, v);
});
} else {
append(key, value);
}
}
};
Object.entries(params).forEach(([key, value]) => {
process(key, value);
});
if (qs.length > 0) {
return `?${qs.join('&')}`;
}
return '';
};
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
const encoder = config.ENCODE_PATH || encodeURI;
const path = options.url
.replace('{api-version}', config.VERSION)
.replace(/{(.*?)}/g, (substring: string, group: string) => {
if (options.path?.hasOwnProperty(group)) {
return encoder(String(options.path[group]));
}
return substring;
});
const url = `${config.BASE}${path}`;
if (options.query) {
return `${url}${getQueryString(options.query)}`;
}
return url;
};
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
if (options.formData) {
const formData = new FormData();
const process = (key: string, value: any) => {
if (isString(value) || isBlob(value)) {
formData.append(key, value);
} else {
formData.append(key, JSON.stringify(value));
}
};
Object.entries(options.formData)
.filter(([_, value]) => isDefined(value))
.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => process(key, v));
} else {
process(key, value);
}
});
return formData;
}
return undefined;
};
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
export const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Resolver<T>): Promise<T | undefined> => {
if (typeof resolver === 'function') {
return (resolver as Resolver<T>)(options);
}
return resolver;
};
export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise<Headers> => {
const [token, username, password, additionalHeaders] = await Promise.all([
resolve(options, config.TOKEN),
resolve(options, config.USERNAME),
resolve(options, config.PASSWORD),
resolve(options, config.HEADERS),
]);
const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
})
.filter(([_, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
if (isStringWithValue(token)) {
headers['Authorization'] = `Bearer ${token}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
}
if (options.body !== undefined) {
if (options.mediaType) {
headers['Content-Type'] = options.mediaType;
} else if (isBlob(options.body)) {
headers['Content-Type'] = options.body.type || 'application/octet-stream';
} else if (isString(options.body)) {
headers['Content-Type'] = 'text/plain';
} else if (!isFormData(options.body)) {
headers['Content-Type'] = 'application/json';
}
}
return new Headers(headers);
};
export const getRequestBody = (options: ApiRequestOptions): any => {
if (options.body !== undefined) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
} else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) {
return options.body;
} else {
return JSON.stringify(options.body);
}
}
return undefined;
};
export const sendRequest = async (
config: OpenAPIConfig,
options: ApiRequestOptions,
url: string,
body: any,
formData: FormData | undefined,
headers: Headers,
onCancel: OnCancel
): Promise<Response> => {
const controller = new AbortController();
const request: RequestInit = {
headers,
body: body ?? formData,
method: options.method,
signal: controller.signal,
};
if (config.WITH_CREDENTIALS) {
request.credentials = config.CREDENTIALS;
}
onCancel(() => controller.abort());
return await fetch(url, request);
};
export const getResponseHeader = (response: Response, responseHeader?: string): string | undefined => {
if (responseHeader) {
const content = response.headers.get(responseHeader);
if (isString(content)) {
return content;
}
}
return undefined;
};
export const getResponseBody = async (response: Response): Promise<any> => {
if (response.status !== 204) {
try {
const contentType = response.headers.get('Content-Type');
if (contentType) {
const jsonTypes = ['application/json', 'application/problem+json']
const isJSON = jsonTypes.some(type => contentType.toLowerCase().startsWith(type));
if (isJSON) {
return await response.json();
} else {
return await response.text();
}
}
} catch (error) {
console.error(error);
}
}
return undefined;
};
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
const errors: Record<number, string> = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable',
...options.errors,
}
const error = errors[result.status];
if (error) {
throw new ApiError(options, result, error);
}
if (!result.ok) {
const errorStatus = result.status ?? 'unknown';
const errorStatusText = result.statusText ?? 'unknown';
const errorBody = (() => {
try {
return JSON.stringify(result.body, null, 2);
} catch (e) {
return undefined;
}
})();
throw new ApiError(options, result,
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
);
}
};
/**
* Request method
* @param config The OpenAPI configuration object
* @param options The request options from the service
* @returns CancelablePromise<T>
* @throws ApiError
*/
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(config, options);
if (!onCancel.isCancelled) {
const response = await sendRequest(config, options, url, body, formData, headers, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader ?? responseBody,
};
catchErrorCodes(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}
});
};
@@ -0,0 +1,242 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Config } from './types.gen';
export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, 'method'> &
Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: typeof fetch;
/**
* Implementing clients can call request interceptors inside this hook.
*/
onRequest?: (url: string, init: RequestInit) => Promise<Request>;
/**
* Callback invoked when a network or parsing error occurs during streaming.
*
* This option applies only if the endpoint returns a stream of events.
*
* @param error The error that occurred.
*/
onSseError?: (error: unknown) => void;
/**
* Callback invoked when an event is streamed from the server.
*
* This option applies only if the endpoint returns a stream of events.
*
* @param event Event streamed from the server.
* @returns Nothing (void).
*/
onSseEvent?: (event: StreamEvent<TData>) => void;
serializedBody?: RequestInit['body'];
/**
* Default retry delay in milliseconds.
*
* This option applies only if the endpoint returns a stream of events.
*
* @default 3000
*/
sseDefaultRetryDelay?: number;
/**
* Maximum number of retry attempts before giving up.
*/
sseMaxRetryAttempts?: number;
/**
* Maximum retry delay in milliseconds.
*
* Applies only when exponential backoff is used.
*
* This option applies only if the endpoint returns a stream of events.
*
* @default 30000
*/
sseMaxRetryDelay?: number;
/**
* Optional sleep function for retry backoff.
*
* Defaults to using `setTimeout`.
*/
sseSleepFn?: (ms: number) => Promise<void>;
url: string;
};
export interface StreamEvent<TData = unknown> {
data: TData;
event?: string;
id?: string;
retry?: number;
}
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
stream: AsyncGenerator<
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
TReturn,
TNext
>;
};
export function createSseClient<TData = unknown>({
onRequest,
onSseError,
onSseEvent,
responseTransformer,
responseValidator,
sseDefaultRetryDelay,
sseMaxRetryAttempts,
sseMaxRetryDelay,
sseSleepFn,
url,
...options
}: ServerSentEventsOptions): ServerSentEventsResult<TData> {
let lastEventId: string | undefined;
const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
const createStream = async function* () {
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
let attempt = 0;
const signal = options.signal ?? new AbortController().signal;
while (true) {
if (signal.aborted) break;
attempt++;
const headers =
options.headers instanceof Headers
? options.headers
: new Headers(options.headers as Record<string, string> | undefined);
if (lastEventId !== undefined) {
headers.set('Last-Event-ID', lastEventId);
}
try {
const requestInit: RequestInit = {
redirect: 'follow',
...options,
body: options.serializedBody,
headers,
signal,
};
let request = new Request(url, requestInit);
if (onRequest) {
request = await onRequest(url, requestInit);
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = options.fetch ?? globalThis.fetch;
const response = await _fetch(request);
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
if (!response.body) throw new Error('No body in SSE response');
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
const abortHandler = () => {
try {
reader.cancel();
} catch {
// noop
}
};
signal.addEventListener('abort', abortHandler);
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += value;
buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings
const chunks = buffer.split('\n\n');
buffer = chunks.pop() ?? '';
for (const chunk of chunks) {
const lines = chunk.split('\n');
const dataLines: Array<string> = [];
let eventName: string | undefined;
for (const line of lines) {
if (line.startsWith('data:')) {
dataLines.push(line.replace(/^data:\s*/, ''));
} else if (line.startsWith('event:')) {
eventName = line.replace(/^event:\s*/, '');
} else if (line.startsWith('id:')) {
lastEventId = line.replace(/^id:\s*/, '');
} else if (line.startsWith('retry:')) {
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10);
if (!Number.isNaN(parsed)) {
retryDelay = parsed;
}
}
}
let data: unknown;
let parsedJson = false;
if (dataLines.length) {
const rawData = dataLines.join('\n');
try {
data = JSON.parse(rawData);
parsedJson = true;
} catch {
data = rawData;
}
}
if (parsedJson) {
if (responseValidator) {
await responseValidator(data);
}
if (responseTransformer) {
data = await responseTransformer(data);
}
}
onSseEvent?.({
data,
event: eventName,
id: lastEventId,
retry: retryDelay,
});
if (dataLines.length) {
yield data as any;
}
}
}
} finally {
signal.removeEventListener('abort', abortHandler);
reader.releaseLock();
}
break; // exit loop on normal completion
} catch (error) {
// connection failed or aborted; retry after delay
onSseError?.(error);
if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) {
break; // stop after firing error
}
// exponential backoff: double retry each attempt, cap at 30s
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000);
await sleep(backoff);
}
}
};
const stream = createStream();
return { stream };
}
@@ -0,0 +1,104 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth, AuthToken } from './auth.gen';
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen';
export type HttpMethod =
| 'connect'
| 'delete'
| 'get'
| 'head'
| 'options'
| 'patch'
| 'post'
| 'put'
| 'trace';
export type Client<
RequestFn = never,
Config = unknown,
MethodFn = never,
BuildUrlFn = never,
SseFn = never,
> = {
/**
* Returns the final request URL.
*/
buildUrl: BuildUrlFn;
getConfig: () => Config;
request: RequestFn;
setConfig: (config: Config) => Config;
} & {
[K in HttpMethod]: MethodFn;
} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } });
export interface Config {
/**
* Auth token or a function returning auth token. The resolved value will be
* added to the request payload as defined by its `security` array.
*/
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
/**
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
bodySerializer?: BodySerializer | null;
/**
* An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with.
*
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
*/
headers?:
| RequestInit['headers']
| Record<
string,
string | number | boolean | (string | number | boolean)[] | null | undefined | unknown
>;
/**
* The request method.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
*/
method?: Uppercase<HttpMethod>;
/**
* A function for serializing request query parameters. By default, arrays
* will be exploded in form style, objects will be exploded in deepObject
* style, and reserved characters are percent-encoded.
*
* This method will have no effect if the native `paramsSerializer()` Axios
* API function is used.
*
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
*/
querySerializer?: QuerySerializer | QuerySerializerOptions;
/**
* A function validating request data. This is useful if you want to ensure
* the request conforms to the desired shape, so it can be safely sent to
* the server.
*/
requestValidator?: (data: unknown) => Promise<unknown>;
/**
* A function transforming response data before it's returned. This is useful
* for post-processing data, e.g., converting ISO strings into Date objects.
*/
responseTransformer?: (data: unknown) => Promise<unknown>;
/**
* A function validating response data. This is useful if you want to ensure
* the response conforms to the desired shape, so it can be safely passed to
* the transformers and returned to the user.
*/
responseValidator?: (data: unknown) => Promise<unknown>;
}
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
? true
: [T] extends [never | undefined]
? [undefined] extends [T]
? false
: true
: false;
export type OmitNever<T extends Record<string, unknown>> = {
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K];
};
@@ -0,0 +1,140 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { BodySerializer, QuerySerializer } from './bodySerializer.gen';
import {
type ArraySeparatorStyle,
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from './pathSerializer.gen';
export interface PathSerializer {
path: Record<string, unknown>;
url: string;
}
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url;
const matches = _url.match(PATH_PARAM_RE);
if (matches) {
for (const match of matches) {
let explode = false;
let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = 'simple';
if (name.endsWith('*')) {
explode = true;
name = name.substring(0, name.length - 1);
}
if (name.startsWith('.')) {
name = name.substring(1);
style = 'label';
} else if (name.startsWith(';')) {
name = name.substring(1);
style = 'matrix';
}
const value = path[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
url = url.replace(match, serializeArrayParam({ explode, name, style, value }));
continue;
}
if (typeof value === 'object') {
url = url.replace(
match,
serializeObjectParam({
explode,
name,
style,
value: value as Record<string, unknown>,
valueOnly: true,
}),
);
continue;
}
if (style === 'matrix') {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
);
continue;
}
const replaceValue = encodeURIComponent(
style === 'label' ? `.${value as string}` : (value as string),
);
url = url.replace(match, replaceValue);
}
}
return url;
};
export const getUrl = ({
baseUrl,
path,
query,
querySerializer,
url: _url,
}: {
baseUrl?: string;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
querySerializer: QuerySerializer;
url: string;
}) => {
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
let url = (baseUrl ?? '') + pathUrl;
if (path) {
url = defaultPathSerializer({ path, url });
}
let search = query ? querySerializer(query) : '';
if (search.startsWith('?')) {
search = search.substring(1);
}
if (search) {
url += `?${search}`;
}
return url;
};
export function getValidRequestBody(options: {
body?: unknown;
bodySerializer?: BodySerializer | null;
serializedBody?: unknown;
}) {
const hasBody = options.body !== undefined;
const isSerializedBody = hasBody && options.bodySerializer;
if (isSerializedBody) {
if ('serializedBody' in options) {
const hasSerializedBody =
options.serializedBody !== undefined && options.serializedBody !== '';
return hasSerializedBody ? options.serializedBody : null;
}
// not all clients implement a serializedBody property (i.e., client-axios)
return options.body !== '' ? options.body : null;
}
// plain/text body
if (hasBody) {
return options.body;
}
// no body was provided
return undefined;
}
+3 -23
View File
@@ -1,24 +1,4 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export { ApiError } from './core/ApiError';
export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI } from './core/OpenAPI';
export type { OpenAPIConfig } from './core/OpenAPI';
// This file is auto-generated by @hey-api/openapi-ts
export type { HTTPValidationError } from './models/HTTPValidationError';
export type { RefreshTokenResponse } from './models/RefreshTokenResponse';
export type { TaskRequest } from './models/TaskRequest';
export type { TodoCreate } from './models/TodoCreate';
export type { TodoRead } from './models/TodoRead';
export type { TodoUpdate } from './models/TodoUpdate';
export type { TokenResponse } from './models/TokenResponse';
export type { UserCreate } from './models/UserCreate';
export type { UserLogin } from './models/UserLogin';
export type { UserRead } from './models/UserRead';
export type { ValidationError } from './models/ValidationError';
export { AuthenticationService } from './services/AuthenticationService';
export { DefaultService } from './services/DefaultService';
export { TodoService } from './services/TodoService';
export { createTodoApiV1TodosPost, deleteTodoApiV1TodosTodoIdDelete, getCurrentUserInfoApiV1AuthMeGet, getSessionsApiV1AuthSessionsGet, loginApiV1AuthLoginPost, logoutAllApiV1AuthLogoutAllPost, logoutApiV1AuthLogoutPost, type Options, readTodoApiV1TodosTodoIdGet, readTodosApiV1TodosGet, refreshTokenApiV1AuthRefreshPost, registerApiV1AuthRegisterPost, rootGet, updateTodoApiV1TodosTodoIdPatch, uploadImageApiV1UploadImagePost } from './sdk.gen';
export type { BodyUploadImageApiV1UploadImagePost, ClientOptions, CreateTodoApiV1TodosPostData, CreateTodoApiV1TodosPostError, CreateTodoApiV1TodosPostErrors, CreateTodoApiV1TodosPostResponse, CreateTodoApiV1TodosPostResponses, DeleteTodoApiV1TodosTodoIdDeleteData, DeleteTodoApiV1TodosTodoIdDeleteError, DeleteTodoApiV1TodosTodoIdDeleteErrors, DeleteTodoApiV1TodosTodoIdDeleteResponses, Detail, GetCurrentUserInfoApiV1AuthMeGetData, GetCurrentUserInfoApiV1AuthMeGetError, GetCurrentUserInfoApiV1AuthMeGetErrors, GetCurrentUserInfoApiV1AuthMeGetResponse, GetCurrentUserInfoApiV1AuthMeGetResponses, GetSessionsApiV1AuthSessionsGetData, GetSessionsApiV1AuthSessionsGetResponses, HttpError, HttpValidationError, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostError, LoginApiV1AuthLoginPostErrors, LoginApiV1AuthLoginPostResponse, LoginApiV1AuthLoginPostResponses, LogoutAllApiV1AuthLogoutAllPostData, LogoutAllApiV1AuthLogoutAllPostResponse, LogoutAllApiV1AuthLogoutAllPostResponses, LogoutApiV1AuthLogoutPostData, LogoutApiV1AuthLogoutPostResponse, LogoutApiV1AuthLogoutPostResponses, ReadTodoApiV1TodosTodoIdGetData, ReadTodoApiV1TodosTodoIdGetError, ReadTodoApiV1TodosTodoIdGetErrors, ReadTodoApiV1TodosTodoIdGetResponse, ReadTodoApiV1TodosTodoIdGetResponses, ReadTodosApiV1TodosGetData, ReadTodosApiV1TodosGetError, ReadTodosApiV1TodosGetErrors, ReadTodosApiV1TodosGetResponse, ReadTodosApiV1TodosGetResponses, RefreshTokenApiV1AuthRefreshPostData, RefreshTokenApiV1AuthRefreshPostError, RefreshTokenApiV1AuthRefreshPostErrors, RefreshTokenApiV1AuthRefreshPostResponse, RefreshTokenApiV1AuthRefreshPostResponses, RefreshTokenResponse, RegisterApiV1AuthRegisterPostData, RegisterApiV1AuthRegisterPostError, RegisterApiV1AuthRegisterPostErrors, RegisterApiV1AuthRegisterPostResponse, RegisterApiV1AuthRegisterPostResponses, RootGetData, RootGetResponses, TodoCreate, TodoRead, TodoUpdate, TokenResponse, UpdateTodoApiV1TodosTodoIdPatchData, UpdateTodoApiV1TodosTodoIdPatchError, UpdateTodoApiV1TodosTodoIdPatchErrors, UpdateTodoApiV1TodosTodoIdPatchResponse, UpdateTodoApiV1TodosTodoIdPatchResponses, UploadImageApiV1UploadImagePostData, UploadImageApiV1UploadImagePostError, UploadImageApiV1UploadImagePostErrors, UploadImageApiV1UploadImagePostResponse, UploadImageApiV1UploadImagePostResponses, UploadResponse, UserCreate, UserLogin, Userlogout, UserRead, ValidationError } from './types.gen';
@@ -1,9 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ValidationError } from './ValidationError';
export type HTTPValidationError = {
detail?: Array<ValidationError>;
};
@@ -1,12 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* 刷新 Token 响应模型
*/
export type RefreshTokenResponse = {
access_token: string;
token_type?: string;
};
@@ -1,9 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type TaskRequest = {
task_name: string;
interval: number;
};
@@ -1,10 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type TodoCreate = {
title: string;
description?: (string | null);
completed?: boolean;
};
@@ -1,11 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type TodoRead = {
title: string;
description?: (string | null);
completed?: boolean;
id: number;
};
@@ -1,10 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type TodoUpdate = {
title?: (string | null);
description?: (string | null);
completed?: (boolean | null);
};
@@ -1,15 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* Token 响应模型
*/
export type TokenResponse = {
access_token: string;
refresh_token: string;
token_type?: string;
expires_in: number;
refresh_expires_in: number;
};
@@ -1,17 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* 用户创建模型
*/
export type UserCreate = {
email: string;
username?: (string | null);
full_name?: (string | null);
is_active?: boolean;
is_superuser?: boolean;
created_at?: (string | null);
password: string;
};
@@ -1,9 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UserLogin = {
email: string;
password: string;
};
@@ -1,17 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* 用户读取模型
*/
export type UserRead = {
email: string;
username?: (string | null);
full_name?: (string | null);
is_active?: boolean;
is_superuser?: boolean;
created_at?: (string | null);
id: number;
};
@@ -1,12 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ValidationError = {
loc: Array<(string | number)>;
msg: string;
type: string;
input?: any;
ctx?: Record<string, any>;
};
@@ -0,0 +1,166 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client';
import { client } from './client.gen';
import type { CreateTodoApiV1TodosPostData, CreateTodoApiV1TodosPostErrors, CreateTodoApiV1TodosPostResponses, DeleteTodoApiV1TodosTodoIdDeleteData, DeleteTodoApiV1TodosTodoIdDeleteErrors, DeleteTodoApiV1TodosTodoIdDeleteResponses, GetCurrentUserInfoApiV1AuthMeGetData, GetCurrentUserInfoApiV1AuthMeGetErrors, GetCurrentUserInfoApiV1AuthMeGetResponses, GetSessionsApiV1AuthSessionsGetData, GetSessionsApiV1AuthSessionsGetResponses, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostErrors, LoginApiV1AuthLoginPostResponses, LogoutAllApiV1AuthLogoutAllPostData, LogoutAllApiV1AuthLogoutAllPostResponses, LogoutApiV1AuthLogoutPostData, LogoutApiV1AuthLogoutPostResponses, ReadTodoApiV1TodosTodoIdGetData, ReadTodoApiV1TodosTodoIdGetErrors, ReadTodoApiV1TodosTodoIdGetResponses, ReadTodosApiV1TodosGetData, ReadTodosApiV1TodosGetErrors, ReadTodosApiV1TodosGetResponses, RefreshTokenApiV1AuthRefreshPostData, RefreshTokenApiV1AuthRefreshPostErrors, RefreshTokenApiV1AuthRefreshPostResponses, RegisterApiV1AuthRegisterPostData, RegisterApiV1AuthRegisterPostErrors, RegisterApiV1AuthRegisterPostResponses, RootGetData, RootGetResponses, UpdateTodoApiV1TodosTodoIdPatchData, UpdateTodoApiV1TodosTodoIdPatchErrors, UpdateTodoApiV1TodosTodoIdPatchResponses, UploadImageApiV1UploadImagePostData, UploadImageApiV1UploadImagePostErrors, UploadImageApiV1UploadImagePostResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & {
/**
* You can provide a client instance returned by `createClient()` instead of
* individual options. This might be also useful if you want to implement a
* custom client.
*/
client?: Client;
/**
* You can pass arbitrary values through the `meta` object. This can be
* used to access values that aren't defined as part of the SDK function.
*/
meta?: Record<string, unknown>;
};
/**
* Read Todos
*/
export const readTodosApiV1TodosGet = <ThrowOnError extends boolean = false>(options?: Options<ReadTodosApiV1TodosGetData, ThrowOnError>) => (options?.client ?? client).get<ReadTodosApiV1TodosGetResponses, ReadTodosApiV1TodosGetErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/v1/todos/',
...options
});
/**
* Create Todo
*/
export const createTodoApiV1TodosPost = <ThrowOnError extends boolean = false>(options: Options<CreateTodoApiV1TodosPostData, ThrowOnError>) => (options.client ?? client).post<CreateTodoApiV1TodosPostResponses, CreateTodoApiV1TodosPostErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/v1/todos/',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Delete Todo
*/
export const deleteTodoApiV1TodosTodoIdDelete = <ThrowOnError extends boolean = false>(options: Options<DeleteTodoApiV1TodosTodoIdDeleteData, ThrowOnError>) => (options.client ?? client).delete<DeleteTodoApiV1TodosTodoIdDeleteResponses, DeleteTodoApiV1TodosTodoIdDeleteErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/v1/todos/{todo_id}',
...options
});
/**
* Read Todo
*/
export const readTodoApiV1TodosTodoIdGet = <ThrowOnError extends boolean = false>(options: Options<ReadTodoApiV1TodosTodoIdGetData, ThrowOnError>) => (options.client ?? client).get<ReadTodoApiV1TodosTodoIdGetResponses, ReadTodoApiV1TodosTodoIdGetErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/v1/todos/{todo_id}',
...options
});
/**
* Update Todo
*/
export const updateTodoApiV1TodosTodoIdPatch = <ThrowOnError extends boolean = false>(options: Options<UpdateTodoApiV1TodosTodoIdPatchData, ThrowOnError>) => (options.client ?? client).patch<UpdateTodoApiV1TodosTodoIdPatchResponses, UpdateTodoApiV1TodosTodoIdPatchErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/v1/todos/{todo_id}',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Register
*/
export const registerApiV1AuthRegisterPost = <ThrowOnError extends boolean = false>(options: Options<RegisterApiV1AuthRegisterPostData, ThrowOnError>) => (options.client ?? client).post<RegisterApiV1AuthRegisterPostResponses, RegisterApiV1AuthRegisterPostErrors, ThrowOnError>({
url: '/api/v1/auth/register',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Login
*/
export const loginApiV1AuthLoginPost = <ThrowOnError extends boolean = false>(options: Options<LoginApiV1AuthLoginPostData, ThrowOnError>) => (options.client ?? client).post<LoginApiV1AuthLoginPostResponses, LoginApiV1AuthLoginPostErrors, ThrowOnError>({
url: '/api/v1/auth/login',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* 刷新访问令牌
*/
export const refreshTokenApiV1AuthRefreshPost = <ThrowOnError extends boolean = false>(options?: Options<RefreshTokenApiV1AuthRefreshPostData, ThrowOnError>) => (options?.client ?? client).post<RefreshTokenApiV1AuthRefreshPostResponses, RefreshTokenApiV1AuthRefreshPostErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/v1/auth/refresh',
...options
});
/**
* Logout
*/
export const logoutApiV1AuthLogoutPost = <ThrowOnError extends boolean = false>(options?: Options<LogoutApiV1AuthLogoutPostData, ThrowOnError>) => (options?.client ?? client).post<LogoutApiV1AuthLogoutPostResponses, unknown, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/v1/auth/logout',
...options
});
/**
* 登出所有设备
*/
export const logoutAllApiV1AuthLogoutAllPost = <ThrowOnError extends boolean = false>(options?: Options<LogoutAllApiV1AuthLogoutAllPostData, ThrowOnError>) => (options?.client ?? client).post<LogoutAllApiV1AuthLogoutAllPostResponses, unknown, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/v1/auth/logout-all',
...options
});
/**
* 获取用户会话
*
* 获取用户的所有活跃会话
*/
export const getSessionsApiV1AuthSessionsGet = <ThrowOnError extends boolean = false>(options?: Options<GetSessionsApiV1AuthSessionsGetData, ThrowOnError>) => (options?.client ?? client).get<GetSessionsApiV1AuthSessionsGetResponses, unknown, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/v1/auth/sessions',
...options
});
/**
* 获取当前用户信息
*
* 获取当前登录用户的信息
*/
export const getCurrentUserInfoApiV1AuthMeGet = <ThrowOnError extends boolean = false>(options?: Options<GetCurrentUserInfoApiV1AuthMeGetData, ThrowOnError>) => (options?.client ?? client).get<GetCurrentUserInfoApiV1AuthMeGetResponses, GetCurrentUserInfoApiV1AuthMeGetErrors, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/v1/auth/me',
...options
});
/**
* Upload Image
*/
export const uploadImageApiV1UploadImagePost = <ThrowOnError extends boolean = false>(options: Options<UploadImageApiV1UploadImagePostData, ThrowOnError>) => (options.client ?? client).post<UploadImageApiV1UploadImagePostResponses, UploadImageApiV1UploadImagePostErrors, ThrowOnError>({
...formDataBodySerializer,
security: [{ scheme: 'bearer', type: 'http' }],
url: '/api/v1/upload/image',
...options,
headers: {
'Content-Type': null,
...options.headers
}
});
/**
* 根路径
*
* 根路径,返回应用信息
*/
export const rootGet = <ThrowOnError extends boolean = false>(options?: Options<RootGetData, ThrowOnError>) => (options?.client ?? client).get<RootGetResponses, unknown, ThrowOnError>({ url: '/', ...options });
@@ -1,109 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { RefreshTokenResponse } from '../models/RefreshTokenResponse';
import type { TokenResponse } from '../models/TokenResponse';
import type { UserCreate } from '../models/UserCreate';
import type { UserLogin } from '../models/UserLogin';
import type { UserRead } from '../models/UserRead';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class AuthenticationService {
/**
* Register
* @param requestBody
* @returns UserRead Successful Response
* @throws ApiError
*/
public static registerApiV1AuthRegisterPost(
requestBody: UserCreate,
): CancelablePromise<UserRead> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/auth/register',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Login
* @param requestBody
* @returns TokenResponse Successful Response
* @throws ApiError
*/
public static loginApiV1AuthLoginPost(
requestBody: UserLogin,
): CancelablePromise<TokenResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/auth/login',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* 刷新访问令牌
* @returns RefreshTokenResponse Successful Response
* @throws ApiError
*/
public static refreshTokenApiV1AuthRefreshPost(): CancelablePromise<RefreshTokenResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/auth/refresh',
});
}
/**
* Logout
* @returns any Successful Response
* @throws ApiError
*/
public static logoutApiV1AuthLogoutPost(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/auth/logout',
});
}
/**
* 登出所有设备
* @returns any Successful Response
* @throws ApiError
*/
public static logoutAllApiV1AuthLogoutAllPost(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/auth/logout-all',
});
}
/**
* 获取用户会话
* 获取用户的所有活跃会话
* @returns any Successful Response
* @throws ApiError
*/
public static getSessionsApiV1AuthSessionsGet(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/auth/sessions',
});
}
/**
* 获取当前用户信息
* 获取当前登录用户的信息
* @returns UserRead Successful Response
* @throws ApiError
*/
public static getCurrentUserInfoApiV1AuthMeGet(): CancelablePromise<UserRead> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/auth/me',
});
}
}
@@ -1,42 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { TaskRequest } from '../models/TaskRequest';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class DefaultService {
/**
* 根路径
* 根路径,返回应用信息
* @returns any Successful Response
* @throws ApiError
*/
public static rootGet(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/',
});
}
/**
* Add Task
* 添加定时任务接口
* @param requestBody
* @returns any Successful Response
* @throws ApiError
*/
public static addTaskSchedulePost(
requestBody: TaskRequest,
): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/schedule',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
}
@@ -1,118 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { TodoCreate } from '../models/TodoCreate';
import type { TodoRead } from '../models/TodoRead';
import type { TodoUpdate } from '../models/TodoUpdate';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class TodoService {
/**
* Create Todo
* @param requestBody
* @returns TodoRead Successful Response
* @throws ApiError
*/
public static createTodoApiV1TodosPost(
requestBody: TodoCreate,
): CancelablePromise<TodoRead> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/todos/',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Read Todos
* @param offset
* @param limit
* @returns TodoRead Successful Response
* @throws ApiError
*/
public static readTodosApiV1TodosGet(
offset?: number,
limit: number = 100,
): CancelablePromise<Array<TodoRead>> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/todos/',
query: {
'offset': offset,
'limit': limit,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Read Todo
* @param todoId
* @returns TodoRead Successful Response
* @throws ApiError
*/
public static readTodoApiV1TodosTodoIdGet(
todoId: number,
): CancelablePromise<TodoRead> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/todos/{todo_id}',
path: {
'todo_id': todoId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Update Todo
* @param todoId
* @param requestBody
* @returns TodoRead Successful Response
* @throws ApiError
*/
public static updateTodoApiV1TodosTodoIdPatch(
todoId: number,
requestBody: TodoUpdate,
): CancelablePromise<TodoRead> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/api/v1/todos/{todo_id}',
path: {
'todo_id': todoId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete Todo
* @param todoId
* @returns any Successful Response
* @throws ApiError
*/
public static deleteTodoApiV1TodosTodoIdDelete(
todoId: number,
): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/api/v1/todos/{todo_id}',
path: {
'todo_id': todoId,
},
errors: {
422: `Validation Error`,
},
});
}
}
@@ -0,0 +1,644 @@
// This file is auto-generated by @hey-api/openapi-ts
export type ClientOptions = {
baseUrl: 'http://127.0.0.1:8000' | (string & {});
};
/**
* Body_upload_image_api_v1_upload_image_post
*/
export type BodyUploadImageApiV1UploadImagePost = {
/**
* File
*/
file: Blob | File;
};
/**
* Detail
*/
export type Detail = {
/**
* Message
*/
message: string;
/**
* Code
*/
code: number;
};
/**
* HTTPError
*/
export type HttpError = {
detail?: Detail | null;
};
/**
* HTTPValidationError
*/
export type HttpValidationError = {
/**
* Detail
*/
detail?: Array<ValidationError>;
};
/**
* RefreshTokenResponse
*
* 刷新 Token 响应模型
*/
export type RefreshTokenResponse = {
/**
* Access Token
*/
access_token: string;
/**
* Token Type
*/
token_type?: string;
};
/**
* TodoCreate
*/
export type TodoCreate = {
/**
* Title
*/
title: string;
/**
* Description
*/
description?: string | null;
/**
* Completed
*/
completed?: boolean;
};
/**
* TodoRead
*/
export type TodoRead = {
/**
* Title
*/
title: string;
/**
* Description
*/
description?: string | null;
/**
* Completed
*/
completed?: boolean;
/**
* Id
*/
id: number;
};
/**
* TodoUpdate
*/
export type TodoUpdate = {
/**
* Title
*/
title?: string | null;
/**
* Description
*/
description?: string | null;
/**
* Completed
*/
completed?: boolean | null;
};
/**
* TokenResponse
*
* Token 响应模型
*/
export type TokenResponse = {
/**
* Access Token
*/
access_token: string;
/**
* Refresh Token
*/
refresh_token: string;
/**
* Token Type
*/
token_type?: string;
/**
* Expires In
*/
expires_in: number;
/**
* Refresh Expires In
*/
refresh_expires_in: number;
};
/**
* UploadResponse
*/
export type UploadResponse = {
/**
* Url
*/
url: string;
/**
* Filename
*/
filename: string;
/**
* File Type
*/
file_type: string;
};
/**
* UserCreate
*
* 用户创建模型
*/
export type UserCreate = {
/**
* Email
*/
email: string;
/**
* Username
*/
username?: string | null;
/**
* Full Name
*/
full_name?: string | null;
/**
* Is Active
*/
is_active?: boolean;
/**
* Is Superuser
*/
is_superuser?: boolean;
/**
* Created At
*/
created_at?: string | null;
/**
* Password
*/
password: string;
};
/**
* UserLogin
*/
export type UserLogin = {
/**
* Email
*/
email: string;
/**
* Password
*/
password: string;
};
/**
* UserRead
*
* 用户读取模型
*/
export type UserRead = {
/**
* Email
*/
email: string;
/**
* Username
*/
username?: string | null;
/**
* Full Name
*/
full_name?: string | null;
/**
* Is Active
*/
is_active?: boolean;
/**
* Is Superuser
*/
is_superuser?: boolean;
/**
* Created At
*/
created_at?: string | null;
/**
* Id
*/
id: string;
};
/**
* Userlogout
*/
export type Userlogout = {
/**
* Code
*/
code?: number;
/**
* Message
*/
message?: string;
};
/**
* ValidationError
*/
export type ValidationError = {
/**
* Location
*/
loc: Array<string | number>;
/**
* Message
*/
msg: string;
/**
* Error Type
*/
type: string;
/**
* Input
*/
input?: unknown;
/**
* Context
*/
ctx?: {
[key: string]: unknown;
};
};
export type ReadTodosApiV1TodosGetData = {
body?: never;
path?: never;
query?: {
/**
* Offset
*/
offset?: number;
/**
* Limit
*/
limit?: number;
};
url: '/api/v1/todos/';
};
export type ReadTodosApiV1TodosGetErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type ReadTodosApiV1TodosGetError = ReadTodosApiV1TodosGetErrors[keyof ReadTodosApiV1TodosGetErrors];
export type ReadTodosApiV1TodosGetResponses = {
/**
* Response Read Todos Api V1 Todos Get
*
* Successful Response
*/
200: Array<TodoRead>;
};
export type ReadTodosApiV1TodosGetResponse = ReadTodosApiV1TodosGetResponses[keyof ReadTodosApiV1TodosGetResponses];
export type CreateTodoApiV1TodosPostData = {
body: TodoCreate;
path?: never;
query?: never;
url: '/api/v1/todos/';
};
export type CreateTodoApiV1TodosPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type CreateTodoApiV1TodosPostError = CreateTodoApiV1TodosPostErrors[keyof CreateTodoApiV1TodosPostErrors];
export type CreateTodoApiV1TodosPostResponses = {
/**
* Successful Response
*/
200: TodoRead;
};
export type CreateTodoApiV1TodosPostResponse = CreateTodoApiV1TodosPostResponses[keyof CreateTodoApiV1TodosPostResponses];
export type DeleteTodoApiV1TodosTodoIdDeleteData = {
body?: never;
path: {
/**
* Todo Id
*/
todo_id: number;
};
query?: never;
url: '/api/v1/todos/{todo_id}';
};
export type DeleteTodoApiV1TodosTodoIdDeleteErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type DeleteTodoApiV1TodosTodoIdDeleteError = DeleteTodoApiV1TodosTodoIdDeleteErrors[keyof DeleteTodoApiV1TodosTodoIdDeleteErrors];
export type DeleteTodoApiV1TodosTodoIdDeleteResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type ReadTodoApiV1TodosTodoIdGetData = {
body?: never;
path: {
/**
* Todo Id
*/
todo_id: number;
};
query?: never;
url: '/api/v1/todos/{todo_id}';
};
export type ReadTodoApiV1TodosTodoIdGetErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type ReadTodoApiV1TodosTodoIdGetError = ReadTodoApiV1TodosTodoIdGetErrors[keyof ReadTodoApiV1TodosTodoIdGetErrors];
export type ReadTodoApiV1TodosTodoIdGetResponses = {
/**
* Successful Response
*/
200: TodoRead;
};
export type ReadTodoApiV1TodosTodoIdGetResponse = ReadTodoApiV1TodosTodoIdGetResponses[keyof ReadTodoApiV1TodosTodoIdGetResponses];
export type UpdateTodoApiV1TodosTodoIdPatchData = {
body: TodoUpdate;
path: {
/**
* Todo Id
*/
todo_id: number;
};
query?: never;
url: '/api/v1/todos/{todo_id}';
};
export type UpdateTodoApiV1TodosTodoIdPatchErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type UpdateTodoApiV1TodosTodoIdPatchError = UpdateTodoApiV1TodosTodoIdPatchErrors[keyof UpdateTodoApiV1TodosTodoIdPatchErrors];
export type UpdateTodoApiV1TodosTodoIdPatchResponses = {
/**
* Successful Response
*/
200: TodoRead;
};
export type UpdateTodoApiV1TodosTodoIdPatchResponse = UpdateTodoApiV1TodosTodoIdPatchResponses[keyof UpdateTodoApiV1TodosTodoIdPatchResponses];
export type RegisterApiV1AuthRegisterPostData = {
body: UserCreate;
path?: never;
query?: never;
url: '/api/v1/auth/register';
};
export type RegisterApiV1AuthRegisterPostErrors = {
/**
* 邮箱已被注册
*/
400: HttpError;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type RegisterApiV1AuthRegisterPostError = RegisterApiV1AuthRegisterPostErrors[keyof RegisterApiV1AuthRegisterPostErrors];
export type RegisterApiV1AuthRegisterPostResponses = {
/**
* Successful Response
*/
200: UserRead;
};
export type RegisterApiV1AuthRegisterPostResponse = RegisterApiV1AuthRegisterPostResponses[keyof RegisterApiV1AuthRegisterPostResponses];
export type LoginApiV1AuthLoginPostData = {
body: UserLogin;
path?: never;
query?: never;
url: '/api/v1/auth/login';
};
export type LoginApiV1AuthLoginPostErrors = {
/**
* 邮箱不存在
*/
400: HttpError;
/**
* 密码错误
*/
401: HttpError;
/**
* 用户被禁用
*/
402: HttpError;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type LoginApiV1AuthLoginPostError = LoginApiV1AuthLoginPostErrors[keyof LoginApiV1AuthLoginPostErrors];
export type LoginApiV1AuthLoginPostResponses = {
/**
* Successful Response
*/
200: TokenResponse;
};
export type LoginApiV1AuthLoginPostResponse = LoginApiV1AuthLoginPostResponses[keyof LoginApiV1AuthLoginPostResponses];
export type RefreshTokenApiV1AuthRefreshPostData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/auth/refresh';
};
export type RefreshTokenApiV1AuthRefreshPostErrors = {
/**
* 无效的刷新令牌
*/
401: HttpError;
};
export type RefreshTokenApiV1AuthRefreshPostError = RefreshTokenApiV1AuthRefreshPostErrors[keyof RefreshTokenApiV1AuthRefreshPostErrors];
export type RefreshTokenApiV1AuthRefreshPostResponses = {
/**
* Successful Response
*/
200: RefreshTokenResponse;
};
export type RefreshTokenApiV1AuthRefreshPostResponse = RefreshTokenApiV1AuthRefreshPostResponses[keyof RefreshTokenApiV1AuthRefreshPostResponses];
export type LogoutApiV1AuthLogoutPostData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/auth/logout';
};
export type LogoutApiV1AuthLogoutPostResponses = {
/**
* Successful Response
*/
200: Userlogout;
};
export type LogoutApiV1AuthLogoutPostResponse = LogoutApiV1AuthLogoutPostResponses[keyof LogoutApiV1AuthLogoutPostResponses];
export type LogoutAllApiV1AuthLogoutAllPostData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/auth/logout-all';
};
export type LogoutAllApiV1AuthLogoutAllPostResponses = {
/**
* Successful Response
*/
200: Userlogout;
};
export type LogoutAllApiV1AuthLogoutAllPostResponse = LogoutAllApiV1AuthLogoutAllPostResponses[keyof LogoutAllApiV1AuthLogoutAllPostResponses];
export type GetSessionsApiV1AuthSessionsGetData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/auth/sessions';
};
export type GetSessionsApiV1AuthSessionsGetResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type GetCurrentUserInfoApiV1AuthMeGetData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/auth/me';
};
export type GetCurrentUserInfoApiV1AuthMeGetErrors = {
/**
* 用户不存在
*/
404: HttpError;
};
export type GetCurrentUserInfoApiV1AuthMeGetError = GetCurrentUserInfoApiV1AuthMeGetErrors[keyof GetCurrentUserInfoApiV1AuthMeGetErrors];
export type GetCurrentUserInfoApiV1AuthMeGetResponses = {
/**
* Successful Response
*/
200: UserRead;
};
export type GetCurrentUserInfoApiV1AuthMeGetResponse = GetCurrentUserInfoApiV1AuthMeGetResponses[keyof GetCurrentUserInfoApiV1AuthMeGetResponses];
export type UploadImageApiV1UploadImagePostData = {
body: BodyUploadImageApiV1UploadImagePost;
path?: never;
query?: never;
url: '/api/v1/upload/image';
};
export type UploadImageApiV1UploadImagePostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type UploadImageApiV1UploadImagePostError = UploadImageApiV1UploadImagePostErrors[keyof UploadImageApiV1UploadImagePostErrors];
export type UploadImageApiV1UploadImagePostResponses = {
/**
* Successful Response
*/
200: UploadResponse;
};
export type UploadImageApiV1UploadImagePostResponse = UploadImageApiV1UploadImagePostResponses[keyof UploadImageApiV1UploadImagePostResponses];
export type RootGetData = {
body?: never;
path?: never;
query?: never;
url: '/';
};
export type RootGetResponses = {
/**
* Successful Response
*/
200: unknown;
};
-86
View File
@@ -1,86 +0,0 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
-35
View File
@@ -1,35 +0,0 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}
@@ -1,41 +0,0 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
You’ve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>
@@ -1,95 +0,0 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vue’s
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener"
>Vue - Official</a
>. If you need to test your components and web pages, check out
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
and
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
/
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
<br />
More instructions are available in
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
(our official Discord server), or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also follow the official
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
Bluesky account or the
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
X account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>
@@ -1,87 +0,0 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>
@@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>
@@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>
@@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>
@@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>
@@ -1,19 +0,0 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>
+16 -4
View File
@@ -1,19 +1,31 @@
import './assets/main.css'
// import './assets/main.css'
import 'element-plus/dist/index.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import App from './App.vue'
import router from './router'
import { OpenAPI } from "./api/generated"
import { useAuthStore } from './stores/auth'
/**
* 打包时注释该项
* 打包时取消该注释
*/
OpenAPI.BASE = "http://localhost:8000"
import { client } from "./api/generated/client.gen"
client.setConfig({
baseUrl: "/"
})
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus, { locale: zhCn })
const authStore = useAuthStore()
authStore.initAuth()
app.mount('#app')
+31 -10
View File
@@ -1,23 +1,44 @@
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ path: '/', redirect: '/todos' },
{
path: '/',
name: 'home',
component: HomeView,
path: '/login',
name: 'login',
component: () => import('@/views/LoginView.vue'),
meta: { requiresGuest: true },
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue'),
path: '/register',
name: 'register',
component: () => import('@/views/RegisterView.vue'),
meta: { requiresGuest: true },
},
{
path: '/todos',
name: 'todos',
component: () => import('@/views/TodoView.vue'),
meta: { requiresAuth: true },
},
],
})
// 路由守卫
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
const isAuthenticated = !!authStore.accessToken
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
} else if (to.meta.requiresGuest && isAuthenticated) {
next('/todos')
} else {
next()
}
})
export default router
+77
View File
@@ -0,0 +1,77 @@
import { defineStore } from "pinia"
import { ElMessage } from 'element-plus'
import { client } from "@/api/generated/client.gen"
import { loginApiV1AuthLoginPost, registerApiV1AuthRegisterPost } from "@/api/generated"
interface AuthState {
accessToken: string | null
refreshToken: string | null
}
export const useAuthStore = defineStore("auth", {
state: (): AuthState => ({
accessToken: localStorage.getItem("access_token"),
refreshToken: localStorage.getItem("refresh_token"),
}),
actions: {
setTokens(access: string, refresh: string) {
this.accessToken = access
this.refreshToken = refresh
localStorage.setItem("access_token", access)
localStorage.setItem("refresh_token", refresh)
client.setConfig({
auth: () => this.accessToken || ''
})
},
clearTokens() {
this.accessToken = null
this.refreshToken = null
localStorage.removeItem("access_token")
localStorage.removeItem("refresh_token")
client.setConfig({ auth: undefined })
},
async login(email: string, password: string) {
try {
const { data } = await loginApiV1AuthLoginPost({
body: { email, password },
throwOnError: true,
})
if (data) {
this.setTokens(data.access_token, data.refresh_token)
ElMessage.success("登录成功")
return true
}
return false
} catch (error: any) {
// console.error("登录失败", error)
ElMessage.error(error.message || "登录失败")
return false
}
},
async register(email: string, password: string, username?: string) {
try {
await registerApiV1AuthRegisterPost({
body: { email, password, username },
throwOnError: true,
})
ElMessage.success("注册成功")
return await this.login(email, password)
} catch (error: any) {
ElMessage.error(error.message || "注册失败")
return false
}
},
logout() {
this.clearTokens()
ElMessage.info("已退出登录")
},
initAuth() {
if (this.accessToken) {
client.setConfig({
auth: () => this.accessToken || ''
})
}
}
}
})
+69
View File
@@ -0,0 +1,69 @@
// src/stores/todo.ts
import { defineStore } from 'pinia'
import {
readTodosApiV1TodosGet,
createTodoApiV1TodosPost,
updateTodoApiV1TodosTodoIdPatch,
deleteTodoApiV1TodosTodoIdDelete,
type TodoRead,
type TodoCreate,
type TodoUpdate,
} from '@/api/generated'
interface TodoState {
todos: TodoRead[]
loading: boolean
}
export const useTodoStore = defineStore('todo', {
state: (): TodoState => ({
todos: [],
loading: false,
}),
actions: {
async fetchTodos() {
this.loading = true
try {
const { data } = await readTodosApiV1TodosGet({
query: { offset: 0, limit: 100 },
throwOnError: true,
})
this.todos = data || []
} catch (error) {
console.error('获取 Todo 列表失败', error)
throw error
} finally {
this.loading = false
}
},
async addTodo(todo: TodoCreate) {
const { data } = await createTodoApiV1TodosPost({
body: todo,
throwOnError: true,
})
if (data) {
this.todos.unshift(data)
}
return data
},
async updateTodo(id: number, updates: TodoUpdate) {
const { data } = await updateTodoApiV1TodosTodoIdPatch({
path: { todo_id: id },
body: updates,
throwOnError: true,
})
if (data) {
const index = this.todos.findIndex((t) => t.id === id)
if (index !== -1) this.todos[index] = data
}
return data
},
async deleteTodo(id: number) {
await deleteTodoApiV1TodosTodoIdDelete({
path: { todo_id: id },
throwOnError: true,
})
this.todos = this.todos.filter((t) => t.id !== id)
},
},
})
-15
View File
@@ -1,15 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>
-9
View File
@@ -1,9 +0,0 @@
<script setup lang="ts">
import TheWelcome from '../components/TheWelcome.vue'
</script>
<template>
<main>
<TheWelcome />
</main>
</template>
+58
View File
@@ -0,0 +1,58 @@
<!-- src/views/LoginView.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { ElCard, ElForm, ElFormItem, ElInput, ElButton, ElLink } from 'element-plus'
const router = useRouter()
const authStore = useAuthStore()
const form = ref({ email: '', password: '' })
const loading = ref(false)
async function handleLogin() {
loading.value = true
const success = await authStore.login(form.value.email, form.value.password)
loading.value = false
if (success) router.push('/todos')
}
</script>
<template>
<div class="page-container">
<el-card class="card">
<template #header>
<h2>登录</h2>
</template>
<el-form :model="form" @submit.prevent="handleLogin">
<el-form-item label="邮箱">
<el-input v-model="form.email" type="email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
</el-form-item>
<el-form-item>
<el-button type="primary" native-type="submit" :loading="loading" style="width: 100%">
登录
</el-button>
</el-form-item>
<el-form-item>
<el-link type="info" @click="$router.push('/register')">还没有账号?立即注册</el-link>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<style scoped>
.page-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #f5f7fa;
}
.card {
width: 400px;
}
</style>
@@ -0,0 +1,61 @@
<!-- src/views/RegisterView.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { ElCard, ElForm, ElFormItem, ElInput, ElButton, ElLink } from 'element-plus'
const router = useRouter()
const authStore = useAuthStore()
const form = ref({ email: '', username: '', password: '' })
const loading = ref(false)
async function handleRegister() {
loading.value = true
const success = await authStore.register(form.value.email, form.value.password, form.value.username)
loading.value = false
if (success) router.push('/todos')
}
</script>
<template>
<div class="page-container">
<el-card class="card">
<template #header>
<h2>注册</h2>
</template>
<el-form :model="form" @submit.prevent="handleRegister">
<el-form-item label="邮箱">
<el-input v-model="form.email" type="email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="用户名">
<el-input v-model="form.username" placeholder="可选" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
</el-form-item>
<el-form-item>
<el-button type="primary" native-type="submit" :loading="loading" style="width: 100%">
注册
</el-button>
</el-form-item>
<el-form-item>
<el-link type="info" @click="$router.push('/login')">已有账号?去登录</el-link>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<style scoped>
.page-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #f5f7fa;
}
.card {
width: 400px;
}
</style>
+245
View File
@@ -0,0 +1,245 @@
<!-- src/views/TodoView.vue -->
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useTodoStore } from '@/stores/todo'
import { useAuthStore } from '@/stores/auth'
import {
ElContainer,
ElHeader,
ElMain,
ElButton,
ElInput,
ElCheckbox,
ElPopconfirm,
ElMessage,
ElCard,
ElRow,
ElCol,
ElSpace,
ElEmpty,
} from 'element-plus'
import { Plus, Edit, Delete, Check, Close } from '@element-plus/icons-vue'
const todoStore = useTodoStore()
const authStore = useAuthStore()
const router = useRouter()
const newTitle = ref('')
const newDesc = ref('')
const adding = ref(false)
const editingId = ref<number | null>(null)
const editTitle = ref('')
const editDesc = ref('')
const editCompleted = ref(false)
onMounted(() => {
todoStore.fetchTodos()
})
async function addTodo() {
if (!newTitle.value.trim()) {
ElMessage.warning('请输入标题')
return
}
adding.value = true
await todoStore.addTodo({
title: newTitle.value,
description: newDesc.value || null,
})
newTitle.value = ''
newDesc.value = ''
adding.value = false
ElMessage.success('添加成功')
}
function startEdit(todo: any) {
editingId.value = todo.id
editTitle.value = todo.title
editDesc.value = todo.description || ''
editCompleted.value = todo.completed
}
async function saveEdit(id: number) {
await todoStore.updateTodo(id, {
title: editTitle.value,
description: editDesc.value || null,
completed: editCompleted.value,
})
editingId.value = null
ElMessage.success('更新成功')
}
async function toggleComplete(todo: any) {
await todoStore.updateTodo(todo.id, { completed: !todo.completed })
}
async function handleDelete(id: number) {
await todoStore.deleteTodo(id)
ElMessage.success('删除成功')
}
function logout() {
authStore.logout()
router.push('/login')
}
</script>
<template>
<el-container class="layout">
<el-header>
<div class="header">
<h2>✅ 待办清单</h2>
<el-button type="danger" plain @click="logout">退出登录</el-button>
</div>
</el-header>
<el-main>
<div class="content-wrapper">
<el-card class="add-card">
<template #header>新建待办</template>
<el-row :gutter="16">
<el-col :span="8">
<el-input v-model="newTitle" placeholder="标题" clearable />
</el-col>
<el-col :span="10">
<el-input v-model="newDesc" placeholder="描述(可选)" clearable />
</el-col>
<el-col :span="6">
<el-button type="primary" :icon="Plus" :loading="adding" @click="addTodo">
添加
</el-button>
</el-col>
</el-row>
</el-card>
<el-card v-loading="todoStore.loading" class="list-card">
<template #header>我的待办 ({{ todoStore.todos.length }})</template>
<div v-if="todoStore.todos.length === 0">
<el-empty description="暂无待办,添加一条吧" />
</div>
<div v-else class="todo-list">
<div v-for="todo in todoStore.todos" :key="todo.id" class="todo-item">
<template v-if="editingId !== todo.id">
<div class="todo-content">
<el-checkbox :model-value="todo.completed" @change="() => toggleComplete(todo)" />
<div class="todo-text" :class="{ completed: todo.completed }">
<strong>{{ todo.title }}</strong>
<span v-if="todo.description" class="desc">{{ todo.description }}</span>
</div>
</div>
<div class="todo-actions">
<el-button text type="primary" :icon="Edit" @click="startEdit(todo)" />
<el-popconfirm title="确定删除此项?" @confirm="handleDelete(todo.id)">
<template #reference>
<el-button text type="danger" :icon="Delete" />
</template>
</el-popconfirm>
</div>
</template>
<template v-else>
<div class="edit-form">
<el-input v-model="editTitle" placeholder="标题" size="small" />
<el-input v-model="editDesc" placeholder="描述" size="small" />
<el-checkbox v-model="editCompleted">已完成</el-checkbox>
<el-space>
<el-button type="primary" :icon="Check" size="small" @click="saveEdit(todo.id)">
保存
</el-button>
<el-button :icon="Close" size="small" @click="editingId = null">
取消
</el-button>
</el-space>
</div>
</template>
</div>
</div>
</el-card>
</div>
</el-main>
</el-container>
</template>
<style scoped>
.layout {
height: 100vh;
background: #f0f2f5;
display: flex;
flex-direction: column;
align-items: center;
}
.el-header {
width: 100%;
background: white;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
padding: 0 24px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
height: 60px;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
.el-main {
width: 100%;
display: flex;
justify-content: center;
padding: 20px;
}
.content-wrapper {
width: 100%;
max-width: 1000px;
}
.add-card {
margin-bottom: 20px;
}
.list-card {
min-height: 400px;
}
.todo-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.todo-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.todo-content {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.todo-text {
display: flex;
flex-direction: column;
}
.todo-text .desc {
font-size: 12px;
color: #909399;
}
.completed {
text-decoration: line-through;
color: #c0c4cc;
}
.todo-actions {
display: flex;
gap: 4px;
}
.edit-form {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
width: 100%;
}
</style>
+11 -5
View File
@@ -1,16 +1,22 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.vue"
],
"exclude": [
"src/**/__tests__/*"
],
"compilerOptions": {
// Extra safety for array and object lookups, but may have false positives.
"noUncheckedIndexedAccess": true,
// Path mapping for cleaner imports.
"paths": {
"@/*": ["./src/*"]
"@/*": [
"./src/*"
]
},
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
// Specified here to keep it out of the root directory.
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
+33 -8
View File
@@ -1,3 +1,4 @@
import time
from pathlib import Path
from contextlib import asynccontextmanager
@@ -8,23 +9,19 @@ from fastapi.staticfiles import StaticFiles
from sqlmodel import SQLModel, text
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.exc import OperationalError
from pydantic import BaseModel
from .core.config import get_config
from .core.cache import get_cache
from .core.logger import get_logger
from .core.scheduler import get_task_service_instance
from .core.static_files import SVFSStaticFiles
from .api import api_router
from .task import init_tasks_callback
class TaskRequest(BaseModel):
task_name: str
interval: int
from plugins.scheduler.scheduler import get_task_service_instance
config = get_config()
cache = get_cache()
logger = get_logger("app")
@@ -38,6 +35,9 @@ except KeyError: # pragma: no cover
@asynccontextmanager
async def lifespan(app: FastAPI):
from plugins import hello
from plugins.jwt.jwt_cache import get_jwt_cache
get_jwt_cache(config, cache)
hello()
database = config["database"]
@@ -73,7 +73,9 @@ async def lifespan(app: FastAPI):
raise
# 启动时:初始化任务服务
task_service = get_task_service_instance(async_session_maker)
task_service = get_task_service_instance(
config, get_logger("scheduler"), async_session_maker
)
app.state.task_service = task_service
await task_service.start(init_tasks_callback)
@@ -82,7 +84,7 @@ async def lifespan(app: FastAPI):
# 关闭时:释放引擎
await task_service.shutdown(wait=True)
await engine.dispose()
get_cache().close()
cache.close()
# 创建 FastAPI 应用
@@ -104,6 +106,21 @@ app.add_middleware(CORSMiddleware, **cors)
app.include_router(api_router, prefix=config["default"]["prefix"])
@app.middleware("http")
async def access_log_middleware(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = (time.perf_counter() - start) * 1000
client = request.client.host if request.client else "-"
logger.info(
f"{request.method} {request.url.path} status={response.status_code} "
f"duration={duration:.2f}ms client={client}",
extra={"status_code": response.status_code}, # ← 关键
)
return response
# 根路由
@app.get("/", summary="根路径")
async def root():
@@ -116,6 +133,14 @@ async def root():
}
# from pydantic import BaseModel
# class TaskRequest(BaseModel):
# task_name: str
# interval: int
# @app.post("/schedule")
# async def add_task(
# request: TaskRequest, task_service: TaskService = Depends(get_task_service)
+1 -1
View File
@@ -2,7 +2,7 @@ from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from ..service.auth import get_auth_service
from ..core.jwt_cache import TokenData
from plugins.jwt.jwt_cache import TokenData
access_scheme = HTTPBearer(scheme_name="AccessToken")
+59 -23
View File
@@ -5,13 +5,15 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from ...core.database import get_session
from ...core.jwt_cache import TokenData, TokenResponse, RefreshTokenResponse
from ...core.security import verify_password
from ...service.user import UserService
from ...service.auth import get_auth_service
from ...model.user import UserCreate, UserRead, UserLogin
from ...model.user import UserCreate, UserRead, UserLogin, Userlogout, UserSession
from ...model.http_error import HTTPError, Detail
from ..deps import get_current_user
from plugins.jwt.jwt_cache import TokenData, TokenResponse, RefreshTokenResponse
router = APIRouter(tags=["authentication"])
@@ -19,16 +21,23 @@ access_scheme = HTTPBearer(scheme_name="AccessToken")
refresh_scheme = HTTPBearer(scheme_name="RefreshToken")
@router.post("/register", response_model=UserRead)
@router.post(
"/register",
response_model=UserRead,
responses={400: {"model": HTTPError, "description": "邮箱已被注册"}},
)
async def register(
user_data: UserCreate,
session: AsyncSession = Depends(get_session),
):
print(user_data.password)
existing = await UserService.get_user_by_email(user_data.email, session)
if existing:
raise HTTPException(status_code=400, detail="邮箱已被注册")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=Detail(
message="邮箱已被注册", code=status.HTTP_400_BAD_REQUEST
).model_dump(),
)
user = await UserService.create_user(
session=session,
@@ -40,7 +49,15 @@ async def register(
return user
@router.post("/login", response_model=TokenResponse)
@router.post(
"/login",
response_model=TokenResponse,
responses={
400: {"model": HTTPError, "description": "邮箱不存在"},
401: {"model": HTTPError, "description": "密码错误"},
402: {"model": HTTPError, "description": "用户被禁用"},
},
)
async def login(
user_data: UserLogin,
request: Request,
@@ -48,16 +65,29 @@ async def login(
):
user = await UserService.get_user_by_email(user_data.email, session)
if not user:
raise HTTPException(status_code=400, detail="邮箱或密码错误")
raise HTTPException(
status_code=400, detail=Detail(message="邮箱不存在", code=400).model_dump()
)
if not verify_password(user_data.password, user.hashed_password):
raise HTTPException(status_code=400, detail="邮箱或密码错误")
raise HTTPException(
status_code=400, detail=Detail(message="密码错误", code=401).model_dump()
)
if not user.is_active:
raise HTTPException(status_code=400, detail="用户被禁用")
raise HTTPException(
status_code=400, detail=Detail(message="用户被禁用", code=402).model_dump()
)
auth_service = get_auth_service()
return await auth_service.create_tokens(user, request, device_id=None)
@router.post("/refresh", response_model=RefreshTokenResponse, summary="刷新访问令牌")
@router.post(
"/refresh",
response_model=RefreshTokenResponse,
responses={
401: {"model": HTTPError, "description": "无效的刷新令牌"},
},
summary="刷新访问令牌",
)
async def refresh_token(
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(refresh_scheme),
@@ -72,28 +102,28 @@ async def refresh_token(
if not new_access_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的刷新令牌",
detail=Detail(message="无效的刷新令牌", code=401).model_dump(),
headers={"WWW-Authenticate": "Bearer"},
)
return {"access_token": new_access_token, "token_type": "bearer"}
@router.post("/logout")
@router.post("/logout", response_model=Userlogout)
async def logout(
credentials: HTTPAuthorizationCredentials = Depends(access_scheme),
):
auth_service = get_auth_service()
token = credentials.credentials
await auth_service.logout(token)
return {"message": "登出成功"}
return {"message": "登出成功", "code": 200}
@router.post("/logout-all", summary="登出所有设备")
@router.post("/logout-all", summary="登出所有设备", response_model=Userlogout)
async def logout_all(current_user: TokenData = Depends(get_current_user)):
auth_service = get_auth_service()
await auth_service.logout_all(current_user.user_id)
return {"message": "所有设备已登出"}
return {"message": "所有设备已登出", "code": 200}
@router.get("/sessions", summary="获取用户会话")
@@ -102,14 +132,17 @@ async def get_sessions(current_user: TokenData = Depends(get_current_user)):
auth_service = get_auth_service()
sessions = await auth_service.get_user_sessions(current_user.user_id)
return {
"user_id": current_user.user_id,
"sessions": sessions,
"count": len(sessions),
}
return UserSession(
user_id=current_user.user_id, sessions=sessions, count=len(sessions)
)
@router.get("/me", response_model=UserRead, summary="获取当前用户信息")
@router.get(
"/me",
response_model=UserRead,
summary="获取当前用户信息",
responses={404: {"model": HTTPError, "description": "用户不存在"}},
)
async def get_current_user_info(
session: AsyncSession = Depends(get_session),
current_user: TokenData = Depends(get_current_user),
@@ -117,6 +150,9 @@ async def get_current_user_info(
"""获取当前登录用户的信息"""
user = await UserService.get_user_by_id(UUID(current_user.user_id), session)
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=Detail(message="用户不存在", code=404).model_dump(),
)
return user
+1
View File
@@ -64,6 +64,7 @@ class Server(TypedDict, total=False):
port: Required[int]
reload: bool
workers: int
access_log: bool
class Logging(TypedDict):
+41 -3
View File
@@ -1,3 +1,4 @@
import re
import logging
import datetime
from pathlib import Path
@@ -12,6 +13,36 @@ server_config = config[env]["server"]
log_config = config[env]["logging"]
class ColoredFormatter(logging.Formatter):
"""为 status_code 添加颜色的日志格式化器"""
COLORS = {
"2": "\033[32m", # 绿色 2xx
"3": "\033[36m", # 青色 3xx
"4": "\033[33m", # 黄色 4xx
"5": "\033[31m", # 红色 5xx
}
RESET = "\033[0m"
def format(self, record):
# 保存原始消息
original_msg = record.msg
# 如果日志记录包含 status_code 属性,则为其添加颜色
status_code = getattr(record, "status_code", None)
if status_code and isinstance(status_code, int):
first_digit = str(status_code)[0]
color = self.COLORS.get(first_digit, "")
colored_status = f"{color}{status_code}{self.RESET}"
# 替换消息中的 "status=数字" 为带颜色的版本
record.msg = re.sub(
rf"status={status_code}\b", f"status={colored_status}", original_msg
)
result = super().format(record)
# 恢复原始消息,避免影响其他 handler
record.msg = original_msg
return result
class ExcludeWatchFilesFilter(logging.Filter):
def __init__(self):
super().__init__()
@@ -29,7 +60,13 @@ class ExcludeWatchFilesFilter(logging.Filter):
return not record.name.startswith(self._filter_starts_with_tulpe)
_configured_loggers = set()
def setup_logging(logger: logging.Logger, log_name: str = "app"):
if log_name in _configured_loggers:
return
_configured_loggers.add(log_name)
log_level = getattr(logging, log_config["level"], logging.INFO)
log_format = log_config["format"]
# 创建文件夹
@@ -41,7 +78,8 @@ def setup_logging(logger: logging.Logger, log_name: str = "app"):
logger.setLevel(log_level)
# 创建格式化器
formatter = logging.Formatter(log_format)
plain_formatter = logging.Formatter(log_format)
color_formatter = ColoredFormatter(log_format)
# 创建轮转文件处理器 - 限制文件大小为 10MB,保留 5 个备份文件
max_bytes = log_config.get("max_size", 10 * 1024 * 1024) # 默认 10MB
@@ -53,12 +91,12 @@ def setup_logging(logger: logging.Logger, log_name: str = "app"):
maxBytes=max_bytes,
backupCount=backup_count,
)
file_handler.setFormatter(formatter)
file_handler.setFormatter(plain_formatter)
file_handler.addFilter(ExcludeWatchFilesFilter())
# 创建控制台处理器并添加过滤器
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
console_handler.setFormatter(color_formatter)
console_handler.addFilter(ExcludeWatchFilesFilter())
# 添加处理器到根日志记录器
+11
View File
@@ -0,0 +1,11 @@
from pydantic import BaseModel
from typing import Optional
class Detail(BaseModel):
message: str
code: int
class HTTPError(BaseModel):
detail: Optional[Detail] = None
+12 -1
View File
@@ -1,4 +1,4 @@
from typing import Optional, List, TYPE_CHECKING
from typing import Optional, List, Dict, TYPE_CHECKING
from sqlmodel import SQLModel, Field, Relationship
from pydantic import BaseModel
@@ -42,3 +42,14 @@ class UserRead(UserBase):
class UserLogin(BaseModel):
email: str
password: str
class Userlogout(BaseModel):
code: int = 200
message: str = "logout success"
class UserSession(BaseModel):
user_id: str
sessions: List[Dict[str, Optional[str]]]
count: int
+2 -1
View File
@@ -8,10 +8,11 @@ from passlib.context import CryptContext
from sqlalchemy.ext.asyncio import AsyncSession
from ..model.user import User
from ..core.jwt_cache import get_jwt_cache, TokenResponse, TokenData
from ..core.config import get_config
from .user import UserService
from plugins.jwt.jwt_cache import get_jwt_cache, TokenResponse, TokenData
class AuthService:
"""认证服务"""
-1
View File
@@ -1,5 +1,4 @@
import os
import time
import uuid
from pathlib import Path
+2 -1
View File
@@ -1,7 +1,8 @@
from ..core.scheduler import TaskService
from ..core.config import get_config
from .dev_init_db import init_db
from plugins.scheduler.scheduler import TaskService
config = get_config()
+6 -2
View File
@@ -1,9 +1,13 @@
from ..core.scheduler import logger
from ..core.scheduler import get_task_service_instance
from ..core.logger import get_logger
from ..core.constant import PermissionConstant
from ..service.user import UserService
from ..service.permission import PermissionService
from plugins.scheduler.scheduler import get_task_service_instance
logger = get_logger("scheduler")
async def dev_init_admin(session):
admin_user = await UserService.get_user_by_email("admin", session)
+7
View File
@@ -250,6 +250,13 @@ async def main():
with open("./build/utils/launch.py", "w", encoding="utf-8") as f:
f.write(context)
jwt_key = Encryption.generate_key()
with open("./build/config.toml", "r", encoding="utf-8") as f:
context = f.read()
context = context.replace("your-secret-key-change-in-production", jwt_key)
with open("build/config.toml", "w", encoding="utf-8") as f:
f.write(context)
# 混淆
run_pyarmor_obfuscate()
@@ -8,9 +8,6 @@ import threading
import jwt
from .config import get_config
from .cache import get_cache
@dataclass
class TokenCacheData:
@@ -38,12 +35,12 @@ class TokenCacheData:
class JWTCache:
"""JWT Token 缓存管理器"""
def __init__(self, db_path: Optional[str] = None):
self.config = get_config()
def __init__(self, config, cache):
self.config = config
self.lock = threading.RLock()
self.cache = get_cache()
self.cache = cache
self.secret_key = self.config["security"]["secret_key"]
self.algorithm = self.config["security"]["algorithm"]
@@ -347,11 +344,13 @@ class JWTCache:
_jwt_cache = None
def get_jwt_cache() -> JWTCache:
def get_jwt_cache(config=None, cache=None) -> JWTCache:
"""获取 JWT 缓存实例(单例模式)"""
global _jwt_cache
if _jwt_cache is None:
_jwt_cache = JWTCache()
if config is None or cache is None:
raise ValueError("参数 config 和 cache 不能为空")
_jwt_cache = JWTCache(config, cache)
return _jwt_cache
View File
View File
@@ -19,13 +19,6 @@ from apscheduler.triggers.interval import IntervalTrigger
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from ..core.config import get_config
from ..core.logger import get_logger
config = get_config()
logger = get_logger("scheduler")
def acquire_scheduler_lock():
lock_path = os.path.join(tempfile.gettempdir(), "chongming_scheduler.lock")
@@ -48,7 +41,7 @@ def acquire_scheduler_lock():
class TaskService:
"""任务调度服务"""
def __init__(self, async_session_maker=None):
def __init__(self, config, logger, async_session_maker=None):
self.async_session_maker = async_session_maker
# 使用独立的 SQLite 数据库存储任务(与业务数据库分开)
self.job_store_db_path = config.get("scheduler", {}).get(
@@ -73,6 +66,7 @@ class TaskService:
)
self._started = False
self.lock_file, self.is_scheduler_worker = acquire_scheduler_lock()
self.logger = logger
async def start(self, init_tasks_callback: Optional[Callable] = None):
"""启动调度器"""
@@ -80,7 +74,7 @@ class TaskService:
if not self._started:
self.scheduler.start()
self._started = True
logger.info("任务调度器已启动")
self.logger.info("任务调度器已启动")
if init_tasks_callback:
await init_tasks_callback(self)
@@ -94,7 +88,7 @@ class TaskService:
if self._started:
self.scheduler.shutdown(wait=wait)
self._started = False
logger.info("任务调度器已关闭")
self.logger.info("任务调度器已关闭")
if sys.platform == "win32":
import msvcrt
@@ -129,7 +123,7 @@ class TaskService:
kwargs=kwargs or {},
replace_existing=replace_existing,
)
logger.info(f"添加间隔任务: {job.id}")
self.logger.info(f"添加间隔任务: {job.id}")
return job
async def add_cron_job(
@@ -167,7 +161,7 @@ class TaskService:
kwargs=kwargs or {},
replace_existing=replace_existing,
)
logger.info(f"添加 cron 任务: {job.id}")
self.logger.info(f"添加 cron 任务: {job.id}")
return job
async def add_date_job(
@@ -189,37 +183,37 @@ class TaskService:
kwargs=kwargs or {},
replace_existing=replace_existing,
)
logger.info(f"添加一次性任务: {job.id}")
self.logger.info(f"添加一次性任务: {job.id}")
return job
async def remove_job(self, job_id: str) -> bool:
"""删除任务"""
try:
self.scheduler.remove_job(job_id)
logger.info(f"删除任务: {job_id}")
self.logger.info(f"删除任务: {job_id}")
return True
except Exception as e:
logger.error(f"删除任务失败 {job_id}: {e}")
self.logger.error(f"删除任务失败 {job_id}: {e}")
return False
async def pause_job(self, job_id: str) -> bool:
"""暂停任务"""
try:
self.scheduler.pause_job(job_id)
logger.info(f"暂停任务: {job_id}")
self.logger.info(f"暂停任务: {job_id}")
return True
except Exception as e:
logger.error(f"暂停任务失败 {job_id}: {e}")
self.logger.error(f"暂停任务失败 {job_id}: {e}")
return False
async def resume_job(self, job_id: str) -> bool:
"""恢复任务"""
try:
self.scheduler.resume_job(job_id)
logger.info(f"恢复任务: {job_id}")
self.logger.info(f"恢复任务: {job_id}")
return True
except Exception as e:
logger.error(f"恢复任务失败 {job_id}: {e}")
self.logger.error(f"恢复任务失败 {job_id}: {e}")
return False
async def get_job(self, job_id: str) -> Optional[Dict]:
@@ -253,10 +247,10 @@ class TaskService:
raise ValueError(f"不支持的 trigger 类型: {trigger_type}")
job.reschedule(trigger=trigger)
logger.info(f"重新调度任务: {job_id}")
self.logger.info(f"重新调度任务: {job_id}")
return job
except Exception as e:
logger.error(f"重新调度任务失败 {job_id}: {e}")
self.logger.error(f"重新调度任务失败 {job_id}: {e}")
return None
def _job_to_dict(self, job: Job) -> Dict:
@@ -277,10 +271,12 @@ class TaskService:
_task_service: Optional[TaskService] = None
def get_task_service_instance(async_session_maker=None):
def get_task_service_instance(config=None, logger=None, async_session_maker=None):
global _task_service
if _task_service is None:
_task_service = TaskService(async_session_maker)
if config is None or logger is None:
raise ValueError("请传入 config 和 logger")
_task_service = TaskService(config, logger, async_session_maker)
return _task_service
View File
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB