Compare commits
282 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce642aceed | ||
|
|
d5411489c0 | ||
|
|
26c66627f8 | ||
|
|
c654986042 | ||
|
|
c5b5c15f99 | ||
|
|
7727b0f1c3 | ||
|
|
3d551ac45b | ||
|
|
555a00b731 | ||
|
|
9f9091b23e | ||
|
|
14c343142f | ||
|
|
890920775a | ||
|
|
7b38d2d74f | ||
|
|
e85c2870e2 | ||
|
|
cfbc5802e4 | ||
|
|
40cdb820fb | ||
|
|
f63beb776e | ||
|
|
20f031b2e2 | ||
|
|
b0f28b7e7c | ||
|
|
62bb6de80d | ||
|
|
2d7880351b | ||
|
|
e1ee3ef2db | ||
|
|
aff30c48a0 | ||
|
|
55eea50a6e | ||
|
|
9ff212c94d | ||
|
|
6350c7e9e6 | ||
|
|
d097c1c17c | ||
|
|
b9ee6b4039 | ||
|
|
f1238a03b3 | ||
|
|
e90cf3ee77 | ||
|
|
468607c8e8 | ||
|
|
5bd9283177 | ||
|
|
117b12348c | ||
|
|
0d325b6eb8 | ||
|
|
86d5903f32 | ||
|
|
3b518d6f33 | ||
|
|
78f57e7d4b | ||
|
|
f710f1bfc0 | ||
|
|
c5d4fc62e6 | ||
|
|
60606d5eb9 | ||
|
|
8751236380 | ||
|
|
2291ce3680 | ||
|
|
16ed589857 | ||
|
|
b59254ca42 | ||
|
|
6e3f9b285d | ||
|
|
8bcff774fa | ||
|
|
9b04b12dec | ||
|
|
b22d81a9e9 | ||
|
|
6c80a3a8cd | ||
|
|
059d836653 | ||
|
|
2c3ecfeb6f | ||
|
|
b07e5eecc3 | ||
|
|
1847bc90cf | ||
|
|
899aaae47c | ||
|
|
bcc05086a4 | ||
|
|
d2cc547875 | ||
|
|
c6127f440e | ||
|
|
c2849ad49f | ||
|
|
9c6ba294f9 | ||
|
|
b2a8707e91 | ||
|
|
193e3085a9 | ||
|
|
2401f38e9f | ||
|
|
4ea65727a1 | ||
|
|
3d6cfe260c | ||
|
|
4c33a09c3c | ||
|
|
8d25743680 | ||
|
|
4bc5b763a2 | ||
|
|
00ea179c90 | ||
|
|
a17d40d2d0 | ||
|
|
62ddd703f1 | ||
|
|
77ab0ccae2 | ||
|
|
f377ac3fcc | ||
|
|
a81becd77b | ||
|
|
a004f1c758 | ||
|
|
b19b015986 | ||
|
|
3f0c1213ad | ||
|
|
f3a781d857 | ||
|
|
a07a32f648 | ||
|
|
1e1117b187 | ||
|
|
0e161b1735 | ||
|
|
98cbb8dc29 | ||
|
|
9c17d2d335 | ||
|
|
d4ea7f48c0 | ||
|
|
3ecfe3ba94 | ||
|
|
a959594348 | ||
|
|
faa1027c04 | ||
|
|
6f68732ef9 | ||
|
|
3a4e936938 | ||
|
|
8b4b79fa10 | ||
|
|
ce0dda0455 | ||
|
|
fd1ee398c4 | ||
|
|
1488017bf2 | ||
|
|
367f4236ad | ||
|
|
ec202f22e8 | ||
|
|
08081da29e | ||
|
|
5511424bd6 | ||
|
|
9f2c848413 | ||
|
|
d5efe2b499 | ||
|
|
9b989fc40f | ||
|
|
567e85d1f8 | ||
|
|
af00036e7f | ||
|
|
e58e6e2a3e | ||
|
|
370039664f | ||
|
|
d244363321 | ||
|
|
984d502259 | ||
|
|
34b418af96 | ||
|
|
eee0c0c878 | ||
|
|
952ef368ab | ||
|
|
dfaa789f7c | ||
|
|
74dd549ffb | ||
|
|
4d8f369ba0 | ||
|
|
824e2d72c7 | ||
|
|
143aa79797 | ||
|
|
0e1120f407 | ||
|
|
c8dbb9672a | ||
|
|
372b74776f | ||
|
|
abcc3c6411 | ||
|
|
ec4ab8762c | ||
|
|
bc93de8ff2 | ||
|
|
b426f3c6f2 | ||
|
|
99d9bb29ce | ||
|
|
5e109c666b | ||
|
|
0bed216735 | ||
|
|
d55bb8d336 | ||
|
|
7c32b3edf0 | ||
|
|
121cb7e442 | ||
|
|
dec3e1ea92 | ||
|
|
664b6610f3 | ||
|
|
44163f0fb2 | ||
|
|
d43865fcad | ||
|
|
fed92f3853 | ||
|
|
823d2a816e | ||
|
|
046c21edf6 | ||
|
|
8236d80b42 | ||
|
|
90e7eb1c79 | ||
|
|
ef09868af1 | ||
|
|
028981e3ae | ||
|
|
e8a6274cf6 | ||
|
|
ffd0265526 | ||
|
|
13d7344bc0 | ||
|
|
2ad36f92c5 | ||
|
|
36b02f4423 | ||
|
|
01df990aa8 | ||
|
|
49b71fcf5d | ||
|
|
9e43d77ac4 | ||
|
|
3ab9af720b | ||
|
|
abae304f87 | ||
|
|
659d8bff66 | ||
|
|
1786e10101 | ||
|
|
bda7f929e7 | ||
|
|
c309f80a94 | ||
|
|
97c987c561 | ||
|
|
48949104e0 | ||
|
|
a38cc4fe34 | ||
|
|
495dfbcb28 | ||
|
|
6e4dbd912b | ||
|
|
82904d956d | ||
|
|
ec7118b376 | ||
|
|
058b32a263 | ||
|
|
e7b960838e | ||
|
|
14e776a287 | ||
|
|
73f11b920f | ||
|
|
5c93040a8e | ||
|
|
a517769e8a | ||
|
|
4bb59a9f05 | ||
|
|
b37879d2d4 | ||
|
|
05defc39d7 | ||
|
|
18bfad07d2 | ||
|
|
b83591255d | ||
|
|
804350bc81 | ||
|
|
46e1cae0bb | ||
|
|
81062d4580 | ||
|
|
55481db2ee | ||
|
|
ecdd12f5a9 | ||
|
|
ef92cdc183 | ||
|
|
08f4a6cf2c | ||
|
|
38889acb4e | ||
|
|
c0517cd29a | ||
|
|
084449ccf3 | ||
|
|
0e8203ae03 | ||
|
|
236440be52 | ||
|
|
6f7e4bb272 | ||
|
|
38dcd3635a | ||
|
|
a3f3330dad | ||
|
|
bbc6c57c08 | ||
|
|
2f36a8edef | ||
|
|
df637fb887 | ||
|
|
be74c92a35 | ||
|
|
a219a64e20 | ||
|
|
25c22a276a | ||
|
|
6e6be057ca | ||
|
|
af69efa48b | ||
|
|
c551083fa4 | ||
|
|
9767feed29 | ||
|
|
4392818e92 | ||
|
|
8d22bafeb6 | ||
|
|
89ddd1fb78 | ||
|
|
24513fa22b | ||
|
|
cddde0c2a0 | ||
|
|
9c674e0018 | ||
|
|
0c6476d283 | ||
|
|
bf0c529a59 | ||
|
|
877bb4d4a2 | ||
|
|
dc4db0b2b3 | ||
|
|
a738d4a3b9 | ||
|
|
e9866a04df | ||
|
|
4f5193d602 | ||
|
|
37b92c55ba | ||
|
|
9299f1bcb6 | ||
|
|
7fe12192df | ||
|
|
1169644ab3 | ||
|
|
6f7770ed43 | ||
|
|
8059fd6f90 | ||
|
|
556dbd8d78 | ||
|
|
6695fd8c14 | ||
|
|
3ab0229275 | ||
|
|
99467127a0 | ||
|
|
90d73b7bd5 | ||
|
|
2e326e1798 | ||
|
|
251eac93c7 | ||
|
|
c74d70808c | ||
|
|
e63b2d7152 | ||
|
|
16b29b56a5 | ||
|
|
6d79c4fe2f | ||
|
|
4b1fb60ee3 | ||
|
|
1d2be54f9e | ||
|
|
83547e32db | ||
|
|
70ddb929f2 | ||
|
|
8b22961394 | ||
|
|
c15d42c179 | ||
|
|
098e473cab | ||
|
|
f6f3d9368a | ||
|
|
9558a420e9 | ||
|
|
4d3b69ca34 | ||
|
|
fdcc4a44c8 | ||
|
|
5de0494538 | ||
|
|
2045f833e4 | ||
|
|
cc4f89aac1 | ||
|
|
1c2f2c17d4 | ||
|
|
ace7a6621f | ||
|
|
d02fe55a1e | ||
|
|
9b753a8f5b | ||
|
|
11e82582b8 | ||
|
|
419358863e | ||
|
|
1d0d7f9975 | ||
|
|
c5f564372b | ||
|
|
a50f0cd727 | ||
|
|
96f6f55138 | ||
|
|
6a45c8b358 | ||
|
|
165937596e | ||
|
|
fb976f043b | ||
|
|
ecb9c4e51a | ||
|
|
9e8c3b495c | ||
|
|
24a37fc33c | ||
|
|
d09a21114d | ||
|
|
6e2b12501f | ||
|
|
2a56e116cf | ||
|
|
6de4f238d8 | ||
|
|
1b426c5957 | ||
|
|
82454a650c | ||
|
|
227b6bd7ef | ||
|
|
9554025daf | ||
|
|
0eb5d607bf | ||
|
|
750f4bc276 | ||
|
|
d0aada1d3d | ||
|
|
8a4848387c | ||
|
|
6904fc7da3 | ||
|
|
28c55a05e6 | ||
|
|
562c829267 | ||
|
|
b200ed242d | ||
|
|
815cfe55df | ||
|
|
40a1094d74 | ||
|
|
346650c091 | ||
|
|
7f74715f51 | ||
|
|
b6fcee517d | ||
|
|
4f62551f6b | ||
|
|
3980249271 | ||
|
|
e3b11b1130 | ||
|
|
f866f23af1 | ||
|
|
c793bc24f0 | ||
|
|
591a46d559 | ||
|
|
2852f26702 | ||
|
|
fc818fdfd6 |
@@ -1 +1 @@
|
||||
VITE_API_BASE_URL=/api/v1/
|
||||
VITE_API_BASE_URL=api/v1/
|
||||
|
||||
2
.github/workflows/build.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '20'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Download Icons
|
||||
|
||||
@@ -21,14 +21,9 @@
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
"max-line-length": [
|
||||
120,
|
||||
{
|
||||
"ignore": "comments"
|
||||
}
|
||||
],
|
||||
"liberty/use-logical-spec": true,
|
||||
"selector-class-pattern": null,
|
||||
"color-function-notation": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"fix": true
|
||||
}
|
||||
7
.vscode/settings.json
vendored
@@ -31,8 +31,8 @@
|
||||
"volar.preview.port": 3000,
|
||||
"volar.completion.preferredTagNameCase": "pascal",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.fixAll.stylelint": true
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.stylelint": "explicit"
|
||||
},
|
||||
"eslint.alwaysShowStatus": true,
|
||||
"eslint.format.enable": true,
|
||||
@@ -53,6 +53,7 @@
|
||||
"stylelint",
|
||||
"touchless",
|
||||
"triggerer",
|
||||
"unref",
|
||||
"vuetify"
|
||||
],
|
||||
// Extension: Comment Anchors
|
||||
@@ -104,4 +105,4 @@
|
||||
]
|
||||
},
|
||||
"vue3snippets.enable-compile-vue-file-on-did-save-code": false
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# MoviePilot-Frontend
|
||||
|
||||
[MoviePilot](https://github.com/jxxghp/MoviePilot) 的前端项目。
|
||||
[MoviePilot](https://github.com/jxxghp/MoviePilot) 的前端项目,NodeJS版本:>= `v20.12.1`。
|
||||
|
||||
## 推荐的IDE设置
|
||||
|
||||
|
||||
322
auto-imports.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
@@ -41,6 +42,7 @@ declare global {
|
||||
const h: typeof import('vue')['h']
|
||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const injectLocal: typeof import('@vueuse/core')['injectLocal']
|
||||
const isDefined: typeof import('@vueuse/core')['isDefined']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
@@ -77,6 +79,7 @@ declare global {
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const provideLocal: typeof import('@vueuse/core')['provideLocal']
|
||||
const reactify: typeof import('@vueuse/core')['reactify']
|
||||
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
@@ -105,6 +108,7 @@ declare global {
|
||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||
@@ -143,6 +147,7 @@ declare global {
|
||||
const useCeil: typeof import('@vueuse/math')['useCeil']
|
||||
const useClamp: typeof import('@vueuse/math')['useClamp']
|
||||
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
|
||||
const useCloned: typeof import('@vueuse/core')['useCloned']
|
||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||
@@ -308,11 +313,13 @@ declare global {
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
// for vue template auto import
|
||||
import { UnwrapRef } from 'vue'
|
||||
declare module 'vue' {
|
||||
interface GlobalComponents {}
|
||||
interface ComponentCustomProperties {
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
@@ -351,6 +358,7 @@ declare module 'vue' {
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
|
||||
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
@@ -387,6 +395,7 @@ declare module 'vue' {
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
|
||||
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
|
||||
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
@@ -415,6 +424,7 @@ declare module 'vue' {
|
||||
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
|
||||
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
|
||||
@@ -453,6 +463,316 @@ declare module 'vue' {
|
||||
readonly useCeil: UnwrapRef<typeof import('@vueuse/math')['useCeil']>
|
||||
readonly useClamp: UnwrapRef<typeof import('@vueuse/math')['useClamp']>
|
||||
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
|
||||
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
|
||||
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
|
||||
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
||||
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
||||
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
|
||||
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
|
||||
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
|
||||
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
|
||||
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
|
||||
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
|
||||
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
|
||||
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
|
||||
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
|
||||
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
|
||||
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
|
||||
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
|
||||
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
|
||||
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
|
||||
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
|
||||
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
|
||||
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
|
||||
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
|
||||
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
|
||||
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
|
||||
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
|
||||
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
|
||||
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
|
||||
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
|
||||
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
|
||||
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
|
||||
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
|
||||
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
|
||||
readonly useFloor: UnwrapRef<typeof import('@vueuse/math')['useFloor']>
|
||||
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
|
||||
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
|
||||
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
|
||||
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
|
||||
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
|
||||
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
|
||||
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
|
||||
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
|
||||
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
|
||||
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
|
||||
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
|
||||
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
|
||||
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
|
||||
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
|
||||
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
||||
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
|
||||
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
|
||||
readonly useMath: UnwrapRef<typeof import('@vueuse/math')['useMath']>
|
||||
readonly useMax: UnwrapRef<typeof import('@vueuse/math')['useMax']>
|
||||
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
|
||||
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
|
||||
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
|
||||
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
|
||||
readonly useMin: UnwrapRef<typeof import('@vueuse/math')['useMin']>
|
||||
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
|
||||
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
|
||||
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
|
||||
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
|
||||
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
|
||||
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
|
||||
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
|
||||
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
|
||||
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
|
||||
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
|
||||
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
|
||||
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
|
||||
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
|
||||
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
|
||||
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
|
||||
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
|
||||
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
|
||||
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
|
||||
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
|
||||
readonly usePrecision: UnwrapRef<typeof import('@vueuse/math')['usePrecision']>
|
||||
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
|
||||
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
|
||||
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
|
||||
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
||||
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||
readonly useProjection: UnwrapRef<typeof import('@vueuse/math')['useProjection']>
|
||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
||||
readonly useRound: UnwrapRef<typeof import('@vueuse/math')['useRound']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
|
||||
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
|
||||
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
|
||||
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
|
||||
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
|
||||
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
||||
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
|
||||
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
||||
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
|
||||
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
||||
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
||||
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
|
||||
readonly useStore: UnwrapRef<typeof import('vuex')['useStore']>
|
||||
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
|
||||
readonly useSum: UnwrapRef<typeof import('@vueuse/math')['useSum']>
|
||||
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
|
||||
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
||||
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
|
||||
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
|
||||
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
|
||||
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
|
||||
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
|
||||
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
|
||||
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
|
||||
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
|
||||
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
|
||||
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
|
||||
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
||||
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
|
||||
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
|
||||
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
|
||||
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
|
||||
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
|
||||
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
|
||||
readonly useTrunc: UnwrapRef<typeof import('@vueuse/math')['useTrunc']>
|
||||
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
|
||||
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
|
||||
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
|
||||
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
|
||||
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
|
||||
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
|
||||
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
|
||||
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
|
||||
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
|
||||
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
|
||||
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
|
||||
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
|
||||
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
|
||||
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
|
||||
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
|
||||
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
|
||||
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
|
||||
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
|
||||
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
|
||||
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
|
||||
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
|
||||
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
|
||||
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
|
||||
}
|
||||
}
|
||||
declare module '@vue/runtime-core' {
|
||||
interface GlobalComponents {}
|
||||
interface ComponentCustomProperties {
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
|
||||
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
|
||||
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
|
||||
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
|
||||
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
|
||||
readonly createGenericProjection: UnwrapRef<typeof import('@vueuse/math')['createGenericProjection']>
|
||||
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
|
||||
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
||||
readonly createLogger: UnwrapRef<typeof import('vuex')['createLogger']>
|
||||
readonly createNamespacedHelpers: UnwrapRef<typeof import('vuex')['createNamespacedHelpers']>
|
||||
readonly createProjection: UnwrapRef<typeof import('@vueuse/math')['createProjection']>
|
||||
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
||||
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
|
||||
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
||||
readonly createStore: UnwrapRef<typeof import('vuex')['createStore']>
|
||||
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
|
||||
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
|
||||
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
|
||||
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly logicAnd: UnwrapRef<typeof import('@vueuse/math')['logicAnd']>
|
||||
readonly logicNot: UnwrapRef<typeof import('@vueuse/math')['logicNot']>
|
||||
readonly logicOr: UnwrapRef<typeof import('@vueuse/math')['logicOr']>
|
||||
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||
readonly mapActions: UnwrapRef<typeof import('vuex')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('vuex')['mapGetters']>
|
||||
readonly mapMutations: UnwrapRef<typeof import('vuex')['mapMutations']>
|
||||
readonly mapState: UnwrapRef<typeof import('vuex')['mapState']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
||||
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
|
||||
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
|
||||
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
|
||||
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
|
||||
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
|
||||
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
|
||||
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
|
||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
|
||||
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
|
||||
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
|
||||
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
|
||||
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
|
||||
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
|
||||
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
|
||||
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
|
||||
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
|
||||
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
|
||||
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
|
||||
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
|
||||
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
|
||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||
readonly useAbs: UnwrapRef<typeof import('@vueuse/math')['useAbs']>
|
||||
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
|
||||
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
|
||||
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
|
||||
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
|
||||
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
|
||||
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
|
||||
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
|
||||
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
|
||||
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
|
||||
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
|
||||
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
|
||||
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
|
||||
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
||||
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useAverage: UnwrapRef<typeof import('@vueuse/math')['useAverage']>
|
||||
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
|
||||
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
|
||||
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
|
||||
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
|
||||
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
|
||||
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
|
||||
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
|
||||
readonly useCeil: UnwrapRef<typeof import('@vueuse/math')['useCeil']>
|
||||
readonly useClamp: UnwrapRef<typeof import('@vueuse/math')['useClamp']>
|
||||
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
|
||||
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
|
||||
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
|
||||
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
||||
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
||||
|
||||
6
components.d.ts
vendored
@@ -3,18 +3,18 @@
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default']
|
||||
ErrorHeader: typeof import('./src/@core/components/ErrorHeader.vue')['default']
|
||||
ExistIcon: typeof import('./src/@core/components/ExistIcon.vue')['default']
|
||||
LoadingBanner: typeof import('./src/@core/components/LoadingBanner.vue')['default']
|
||||
MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
StatIcon: typeof import('./src/@core/components/StatIcon.vue')['default']
|
||||
ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
352
index.html
@@ -1,214 +1,162 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width, user-scalable=no" />
|
||||
<title>MoviePilot</title>
|
||||
<meta name="Robots" content="noindex,nofollow,noarchive" />
|
||||
<meta name="referrer" content="origin" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="manifest.json" crossorigin="use-credentials" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="MoviePilot" />
|
||||
<meta name="description" content="MoviePilot" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="referrer" content="never" />
|
||||
<meta name="msapplication-TileColor" content="#7D34FD" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#28243D" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)" />
|
||||
<meta name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<link rel="stylesheet" type="text/css" href="/loader.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="loading-bg">
|
||||
<div class="loading-logo">
|
||||
<!-- Logo -->
|
||||
<svg
|
||||
width="100px"
|
||||
height="100px"
|
||||
viewBox="0 0 192 192"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2"
|
||||
>
|
||||
<g transform="matrix(1,0,0,1,-2606,-236)">
|
||||
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">
|
||||
<rect x="0" y="0" width="192" height="192" style="fill: none" />
|
||||
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)">
|
||||
<head>
|
||||
<meta http-equiv="pragma" content="no-cache">
|
||||
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate">
|
||||
<meta http-equiv="expires" content="0">
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width, user-scalable=no" />
|
||||
<title>MoviePilot</title>
|
||||
<meta name="Robots" content="noindex,nofollow,noarchive" />
|
||||
<meta name="referrer" content="origin" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="manifest.json" crossorigin="use-credentials" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="MoviePilot" />
|
||||
<meta name="description" content="MoviePilot" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="referrer" content="never" />
|
||||
<meta name="msapplication-TileColor" content="#7D34FD" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#28243D" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)" />
|
||||
<meta name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<link rel="stylesheet" type="text/css" href="/loader.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="loading-bg">
|
||||
<div class="loading-logo">
|
||||
<!-- Logo -->
|
||||
<svg width="100px" height="100px" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2">
|
||||
<g transform="matrix(1,0,0,1,-2606,-236)">
|
||||
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">
|
||||
<rect x="0" y="0" width="192" height="192" style="fill: none" />
|
||||
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)">
|
||||
<path
|
||||
d="M2241.27,-28.175C2238.86,-28.931 2236.64,-29.181 2234.48,-29.254L2159.78,-29.286L2165.01,-11.207C2167.16,-13.121 2169.64,-13.722 2172.26,-13.808L2222.12,-13.822C2223.52,-13.824 2225,-13.701 2226.78,-13.108L2241.27,-28.175Z"
|
||||
style="fill: url(#_Linear1)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2205.67,331.428L2205.67,332.25L2205.67,352.835C2205.67,354.263 2204.91,355.583 2203.67,356.298C2202.43,357.012 2200.91,357.013 2199.67,356.3L2190.78,351.174C2189.73,350.595 2188.83,350.083 2188.03,349.59L2187.45,349.257C2186.66,348.725 2185.91,348.142 2185.21,347.461C2185.08,347.331 2184.95,347.198 2184.82,347.061C2184.26,346.457 2183.75,345.778 2183.3,344.995C2182.16,343.05 2181.69,341.024 2181.68,338.948L2181.67,268.923L2209.77,274.425C2207.5,275.639 2205.68,278.3 2205.67,281.429L2205.67,331.428Z"
|
||||
style="fill: url(#_Linear2)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2295.93,363.064C2295.73,363.184 2295.53,363.301 2295.32,363.414L2295.93,363.064Z"
|
||||
style="fill: rgb(141, 81, 249)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2299.79,360.238C2299.79,360.238 2320.03,348.464 2320.04,348.461C2323.1,346.372 2324.69,343.444 2325.17,339.877C2325.17,339.877 2325.17,269.846 2325.17,269.839C2325.06,267.482 2324.56,265.739 2323.61,264.133C2322.56,262.445 2321.26,261.005 2319.55,259.97L2304.42,251.217C2303.96,250.949 2303.39,250.948 2302.92,251.216C2302.46,251.484 2302.17,251.979 2302.17,252.515L2302.17,276.775L2302.17,277.879L2302.17,352.926C2302.17,352.933 2302.17,352.941 2302.17,352.948C2302.04,355.861 2301.23,358.279 2299.79,360.238Z"
|
||||
style="fill: url(#_Linear3)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256Z"
|
||||
style="fill: rgb(165, 118, 255)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256ZM2253.68,223.756C2251.6,223.789 2249.87,224.269 2248.47,224.996L2188.17,259.754C2184.35,261.992 2182.35,265.367 2182.18,269.874C2182.18,269.874 2182.17,292.759 2182.17,292.757C2183.25,290.047 2185.13,288.051 2187.62,286.607L2249.57,250.919C2249.58,250.917 2249.58,250.915 2249.59,250.913C2250.83,250.243 2252.17,249.839 2253.67,249.847C2255.21,249.841 2256.54,250.253 2257.76,250.914C2257.76,250.916 2257.76,250.917 2257.76,250.919L2274.92,260.807C2275.38,261.075 2275.95,261.074 2276.42,260.806C2276.88,260.538 2277.17,260.043 2277.17,259.508L2277.17,237.568C2277.17,236.317 2276.5,235.16 2275.42,234.535C2275.42,234.535 2258.88,225 2258.87,224.996C2256.87,224.049 2255.2,223.746 2253.68,223.756Z"
|
||||
style="fill: url(#_Linear4)" />
|
||||
</g>
|
||||
<g transform="matrix(0.800798,0.462341,0.769972,-1.33363,-1677.22,-896.858)">
|
||||
<path
|
||||
d="M2241.55,-28.184C2239.1,-28.989 2236.83,-29.204 2234.68,-29.295C2234.68,-29.295 2220.82,-29.3 2215.03,-29.303C2213.48,-29.303 2212.05,-28.808 2211.28,-28.004C2208.65,-25.275 2202.56,-18.936 2199.45,-15.709C2199.07,-15.306 2199.07,-14.809 2199.46,-14.406C2199.85,-14.004 2200.57,-13.758 2201.34,-13.761C2208.36,-13.788 2222.72,-13.845 2222.72,-13.845C2223.98,-13.851 2225.44,-13.657 2227.06,-13.117L2241.55,-28.184Z"
|
||||
style="fill: rgb(141, 81, 249)" />
|
||||
</g>
|
||||
<g transform="matrix(-4.32309,0,0,12.4454,9610.35,-1450.35)">
|
||||
<path
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
|
||||
style="fill: rgb(104, 0, 197)" />
|
||||
<clipPath id="_clip5">
|
||||
<path
|
||||
d="M2241.27,-28.175C2238.86,-28.931 2236.64,-29.181 2234.48,-29.254L2159.78,-29.286L2165.01,-11.207C2167.16,-13.121 2169.64,-13.722 2172.26,-13.808L2222.12,-13.822C2223.52,-13.824 2225,-13.701 2226.78,-13.108L2241.27,-28.175Z"
|
||||
style="fill: url(#_Linear1)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2205.67,331.428L2205.67,332.25L2205.67,352.835C2205.67,354.263 2204.91,355.583 2203.67,356.298C2202.43,357.012 2200.91,357.013 2199.67,356.3L2190.78,351.174C2189.73,350.595 2188.83,350.083 2188.03,349.59L2187.45,349.257C2186.66,348.725 2185.91,348.142 2185.21,347.461C2185.08,347.331 2184.95,347.198 2184.82,347.061C2184.26,346.457 2183.75,345.778 2183.3,344.995C2182.16,343.05 2181.69,341.024 2181.68,338.948L2181.67,268.923L2209.77,274.425C2207.5,275.639 2205.68,278.3 2205.67,281.429L2205.67,331.428Z"
|
||||
style="fill: url(#_Linear2)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2295.93,363.064C2295.73,363.184 2295.53,363.301 2295.32,363.414L2295.93,363.064Z"
|
||||
style="fill: rgb(141, 81, 249)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2299.79,360.238C2299.79,360.238 2320.03,348.464 2320.04,348.461C2323.1,346.372 2324.69,343.444 2325.17,339.877C2325.17,339.877 2325.17,269.846 2325.17,269.839C2325.06,267.482 2324.56,265.739 2323.61,264.133C2322.56,262.445 2321.26,261.005 2319.55,259.97L2304.42,251.217C2303.96,250.949 2303.39,250.948 2302.92,251.216C2302.46,251.484 2302.17,251.979 2302.17,252.515L2302.17,276.775L2302.17,277.879L2302.17,352.926C2302.17,352.933 2302.17,352.941 2302.17,352.948C2302.04,355.861 2301.23,358.279 2299.79,360.238Z"
|
||||
style="fill: url(#_Linear3)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256Z"
|
||||
style="fill: rgb(165, 118, 255)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256ZM2253.68,223.756C2251.6,223.789 2249.87,224.269 2248.47,224.996L2188.17,259.754C2184.35,261.992 2182.35,265.367 2182.18,269.874C2182.18,269.874 2182.17,292.759 2182.17,292.757C2183.25,290.047 2185.13,288.051 2187.62,286.607L2249.57,250.919C2249.58,250.917 2249.58,250.915 2249.59,250.913C2250.83,250.243 2252.17,249.839 2253.67,249.847C2255.21,249.841 2256.54,250.253 2257.76,250.914C2257.76,250.916 2257.76,250.917 2257.76,250.919L2274.92,260.807C2275.38,261.075 2275.95,261.074 2276.42,260.806C2276.88,260.538 2277.17,260.043 2277.17,259.508L2277.17,237.568C2277.17,236.317 2276.5,235.16 2275.42,234.535C2275.42,234.535 2258.88,225 2258.87,224.996C2256.87,224.049 2255.2,223.746 2253.68,223.756Z"
|
||||
style="fill: url(#_Linear4)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(0.800798,0.462341,0.769972,-1.33363,-1677.22,-896.858)">
|
||||
<path
|
||||
d="M2241.55,-28.184C2239.1,-28.989 2236.83,-29.204 2234.68,-29.295C2234.68,-29.295 2220.82,-29.3 2215.03,-29.303C2213.48,-29.303 2212.05,-28.808 2211.28,-28.004C2208.65,-25.275 2202.56,-18.936 2199.45,-15.709C2199.07,-15.306 2199.07,-14.809 2199.46,-14.406C2199.85,-14.004 2200.57,-13.758 2201.34,-13.761C2208.36,-13.788 2222.72,-13.845 2222.72,-13.845C2223.98,-13.851 2225.44,-13.657 2227.06,-13.117L2241.55,-28.184Z"
|
||||
style="fill: rgb(141, 81, 249)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-4.32309,0,0,12.4454,9610.35,-1450.35)">
|
||||
<path
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
|
||||
style="fill: rgb(104, 0, 197)"
|
||||
/>
|
||||
<clipPath id="_clip5">
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip5)">
|
||||
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
|
||||
<path
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
|
||||
/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip5)">
|
||||
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
|
||||
<path
|
||||
d="M1726.17,-64.249L1708.16,-72.303L1708.05,-23.514L1721.88,-32.386C1722.96,-33.241 1723.09,-33.944 1723.15,-34.636L1723.15,-54.373C1723.19,-56.238 1724.96,-57.594 1726.87,-56.686L1726.17,-64.249Z"
|
||||
style="fill: url(#_Linear6)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1704.47,-40.254C1706.28,-40.527 1708.14,-40.212 1708.16,-39.416L1708.16,-18.976L1726.17,-18.976L1726.17,-45.661Z"
|
||||
style="fill: rgb(141, 81, 249)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1726.17,-18.976L1708.16,-18.976L1708.16,-39.416C1707.79,-40.732 1704.5,-40.298 1702.68,-40.025L1726.17,-45.661ZM1705.49,-40.491C1706.2,-40.507 1706.87,-40.464 1707.4,-40.327C1708.01,-40.173 1708.48,-39.899 1708.62,-39.436C1708.62,-39.429 1708.62,-39.423 1708.62,-39.416L1708.62,-19.152C1708.62,-19.152 1725.72,-19.152 1725.72,-19.152L1725.72,-45.345L1705.49,-40.491Z"
|
||||
style="fill: url(#_Radial7)"
|
||||
/>
|
||||
</g>
|
||||
d="M1726.17,-64.249L1708.16,-72.303L1708.05,-23.514L1721.88,-32.386C1722.96,-33.241 1723.09,-33.944 1723.15,-34.636L1723.15,-54.373C1723.19,-56.238 1724.96,-57.594 1726.87,-56.686L1726.17,-64.249Z"
|
||||
style="fill: url(#_Linear6)" />
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1704.47,-40.254C1706.28,-40.527 1708.14,-40.212 1708.16,-39.416L1708.16,-18.976L1726.17,-18.976L1726.17,-45.661Z"
|
||||
style="fill: rgb(141, 81, 249)" />
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1726.17,-18.976L1708.16,-18.976L1708.16,-39.416C1707.79,-40.732 1704.5,-40.298 1702.68,-40.025L1726.17,-45.661ZM1705.49,-40.491C1706.2,-40.507 1706.87,-40.464 1707.4,-40.327C1708.01,-40.173 1708.48,-39.899 1708.62,-39.436C1708.62,-39.429 1708.62,-39.423 1708.62,-39.416L1708.62,-19.152C1708.62,-19.152 1725.72,-19.152 1725.72,-19.152L1725.72,-45.345L1705.49,-40.491Z"
|
||||
style="fill: url(#_Radial7)" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="_Linear1"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear2"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear3"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear4"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear6"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
<stop offset="0.51" style="stop-color: rgb(110, 38, 217); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(91, 0, 197); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="_Radial7"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="loading">
|
||||
<div class="effect-1 effects"></div>
|
||||
<div class="effect-2 effects"></div>
|
||||
<div class="effect-3 effects"></div>
|
||||
</div>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)">
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)">
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)">
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)">
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)">
|
||||
<stop offset="0" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
<stop offset="0.51" style="stop-color: rgb(110, 38, 217); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(91, 0, 197); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<radialGradient id="_Radial7" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)">
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<script>
|
||||
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
|
||||
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
|
||||
<div class="loading">
|
||||
<div class="effect-1 effects"></div>
|
||||
<div class="effect-2 effects"></div>
|
||||
<div class="effect-3 effects"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<script>
|
||||
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
|
||||
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
|
||||
|
||||
if (loaderColor) document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
|
||||
if (loaderColor)
|
||||
document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
|
||||
|
||||
if (primaryColor) document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
if (primaryColor)
|
||||
document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
66
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "1.4.8",
|
||||
"version": "1.8.2-2",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
@@ -21,43 +21,47 @@
|
||||
"dependencies": {
|
||||
"@casl/ability": "^6.2.0",
|
||||
"@casl/vue": "^2.2.0",
|
||||
"@floating-ui/dom": "1.2.8",
|
||||
"@floating-ui/dom": "1.6.3",
|
||||
"@iconify/utils": "^2.1.22",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vueuse/math": "^10.1.2",
|
||||
"ace-builds": "^1.32.6",
|
||||
"apexcharts-clevision": "^3.28.5",
|
||||
"axios": "1.4.0",
|
||||
"axios": "1.6.8",
|
||||
"axios-mock-adapter": "^1.21.4",
|
||||
"chart.js": "^4.1.2",
|
||||
"colorthief": "^2.4.0",
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"postcss-purgecss": "^5.0.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"pull-refresh-vue3": "^0.3.1",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"sass": "^1.59.3",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"unplugin-vue-define-options": "^1.3.5",
|
||||
"vite-plugin-pwa": "^0.16.4",
|
||||
"vite-plugin-pwa": "^0.19.8",
|
||||
"vue": "^3.3.2",
|
||||
"vue-chartjs": "^5.2.0",
|
||||
"vue-flatpickr-component": "11.0.3",
|
||||
"vue-flatpickr-component": "11.0.5",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-prism-component": "^2.0.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"vue-toast-notification": "^3",
|
||||
"vue3-ace-editor": "^2.2.4",
|
||||
"vue3-apexcharts": "^1.4.1",
|
||||
"vue3-perfect-scrollbar": "^1.6.0",
|
||||
"vuetify": "3.3.5",
|
||||
"vue3-perfect-scrollbar": "^2.0.0",
|
||||
"vuetify": "3.5.14",
|
||||
"vuetify-use-dialog": "^0.6.0",
|
||||
"vuex": "^4.1.0",
|
||||
"vuex-persistedstate": "^4.1.0",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config-vue": "^0.38.6",
|
||||
"@antfu/eslint-config-vue": "^0.43.1",
|
||||
"@fullcalendar/core": "^6.1.8",
|
||||
"@fullcalendar/daygrid": "^6.1.8",
|
||||
"@fullcalendar/interaction": "^6.1.7",
|
||||
@@ -65,43 +69,45 @@
|
||||
"@fullcalendar/timegrid": "^6.1.7",
|
||||
"@fullcalendar/vue3": "^6.1.8",
|
||||
"@iconify-json/mdi": "^1.1.52",
|
||||
"@iconify/tools": "^2.2.0",
|
||||
"@iconify/tools": "^4.0.4",
|
||||
"@iconify/vue": "4.1.1",
|
||||
"@intlify/unplugin-vue-i18n": "^0.10.0",
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/node": "^20.1.4",
|
||||
"@types/webfontloader": "^1.6.34",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.5",
|
||||
"@typescript-eslint/parser": "^5.59.5",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||
"@typescript-eslint/parser": "^7.5.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.40.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.0.1",
|
||||
"eslint-plugin-regex": "^1.10.0",
|
||||
"eslint-plugin-sonarjs": "^0.19.0",
|
||||
"eslint-plugin-unicorn": "^47.0.0",
|
||||
"eslint-plugin-sonarjs": "^0.25.1",
|
||||
"eslint-plugin-unicorn": "^52.0.0",
|
||||
"eslint-plugin-vue": "^9.12.0",
|
||||
"postcss": "^8.4.24",
|
||||
"lodash": "^4.17.21",
|
||||
"postcss": "8",
|
||||
"postcss-html": "^1.5.0",
|
||||
"stylelint": "14.15.0",
|
||||
"stylelint-config-idiomatic-order": "9.0.0",
|
||||
"stylelint-config-standard-scss": "6.1.0",
|
||||
"stylelint-use-logical-spec": "4.1.0",
|
||||
"type-fest": "^3.10.0",
|
||||
"stylelint": "16.3.1",
|
||||
"stylelint-config-idiomatic-order": "10.0.0",
|
||||
"stylelint-config-standard-scss": "13.1.0",
|
||||
"stylelint-use-logical-spec": "5.0.1",
|
||||
"type-fest": "^4.15.0",
|
||||
"typescript": "^5.0.4",
|
||||
"unplugin-auto-import": "^0.15.1",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
"vite": "^4.3.5",
|
||||
"vite-plugin-pages": "^0.29.0",
|
||||
"vite-plugin-vue-layouts": "^0.8.0",
|
||||
"vite-plugin-vuetify": "1.0.2",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-pages": "^0.32.1",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vite-plugin-vuetify": "2.0.3",
|
||||
"vue-shepherd": "^3.0.0",
|
||||
"vue-tsc": "^1.6.5"
|
||||
"vue-tsc": "^2.0.10"
|
||||
},
|
||||
"packageManager": "yarn@1.22.18",
|
||||
"resolutions": {
|
||||
|
||||
@@ -75,6 +75,26 @@ http {
|
||||
# 超时设置
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location /cookiecloud {
|
||||
# 后端cookiecloud地址
|
||||
proxy_pass http://backend_api;
|
||||
rewrite ^.+mock-server/?(.*)$ /$1 break;
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Nginx-Proxy true;
|
||||
|
||||
# 超时设置
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
upstream backend_api {
|
||||
|
||||
@@ -25,10 +25,25 @@ app.use(
|
||||
})
|
||||
);
|
||||
|
||||
// 配置代理中间件将CookieCloud请求转发给后端API
|
||||
app.use(
|
||||
'/cookiecloud',
|
||||
proxy(`${proxyConfig.URL}:${proxyConfig.PORT}`, {
|
||||
// 路径加上 /cookiecloud 前缀
|
||||
proxyReqPathResolver: (req) => {
|
||||
return `/cookiecloud${req.url}`
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 处理根路径的请求
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'index.html')) // 指向你的前端入口文件
|
||||
res.sendFile(path.join(__dirname, 'index.html'))
|
||||
})
|
||||
|
||||
// 处理所有其他请求,重定向到前端入口文件
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'index.html'))
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
const emit = defineEmits(['click', 'update:modelValue'])
|
||||
// 按钮点击
|
||||
function onClick() {
|
||||
emit('update:modelValue', false)
|
||||
emit('click')
|
||||
}
|
||||
</script>
|
||||
|
||||
28
src/@core/components/LoadingBanner.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
// 定义输入参数
|
||||
const props = defineProps({
|
||||
progress: Number,
|
||||
text: String
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full text-center text-gray-500 text-sm flex flex-col items-center"
|
||||
>
|
||||
<VProgressCircular
|
||||
v-if="!props.text"
|
||||
size="48"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<VProgressCircular
|
||||
v-if="props.progress"
|
||||
class="mb-3"
|
||||
color="primary"
|
||||
:model-value="props.progress"
|
||||
size="64"
|
||||
/>
|
||||
<span>{{ props.text }}</span>
|
||||
</div>
|
||||
</template>
|
||||
18
src/@core/components/StatIcon.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
color?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute top-2 right-2 flex items-center justify-between p-2 shadow">
|
||||
<VBadge :color="props.color" bordered>
|
||||
<template #badge>
|
||||
<VIcon icon="mdi-pulse"></VIcon>
|
||||
</template>
|
||||
</VBadge>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import type { ThemeSwitcherTheme } from '@layouts/types'
|
||||
|
||||
@@ -20,26 +20,30 @@ const {
|
||||
{ initialValue: savedTheme.value },
|
||||
)
|
||||
|
||||
function changeTheme() {
|
||||
const nextTheme = getNextThemeName()
|
||||
|
||||
globalTheme.name.value = nextTheme
|
||||
savedTheme.value = nextTheme
|
||||
localStorage.setItem('theme', nextTheme)
|
||||
function updateTheme() {
|
||||
const autoTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
const theme = currentThemeName.value === 'auto' ? autoTheme : currentThemeName.value
|
||||
globalTheme.name.value = theme
|
||||
savedTheme.value = theme
|
||||
// 修改载入时背景色
|
||||
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
|
||||
|
||||
themeTransition()
|
||||
}
|
||||
|
||||
// Update icon if theme is changed from other sources
|
||||
// 监听系统主题变化
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
|
||||
|
||||
watch(
|
||||
() => globalTheme.name.value,
|
||||
(val) => {
|
||||
currentThemeName.value = val
|
||||
},
|
||||
() => currentThemeName.value,
|
||||
() => updateTheme(),
|
||||
)
|
||||
|
||||
function changeTheme() {
|
||||
const nextTheme = getNextThemeName()
|
||||
currentThemeName.value = nextTheme
|
||||
localStorage.setItem('theme', nextTheme)
|
||||
}
|
||||
|
||||
// Apply saved theme on page load
|
||||
// onMounted(() => {
|
||||
// globalTheme.name.value = savedTheme.value
|
||||
|
||||
18
src/@core/utils/compatibility.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 浏览器兼容性处理
|
||||
*/
|
||||
|
||||
/**
|
||||
* 修复低版本Safari等浏览器数组不支持at函数的问题
|
||||
*/
|
||||
export function fixArrayAt() {
|
||||
if (!Array.prototype.at) {
|
||||
Array.prototype.at = function(index: number) {
|
||||
if (index >= 0) {
|
||||
return this[index]
|
||||
} else {
|
||||
return this[this.length + index]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/@core/utils/dom.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export function removeEl(selector: string) {
|
||||
if (selector) {
|
||||
const el = document.querySelector(selector)
|
||||
el?.parentNode?.removeChild(el)
|
||||
}
|
||||
}
|
||||
|
||||
export function useDefer(maxFrameCount = 1) {
|
||||
const frameCount = ref(0)
|
||||
const refreshFrameCount = () => {
|
||||
requestAnimationFrame(() => {
|
||||
frameCount.value++
|
||||
if (frameCount.value < maxFrameCount)
|
||||
refreshFrameCount()
|
||||
})
|
||||
}
|
||||
refreshFrameCount()
|
||||
return function (showInFrameCount: number) {
|
||||
return frameCount.value >= showInFrameCount
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import ZH_CN from 'dayjs/locale/zh-cn'
|
||||
|
||||
import { isToday } from './index'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.locale(ZH_CN)
|
||||
|
||||
export function avatarText(value: string) {
|
||||
if (!value)
|
||||
return ''
|
||||
@@ -19,7 +26,7 @@ export function kFormatter(num: number) {
|
||||
* Format and return date in Humanize format
|
||||
* Intl docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format
|
||||
* Intl Constructor: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
|
||||
* @param {String} value date to format
|
||||
* @param {string} value date to format
|
||||
* @param {Intl.DateTimeFormatOptions} formatting Intl object to format with
|
||||
*/
|
||||
export function formatDate(value: string, formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' }) {
|
||||
@@ -32,8 +39,8 @@ export function formatDate(value: string, formatting: Intl.DateTimeFormatOptions
|
||||
/**
|
||||
* Return short human friendly month representation of date
|
||||
* Can also convert date to only time if date is of today (Better UX)
|
||||
* @param {String} value date to format
|
||||
* @param {Boolean} toTimeForCurrentDay Shall convert to time if day is today/current
|
||||
* @param {string} value date to format
|
||||
* @param {boolean} toTimeForCurrentDay Shall convert to time if day is today/current
|
||||
*/
|
||||
export function formatDateToMonthShort(value: string, toTimeForCurrentDay = true) {
|
||||
const date = new Date(value)
|
||||
@@ -107,5 +114,63 @@ export function formatBytes(bytes: number, decimals = 2) {
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
|
||||
return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
// 格式化剧集列表
|
||||
export function formatEp(nums: number[]): string {
|
||||
if (!nums.length)
|
||||
return ''
|
||||
|
||||
if (nums.length === 1)
|
||||
return nums[0].toString()
|
||||
|
||||
// 将数组升序排序
|
||||
nums.sort((a, b) => a - b)
|
||||
const formattedRanges: string[] = []
|
||||
let start = nums[0]
|
||||
let end = nums[0]
|
||||
|
||||
for (let i = 1; i < nums.length; i++) {
|
||||
if (nums[i] === end + 1) {
|
||||
end = nums[i]
|
||||
}
|
||||
else {
|
||||
if (start === end)
|
||||
formattedRanges.push(start.toString())
|
||||
|
||||
else
|
||||
formattedRanges.push(`${start.toString()}-${end.toString()}`)
|
||||
|
||||
start = end = nums[i]
|
||||
}
|
||||
}
|
||||
|
||||
if (start === end)
|
||||
formattedRanges.push(start.toString())
|
||||
|
||||
else
|
||||
formattedRanges.push(`${start.toString()}-${end.toString()}`)
|
||||
|
||||
return formattedRanges.join('、')
|
||||
}
|
||||
|
||||
// 将yyyy-mm-dd hh:mm:ss转换为时间差,如:1小时前,1天前
|
||||
export function formatDateDifference(dateString: string): string {
|
||||
// const timeDifference = dayjs().millisecond() - dayjs(dateString).millisecond()
|
||||
// const secondsDifference = Math.floor(timeDifference / 1000)
|
||||
// const minutesDifference = Math.floor(secondsDifference / 60)
|
||||
// const hoursDifference = Math.floor(minutesDifference / 60)
|
||||
// const daysDifference = Math.floor(hoursDifference / 24)
|
||||
|
||||
// if (daysDifference > 0)
|
||||
// return `${daysDifference}天前`
|
||||
// else if (hoursDifference > 0)
|
||||
// return `${hoursDifference}小时前`
|
||||
// else if (minutesDifference > 0)
|
||||
// return `${minutesDifference}分钟前`
|
||||
// else
|
||||
// return '刚刚'
|
||||
|
||||
return dayjs(dateString).fromNow()
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function isToday(date: Date) {
|
||||
)
|
||||
}
|
||||
|
||||
// 计算时间差,返回xx天xx小时xx分钟
|
||||
// 计算时间差,返回xx天/xx小时/xx分钟/xx秒
|
||||
export function calculateTimeDifference(inputTime: string): string {
|
||||
if (!inputTime)
|
||||
return ''
|
||||
@@ -64,6 +64,38 @@ export function calculateTimeDifference(inputTime: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算时间差,返回xx天xx小时xx分钟
|
||||
export function calculateTimeDiff(inputTime: string): string {
|
||||
if (!inputTime)
|
||||
return ''
|
||||
|
||||
// 使用当前时区
|
||||
const inputDate = new Date(inputTime)
|
||||
const currentDate = new Date()
|
||||
|
||||
const timeDifference = currentDate.getTime() - inputDate.getTime()
|
||||
const secondsDifference = Math.floor(timeDifference / 1000)
|
||||
|
||||
const days = Math.floor(secondsDifference / 86400)
|
||||
const hours = Math.floor(secondsDifference % 86400 / 3600)
|
||||
const minutes = Math.floor(secondsDifference % 86400 % 3600 / 60)
|
||||
const secones = Math.floor(secondsDifference % 60)
|
||||
|
||||
if (days > 0)
|
||||
return `${days}天${hours}小时${minutes}分钟`
|
||||
|
||||
else if (hours > 0)
|
||||
return `${hours}小时${minutes}分钟`
|
||||
|
||||
else if (minutes > 0)
|
||||
return `${minutes}分钟`
|
||||
|
||||
else if (secones > 0)
|
||||
return `${secones}秒`
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// 判断一个数组subArray是不是在另一个数组mainArray中
|
||||
export function isContained(subArray: any[], mainArray: any[]): boolean {
|
||||
return subArray.every(element => mainArray.includes(element))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Component } from 'vue'
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import logo from '@images/logo.svg?raw'
|
||||
|
||||
@@ -85,6 +84,7 @@ function handleNavScroll(evt: Event) {
|
||||
.visible {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
// 👉 Vertical Nav
|
||||
.layout-vertical-nav {
|
||||
position: fixed;
|
||||
@@ -96,8 +96,8 @@ function handleNavScroll(evt: Event) {
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
transition: transform 0.25s ease-in-out, inline-size 0.25s ease-in-out, box-shadow 0.25s ease-in-out;
|
||||
will-change: transform, inline-size;
|
||||
visibility: hidden;
|
||||
will-change: transform, inline-size;
|
||||
|
||||
&:not(.overlay-nav) {
|
||||
visibility: visible;
|
||||
|
||||
@@ -52,7 +52,7 @@ export default defineComponent({
|
||||
'main',
|
||||
{ class: 'layout-page-content' },
|
||||
h(Transition, { name: 'fade-slide', mode: 'out-in', appear: true },
|
||||
h('section', { class: 'page-content-container' }, slots.default?.()),
|
||||
() => h('section', { class: 'page-content-container' }, slots.default?.()),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ $layout-horizontal-nav-layout-navbar-z-index: 11 !default;
|
||||
$layout-boxed-content-width: 90rem !default;
|
||||
|
||||
// 👉Footer
|
||||
$layout-vertical-nav-footer-height: 3.5rem !default;
|
||||
$layout-vertical-nav-footer-height: 0rem !default;
|
||||
|
||||
// 👉 Layout overlay
|
||||
$layout-overlay-z-index: 11 !default;
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
@use "_global";
|
||||
@use "vue3-perfect-scrollbar/dist/vue3-perfect-scrollbar.min.css";
|
||||
@use "_classes";
|
||||
|
||||
13
src/App.vue
@@ -3,13 +3,19 @@ import { useToast } from 'vue-toast-notification'
|
||||
import { useTheme } from 'vuetify'
|
||||
import store from './store'
|
||||
|
||||
// 第一时间应用主题
|
||||
const { global: globalTheme } = useTheme()
|
||||
globalTheme.name.value = localStorage.getItem('theme') || 'light'
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 设置主题
|
||||
function setTheme() {
|
||||
const { global: globalTheme } = useTheme()
|
||||
let theme = localStorage.getItem('theme') || 'light'
|
||||
if (theme === 'auto')
|
||||
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
globalTheme.name.value = theme
|
||||
}
|
||||
|
||||
// SSE持续接收消息
|
||||
function startSSEMessager() {
|
||||
const token = store.state.auth.token
|
||||
@@ -32,6 +38,7 @@ function startSSEMessager() {
|
||||
|
||||
// 页面加载时,加载当前用户数据
|
||||
onBeforeMount(async () => {
|
||||
setTheme()
|
||||
startSSEMessager()
|
||||
})
|
||||
</script>
|
||||
|
||||
54
src/ace-config.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import ace from 'ace-builds'
|
||||
|
||||
import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url'
|
||||
|
||||
import modeJavascriptUrl from 'ace-builds/src-noconflict/mode-javascript?url'
|
||||
|
||||
import modeHtmlUrl from 'ace-builds/src-noconflict/mode-html?url'
|
||||
|
||||
import modeYamlUrl from 'ace-builds/src-noconflict/mode-yaml?url'
|
||||
|
||||
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url'
|
||||
|
||||
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url'
|
||||
|
||||
import themeMonokaiUrl from 'ace-builds/src-noconflict/theme-monokai?url'
|
||||
|
||||
import workerBaseUrl from 'ace-builds/src-noconflict/worker-base?url'
|
||||
|
||||
import workerJsonUrl from 'ace-builds/src-noconflict/worker-json?url'
|
||||
|
||||
import workerJavascriptUrl from 'ace-builds/src-noconflict/worker-javascript?url'
|
||||
|
||||
import workerHtmlUrl from 'ace-builds/src-noconflict/worker-html?url'
|
||||
|
||||
import workerYamlUrl from 'ace-builds/src-noconflict/worker-yaml?url'
|
||||
|
||||
import snippetsHtmlUrl from 'ace-builds/src-noconflict/snippets/html?url'
|
||||
|
||||
import snippetsJsUrl from 'ace-builds/src-noconflict/snippets/javascript?url'
|
||||
|
||||
import snippetsYamlUrl from 'ace-builds/src-noconflict/snippets/yaml?url'
|
||||
|
||||
import snippetsJsonUrl from 'ace-builds/src-noconflict/snippets/json?url'
|
||||
|
||||
import 'ace-builds/src-noconflict/ext-language_tools'
|
||||
|
||||
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl)
|
||||
ace.config.setModuleUrl('ace/mode/javascript', modeJavascriptUrl)
|
||||
ace.config.setModuleUrl('ace/mode/html', modeHtmlUrl)
|
||||
ace.config.setModuleUrl('ace/mode/yaml', modeYamlUrl)
|
||||
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl)
|
||||
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
|
||||
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
|
||||
ace.config.setModuleUrl('ace/mode/base', workerBaseUrl)
|
||||
ace.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl)
|
||||
ace.config.setModuleUrl('ace/mode/javascript_worker', workerJavascriptUrl)
|
||||
ace.config.setModuleUrl('ace/mode/html_worker', workerHtmlUrl)
|
||||
ace.config.setModuleUrl('ace/mode/yaml_worker', workerYamlUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/html', snippetsHtmlUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/javascript', snippetsJsUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/javascript', snippetsYamlUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl)
|
||||
|
||||
ace.require('ace/ext/language_tools')
|
||||
215
src/api/types.ts
@@ -80,8 +80,17 @@ export interface Subscribe {
|
||||
// 是否洗版,数字或者boolean
|
||||
best_version: any
|
||||
|
||||
// 使用 imdbid 搜索
|
||||
search_imdbid?: any
|
||||
|
||||
// 当前优先级
|
||||
current_priority: number
|
||||
|
||||
// 保存目录
|
||||
save_path: string
|
||||
|
||||
// 时间
|
||||
date: string
|
||||
}
|
||||
|
||||
// 历史记录
|
||||
@@ -175,6 +184,9 @@ export interface MediaInfo {
|
||||
// 豆瓣ID
|
||||
douban_id?: string
|
||||
|
||||
// Bangumi ID
|
||||
bangumi_id?: string
|
||||
|
||||
// 媒体原语种
|
||||
original_language?: string
|
||||
|
||||
@@ -276,6 +288,9 @@ export interface MediaInfo {
|
||||
|
||||
// 下一集
|
||||
next_episode_to_air?: object
|
||||
|
||||
// 别名
|
||||
names?: string[]
|
||||
}
|
||||
|
||||
// TMDB季信息
|
||||
@@ -416,6 +431,28 @@ export interface DoubanPerson {
|
||||
|
||||
}
|
||||
|
||||
// Bangumi人物信息
|
||||
export interface BangumiPerson {
|
||||
// ID
|
||||
id?: number
|
||||
|
||||
// 名称
|
||||
name?: string
|
||||
|
||||
// 类型
|
||||
type?: number
|
||||
|
||||
// 角色
|
||||
career?: string[]
|
||||
|
||||
// images large/normal
|
||||
images?: { [key: string]: string }
|
||||
|
||||
// 关系
|
||||
relation?: string
|
||||
|
||||
}
|
||||
|
||||
// 站点
|
||||
export interface Site {
|
||||
|
||||
@@ -471,6 +508,33 @@ export interface Site {
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
// 站点使用统计
|
||||
export interface SiteStatistic {
|
||||
|
||||
// 站点主域名Key
|
||||
domain?: string
|
||||
|
||||
// 成功次数
|
||||
success?: number
|
||||
|
||||
// 失败次数
|
||||
fail?: number
|
||||
|
||||
// 平均耗时
|
||||
seconds?: number
|
||||
|
||||
// 最后一次访问状态 0-成功 1-失败
|
||||
lst_state?: number
|
||||
|
||||
// 最后访问时间
|
||||
lst_mod_date?: string
|
||||
|
||||
// 耗时记录 JSON
|
||||
note?: string
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 正在下载
|
||||
export interface DownloadingInfo {
|
||||
|
||||
@@ -507,8 +571,11 @@ export interface DownloadingInfo {
|
||||
// 媒体信息
|
||||
media: { [key: string]: any }
|
||||
|
||||
// 下载用户
|
||||
// 下载用户ID
|
||||
userid?: string
|
||||
|
||||
// 下载用户名称
|
||||
username?: string
|
||||
}
|
||||
|
||||
// 缺失剧集信息
|
||||
@@ -575,6 +642,9 @@ export interface Plugin {
|
||||
|
||||
// 插件仓库地址
|
||||
repo_url?: string
|
||||
|
||||
// 变更历史
|
||||
history?: { [key: string]: string }
|
||||
}
|
||||
|
||||
// 种子信息
|
||||
@@ -648,6 +718,16 @@ export interface TorrentInfo {
|
||||
|
||||
// 促销描述
|
||||
volume_factor: string
|
||||
|
||||
// 免费时间
|
||||
freedate: string
|
||||
|
||||
// 剩余免费时间
|
||||
freedate_diff: string
|
||||
|
||||
// 种子类型
|
||||
category: string
|
||||
|
||||
}
|
||||
|
||||
// 识别元数据
|
||||
@@ -783,18 +863,37 @@ export interface Context {
|
||||
|
||||
// 用户信息
|
||||
export interface User {
|
||||
// 用户ID
|
||||
id: number
|
||||
|
||||
// 用户名称
|
||||
name: string
|
||||
|
||||
// 用户密码
|
||||
password: string
|
||||
|
||||
// 用户邮箱
|
||||
email: string
|
||||
|
||||
// 是否激活
|
||||
is_active: boolean
|
||||
|
||||
// 是否管理员
|
||||
is_superuser: boolean
|
||||
|
||||
// 头像
|
||||
avatar: string
|
||||
|
||||
// 是否开启双重验证
|
||||
is_otp: boolean
|
||||
}
|
||||
|
||||
// 存储空间
|
||||
export interface Storage {
|
||||
// 总空间
|
||||
total_storage: number
|
||||
|
||||
// 已使用空间
|
||||
used_storage: number
|
||||
}
|
||||
|
||||
@@ -867,6 +966,9 @@ export interface ScheduleInfo {
|
||||
// 名称
|
||||
name: string
|
||||
|
||||
// 提供者
|
||||
provider: string
|
||||
|
||||
// 状态
|
||||
status: string
|
||||
|
||||
@@ -885,6 +987,7 @@ export interface NotificationSwitch {
|
||||
telegram: boolean
|
||||
slack: boolean
|
||||
synologychat: boolean
|
||||
vocechat: boolean
|
||||
}
|
||||
|
||||
// 环境设置
|
||||
@@ -895,22 +998,132 @@ export interface Setting {
|
||||
|
||||
// 文件浏览接口
|
||||
export interface EndPoints {
|
||||
// 文件列表
|
||||
list: any
|
||||
|
||||
// 创建目录
|
||||
mkdir: any
|
||||
|
||||
// 删除文件
|
||||
delete: any
|
||||
|
||||
// 下载文件
|
||||
download: any
|
||||
|
||||
// 图片预览
|
||||
image: any
|
||||
|
||||
// 重命名
|
||||
rename: any
|
||||
}
|
||||
|
||||
// 文件浏览项目
|
||||
export interface FileItem {
|
||||
// 类型
|
||||
type: string
|
||||
|
||||
// 文件名
|
||||
name: string
|
||||
|
||||
// 文件名不含扩展名
|
||||
basename: string
|
||||
|
||||
// 文件路径
|
||||
path: string
|
||||
|
||||
// 文件扩展名
|
||||
extension: string
|
||||
|
||||
// 文件大小
|
||||
size: number
|
||||
|
||||
// 文件子元素
|
||||
children: FileItem[]
|
||||
|
||||
// 文件创建时间
|
||||
modify_time: number
|
||||
}
|
||||
|
||||
// 媒体服务器播放条目
|
||||
export interface MediaServerPlayItem {
|
||||
// ID
|
||||
id?: string | number
|
||||
|
||||
// 标题
|
||||
title: string
|
||||
|
||||
// 副标题
|
||||
subtitle?: string
|
||||
|
||||
// 类型
|
||||
type?: string
|
||||
|
||||
// 海报
|
||||
image?: string
|
||||
|
||||
// 链接
|
||||
link?: string
|
||||
|
||||
// 播放百分比
|
||||
percent?: number
|
||||
}
|
||||
|
||||
// 媒体服务器媒体库
|
||||
export interface MediaServerLibrary {
|
||||
// 服务器名称
|
||||
server: string
|
||||
|
||||
// ID
|
||||
id?: string | number
|
||||
|
||||
// 名称
|
||||
name: string
|
||||
|
||||
// 路径
|
||||
path?: string
|
||||
|
||||
// 类型
|
||||
type?: string
|
||||
|
||||
// 图片
|
||||
image?: string
|
||||
|
||||
// 图片列表
|
||||
image_list?: string[]
|
||||
|
||||
// 链接
|
||||
link?: string
|
||||
}
|
||||
|
||||
// 消息通知
|
||||
export interface Message {
|
||||
// 消息类型
|
||||
mtype?: string
|
||||
|
||||
// 消息标题
|
||||
title?: string
|
||||
|
||||
// 消息内容
|
||||
text?: string
|
||||
|
||||
// 消息链接
|
||||
link?: string
|
||||
|
||||
// 消息图片
|
||||
image?: string
|
||||
|
||||
// 消息时间
|
||||
date?: string
|
||||
|
||||
// 登记时间
|
||||
reg_time?: string
|
||||
|
||||
// 用户ID
|
||||
userid?: string
|
||||
|
||||
// 消息方向:0-接收,1-发送
|
||||
action?: number
|
||||
|
||||
// JSON
|
||||
note?: string
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 27 KiB |
BIN
src/assets/images/logos/fanart.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/images/logos/thetvdb.jpeg
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
src/assets/images/misc/emby.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/images/misc/jellyfin.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/images/misc/plex.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
src/assets/images/misc/teamwork.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 253 KiB |
@@ -1,10 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Axios } from 'axios'
|
||||
import axios from 'axios'
|
||||
|
||||
import Toolbar from './filebrowser/Toolbar.vue'
|
||||
import Tree from './filebrowser/Tree.vue'
|
||||
import List from './filebrowser/List.vue'
|
||||
import FileList from './filebrowser/FileList.vue'
|
||||
import FileToolbar from './filebrowser/FileToolbar.vue'
|
||||
import type { EndPoints } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
@@ -92,56 +90,40 @@ function sortChanged(s: string) {
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onBeforeMount(() => {
|
||||
onMounted(() => {
|
||||
activeStorage.value = props.storage ?? 'local'
|
||||
axiosInstance.value = props.axios ?? axios.create(props.axiosconfig)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="mx-auto" :loading="loading > 0">
|
||||
<Toolbar
|
||||
:path="props.path"
|
||||
:storages="storagesArray"
|
||||
:storage="activeStorage"
|
||||
:endpoints="props.endpoints"
|
||||
:axios="axiosInstance"
|
||||
@storagechanged="storageChanged"
|
||||
@pathchanged="pathChanged"
|
||||
@foldercreated="refreshPending = true"
|
||||
@sortchanged="sortChanged"
|
||||
/>
|
||||
<VRow no-gutters>
|
||||
<VCol v-if="tree" sm="auto" class="d-none d-md-block">
|
||||
<Tree
|
||||
:path="props.path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
/>
|
||||
</VCol>
|
||||
<VDivider v-if="tree" vertical />
|
||||
<VCol>
|
||||
<List
|
||||
:path="props.path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
:sort="sort"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
@filedeleted="refreshPending = true"
|
||||
@renamed="refreshPending = true"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VCard class="mx-auto" :loading="loading > 0 || !path">
|
||||
<div v-if="path">
|
||||
<FileToolbar
|
||||
:path="path"
|
||||
:storages="storagesArray"
|
||||
:storage="activeStorage"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
@storagechanged="storageChanged"
|
||||
@pathchanged="pathChanged"
|
||||
@foldercreated="refreshPending = true"
|
||||
@sortchanged="sortChanged"
|
||||
/>
|
||||
<FileList
|
||||
:path="path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
:sort="sort"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
@filedeleted="refreshPending = true"
|
||||
@renamed="refreshPending = true"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import miscpose from '@images/pages/pose-fs-9.png'
|
||||
import image from '@images/misc/teamwork.png'
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
@@ -11,25 +11,24 @@ interface Props {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<ErrorHeader
|
||||
:error-code="props.errorCode"
|
||||
:error-title="props.errorTitle"
|
||||
:error-description="props.errorDescription"
|
||||
/>
|
||||
<VEmptyState
|
||||
:image="image"
|
||||
size="250"
|
||||
>
|
||||
<template #title>
|
||||
<div class="mt-8 text-2xl">
|
||||
{{ props.errorTitle }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 👉 Image -->
|
||||
<div class="text-center">
|
||||
<VImg
|
||||
:src="miscpose"
|
||||
class="mx-auto pt-10"
|
||||
max-width="250"
|
||||
cover
|
||||
/>
|
||||
<template #text>
|
||||
<div class="text-subtitle">
|
||||
{{ props.errorDescription }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<slot name="button" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VEmptyState>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
|
||||
89
src/components/cards/BackdropCard.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<MediaServerPlayItem>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
// 图片加载完成响应
|
||||
function imageLoadHandler() {
|
||||
imageLoaded.value = true
|
||||
}
|
||||
|
||||
// 跳转播放
|
||||
function goPlay() {
|
||||
if (props.media?.link)
|
||||
window.open(props.media?.link, '_blank')
|
||||
}
|
||||
|
||||
// 计算图片地址
|
||||
const getImgUrl = computed(() => {
|
||||
const image = props.media?.image || ''
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover
|
||||
v-bind="props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
class="ring-gray-500"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'ring-1': imageLoaded,
|
||||
}"
|
||||
@click="goPlay"
|
||||
>
|
||||
<template #image>
|
||||
<VImg
|
||||
:src="getImgUrl"
|
||||
aspect-ratio="2/3"
|
||||
cover
|
||||
@load="imageLoadHandler"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<VCardText
|
||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
>
|
||||
<h1 class="mb-1 text-white text-shadow font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.title }}
|
||||
</h1>
|
||||
<span class="text-shadow">{{ props.media?.subtitle }}</span>
|
||||
</VCardText>
|
||||
</VImg>
|
||||
</template>
|
||||
<div class="w-full absolute bottom-0">
|
||||
<VProgressLinear
|
||||
v-if="props.media?.percent"
|
||||
:model-value="props.media?.percent"
|
||||
bg-color="success"
|
||||
color="success"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.text-shadow{
|
||||
text-shadow:1px 1px #777;
|
||||
}
|
||||
</style>
|
||||
95
src/components/cards/BangumiPersonCard.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script lang="ts" setup>
|
||||
import personIcon from '@images/misc/person-icon.png'
|
||||
import type { BangumiPerson } from '@/api/types'
|
||||
|
||||
const personProps = defineProps({
|
||||
person: Object as PropType<BangumiPerson>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 当前人物
|
||||
const personInfo = ref(personProps.person)
|
||||
|
||||
// 人物图片是否加载
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 人物图片地址
|
||||
function getPersonImage() {
|
||||
if (!personInfo.value?.images)
|
||||
return personIcon
|
||||
return personInfo.value?.images?.medium
|
||||
}
|
||||
|
||||
// 使用、拼装人物角色
|
||||
function getPersonCharacter() {
|
||||
if (!personInfo.value?.career)
|
||||
return ''
|
||||
return personInfo.value?.career.join('、')
|
||||
}
|
||||
|
||||
// 打开人物详情
|
||||
function goPersonDetail() {
|
||||
if (!personInfo.value?.id)
|
||||
return
|
||||
window.open(`https://bangumi.tv/person/${personInfo.value?.id}`, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:height="personProps.height"
|
||||
:width="personProps.width"
|
||||
class="rounded-lg"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105': hover.isHovering,
|
||||
}"
|
||||
@click.stop="goPersonDetail"
|
||||
>
|
||||
<div
|
||||
class="person-card relative transform-gpu cursor-pointer rounded shadow ring-1 transition duration-150 ease-in-out scale-100 ring-gray-700"
|
||||
>
|
||||
<div style="padding-bottom: 150%;">
|
||||
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2">
|
||||
<div class="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
|
||||
<VAvatar
|
||||
size="120"
|
||||
:class="{
|
||||
'ring-1 ring-gray-700': isImageLoaded,
|
||||
}"
|
||||
>
|
||||
<VImg
|
||||
v-img
|
||||
:src="getPersonImage()"
|
||||
cover
|
||||
@load="isImageLoaded = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
<div class="w-full truncate text-center font-bold">
|
||||
{{ personInfo?.name }}
|
||||
</div>
|
||||
<div class="overflow-hidden whitespace-normal text-center text-sm" style=" display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical;-webkit-line-clamp: 2;">
|
||||
{{ getPersonCharacter() }}
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 right-0 h-12 rounded-b" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.person-card {
|
||||
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-theme-surface)) 60%);
|
||||
}
|
||||
|
||||
.person-card:hover {
|
||||
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-custom-background)) 60%);
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { kFormatter } from '@core/utils/formatters'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
color?: string
|
||||
icon: string
|
||||
stats: number
|
||||
change: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
color: 'primary',
|
||||
})
|
||||
|
||||
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardText class="d-flex align-center">
|
||||
<VAvatar
|
||||
size="44"
|
||||
rounded
|
||||
:color="props.color"
|
||||
variant="tonal"
|
||||
class="me-4"
|
||||
>
|
||||
<VIcon
|
||||
:icon="props.icon"
|
||||
size="30"
|
||||
/>
|
||||
</VAvatar>
|
||||
|
||||
<div>
|
||||
<span class="text-caption">{{ props.title }}</span>
|
||||
<div class="d-flex align-center flex-wrap">
|
||||
<span class="text-h6 font-weight-semibold">{{ kFormatter(props.stats) }}</span>
|
||||
<div
|
||||
v-if="props.change"
|
||||
:class="`${isPositive ? 'text-success' : 'text-error'} mt-1`"
|
||||
>
|
||||
<VIcon :icon="isPositive ? 'mdi-chevron-up' : 'mdi-chevron-down'" />
|
||||
<span class="text-caption font-weight-semibold">{{ Math.abs(props.change) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -1,56 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string
|
||||
color?: string
|
||||
icon: string
|
||||
stats: string
|
||||
change: number
|
||||
subtitle: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
color: 'primary',
|
||||
})
|
||||
|
||||
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardText class="d-flex align-center">
|
||||
<VAvatar
|
||||
v-if="props.icon"
|
||||
size="38"
|
||||
:color="props.color"
|
||||
>
|
||||
<VIcon
|
||||
:icon="props.icon"
|
||||
size="24"
|
||||
/>
|
||||
</VAvatar>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<MoreBtn class="me-n3 mt-n1" />
|
||||
</VCardText>
|
||||
|
||||
<VCardText>
|
||||
<h6 class="text-sm font-weight-semibold mb-2">
|
||||
{{ props.title }}
|
||||
</h6>
|
||||
<div
|
||||
v-if="props.change"
|
||||
class="d-flex align-center mb-2"
|
||||
>
|
||||
<span class="font-weight-semibold text-h5 me-2">{{ props.stats }}</span>
|
||||
<span
|
||||
:class="isPositive ? 'text-success' : 'text-error'"
|
||||
class="text-caption"
|
||||
>
|
||||
{{ isPositive ? `+${props.change}` : props.change }}%
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-caption">{{ props.subtitle }}</span>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -1,65 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string
|
||||
subtitle: string
|
||||
stats: string
|
||||
change: number
|
||||
image: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
color: 'primary',
|
||||
})
|
||||
|
||||
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="overflow-visible">
|
||||
<div class="d-flex position-relative">
|
||||
<VCardText>
|
||||
<h6 class="text-base font-weight-semibold mb-4">
|
||||
{{ props.title }}
|
||||
</h6>
|
||||
<div class="d-flex align-center flex-wrap mb-4">
|
||||
<h5 class="text-h5 font-weight-semibold me-2">
|
||||
{{ props.stats }}
|
||||
</h5>
|
||||
<span
|
||||
class="text-caption"
|
||||
:class="isPositive ? 'text-success' : 'text-error'"
|
||||
>
|
||||
{{ isPositive ? `+${props.change}` : props.change }}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<VChip
|
||||
v-if="props.subtitle"
|
||||
size="small"
|
||||
:color="props.color"
|
||||
>
|
||||
{{ props.subtitle }}
|
||||
</VChip>
|
||||
</VCardText>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<div class="illustrator-img">
|
||||
<VImg
|
||||
v-if="props.image"
|
||||
:src="props.image"
|
||||
:width="110"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.illustrator-img {
|
||||
position: absolute;
|
||||
inset-block-end: 0;
|
||||
inset-inline-end: 5%;
|
||||
}
|
||||
</style>
|
||||
@@ -30,7 +30,7 @@ function goPersonDetail() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover v-bind="personProps">
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
|
||||
@@ -23,6 +23,11 @@ function getSpeedText() {
|
||||
// 下载状态
|
||||
const isDownloading = ref(props.info?.state === 'downloading')
|
||||
|
||||
// 监听props.info?.state的变化
|
||||
watch(() => props.info?.state, (newValue) => {
|
||||
isDownloading.value = newValue === 'downloading'
|
||||
})
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
||||
{ title: '特效字幕', value: ' SPECSUB ' },
|
||||
{ title: '中文字幕', value: ' CNSUB ' },
|
||||
{ title: '国语配音', value: ' CNVOI ' },
|
||||
{ title: '官种', value: ' GZ ' },
|
||||
{ title: '排除: 国语配音', value: ' !CNVOI ' },
|
||||
{ title: '粤语配音', value: ' HKVOI ' },
|
||||
{ title: '排除: 粤语配音', value: ' !HKVOI ' },
|
||||
|
||||
202
src/components/cards/LibraryCard.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MediaServerLibrary } from '@/api/types'
|
||||
import plex from '@images/misc/plex.png'
|
||||
import emby from '@images/misc/emby.png'
|
||||
import jellyfin from '@images/misc/jellyfin.png'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<MediaServerLibrary>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// canvas
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
|
||||
// 图片地址
|
||||
const imgUrl = ref('')
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
// 图片是否加载错误
|
||||
const imageError = ref(false)
|
||||
|
||||
// 图片加载完成响应
|
||||
function imageLoadHandler() {
|
||||
imageLoaded.value = true
|
||||
}
|
||||
|
||||
// 图片加载错误
|
||||
function imageErrorHandler() {
|
||||
imageError.value = true
|
||||
}
|
||||
|
||||
// 默认图片
|
||||
function getDefaultImage() {
|
||||
if (props.media?.server === 'plex')
|
||||
return plex
|
||||
else if (props.media?.server === 'emby')
|
||||
return emby
|
||||
else if (props.media?.server === 'jellyfin')
|
||||
return jellyfin
|
||||
else
|
||||
return plex
|
||||
}
|
||||
|
||||
// 跳转播放
|
||||
function goPlay() {
|
||||
if (props.media?.link)
|
||||
window.open(props.media?.link, '_blank')
|
||||
}
|
||||
|
||||
// 生成图片代理路径
|
||||
function getImgUrl(url: string) {
|
||||
if (!url)
|
||||
return getDefaultImage()
|
||||
else
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
|
||||
}
|
||||
|
||||
// 根据多张图片生成媒体库封面
|
||||
async function drawImages(imageList: string[]) {
|
||||
// 图片
|
||||
const IMAGES = imageList
|
||||
if (IMAGES.length === 0)
|
||||
return getDefaultImage()
|
||||
|
||||
// 为所有图片添加system/img前缀
|
||||
for (let i = 0; i < IMAGES.length; i++)
|
||||
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(IMAGES[i])}`
|
||||
|
||||
// canvas
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas)
|
||||
return getDefaultImage()
|
||||
|
||||
// 画布参数
|
||||
const POSTER_WIDTH = (canvas.width - 32) / 4
|
||||
const POSTER_HEIGHT = canvas.height * 0.75 - 8
|
||||
const MARGIN_WIDTH = 4
|
||||
const MARGIN_HEIGHT = 4
|
||||
const REFLECTION_HEIGHT = POSTER_HEIGHT / 2
|
||||
const REFLECTION_SHOW_HEIGHT = canvas.height / 4
|
||||
|
||||
// 获取画布上下文
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx)
|
||||
return getDefaultImage()
|
||||
|
||||
// 设置背景色为黑色
|
||||
ctx.fillStyle = '#000000'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 绘制图片
|
||||
async function drawImageWithReflection(imgSrc: string, index: number) {
|
||||
if (!canvas)
|
||||
return
|
||||
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
const img = new Image()
|
||||
img.setAttribute('crossorigin', 'anonymous')
|
||||
img.src = imgSrc
|
||||
await new Promise(resolve => img.onload = resolve)
|
||||
|
||||
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
|
||||
const y = MARGIN_HEIGHT
|
||||
|
||||
ctx.drawImage(img, x, y, POSTER_WIDTH, POSTER_HEIGHT)
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(0, canvas.height)
|
||||
ctx.scale(1, -1)
|
||||
ctx.drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
img.width,
|
||||
img.height,
|
||||
x,
|
||||
REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT,
|
||||
POSTER_WIDTH,
|
||||
REFLECTION_HEIGHT,
|
||||
)
|
||||
|
||||
const gradient = ctx.createLinearGradient(
|
||||
0,
|
||||
REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT,
|
||||
0,
|
||||
REFLECTION_HEIGHT,
|
||||
)
|
||||
|
||||
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)')
|
||||
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.3)')
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(x, 0, POSTER_WIDTH, REFLECTION_SHOW_HEIGHT)
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
// 绘制多张图片
|
||||
const loopCount = Math.min(4, IMAGES.length)
|
||||
for (let i = 0; i < loopCount; i++)
|
||||
await drawImageWithReflection(IMAGES[i], i + 1)
|
||||
|
||||
// 转换为图片地址
|
||||
return canvas.toDataURL('image/png')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.media?.image_list && props.media?.image_list.length > 0)
|
||||
imgUrl.value = await drawImages(props.media?.image_list || [])
|
||||
else
|
||||
imgUrl.value = getImgUrl(props.media?.image || '')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover
|
||||
v-bind="props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
}"
|
||||
@click="goPlay"
|
||||
>
|
||||
<template #image>
|
||||
<canvas ref="canvasRef" class="w-full h-full hidden" />
|
||||
<VImg
|
||||
:src="imgUrl"
|
||||
aspect-ratio="2/3"
|
||||
cover
|
||||
@load="imageLoadHandler"
|
||||
@error="imageErrorHandler"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<VCardText
|
||||
class="w-full flex flex-col flex-wrap justify-end align-center text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
>
|
||||
<h1 class="mb-1 text-white font-bold line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.name }}
|
||||
</h1>
|
||||
</VCardText>
|
||||
</VImg>
|
||||
</template>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType, Ref } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
|
||||
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
@@ -52,6 +52,15 @@ const seasonInfos = ref<TmdbSeason[]>([])
|
||||
// 选中的订阅季
|
||||
const seasonsSelected = ref<TmdbSeason[]>([])
|
||||
|
||||
// 获得mediaid
|
||||
function getMediaId() {
|
||||
return props.media?.tmdb_id
|
||||
? `tmdb:${props.media?.tmdb_id}`
|
||||
: props.media?.douban_id
|
||||
? `douban:${props.media?.douban_id}`
|
||||
: `bangumi:${props.media?.bangumi_id}`
|
||||
}
|
||||
|
||||
// 订阅弹窗选择的多季
|
||||
function subscribeSeasons() {
|
||||
subscribeSeasonDialog.value = false
|
||||
@@ -126,6 +135,7 @@ async function addSubscribe(season = 0) {
|
||||
year: props.media?.year,
|
||||
tmdbid: props.media?.tmdb_id,
|
||||
doubanid: props.media?.douban_id,
|
||||
bangumiid: props.media?.bangumi_id,
|
||||
season,
|
||||
best_version,
|
||||
})
|
||||
@@ -147,14 +157,18 @@ async function addSubscribe(season = 0) {
|
||||
|
||||
// 弹出订阅编辑弹窗
|
||||
if (result.success && seasonsSelected.value.length <= 1) {
|
||||
subscribeId.value = result.data.id
|
||||
subscribeEditDialog.value = true
|
||||
const show_edit_dialog = await queryDefaultSubscribeConfig()
|
||||
if (show_edit_dialog) {
|
||||
subscribeId.value = result.data.id
|
||||
subscribeEditDialog.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
doneNProgress()
|
||||
}
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 弹出添加订阅提示
|
||||
@@ -181,9 +195,7 @@ async function removeSubscribe() {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
const mediaid = props.media?.tmdb_id
|
||||
? `tmdb:${props.media?.tmdb_id}`
|
||||
: `douban:${props.media?.douban_id}`
|
||||
const mediaid = getMediaId()
|
||||
|
||||
const result: { [key: string]: any } = await api.delete(
|
||||
`subscribe/media/${mediaid}`,
|
||||
@@ -223,7 +235,7 @@ async function handleCheckSubscribe() {
|
||||
// 查询当前媒体是否已入库
|
||||
async function handleCheckExists() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('media/exists', {
|
||||
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||
params: {
|
||||
tmdbid: props.media?.tmdb_id,
|
||||
title: props.media?.title,
|
||||
@@ -244,9 +256,7 @@ async function handleCheckExists() {
|
||||
// 调用API检查是否已订阅,电视剧需要指定季
|
||||
async function checkSubscribe(season = 0) {
|
||||
try {
|
||||
const mediaid = props.media?.tmdb_id
|
||||
? `tmdb:${props.media?.tmdb_id}`
|
||||
: `douban:${props.media?.douban_id}`
|
||||
const mediaid = getMediaId()
|
||||
|
||||
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
||||
params: {
|
||||
@@ -269,7 +279,7 @@ async function checkSeasonsNotExists() {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
const result: NotExistMediaInfo[] = await api.post('download/notexists', props.media)
|
||||
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', props.media)
|
||||
if (result) {
|
||||
result.forEach((item) => {
|
||||
// 0-已入库 1-部分缺失 2-全部缺失
|
||||
@@ -302,6 +312,26 @@ async function getMediaSeasons() {
|
||||
}
|
||||
}
|
||||
|
||||
// 查询订阅弹窗规则
|
||||
async function queryDefaultSubscribeConfig() {
|
||||
try {
|
||||
let subscribe_config_url = ''
|
||||
if (props.media?.type === '电影')
|
||||
subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
|
||||
else
|
||||
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
|
||||
|
||||
const result: { [key: string]: any } = await api.get(subscribe_config_url)
|
||||
|
||||
if (result.data?.value)
|
||||
return result.data.value.show_edit_dialog
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 爱心订阅按钮响应
|
||||
function handleSubscribe() {
|
||||
if (isSubscribed.value)
|
||||
@@ -339,18 +369,16 @@ function getExistText(season: number) {
|
||||
}
|
||||
|
||||
// 打开详情页
|
||||
function goMediaDetail() {
|
||||
router.push({
|
||||
path: '/media',
|
||||
query: {
|
||||
mediaid: `${
|
||||
props.media?.tmdb_id
|
||||
? `tmdb:${props.media?.tmdb_id}`
|
||||
: `douban:${props.media?.douban_id}`
|
||||
}`,
|
||||
type: props.media?.type,
|
||||
},
|
||||
})
|
||||
function goMediaDetail(isHovering = false) {
|
||||
if (isHovering) {
|
||||
router.push({
|
||||
path: '/media',
|
||||
query: {
|
||||
mediaid: getMediaId(),
|
||||
type: props.media?.type,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 开始搜索
|
||||
@@ -358,13 +386,10 @@ function handleSearch() {
|
||||
router.push({
|
||||
path: '/resource',
|
||||
query: {
|
||||
keyword: `${
|
||||
props.media?.tmdb_id
|
||||
? `tmdb:${props.media?.tmdb_id}`
|
||||
: `douban:${props.media?.douban_id}`
|
||||
}`,
|
||||
keyword: getMediaId(),
|
||||
type: props.media?.type,
|
||||
area: 'title',
|
||||
season: props.media?.season,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -382,7 +407,7 @@ const getImgUrl: Ref<string> = computed(() => {
|
||||
const url = props.media?.poster_path?.replace('original', 'w500') ?? noImage
|
||||
// 如果地址中包含douban则使用中转代理
|
||||
if (url.includes('doubanio.com'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}douban/img/${encodeURIComponent(url)}`
|
||||
return `${import.meta.env.VITE_API_BASE_URL}douban/img?imgurl=${encodeURIComponent(url)}`
|
||||
|
||||
return url
|
||||
})
|
||||
@@ -411,7 +436,7 @@ function getYear(airDate: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover v-bind="props">
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
@@ -422,6 +447,7 @@ function getYear(airDate: string) {
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'ring-1': isImageLoaded,
|
||||
}"
|
||||
@click.stop="goMediaDetail(hover.isHovering)"
|
||||
>
|
||||
<VImg
|
||||
aspect-ratio="2/3"
|
||||
@@ -437,60 +463,60 @@ function getYear(airDate: string) {
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
<!-- 类型角标 -->
|
||||
<VChip
|
||||
v-show="isImageLoaded"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
:class="getChipColor(props.media?.type || '')"
|
||||
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||
>
|
||||
{{ props.media?.type }}
|
||||
</VChip>
|
||||
<!-- 本地存在标识 -->
|
||||
<ExistIcon v-if="isExists" />
|
||||
<!-- 评分角标 -->
|
||||
<VChip
|
||||
v-if="isImageLoaded && props.media?.vote_average && !isExists"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
:class="getChipColor('rating')"
|
||||
class="absolute right-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||
>
|
||||
{{ props.media?.vote_average }}
|
||||
</VChip>
|
||||
<!-- 详情 -->
|
||||
<VCardText
|
||||
v-show="hover.isHovering || imageLoadError"
|
||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
@click.stop="goMediaDetail"
|
||||
>
|
||||
<span class="font-bold">{{ props.media?.year }}</span>
|
||||
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.title }}
|
||||
</h1>
|
||||
<p class="leading-4 line-clamp-4 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.overview }}
|
||||
</p>
|
||||
<div class="flex align-center justify-between">
|
||||
<IconBtn
|
||||
icon="mdi-magnify"
|
||||
color="white"
|
||||
@click.stop="handleSearch"
|
||||
/>
|
||||
<IconBtn
|
||||
icon="mdi-heart"
|
||||
:color="isSubscribed ? 'error' : 'white'"
|
||||
@click.stop="handleSubscribe"
|
||||
/>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VImg>
|
||||
<!-- 类型角标 -->
|
||||
<VChip
|
||||
v-show="isImageLoaded"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
:class="getChipColor(props.media?.type || '')"
|
||||
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||
>
|
||||
{{ props.media?.type }}
|
||||
</VChip>
|
||||
<!-- 本地存在标识 -->
|
||||
<ExistIcon v-if="isExists" />
|
||||
<!-- 评分角标 -->
|
||||
<VChip
|
||||
v-if="isImageLoaded && props.media?.vote_average && !isExists"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
:class="getChipColor('rating')"
|
||||
class="absolute right-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||
>
|
||||
{{ props.media?.vote_average }}
|
||||
</VChip>
|
||||
<!-- 详情 -->
|
||||
<VCardText
|
||||
v-show="hover.isHovering || imageLoadError"
|
||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
>
|
||||
<span class="font-bold">{{ props.media?.year }}</span>
|
||||
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.title }}
|
||||
</h1>
|
||||
<p class="leading-4 line-clamp-4 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.overview }}
|
||||
</p>
|
||||
<div class="flex align-center justify-between">
|
||||
<IconBtn
|
||||
icon="mdi-magnify"
|
||||
color="white"
|
||||
@click.stop="handleSearch"
|
||||
/>
|
||||
<IconBtn
|
||||
icon="mdi-heart"
|
||||
:color="isSubscribed ? 'error' : 'white'"
|
||||
@click.stop="handleSubscribe"
|
||||
/>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
<!-- 订阅季弹窗 -->
|
||||
<VBottomSheet
|
||||
v-if="subscribeSeasonDialog"
|
||||
v-model="subscribeSeasonDialog"
|
||||
inset
|
||||
scrollable
|
||||
@@ -573,7 +599,8 @@ function getYear(airDate: string) {
|
||||
</VCard>
|
||||
</VBottomSheet>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<SubscribeEditForm
|
||||
<SubscribeEditDialog
|
||||
v-if="subscribeEditDialog"
|
||||
v-model="subscribeEditDialog"
|
||||
:subid="subscribeId"
|
||||
@close="subscribeEditDialog = false"
|
||||
|
||||
112
src/components/cards/MessageCard.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Message } from '@/api/types'
|
||||
import { formatDateDifference } from '@core/utils/formatters'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
message: Object as PropType<Message>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 图片是否加载完成
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 图片是否加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 图片加载完成
|
||||
async function imageLoaded() {
|
||||
isImageLoaded.value = true
|
||||
}
|
||||
|
||||
// 链接打开新窗口
|
||||
function openLink() {
|
||||
if (props.message?.link)
|
||||
window.open(props.message.link, '_blank')
|
||||
}
|
||||
|
||||
// 将note转换为json
|
||||
function noteToJson() {
|
||||
if (props.message?.note) {
|
||||
try {
|
||||
return JSON.parse(props.message.note)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
// 将\n转换为html属性的换行符
|
||||
function replaceNewLine(value: string) {
|
||||
if (!value)
|
||||
return ''
|
||||
return value.replace(/\n/g, '<br/>')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
:width="props.width"
|
||||
:height="props.height"
|
||||
variant="tonal"
|
||||
@click="openLink"
|
||||
>
|
||||
<div
|
||||
v-if="props.message?.image"
|
||||
class="relative text-center card-cover-blurred"
|
||||
>
|
||||
<VImg
|
||||
:src="props.message?.image"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</div>
|
||||
<VCardTitle v-if="props.message?.title" class="whitespace-break-spaces">
|
||||
{{ props.message?.title }}
|
||||
</VCardTitle>
|
||||
<VAlert
|
||||
v-if="props.message?.text && props.message?.action === 0"
|
||||
variant="tonal"
|
||||
type="success"
|
||||
>
|
||||
<template #prepend />
|
||||
{{ props.message?.text }}
|
||||
</VAlert>
|
||||
<VCardText
|
||||
v-if="props.message?.text && props.message?.action === 1"
|
||||
v-html="replaceNewLine(props.message?.text)"
|
||||
/>
|
||||
<VCardText v-if="props.message?.note">
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(value, key) in noteToJson()"
|
||||
:key="key"
|
||||
two-line
|
||||
>
|
||||
<VListItemTitle v-if="value.title_year" class="font-bold">
|
||||
{{ key + 1 }}. {{ value.title_year }}
|
||||
</VListItemTitle>
|
||||
<VListItemTitle v-if="value.enclosure" class="font-bold whitespace-break-spaces">
|
||||
{{ key + 1 }}. {{ value.title }} {{ value.volume_factor }} ↑{{ value.seeders }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle v-if="value.type">
|
||||
类型:{{ value.type }} 评分:{{ value.vote_average }}
|
||||
</VListItemSubtitle>
|
||||
<VListItemSubtitle v-if="value.enclosure" class="whitespace-break-spaces">
|
||||
{{ value.description }}
|
||||
</VListItemSubtitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
<div class="text-end">
|
||||
<span v-if="props.message?.action === 0" class="text-sm italic me-2">{{ props.message?.userid }}</span>
|
||||
<span class="text-sm italic me-2">{{ formatDateDifference(props.message?.reg_time || props.message?.date || '') }}</span>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -1,15 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import VersionHistory from '../misc/VersionHistory.vue'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { getDominantColor } from '@/@core/utils/image'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
plugin: Object as PropType<Plugin>,
|
||||
width: String,
|
||||
height: String,
|
||||
count: Number,
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
@@ -36,6 +39,9 @@ const isImageLoaded = ref(false)
|
||||
// 图片是否加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 更新日志弹窗
|
||||
const releaseDialog = ref(false)
|
||||
|
||||
// 图片加载完成
|
||||
async function imageLoaded() {
|
||||
isImageLoaded.value = true
|
||||
@@ -49,7 +55,7 @@ async function installPlugin() {
|
||||
try {
|
||||
// 显示等待提示框
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在安装 ${props.plugin?.plugin_name} ${props?.plugin?.plugin_version} 插件...`
|
||||
progressText.value = `正在安装 ${props.plugin?.plugin_name} v${props?.plugin?.plugin_version} ...`
|
||||
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`plugin/install/${props.plugin?.id}`,
|
||||
@@ -85,9 +91,9 @@ const iconPath: Ref<string> = computed(() => {
|
||||
return noImage
|
||||
// 如果是网络图片则使用代理后返回
|
||||
if (props.plugin?.plugin_icon?.startsWith('http'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}`
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(props.plugin?.plugin_icon)}`
|
||||
|
||||
return `/plugin_icon/${props.plugin?.plugin_icon}`
|
||||
return `./plugin_icon/${props.plugin?.plugin_icon}`
|
||||
})
|
||||
|
||||
// 访问插件页面
|
||||
@@ -117,15 +123,29 @@ function visitPluginPage() {
|
||||
window.open(repoUrl, '_blank')
|
||||
}
|
||||
|
||||
// 显示更新日志
|
||||
function showUpdateHistory() {
|
||||
releaseDialog.value = true
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '查看详情',
|
||||
title: '项目主页',
|
||||
value: 1,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-information-outline',
|
||||
prependIcon: 'mdi-github',
|
||||
click: visitPluginPage,
|
||||
},
|
||||
}, {
|
||||
title: '更新说明',
|
||||
value: 2,
|
||||
show: !isNullOrEmptyObject(props.plugin?.history || {}),
|
||||
props: {
|
||||
prependIcon: 'mdi-update',
|
||||
click: showUpdateHistory,
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
@@ -150,6 +170,7 @@ const dropdownItems = ref([
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
v-show="item.show"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
@click="item.props.click"
|
||||
@@ -163,15 +184,6 @@ const dropdownItems = ref([
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.plugin?.has_update"
|
||||
class="me-n3 absolute top-0 left-1"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-new-box"
|
||||
class="text-white"
|
||||
/>
|
||||
</div>
|
||||
<VAvatar
|
||||
size="8rem"
|
||||
>
|
||||
@@ -186,20 +198,28 @@ const dropdownItems = ref([
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
<VCardTitle>{{ props.plugin?.plugin_name }}</VCardTitle>
|
||||
|
||||
<VCardText>
|
||||
<VCardTitle>
|
||||
{{ props.plugin?.plugin_name }}
|
||||
<span class="text-sm text-gray-500">v{{ props.plugin?.plugin_version }}</span>
|
||||
</VCardTitle>
|
||||
<VCardText class="pb-2">
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
作者:<a
|
||||
:href="props.plugin?.author_url"
|
||||
target="_blank"
|
||||
@click.stop
|
||||
>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a><br>
|
||||
版本:{{ props.plugin?.plugin_version }}
|
||||
<VCardText class="flex items-center justify-start pb-2">
|
||||
<span>
|
||||
<VIcon icon="mdi-account" class="me-1" />
|
||||
<a
|
||||
:href="props.plugin?.author_url"
|
||||
target="_blank"
|
||||
@click.stop
|
||||
>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a>
|
||||
</span>
|
||||
<span v-if="props.count" class="ms-3">
|
||||
<VIcon icon="mdi-download" />
|
||||
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||
</span>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<!-- 安装插件进度框 -->
|
||||
@@ -221,6 +241,19 @@ const dropdownItems = ref([
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 更新日志 -->
|
||||
<VDialog
|
||||
v-if="releaseDialog"
|
||||
v-model="releaseDialog"
|
||||
width="600"
|
||||
scrollable
|
||||
>
|
||||
<VCard>
|
||||
<DialogCloseBtn @click="releaseDialog = false" />
|
||||
<VCardTitle>{{ props.plugin?.plugin_name }} 更新说明</VCardTitle>
|
||||
<VersionHistory :history="props.plugin?.history" />
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { VIcon } from 'vuetify/lib/components/index.mjs'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import FormRender from '@/components/render/FormRender.vue'
|
||||
import PageRender from '@/components/render/PageRender.vue'
|
||||
import VersionHistory from '@/components/misc/VersionHistory.vue'
|
||||
import { isNullOrEmptyObject } from '@core/utils'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { getDominantColor } from '@/@core/utils/image'
|
||||
import store from '@/store'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
plugin: Object as PropType<Plugin>,
|
||||
count: Number, // 下载次数
|
||||
action: Boolean, // 动作标识
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['remove', 'save'])
|
||||
const emit = defineEmits(['remove', 'save', 'actionDone'])
|
||||
|
||||
// 背景颜色
|
||||
const backgroundColor = ref('#28A9E1')
|
||||
@@ -40,12 +45,18 @@ const pluginConfigDialog = ref(false)
|
||||
// 插件配置表单数据
|
||||
const pluginConfigForm = ref({})
|
||||
|
||||
// 进度框
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 插件表单配置项
|
||||
let pluginFormItems = reactive([])
|
||||
|
||||
// 插件数据页面
|
||||
const pluginInfoDialog = ref(false)
|
||||
|
||||
// 进度框文本
|
||||
const progressText = ref('正在更新插件...')
|
||||
|
||||
// 插件数据页面配置项
|
||||
let pluginPageItems = reactive([])
|
||||
|
||||
@@ -55,6 +66,17 @@ const isImageLoaded = ref(false)
|
||||
// 图片是否加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 更新日志弹窗
|
||||
const releaseDialog = ref(false)
|
||||
|
||||
// 监听动作标识,如为true则打开详情
|
||||
watch(() => props.action, (newAction, oldAction) => {
|
||||
if (newAction && !oldAction) {
|
||||
openPluginDetail()
|
||||
emit('actionDone')
|
||||
}
|
||||
})
|
||||
|
||||
// 图片加载完成
|
||||
async function imageLoaded() {
|
||||
isImageLoaded.value = true
|
||||
@@ -63,6 +85,16 @@ async function imageLoaded() {
|
||||
backgroundColor.value = await getDominantColor(imageElement)
|
||||
}
|
||||
|
||||
// 显示更新日志
|
||||
function showUpdateHistory() {
|
||||
// 检查当前版本是否有更新日志
|
||||
if (isNullOrEmptyObject(props.plugin?.history)) {
|
||||
updatePlugin()
|
||||
} else{
|
||||
releaseDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API卸载插件
|
||||
async function uninstallPlugin() {
|
||||
const isConfirmed = await createConfirm({
|
||||
@@ -82,7 +114,12 @@ async function uninstallPlugin() {
|
||||
return
|
||||
|
||||
try {
|
||||
// 显示等待提示框
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在卸载 ${props.plugin?.plugin_name} ...`
|
||||
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`)
|
||||
// 隐藏等待提示框
|
||||
progressDialog.value = false
|
||||
if (result.success) {
|
||||
$toast.success(`插件 ${props.plugin?.plugin_name} 已卸载`)
|
||||
|
||||
@@ -139,15 +176,20 @@ async function loadPluginConf() {
|
||||
|
||||
// 调用API保存配置数据
|
||||
async function savePluginConf() {
|
||||
// 显示等待提示框
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在保存 ${props.plugin?.plugin_name} 配置...`
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.put(`plugin/${props.plugin?.id}`, pluginConfigForm.value)
|
||||
if (result.success) {
|
||||
$toast.success(`插件 ${props.plugin?.plugin_name} 配置已保存`)
|
||||
progressDialog.value = false
|
||||
pluginConfigDialog.value = false
|
||||
$toast.success(`插件 ${props.plugin?.plugin_name} 配置已保存`)
|
||||
// 通知父组件刷新
|
||||
emit('save')
|
||||
}
|
||||
else {
|
||||
progressDialog.value = false
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 配置保存失败:${result.message}}`)
|
||||
}
|
||||
}
|
||||
@@ -181,9 +223,9 @@ const iconPath: Ref<string> = computed(() => {
|
||||
return noImage
|
||||
// 如果是网络图片则使用代理后返回
|
||||
if (props.plugin?.plugin_icon?.startsWith('http'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}`
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(props.plugin?.plugin_icon)}`
|
||||
|
||||
return `/plugin_icon/${props.plugin?.plugin_icon}`
|
||||
return `./plugin_icon/${props.plugin?.plugin_icon}`
|
||||
})
|
||||
|
||||
// 重置插件
|
||||
@@ -220,11 +262,62 @@ async function resetPlugin() {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新插件
|
||||
async function updatePlugin() {
|
||||
try {
|
||||
releaseDialog.value = false
|
||||
// 显示等待提示框
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在更新 ${props.plugin?.plugin_name} ...`
|
||||
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`plugin/install/${props.plugin?.id}`,
|
||||
{
|
||||
params: {
|
||||
repo_url: props.plugin?.repo_url,
|
||||
force: true,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// 隐藏等待提示框
|
||||
progressDialog.value = false
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(`插件 ${props.plugin?.plugin_name} 更新成功!`)
|
||||
|
||||
// 通知父组件刷新
|
||||
emit('save')
|
||||
}
|
||||
else {
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 更新失败:${result.message}`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 访问作者主页
|
||||
function visitAuthorPage() {
|
||||
window.open(props.plugin?.author_url, '_blank')
|
||||
}
|
||||
|
||||
// 查看日志URL
|
||||
function openLoggerWindow() {
|
||||
const token = store.state.auth.token
|
||||
const url = `${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}&length=-1&logfile=plugins/${props.plugin?.id?.toLowerCase()}.log`
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// 打开插件详情
|
||||
function openPluginDetail() {
|
||||
if (props.plugin?.has_page)
|
||||
showPluginInfo()
|
||||
else
|
||||
showPluginConfig()
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
@@ -246,8 +339,18 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '重置',
|
||||
title: '更新',
|
||||
value: 3,
|
||||
show: props.plugin?.has_update,
|
||||
props: {
|
||||
prependIcon: 'mdi-arrow-up-circle-outline',
|
||||
color: 'success',
|
||||
click: showUpdateHistory,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '重置',
|
||||
value: 4,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-cancel',
|
||||
@@ -257,7 +360,7 @@ const dropdownItems = ref([
|
||||
},
|
||||
{
|
||||
title: '卸载',
|
||||
value: 4,
|
||||
value: 5,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
@@ -265,9 +368,20 @@ const dropdownItems = ref([
|
||||
click: uninstallPlugin,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '查看日志',
|
||||
value: 6,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-file-document-outline',
|
||||
click: () => {
|
||||
openLoggerWindow()
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '作者主页',
|
||||
value: 4,
|
||||
value: 7,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-home-circle-outline',
|
||||
@@ -275,6 +389,13 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
// 监听插件状态变化
|
||||
watch(() => props.plugin?.has_update, (newHasUpdate, oldHasUpdate) => {
|
||||
const updateItemIndex = dropdownItems.value.findIndex(item => item.value === 3)
|
||||
if (updateItemIndex !== -1)
|
||||
dropdownItems.value[updateItemIndex].show = newHasUpdate
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -283,17 +404,21 @@ const dropdownItems = ref([
|
||||
v-if="isVisible"
|
||||
:width="props.width"
|
||||
:height="props.height"
|
||||
@click="() => {
|
||||
if (props.plugin?.has_page)
|
||||
showPluginInfo()
|
||||
else
|
||||
showPluginConfig()
|
||||
}"
|
||||
@click="openPluginDetail"
|
||||
>
|
||||
<div
|
||||
class="relative pa-4 text-center card-cover-blurred"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div
|
||||
v-if="props.plugin?.has_update"
|
||||
class="me-n3 absolute top-0 left-1"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-new-box"
|
||||
class="text-white"
|
||||
/>
|
||||
</div>
|
||||
<div class="me-n3 absolute top-0 right-3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" class="text-white" />
|
||||
@@ -333,10 +458,14 @@ const dropdownItems = ref([
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
<span v-if="props.count" class="absolute bottom-1 right-2 flex items-center">
|
||||
<VIcon icon="mdi-fire" />
|
||||
<span class="text-sm ms-1">{{ props.count?.toLocaleString() }}</span>
|
||||
</span>
|
||||
<VCardItem class="py-2">
|
||||
<VCardTitle class="flex items-center flex-row">
|
||||
<VBadge v-if="props.plugin?.state" dot inline color="success" class="me-1 mb-1" />
|
||||
{{ props.plugin?.plugin_name }}<span class="text-sm ms-2 mt-1 text-gray-500">{{ props.plugin?.plugin_version }}</span>
|
||||
{{ props.plugin?.plugin_name }}<span class="text-sm ms-2 mt-1 text-gray-500">v{{ props.plugin?.plugin_version }}</span>
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
@@ -353,7 +482,7 @@ const dropdownItems = ref([
|
||||
:title="`${props.plugin?.plugin_name} - 配置`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<DialogCloseBtn @click="pluginConfigDialog = false" />
|
||||
<DialogCloseBtn v-model='pluginConfigDialog' />
|
||||
<VCardText>
|
||||
<FormRender
|
||||
v-for="(item, index) in pluginFormItems"
|
||||
@@ -387,7 +516,7 @@ const dropdownItems = ref([
|
||||
:title="`${props.plugin?.plugin_name}`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<DialogCloseBtn @click="pluginInfoDialog = false" />
|
||||
<DialogCloseBtn v-model='pluginInfoDialog' />
|
||||
<VCardText>
|
||||
<PageRender
|
||||
v-for="(item, index) in pluginPageItems"
|
||||
@@ -411,6 +540,49 @@ const dropdownItems = ref([
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 更新插件进度框 -->
|
||||
<VDialog
|
||||
v-model="progressDialog"
|
||||
:scrim="false"
|
||||
width="25rem"
|
||||
>
|
||||
<VCard
|
||||
color="primary"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
{{ progressText }}
|
||||
<VProgressLinear
|
||||
indeterminate
|
||||
color="white"
|
||||
class="mb-0 mt-1"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 更新日志 -->
|
||||
<VDialog
|
||||
v-if="releaseDialog"
|
||||
v-model="releaseDialog"
|
||||
width="600"
|
||||
scrollable
|
||||
>
|
||||
<VCard>
|
||||
<DialogCloseBtn @click="releaseDialog = false" />
|
||||
<VCardTitle>{{ props.plugin?.plugin_name }} 更新说明</VCardTitle>
|
||||
<VersionHistory :history="props.plugin?.history" />
|
||||
<VCardText>
|
||||
<VBtn
|
||||
@click="updatePlugin"
|
||||
block
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-arrow-up-circle-outline" />
|
||||
</template>
|
||||
更新到最新版本
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
102
src/components/cards/PosterCard.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
import noImage from '@images/no-image.jpeg'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<MediaServerPlayItem>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 图片加载状态
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 图片加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 角标颜色
|
||||
function getChipColor(type: string) {
|
||||
if (type === '电影')
|
||||
return 'border-blue-500 bg-blue-600'
|
||||
else if (type === '电视剧')
|
||||
return ' bg-indigo-500 border-indigo-600'
|
||||
else
|
||||
return 'border-purple-600 bg-purple-600'
|
||||
}
|
||||
|
||||
// 计算图片地址
|
||||
const getImgUrl = computed(() => {
|
||||
if (imageLoadError.value)
|
||||
return noImage
|
||||
const image = props.media?.image || ''
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
|
||||
})
|
||||
|
||||
// 跳转播放
|
||||
function goPlay(isHovering = false) {
|
||||
if (props.media?.link && isHovering)
|
||||
window.open(props.media?.link, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover v-bind="props">
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
class="outline-none shadow ring-gray-500 rounded-lg"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'ring-1': isImageLoaded,
|
||||
}"
|
||||
@click.stop="goPlay(hover.isHovering)"
|
||||
>
|
||||
<VImg
|
||||
aspect-ratio="2/3"
|
||||
:src="getImgUrl"
|
||||
class="object-cover aspect-w-2 aspect-h-3"
|
||||
:class="hover.isHovering ? 'on-hover' : ''"
|
||||
cover
|
||||
@load="isImageLoaded = true"
|
||||
@error="imageLoadError = true"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
<!-- 类型角标 -->
|
||||
<VChip
|
||||
v-show="isImageLoaded"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
:class="getChipColor(props.media?.type || '')"
|
||||
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||
>
|
||||
{{ props.media?.type }}
|
||||
</VChip>
|
||||
<!-- 详情 -->
|
||||
<VCardText
|
||||
v-show="hover.isHovering || imageLoadError"
|
||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
>
|
||||
<span class="font-bold">{{ props.media?.subtitle }}</span>
|
||||
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.title }}
|
||||
</h1>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.on-hover img {
|
||||
@apply brightness-50;
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import SiteAddEditForm from '../form/SiteAddEditForm.vue'
|
||||
import { formatFileSize } from '@core/utils/formatters'
|
||||
import SiteAddEditDialog from '../dialog/SiteAddEditDialog.vue'
|
||||
import SiteTorrentTable from '../table/SiteTorrentTable.vue'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Site, TorrentInfo } from '@/api/types'
|
||||
import type { Site, SiteStatistic } from '@/api/types'
|
||||
import ExistIcon from '@core/components/ExistIcon.vue'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
|
||||
// 输入参数
|
||||
const cardProps = defineProps({
|
||||
@@ -51,46 +52,15 @@ const progressDialog = ref(false)
|
||||
// 进度文本
|
||||
const progressText = ref('请稍候 ...')
|
||||
|
||||
// 资源浏览表头
|
||||
const resourceHeaders = [
|
||||
{ title: '标题', key: 'title', sortable: false },
|
||||
{ title: '时间', key: 'pubdate', sortable: true },
|
||||
{ title: '大小', key: 'size', sortable: true },
|
||||
{ title: '做种', key: 'seeders', sortable: true },
|
||||
{ title: '下载', key: 'peers', sortable: true },
|
||||
{ title: '', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
// 数据列表
|
||||
const resourceDataList = ref<TorrentInfo[]>([])
|
||||
|
||||
// 搜索
|
||||
const resourceSearch = ref('')
|
||||
|
||||
// 加载状态
|
||||
const resourceLoading = ref(false)
|
||||
|
||||
// 总条数
|
||||
const resourceTotalItems = ref(0)
|
||||
|
||||
// 每页条数
|
||||
const resourceItemsPerPage = ref(25)
|
||||
|
||||
// 用户名密码表单
|
||||
const userPwForm = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
code: '',
|
||||
})
|
||||
|
||||
// 打开种子详情页面
|
||||
function openTorrentDetail(page_url: string) {
|
||||
window.open(page_url, '_blank')
|
||||
}
|
||||
|
||||
// 下载种子文件
|
||||
async function downloadTorrentFile(enclosure: string) {
|
||||
window.open(enclosure, '_blank')
|
||||
}
|
||||
// 站点使用统计
|
||||
const siteStats = ref<SiteStatistic>({})
|
||||
|
||||
// 查询站点图标
|
||||
async function getSiteIcon() {
|
||||
@@ -116,6 +86,18 @@ async function testSite() {
|
||||
|
||||
testButtonText.value = '测试'
|
||||
testButtonDisable.value = false
|
||||
|
||||
getSiteStats()
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询站点使用统计
|
||||
async function getSiteStats() {
|
||||
try {
|
||||
siteStats.value = (await api.get(`site/statistic/${cardProps.site?.domain}`))
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
@@ -130,7 +112,6 @@ async function handleSiteUpdate() {
|
||||
// 打开资源浏览弹窗
|
||||
async function handleResourceBrowse() {
|
||||
resourceDialog.value = true
|
||||
getResourceList()
|
||||
}
|
||||
|
||||
// 调用API,更新站点Cookie UA
|
||||
@@ -152,6 +133,7 @@ async function updateSiteCookie() {
|
||||
params: {
|
||||
username: userPwForm.value.username,
|
||||
password: userPwForm.value.password,
|
||||
code: userPwForm.value.code,
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -169,38 +151,38 @@ async function updateSiteCookie() {
|
||||
}
|
||||
}
|
||||
|
||||
// 促销Chip类
|
||||
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||
if (downloadVolume === 0)
|
||||
return 'text-white bg-lime-500'
|
||||
else if (downloadVolume < 1)
|
||||
return 'text-white bg-green-500'
|
||||
else if (uploadVolume !== 1)
|
||||
return 'text-white bg-sky-500'
|
||||
else
|
||||
return 'text-white bg-gray-500'
|
||||
}
|
||||
|
||||
// 调用API,查询站点资源
|
||||
async function getResourceList() {
|
||||
resourceLoading.value = true
|
||||
try {
|
||||
resourceDataList.value = await api.get(`site/resource/${cardProps.site?.id}`)
|
||||
resourceLoading.value = false
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开站点页面
|
||||
function openSitePage() {
|
||||
window.open(cardProps.site?.url, '_blank')
|
||||
}
|
||||
|
||||
// 根据站点状态显示不同的状态图标
|
||||
const statColor = computed(() => {
|
||||
if (isNullOrEmptyObject(siteStats.value)){
|
||||
return 'secondary'
|
||||
}
|
||||
if (siteStats.value?.lst_state == 1){
|
||||
return 'error'
|
||||
}
|
||||
else if (siteStats.value?.lst_state == 0){
|
||||
if (!siteStats.value?.seconds)
|
||||
return 'secondary'
|
||||
if (siteStats.value?.seconds >= 5)
|
||||
return 'warning'
|
||||
return 'success'
|
||||
}
|
||||
})
|
||||
|
||||
// 监听resourceDialog,如果为false则重新查询站点使用统计
|
||||
watch(resourceDialog, (value) => {
|
||||
if (!value)
|
||||
getSiteStats()
|
||||
})
|
||||
|
||||
// 装载时查询站点图标
|
||||
onMounted(() => {
|
||||
getSiteIcon()
|
||||
getSiteStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -208,7 +190,7 @@ onMounted(() => {
|
||||
<VCard
|
||||
:height="cardProps.height"
|
||||
:width="cardProps.width"
|
||||
:flat="!cardProps.site?.is_active"
|
||||
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
|
||||
class="overflow-hidden"
|
||||
@click="siteEditDialog = true"
|
||||
>
|
||||
@@ -221,16 +203,17 @@ onMounted(() => {
|
||||
<VImg :src="siteIcon" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
|
||||
<VCardItem>
|
||||
<VCardTitle class="font-bold">
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
{{ cardProps.site?.url }}
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
|
||||
<ExistIcon v-if="cardProps.site?.is_active" />
|
||||
<StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
|
||||
|
||||
<VCardText class="py-2">
|
||||
<VTooltip
|
||||
@@ -335,7 +318,7 @@ onMounted(() => {
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="userPwForm.username"
|
||||
@@ -345,7 +328,7 @@ onMounted(() => {
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="userPwForm.password"
|
||||
@@ -359,6 +342,15 @@ onMounted(() => {
|
||||
@keydown.enter="updateSiteCookie"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="userPwForm.code"
|
||||
label="两步验证"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -374,7 +366,8 @@ onMounted(() => {
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<SiteAddEditForm
|
||||
<SiteAddEditDialog
|
||||
v-if="siteEditDialog"
|
||||
v-model="siteEditDialog"
|
||||
:siteid="cardProps.site?.id"
|
||||
@save="siteEditDialog = false; emit('update')"
|
||||
@@ -383,113 +376,17 @@ onMounted(() => {
|
||||
/>
|
||||
<!-- 站点资源弹窗 -->
|
||||
<VDialog
|
||||
v-if="resourceDialog"
|
||||
v-model="resourceDialog"
|
||||
max-width="80rem"
|
||||
scrollable
|
||||
z-index="1010"
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard :title="`浏览站点 - ${cardProps.site?.name}`">
|
||||
<DialogCloseBtn @click="resourceDialog = false" />
|
||||
<VCardText class="pt-2">
|
||||
<VDataTable
|
||||
v-model:items-per-page="resourceItemsPerPage"
|
||||
:headers="resourceHeaders"
|
||||
:items="resourceDataList"
|
||||
:items-length="resourceTotalItems"
|
||||
:search="resourceSearch"
|
||||
:loading="resourceLoading"
|
||||
density="compact"
|
||||
item-value="title"
|
||||
return-object
|
||||
fixed-header
|
||||
items-per-page-text="每页条数"
|
||||
page-text="{0}-{1} 共 {2} 条"
|
||||
>
|
||||
<template #item.title="{ item }">
|
||||
<div class="text-high-emphasis pt-1">
|
||||
{{ item.raw.title }}
|
||||
</div>
|
||||
<div class="text-sm my-1">
|
||||
{{ item.raw.description }}
|
||||
</div>
|
||||
<VChip
|
||||
v-for="(label, index) in item.raw?.labels"
|
||||
:key="index"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
color="primary"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ label }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="item.raw?.downloadvolumefactor !== 1 || item.raw?.uploadvolumefactor !== 1"
|
||||
:class="
|
||||
getVolumeFactorClass(item.raw?.downloadvolumefactor, item.raw?.uploadvolumefactor)
|
||||
"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ item.raw?.volume_factor }}
|
||||
</VChip>
|
||||
</template>
|
||||
<template #item.pubdate="{ item }">
|
||||
<div>{{ item.raw.date_elapsed }}</div>
|
||||
<div class="text-sm">
|
||||
{{ item.raw.pubdate }}
|
||||
</div>
|
||||
</template>
|
||||
<template #item.size="{ item }">
|
||||
<div class="text-nowrap whitespace-nowrap">
|
||||
{{ formatFileSize(item.raw.size) }}
|
||||
</div>
|
||||
</template>
|
||||
<template #item.seeders="{ item }">
|
||||
<div>{{ item.raw.seeders }}</div>
|
||||
</template>
|
||||
<template #item.peers="{ item }">
|
||||
<div>{{ item.raw.peers }}</div>
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="openTorrentDetail(item.raw.page_url)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-information" />
|
||||
</template>
|
||||
<VListItemTitle>查看详情</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
v-if="item.raw.enclosure?.startsWith('http')"
|
||||
variant="plain"
|
||||
@click="downloadTorrentFile(item.raw.enclosure)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-download" />
|
||||
</template>
|
||||
<VListItemTitle>下载种子文件</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
<template #no-data>
|
||||
没有数据
|
||||
</template>
|
||||
</VDataTable>
|
||||
<SiteTorrentTable :site="cardProps.site?.id" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
<script lang='ts' setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
|
||||
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
||||
import { calculateTimeDifference } from '@/@core/utils'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import type { Subscribe } from '@/api/types'
|
||||
import router from '@/router'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -55,7 +56,7 @@ function getPercentage() {
|
||||
return Math.round(
|
||||
(((props.media?.total_episode ?? 0) - (props.media?.lack_episode ?? 0))
|
||||
/ (props.media?.total_episode ?? 1))
|
||||
* 100,
|
||||
* 100,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -126,8 +127,28 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '取消订阅',
|
||||
title: '查看详情',
|
||||
value: 3,
|
||||
props: {
|
||||
prependIcon: 'mdi-open-in-new',
|
||||
click: () => {
|
||||
router.push({
|
||||
path: '/media',
|
||||
query: {
|
||||
mediaid: `${
|
||||
props.media?.tmdbid
|
||||
? `tmdb:${props.media?.tmdbid}`
|
||||
: `douban:${props.media?.doubanid}`
|
||||
}`,
|
||||
type: props.media?.type,
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '取消订阅',
|
||||
value: 4,
|
||||
props: {
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
@@ -162,7 +183,7 @@ const dropdownItems = ref([
|
||||
</template>
|
||||
<VCardTitle :class="getTextClass()">
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : "") }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||
</VCardTitle>
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
@@ -252,7 +273,8 @@ const dropdownItems = ref([
|
||||
<VIcon
|
||||
icon="mdi-download"
|
||||
class="me-1"
|
||||
/> {{ lastUpdateText }}
|
||||
/>
|
||||
{{ lastUpdateText }}
|
||||
</VCardText>
|
||||
<VProgressLinear
|
||||
v-if="getPercentage() > 0"
|
||||
@@ -262,7 +284,8 @@ const dropdownItems = ref([
|
||||
/>
|
||||
</VCard>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<SubscribeEditForm
|
||||
<SubscribeEditDialog
|
||||
v-if="subscribeEditDialog"
|
||||
v-model="subscribeEditDialog"
|
||||
:subid="props.media?.id"
|
||||
@remove="() => { emit('remove');subscribeEditDialog = false; }"
|
||||
|
||||
@@ -36,7 +36,7 @@ function goPersonDetail() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover v-bind="personProps">
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
@@ -61,7 +61,6 @@ function goPersonDetail() {
|
||||
}"
|
||||
>
|
||||
<VImg
|
||||
v-img
|
||||
:src="getPersonImage()"
|
||||
cover
|
||||
@load="isImageLoaded = true"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import type { Context } from '@/api/types'
|
||||
import type { Context, MediaInfo, TorrentInfo } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -36,6 +36,9 @@ const meta = ref(props.torrent?.meta_info)
|
||||
// 站点图标
|
||||
const siteIcon = ref('')
|
||||
|
||||
// 存储是否已经下载过的记录
|
||||
const downloaded = ref<String[]>([])
|
||||
|
||||
// 查询站点图标
|
||||
async function getSiteIcon() {
|
||||
try {
|
||||
@@ -76,7 +79,7 @@ async function handleAddDownload(_site: any = undefined,
|
||||
}
|
||||
|
||||
// 添加下载
|
||||
async function addDownload(_media: any, _torrent: any) {
|
||||
async function addDownload(_media: MediaInfo, _torrent: TorrentInfo) {
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('download/', {
|
||||
@@ -87,6 +90,7 @@ async function addDownload(_media: any, _torrent: any) {
|
||||
if (result.success) {
|
||||
// 添加下载成功
|
||||
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
|
||||
downloaded.value.push(_torrent?.enclosure || '')
|
||||
}
|
||||
else {
|
||||
// 添加下载失败
|
||||
@@ -131,6 +135,7 @@ onMounted(() => {
|
||||
<VCard
|
||||
:width="props.width"
|
||||
:height="props.height"
|
||||
:variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'elevated'"
|
||||
@click="handleAddDownload"
|
||||
>
|
||||
<template
|
||||
@@ -195,6 +200,23 @@ onMounted(() => {
|
||||
v-if="torrent?.labels"
|
||||
class="pb-3 pt-0 pe-12"
|
||||
>
|
||||
<VChip
|
||||
v-if="torrent?.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ torrent?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(label, index) in torrent?.labels"
|
||||
:key="index"
|
||||
@@ -271,7 +293,7 @@ onMounted(() => {
|
||||
<VExpandTransition>
|
||||
<div v-show="showMoreTorrents">
|
||||
<VDivider />
|
||||
<VChipGroup class="p-3">
|
||||
<VChipGroup class="p-3" column>
|
||||
<VChip
|
||||
v-for="(item, index) in props.more"
|
||||
:key="index"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import type { Context } from '@/api/types'
|
||||
import type { Context, MediaInfo, TorrentInfo } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -33,6 +33,9 @@ const meta = ref(props.torrent?.meta_info)
|
||||
// 站点图标
|
||||
const siteIcon = ref('')
|
||||
|
||||
// 存储是否已经下载过的记录
|
||||
const downloaded = ref<String[]>([])
|
||||
|
||||
// 查询站点图标
|
||||
async function getSiteIcon() {
|
||||
try {
|
||||
@@ -73,7 +76,7 @@ async function handleAddDownload(_site: any = undefined,
|
||||
}
|
||||
|
||||
// 添加下载
|
||||
async function addDownload(_media: any, _torrent: any) {
|
||||
async function addDownload(_media: MediaInfo, _torrent: TorrentInfo) {
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('download/', {
|
||||
@@ -84,6 +87,7 @@ async function addDownload(_media: any, _torrent: any) {
|
||||
if (result.success) {
|
||||
// 添加下载成功
|
||||
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
|
||||
downloaded.value.push(_torrent?.enclosure || '')
|
||||
}
|
||||
else {
|
||||
// 添加下载失败
|
||||
@@ -125,7 +129,10 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VListItem @click="handleAddDownload">
|
||||
<VListItem
|
||||
@click="handleAddDownload"
|
||||
:variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'flat'"
|
||||
>
|
||||
<template
|
||||
v-if="!showMoreTorrents"
|
||||
#prepend
|
||||
@@ -150,6 +157,23 @@ onMounted(() => {
|
||||
v-if="torrent?.labels"
|
||||
class="pt-2"
|
||||
>
|
||||
<VChip
|
||||
v-if="torrent?.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ torrent?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(label, index) in torrent?.labels"
|
||||
:key="index"
|
||||
|
||||
45
src/components/dialog/ImportCodeDialog.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
// 代码
|
||||
const codeString = ref('')
|
||||
|
||||
// 导入
|
||||
function handleImport() {
|
||||
emit('update:modelValue', codeString.value)
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
width="40rem"
|
||||
scrollable
|
||||
max-height="85vh"
|
||||
>
|
||||
<VCard
|
||||
:title="props.title"
|
||||
class="rounded-t"
|
||||
>
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VCardText class="pt-2">
|
||||
<VTextarea v-model="codeString" />
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="handleImport"
|
||||
>
|
||||
导入
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import TmdbSelectorCard from '../cards/TmdbSelectorCard.vue'
|
||||
import TmdbSelector from '../misc/TmdbSelector.vue'
|
||||
import store from '@/store'
|
||||
import api from '@/api'
|
||||
import { numberValidator } from '@/@validators'
|
||||
@@ -161,6 +161,7 @@ async function transfer() {
|
||||
v-model="transferForm.target"
|
||||
label="目的路径"
|
||||
placeholder="留空自动"
|
||||
hint="留空将自动整理到媒体库目录"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
@@ -204,6 +205,7 @@ async function transfer() {
|
||||
placeholder="留空自动识别"
|
||||
:rules="[numberValidator]"
|
||||
append-inner-icon="mdi-magnify"
|
||||
hint="点击图标按名称搜索,留空将自动重新识别"
|
||||
@click:append-inner="tmdbSelectorDialog = true"
|
||||
/>
|
||||
</VCol>
|
||||
@@ -225,6 +227,7 @@ async function transfer() {
|
||||
v-model="transferForm.episode_format"
|
||||
label="集数定位"
|
||||
placeholder="使用{ep}定位集数"
|
||||
hint="使用{ep}定位文件名中的集数部分,其余相同部分直接填写,不同部分使用{a}进行忽略,例如:{a}葬送的芙莉莲_Sousou no Frieren 第{ep}话{b}"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -232,6 +235,7 @@ async function transfer() {
|
||||
v-model="transferForm.episode_detail"
|
||||
label="指定集数"
|
||||
placeholder="起始集,终止集,如1或1,2"
|
||||
hint="直接指定集数或者范围,格式:起始集,终止集,如1或1,2"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -239,6 +243,7 @@ async function transfer() {
|
||||
v-model="transferForm.episode_part"
|
||||
label="指定Part"
|
||||
placeholder="如part1"
|
||||
hint="指定集数的Part,如part1"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -246,6 +251,7 @@ async function transfer() {
|
||||
v-model.number="transferForm.episode_offset"
|
||||
label="集数偏移"
|
||||
placeholder="如-10"
|
||||
hint="对集数进行偏移运算,如-10表示文件名中的集数减10为整理后集数"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -254,6 +260,7 @@ async function transfer() {
|
||||
label="最小文件大小(MB)"
|
||||
:rules="[numberValidator]"
|
||||
placeholder="0"
|
||||
hint="最小文件大小,小于此大小的文件将被忽略不进行整理"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -297,8 +304,9 @@ async function transfer() {
|
||||
v-model="tmdbSelectorDialog"
|
||||
width="40rem"
|
||||
scrollable
|
||||
max-height="85vh"
|
||||
>
|
||||
<TmdbSelectorCard
|
||||
<TmdbSelector
|
||||
v-model="transferForm.tmdbid"
|
||||
@close="tmdbSelectorDialog = false"
|
||||
/>
|
||||
@@ -121,6 +121,9 @@ async function updateSiteInfo() {
|
||||
<template>
|
||||
<VDialog
|
||||
scrollable
|
||||
:close-on-back="false"
|
||||
persistent
|
||||
eager
|
||||
max-width="60rem"
|
||||
>
|
||||
<VCard
|
||||
@@ -139,6 +142,7 @@ async function updateSiteInfo() {
|
||||
v-model="siteForm.url"
|
||||
label="站点地址"
|
||||
:rules="[requiredValidator]"
|
||||
hint="格式:http://www.example.com/"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
@@ -150,6 +154,7 @@ async function updateSiteInfo() {
|
||||
label="优先级"
|
||||
:items="priorityItems"
|
||||
:rules="[requiredValidator]"
|
||||
hint="站点资源下载优先级,优先级数字越小越优先下载"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
@@ -168,18 +173,21 @@ async function updateSiteInfo() {
|
||||
<VTextField
|
||||
v-model="siteForm.rss"
|
||||
label="RSS地址"
|
||||
hint="订阅模式为站点RSS时,将会使用此地址获取站点种子资源,该地址一般会自动获取,也可手动补充"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="siteForm.cookie"
|
||||
label="站点Cookie"
|
||||
hint="浏览器打开站点首页,打开开发人员工具,刷新页面后在网络选项中找到首页地址,在请求头中获取Cookie信息"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="siteForm.ua"
|
||||
label="站点User-Agent"
|
||||
hint="在开发人员工具,网络请求头中获取User-Agent信息,需与站点Cookie配套使用"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -192,6 +200,7 @@ async function updateSiteInfo() {
|
||||
v-model="siteForm.limit_interval"
|
||||
label="单位周期(秒)"
|
||||
:rules="[numberValidator]"
|
||||
hint="设定站点限流的单位周期,单位为秒,0为不限流"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
@@ -199,9 +208,10 @@ async function updateSiteInfo() {
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_seconds"
|
||||
v-model="siteForm.limit_count"
|
||||
label="访问次数"
|
||||
:rules="[numberValidator]"
|
||||
hint="设定单位周期内站点允许的访问次数,0为不限制"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
@@ -212,6 +222,7 @@ async function updateSiteInfo() {
|
||||
v-model="siteForm.limit_seconds"
|
||||
label="访问间隔(秒)"
|
||||
:rules="[numberValidator]"
|
||||
hint="设定单位周期内每次站点访问需间隔时间,单位为秒,0为不限制"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -223,6 +234,7 @@ async function updateSiteInfo() {
|
||||
<VSwitch
|
||||
v-model="siteForm.proxy"
|
||||
label="代理"
|
||||
hint="站点是否需要代理访问,需要设置好代理服务器信息"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
@@ -232,6 +244,7 @@ async function updateSiteInfo() {
|
||||
<VSwitch
|
||||
v-model="siteForm.render"
|
||||
label="仿真"
|
||||
hint="站点是否需要使用浏览器模拟访问,开启可以一定程度上提升连通性,但会大大增加站点请求时间"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -7,6 +7,8 @@ import type { Site, Subscribe } from '@/api/types'
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
subid: Number,
|
||||
default: Boolean,
|
||||
type: String,
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
@@ -30,6 +32,7 @@ const subscribeForm = ref<Subscribe>({
|
||||
total_episode: 0,
|
||||
start_episode: 0,
|
||||
best_version: 0,
|
||||
search_imdbid: 0,
|
||||
sites: [],
|
||||
type: '',
|
||||
name: '',
|
||||
@@ -39,6 +42,9 @@ const subscribeForm = ref<Subscribe>({
|
||||
last_update: '',
|
||||
username: '',
|
||||
current_priority: 0,
|
||||
save_path: '',
|
||||
date: '',
|
||||
show_edit_dialog: false
|
||||
})
|
||||
|
||||
// 提示框
|
||||
@@ -61,6 +67,50 @@ async function updateSubscribeInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
// 设置用户设置的默认订阅规则
|
||||
async function saveDefaultSubscribeConfig() {
|
||||
try {
|
||||
let subscribe_config_url = ''
|
||||
if (props.type === '电影')
|
||||
subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
|
||||
else
|
||||
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
|
||||
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
subscribe_config_url,
|
||||
subscribeForm.value)
|
||||
if (result.success)
|
||||
$toast.success(`${props.type}订阅默认规则保存成功`)
|
||||
else
|
||||
$toast.error(`${props.type}订阅默认规则保存失败!`)
|
||||
|
||||
// 通知父组件刷新
|
||||
emit('save')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询用户设置的默认订阅规则
|
||||
async function queryDefaultSubscribeConfig() {
|
||||
try {
|
||||
let subscribe_config_url = ''
|
||||
if (props.type === '电影')
|
||||
subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
|
||||
else
|
||||
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
|
||||
|
||||
const result: { [key: string]: any } = await api.get(subscribe_config_url)
|
||||
|
||||
if (result.data.value)
|
||||
subscribeForm.value = result.data?.value ?? ''
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取站点列表数据
|
||||
async function loadSites() {
|
||||
try {
|
||||
@@ -98,6 +148,7 @@ async function getSubscribeInfo() {
|
||||
)
|
||||
subscribeForm.value = result
|
||||
subscribeForm.value.best_version = subscribeForm.value.best_version === 1
|
||||
subscribeForm.value.search_imdbid = subscribeForm.value.search_imdbid === 1
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
@@ -205,11 +256,13 @@ const effectOptions = ref([
|
||||
},
|
||||
])
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.subid) {
|
||||
getSiteList()
|
||||
onMounted(() => {
|
||||
getSiteList()
|
||||
if (props.subid)
|
||||
getSubscribeInfo()
|
||||
}
|
||||
|
||||
if (props.default)
|
||||
queryDefaultSubscribeConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -219,7 +272,7 @@ watchEffect(() => {
|
||||
max-width="60rem"
|
||||
>
|
||||
<VCard
|
||||
:title="`编辑订阅 - ${subscribeForm.name} ${subscribeForm.season ? `第 ${subscribeForm.season} 季` : ''}`"
|
||||
:title="`${props.default ? `${props.type}默认订阅规则` : `编辑订阅 - ${subscribeForm.name} ${subscribeForm.season ? `第 ${subscribeForm.season} 季` : ''}`}`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<VCardText class="pt-2">
|
||||
@@ -231,8 +284,10 @@ watchEffect(() => {
|
||||
md="8"
|
||||
>
|
||||
<VTextField
|
||||
v-if="!props.default"
|
||||
v-model="subscribeForm.keyword"
|
||||
label="搜索关键词"
|
||||
hint="设定搜索关键词后,将使用此关键词搜索站点资源,否则自动使用themoviedb中的名称搜索"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
@@ -244,6 +299,7 @@ watchEffect(() => {
|
||||
v-model="subscribeForm.total_episode"
|
||||
label="总集数"
|
||||
:rules="[numberValidator]"
|
||||
hint="设定剧集的总集数,以应对themoviedb中剧集信息未维护完整,导致提前结束订阅的情况"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
@@ -255,6 +311,7 @@ watchEffect(() => {
|
||||
v-model="subscribeForm.start_episode"
|
||||
label="开始集数"
|
||||
:rules="[numberValidator]"
|
||||
hint="只订阅下载此集数及之后的剧集"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -298,6 +355,7 @@ watchEffect(() => {
|
||||
<VTextField
|
||||
v-model="subscribeForm.include"
|
||||
label="包含(关键字、正则式)"
|
||||
hint="支持正则表达式,多个关键字用 | 分隔表示或"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
@@ -307,6 +365,7 @@ watchEffect(() => {
|
||||
<VTextField
|
||||
v-model="subscribeForm.exclude"
|
||||
label="排除(关键字、正则式)"
|
||||
hint="支持正则表达式,多个关键字用 | 分隔表示或"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
@@ -319,6 +378,18 @@ watchEffect(() => {
|
||||
chips
|
||||
label="订阅站点"
|
||||
multiple
|
||||
hint="只订阅选中的订阅站点,不选则订阅所有可订阅站点"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.save_path"
|
||||
label="保存路径"
|
||||
hint="指定该订阅的下载保存路径,留空自动使用设定的下载目录"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -330,6 +401,27 @@ watchEffect(() => {
|
||||
<VSwitch
|
||||
v-model="subscribeForm.best_version"
|
||||
label="洗版"
|
||||
hint="开启后不管媒体库是否存在,均会根据洗版优先级进行过滤下载,直到下载到了最高优先级的资源为止"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="subscribeForm.search_imdbid"
|
||||
label="使用 ImdbID 搜索"
|
||||
hint="开启后将使用 ImdbID 搜索资源,搜索结果更精确,但不是所有站点都支持"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="props.default"
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="subscribeForm.show_edit_dialog"
|
||||
label="订阅时编辑更多规则"
|
||||
hint="开启后将在添加订阅后弹出编辑订阅的对话框,方便用户编辑订阅规则"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -337,13 +429,13 @@ watchEffect(() => {
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VBtn color="error" @click="removeSubscribe">
|
||||
<VBtn v-if="!props.default" color="error" @click="removeSubscribe">
|
||||
取消订阅
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="updateSubscribeInfo"
|
||||
@click="`${props.default ? saveDefaultSubscribeConfig() : updateSubscribeInfo()}`"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
239
src/components/dialog/SubscribeHistoryDialog.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api';
|
||||
import { Subscribe } from '@/api/types';
|
||||
import { formatDateDifference } from '@core/utils/formatters'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
type: String,
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['close', 'save'])
|
||||
|
||||
// 订阅历史列表
|
||||
const historyList = ref<Subscribe[]>([])
|
||||
|
||||
// 当前加载数据
|
||||
const currData = ref<Subscribe[]>([])
|
||||
|
||||
// 当前页
|
||||
const currentPage = ref(1)
|
||||
|
||||
// 每页数量
|
||||
const pageSize = ref(30)
|
||||
|
||||
// 是否加载中
|
||||
const loading = ref(false)
|
||||
|
||||
// 是否加载完成
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 进度框
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 进度文字
|
||||
const progressText = ref('正在重新订阅...')
|
||||
|
||||
// 调用API查询列表
|
||||
async function loadHistory({ done }: { done: any }) {
|
||||
// 如果正在加载中,直接返回
|
||||
if (loading.value) {
|
||||
done('ok')
|
||||
return
|
||||
}
|
||||
|
||||
// 调用API查询列表
|
||||
try {
|
||||
// 设置加载中
|
||||
loading.value = true
|
||||
currData.value = await api.get(`subscribe/history/${props.type}`, {
|
||||
params: {
|
||||
page: currentPage.value,
|
||||
count: pageSize.value,
|
||||
},
|
||||
})
|
||||
// 标计为已请求完成
|
||||
isRefreshed.value = true
|
||||
if (currData.value.length === 0) {
|
||||
// 如果没有数据,跳出
|
||||
done('empty')
|
||||
} else {
|
||||
// 合并数据
|
||||
historyList.value = [...historyList.value, ...currData.value]
|
||||
// 页码+1
|
||||
currentPage.value++
|
||||
// 返回加载成功
|
||||
done('ok')
|
||||
}
|
||||
// 取消加载中
|
||||
loading.value = false
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
// 返回加载失败
|
||||
done('error')
|
||||
}
|
||||
}
|
||||
|
||||
// 重新订阅
|
||||
async function reSubscribe(item: Subscribe) {
|
||||
if (item.type === '电影')
|
||||
progressText.value = `正在重新订阅 ${item.name} ...`
|
||||
else
|
||||
progressText.value = `正在重新订阅 ${item.name} 第 ${item.season} 季 ...`
|
||||
progressDialog.value = true
|
||||
try {
|
||||
const result: {[key: string]: any} = await api.post('subscribe', item)
|
||||
if (result.success){
|
||||
emit('save')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
progressDialog.value = false
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
async function deleteHistory(item: Subscribe) {
|
||||
try {
|
||||
const result: {[key: string]: any} = await api.delete(`subscribe/history/${item.id}`)
|
||||
if (result.success){
|
||||
historyList.value = historyList.value.filter((i) => i.id !== item.id)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '重新订阅',
|
||||
value: 1,
|
||||
color: '',
|
||||
props: {
|
||||
prependIcon: 'mdi-redo',
|
||||
click: reSubscribe,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
value: 2,
|
||||
color: 'error',
|
||||
props: {
|
||||
prependIcon: 'mdi-delete',
|
||||
click: deleteHistory,
|
||||
},
|
||||
}
|
||||
])
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
scrollable
|
||||
max-width="50rem"
|
||||
max-height="90vh"
|
||||
>
|
||||
<VCard
|
||||
class="mx-auto"
|
||||
width="100%"
|
||||
>
|
||||
<VCardItem class="pb-0">
|
||||
<VCardTitle>{{ props.type + '订阅历史' }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<DialogCloseBtn @click="() => { emit('close') }" />
|
||||
<VList
|
||||
lines="two"
|
||||
>
|
||||
<VInfiniteScroll
|
||||
mode="intersect"
|
||||
side="end"
|
||||
:items="historyList"
|
||||
class="overflow-hidden"
|
||||
@load="loadHistory"
|
||||
>
|
||||
<template #loading>
|
||||
<LoadingBanner />
|
||||
</template>
|
||||
<template #empty />
|
||||
<template v-for="(item, i) in historyList" :key="i">
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VImg
|
||||
height="75"
|
||||
width="50"
|
||||
:src="item.poster"
|
||||
aspect-ratio="2/3"
|
||||
class="object-cover rounded shadow ring-gray-500 me-3"
|
||||
cover
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</template>
|
||||
<VListItemTitle v-if="item.type == '电视剧'">
|
||||
{{ item.name }} <span class="text-sm">第 {{ item.season }} 季</span>
|
||||
</VListItemTitle>
|
||||
<VListItemTitle v-else>
|
||||
{{ item.name }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle class="mt-2">{{ formatDateDifference(item.date) }}</VListItemSubtitle>
|
||||
<VListItemSubtitle class="mt-2">{{ item.description }}</VListItemSubtitle>
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.color"
|
||||
@click="menu.props.click(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VInfiniteScroll>
|
||||
</VList>
|
||||
</VCard>
|
||||
<!-- 进度框 -->
|
||||
<VDialog
|
||||
v-model="progressDialog"
|
||||
:scrim="false"
|
||||
width="25rem"
|
||||
>
|
||||
<VCard
|
||||
color="primary"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
{{ progressText }}
|
||||
<VProgressLinear
|
||||
indeterminate
|
||||
color="white"
|
||||
class="mb-0 mt-1"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</VDialog>
|
||||
</template>
|
||||
@@ -4,7 +4,7 @@ import type { PropType } from 'vue'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import axios from 'axios'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import ReorganizeForm from '../form/ReorganizeForm.vue'
|
||||
import ReorganizeDialog from '../dialog/ReorganizeDialog.vue'
|
||||
import { formatBytes } from '@core/utils/formatters'
|
||||
import type { Context, EndPoints, FileItem } from '@/api/types'
|
||||
import store from '@/store'
|
||||
@@ -73,6 +73,9 @@ const nameTestResult = ref<Context>()
|
||||
// 识别结果对话框
|
||||
const nameTestDialog = ref(false)
|
||||
|
||||
// 延迟加载
|
||||
const defer = (_: number) => true
|
||||
|
||||
// 目录过滤
|
||||
const dirs = computed(() =>
|
||||
items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)),
|
||||
@@ -267,6 +270,28 @@ async function recognize(path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API刮削
|
||||
async function scrape(path: string) {
|
||||
try {
|
||||
// 显示进度条
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在刮削 ${path} ...`
|
||||
const result: { [key: string]: any } = await api.get('media/scrape', {
|
||||
params: {
|
||||
path,
|
||||
},
|
||||
})
|
||||
// 关闭进度条
|
||||
progressDialog.value = false
|
||||
if (!result.success)
|
||||
$toast.error(result.message)
|
||||
else
|
||||
$toast.success(`${path}削刮完成!`)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
@@ -279,8 +304,17 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
}, {
|
||||
title: '重命名',
|
||||
title: '刮削',
|
||||
value: 2,
|
||||
props: {
|
||||
prependIcon: 'mdi-auto-fix',
|
||||
click: (_item: FileItem) => {
|
||||
scrape(_item.path || '')
|
||||
},
|
||||
},
|
||||
}, {
|
||||
title: '重命名',
|
||||
value: 3,
|
||||
props: {
|
||||
prependIcon: 'mdi-rename',
|
||||
click: showRenmae,
|
||||
@@ -288,7 +322,7 @@ const dropdownItems = ref([
|
||||
},
|
||||
{
|
||||
title: '整理',
|
||||
value: 3,
|
||||
value: 4,
|
||||
props: {
|
||||
prependIcon: 'mdi-folder-arrow-right',
|
||||
click: showTransfer,
|
||||
@@ -296,7 +330,7 @@ const dropdownItems = ref([
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
value: 4,
|
||||
value: 5,
|
||||
props: {
|
||||
prependIcon: 'mdi-delete-outline',
|
||||
color: 'error',
|
||||
@@ -312,6 +346,36 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<VCard class="d-flex flex-column">
|
||||
<VToolbar v-if="!loading" density="compact" flat color="gray">
|
||||
<VTextField
|
||||
v-if="!isFile"
|
||||
v-model="filter"
|
||||
hide-details
|
||||
flat
|
||||
density="compact"
|
||||
variant="solo-filled"
|
||||
placeholder="搜索 ..."
|
||||
prepend-inner-icon="mdi-filter-outline"
|
||||
class="me-2"
|
||||
rounded="0"
|
||||
/>
|
||||
<VSpacer v-if="isFile" />
|
||||
<IconBtn v-if="isFile" @click="recognize(inProps.path || '')">
|
||||
<VIcon color="primary">
|
||||
mdi-text-recognition
|
||||
</VIcon>
|
||||
</IconBtn>
|
||||
<IconBtn v-if="isFile" @click="download(inProps.path || '')">
|
||||
<VIcon color="primary">
|
||||
mdi-download
|
||||
</VIcon>
|
||||
</IconBtn>
|
||||
<IconBtn v-if="!isFile" @click="load">
|
||||
<VIcon color="primary">
|
||||
mdi-refresh
|
||||
</VIcon>
|
||||
</IconBtn>
|
||||
</VToolbar>
|
||||
<VCardText
|
||||
v-if="loading"
|
||||
class="text-center flex flex-col items-center"
|
||||
@@ -343,113 +407,92 @@ onMounted(() => {
|
||||
<VImg :src="getImgLink(path)" max-width="100%" max-height="100%" />
|
||||
</VCardText>
|
||||
<VCardText v-else-if="dirs.length || files.length" class="p-0">
|
||||
<VList v-if="dirs.length" subheader>
|
||||
<VListSubheader>目录</VListSubheader>
|
||||
<VListItem
|
||||
v-for="(item, index) in dirs"
|
||||
:key="index"
|
||||
class="px-3 pe-1"
|
||||
@click="changePath(item.path)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-folder-outline" />
|
||||
<VList subheader>
|
||||
<VVirtualScroll class="virtual-scroll-div" :items="[...dirs, ...files]">
|
||||
<template #default="{ item }">
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
v-bind="hover.props"
|
||||
class="px-3 pe-1"
|
||||
@click="changePath(item.path)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon v-if="inProps.icons && item.extension" :icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other" />
|
||||
<VIcon v-else icon="mdi-folder-outline" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.name" />
|
||||
<VListItemSubtitle v-if="item.size">
|
||||
{{ formatBytes(item.size) }}
|
||||
</VListItemSubtitle>
|
||||
<template #append>
|
||||
<IconBtn class="d-sm-none">
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click="menu.props.click(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<span v-if="hover.isHovering" class="flex">
|
||||
<VTooltip text="识别">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
||||
<VIcon icon="mdi-text-recognition" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip text="刮削">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="scrape(item.path)">
|
||||
<VIcon icon="mdi-auto-fix" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip text="重命名">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
||||
<VIcon icon="mdi-rename" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip text="整理">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
||||
<VIcon icon="mdi-folder-arrow-right" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip text="删除">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
||||
<VIcon icon="mdi-delete-outline" color="error" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
</span>
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
<VListItemTitle v-text="item.name" />
|
||||
<template #append>
|
||||
<IconBtn class="d-sm-none">
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click="menu.props.click(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
||||
<VIcon icon="mdi-text-recognition" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
||||
<VIcon icon="mdi-rename" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
||||
<VIcon icon="mdi-folder-arrow-right" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
||||
<VIcon icon="mdi-delete-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<VDivider v-if="dirs.length && files.length" />
|
||||
<VList v-if="files.length" subheader>
|
||||
<VListSubheader>文件</VListSubheader>
|
||||
<VListItem
|
||||
v-for="(item, index) in files"
|
||||
:key="index"
|
||||
class="pl-3 pe-1"
|
||||
@click="changePath(item.path)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon v-if="inProps.icons" :icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other" />
|
||||
</template>
|
||||
|
||||
<VListItemTitle v-text="item.name" />
|
||||
<VListItemSubtitle> {{ formatBytes(item.size) }}</VListItemSubtitle>
|
||||
|
||||
<template #append>
|
||||
<IconBtn class="d-sm-none">
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click="menu.props.click(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
||||
<VIcon icon="mdi-text-recognition" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
||||
<VIcon icon="mdi-rename" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
||||
<VIcon icon="mdi-folder-arrow-right" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
||||
<VIcon icon="mdi-delete-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VVirtualScroll>
|
||||
</VList>
|
||||
</VCardText>
|
||||
<VCardText
|
||||
@@ -464,39 +507,10 @@ onMounted(() => {
|
||||
>
|
||||
空目录
|
||||
</VCardText>
|
||||
<VDivider v-if="path" />
|
||||
<VToolbar v-if="!loading" density="compact" flat color="gray">
|
||||
<VTextField
|
||||
v-if="!isFile"
|
||||
v-model="filter"
|
||||
hide-details
|
||||
flat
|
||||
density="compact"
|
||||
variant="solo-filled"
|
||||
placeholder="搜索 ..."
|
||||
prepend-inner-icon="mdi-filter-outline"
|
||||
class="me-2"
|
||||
/>
|
||||
<VSpacer v-if="isFile" />
|
||||
<IconBtn v-if="isFile" @click="recognize(inProps.path || '')">
|
||||
<VIcon color="primary">
|
||||
mdi-text-recognition
|
||||
</VIcon>
|
||||
</IconBtn>
|
||||
<IconBtn v-if="isFile" @click="download(inProps.path || '')">
|
||||
<VIcon color="primary">
|
||||
mdi-download
|
||||
</VIcon>
|
||||
</IconBtn>
|
||||
<IconBtn v-if="!isFile" @click="load">
|
||||
<VIcon color="primary">
|
||||
mdi-refresh
|
||||
</VIcon>
|
||||
</IconBtn>
|
||||
</VToolbar>
|
||||
</VCard>
|
||||
<!-- 重命名弹窗 -->
|
||||
<VDialog
|
||||
v-if="renamePopper"
|
||||
v-model="renamePopper"
|
||||
max-width="50rem"
|
||||
>
|
||||
@@ -521,7 +535,8 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 文件整理弹窗 -->
|
||||
<ReorganizeForm
|
||||
<ReorganizeDialog
|
||||
v-if="transferPopper"
|
||||
v-model="transferPopper"
|
||||
:path="currentItem?.path"
|
||||
@done="transferPopper = false; load()"
|
||||
@@ -549,6 +564,7 @@ onMounted(() => {
|
||||
</VDialog>
|
||||
<!-- 识别结果对话框 -->
|
||||
<VDialog
|
||||
v-if="nameTestDialog"
|
||||
v-model="nameTestDialog"
|
||||
width="50rem"
|
||||
>
|
||||
@@ -563,9 +579,21 @@ onMounted(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-card {
|
||||
height: 100%;
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
.v-toolbar{
|
||||
background: rgb(var(--v-table-header-background));
|
||||
}
|
||||
|
||||
.virtual-scroll-div {
|
||||
block-size: calc(100vh - 14rem);
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.virtual-scroll-div {
|
||||
block-size: calc(100vh - 17rem);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -144,19 +144,31 @@ const sortIcon = computed(() => {
|
||||
</template>
|
||||
</VToolbarItems>
|
||||
<div class="flex-grow-1" />
|
||||
<IconBtn @click="changeSort">
|
||||
<VIcon :icon="sortIcon" />
|
||||
</IconBtn>
|
||||
<IconBtn v-if="pathSegments.length > 0" @click="goUp">
|
||||
<VIcon icon="mdi-arrow-up-bold-outline" />
|
||||
</IconBtn>
|
||||
<VTooltip text="调整排序">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" @click="changeSort">
|
||||
<VIcon :icon="sortIcon" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip text="返回上一级" v-if="pathSegments.length > 0">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn v-bind="props" @click="goUp">
|
||||
<VIcon icon="mdi-arrow-up-bold-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VDialog
|
||||
v-model="newFolderPopper"
|
||||
max-width="50rem"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<IconBtn title="新建文件夹" v-bind="props">
|
||||
<VIcon icon="mdi-folder-plus-outline" />
|
||||
<IconBtn v-bind="props">
|
||||
<VTooltip text="新建文件夹">
|
||||
<template #activator="{ props: _props }">
|
||||
<VIcon v-bind="_props" icon="mdi-folder-plus-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCard title="新建文件夹">
|
||||
@@ -1,203 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Axios } from 'axios'
|
||||
import type { EndPoints, FileItem } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
icons: Object,
|
||||
storage: String,
|
||||
path: String,
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
axios: Object as PropType<Axios>,
|
||||
refreshpending: Boolean,
|
||||
})
|
||||
|
||||
// 对外事件
|
||||
const emit = defineEmits(['pathchanged', 'loading', 'refreshed'])
|
||||
|
||||
// 变量
|
||||
const open = ref<string[]>([])
|
||||
// 活跃的文件夹
|
||||
const active = ref<string[]>([])
|
||||
// 内容
|
||||
const items = ref<FileItem[]>([])
|
||||
// 过滤
|
||||
const filter = ref('')
|
||||
|
||||
// 方法
|
||||
function init() {
|
||||
open.value = []
|
||||
items.value = [{
|
||||
type: 'dir',
|
||||
path: '/',
|
||||
basename: 'root',
|
||||
extension: '',
|
||||
name: 'root',
|
||||
children: [],
|
||||
size: 0,
|
||||
modify_time: 0,
|
||||
}]
|
||||
}
|
||||
|
||||
// 调用API读取文件夹
|
||||
async function readFolder(item: FileItem) {
|
||||
emit('loading', true)
|
||||
const url = props.endpoints?.list.url
|
||||
.replace(/{storage}/g, props.storage)
|
||||
.replace(/{path}/g, item.path)
|
||||
|
||||
const config = {
|
||||
url,
|
||||
method: props.endpoints?.list.method || 'get',
|
||||
}
|
||||
|
||||
const response: FileItem[] = await props.axios?.request(config) ?? []
|
||||
|
||||
item.children = response.map((item: FileItem) => {
|
||||
if (item.type === 'dir')
|
||||
item.children = []
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
emit('loading', false)
|
||||
}
|
||||
|
||||
// 选中变化
|
||||
function activeChanged(_active: string[]) {
|
||||
let path = ''
|
||||
if (active.value.length)
|
||||
path = active.value[0]
|
||||
|
||||
if (props.path !== path)
|
||||
emit('pathchanged', path)
|
||||
}
|
||||
|
||||
// 查找文件
|
||||
function findItem(path: string) {
|
||||
const stack: FileItem[] = []
|
||||
stack.push(items.value[0])
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()
|
||||
if (node?.path === path) {
|
||||
return node
|
||||
}
|
||||
else if (node?.children && node.children.length) {
|
||||
for (const element of node.children)
|
||||
stack.push(element)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 监听存储空间变量
|
||||
watch(() => props.storage, () => {
|
||||
init()
|
||||
})
|
||||
|
||||
// 监听路径变化
|
||||
watch(
|
||||
() => props.path,
|
||||
() => {
|
||||
if (props.path) {
|
||||
active.value = [props.path]
|
||||
if (!open.value.includes(props.path))
|
||||
open.value.push(props.path)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 refreshPending
|
||||
watch(
|
||||
() => props.refreshpending,
|
||||
async () => {
|
||||
if (props.refreshpending && props.path) {
|
||||
const item = findItem(props.path)
|
||||
if (item) {
|
||||
await readFolder(item)
|
||||
emit('refreshed')
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard flat width="250" min-height="500" class="d-flex flex-column folders-tree-card">
|
||||
<div class="grow scroll-x">
|
||||
<VTreeview
|
||||
:open="open"
|
||||
:active="active"
|
||||
:items="items"
|
||||
:search="filter"
|
||||
:load-children="readFolder"
|
||||
item-key="path"
|
||||
item-text="basename"
|
||||
dense
|
||||
activatable
|
||||
transition
|
||||
class="folders-tree"
|
||||
@update:active="activeChanged"
|
||||
>
|
||||
<template #prepend="{ item, open }">
|
||||
<VIcon
|
||||
v-if="item.type === 'dir'"
|
||||
>
|
||||
{{ open ? 'mdi-folder-open-outline' : 'mdi-folder-outline' }}
|
||||
</VIcon>
|
||||
<VIcon v-else-if="props.icons" :icon="props.icons[item.extension.toLowerCase()] || props.icons.other" />
|
||||
</template>
|
||||
<template #label="{ item }">
|
||||
{{ item.basename }}
|
||||
<VBtn
|
||||
v-if="item.type === 'dir'"
|
||||
icon
|
||||
class="ml-1"
|
||||
@click.stop="readFolder(item)"
|
||||
>
|
||||
<VIcon class="pa-0 mdi-18px" color="grey lighten-1">
|
||||
mdi-refresh
|
||||
</VIcon>
|
||||
</VBtn>
|
||||
</template>
|
||||
</VTreeview>
|
||||
</div>
|
||||
<VDivider />
|
||||
<VToolbar
|
||||
density="compact"
|
||||
>
|
||||
<VBtn icon @click="init">
|
||||
<VIcon icon="mdi-collapse-all-outline" />
|
||||
</VBtn>
|
||||
</VToolbar>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.folders-tree-card {
|
||||
height: 100%;
|
||||
|
||||
.scroll-x {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
::v-deep .folders-tree {
|
||||
width: fit-content;
|
||||
min-width: 250px;
|
||||
|
||||
.v-treeview-node {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.v-toolbar{
|
||||
background: rgb(var(--v-table-header-background));
|
||||
}
|
||||
</style>
|
||||
@@ -1,39 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
// 代码
|
||||
const codeString = ref('')
|
||||
|
||||
// 导入
|
||||
function handleImport() {
|
||||
emit('update:modelValue', codeString.value)
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
:title="props.title"
|
||||
class="rounded-t"
|
||||
>
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VCardText class="pt-2">
|
||||
<VTextarea v-model="codeString" />
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="handleImport"
|
||||
>
|
||||
导入
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -93,7 +93,7 @@ onMounted(() => {
|
||||
single-line
|
||||
placeholder="电影或电视剧名称"
|
||||
variant="solo"
|
||||
append-inner-icon="mdi-magnify"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
flat
|
||||
class="mx-1"
|
||||
:loading="loading"
|
||||
@@ -101,7 +101,7 @@ onMounted(() => {
|
||||
@keydown.enter="searchMedias"
|
||||
/>
|
||||
</VToolbar>
|
||||
|
||||
<DialogCloseBtn @click="() => { emit('close') }" />
|
||||
<VList
|
||||
v-if="items.length > 0"
|
||||
lines="three"
|
||||
@@ -131,7 +131,6 @@ onMounted(() => {
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle class="mt-2" v-html="item.overview" />
|
||||
</VListItem>
|
||||
<VDivider v-if="i < items.length - 1" class="mt-1" inset />
|
||||
</template>
|
||||
</VList>
|
||||
</VCard>
|
||||
24
src/components/misc/VersionHistory.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
history: Object as PropType<{ [key: string]: string }>,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCardItem>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(value, key) in props.history"
|
||||
:key="key"
|
||||
>
|
||||
<VListItemTitle>{{ key }}</VListItemTitle>
|
||||
<div class="text-gray-500">
|
||||
{{ value }}
|
||||
</div>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardItem>
|
||||
</template>
|
||||
@@ -32,23 +32,58 @@ const formData = ref<any>(elementProps.form || {})
|
||||
<template>
|
||||
<Component
|
||||
:is="formItem.component"
|
||||
v-if="!formItem.html"
|
||||
v-if="!formItem.html && !!formItem.props?.modelvalue"
|
||||
v-bind="formItem.props"
|
||||
v-model="formData[formItem.props?.model || '']"
|
||||
v-model:value="formData[formItem.props?.modelvalue]"
|
||||
>
|
||||
{{ formItem.text }}
|
||||
<FormRender
|
||||
<template
|
||||
v-for="(innerItem, innerIndex) in (formItem.content || [])"
|
||||
:key="innerIndex"
|
||||
v-model="formData[innerItem.props?.model || '']"
|
||||
:config="innerItem"
|
||||
:form="formData"
|
||||
/>
|
||||
>
|
||||
<FormRender
|
||||
v-if="!!innerItem.props?.modelvalue"
|
||||
v-model:value="formData[innerItem.props?.modelvalue]"
|
||||
:config="innerItem"
|
||||
:form="formData"
|
||||
/>
|
||||
<FormRender
|
||||
v-else
|
||||
v-model="formData[innerItem.props?.model]"
|
||||
:config="innerItem"
|
||||
:form="formData"
|
||||
/>
|
||||
</template>
|
||||
</Component>
|
||||
<Component
|
||||
:is="formItem.component"
|
||||
v-if="formItem.html"
|
||||
v-else-if="formItem.html"
|
||||
v-bind="formItem.props"
|
||||
v-html="formItem.html"
|
||||
/>
|
||||
<Component
|
||||
:is="formItem.component"
|
||||
v-else
|
||||
v-bind="formItem.props"
|
||||
v-model="formData[formItem.props?.model]"
|
||||
>
|
||||
{{ formItem.text }}
|
||||
<template
|
||||
v-for="(innerItem, innerIndex) in (formItem.content || [])"
|
||||
:key="innerIndex"
|
||||
>
|
||||
<FormRender
|
||||
v-if="!!innerItem.props?.modelvalue"
|
||||
v-model:value="formData[innerItem.props?.modelvalue]"
|
||||
:config="innerItem"
|
||||
:form="formData"
|
||||
/>
|
||||
<FormRender
|
||||
v-else
|
||||
v-model="formData[innerItem.props?.model]"
|
||||
:config="innerItem"
|
||||
:form="formData"
|
||||
/>
|
||||
</template>
|
||||
</Component>
|
||||
</template>
|
||||
|
||||
@@ -7,6 +7,7 @@ interface RenderProps {
|
||||
text: string
|
||||
html: string
|
||||
content?: any
|
||||
slots?: any
|
||||
props?: any
|
||||
}
|
||||
|
||||
@@ -22,6 +23,7 @@ const formItem = ref<RenderProps>(elementProps.config ?? {
|
||||
html: '',
|
||||
props: {},
|
||||
content: [],
|
||||
slots: {},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -32,6 +34,15 @@ const formItem = ref<RenderProps>(elementProps.config ?? {
|
||||
v-bind="formItem.props"
|
||||
>
|
||||
{{ formItem.text }}
|
||||
<template v-for="(content, name) in (formItem.slots || [])" :key="name" v-slot:[name]="{_props}">
|
||||
<slot :name="name" v-bind="_props">
|
||||
<PageRender
|
||||
v-for="(slotItem, slotIndex) in (content || [])"
|
||||
:key="slotIndex"
|
||||
:config="slotItem"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
<PageRender
|
||||
v-for="(innerItem, innerIndex) in (formItem.content || [])"
|
||||
:key="innerIndex"
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import SlideViewTitle from '@/components/slide/SlideViewTitle.vue'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
linkurl: String,
|
||||
title: String,
|
||||
})
|
||||
|
||||
// 元素
|
||||
const slideview_content = ref()
|
||||
// 分页切换状态
|
||||
@@ -95,7 +89,7 @@ onActivated(() => {
|
||||
<template>
|
||||
<div class="flex justify-between mt-3">
|
||||
<slot name="title">
|
||||
<SlideViewTitle v-bind="props" />
|
||||
<SlideViewTitle />
|
||||
</slot>
|
||||
<div v-if="disabled !== 3" class="me-1 d-none d-md-flex">
|
||||
<VBtn
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
linkurl: String,
|
||||
title: String,
|
||||
})
|
||||
const props = inject('rankingPropsKey')
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
245
src/components/table/SiteTorrentTable.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import api from '@/api'
|
||||
import type { TorrentInfo } from '@/api/types'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import { formatFileSize } from '@core/utils/formatters'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
site: Number,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 数据列表
|
||||
const resourceDataList = ref<TorrentInfo[]>([])
|
||||
|
||||
// 搜索
|
||||
const resourceSearch = ref('')
|
||||
|
||||
// 总条数
|
||||
const resourceTotalItems = ref(0)
|
||||
|
||||
// 每页条数
|
||||
const resourceItemsPerPage = ref(25)
|
||||
|
||||
// 加载状态
|
||||
const resourceLoading = ref(false)
|
||||
|
||||
// 资源浏览表头
|
||||
const resourceHeaders = [
|
||||
{ title: '标题', key: 'title', sortable: false },
|
||||
{ title: '时间', key: 'pubdate', sortable: true },
|
||||
{ title: '大小', key: 'size', sortable: true },
|
||||
{ title: '做种', key: 'seeders', sortable: true },
|
||||
{ title: '下载', key: 'peers', sortable: true },
|
||||
{ title: '', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
// 打开种子详情页面
|
||||
function openTorrentDetail(page_url: string) {
|
||||
window.open(page_url, '_blank')
|
||||
}
|
||||
|
||||
// 下载种子文件
|
||||
async function downloadTorrentFile(enclosure: string) {
|
||||
window.open(enclosure, '_blank')
|
||||
}
|
||||
|
||||
// 调用API,查询站点资源
|
||||
async function getResourceList() {
|
||||
resourceLoading.value = true
|
||||
try {
|
||||
resourceDataList.value = await api.get(`site/resource/${props.site}`)
|
||||
resourceLoading.value = false
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 促销Chip类
|
||||
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||
if (downloadVolume === 0)
|
||||
return 'text-white bg-lime-500'
|
||||
else if (downloadVolume < 1)
|
||||
return 'text-white bg-green-500'
|
||||
else if (uploadVolume !== 1)
|
||||
return 'text-white bg-sky-500'
|
||||
else
|
||||
return 'text-white bg-gray-500'
|
||||
}
|
||||
|
||||
// 添加下载
|
||||
async function addDownload(_torrent: any) {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认下载【${_torrent.site_name}】${_torrent?.title} ?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (!isConfirmed)
|
||||
return
|
||||
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('download/add', _torrent)
|
||||
|
||||
if (result.success) {
|
||||
// 添加下载成功
|
||||
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
|
||||
}
|
||||
else {
|
||||
// 添加下载失败
|
||||
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败:${result.message || '未知错误'}`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 装载时查询站点图标
|
||||
onMounted(() => {
|
||||
getResourceList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDataTable
|
||||
v-model:items-per-page="resourceItemsPerPage"
|
||||
:headers="resourceHeaders"
|
||||
:items="resourceDataList"
|
||||
:items-length="resourceTotalItems"
|
||||
:search="resourceSearch"
|
||||
:loading="resourceLoading"
|
||||
density="compact"
|
||||
item-value="title"
|
||||
return-object
|
||||
fixed-header
|
||||
hover
|
||||
items-per-page-text="每页条数"
|
||||
page-text="{0}-{1} 共 {2} 条"
|
||||
loading-text="加载中..."
|
||||
>
|
||||
<template #item.title="{ item }">
|
||||
<a href="javascript:void(0)" @click.stop="addDownload(item)">
|
||||
<div class="text-high-emphasis pt-1">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="text-sm my-1">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
<VChip
|
||||
v-if="item.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="item.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ item.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(label, index) in item.labels"
|
||||
:key="index"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
color="primary"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ label }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="item.downloadvolumefactor !== 1 || item.uploadvolumefactor !== 1"
|
||||
:class="
|
||||
getVolumeFactorClass(item.downloadvolumefactor, item.uploadvolumefactor)
|
||||
"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ item.volume_factor }}
|
||||
</VChip>
|
||||
</a>
|
||||
</template>
|
||||
<template #item.pubdate="{ item }">
|
||||
<div>{{ item.date_elapsed }}</div>
|
||||
<div class="text-sm">
|
||||
{{ item.pubdate }}
|
||||
</div>
|
||||
</template>
|
||||
<template #item.size="{ item }">
|
||||
<div class="text-nowrap whitespace-nowrap">
|
||||
{{ formatFileSize(item.size) }}
|
||||
</div>
|
||||
</template>
|
||||
<template #item.seeders="{ item }">
|
||||
<div>{{ item.seeders }}</div>
|
||||
</template>
|
||||
<template #item.peers="{ item }">
|
||||
<div>{{ item.peers }}</div>
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="openTorrentDetail(item.page_url || '')"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-information" />
|
||||
</template>
|
||||
<VListItemTitle>查看详情</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
v-if="item.enclosure?.startsWith('http')"
|
||||
variant="plain"
|
||||
@click="downloadTorrentFile(item.enclosure)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-download" />
|
||||
</template>
|
||||
<VListItemTitle>下载种子文件</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
<template #no-data>
|
||||
没有数据
|
||||
</template>
|
||||
</VDataTable>
|
||||
</template>
|
||||
@@ -44,7 +44,7 @@ const superUser = store.state.auth.superUser
|
||||
</IconBtn>
|
||||
|
||||
<!-- 👉 Shortcuts -->
|
||||
<ShortcutBar />
|
||||
<ShortcutBar v-if="superUser" />
|
||||
|
||||
<!-- 👉 Theme -->
|
||||
<NavbarThemeSwitcher class="me-2" />
|
||||
|
||||
@@ -14,6 +14,10 @@ const themes: ThemeSwitcherTheme[] = [
|
||||
name: 'purple',
|
||||
icon: 'mdi-brightness-4',
|
||||
},
|
||||
{
|
||||
name: 'auto',
|
||||
icon: 'mdi-brightness-auto',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ function openSearchDialog() {
|
||||
<VTextField
|
||||
key="search_navbar"
|
||||
v-model="searchWord"
|
||||
class="d-none d-lg-block text-disabled"
|
||||
class="d-none d-lg-block text-disabled search-box"
|
||||
density="compact"
|
||||
variant="solo"
|
||||
label="搜索电影、电视剧"
|
||||
@@ -98,3 +98,9 @@ function openSearchDialog() {
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.search-box div.v-input__control div[role="textbox"] {
|
||||
border: 1px solid rgb(var(--v-theme-background));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,10 @@ import NameTestView from '@/views/system/NameTestView.vue'
|
||||
import NetTestView from '@/views/system/NetTestView.vue'
|
||||
import LoggingView from '@/views/system/LoggingView.vue'
|
||||
import RuleTestView from '@/views/system/RuleTestView.vue'
|
||||
import ModuleTestView from '@/views/system/ModuleTestView.vue'
|
||||
import MessageView from '@/views/system/MessageView.vue'
|
||||
import store from '@/store'
|
||||
import api from '@/api'
|
||||
|
||||
// App捷径
|
||||
const appsMenu = ref(false)
|
||||
@@ -18,6 +22,57 @@ const loggingDialog = ref(false)
|
||||
|
||||
// 过滤规则弹窗
|
||||
const ruleTestDialog = ref(false)
|
||||
|
||||
// 系统健康检查弹窗
|
||||
const systemTestDialog = ref(false)
|
||||
|
||||
// 消息中心弹窗
|
||||
const messageDialog = ref(false)
|
||||
|
||||
// 输入消息
|
||||
const user_message = ref('')
|
||||
|
||||
// 发送按钮是否可用
|
||||
const sendButtonDisabled = ref(false)
|
||||
|
||||
// 聊天容器
|
||||
const chatContainer = ref<HTMLDivElement>()
|
||||
|
||||
// 滚动到底部
|
||||
function scrollMessageToEnd() {
|
||||
nextTick(() => {
|
||||
if (chatContainer.value) {
|
||||
const scrollDiv = chatContainer.value.$el
|
||||
scrollDiv.scrollTop = scrollDiv.scrollHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 拼接全部日志url
|
||||
function allLoggingUrl() {
|
||||
const token = store.state.auth.token
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}&length=-1`
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
async function sendMessage() {
|
||||
if (user_message.value) {
|
||||
try {
|
||||
sendButtonDisabled.value = true
|
||||
await api.post(`message/web?text=${user_message.value}`)
|
||||
user_message.value = ''
|
||||
sendButtonDisabled.value = false
|
||||
scrollMessageToEnd()
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
scrollMessageToEnd()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -74,23 +129,23 @@ const ruleTestDialog = ref(false)
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="6"
|
||||
class="text-center cursor-pointer pa-0 shortcut-icon"
|
||||
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
|
||||
@click="() => {}"
|
||||
>
|
||||
<VListItem
|
||||
class="pa-4"
|
||||
@click="netTestDialog = true"
|
||||
@click="ruleTestDialog = true"
|
||||
>
|
||||
<VAvatar
|
||||
size="48"
|
||||
variant="tonal"
|
||||
>
|
||||
<VIcon icon="mdi-network-outline" />
|
||||
<VIcon icon="mdi-filter-cog-outline" />
|
||||
</VAvatar>
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||
网络
|
||||
优先级
|
||||
</h6>
|
||||
<span class="text-sm">测试网速连通性</span>
|
||||
<span class="text-sm">优先级规则测试</span>
|
||||
</VListItem>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -113,7 +168,51 @@ const ruleTestDialog = ref(false)
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||
日志
|
||||
</h6>
|
||||
<span class="text-sm">系统实时日志</span>
|
||||
<span class="text-sm">实时日志</span>
|
||||
</VListItem>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="6"
|
||||
class="text-center cursor-pointer pa-0 shortcut-icon"
|
||||
@click="() => {}"
|
||||
>
|
||||
<VListItem
|
||||
class="pa-4"
|
||||
@click="netTestDialog = true"
|
||||
>
|
||||
<VAvatar
|
||||
size="48"
|
||||
variant="tonal"
|
||||
>
|
||||
<VIcon icon="mdi-network-outline" />
|
||||
</VAvatar>
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||
网络
|
||||
</h6>
|
||||
<span class="text-sm">网速连通性测试</span>
|
||||
</VListItem>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow class="ma-0 mt-n1 border-t">
|
||||
<VCol
|
||||
cols="6"
|
||||
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
|
||||
@click="() => {}"
|
||||
>
|
||||
<VListItem
|
||||
class="pa-4"
|
||||
@click="systemTestDialog = true"
|
||||
>
|
||||
<VAvatar
|
||||
size="48"
|
||||
variant="tonal"
|
||||
>
|
||||
<VIcon icon="mdi-cog-outline" />
|
||||
</VAvatar>
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||
系统
|
||||
</h6>
|
||||
<span class="text-sm">健康检查</span>
|
||||
</VListItem>
|
||||
</VCol>
|
||||
<VCol
|
||||
@@ -123,18 +222,18 @@ const ruleTestDialog = ref(false)
|
||||
>
|
||||
<VListItem
|
||||
class="pa-4"
|
||||
@click="ruleTestDialog = true"
|
||||
@click="messageDialog = true"
|
||||
>
|
||||
<VAvatar
|
||||
size="48"
|
||||
variant="tonal"
|
||||
>
|
||||
<VIcon icon="mdi-filter-cog-outline" />
|
||||
<VIcon icon="mdi-message-outline" />
|
||||
</VAvatar>
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||
优先级
|
||||
消息
|
||||
</h6>
|
||||
<span class="text-sm">优先级规则测试</span>
|
||||
<span class="text-sm">消息中心</span>
|
||||
</VListItem>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -143,6 +242,7 @@ const ruleTestDialog = ref(false)
|
||||
</VMenu>
|
||||
<!-- 名称测试弹窗 -->
|
||||
<VDialog
|
||||
v-if="nameTestDialog"
|
||||
v-model="nameTestDialog"
|
||||
max-width="50rem"
|
||||
>
|
||||
@@ -155,6 +255,7 @@ const ruleTestDialog = ref(false)
|
||||
</VDialog>
|
||||
<!-- 网络测试弹窗 -->
|
||||
<VDialog
|
||||
v-if="netTestDialog"
|
||||
v-model="netTestDialog"
|
||||
max-width="35rem"
|
||||
>
|
||||
@@ -167,12 +268,24 @@ const ruleTestDialog = ref(false)
|
||||
</VDialog>
|
||||
<!-- 实时日志弹窗 -->
|
||||
<VDialog
|
||||
v-if="loggingDialog"
|
||||
v-model="loggingDialog"
|
||||
class="w-full lg:w-4/5"
|
||||
scrollable
|
||||
>
|
||||
<VCard title="实时日志">
|
||||
<VCard>
|
||||
<DialogCloseBtn @click="loggingDialog = false" />
|
||||
<VCardItem>
|
||||
<VCardTitle class="inline-flex">
|
||||
实时日志
|
||||
<a class="mx-2 inline-flex items-center justify-center" :href="allLoggingUrl()" target="_blank">
|
||||
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700">
|
||||
<VIcon icon="mdi-open-in-new" />
|
||||
<span class="ms-1">在新窗口中打开</span>
|
||||
</div>
|
||||
</a>
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<LoggingView />
|
||||
</VCardText>
|
||||
@@ -180,6 +293,7 @@ const ruleTestDialog = ref(false)
|
||||
</VDialog>
|
||||
<!-- 规则测试弹窗 -->
|
||||
<VDialog
|
||||
v-if="ruleTestDialog"
|
||||
v-model="ruleTestDialog"
|
||||
max-width="50rem"
|
||||
scrollable
|
||||
@@ -191,4 +305,56 @@ const ruleTestDialog = ref(false)
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 系统健康检查弹窗 -->
|
||||
<VDialog
|
||||
v-if="systemTestDialog"
|
||||
v-model="systemTestDialog"
|
||||
max-width="50rem"
|
||||
scrollable
|
||||
>
|
||||
<VCard title="系统健康检查">
|
||||
<DialogCloseBtn @click="systemTestDialog = false" />
|
||||
<VCardText>
|
||||
<ModuleTestView />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 消息中心弹窗 -->
|
||||
<VDialog
|
||||
v-if="messageDialog"
|
||||
v-model="messageDialog"
|
||||
max-width="60rem"
|
||||
scrollable
|
||||
>
|
||||
<VCard title="消息中心">
|
||||
<DialogCloseBtn @click="messageDialog = false" />
|
||||
<VCardText ref="chatContainer">
|
||||
<MessageView @scroll="scrollMessageToEnd" />
|
||||
</VCardText>
|
||||
|
||||
<VCardItem>
|
||||
<VTextField
|
||||
v-model="user_message"
|
||||
placeholder="输入消息或命令"
|
||||
outlined
|
||||
hide-details
|
||||
single-line
|
||||
clearable
|
||||
density="compact"
|
||||
:disabled="sendButtonDisabled"
|
||||
@keydown.enter="sendMessage"
|
||||
>
|
||||
<template #append>
|
||||
<VBtn
|
||||
color="primary"
|
||||
:disabled="sendButtonDisabled"
|
||||
@click="sendMessage"
|
||||
>
|
||||
发送
|
||||
</VBtn>
|
||||
</template>
|
||||
</VTextField>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import DefaultLayoutWithVerticalNav from './components/DefaultLayoutWithVerticalNav.vue'
|
||||
import api from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
api.get('user/current')
|
||||
.catch(() => {
|
||||
router.replace('/login')
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefaultLayoutWithVerticalNav>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<component :is="Component" v-if="$route.meta.keepAlive" :key="$route.fullPath" />
|
||||
<component :is="Component" v-if="route.meta.keepAlive" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
<component :is="Component" v-if="!$route.meta.keepAlive" :key="$route.fullPath" />
|
||||
<component :is="Component" v-if="!route.meta.keepAlive" :key="route.fullPath" />
|
||||
</router-view>
|
||||
</DefaultLayoutWithVerticalNav>
|
||||
</template>
|
||||
|
||||
23
src/main.ts
@@ -1,7 +1,11 @@
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
import { createApp } from 'vue'
|
||||
import '@/@iconify/icons-bundle'
|
||||
import ToastPlugin from 'vue-toast-notification'
|
||||
import VuetifyUseDialog from 'vuetify-use-dialog'
|
||||
import './ace-config'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
import { removeEl } from './@core/utils/dom'
|
||||
import App from '@/App.vue'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
import { loadFonts } from '@/plugins/webfontloader'
|
||||
@@ -11,14 +15,26 @@ import '@core/scss/template/index.scss'
|
||||
import '@layouts/styles/index.scss'
|
||||
import '@styles/styles.scss'
|
||||
import 'vue-toast-notification/dist/theme-bootstrap.css'
|
||||
import { removeEl } from '@/util'
|
||||
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar';
|
||||
import 'vue3-perfect-scrollbar/style.css';
|
||||
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
|
||||
import { fixArrayAt } from '@/@core/utils/compatibility'
|
||||
|
||||
// 修复低版本Safari等浏览器数组不支持at函数的问题
|
||||
fixArrayAt()
|
||||
|
||||
// 加载字体
|
||||
loadFonts()
|
||||
|
||||
// Create vue app
|
||||
// 创建Vue实例
|
||||
const app = createApp(App)
|
||||
|
||||
// Use plugins Mount vue app
|
||||
// 注册全局组件
|
||||
app.component('VAceEditor', VAceEditor)
|
||||
.component('VApexChart', VueApexCharts)
|
||||
.component('VDialogCloseBtn', DialogCloseBtn)
|
||||
|
||||
// 注册插件
|
||||
app
|
||||
.use(vuetify)
|
||||
.use(router)
|
||||
@@ -27,5 +43,6 @@ app
|
||||
position: 'bottom-right',
|
||||
})
|
||||
.use(VuetifyUseDialog)
|
||||
.use(PerfectScrollbarPlugin)
|
||||
.mount('#app')
|
||||
.$nextTick(() => removeEl('#loading-bg'))
|
||||
|
||||
@@ -6,11 +6,67 @@ import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue'
|
||||
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
|
||||
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
|
||||
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue'
|
||||
import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
|
||||
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
|
||||
|
||||
// 仪表盘配置
|
||||
const dashboard_names = {
|
||||
storage: '存储空间',
|
||||
mediaStatistic: '媒体统计',
|
||||
weeklyOverview: '最近入库',
|
||||
speed: '实时速率',
|
||||
scheduler: '后台任务',
|
||||
cpu: 'CPU',
|
||||
memory: '内存',
|
||||
library: '我的媒体库',
|
||||
playing: '继续观看',
|
||||
latest: '最近添加',
|
||||
}
|
||||
|
||||
// 弹窗
|
||||
const dialog = ref(false)
|
||||
|
||||
// 从localStorage中获取数据
|
||||
const default_config = {
|
||||
mediaStatistic: true,
|
||||
scheduler: false,
|
||||
speed: false,
|
||||
storage: true,
|
||||
weeklyOverview: false,
|
||||
cpu: false,
|
||||
memory: false,
|
||||
library: true,
|
||||
playing: true,
|
||||
latest: true,
|
||||
}
|
||||
const config = ref(JSON.parse(localStorage.getItem('MP_DASHBOARD') || '{}'))
|
||||
if (Object.keys(config.value).length === 0) {
|
||||
config.value = default_config
|
||||
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value))
|
||||
}
|
||||
|
||||
// 设置项目
|
||||
function setDashboardConfig() {
|
||||
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value))
|
||||
dialog.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 底部操作按钮 -->
|
||||
<VFab
|
||||
icon="mdi-view-dashboard-edit"
|
||||
location="bottom end"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="dialog = true"
|
||||
/>
|
||||
<VRow class="match-height">
|
||||
<VCol
|
||||
v-if="config.storage"
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
@@ -18,6 +74,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.mediaStatistic"
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
@@ -25,6 +82,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.weeklyOverview"
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
@@ -32,6 +90,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.speed"
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
@@ -39,6 +98,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.scheduler"
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
@@ -46,6 +106,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.cpu"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
@@ -53,10 +114,72 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.memory"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AnalyticsMemory />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.library"
|
||||
cols="12"
|
||||
>
|
||||
<MediaServerLibrary />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.playing"
|
||||
cols="12"
|
||||
>
|
||||
<MediaServerPlaying />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.latest"
|
||||
cols="12"
|
||||
>
|
||||
<MediaServerLatest />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<!-- 弹窗,根据配置生成选项 -->
|
||||
<VDialog
|
||||
v-model="dialog"
|
||||
max-width="600"
|
||||
scrollable
|
||||
>
|
||||
<VCard title="设置仪表板">
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol
|
||||
v-for="(item, key) in dashboard_names"
|
||||
:key="key"
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VCheckbox
|
||||
v-model="config[key]"
|
||||
:label="dashboard_names[key]"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn
|
||||
color="primary"
|
||||
@click="dialog = false"
|
||||
>
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="setDashboardConfig"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</vdialog>
|
||||
</template>
|
||||
|
||||
@@ -13,6 +13,7 @@ const store = useStore()
|
||||
const form = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
otp_password: '',
|
||||
remember: true,
|
||||
})
|
||||
|
||||
@@ -30,6 +31,12 @@ const backgroundImageUrl = ref('')
|
||||
// 背景图片加载状态
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 是否开启双重验证
|
||||
const isOTP = ref(false)
|
||||
|
||||
// 用户名称输入框
|
||||
const usernameInput = ref()
|
||||
|
||||
// 获取背景图片
|
||||
async function fetchBackgroundImage() {
|
||||
api
|
||||
@@ -42,19 +49,38 @@ async function fetchBackgroundImage() {
|
||||
})
|
||||
}
|
||||
|
||||
// 查询是否开启双重验证
|
||||
async function fetchOTP() {
|
||||
const userid = usernameInput.value?.value
|
||||
if (!userid) {
|
||||
isOTP.value = false
|
||||
return
|
||||
}
|
||||
api
|
||||
.get(`/user/otp/${userid}`)
|
||||
.then((response: any) => {
|
||||
isOTP.value = response.success
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
|
||||
// 登录获取token事件
|
||||
function login() {
|
||||
errorMessage.value = ''
|
||||
|
||||
// 进行表单校验
|
||||
if (!form.value.username || !form.value.password)
|
||||
if (!form.value.username || !form.value.password || (isOTP.value && !form.value.otp_password)) {
|
||||
errorMessage.value = '请输入完整信息'
|
||||
return
|
||||
|
||||
}
|
||||
// 用户名密码
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('username', form.value.username)
|
||||
formData.append('password', form.value.password)
|
||||
formData.append('otp_password', form.value.otp_password)
|
||||
|
||||
// 请求token
|
||||
api
|
||||
@@ -77,21 +103,21 @@ function login() {
|
||||
store.dispatch('auth/updateUserName', username)
|
||||
store.dispatch('auth/updateAvatar', avatar)
|
||||
|
||||
// 跳转到首页
|
||||
router.push('/')
|
||||
// 跳转到首页或回原始页面
|
||||
router.push(store.state.auth.originalPath ?? '/')
|
||||
})
|
||||
.catch((error: any) => {
|
||||
// 登录失败,显示错误提示
|
||||
if (!error.response)
|
||||
errorMessage.value = '登录失败,请检查网络连接'
|
||||
else if (error.response.status === 401)
|
||||
errorMessage.value = '登录失败,请检查用户名和密码是否正确'
|
||||
errorMessage.value = '登录失败,请检查用户名、密码或双重验证是否正确'
|
||||
else if (error.response.status === 403)
|
||||
errorMessage.value = '登录失败,您没有权限访问'
|
||||
else if (error.response.status === 500)
|
||||
errorMessage.value = '登录失败,服务器错误'
|
||||
else
|
||||
errorMessage.value = `登录失败 ${error.response.status},请检查用户名和密码是否正确`
|
||||
errorMessage.value = `登录失败 ${error.response.status},请检查用户名、密码或双重验证码是否正确`
|
||||
})
|
||||
}
|
||||
|
||||
@@ -148,13 +174,14 @@ onMounted(() => {
|
||||
<!-- username -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
ref="usernameInput"
|
||||
v-model="form.username"
|
||||
label="用户名"
|
||||
type="text"
|
||||
:rules="[requiredValidator]"
|
||||
@input="fetchOTP"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- password -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
@@ -167,23 +194,24 @@ onMounted(() => {
|
||||
:rules="[requiredValidator]"
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
class="text-error mt-1"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-if="isOTP"
|
||||
v-model="form.otp_password"
|
||||
label="双重验证码"
|
||||
type="input"
|
||||
/>
|
||||
<!-- remember me checkbox -->
|
||||
<div class="d-flex align-center justify-space-between flex-wrap mt-1 mb-4">
|
||||
<div class="d-flex align-center justify-space-between flex-wrap">
|
||||
<VCheckbox
|
||||
v-model="form.remember"
|
||||
label="保持登录"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<!-- login button -->
|
||||
<VBtn
|
||||
block
|
||||
@@ -192,6 +220,12 @@ onMounted(() => {
|
||||
>
|
||||
登录
|
||||
</VBtn>
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
class="text-error mt-2 text-shadow"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
|
||||
@@ -1,79 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
|
||||
|
||||
const viewList = reactive<{apipath: string, linkurl: string, title: string}[]>([
|
||||
{
|
||||
apipath: 'tmdb/trending',
|
||||
linkurl: "/browse/tmdb/trending?title=流行趋势",
|
||||
title: "流行趋势",
|
||||
},
|
||||
{
|
||||
apipath: "douban/showing",
|
||||
linkurl: "/browse/douban/showing?title=正在热映",
|
||||
title: "正在热映"
|
||||
},
|
||||
{
|
||||
apipath: "bangumi/calendar",
|
||||
linkurl: "/browse/bangumi/calendar?title=Bangumi每日放送",
|
||||
title: "Bangumi每日放送"
|
||||
},
|
||||
{
|
||||
apipath: "tmdb/movies",
|
||||
linkurl: "/browse/tmdb/movies?title=热门电影",
|
||||
title: "热门电影"
|
||||
},
|
||||
{
|
||||
apipath: "tmdb/tvs?with_original_language=zh|en|ja|ko",
|
||||
linkurl: "/browse/tmdb/tvs??with_original_language=zh|en|ja|ko&title=热门电视剧",
|
||||
title: "热门电视剧"
|
||||
},
|
||||
{
|
||||
apipath: "douban/movie_hot",
|
||||
linkurl: "/browse/douban/movie_hot?title=热门电影",
|
||||
title: "热门电影"
|
||||
},
|
||||
{
|
||||
apipath: "douban/tv_hot",
|
||||
linkurl: "/browse/douban/tv_hot?title=热门电视剧",
|
||||
title: "热门电视剧"
|
||||
},
|
||||
{
|
||||
apipath: "douban/tv_animation",
|
||||
linkurl: "/browse/douban/tv_animation?title=热门动漫",
|
||||
title: "热门动漫"
|
||||
},
|
||||
{
|
||||
apipath: "douban/movies",
|
||||
linkurl: "/browse/douban/movies?title=最新电影",
|
||||
title: "最新电影"
|
||||
},
|
||||
{
|
||||
apipath: "douban/tvs",
|
||||
linkurl: "/browse/douban/tvs?title=最新电视剧",
|
||||
title: "最新电视剧"
|
||||
},
|
||||
{
|
||||
apipath: "douban/movie_top250",
|
||||
linkurl: "/browse/douban/movie_top250?title=电影TOP250",
|
||||
title: "电影TOP250"
|
||||
},
|
||||
{
|
||||
apipath: "douban/tv_weekly_chinese",
|
||||
linkurl: "/browse/douban/tv_weekly_chinese?title=国产剧集榜",
|
||||
title: "国产剧集榜"
|
||||
},
|
||||
{
|
||||
apipath: "douban/tv_weekly_global",
|
||||
linkurl: "/browse/douban/tv_weekly_global?title=全球剧集榜",
|
||||
title: "全球剧集榜"
|
||||
}
|
||||
])
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MediaCardSlideView
|
||||
apipath="tmdb/trending"
|
||||
linkurl="/browse/tmdb/trending?title=流行趋势"
|
||||
title="流行趋势"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="douban/showing"
|
||||
linkurl="/browse/douban/showing?title=正在热映"
|
||||
title="正在热映"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="tmdb/movies"
|
||||
linkurl="/browse/tmdb/movies?title=热门电影"
|
||||
title="热门电影"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="tmdb/tvs?with_original_language=zh|en|ja|ko"
|
||||
linkurl="/browse/tmdb/tvs??with_original_language=zh|en|ja|ko&title=热门电视剧"
|
||||
title="热门电视剧"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="douban/movie_hot"
|
||||
linkurl="/browse/douban/movie_hot?title=热门电影"
|
||||
title="热门电影"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="douban/tv_hot"
|
||||
linkurl="/browse/douban/tv_hot?title=热门电视剧"
|
||||
title="热门电视剧"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="douban/tv_animation"
|
||||
linkurl="/browse/douban/tv_animation?title=热门动漫"
|
||||
title="热门动漫"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="douban/movies"
|
||||
linkurl="/browse/douban/movies?title=最新电影"
|
||||
title="最新电影"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="douban/tvs"
|
||||
linkurl="/browse/douban/tvs?title=最新电视剧"
|
||||
title="最新电视剧"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="douban/movie_top250"
|
||||
linkurl="/browse/douban/movie_top250?title=电影TOP250"
|
||||
title="电影TOP250"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="douban/tv_weekly_chinese"
|
||||
linkurl="/browse/douban/tv_weekly_chinese?title=国产剧集榜"
|
||||
title="国产剧集榜"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="douban/tv_weekly_global"
|
||||
linkurl="/browse/douban/tv_weekly_global?title=全球剧集榜"
|
||||
title="全球剧集榜"
|
||||
<MediaCardSlideView
|
||||
v-for="item in viewList"
|
||||
:key="item.apipath"
|
||||
v-bind="item"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -18,6 +18,9 @@ const type = route.query?.type?.toString() ?? ''
|
||||
// 搜索字段
|
||||
const area = route.query?.area?.toString() ?? ''
|
||||
|
||||
// 搜索季
|
||||
const season = route.query?.season?.toString() ?? ''
|
||||
|
||||
// 视图类型,从localStorage中读取
|
||||
const viewType = ref<string>(localStorage.getItem('MPTorrentsViewType') ?? 'card')
|
||||
|
||||
@@ -36,6 +39,12 @@ const progressValue = ref(0)
|
||||
// 加载进度SSE
|
||||
const progressEventSource = ref<EventSource>()
|
||||
|
||||
// 错误标题
|
||||
const errorTitle = ref('没有数据')
|
||||
|
||||
// 错误描述
|
||||
const errorDescription = ref('未搜索到任何资源')
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
function startLoadingProgress() {
|
||||
progressText.value = '正在搜索,请稍候...'
|
||||
@@ -75,13 +84,19 @@ async function fetchData() {
|
||||
else {
|
||||
startLoadingProgress()
|
||||
// 优先按TMDBID精确查询
|
||||
if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:')) {
|
||||
dataList.value = await api.get(`search/media/${keyword}`, {
|
||||
if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:') || keyword?.startsWith('bangumi:')) {
|
||||
const result: {[key: string]: any} = await api.get(`search/media/${keyword}`, {
|
||||
params: {
|
||||
mtype: type,
|
||||
area,
|
||||
season,
|
||||
},
|
||||
})
|
||||
if (result.success){
|
||||
dataList.value = result.data
|
||||
} else {
|
||||
errorDescription.value = result.message
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 按标题模糊查询
|
||||
@@ -105,16 +120,16 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!isRefreshed" class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center">
|
||||
<VProgressCircular v-if="!keyword" size="48" indeterminate color="primary" />
|
||||
<VProgressCircular v-if="keyword" class="mb-3" color="primary" :model-value="progressValue" size="64" />
|
||||
<span>{{ progressText }}</span>
|
||||
</div>
|
||||
<LoadingBanner
|
||||
v-if="!isRefreshed"
|
||||
class="mt-12"
|
||||
:text="progressText"
|
||||
:progress="progressValue"
|
||||
/>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
error-title="没有资源"
|
||||
error-description="没有搜索到符合条件的资源。"
|
||||
:error-title="errorTitle"
|
||||
:error-description="errorDescription"
|
||||
/>
|
||||
<div v-if="dataList.length > 0">
|
||||
<TorrentRowListView
|
||||
@@ -127,20 +142,24 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
<!-- 视图切换 -->
|
||||
<span v-if="dataList.length > 0" class="fixed right-5 bottom-5">
|
||||
<VBtn
|
||||
v-if="viewType === 'list'"
|
||||
size="x-large"
|
||||
icon="mdi-view-grid"
|
||||
color="primary"
|
||||
@click="setViewType('card')"
|
||||
/>
|
||||
<VBtn
|
||||
v-else
|
||||
size="x-large"
|
||||
icon="mdi-view-list"
|
||||
color="primary"
|
||||
@click="setViewType('list')"
|
||||
/>
|
||||
</span>
|
||||
<VFab
|
||||
v-if="viewType === 'list'"
|
||||
icon="mdi-view-grid"
|
||||
location="bottom end"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="setViewType('card')"
|
||||
/>
|
||||
<VFab
|
||||
v-else
|
||||
icon="mdi-view-list"
|
||||
location="bottom end"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="setViewType('list')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -8,6 +8,7 @@ import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
|
||||
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
|
||||
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
|
||||
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
|
||||
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -20,6 +21,11 @@ const tabs = [
|
||||
icon: 'mdi-account',
|
||||
tab: 'account',
|
||||
},
|
||||
{
|
||||
title: '系统',
|
||||
icon: 'mdi-cog',
|
||||
tab: 'system',
|
||||
},
|
||||
{
|
||||
title: '站点',
|
||||
icon: 'mdi-web',
|
||||
@@ -83,6 +89,13 @@ const tabs = [
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 系统 -->
|
||||
<VWindowItem value="system">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingSystem />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 站点 -->
|
||||
<VWindowItem value="site">
|
||||
<transition name="fade-slide" appear>
|
||||
|
||||
@@ -31,7 +31,7 @@ export default {
|
||||
elevation: 0,
|
||||
},
|
||||
VList: {
|
||||
activeColor: 'primary',
|
||||
color: 'primary',
|
||||
},
|
||||
VPagination: {
|
||||
activeColor: 'primary',
|
||||
@@ -80,6 +80,7 @@ export default {
|
||||
// set v-rating default color to primary
|
||||
color: 'rgba(var(--v-theme-on-background),0.23)',
|
||||
activeColor: 'warning',
|
||||
halfIncrements: true,
|
||||
},
|
||||
VProgressCircular: {
|
||||
// set v-progress-circular default color to primary
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { configureNProgress, doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import store from '@/store'
|
||||
|
||||
@@ -7,7 +7,7 @@ configureNProgress()
|
||||
|
||||
// Router
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// 如果页面有缓存那么恢复其位置, 否则始终滚动到顶部
|
||||
if (to.meta.keepAlive && savedPosition)
|
||||
@@ -162,6 +162,7 @@ router.beforeEach((to, from, next) => {
|
||||
const isAuthenticated = store.state.auth.token !== null
|
||||
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
store.state.auth.originalPath = to.fullPath
|
||||
next('/login')
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -7,6 +7,7 @@ interface AuthState {
|
||||
superUser: boolean
|
||||
userName: string
|
||||
avatar: string
|
||||
originalPath: string | null
|
||||
}
|
||||
|
||||
// 定义根状态类型
|
||||
@@ -23,6 +24,7 @@ const authModule: Module<AuthState, RootState> = {
|
||||
superUser: false,
|
||||
userName: '',
|
||||
avatar: '',
|
||||
originalPath: null,
|
||||
},
|
||||
mutations: {
|
||||
setToken(state, token: string) {
|
||||
|
||||
@@ -5,39 +5,37 @@
|
||||
|
||||
#nprogress .bar {
|
||||
background: rgb(var(--v-theme-primary)) !important;
|
||||
top: env(safe-area-inset-top) !important;
|
||||
inset-block-start: env(safe-area-inset-top) !important;
|
||||
}
|
||||
|
||||
#nprogress .peg {
|
||||
box-shadow: 0 0 10px rgb(var(--v-theme-primary)), 0 0 5px rgb(var(--v-theme-primary)) !important;
|
||||
-webkit-transform: rotate(0deg) translate(0px, -1px);
|
||||
-ms-transform: rotate(0deg) translate(0px, -1px);
|
||||
transform: rotate(0deg) translate(0px, -1px);
|
||||
transform: rotate(0deg) translate(0, -1px);
|
||||
}
|
||||
|
||||
.v-toast--bottom {
|
||||
margin-bottom: env(safe-area-inset-bottom);
|
||||
z-index: 2500;
|
||||
margin-block-end: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.v-toast--top {
|
||||
margin-top: env(safe-area-inset-top);
|
||||
z-index: 2500;
|
||||
margin-block-start: env(safe-area-inset-top);
|
||||
}
|
||||
|
||||
.v-dialog > .v-overlay__content {
|
||||
margin-top: calc(env(safe-area-inset-top) + 1rem);
|
||||
max-height: calc(100% - env(safe-area-inset-top) - 1rem);
|
||||
margin-block-start: calc(env(safe-area-inset-top) + 1rem);
|
||||
max-block-size: calc(100% - env(safe-area-inset-top) - 1rem);
|
||||
}
|
||||
|
||||
.v-dialog > .v-overlay__content{
|
||||
width: calc(100% - 1rem);
|
||||
inline-size: calc(100% - 1rem);
|
||||
}
|
||||
|
||||
.v-dialog--fullscreen > .v-overlay__content{
|
||||
margin-top: env(safe-area-inset-top);
|
||||
max-height: calc(100% - env(safe-area-inset-top));
|
||||
width: 100%;
|
||||
inline-size: 100%;
|
||||
margin-block-start: env(safe-area-inset-top);
|
||||
max-block-size: calc(100% - env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
/* router view transition fade-slide */
|
||||
@@ -62,21 +60,20 @@
|
||||
}
|
||||
|
||||
.text-moviepilot {
|
||||
background-clip: text;
|
||||
background-image: linear-gradient(to bottom right,var(--tw-gradient-stops));
|
||||
color: transparent;
|
||||
|
||||
--tw-gradient-from: #818cf8;
|
||||
--tw-gradient-to: rgba(129,140,248,0);
|
||||
--tw-gradient-to: rgba(129,140,248,0%);
|
||||
--tw-gradient-stops: var(--tw-gradient-from),var(--tw-gradient-to);
|
||||
--tw-gradient-to: #c084fc;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.slider-header {
|
||||
position: relative;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
margin-block: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.slider-title {
|
||||
@@ -87,25 +84,27 @@
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px){
|
||||
@media (width >= 640px){
|
||||
.slider-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2.25rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// 美化滚动条
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
block-size: 8px;
|
||||
inline-size: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
background: rgb(var(--v-theme-perfect-scrollbar-thumb));
|
||||
-webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.2);
|
||||
box-shadow: inset 0 0 10px rgba(0,0,0,20%);
|
||||
|
||||
@media(hover){
|
||||
&:hover{
|
||||
background: #a1a1a1;
|
||||
@@ -120,10 +119,14 @@
|
||||
|
||||
.backdrop-blur {
|
||||
--tw-backdrop-blur: blur(8px)!important;
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important;
|
||||
|
||||
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important;
|
||||
}
|
||||
|
||||
.v-toolbar{
|
||||
background: rgb(var(--v-table-header-background));
|
||||
}
|
||||
|
||||
.v-toast {
|
||||
z-index: 2500 !important;
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export function removeEl(selector: string) {
|
||||
if (selector) {
|
||||
const el = document.querySelector(selector)
|
||||
el?.parentNode?.removeChild(el)
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './dom'
|
||||
48
src/views/dashboard/MediaServerLatest.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
import PosterCard from '@/components/cards/PosterCard.vue'
|
||||
|
||||
// 最近入库列表
|
||||
const latestList = ref<MediaServerPlayItem[]>([])
|
||||
|
||||
// 调用API查询
|
||||
async function loadLatest() {
|
||||
try {
|
||||
latestList.value = await api.get('mediaserver/latest')
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLatest()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>最近添加</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<div
|
||||
v-if="latestList.length > 0"
|
||||
class="grid gap-4 grid-media-card mx-3 mb-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<PosterCard
|
||||
v-for="data in latestList"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
</style>
|
||||
50
src/views/dashboard/MediaServerLibrary.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
import LibraryCard from '@/components/cards/LibraryCard.vue'
|
||||
|
||||
// 媒体库列表
|
||||
const libraryList = ref<MediaServerPlayItem[]>([])
|
||||
|
||||
// 调用API查询
|
||||
async function loadLibrary() {
|
||||
try {
|
||||
libraryList.value = await api.get('mediaserver/library')
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLibrary()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>我的媒体库</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<div
|
||||
v-if="libraryList.length > 0"
|
||||
class="grid gap-4 grid-backdrop-card mx-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<LibraryCard
|
||||
v-for="data in libraryList"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
height="10rem"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-backdrop-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
50
src/views/dashboard/MediaServerPlaying.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
import BackdropCard from '@/components/cards/BackdropCard.vue'
|
||||
|
||||
// 继续播放列表
|
||||
const playingList = ref<MediaServerPlayItem[]>([])
|
||||
|
||||
// 调用API查询
|
||||
async function loadPlayingList() {
|
||||
try {
|
||||
playingList.value = await api.get('mediaserver/playing')
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPlayingList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>继续观看</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<div
|
||||
v-if="playingList.length > 0"
|
||||
class="grid gap-4 grid-backdrop-card mx-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<BackdropCard
|
||||
v-for="data in playingList"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
height="10rem"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-backdrop-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -52,70 +52,63 @@ async function fetchData({ done }: { done: any }) {
|
||||
// 如果正在加载中,直接返回
|
||||
if (loading.value) {
|
||||
done('ok')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 设置加载中
|
||||
loading.value = true
|
||||
|
||||
// 加载到满屏或者加载出错
|
||||
if (!hasScroll()) {
|
||||
// 加载多次
|
||||
while (!hasScroll()) {
|
||||
// 设置加载中
|
||||
loading.value = true
|
||||
// 请求API
|
||||
currData.value = await api.get(props.apipath, {
|
||||
params: getParams(),
|
||||
})
|
||||
|
||||
// 取消加载中
|
||||
loading.value = false
|
||||
// 标计为已请求完成
|
||||
isRefreshed.value = true
|
||||
if (currData.value.length === 0) {
|
||||
// 如果没有数据,跳出
|
||||
done('ok')
|
||||
|
||||
done('empty')
|
||||
return
|
||||
}
|
||||
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
|
||||
// 页码+1
|
||||
page.value++
|
||||
// 返回加载成功
|
||||
done('ok')
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 加载一次
|
||||
// 设置加载中
|
||||
loading.value = true
|
||||
// 请求API
|
||||
currData.value = await api.get(props.apipath, {
|
||||
params: getParams(),
|
||||
})
|
||||
|
||||
// 标计为已请求完成
|
||||
isRefreshed.value = true
|
||||
if (currData.value.length === 0) {
|
||||
// 如果没有数据,跳出
|
||||
done('empty')
|
||||
} else {
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
// 页码+1
|
||||
page.value++
|
||||
// 返回加载成功
|
||||
done('ok')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
|
||||
// 页码+1
|
||||
page.value++
|
||||
}
|
||||
|
||||
// 取消加载中
|
||||
loading.value = false
|
||||
|
||||
// 返回加载成功
|
||||
done('ok')
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
|
||||
// 返回加载失败
|
||||
done('error')
|
||||
}
|
||||
@@ -123,16 +116,10 @@ async function fetchData({ done }: { done: any }) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
<LoadingBanner
|
||||
v-if="!isRefreshed"
|
||||
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
|
||||
>
|
||||
<VProgressCircular
|
||||
size="48"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
class="mt-12"
|
||||
/>
|
||||
<VInfiniteScroll
|
||||
mode="intersect"
|
||||
side="end"
|
||||
@@ -141,6 +128,7 @@ async function fetchData({ done }: { done: any }) {
|
||||
@load="fetchData"
|
||||
>
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div
|
||||
v-if="dataList.length > 0"
|
||||
class="grid gap-4 grid-media-card mx-3"
|
||||
|
||||
@@ -11,6 +11,8 @@ const props = defineProps({
|
||||
title: String,
|
||||
})
|
||||
|
||||
provide('rankingPropsKey', reactive({...props}))
|
||||
|
||||
// 组件加载完成
|
||||
const componentLoaded = ref(false)
|
||||
|
||||
@@ -39,12 +41,11 @@ onMounted(fetchData)
|
||||
<template>
|
||||
<SlideView
|
||||
v-if="componentLoaded"
|
||||
v-bind="props"
|
||||
>
|
||||
<template #content>
|
||||
<template
|
||||
v-for="data in dataList"
|
||||
:key="data.tmdb_id || data.douban_id"
|
||||
:key="data.tmdb_id || data.douban_id || data.bangumi_id"
|
||||
>
|
||||
<MediaCard
|
||||
:media="data"
|
||||
|
||||
@@ -8,7 +8,7 @@ import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
import router from '@/router'
|
||||
import SubscribeEditForm from '@/components/form/SubscribeEditForm.vue'
|
||||
import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
|
||||
|
||||
// 输入参数
|
||||
const mediaProps = defineProps({
|
||||
@@ -25,8 +25,8 @@ const mediaDetail = ref<MediaInfo>({} as MediaInfo)
|
||||
// 订阅编辑弹窗
|
||||
const subscribeEditDialog = ref(false)
|
||||
|
||||
// 本地是否存在
|
||||
const isExists = ref(false)
|
||||
// 本地是否存在,存在则包括Item信息
|
||||
const existsItemId = ref('')
|
||||
|
||||
// 是否已订阅
|
||||
const isSubscribed = ref(false)
|
||||
@@ -46,6 +46,15 @@ const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
|
||||
// 订阅编号
|
||||
const subscribeId = ref<number>()
|
||||
|
||||
// 获得mediaid
|
||||
function getMediaId() {
|
||||
return mediaDetail.value?.tmdb_id
|
||||
? `tmdb:${mediaDetail.value?.tmdb_id}`
|
||||
: mediaDetail.value?.douban_id
|
||||
? `douban:${mediaDetail.value?.douban_id}`
|
||||
: `bangumi:${mediaDetail.value?.bangumi_id}`
|
||||
}
|
||||
|
||||
// 调用API查询详情
|
||||
async function getMediaDetail() {
|
||||
if (mediaProps.mediaid && mediaProps.type) {
|
||||
@@ -55,13 +64,12 @@ async function getMediaDetail() {
|
||||
},
|
||||
})
|
||||
isRefreshed.value = true
|
||||
if (!mediaDetail.value.tmdb_id && !mediaDetail.value.douban_id)
|
||||
if (!mediaDetail.value.tmdb_id && !mediaDetail.value.douban_id && !mediaDetail.value.bangumi_id)
|
||||
return
|
||||
|
||||
// 检查存在状态
|
||||
if (mediaDetail.value.type === '电影')
|
||||
checkMovieExists()
|
||||
else
|
||||
checkExists()
|
||||
if (mediaDetail.value.type === '电视剧')
|
||||
checkSeasonsNotExists()
|
||||
// 检查订阅状态
|
||||
if (mediaDetail.value.type === '电影')
|
||||
@@ -86,9 +94,9 @@ async function loadSeasonEpisodes(season: number) {
|
||||
}
|
||||
|
||||
// 查询当前媒体是否已入库
|
||||
async function checkMovieExists() {
|
||||
async function checkExists() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('media/exists', {
|
||||
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||
params: {
|
||||
tmdbid: mediaDetail.value.tmdb_id,
|
||||
title: mediaDetail.value.title,
|
||||
@@ -99,7 +107,7 @@ async function checkMovieExists() {
|
||||
})
|
||||
|
||||
if (result.success)
|
||||
isExists.value = true
|
||||
existsItemId.value = result.data.item.id
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
@@ -109,7 +117,7 @@ async function checkMovieExists() {
|
||||
// 查询当前媒体是否已订阅
|
||||
async function checkSubscribe(season = 0) {
|
||||
try {
|
||||
const mediaid = mediaDetail.value.tmdb_id ? `tmdb:${mediaDetail.value.tmdb_id}` : `douban:${mediaDetail.value.douban_id}`
|
||||
const mediaid = getMediaId()
|
||||
|
||||
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
||||
params: {
|
||||
@@ -133,11 +141,8 @@ async function checkSeasonsNotExists() {
|
||||
if (mediaDetail.value.type !== '电视剧')
|
||||
return
|
||||
try {
|
||||
const result: NotExistMediaInfo[] = await api.post('download/notexists', mediaDetail.value)
|
||||
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', mediaDetail.value)
|
||||
if (result) {
|
||||
if (result.length === 0)
|
||||
isExists.value = true
|
||||
|
||||
result.forEach((item) => {
|
||||
// 0-已入库 1-部分缺失 2-全部缺失
|
||||
let state = 0
|
||||
@@ -145,8 +150,6 @@ async function checkSeasonsNotExists() {
|
||||
state = 2
|
||||
else if (item.episodes.length < item.total_episode)
|
||||
state = 1
|
||||
if (state !== 2)
|
||||
isExists.value = true
|
||||
seasonsNotExisted.value[item.season] = state
|
||||
})
|
||||
}
|
||||
@@ -188,7 +191,7 @@ async function addSubscribe(season = 0) {
|
||||
startNProgress()
|
||||
try {
|
||||
// 是否洗版
|
||||
let best_version = isExists.value ? 1 : 0
|
||||
let best_version = existsItemId.value ? 1 : 0
|
||||
if (season)
|
||||
// 全部存在时洗版
|
||||
best_version = !seasonsNotExisted.value[season] ? 1 : 0
|
||||
@@ -199,6 +202,7 @@ async function addSubscribe(season = 0) {
|
||||
year: mediaDetail.value?.year,
|
||||
tmdbid: mediaDetail.value?.tmdb_id,
|
||||
doubanid: mediaDetail.value?.douban_id,
|
||||
bangumiid: mediaDetail.value?.bangumi_id,
|
||||
season,
|
||||
best_version,
|
||||
})
|
||||
@@ -222,8 +226,11 @@ async function addSubscribe(season = 0) {
|
||||
|
||||
// 显示编辑弹窗
|
||||
if (result.success) {
|
||||
subscribeId.value = result.data.id
|
||||
subscribeEditDialog.value = true
|
||||
const show_edit_dialog = await queryDefaultSubscribeConfig()
|
||||
if (show_edit_dialog) {
|
||||
subscribeId.value = result.data.id
|
||||
subscribeEditDialog.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@@ -254,9 +261,7 @@ async function removeSubscribe(season: number) {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
const mediaid = mediaDetail.value?.tmdb_id
|
||||
? `tmdb:${mediaDetail.value?.tmdb_id}`
|
||||
: `douban:${mediaDetail.value?.douban_id}`
|
||||
const mediaid = getMediaId()
|
||||
|
||||
const result: { [key: string]: any } = await api.delete(
|
||||
`subscribe/media/${mediaid}`,
|
||||
@@ -317,6 +322,11 @@ function getTvdbLink() {
|
||||
return `https://www.thetvdb.com/series/${mediaDetail.value.tvdb_id}`
|
||||
}
|
||||
|
||||
// 拼装Bangumi地址
|
||||
function getBangumiLink() {
|
||||
return `https://bgm.tv/subject/${mediaDetail.value.bangumi_id}`
|
||||
}
|
||||
|
||||
// 拼装集图片地址
|
||||
function getEpisodeImage(stillPath: string) {
|
||||
if (!stillPath)
|
||||
@@ -392,34 +402,68 @@ function joinArray(arr: string[]) {
|
||||
|
||||
// 开始搜索
|
||||
function handleSearch(area: string) {
|
||||
const keyword = mediaDetail.value.tmdb_id ? `tmdb:${mediaDetail.value.tmdb_id}` : `douban:${mediaDetail.value.douban_id}`
|
||||
const keyword = getMediaId()
|
||||
router.push({
|
||||
path: '/resource',
|
||||
query: {
|
||||
keyword,
|
||||
type: mediaDetail.value.type,
|
||||
area,
|
||||
season: mediaDetail.value.season,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转播放页面
|
||||
async function handlePlay() {
|
||||
// 获取播放链接地址
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`mediaserver/play/${existsItemId.value}`,
|
||||
)
|
||||
if (result?.success) {
|
||||
// 打开链接地址
|
||||
setTimeout(() => {
|
||||
window.open(result.data.url, '_blank')
|
||||
}, 100)
|
||||
}
|
||||
else { $toast.error(`获取播放链接失败:${result.message}!`) }
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function queryDefaultSubscribeConfig() {
|
||||
try {
|
||||
let subscribe_config_url = ''
|
||||
if (mediaProps.type === '电影')
|
||||
subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
|
||||
else
|
||||
subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
|
||||
|
||||
const result: { [key: string]: any } = await api.get(subscribe_config_url)
|
||||
|
||||
if (result.data?.value)
|
||||
return result.data.value.show_edit_dialog
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
getMediaDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
<LoadingBanner
|
||||
v-if="!isRefreshed"
|
||||
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
|
||||
>
|
||||
<VProgressCircular
|
||||
size="48"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" class="max-w-8xl mx-auto px-4">
|
||||
class="mt-12"
|
||||
/>
|
||||
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" class="max-w-8xl mx-auto px-4">
|
||||
<template v-if="mediaDetail.backdrop_path || mediaDetail.poster_path">
|
||||
<div class="vue-media-back absolute left-0 top-0 w-full h-96">
|
||||
<VImg class="h-96" :src="mediaDetail.backdrop_path || mediaDetail.poster_path" cover />
|
||||
@@ -438,7 +482,7 @@ onBeforeMount(() => {
|
||||
</VImg>
|
||||
</div>
|
||||
<div class="media-title">
|
||||
<div v-if="isExists" class="media-status">
|
||||
<div v-if="existsItemId" class="media-status">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap transition !no-underline bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 hover:bg-green-500 hover:bg-opacity-100 false overflow-hidden">
|
||||
<div class="relative z-20 flex items-center false"><span>已入库</span></div>
|
||||
</span>
|
||||
@@ -458,7 +502,7 @@ onBeforeMount(() => {
|
||||
</span>
|
||||
</div>
|
||||
<div class="media-actions">
|
||||
<VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" variant="tonal" color="info">
|
||||
<VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" variant="tonal" color="info" class="mb-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-magnify" />
|
||||
</template>
|
||||
@@ -484,12 +528,18 @@ onBeforeMount(() => {
|
||||
</VList>
|
||||
</VMenu>
|
||||
</VBtn>
|
||||
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id" class="ms-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
|
||||
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id || mediaDetail.bangumi_id" class="ms-2 mb-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
|
||||
<template #prepend>
|
||||
<VIcon :icon="getSubscribeIcon" />
|
||||
</template>
|
||||
{{ isSubscribed ? '已订阅' : '订阅' }}
|
||||
</VBtn>
|
||||
<VBtn v-if="existsItemId" class="ms-2 mb-2" variant="tonal" @click="handlePlay()">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-play" />
|
||||
</template>
|
||||
在线播放
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-overview">
|
||||
@@ -504,7 +554,9 @@ onBeforeMount(() => {
|
||||
<ul v-if="mediaDetail.tmdb_id" class="media-crew">
|
||||
<li v-for="director in mediaDetail.directors" :key="director.id">
|
||||
<span>{{ director.job }}</span>
|
||||
<a class="crew-name" :href="`person?personid=${director.id}`" target="_blank">{{ director.name }}</a>
|
||||
<RouterLink :to="`/person?personid=${director.id}`" class="crew-name" target="_blank">
|
||||
{{ director.name }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-if="!mediaDetail.tmdb_id && mediaDetail.douban_id" class="media-crew">
|
||||
@@ -538,6 +590,12 @@ onBeforeMount(() => {
|
||||
<span class="ms-1">TheTvDb</span>
|
||||
</div>
|
||||
</a>
|
||||
<a v-if="mediaDetail.bangumi_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getBangumiLink()" target="_blank">
|
||||
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 py-1 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700">
|
||||
<VIcon icon="mdi-link" />
|
||||
<span class="ms-1">Bangumi</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<h2 v-if="mediaDetail.type === '电视剧' && mediaDetail.tmdb_id" class="py-4">
|
||||
季
|
||||
@@ -576,16 +634,10 @@ onBeforeMount(() => {
|
||||
</VExpansionPanelTitle>
|
||||
<VExpansionPanelText>
|
||||
<template #default>
|
||||
<div
|
||||
<LoadingBanner
|
||||
v-if="!seasonEpisodesInfo[season.season_number || 0]"
|
||||
class="mt-3 w-full text-center text-gray-500 text-sm flex flex-col items-center"
|
||||
>
|
||||
<VProgressCircular
|
||||
size="48"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
class="mt-3"
|
||||
/>
|
||||
<div class="flex flex-col justify-center divide-y divide-gray-700">
|
||||
<div v-for="episode in seasonEpisodesInfo[season.season_number || 0]" :key="episode.episode_number" class="flex flex-col space-y-4 py-4 xl:flex-row xl:space-y-4 xl:space-x-4">
|
||||
<div class="flex-1">
|
||||
@@ -698,6 +750,33 @@ onBeforeMount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="mediaDetail.bangumi_id" class="media-overview-right">
|
||||
<div class="media-facts">
|
||||
<div v-if="mediaDetail.vote_average" class="media-ratings">
|
||||
<VRating
|
||||
v-model="mediaDetail.vote_average"
|
||||
density="compact"
|
||||
length="10"
|
||||
class="ma-2"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mediaDetail.bangumi_id" class="media-fact">
|
||||
<span>ID</span>
|
||||
<span class="media-fact-value">{{ mediaDetail.bangumi_id }}</span>
|
||||
</div>
|
||||
<div v-if="mediaDetail.original_title" class="media-fact">
|
||||
<span>原始标题</span>
|
||||
<span class="media-fact-value">{{ mediaDetail.original_title }}</span>
|
||||
</div>
|
||||
<div v-if="mediaDetail.release_date" class="media-fact border-b-0">
|
||||
<span>上映日期</span>
|
||||
<span class="media-fact-value">
|
||||
{{ mediaDetail.release_date }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id">
|
||||
<PersonCardSlideView
|
||||
@@ -715,6 +794,14 @@ onBeforeMount(() => {
|
||||
type="douban"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="mediaDetail.bangumi_id">
|
||||
<PersonCardSlideView
|
||||
:apipath="`bangumi/credits/${mediaDetail.bangumi_id}`"
|
||||
:linkurl="`/credits/bangumi/credits/${mediaDetail.bangumi_id}?title=演员阵容&type=bangumi`"
|
||||
title="演员阵容"
|
||||
type="bangumi"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id">
|
||||
<MediaCardSlideView
|
||||
:apipath="`tmdb/recommend/${mediaDetail.tmdb_id}/${mediaProps.type}`"
|
||||
@@ -729,6 +816,13 @@ onBeforeMount(() => {
|
||||
title="推荐"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="mediaDetail.bangumi_id">
|
||||
<MediaCardSlideView
|
||||
:apipath="`bangumi/recommend/${mediaDetail.bangumi_id}`"
|
||||
:linkurl="`/browse/bangumi/recommend/${mediaDetail.bangumi_id}?title=推荐`"
|
||||
title="推荐"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id">
|
||||
<MediaCardSlideView
|
||||
:apipath="`tmdb/similar/${mediaDetail.tmdb_id}/${mediaProps.type}`"
|
||||
@@ -739,13 +833,13 @@ onBeforeMount(() => {
|
||||
</div>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="!mediaDetail.tmdb_id && !mediaDetail.douban_id && isRefreshed"
|
||||
v-if="!mediaDetail.tmdb_id && !mediaDetail.douban_id && !mediaDetail.bangumi_id && isRefreshed"
|
||||
error-code="500"
|
||||
error-title="出错啦!"
|
||||
error-description="未识别到媒体信息。"
|
||||
/>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<SubscribeEditForm
|
||||
<SubscribeEditDialog
|
||||
v-model="subscribeEditDialog"
|
||||
:subid="subscribeId"
|
||||
@close="subscribeEditDialog = false"
|
||||
@@ -787,7 +881,7 @@ onBeforeMount(() => {
|
||||
padding-block-start: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
@media (width >= 1280px) {
|
||||
.media-header {
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
@@ -797,65 +891,66 @@ onBeforeMount(() => {
|
||||
.media-overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
padding-block: 2rem 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
@media (width >= 1024px) {
|
||||
.media-overview {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.media-poster {
|
||||
width: 8rem;
|
||||
overflow: hidden;
|
||||
border-radius: .25rem;
|
||||
--tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1);
|
||||
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
inline-size: 8rem;
|
||||
|
||||
--tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, 10%), 0 1px 2px -1px rgba(0, 0, 0, 10%);
|
||||
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
@media (width >= 1280px) {
|
||||
.media-poster {
|
||||
margin-right: 1rem;
|
||||
width: 13rem;
|
||||
inline-size: 13rem;
|
||||
margin-inline-end: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@media (width >= 768px) {
|
||||
.media-poster {
|
||||
width: 11rem;
|
||||
border-radius: .5rem;
|
||||
--tw-shadow: 0 25px 50px -12px rgba(0, 0, 0, .25);
|
||||
--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
inline-size: 11rem;
|
||||
|
||||
--tw-shadow: 0 25px 50px -12px rgba(0, 0, 0, 25%);
|
||||
--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
|
||||
}
|
||||
}
|
||||
|
||||
.media-title {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
flex-direction: column;
|
||||
margin-block-start: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
@media (width >= 1280px) {
|
||||
.media-title {
|
||||
margin-right: 1rem;
|
||||
margin-top: 0;
|
||||
text-align: left;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 1rem;
|
||||
text-align: start;
|
||||
}
|
||||
}
|
||||
|
||||
.media-title>h1 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
font-weight: 700;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
@media (width >= 1280px) {
|
||||
.media-title>h1 {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
@@ -863,23 +958,23 @@ onBeforeMount(() => {
|
||||
}
|
||||
|
||||
ul.media-crew {
|
||||
margin-top: 1.5rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2,minmax(0,1fr));
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: repeat(2,minmax(0,1fr));
|
||||
margin-block-start: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
@media (width >= 640px) {
|
||||
ul.media-crew {
|
||||
grid-template-columns: repeat(3,minmax(0,1fr));
|
||||
}
|
||||
}
|
||||
|
||||
ul.media-crew>li {
|
||||
grid-column: span 1/span 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-weight: 700;
|
||||
grid-column: span 1/span 1;
|
||||
}
|
||||
|
||||
a.crew-name {
|
||||
@@ -887,27 +982,27 @@ a.crew-name {
|
||||
}
|
||||
|
||||
.media-status {
|
||||
margin-bottom: .5rem;
|
||||
margin-block-end: .5rem;
|
||||
}
|
||||
|
||||
.media-attributes {
|
||||
margin-top: .25rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-block-start: .25rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
@media (width >= 1280px) {
|
||||
.media-attributes {
|
||||
margin-top: 0;
|
||||
justify-content: flex-start;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
margin-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
@media (width >= 640px) {
|
||||
.media-attributes {
|
||||
font-size: .875rem;
|
||||
line-height: 1.25rem;
|
||||
@@ -916,21 +1011,21 @@ a.crew-name {
|
||||
|
||||
.media-actions {
|
||||
position: relative;
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-block-start: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
@media (width >= 1280px) {
|
||||
.media-actions {
|
||||
margin-top: 0;
|
||||
margin-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
@media (width >= 640px) {
|
||||
.media-actions {
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-end;
|
||||
@@ -941,42 +1036,45 @@ a.crew-name {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
@media (width >= 1024px) {
|
||||
.media-overview-left {
|
||||
margin-right: 2rem;
|
||||
margin-inline-end: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.media-overview-right {
|
||||
margin-top: 2rem;
|
||||
width: 100%;
|
||||
inline-size: 100%;
|
||||
margin-block-start: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
@media (width >= 1024px) {
|
||||
.media-overview-right {
|
||||
margin-top: 0;
|
||||
width: 20rem;
|
||||
inline-size: 20rem;
|
||||
margin-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.media-facts {
|
||||
border-radius: 0.5rem;
|
||||
border-width: 1px;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(55 65 81/var(--tw-border-opacity));
|
||||
--tw-bg-opacity: 1;
|
||||
border-radius: 0.5rem;
|
||||
font-size: .875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25rem;
|
||||
|
||||
--tw-border-opacity: 1;
|
||||
--tw-bg-opacity: 1;
|
||||
--tw-text-opacity: 1;
|
||||
}
|
||||
|
||||
.media-ratings {
|
||||
border-bottom-width: 1px;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(55 65 81/var(--tw-border-opacity));
|
||||
padding: 0.5rem 1rem;
|
||||
border-block-end-width: 1px;
|
||||
font-weight: 500;
|
||||
padding-block: 0.5rem;
|
||||
padding-inline: 1rem;
|
||||
|
||||
--tw-border-opacity: 1;
|
||||
}
|
||||
|
||||
.media-ratings {
|
||||
@@ -988,19 +1086,21 @@ a.crew-name {
|
||||
.media-fact {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom-width: 1px;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(55 65 81/var(--tw-border-opacity));
|
||||
padding: 0.5rem 1rem;
|
||||
border-block-end-width: 1px;
|
||||
padding-block: 0.5rem;
|
||||
padding-inline: 1rem;
|
||||
|
||||
--tw-border-opacity: 1;
|
||||
}
|
||||
|
||||
.media-overview h2 {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
@media (width >= 640px) {
|
||||
.media-overview h2 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
@@ -1008,13 +1108,13 @@ a.crew-name {
|
||||
}
|
||||
|
||||
.tagline {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
font-style: italic;
|
||||
line-height: 1.75rem;
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
@media (width >= 1024px) {
|
||||
.tagline {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
|
||||
@@ -42,74 +42,67 @@ async function fetchData({ done }: { done: any }) {
|
||||
// 如果正在加载中,直接返回
|
||||
if (loading.value) {
|
||||
done('ok')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 设置加载中
|
||||
loading.value = true
|
||||
|
||||
// 加载到满屏或者加载出错
|
||||
if (!hasScroll()) {
|
||||
// 加载多次
|
||||
while (!hasScroll()) {
|
||||
// 设置加载中
|
||||
loading.value = true
|
||||
// 请求API
|
||||
currData.value = await api.get(props.apipath, {
|
||||
params: {
|
||||
page: page.value,
|
||||
},
|
||||
})
|
||||
|
||||
// 取消加载中
|
||||
loading.value = false
|
||||
// 标计为已请求完成
|
||||
isRefreshed.value = true
|
||||
if (currData.value.length === 0) {
|
||||
// 如果没有数据,跳出
|
||||
done('empty')
|
||||
} else {
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
// 页码+1
|
||||
page.value++
|
||||
// 返回加载成功
|
||||
done('ok')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
|
||||
// 页码+1
|
||||
page.value++
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 加载一次
|
||||
// 设置加载中
|
||||
loading.value = true
|
||||
// 请求API
|
||||
currData.value = await api.get(props.apipath, {
|
||||
params: {
|
||||
page: page.value,
|
||||
},
|
||||
})
|
||||
|
||||
// 标计为已请求完成
|
||||
isRefreshed.value = true
|
||||
if (currData.value.length === 0) {
|
||||
// 如果没有数据,跳出
|
||||
done('empty')
|
||||
} else {
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
// 页码+1
|
||||
page.value++
|
||||
// 返回加载成功
|
||||
done('ok')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
|
||||
// 页码+1
|
||||
page.value++
|
||||
// 取消加载中
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 取消加载中
|
||||
loading.value = false
|
||||
|
||||
// 返回加载成功
|
||||
done('ok')
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
|
||||
// 返回加载失败
|
||||
done('error')
|
||||
}
|
||||
@@ -117,16 +110,10 @@ async function fetchData({ done }: { done: any }) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
<LoadingBanner
|
||||
v-if="!isRefreshed"
|
||||
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
|
||||
>
|
||||
<VProgressCircular
|
||||
size="48"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
class="mt-12"
|
||||
/>
|
||||
<VInfiniteScroll
|
||||
mode="intersect"
|
||||
side="end"
|
||||
@@ -135,6 +122,7 @@ async function fetchData({ done }: { done: any }) {
|
||||
@load="fetchData"
|
||||
>
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div
|
||||
v-if="dataList.length > 0 && props.type === 'tmdb'"
|
||||
class="grid gap-4 grid-media-card mx-3"
|
||||
|
||||
@@ -3,6 +3,7 @@ import TmdbPersonCard from '@/components/cards/TmdbPersonCard.vue'
|
||||
import api from '@/api'
|
||||
import SlideView from '@/components/slide/SlideView.vue'
|
||||
import DoubanPersonCard from '@/components/cards/DoubanPersonCard.vue'
|
||||
import BangumiPersonCard from '@/components/cards/BangumiPersonCard.vue'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -12,6 +13,8 @@ const props = defineProps({
|
||||
type: String,
|
||||
})
|
||||
|
||||
provide('rankingPropsKey', reactive({...props}))
|
||||
|
||||
// 组件加载完成
|
||||
const componentLoaded = ref(false)
|
||||
|
||||
@@ -40,7 +43,6 @@ onMounted(fetchData)
|
||||
<template>
|
||||
<SlideView
|
||||
v-if="componentLoaded"
|
||||
v-bind="props"
|
||||
>
|
||||
<template #content>
|
||||
<template
|
||||
@@ -59,6 +61,12 @@ onMounted(fetchData)
|
||||
height="15rem"
|
||||
width="10rem"
|
||||
/>
|
||||
<BangumiPersonCard
|
||||
v-if="props.type === 'bangumi'"
|
||||
:person="data"
|
||||
height="15rem"
|
||||
width="10rem"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</SlideView>
|
||||
|
||||