refactor api
This commit is contained in:
@@ -1,18 +1,10 @@
|
|||||||
# Vue 3 + TypeScript + Vite
|
# timi-web
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
Vue 3 通用组件库与工具库。业务 API、领域类型和用户状态位于 [timi-web-api](https://gitea.imyeyu.com/timi/timi-web-api)。
|
||||||
|
|
||||||
## Recommended IDE Setup
|
## 开发
|
||||||
|
|
||||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
```bash
|
||||||
|
pnpm install
|
||||||
## Type Support For `.vue` Imports in TS
|
pnpm run build
|
||||||
|
```
|
||||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
|
||||||
|
|
||||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
|
||||||
|
|
||||||
1. Disable the built-in TypeScript Extension
|
|
||||||
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
|
||||||
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
|
||||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
|
||||||
|
|||||||
@@ -9,15 +9,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import {axios, Copyright, SettingMapper} from "timi-web";
|
|
||||||
import CommonAPI from "../src/api/CommonAPI";
|
|
||||||
import PassTimeLabel from "../src/components/passtime-label/index.vue";
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.root {
|
.root {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
import {createApp} from "vue";
|
import {createApp} from "vue";
|
||||||
import Root from "./Root.vue";
|
import Root from "./Root.vue";
|
||||||
import {axios, Network, VPopup} from "timi-web";
|
import axios from "axios";
|
||||||
|
import {VPopup} from "timi-web";
|
||||||
|
|
||||||
axios.defaults.baseURL = import.meta.env.VITE_API;
|
axios.defaults.baseURL = import.meta.env.VITE_API;
|
||||||
axios.interceptors.request.use((config) => {
|
axios.interceptors.request.use((config) => {
|
||||||
|
|||||||
+1
-2
@@ -62,9 +62,7 @@
|
|||||||
"@tiptap/starter-kit": "^3.22.4",
|
"@tiptap/starter-kit": "^3.22.4",
|
||||||
"@tiptap/vue-3": "^3.22.4",
|
"@tiptap/vue-3": "^3.22.4",
|
||||||
"@marsio/vue-split-pane": "^1.0.0",
|
"@marsio/vue-split-pane": "^1.0.0",
|
||||||
"axios": "1.18.0",
|
|
||||||
"less": "4.5.1",
|
"less": "4.5.1",
|
||||||
"pinia": "^3.0.2",
|
|
||||||
"marked": "^17.0.1",
|
"marked": "^17.0.1",
|
||||||
"marked-gfm-heading-id": "^4.1.3",
|
"marked-gfm-heading-id": "^4.1.3",
|
||||||
"marked-highlight": "^2.2.3",
|
"marked-highlight": "^2.2.3",
|
||||||
@@ -78,6 +76,7 @@
|
|||||||
"@types/prismjs": "1.26.5",
|
"@types/prismjs": "1.26.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
||||||
"@typescript-eslint/parser": "^8.53.0",
|
"@typescript-eslint/parser": "^8.53.0",
|
||||||
|
"axios": "1.18.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.3",
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
"@vitest/coverage-v8": "^4.1.4",
|
"@vitest/coverage-v8": "^4.1.4",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
|
|||||||
Generated
+4
-102
@@ -62,9 +62,6 @@ importers:
|
|||||||
'@tiptap/vue-3':
|
'@tiptap/vue-3':
|
||||||
specifier: ^3.22.4
|
specifier: ^3.22.4
|
||||||
version: 3.30.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)(vue@3.5.26(typescript@5.9.3))
|
version: 3.30.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.30.2(@tiptap/pm@3.30.2))(@tiptap/pm@3.30.2)(vue@3.5.26(typescript@5.9.3))
|
||||||
axios:
|
|
||||||
specifier: 1.18.0
|
|
||||||
version: 1.18.0
|
|
||||||
less:
|
less:
|
||||||
specifier: 4.5.1
|
specifier: 4.5.1
|
||||||
version: 4.5.1
|
version: 4.5.1
|
||||||
@@ -80,9 +77,6 @@ importers:
|
|||||||
marked-mangle:
|
marked-mangle:
|
||||||
specifier: ^1.1.12
|
specifier: ^1.1.12
|
||||||
version: 1.1.12(marked@17.0.1)
|
version: 1.1.12(marked@17.0.1)
|
||||||
pinia:
|
|
||||||
specifier: ^3.0.2
|
|
||||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
|
|
||||||
prismjs:
|
prismjs:
|
||||||
specifier: 1.30.0
|
specifier: 1.30.0
|
||||||
version: 1.30.0
|
version: 1.30.0
|
||||||
@@ -114,6 +108,9 @@ importers:
|
|||||||
'@vue/test-utils':
|
'@vue/test-utils':
|
||||||
specifier: ^2.4.6
|
specifier: ^2.4.6
|
||||||
version: 2.4.6
|
version: 2.4.6
|
||||||
|
axios:
|
||||||
|
specifier: 1.18.0
|
||||||
|
version: 1.18.0
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.39.2
|
specifier: ^9.39.2
|
||||||
version: 9.39.2
|
version: 9.39.2
|
||||||
@@ -1177,15 +1174,6 @@ packages:
|
|||||||
'@vue/compiler-vue2@2.7.16':
|
'@vue/compiler-vue2@2.7.16':
|
||||||
resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
|
resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
|
||||||
|
|
||||||
'@vue/devtools-api@7.7.10':
|
|
||||||
resolution: {integrity: sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==}
|
|
||||||
|
|
||||||
'@vue/devtools-kit@7.7.10':
|
|
||||||
resolution: {integrity: sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==}
|
|
||||||
|
|
||||||
'@vue/devtools-shared@7.7.10':
|
|
||||||
resolution: {integrity: sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==}
|
|
||||||
|
|
||||||
'@vue/language-core@2.2.0':
|
'@vue/language-core@2.2.0':
|
||||||
resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==}
|
resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1328,9 +1316,6 @@ packages:
|
|||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
birpc@2.9.0:
|
|
||||||
resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==}
|
|
||||||
|
|
||||||
boolbase@1.0.0:
|
boolbase@1.0.0:
|
||||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||||
|
|
||||||
@@ -1427,10 +1412,6 @@ packages:
|
|||||||
copy-anything@2.0.6:
|
copy-anything@2.0.6:
|
||||||
resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==}
|
resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==}
|
||||||
|
|
||||||
copy-anything@4.0.5:
|
|
||||||
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
cosmiconfig@7.1.0:
|
cosmiconfig@7.1.0:
|
||||||
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
|
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1620,6 +1601,7 @@ packages:
|
|||||||
eslint@9.39.2:
|
eslint@9.39.2:
|
||||||
resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
|
resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
jiti: '*'
|
jiti: '*'
|
||||||
@@ -1802,9 +1784,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
hookable@5.5.3:
|
|
||||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
|
||||||
|
|
||||||
html-escaper@2.0.2:
|
html-escaper@2.0.2:
|
||||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||||
|
|
||||||
@@ -1871,10 +1850,6 @@ packages:
|
|||||||
is-what@3.14.1:
|
is-what@3.14.1:
|
||||||
resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
|
resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
|
||||||
|
|
||||||
is-what@5.5.0:
|
|
||||||
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
isexe@2.0.0:
|
isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
@@ -2144,9 +2119,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
mitt@3.0.1:
|
|
||||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
|
||||||
|
|
||||||
mlly@1.7.4:
|
mlly@1.7.4:
|
||||||
resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
|
resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
|
||||||
|
|
||||||
@@ -2249,9 +2221,6 @@ packages:
|
|||||||
pathe@2.0.3:
|
pathe@2.0.3:
|
||||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
|
||||||
perfect-debounce@1.0.0:
|
|
||||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
|
||||||
|
|
||||||
picocolors@1.0.0:
|
picocolors@1.0.0:
|
||||||
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
||||||
|
|
||||||
@@ -2274,15 +2243,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
|
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
pinia@3.0.4:
|
|
||||||
resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
|
|
||||||
peerDependencies:
|
|
||||||
typescript: '>=4.5.0'
|
|
||||||
vue: ^3.5.11
|
|
||||||
peerDependenciesMeta:
|
|
||||||
typescript:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
pkg-types@1.3.1:
|
pkg-types@1.3.1:
|
||||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||||
|
|
||||||
@@ -2400,9 +2360,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
|
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
rfdc@1.4.1:
|
|
||||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
|
||||||
|
|
||||||
rolldown@1.0.0-rc.12:
|
rolldown@1.0.0-rc.12:
|
||||||
resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
|
resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -2486,10 +2443,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
|
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
|
||||||
deprecated: Please use @jridgewell/sourcemap-codec instead
|
deprecated: Please use @jridgewell/sourcemap-codec instead
|
||||||
|
|
||||||
speakingurl@14.0.1:
|
|
||||||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
|
|
||||||
sprintf-js@1.0.3:
|
sprintf-js@1.0.3:
|
||||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||||
|
|
||||||
@@ -2529,10 +2482,6 @@ packages:
|
|||||||
stylis@4.2.0:
|
stylis@4.2.0:
|
||||||
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
|
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
|
||||||
|
|
||||||
superjson@2.2.6:
|
|
||||||
resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==}
|
|
||||||
engines: {node: '>=16'}
|
|
||||||
|
|
||||||
supports-color@5.5.0:
|
supports-color@5.5.0:
|
||||||
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -3949,24 +3898,6 @@ snapshots:
|
|||||||
de-indent: 1.0.2
|
de-indent: 1.0.2
|
||||||
he: 1.2.0
|
he: 1.2.0
|
||||||
|
|
||||||
'@vue/devtools-api@7.7.10':
|
|
||||||
dependencies:
|
|
||||||
'@vue/devtools-kit': 7.7.10
|
|
||||||
|
|
||||||
'@vue/devtools-kit@7.7.10':
|
|
||||||
dependencies:
|
|
||||||
'@vue/devtools-shared': 7.7.10
|
|
||||||
birpc: 2.9.0
|
|
||||||
hookable: 5.5.3
|
|
||||||
mitt: 3.0.1
|
|
||||||
perfect-debounce: 1.0.0
|
|
||||||
speakingurl: 14.0.1
|
|
||||||
superjson: 2.2.6
|
|
||||||
|
|
||||||
'@vue/devtools-shared@7.7.10':
|
|
||||||
dependencies:
|
|
||||||
rfdc: 1.4.1
|
|
||||||
|
|
||||||
'@vue/language-core@2.2.0(typescript@5.9.3)':
|
'@vue/language-core@2.2.0(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@volar/language-core': 2.4.11
|
'@volar/language-core': 2.4.11
|
||||||
@@ -4124,8 +4055,6 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
birpc@2.9.0: {}
|
|
||||||
|
|
||||||
boolbase@1.0.0: {}
|
boolbase@1.0.0: {}
|
||||||
|
|
||||||
brace-expansion@1.1.11:
|
brace-expansion@1.1.11:
|
||||||
@@ -4215,10 +4144,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-what: 3.14.1
|
is-what: 3.14.1
|
||||||
|
|
||||||
copy-anything@4.0.5:
|
|
||||||
dependencies:
|
|
||||||
is-what: 5.5.0
|
|
||||||
|
|
||||||
cosmiconfig@7.1.0:
|
cosmiconfig@7.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/parse-json': 4.0.2
|
'@types/parse-json': 4.0.2
|
||||||
@@ -4559,8 +4484,6 @@ snapshots:
|
|||||||
|
|
||||||
he@1.2.0: {}
|
he@1.2.0: {}
|
||||||
|
|
||||||
hookable@5.5.3: {}
|
|
||||||
|
|
||||||
html-escaper@2.0.2: {}
|
html-escaper@2.0.2: {}
|
||||||
|
|
||||||
https-proxy-agent@5.0.1:
|
https-proxy-agent@5.0.1:
|
||||||
@@ -4615,8 +4538,6 @@ snapshots:
|
|||||||
|
|
||||||
is-what@3.14.1: {}
|
is-what@3.14.1: {}
|
||||||
|
|
||||||
is-what@5.5.0: {}
|
|
||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
istanbul-lib-coverage@3.2.2: {}
|
istanbul-lib-coverage@3.2.2: {}
|
||||||
@@ -4860,8 +4781,6 @@ snapshots:
|
|||||||
|
|
||||||
minipass@7.1.3: {}
|
minipass@7.1.3: {}
|
||||||
|
|
||||||
mitt@3.0.1: {}
|
|
||||||
|
|
||||||
mlly@1.7.4:
|
mlly@1.7.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.14.1
|
acorn: 8.14.1
|
||||||
@@ -4957,8 +4876,6 @@ snapshots:
|
|||||||
|
|
||||||
pathe@2.0.3: {}
|
pathe@2.0.3: {}
|
||||||
|
|
||||||
perfect-debounce@1.0.0: {}
|
|
||||||
|
|
||||||
picocolors@1.0.0: {}
|
picocolors@1.0.0: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
@@ -4972,13 +4889,6 @@ snapshots:
|
|||||||
pify@4.0.1:
|
pify@4.0.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)):
|
|
||||||
dependencies:
|
|
||||||
'@vue/devtools-api': 7.7.10
|
|
||||||
vue: 3.5.26(typescript@5.9.3)
|
|
||||||
optionalDependencies:
|
|
||||||
typescript: 5.9.3
|
|
||||||
|
|
||||||
pkg-types@1.3.1:
|
pkg-types@1.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
confbox: 0.1.8
|
confbox: 0.1.8
|
||||||
@@ -5129,8 +5039,6 @@ snapshots:
|
|||||||
path-parse: 1.0.7
|
path-parse: 1.0.7
|
||||||
supports-preserve-symlinks-flag: 1.0.0
|
supports-preserve-symlinks-flag: 1.0.0
|
||||||
|
|
||||||
rfdc@1.4.1: {}
|
|
||||||
|
|
||||||
rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2):
|
rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@oxc-project/types': 0.122.0
|
'@oxc-project/types': 0.122.0
|
||||||
@@ -5235,8 +5143,6 @@ snapshots:
|
|||||||
|
|
||||||
sourcemap-codec@1.4.8: {}
|
sourcemap-codec@1.4.8: {}
|
||||||
|
|
||||||
speakingurl@14.0.1: {}
|
|
||||||
|
|
||||||
sprintf-js@1.0.3: {}
|
sprintf-js@1.0.3: {}
|
||||||
|
|
||||||
stackback@0.0.2: {}
|
stackback@0.0.2: {}
|
||||||
@@ -5273,10 +5179,6 @@ snapshots:
|
|||||||
|
|
||||||
stylis@4.2.0: {}
|
stylis@4.2.0: {}
|
||||||
|
|
||||||
superjson@2.2.6:
|
|
||||||
dependencies:
|
|
||||||
copy-anything: 4.0.5
|
|
||||||
|
|
||||||
supports-color@5.5.0:
|
supports-color@5.5.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 3.0.0
|
has-flag: 3.0.0
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { Article } from "../types";
|
|
||||||
import { axios } from "./BaseAPI";
|
|
||||||
|
|
||||||
export const ArticleAPI = {
|
|
||||||
/**
|
|
||||||
* 获取文章
|
|
||||||
*
|
|
||||||
* @param id 文章 ID
|
|
||||||
* @returns 文章数据
|
|
||||||
*/
|
|
||||||
async view(id: string): Promise<Article> {
|
|
||||||
return axios.get(`/article/${id}`);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 喜欢文章,后端有限调用,1240ms 一次
|
|
||||||
*
|
|
||||||
* @param id 文章 ID
|
|
||||||
* @returns 最新喜欢数量
|
|
||||||
*/
|
|
||||||
async like(id: string): Promise<number> {
|
|
||||||
return axios.get(`/article/like/${id}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ArticleAPI;
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { Attachment, BizIdUpdate, TempFile } from "../types";
|
|
||||||
import { axios } from "./BaseAPI";
|
|
||||||
import CommonAPI from "./CommonAPI";
|
|
||||||
|
|
||||||
const BASE_URI = "/attach";
|
|
||||||
|
|
||||||
export const AttachmentAPI = {
|
|
||||||
/**
|
|
||||||
* 上传临时附件
|
|
||||||
*
|
|
||||||
* @param file 文件或文件列表
|
|
||||||
* @param ttl 有效期
|
|
||||||
* @returns 临时附件列表
|
|
||||||
*/
|
|
||||||
async upload(file: File | File[], ttl?: string): Promise<TempFile[]> {
|
|
||||||
const formData = new FormData();
|
|
||||||
const fileList = Array.isArray(file) ? file : [file];
|
|
||||||
for (const item of fileList) {
|
|
||||||
formData.append("file", item);
|
|
||||||
}
|
|
||||||
if (ttl) {
|
|
||||||
formData.append("ttl", ttl);
|
|
||||||
}
|
|
||||||
return await axios.post(`${BASE_URI}/temp/upload`, formData);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 按业务差分更新附件
|
|
||||||
*
|
|
||||||
* @param req 更新请求
|
|
||||||
*/
|
|
||||||
async updateByBizId(req: BizIdUpdate): Promise<void> {
|
|
||||||
return await axios.post(`${BASE_URI}/update/biz/id`, req);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 按业务查询附件列表
|
|
||||||
*
|
|
||||||
* @param bizType 业务类型
|
|
||||||
* @param bizId 业务 ID
|
|
||||||
* @param attachTypeList 附件类型列表
|
|
||||||
*/
|
|
||||||
async listByBizId(bizType: string, bizId: string, attachTypeList?: string[]): Promise<Attachment[]> {
|
|
||||||
return await axios.get(`${BASE_URI}/list/biz/id`, {
|
|
||||||
params: {
|
|
||||||
bizType,
|
|
||||||
bizId,
|
|
||||||
attachTypeList
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getReadURL(id: string): string {
|
|
||||||
return CommonAPI.getAttachmentReadAPI(id);
|
|
||||||
},
|
|
||||||
|
|
||||||
getTempReadURL(id: string): string {
|
|
||||||
return CommonAPI.getAttachmentTempReadAPI(id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AttachmentAPI;
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import Network from "../utils/Network";
|
|
||||||
|
|
||||||
let serviceAxios: any | null = null;
|
|
||||||
|
|
||||||
function getServiceAxios(): any {
|
|
||||||
if (serviceAxios === null) {
|
|
||||||
serviceAxios = Network.axios;
|
|
||||||
}
|
|
||||||
return serviceAxios;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const axios = new Proxy({} as any, {
|
|
||||||
get(_target: any, prop: string | symbol, receiver: any): any {
|
|
||||||
return Reflect.get(getServiceAxios(), prop, receiver);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { CaptchaData, Comment, Page, PageResult } from "../types";
|
|
||||||
import { axios } from "./BaseAPI";
|
|
||||||
|
|
||||||
const BASE_URI = "/comment";
|
|
||||||
|
|
||||||
export const CommentAPI = {
|
|
||||||
async page(page: Page<Comment>): Promise<PageResult<Comment>> {
|
|
||||||
return axios.post(`${BASE_URI}/list`, page);
|
|
||||||
},
|
|
||||||
async create(captchaData: CaptchaData<Comment>): Promise<void> {
|
|
||||||
return axios.post(`${BASE_URI}/create`, captchaData);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CommentAPI;
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { CaptchaResult, Setting } from "../types";
|
|
||||||
import { axios } from "./BaseAPI";
|
|
||||||
|
|
||||||
function getBaseURI(): string {
|
|
||||||
const baseURL = axios.defaults.baseURL;
|
|
||||||
if (!baseURL || baseURL === "undefined") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return baseURL.replace(/\/$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CommonAPI = {
|
|
||||||
getCaptchaAPI(): string {
|
|
||||||
return `${getBaseURI()}/captcha`;
|
|
||||||
},
|
|
||||||
async captcha(width: number, height: number): Promise<CaptchaResult> {
|
|
||||||
return await axios.get(`/captcha?width=${width}&height=${height}`);
|
|
||||||
},
|
|
||||||
getAttachmentReadAPI(id: string): string {
|
|
||||||
return `${getBaseURI()}/attach/read/${id}`;
|
|
||||||
},
|
|
||||||
getAttachmentTempReadAPI(id: string): string {
|
|
||||||
return `${getBaseURI()}/attach/temp/read?id=${id}`;
|
|
||||||
},
|
|
||||||
async settingMap(map: Record<string, string[]>): Promise<Map<string, Map<string, Setting>>> {
|
|
||||||
const raw = await axios.post("/setting/map", map);
|
|
||||||
const moduleMap = new Map<string, Map<string, Setting>>();
|
|
||||||
if (!raw || typeof raw !== "object") {
|
|
||||||
return moduleMap;
|
|
||||||
}
|
|
||||||
for (const [module, settingRawMap] of Object.entries(raw as Record<string, unknown>)) {
|
|
||||||
if (!settingRawMap || typeof settingRawMap !== "object") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const settingMap = new Map<string, Setting>();
|
|
||||||
for (const [key, setting] of Object.entries(settingRawMap as Record<string, Setting>)) {
|
|
||||||
settingMap.set(key, setting);
|
|
||||||
}
|
|
||||||
moduleMap.set(module, settingMap);
|
|
||||||
}
|
|
||||||
return moduleMap;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CommonAPI;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Developer } from "../types";
|
|
||||||
import { axios } from "./BaseAPI";
|
|
||||||
|
|
||||||
const BASE_URI = "/git/developer";
|
|
||||||
|
|
||||||
export const DeveloperAPI = {
|
|
||||||
async get(): Promise<Developer> {
|
|
||||||
return axios.post(`${BASE_URI}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DeveloperAPI;
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { axios } from "./BaseAPI";
|
|
||||||
import type { CaptchaData, Notify, NotifyDetail, Page, PageResult } from "../types";
|
|
||||||
|
|
||||||
const BASE_URI = "/notify/internal";
|
|
||||||
|
|
||||||
export const NotifyAPI = {
|
|
||||||
|
|
||||||
async smsSend(captchaData: CaptchaData<Notify>) {
|
|
||||||
return await axios.post("/notify/sms/captcha/create", captchaData);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 查询当前用户的站内通知详情。 */
|
|
||||||
async internalDetail(id: string): Promise<NotifyDetail> {
|
|
||||||
return await axios.post(`${BASE_URI}/detail`, undefined, {
|
|
||||||
params: { id }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 查询当前用户的站内通知。 */
|
|
||||||
async listInternal(page?: Page<NotifyDetail>, unreadOnly = false): Promise<PageResult<NotifyDetail>> {
|
|
||||||
return await axios.post(`${BASE_URI}/internal`, page, {
|
|
||||||
params: { unreadOnly }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 查询当前用户未读站内通知数量。 */
|
|
||||||
async countUnreadInternal(): Promise<number> {
|
|
||||||
return await axios.post(`${BASE_URI}/unread/count`);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 标记当前用户的一条站内通知已读。 */
|
|
||||||
async markInternalRead(id: string): Promise<void> {
|
|
||||||
await axios.post(`${BASE_URI}/read`, undefined, {
|
|
||||||
params: { id }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 删除当前用户的一条站内通知。 */
|
|
||||||
async deleteInternal(id: string): Promise<void> {
|
|
||||||
await axios.post(`${BASE_URI}/delete`, undefined, {
|
|
||||||
params: { id }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 标记当前用户全部站内通知已读。 */
|
|
||||||
async markAllInternalRead(): Promise<void> {
|
|
||||||
await axios.post(`${BASE_URI}/read/all`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NotifyAPI;
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { axios } from "./BaseAPI";
|
|
||||||
import type { ModuleCode, Page, PageResult, Permission, PermissionPayload } from "../types";
|
|
||||||
|
|
||||||
const BASE_URI = "/user/permission";
|
|
||||||
|
|
||||||
export const PermissionAPI = {
|
|
||||||
async list(req: Page<Permission>): Promise<PageResult<Permission>> {
|
|
||||||
return axios.post(`${BASE_URI}/list`, req);
|
|
||||||
},
|
|
||||||
async create(req: PermissionPayload): Promise<void> {
|
|
||||||
return axios.post(`${BASE_URI}/create`, req);
|
|
||||||
},
|
|
||||||
async update(req: PermissionPayload): Promise<void> {
|
|
||||||
return axios.post(`${BASE_URI}/update`, req);
|
|
||||||
},
|
|
||||||
async remove(id: string): Promise<void> {
|
|
||||||
return axios.post(`${BASE_URI}/delete`, undefined, {
|
|
||||||
params: { id }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/** 查询当前登录用户在指定模块内可转授的权限。 */
|
|
||||||
async grantableList(moduleCode: ModuleCode): Promise<Permission[]> {
|
|
||||||
return await axios.post(`${BASE_URI}/grantable/list`, undefined, {
|
|
||||||
params: { moduleCode }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PermissionAPI;
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import { axios } from "./BaseAPI";
|
|
||||||
import type { ModuleCode, Page, PageResult, Permission, Role, RolePayload, AuthorizeUserRole } from "../types";
|
|
||||||
|
|
||||||
const BASE_URI = "/user/role";
|
|
||||||
|
|
||||||
export const RoleAPI = {
|
|
||||||
async list(req: Page<Role>): Promise<PageResult<Role>> {
|
|
||||||
return axios.post(`${BASE_URI}/list`, req);
|
|
||||||
},
|
|
||||||
async detail(id: string): Promise<Role> {
|
|
||||||
return axios.post(`${BASE_URI}/detail`, undefined, {
|
|
||||||
params: { id }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async create(req: RolePayload): Promise<void> {
|
|
||||||
return axios.post(`${BASE_URI}/create`, req);
|
|
||||||
},
|
|
||||||
async update(req: RolePayload): Promise<void> {
|
|
||||||
return axios.post(`${BASE_URI}/update`, req);
|
|
||||||
},
|
|
||||||
async remove(id: string): Promise<void> {
|
|
||||||
return axios.post(`${BASE_URI}/delete`, undefined, {
|
|
||||||
params: { id }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/** 查询当前登录用户在模块内可管理的角色。 */
|
|
||||||
async manageableList(moduleCode: ModuleCode): Promise<Role[]> {
|
|
||||||
return await axios.post(`${BASE_URI}/manageable/list`, undefined, {
|
|
||||||
params: { moduleCode }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/** 查询当前登录用户在模块内可授权给账号的角色。 */
|
|
||||||
async grantableList(moduleCode: ModuleCode): Promise<Role[]> {
|
|
||||||
return await axios.post(`${BASE_URI}/grantable/list`, undefined, {
|
|
||||||
params: { moduleCode }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/** 查询当前登录用户直属角色详情,不按可管理范围过滤。 */
|
|
||||||
async currentDetailList(moduleCode?: ModuleCode): Promise<Role[]> {
|
|
||||||
return await axios.post(`${BASE_URI}/current/detail/list`, undefined, {
|
|
||||||
params: { moduleCode }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/** 查询当前登录用户可以给目标角色配置的直接权限 P 候选项。 */
|
|
||||||
async grantablePermissionList(roleId: string): Promise<Permission[]> {
|
|
||||||
return await axios.post(`${BASE_URI}/permission/grantable/list`, undefined, {
|
|
||||||
params: { roleId }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/** 查询目标角色当前可继续转授的权限 D。 */
|
|
||||||
async delegationList(roleId: string): Promise<Permission[]> {
|
|
||||||
return await axios.post(`${BASE_URI}/delegation/list`, undefined, {
|
|
||||||
params: { roleId }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/** 查询当前登录用户可以给目标角色配置的 D 候选项。 */
|
|
||||||
async grantableDelegationList(roleId: string): Promise<Permission[]> {
|
|
||||||
return await axios.post(`${BASE_URI}/delegation/grantable/list`, undefined, {
|
|
||||||
params: { roleId }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/** 覆盖保存目标角色可继续转授的权限 D。 */
|
|
||||||
async updateDelegation(req: Pick<RolePayload, "id" | "delegationPermissionIdList">): Promise<void> {
|
|
||||||
await axios.post(`${BASE_URI}/delegation/update`, req);
|
|
||||||
},
|
|
||||||
/** 查询目标角色可选管理父级。 */
|
|
||||||
async parentGrantableList(roleId: string): Promise<Role[]> {
|
|
||||||
return await axios.post(`${BASE_URI}/parent/grantable/list`, undefined, {
|
|
||||||
params: { roleId }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
/** 保存目标角色管理父级。 */
|
|
||||||
async updateParent(req: Pick<RolePayload, "id" | "parentRoleId">): Promise<void> {
|
|
||||||
await axios.post(`${BASE_URI}/parent/update`, req);
|
|
||||||
},
|
|
||||||
async authorizedList(userId: string, moduleCode?: ModuleCode): Promise<Role[]> {
|
|
||||||
return axios.post(`${BASE_URI}/authorized/list`, undefined, {
|
|
||||||
params: {
|
|
||||||
userId,
|
|
||||||
moduleCode
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async authorize(req: AuthorizeUserRole): Promise<void> {
|
|
||||||
return axios.post(`${BASE_URI}/authorized/create`, req);
|
|
||||||
},
|
|
||||||
async authorizedPermission(userId: string): Promise<Permission[]> {
|
|
||||||
return axios.post(`${BASE_URI}/authorized/permission`, undefined, {
|
|
||||||
params: { userId }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RoleAPI;
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import type {Attachment, CaptchaData, Gender, LoginRequest, LoginResponse, RegisterRequest, SmsLoginRequest, UpdatePasswordRequest, User, UserAttachType} from "../types";
|
|
||||||
import {axios} from "./BaseAPI";
|
|
||||||
import CommonAPI from "./CommonAPI";
|
|
||||||
|
|
||||||
const BASE_URI = "/user";
|
|
||||||
type UserAttachmentOwner = {
|
|
||||||
attachmentList?: readonly Attachment[];
|
|
||||||
};
|
|
||||||
type UpdateCurrentRequest = Omit<Partial<User>, "gender"> & {
|
|
||||||
gender?: Gender | "";
|
|
||||||
};
|
|
||||||
|
|
||||||
function findAttachmentByType(attachmentList: readonly Attachment[], ...types: UserAttachType[]) {
|
|
||||||
for (let i = 0; i < attachmentList.length; i++) {
|
|
||||||
const attachType = attachmentList[i].attachType as UserAttachType | undefined;
|
|
||||||
const id = attachmentList[i].id;
|
|
||||||
if (id && attachType && types.includes(attachType)) {
|
|
||||||
return CommonAPI.getAttachmentReadAPI(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserAPI = {
|
|
||||||
async register(req: CaptchaData<RegisterRequest>): Promise<LoginResponse> {
|
|
||||||
return await axios.post(`${BASE_URI}/register`, req);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 登录
|
|
||||||
*
|
|
||||||
* @param req 验证码登录对象
|
|
||||||
* @returns LoginResponse
|
|
||||||
*/
|
|
||||||
async login(req: CaptchaData<LoginRequest>): Promise<LoginResponse> {
|
|
||||||
return await axios.post(`${BASE_URI}/login`, req);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 短信验证码登录
|
|
||||||
*
|
|
||||||
* @param req 短信验证码登录请求
|
|
||||||
* @returns LoginResponse
|
|
||||||
*/
|
|
||||||
async loginBySms(req: SmsLoginRequest): Promise<LoginResponse> {
|
|
||||||
return await axios.post(`${BASE_URI}/login/sms`, req);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 验证是否已登录
|
|
||||||
*
|
|
||||||
* @returns true 为已登录
|
|
||||||
*/
|
|
||||||
async loginByToken(): Promise<LoginResponse> {
|
|
||||||
return await axios.post(`${BASE_URI}/login/token`);
|
|
||||||
},
|
|
||||||
async logout(): Promise<void> {
|
|
||||||
return await axios.post(`${BASE_URI}/logout`);
|
|
||||||
},
|
|
||||||
async updateCurrent(req: UpdateCurrentRequest): Promise<LoginResponse> {
|
|
||||||
return await axios.post(`${BASE_URI}/current/update`, req);
|
|
||||||
},
|
|
||||||
async updatePassword(req: UpdatePasswordRequest): Promise<void> {
|
|
||||||
return await axios.post(`${BASE_URI}/update/password`, req);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 获取用户数据
|
|
||||||
*
|
|
||||||
* @param id 用户 ID
|
|
||||||
* @returns 用户数据
|
|
||||||
*/
|
|
||||||
async view(id: string): Promise<User> {
|
|
||||||
return await axios.post(`${BASE_URI}/view/${id}`);
|
|
||||||
},
|
|
||||||
getAvatarURL(user?: UserAttachmentOwner) {
|
|
||||||
if (user?.attachmentList) {
|
|
||||||
return findAttachmentByType(user.attachmentList, "AVATAR");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getWrapperURL(user?: UserAttachmentOwner) {
|
|
||||||
if (user?.attachmentList) {
|
|
||||||
return findAttachmentByType(user.attachmentList, "WRAPPER");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UserAPI;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export { ArticleAPI } from "./ArticleAPI";
|
|
||||||
export { AttachmentAPI } from "./AttachmentAPI";
|
|
||||||
export { CommentAPI } from "./CommentAPI";
|
|
||||||
export { CommonAPI } from "./CommonAPI";
|
|
||||||
export { DeveloperAPI } from "./DeveloperAPI";
|
|
||||||
export { NotifyAPI } from "./NotifyAPI";
|
|
||||||
export { PermissionAPI } from "./PermissionAPI";
|
|
||||||
export { RoleAPI } from "./RoleAPI";
|
|
||||||
export { UserAPI } from "./UserAPI";
|
|
||||||
@@ -3,18 +3,16 @@
|
|||||||
.custom-scroll-bar *::-webkit-scrollbar {
|
.custom-scroll-bar *::-webkit-scrollbar {
|
||||||
width: 10px !important;
|
width: 10px !important;
|
||||||
height: 10px !important;
|
height: 10px !important;
|
||||||
cursor: var(--tui-cur-default);
|
|
||||||
background: #CFD2E0;
|
background: #CFD2E0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scroll-bar *::-webkit-scrollbar-corner {
|
.custom-scroll-bar *::-webkit-scrollbar-corner {
|
||||||
background: #CFD2E0;
|
background: #CFD2E0;
|
||||||
cursor: var(--tui-cur-default);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scroll-bar *::-webkit-scrollbar-thumb {
|
.custom-scroll-bar *::-webkit-scrollbar-thumb {
|
||||||
cursor: var(--tui-cur-default);
|
|
||||||
background: #525870;
|
background: #525870;
|
||||||
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::selection {
|
*::selection {
|
||||||
@@ -23,12 +21,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
cursor: var(--tui-cur-default);
|
font-family: pingfang sc, microsoft yahei, arial regular, serif;
|
||||||
text-underline-offset: .16rem;
|
text-underline-offset: .16rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
width: 100% !important;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
@@ -38,17 +35,10 @@ body {
|
|||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: @tuiColors[blue];
|
color: @tuiColors[blue];
|
||||||
cursor: var(--tui-cur-pointer);
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -71,20 +61,11 @@ input, select, textarea {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
label,
|
|
||||||
select,
|
|
||||||
input[type="radio"],
|
|
||||||
input[type="file"],
|
|
||||||
input[type="checkbox"] {
|
|
||||||
cursor: var(--tui-cur-default);
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea,
|
textarea,
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="date"],
|
input[type="date"],
|
||||||
input[type="email"],
|
input[type="email"],
|
||||||
input[type="password"] {
|
input[type="password"] {
|
||||||
cursor: var(--tui-cur-text);
|
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
|
||||||
&:-webkit-autofill,
|
&:-webkit-autofill,
|
||||||
|
|||||||
@@ -58,3 +58,13 @@ each(@tuiColors, {
|
|||||||
background: @value;
|
background: @value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
.build-width-class(@width, @max) when (@width =< @max) {
|
||||||
|
.w-@{width} {
|
||||||
|
width: unit((@width / 16), rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-width-class((@width + 10), @max);
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-width-class(80, 600);
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import view from "./index.vue";
|
||||||
|
import Toolkit from "../../utils/Toolkit";
|
||||||
|
|
||||||
|
export const AnimatedNumber = Toolkit.withInstall(view);
|
||||||
|
export default AnimatedNumber;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
<template>
|
||||||
|
<span class="tui-animated-number">{{ displayValue }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineOptions({
|
||||||
|
name: "AnimatedNumber"
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
/** 目标数值 */
|
||||||
|
value: number;
|
||||||
|
/** 动画持续时间(毫秒),默认 1000ms */
|
||||||
|
duration?: number;
|
||||||
|
/** 小数位数,undefined 表示自动 */
|
||||||
|
decimals?: number;
|
||||||
|
/** 缓动函数类型 */
|
||||||
|
easing?: "linear" | "easeOut" | "easeInOut";
|
||||||
|
/** 是否启用千分位分隔符 */
|
||||||
|
thousands?: boolean;
|
||||||
|
/** 千分位分隔符,默认逗号 */
|
||||||
|
separator?: string;
|
||||||
|
}>(), {
|
||||||
|
duration: 1000,
|
||||||
|
easing: "easeOut",
|
||||||
|
thousands: false,
|
||||||
|
separator: ","
|
||||||
|
});
|
||||||
|
|
||||||
|
const { value, duration, decimals, easing, thousands, separator } = toRefs(props);
|
||||||
|
|
||||||
|
// 当前显示的数值
|
||||||
|
const currentValue = ref(value.value);
|
||||||
|
// 格式化后的显示值
|
||||||
|
const displayValue = ref(formatNumber(value.value));
|
||||||
|
// 动画相关
|
||||||
|
let animationFrameId: number | null = null;
|
||||||
|
let startValue = value.value;
|
||||||
|
let startTime: number | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓动函数
|
||||||
|
*/
|
||||||
|
function ease(t: number, type: string): number {
|
||||||
|
switch (type) {
|
||||||
|
case "linear":
|
||||||
|
return t;
|
||||||
|
case "easeOut":
|
||||||
|
return 1 - Math.pow(1 - t, 3);
|
||||||
|
case "easeInOut":
|
||||||
|
return t < 0.5
|
||||||
|
? 4 * t * t * t
|
||||||
|
: 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||||
|
default:
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加千分位分隔符
|
||||||
|
*/
|
||||||
|
function addThousandsSeparator(numStr: string, sep: string): string {
|
||||||
|
const parts = numStr.split(".");
|
||||||
|
const integerPart = parts[0];
|
||||||
|
const decimalPart = parts[1] ? "." + parts[1] : "";
|
||||||
|
|
||||||
|
// 处理负号
|
||||||
|
const isNegative = integerPart.startsWith("-");
|
||||||
|
const absoluteInteger = isNegative ? integerPart.slice(1) : integerPart;
|
||||||
|
|
||||||
|
// 对整数部分添加千分位分隔符
|
||||||
|
const formatted = absoluteInteger.replace(/\B(?=(\d{3})+(?!\d))/g, sep);
|
||||||
|
|
||||||
|
return (isNegative ? "-" : "") + formatted + decimalPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化数字
|
||||||
|
*/
|
||||||
|
function formatNumber(num: number): string {
|
||||||
|
let result: string;
|
||||||
|
|
||||||
|
if (decimals.value !== undefined) {
|
||||||
|
result = num.toFixed(decimals.value);
|
||||||
|
} else {
|
||||||
|
// 自动检测小数位数
|
||||||
|
const str = num.toString();
|
||||||
|
if (str.includes(".")) {
|
||||||
|
// 保留原有的小数位数,但最多保留 6 位
|
||||||
|
const decimalPart = str.split(".")[1];
|
||||||
|
const decimalLength = Math.min(decimalPart.length, 6);
|
||||||
|
result = num.toFixed(decimalLength);
|
||||||
|
} else {
|
||||||
|
result = num.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用千分位分隔符
|
||||||
|
if (thousands.value) {
|
||||||
|
result = addThousandsSeparator(result, separator.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动画更新函数
|
||||||
|
*/
|
||||||
|
function animate(timestamp: number) {
|
||||||
|
if (startTime === null) {
|
||||||
|
startTime = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = timestamp - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration.value, 1);
|
||||||
|
const easedProgress = ease(progress, easing.value);
|
||||||
|
|
||||||
|
// 计算当前值
|
||||||
|
currentValue.value = startValue + (value.value - startValue) * easedProgress;
|
||||||
|
displayValue.value = formatNumber(currentValue.value);
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
|
} else {
|
||||||
|
// 动画结束,确保显示精确的目标值
|
||||||
|
currentValue.value = value.value;
|
||||||
|
displayValue.value = formatNumber(value.value);
|
||||||
|
animationFrameId = null;
|
||||||
|
startTime = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始动画
|
||||||
|
*/
|
||||||
|
function startAnimation() {
|
||||||
|
// 取消之前的动画
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
startValue = currentValue.value;
|
||||||
|
startTime = null;
|
||||||
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 value 变化
|
||||||
|
watch(value, (newValue) => {
|
||||||
|
if (newValue !== currentValue.value) {
|
||||||
|
startAnimation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 decimals 变化,立即更新显示格式
|
||||||
|
watch(decimals, () => {
|
||||||
|
displayValue.value = formatNumber(currentValue.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 thousands 和 separator 变化,立即更新显示格式
|
||||||
|
watch([thousands, separator], () => {
|
||||||
|
displayValue.value = formatNumber(currentValue.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件卸载时清理动画
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
currentValue.value = value.value;
|
||||||
|
displayValue.value = formatNumber(value.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.tui-animated-number {
|
||||||
|
display: inline-block;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-feature-settings: "tnum";
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
@@ -3,3 +3,4 @@ import Toolkit from "../../utils/Toolkit";
|
|||||||
|
|
||||||
export const Captcha = Toolkit.withInstall(view);
|
export const Captcha = Toolkit.withInstall(view);
|
||||||
export default Captcha;
|
export default Captcha;
|
||||||
|
export type { CaptchaRequest, CaptchaResult } from "./types";
|
||||||
|
|||||||
@@ -13,8 +13,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {CommonAPI} from "../../api";
|
import type {CaptchaRequest, CaptchaResult} from "./types";
|
||||||
import type {CaptchaResult} from "../../types";
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "Captcha"
|
name: "Captcha"
|
||||||
@@ -25,6 +24,7 @@ const props = withDefaults(defineProps<{
|
|||||||
height?: number,
|
height?: number,
|
||||||
title?: string,
|
title?: string,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
|
request?: CaptchaRequest,
|
||||||
}>(), {
|
}>(), {
|
||||||
width: 90,
|
width: 90,
|
||||||
height: 40,
|
height: 40,
|
||||||
@@ -49,9 +49,12 @@ const captchaStyle = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function update(): Promise<void> {
|
async function update(): Promise<void> {
|
||||||
|
if (!props.request) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await CommonAPI.captcha(width.value, height.value);
|
const result: CaptchaResult = await props.request(width.value, height.value);
|
||||||
captchaId.value = result.id;
|
captchaId.value = result.id;
|
||||||
src.value = result.data;
|
src.value = result.data;
|
||||||
emit("update", result);
|
emit("update", result);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/** 验证码组件展示数据。 */
|
||||||
|
export type CaptchaResult = {
|
||||||
|
id: string;
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 验证码组件的数据请求函数。 */
|
||||||
|
export type CaptchaRequest = (width: number, height: number) => Promise<CaptchaResult>;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import view from "./index.vue";
|
||||||
|
import Toolkit from "../../utils/Toolkit";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
DescriptionGridAlign,
|
||||||
|
DescriptionGridItem,
|
||||||
|
DescriptionGridProps
|
||||||
|
} from "./type";
|
||||||
|
|
||||||
|
export const DescriptionGrid = Toolkit.withInstall(view);
|
||||||
|
export default DescriptionGrid;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="cw-description-grid"
|
||||||
|
:class="{
|
||||||
|
bordered,
|
||||||
|
'mobile-single': singleColumnOnMobile
|
||||||
|
}"
|
||||||
|
:style="gridStyle"
|
||||||
|
>
|
||||||
|
<template v-for="item in items" :key="item.key">
|
||||||
|
<div class="cell label" :class="item.labelClass">
|
||||||
|
<slot :name="`label-${item.key}`" :item="item">
|
||||||
|
<span v-text="item.label" />
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="cell value" :class="item.valueClass">
|
||||||
|
<slot :name="`value-${item.key}`" :item="item">
|
||||||
|
<span v-text="formatValue(item.value)" />
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import type { CSSProperties, PropType } from "vue";
|
||||||
|
import type { DescriptionGridAlign, DescriptionGridItem } from "./type";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "DescriptionGrid"
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array as PropType<DescriptionGridItem[]>,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
type: Number,
|
||||||
|
default: 2
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
type: String,
|
||||||
|
default: "-"
|
||||||
|
},
|
||||||
|
bordered: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
singleColumnOnMobile: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
labelAlign: {
|
||||||
|
type: String as PropType<DescriptionGridAlign>,
|
||||||
|
default: "left"
|
||||||
|
},
|
||||||
|
valueAlign: {
|
||||||
|
type: String as PropType<DescriptionGridAlign>,
|
||||||
|
default: "left"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizedColumns = computed(() => {
|
||||||
|
if (!props.columns || props.columns < 1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return Math.floor(props.columns);
|
||||||
|
});
|
||||||
|
|
||||||
|
const gridStyle = computed<CSSProperties>(() => {
|
||||||
|
return {
|
||||||
|
"--cw-description-columns": String(normalizedColumns.value),
|
||||||
|
"--cw-description-grid-template": Array
|
||||||
|
.from({ length: normalizedColumns.value }, () => "max-content minmax(0, 1fr)")
|
||||||
|
.join(" "),
|
||||||
|
"--cw-description-label-align": props.labelAlign,
|
||||||
|
"--cw-description-value-align": props.valueAlign
|
||||||
|
} as CSSProperties;
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatValue(value: DescriptionGridItem["value"]): string {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return props.emptyText;
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.cw-description-grid {
|
||||||
|
display: grid;
|
||||||
|
max-width: 100%;
|
||||||
|
grid-template-columns: var(--cw-description-grid-template);
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: var(--t-color-text-placeholder, var(--td-text-color-placeholder));
|
||||||
|
text-align: var(--cw-description-label-align);
|
||||||
|
white-space: nowrap;
|
||||||
|
background: var(--t-color-bg-container-secondary, var(--td-bg-color-secondarycontainer));
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
text-align: var(--cw-description-value-align);
|
||||||
|
word-break: break-all;
|
||||||
|
background: var(--t-color-bg-container, var(--td-bg-color-container));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bordered {
|
||||||
|
border-top: 1px solid var(--t-color-border-1, var(--td-component-border));
|
||||||
|
border-left: 1px solid var(--t-color-border-1, var(--td-component-border));
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
border-right: 1px solid var(--t-color-border-1, var(--td-component-border));
|
||||||
|
border-bottom: 1px solid var(--t-color-border-1, var(--td-component-border));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 48rem) {
|
||||||
|
&.mobile-single {
|
||||||
|
grid-template-columns: max-content minmax(0, max-content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export type DescriptionGridAlign = "left" | "center" | "right";
|
||||||
|
|
||||||
|
export interface DescriptionGridItem {
|
||||||
|
/** 唯一标识,同时用于生成具名插槽名称 */
|
||||||
|
key: string;
|
||||||
|
/** 标签文本 */
|
||||||
|
label: string;
|
||||||
|
/** 默认展示值 */
|
||||||
|
value?: string | number | boolean | null;
|
||||||
|
/** 标签单元格自定义类名 */
|
||||||
|
labelClass?: string;
|
||||||
|
/** 内容单元格自定义类名 */
|
||||||
|
valueClass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DescriptionGridProps {
|
||||||
|
/** 描述项列表 */
|
||||||
|
items: DescriptionGridItem[];
|
||||||
|
/** 一行显示几组 key/value */
|
||||||
|
columns?: number;
|
||||||
|
/** 空值占位文本 */
|
||||||
|
emptyText?: string;
|
||||||
|
/** 是否显示边框 */
|
||||||
|
bordered?: boolean;
|
||||||
|
/** 是否在窄屏下自动降为单列 */
|
||||||
|
singleColumnOnMobile?: boolean;
|
||||||
|
/** 标签内容对齐方式 */
|
||||||
|
labelAlign?: DescriptionGridAlign;
|
||||||
|
/** 值内容对齐方式 */
|
||||||
|
valueAlign?: DescriptionGridAlign;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+15
-5
@@ -1,9 +1,9 @@
|
|||||||
/** 导出所有组件 */
|
/** 导出所有组件 */
|
||||||
import Icon from "./icon";
|
import Icon from "./icon";
|
||||||
import Popup from "./popup";
|
import Popup from "./popup";
|
||||||
|
import HoverPopup, { VPopupComponent } from "./v-popup";
|
||||||
import Captcha from "./captcha";
|
import Captcha from "./captcha";
|
||||||
import Loading from "./loading";
|
import Loading from "./loading";
|
||||||
import UserLevel from "./user-level";
|
|
||||||
import Copyright from "./copyright";
|
import Copyright from "./copyright";
|
||||||
import EmptyTips from "./empty-tips";
|
import EmptyTips from "./empty-tips";
|
||||||
import MarkdownView from "./markdown-view";
|
import MarkdownView from "./markdown-view";
|
||||||
@@ -11,33 +11,43 @@ import BEFlowerFall from "./background-effect/flower-fall";
|
|||||||
import MarkdownEditor from "./markdown-editor";
|
import MarkdownEditor from "./markdown-editor";
|
||||||
import PassTimeLabel from "./passtime-label";
|
import PassTimeLabel from "./passtime-label";
|
||||||
import ContactNumber from "./contact-number";
|
import ContactNumber from "./contact-number";
|
||||||
|
import AnimatedNumber from "./animated-number";
|
||||||
|
import DescriptionGrid from "./description-grid";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
Icon,
|
Icon,
|
||||||
Popup,
|
Popup,
|
||||||
|
HoverPopup,
|
||||||
Captcha,
|
Captcha,
|
||||||
Loading,
|
Loading,
|
||||||
UserLevel,
|
|
||||||
Copyright,
|
Copyright,
|
||||||
EmptyTips,
|
EmptyTips,
|
||||||
MarkdownView,
|
MarkdownView,
|
||||||
BEFlowerFall,
|
BEFlowerFall,
|
||||||
MarkdownEditor,
|
MarkdownEditor,
|
||||||
PassTimeLabel,
|
PassTimeLabel,
|
||||||
ContactNumber
|
ContactNumber,
|
||||||
|
AnimatedNumber,
|
||||||
|
DescriptionGrid
|
||||||
];
|
];
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Icon,
|
Icon,
|
||||||
Popup,
|
Popup,
|
||||||
|
HoverPopup,
|
||||||
|
VPopupComponent,
|
||||||
Captcha,
|
Captcha,
|
||||||
Loading,
|
Loading,
|
||||||
UserLevel,
|
|
||||||
Copyright,
|
Copyright,
|
||||||
EmptyTips,
|
EmptyTips,
|
||||||
MarkdownView,
|
MarkdownView,
|
||||||
BEFlowerFall,
|
BEFlowerFall,
|
||||||
MarkdownEditor,
|
MarkdownEditor,
|
||||||
PassTimeLabel,
|
PassTimeLabel,
|
||||||
ContactNumber
|
ContactNumber,
|
||||||
|
AnimatedNumber,
|
||||||
|
DescriptionGrid
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type { CaptchaRequest, CaptchaResult } from "./captcha";
|
||||||
|
export type { DescriptionGridAlign, DescriptionGridItem, DescriptionGridProps } from "./description-grid";
|
||||||
|
|||||||
@@ -262,7 +262,6 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
max-height: var(--data-max-height);
|
max-height: var(--data-max-height);
|
||||||
transition: max-height .5s var(--tui-bezier);
|
transition: max-height .5s var(--tui-bezier);
|
||||||
font-family: var(--tui-font);
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
||||||
@@ -270,7 +269,6 @@
|
|||||||
color: #333;
|
color: #333;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
text-shadow: none !important;
|
text-shadow: none !important;
|
||||||
font-family: var(--tui-font);
|
|
||||||
|
|
||||||
.line-numbers-rows {
|
.line-numbers-rows {
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="tui-popup"></div>
|
<div id="tui-popup">
|
||||||
|
<div id="tui-popup-content"></div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@@ -11,7 +13,7 @@ defineOptions({
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
#tui-popup {
|
#tui-popup {
|
||||||
border: var(--tui-border);
|
border: var(--tui-border);
|
||||||
z-index: 20;
|
z-index: 3000;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@@ -20,7 +22,10 @@ defineOptions({
|
|||||||
|
|
||||||
:deep(.text) {
|
:deep(.text) {
|
||||||
padding: 3px 6px 5px 6px;
|
padding: 3px 6px 5px 6px;
|
||||||
max-width: 420px;
|
}
|
||||||
|
|
||||||
|
#tui-popup-content {
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 745 B |
@@ -1,5 +0,0 @@
|
|||||||
import view from "./index.vue";
|
|
||||||
import Toolkit from "../../utils/Toolkit";
|
|
||||||
|
|
||||||
export const UserLevel = Toolkit.withInstall(view);
|
|
||||||
export default UserLevel;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="tui-user-level" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
defineOptions({
|
|
||||||
name: "UserLevel"
|
|
||||||
});
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
value: number;
|
|
||||||
}>(), {});
|
|
||||||
const { value } = toRefs(props);
|
|
||||||
|
|
||||||
const offset = computed(() => -9 * value.value + "px");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.tui-user-level {
|
|
||||||
width: 19px;
|
|
||||||
height: 9px;
|
|
||||||
background: url("./icon.png");
|
|
||||||
background-position-y: v-bind(offset);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import view from "./index.vue";
|
||||||
|
import Toolkit from "../../utils/Toolkit";
|
||||||
|
|
||||||
|
export const HoverPopup = Toolkit.withInstall(view);
|
||||||
|
|
||||||
|
// 兼容旧导出,后续应统一改为 HoverPopup
|
||||||
|
export const VPopupComponent = HoverPopup;
|
||||||
|
export default HoverPopup;
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="tag"
|
||||||
|
class="v-popup-root"
|
||||||
|
@mouseenter="onMouseEnter"
|
||||||
|
@mousemove="onMouseMove"
|
||||||
|
@mouseleave="onMouseLeave"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</component>
|
||||||
|
<teleport v-if="isShowing && hasPopupSlot" to="#tui-popup-content">
|
||||||
|
<div class="v-popup-content">
|
||||||
|
<slot name="popup" />
|
||||||
|
</div>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, nextTick, ref, useSlots } from "vue";
|
||||||
|
import { appendPopupValue, clearPopup, getPopup, getPopupContent, hidePopup, movePopup, PopupType, showPopup } from "../../utils/Popup";
|
||||||
|
|
||||||
|
type PopupValue = string | HTMLElement;
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "HoverPopup"
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
tag?: string;
|
||||||
|
type?: PopupType;
|
||||||
|
value?: PopupValue;
|
||||||
|
canShow?: () => boolean;
|
||||||
|
beforeShow?: (type: PopupType, value?: PopupValue) => void | Promise<void>;
|
||||||
|
afterHidden?: (type: PopupType, value?: PopupValue) => void | Promise<void>;
|
||||||
|
}>(), {
|
||||||
|
tag: "span",
|
||||||
|
type: PopupType.TEXT,
|
||||||
|
canShow: () => true
|
||||||
|
});
|
||||||
|
|
||||||
|
const slots = useSlots();
|
||||||
|
|
||||||
|
const hasPopupSlot = computed(() => !!slots.popup);
|
||||||
|
const isShowing = ref(false);
|
||||||
|
|
||||||
|
async function onMouseEnter(e: MouseEvent) {
|
||||||
|
const popup = getPopup();
|
||||||
|
const popupContent = getPopupContent();
|
||||||
|
if (!popup || !popupContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hasPopupSlot.value && !props.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (props.beforeShow) {
|
||||||
|
await props.beforeShow(props.type, props.value);
|
||||||
|
}
|
||||||
|
if (!props.canShow()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearPopup(popupContent);
|
||||||
|
if (hasPopupSlot.value) {
|
||||||
|
isShowing.value = true;
|
||||||
|
await nextTick();
|
||||||
|
} else if (props.value) {
|
||||||
|
appendPopupValue(popupContent, props.type, props.value);
|
||||||
|
}
|
||||||
|
movePopup(popup, e.x, e.y);
|
||||||
|
showPopup(popup);
|
||||||
|
isShowing.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(e: MouseEvent) {
|
||||||
|
const popup = getPopup();
|
||||||
|
if (!popup || !isShowing.value || !props.canShow()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
movePopup(popup, e.x, e.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onMouseLeave() {
|
||||||
|
const popup = getPopup();
|
||||||
|
const popupContent = getPopupContent();
|
||||||
|
isShowing.value = false;
|
||||||
|
await nextTick();
|
||||||
|
if (popup) {
|
||||||
|
hidePopup(popup);
|
||||||
|
}
|
||||||
|
if (popupContent) {
|
||||||
|
clearPopup(popupContent);
|
||||||
|
}
|
||||||
|
if (props.afterHidden) {
|
||||||
|
await props.afterHidden(props.type, props.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.v-popup-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
/**
|
|
||||||
* 文章编辑器的公共数据处理。
|
|
||||||
* 负责统一内容类型、附件标识和存储前的富文本格式,平台层不参与这些规则。
|
|
||||||
*/
|
|
||||||
import type { ArticleContentType, Attachment } from "../types";
|
|
||||||
import { ATTACHMENT_ID_ATTR, isLocalMediaId, normalizeContent, TEMP_FILE_ID_ATTR } from "./tiptap/shared";
|
|
||||||
|
|
||||||
/** 文章内容中附件和临时文件的标识前缀。 */
|
|
||||||
export const ATTACHMENT_ID_PREFIX = "attachment-id:";
|
|
||||||
export const TEMP_FILE_ID_PREFIX = "temp-file-id:";
|
|
||||||
export const ARTICLE_IMAGE_ATTACH_TYPE = "CONTENT_IMAGE";
|
|
||||||
export const ARTICLE_VIDEO_ATTACH_TYPE = "CONTENT_VIDEO";
|
|
||||||
/** 支持的文章内容类型和默认类型。 */
|
|
||||||
export const DEFAULT_ARTICLE_CONTENT_TYPE: ArticleContentType = "TIP_TAP";
|
|
||||||
export const ARTICLE_CONTENT_TYPE_LIST: ArticleContentType[] = ["TEXT", "MARKDOWN", "TIP_TAP"];
|
|
||||||
|
|
||||||
/** 将后端历史值和未知值归一化为当前支持的内容类型。 */
|
|
||||||
export function normalizeArticleContentType(contentType?: string): ArticleContentType {
|
|
||||||
return ARTICLE_CONTENT_TYPE_LIST.includes(contentType as ArticleContentType) ? contentType as ArticleContentType : DEFAULT_ARTICLE_CONTENT_TYPE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 将空内容转换为空字符串,避免编辑器收到 undefined。 */
|
|
||||||
export function normalizeRawArticleContent(content?: string) {
|
|
||||||
return content || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 从媒体 src 中读取持久化附件 ID。 */
|
|
||||||
export function getAttachmentIdFromSrc(src?: string) {
|
|
||||||
if (!src?.startsWith(ATTACHMENT_ID_PREFIX)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return src.slice(ATTACHMENT_ID_PREFIX.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 从媒体 src 中读取临时文件 ID。 */
|
|
||||||
export function getTempFileIdFromSrc(src?: string) {
|
|
||||||
if (!src?.startsWith(TEMP_FILE_ID_PREFIX)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return src.slice(TEMP_FILE_ID_PREFIX.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 构造带有附件 ID 的内部媒体地址。 */
|
|
||||||
export function buildAttachmentIdSrc(attachmentId: string) {
|
|
||||||
return `${ATTACHMENT_ID_PREFIX}${attachmentId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 根据媒体标签判断附件类型。 */
|
|
||||||
function getMediaAttachType(tagName: string) {
|
|
||||||
return tagName.toLowerCase() === "video" ? ARTICLE_VIDEO_ATTACH_TYPE : ARTICLE_IMAGE_ATTACH_TYPE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 仅为 blob 预览地址保留预览元数据,服务器地址不重复保存。 */
|
|
||||||
function buildAttachmentMetadata(src: string) {
|
|
||||||
if (!src.startsWith("blob:")) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
previewUrl: src
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 过滤本地临时媒体 ID,避免把前端占位 ID 当成可上传附件。 */
|
|
||||||
function normalizeAttachmentTempFileId(tempFileId?: string) {
|
|
||||||
if (!tempFileId || isLocalMediaId(tempFileId)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return tempFileId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 从文章 HTML 中提取图片和视频附件,并按 ID 去重。 */
|
|
||||||
export function buildAttachmentListFromContent(content?: string) {
|
|
||||||
const rawContent = normalizeContent(content);
|
|
||||||
if (!rawContent) {
|
|
||||||
return [] as Attachment[];
|
|
||||||
}
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(rawContent, "text/html");
|
|
||||||
const mediaList = Array.from(doc.querySelectorAll("img, video"));
|
|
||||||
const attachmentMap = new Map<string, Attachment>();
|
|
||||||
|
|
||||||
for (const media of mediaList) {
|
|
||||||
const src = media.getAttribute("src") || "";
|
|
||||||
const attachmentId = media.getAttribute(ATTACHMENT_ID_ATTR) || getAttachmentIdFromSrc(src);
|
|
||||||
const tempFileId = media.getAttribute(TEMP_FILE_ID_ATTR) || getTempFileIdFromSrc(src);
|
|
||||||
const key = attachmentId || tempFileId;
|
|
||||||
if (!key || attachmentMap.has(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
attachmentMap.set(key, {
|
|
||||||
id: attachmentId || key,
|
|
||||||
tempFileId: normalizeAttachmentTempFileId(tempFileId),
|
|
||||||
name: media.getAttribute("title") || media.getAttribute("alt") || "",
|
|
||||||
attachType: getMediaAttachType(media.tagName),
|
|
||||||
metadata: buildAttachmentMetadata(src)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(attachmentMap.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 将媒体地址统一改写为附件地址,供后端保存文章内容。 */
|
|
||||||
export function normalizeContentForStore(content?: string) {
|
|
||||||
const rawContent = normalizeContent(content);
|
|
||||||
if (!rawContent) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(rawContent, "text/html");
|
|
||||||
const mediaList = Array.from(doc.querySelectorAll("img, video"));
|
|
||||||
for (const media of mediaList) {
|
|
||||||
const src = media.getAttribute("src") || "";
|
|
||||||
const attachmentId = media.getAttribute(ATTACHMENT_ID_ATTR) || getAttachmentIdFromSrc(src);
|
|
||||||
const tempFileId = media.getAttribute(TEMP_FILE_ID_ATTR) || getTempFileIdFromSrc(src);
|
|
||||||
const finalAttachmentId = attachmentId || tempFileId;
|
|
||||||
if (!finalAttachmentId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
media.setAttribute(ATTACHMENT_ID_ATTR, finalAttachmentId);
|
|
||||||
media.removeAttribute(TEMP_FILE_ID_ATTR);
|
|
||||||
media.setAttribute("src", buildAttachmentIdSrc(finalAttachmentId));
|
|
||||||
}
|
|
||||||
return normalizeContent(doc.body.innerHTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 比较附件列表的业务字段,不比较对象引用。 */
|
|
||||||
export function sameAttachmentList(left?: Attachment[], right?: Attachment[]) {
|
|
||||||
const leftList = left || [];
|
|
||||||
const rightList = right || [];
|
|
||||||
if (leftList.length !== rightList.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < leftList.length; i += 1) {
|
|
||||||
const leftItem = leftList[i];
|
|
||||||
const rightItem = rightList[i];
|
|
||||||
if (!rightItem) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (leftItem.id !== rightItem.id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if ((leftItem.tempFileId || "") !== (rightItem.tempFileId || "")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if ((leftItem.accessKey?.value || "") !== (rightItem.accessKey?.value || "")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,9 @@
|
|||||||
/** 编辑器公共能力出口。平台组件只依赖这里的无界面逻辑和类型。 */
|
/** 编辑器公共能力出口。平台组件只依赖这里的无界面逻辑和类型。 */
|
||||||
export * from "./article";
|
|
||||||
export * from "./code";
|
export * from "./code";
|
||||||
export * from "./notify";
|
|
||||||
export * from "./search";
|
export * from "./search";
|
||||||
export * from "./tiptap/shared";
|
export * from "./tiptap/shared";
|
||||||
export * from "./tiptap/extensions";
|
export * from "./tiptap/extensions";
|
||||||
export * from "./tiptap/useTiptapEditor";
|
export * from "./tiptap/useTiptapEditor";
|
||||||
export * from "./tiptap/useTiptapView";
|
export * from "./tiptap/useTiptapView";
|
||||||
export type { Article, ArticleContentType } from "../types/Article";
|
|
||||||
export type { Attachment } from "../types/Attachment";
|
|
||||||
export type { NotifyDetail } from "../types/Notify";
|
|
||||||
export { default as AttachmentAPI } from "../api/AttachmentAPI";
|
|
||||||
export { default as PrismCodeBlock } from "./tiptap/PrismCodeBlock";
|
export { default as PrismCodeBlock } from "./tiptap/PrismCodeBlock";
|
||||||
export { enhancePrismCodeBlock } from "./tiptap/PrismCodeBlockView";
|
export { enhancePrismCodeBlock } from "./tiptap/PrismCodeBlockView";
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
/** 通知编辑器的公共数据规则。 */
|
|
||||||
import type { NotifyContentType } from "../types";
|
|
||||||
|
|
||||||
/** 通知缺少内容类型时默认使用 Tiptap。 */
|
|
||||||
export const DEFAULT_NOTIFY_CONTENT_TYPE: NotifyContentType = "TIP_TAP";
|
|
||||||
|
|
||||||
/** 规范化通知内容类型,兼容旧通知未返回内容类型的情况。 */
|
|
||||||
export function normalizeNotifyContentType(contentType?: string): NotifyContentType {
|
|
||||||
return contentType === "TEXT" ? "TEXT" : DEFAULT_NOTIFY_CONTENT_TYPE;
|
|
||||||
}
|
|
||||||
@@ -6,18 +6,14 @@ import "./assets/style/variable.less";
|
|||||||
import "./assets/style/timi-web.less";
|
import "./assets/style/timi-web.less";
|
||||||
import "prismjs/themes/prism.css";
|
import "prismjs/themes/prism.css";
|
||||||
import "./components/prism-code-block.less";
|
import "./components/prism-code-block.less";
|
||||||
import { Network } from "./utils";
|
|
||||||
import VPopup from "./utils/directives/Popup";
|
import VPopup from "./utils/directives/Popup";
|
||||||
|
|
||||||
export * from "./api";
|
|
||||||
export * from "./components";
|
export * from "./components";
|
||||||
export * from "./editor";
|
export * from "./editor";
|
||||||
export * from "./store";
|
export * from "./store";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
|
|
||||||
export const axios = Network.axios;
|
|
||||||
|
|
||||||
const install = function (app: App) {
|
const install = function (app: App) {
|
||||||
components.forEach(component => {
|
components.forEach(component => {
|
||||||
app.use(component as unknown as { install: () => any });
|
app.use(component as unknown as { install: () => any });
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
export * from "./deviceStore";
|
export * from "./deviceStore";
|
||||||
export * from "./userStore";
|
export * from "./windowStore";
|
||||||
|
|||||||
@@ -1,218 +0,0 @@
|
|||||||
import {defineStore} from "pinia";
|
|
||||||
import {computed, reactive, readonly, ref} from "vue";
|
|
||||||
import {CaptchaData, LoginRequest, LoginResponse, LoginUser} from "../types";
|
|
||||||
import Cooker from "../utils/Cooker";
|
|
||||||
import Storage from "../utils/Storage";
|
|
||||||
import {UserAPI} from "../api";
|
|
||||||
|
|
||||||
/** 用户 Store 配置项 */
|
|
||||||
interface UserStoreOptions {
|
|
||||||
/** localStorage 储存 key,默认 "loginUser" */
|
|
||||||
storageKey?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 访问控制匹配模式 */
|
|
||||||
export type AccessMatchMode = "and" | "or";
|
|
||||||
|
|
||||||
let _storageKey = "loginUser";
|
|
||||||
|
|
||||||
type AccessItem = string | {
|
|
||||||
moduleCode?: string;
|
|
||||||
code?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function normalizeAccessItem(item?: AccessItem): string {
|
|
||||||
if (!item) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof item === "string") {
|
|
||||||
return item.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = item.code?.trim();
|
|
||||||
if (!code) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const moduleCode = item.moduleCode?.trim();
|
|
||||||
return moduleCode && !code.includes(":") ? `${moduleCode}:${code}` : code;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeAccessList(list?: AccessItem[]): string[] {
|
|
||||||
if (!list?.length) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
.map(normalizeAccessItem)
|
|
||||||
.filter((item): item is string => !!item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchAccess(sourceList?: string[], targetList?: string | string[], mode: AccessMatchMode = "or"): boolean {
|
|
||||||
const normalizedTargetList = normalizeAccessList(Array.isArray(targetList) ? targetList : [targetList || ""]);
|
|
||||||
if (!normalizedTargetList.length) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceSet = new Set(normalizeAccessList(sourceList));
|
|
||||||
if (!sourceSet.size) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === "and") {
|
|
||||||
return normalizedTargetList.every(item => sourceSet.has(item));
|
|
||||||
}
|
|
||||||
return normalizedTargetList.some(item => sourceSet.has(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置用户 Store,应在使用 useUserStore 之前调用
|
|
||||||
* @param options 配置项
|
|
||||||
*/
|
|
||||||
function configureUserStore(options: UserStoreOptions): void {
|
|
||||||
if (options.storageKey) {
|
|
||||||
_storageKey = options.storageKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const useUserStore = defineStore("user", () => {
|
|
||||||
type LoginUserState = LoginUser & Record<string, unknown>;
|
|
||||||
|
|
||||||
const loginUser = reactive<LoginUserState>({});
|
|
||||||
const isRestored = ref(false);
|
|
||||||
|
|
||||||
const token = computed(() => loginUser.token ?? "");
|
|
||||||
const isLoggedIn = computed(() => isValidLoginUser(loginUser));
|
|
||||||
|
|
||||||
function resetLoginUser(nextUser?: LoginUserState): void {
|
|
||||||
Object.keys(loginUser).forEach((key) => {
|
|
||||||
delete loginUser[key];
|
|
||||||
});
|
|
||||||
if (nextUser) {
|
|
||||||
Object.assign(loginUser, nextUser);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidLoginUser(user?: LoginUserState | null): user is LoginResponse {
|
|
||||||
return !!user?.token && !!user.expireAt && new Date().getTime() < user.expireAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLogged(): boolean {
|
|
||||||
return isLoggedIn.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLoginUser<T extends LoginUser = LoginUser>(): T | null {
|
|
||||||
if (!isLogged()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return loginUser as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRoleList(): string[] {
|
|
||||||
return normalizeAccessList(loginUser.user?.roleList);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPermissionList(): string[] {
|
|
||||||
return normalizeAccessList(loginUser.user?.permissionList);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasRoles(roleList: string | string[], mode: AccessMatchMode = "or"): boolean {
|
|
||||||
return matchAccess(getRoleList(), roleList, mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasPermissions(permissionList: string | string[], mode: AccessMatchMode = "or"): boolean {
|
|
||||||
return matchAccess(getPermissionList(), permissionList, mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function login<T extends LoginUser = LoginUser>(req: CaptchaData<LoginRequest>): Promise<T> {
|
|
||||||
const result = await UserAPI.login(req);
|
|
||||||
const nextUser = result as unknown as T;
|
|
||||||
await updateLoginUser(nextUser);
|
|
||||||
return nextUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loginByToken<T extends LoginUser = LoginUser>(token: string): Promise<T | null> {
|
|
||||||
loginUser.token = token;
|
|
||||||
// 未过期
|
|
||||||
try {
|
|
||||||
const result = await UserAPI.loginByToken();
|
|
||||||
if (!result) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const nextUser = result as unknown as T;
|
|
||||||
await updateLoginUser(nextUser);
|
|
||||||
return nextUser;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
clearLoginUser();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loginByStorage<T extends LoginUser = LoginUser>(): Promise<T | null> {
|
|
||||||
if (loginUser.token) {
|
|
||||||
isRestored.value = true;
|
|
||||||
return getLoginUser<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Storage.has(_storageKey)) {
|
|
||||||
isRestored.value = true;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 未登录,储存有令牌。
|
|
||||||
const storageUser = Storage.getJSON(_storageKey) as T;
|
|
||||||
if (isValidLoginUser(storageUser)) {
|
|
||||||
const result = await loginByToken<T>(storageUser.token);
|
|
||||||
isRestored.value = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearLoginUser();
|
|
||||||
isRestored.value = true;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateLoginUser<T extends LoginUser = LoginUser>(user: T): Promise<void> {
|
|
||||||
resetLoginUser(user as LoginUserState);
|
|
||||||
Storage.setJSON(_storageKey, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearLoginUser(): void {
|
|
||||||
resetLoginUser();
|
|
||||||
Cooker.remove("Token");
|
|
||||||
Storage.remove(_storageKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logout(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await UserAPI.logout();
|
|
||||||
} finally {
|
|
||||||
clearLoginUser();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
token,
|
|
||||||
isLoggedIn,
|
|
||||||
isRestored,
|
|
||||||
getRoleList,
|
|
||||||
getPermissionList,
|
|
||||||
hasRoles,
|
|
||||||
hasPermissions,
|
|
||||||
isLogged,
|
|
||||||
loginUser: readonly(loginUser),
|
|
||||||
getLoginUser,
|
|
||||||
login,
|
|
||||||
loginByToken,
|
|
||||||
loginByStorage,
|
|
||||||
updateLoginUser,
|
|
||||||
clearLoginUser,
|
|
||||||
logout
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export {
|
|
||||||
useUserStore,
|
|
||||||
configureUserStore
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/** 窗口级通用状态操作。 */
|
||||||
|
function setTitle(title?: string) {
|
||||||
|
window.document.title = title || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowStore = {
|
||||||
|
setTitle
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
windowStore
|
||||||
|
};
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import type { Attachment } from "./Attachment";
|
|
||||||
import type { Comment } from "./Comment";
|
|
||||||
import type { Model } from "./Model";
|
|
||||||
import type { Setting } from "./Setting";
|
|
||||||
|
|
||||||
/** 文章业务类型 */
|
|
||||||
export type ArticleBizType = "BLOG" | "FEEDBACK";
|
|
||||||
|
|
||||||
/** 文章正文类型 */
|
|
||||||
export type ArticleContentType = "TEXT" | "MARKDOWN" | "TIP_TAP";
|
|
||||||
|
|
||||||
/** 文章状态 */
|
|
||||||
export type ArticleStatus = "DART" | "NORMAL" | "HIDDEN";
|
|
||||||
|
|
||||||
/** 文章实体 */
|
|
||||||
export type Article = {
|
|
||||||
/** 业务类型 */
|
|
||||||
bizType?: ArticleBizType;
|
|
||||||
|
|
||||||
/** 业务 ID */
|
|
||||||
bizId?: string;
|
|
||||||
|
|
||||||
/** 标题 */
|
|
||||||
title?: string;
|
|
||||||
|
|
||||||
/** 描述 */
|
|
||||||
description?: string;
|
|
||||||
|
|
||||||
/** 正文类型 */
|
|
||||||
contentType?: ArticleContentType;
|
|
||||||
|
|
||||||
/** 正文内容 */
|
|
||||||
content?: string;
|
|
||||||
|
|
||||||
/** 阅读数量 */
|
|
||||||
reads?: number;
|
|
||||||
|
|
||||||
/** 喜欢数量 */
|
|
||||||
likes?: number;
|
|
||||||
|
|
||||||
/** 文章状态 */
|
|
||||||
status?: ArticleStatus;
|
|
||||||
|
|
||||||
/** 设置列表 */
|
|
||||||
settingList?: Setting[];
|
|
||||||
|
|
||||||
/** 评论列表 */
|
|
||||||
commentList?: Comment[];
|
|
||||||
|
|
||||||
/** 附件列表 */
|
|
||||||
attachmentList?: Attachment[];
|
|
||||||
} & Model;
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { Model } from "./Model";
|
|
||||||
|
|
||||||
export type Attachment = {
|
|
||||||
bizType?: AttachmentBizType;
|
|
||||||
bizId?: string;
|
|
||||||
attachType?: string;
|
|
||||||
mongoId?: string;
|
|
||||||
title?: string;
|
|
||||||
name?: string;
|
|
||||||
mimeType?: string;
|
|
||||||
metadata?: Record<string, any>;
|
|
||||||
accessKey?: AttachmentAccessKey;
|
|
||||||
size?: number;
|
|
||||||
md5?: string;
|
|
||||||
uploaderIp?: string;
|
|
||||||
isDestroyed?: boolean;
|
|
||||||
destroyAt?: number;
|
|
||||||
tempFileId?: string;
|
|
||||||
} & Model;
|
|
||||||
|
|
||||||
export type TempFile = {
|
|
||||||
id: string;
|
|
||||||
expireAt?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BizIdUpdate = {
|
|
||||||
bizType: AttachmentBizType;
|
|
||||||
bizId: string;
|
|
||||||
items: Partial<Attachment>[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AttachmentBizType =
|
|
||||||
| "ARTICLE"
|
|
||||||
| "USER"
|
|
||||||
| "GAO_CUSTOMER"
|
|
||||||
| "GAO_EVENT_RECORD"
|
|
||||||
| "GAO_REGISTER_RECORD"
|
|
||||||
| "TEMP_FILE"
|
|
||||||
| "MIRROR";
|
|
||||||
|
|
||||||
/** 附件访问密钥 */
|
|
||||||
export type AttachmentAccessKey = {
|
|
||||||
value: string;
|
|
||||||
attachmentId: string;
|
|
||||||
isDisposable: boolean;
|
|
||||||
expiredAt: number;
|
|
||||||
createdAt: number;
|
|
||||||
};
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { Model, Page } from "./Model";
|
|
||||||
import { User } from "./User";
|
|
||||||
|
|
||||||
/** 评论 */
|
|
||||||
export type Comment = {
|
|
||||||
bizType?: CommentBizType;
|
|
||||||
bizId?: number;
|
|
||||||
userId?: number;
|
|
||||||
nick?: string;
|
|
||||||
content?: string;
|
|
||||||
|
|
||||||
/** 所属用户 */
|
|
||||||
user?: User;
|
|
||||||
|
|
||||||
/** 回复列表 */
|
|
||||||
replies?: Comment[];
|
|
||||||
|
|
||||||
/** 回复分页 */
|
|
||||||
repliesPage?: Page<Comment>;
|
|
||||||
|
|
||||||
/** 用于绑定组件当前页下标 */
|
|
||||||
repliesCurrent?: number;
|
|
||||||
|
|
||||||
/** 回复数量 */
|
|
||||||
repliesLength?: number;
|
|
||||||
|
|
||||||
/** 关联文章 */
|
|
||||||
article?: object;
|
|
||||||
|
|
||||||
/** 关联 Git 仓库 */
|
|
||||||
repository?: object;
|
|
||||||
} & Model
|
|
||||||
|
|
||||||
export type CommentBizType = "ARTICLE" | "GIT_ISSUE" | "GIT_MERGE";
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/** 开发者配置 */
|
|
||||||
export type Developer = {
|
|
||||||
developerId?: number;
|
|
||||||
name?: string;
|
|
||||||
rsa?: string;
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
// 基本实体模型
|
|
||||||
export type Model = {
|
|
||||||
id?: string;
|
|
||||||
|
|
||||||
createdAt?: number;
|
|
||||||
updatedAt?: number;
|
|
||||||
deletedAt?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ApiResponse<T> = {
|
|
||||||
code: number;
|
|
||||||
message: string;
|
|
||||||
messageCode: string;
|
|
||||||
success: boolean;
|
|
||||||
fail: boolean;
|
|
||||||
data: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Page<T> = {
|
|
||||||
orderMap?: { [K in keyof T]?: OrderType };
|
|
||||||
equalsExample?: Partial<T>;
|
|
||||||
likesExample?: Partial<T>;
|
|
||||||
} & BasePage;
|
|
||||||
|
|
||||||
export type BasePage = {
|
|
||||||
index: number;
|
|
||||||
size: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OrderType = "ASC" | "DESC";
|
|
||||||
|
|
||||||
export type PageResult<T> = {
|
|
||||||
total: number;
|
|
||||||
list: T[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 携带验证码的请求体
|
|
||||||
export type CaptchaData<T> = {
|
|
||||||
captchaId: string;
|
|
||||||
captcha: string;
|
|
||||||
data: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 图形验证码
|
|
||||||
export type CaptchaResult = {
|
|
||||||
id: string;
|
|
||||||
data: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ImageType = "IR-AUTO" | "IR-SMOOTH" | "IR-PIXELATED";
|
|
||||||
|
|
||||||
export type KeyValue<V, K = string> = {
|
|
||||||
key: K;
|
|
||||||
value?: V;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LabelValue<L, V = string> = {
|
|
||||||
label: L;
|
|
||||||
value?: V;
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import type { Model } from "./Model";
|
|
||||||
import type { Attachment } from "./Attachment";
|
|
||||||
import type { User } from "./User";
|
|
||||||
|
|
||||||
/** 通知消息类型 */
|
|
||||||
export type NotifyMsgType = "INTERNAL" | "SMS" | "MAIL" | "HTTP" | "WECHAT";
|
|
||||||
|
|
||||||
/** 通知投递状态 */
|
|
||||||
export type NotifyStatus = "WAITING" | "REMINDED" | "TIMEOUT" | "CANCEL" | "FAIL";
|
|
||||||
|
|
||||||
/** 通知内容类型 */
|
|
||||||
export type NotifyContentType = "TEXT" | "TIP_TAP";
|
|
||||||
|
|
||||||
/** 通知 */
|
|
||||||
export type Notify = {
|
|
||||||
/** 业务类型 */
|
|
||||||
bizType?: string;
|
|
||||||
/** 提醒业务类型 */
|
|
||||||
bizRemindedType?: string;
|
|
||||||
/** 提醒业务 ID */
|
|
||||||
bizRemindedId?: string;
|
|
||||||
/** 参数 */
|
|
||||||
args?: string;
|
|
||||||
/** 创建者 */
|
|
||||||
createdBy?: string;
|
|
||||||
/** 通知详情列表 */
|
|
||||||
detailList?: NotifyDetail[];
|
|
||||||
} & Model;
|
|
||||||
|
|
||||||
/** 通知详情 */
|
|
||||||
export type NotifyDetail = {
|
|
||||||
/** 所属通知 ID */
|
|
||||||
notifyId?: string;
|
|
||||||
/** 消息类型 */
|
|
||||||
msgType?: NotifyMsgType;
|
|
||||||
/** 模板 ID */
|
|
||||||
templateId?: string;
|
|
||||||
/** 标题 */
|
|
||||||
subject?: string;
|
|
||||||
/** 内容 */
|
|
||||||
content?: string;
|
|
||||||
/** 内容类型 */
|
|
||||||
contentType?: NotifyContentType;
|
|
||||||
/** 媒体附件列表 */
|
|
||||||
attachmentList?: Attachment[];
|
|
||||||
/** 参数 */
|
|
||||||
args?: string;
|
|
||||||
/** 发送来源 */
|
|
||||||
sendFrom?: string;
|
|
||||||
/** 发送去向 */
|
|
||||||
sendTo?: string;
|
|
||||||
/** 发送时间 */
|
|
||||||
sendAt?: number;
|
|
||||||
/** 重试计数 */
|
|
||||||
retry?: number;
|
|
||||||
/** 超时时间 */
|
|
||||||
timeoutAt?: number;
|
|
||||||
/** 结果描述 */
|
|
||||||
resultDesc?: string;
|
|
||||||
/** 投递状态 */
|
|
||||||
status?: NotifyStatus;
|
|
||||||
/** 已读时间,站内通知未读时为空 */
|
|
||||||
readAt?: number;
|
|
||||||
/** 所属通知 */
|
|
||||||
notify?: Notify;
|
|
||||||
/** 发送用户 */
|
|
||||||
fromUser?: User;
|
|
||||||
/** 接收用户 */
|
|
||||||
toUser?: User;
|
|
||||||
} & Model;
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
export type ModuleCode = "CORE" | "GAO";
|
|
||||||
|
|
||||||
export interface Permission {
|
|
||||||
id: string;
|
|
||||||
moduleCode: ModuleCode;
|
|
||||||
code: string;
|
|
||||||
nameLangId?: string;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
builtin?: boolean;
|
|
||||||
protectedPermission?: boolean;
|
|
||||||
ownerType?: AuthOwnerType;
|
|
||||||
ownerId?: string;
|
|
||||||
createdBy?: string;
|
|
||||||
createdAt?: number;
|
|
||||||
updatedAt?: number;
|
|
||||||
deletedAt?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AuthOwnerType = "SYSTEM" | "MODULE" | "TENANT" | "STORE" | "USER";
|
|
||||||
|
|
||||||
export type PermissionPayload = Pick<Permission, "moduleCode" | "code" | "nameLangId" | "name" | "description"> & {
|
|
||||||
id?: string;
|
|
||||||
};
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import type { AuthOwnerType, ModuleCode, Permission } from "./Permission";
|
|
||||||
import { Model } from "./Model";
|
|
||||||
|
|
||||||
export type Role = {
|
|
||||||
moduleCode: ModuleCode;
|
|
||||||
code: string;
|
|
||||||
nameLangId?: string;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
builtin?: boolean;
|
|
||||||
protectedRole?: boolean;
|
|
||||||
parentRoleId?: string;
|
|
||||||
ownerType?: AuthOwnerType;
|
|
||||||
ownerId?: string;
|
|
||||||
createdBy?: string;
|
|
||||||
|
|
||||||
permissionList?: Permission[];
|
|
||||||
allPermissionList?: Permission[];
|
|
||||||
childRoleList?: Role[];
|
|
||||||
parentRole?: Role;
|
|
||||||
delegationPermissionList?: Permission[];
|
|
||||||
permissionIdList?: string[];
|
|
||||||
childRoleIdList?: string[];
|
|
||||||
delegationPermissionIdList?: string[];
|
|
||||||
} & Model;
|
|
||||||
|
|
||||||
export type RolePayload = Partial<Pick<Role, "moduleCode" | "code" | "nameLangId" | "name" | "description">> & {
|
|
||||||
id?: string;
|
|
||||||
parentRoleId?: string;
|
|
||||||
ownerType?: AuthOwnerType;
|
|
||||||
ownerId?: string;
|
|
||||||
permissionIdList?: string[];
|
|
||||||
childRoleIdList?: string[];
|
|
||||||
delegationPermissionIdList?: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface AuthorizeUserRole {
|
|
||||||
userId: string;
|
|
||||||
moduleCode?: ModuleCode;
|
|
||||||
roleIdList?: string[];
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import {Model} from "./Model";
|
|
||||||
|
|
||||||
export type SettingValueType =
|
|
||||||
| "STRING"
|
|
||||||
| "SELECT_RADIO"
|
|
||||||
| "SELECT_CHECKBOX"
|
|
||||||
| "INTEGER"
|
|
||||||
| "COLOR"
|
|
||||||
| "FLOAT"
|
|
||||||
| "DATE"
|
|
||||||
| "DATETIME"
|
|
||||||
| "TIME"
|
|
||||||
| "DURATION"
|
|
||||||
| "BOOLEAN"
|
|
||||||
| "JSON_LIST"
|
|
||||||
| "JSON_OBJECT"
|
|
||||||
;
|
|
||||||
|
|
||||||
export type Setting = {
|
|
||||||
module: string;
|
|
||||||
key: string;
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
value?: string;
|
|
||||||
valueType: SettingValueType;
|
|
||||||
valueArgs?: any;
|
|
||||||
} & Model;
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { ImageType, Model } from "./Model";
|
|
||||||
import { Attachment } from "./Attachment";
|
|
||||||
import type { Setting } from "./Setting";
|
|
||||||
import { Role } from "./Role";
|
|
||||||
|
|
||||||
export type Gender = "MALE" | "FEMALE";
|
|
||||||
|
|
||||||
export type User = {
|
|
||||||
name: string;
|
|
||||||
nick?: string;
|
|
||||||
password?: string;
|
|
||||||
email?: string;
|
|
||||||
emailVerifyAt?: number;
|
|
||||||
phoneNo?: string;
|
|
||||||
phoneNoVerifyAt?: number;
|
|
||||||
wrapperType?: ImageType;
|
|
||||||
avatarType?: ImageType;
|
|
||||||
exp?: number;
|
|
||||||
gender?: Gender;
|
|
||||||
birthdate?: number;
|
|
||||||
qq?: string;
|
|
||||||
description?: string;
|
|
||||||
lastLoginIP?: string;
|
|
||||||
lastLoginAt?: number;
|
|
||||||
unmuteAt?: number;
|
|
||||||
unbanAt?: number;
|
|
||||||
|
|
||||||
roleList?: string[];
|
|
||||||
roleEntityList?: Role[];
|
|
||||||
permissionList?: string[];
|
|
||||||
attachmentList?: Attachment[];
|
|
||||||
settingList?: Setting[];
|
|
||||||
tempFileIdList?: string[];
|
|
||||||
muting?: boolean;
|
|
||||||
banning?: boolean;
|
|
||||||
emailVerified?: boolean;
|
|
||||||
phoneNoVerified?: boolean;
|
|
||||||
} & Model;
|
|
||||||
|
|
||||||
export type UserAttachType = "AVATAR" | "WRAPPER";
|
|
||||||
|
|
||||||
export type RegisterRequest = {
|
|
||||||
name: string;
|
|
||||||
password: string;
|
|
||||||
email?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LoginRequest = {
|
|
||||||
user: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SmsLoginRequest = {
|
|
||||||
detailId: string;
|
|
||||||
captcha: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpdatePasswordRequest = {
|
|
||||||
oldValue: string;
|
|
||||||
newValue: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 登录返回
|
|
||||||
export type LoginResponse = {
|
|
||||||
id: string;
|
|
||||||
token: string;
|
|
||||||
expireAt: number;
|
|
||||||
user?: User;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LoginUser = {
|
|
||||||
id?: string;
|
|
||||||
token?: string;
|
|
||||||
expireAt?: number;
|
|
||||||
user?: User;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UserLevelType = {
|
|
||||||
exp: number; // 经验数值,和 UserData.exp 一样
|
|
||||||
value: number; // 经验对应等级,[0, 8]
|
|
||||||
percent: number; // 该经验在该等级的百分比 [0, 1]
|
|
||||||
nextLevelUp: number; // 下一级经验值
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,5 @@
|
|||||||
import type { Plugin } from "vue";
|
import type { Plugin } from "vue";
|
||||||
|
|
||||||
export * from "./Model";
|
|
||||||
export * from "./Article";
|
|
||||||
export * from "./Setting";
|
|
||||||
export * from "./Attachment";
|
|
||||||
export * from "./Model";
|
|
||||||
export * from "./Permission";
|
|
||||||
export * from "./Role";
|
|
||||||
export * from "./User";
|
|
||||||
export * from "./Comment";
|
|
||||||
export * from "./Developer";
|
|
||||||
export * from "./Notify";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 安装方法
|
* 安装方法
|
||||||
*/
|
*/
|
||||||
|
|||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import type { Directive } from "vue";
|
||||||
|
import type { PopupConfig } from "../utils/directives/Popup";
|
||||||
|
|
||||||
|
declare module "vue" {
|
||||||
|
export interface GlobalDirectives {
|
||||||
|
popup: Directive<HTMLElement, string | PopupConfig>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import axios, {type AxiosInstance, type AxiosResponse, type InternalAxiosRequestConfig} from "axios";
|
|
||||||
import {ApiResponse} from "../types";
|
|
||||||
|
|
||||||
export type ErrorCallback = (response: ApiResponse<any>) => void;
|
|
||||||
export type RequestInterceptor = (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig;
|
|
||||||
export interface ResponseObserverPayload {
|
|
||||||
config?: InternalAxiosRequestConfig;
|
|
||||||
status?: number;
|
|
||||||
response?: AxiosResponse;
|
|
||||||
error?: any;
|
|
||||||
serverResponse?: ApiResponse<any>;
|
|
||||||
}
|
|
||||||
export type ResponseObserver = (payload: ResponseObserverPayload) => void;
|
|
||||||
|
|
||||||
let globalErrorCallback: ErrorCallback | null = null;
|
|
||||||
let globalRequestInterceptor: RequestInterceptor | null = null;
|
|
||||||
let globalResponseObserver: ResponseObserver | null = null;
|
|
||||||
|
|
||||||
export const setGlobalErrorCallback = (callback: ErrorCallback) => {
|
|
||||||
globalErrorCallback = callback;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setGlobalRequestInterceptor = (callback: RequestInterceptor | null) => {
|
|
||||||
globalRequestInterceptor = callback;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setGlobalResponseObserver = (callback: ResponseObserver | null) => {
|
|
||||||
globalResponseObserver = callback;
|
|
||||||
};
|
|
||||||
|
|
||||||
const notifyResponseObserver = (payload: ResponseObserverPayload) => {
|
|
||||||
if (!globalResponseObserver) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
globalResponseObserver(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyDefaultInterceptors = (instance: AxiosInstance) => {
|
|
||||||
instance.defaults.withCredentials = true;
|
|
||||||
instance.interceptors.request.use((config) => {
|
|
||||||
if (globalRequestInterceptor) {
|
|
||||||
return globalRequestInterceptor(config);
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
instance.interceptors.response.use((axiosResp: any) => {
|
|
||||||
if (!axiosResp.config.responseType) {
|
|
||||||
// 服务端返回
|
|
||||||
const serverResp = axiosResp.data as ApiResponse<any>;
|
|
||||||
if (serverResp.code < 40000) {
|
|
||||||
notifyResponseObserver({
|
|
||||||
config: axiosResp?.config,
|
|
||||||
status: axiosResp?.status,
|
|
||||||
response: axiosResp,
|
|
||||||
serverResponse: serverResp
|
|
||||||
});
|
|
||||||
// 200 或 300 HTTP 状态段视为成功
|
|
||||||
return serverResp.data;
|
|
||||||
} else {
|
|
||||||
notifyResponseObserver({
|
|
||||||
config: axiosResp?.config,
|
|
||||||
status: axiosResp?.status,
|
|
||||||
response: axiosResp,
|
|
||||||
serverResponse: serverResp,
|
|
||||||
error: serverResp.message
|
|
||||||
});
|
|
||||||
if (globalErrorCallback) {
|
|
||||||
globalErrorCallback(serverResp);
|
|
||||||
}
|
|
||||||
// 由调用方处理
|
|
||||||
return Promise.reject(serverResp.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notifyResponseObserver({
|
|
||||||
config: axiosResp?.config,
|
|
||||||
status: axiosResp?.status,
|
|
||||||
response: axiosResp
|
|
||||||
});
|
|
||||||
return axiosResp.data;
|
|
||||||
}, (error: any) => {
|
|
||||||
notifyResponseObserver({
|
|
||||||
config: error?.config,
|
|
||||||
status: error?.response?.status,
|
|
||||||
response: error?.response,
|
|
||||||
error
|
|
||||||
});
|
|
||||||
// 请求错误
|
|
||||||
if (error) {
|
|
||||||
if (error.response && error.response.status) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
if (error.request) {
|
|
||||||
if (error.message && error.message.startsWith("timeout")) {
|
|
||||||
throw new Error("time out");
|
|
||||||
} else {
|
|
||||||
throw new Error(error.message || "request error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const axiosInstance = axios.create();
|
|
||||||
applyDefaultInterceptors(axiosInstance);
|
|
||||||
|
|
||||||
export const createServiceAxios = (baseURL?: string) => {
|
|
||||||
const instance = axios.create({ baseURL });
|
|
||||||
applyDefaultInterceptors(instance);
|
|
||||||
return instance;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
axios: axiosInstance,
|
|
||||||
setGlobalErrorCallback,
|
|
||||||
setGlobalRequestInterceptor,
|
|
||||||
setGlobalResponseObserver,
|
|
||||||
createServiceAxios
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import Toolkit from "./Toolkit";
|
||||||
|
|
||||||
|
export enum PopupType {
|
||||||
|
TEXT,
|
||||||
|
IMG,
|
||||||
|
HTML,
|
||||||
|
EL
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PopupConfig = {
|
||||||
|
type: PopupType,
|
||||||
|
value?: string | HTMLElement;
|
||||||
|
canShow?: () => boolean;
|
||||||
|
beforeShow?: (type: PopupType, value: string | HTMLElement) => Promise<void>;
|
||||||
|
afterHidden?: (type: PopupType, value: string | HTMLElement) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POPUP_ID = "tui-popup";
|
||||||
|
export const POPUP_CONTENT_ID = "tui-popup-content";
|
||||||
|
|
||||||
|
export function getPopup() {
|
||||||
|
return document.getElementById(POPUP_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPopupContent() {
|
||||||
|
return document.getElementById(POPUP_CONTENT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearPopup(popup: HTMLElement) {
|
||||||
|
popup.replaceChildren();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function movePopup(popup: HTMLElement, x: number, y: number) {
|
||||||
|
const offsetX = 20;
|
||||||
|
const offsetY = 32;
|
||||||
|
popup.style.left = `${x + offsetX}px`;
|
||||||
|
popup.style.top = `${y + offsetY}px`;
|
||||||
|
|
||||||
|
const popupRect = popup.getBoundingClientRect();
|
||||||
|
if (window.innerWidth < popupRect.right + offsetX) {
|
||||||
|
popup.style.left = `${Math.max(0, window.innerWidth - popupRect.width - offsetX)}px`;
|
||||||
|
}
|
||||||
|
if (window.innerHeight < popupRect.bottom) {
|
||||||
|
popup.style.top = `${Math.max(0, y - popupRect.height - 6)}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showPopup(popup: HTMLElement) {
|
||||||
|
popup.style.visibility = "visible";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hidePopup(popup: HTMLElement) {
|
||||||
|
popup.style.visibility = "hidden";
|
||||||
|
popup.style.left = "0px";
|
||||||
|
popup.style.top = "0px";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function appendPopupValue(popup: HTMLElement, type: PopupType, value: string | HTMLElement) {
|
||||||
|
let popupChild: HTMLElement;
|
||||||
|
switch (type) {
|
||||||
|
case PopupType.TEXT:
|
||||||
|
popupChild = document.createElement("div");
|
||||||
|
popupChild.className = "text";
|
||||||
|
popupChild.textContent = value as string;
|
||||||
|
popup.appendChild(popupChild);
|
||||||
|
break;
|
||||||
|
case PopupType.IMG:
|
||||||
|
popupChild = document.createElement("img");
|
||||||
|
(popupChild as HTMLImageElement).src = value as string;
|
||||||
|
popup.appendChild(popupChild);
|
||||||
|
break;
|
||||||
|
case PopupType.HTML:
|
||||||
|
popup.appendChild(Toolkit.toDOM(value as string));
|
||||||
|
break;
|
||||||
|
case PopupType.EL:
|
||||||
|
if (value instanceof HTMLElement) {
|
||||||
|
value.style.display = "block";
|
||||||
|
popup.appendChild(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
throw new Error("Vue 指令错误:v-popup:el 的值不是 HTML 元素");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import {ref, Ref} from "vue";
|
|
||||||
import {Setting} from "../types";
|
|
||||||
import {CommonAPI} from "../api";
|
|
||||||
|
|
||||||
export default class SettingMapper {
|
|
||||||
|
|
||||||
private static instance: SettingMapper;
|
|
||||||
|
|
||||||
private moduleMap = new Map<string, Map<string, Ref<Setting>>>();
|
|
||||||
|
|
||||||
public static async loadSetting(map: Record<string, string[]>): Promise<void> {
|
|
||||||
const result = await CommonAPI.settingMap(map);
|
|
||||||
this.appendSettingMap(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static appendSettingMap(result: Map<string, Map<string, Setting>>): void {
|
|
||||||
for (const [module, settingMap] of result) {
|
|
||||||
for (const [key, setting] of settingMap) {
|
|
||||||
this.setValue(module, key, setting);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createModule(module: string): Map<string, Ref<Setting>> {
|
|
||||||
const instance = this.getInstance();
|
|
||||||
const moduleVal = instance.moduleMap.get(module);
|
|
||||||
if (moduleVal) {
|
|
||||||
return moduleVal;
|
|
||||||
}
|
|
||||||
const created = new Map<string, Ref<Setting>>();
|
|
||||||
instance.moduleMap.set(module, created);
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getModule(module: string): Map<string, Ref<Setting>> | undefined {
|
|
||||||
return this.getInstance().moduleMap.get(module);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static hasModule(module: string): boolean {
|
|
||||||
return this.getInstance().moduleMap.has(module);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static deleteModule(module: string): boolean {
|
|
||||||
return this.getInstance().moduleMap.delete(module);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static setModule(module: string, moduleVal: Map<string, Ref<Setting>>): void {
|
|
||||||
this.getInstance().moduleMap.set(module, moduleVal);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static is(module: string, key: string): boolean {
|
|
||||||
const setting = this.getValueRef(module, key).value;
|
|
||||||
return !!setting.value && setting.value === "true";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static not(module: string, key: string): boolean {
|
|
||||||
return !this.is(module, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getValue(module: string, key: string): Setting {
|
|
||||||
return this.getValueRef(module, key).value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static setValue(module: string, key: string, setting: Setting): void {
|
|
||||||
const moduleVal = this.createModule(module);
|
|
||||||
const settingRef = moduleVal.get(key);
|
|
||||||
if (settingRef) {
|
|
||||||
settingRef.value = setting;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
moduleVal.set(key, ref(setting));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static setValueRef(module: string, key: string, settingRef: Ref<Setting>): void {
|
|
||||||
const moduleVal = this.createModule(module);
|
|
||||||
moduleVal.set(key, settingRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static hasValue(module: string, key: string): boolean {
|
|
||||||
return !!this.getModule(module)?.has(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static deleteValue(module: string, key: string): boolean {
|
|
||||||
return !!this.getModule(module)?.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getValueRef(module: string, key: string): Ref<Setting> {
|
|
||||||
const moduleVal = this.getModule(module);
|
|
||||||
if (!moduleVal) {
|
|
||||||
throw new Error(`Setting module not found: ${module}`);
|
|
||||||
}
|
|
||||||
const valueRef = moduleVal.get(key);
|
|
||||||
if (!valueRef) {
|
|
||||||
throw new Error(`Setting key not found: ${module}.${key}`);
|
|
||||||
}
|
|
||||||
return valueRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getInstance(): SettingMapper {
|
|
||||||
if (SettingMapper.instance) {
|
|
||||||
return SettingMapper.instance;
|
|
||||||
}
|
|
||||||
return SettingMapper.instance = new SettingMapper();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { AppContext } from "vue";
|
||||||
|
|
||||||
|
type MessageType = "info" | "success" | "warning" | "error";
|
||||||
|
|
||||||
|
type MessageHandler = (content: string) => unknown | Promise<unknown>;
|
||||||
|
|
||||||
|
type MessageApi = {
|
||||||
|
[type in MessageType]?: MessageHandler;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showTDesignMessage = async (
|
||||||
|
appContext: AppContext | undefined,
|
||||||
|
type: MessageType,
|
||||||
|
content: string
|
||||||
|
) => {
|
||||||
|
const handler = (appContext?.config.globalProperties.$message as MessageApi | undefined)?.[type];
|
||||||
|
if (handler) {
|
||||||
|
await handler(content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("error" === type) {
|
||||||
|
console.error(content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.warn(content);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -56,6 +56,13 @@ export default class Time {
|
|||||||
return `${Text.pad(d.getHours())}:${Text.pad(d.getMinutes())}`;
|
return `${Text.pad(d.getHours())}:${Text.pad(d.getMinutes())}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Unix 时间戳转带秒时间,兼容旧业务项目调用。 */
|
||||||
|
public static toLongTime(unix?: number): string {
|
||||||
|
if (!unix) return "";
|
||||||
|
const d = new Date(unix);
|
||||||
|
return `${Text.pad(d.getHours())}:${Text.pad(d.getMinutes())}:${Text.pad(d.getSeconds())}`;
|
||||||
|
}
|
||||||
|
|
||||||
public static toShortTime(unix?: number): string {
|
public static toShortTime(unix?: number): string {
|
||||||
if (!unix) return "";
|
if (!unix) return "";
|
||||||
const d = new Date(unix);
|
const d = new Date(unix);
|
||||||
@@ -72,6 +79,12 @@ export default class Time {
|
|||||||
return `${this.toDate(unix)} ${this.toTime(unix)}`;
|
return `${this.toDate(unix)} ${this.toTime(unix)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Unix 时间戳转带秒日期和时间,兼容旧业务项目调用。 */
|
||||||
|
public static toLongDateTime(unix?: number): string {
|
||||||
|
if (!unix) return "";
|
||||||
|
return `${this.toDate(unix)} ${this.toLongTime(unix)}`;
|
||||||
|
}
|
||||||
|
|
||||||
public static toPassedDate(unix?: number): string {
|
public static toPassedDate(unix?: number): string {
|
||||||
return this.toPassedDateTime(unix, false);
|
return this.toPassedDateTime(unix, false);
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-25
@@ -1,5 +1,5 @@
|
|||||||
import type { App } from "vue";
|
import type { App } from "vue";
|
||||||
import { InstallRecord, UserLevelType } from "../types";
|
import type { InstallRecord } from "../types";
|
||||||
|
|
||||||
export default class Toolkit {
|
export default class Toolkit {
|
||||||
|
|
||||||
@@ -301,27 +301,6 @@ export default class Toolkit {
|
|||||||
return debounced as ((...args: Parameters<T>) => void) & { cancel(): void };
|
return debounced as ((...args: Parameters<T>) => void) & { cancel(): void };
|
||||||
}
|
}
|
||||||
|
|
||||||
public static toUserLevel(exp?: number): UserLevelType {
|
|
||||||
exp = exp ?? 0;
|
|
||||||
const levels = [0, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096];
|
|
||||||
for (let i = 1; i < levels.length; i++) {
|
|
||||||
if (exp < levels[i]) {
|
|
||||||
return {
|
|
||||||
exp,
|
|
||||||
value: i - 1,
|
|
||||||
percent: (exp - levels[i - 1]) / (levels[i] - levels[i - 1]),
|
|
||||||
nextLevelUp: i === levels.length - 1 ? 4096 : levels[i]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
exp,
|
|
||||||
value: 8,
|
|
||||||
percent: 1,
|
|
||||||
nextLevelUp: 4096
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static toFormData(root?: object): FormData {
|
public static toFormData(root?: object): FormData {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
if (!root) {
|
if (!root) {
|
||||||
@@ -422,11 +401,22 @@ export default class Toolkit {
|
|||||||
}, interval);
|
}, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static formatJson(jsonStr?: string): string {
|
public static formatJson(jsonValue?: string | object): string {
|
||||||
if (!jsonStr) {
|
if (jsonValue === undefined || jsonValue === null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return JSON.stringify(JSON.parse(jsonStr), null, 4);
|
if (typeof jsonValue === "string") {
|
||||||
|
const value = jsonValue.trim();
|
||||||
|
if (!value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(value), null, 4);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return JSON.stringify(jsonValue, null, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static downloadBlob(blob: Blob, fileName: string): void {
|
public static downloadBlob(blob: Blob, fileName: string): void {
|
||||||
|
|||||||
@@ -1,226 +0,0 @@
|
|||||||
import type { Directive, DirectiveBinding, EffectScope } from "vue";
|
|
||||||
import { effectScope, watch } from "vue";
|
|
||||||
import { AccessMatchMode, useUserStore } from "../../store";
|
|
||||||
|
|
||||||
/** 访问控制类型 */
|
|
||||||
export type AccessControlType = "permission" | "role";
|
|
||||||
/** 指令结果生效方式 */
|
|
||||||
export type AccessEffectMode = "if" | "show";
|
|
||||||
|
|
||||||
/** 自定义回调参数 */
|
|
||||||
export type AccessDirectiveCallbackPayload = {
|
|
||||||
type: AccessControlType;
|
|
||||||
granted: boolean;
|
|
||||||
valueList: string[];
|
|
||||||
mode: AccessMatchMode;
|
|
||||||
el: HTMLElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 访问控制指令配置
|
|
||||||
* `value` 支持单个值或值数组
|
|
||||||
* 数组默认按 `or` 逻辑处理,需要 `and` 时显式传 `mode`
|
|
||||||
* `callback` 存在时优先执行回调,不再使用 `effect`
|
|
||||||
*/
|
|
||||||
export type AccessDirectiveConfig = {
|
|
||||||
value: string | string[];
|
|
||||||
mode?: AccessMatchMode;
|
|
||||||
effect?: AccessEffectMode;
|
|
||||||
reverse?: boolean;
|
|
||||||
display?: string;
|
|
||||||
callback?: (payload: AccessDirectiveCallbackPayload) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccessDirectiveValue = string | string[] | AccessDirectiveConfig;
|
|
||||||
|
|
||||||
type AccessDirectiveState = {
|
|
||||||
scope: EffectScope;
|
|
||||||
placeholder: Comment | null;
|
|
||||||
display: string;
|
|
||||||
config: AccessDirectiveConfig;
|
|
||||||
lastGranted?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccessMatcher = (store: ReturnType<typeof useUserStore>, valueList: string[], mode: AccessMatchMode) => boolean;
|
|
||||||
|
|
||||||
const ACCESS_STATE_KEY = "__vAccessState";
|
|
||||||
|
|
||||||
/** 过滤空字符串并统一裁剪空白 */
|
|
||||||
function normalizeValueList(value: string | string[]): string[] {
|
|
||||||
return (Array.isArray(value) ? value : [value])
|
|
||||||
.map(item => item?.trim())
|
|
||||||
.filter((item): item is string => !!item);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 统一把指令值转成标准配置 */
|
|
||||||
function normalizeConfig(value: AccessDirectiveValue): AccessDirectiveConfig {
|
|
||||||
if (typeof value === "string" || Array.isArray(value)) {
|
|
||||||
return {
|
|
||||||
value,
|
|
||||||
mode: "or",
|
|
||||||
effect: "if",
|
|
||||||
reverse: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: value?.value || [],
|
|
||||||
mode: value?.mode || "or",
|
|
||||||
effect: value?.effect || "if",
|
|
||||||
reverse: !!value?.reverse,
|
|
||||||
display: value?.display,
|
|
||||||
callback: value?.callback
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 恢复被 `if` 模式替换掉的真实元素 */
|
|
||||||
function restoreElement(el: HTMLElement, state: AccessDirectiveState): void {
|
|
||||||
if (state.placeholder?.parentNode) {
|
|
||||||
state.placeholder.parentNode.replaceChild(el, state.placeholder);
|
|
||||||
}
|
|
||||||
state.placeholder = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** `if` 模式:无权限时直接移出 DOM */
|
|
||||||
function applyIfEffect(el: HTMLElement, state: AccessDirectiveState, granted: boolean): void {
|
|
||||||
if (granted) {
|
|
||||||
restoreElement(el, state);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.placeholder && el.parentNode) {
|
|
||||||
state.placeholder = document.createComment("v-access");
|
|
||||||
el.parentNode.replaceChild(state.placeholder, el);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** `show` 模式:保留 DOM,仅切换 display */
|
|
||||||
function applyShowEffect(el: HTMLElement, state: AccessDirectiveState, granted: boolean): void {
|
|
||||||
restoreElement(el, state);
|
|
||||||
el.style.display = granted ? state.display : "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 自定义模式:交给调用方自己处理样式或行为 */
|
|
||||||
function applyCustomCallback(
|
|
||||||
el: HTMLElement,
|
|
||||||
state: AccessDirectiveState,
|
|
||||||
granted: boolean,
|
|
||||||
type: AccessControlType,
|
|
||||||
valueList: string[],
|
|
||||||
mode: AccessMatchMode
|
|
||||||
): void {
|
|
||||||
restoreElement(el, state);
|
|
||||||
if (state.lastGranted === granted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.config.callback?.({
|
|
||||||
type,
|
|
||||||
granted,
|
|
||||||
valueList,
|
|
||||||
mode,
|
|
||||||
el
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 根据当前登录用户状态刷新指令效果 */
|
|
||||||
function updateAccess(
|
|
||||||
el: HTMLElement,
|
|
||||||
state: AccessDirectiveState,
|
|
||||||
type: AccessControlType,
|
|
||||||
matcher: AccessMatcher
|
|
||||||
): void {
|
|
||||||
const store = useUserStore();
|
|
||||||
const config = state.config;
|
|
||||||
const mode = config.mode || "or";
|
|
||||||
const valueList = normalizeValueList(config.value);
|
|
||||||
const granted = config.reverse ? !matcher(store, valueList, mode) : matcher(store, valueList, mode);
|
|
||||||
|
|
||||||
if (config.callback) {
|
|
||||||
applyCustomCallback(el, state, granted, type, valueList, mode);
|
|
||||||
} else if (config.effect === "show") {
|
|
||||||
applyShowEffect(el, state, granted);
|
|
||||||
} else {
|
|
||||||
applyIfEffect(el, state, granted);
|
|
||||||
}
|
|
||||||
|
|
||||||
state.lastGranted = granted;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getState(el: HTMLElement): AccessDirectiveState | undefined {
|
|
||||||
return (el as HTMLElement & { [ACCESS_STATE_KEY]?: AccessDirectiveState })[ACCESS_STATE_KEY];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setState(el: HTMLElement, state: AccessDirectiveState): void {
|
|
||||||
(el as HTMLElement & { [ACCESS_STATE_KEY]?: AccessDirectiveState })[ACCESS_STATE_KEY] = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearState(el: HTMLElement): void {
|
|
||||||
delete (el as HTMLElement & { [ACCESS_STATE_KEY]?: AccessDirectiveState })[ACCESS_STATE_KEY];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 卸载时清掉占位注释,避免遗留无用节点 */
|
|
||||||
function clearPlaceholder(state: AccessDirectiveState): void {
|
|
||||||
if (state.placeholder?.parentNode) {
|
|
||||||
state.placeholder.parentNode.removeChild(state.placeholder);
|
|
||||||
}
|
|
||||||
state.placeholder = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 创建角色/权限通用指令 */
|
|
||||||
function createAccessDirective(type: AccessControlType, matcher: AccessMatcher): Directive<HTMLElement, AccessDirectiveValue> {
|
|
||||||
return {
|
|
||||||
mounted(el: HTMLElement, binding: DirectiveBinding<AccessDirectiveValue>) {
|
|
||||||
const state: AccessDirectiveState = {
|
|
||||||
scope: effectScope(),
|
|
||||||
placeholder: null,
|
|
||||||
display: el.style.display,
|
|
||||||
config: normalizeConfig(binding.value)
|
|
||||||
};
|
|
||||||
|
|
||||||
setState(el, state);
|
|
||||||
|
|
||||||
state.scope.run(() => {
|
|
||||||
const store = useUserStore();
|
|
||||||
watch(
|
|
||||||
() => {
|
|
||||||
return type === "permission" ? store.getPermissionList().join("|") : store.getRoleList().join("|");
|
|
||||||
},
|
|
||||||
() => updateAccess(el, state, type, matcher),
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
updated(el: HTMLElement, binding: DirectiveBinding<AccessDirectiveValue>) {
|
|
||||||
const state = getState(el);
|
|
||||||
if (!state) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.config = normalizeConfig(binding.value);
|
|
||||||
updateAccess(el, state, type, matcher);
|
|
||||||
},
|
|
||||||
unmounted(el: HTMLElement) {
|
|
||||||
const state = getState(el);
|
|
||||||
if (!state) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.scope.stop();
|
|
||||||
clearPlaceholder(state);
|
|
||||||
clearState(el);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 权限指令:`v-can` */
|
|
||||||
export const VCan = createAccessDirective("permission", (store, valueList, mode) => {
|
|
||||||
return store.hasPermissions(valueList, mode);
|
|
||||||
});
|
|
||||||
|
|
||||||
/** 角色指令:`v-role` */
|
|
||||||
export const VRole = createAccessDirective("role", (store, valueList, mode) => {
|
|
||||||
return store.hasRoles(valueList, mode);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default VCan;
|
|
||||||
+126
-99
@@ -1,115 +1,142 @@
|
|||||||
import type { Directive, DirectiveBinding } from "vue";
|
import type { Directive, DirectiveBinding } from "vue";
|
||||||
import Toolkit from "../Toolkit";
|
import { appendPopupValue, clearPopup, getPopup, getPopupContent, hidePopup, movePopup, PopupType, showPopup, type PopupConfig } from "../Popup";
|
||||||
|
|
||||||
export type PopupType = "TEXT" | "IMG" | "HTML" | "ELEMENT";
|
|
||||||
|
|
||||||
/** */
|
|
||||||
export type PopupConfig = {
|
|
||||||
|
|
||||||
type: PopupType,
|
|
||||||
value?: string | HTMLElement;
|
|
||||||
canShow?: () => boolean;
|
|
||||||
beforeShow?: (type: PopupType, value: string | HTMLElement) => Promise<void>;
|
|
||||||
afterHidden?: (type: PopupType, value: string | HTMLElement) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Popup 弹出提示 DOM 节点,全局唯一
|
// Popup 弹出提示 DOM 节点,全局唯一
|
||||||
let popup: HTMLElement | null;
|
let popup: HTMLElement | null;
|
||||||
|
let popupContent: HTMLElement | null;
|
||||||
|
|
||||||
const VPopup: Directive = {
|
type PopupBindingValue = string | PopupConfig;
|
||||||
|
|
||||||
mounted(el: HTMLElement, binding: DirectiveBinding<PopupConfig>) {
|
type PopupState = {
|
||||||
// 转配置
|
config: PopupConfig;
|
||||||
let config: PopupConfig;
|
isShowing: boolean;
|
||||||
if (binding.arg && binding.arg === "config") {
|
onMouseEnter: (e: MouseEvent) => Promise<void>;
|
||||||
config = binding.value as PopupConfig;
|
onMouseMove: (e: MouseEvent) => void;
|
||||||
} else {
|
onMouseLeave: () => Promise<void>;
|
||||||
config = {
|
};
|
||||||
type: "TEXT",
|
|
||||||
value: binding.value as any as string,
|
const popupStates = new WeakMap<HTMLElement, PopupState>();
|
||||||
canShow: () => true
|
|
||||||
};
|
function getPopupElements() {
|
||||||
}
|
if (!popup) {
|
||||||
// Popup 节点
|
popup = getPopup();
|
||||||
if (!popup) {
|
}
|
||||||
popup = document.getElementById("tui-popup");
|
if (!popupContent) {
|
||||||
}
|
popupContent = getPopupContent();
|
||||||
let isShowing = false;
|
}
|
||||||
// 显示
|
return {
|
||||||
el.addEventListener("mouseenter", async e => {
|
popup,
|
||||||
if (!config.value) {
|
popupContent
|
||||||
console.warn("not found popup value", config);
|
};
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
if (config.beforeShow) {
|
function getConfig(binding: DirectiveBinding<PopupBindingValue>): PopupConfig {
|
||||||
await config.beforeShow(config.type, config.value);
|
if (binding.arg === "config") {
|
||||||
}
|
const config = binding.value as PopupConfig;
|
||||||
if (config.canShow && config.canShow() && popup) {
|
return {
|
||||||
let el: HTMLElement | null = null;
|
...config,
|
||||||
if (!config) {
|
type: config.type ?? PopupType.TEXT,
|
||||||
el = document.createElement("div");
|
canShow: config.canShow ?? (() => true)
|
||||||
el.className = "text";
|
};
|
||||||
el.textContent = config as string;
|
}
|
||||||
popup.appendChild(el);
|
return {
|
||||||
|
type: PopupType.TEXT,
|
||||||
|
value: binding.value as string,
|
||||||
|
canShow: () => true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshPopupContent(config: PopupConfig) {
|
||||||
|
const popupElements = getPopupElements();
|
||||||
|
if (!config.value || !popupElements.popupContent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
clearPopup(popupElements.popupContent);
|
||||||
|
appendPopupValue(popupElements.popupContent, config.type, config.value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePopupContent(state: PopupState) {
|
||||||
|
const popupElements = getPopupElements();
|
||||||
|
state.isShowing = false;
|
||||||
|
if (popupElements.popup) {
|
||||||
|
hidePopup(popupElements.popup);
|
||||||
|
}
|
||||||
|
// 内容和定位分离,避免直接清空根节点破坏 teleport
|
||||||
|
if (popupElements.popupContent) {
|
||||||
|
clearPopup(popupElements.popupContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const VPopup: Directive<HTMLElement, PopupBindingValue> = {
|
||||||
|
|
||||||
|
mounted(el, binding) {
|
||||||
|
const state: PopupState = {
|
||||||
|
config: getConfig(binding),
|
||||||
|
isShowing: false,
|
||||||
|
onMouseEnter: async e => {
|
||||||
|
const popupElements = getPopupElements();
|
||||||
|
let config = state.config;
|
||||||
|
if (!config.value) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
switch (config.type) {
|
if (config.beforeShow) {
|
||||||
case "TEXT":
|
await config.beforeShow(config.type, config.value);
|
||||||
// 文本
|
|
||||||
el = document.createElement("div");
|
|
||||||
el.className = "text";
|
|
||||||
el.textContent = config.value as string;
|
|
||||||
popup.appendChild(el);
|
|
||||||
break;
|
|
||||||
case "IMG":
|
|
||||||
// 图片
|
|
||||||
el = document.createElement("img");
|
|
||||||
(el as HTMLImageElement).src = config.value as string;
|
|
||||||
popup.appendChild(el);
|
|
||||||
break;
|
|
||||||
case "HTML":
|
|
||||||
// HTML 字符串
|
|
||||||
popup.appendChild(Toolkit.toDOM(config.value as string));
|
|
||||||
break;
|
|
||||||
case "ELEMENT":
|
|
||||||
// DOM 节点
|
|
||||||
if (config.value instanceof HTMLElement) {
|
|
||||||
const valueEl = config.value as HTMLElement;
|
|
||||||
valueEl.style.display = "block";
|
|
||||||
popup.appendChild(valueEl);
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
console.error(config);
|
|
||||||
throw new Error("Vue 指令错误:v-popup:el 的值不是 HTML 元素");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
popup.style.left = (e.x + 20) + "px";
|
config = state.config;
|
||||||
popup.style.top = (e.y + 14) + "px";
|
if (config.canShow?.() && popupElements.popup && refreshPopupContent(config)) {
|
||||||
popup.style.visibility = "visible";
|
movePopup(popupElements.popup, e.x, e.y);
|
||||||
isShowing = true;
|
showPopup(popupElements.popup);
|
||||||
}
|
state.isShowing = true;
|
||||||
}, false);
|
}
|
||||||
// 移动
|
},
|
||||||
el.addEventListener("mousemove", async (e) => {
|
onMouseMove: e => {
|
||||||
if (config.canShow && config.canShow() && isShowing && popup) {
|
const popupElements = getPopupElements();
|
||||||
popup.style.left = (e.x + 20) + "px";
|
if (state.config.canShow?.() && state.isShowing && popupElements.popup) {
|
||||||
popup.style.top = (e.y + 14) + "px";
|
movePopup(popupElements.popup, e.x, e.y);
|
||||||
}
|
}
|
||||||
}, false);
|
},
|
||||||
// 隐藏
|
onMouseLeave: async () => {
|
||||||
el.addEventListener("mouseleave", async () => {
|
const config = state.config;
|
||||||
if (popup) {
|
hidePopupContent(state);
|
||||||
popup.style.visibility = "hidden";
|
|
||||||
popup.innerText = "";
|
|
||||||
popup.style.left = "0px";
|
|
||||||
popup.style.top = "0px";
|
|
||||||
|
|
||||||
// 隐藏后事件
|
|
||||||
if (config.afterHidden && config.value) {
|
if (config.afterHidden && config.value) {
|
||||||
await config.afterHidden(config.type, config.value);
|
await config.afterHidden(config.type, config.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, false);
|
};
|
||||||
|
popupStates.set(el, state);
|
||||||
|
el.addEventListener("mouseenter", state.onMouseEnter, false);
|
||||||
|
el.addEventListener("mousemove", state.onMouseMove, false);
|
||||||
|
el.addEventListener("mouseleave", state.onMouseLeave, false);
|
||||||
|
},
|
||||||
|
|
||||||
|
updated(el, binding) {
|
||||||
|
const state = popupStates.get(el);
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.config = getConfig(binding);
|
||||||
|
if (!state.isShowing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.config.canShow?.() || !refreshPopupContent(state.config)) {
|
||||||
|
hidePopupContent(state);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unmounted(el) {
|
||||||
|
const state = popupStates.get(el);
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.removeEventListener("mouseenter", state.onMouseEnter, false);
|
||||||
|
el.removeEventListener("mousemove", state.onMouseMove, false);
|
||||||
|
el.removeEventListener("mouseleave", state.onMouseLeave, false);
|
||||||
|
hidePopupContent(state);
|
||||||
|
popupStates.delete(el);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VPopup;
|
export default VPopup;
|
||||||
|
export { PopupType };
|
||||||
|
export type { PopupConfig };
|
||||||
|
|||||||
+1
-5
@@ -2,7 +2,6 @@ export { default as Cooker } from "./Cooker";
|
|||||||
export { default as Events } from "./Events";
|
export { default as Events } from "./Events";
|
||||||
export { default as IOSize } from "./IOSize";
|
export { default as IOSize } from "./IOSize";
|
||||||
export { default as Markdown } from "./Markdown";
|
export { default as Markdown } from "./Markdown";
|
||||||
export { default as Network } from "./Network";
|
|
||||||
export { default as Prismjs } from "./Prismjs";
|
export { default as Prismjs } from "./Prismjs";
|
||||||
export { default as Resizer } from "./Resizer";
|
export { default as Resizer } from "./Resizer";
|
||||||
export { default as Scroller } from "./Scroller";
|
export { default as Scroller } from "./Scroller";
|
||||||
@@ -11,15 +10,12 @@ export { default as Text } from "./Text";
|
|||||||
export { default as Time } from "./Time";
|
export { default as Time } from "./Time";
|
||||||
export { default as Toolkit } from "./Toolkit";
|
export { default as Toolkit } from "./Toolkit";
|
||||||
export { default as IconMapper } from "./IconMapper";
|
export { default as IconMapper } from "./IconMapper";
|
||||||
export { default as SettingMapper } from "./SettingMapper";
|
export { showTDesignMessage } from "./TDesignMessage";
|
||||||
|
|
||||||
export { default as VCan } from "./directives/Access";
|
|
||||||
export { default as VDraggable } from "./directives/Draggable";
|
export { default as VDraggable } from "./directives/Draggable";
|
||||||
export { default as VPopup } from "./directives/Popup";
|
export { default as VPopup } from "./directives/Popup";
|
||||||
|
|
||||||
export * from "./MethodLocker";
|
export * from "./MethodLocker";
|
||||||
export * from "./Network";
|
|
||||||
export * from "./Prismjs";
|
export * from "./Prismjs";
|
||||||
export * from "./directives/Access";
|
|
||||||
export * from "./directives/Draggable";
|
export * from "./directives/Draggable";
|
||||||
export * from "./directives/Popup";
|
export * from "./directives/Popup";
|
||||||
|
|||||||
Reference in New Issue
Block a user