modify: task 集成数据库操作
This commit is contained in:
Generated
+208
@@ -18,6 +18,7 @@
|
|||||||
"@vitejs/plugin-vue": "^6.0.5",
|
"@vitejs/plugin-vue": "^6.0.5",
|
||||||
"@vue/tsconfig": "^0.9.1",
|
"@vue/tsconfig": "^0.9.1",
|
||||||
"npm-run-all2": "^8.0.4",
|
"npm-run-all2": "^8.0.4",
|
||||||
|
"openapi-typescript-codegen": "^0.29.0",
|
||||||
"prettier": "3.8.1",
|
"prettier": "3.8.1",
|
||||||
"typescript": "~6.0.0",
|
"typescript": "~6.0.0",
|
||||||
"vite": "^8.0.3",
|
"vite": "^8.0.3",
|
||||||
@@ -28,6 +29,24 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"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": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||||
@@ -561,6 +580,13 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jsdevtools/ono": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
|
"resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
|
||||||
@@ -879,6 +905,13 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/json-schema": {
|
||||||
|
"version": "7.0.15",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.12.2",
|
"version": "24.12.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-24.12.2.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/node/-/node-24.12.2.tgz",
|
||||||
@@ -1266,6 +1299,13 @@
|
|||||||
"node": ">=14"
|
"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": {
|
"node_modules/ast-kit": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/ast-kit/-/ast-kit-2.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/ast-kit/-/ast-kit-2.2.0.tgz",
|
||||||
@@ -1370,6 +1410,19 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001788",
|
"version": "1.0.30001788",
|
||||||
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
|
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
|
||||||
@@ -1406,6 +1459,16 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/confbox": {
|
"node_modules/confbox": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz",
|
"resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz",
|
||||||
@@ -1617,6 +1680,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -1642,6 +1720,35 @@
|
|||||||
"node": ">=6.9.0"
|
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.5",
|
||||||
|
"neo-async": "^2.6.2",
|
||||||
|
"source-map": "^0.6.1",
|
||||||
|
"wordwrap": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"handlebars": "bin/handlebars"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.7"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"uglify-js": "^3.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hookable": {
|
"node_modules/hookable": {
|
||||||
"version": "5.5.3",
|
"version": "5.5.3",
|
||||||
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
|
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
|
||||||
@@ -1728,6 +1835,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/jsesc": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
|
||||||
@@ -1762,6 +1882,19 @@
|
|||||||
"node": ">=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": {
|
"node_modules/kolorist": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz",
|
"resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz",
|
||||||
@@ -2090,6 +2223,16 @@
|
|||||||
"node": ">= 0.10.0"
|
"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": {
|
"node_modules/mitt": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
|
||||||
@@ -2166,6 +2309,13 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"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": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.37",
|
"version": "2.0.37",
|
||||||
"resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.37.tgz",
|
"resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.37.tgz",
|
||||||
@@ -2236,6 +2386,23 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/path-browserify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||||
@@ -2542,6 +2709,16 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -2626,6 +2803,20 @@
|
|||||||
"integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
|
"integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
@@ -2633,6 +2824,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/unplugin": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-3.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-3.0.0.tgz",
|
||||||
@@ -3048,6 +3249,13 @@
|
|||||||
"node": "^18.17.0 || >=20.5.0"
|
"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": {
|
"node_modules/wsl-utils": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
"type-check": "vue-tsc --build",
|
"type-check": "vue-tsc --build",
|
||||||
"format": "prettier --write --experimental-cli src/"
|
"format": "prettier --write --experimental-cli src/",
|
||||||
|
"openapi": "npx openapi-typescript-codegen --input http://127.0.0.1:8000/openapi.json --output ./src/api/generated/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
"@vitejs/plugin-vue": "^6.0.5",
|
"@vitejs/plugin-vue": "^6.0.5",
|
||||||
"@vue/tsconfig": "^0.9.1",
|
"@vue/tsconfig": "^0.9.1",
|
||||||
"npm-run-all2": "^8.0.4",
|
"npm-run-all2": "^8.0.4",
|
||||||
|
"openapi-typescript-codegen": "^0.29.0",
|
||||||
"prettier": "3.8.1",
|
"prettier": "3.8.1",
|
||||||
"typescript": "~6.0.0",
|
"typescript": "~6.0.0",
|
||||||
"vite": "^8.0.3",
|
"vite": "^8.0.3",
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
import { RouterLink, RouterView } from 'vue-router'
|
import { RouterLink, RouterView } from 'vue-router'
|
||||||
import HelloWorld from './components/HelloWorld.vue'
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -10,6 +29,13 @@ import HelloWorld from './components/HelloWorld.vue'
|
|||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<HelloWorld msg="You did it!" />
|
<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>
|
<nav>
|
||||||
<RouterLink to="/">Home</RouterLink>
|
<RouterLink to="/">Home</RouterLink>
|
||||||
<RouterLink to="/about">About</RouterLink>
|
<RouterLink to="/about">About</RouterLink>
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/* 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>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/* 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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/* 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,322 @@
|
|||||||
|
/* 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,24 @@
|
|||||||
|
/* 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';
|
||||||
|
|
||||||
|
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';
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/* 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>;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/* 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;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type TaskRequest = {
|
||||||
|
task_name: string;
|
||||||
|
interval: number;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/* 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;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/* 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;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/* 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);
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/* 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;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/* 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;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type UserLogin = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/* 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;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/* 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,109 @@
|
|||||||
|
/* 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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/* 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`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/* 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`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,12 @@ import { createPinia } from 'pinia'
|
|||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import { OpenAPI } from "./api/generated"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打包时注释该项
|
||||||
|
*/
|
||||||
|
OpenAPI.BASE = "http://localhost:8000"
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ export default defineConfig({
|
|||||||
vue(),
|
vue(),
|
||||||
vueDevTools(),
|
vueDevTools(),
|
||||||
],
|
],
|
||||||
base: '/stats/',
|
/**
|
||||||
|
* 打包开启此配置项
|
||||||
|
*/
|
||||||
|
base: '/static/',
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, Depends
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from sqlmodel import SQLModel, text
|
from sqlmodel import SQLModel, text
|
||||||
@@ -12,10 +12,9 @@ from pydantic import BaseModel
|
|||||||
from .core.config import get_config
|
from .core.config import get_config
|
||||||
from .core.cache import get_cache
|
from .core.cache import get_cache
|
||||||
from .core.logger import get_logger
|
from .core.logger import get_logger
|
||||||
from .core.scheduler import get_task_service, get_task_service_instance, TaskService
|
from .core.scheduler import get_task_service_instance
|
||||||
from .core.static_files import static_files_handler
|
from .core.static_files import SVFSStaticFiles
|
||||||
from .api import api_router
|
from .api import api_router
|
||||||
from .task.execute_background import execute_background_task
|
|
||||||
from .task import init_tasks_callback
|
from .task import init_tasks_callback
|
||||||
|
|
||||||
|
|
||||||
@@ -45,12 +44,6 @@ async def lifespan(app: FastAPI):
|
|||||||
if database_config is None:
|
if database_config is None:
|
||||||
raise ValueError("配置不存在")
|
raise ValueError("配置不存在")
|
||||||
|
|
||||||
task_service = get_task_service_instance()
|
|
||||||
|
|
||||||
await task_service.start(init_tasks_callback)
|
|
||||||
|
|
||||||
app.state.task_service = task_service
|
|
||||||
|
|
||||||
# 启动时:创建引擎和会话工厂,初始化数据库表
|
# 启动时:创建引擎和会话工厂,初始化数据库表
|
||||||
engine = create_async_engine(database["url"], **database_config)
|
engine = create_async_engine(database["url"], **database_config)
|
||||||
async_session_maker = async_sessionmaker(
|
async_session_maker = async_sessionmaker(
|
||||||
@@ -77,6 +70,10 @@ async def lifespan(app: FastAPI):
|
|||||||
if "already exists" in str(e):
|
if "already exists" in str(e):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
task_service = get_task_service_instance(async_session_maker)
|
||||||
|
app.state.task_service = task_service
|
||||||
|
await task_service.start(init_tasks_callback)
|
||||||
|
|
||||||
yield # 应用运行期间
|
yield # 应用运行期间
|
||||||
|
|
||||||
# 关闭时:释放引擎
|
# 关闭时:释放引擎
|
||||||
@@ -114,21 +111,23 @@ async def root():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/schedule")
|
# @app.post("/schedule")
|
||||||
async def add_task(
|
# async def add_task(
|
||||||
request: TaskRequest, task_service: TaskService = Depends(get_task_service)
|
# request: TaskRequest, task_service: TaskService = Depends(get_task_service)
|
||||||
):
|
# ):
|
||||||
"""添加定时任务接口"""
|
# """添加定时任务接口"""
|
||||||
job = await task_service.add_interval_job(
|
# job = await task_service.add_interval_job(
|
||||||
execute_background_task,
|
# execute_background_task,
|
||||||
seconds=request.interval,
|
# seconds=request.interval,
|
||||||
args=[request.task_name],
|
# args=[request.task_name],
|
||||||
job_id=request.task_name,
|
# job_id=request.task_name,
|
||||||
)
|
# )
|
||||||
return {"status": "success", "job_id": job.id}
|
# return {"status": "success", "job_id": job.id}
|
||||||
|
|
||||||
|
|
||||||
if os.path.exists("static.svfs"):
|
if os.path.exists("static.svfs"):
|
||||||
|
logger.info("静态文件已存在,将使用静态文件服务")
|
||||||
|
static_files_handler = SVFSStaticFiles(directory="/")
|
||||||
app.mount("/static", static_files_handler, name="static")
|
app.mount("/static", static_files_handler, name="static")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from ...core.database import get_session
|
from ...core.database import get_session
|
||||||
from ...core.jwt_cache import TokenData
|
from ...core.jwt_cache import TokenData, TokenResponse, RefreshTokenResponse
|
||||||
from ...core.security import verify_password
|
from ...core.security import verify_password
|
||||||
from ...service.user import UserService
|
from ...service.user import UserService
|
||||||
from ...service.auth import get_auth_service
|
from ...service.auth import get_auth_service
|
||||||
@@ -38,7 +38,7 @@ async def register(
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login")
|
@router.post("/login", response_model=TokenResponse)
|
||||||
async def login(
|
async def login(
|
||||||
user_data: UserLogin,
|
user_data: UserLogin,
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -55,7 +55,7 @@ async def login(
|
|||||||
return await auth_service.create_tokens(user, request, device_id=None)
|
return await auth_service.create_tokens(user, request, device_id=None)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/refresh", response_model=dict, summary="刷新访问令牌")
|
@router.post("/refresh", response_model=RefreshTokenResponse, summary="刷新访问令牌")
|
||||||
async def refresh_token(
|
async def refresh_token(
|
||||||
request: Request,
|
request: Request,
|
||||||
credentials: HTTPAuthorizationCredentials = Depends(refresh_scheme),
|
credentials: HTTPAuthorizationCredentials = Depends(refresh_scheme),
|
||||||
|
|||||||
@@ -366,6 +366,13 @@ class TokenResponse(BaseModel):
|
|||||||
refresh_expires_in: int
|
refresh_expires_in: int
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshTokenResponse(BaseModel):
|
||||||
|
"""刷新 Token 响应模型"""
|
||||||
|
|
||||||
|
access_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
|
|
||||||
|
|
||||||
# Token 数据模型
|
# Token 数据模型
|
||||||
class TokenData(BaseModel):
|
class TokenData(BaseModel):
|
||||||
"""Token 数据模型"""
|
"""Token 数据模型"""
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ def acquire_scheduler_lock():
|
|||||||
class TaskService:
|
class TaskService:
|
||||||
"""任务调度服务"""
|
"""任务调度服务"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, async_session_maker=None):
|
||||||
|
self.async_session_maker = async_session_maker
|
||||||
# 使用独立的 SQLite 数据库存储任务(与业务数据库分开)
|
# 使用独立的 SQLite 数据库存储任务(与业务数据库分开)
|
||||||
self.job_store_db_path = config.get("scheduler", {}).get(
|
self.job_store_db_path = config.get("scheduler", {}).get(
|
||||||
"job_store_path", "scheduler_jobs.db"
|
"job_store_path", "scheduler_jobs.db"
|
||||||
@@ -172,15 +173,21 @@ class TaskService:
|
|||||||
async def add_date_job(
|
async def add_date_job(
|
||||||
self,
|
self,
|
||||||
func: Callable,
|
func: Callable,
|
||||||
run_date: datetime,
|
run_date: Optional[datetime] = None,
|
||||||
job_id: Optional[str] = None,
|
job_id: Optional[str] = None,
|
||||||
args: Optional[List] = None,
|
args: Optional[List] = None,
|
||||||
kwargs: Optional[Dict] = None,
|
kwargs: Optional[Dict] = None,
|
||||||
|
replace_existing: bool = True,
|
||||||
) -> Job:
|
) -> Job:
|
||||||
"""添加一次性任务"""
|
"""添加一次性任务"""
|
||||||
trigger = DateTrigger(run_date=run_date)
|
trigger = DateTrigger(run_date=run_date)
|
||||||
job = self.scheduler.add_job(
|
job = self.scheduler.add_job(
|
||||||
func, trigger=trigger, id=job_id, args=args or [], kwargs=kwargs or {}
|
func,
|
||||||
|
trigger=trigger,
|
||||||
|
id=job_id,
|
||||||
|
args=args or [],
|
||||||
|
kwargs=kwargs or {},
|
||||||
|
replace_existing=replace_existing,
|
||||||
)
|
)
|
||||||
logger.info(f"添加一次性任务: {job.id}")
|
logger.info(f"添加一次性任务: {job.id}")
|
||||||
return job
|
return job
|
||||||
@@ -270,10 +277,10 @@ class TaskService:
|
|||||||
_task_service: Optional[TaskService] = None
|
_task_service: Optional[TaskService] = None
|
||||||
|
|
||||||
|
|
||||||
def get_task_service_instance():
|
def get_task_service_instance(async_session_maker=None):
|
||||||
global _task_service
|
global _task_service
|
||||||
if _task_service is None:
|
if _task_service is None:
|
||||||
_task_service = TaskService()
|
_task_service = TaskService(async_session_maker)
|
||||||
return _task_service
|
return _task_service
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import os
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from fastapi.responses import Response
|
from fastapi.responses import Response
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
@@ -7,12 +6,8 @@ from sqlite_vfs.core import SQLiteVFS
|
|||||||
|
|
||||||
class SVFSStaticFiles(StaticFiles):
|
class SVFSStaticFiles(StaticFiles):
|
||||||
def __init__(self, directory, vfs_db_path="static.svfs"):
|
def __init__(self, directory, vfs_db_path="static.svfs"):
|
||||||
if not os.path.exists(vfs_db_path):
|
vfs_db_path = vfs_db_path
|
||||||
raise RuntimeError(
|
self._vfs = SQLiteVFS(vfs_db_path, compress=False)
|
||||||
f"虚拟文件系统数据库 {vfs_db_path} 不存在,请先运行打包脚本"
|
|
||||||
)
|
|
||||||
self.vfs_db_path = vfs_db_path
|
|
||||||
self.vfs = SQLiteVFS(vfs_db_path, compress=False)
|
|
||||||
super().__init__(directory=directory)
|
super().__init__(directory=directory)
|
||||||
|
|
||||||
async def get_response(self, path: str, scope):
|
async def get_response(self, path: str, scope):
|
||||||
@@ -20,7 +15,7 @@ class SVFSStaticFiles(StaticFiles):
|
|||||||
normalized_path = "/" + path.replace("\\", "/")
|
normalized_path = "/" + path.replace("\\", "/")
|
||||||
|
|
||||||
# 获取文件信息
|
# 获取文件信息
|
||||||
file_info = self.vfs.get_file_info(normalized_path)
|
file_info = self._vfs.get_file_info(normalized_path)
|
||||||
|
|
||||||
if file_info is None:
|
if file_info is None:
|
||||||
# SPA 回退:如果不是静态资源扩展名,尝试返回 index.html
|
# SPA 回退:如果不是静态资源扩展名,尝试返回 index.html
|
||||||
@@ -37,22 +32,22 @@ class SVFSStaticFiles(StaticFiles):
|
|||||||
".json",
|
".json",
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
index_info = self.vfs.get_file_info("/index.html")
|
index_info = self._vfs.get_file_info("/index.html")
|
||||||
if index_info:
|
if index_info:
|
||||||
content = self.vfs.read_file("/index.html")
|
content = self._vfs.read_file("/index.html")
|
||||||
return Response(content=content, media_type="text/html")
|
return Response(content=content, media_type="text/html")
|
||||||
raise HTTPException(status_code=404, detail="File not found")
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
|
||||||
# 如果是目录,也返回 index.html(某些情况)
|
# 如果是目录,也返回 index.html(某些情况)
|
||||||
if file_info["is_directory"]:
|
if file_info["is_directory"]:
|
||||||
index_info = self.vfs.get_file_info(f"{normalized_path}/index.html")
|
index_info = self._vfs.get_file_info(f"{normalized_path}/index.html")
|
||||||
if index_info:
|
if index_info:
|
||||||
content = self.vfs.read_file(f"{normalized_path}/index.html")
|
content = self._vfs.read_file(f"{normalized_path}/index.html")
|
||||||
return Response(content=content, media_type="text/html")
|
return Response(content=content, media_type="text/html")
|
||||||
raise HTTPException(status_code=404, detail="Directory index not found")
|
raise HTTPException(status_code=404, detail="Directory index not found")
|
||||||
|
|
||||||
# 读取文件内容
|
# 读取文件内容
|
||||||
content = self.vfs.read_file(normalized_path)
|
content = self._vfs.read_file(normalized_path)
|
||||||
content_type = file_info.get("content_type") or "application/octet-stream"
|
content_type = file_info.get("content_type") or "application/octet-stream"
|
||||||
# 简单根据扩展名补充 MIME 类型(可选)
|
# 简单根据扩展名补充 MIME 类型(可选)
|
||||||
if normalized_path.endswith(".js"):
|
if normalized_path.endswith(".js"):
|
||||||
@@ -65,6 +60,3 @@ class SVFSStaticFiles(StaticFiles):
|
|||||||
content_type = "image/x-icon"
|
content_type = "image/x-icon"
|
||||||
|
|
||||||
return Response(content=content, media_type=content_type)
|
return Response(content=content, media_type=content_type)
|
||||||
|
|
||||||
|
|
||||||
static_files_handler = SVFSStaticFiles(directory="/")
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from .user import User
|
||||||
|
from .role import Role
|
||||||
|
from .permission import Permission
|
||||||
|
from .relationship_table import UserRole, RolePermission
|
||||||
|
from .todo import Todo
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"User",
|
||||||
|
"Role",
|
||||||
|
"Permission",
|
||||||
|
"UserRole",
|
||||||
|
"RolePermission",
|
||||||
|
"Todo",
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from typing import Optional, List, TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlmodel import SQLModel, Field, Relationship
|
||||||
|
|
||||||
|
from .relationship_table import RolePermission
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .role import Role
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 权限表 ----------
|
||||||
|
class Permission(SQLModel, table=True):
|
||||||
|
"""权限项,如 user:create, order:delete"""
|
||||||
|
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
name: str = Field(unique=True, index=True) # 权限标识符
|
||||||
|
description: Optional[str] = None
|
||||||
|
resource: str = Field(index=True) # 资源,如 user, order, product
|
||||||
|
action: str # 操作,如 create, read, update, delete
|
||||||
|
|
||||||
|
# 多对多关联
|
||||||
|
roles: List["Role"] = Relationship(
|
||||||
|
back_populates="permissions", link_model=RolePermission
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from uuid import UUID
|
||||||
|
from sqlmodel import SQLModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 关联表 ----------
|
||||||
|
class UserRole(SQLModel, table=True):
|
||||||
|
"""用户角色关联表"""
|
||||||
|
|
||||||
|
user_id: UUID = Field(foreign_key="user.id", primary_key=True)
|
||||||
|
role_id: int = Field(foreign_key="role.id", primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
|
class RolePermission(SQLModel, table=True):
|
||||||
|
"""角色权限关联表"""
|
||||||
|
|
||||||
|
role_id: int = Field(foreign_key="role.id", primary_key=True)
|
||||||
|
permission_id: int = Field(foreign_key="permission.id", primary_key=True)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from typing import Optional, List, TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlmodel import SQLModel, Field, Relationship
|
||||||
|
|
||||||
|
from .relationship_table import UserRole, RolePermission
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .user import User
|
||||||
|
from .permission import Permission
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 角色表 ----------
|
||||||
|
class Role(SQLModel, table=True):
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
name: str = Field(unique=True, index=True) # admin, manager, operator, viewer
|
||||||
|
description: Optional[str] = None
|
||||||
|
is_only_user: bool = Field(default=False)
|
||||||
|
users: List["User"] = Relationship(back_populates="roles", link_model=UserRole)
|
||||||
|
permissions: List["Permission"] = Relationship(
|
||||||
|
back_populates="roles", link_model=RolePermission
|
||||||
|
)
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
from typing import Optional
|
from typing import Optional, List, TYPE_CHECKING
|
||||||
|
|
||||||
from sqlmodel import SQLModel, Field
|
from sqlmodel import SQLModel, Field, Relationship
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .relationship_table import UserRole
|
||||||
|
from uuid import uuid4, UUID
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .role import Role
|
||||||
|
|
||||||
|
|
||||||
class UserBase(SQLModel):
|
class UserBase(SQLModel):
|
||||||
email: str = Field(unique=True, index=True)
|
email: str = Field(unique=True, index=True)
|
||||||
@@ -16,8 +22,9 @@ class UserBase(SQLModel):
|
|||||||
class User(UserBase, table=True):
|
class User(UserBase, table=True):
|
||||||
"""用户模型"""
|
"""用户模型"""
|
||||||
|
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||||
hashed_password: str
|
hashed_password: str
|
||||||
|
roles: List["Role"] = Relationship(back_populates="users", link_model=UserRole)
|
||||||
|
|
||||||
|
|
||||||
class UserCreate(UserBase):
|
class UserCreate(UserBase):
|
||||||
@@ -29,7 +36,7 @@ class UserCreate(UserBase):
|
|||||||
class UserRead(UserBase):
|
class UserRead(UserBase):
|
||||||
"""用户读取模型"""
|
"""用户读取模型"""
|
||||||
|
|
||||||
id: int
|
id: UUID
|
||||||
|
|
||||||
|
|
||||||
class UserLogin(BaseModel):
|
class UserLogin(BaseModel):
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
from ..core.scheduler import TaskService
|
from ..core.scheduler import TaskService
|
||||||
from .execute_background import execute_background_task
|
from .execute_background import dev_init_admin
|
||||||
|
|
||||||
|
|
||||||
task_dict = {"execute_background_task": execute_background_task}
|
|
||||||
|
|
||||||
|
|
||||||
def get_task(task_name: str) -> Optional[Callable]:
|
|
||||||
return task_dict.get(task_name, None)
|
|
||||||
|
|
||||||
|
|
||||||
async def init_tasks_callback(task_service: TaskService):
|
async def init_tasks_callback(task_service: TaskService):
|
||||||
await task_service.add_interval_job(
|
await task_service.add_date_job(
|
||||||
execute_background_task,
|
dev_init_admin,
|
||||||
seconds=1,
|
run_date=None,
|
||||||
args=["task_name"],
|
args=[],
|
||||||
job_id="task_name",
|
job_id="task_name",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
from ..core.scheduler import logger
|
from ..core.scheduler import logger
|
||||||
|
from ..service.user import UserService
|
||||||
|
from ..core.scheduler import get_task_service_instance
|
||||||
|
|
||||||
|
|
||||||
async def execute_background_task(task_name: str):
|
async def dev_init_admin():
|
||||||
from datetime import datetime
|
task_service = get_task_service_instance()
|
||||||
|
if task_service.async_session_maker is None:
|
||||||
"""模拟后台任务执行"""
|
return
|
||||||
logger.info(f"Executing task: {task_name} at {datetime.utcnow()}")
|
async with task_service.async_session_maker() as session:
|
||||||
|
admin_user = await UserService.get_user_by_email("admin", session)
|
||||||
|
if admin_user: # type: ignore
|
||||||
|
logger.info("管理员用户已存在") # type: ignore
|
||||||
|
return
|
||||||
|
await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
|
||||||
|
|||||||
@@ -157,6 +157,8 @@ async def init_database():
|
|||||||
from ..app.core.config import get_config
|
from ..app.core.config import get_config
|
||||||
from sqlmodel import SQLModel, text
|
from sqlmodel import SQLModel, text
|
||||||
from sqlalchemy.exc import OperationalError
|
from sqlalchemy.exc import OperationalError
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||||
|
from ..app.service.user import UserService
|
||||||
|
|
||||||
database = get_config()["database"]
|
database = get_config()["database"]
|
||||||
database_type = database["type"]
|
database_type = database["type"]
|
||||||
@@ -167,6 +169,9 @@ async def init_database():
|
|||||||
engine = create_async_engine(
|
engine = create_async_engine(
|
||||||
"sqlite+aiosqlite:///./build/database.db", **database_config
|
"sqlite+aiosqlite:///./build/database.db", **database_config
|
||||||
)
|
)
|
||||||
|
async_session_maker = async_sessionmaker(
|
||||||
|
engine, class_=AsyncSession, expire_on_commit=False
|
||||||
|
)
|
||||||
|
|
||||||
async with engine.begin() as conn:
|
async with engine.begin() as conn:
|
||||||
if database_type == "sqlite":
|
if database_type == "sqlite":
|
||||||
@@ -176,9 +181,13 @@ async def init_database():
|
|||||||
await conn.run_sync(
|
await conn.run_sync(
|
||||||
lambda conn: SQLModel.metadata.create_all(bind=conn, checkfirst=True)
|
lambda conn: SQLModel.metadata.create_all(bind=conn, checkfirst=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
except OperationalError as e:
|
except OperationalError as e:
|
||||||
if "already exists" in str(e):
|
if "already exists" in str(e):
|
||||||
raise
|
raise
|
||||||
|
# 添加管理员用户
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
await UserService.create_user(session, "admin", "admin", is_superuser=True) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
|||||||
Reference in New Issue
Block a user