mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-11 10:00:08 +08:00
Compare commits
377 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc36496aee | ||
|
|
1c8881d7a4 | ||
|
|
f6e8aacd0f | ||
|
|
79ddc39492 | ||
|
|
e63c5fb8e5 | ||
|
|
695f4827fd | ||
|
|
5a8b183c0f | ||
|
|
2845a889ed | ||
|
|
6333103050 | ||
|
|
cb6be91538 | ||
|
|
8cdd4b4af5 | ||
|
|
f4ec2029d9 | ||
|
|
b84b0f229f | ||
|
|
ef6a01a32f | ||
|
|
b451b8066a | ||
|
|
57efd516c5 | ||
|
|
d5979e6bf3 | ||
|
|
d75970cb2a | ||
|
|
ad4bb07cd7 | ||
|
|
9c558c3625 | ||
|
|
b467bb6c56 | ||
|
|
5cd021ea85 | ||
|
|
3d64382c9b | ||
|
|
6d5d4354d9 | ||
|
|
1b43446b5c | ||
|
|
7a9984f392 | ||
|
|
3c6fbfb106 | ||
|
|
bab46964ff | ||
|
|
661919f27a | ||
|
|
f3a03349b4 | ||
|
|
29791bf986 | ||
|
|
a06f0f29c6 | ||
|
|
b426d94180 | ||
|
|
5618d87e58 | ||
|
|
721d4f7685 | ||
|
|
7a025bcd38 | ||
|
|
24a8125621 | ||
|
|
468584a906 | ||
|
|
c056ec9377 | ||
|
|
87239994ae | ||
|
|
da09860a53 | ||
|
|
195ee5b2a6 | ||
|
|
32621ee299 | ||
|
|
40645180a0 | ||
|
|
59d4b1e544 | ||
|
|
8962a2c4ac | ||
|
|
6955f35ad1 | ||
|
|
1f722e7d7f | ||
|
|
5e587dfd88 | ||
|
|
2c687e5648 | ||
|
|
fdb0f63283 | ||
|
|
002e675b47 | ||
|
|
114f2a2dd0 | ||
|
|
c314d49e11 | ||
|
|
f5d0556808 | ||
|
|
27bc2a488f | ||
|
|
3a5999c341 | ||
|
|
a80a099ee7 | ||
|
|
68f458738a | ||
|
|
0f08f69738 | ||
|
|
a664066465 | ||
|
|
e6c11665a5 | ||
|
|
c119384c22 | ||
|
|
787cccb89f | ||
|
|
3df5d75c46 | ||
|
|
de6ad2479e | ||
|
|
632dfbaf10 | ||
|
|
68c14c24b8 | ||
|
|
d343d6d54d | ||
|
|
391a160f97 | ||
|
|
2d95110f75 | ||
|
|
e2ced8d36d | ||
|
|
a2b4511602 | ||
|
|
bdccc71b64 | ||
|
|
d7038a7d18 | ||
|
|
3998e1f685 | ||
|
|
5def9d5f81 | ||
|
|
c62937371e | ||
|
|
52843dcf97 | ||
|
|
ef5680d5ad | ||
|
|
bd3f24c84b | ||
|
|
399f85c52e | ||
|
|
14430e5c89 | ||
|
|
b703757d28 | ||
|
|
b642eabbb3 | ||
|
|
673596d8f9 | ||
|
|
b14e927e6c | ||
|
|
b03ae41ac7 | ||
|
|
92a0a9fe2f | ||
|
|
2511acfea1 | ||
|
|
361a4e0414 | ||
|
|
7e310236fe | ||
|
|
8705606c70 | ||
|
|
1f812a5258 | ||
|
|
e9264fa472 | ||
|
|
9164a1aefc | ||
|
|
30351a02ee | ||
|
|
7f918408a6 | ||
|
|
82f69bcad0 | ||
|
|
83b25eabbb | ||
|
|
47da6db51a | ||
|
|
eee092a7fd | ||
|
|
4c0f65fcbc | ||
|
|
acbd979569 | ||
|
|
52b68c18bf | ||
|
|
c6a74a75da | ||
|
|
e39eb62f52 | ||
|
|
4ecec4865d | ||
|
|
589007a22a | ||
|
|
4d4c9516c6 | ||
|
|
8491f26617 | ||
|
|
fcb3768a76 | ||
|
|
966bb769df | ||
|
|
dc8f7caab0 | ||
|
|
683346d652 | ||
|
|
f5fe39b2d2 | ||
|
|
51beb53f51 | ||
|
|
9d3f03c83a | ||
|
|
3eda1e4ef7 | ||
|
|
7181f83d66 | ||
|
|
fffad6e1b8 | ||
|
|
7f3906e5cb | ||
|
|
f836d175f0 | ||
|
|
f49cafc0cc | ||
|
|
a3ecad3436 | ||
|
|
a019dbd44e | ||
|
|
b316f960a1 | ||
|
|
d049b26825 | ||
|
|
852579c6ee | ||
|
|
5adcfa1877 | ||
|
|
f74458629e | ||
|
|
798f9249f8 | ||
|
|
6b4383643f | ||
|
|
256e8d0452 | ||
|
|
4112214c1f | ||
|
|
c183158ffe | ||
|
|
d523790c0f | ||
|
|
615ce34a72 | ||
|
|
1d59b3566c | ||
|
|
8071b90a2b | ||
|
|
8966584ca0 | ||
|
|
822711a530 | ||
|
|
1fe8aeb9e1 | ||
|
|
f021ba8a98 | ||
|
|
e4af05cd56 | ||
|
|
43d1cdb91c | ||
|
|
ed3f66681f | ||
|
|
c718d57e77 | ||
|
|
ce2e88a532 | ||
|
|
e60015a477 | ||
|
|
761e3ac76d | ||
|
|
2cf5535376 | ||
|
|
1a3d76d7b9 | ||
|
|
942a536289 | ||
|
|
fb1f6abf2e | ||
|
|
61ecb421e6 | ||
|
|
0098f9db2f | ||
|
|
2a348a7f18 | ||
|
|
838dff4758 | ||
|
|
7fb78a86ba | ||
|
|
07c815e943 | ||
|
|
9a4392eceb | ||
|
|
dc25e457eb | ||
|
|
d65ed9725c | ||
|
|
41ce095505 | ||
|
|
0e2290ce8a | ||
|
|
1b8db5b7f1 | ||
|
|
0cb42c1117 | ||
|
|
a289fe3da5 | ||
|
|
f53192cfa2 | ||
|
|
235e014542 | ||
|
|
211b05c643 | ||
|
|
3e1bd687f1 | ||
|
|
072fb01a04 | ||
|
|
81fbf4f5ba | ||
|
|
88c86f49bf | ||
|
|
3023214072 | ||
|
|
6ea6f89ab2 | ||
|
|
43c6672ab1 | ||
|
|
5cb56127d5 | ||
|
|
afa333243f | ||
|
|
047e99e27c | ||
|
|
eef6f37ace | ||
|
|
e8ede6e606 | ||
|
|
bfb4ea4123 | ||
|
|
51b0403f64 | ||
|
|
a5cd396de6 | ||
|
|
754bc3d3c9 | ||
|
|
07a2bcfb97 | ||
|
|
20222201ae | ||
|
|
a2a5ddd66c | ||
|
|
7bfc7602a7 | ||
|
|
b52b2cedad | ||
|
|
e93df6ba2c | ||
|
|
f9f29ccc3c | ||
|
|
3bd63ab7c8 | ||
|
|
301ea445bb | ||
|
|
475bee28c6 | ||
|
|
cd69920b41 | ||
|
|
83aab4e47d | ||
|
|
e12093c966 | ||
|
|
f21d546d18 | ||
|
|
26c8a6ba43 | ||
|
|
827bb8ba69 | ||
|
|
d1d2ef37d2 | ||
|
|
659594898b | ||
|
|
7569401fe0 | ||
|
|
dc9c86273d | ||
|
|
0e816e678a | ||
|
|
ff1c2a890c | ||
|
|
b802ad8a75 | ||
|
|
c11fb54b0b | ||
|
|
856dec3991 | ||
|
|
1d8c71da3f | ||
|
|
4152d0f715 | ||
|
|
0ead8cc052 | ||
|
|
2b5ecf3f8a | ||
|
|
de7aeeaeb3 | ||
|
|
994c52f6aa | ||
|
|
c6eb744257 | ||
|
|
4f462c5cfd | ||
|
|
60850970a8 | ||
|
|
3b2d5e45bb | ||
|
|
a604d3223a | ||
|
|
00bd1c45a1 | ||
|
|
4bc6dc7af7 | ||
|
|
3a8effd01f | ||
|
|
da67088e9c | ||
|
|
bacd4d23a3 | ||
|
|
020f667749 | ||
|
|
84652e8c82 | ||
|
|
565ebd936e | ||
|
|
3af127c66f | ||
|
|
849bb04249 | ||
|
|
09d647877f | ||
|
|
c868afbcbf | ||
|
|
3f033bfdec | ||
|
|
eca2f43e0e | ||
|
|
eeb17040f7 | ||
|
|
11dee1ed62 | ||
|
|
11a6232f83 | ||
|
|
9eded24e0e | ||
|
|
7548882148 | ||
|
|
4ad89955d4 | ||
|
|
a53553d658 | ||
|
|
2602cb0998 | ||
|
|
e402de29d5 | ||
|
|
a4cc1cc615 | ||
|
|
2d900baad1 | ||
|
|
17d6f6db05 | ||
|
|
7f3ba543b7 | ||
|
|
b33cb8a12c | ||
|
|
cfa8b78c2e | ||
|
|
4024daf189 | ||
|
|
77d7c3bb61 | ||
|
|
868ad57e12 | ||
|
|
eaf9724295 | ||
|
|
30e98de38a | ||
|
|
79c606370c | ||
|
|
b70597b5f5 | ||
|
|
a469282730 | ||
|
|
c3708360fa | ||
|
|
80f0560e0f | ||
|
|
84951cdc44 | ||
|
|
a72cb797ab | ||
|
|
6898e6b816 | ||
|
|
adb0b966ff | ||
|
|
81284b8d21 | ||
|
|
1a2b112e64 | ||
|
|
442c484dc9 | ||
|
|
2368c2f25f | ||
|
|
2320c58254 | ||
|
|
c9a4f36414 | ||
|
|
1df9a981b2 | ||
|
|
cbc917b834 | ||
|
|
240a568d16 | ||
|
|
eb1a847faa | ||
|
|
e09e57879b | ||
|
|
ddd2982971 | ||
|
|
621da7e4ef | ||
|
|
420827c389 | ||
|
|
ce9399b894 | ||
|
|
1bdd08c59a | ||
|
|
52d62dda81 | ||
|
|
d69e3cedae | ||
|
|
648bfcdd0d | ||
|
|
e4b8ff0a64 | ||
|
|
4576ef854d | ||
|
|
7323668db5 | ||
|
|
b11d709070 | ||
|
|
e25ac006c2 | ||
|
|
9e85e7edce | ||
|
|
804bcd440c | ||
|
|
6e4c896cb7 | ||
|
|
0cae89f8e3 | ||
|
|
59a7607c07 | ||
|
|
6dca0c157f | ||
|
|
d2aa5a64aa | ||
|
|
2620a55c5a | ||
|
|
14e33215f8 | ||
|
|
6862c2a744 | ||
|
|
fb215e8d87 | ||
|
|
f52ad2151b | ||
|
|
1a47b7d09d | ||
|
|
f292071a34 | ||
|
|
dd616d29e8 | ||
|
|
0509f18d66 | ||
|
|
f59fb119e4 | ||
|
|
46127cac1f | ||
|
|
c1abf76211 | ||
|
|
fe5b45d48d | ||
|
|
10ac1ebf7b | ||
|
|
e5d8144510 | ||
|
|
f9a65fba7a | ||
|
|
9b4138349b | ||
|
|
db9c9db5a9 | ||
|
|
24e992339f | ||
|
|
f26d1babf7 | ||
|
|
de3347cea1 | ||
|
|
e900fac4bd | ||
|
|
396218a467 | ||
|
|
d3a66ffa8c | ||
|
|
1e7ffb4c2e | ||
|
|
3df5d4c690 | ||
|
|
02a8331996 | ||
|
|
a29ad6a091 | ||
|
|
3ef1e65412 | ||
|
|
2deaec1fc6 | ||
|
|
c9b0b23d36 | ||
|
|
f06cca4ead | ||
|
|
a1990ce3e4 | ||
|
|
cbbf023030 | ||
|
|
307aa724eb | ||
|
|
cd6f37d80f | ||
|
|
b903134770 | ||
|
|
11effdd297 | ||
|
|
8873d8372d | ||
|
|
964aa29d12 | ||
|
|
b45a3c6539 | ||
|
|
b72b7ad0fb | ||
|
|
0e3106d8c1 | ||
|
|
71a6626fa9 | ||
|
|
68006bac88 | ||
|
|
34cbcc38a6 | ||
|
|
f4daee85c7 | ||
|
|
dd347039b5 | ||
|
|
0c9367d58a | ||
|
|
af10c4f1c3 | ||
|
|
52fbeda941 | ||
|
|
ace23af363 | ||
|
|
a097d89d68 | ||
|
|
77cb817523 | ||
|
|
c956e271a2 | ||
|
|
6413f30d18 | ||
|
|
789e748df0 | ||
|
|
c89edae375 | ||
|
|
f4dca4922b | ||
|
|
73b9ef5ee7 | ||
|
|
462742961a | ||
|
|
5a647fabfa | ||
|
|
2580ceac20 | ||
|
|
6905391785 | ||
|
|
7406226e68 | ||
|
|
af9ee00ad3 | ||
|
|
01f63a4b6b | ||
|
|
45e48755d3 | ||
|
|
f6c740738f | ||
|
|
29780cd4b7 | ||
|
|
a050b7c7d5 | ||
|
|
1f25387f81 | ||
|
|
36fb7b53ba | ||
|
|
354295ffda | ||
|
|
4f28018f4f | ||
|
|
5d37666bea | ||
|
|
705e81db7f | ||
|
|
57d5859727 | ||
|
|
06387ab33e |
45
.github/ISSUE_TEMPLATE/rfc.yml
vendored
Normal file
45
.github/ISSUE_TEMPLATE/rfc.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: 功能提案
|
||||||
|
description: Request for Comments
|
||||||
|
title: '[RFC]'
|
||||||
|
labels: ['RFC']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
一份提案(RFC)定位为 **「在某功能/重构的具体开发前,用于开发者间 review 技术设计/方案的文档」**,
|
||||||
|
目的是让协作的开发者间清晰的知道「要做什么」和「具体会怎么做」,以及所有的开发者都能公开透明的参与讨论;
|
||||||
|
以便评估和讨论产生的影响 (遗漏的考虑、向后兼容性、与现有功能的冲突),
|
||||||
|
因此提案侧重在对解决问题的 **方案、设计、步骤** 的描述上。
|
||||||
|
|
||||||
|
如果仅希望讨论是否添加或改进某功能本身,请使用 -> [Issue: 功能改进](https://github.com/jxxghp/MoviePilot/issues/new?assignees=&labels=feature+request&projects=&template=feature_request.yml&title=%5BFeature+Request%5D%3A+)
|
||||||
|
- type: textarea
|
||||||
|
id: background
|
||||||
|
attributes:
|
||||||
|
label: 背景 or 问题
|
||||||
|
description: 简单描述遇到的什么问题或需要改动什么。可以引用其他 issue、讨论、文档等。
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: goal
|
||||||
|
attributes:
|
||||||
|
label: '目标 & 方案简述'
|
||||||
|
description: 简单描述提案此提案实现后,**预期的目标效果**,以及简单大致描述会采取的方案/步骤,可能会/不会产生什么影响。
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: design
|
||||||
|
attributes:
|
||||||
|
label: '方案设计 & 实现步骤'
|
||||||
|
description: |
|
||||||
|
详细描述你设计的具体方案,可以考虑拆分列表或要点,一步步描述具体打算如何实现的步骤和相关细节。
|
||||||
|
这部份不需要一次性写完整,即使在创建完此提案 issue 后,依旧可以再次编辑修改。
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: alternative
|
||||||
|
attributes:
|
||||||
|
label: '替代方案 & 对比'
|
||||||
|
description: |
|
||||||
|
[可选] 为来实现目标效果,还考虑过什么其他方案,有什么对比?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
369
auto-imports.d.ts
vendored
369
auto-imports.d.ts
vendored
@@ -3,9 +3,11 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
// Generated by unplugin-auto-import
|
// Generated by unplugin-auto-import
|
||||||
|
// biome-ignore lint: disable
|
||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const EffectScope: typeof import('vue')['EffectScope']
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
|
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||||
const computed: typeof import('vue')['computed']
|
const computed: typeof import('vue')['computed']
|
||||||
@@ -20,13 +22,11 @@ declare global {
|
|||||||
const createGenericProjection: typeof import('@vueuse/math')['createGenericProjection']
|
const createGenericProjection: typeof import('@vueuse/math')['createGenericProjection']
|
||||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||||
const createLogger: typeof import('vuex')['createLogger']
|
const createPinia: typeof import('pinia')['createPinia']
|
||||||
const createNamespacedHelpers: typeof import('vuex')['createNamespacedHelpers']
|
|
||||||
const createProjection: typeof import('@vueuse/math')['createProjection']
|
const createProjection: typeof import('@vueuse/math')['createProjection']
|
||||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||||
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
||||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||||
const createStore: typeof import('vuex')['createStore']
|
|
||||||
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
||||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||||
const customRef: typeof import('vue')['customRef']
|
const customRef: typeof import('vue')['customRef']
|
||||||
@@ -34,9 +34,11 @@ declare global {
|
|||||||
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
||||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
const defineComponent: typeof import('vue')['defineComponent']
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
|
const defineStore: typeof import('pinia')['defineStore']
|
||||||
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||||
const effectScope: typeof import('vue')['effectScope']
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||||
|
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
const h: typeof import('vue')['h']
|
const h: typeof import('vue')['h']
|
||||||
@@ -52,10 +54,11 @@ declare global {
|
|||||||
const logicNot: typeof import('@vueuse/math')['logicNot']
|
const logicNot: typeof import('@vueuse/math')['logicNot']
|
||||||
const logicOr: typeof import('@vueuse/math')['logicOr']
|
const logicOr: typeof import('@vueuse/math')['logicOr']
|
||||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||||
const mapActions: typeof import('vuex')['mapActions']
|
const mapActions: typeof import('pinia')['mapActions']
|
||||||
const mapGetters: typeof import('vuex')['mapGetters']
|
const mapGetters: typeof import('pinia')['mapGetters']
|
||||||
const mapMutations: typeof import('vuex')['mapMutations']
|
const mapState: typeof import('pinia')['mapState']
|
||||||
const mapState: typeof import('vuex')['mapState']
|
const mapStores: typeof import('pinia')['mapStores']
|
||||||
|
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||||
const markRaw: typeof import('vue')['markRaw']
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
const nextTick: typeof import('vue')['nextTick']
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
const onActivated: typeof import('vue')['onActivated']
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
@@ -66,6 +69,7 @@ declare global {
|
|||||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||||
|
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
|
||||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||||
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||||
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||||
@@ -77,6 +81,7 @@ declare global {
|
|||||||
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
||||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
const onUpdated: typeof import('vue')['onUpdated']
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
|
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||||
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
||||||
const provide: typeof import('vue')['provide']
|
const provide: typeof import('vue')['provide']
|
||||||
const provideLocal: typeof import('@vueuse/core')['provideLocal']
|
const provideLocal: typeof import('@vueuse/core')['provideLocal']
|
||||||
@@ -96,9 +101,12 @@ declare global {
|
|||||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||||
|
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||||
|
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
const shallowRef: typeof import('vue')['shallowRef']
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
|
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||||
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
||||||
const templateRef: typeof import('@vueuse/core')['templateRef']
|
const templateRef: typeof import('@vueuse/core')['templateRef']
|
||||||
@@ -190,6 +198,7 @@ declare global {
|
|||||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||||
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||||
|
const useId: typeof import('vue')['useId']
|
||||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||||
const useImage: typeof import('@vueuse/core')['useImage']
|
const useImage: typeof import('@vueuse/core')['useImage']
|
||||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||||
@@ -209,6 +218,7 @@ declare global {
|
|||||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||||
const useMin: typeof import('@vueuse/math')['useMin']
|
const useMin: typeof import('@vueuse/math')['useMin']
|
||||||
|
const useModel: typeof import('vue')['useModel']
|
||||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||||
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
||||||
@@ -234,6 +244,7 @@ declare global {
|
|||||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||||
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||||
|
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
|
||||||
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||||
const useProjection: typeof import('@vueuse/math')['useProjection']
|
const useProjection: typeof import('@vueuse/math')['useProjection']
|
||||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||||
@@ -242,6 +253,7 @@ declare global {
|
|||||||
const useRound: typeof import('@vueuse/math')['useRound']
|
const useRound: typeof import('@vueuse/math')['useRound']
|
||||||
const useRoute: typeof import('vue-router')['useRoute']
|
const useRoute: typeof import('vue-router')['useRoute']
|
||||||
const useRouter: typeof import('vue-router')['useRouter']
|
const useRouter: typeof import('vue-router')['useRouter']
|
||||||
|
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
|
||||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||||
@@ -256,11 +268,11 @@ declare global {
|
|||||||
const useStepper: typeof import('@vueuse/core')['useStepper']
|
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||||
const useStorage: typeof import('@vueuse/core')['useStorage']
|
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||||
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||||
const useStore: typeof import('vuex')['useStore']
|
|
||||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||||
const useSum: typeof import('@vueuse/math')['useSum']
|
const useSum: typeof import('@vueuse/math')['useSum']
|
||||||
const useSupported: typeof import('@vueuse/core')['useSupported']
|
const useSupported: typeof import('@vueuse/core')['useSupported']
|
||||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||||
|
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||||
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
|
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
|
||||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||||
@@ -313,15 +325,17 @@ declare global {
|
|||||||
// for type re-export
|
// for type re-export
|
||||||
declare global {
|
declare global {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
import('vue')
|
import('vue')
|
||||||
}
|
}
|
||||||
|
|
||||||
// for vue template auto import
|
// for vue template auto import
|
||||||
import { UnwrapRef } from 'vue'
|
import { UnwrapRef } from 'vue'
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
interface GlobalComponents {}
|
interface GlobalComponents {}
|
||||||
interface ComponentCustomProperties {
|
interface ComponentCustomProperties {
|
||||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||||
|
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
||||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||||
@@ -336,13 +350,11 @@ declare module 'vue' {
|
|||||||
readonly createGenericProjection: UnwrapRef<typeof import('@vueuse/math')['createGenericProjection']>
|
readonly createGenericProjection: UnwrapRef<typeof import('@vueuse/math')['createGenericProjection']>
|
||||||
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
|
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
|
||||||
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
||||||
readonly createLogger: UnwrapRef<typeof import('vuex')['createLogger']>
|
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
|
||||||
readonly createNamespacedHelpers: UnwrapRef<typeof import('vuex')['createNamespacedHelpers']>
|
|
||||||
readonly createProjection: UnwrapRef<typeof import('@vueuse/math')['createProjection']>
|
readonly createProjection: UnwrapRef<typeof import('@vueuse/math')['createProjection']>
|
||||||
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
||||||
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
|
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
|
||||||
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
||||||
readonly createStore: UnwrapRef<typeof import('vuex')['createStore']>
|
|
||||||
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
|
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
|
||||||
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
|
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
|
||||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||||
@@ -350,9 +362,11 @@ declare module 'vue' {
|
|||||||
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
|
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
|
||||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||||
|
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
|
||||||
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
|
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
|
||||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||||
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
|
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
|
||||||
|
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||||
@@ -368,10 +382,11 @@ declare module 'vue' {
|
|||||||
readonly logicNot: UnwrapRef<typeof import('@vueuse/math')['logicNot']>
|
readonly logicNot: UnwrapRef<typeof import('@vueuse/math')['logicNot']>
|
||||||
readonly logicOr: UnwrapRef<typeof import('@vueuse/math')['logicOr']>
|
readonly logicOr: UnwrapRef<typeof import('@vueuse/math')['logicOr']>
|
||||||
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||||
readonly mapActions: UnwrapRef<typeof import('vuex')['mapActions']>
|
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||||
readonly mapGetters: UnwrapRef<typeof import('vuex')['mapGetters']>
|
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||||
readonly mapMutations: UnwrapRef<typeof import('vuex')['mapMutations']>
|
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
|
||||||
readonly mapState: UnwrapRef<typeof import('vuex')['mapState']>
|
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
|
||||||
|
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
|
||||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||||
@@ -382,6 +397,7 @@ declare module 'vue' {
|
|||||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||||
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
|
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
|
||||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||||
|
readonly onElementRemoval: UnwrapRef<typeof import('@vueuse/core')['onElementRemoval']>
|
||||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||||
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
|
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
|
||||||
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
|
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
|
||||||
@@ -393,6 +409,7 @@ declare module 'vue' {
|
|||||||
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
|
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
|
||||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||||
|
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
|
||||||
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
|
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
|
||||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||||
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
|
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
|
||||||
@@ -412,9 +429,12 @@ declare module 'vue' {
|
|||||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||||
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
|
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
|
||||||
|
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||||
|
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||||
|
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
|
||||||
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
|
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
|
||||||
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
|
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
|
||||||
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
|
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
|
||||||
@@ -506,6 +526,7 @@ declare module 'vue' {
|
|||||||
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
|
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
|
||||||
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
|
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
|
||||||
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
|
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
|
||||||
|
readonly useId: UnwrapRef<typeof import('vue')['useId']>
|
||||||
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
|
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
|
||||||
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
|
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
|
||||||
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
|
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
|
||||||
@@ -525,6 +546,7 @@ declare module 'vue' {
|
|||||||
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
|
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
|
||||||
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
|
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
|
||||||
readonly useMin: UnwrapRef<typeof import('@vueuse/math')['useMin']>
|
readonly useMin: UnwrapRef<typeof import('@vueuse/math')['useMin']>
|
||||||
|
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
|
||||||
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
|
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
|
||||||
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
|
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
|
||||||
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
|
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
|
||||||
@@ -550,6 +572,7 @@ declare module 'vue' {
|
|||||||
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
|
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
|
||||||
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
||||||
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||||
|
readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
|
||||||
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||||
readonly useProjection: UnwrapRef<typeof import('@vueuse/math')['useProjection']>
|
readonly useProjection: UnwrapRef<typeof import('@vueuse/math')['useProjection']>
|
||||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||||
@@ -558,6 +581,7 @@ declare module 'vue' {
|
|||||||
readonly useRound: UnwrapRef<typeof import('@vueuse/math')['useRound']>
|
readonly useRound: UnwrapRef<typeof import('@vueuse/math')['useRound']>
|
||||||
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||||
|
readonly useSSRWidth: UnwrapRef<typeof import('@vueuse/core')['useSSRWidth']>
|
||||||
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
|
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
|
||||||
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
|
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
|
||||||
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
|
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
|
||||||
@@ -572,11 +596,11 @@ declare module 'vue' {
|
|||||||
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
||||||
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
||||||
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
|
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
|
||||||
readonly useStore: UnwrapRef<typeof import('vuex')['useStore']>
|
|
||||||
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
|
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
|
||||||
readonly useSum: UnwrapRef<typeof import('@vueuse/math')['useSum']>
|
readonly useSum: UnwrapRef<typeof import('@vueuse/math')['useSum']>
|
||||||
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
|
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
|
||||||
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
||||||
|
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
|
||||||
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
|
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
|
||||||
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
|
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
|
||||||
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
|
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
|
||||||
@@ -626,313 +650,4 @@ declare module 'vue' {
|
|||||||
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
|
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
|
||||||
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
|
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']>
|
|
||||||
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']>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* prettier-ignore */
|
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// Generated by unplugin-vue-components
|
// Generated by unplugin-vue-components
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
export {}
|
export {}
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default']
|
DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default']
|
||||||
|
|||||||
25
index.html
25
index.html
@@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en"
|
||||||
|
style="overflow: hidden auto; min-block-size: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));background: var(--initial-loader-bg, #fff);">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="pragma" content="no-cache">
|
<meta http-equiv="pragma" content="no-cache">
|
||||||
@@ -29,9 +30,17 @@
|
|||||||
<meta name="HandheldFriendly" content="True" />
|
<meta name="HandheldFriendly" content="True" />
|
||||||
<meta name="MobileOptimized" content="320" />
|
<meta name="MobileOptimized" content="320" />
|
||||||
<link rel="stylesheet" type="text/css" href="/loader.css" />
|
<link rel="stylesheet" type="text/css" href="/loader.css" />
|
||||||
|
<script>
|
||||||
|
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
|
||||||
|
if (loaderColor)
|
||||||
|
document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
|
||||||
|
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
|
||||||
|
if (primaryColor)
|
||||||
|
document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body style="margin: 0;">
|
||||||
<div id="loading-bg">
|
<div id="loading-bg">
|
||||||
<div class="loading-logo">
|
<div class="loading-logo">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
@@ -146,16 +155,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<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 (primaryColor)
|
|
||||||
document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
120
package.json
120
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "moviepilot",
|
"name": "moviepilot",
|
||||||
"version": "2.0.5",
|
"version": "2.3.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"bin": "dist/service.js",
|
"bin": "dist/service.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -19,86 +19,92 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fullcalendar/core": "^6.1.8",
|
"@fullcalendar/core": "^6.1.15",
|
||||||
"@fullcalendar/daygrid": "^6.1.8",
|
"@fullcalendar/daygrid": "^6.1.15",
|
||||||
"@fullcalendar/interaction": "^6.1.7",
|
"@fullcalendar/interaction": "^6.1.15",
|
||||||
"@fullcalendar/list": "^6.1.7",
|
"@fullcalendar/list": "^6.1.15",
|
||||||
"@fullcalendar/timegrid": "^6.1.7",
|
"@fullcalendar/timegrid": "^6.1.15",
|
||||||
"@fullcalendar/vue3": "^6.1.8",
|
"@fullcalendar/vue3": "^6.1.15",
|
||||||
"@iconify/utils": "^2.1.22",
|
"@iconify/utils": "^2.2.1",
|
||||||
"@vueuse/core": "^10.1.2",
|
"@vue-flow/background": "^1.3.2",
|
||||||
"@vueuse/math": "^10.1.2",
|
"@vue-flow/controls": "^1.1.2",
|
||||||
"ace-builds": "^1.32.6",
|
"@vue-flow/core": "^1.42.1",
|
||||||
"apexcharts-clevision": "^3.28.5",
|
"@vue-flow/minimap": "^1.5.2",
|
||||||
"axios": "1.6.8",
|
"@vue-flow/node-resizer": "^1.4.0",
|
||||||
"colorthief": "^2.4.0",
|
"@vue-flow/node-toolbar": "^1.1.0",
|
||||||
"dayjs": "^1.11.10",
|
"@vue-js-cron/vuetify": "^5.0.9",
|
||||||
"express": "^4.18.2",
|
"@vueuse/core": "^12.4.0",
|
||||||
"express-http-proxy": "^2.0.0",
|
"@vueuse/math": "^12.4.0",
|
||||||
"lodash": "^4.17.21",
|
"ace-builds": "^1.37.4",
|
||||||
|
"apexcharts": "^4.0.0",
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"colorthief": "^2.6.0",
|
||||||
|
"copy-to-clipboard": "^3.3.3",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"express-http-proxy": "^2.1.1",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"qrcode.vue": "^3.4.1",
|
"pinia": "^3.0.1",
|
||||||
"sass": "^1.59.3",
|
"pinia-plugin-persistedstate": "^4.2.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"qrcode.vue": "^3.6.0",
|
||||||
"unplugin-vue-define-options": "^1.3.5",
|
"sass": "^1.83.4",
|
||||||
"vue": "^3.3.2",
|
"tailwindcss": "^ 3.4.17",
|
||||||
"vue-router": "^4.2.0",
|
"vue": "^3.5.13",
|
||||||
"vue-toast-notification": "^3",
|
"vue-router": "^4.5.0",
|
||||||
|
"vue-toast-notification": "^3.1.3",
|
||||||
"vue3-ace-editor": "^2.2.4",
|
"vue3-ace-editor": "^2.2.4",
|
||||||
"vue3-apexcharts": "^1.4.1",
|
"vue3-apexcharts": "^1.8.0",
|
||||||
"vue3-perfect-scrollbar": "^2.0.0",
|
"vue3-perfect-scrollbar": "^2.0.0",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "^4.1.0",
|
||||||
"vuetify": "3.6.8",
|
"vuetify": "3.7.3",
|
||||||
"vuetify-use-dialog": "^0.6.11",
|
"vuetify-use-dialog": "^0.6.11",
|
||||||
"vuex": "^4.1.0",
|
|
||||||
"vuex-persistedstate": "^4.1.0",
|
|
||||||
"webfontloader": "^1.6.28"
|
"webfontloader": "^1.6.28"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config-vue": "^0.43.1",
|
|
||||||
"@iconify-json/mdi": "^1.1.52",
|
"@iconify-json/mdi": "^1.1.52",
|
||||||
"@iconify/tools": "^4.0.4",
|
"@iconify/tools": "^4.0.4",
|
||||||
"@iconify/vue": "4.1.1",
|
"@iconify/vue": "^4.3.0",
|
||||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
||||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||||
"@types/lodash": "^4.14.197",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^20.1.4",
|
"@types/node": "^20.1.4",
|
||||||
"@types/webfontloader": "^1.6.34",
|
"@types/webfontloader": "^1.6.34",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
"@typescript-eslint/eslint-plugin": "^8.20.0",
|
||||||
"@typescript-eslint/parser": "^7.5.0",
|
"@typescript-eslint/parser": "^8.20.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
|
||||||
"eslint-import-resolver-typescript": "^3.5.1",
|
"eslint-import-resolver-typescript": "^3.5.1",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-promise": "^6.0.1",
|
"eslint-plugin-promise": "^7.2.1",
|
||||||
"eslint-plugin-regex": "^1.10.0",
|
"eslint-plugin-regex": "^1.10.0",
|
||||||
"eslint-plugin-sonarjs": "^0.25.1",
|
"eslint-plugin-sonarjs": "^3.0.1",
|
||||||
"eslint-plugin-unicorn": "^52.0.0",
|
"eslint-plugin-unicorn": "^56.0.1",
|
||||||
"eslint-plugin-vue": "^9.12.0",
|
"eslint-plugin-vue": "^9.12.0",
|
||||||
"postcss": "8",
|
"postcss": "^8.5.1",
|
||||||
"postcss-html": "^1.5.0",
|
"postcss-html": "^1.5.0",
|
||||||
"stylelint": "16.3.1",
|
"stylelint": "^16.13.2",
|
||||||
"stylelint-config-idiomatic-order": "10.0.0",
|
"stylelint-config-idiomatic-order": "^10.0.0",
|
||||||
"stylelint-config-standard-scss": "13.1.0",
|
"stylelint-config-standard-scss": "^14.0.0",
|
||||||
"stylelint-use-logical-spec": "5.0.1",
|
"stylelint-use-logical-spec": "5.0.1",
|
||||||
|
"terser": "^5.36.0",
|
||||||
"type-fest": "^4.15.0",
|
"type-fest": "^4.15.0",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"unplugin-auto-import": "^0.17.5",
|
"unplugin-auto-import": "^19.0.0",
|
||||||
"unplugin-vue-components": "^0.26.0",
|
"unplugin-vue-components": "^28.0.0",
|
||||||
"vite": "^5.2.8",
|
"unplugin-vue-define-options": "^1.5.3",
|
||||||
|
"vite": "^5.4.11",
|
||||||
"vite-plugin-pages": "^0.32.1",
|
"vite-plugin-pages": "^0.32.1",
|
||||||
"vite-plugin-pwa": "^0.20.0",
|
"vite-plugin-pwa": "^0.21.1",
|
||||||
"vite-plugin-vue-layouts": "^0.11.0",
|
"vite-plugin-vue-layouts": "^0.11.0",
|
||||||
"vite-plugin-vuetify": "2.0.3",
|
"vite-plugin-vuetify": "2.0.4",
|
||||||
"vue-shepherd": "^3.0.0",
|
"vue-shepherd": "^4.1.0",
|
||||||
"vue-tsc": "^2.0.10"
|
"vue-tsc": "^2.0.10",
|
||||||
|
"workbox-build": "^7.3.0",
|
||||||
|
"workbox-window": "^7.3.0"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.18",
|
"packageManager": "yarn@1.22.18"
|
||||||
"resolutions": {
|
}
|
||||||
"postcss": "8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
overflow: hidden auto;
|
|
||||||
background: var(--initial-loader-bg, #fff);
|
|
||||||
min-block-size: calc(100% + env(safe-area-inset-top));
|
|
||||||
}
|
|
||||||
|
|
||||||
#loading-bg {
|
#loading-bg {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
z-index: 999;
|
z-index: 9999;
|
||||||
display: block;
|
display: block;
|
||||||
background: var(--initial-loader-bg, #fff);
|
background: var(--initial-loader-bg, #fff);
|
||||||
block-size: 100vh;
|
block-size: 100vh;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { ThemeSwitcherTheme } from '@layouts/types'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import { VAceEditor } from 'vue3-ace-editor'
|
import { saveLocalTheme } from '../utils/theme'
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
@@ -103,8 +103,7 @@ function updateTheme() {
|
|||||||
savedTheme.value = theme
|
savedTheme.value = theme
|
||||||
themeTransition()
|
themeTransition()
|
||||||
// 保存主题到本地
|
// 保存主题到本地
|
||||||
localStorage.setItem('theme', theme)
|
saveLocalTheme(theme, globalTheme)
|
||||||
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换主题
|
// 切换主题
|
||||||
@@ -115,7 +114,7 @@ function changeTheme(theme: string) {
|
|||||||
// 保存主题到服务端
|
// 保存主题到服务端
|
||||||
try {
|
try {
|
||||||
api.post('/user/config/Layout', {
|
api.post('/user/config/Layout', {
|
||||||
theme: nextTheme
|
theme: nextTheme,
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存主题到服务端失败')
|
console.error('保存主题到服务端失败')
|
||||||
@@ -176,7 +175,7 @@ async function saveCustomCSS() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.success) $toast.success('自定义CSS保存成功!')
|
if (result.success) $toast.success('自定义CSS保存成功,请刷新页面生效!')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存自定义 CSS 到服务端失败')
|
console.error('保存自定义 CSS 到服务端失败')
|
||||||
}
|
}
|
||||||
@@ -210,7 +209,7 @@ onMounted(() => {
|
|||||||
</VList>
|
</VList>
|
||||||
</VMenu>
|
</VMenu>
|
||||||
<!-- 自定义 CSS -- -->
|
<!-- 自定义 CSS -- -->
|
||||||
<VDialog v-model="cssDialog" persistent max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
<VDialog v-if="cssDialog" v-model="cssDialog" persistent max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||||
<VCard title="自定义主题风格">
|
<VCard title="自定义主题风格">
|
||||||
<DialogCloseBtn @click="cssDialog = false" />
|
<DialogCloseBtn @click="cssDialog = false" />
|
||||||
<VDivider />
|
<VDivider />
|
||||||
|
|||||||
@@ -212,10 +212,6 @@ h6,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-data-table-footer {
|
|
||||||
margin-block-start: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 👉 v-field
|
// 👉 v-field
|
||||||
.v-field:hover .v-field__outline {
|
.v-field:hover .v-field__outline {
|
||||||
--v-field-border-opacity: var(--v-medium-emphasis-opacity);
|
--v-field-border-opacity: var(--v-medium-emphasis-opacity);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.auth-wrapper {
|
.auth-wrapper {
|
||||||
min-block-size: calc(var(--vh, 1vh) * 100 + env(safe-area-inset-top));
|
min-block-size: calc(var(--vh, 1vh) * 100 + env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-footer-mask {
|
.auth-footer-mask {
|
||||||
|
|||||||
@@ -176,10 +176,6 @@
|
|||||||
th {
|
th {
|
||||||
background: rgb(var(--v-table-header-background)) !important;
|
background: rgb(var(--v-table-header-background)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-data-table-footer {
|
|
||||||
margin-block-start: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 👉 Pagination
|
// 👉 Pagination
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
@use "sass:map";
|
||||||
@use "vuetify/lib/styles/settings" as vuetify_settings;
|
@use "vuetify/lib/styles/settings" as vuetify_settings;
|
||||||
|
|
||||||
@mixin avatar-font-sizes($map: $avatar-sizes) {
|
@mixin avatar-font-sizes($map: $avatar-sizes) {
|
||||||
@each $sizeName, $multiplier in vuetify_settings.$size-scales {
|
@each $sizeName, $multiplier in vuetify_settings.$size-scales {
|
||||||
/* stylelint-disable-next-line scss/no-global-function-names */
|
/* stylelint-disable-next-line scss/no-global-function-names */
|
||||||
$size: map-get($map, $sizeName);
|
$size: map.get($map, $sizeName);
|
||||||
|
|
||||||
&.v-avatar--size-#{$sizeName} {
|
&.v-avatar--size-#{$sizeName} {
|
||||||
font-size: #{$size}px;
|
font-size: #{$size}px;
|
||||||
|
|||||||
@@ -92,8 +92,7 @@
|
|||||||
.fc-header-toolbar {
|
.fc-header-toolbar {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin: 1.25rem;
|
margin: 1.25rem;
|
||||||
column-gap: 0.5rem;
|
gap: 1rem 0.5rem;
|
||||||
row-gap: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-toolbar-chunk {
|
.fc-toolbar-chunk {
|
||||||
@@ -238,7 +237,7 @@
|
|||||||
inline-size: 1.5625rem;
|
inline-size: 1.5625rem;
|
||||||
margin-inline-end: 0.25rem;
|
margin-inline-end: 0.25rem;
|
||||||
|
|
||||||
@media (max-width: 1264px) {
|
@media (width <= 1264px) {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ export function useDefer(maxFrameCount = 1) {
|
|||||||
const refreshFrameCount = () => {
|
const refreshFrameCount = () => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
frameCount.value++
|
frameCount.value++
|
||||||
if (frameCount.value < maxFrameCount)
|
if (frameCount.value < maxFrameCount) refreshFrameCount()
|
||||||
refreshFrameCount()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
refreshFrameCount()
|
refreshFrameCount()
|
||||||
@@ -19,3 +18,9 @@ export function useDefer(maxFrameCount = 1) {
|
|||||||
return frameCount.value >= showInFrameCount
|
return frameCount.value >= showInFrameCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ensureRenderComplete(callback: () => void) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(callback)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -153,3 +153,12 @@ export function formatDateDifference(dateString: string): string {
|
|||||||
if (!dateString) return ''
|
if (!dateString) return ''
|
||||||
return dayjs(dateString).fromNow()
|
return dayjs(dateString).fromNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化评份,如为10及以下的数按原值显示,否则格式化为xxM、xxK显示
|
||||||
|
export function formatRating(rating: number): string {
|
||||||
|
if (!rating) return ''
|
||||||
|
if (rating <= 10) return rating.toString()
|
||||||
|
if (rating < 1000) return rating.toLocaleString()
|
||||||
|
if (rating < 1000 * 1000) return `${(rating / 1000).toFixed(1)}K`
|
||||||
|
return `${(rating / 1000 / 1000).toFixed(1)}M`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import copy from 'copy-to-clipboard'
|
||||||
|
|
||||||
// 请求和获取剪贴板内容
|
// 请求和获取剪贴板内容
|
||||||
export async function getClipboardContent() {
|
export async function getClipboardContent() {
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
@@ -13,20 +15,10 @@ export async function getClipboardContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将内容复制到剪贴板,兼容非安全域场景
|
// 将内容复制到剪贴板
|
||||||
export async function copyToClipboard(content: string) {
|
export async function copyToClipboard(content: string) {
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
const success = copy(content)
|
||||||
await navigator.clipboard.writeText(content)
|
return success
|
||||||
} else {
|
|
||||||
const input = document.createElement('textarea')
|
|
||||||
input.value = content
|
|
||||||
document.body.appendChild(input)
|
|
||||||
// 阻止事件冒泡到其他元素,确保 focusin 事件只在 textarea 元素上处理,不会影响其他元素
|
|
||||||
input.addEventListener('focusin', e => e.stopPropagation())
|
|
||||||
input.select()
|
|
||||||
document.execCommand('copy')
|
|
||||||
document.body.removeChild(input)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// VAPID公钥转Uint8Array
|
// VAPID公钥转Uint8Array
|
||||||
|
|||||||
6
src/@core/utils/theme.ts
Normal file
6
src/@core/utils/theme.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export function saveLocalTheme(name: string, theme: any) {
|
||||||
|
// 存储主题到本地
|
||||||
|
localStorage.setItem('theme', name)
|
||||||
|
localStorage.setItem('materio-initial-loader-bg', theme.current.value.colors.background)
|
||||||
|
localStorage.setItem('materio-initial-loader-color', theme.current.value.colors.primary)
|
||||||
|
}
|
||||||
121
src/@core/utils/workflow.ts
Normal file
121
src/@core/utils/workflow.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { useVueFlow } from '@vue-flow/core'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string} - A unique id.
|
||||||
|
*/
|
||||||
|
function getId() {
|
||||||
|
// 生成以act_开头的唯一id
|
||||||
|
return 'act_' + Math.random().toString(36).substr(2, 9)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In a real world scenario you'd want to avoid creating refs in a global scope like this as they might not be cleaned up properly.
|
||||||
|
* @type {{draggedData: Ref<any>, isDragOver: Ref<boolean>, isDragging: Ref<boolean>}}
|
||||||
|
*/
|
||||||
|
const state = {
|
||||||
|
/**
|
||||||
|
* The type of the node being dragged.
|
||||||
|
*/
|
||||||
|
draggedData: ref<any | null>({}),
|
||||||
|
isDragOver: ref(false),
|
||||||
|
isDragging: ref(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useDragAndDrop() {
|
||||||
|
const { draggedData, isDragOver, isDragging } = state
|
||||||
|
|
||||||
|
const { addNodes, screenToFlowCoordinate, onNodesInitialized, updateNode } = useVueFlow()
|
||||||
|
|
||||||
|
watch(isDragging, dragging => {
|
||||||
|
document.body.style.userSelect = dragging ? 'none' : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
function onDragStart(event: any, data: any) {
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.setData('application/vueflow', data)
|
||||||
|
event.dataTransfer.effectAllowed = 'move'
|
||||||
|
}
|
||||||
|
|
||||||
|
draggedData.value = data
|
||||||
|
isDragging.value = true
|
||||||
|
|
||||||
|
document.addEventListener('drop', onDragEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the drag over event.
|
||||||
|
*
|
||||||
|
* @param {DragEvent} event
|
||||||
|
*/
|
||||||
|
function onDragOver(event: any) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (draggedData.value) {
|
||||||
|
isDragOver.value = true
|
||||||
|
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.dropEffect = 'move'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragLeave() {
|
||||||
|
isDragOver.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragEnd() {
|
||||||
|
isDragging.value = false
|
||||||
|
isDragOver.value = false
|
||||||
|
draggedData.value = null
|
||||||
|
document.removeEventListener('drop', onDragEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the drop event.
|
||||||
|
*
|
||||||
|
* @param {DragEvent} event
|
||||||
|
*/
|
||||||
|
function onDrop(event: any) {
|
||||||
|
const position = screenToFlowCoordinate({
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
})
|
||||||
|
|
||||||
|
const nodeId = getId()
|
||||||
|
|
||||||
|
const newNode = {
|
||||||
|
id: nodeId,
|
||||||
|
type: draggedData.value?.type,
|
||||||
|
name: draggedData.value?.name,
|
||||||
|
description: draggedData.value?.description,
|
||||||
|
position,
|
||||||
|
data: draggedData.value?.data,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Align node position after drop, so it's centered to the mouse
|
||||||
|
*
|
||||||
|
* We can hook into events even in a callback, and we can remove the event listener after it's been called.
|
||||||
|
*/
|
||||||
|
const { off } = onNodesInitialized(() => {
|
||||||
|
updateNode(nodeId, node => ({
|
||||||
|
position: { x: node.position.x - node.dimensions.width / 2, y: node.position.y - node.dimensions.height / 2 },
|
||||||
|
}))
|
||||||
|
|
||||||
|
off()
|
||||||
|
})
|
||||||
|
|
||||||
|
addNodes(newNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
draggedData,
|
||||||
|
isDragOver,
|
||||||
|
isDragging,
|
||||||
|
onDragStart,
|
||||||
|
onDragLeave,
|
||||||
|
onDragOver,
|
||||||
|
onDrop,
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/@iconify/tsconfig.tsbuildinfo
Normal file
1
src/@iconify/tsconfig.tsbuildinfo
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"root":["./build-icons.ts"],"version":"5.7.3"}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
@use "@configured-variables" as variables;
|
@use "@configured-variables" as variables;
|
||||||
|
|
||||||
html {
|
html {
|
||||||
min-height: calc(100% + env(safe-area-inset-top));
|
min-height: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
||||||
background: rgb(var(--v-theme-background));
|
background: rgb(var(--v-theme-background));
|
||||||
overflow-y: overlay;
|
overflow-y: overlay;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-height: calc(100% + env(safe-area-inset-top))
|
min-height: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ $layout-horizontal-nav-layout-navbar-z-index: 11 !default;
|
|||||||
$layout-boxed-content-width: 90rem !default;
|
$layout-boxed-content-width: 90rem !default;
|
||||||
|
|
||||||
// 👉Footer
|
// 👉Footer
|
||||||
$layout-vertical-nav-footer-height: 0rem !default;
|
$layout-vertical-nav-footer-height: 3.5rem !default;
|
||||||
|
|
||||||
// 👉 Layout overlay
|
// 👉 Layout overlay
|
||||||
$layout-overlay-z-index: 11 !default;
|
$layout-overlay-z-index: 11 !default;
|
||||||
|
|||||||
33
src/App.vue
33
src/App.vue
@@ -1,15 +1,16 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useTheme } from 'vuetify'
|
import { useTheme } from 'vuetify'
|
||||||
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||||
|
import { ensureRenderComplete, removeEl } from './@core/utils/dom'
|
||||||
const { global: globalTheme } = useTheme()
|
|
||||||
|
|
||||||
// 生效主题
|
// 生效主题
|
||||||
async function setTheme() {
|
const { global: globalTheme } = useTheme()
|
||||||
let themeValue = localStorage.getItem('theme') || 'light'
|
let themeValue = localStorage.getItem('theme') || 'light'
|
||||||
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
|
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
|
||||||
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
|
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
|
||||||
}
|
|
||||||
|
// 显示状态
|
||||||
|
const show = ref(false)
|
||||||
|
|
||||||
// ApexCharts 全局配置
|
// ApexCharts 全局配置
|
||||||
declare global {
|
declare global {
|
||||||
@@ -41,14 +42,24 @@ if (window.Apex) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面加载时,加载当前用户数据
|
onMounted(() => {
|
||||||
onBeforeMount(async () => {
|
ensureRenderComplete(() => {
|
||||||
setTheme()
|
nextTick(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// 移除加载动画
|
||||||
|
removeEl('#loading-bg')
|
||||||
|
// 将background属性从html的style中移除
|
||||||
|
document.documentElement.style.removeProperty('background')
|
||||||
|
// 显示页面
|
||||||
|
show.value = true
|
||||||
|
}, 1500)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VApp>
|
<VApp v-show="show">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</VApp>
|
</VApp>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -71,3 +71,10 @@ export const storageDict = storageOptions.reduce((dict, item) => {
|
|||||||
dict[item.value] = item.title
|
dict[item.value] = item.title
|
||||||
return dict
|
return dict
|
||||||
}, {} as Record<string, string>)
|
}, {} as Record<string, string>)
|
||||||
|
|
||||||
|
export const transferTypeOptions = [
|
||||||
|
{ title: '复制', value: 'copy' },
|
||||||
|
{ title: '移动', value: 'move' },
|
||||||
|
{ title: '硬链接', value: 'link' },
|
||||||
|
{ title: '软链接', value: 'softlink' },
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import store from '@/store'
|
import { useAuthStore } from '@/stores'
|
||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
@@ -9,10 +9,12 @@ const api = axios.create({
|
|||||||
|
|
||||||
// 添加请求拦截器
|
// 添加请求拦截器
|
||||||
api.interceptors.request.use(config => {
|
api.interceptors.request.use(config => {
|
||||||
|
// 认证 Store
|
||||||
|
const authStore = useAuthStore()
|
||||||
// 在请求头中添加token
|
// 在请求头中添加token
|
||||||
const token = store.state.auth.token
|
if (authStore.token) {
|
||||||
if (token) config.headers.Authorization = `Bearer ${token}`
|
config.headers.Authorization = `Bearer ${authStore.token}`
|
||||||
|
}
|
||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -26,8 +28,10 @@ api.interceptors.response.use(
|
|||||||
// 请求超时
|
// 请求超时
|
||||||
return Promise.reject(new Error(error))
|
return Promise.reject(new Error(error))
|
||||||
} else if (error.response.status === 403) {
|
} else if (error.response.status === 403) {
|
||||||
|
// 认证 Store
|
||||||
|
const authStore = useAuthStore()
|
||||||
// 清除登录状态信息
|
// 清除登录状态信息
|
||||||
store.dispatch('auth/logout')
|
authStore.logout()
|
||||||
// token验证失败,跳转到登录页面
|
// token验证失败,跳转到登录页面
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
|
|||||||
160
src/api/types.ts
160
src/api/types.ts
@@ -14,6 +14,10 @@ export interface Subscribe {
|
|||||||
tmdbid: number
|
tmdbid: number
|
||||||
// 豆瓣ID
|
// 豆瓣ID
|
||||||
doubanid?: string
|
doubanid?: string
|
||||||
|
// Bangumi ID
|
||||||
|
bangumiid?: string
|
||||||
|
// 其它媒体ID
|
||||||
|
mediaid?: string
|
||||||
// 季号
|
// 季号
|
||||||
season?: number
|
season?: number
|
||||||
// 海报
|
// 海报
|
||||||
@@ -44,7 +48,7 @@ export interface Subscribe {
|
|||||||
lack_episode?: number
|
lack_episode?: number
|
||||||
// 附加信息
|
// 附加信息
|
||||||
note?: string
|
note?: string
|
||||||
// 状态:N-新建, R-订阅中
|
// 状态:N-新建 R-订阅中 P-待定 S-暂停
|
||||||
state: string
|
state: string
|
||||||
// 最后更新时间
|
// 最后更新时间
|
||||||
last_update: string
|
last_update: string
|
||||||
@@ -73,7 +77,7 @@ export interface Subscribe {
|
|||||||
// 过滤规则组
|
// 过滤规则组
|
||||||
filter_groups?: string[]
|
filter_groups?: string[]
|
||||||
// 下载器
|
// 下载器
|
||||||
downloader?: string
|
downloader: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 订阅分享
|
// 订阅分享
|
||||||
@@ -88,6 +92,8 @@ export interface SubscribeShare {
|
|||||||
share_comment?: string
|
share_comment?: string
|
||||||
// 分享人
|
// 分享人
|
||||||
share_user?: string
|
share_user?: string
|
||||||
|
// 分享人唯一ID
|
||||||
|
share_uid?: string
|
||||||
// 订阅名称
|
// 订阅名称
|
||||||
name?: string
|
name?: string
|
||||||
// 订阅年份
|
// 订阅年份
|
||||||
@@ -184,7 +190,7 @@ export interface TransferHistory {
|
|||||||
export interface MediaInfo {
|
export interface MediaInfo {
|
||||||
// 来源:themoviedb、douban、bangumi
|
// 来源:themoviedb、douban、bangumi
|
||||||
source?: string
|
source?: string
|
||||||
// 类型 电影、电视剧
|
// 类型 电影、电视剧、合集
|
||||||
type?: string
|
type?: string
|
||||||
// 媒体标题
|
// 媒体标题
|
||||||
title?: string
|
title?: string
|
||||||
@@ -204,6 +210,12 @@ export interface MediaInfo {
|
|||||||
douban_id?: string
|
douban_id?: string
|
||||||
// Bangumi ID
|
// Bangumi ID
|
||||||
bangumi_id?: string
|
bangumi_id?: string
|
||||||
|
// 合集ID
|
||||||
|
collection_id?: number
|
||||||
|
// 其它媒体ID前缀
|
||||||
|
mediaid_prefix?: string
|
||||||
|
// 其它媒体ID值
|
||||||
|
media_id?: string
|
||||||
// 媒体原语种
|
// 媒体原语种
|
||||||
original_language?: string
|
original_language?: string
|
||||||
// 媒体原发行标题
|
// 媒体原发行标题
|
||||||
@@ -276,6 +288,24 @@ export interface MediaInfo {
|
|||||||
names?: string[]
|
names?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 季信息
|
||||||
|
export interface MediaSeason {
|
||||||
|
// 上映日期
|
||||||
|
air_date?: string
|
||||||
|
// 总集数
|
||||||
|
episode_count?: number
|
||||||
|
// 季名称
|
||||||
|
name?: string
|
||||||
|
// 描述
|
||||||
|
overview?: string
|
||||||
|
// 海报
|
||||||
|
poster_path?: string
|
||||||
|
// 季号
|
||||||
|
season_number?: number
|
||||||
|
// 评分
|
||||||
|
vote_average?: number
|
||||||
|
}
|
||||||
|
|
||||||
// TMDB季信息
|
// TMDB季信息
|
||||||
export interface TmdbSeason {
|
export interface TmdbSeason {
|
||||||
// 上映日期
|
// 上映日期
|
||||||
@@ -390,7 +420,7 @@ export interface Site {
|
|||||||
// RSS地址
|
// RSS地址
|
||||||
rss?: string
|
rss?: string
|
||||||
// 下载器
|
// 下载器
|
||||||
downloader?: string
|
downloader: string
|
||||||
// Cookie
|
// Cookie
|
||||||
cookie?: string
|
cookie?: string
|
||||||
// ApiKey
|
// ApiKey
|
||||||
@@ -1051,7 +1081,7 @@ export interface TransferDirectoryConf {
|
|||||||
// 监控模式 fast/compatibility
|
// 监控模式 fast/compatibility
|
||||||
monitor_mode?: string
|
monitor_mode?: string
|
||||||
// 整理方式 move/copy/link/softlink
|
// 整理方式 move/copy/link/softlink
|
||||||
transfer_type?: string
|
transfer_type: string
|
||||||
// 文件覆盖模式 always/size/never/latest
|
// 文件覆盖模式 always/size/never/latest
|
||||||
overwrite_mode?: string
|
overwrite_mode?: string
|
||||||
// 整理到媒体库目录
|
// 整理到媒体库目录
|
||||||
@@ -1100,6 +1130,7 @@ export interface FilterRuleGroup {
|
|||||||
category?: string
|
category?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 订阅下载文件详情
|
||||||
export interface SubscribeDownloadFileInfo {
|
export interface SubscribeDownloadFileInfo {
|
||||||
// 种子名称
|
// 种子名称
|
||||||
torrent_title?: string
|
torrent_title?: string
|
||||||
@@ -1113,6 +1144,7 @@ export interface SubscribeDownloadFileInfo {
|
|||||||
file_path?: string
|
file_path?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 订阅媒体库文件详情
|
||||||
export interface SubscribeLibraryFileInfo {
|
export interface SubscribeLibraryFileInfo {
|
||||||
// 存储
|
// 存储
|
||||||
storage?: string
|
storage?: string
|
||||||
@@ -1120,6 +1152,7 @@ export interface SubscribeLibraryFileInfo {
|
|||||||
file_path?: string
|
file_path?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 订阅集详情
|
||||||
export interface SubscribeEpisodeInfo {
|
export interface SubscribeEpisodeInfo {
|
||||||
// 标题
|
// 标题
|
||||||
title?: string
|
title?: string
|
||||||
@@ -1133,9 +1166,126 @@ export interface SubscribeEpisodeInfo {
|
|||||||
library?: SubscribeLibraryFileInfo[]
|
library?: SubscribeLibraryFileInfo[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 订阅详情
|
||||||
export interface SubscrbieInfo {
|
export interface SubscrbieInfo {
|
||||||
// 订阅信息
|
// 订阅信息
|
||||||
subscribe: Subscribe
|
subscribe: Subscribe
|
||||||
// 集信息 {集号: {download: 文件路径,library: 文件路径, backdrop: url, title: 标题, description: 描述}}
|
// 集信息 {集号: {download: 文件路径,library: 文件路径, backdrop: url, title: 标题, description: 描述}}
|
||||||
episodes: Record<number, SubscribeEpisodeInfo>
|
episodes: Record<number, SubscribeEpisodeInfo>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 整理表单
|
||||||
|
export interface TransferForm {
|
||||||
|
// 文件项
|
||||||
|
fileitem: FileItem
|
||||||
|
// 历史ID
|
||||||
|
logid: number
|
||||||
|
// 目标存储
|
||||||
|
target_storage: string
|
||||||
|
// 目标路径
|
||||||
|
target_path: string
|
||||||
|
// TMDB ID
|
||||||
|
tmdbid?: number
|
||||||
|
// 豆瓣 ID
|
||||||
|
doubanid?: string
|
||||||
|
// 季号
|
||||||
|
season?: number
|
||||||
|
// 类型
|
||||||
|
type_name?: string
|
||||||
|
// 整理方式
|
||||||
|
transfer_type: string
|
||||||
|
// 自定义格式
|
||||||
|
episode_format?: string
|
||||||
|
// 指定集数
|
||||||
|
episode_detail?: string
|
||||||
|
// 指定PART
|
||||||
|
episode_part?: string
|
||||||
|
// 集数偏移
|
||||||
|
episode_offset?: string
|
||||||
|
// 最小文件大小
|
||||||
|
min_filesize: number
|
||||||
|
// 刮削
|
||||||
|
scrape: boolean
|
||||||
|
// 复用历史识别信息
|
||||||
|
from_history: boolean
|
||||||
|
// 媒体库类型子目录
|
||||||
|
library_type_folder?: boolean
|
||||||
|
// 媒体库类别子目录
|
||||||
|
library_category_folder?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 整理队列
|
||||||
|
export interface TransferQueue {
|
||||||
|
// 媒体信息
|
||||||
|
media: MediaInfo
|
||||||
|
// 季
|
||||||
|
season?: number
|
||||||
|
// 任务列表
|
||||||
|
tasks: {
|
||||||
|
// 文件项
|
||||||
|
fileitem: FileItem
|
||||||
|
// 元数据
|
||||||
|
meta: MetaInfo
|
||||||
|
// 状态
|
||||||
|
state: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 探索的数据源
|
||||||
|
export interface DiscoverSource {
|
||||||
|
// 数据源名称
|
||||||
|
name: string
|
||||||
|
// 媒体ID的前缀,不含:
|
||||||
|
mediaid_prefix: string
|
||||||
|
// 媒体数据源API地址
|
||||||
|
api_path: string
|
||||||
|
// 过滤参数
|
||||||
|
filter_params: { [key: string]: any }
|
||||||
|
// 过滤参数UI配置
|
||||||
|
filter_ui: RenderProps[]
|
||||||
|
// UI依赖关系字典
|
||||||
|
depends?: { [key: string]: string[] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 推荐的数据源
|
||||||
|
export interface RecommendSource {
|
||||||
|
// 数据源名称
|
||||||
|
name: string
|
||||||
|
// 媒体数据源API地址
|
||||||
|
api_path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 站点资源分类
|
||||||
|
export interface SiteCategory {
|
||||||
|
id: number
|
||||||
|
cat: string
|
||||||
|
desc: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工作流
|
||||||
|
export interface Workflow {
|
||||||
|
// 工作流ID
|
||||||
|
id?: string
|
||||||
|
// 工作流名称
|
||||||
|
name?: string
|
||||||
|
// 工作流描述
|
||||||
|
description?: string
|
||||||
|
// 定时器
|
||||||
|
timer?: string
|
||||||
|
// 状态
|
||||||
|
state?: string
|
||||||
|
// 当前执行动作
|
||||||
|
current_action?: string
|
||||||
|
// 任务执行结果
|
||||||
|
result?: string
|
||||||
|
// 已执行次数
|
||||||
|
run_count?: number
|
||||||
|
// 动作列表
|
||||||
|
actions?: any[]
|
||||||
|
// 动作流
|
||||||
|
flows?: any[]
|
||||||
|
// 创建时间
|
||||||
|
add_time?: string
|
||||||
|
// 最后执行时间
|
||||||
|
last_time?: string
|
||||||
|
}
|
||||||
|
|||||||
BIN
src/assets/images/logos/bangumi_title.png
Normal file
BIN
src/assets/images/logos/bangumi_title.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
BIN
src/assets/images/logos/douban_title.png
Normal file
BIN
src/assets/images/logos/douban_title.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
1
src/assets/images/logos/tmdb_title.svg
Normal file
1
src/assets/images/logos/tmdb_title.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 273.42 35.52"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" y1="17.76" x2="273.42" y2="17.76" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset="0.56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 3</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M191.85,35.37h63.9A17.67,17.67,0,0,0,273.42,17.7h0A17.67,17.67,0,0,0,255.75,0h-63.9A17.67,17.67,0,0,0,174.18,17.7h0A17.67,17.67,0,0,0,191.85,35.37ZM10.1,35.42h7.8V6.92H28V0H0v6.9H10.1Zm28.1,0H46V8.25h.1L55.05,35.4h6L70.3,8.25h.1V35.4h7.8V0H66.45l-8.2,23.1h-.1L50,0H38.2ZM89.14.12h11.7a33.56,33.56,0,0,1,8.08,1,18.52,18.52,0,0,1,6.67,3.08,15.09,15.09,0,0,1,4.53,5.52,18.5,18.5,0,0,1,1.67,8.25,16.91,16.91,0,0,1-1.62,7.58,16.3,16.3,0,0,1-4.38,5.5,19.24,19.24,0,0,1-6.35,3.37,24.53,24.53,0,0,1-7.55,1.15H89.14Zm7.8,28.2h4a21.66,21.66,0,0,0,5-.55A10.58,10.58,0,0,0,110,26a8.73,8.73,0,0,0,2.68-3.35,11.9,11.9,0,0,0,1-5.08,9.87,9.87,0,0,0-1-4.52,9.17,9.17,0,0,0-2.63-3.18A11.61,11.61,0,0,0,106.22,8a17.06,17.06,0,0,0-4.68-.63h-4.6ZM133.09.12h13.2a32.87,32.87,0,0,1,4.63.33,12.66,12.66,0,0,1,4.17,1.3,7.94,7.94,0,0,1,3,2.72,8.34,8.34,0,0,1,1.15,4.65,7.48,7.48,0,0,1-1.67,5,9.13,9.13,0,0,1-4.43,2.82V17a10.28,10.28,0,0,1,3.18,1,8.51,8.51,0,0,1,2.45,1.85,7.79,7.79,0,0,1,1.57,2.62,9.16,9.16,0,0,1,.55,3.2,8.52,8.52,0,0,1-1.2,4.68,9.32,9.32,0,0,1-3.1,3A13.38,13.38,0,0,1,152.32,35a22.5,22.5,0,0,1-4.73.5h-14.5Zm7.8,14.15h5.65a7.65,7.65,0,0,0,1.78-.2,4.78,4.78,0,0,0,1.57-.65,3.43,3.43,0,0,0,1.13-1.2,3.63,3.63,0,0,0,.42-1.8A3.3,3.3,0,0,0,151,8.6a3.42,3.42,0,0,0-1.23-1.13A6.07,6.07,0,0,0,148,6.9a9.9,9.9,0,0,0-1.85-.18h-5.3Zm0,14.65h7a8.27,8.27,0,0,0,1.83-.2,4.67,4.67,0,0,0,1.67-.7,3.93,3.93,0,0,0,1.23-1.3,3.8,3.8,0,0,0,.47-1.95,3.16,3.16,0,0,0-.62-2,4,4,0,0,0-1.58-1.18,8.23,8.23,0,0,0-2-.55,15.12,15.12,0,0,0-2.05-.15h-5.9Z"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -2,7 +2,7 @@
|
|||||||
import { CustomRule } from '@/api/types'
|
import { CustomRule } from '@/api/types'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import filter_svg from '@images/svg/filter.svg'
|
import filter_svg from '@images/svg/filter.svg'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash-es'
|
||||||
import { innerFilterRules } from '@/api/constants'
|
import { innerFilterRules } from '@/api/constants'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
@@ -98,13 +98,13 @@ function onClose() {
|
|||||||
<DialogCloseBtn @click="onClose" />
|
<DialogCloseBtn @click="onClose" />
|
||||||
<VCardText class="flex justify-space-between align-center gap-3">
|
<VCardText class="flex justify-space-between align-center gap-3">
|
||||||
<div class="align-self-start">
|
<div class="align-self-start">
|
||||||
<h5 class="text-h6 mb-1">{{ props.rule.id }}</h5>
|
<h5 class="text-h6 mb-1">{{ props.rule.name }}</h5>
|
||||||
<div class="text-body-1 mb-3">{{ props.rule.name }}</div>
|
<div class="text-body-1 mb-3">{{ props.rule.id }}</div>
|
||||||
</div>
|
</div>
|
||||||
<VImg :src="filter_svg" cover class="mt-7" max-width="3rem" />
|
<VImg :src="filter_svg" cover class="mt-7" max-width="3rem" />
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
<VDialog v-model="ruleInfoDialog" scrollable max-width="40rem" persistent>
|
<VDialog v-if="ruleInfoDialog" v-model="ruleInfoDialog" scrollable max-width="40rem" persistent>
|
||||||
<VCard :title="`${props.rule.id} - 配置`" class="rounded-t">
|
<VCard :title="`${props.rule.id} - 配置`" class="rounded-t">
|
||||||
<DialogCloseBtn v-model="ruleInfoDialog" />
|
<DialogCloseBtn v-model="ruleInfoDialog" />
|
||||||
<VDivider />
|
<VDivider />
|
||||||
@@ -174,9 +174,9 @@ function onClose() {
|
|||||||
<VCol cols="6">
|
<VCol cols="6">
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="ruleInfo.publish_time"
|
v-model="ruleInfo.publish_time"
|
||||||
placeholder="0"
|
placeholder="0/1-10"
|
||||||
label="发布时间(分钟)"
|
label="发布时间(分钟)"
|
||||||
hint="距离资源发布的最小时间间隔"
|
hint="距离资源发布的最小时间间隔或时间区间"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
active
|
active
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { TransferDirectoryConf } from '@/api/types'
|
import type { TransferDirectoryConf } from '@/api/types'
|
||||||
import { VDivider, VSpacer, VTextField } from 'vuetify/lib/components/index.mjs'
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import { storageOptions } from '@/api/constants'
|
import { storageOptions } from '@/api/constants'
|
||||||
@@ -20,12 +19,6 @@ const props = defineProps({
|
|||||||
height: String,
|
height: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 下载路径
|
|
||||||
const downloadPath = ref<string>('')
|
|
||||||
|
|
||||||
// 媒体库路径
|
|
||||||
const libraryPath = ref<string>('')
|
|
||||||
|
|
||||||
// 卡版是否折叠状态
|
// 卡版是否折叠状态
|
||||||
const isCollapsed = ref(true)
|
const isCollapsed = ref(true)
|
||||||
|
|
||||||
@@ -105,13 +98,13 @@ async function loadTransferTypeItems() {
|
|||||||
// 整理方式无数据提示
|
// 整理方式无数据提示
|
||||||
const computedNoDataText = computed(() => {
|
const computedNoDataText = computed(() => {
|
||||||
if (!props.directory.library_storage && !props.directory.storage) {
|
if (!props.directory.library_storage && !props.directory.storage) {
|
||||||
return '无可用整理方式!请先选择下载器储存与媒体库储存!'
|
return '请选择储存'
|
||||||
} else if (!props.directory.library_storage) {
|
} else if (!props.directory.library_storage) {
|
||||||
return '无可用整理方式!请先选择媒体库储存!'
|
return '请选择媒体库储存'
|
||||||
} else if (!props.directory.storage) {
|
} else if (!props.directory.storage) {
|
||||||
return '无可用整理方式!请先选择下载器储存!'
|
return '请选择下载器储存'
|
||||||
} else {
|
} else {
|
||||||
return '选择的存储没有支持的整理方法!'
|
return '选择的存储类型没有支持的整理方式'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -131,24 +124,6 @@ function onClose() {
|
|||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载路径更新
|
|
||||||
function updateDownloadPath(value: string) {
|
|
||||||
downloadPath.value = value
|
|
||||||
emit('update:modelValue', {
|
|
||||||
download: downloadPath.value,
|
|
||||||
library: libraryPath.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 媒体库路径更新
|
|
||||||
function updateLibraryPath(value: string) {
|
|
||||||
libraryPath.value = value
|
|
||||||
emit('update:modelValue', {
|
|
||||||
download: downloadPath.value,
|
|
||||||
library: libraryPath.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据选中的媒体类型,获取对应的媒体类别
|
// 根据选中的媒体类型,获取对应的媒体类别
|
||||||
const getCategories = computed(() => {
|
const getCategories = computed(() => {
|
||||||
const default_value = [{ title: '全部', value: '' }]
|
const default_value = [{ title: '全部', value: '' }]
|
||||||
@@ -220,19 +195,20 @@ watch(
|
|||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="4">
|
<VCol cols="4">
|
||||||
<VSelect v-model="props.directory.storage" variant="underlined" :items="storageOptions" label="下载存储" />
|
<VSelect
|
||||||
|
v-model="props.directory.storage"
|
||||||
|
variant="underlined"
|
||||||
|
:items="storageOptions"
|
||||||
|
label="下载存储/源存储"
|
||||||
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="8">
|
<VCol cols="8">
|
||||||
<VPathField @update:modelValue="updateDownloadPath" :storage="props.directory.storage">
|
<VPathField
|
||||||
<template #activator="{ menuprops }">
|
v-model="props.directory.download_path"
|
||||||
<VTextField
|
:storage="props.directory.storage"
|
||||||
v-model="props.directory.download_path"
|
variant="underlined"
|
||||||
v-bind="menuprops"
|
label="下载目录/源目录"
|
||||||
variant="underlined"
|
/>
|
||||||
label="下载目录"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</VPathField>
|
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''">
|
<VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''">
|
||||||
<VSwitch v-model="props.directory.download_type_folder" label="按类型分类"></VSwitch>
|
<VSwitch v-model="props.directory.download_type_folder" label="按类型分类"></VSwitch>
|
||||||
@@ -270,16 +246,12 @@ watch(
|
|||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="8">
|
<VCol cols="8">
|
||||||
<VPathField @update:modelValue="updateLibraryPath" :storage="props.directory.library_storage">
|
<VPathField
|
||||||
<template #activator="{ menuprops }">
|
v-model="props.directory.library_path"
|
||||||
<VTextField
|
:storage="props.directory.library_storage"
|
||||||
v-model="props.directory.library_path"
|
variant="underlined"
|
||||||
v-bind="menuprops"
|
label="媒体库目录"
|
||||||
variant="underlined"
|
/>
|
||||||
label="媒体库目录"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</VPathField>
|
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="4">
|
<VCol cols="4">
|
||||||
<VSelect
|
<VSelect
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useToast } from 'vue-toast-notification'
|
|||||||
import type { DownloaderInfo } from '@/api/types'
|
import type { DownloaderInfo } from '@/api/types'
|
||||||
import qbittorrent_image from '@images/logos/qbittorrent.png'
|
import qbittorrent_image from '@images/logos/qbittorrent.png'
|
||||||
import transmission_image from '@images/logos/transmission.png'
|
import transmission_image from '@images/logos/transmission.png'
|
||||||
import {cloneDeep} from "lodash";
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
|
||||||
// 定义输入
|
// 定义输入
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -104,7 +104,7 @@ function saveDownloaderInfo() {
|
|||||||
props.downloaders.forEach(item => {
|
props.downloaders.forEach(item => {
|
||||||
if (item.default && item !== props.downloader) {
|
if (item.default && item !== props.downloader) {
|
||||||
item.default = false
|
item.default = false
|
||||||
$toast.info(`【${item.name}】存在默认下载器,已替换成【${downloaderInfo.value.name}】`)
|
$toast.info(`存在默认下载器【${item.name}】,已替换成【${downloaderInfo.value.name}】`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -172,7 +172,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
<VDialog v-model="downloaderInfoDialog" scrollable max-width="40rem" persistent>
|
<VDialog v-if="downloaderInfoDialog" v-model="downloaderInfoDialog" scrollable max-width="40rem" persistent>
|
||||||
<VCard :title="`${props.downloader.name} - 配置`" class="rounded-t">
|
<VCard :title="`${props.downloader.name} - 配置`" class="rounded-t">
|
||||||
<DialogCloseBtn v-model="downloaderInfoDialog" />
|
<DialogCloseBtn v-model="downloaderInfoDialog" />
|
||||||
<VDivider />
|
<VDivider />
|
||||||
@@ -211,7 +211,6 @@ onUnmounted(() => {
|
|||||||
<VTextField
|
<VTextField
|
||||||
v-model="downloaderInfo.config.username"
|
v-model="downloaderInfo.config.username"
|
||||||
label="用户名"
|
label="用户名"
|
||||||
placeholder="admin"
|
|
||||||
hint="登录使用的用户名"
|
hint="登录使用的用户名"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
active
|
active
|
||||||
@@ -289,7 +288,6 @@ onUnmounted(() => {
|
|||||||
<VTextField
|
<VTextField
|
||||||
v-model="downloaderInfo.config.username"
|
v-model="downloaderInfo.config.username"
|
||||||
label="用户名"
|
label="用户名"
|
||||||
placeholder="admin"
|
|
||||||
hint="登录使用的用户名"
|
hint="登录使用的用户名"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
active
|
active
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { innerFilterRules } from '@/api/constants'
|
import { innerFilterRules } from '@/api/constants'
|
||||||
import { CustomRule } from '@/api/types'
|
import { CustomRule } from '@/api/types'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
|||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
|
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
|
||||||
import filter_group_svg from '@images/svg/filter-group.svg'
|
import filter_group_svg from '@images/svg/filter-group.svg'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -72,12 +72,17 @@ const getCategories = computed(() => {
|
|||||||
|
|
||||||
// 规则组规则卡片列表
|
// 规则组规则卡片列表
|
||||||
const filterRuleCards = ref<FilterCard[]>([])
|
const filterRuleCards = ref<FilterCard[]>([])
|
||||||
|
// 规则组类型,仅用于导入判断
|
||||||
|
const filterRuleCardsType = ref<FilterCard>({
|
||||||
|
pri: '',
|
||||||
|
rules: [],
|
||||||
|
})
|
||||||
|
|
||||||
// 导入代码弹窗
|
// 导入代码弹窗
|
||||||
const importCodeDialog = ref(false)
|
const importCodeDialog = ref(false)
|
||||||
|
|
||||||
// 导入的代码
|
// 导入代码类型
|
||||||
const importCodeString = ref('')
|
const importCodeType = ref('')
|
||||||
|
|
||||||
// 更新规则卡片的值
|
// 更新规则卡片的值
|
||||||
function updateFilterCardValue(pri: string, rules: string[]) {
|
function updateFilterCardValue(pri: string, rules: string[]) {
|
||||||
@@ -96,7 +101,7 @@ function filterCardClose(pri: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 分享规则
|
// 分享规则
|
||||||
function shareRules() {
|
async function shareRules() {
|
||||||
if (filterRuleCards.value.length === 0) return
|
if (filterRuleCards.value.length === 0) return
|
||||||
|
|
||||||
const value = filterRuleCards.value
|
const value = filterRuleCards.value
|
||||||
@@ -105,32 +110,43 @@ function shareRules() {
|
|||||||
.join('>')
|
.join('>')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
copyToClipboard(value)
|
let success
|
||||||
$toast.success('优先级规则已复制到剪贴板')
|
success = copyToClipboard(value)
|
||||||
|
if (await success) $toast.success('优先级规则已复制到剪贴板!')
|
||||||
|
else $toast.error('优先级规则复制失败:可能是浏览器不支持或被用户阻止!')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
$toast.error('优先级规则复制失败!')
|
$toast.error('优先级规则复制失败!')
|
||||||
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导入规则
|
// 导入规则
|
||||||
async function importRules() {
|
async function importRules(ruleType: string) {
|
||||||
importCodeString.value = ''
|
importCodeType.value = ruleType
|
||||||
importCodeDialog.value = true
|
importCodeDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听导入代码变化
|
// 保存导入的代码,直接覆盖原有值
|
||||||
watchEffect(() => {
|
function saveCodeString(type: string, code: any) {
|
||||||
if (!importCodeString.value) return
|
try {
|
||||||
|
code = code.value
|
||||||
if (!importCodeString.value.startsWith(' ')) importCodeString.value = ` ${importCodeString.value}`
|
if (type === 'priority') {
|
||||||
if (!importCodeString.value.endsWith(' ')) importCodeString.value = `${importCodeString.value} `
|
// 解析值
|
||||||
|
if (!code) return
|
||||||
const groups = importCodeString.value.split('>')
|
// 首尾增加空格
|
||||||
filterRuleCards.value = groups.map((group: string, index: number) => ({
|
if (!code.startsWith(' ')) code = ` ${code}`
|
||||||
pri: (index + 1).toString(),
|
if (!code.endsWith(' ')) code = `${code} `
|
||||||
rules: group.split('&').filter(rule => rule),
|
const groups = code.split('>')
|
||||||
}))
|
filterRuleCards.value = groups.map((group: string, index: number) => ({
|
||||||
})
|
pri: (index + 1).toString(),
|
||||||
|
rules: group.split('&').filter(rule => rule),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
$toast.error('导入失败!')
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 增加卡片
|
// 增加卡片
|
||||||
function addFilterCard() {
|
function addFilterCard() {
|
||||||
@@ -204,12 +220,12 @@ function onClose() {
|
|||||||
<VImg :src="filter_group_svg" cover class="mt-10" max-width="3rem" />
|
<VImg :src="filter_group_svg" cover class="mt-10" max-width="3rem" />
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
<VDialog v-model="groupInfoDialog" scrollable max-width="80rem" persistent>
|
<VDialog v-if="groupInfoDialog" v-model="groupInfoDialog" scrollable max-width="80rem" persistent>
|
||||||
<VCard :title="`${props.group.name} - 配置`" class="rounded-t">
|
<VCard :title="`${props.group.name} - 配置`" class="rounded-t">
|
||||||
<DialogCloseBtn v-model="groupInfoDialog" />
|
<DialogCloseBtn v-model="groupInfoDialog" />
|
||||||
<VDivider />
|
<VDivider />
|
||||||
<VCardText>
|
<VCardItem class="pt-1">
|
||||||
<VRow>
|
<VRow class="mt-1">
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="groupInfo.name"
|
v-model="groupInfo.name"
|
||||||
@@ -241,7 +257,7 @@ function onClose() {
|
|||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</VCardText>
|
</VCardItem>
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<draggable
|
<draggable
|
||||||
v-model="filterRuleCards"
|
v-model="filterRuleCards"
|
||||||
@@ -268,7 +284,7 @@ function onClose() {
|
|||||||
<VBtn color="primary" variant="tonal" @click="addFilterCard">
|
<VBtn color="primary" variant="tonal" @click="addFilterCard">
|
||||||
<VIcon icon="mdi-plus" />
|
<VIcon icon="mdi-plus" />
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn color="success" variant="tonal" @click="importRules">
|
<VBtn color="success" variant="tonal" @click="importRules('priority')">
|
||||||
<VIcon icon="mdi-import" />
|
<VIcon icon="mdi-import" />
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn color="info" variant="tonal" @click="shareRules">
|
<VBtn color="info" variant="tonal" @click="shareRules">
|
||||||
@@ -279,8 +295,13 @@ function onClose() {
|
|||||||
</VCardActions>
|
</VCardActions>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
<VDialog v-model="importCodeDialog" width="60rem" scrollable>
|
<ImportCodeDialog
|
||||||
<ImportCodeDialog v-model="importCodeString" title="导入优先级规则" @close="importCodeDialog = false" />
|
v-if="importCodeDialog"
|
||||||
</VDialog>
|
v-model="importCodeDialog"
|
||||||
|
title="导入规则优先级"
|
||||||
|
:dataType="importCodeType"
|
||||||
|
@close="importCodeDialog = false"
|
||||||
|
@save="saveCodeString"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -91,7 +91,17 @@ async function drawImages(imageList: string[]) {
|
|||||||
const img = new Image()
|
const img = new Image()
|
||||||
img.setAttribute('crossorigin', 'anonymous')
|
img.setAttribute('crossorigin', 'anonymous')
|
||||||
img.src = imgSrc
|
img.src = imgSrc
|
||||||
await new Promise(resolve => (img.onload = resolve))
|
try {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
img.onload = () => resolve()
|
||||||
|
img.onerror = () => reject(new Error(`Failed to load image: ${imgSrc}`))
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
ctx.fillStyle = '#e5e7eb'
|
||||||
|
ctx.fillRect(MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1), MARGIN_HEIGHT, POSTER_WIDTH, POSTER_HEIGHT)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
|
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
|
||||||
const y = MARGIN_HEIGHT
|
const y = MARGIN_HEIGHT
|
||||||
|
|||||||
@@ -2,15 +2,16 @@
|
|||||||
import type { PropType, Ref } from 'vue'
|
import type { PropType, Ref } from 'vue'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
||||||
import { formatSeason } from '@/@core/utils/formatters'
|
import { formatSeason, formatRating } from '@/@core/utils/formatters'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||||
import type { MediaInfo, NotExistMediaInfo, Subscribe, TmdbSeason } from '@/api/types'
|
import type { MediaInfo, NotExistMediaInfo, Subscribe, MediaSeason, Site } from '@/api/types'
|
||||||
import router from '@/router'
|
import router, { registerAbortController } from '@/router'
|
||||||
import noImage from '@images/no-image.jpeg'
|
import noImage from '@images/no-image.jpeg'
|
||||||
import tmdbImage from '@images/logos/tmdb.png'
|
import tmdbImage from '@images/logos/tmdb.png'
|
||||||
import doubanImage from '@images/logos/douban-black.png'
|
import doubanImage from '@images/logos/douban-black.png'
|
||||||
import bangumiImage from '@images/logos/bangumi.png'
|
import bangumiImage from '@images/logos/bangumi.png'
|
||||||
|
import { useUserStore } from '@/stores'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -22,7 +23,8 @@ const props = defineProps({
|
|||||||
// 从 provide 中获取全局设置
|
// 从 provide 中获取全局设置
|
||||||
const globalSettings: any = inject('globalSettings')
|
const globalSettings: any = inject('globalSettings')
|
||||||
|
|
||||||
const store = useStore()
|
// 用户 Store
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
@@ -55,10 +57,10 @@ const subscribeEditDialog = ref(false)
|
|||||||
const subscribeId = ref<number>()
|
const subscribeId = ref<number>()
|
||||||
|
|
||||||
// 季详情
|
// 季详情
|
||||||
const seasonInfos = ref<TmdbSeason[]>([])
|
const seasonInfos = ref<MediaSeason[]>([])
|
||||||
|
|
||||||
// 选中的订阅季
|
// 选中的订阅季
|
||||||
const seasonsSelected = ref<TmdbSeason[]>([])
|
const seasonsSelected = ref<MediaSeason[]>([])
|
||||||
|
|
||||||
// 来源角标字典
|
// 来源角标字典
|
||||||
const sourceIconDict: { [key: string]: any } = {
|
const sourceIconDict: { [key: string]: any } = {
|
||||||
@@ -67,11 +69,50 @@ const sourceIconDict: { [key: string]: any } = {
|
|||||||
bangumi: bangumiImage,
|
bangumi: bangumiImage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 绑定MediaCard元素
|
||||||
|
const mediaCardRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
// 创建Intersection Observer实例
|
||||||
|
const observer = ref<IntersectionObserver | null>(null)
|
||||||
|
|
||||||
|
// 所有站点
|
||||||
|
const allSites = ref<Site[]>([])
|
||||||
|
|
||||||
|
// 选中的站点
|
||||||
|
const selectedSites = ref<number[]>([])
|
||||||
|
|
||||||
|
// 搜索菜单显示状态
|
||||||
|
const searchMenuShow = ref(false)
|
||||||
|
|
||||||
|
// 查询所有站点
|
||||||
|
async function querySites() {
|
||||||
|
try {
|
||||||
|
const data: Site[] = await api.get('site/')
|
||||||
|
|
||||||
|
// 过滤站点,只有启用的站点才显示
|
||||||
|
allSites.value = data.filter(item => item.is_active)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用户选中的站点
|
||||||
|
async function querySelectedSites() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get('system/setting/IndexerSites')
|
||||||
|
|
||||||
|
selectedSites.value = result.data?.value ?? []
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获得mediaid
|
// 获得mediaid
|
||||||
function getMediaId() {
|
function getMediaId() {
|
||||||
if (props.media?.tmdb_id) return `tmdb:${props.media?.tmdb_id}`
|
if (props.media?.tmdb_id) return `tmdb:${props.media?.tmdb_id}`
|
||||||
else if (props.media?.douban_id) return `douban:${props.media?.douban_id}`
|
else if (props.media?.douban_id) return `douban:${props.media?.douban_id}`
|
||||||
else return `bangumi:${props.media?.bangumi_id}`
|
else if (props.media?.bangumi_id) return `bangumi:${props.media?.bangumi_id}`
|
||||||
|
else return `${props.media?.mediaid_prefix}:${props.media?.media_id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 订阅弹窗选择的多季
|
// 订阅弹窗选择的多季
|
||||||
@@ -90,13 +131,11 @@ function getChipColor(type: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 添加订阅处理
|
// 添加订阅处理
|
||||||
|
|
||||||
async function handleAddSubscribe() {
|
async function handleAddSubscribe() {
|
||||||
if (props.media?.type === '电视剧' && props.media?.tmdb_id) {
|
if (props.media?.type === '电视剧') {
|
||||||
// TMDB电视剧
|
// 查询所有季信息
|
||||||
// 查询TMDB所有季信息
|
|
||||||
await getMediaSeasons()
|
await getMediaSeasons()
|
||||||
if (!seasonInfos.value) {
|
if (!seasonInfos.value || seasonInfos.value.length === 0) {
|
||||||
$toast.error(`${props.media?.title} 查询剧集信息失败!`)
|
$toast.error(`${props.media?.title} 查询剧集信息失败!`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -112,11 +151,6 @@ async function handleAddSubscribe() {
|
|||||||
seasonsSelected.value = []
|
seasonsSelected.value = []
|
||||||
subscribeSeasonDialog.value = true
|
subscribeSeasonDialog.value = true
|
||||||
}
|
}
|
||||||
} else if (props.media?.type === '电视剧') {
|
|
||||||
// 豆瓣电视剧,只会有一季
|
|
||||||
const season = props.media?.season ?? 1
|
|
||||||
// 添加订阅
|
|
||||||
addSubscribe(season)
|
|
||||||
} else {
|
} else {
|
||||||
// 电影
|
// 电影
|
||||||
addSubscribe()
|
addSubscribe()
|
||||||
@@ -141,6 +175,7 @@ async function addSubscribe(season = 0) {
|
|||||||
tmdbid: props.media?.tmdb_id,
|
tmdbid: props.media?.tmdb_id,
|
||||||
doubanid: props.media?.douban_id,
|
doubanid: props.media?.douban_id,
|
||||||
bangumiid: props.media?.bangumi_id,
|
bangumiid: props.media?.bangumi_id,
|
||||||
|
mediaid: props.media?.media_id ? `${props.media?.mediaid_prefix}:${props.media?.media_id}` : '',
|
||||||
season,
|
season,
|
||||||
best_version,
|
best_version,
|
||||||
})
|
})
|
||||||
@@ -219,6 +254,9 @@ async function handleCheckSubscribe() {
|
|||||||
// 查询当前媒体是否已入库
|
// 查询当前媒体是否已入库
|
||||||
async function handleCheckExists() {
|
async function handleCheckExists() {
|
||||||
try {
|
try {
|
||||||
|
const abortController = new AbortController()
|
||||||
|
registerAbortController(abortController)
|
||||||
|
const { signal } = abortController
|
||||||
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||||
params: {
|
params: {
|
||||||
tmdbid: props.media?.tmdb_id,
|
tmdbid: props.media?.tmdb_id,
|
||||||
@@ -227,6 +265,7 @@ async function handleCheckExists() {
|
|||||||
season: props.media?.season,
|
season: props.media?.season,
|
||||||
mtype: props.media?.type,
|
mtype: props.media?.type,
|
||||||
},
|
},
|
||||||
|
signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.success) isExists.value = true
|
if (result.success) isExists.value = true
|
||||||
@@ -238,13 +277,16 @@ async function handleCheckExists() {
|
|||||||
// 调用API检查是否已订阅,电视剧需要指定季
|
// 调用API检查是否已订阅,电视剧需要指定季
|
||||||
async function checkSubscribe(season = 0) {
|
async function checkSubscribe(season = 0) {
|
||||||
try {
|
try {
|
||||||
|
const abortController = new AbortController()
|
||||||
|
registerAbortController(abortController)
|
||||||
|
const { signal } = abortController
|
||||||
const mediaid = getMediaId()
|
const mediaid = getMediaId()
|
||||||
|
|
||||||
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
||||||
params: {
|
params: {
|
||||||
season,
|
season,
|
||||||
title: props.media?.title,
|
title: props.media?.title,
|
||||||
},
|
},
|
||||||
|
signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
return result.id || null
|
return result.id || null
|
||||||
@@ -267,7 +309,6 @@ async function checkSeasonsNotExists() {
|
|||||||
let state = 0
|
let state = 0
|
||||||
if (item.episodes.length === 0) state = 2
|
if (item.episodes.length === 0) state = 2
|
||||||
else if (item.episodes.length < item.total_episode) state = 1
|
else if (item.episodes.length < item.total_episode) state = 1
|
||||||
|
|
||||||
seasonsNotExisted.value[item.season] = state
|
seasonsNotExisted.value[item.season] = state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -282,17 +323,26 @@ async function checkSeasonsNotExists() {
|
|||||||
|
|
||||||
// 查询TMDB的所有季信息
|
// 查询TMDB的所有季信息
|
||||||
async function getMediaSeasons() {
|
async function getMediaSeasons() {
|
||||||
|
startNProgress()
|
||||||
try {
|
try {
|
||||||
seasonInfos.value = await api.get(`tmdb/seasons/${props.media?.tmdb_id}`)
|
seasonInfos.value = await api.get('media/seasons', {
|
||||||
|
params: {
|
||||||
|
mediaid: getMediaId(),
|
||||||
|
title: props.media?.title,
|
||||||
|
year: props.media?.year,
|
||||||
|
season: props.media?.season,
|
||||||
|
},
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
doneNProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询订阅弹窗规则
|
// 查询订阅弹窗规则
|
||||||
async function queryDefaultSubscribeConfig() {
|
async function queryDefaultSubscribeConfig() {
|
||||||
// 非管理员不显示
|
// 非管理员不显示
|
||||||
if (!store.state.auth.superUser) return false
|
if (!userStore.superUser) return false
|
||||||
try {
|
try {
|
||||||
let subscribe_config_url = ''
|
let subscribe_config_url = ''
|
||||||
if (props.media?.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
|
if (props.media?.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
|
||||||
@@ -336,16 +386,36 @@ function getExistText(season: number) {
|
|||||||
// 打开详情页
|
// 打开详情页
|
||||||
function goMediaDetail(isHovering = false) {
|
function goMediaDetail(isHovering = false) {
|
||||||
if (isHovering) {
|
if (isHovering) {
|
||||||
router.push({
|
if (props.media?.collection_id) {
|
||||||
path: '/media',
|
// 跳转到合集列表
|
||||||
query: {
|
router.push({
|
||||||
mediaid: getMediaId(),
|
path: `/browse/tmdb/collection/${props.media?.collection_id}`,
|
||||||
type: props.media?.type,
|
query: {
|
||||||
},
|
title: props.media?.title,
|
||||||
})
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 跳转到媒体详情页
|
||||||
|
router.push({
|
||||||
|
path: '/media',
|
||||||
|
query: {
|
||||||
|
mediaid: getMediaId(),
|
||||||
|
title: props.media?.title,
|
||||||
|
year: props.media?.year,
|
||||||
|
type: props.media?.type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 点击搜索
|
||||||
|
async function clickSearch() {
|
||||||
|
if (allSites.value?.length > 0) return
|
||||||
|
querySites()
|
||||||
|
querySelectedSites()
|
||||||
|
}
|
||||||
|
|
||||||
// 开始搜索
|
// 开始搜索
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
router.push({
|
router.push({
|
||||||
@@ -354,15 +424,51 @@ function handleSearch() {
|
|||||||
keyword: getMediaId(),
|
keyword: getMediaId(),
|
||||||
type: props.media?.type,
|
type: props.media?.type,
|
||||||
area: 'title',
|
area: 'title',
|
||||||
|
title: props.media?.title,
|
||||||
|
year: props.media?.year,
|
||||||
season: props.media?.season,
|
season: props.media?.season,
|
||||||
|
sites: selectedSites.value.join(','),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 装载时检查是否已订阅
|
// 懒加载检查
|
||||||
onBeforeMount(() => {
|
function handleCheckLazy() {
|
||||||
|
if (props.media?.collection_id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
handleCheckSubscribe()
|
handleCheckSubscribe()
|
||||||
handleCheckExists()
|
handleCheckExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在元素进入视窗时触发懒加载函数
|
||||||
|
function setupIntersectionObserver() {
|
||||||
|
if (mediaCardRef.value) {
|
||||||
|
observer.value = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
// 只要MediaCard进入视窗,就调用懒加载的操作
|
||||||
|
handleCheckLazy()
|
||||||
|
// 加载后销毁观察者实例
|
||||||
|
observer.value?.disconnect()
|
||||||
|
observer.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 },
|
||||||
|
)
|
||||||
|
observer.value.observe(mediaCardRef.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setupIntersectionObserver()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
observer.value?.disconnect()
|
||||||
|
observer.value = null
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算图片地址
|
// 计算图片地址
|
||||||
@@ -407,81 +513,109 @@ function onRemoveSubscribe() {
|
|||||||
<template>
|
<template>
|
||||||
<VHover>
|
<VHover>
|
||||||
<template #default="hover">
|
<template #default="hover">
|
||||||
<VCard
|
<div ref="mediaCardRef">
|
||||||
v-bind="hover.props"
|
<VCard
|
||||||
:height="props.height"
|
v-bind="hover.props"
|
||||||
:width="props.width"
|
:height="props.height"
|
||||||
class="outline-none shadow ring-gray-500 rounded-lg"
|
:width="props.width"
|
||||||
:class="{
|
class="outline-none shadow ring-gray-500 rounded-lg"
|
||||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
:class="{
|
||||||
'ring-1': isImageLoaded,
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
}"
|
'ring-1': isImageLoaded,
|
||||||
@click.stop="goMediaDetail(hover.isHovering ?? false)"
|
}"
|
||||||
>
|
@click.stop="goMediaDetail(hover.isHovering ?? false)"
|
||||||
<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>
|
<VImg
|
||||||
<div class="w-full h-full">
|
aspect-ratio="2/3"
|
||||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
:src="getImgUrl"
|
||||||
|
class="object-cover aspect-w-2 aspect-h-3"
|
||||||
|
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>
|
||||||
|
<!-- 详情 -->
|
||||||
|
<VCardText
|
||||||
|
v-show="hover.isHovering || imageLoadError || searchMenuShow"
|
||||||
|
class="w-full h-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||||
|
style="background: linear-gradient(rgba(45, 55, 72, 40%) 0%, rgba(45, 55, 72, 90%) 100%)"
|
||||||
|
>
|
||||||
|
<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 v-if="props.media?.collection_id" class="mb-3" @click.stop=""></div>
|
||||||
|
<div v-else class="flex align-center justify-between">
|
||||||
|
<VMenu close-on-content-click v-model="searchMenuShow" max-width="450">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<IconBtn v-bind="props" icon="mdi-magnify" color="white" @click.stop="clickSearch" />
|
||||||
|
</template>
|
||||||
|
<VList>
|
||||||
|
<VListItem>
|
||||||
|
<VChipGroup v-model="selectedSites" column multiple @click.stop>
|
||||||
|
<VChip
|
||||||
|
v-for="site in allSites"
|
||||||
|
:key="site.id"
|
||||||
|
:color="selectedSites.includes(site.id) ? 'primary' : ''"
|
||||||
|
filter
|
||||||
|
variant="outlined"
|
||||||
|
:value="site.id"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ site.name }}
|
||||||
|
</VChip>
|
||||||
|
</VChipGroup>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem>
|
||||||
|
<VBtn @click="handleSearch" block>搜索</VBtn>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
|
<IconBtn icon="mdi-heart" :color="isSubscribed ? 'error' : 'white'" @click.stop="handleSubscribe" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</VCardText>
|
||||||
</VImg>
|
<!-- 类型角标 -->
|
||||||
<!-- 类型角标 -->
|
<VChip
|
||||||
<VChip
|
v-show="isImageLoaded"
|
||||||
v-show="isImageLoaded"
|
variant="elevated"
|
||||||
variant="elevated"
|
size="small"
|
||||||
size="small"
|
:class="getChipColor(props.media?.type || '')"
|
||||||
:class="getChipColor(props.media?.type || '')"
|
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||||
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
>
|
||||||
>
|
{{ props.media?.type }}
|
||||||
{{ props.media?.type }}
|
</VChip>
|
||||||
</VChip>
|
<!-- 本地存在标识 -->
|
||||||
<!-- 本地存在标识 -->
|
<ExistIcon v-if="isExists && !hover.isHovering" />
|
||||||
<ExistIcon v-if="isExists && !hover.isHovering" />
|
<!-- 评分角标 -->
|
||||||
<!-- 评分角标 -->
|
<VChip
|
||||||
<VChip
|
v-if="isImageLoaded && props.media?.vote_average && !(isExists && !hover.isHovering)"
|
||||||
v-if="isImageLoaded && props.media?.vote_average && !(isExists && !hover.isHovering)"
|
variant="elevated"
|
||||||
variant="elevated"
|
size="small"
|
||||||
size="small"
|
:class="getChipColor('rating')"
|
||||||
:class="getChipColor('rating')"
|
class="absolute right-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||||
class="absolute right-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
>
|
||||||
>
|
{{ formatRating(props.media?.vote_average) }}
|
||||||
{{ props.media?.vote_average }}
|
</VChip>
|
||||||
</VChip>
|
<!--来源图标-->
|
||||||
<!-- 详情 -->
|
<VAvatar
|
||||||
<VCardText
|
size="24"
|
||||||
v-show="hover.isHovering || imageLoadError"
|
density="compact"
|
||||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
class="absolute bottom-1 right-1"
|
||||||
>
|
tile
|
||||||
<span class="font-bold">{{ props.media?.year }}</span>
|
v-if="!hover.isHovering && isImageLoaded && props.media?.source && !imageLoadError"
|
||||||
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
>
|
||||||
{{ props.media?.title }}
|
<VImg cover :src="sourceIconDict[props.media?.source]" class="shadow-lg" />
|
||||||
</h1>
|
</VAvatar>
|
||||||
<p class="leading-4 line-clamp-4 overflow-hidden text-ellipsis ...">
|
</VCard>
|
||||||
{{ props.media?.overview }}
|
</div>
|
||||||
</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>
|
|
||||||
<VAvatar
|
|
||||||
size="24"
|
|
||||||
density="compact"
|
|
||||||
class="absolute bottom-1 right-1"
|
|
||||||
tile
|
|
||||||
v-if="!hover.isHovering && isImageLoaded && props.media?.source"
|
|
||||||
>
|
|
||||||
<VImg cover :src="sourceIconDict[props.media?.source]" class="shadow-lg" />
|
|
||||||
</VAvatar>
|
|
||||||
</VCard>
|
|
||||||
</template>
|
</template>
|
||||||
</VHover>
|
</VHover>
|
||||||
<!-- 订阅季弹窗 -->
|
<!-- 订阅季弹窗 -->
|
||||||
@@ -535,7 +669,7 @@ function onRemoveSubscribe() {
|
|||||||
</VList>
|
</VList>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<div class="my-2 text-center">
|
<div class="my-2 text-center">
|
||||||
<VBtn :disabled="seasonsSelected.length === 0" width="30%" @click="subscribeSeasons">
|
<VBtn size="large" :disabled="seasonsSelected.length === 0" width="30%" @click="subscribeSeasons">
|
||||||
{{ seasonsSelected.length === 0 ? '请选择订阅季' : '提交订阅' }}
|
{{ seasonsSelected.length === 0 ? '请选择订阅季' : '提交订阅' }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</div>
|
</div>
|
||||||
@@ -551,9 +685,3 @@ function onRemoveSubscribe() {
|
|||||||
@remove="onRemoveSubscribe"
|
@remove="onRemoveSubscribe"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.on-hover img {
|
|
||||||
@apply brightness-50;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Context } from '@/api/types'
|
|||||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
defineProps({
|
||||||
context: Object as PropType<Context>,
|
context: Object as PropType<Context>,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -45,8 +45,7 @@ function openTmdbPage(type: string, tmdbId: number) {
|
|||||||
</template>
|
</template>
|
||||||
</VImg>
|
</VImg>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
<div>
|
|
||||||
<VCardItem class="pb-1">
|
<VCardItem class="pb-1">
|
||||||
<VCardTitle class="text-center text-md-left">
|
<VCardTitle class="text-center text-md-left">
|
||||||
{{ context?.media_info?.title || context?.meta_info?.name }}
|
{{ context?.media_info?.title || context?.meta_info?.name }}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import emby_image from '@images/logos/emby.png'
|
|||||||
import jellyfin_image from '@images/logos/jellyfin.png'
|
import jellyfin_image from '@images/logos/jellyfin.png'
|
||||||
import plex_image from '@images/logos/plex.png'
|
import plex_image from '@images/logos/plex.png'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
|
||||||
// 定义输入
|
// 定义输入
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -185,7 +185,7 @@ onMounted(() => {
|
|||||||
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" min-width="3rem" />
|
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" min-width="3rem" />
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
<VDialog v-model="mediaServerInfoDialog" scrollable max-width="40rem" persistent>
|
<VDialog v-if="mediaServerInfoDialog" v-model="mediaServerInfoDialog" scrollable max-width="40rem" persistent>
|
||||||
<VCard :title="`${props.mediaserver.name} - 配置`" class="rounded-t">
|
<VCard :title="`${props.mediaserver.name} - 配置`" class="rounded-t">
|
||||||
<DialogCloseBtn v-model="mediaServerInfoDialog" />
|
<DialogCloseBtn v-model="mediaServerInfoDialog" />
|
||||||
<VDivider />
|
<VDivider />
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||||
import type { Message } from '@/api/types'
|
import type { Message } from '@/api/types'
|
||||||
import { formatDateDifference } from '@core/utils/formatters'
|
import { formatDateDifference } from '@core/utils/formatters'
|
||||||
|
|
||||||
@@ -45,24 +46,31 @@ function replaceNewLine(value: string) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VCard variant="tonal" :width="props.width" :height="props.height" @click="openLink">
|
<VCard variant="tonal" :width="props.width" :height="props.height" @click="openLink" max-width="23rem">
|
||||||
<div v-if="props.message?.image" class="relative text-center card-cover-blurred">
|
<div v-if="props.message?.image" class="relative text-center card-cover-blurred">
|
||||||
<VImg
|
<VImg
|
||||||
:src="props.message?.image"
|
:src="props.message?.image"
|
||||||
aspect-ratio="4/3"
|
aspect-ratio="3/2"
|
||||||
cover
|
cover
|
||||||
|
position="top"
|
||||||
:class="{ shadow: isImageLoaded }"
|
:class="{ shadow: isImageLoaded }"
|
||||||
@load="imageLoaded"
|
@load="imageLoaded"
|
||||||
@error="imageLoadError = true"
|
@error="imageLoadError = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="props.message?.title && !props.message?.image && !props.message?.note"
|
v-if="
|
||||||
|
props.message?.title &&
|
||||||
|
!props.message?.text &&
|
||||||
|
!props.message?.image &&
|
||||||
|
isNullOrEmptyObject(props.message?.note) &&
|
||||||
|
props.message?.action === 0
|
||||||
|
"
|
||||||
class="rounded-md text-body-1 py-2 px-4 elevation-2 bg-primary text-white chat-right mb-1"
|
class="rounded-md text-body-1 py-2 px-4 elevation-2 bg-primary text-white chat-right mb-1"
|
||||||
>
|
>
|
||||||
<p class="mb-0">{{ props.message?.title }}</p>
|
<p class="mb-0">{{ props.message?.title }}</p>
|
||||||
</div>
|
</div>
|
||||||
<VCardTitle v-else-if="props.message?.title">
|
<VCardTitle v-else-if="props.message?.title" class="break-words whitespace-break-spaces">
|
||||||
{{ props.message?.title }}
|
{{ props.message?.title }}
|
||||||
</VCardTitle>
|
</VCardTitle>
|
||||||
<div
|
<div
|
||||||
@@ -72,13 +80,13 @@ function replaceNewLine(value: string) {
|
|||||||
<p class="mb-0">{{ props.message?.text }}</p>
|
<p class="mb-0">{{ props.message?.text }}</p>
|
||||||
</div>
|
</div>
|
||||||
<VCardText v-if="props.message?.text && props.message?.action === 1" v-html="replaceNewLine(props.message?.text)" />
|
<VCardText v-if="props.message?.text && props.message?.action === 1" v-html="replaceNewLine(props.message?.text)" />
|
||||||
<VCardText v-if="props.message?.note">
|
<VCardText v-if="!isNullOrEmptyObject(props.message?.note)">
|
||||||
<VList>
|
<VList>
|
||||||
<VListItem v-for="(value, key) in noteToJson()" :key="key" two-line>
|
<VListItem v-for="(value, key) in noteToJson()" :key="key" two-line>
|
||||||
<VListItemTitle v-if="value.title_year" class="font-bold">
|
<VListItemTitle v-if="value.title_year" class="font-bold break-words whitespace-break-spaces">
|
||||||
{{ key + 1 }}. {{ value.title_year }}
|
{{ key + 1 }}. {{ value.title_year }}
|
||||||
</VListItemTitle>
|
</VListItemTitle>
|
||||||
<VListItemTitle v-if="value.enclosure" class="font-bold whitespace-break-spaces">
|
<VListItemTitle v-if="value.enclosure" class="font-bold break-words whitespace-break-spaces">
|
||||||
{{ key + 1 }}. {{ value.title }} {{ value.volume_factor }} ↑{{ value.seeders }}
|
{{ key + 1 }}. {{ value.title }} {{ value.volume_factor }} ↑{{ value.seeders }}
|
||||||
</VListItemTitle>
|
</VListItemTitle>
|
||||||
<VListItemSubtitle v-if="value.type">
|
<VListItemSubtitle v-if="value.type">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import synologychat_image from '@images/logos/synologychat.png'
|
|||||||
import slack_image from '@images/logos/slack.webp'
|
import slack_image from '@images/logos/slack.webp'
|
||||||
import chrome_image from '@images/logos/chrome.png'
|
import chrome_image from '@images/logos/chrome.png'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import { cloneDeep } from "lodash"
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
|
||||||
// 定义输入
|
// 定义输入
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -132,7 +132,7 @@ function onClose() {
|
|||||||
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" />
|
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" />
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
<VDialog v-model="notificationInfoDialog" scrollable max-width="40rem" persistent>
|
<VDialog v-if="notificationInfoDialog" v-model="notificationInfoDialog" scrollable max-width="40rem" persistent>
|
||||||
<VCard :title="`${props.notification.name} - 配置`" class="rounded-t">
|
<VCard :title="`${props.notification.name} - 配置`" class="rounded-t">
|
||||||
<DialogCloseBtn v-model="notificationInfoDialog" />
|
<DialogCloseBtn v-model="notificationInfoDialog" />
|
||||||
<VDivider />
|
<VDivider />
|
||||||
|
|||||||
@@ -43,11 +43,8 @@ const imageLoadError = ref(false)
|
|||||||
// 更新日志弹窗
|
// 更新日志弹窗
|
||||||
const releaseDialog = ref(false)
|
const releaseDialog = ref(false)
|
||||||
|
|
||||||
// 计算插件标签
|
// 插件详情弹窗
|
||||||
const pluginLabels = computed(() => {
|
const detailDialog = ref(false)
|
||||||
if (!props.plugin?.plugin_label) return []
|
|
||||||
return props.plugin.plugin_label.split(',')
|
|
||||||
})
|
|
||||||
|
|
||||||
// 图片加载完成
|
// 图片加载完成
|
||||||
async function imageLoaded() {
|
async function imageLoaded() {
|
||||||
@@ -76,7 +73,7 @@ async function installPlugin() {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
$toast.success(`插件 ${props.plugin?.plugin_name} 安装成功!`)
|
$toast.success(`插件 ${props.plugin?.plugin_name} 安装成功!`)
|
||||||
|
detailDialog.value = false
|
||||||
// 通知父组件刷新
|
// 通知父组件刷新
|
||||||
emit('install')
|
emit('install')
|
||||||
} else {
|
} else {
|
||||||
@@ -149,80 +146,141 @@ const dropdownItems = ref([
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VCard :width="props.width" :height="props.height" @click="installPlugin" class="flex flex-col">
|
<div>
|
||||||
<div class="me-n3 absolute bottom-0 right-3">
|
<VCard :width="props.width" :height="props.height" @click="detailDialog = true" class="flex flex-col h-full">
|
||||||
<IconBtn>
|
|
||||||
<VIcon icon="mdi-dots-vertical" />
|
|
||||||
<VMenu activator="parent" close-on-content-click>
|
|
||||||
<VList>
|
|
||||||
<VListItem
|
|
||||||
v-for="(item, i) in dropdownItems"
|
|
||||||
v-show="item.show"
|
|
||||||
:key="i"
|
|
||||||
variant="plain"
|
|
||||||
@click="item.props.click"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon :icon="item.props.prependIcon" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle v-text="item.title" />
|
|
||||||
</VListItem>
|
|
||||||
</VList>
|
|
||||||
</VMenu>
|
|
||||||
</IconBtn>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
|
||||||
:style="{ background: `${backgroundColor}` }"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-cover bg-center"
|
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
:style="{ background: `${backgroundColor}` }"
|
||||||
></div>
|
>
|
||||||
<div class="relative flex-1 min-w-0">
|
<div
|
||||||
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
class="absolute inset-0 bg-cover bg-center"
|
||||||
{{ props.plugin?.plugin_name }}
|
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
||||||
<span class="text-sm text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
></div>
|
||||||
</VCardTitle>
|
<div class="relative flex-1 min-w-0">
|
||||||
<VCardText class="text-white px-2 py-1 text-shadow line-clamp-3">
|
<VCardTitle class="text-white text-lg px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis ...">
|
||||||
{{ props.plugin?.plugin_desc }}
|
{{ props.plugin?.plugin_name }}
|
||||||
</VCardText>
|
<span class="text-sm text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
||||||
|
</VCardTitle>
|
||||||
|
<VCardText class="text-white text-sm px-2 py-0 text-shadow overflow-hidden line-clamp-3 ...">
|
||||||
|
{{ props.plugin?.plugin_desc }}
|
||||||
|
</VCardText>
|
||||||
|
</div>
|
||||||
|
<div class="relative flex-shrink-0 self-center">
|
||||||
|
<VAvatar size="64">
|
||||||
|
<VImg
|
||||||
|
ref="imageRef"
|
||||||
|
:src="iconPath"
|
||||||
|
aspect-ratio="4/3"
|
||||||
|
cover
|
||||||
|
:class="{ shadow: isImageLoaded }"
|
||||||
|
@load="imageLoaded"
|
||||||
|
@error="imageLoadError = true"
|
||||||
|
/>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative flex-shrink-0 self-center">
|
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||||
<VAvatar size="64">
|
<span>
|
||||||
<VImg
|
<VIcon icon="mdi-github" class="me-1" />
|
||||||
ref="imageRef"
|
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||||
:src="iconPath"
|
{{ props.plugin?.plugin_author }}
|
||||||
aspect-ratio="4/3"
|
</a>
|
||||||
cover
|
</span>
|
||||||
:class="{ shadow: isImageLoaded }"
|
<span v-if="props.count" class="ms-3">
|
||||||
@load="imageLoaded"
|
<VIcon icon="mdi-download" />
|
||||||
@error="imageLoadError = true"
|
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||||
/>
|
</span>
|
||||||
</VAvatar>
|
<div class="me-n3 absolute bottom-1 right-3">
|
||||||
</div>
|
<IconBtn>
|
||||||
</div>
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
<VMenu activator="parent" close-on-content-click>
|
||||||
<span>
|
<VList>
|
||||||
<VIcon icon="mdi-github" class="me-1" />
|
<VListItem
|
||||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
v-for="(item, i) in dropdownItems"
|
||||||
{{ props.plugin?.plugin_author }}
|
v-show="item.show"
|
||||||
</a>
|
:key="i"
|
||||||
</span>
|
variant="plain"
|
||||||
<span v-if="props.count" class="ms-3">
|
@click="item.props.click"
|
||||||
<VIcon icon="mdi-download" />
|
>
|
||||||
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
<template #prepend>
|
||||||
</span>
|
<VIcon :icon="item.props.prependIcon" />
|
||||||
</VCardText>
|
</template>
|
||||||
</VCard>
|
<VListItemTitle v-text="item.title" />
|
||||||
<!-- 安装插件进度框 -->
|
</VListItem>
|
||||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
</VList>
|
||||||
<!-- 更新日志 -->
|
</VMenu>
|
||||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
</IconBtn>
|
||||||
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
</div>
|
||||||
<DialogCloseBtn @click="releaseDialog = false" />
|
</VCardText>
|
||||||
<VDivider />
|
|
||||||
<VersionHistory :history="props.plugin?.history" />
|
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
<!-- 安装插件进度框 -->
|
||||||
|
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||||
|
<!-- 更新日志 -->
|
||||||
|
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||||
|
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
||||||
|
<DialogCloseBtn @click="releaseDialog = false" />
|
||||||
|
<VDivider />
|
||||||
|
<VersionHistory :history="props.plugin?.history" />
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
<!-- 插件详情-->
|
||||||
|
<VDialog v-if="detailDialog" v-model="detailDialog" max-width="30rem">
|
||||||
|
<VCard>
|
||||||
|
<DialogCloseBtn @click="detailDialog = false" />
|
||||||
|
<VCardText>
|
||||||
|
<VCol>
|
||||||
|
<div class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row">
|
||||||
|
<div class="mx-auto mt-5">
|
||||||
|
<VAvatar size="64">
|
||||||
|
<VImg
|
||||||
|
ref="imageRef"
|
||||||
|
:src="iconPath"
|
||||||
|
aspect-ratio="4/3"
|
||||||
|
cover
|
||||||
|
:class="{ shadow: isImageLoaded }"
|
||||||
|
@load="imageLoaded"
|
||||||
|
@error="imageLoadError = true"
|
||||||
|
/>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<VCardItem>
|
||||||
|
<VCardTitle class="text-center text-md-left">
|
||||||
|
{{ props.plugin?.plugin_name }}
|
||||||
|
</VCardTitle>
|
||||||
|
<VCardSubtitle
|
||||||
|
class="text-center text-md-left break-words whitespace-break-spaces line-clamp-4 overflow-hidden text-ellipsis ..."
|
||||||
|
>
|
||||||
|
{{ props.plugin?.plugin_desc }}
|
||||||
|
</VCardSubtitle>
|
||||||
|
<VList lines="one">
|
||||||
|
<VListItem class="ps-0">
|
||||||
|
<VListItemTitle class="text-center text-md-left">
|
||||||
|
<span class="font-weight-medium">版本:</span>
|
||||||
|
<span class="text-body-1"> v{{ props.plugin?.plugin_version }}</span>
|
||||||
|
</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem class="ps-0">
|
||||||
|
<VListItemTitle class="text-center text-md-left">
|
||||||
|
<span class="font-weight-medium">作者:</span>
|
||||||
|
<span class="text-body-1 cursor-pointer" @click="visitPluginPage">
|
||||||
|
{{ props.plugin?.plugin_author }}
|
||||||
|
</span>
|
||||||
|
</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
<div class="text-center text-md-left">
|
||||||
|
<VBtn color="primary" @click="installPlugin" prepend-icon="mdi-download"> 安装到本地 </VBtn>
|
||||||
|
<div class="text-xs mt-2" v-if="props.count">
|
||||||
|
<VIcon icon="mdi-fire" />共 {{ props.count?.toLocaleString() }} 次下载
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCardItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCol>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import { useConfirm } from 'vuetify-use-dialog'
|
import { useConfirm } from 'vuetify-use-dialog'
|
||||||
import { VIcon } from 'vuetify/lib/components/index.mjs'
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Plugin } from '@/api/types'
|
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 { isNullOrEmptyObject } from '@core/utils'
|
||||||
import noImage from '@images/logos/plugin.png'
|
import noImage from '@images/logos/plugin.png'
|
||||||
import { getDominantColor } from '@/@core/utils/image'
|
import { getDominantColor } from '@/@core/utils/image'
|
||||||
import { useDisplay } from 'vuetify'
|
import VersionHistory from '@/components/misc/VersionHistory.vue'
|
||||||
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
||||||
|
import PluginConfigDialog from '../dialog/PluginConfigDialog.vue'
|
||||||
// 显示器宽度
|
import PluginDataDialog from '../dialog/PluginDataDialog.vue'
|
||||||
const display = useDisplay()
|
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -46,15 +41,12 @@ const isVisible = ref(true)
|
|||||||
// 插件配置页面
|
// 插件配置页面
|
||||||
const pluginConfigDialog = ref(false)
|
const pluginConfigDialog = ref(false)
|
||||||
|
|
||||||
// 插件配置表单数据
|
// 菜单显示状态
|
||||||
const pluginConfigForm = ref({})
|
const menuVisible = ref(false)
|
||||||
|
|
||||||
// 进度框
|
// 进度框
|
||||||
const progressDialog = ref(false)
|
const progressDialog = ref(false)
|
||||||
|
|
||||||
// 插件表单配置项
|
|
||||||
let pluginFormItems = reactive([])
|
|
||||||
|
|
||||||
// 插件数据页面
|
// 插件数据页面
|
||||||
const pluginInfoDialog = ref(false)
|
const pluginInfoDialog = ref(false)
|
||||||
|
|
||||||
@@ -64,9 +56,6 @@ const progressText = ref('正在更新插件...')
|
|||||||
// 用户头像是否加载完成
|
// 用户头像是否加载完成
|
||||||
const isAvatarLoaded = ref(false)
|
const isAvatarLoaded = ref(false)
|
||||||
|
|
||||||
// 插件数据页面配置项
|
|
||||||
let pluginPageItems = ref([])
|
|
||||||
|
|
||||||
// 图片是否加载完成
|
// 图片是否加载完成
|
||||||
const isImageLoaded = ref(false)
|
const isImageLoaded = ref(false)
|
||||||
|
|
||||||
@@ -134,75 +123,14 @@ async function uninstallPlugin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用API读取表单页面
|
|
||||||
async function loadPluginForm() {
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.get(`plugin/form/${props.plugin?.id}`)
|
|
||||||
if (result) {
|
|
||||||
pluginFormItems = result.conf
|
|
||||||
if (result.model) pluginConfigForm.value = result.model
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用API读取数据页面
|
|
||||||
async function loadPluginPage() {
|
|
||||||
try {
|
|
||||||
const result: [] = await api.get(`plugin/page/${props.plugin?.id}`)
|
|
||||||
if (result) pluginPageItems.value = result
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用API读取配置数据
|
|
||||||
async function loadPluginConf() {
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.get(`plugin/${props.plugin?.id}`)
|
|
||||||
if (!isNullOrEmptyObject(result)) pluginConfigForm.value = result
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用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) {
|
|
||||||
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}}`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示插件数据
|
// 显示插件数据
|
||||||
async function showPluginInfo() {
|
async function showPluginInfo() {
|
||||||
// 加载数据
|
|
||||||
await loadPluginPage()
|
|
||||||
pluginConfigDialog.value = false
|
pluginConfigDialog.value = false
|
||||||
pluginInfoDialog.value = true
|
pluginInfoDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示插件配置
|
// 显示插件配置
|
||||||
async function showPluginConfig() {
|
async function showPluginConfig() {
|
||||||
// 加载表单
|
|
||||||
await loadPluginForm()
|
|
||||||
// 加载配置
|
|
||||||
await loadPluginConf()
|
|
||||||
// 显示对话框
|
// 显示对话框
|
||||||
pluginInfoDialog.value = false
|
pluginInfoDialog.value = false
|
||||||
pluginConfigDialog.value = true
|
pluginConfigDialog.value = true
|
||||||
@@ -299,6 +227,12 @@ function openPluginDetail() {
|
|||||||
else showPluginConfig()
|
else showPluginConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 配置完成
|
||||||
|
function configDone() {
|
||||||
|
pluginConfigDialog.value = false
|
||||||
|
emit('save')
|
||||||
|
}
|
||||||
|
|
||||||
// 弹出菜单
|
// 弹出菜单
|
||||||
const dropdownItems = ref([
|
const dropdownItems = ref([
|
||||||
{
|
{
|
||||||
@@ -390,130 +324,136 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 插件卡片 -->
|
<div>
|
||||||
<VCard v-if="isVisible" :width="props.width" :height="props.height" @click="openPluginDetail" class="flex flex-col">
|
<!-- 插件卡片 -->
|
||||||
<div class="me-n3 absolute bottom-0 right-3">
|
<VHover>
|
||||||
<IconBtn>
|
<template #default="hover">
|
||||||
<VIcon icon="mdi-dots-vertical" />
|
<VCard
|
||||||
<VMenu activator="parent" close-on-content-click>
|
v-if="isVisible"
|
||||||
<VList>
|
v-bind="hover.props"
|
||||||
<VListItem
|
:width="props.width"
|
||||||
v-for="(item, i) in dropdownItems"
|
:height="props.height"
|
||||||
v-show="item.show"
|
@click="openPluginDetail"
|
||||||
:key="i"
|
class="flex flex-col h-full"
|
||||||
variant="plain"
|
>
|
||||||
:base-color="item.props.color"
|
<div
|
||||||
@click="item.props.click"
|
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||||
>
|
:style="{ background: `${backgroundColor}` }"
|
||||||
<template #prepend>
|
>
|
||||||
<VIcon :icon="item.props.prependIcon" />
|
<div
|
||||||
</template>
|
class="absolute inset-0 bg-cover bg-center"
|
||||||
<VListItemTitle v-text="item.title" />
|
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
||||||
</VListItem>
|
/>
|
||||||
</VList>
|
<div class="relative flex-1 min-w-0">
|
||||||
</VMenu>
|
<VCardTitle class="text-white text-lg px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
||||||
</IconBtn>
|
<VBadge v-if="props.plugin?.state" dot inline color="success" />
|
||||||
</div>
|
{{ props.plugin?.plugin_name }}
|
||||||
<div
|
<span class="text-sm mt-1 text-gray-200"> v{{ props.plugin?.plugin_version }} </span>
|
||||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
</VCardTitle>
|
||||||
:style="{ background: `${backgroundColor}` }"
|
<VCardText class="px-2 py-0 text-white text-sm text-shadow overflow-hidden line-clamp-3 ...">
|
||||||
>
|
{{ props.plugin?.plugin_desc }}
|
||||||
<div
|
</VCardText>
|
||||||
class="absolute inset-0 bg-cover bg-center"
|
</div>
|
||||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
<div class="relative flex-shrink-0 self-center">
|
||||||
/>
|
<VAvatar size="64">
|
||||||
<div class="relative flex-1 min-w-0">
|
<VImg
|
||||||
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
ref="imageRef"
|
||||||
<VBadge v-if="props.plugin?.state" dot inline color="success" />
|
:src="iconPath"
|
||||||
{{ props.plugin?.plugin_name }}
|
aspect-ratio="4/3"
|
||||||
<span class="text-sm mt-1 text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
cover
|
||||||
</VCardTitle>
|
:class="{ shadow: isImageLoaded }"
|
||||||
<VCardText class="px-2 py-1 text-white text-shadow line-clamp-3">
|
@load="imageLoaded"
|
||||||
{{ props.plugin?.plugin_desc }}
|
@error="imageLoadError = true"
|
||||||
</VCardText>
|
/>
|
||||||
</div>
|
</VAvatar>
|
||||||
<div class="relative flex-shrink-0 self-center">
|
</div>
|
||||||
<VAvatar size="64">
|
</div>
|
||||||
<VImg
|
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||||
ref="imageRef"
|
<span class="author-info">
|
||||||
:src="iconPath"
|
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
|
||||||
aspect-ratio="4/3"
|
<VIcon v-if="!isAvatarLoaded" icon="mdi-github" class="me-1" />
|
||||||
cover
|
</VImg>
|
||||||
:class="{ shadow: isImageLoaded }"
|
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||||
@load="imageLoaded"
|
{{ props.plugin?.plugin_author }}
|
||||||
@error="imageLoadError = true"
|
</a>
|
||||||
/>
|
</span>
|
||||||
</VAvatar>
|
<span v-if="props.count" class="ms-3">
|
||||||
</div>
|
<VIcon icon="mdi-download" />
|
||||||
</div>
|
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||||
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
</span>
|
||||||
<span class="author-info">
|
<div class="me-n3 absolute bottom-1 right-3">
|
||||||
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
|
<IconBtn>
|
||||||
<VIcon v-if="!isAvatarLoaded" icon="mdi-github" class="me-1" />
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
</VImg>
|
<VMenu v-model="menuVisible" activator="parent" close-on-content-click>
|
||||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
<VList>
|
||||||
{{ props.plugin?.plugin_author }}
|
<VListItem
|
||||||
</a>
|
v-for="(item, i) in dropdownItems"
|
||||||
</span>
|
v-show="item.show"
|
||||||
<span v-if="props.count" class="ms-3">
|
:key="i"
|
||||||
<VIcon icon="mdi-download" />
|
variant="plain"
|
||||||
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
:base-color="item.props.color"
|
||||||
</span>
|
@click="item.props.click"
|
||||||
</VCardText>
|
>
|
||||||
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
|
<template #prepend>
|
||||||
<VIcon icon="mdi-new-box" class="text-white" />
|
<VIcon :icon="item.props.prependIcon" />
|
||||||
</div>
|
</template>
|
||||||
</VCard>
|
<VListItemTitle v-text="item.title" />
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
|
</IconBtn>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
<div v-if="hover.isHovering" class="me-n3 absolute top-0 right-5">
|
||||||
|
<VIcon class="cursor-move text-white">mdi-drag</VIcon>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
|
||||||
|
<VIcon icon="mdi-new-box" class="text-white" />
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
</VHover>
|
||||||
|
|
||||||
<!-- 插件配置页面 -->
|
<!-- 插件配置页面 -->
|
||||||
<VDialog v-model="pluginConfigDialog" scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
<PluginConfigDialog
|
||||||
<VCard :title="`${props.plugin?.plugin_name} - 配置`" class="rounded-t">
|
v-if="pluginConfigDialog"
|
||||||
<DialogCloseBtn v-model="pluginConfigDialog" />
|
v-model="pluginConfigDialog"
|
||||||
<VDivider />
|
:plugin="props.plugin"
|
||||||
<VCardText>
|
@save="configDone"
|
||||||
<FormRender v-for="(item, index) in pluginFormItems" :key="index" :config="item" :form="pluginConfigForm" />
|
@close="pluginConfigDialog = false"
|
||||||
</VCardText>
|
@switch="showPluginInfo"
|
||||||
<VCardActions class="pt-3">
|
/>
|
||||||
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo" variant="outlined" color="info">
|
|
||||||
查看数据
|
|
||||||
</VBtn>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn @click="savePluginConf" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 保存 </VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
|
|
||||||
<!-- 插件数据页面 -->
|
<!-- 插件数据页面 -->
|
||||||
<VDialog v-model="pluginInfoDialog" scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
<PluginDataDialog
|
||||||
<VCard :title="`${props.plugin?.plugin_name}`" class="rounded-t">
|
v-if="pluginInfoDialog"
|
||||||
<DialogCloseBtn v-model="pluginInfoDialog" />
|
v-model="pluginInfoDialog"
|
||||||
<VCardText class="min-h-40">
|
:plugin="props.plugin"
|
||||||
<PageRender @action="loadPluginPage" v-for="(item, index) in pluginPageItems" :key="index" :config="item" />
|
@close="pluginInfoDialog = false"
|
||||||
</VCardText>
|
@switch="showPluginConfig"
|
||||||
<VFab icon="mdi-cog" location="bottom" size="x-large" fixed app appear @click="showPluginConfig" />
|
/>
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
|
|
||||||
<!-- 进度框 -->
|
<!-- 进度框 -->
|
||||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||||
|
|
||||||
<!-- 更新日志 -->
|
<!-- 更新日志 -->
|
||||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||||
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
||||||
<DialogCloseBtn @click="releaseDialog = false" />
|
<DialogCloseBtn @click="releaseDialog = false" />
|
||||||
<VDivider />
|
<VDivider />
|
||||||
<VersionHistory :history="props.plugin?.history" />
|
<VersionHistory :history="props.plugin?.history" />
|
||||||
<VDivider />
|
<VDivider />
|
||||||
<VCardText>
|
<VCardItem>
|
||||||
<VBtn @click="updatePlugin" block>
|
<VBtn @click="updatePlugin" block>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon icon="mdi-arrow-up-circle-outline" />
|
<VIcon icon="mdi-arrow-up-circle-outline" />
|
||||||
</template>
|
</template>
|
||||||
更新到最新版本
|
更新到最新版本
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</VCardText>
|
</VCardItem>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const getImgUrl = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 跳转播放
|
// 跳转播放
|
||||||
function goPlay(isHovering = false) {
|
function goPlay(isHovering: boolean | null = false) {
|
||||||
if (props.media?.link && isHovering) window.open(props.media?.link, '_blank')
|
if (props.media?.link && isHovering) window.open(props.media?.link, '_blank')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -48,13 +48,11 @@ function goPlay(isHovering = false) {
|
|||||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
'ring-1': isImageLoaded,
|
'ring-1': isImageLoaded,
|
||||||
}"
|
}"
|
||||||
@click.stop="goPlay(hover.isHovering)"
|
|
||||||
>
|
>
|
||||||
<VImg
|
<VImg
|
||||||
aspect-ratio="2/3"
|
aspect-ratio="2/3"
|
||||||
:src="getImgUrl"
|
:src="getImgUrl"
|
||||||
class="object-cover aspect-w-2 aspect-h-3"
|
class="object-cover aspect-w-2 aspect-h-3"
|
||||||
:class="hover.isHovering ? 'on-hover' : ''"
|
|
||||||
cover
|
cover
|
||||||
@load="isImageLoaded = true"
|
@load="isImageLoaded = true"
|
||||||
@error="imageLoadError = true"
|
@error="imageLoadError = true"
|
||||||
@@ -78,7 +76,9 @@ function goPlay(isHovering = false) {
|
|||||||
<!-- 详情 -->
|
<!-- 详情 -->
|
||||||
<VCardText
|
<VCardText
|
||||||
v-show="hover.isHovering || imageLoadError"
|
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"
|
class="w-full h-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2 pb-5"
|
||||||
|
style="background: linear-gradient(rgba(45, 55, 72, 40%) 0%, rgba(45, 55, 72, 90%) 100%)"
|
||||||
|
@click.stop="goPlay(hover.isHovering)"
|
||||||
>
|
>
|
||||||
<span class="font-bold">{{ props.media?.subtitle }}</span>
|
<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 ...">
|
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||||
@@ -89,9 +89,3 @@ function goPlay(isHovering = false) {
|
|||||||
</template>
|
</template>
|
||||||
</VHover>
|
</VHover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.on-hover img {
|
|
||||||
@apply brightness-50;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import SiteCookieUpdateDialog from '../dialog/SiteCookieUpdateDialog.vue'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Site, SiteStatistic, SiteUserData } from '@/api/types'
|
import type { Site, SiteStatistic, SiteUserData } from '@/api/types'
|
||||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||||
import { VCardActions, VExpandTransition, VProgressLinear, VSpacer } from 'vuetify/lib/components/index.mjs'
|
|
||||||
import { formatFileSize } from '@/@core/utils/formatters'
|
import { formatFileSize } from '@/@core/utils/formatters'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
@@ -27,7 +26,7 @@ const siteIcon = ref<string>('')
|
|||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
// 测试按钮文字
|
// 测试按钮文字
|
||||||
const testButtonText = ref('测试')
|
const testButtonText = ref('测试连通性')
|
||||||
|
|
||||||
// 测试按钮可用性
|
// 测试按钮可用性
|
||||||
const testButtonDisable = ref(false)
|
const testButtonDisable = ref(false)
|
||||||
@@ -44,9 +43,6 @@ const resourceDialog = ref(false)
|
|||||||
// 用户数据弹窗
|
// 用户数据弹窗
|
||||||
const siteUserDataDialog = ref(false)
|
const siteUserDataDialog = ref(false)
|
||||||
|
|
||||||
// 站点操作显示
|
|
||||||
const siteActionShow = ref(false)
|
|
||||||
|
|
||||||
// 站点使用统计
|
// 站点使用统计
|
||||||
const siteStats = ref<SiteStatistic>({})
|
const siteStats = ref<SiteStatistic>({})
|
||||||
|
|
||||||
@@ -69,7 +65,7 @@ async function testSite() {
|
|||||||
if (result.success) $toast.success(`${cardProps.site?.name} 连通性测试成功,可正常使用!`)
|
if (result.success) $toast.success(`${cardProps.site?.name} 连通性测试成功,可正常使用!`)
|
||||||
else $toast.error(`${cardProps.site?.name} 连通性测试失败:${result.message}`)
|
else $toast.error(`${cardProps.site?.name} 连通性测试失败:${result.message}`)
|
||||||
|
|
||||||
testButtonText.value = '测试'
|
testButtonText.value = '测试连通性'
|
||||||
testButtonDisable.value = false
|
testButtonDisable.value = false
|
||||||
|
|
||||||
getSiteStats()
|
getSiteStats()
|
||||||
@@ -156,8 +152,9 @@ onMounted(() => {
|
|||||||
<div>
|
<div>
|
||||||
<VCard
|
<VCard
|
||||||
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
|
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
|
||||||
class="overflow-hidden"
|
class="overflow-hidden h-full flex flex-col"
|
||||||
@click="siteEditDialog = true"
|
@click="handleResourceBrowse"
|
||||||
|
:ripple="false"
|
||||||
>
|
>
|
||||||
<template #image>
|
<template #image>
|
||||||
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
|
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
|
||||||
@@ -166,10 +163,10 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
<VCardItem style="padding-block-end: 0">
|
<VCardItem style="padding-block-end: 0">
|
||||||
<VCardTitle class="font-bold">
|
<VCardTitle class="font-bold">
|
||||||
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
|
{{ cardProps.site?.name }}
|
||||||
</VCardTitle>
|
</VCardTitle>
|
||||||
<VCardSubtitle>
|
<VCardSubtitle>
|
||||||
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
|
{{ cardProps.site?.url }}
|
||||||
</VCardSubtitle>
|
</VCardSubtitle>
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
<VCardText class="py-1">
|
<VCardText class="py-1">
|
||||||
@@ -195,44 +192,48 @@ onMounted(() => {
|
|||||||
</VTooltip>
|
</VTooltip>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions>
|
<VCardActions>
|
||||||
<VBtn
|
<div class="text-sm">
|
||||||
:icon="siteActionShow ? 'mdi-chevron-up' : 'mdi-chevron-down'"
|
|
||||||
@click.stop="siteActionShow = !siteActionShow"
|
|
||||||
/>
|
|
||||||
<span class="text-sm">
|
|
||||||
↑ {{ formatFileSize(cardProps.data?.upload || 0) }} / ↓ {{ formatFileSize(cardProps.data?.download || 0) }}
|
↑ {{ formatFileSize(cardProps.data?.upload || 0) }} / ↓ {{ formatFileSize(cardProps.data?.download || 0) }}
|
||||||
</span>
|
</div>
|
||||||
|
<IconBtn>
|
||||||
|
<VIcon icon="mdi-chevron-down" color="primary" />
|
||||||
|
<VMenu activator="parent" close-on-content-click>
|
||||||
|
<VList>
|
||||||
|
<VListItem variant="plain" @click="siteEditDialog = true" base-color="info">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-file-edit-outline" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>编辑站点</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem variant="plain" @click="handleSiteUserData">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-chart-bell-curve" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>查看站点数据</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem variant="plain" :disabled="testButtonDisable" @click.stop="testSite">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-link" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>{{ testButtonText }}</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem variant="plain" v-if="!cardProps.site?.public" @click="handleSiteUpdate">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-refresh" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>更新 Cookie & UA</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem variant="plain" @click="openSitePage">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-open-in-new" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>访问站点</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
|
</IconBtn>
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
</VCardActions>
|
</VCardActions>
|
||||||
<VDivider class="mb-1" v-if="siteActionShow" />
|
|
||||||
<VExpandTransition>
|
|
||||||
<div v-show="siteActionShow" class="py-1 pe-12">
|
|
||||||
<VBtn v-if="!cardProps.site?.public" @click.stop="handleSiteUpdate" variant="text">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-refresh" />
|
|
||||||
</template>
|
|
||||||
更新
|
|
||||||
</VBtn>
|
|
||||||
<VBtn :disabled="testButtonDisable" @click.stop="testSite" variant="text">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-link" />
|
|
||||||
</template>
|
|
||||||
{{ testButtonText }}
|
|
||||||
</VBtn>
|
|
||||||
<VBtn @click.stop="handleResourceBrowse" variant="text">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-web" />
|
|
||||||
</template>
|
|
||||||
浏览
|
|
||||||
</VBtn>
|
|
||||||
<VBtn @click.stop="handleSiteUserData" variant="text">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-chart-bell-curve" />
|
|
||||||
</template>
|
|
||||||
数据
|
|
||||||
</VBtn>
|
|
||||||
</div>
|
|
||||||
</VExpandTransition>
|
|
||||||
<StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
|
<StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
|
||||||
<span class="absolute top-1 right-8">
|
<span class="absolute top-1 right-8">
|
||||||
<VIcon class="cursor-move">mdi-drag</VIcon>
|
<VIcon class="cursor-move">mdi-drag</VIcon>
|
||||||
|
|||||||
@@ -144,10 +144,17 @@ onMounted(() => {
|
|||||||
<AliyunAuthDialog
|
<AliyunAuthDialog
|
||||||
v-if="aliyunAuthDialog"
|
v-if="aliyunAuthDialog"
|
||||||
v-model="aliyunAuthDialog"
|
v-model="aliyunAuthDialog"
|
||||||
|
:conf="props.storage.config || {}"
|
||||||
@close="aliyunAuthDialog = false"
|
@close="aliyunAuthDialog = false"
|
||||||
@done="handleDone"
|
@done="handleDone"
|
||||||
/>
|
/>
|
||||||
<U115AuthDialog v-if="u115AuthDialog" v-model="u115AuthDialog" @close="u115AuthDialog = false" @done="handleDone" />
|
<U115AuthDialog
|
||||||
|
v-if="u115AuthDialog"
|
||||||
|
v-model="u115AuthDialog"
|
||||||
|
:conf="props.storage.config || {}"
|
||||||
|
@close="u115AuthDialog = false"
|
||||||
|
@done="handleDone"
|
||||||
|
/>
|
||||||
<RcloneConfigDialog
|
<RcloneConfigDialog
|
||||||
v-if="rcloneConfigDialog"
|
v-if="rcloneConfigDialog"
|
||||||
v-model="rcloneConfigDialog"
|
v-model="rcloneConfigDialog"
|
||||||
|
|||||||
@@ -38,8 +38,11 @@ const subscribeFilesDialog = ref(false)
|
|||||||
// 分享订阅弹窗
|
// 分享订阅弹窗
|
||||||
const subscribeShareDialog = ref(false)
|
const subscribeShareDialog = ref(false)
|
||||||
|
|
||||||
|
// 当前的订阅状态
|
||||||
|
const subscribeState = ref<string>(props.media?.state ?? 'P')
|
||||||
|
|
||||||
// 上一次更新时间
|
// 上一次更新时间
|
||||||
const lastUpdateText = ref(props.media && props.media.last_update ? formatDateDifference(props.media.last_update) : '')
|
const lastUpdateText = computed(() => (props.media?.last_update ? formatDateDifference(props.media.last_update) : ''))
|
||||||
|
|
||||||
// 图片加载完成响应
|
// 图片加载完成响应
|
||||||
function imageLoadHandler() {
|
function imageLoadHandler() {
|
||||||
@@ -81,13 +84,39 @@ async function searchSubscribe() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换订阅状态
|
||||||
|
async function toggleSubscribeStatus(state: 'R' | 'S') {
|
||||||
|
try {
|
||||||
|
// 根据传入的 state 判断对应的操作文字
|
||||||
|
const action = state === 'S' ? '暂停' : '启用'
|
||||||
|
// 弹出确认框
|
||||||
|
const isConfirmed = await createConfirm({
|
||||||
|
title: `确认${action}`,
|
||||||
|
content: `是否${action}订阅 ${props.media?.name}?`,
|
||||||
|
})
|
||||||
|
if (!isConfirmed) return
|
||||||
|
// 调用 API 更新订阅状态
|
||||||
|
const result: { [key: string]: any } = await api.put(`subscribe/status/${props.media?.id}?state=${state}`)
|
||||||
|
// 提示
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success(`${props.media?.name} 已${action}!`)
|
||||||
|
subscribeState.value = state
|
||||||
|
emit('save')
|
||||||
|
} else {
|
||||||
|
$toast.error(`${action}失败:${result.message}`)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 重置订阅
|
// 重置订阅
|
||||||
async function resetSubscribe() {
|
async function resetSubscribe() {
|
||||||
// 确认
|
// 确认
|
||||||
try {
|
try {
|
||||||
const isConfirmed = await createConfirm({
|
const isConfirmed = await createConfirm({
|
||||||
title: '确认',
|
title: '确认',
|
||||||
content: `重置后 ${props.media?.name} 已下载记录将被清除,未入库的剧集将会重新下载,是否确认?`,
|
content: `重置后 ${props.media?.name} 将恢复初始状态,已下载记录将被清除,未入库的内容将会重新下载,是否确认?`,
|
||||||
})
|
})
|
||||||
if (!isConfirmed) return
|
if (!isConfirmed) return
|
||||||
// 重置
|
// 重置
|
||||||
@@ -95,6 +124,7 @@ async function resetSubscribe() {
|
|||||||
// 提示
|
// 提示
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
$toast.success(`${props.media?.name} 重置成功!`)
|
$toast.success(`${props.media?.name} 重置成功!`)
|
||||||
|
subscribeState.value = 'R'
|
||||||
emit('save')
|
emit('save')
|
||||||
} else $toast.error(`${props.media?.name} 重置失败:${result.message}`)
|
} else $toast.error(`${props.media?.name} 重置失败:${result.message}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -112,12 +142,22 @@ async function editSubscribeDialog() {
|
|||||||
subscribeEditDialog.value = true
|
subscribeEditDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获得mediaid
|
||||||
|
function getMediaId() {
|
||||||
|
if (props.media?.tmdbid) return `tmdb:${props.media?.tmdbid}`
|
||||||
|
else if (props.media?.doubanid) return `douban:${props.media?.doubanid}`
|
||||||
|
else if (props.media?.bangumiid) return `bangumi:${props.media?.bangumiid}`
|
||||||
|
else return props.media?.mediaid
|
||||||
|
}
|
||||||
|
|
||||||
// 查看媒体详情
|
// 查看媒体详情
|
||||||
async function viewMediaDetail() {
|
async function viewMediaDetail() {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/media',
|
path: '/media',
|
||||||
query: {
|
query: {
|
||||||
mediaid: `${props.media?.tmdbid ? `tmdb:${props.media?.tmdbid}` : `douban:${props.media?.doubanid}`}`,
|
mediaid: getMediaId(),
|
||||||
|
title: props.media?.name,
|
||||||
|
year: props.media?.year,
|
||||||
type: props.media?.type,
|
type: props.media?.type,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -129,7 +169,7 @@ async function viewSubscribeFiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 弹出菜单
|
// 弹出菜单
|
||||||
const dropdownItems = ref([
|
const dropdownItems = computed(() => [
|
||||||
{
|
{
|
||||||
title: '编辑',
|
title: '编辑',
|
||||||
value: 1,
|
value: 1,
|
||||||
@@ -163,18 +203,26 @@ const dropdownItems = ref([
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '重置',
|
title: subscribeState.value === 'S' ? '启用' : '暂停',
|
||||||
value: 5,
|
value: 5,
|
||||||
|
props: {
|
||||||
|
prependIcon: subscribeState.value === 'S' ? 'mdi-play' : 'mdi-pause',
|
||||||
|
click: () => toggleSubscribeStatus(subscribeState.value === 'S' ? 'R' : 'S'),
|
||||||
|
color: subscribeState.value === 'S' ? 'success' : 'info',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '重置',
|
||||||
|
value: 6,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-restore-alert',
|
prependIcon: 'mdi-restore-alert',
|
||||||
click: resetSubscribe,
|
click: resetSubscribe,
|
||||||
color: 'warning',
|
color: 'warning',
|
||||||
},
|
},
|
||||||
show: props.media?.type === '电视剧',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '分享',
|
title: '分享',
|
||||||
value: 6,
|
value: 7,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-share',
|
prependIcon: 'mdi-share',
|
||||||
click: shareSubscribe,
|
click: shareSubscribe,
|
||||||
@@ -184,7 +232,7 @@ const dropdownItems = ref([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '取消订阅',
|
title: '取消订阅',
|
||||||
value: 7,
|
value: 8,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-trash-can-outline',
|
prependIcon: 'mdi-trash-can-outline',
|
||||||
color: 'error',
|
color: 'error',
|
||||||
@@ -201,6 +249,14 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 监听订阅状态
|
||||||
|
watch(
|
||||||
|
() => props.media?.state,
|
||||||
|
newState => {
|
||||||
|
subscribeState.value = newState ?? 'P'
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// 计算backdrop图片地址
|
// 计算backdrop图片地址
|
||||||
const backdropUrl = computed(() => {
|
const backdropUrl = computed(() => {
|
||||||
const url = props.media?.backdrop || props.media?.poster
|
const url = props.media?.backdrop || props.media?.poster
|
||||||
@@ -233,129 +289,137 @@ function onSubscribeEditRemove() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VHover>
|
<div>
|
||||||
<template #default="hover">
|
<VHover>
|
||||||
<VCard
|
<template #default="hover">
|
||||||
v-bind="hover.props"
|
<VCard
|
||||||
:key="props.media?.id"
|
v-bind="hover.props"
|
||||||
class="flex flex-col rounded-lg"
|
:key="props.media?.id"
|
||||||
:class="{
|
class="flex flex-col rounded-lg h-full"
|
||||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
:class="{
|
||||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||||
}"
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
min-height="170"
|
'opacity-70': subscribeState === 'S',
|
||||||
@click="editSubscribeDialog"
|
}"
|
||||||
>
|
min-height="170"
|
||||||
<div class="me-n3 absolute top-1 right-2">
|
@click="editSubscribeDialog"
|
||||||
<IconBtn>
|
:ripple="false"
|
||||||
<VIcon icon="mdi-dots-vertical" color="white" />
|
>
|
||||||
<VMenu activator="parent" close-on-content-click>
|
<div class="me-n3 absolute top-1 right-2">
|
||||||
<VList>
|
<IconBtn>
|
||||||
<template v-for="(item, i) in dropdownItems" :key="i">
|
<VIcon icon="mdi-dots-vertical" color="white" />
|
||||||
<VListItem
|
<VMenu activator="parent" close-on-content-click>
|
||||||
v-if="item.show !== false"
|
<VList>
|
||||||
variant="plain"
|
<template v-for="(item, i) in dropdownItems" :key="i">
|
||||||
:base-color="item.props.color"
|
<VListItem
|
||||||
@click="item.props.click"
|
v-if="item.show !== false"
|
||||||
>
|
variant="plain"
|
||||||
<template #prepend>
|
:base-color="item.props.color"
|
||||||
<VIcon :icon="item.props.prependIcon" />
|
@click="item.props.click"
|
||||||
</template>
|
>
|
||||||
<VListItemTitle v-text="item.title" />
|
<template #prepend>
|
||||||
</VListItem>
|
<VIcon :icon="item.props.prependIcon" />
|
||||||
</template>
|
</template>
|
||||||
</VList>
|
<VListItemTitle v-text="item.title" />
|
||||||
</VMenu>
|
</VListItem>
|
||||||
</IconBtn>
|
</template>
|
||||||
</div>
|
</VList>
|
||||||
<template #image>
|
</VMenu>
|
||||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
</IconBtn>
|
||||||
<template #placeholder>
|
|
||||||
<div class="w-full h-full">
|
|
||||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
|
||||||
</VImg>
|
|
||||||
</template>
|
|
||||||
<div>
|
|
||||||
<VCardText class="flex items-center">
|
|
||||||
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
|
|
||||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
|
||||||
<template #placeholder>
|
|
||||||
<div class="w-full h-full">
|
|
||||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</VImg>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
|
||||||
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
|
||||||
<div class="mr-2 min-w-0 text-lg font-bold text-white">
|
|
||||||
{{ props.media?.name }}
|
|
||||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
<VCardText class="flex justify-space-between align-center flex-wrap">
|
|
||||||
<div class="flex align-center">
|
|
||||||
<IconBtn
|
|
||||||
v-if="props.media?.total_episode"
|
|
||||||
v-bind="props"
|
|
||||||
icon="mdi-progress-download"
|
|
||||||
color="white"
|
|
||||||
class="me-1"
|
|
||||||
/>
|
|
||||||
<div v-if="props.media?.season" class="text-subtitle-2 me-4 text-white">
|
|
||||||
{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
|
|
||||||
{{ props.media?.total_episode }}
|
|
||||||
</div>
|
|
||||||
<IconBtn v-if="props.media?.username" icon="mdi-account" color="white" class="me-1" />
|
|
||||||
<span v-if="props.media?.username" class="text-subtitle-2 me-4 text-white">
|
|
||||||
{{ props.media?.username }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
<VCardText v-if="lastUpdateText" class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
|
||||||
<VIcon icon="mdi-download" class="me-1" />
|
|
||||||
{{ lastUpdateText }}
|
|
||||||
</VCardText>
|
|
||||||
<div class="w-full absolute bottom-0">
|
|
||||||
<VProgressLinear
|
|
||||||
v-if="getPercentage() > 0"
|
|
||||||
:model-value="getPercentage()"
|
|
||||||
bg-color="success"
|
|
||||||
color="success"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<template #image>
|
||||||
</VCard>
|
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
||||||
</template>
|
<template #placeholder>
|
||||||
</VHover>
|
<div class="w-full h-full">
|
||||||
<!-- 订阅编辑弹窗 -->
|
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||||
<SubscribeEditDialog
|
</div>
|
||||||
v-if="subscribeEditDialog"
|
</template>
|
||||||
v-model="subscribeEditDialog"
|
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||||
:subid="props.media?.id"
|
</VImg>
|
||||||
@remove="onSubscribeEditRemove"
|
<div v-if="subscribeState === 'P'" class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none" />
|
||||||
@save="onSubscribeEditSave"
|
</template>
|
||||||
@close="subscribeEditDialog = false"
|
<div>
|
||||||
/>
|
<VCardText class="flex items-center">
|
||||||
|
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
|
||||||
|
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VImg>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||||
|
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
||||||
|
<div class="mr-2 min-w-0 text-lg font-bold text-white">
|
||||||
|
{{ props.media?.name }}
|
||||||
|
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
<VCardText class="flex justify-space-between align-center flex-wrap">
|
||||||
|
<div class="flex align-center">
|
||||||
|
<IconBtn
|
||||||
|
v-if="props.media?.total_episode"
|
||||||
|
v-bind="props"
|
||||||
|
icon="mdi-progress-download"
|
||||||
|
color="white"
|
||||||
|
class="me-1"
|
||||||
|
/>
|
||||||
|
<div v-if="props.media?.season" class="text-subtitle-2 me-4 text-white">
|
||||||
|
{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
|
||||||
|
{{ props.media?.total_episode }}
|
||||||
|
</div>
|
||||||
|
<IconBtn v-if="props.media?.username" icon="mdi-account" color="white" class="me-1" />
|
||||||
|
<span v-if="props.media?.username" class="text-subtitle-2 me-4 text-white">
|
||||||
|
{{ props.media?.username }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
<VCardText v-if="lastUpdateText" class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
||||||
|
<VIcon icon="mdi-download" class="me-1" />
|
||||||
|
{{ lastUpdateText }}
|
||||||
|
</VCardText>
|
||||||
|
<div class="w-full absolute bottom-0">
|
||||||
|
<VProgressLinear
|
||||||
|
v-if="getPercentage() > 0"
|
||||||
|
:model-value="getPercentage()"
|
||||||
|
bg-color="success"
|
||||||
|
color="success"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="hover.isHovering" class="me-n3 absolute top-1 right-10">
|
||||||
|
<IconBtn><VIcon class="cursor-move text-white">mdi-drag</VIcon></IconBtn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
</VHover>
|
||||||
|
<!-- 订阅编辑弹窗 -->
|
||||||
|
<SubscribeEditDialog
|
||||||
|
v-if="subscribeEditDialog"
|
||||||
|
v-model="subscribeEditDialog"
|
||||||
|
:subid="props.media?.id"
|
||||||
|
@remove="onSubscribeEditRemove"
|
||||||
|
@save="onSubscribeEditSave"
|
||||||
|
@close="subscribeEditDialog = false"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 订阅文件信息弹窗 -->
|
<!-- 订阅文件信息弹窗 -->
|
||||||
<SubscribeFilesDialog
|
<SubscribeFilesDialog
|
||||||
v-if="subscribeFilesDialog"
|
v-if="subscribeFilesDialog"
|
||||||
v-model="subscribeFilesDialog"
|
v-model="subscribeFilesDialog"
|
||||||
:subid="props.media?.id"
|
:subid="props.media?.id"
|
||||||
@close="subscribeFilesDialog = false"
|
@close="subscribeFilesDialog = false"
|
||||||
/>
|
/>
|
||||||
<!-- 分享订阅弹窗 -->
|
<!-- 分享订阅弹窗 -->
|
||||||
<SubscribeShareDialog
|
<SubscribeShareDialog
|
||||||
v-if="subscribeShareDialog"
|
v-if="subscribeShareDialog"
|
||||||
v-model="subscribeShareDialog"
|
v-model="subscribeShareDialog"
|
||||||
:sub="props.media"
|
:sub="props.media"
|
||||||
@close="subscribeShareDialog = false"
|
@close="subscribeShareDialog = false"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.subscribe-card-background {
|
.subscribe-card-background {
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { formatDateDifference } from '@/@core/utils/formatters'
|
import { formatDateDifference } from '@/@core/utils/formatters'
|
||||||
import api from '@/api'
|
|
||||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
|
||||||
import type { SubscribeShare } from '@/api/types'
|
import type { SubscribeShare } from '@/api/types'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import { useConfirm } from 'vuetify-use-dialog'
|
|
||||||
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
||||||
|
import ForkSubscribeDialog from '../dialog/ForkSubscribeDialog.vue'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
media: Object as PropType<SubscribeShare>,
|
media: Object as PropType<SubscribeShare>,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 提示框
|
// 定义删除事件
|
||||||
const $toast = useToast()
|
const emit = defineEmits(['delete'])
|
||||||
|
|
||||||
// 确认框
|
|
||||||
const createConfirm = useConfirm()
|
|
||||||
|
|
||||||
// 从 provide 中获取全局设置
|
// 从 provide 中获取全局设置
|
||||||
const globalSettings: any = inject('globalSettings')
|
const globalSettings: any = inject('globalSettings')
|
||||||
@@ -28,6 +22,9 @@ const imageLoaded = ref(false)
|
|||||||
// 订阅编辑弹窗
|
// 订阅编辑弹窗
|
||||||
const subscribeEditDialog = ref(false)
|
const subscribeEditDialog = ref(false)
|
||||||
|
|
||||||
|
// 复用订阅弹窗
|
||||||
|
const forkSubscribeDialog = ref(false)
|
||||||
|
|
||||||
// 订阅ID
|
// 订阅ID
|
||||||
const subscribeId = ref<number>()
|
const subscribeId = ref<number>()
|
||||||
|
|
||||||
@@ -57,121 +54,128 @@ const posterUrl = computed(() => {
|
|||||||
return url
|
return url
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 获得mediaid
|
||||||
|
function getMediaId() {
|
||||||
|
if (props.media?.tmdbid) return `tmdb:${props.media?.tmdbid}`
|
||||||
|
else if (props.media?.doubanid) return `douban:${props.media?.doubanid}`
|
||||||
|
}
|
||||||
|
|
||||||
// 查看媒体详情
|
// 查看媒体详情
|
||||||
async function viewMediaDetail() {
|
async function viewMediaDetail() {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/media',
|
path: '/media',
|
||||||
query: {
|
query: {
|
||||||
mediaid: `${props.media?.tmdbid ? `tmdb:${props.media?.tmdbid}` : `douban:${props.media?.doubanid}`}`,
|
mediaid: getMediaId(),
|
||||||
|
title: props.media?.name,
|
||||||
|
year: props.media?.year,
|
||||||
type: props.media?.type,
|
type: props.media?.type,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复用订阅
|
// 复用订阅
|
||||||
async function forkSubscribe() {
|
function showForkSubscribe() {
|
||||||
// 开始处理
|
forkSubscribeDialog.value = true
|
||||||
startNProgress()
|
}
|
||||||
try {
|
|
||||||
// 确认
|
|
||||||
const isConfirmed = await createConfirm({
|
|
||||||
title: '确认',
|
|
||||||
content: `是否确认添加来自 ${props.media?.share_user} 分享的订阅:${props.media?.share_title}?`,
|
|
||||||
})
|
|
||||||
if (!isConfirmed) return
|
|
||||||
|
|
||||||
// 请求API
|
// 完成复用订阅
|
||||||
const result: { [key: string]: any } = await api.post('subscribe/fork', props.media)
|
function finishForkSubscribe(subid: number) {
|
||||||
|
subscribeId.value = subid
|
||||||
|
forkSubscribeDialog.value = false
|
||||||
|
subscribeEditDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 订阅状态
|
// 删除订阅分享时处理
|
||||||
if (result.success) {
|
function doDelete() {
|
||||||
$toast.success(`${props.media?.share_title} 添加订阅成功!`)
|
forkSubscribeDialog.value = false
|
||||||
// 弹出订阅编辑弹窗
|
// 通知父组件刷新
|
||||||
subscribeId.value = result.data.id
|
emit('delete')
|
||||||
subscribeEditDialog.value = true
|
|
||||||
} else {
|
|
||||||
$toast.error(`${props.media?.share_title} 添加订阅失败:${result.message}!`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
} finally {
|
|
||||||
doneNProgress()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VHover>
|
<div class="h-full">
|
||||||
<template #default="hover">
|
<VHover>
|
||||||
<VCard
|
<template #default="hover">
|
||||||
v-bind="hover.props"
|
<VCard
|
||||||
:key="props.media?.id"
|
v-bind="hover.props"
|
||||||
class="flex flex-col rounded-lg"
|
:key="props.media?.id"
|
||||||
:class="{
|
class="flex flex-col rounded-lg h-full"
|
||||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
:class="{
|
||||||
}"
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
min-height="170"
|
}"
|
||||||
@click="forkSubscribe"
|
min-height="170"
|
||||||
>
|
@click="showForkSubscribe"
|
||||||
<template #image>
|
>
|
||||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
<template #image>
|
||||||
<template #placeholder>
|
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
||||||
<div class="w-full h-full">
|
<template #placeholder>
|
||||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||||
|
</VImg>
|
||||||
|
</template>
|
||||||
|
<div class="h-full flex flex-col">
|
||||||
|
<VCardText class="flex items-center pb-1 grow">
|
||||||
|
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
|
||||||
|
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VImg>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div class="flex flex-col justify-center pl-2 xl:pl-4">
|
||||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
<div class="mr-2 min-w-0 text-lg font-bold text-white line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||||
</VImg>
|
{{ props.media?.share_title }}
|
||||||
</template>
|
</div>
|
||||||
<div>
|
<div class="text-sm font-medium text-gray-200 sm:pt-1 line-clamp-3 overflow-hidden text-ellipsis ...">
|
||||||
<VCardText class="flex items-center pb-1">
|
{{ props.media?.share_comment }}
|
||||||
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
|
</div>
|
||||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
|
||||||
<template #placeholder>
|
|
||||||
<div class="w-full h-full">
|
|
||||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</VImg>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-center pl-2 xl:pl-4">
|
|
||||||
<div class="mr-2 min-w-0 text-lg font-bold text-white line-clamp-2 overflow-hidden text-ellipsis ...">
|
|
||||||
{{ props.media?.share_title }}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm font-medium text-gray-200 sm:pt-1 line-clamp-3 overflow-hidden text-ellipsis ...">
|
</VCardText>
|
||||||
{{ props.media?.share_comment }}
|
<VCardText class="flex justify-space-between align-center flex-wrap">
|
||||||
|
<div class="flex align-center">
|
||||||
|
<IconBtn v-bind="props" icon="mdi-account" color="white" class="me-1" />
|
||||||
|
<div class="text-subtitle-2 me-4 text-white">
|
||||||
|
{{ props.media?.share_user }}
|
||||||
|
</div>
|
||||||
|
<IconBtn v-if="props.media?.count" icon="mdi-fire" color="white" class="me-1" />
|
||||||
|
<span v-if="props.media?.count" class="text-subtitle-2 me-4 text-white">
|
||||||
|
{{ props.media?.count.toLocaleString() }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</VCardText>
|
||||||
</VCardText>
|
<VCardText class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
||||||
<VCardText class="flex justify-space-between align-center flex-wrap">
|
<VIcon icon="mdi-calcdar" class="me-1" />
|
||||||
<div class="flex align-center">
|
{{ dateText }}
|
||||||
<IconBtn v-bind="props" icon="mdi-account" color="white" class="me-1" />
|
</VCardText>
|
||||||
<div class="text-subtitle-2 me-4 text-white">
|
</div>
|
||||||
{{ props.media?.share_user }}
|
</VCard>
|
||||||
</div>
|
</template>
|
||||||
<IconBtn v-if="props.media?.count" icon="mdi-fire" color="error" class="me-1" />
|
</VHover>
|
||||||
<span v-if="props.media?.count" class="text-subtitle-2 me-4 text-white">
|
<!-- 订阅编辑弹窗 -->
|
||||||
{{ props.media?.count.toLocaleString() }}
|
<SubscribeEditDialog
|
||||||
</span>
|
v-if="subscribeEditDialog"
|
||||||
</div>
|
v-model="subscribeEditDialog"
|
||||||
</VCardText>
|
:subid="subscribeId"
|
||||||
<VCardText class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
@close="subscribeEditDialog = false"
|
||||||
<VIcon icon="mdi-calcdar" class="me-1" />
|
@save="subscribeEditDialog = false"
|
||||||
{{ dateText }}
|
@remove="subscribeEditDialog = false"
|
||||||
</VCardText>
|
/>
|
||||||
</div>
|
<!-- 复用订阅弹窗 -->
|
||||||
</VCard>
|
<ForkSubscribeDialog
|
||||||
</template>
|
v-if="forkSubscribeDialog"
|
||||||
</VHover>
|
v-model="forkSubscribeDialog"
|
||||||
<!-- 订阅编辑弹窗 -->
|
:media="props.media"
|
||||||
<SubscribeEditDialog
|
@close="forkSubscribeDialog = false"
|
||||||
v-if="subscribeEditDialog"
|
@fork="finishForkSubscribe"
|
||||||
v-model="subscribeEditDialog"
|
@delete="doDelete"
|
||||||
:subid="subscribeId"
|
/>
|
||||||
@close="subscribeEditDialog = false"
|
</div>
|
||||||
@save="subscribeEditDialog = false"
|
|
||||||
@remove="subscribeEditDialog = false"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.subscribe-card-background {
|
.subscribe-card-background {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { Subscribe, User } from '@/api/types'
|
import { Subscribe, User } from '@/api/types'
|
||||||
import store from '@/store'
|
import { useUserStore } from '@/stores'
|
||||||
import avatar1 from '@images/avatars/avatar-1.png'
|
import avatar1 from '@images/avatars/avatar-1.png'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import { useConfirm } from 'vuetify-use-dialog'
|
import { useConfirm } from 'vuetify-use-dialog'
|
||||||
@@ -22,10 +22,10 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 当前用户的ID
|
// 当前用户的ID
|
||||||
const currentLoginUserId = computed(() => store.state.auth.userID)
|
const currentLoginUserId = computed(() => useUserStore().userID)
|
||||||
|
|
||||||
// 当前用户是否是管理员
|
// 当前用户是否是管理员
|
||||||
const currentUserIsSuperuser = computed(() => store.state.auth.superUser)
|
const currentUserIsSuperuser = computed(() => useUserStore().superUser)
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
// 定义触发的自定义事件
|
||||||
const emit = defineEmits(['remove', 'save'])
|
const emit = defineEmits(['remove', 'save'])
|
||||||
@@ -161,14 +161,7 @@ onMounted(() => {
|
|||||||
</VList>
|
</VList>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardText class="flex flex-row justify-center">
|
<VCardText class="flex flex-row justify-center">
|
||||||
<VBtn
|
<VBtn v-if="currentUserIsSuperuser" color="primary" class="me-4" @click="editUser"> 编辑 </VBtn>
|
||||||
v-if="currentUserIsSuperuser"
|
|
||||||
color="primary"
|
|
||||||
class="me-4"
|
|
||||||
@click="editUser"
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</VBtn>
|
|
||||||
<VBtn
|
<VBtn
|
||||||
v-if="currentUserIsSuperuser && props.user.id != currentLoginUserId"
|
v-if="currentUserIsSuperuser && props.user.id != currentLoginUserId"
|
||||||
color="error"
|
color="error"
|
||||||
|
|||||||
318
src/components/cards/WorkflowTaskCard.vue
Normal file
318
src/components/cards/WorkflowTaskCard.vue
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Workflow } from '@/api/types'
|
||||||
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import { useConfirm } from 'vuetify-use-dialog'
|
||||||
|
import WorkflowAddEditDialog from '@/components/dialog/WorkflowAddEditDialog.vue'
|
||||||
|
import WorkflowActionsDialog from '@/components/dialog/WorkflowActionsDialog.vue'
|
||||||
|
import api from '@/api'
|
||||||
|
|
||||||
|
// 定义输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
workflow: {
|
||||||
|
required: true,
|
||||||
|
type: Object as PropType<Workflow>,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['refresh'])
|
||||||
|
|
||||||
|
// 提示框
|
||||||
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 确认框
|
||||||
|
const createConfirm = useConfirm()
|
||||||
|
|
||||||
|
// 编辑对话框
|
||||||
|
const editDialog = ref(false)
|
||||||
|
|
||||||
|
// 流程对话框
|
||||||
|
const flowDialog = ref(false)
|
||||||
|
|
||||||
|
// 加载中
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 编辑任务
|
||||||
|
function handleEdit(item: Workflow) {
|
||||||
|
editDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑流程
|
||||||
|
function handleFlow(item: Workflow) {
|
||||||
|
flowDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算已完成的动作数
|
||||||
|
function resolveDoneActions(item: Workflow) {
|
||||||
|
return item.current_action?.split(',').length || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑完成
|
||||||
|
function editDone() {
|
||||||
|
editDialog.value = false
|
||||||
|
flowDialog.value = false
|
||||||
|
emit('refresh')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除任务
|
||||||
|
async function handleDelete(item: Workflow) {
|
||||||
|
const isConfirmed = await createConfirm({
|
||||||
|
title: '确认',
|
||||||
|
content: `是否确认删除任务 ${item.name} ?`,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isConfirmed) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: string } = await api.delete(`workflow/${item.id}`)
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success('删除任务成功!')
|
||||||
|
emit('refresh')
|
||||||
|
} else {
|
||||||
|
$toast.error(`删除任务失败:${result.message}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始任务
|
||||||
|
async function handleEnable(item: Workflow) {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: string } = await api.post(`workflow/${item.id}/start`)
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success('启用任务成功!')
|
||||||
|
emit('refresh')
|
||||||
|
} else {
|
||||||
|
$toast.error(`启用任务失败:${result.message}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停用任务
|
||||||
|
async function handlePause(item: Workflow) {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: string } = await api.post(`workflow/${item.id}/pause`)
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success('停用任务成功!')
|
||||||
|
emit('refresh')
|
||||||
|
} else {
|
||||||
|
$toast.error(`停用任务失败:${result.message}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即执行任务
|
||||||
|
async function handleRun(item: Workflow, from_begin: boolean) {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
setTimeout(() => {
|
||||||
|
emit('refresh')
|
||||||
|
}, 500)
|
||||||
|
const result: { [key: string]: string } = await api.post(`workflow/${item.id}/run?from_begin=${from_begin}`, {
|
||||||
|
from_begin,
|
||||||
|
})
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success('任务执行完成!')
|
||||||
|
emit('refresh')
|
||||||
|
} else {
|
||||||
|
$toast.error(`任务执行失败:${result.message}`)
|
||||||
|
emit('refresh')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置任务
|
||||||
|
async function handleReset(item: Workflow) {
|
||||||
|
const isConfirmed = await createConfirm({
|
||||||
|
title: '确认',
|
||||||
|
content: `是否确认重置任务 ${item.name} ?`,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isConfirmed) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: string } = await api.post(`workflow/${item.id}/reset`)
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success('重置任务成功!')
|
||||||
|
emit('refresh')
|
||||||
|
} else {
|
||||||
|
$toast.error(`重置任务失败:${result.message}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算状态颜色
|
||||||
|
const resolveStatusVariant = (status: string | undefined) => {
|
||||||
|
if (status === 'S') return { color: 'success', text: '成功' }
|
||||||
|
else if (status === 'R') return { color: 'primary', text: '运行中' }
|
||||||
|
else if (status === 'F') return { color: 'error', text: '失败' }
|
||||||
|
else if (status === 'P') return { color: 'secondary', text: '暂停' }
|
||||||
|
else return { color: 'info', text: '等待' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算当前动作占比
|
||||||
|
const resolveProgress = (item: Workflow) => {
|
||||||
|
const current_action_length = item.current_action?.split(',').length || 0
|
||||||
|
return item.actions?.length ? Math.round((current_action_length / (item.actions.length || 1)) * 100) : 0
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="h-full">
|
||||||
|
<VCard class="mx-auto h-full" @click="handleFlow(workflow)" :ripple="false" :loading="loading">
|
||||||
|
<VCardItem class="py-3" :class="`bg-${resolveStatusVariant(workflow?.state).color}`">
|
||||||
|
<template #prepend>
|
||||||
|
<VAvatar variant="text" class="me-2">
|
||||||
|
<VIcon
|
||||||
|
v-if="workflow?.state === 'P'"
|
||||||
|
color="success"
|
||||||
|
size="x-large"
|
||||||
|
icon="mdi-play"
|
||||||
|
@click.stop="handleEnable(workflow)"
|
||||||
|
/>
|
||||||
|
<VIcon v-else color="warning" icon="mdi-pause" size="x-large" @click.stop="handlePause(workflow)" />
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VCardTitle class="text-white">
|
||||||
|
{{ workflow?.name }}
|
||||||
|
</VCardTitle>
|
||||||
|
<VCardSubtitle class="text-white">{{ workflow?.description }}</VCardSubtitle>
|
||||||
|
<template #append>
|
||||||
|
<IconBtn>
|
||||||
|
<VIcon icon="mdi-vector-polyline-edit" @click.stop="handleFlow(workflow)" />
|
||||||
|
</IconBtn>
|
||||||
|
<IconBtn>
|
||||||
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
|
<VMenu activator="parent" close-on-content-click>
|
||||||
|
<VList>
|
||||||
|
<VListItem variant="plain" base-color="primary" @click="handleEdit(workflow)">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-note-edit" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>编辑任务</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem
|
||||||
|
v-if="workflow.current_action"
|
||||||
|
variant="plain"
|
||||||
|
base-color="info"
|
||||||
|
@click="handleRun(workflow, false)"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-play-speed" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>继续执行</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem
|
||||||
|
v-if="workflow.current_action"
|
||||||
|
variant="plain"
|
||||||
|
base-color="info"
|
||||||
|
@click="handleRun(workflow, true)"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-replay" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>重新执行</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem v-else variant="plain" base-color="info" @click="handleRun(workflow, true)">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-run" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>立即执行</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem variant="plain" base-color="warning" @click="handleReset(workflow)">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-restore-alert" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>重置任务</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem variant="plain" base-color="error" @click="handleDelete(workflow)">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-delete" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>删除任务</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
|
</IconBtn>
|
||||||
|
</template>
|
||||||
|
</VCardItem>
|
||||||
|
<VDivider />
|
||||||
|
<VCardText>
|
||||||
|
<div class="d-flex flex-column gap-y-4">
|
||||||
|
<div class="d-flex flex-wrap gap-x-6">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="mb-1">定时</div>
|
||||||
|
<h5 class="text-h6">{{ workflow?.timer }}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="mb-1">状态</div>
|
||||||
|
<h5 class="text-h6" :class="`text-${resolveStatusVariant(workflow?.state).color}`">
|
||||||
|
{{ resolveStatusVariant(workflow?.state).text }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap gap-x-6">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="mb-1">动作数</div>
|
||||||
|
<div>
|
||||||
|
<VAvatar size="32" color="primary" variant="tonal">
|
||||||
|
<span class="text-sm">{{ workflow?.actions?.length }}</span>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="mb-1">已执行次数</div>
|
||||||
|
<h5 class="text-h6">{{ workflow?.run_count }}</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap gap-x-6">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="mb-1">进度</div>
|
||||||
|
<div class="d-flex align-center gap-5">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<VProgressLinear color="info" rounded :model-value="resolveProgress(workflow)" />
|
||||||
|
</div>
|
||||||
|
<span> {{ resolveProgress(workflow) }}% </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap gap-x-6" v-if="workflow?.result">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="mb-1">错误信息</div>
|
||||||
|
<div class="text-error">{{ workflow?.result }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
<!-- 流程对话框 -->
|
||||||
|
<WorkflowActionsDialog
|
||||||
|
v-if="flowDialog"
|
||||||
|
v-model="flowDialog"
|
||||||
|
@close="flowDialog = false"
|
||||||
|
@save="editDone"
|
||||||
|
:workflow="workflow"
|
||||||
|
/>
|
||||||
|
<!-- 编辑对话框 -->
|
||||||
|
<WorkflowAddEditDialog
|
||||||
|
v-if="editDialog"
|
||||||
|
v-model="editDialog"
|
||||||
|
@close="editDialog = false"
|
||||||
|
@save="editDone"
|
||||||
|
:workflow="workflow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -13,6 +13,9 @@ const props = defineProps({
|
|||||||
torrent: Object as PropType<TorrentInfo>,
|
torrent: Object as PropType<TorrentInfo>,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 定义成功和失败事件
|
||||||
|
const emit = defineEmits(['done', 'error', 'close'])
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
@@ -22,8 +25,8 @@ const selectedDownloader = ref<string | null>(null)
|
|||||||
// 选择的保存目录
|
// 选择的保存目录
|
||||||
const selectedDirectory = ref<string | null>(null)
|
const selectedDirectory = ref<string | null>(null)
|
||||||
|
|
||||||
// 定义成功和失败事件
|
// 下载器
|
||||||
const emit = defineEmits(['done', 'error', 'close'])
|
const downloaders = ref<DownloaderConf[]>([])
|
||||||
|
|
||||||
// 所有目录设置
|
// 所有目录设置
|
||||||
const directories = ref<TransferDirectoryConf[]>([])
|
const directories = ref<TransferDirectoryConf[]>([])
|
||||||
@@ -53,14 +56,10 @@ const targetDirectories = computed(() => {
|
|||||||
return [...new Set(downloadDirectories)]
|
return [...new Set(downloadDirectories)]
|
||||||
})
|
})
|
||||||
|
|
||||||
// 下载器
|
|
||||||
const downloaders = ref<DownloaderConf[]>([])
|
|
||||||
|
|
||||||
// 调用API查询下载器设置
|
// 调用API查询下载器设置
|
||||||
async function loadDownloaderSetting() {
|
async function loadDownloaderSetting() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
|
downloaders.value = await api.get('download/clients')
|
||||||
downloaders.value = result.data?.value ?? []
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
import QrcodeVue from 'qrcode.vue'
|
import QrcodeVue from 'qrcode.vue'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
|
|
||||||
|
// 定义输入
|
||||||
|
const props = defineProps({
|
||||||
|
conf: {
|
||||||
|
type: Object as PropType<{ [key: string]: any }>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// 定义事件
|
// 定义事件
|
||||||
const emit = defineEmits(['done', 'close'])
|
const emit = defineEmits(['done', 'close'])
|
||||||
|
|
||||||
@@ -25,6 +33,10 @@ let timeoutTimer: NodeJS.Timeout | undefined = undefined
|
|||||||
|
|
||||||
// 完成
|
// 完成
|
||||||
async function handleDone() {
|
async function handleDone() {
|
||||||
|
clearTimeout(timeoutTimer)
|
||||||
|
if (props.conf?.refreshToken) {
|
||||||
|
await savaAliPanConfig()
|
||||||
|
}
|
||||||
emit('done')
|
emit('done')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +87,15 @@ async function checkQrcode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存cookie设置
|
||||||
|
async function savaAliPanConfig() {
|
||||||
|
try {
|
||||||
|
await api.post(`storage/save/alipan`, props.conf)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await getQrcode()
|
await getQrcode()
|
||||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||||
@@ -97,6 +118,13 @@ onUnmounted(() => {
|
|||||||
<template #prepend />
|
<template #prepend />
|
||||||
</VAlert>
|
</VAlert>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol class="mt-2">
|
||||||
|
<VTextField label="自定义refreshToken" v-model="props.conf.refreshToken" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
<VCardActions>
|
<VCardActions>
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
|
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
|
||||||
|
|||||||
277
src/components/dialog/ForkSubscribeDialog.vue
Normal file
277
src/components/dialog/ForkSubscribeDialog.vue
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import api from '@/api'
|
||||||
|
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||||
|
import { SubscribeShare } from '@/api/types'
|
||||||
|
import router from '@/router'
|
||||||
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import { VBtn } from 'vuetify/lib/components/index.mjs'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
media: Object as PropType<SubscribeShare>,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['fork', 'delete', 'close'])
|
||||||
|
|
||||||
|
// 从 provide 中获取全局设置
|
||||||
|
const globalSettings: any = inject('globalSettings')
|
||||||
|
|
||||||
|
// 提示框
|
||||||
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 处理中
|
||||||
|
const processing = ref(false)
|
||||||
|
|
||||||
|
// 删除中
|
||||||
|
const deleting = ref(false)
|
||||||
|
|
||||||
|
// 是否折叠
|
||||||
|
const isExpanded = ref(false)
|
||||||
|
|
||||||
|
// follow用户列表
|
||||||
|
const followUsers = ref<string[]>([])
|
||||||
|
|
||||||
|
// 当前用户是否已follow
|
||||||
|
const isFollowed = computed(() => followUsers.value.includes(props.media?.share_uid || ''))
|
||||||
|
|
||||||
|
// 折叠展开
|
||||||
|
function toggleExpand() {
|
||||||
|
isExpanded.value = !isExpanded.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载follow用户列表
|
||||||
|
async function queryFollowUsers() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get('system/setting/FollowSubscribers')
|
||||||
|
followUsers.value = result.data?.value ?? []
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// follow用户
|
||||||
|
async function followUser() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.post(`subscribe/follow?share_uid=${props.media?.share_uid}`)
|
||||||
|
if (result.success) {
|
||||||
|
queryFollowUsers()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unfollow用户
|
||||||
|
async function unfollowUser() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.delete('subscribe/follow', {
|
||||||
|
params: {
|
||||||
|
share_uid: props.media?.share_uid,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (result.success) {
|
||||||
|
queryFollowUsers()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算海报图片地址
|
||||||
|
const posterUrl = computed(() => {
|
||||||
|
const url = props.media?.poster
|
||||||
|
// 使用图片缓存
|
||||||
|
if (globalSettings.GLOBAL_IMAGE_CACHE && url)
|
||||||
|
return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`
|
||||||
|
return url
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获得mediaid
|
||||||
|
function getMediaId() {
|
||||||
|
if (props.media?.tmdbid) return `tmdb:${props.media?.tmdbid}`
|
||||||
|
else if (props.media?.doubanid) return `douban:${props.media?.doubanid}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看媒体详情
|
||||||
|
async function viewMediaDetail() {
|
||||||
|
router.push({
|
||||||
|
path: '/media',
|
||||||
|
query: {
|
||||||
|
mediaid: getMediaId(),
|
||||||
|
title: props.media?.name,
|
||||||
|
year: props.media?.year,
|
||||||
|
type: props.media?.type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复用订阅
|
||||||
|
async function doFork() {
|
||||||
|
// 开始处理
|
||||||
|
startNProgress()
|
||||||
|
try {
|
||||||
|
processing.value = true
|
||||||
|
// 请求API
|
||||||
|
const result: { [key: string]: any } = await api.post('subscribe/fork', props.media)
|
||||||
|
// 订阅状态
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success(`${props.media?.share_title} 添加订阅成功!`)
|
||||||
|
// 完成
|
||||||
|
emit('fork', result.data.id)
|
||||||
|
} else {
|
||||||
|
$toast.error(`${props.media?.share_title} 添加订阅失败:${result.message}!`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
processing.value = false
|
||||||
|
doneNProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除订阅分享
|
||||||
|
async function doDelete() {
|
||||||
|
// 开始处理
|
||||||
|
startNProgress()
|
||||||
|
try {
|
||||||
|
deleting.value = true
|
||||||
|
// 请求API
|
||||||
|
const result: { [key: string]: any } = await api.delete(`subscribe/share/${props.media?.id}`, {
|
||||||
|
params: {
|
||||||
|
share_uid: globalSettings.USER_UNIQUE_ID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// 订阅状态
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success(`${props.media?.share_title} 取消分享成功!`)
|
||||||
|
// 完成
|
||||||
|
emit('delete', result.data.id)
|
||||||
|
} else {
|
||||||
|
$toast.error(`${props.media?.share_title} 取消分享失败:${result.message}!`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
deleting.value = false
|
||||||
|
doneNProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
queryFollowUsers()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VDialog max-width="40rem" scrollable>
|
||||||
|
<VCard>
|
||||||
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
|
<VCardText>
|
||||||
|
<VCol>
|
||||||
|
<div class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row">
|
||||||
|
<div class="ma-auto">
|
||||||
|
<VImg
|
||||||
|
width="10rem"
|
||||||
|
aspect-ratio="2/3"
|
||||||
|
class="object-cover aspect-w-2 aspect-h-3 rounded-lg ring-1 ring-gray-500"
|
||||||
|
:src="posterUrl"
|
||||||
|
@click="viewMediaDetail"
|
||||||
|
cover
|
||||||
|
>
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VImg>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<VCardItem>
|
||||||
|
<VCardTitle
|
||||||
|
class="text-center text-md-left break-words whitespace-break-spaces line-clamp-2 overflow-hidden text-ellipsis"
|
||||||
|
>
|
||||||
|
{{ props.media?.share_title }}
|
||||||
|
</VCardTitle>
|
||||||
|
<VCardSubtitle
|
||||||
|
class="text-center text-md-left break-words whitespace-break-spaces line-clamp-4 overflow-hidden text-ellipsis"
|
||||||
|
>
|
||||||
|
{{ props.media?.share_comment }}
|
||||||
|
</VCardSubtitle>
|
||||||
|
<VList lines="one">
|
||||||
|
<VListItem class="ps-0">
|
||||||
|
<VListItemTitle class="text-center text-md-left">
|
||||||
|
<span class="font-weight-medium">分享人:</span>
|
||||||
|
<span class="text-body-1"> {{ media?.share_user }}</span>
|
||||||
|
</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem class="ps-0" v-if="media?.keyword">
|
||||||
|
<VListItemTitle class="text-center text-md-left">
|
||||||
|
<span class="font-weight-medium">搜索词:</span>
|
||||||
|
<span class="text-body-1"> {{ media?.keyword }}</span>
|
||||||
|
</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem class="ps-0" v-if="media?.custom_words" @click.stop="toggleExpand">
|
||||||
|
<VListItemTitle
|
||||||
|
class="text-center text-md-left break-words whitespace-break-spaces"
|
||||||
|
:class="{
|
||||||
|
'line-clamp-4 overflow-hidden text-ellipsis': !isExpanded,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="font-weight-medium">识别词:</span>
|
||||||
|
<span class="text-body-1"> {{ media?.custom_words }}</span>
|
||||||
|
</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
<div class="text-center text-md-left">
|
||||||
|
<div>
|
||||||
|
<VBtn
|
||||||
|
color="primary"
|
||||||
|
:disabled="processing"
|
||||||
|
@click="doFork"
|
||||||
|
prepend-icon="mdi-heart"
|
||||||
|
:loading="processing"
|
||||||
|
>
|
||||||
|
订阅
|
||||||
|
</VBtn>
|
||||||
|
<VBtn
|
||||||
|
v-if="props.media?.share_uid && props.media?.share_uid === globalSettings.USER_UNIQUE_ID"
|
||||||
|
color="error"
|
||||||
|
:disabled="deleting"
|
||||||
|
@click="doDelete"
|
||||||
|
prepend-icon="mdi-delete"
|
||||||
|
:loading="deleting"
|
||||||
|
class="ms-2"
|
||||||
|
>
|
||||||
|
取消分享
|
||||||
|
</VBtn>
|
||||||
|
<VBtn
|
||||||
|
v-else-if="isFollowed && props.media?.share_uid"
|
||||||
|
color="warning"
|
||||||
|
@click="unfollowUser"
|
||||||
|
prepend-icon="mdi-account-remove"
|
||||||
|
class="ms-2"
|
||||||
|
>
|
||||||
|
取消关注
|
||||||
|
</VBtn>
|
||||||
|
<VBtn
|
||||||
|
v-else-if="props.media?.share_uid"
|
||||||
|
@click="followUser"
|
||||||
|
color="info"
|
||||||
|
prepend-icon="mdi-account-plus"
|
||||||
|
class="ms-2"
|
||||||
|
>
|
||||||
|
关注
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs mt-2" v-if="props.media?.count">
|
||||||
|
<VIcon icon="mdi-fire" />共 {{ props.media?.count?.toLocaleString() }} 次复用
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCardItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCol>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
</template>
|
||||||
@@ -2,17 +2,18 @@
|
|||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: String,
|
title: String,
|
||||||
|
dataType: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 定义事件
|
|
||||||
const emit = defineEmits(['update:modelValue', 'close'])
|
|
||||||
|
|
||||||
// 代码
|
// 代码
|
||||||
const codeString = ref('')
|
const codeString = ref('')
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['close', 'save'])
|
||||||
|
|
||||||
// 导入
|
// 导入
|
||||||
function handleImport() {
|
function handleImport() {
|
||||||
emit('update:modelValue', codeString.value)
|
emit('save', props.dataType, codeString)
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
22
src/components/dialog/MediaInfoDialog.vue
Normal file
22
src/components/dialog/MediaInfoDialog.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Context } from '@/api/types'
|
||||||
|
import MediaInfoCard from '../cards/MediaInfoCard.vue'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
defineProps({
|
||||||
|
context: Object as PropType<Context>,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VDialog max-width="50rem">
|
||||||
|
<VCard>
|
||||||
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
|
<VCardItem>
|
||||||
|
<MediaInfoCard :context="context" />
|
||||||
|
</VCardItem>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
</template>
|
||||||
109
src/components/dialog/PluginConfigDialog.vue
Normal file
109
src/components/dialog/PluginConfigDialog.vue
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
import type { Plugin } from '@/api/types'
|
||||||
|
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||||
|
import api from '@/api'
|
||||||
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import FormRender from '../render/FormRender.vue'
|
||||||
|
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
plugin: {
|
||||||
|
type: Object as PropType<Plugin>,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['close', 'save', 'switch'])
|
||||||
|
|
||||||
|
// 显示器宽度
|
||||||
|
const display = useDisplay()
|
||||||
|
|
||||||
|
// 插件配置表单数据
|
||||||
|
const pluginConfigForm = ref({})
|
||||||
|
|
||||||
|
// 插件表单配置项
|
||||||
|
let pluginFormItems = reactive([])
|
||||||
|
|
||||||
|
// 进度框
|
||||||
|
const progressDialog = ref(false)
|
||||||
|
|
||||||
|
// 进度文字
|
||||||
|
const progressText = ref('')
|
||||||
|
|
||||||
|
// 提示框
|
||||||
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 是否刷新
|
||||||
|
const isRefreshed = ref(false)
|
||||||
|
|
||||||
|
// 调用API读取表单页面
|
||||||
|
async function loadPluginForm() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get(`plugin/form/${props.plugin?.id}`)
|
||||||
|
if (result) {
|
||||||
|
pluginFormItems = result.conf
|
||||||
|
if (result.model) pluginConfigForm.value = result.model
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
isRefreshed.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API读取配置数据
|
||||||
|
async function loadPluginConf() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get(`plugin/${props.plugin?.id}`)
|
||||||
|
if (!isNullOrEmptyObject(result)) pluginConfigForm.value = result
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
isRefreshed.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用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) {
|
||||||
|
progressDialog.value = false
|
||||||
|
$toast.success(`插件 ${props.plugin?.plugin_name} 配置已保存`)
|
||||||
|
// 通知父组件刷新
|
||||||
|
emit('save')
|
||||||
|
} else {
|
||||||
|
progressDialog.value = false
|
||||||
|
$toast.error(`插件 ${props.plugin?.plugin_name} 配置保存失败:${result.message}}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
await loadPluginForm()
|
||||||
|
await loadPluginConf()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VDialog scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
||||||
|
<VCard :title="`${props.plugin?.plugin_name} - 配置`" class="rounded-t">
|
||||||
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
|
<VDivider />
|
||||||
|
<VCardText v-if="isRefreshed">
|
||||||
|
<FormRender v-for="(item, index) in pluginFormItems" :key="index" :config="item" :model="pluginConfigForm" />
|
||||||
|
</VCardText>
|
||||||
|
<VCardActions class="pt-3">
|
||||||
|
<VBtn v-if="props.plugin?.has_page" @click="emit('switch')" variant="outlined" color="info"> 查看数据 </VBtn>
|
||||||
|
<VSpacer />
|
||||||
|
<VBtn @click="savePluginConf" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 保存 </VBtn>
|
||||||
|
</VCardActions>
|
||||||
|
</VCard>
|
||||||
|
<!-- 进度框 -->
|
||||||
|
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||||
|
</VDialog>
|
||||||
|
</template>
|
||||||
63
src/components/dialog/PluginDataDialog.vue
Normal file
63
src/components/dialog/PluginDataDialog.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
import type { Plugin } from '@/api/types'
|
||||||
|
import PageRender from '@/components/render/PageRender.vue'
|
||||||
|
import api from '@/api'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
plugin: {
|
||||||
|
type: Object as PropType<Plugin>,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['close', 'save', 'switch'])
|
||||||
|
|
||||||
|
// 显示器宽度
|
||||||
|
const display = useDisplay()
|
||||||
|
// APP
|
||||||
|
const appMode = inject('pwaMode') && display.mdAndDown.value
|
||||||
|
|
||||||
|
// 是否刷新
|
||||||
|
const isRefreshed = ref(false)
|
||||||
|
|
||||||
|
// 插件数据页面配置项
|
||||||
|
let pluginPageItems = ref([])
|
||||||
|
|
||||||
|
// 调用API读取数据页面
|
||||||
|
async function loadPluginPage() {
|
||||||
|
try {
|
||||||
|
const result: [] = await api.get(`plugin/page/${props.plugin?.id}`)
|
||||||
|
if (result) pluginPageItems.value = result
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
isRefreshed.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadPluginPage()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VDialog scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||||
|
<VCard :title="`${props.plugin?.plugin_name}`" class="rounded-t">
|
||||||
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
|
<LoadingBanner v-if="!isRefreshed" class="mt-5" />
|
||||||
|
<VCardText v-else class="min-h-40">
|
||||||
|
<PageRender @action="loadPluginPage" v-for="(item, index) in pluginPageItems" :key="index" :config="item" />
|
||||||
|
</VCardText>
|
||||||
|
<VFab
|
||||||
|
icon="mdi-cog"
|
||||||
|
location="bottom"
|
||||||
|
size="x-large"
|
||||||
|
fixed
|
||||||
|
app
|
||||||
|
appear
|
||||||
|
@click="emit('switch')"
|
||||||
|
:class="{ 'mb-10': appMode }"
|
||||||
|
/>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
</template>
|
||||||
@@ -34,7 +34,7 @@ async function saveHandle() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
$toast.success('插件仓库保存成功')
|
$toast.success('插件仓库保存成功')
|
||||||
emit('save')
|
emit('save')
|
||||||
} else $toast.error('插件仓库保存失败!')
|
} else $toast.error(`插件仓库保存失败:${result?.message}!`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import MediaIdSelector from '../misc/MediaIdSelector.vue'
|
import MediaIdSelector from '../misc/MediaIdSelector.vue'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { storageOptions } from '@/api/constants'
|
import { storageOptions, transferTypeOptions } from '@/api/constants'
|
||||||
import { numberValidator } from '@/@validators'
|
import { numberValidator } from '@/@validators'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import ProgressDialog from './ProgressDialog.vue'
|
import ProgressDialog from './ProgressDialog.vue'
|
||||||
import { FileItem, TransferDirectoryConf } from '@/api/types'
|
import { FileItem, TransferDirectoryConf, TransferForm } from '@/api/types'
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
@@ -49,7 +49,7 @@ const progressEventSource = ref<EventSource>()
|
|||||||
const progressDialog = ref(false)
|
const progressDialog = ref(false)
|
||||||
|
|
||||||
// 整理进度文本
|
// 整理进度文本
|
||||||
const progressText = ref('请稍候 ...')
|
const progressText = ref('正在处理 ...')
|
||||||
|
|
||||||
// 整理进度
|
// 整理进度
|
||||||
const progressValue = ref(0)
|
const progressValue = ref(0)
|
||||||
@@ -65,21 +65,21 @@ const dialogTitle = computed(() => {
|
|||||||
return '手动整理'
|
return '手动整理'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 禁用指定集数
|
||||||
|
const disableEpisodeDetail = computed(() => {
|
||||||
|
if (props.items) {
|
||||||
|
if (transferForm.episode_format) return false
|
||||||
|
return !(props.items.length === 1 && props.items[0].type !== 'dir')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 表单
|
// 表单
|
||||||
const transferForm = reactive({
|
const transferForm = reactive<TransferForm>({
|
||||||
fileitem: {},
|
fileitem: {} as FileItem,
|
||||||
logid: 0,
|
logid: 0,
|
||||||
target_storage: props.target_storage ?? 'local',
|
target_storage: props.target_storage ?? 'local',
|
||||||
target_path: props.target_path ?? null,
|
|
||||||
tmdbid: null,
|
|
||||||
doubanid: null,
|
|
||||||
season: null,
|
|
||||||
type_name: '',
|
|
||||||
transfer_type: '',
|
transfer_type: '',
|
||||||
episode_format: '',
|
target_path: '',
|
||||||
episode_detail: '',
|
|
||||||
episode_part: '',
|
|
||||||
episode_offset: null,
|
|
||||||
min_filesize: 0,
|
min_filesize: 0,
|
||||||
scrape: false,
|
scrape: false,
|
||||||
from_history: false,
|
from_history: false,
|
||||||
@@ -87,7 +87,6 @@ const transferForm = reactive({
|
|||||||
|
|
||||||
// 所有媒体库目录
|
// 所有媒体库目录
|
||||||
const directories = ref<TransferDirectoryConf[]>([])
|
const directories = ref<TransferDirectoryConf[]>([])
|
||||||
|
|
||||||
// 查询目录
|
// 查询目录
|
||||||
async function loadDirectories() {
|
async function loadDirectories() {
|
||||||
try {
|
try {
|
||||||
@@ -104,16 +103,58 @@ const targetDirectories = computed(() => {
|
|||||||
return [...new Set(libraryDirectories)]
|
return [...new Set(libraryDirectories)]
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听目的路径变化,自动查询目录削配置
|
// 监听目的路径变化,配置默认值
|
||||||
watch(transferForm, async () => {
|
watch(
|
||||||
if (transferForm.target_path) {
|
() => transferForm.target_path,
|
||||||
const directory = directories.value.find(item => item.library_path === transferForm.target_path)
|
async newPath => {
|
||||||
if (directory) {
|
if (newPath) {
|
||||||
transferForm.scrape = directory.scraping ?? false
|
const directory = directories.value.find(item => item.library_path === newPath)
|
||||||
transferForm.transfer_type = directory.transfer_type ?? ''
|
if (directory) {
|
||||||
|
transferForm.target_storage = directory.library_storage ?? 'local'
|
||||||
|
transferForm.transfer_type = transferForm.transfer_type || directory.transfer_type
|
||||||
|
transferForm.scrape = directory.scraping ?? false
|
||||||
|
transferForm.library_category_folder = directory.library_category_folder ?? false
|
||||||
|
transferForm.library_type_folder = directory.library_type_folder ?? false
|
||||||
|
} else {
|
||||||
|
transferForm.transfer_type = transferForm.transfer_type || 'copy'
|
||||||
|
transferForm.scrape = false
|
||||||
|
transferForm.library_category_folder = false
|
||||||
|
transferForm.library_type_folder = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 路径为空时, 恢复到`自动`条件
|
||||||
|
transferForm.transfer_type = ''
|
||||||
|
transferForm.library_type_folder = undefined
|
||||||
|
transferForm.library_category_folder = undefined
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 整理文件
|
||||||
|
async function handleTransfer(item: FileItem, background: boolean = false) {
|
||||||
|
transferForm.fileitem = item
|
||||||
|
transferForm.logid = 0
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.post(`transfer/manual?background=${background}`, transferForm)
|
||||||
|
if (!result.success) $toast.error(result.message)
|
||||||
|
else if (background) $toast.success(`文件 ${item.name} 已加入整理队列!`)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// 整理日志
|
||||||
|
async function handleTransferLog(logid: number, background: boolean = false) {
|
||||||
|
transferForm.logid = logid
|
||||||
|
transferForm.fileitem = {} as FileItem
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.post(`transfer/manual?background=${background}`, transferForm)
|
||||||
|
if (!result.success) $toast.error(result.message)
|
||||||
|
else if (background) $toast.success(`历史记录 ${logid} 已加入整理队列!`)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 使用SSE监听加载进度
|
// 使用SSE监听加载进度
|
||||||
function startLoadingProgress() {
|
function startLoadingProgress() {
|
||||||
@@ -134,63 +175,49 @@ function stopLoadingProgress() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 整理文件
|
// 整理文件
|
||||||
async function transfer() {
|
async function transfer(background: boolean = false) {
|
||||||
if (!props.logids && !props.items) return
|
if (!props.logids && !props.items) return
|
||||||
|
|
||||||
// 显示进度条
|
// 显示进度条
|
||||||
progressDialog.value = true
|
progressDialog.value = true
|
||||||
// 开始监听进度
|
|
||||||
startLoadingProgress()
|
if (!background) {
|
||||||
|
// 开始监听进度
|
||||||
|
startLoadingProgress()
|
||||||
|
}
|
||||||
|
|
||||||
// 文件整理
|
// 文件整理
|
||||||
if (props.items) {
|
if (props.items) {
|
||||||
for (const item of props.items) {
|
for (const item of props.items) {
|
||||||
await handleTransfer(item)
|
await handleTransfer(item, background)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 日志整理
|
// 日志整理
|
||||||
if (props.logids) {
|
if (props.logids) {
|
||||||
for (const logid of props.logids) {
|
for (const logid of props.logids) {
|
||||||
await handleTransferLog(logid)
|
await handleTransferLog(logid, background)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止监听进度
|
if (!background) {
|
||||||
stopLoadingProgress()
|
// 停止监听进度
|
||||||
|
stopLoadingProgress()
|
||||||
|
}
|
||||||
|
|
||||||
// 关闭进度条
|
// 关闭进度条
|
||||||
progressDialog.value = false
|
progressDialog.value = false
|
||||||
// 重新加载
|
// 重新加载
|
||||||
emit('done')
|
emit('done')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 整理文件
|
|
||||||
async function handleTransfer(item: FileItem) {
|
|
||||||
transferForm.fileitem = item
|
|
||||||
transferForm.logid = 0
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.post('transfer/manual', transferForm)
|
|
||||||
if (!result.success) $toast.error(`文件 ${item.path} 整理失败:${result.message}!`)
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 整理日志
|
|
||||||
async function handleTransferLog(logid: number) {
|
|
||||||
transferForm.logid = logid
|
|
||||||
transferForm.fileitem = {}
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.post('transfer/manual', transferForm)
|
|
||||||
if (!result.success) $toast.error(`历史记录 ${logid} 重新整理失败:${result.message}!`)
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadDirectories()
|
loadDirectories()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopLoadingProgress()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -215,18 +242,16 @@ onMounted(() => {
|
|||||||
<VSelect
|
<VSelect
|
||||||
v-model="transferForm.transfer_type"
|
v-model="transferForm.transfer_type"
|
||||||
label="整理方式"
|
label="整理方式"
|
||||||
:items="[
|
:items="transferTypeOptions"
|
||||||
{ title: '默认', value: '' },
|
|
||||||
{ title: '移动', value: 'move' },
|
|
||||||
{ title: '复制', value: 'copy' },
|
|
||||||
{ title: '硬链接', value: 'link' },
|
|
||||||
{ title: '软链接', value: 'softlink' },
|
|
||||||
]"
|
|
||||||
hint="文件操作整理方式"
|
hint="文件操作整理方式"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
>
|
||||||
|
<template v-slot:selection="{ item }">
|
||||||
|
{{ transferForm.transfer_type === '' ? '自动' : item.title }}
|
||||||
|
</template>
|
||||||
|
</VSelect>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12" md="12">
|
<VCol cols="12">
|
||||||
<VCombobox
|
<VCombobox
|
||||||
v-model="transferForm.target_path"
|
v-model="transferForm.target_path"
|
||||||
:items="targetDirectories"
|
:items="targetDirectories"
|
||||||
@@ -301,6 +326,7 @@ onMounted(() => {
|
|||||||
<VCol cols="12" md="4">
|
<VCol cols="12" md="4">
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="transferForm.episode_detail"
|
v-model="transferForm.episode_detail"
|
||||||
|
:disabled="disableEpisodeDetail"
|
||||||
label="指定集数"
|
label="指定集数"
|
||||||
placeholder="起始集,终止集,如1或1,2"
|
placeholder="起始集,终止集,如1或1,2"
|
||||||
hint="指定集数或范围,如1或1,2"
|
hint="指定集数或范围,如1或1,2"
|
||||||
@@ -337,6 +363,22 @@ onMounted(() => {
|
|||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
<VRow>
|
<VRow>
|
||||||
|
<VCol cols="12" md="6" v-if="transferForm.target_path">
|
||||||
|
<VSwitch
|
||||||
|
v-model="transferForm.library_type_folder"
|
||||||
|
label="按类型分类"
|
||||||
|
hint="整理时目的路径下按媒体类型添加子目录"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6" v-if="transferForm.target_path">
|
||||||
|
<VSwitch
|
||||||
|
v-model="transferForm.library_category_folder"
|
||||||
|
label="按类别分类"
|
||||||
|
hint="整理时在目的路径下按媒体类别添加子目录"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
<VSwitch
|
<VSwitch
|
||||||
v-model="transferForm.scrape"
|
v-model="transferForm.scrape"
|
||||||
@@ -349,7 +391,7 @@ onMounted(() => {
|
|||||||
<VSwitch
|
<VSwitch
|
||||||
v-model="transferForm.from_history"
|
v-model="transferForm.from_history"
|
||||||
label="复用历史识别信息"
|
label="复用历史识别信息"
|
||||||
hint="使用历史记录中已识别的媒体信息"
|
hint="使用历史整理记录中已识别的媒体信息"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
@@ -358,7 +400,12 @@ onMounted(() => {
|
|||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions class="pt-3">
|
<VCardActions class="pt-3">
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
<VBtn variant="elevated" @click="transfer" prepend-icon="mdi-arrow-right-bold" class="px-5"> 开始整理 </VBtn>
|
<VBtn variant="elevated" color="success" @click="transfer(true)" prepend-icon="mdi-plus" class="px-5">
|
||||||
|
加入整理队列
|
||||||
|
</VBtn>
|
||||||
|
<VBtn variant="elevated" @click="transfer(false)" prepend-icon="mdi-arrow-right-bold" class="px-5">
|
||||||
|
立即整理
|
||||||
|
</VBtn>
|
||||||
</VCardActions>
|
</VCardActions>
|
||||||
</VCard>
|
</VCard>
|
||||||
<!-- 手动整理进度框 -->
|
<!-- 手动整理进度框 -->
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import type { Site } from '@/api/types'
|
import type { DownloaderConf, Site } from '@/api/types'
|
||||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||||
import { numberValidator, requiredValidator } from '@/@validators'
|
import { numberValidator, requiredValidator } from '@/@validators'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
@@ -35,6 +35,7 @@ const siteForm = ref<Site>({
|
|||||||
limit_seconds: 0,
|
limit_seconds: 0,
|
||||||
name: '',
|
name: '',
|
||||||
domain: '',
|
domain: '',
|
||||||
|
downloader: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
@@ -65,10 +66,9 @@ const downloaderOptions = ref<{ title: string; value: string }[]>([])
|
|||||||
|
|
||||||
async function loadDownloaderSetting() {
|
async function loadDownloaderSetting() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
|
const downloaders: DownloaderConf[] = await api.get('download/clients')
|
||||||
const downloaders = result.data?.value ?? []
|
|
||||||
downloaderOptions.value = [
|
downloaderOptions.value = [
|
||||||
{ title: '默认', value: null },
|
{ title: '默认', value: '' },
|
||||||
...downloaders.map((item: { name: any }) => ({
|
...downloaders.map((item: { name: any }) => ({
|
||||||
title: item.name,
|
title: item.name,
|
||||||
value: item.name,
|
value: item.name,
|
||||||
@@ -215,7 +215,12 @@ onMounted(async () => {
|
|||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12" md="3">
|
<VCol cols="12" md="3">
|
||||||
<VTextField v-model="siteForm.timeout" label="超时时间(秒)" hint="站点请求超时时间" persistent-hint />
|
<VTextField
|
||||||
|
v-model="siteForm.timeout"
|
||||||
|
label="超时时间(秒)"
|
||||||
|
hint="站点请求超时时间,为0时不限制"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="6" md="3">
|
<VCol cols="6" md="3">
|
||||||
<VSelect
|
<VSelect
|
||||||
|
|||||||
@@ -67,17 +67,18 @@ async function updateSiteCookie() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VDialog max-width="50rem">
|
<VDialog max-width="30rem">
|
||||||
<!-- Dialog Content -->
|
<!-- Dialog Content -->
|
||||||
<VCard title="更新站点Cookie & UA">
|
<VCard title="更新站点Cookie & UA">
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
|
<VDivider />
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VForm @submit.prevent="() => {}">
|
<VForm @submit.prevent="() => {}">
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="12" md="4">
|
<VCol cols="12">
|
||||||
<VTextField v-model="userPwForm.username" label="用户名" :rules="[requiredValidator]" />
|
<VTextField v-model="userPwForm.username" label="用户名" :rules="[requiredValidator]" />
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12" md="4">
|
<VCol cols="12">
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="userPwForm.password"
|
v-model="userPwForm.password"
|
||||||
label="密码"
|
label="密码"
|
||||||
@@ -88,19 +89,19 @@ async function updateSiteCookie() {
|
|||||||
@keydown.enter="updateSiteCookie"
|
@keydown.enter="updateSiteCookie"
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12" md="4">
|
<VCol cols="12">
|
||||||
<VTextField v-model="userPwForm.code" label="两步验证" />
|
<VTextField v-model="userPwForm.code" label="两步验证" />
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</VForm>
|
</VForm>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
<VCardActions class="mx-auto">
|
||||||
<VCardActions>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn
|
<VBtn
|
||||||
|
size="large"
|
||||||
variant="elevated"
|
variant="elevated"
|
||||||
@click="updateSiteCookie"
|
@click="updateSiteCookie"
|
||||||
:disabled="updateButtonDisable"
|
:disabled="updateButtonDisable"
|
||||||
|
:loading="updateButtonDisable"
|
||||||
prepend-icon="mdi-refresh"
|
prepend-icon="mdi-refresh"
|
||||||
class="px-5"
|
class="px-5"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,19 +1,31 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Site } from '@/api/types'
|
import { Site } from '@/api/types'
|
||||||
import { useDisplay } from 'vuetify'
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { TorrentInfo } from '@/api/types'
|
import type { TorrentInfo, SiteCategory } from '@/api/types'
|
||||||
import { formatFileSize } from '@core/utils/formatters'
|
import { formatFileSize } from '@core/utils/formatters'
|
||||||
import AddDownloadDialog from '../dialog/AddDownloadDialog.vue'
|
import AddDownloadDialog from '../dialog/AddDownloadDialog.vue'
|
||||||
|
|
||||||
// 显示器宽度
|
|
||||||
const display = useDisplay()
|
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
site: Object as PropType<Site>,
|
site: Object as PropType<Site>,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 关键字
|
||||||
|
const keyword = ref<string>()
|
||||||
|
|
||||||
|
// 选择分类
|
||||||
|
const selectCategory = ref<number[]>([])
|
||||||
|
|
||||||
|
// 全部分类
|
||||||
|
const siteCategoryList = ref<SiteCategory[]>()
|
||||||
|
|
||||||
|
// 分类选项
|
||||||
|
const categoryOptions = computed(() => {
|
||||||
|
return siteCategoryList.value?.map(item => {
|
||||||
|
return { title: item.desc, value: item.id }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// 注册事件
|
// 注册事件
|
||||||
const emit = defineEmits(['close'])
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
@@ -55,17 +67,6 @@ async function downloadTorrentFile(enclosure: string) {
|
|||||||
window.open(enclosure, '_blank')
|
window.open(enclosure, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用API,查询站点资源
|
|
||||||
async function getResourceList() {
|
|
||||||
resourceLoading.value = true
|
|
||||||
try {
|
|
||||||
resourceDataList.value = await api.get(`site/resource/${props.site?.id}`)
|
|
||||||
resourceLoading.value = false
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 促销Chip类
|
// 促销Chip类
|
||||||
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||||
if (downloadVolume === 0) return 'text-white bg-lime-500'
|
if (downloadVolume === 0) return 'text-white bg-lime-500'
|
||||||
@@ -93,17 +94,75 @@ function addDownloadError(error: string) {
|
|||||||
addDownloadDialog.value = false
|
addDownloadDialog.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 调用API,查询站点资源
|
||||||
|
async function getResourceList() {
|
||||||
|
resourceLoading.value = true
|
||||||
|
try {
|
||||||
|
resourceDataList.value = await api.get(`site/resource/${props.site?.id}`, {
|
||||||
|
params: {
|
||||||
|
keyword: keyword.value,
|
||||||
|
cat: selectCategory.value?.join(','),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
resourceLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载站点分类
|
||||||
|
async function getSiteCategoryList() {
|
||||||
|
try {
|
||||||
|
siteCategoryList.value = await api.get(`site/category/${props.site?.id}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 装载时查询站点图标
|
// 装载时查询站点图标
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
getSiteCategoryList()
|
||||||
getResourceList()
|
getResourceList()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VDialog max-width="80rem" scrollable z-index="1010" :fullscreen="!display.mdAndUp.value">
|
<VDialog scrollable fullscreen :scrim="false" transition="dialog-bottom-transition">
|
||||||
<VCard :title="`浏览 - ${props.site?.name}`">
|
<VCard>
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
<!-- Toolbar -->
|
||||||
<VDivider />
|
<div>
|
||||||
<VCardText class="pt-2">
|
<VToolbar color="primary">
|
||||||
|
<VToolbarTitle>{{ `浏览 - ${props.site?.name}` }}</VToolbarTitle>
|
||||||
|
<VSpacer />
|
||||||
|
<VToolbarItems>
|
||||||
|
<VBtn icon variant="plain" @click="emit('close')" class="me-3">
|
||||||
|
<VIcon size="large" color="white" icon="ri-close-line" />
|
||||||
|
</VBtn>
|
||||||
|
</VToolbarItems>
|
||||||
|
</VToolbar>
|
||||||
|
</div>
|
||||||
|
<div class="p-3">
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="6" md="5">
|
||||||
|
<VTextField v-model="keyword" size="small" density="compact" label="搜索关键字" clearable />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="6" md="5">
|
||||||
|
<VSelect
|
||||||
|
v-model="selectCategory"
|
||||||
|
:items="categoryOptions"
|
||||||
|
size="small"
|
||||||
|
density="compact"
|
||||||
|
chips
|
||||||
|
label="资源分类"
|
||||||
|
multiple
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="2" class="text-center">
|
||||||
|
<VBtn block prepend-icon="mdi-magnify" @click="getResourceList">搜索</VBtn>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</div>
|
||||||
|
<VCardText class="px-0 py-0 my-0">
|
||||||
<VDataTable
|
<VDataTable
|
||||||
v-model:items-per-page="resourceItemsPerPage"
|
v-model:items-per-page="resourceItemsPerPage"
|
||||||
:headers="resourceHeaders"
|
:headers="resourceHeaders"
|
||||||
@@ -119,6 +178,7 @@ onMounted(() => {
|
|||||||
items-per-page-text="每页条数"
|
items-per-page-text="每页条数"
|
||||||
page-text="{0}-{1} 共 {2} 条"
|
page-text="{0}-{1} 共 {2} 条"
|
||||||
loading-text="加载中..."
|
loading-text="加载中..."
|
||||||
|
class="h-full"
|
||||||
>
|
>
|
||||||
<template #item.title="{ item }">
|
<template #item.title="{ item }">
|
||||||
<a href="javascript:void(0)" @click.stop="addDownload(item)">
|
<a href="javascript:void(0)" @click.stop="addDownload(item)">
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
import type { Site, SiteUserData } from '@/api/types'
|
import type { Site, SiteUserData } from '@/api/types'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { useDisplay, useTheme } from 'vuetify'
|
import { useDisplay, useTheme } from 'vuetify'
|
||||||
import { VAvatar, VCardText, VIcon } from 'vuetify/lib/components/index.mjs'
|
|
||||||
import { formatFileSize } from '@/@core/utils/formatters'
|
import { formatFileSize } from '@/@core/utils/formatters'
|
||||||
import VueApexCharts from 'vue3-apexcharts'
|
|
||||||
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
|
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
@@ -34,7 +32,6 @@ const siteDatas = ref<SiteUserData[]>([])
|
|||||||
// 最新一天的数据
|
// 最新一天的数据
|
||||||
const siteData = computed(() => siteDatas.value[siteDatas.value.length - 1])
|
const siteData = computed(() => siteDatas.value[siteDatas.value.length - 1])
|
||||||
|
|
||||||
|
|
||||||
// 站点数据列表中的上传量、下载量数据生成图形使用的数据
|
// 站点数据列表中的上传量、下载量数据生成图形使用的数据
|
||||||
const historySeries = computed(() => {
|
const historySeries = computed(() => {
|
||||||
return [
|
return [
|
||||||
@@ -243,13 +240,12 @@ async function fetchSiteUserData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 刷新站点数据
|
// 刷新站点数据
|
||||||
async function refreshSiteData(){
|
async function refreshSiteData() {
|
||||||
progressDialog.value = true
|
progressDialog.value = true
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.post(`site/userdata/${props.site?.id}`)
|
const result: { [key: string]: any } = await api.post(`site/userdata/${props.site?.id}`)
|
||||||
if (result.success){
|
if (result.success) {
|
||||||
await fetchSiteUserData()
|
await fetchSiteUserData()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -267,12 +263,14 @@ onBeforeMount(async () => {
|
|||||||
<VDialog scrollable eager max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
<VDialog scrollable eager max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||||
<VCard class="rounded-t">
|
<VCard class="rounded-t">
|
||||||
<VCardItem>
|
<VCardItem>
|
||||||
<VCardTitle>{{ `数据 - ${props.site?.name}` }}
|
<VCardTitle
|
||||||
<IconBtn @click.stop="refreshSiteData" color="info"><VIcon icon="mdi-refresh"</VIcon></IconBtn>
|
>{{ `数据 - ${props.site?.name}` }}
|
||||||
|
<IconBtn @click.stop="refreshSiteData" color="info"><VIcon icon="mdi-refresh" /></IconBtn>
|
||||||
</VCardTitle>
|
</VCardTitle>
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
<VCardText>
|
<VDivider />
|
||||||
|
<VCardText class="pt-5">
|
||||||
<VRow class="match-height">
|
<VRow class="match-height">
|
||||||
<!-- 用户信息 -->
|
<!-- 用户信息 -->
|
||||||
<VCol cols="12" md="3">
|
<VCol cols="12" md="3">
|
||||||
@@ -441,7 +439,7 @@ onBeforeMount(async () => {
|
|||||||
<VCol>
|
<VCol>
|
||||||
<VCard title="历史流量">
|
<VCard title="历史流量">
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VueApexCharts type="line" :options="historyChartOptions" :series="historySeries" :height="300" />
|
<VApexChart type="line" :options="historyChartOptions" :series="historySeries" :height="300" />
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VCol>
|
</VCol>
|
||||||
@@ -450,7 +448,7 @@ onBeforeMount(async () => {
|
|||||||
<VCol>
|
<VCol>
|
||||||
<VCard title="做种分布">
|
<VCard title="做种分布">
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VueApexCharts type="scatter" :options="seedingChartOptions" :series="seedingSeries" :height="300" />
|
<VApexChart type="scatter" :options="seedingChartOptions" :series="seedingSeries" :height="300" />
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import { numberValidator } from '@/@validators'
|
import { numberValidator } from '@/@validators'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { FilterRuleGroup, Site, Subscribe, TransferDirectoryConf } from '@/api/types'
|
import type { DownloaderConf, FilterRuleGroup, Site, Subscribe, TransferDirectoryConf } from '@/api/types'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { useConfirm } from 'vuetify-use-dialog'
|
import { useConfirm } from 'vuetify-use-dialog'
|
||||||
import { VTextarea, VTextField } from 'vuetify/lib/components/index.mjs'
|
import { VTextarea, VTextField } from 'vuetify/lib/components/index.mjs'
|
||||||
@@ -50,6 +50,7 @@ const subscribeForm = ref<Subscribe>({
|
|||||||
sites: [],
|
sites: [],
|
||||||
best_version: undefined,
|
best_version: undefined,
|
||||||
current_priority: 0,
|
current_priority: 0,
|
||||||
|
downloader: '',
|
||||||
date: '',
|
date: '',
|
||||||
show_edit_dialog: false,
|
show_edit_dialog: false,
|
||||||
})
|
})
|
||||||
@@ -62,10 +63,9 @@ const downloaderOptions = ref<{ title: string; value: string }[]>([])
|
|||||||
|
|
||||||
async function loadDownloaderSetting() {
|
async function loadDownloaderSetting() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
|
const downloaders: DownloaderConf[] = await api.get('download/clients')
|
||||||
const downloaders = result.data?.value ?? []
|
|
||||||
downloaderOptions.value = [
|
downloaderOptions.value = [
|
||||||
{ title: '默认', value: null },
|
{ title: '默认', value: '' },
|
||||||
...downloaders.map((item: { name: any }) => ({
|
...downloaders.map((item: { name: any }) => ({
|
||||||
title: item.name,
|
title: item.name,
|
||||||
value: item.name,
|
value: item.name,
|
||||||
@@ -207,7 +207,7 @@ async function removeSubscribe() {
|
|||||||
// 查询下载目录
|
// 查询下载目录
|
||||||
async function loadDownloadDirectories() {
|
async function loadDownloadDirectories() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('system/setting/DownloadDirectories')
|
const result: { [key: string]: any } = await api.get('system/setting/Directories')
|
||||||
if (result.success && result.data?.value) {
|
if (result.success && result.data?.value) {
|
||||||
downloadDirectories.value = result.data.value
|
downloadDirectories.value = result.data.value
|
||||||
}
|
}
|
||||||
@@ -219,8 +219,7 @@ async function loadDownloadDirectories() {
|
|||||||
// 保存目录下拉框
|
// 保存目录下拉框
|
||||||
const targetDirectories = computed(() => {
|
const targetDirectories = computed(() => {
|
||||||
// 去重后的下载目录
|
// 去重后的下载目录
|
||||||
const directories = downloadDirectories.value.map(item => item.download_path)
|
return downloadDirectories.value.map(item => item.download_path)
|
||||||
return [...new Set(directories)]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 质量选择框数据
|
// 质量选择框数据
|
||||||
@@ -418,7 +417,7 @@ onMounted(() => {
|
|||||||
v-model="subscribeForm.downloader"
|
v-model="subscribeForm.downloader"
|
||||||
:items="downloaderOptions"
|
:items="downloaderOptions"
|
||||||
label="下载器"
|
label="下载器"
|
||||||
hint="指定该订阅使用的下载器,留空自动使用默认下载器"
|
hint="指定该订阅使用的下载器"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
|||||||
@@ -132,59 +132,63 @@ onBeforeMount(() => {
|
|||||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||||
<VWindowItem value="download">
|
<VWindowItem value="download">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<VDataTable
|
<div>
|
||||||
items-per-page="50"
|
<VDataTable
|
||||||
:headers="downloadHeaders"
|
items-per-page="50"
|
||||||
:items="downloadInfos"
|
:headers="downloadHeaders"
|
||||||
:items-length="totalCount"
|
:items="downloadInfos"
|
||||||
density="compact"
|
:items-length="totalCount"
|
||||||
item-value="title"
|
density="compact"
|
||||||
return-object
|
item-value="title"
|
||||||
fixed-header
|
return-object
|
||||||
hover
|
fixed-header
|
||||||
items-per-page-text="每页条数"
|
hover
|
||||||
page-text="{0}-{1} 共 {2} 条"
|
items-per-page-text="每页条数"
|
||||||
loading-text="加载中..."
|
page-text="{0}-{1} 共 {2} 条"
|
||||||
>
|
loading-text="加载中..."
|
||||||
<template #item.episode_number="{ item }">
|
>
|
||||||
<div class="text-high-emphasis pt-1">{{ item.episode_number }}. {{ item.title }}</div>
|
<template #item.episode_number="{ item }">
|
||||||
</template>
|
<div class="text-high-emphasis pt-1">{{ item.episode_number }}. {{ item.title }}</div>
|
||||||
<template #item.torrent_title="{ item }">
|
</template>
|
||||||
<div class="text-xs" v-for="file in item.download">
|
<template #item.torrent_title="{ item }">
|
||||||
【{{ file.site_name }}】{{ file.torrent_title }}
|
<div class="text-xs" v-for="file in item.download">
|
||||||
</div>
|
【{{ file.site_name }}】{{ file.torrent_title }}
|
||||||
</template>
|
</div>
|
||||||
<template #item.file_path="{ item }">
|
</template>
|
||||||
<div class="text-xs" v-for="file in item.download">{{ file.file_path }}</div>
|
<template #item.file_path="{ item }">
|
||||||
</template>
|
<div class="text-xs" v-for="file in item.download">{{ file.file_path }}</div>
|
||||||
<template #no-data> 没有数据 </template>
|
</template>
|
||||||
</VDataTable>
|
<template #no-data> 没有数据 </template>
|
||||||
|
</VDataTable>
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
<VWindowItem value="library">
|
<VWindowItem value="library">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
<VDataTable
|
<div>
|
||||||
items-per-page="50"
|
<VDataTable
|
||||||
:headers="libraryHeaders"
|
items-per-page="50"
|
||||||
:items="libraryInfos"
|
:headers="libraryHeaders"
|
||||||
:items-length="totalCount"
|
:items="libraryInfos"
|
||||||
density="compact"
|
:items-length="totalCount"
|
||||||
item-value="title"
|
density="compact"
|
||||||
return-object
|
item-value="title"
|
||||||
fixed-header
|
return-object
|
||||||
hover
|
fixed-header
|
||||||
items-per-page-text="每页条数"
|
hover
|
||||||
page-text="{0}-{1} 共 {2} 条"
|
items-per-page-text="每页条数"
|
||||||
loading-text="加载中..."
|
page-text="{0}-{1} 共 {2} 条"
|
||||||
>
|
loading-text="加载中..."
|
||||||
<template #item.episode_number="{ item }">
|
>
|
||||||
<div class="text-high-emphasis pt-1">{{ item.episode_number }}. {{ item.title }}</div>
|
<template #item.episode_number="{ item }">
|
||||||
</template>
|
<div class="text-high-emphasis pt-1">{{ item.episode_number }}. {{ item.title }}</div>
|
||||||
<template #item.file_path="{ item }">
|
</template>
|
||||||
<div class="text-xs" v-for="file in item.library">{{ file.file_path }}</div>
|
<template #item.file_path="{ item }">
|
||||||
</template>
|
<div class="text-xs" v-for="file in item.library">{{ file.file_path }}</div>
|
||||||
<template #no-data> 没有数据 </template>
|
</template>
|
||||||
</VDataTable>
|
<template #no-data> 没有数据 </template>
|
||||||
|
</VDataTable>
|
||||||
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
</VWindow>
|
</VWindow>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ async function reSubscribe(item: Subscribe) {
|
|||||||
else progressText.value = `正在重新订阅 ${item.name} 第 ${item.season} 季 ...`
|
else progressText.value = `正在重新订阅 ${item.name} 第 ${item.season} 季 ...`
|
||||||
progressDialog.value = true
|
progressDialog.value = true
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.post('subscribe', item)
|
const result: { [key: string]: any } = await api.post('subscribe/', item)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
emit('save')
|
emit('save')
|
||||||
}
|
}
|
||||||
@@ -138,14 +138,7 @@ const dropdownItems = ref([
|
|||||||
<VCardTitle>{{ props.type + '订阅历史' }}</VCardTitle>
|
<VCardTitle>{{ props.type + '订阅历史' }}</VCardTitle>
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
<VDivider />
|
<VDivider />
|
||||||
<DialogCloseBtn
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
emit('close')
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<!-- <VList lines="two" v-if="historyList.length > 0"> -->
|
|
||||||
<VList lines="two">
|
<VList lines="two">
|
||||||
<VInfiniteScroll mode="intersect" side="end" :items="historyList" class="overflow-hidden" @load="loadHistory">
|
<VInfiniteScroll mode="intersect" side="end" :items="historyList" class="overflow-hidden" @load="loadHistory">
|
||||||
<template #loading>
|
<template #loading>
|
||||||
@@ -207,7 +200,7 @@ const dropdownItems = ref([
|
|||||||
</template>
|
</template>
|
||||||
</VInfiniteScroll>
|
</VInfiniteScroll>
|
||||||
</VList>
|
</VList>
|
||||||
<VCardText v-if="historyList.length === 0" class="text-center"> 没有已完成的订阅 </VCardText>
|
<VCardText v-if="historyList.length === 0 && isRefreshed" class="text-center"> 没有已完成的订阅 </VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
<!-- 进度框 -->
|
<!-- 进度框 -->
|
||||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { requiredValidator } from '@/@validators'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Subscribe, SubscribeShare } from '@/api/types'
|
import type { Subscribe, SubscribeShare } from '@/api/types'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
import { formatSeason } from '@/@core/utils/formatters'
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
@@ -16,16 +17,22 @@ const props = defineProps({
|
|||||||
// 定义触发的自定义事件
|
// 定义触发的自定义事件
|
||||||
const emit = defineEmits(['close'])
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
// 分享处理状态
|
||||||
|
const shareDoing = ref(false)
|
||||||
|
|
||||||
// 订阅编辑表单
|
// 订阅编辑表单
|
||||||
const shareForm = ref<SubscribeShare>({
|
const shareForm = ref<SubscribeShare>({
|
||||||
subscribe_id: props.sub?.id ?? 0,
|
subscribe_id: props.sub?.id ?? 0,
|
||||||
|
share_title: `${props.sub?.name} ${formatSeason(props.sub?.season ? props.sub?.season.toString() : '')}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 分享订阅
|
// 分享订阅
|
||||||
async function doShare() {
|
async function doShare() {
|
||||||
if (!shareForm.value.share_title || !shareForm.value.share_comment || !shareForm.value.share_user) return
|
if (!shareForm.value.share_title || !shareForm.value.share_comment || !shareForm.value.share_user) return
|
||||||
try {
|
try {
|
||||||
|
shareDoing.value = true
|
||||||
const result: { [key: string]: any } = await api.post('subscribe/share', shareForm.value)
|
const result: { [key: string]: any } = await api.post('subscribe/share', shareForm.value)
|
||||||
|
shareDoing.value = false
|
||||||
// 提示
|
// 提示
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
$toast.success(`${props.sub?.name} 分享成功!`)
|
$toast.success(`${props.sub?.name} 分享成功!`)
|
||||||
@@ -56,8 +63,8 @@ const $toast = useToast()
|
|||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="shareForm.share_title"
|
v-model="shareForm.share_title"
|
||||||
|
readonly
|
||||||
label="标题"
|
label="标题"
|
||||||
hint="给分享取一个便于识别的名称"
|
|
||||||
:rules="[requiredValidator]"
|
:rules="[requiredValidator]"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
@@ -67,7 +74,7 @@ const $toast = useToast()
|
|||||||
v-model="shareForm.share_comment"
|
v-model="shareForm.share_comment"
|
||||||
label="说明"
|
label="说明"
|
||||||
:rules="[requiredValidator]"
|
:rules="[requiredValidator]"
|
||||||
hint="关于该订阅的说明"
|
hint="填写关于该订阅的说明,订阅中的搜索词、识别词等将会默认包含在分享中"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
@@ -85,7 +92,16 @@ const $toast = useToast()
|
|||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions class="pt-3">
|
<VCardActions class="pt-3">
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
<VBtn variant="elevated" @click="doShare" prepend-icon="mdi-share" class="px-5"> 确认分享 </VBtn>
|
<VBtn
|
||||||
|
variant="elevated"
|
||||||
|
:disabled="shareDoing"
|
||||||
|
@click="doShare"
|
||||||
|
prepend-icon="mdi-share"
|
||||||
|
class="px-5"
|
||||||
|
:loading="shareDoing"
|
||||||
|
>
|
||||||
|
确认分享
|
||||||
|
</VBtn>
|
||||||
</VCardActions>
|
</VCardActions>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
|
|||||||
187
src/components/dialog/TransferQueueDialog.vue
Normal file
187
src/components/dialog/TransferQueueDialog.vue
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { formatFileSize } from '@/@core/utils/formatters'
|
||||||
|
import api from '@/api'
|
||||||
|
import { FileItem, TransferQueue } from '@/api/types'
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
|
||||||
|
// 显示器宽度
|
||||||
|
const display = useDisplay()
|
||||||
|
// 定义触发的自定义事件
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
// 数据列表
|
||||||
|
const dataList = ref<TransferQueue[]>([])
|
||||||
|
|
||||||
|
// 加载进度SSE
|
||||||
|
const progressEventSource = ref<EventSource>()
|
||||||
|
|
||||||
|
// 整理进度文本
|
||||||
|
const progressText = ref('请稍候 ...')
|
||||||
|
|
||||||
|
// 整理进度
|
||||||
|
const progressValue = ref(0)
|
||||||
|
|
||||||
|
// 数据可刷新标志
|
||||||
|
const refreshFlag = ref(false)
|
||||||
|
|
||||||
|
// 活动标签
|
||||||
|
const activeTab = ref('')
|
||||||
|
|
||||||
|
// 状态标签
|
||||||
|
const stateDict: { [key: string]: string } = {
|
||||||
|
'waiting': '等待中',
|
||||||
|
'running': '正在整理',
|
||||||
|
'completed': '完成',
|
||||||
|
'failed': '失败',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态颜色
|
||||||
|
function getStateColor(state: string) {
|
||||||
|
if (state === 'waiting') return 'gray'
|
||||||
|
else if (state === 'running') return 'primary'
|
||||||
|
else if (state === 'completed') return 'success'
|
||||||
|
else return 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从dataList中提取所有的媒体信息
|
||||||
|
const mediaList = computed(() => {
|
||||||
|
return dataList.value.map(item => item.media)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 按media计算总数和完成数,返回 x/x
|
||||||
|
function getMediaCount(title_year: string) {
|
||||||
|
// 按title_year查询出所有media列表
|
||||||
|
const medias = dataList.value.filter(item => item.media.title_year === title_year)
|
||||||
|
// 计算media下任务的总数
|
||||||
|
const total = medias.reduce((acc, cur) => acc + cur.tasks.length, 0)
|
||||||
|
// 计算media下任务的完成数
|
||||||
|
const completed = medias.reduce((acc, cur) => acc + cur.tasks.filter(task => task.state === 'completed').length, 0)
|
||||||
|
return `${completed} / ${total}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据媒体信息获取对应的整理任务
|
||||||
|
const activeTasks = computed(() => {
|
||||||
|
return dataList.value.find(item => item.media.title_year === activeTab.value)?.tasks
|
||||||
|
})
|
||||||
|
|
||||||
|
// 调用API获取队列信息
|
||||||
|
async function get_transfer_queue() {
|
||||||
|
try {
|
||||||
|
dataList.value = await api.get('transfer/queue')
|
||||||
|
if (dataList.value.length > 0) {
|
||||||
|
if (!activeTab.value || activeTasks.value?.length == 0) activeTab.value = dataList.value[0].media.title_year || ''
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除队列任务
|
||||||
|
async function remove_queue_task(fileitem: FileItem) {
|
||||||
|
try {
|
||||||
|
await api.delete(`transfer/queue`, { data: fileitem })
|
||||||
|
get_transfer_queue()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用SSE监听加载进度
|
||||||
|
function startLoadingProgress() {
|
||||||
|
progressText.value = '请稍候 ...'
|
||||||
|
progressEventSource.value = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`)
|
||||||
|
progressEventSource.value.onmessage = event => {
|
||||||
|
const progress = JSON.parse(event.data)
|
||||||
|
if (progress) {
|
||||||
|
if (!progress.enable) {
|
||||||
|
progressText.value = '请稍候 ...'
|
||||||
|
progressValue.value = 0
|
||||||
|
if (refreshFlag.value) {
|
||||||
|
refreshFlag.value = false
|
||||||
|
get_transfer_queue()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
progressText.value = progress.text
|
||||||
|
progressValue.value = progress.value
|
||||||
|
if (progress.value >= 100 && refreshFlag.value) {
|
||||||
|
refreshFlag.value = false
|
||||||
|
get_transfer_queue()
|
||||||
|
} else {
|
||||||
|
if (progress.value > 0 && refreshFlag.value && progress.text?.includes('整理完成')) {
|
||||||
|
refreshFlag.value = false
|
||||||
|
get_transfer_queue()
|
||||||
|
} else {
|
||||||
|
refreshFlag.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止监听加载进度
|
||||||
|
function stopLoadingProgress() {
|
||||||
|
progressEventSource.value?.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
get_transfer_queue()
|
||||||
|
startLoadingProgress()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopLoadingProgress()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||||
|
<VCard class="mx-auto" width="100%">
|
||||||
|
<VCardItem>
|
||||||
|
<VCardTitle>整理队列</VCardTitle>
|
||||||
|
</VCardItem>
|
||||||
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
|
<VDivider />
|
||||||
|
<VProgressLinear
|
||||||
|
v-if="dataList.length > 0 && progressValue > 0"
|
||||||
|
:value="progressValue"
|
||||||
|
color="primary"
|
||||||
|
indeterminate
|
||||||
|
/>
|
||||||
|
<VCardItem v-if="dataList.length > 0 && progressValue > 0" class="text-center pt-2">
|
||||||
|
<span class="text-sm">{{ progressText }}</span>
|
||||||
|
</VCardItem>
|
||||||
|
<VCardText v-if="dataList.length === 0" class="text-center"> 没有正在整理的任务 </VCardText>
|
||||||
|
<VCardText>
|
||||||
|
<VTabs v-model="activeTab" show-arrows class="v-tabs-pill" stacked>
|
||||||
|
<VTab
|
||||||
|
v-for="media in mediaList"
|
||||||
|
:value="media.title_year"
|
||||||
|
selected-class="v-slide-group-item--active v-tab--selected"
|
||||||
|
>
|
||||||
|
<div class="font-bold text-lg">{{ media.title }}</div>
|
||||||
|
<div>({{ getMediaCount(media.title_year || '') }})</div>
|
||||||
|
</VTab>
|
||||||
|
</VTabs>
|
||||||
|
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||||
|
<VWindowItem v-for="media in mediaList" :value="media.title_year">
|
||||||
|
<VList>
|
||||||
|
<VListItem v-for="task in activeTasks">
|
||||||
|
<VListItemTitle>{{ task.fileitem.name }}</VListItemTitle>
|
||||||
|
<VListItemSubtitle>
|
||||||
|
大小:{{ formatFileSize(task.fileitem.size || 0) }}
|
||||||
|
<VChip size="small" :color="getStateColor(task.state)" class="ms-2">
|
||||||
|
{{ stateDict[task.state] }}
|
||||||
|
</VChip>
|
||||||
|
</VListItemSubtitle>
|
||||||
|
<template #append>
|
||||||
|
<IconBtn size="small" icon="mdi-cancel" @click="remove_queue_task(task.fileitem)" />
|
||||||
|
</template>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VWindowItem>
|
||||||
|
</VWindow>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import QrcodeVue from 'qrcode.vue'
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
|
import QrcodeVue from 'qrcode.vue'
|
||||||
|
import { VCardItem, VTextField } from 'vuetify/lib/components/index.mjs'
|
||||||
|
|
||||||
|
// 定义输入
|
||||||
|
const props = defineProps({
|
||||||
|
conf: {
|
||||||
|
type: Object as PropType<{ [key: string]: any }>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// 定义事件
|
// 定义事件
|
||||||
const emit = defineEmits(['done', 'close'])
|
const emit = defineEmits(['done', 'close'])
|
||||||
@@ -9,7 +18,7 @@ const emit = defineEmits(['done', 'close'])
|
|||||||
const qrCodeContent = ref('')
|
const qrCodeContent = ref('')
|
||||||
|
|
||||||
// 下方的提示信息
|
// 下方的提示信息
|
||||||
const text = ref('请使用微信或115客户端扫码')
|
const text = ref('请使用微信或115客户端扫码,或在下方输入Cookie')
|
||||||
|
|
||||||
// 提醒类型
|
// 提醒类型
|
||||||
const alertType = ref<'success' | 'info' | 'error' | 'warning' | undefined>('info')
|
const alertType = ref<'success' | 'info' | 'error' | 'warning' | undefined>('info')
|
||||||
@@ -19,6 +28,10 @@ let timeoutTimer: NodeJS.Timeout | undefined = undefined
|
|||||||
|
|
||||||
// 完成
|
// 完成
|
||||||
async function handleDone() {
|
async function handleDone() {
|
||||||
|
clearTimeout(timeoutTimer)
|
||||||
|
if (props.conf?.cookie) {
|
||||||
|
await savaU115Config()
|
||||||
|
}
|
||||||
emit('done')
|
emit('done')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,15 +56,21 @@ async function checkQrcode() {
|
|||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
const status = result.data.status
|
const status = result.data.status
|
||||||
text.value = result.data.tip
|
text.value = result.data.tip
|
||||||
if (status == 1) {
|
if (status == 0) {
|
||||||
// 已确认完成
|
|
||||||
alertType.value = 'success'
|
|
||||||
handleDone()
|
|
||||||
} else if (status == 0) {
|
|
||||||
alertType.value = 'info'
|
alertType.value = 'info'
|
||||||
// 新建、待扫码
|
// 新建、待扫码
|
||||||
clearTimeout(timeoutTimer)
|
clearTimeout(timeoutTimer)
|
||||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||||
|
} else if (status == 1) {
|
||||||
|
// 已扫码
|
||||||
|
alertType.value = 'info'
|
||||||
|
text.value = '已扫码,请确认登录'
|
||||||
|
clearTimeout(timeoutTimer)
|
||||||
|
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||||
|
} else if (status == 2) {
|
||||||
|
// 已确认完成
|
||||||
|
alertType.value = 'success'
|
||||||
|
handleDone()
|
||||||
} else {
|
} else {
|
||||||
// 过期或者已取消
|
// 过期或者已取消
|
||||||
alertType.value = 'error'
|
alertType.value = 'error'
|
||||||
@@ -65,6 +84,15 @@ async function checkQrcode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存cookie设置
|
||||||
|
async function savaU115Config() {
|
||||||
|
try {
|
||||||
|
await api.post(`storage/save/u115`, props.conf)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await getQrcode()
|
await getQrcode()
|
||||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||||
@@ -80,15 +108,20 @@ onUnmounted(() => {
|
|||||||
<VCard title="115网盘登录" class="rounded-t">
|
<VCard title="115网盘登录" class="rounded-t">
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
<VCardText class="pt-2 flex flex-col items-center">
|
<VCardText class="pt-2 flex flex-col items-center">
|
||||||
<div class="my-6 shadow-lg rounded border">
|
<div class="my-6 shadow-lg rounded text-center p-3 border">
|
||||||
<VImg class="mx-auto" :src="qrCodeContent" style="block-size: 200px; inline-size: 200px">
|
<QrcodeVue class="mx-auto" :value="qrCodeContent" :size="200" />
|
||||||
<VSkeletonLoader v-if="!qrCodeContent" class="w-full h-full" />
|
|
||||||
</VImg>
|
|
||||||
</div>
|
</div>
|
||||||
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
|
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
|
||||||
<template #prepend />
|
<template #prepend />
|
||||||
</VAlert>
|
</VAlert>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol class="mt-2">
|
||||||
|
<VTextField label="自定义Cookie" v-model="props.conf.cookie" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
<VCardActions>
|
<VCardActions>
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
|
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { doneNProgress, startNProgress } from '@/api/nprogress'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import avatar1 from '@images/avatars/avatar-1.png'
|
import avatar1 from '@images/avatars/avatar-1.png'
|
||||||
import store from '@/store'
|
import { useUserStore } from '@/stores'
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
@@ -23,8 +23,11 @@ const props = defineProps({
|
|||||||
oper: String,
|
oper: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 用户 Store
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// 当前登录用户名称
|
// 当前登录用户名称
|
||||||
const currentLoginUser = store.state.auth.userName
|
const currentLoginUser = userStore.userName
|
||||||
|
|
||||||
// 用户名
|
// 用户名
|
||||||
const userName = ref('')
|
const userName = ref('')
|
||||||
@@ -199,13 +202,15 @@ async function updateUser() {
|
|||||||
if (oldUserName !== currentUserName.value) {
|
if (oldUserName !== currentUserName.value) {
|
||||||
$toast.success(`【${oldUserName}】更名【${currentUserName.value}】, 更新成功!`)
|
$toast.success(`【${oldUserName}】更名【${currentUserName.value}】, 更新成功!`)
|
||||||
// 如果是当前登录用户,更新当前用户名称显示
|
// 如果是当前登录用户,更新当前用户名称显示
|
||||||
if (isCurrentUser.value) store.commit('auth/setUserName', currentUserName.value)
|
if (isCurrentUser.value) {
|
||||||
|
userStore.setUserName(currentUserName.value)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$toast.success(`【${userForm.value?.name}】更新成功!`)
|
$toast.success(`【${userForm.value?.name}】更新成功!`)
|
||||||
}
|
}
|
||||||
// 更新本地头像显示
|
// 更新本地头像显示
|
||||||
if (oldAvatar !== currentAvatar.value && isCurrentUser.value) {
|
if (oldAvatar !== currentAvatar.value && isCurrentUser.value) {
|
||||||
store.commit('auth/setAvatar', currentAvatar.value)
|
userStore.setAvatar(currentAvatar.value)
|
||||||
}
|
}
|
||||||
emit('save')
|
emit('save')
|
||||||
} else {
|
} else {
|
||||||
@@ -269,41 +274,48 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
<VDivider />
|
<VDivider />
|
||||||
<VCardText class="d-flex">
|
<VCardItem>
|
||||||
<!-- 👉 Avatar -->
|
<!-- 👉 Avatar -->
|
||||||
<VAvatar rounded="lg" size="100" class="me-6" :image="currentAvatar" />
|
<div class="flex flex-row">
|
||||||
|
<VAvatar rounded="lg" size="100" class="me-5" :image="currentAvatar" />
|
||||||
|
<!-- 👉 Upload Photo -->
|
||||||
|
<div class="flex flex-col justify-center gap-5">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<VBtn color="primary" @click="refInputEl?.click()">
|
||||||
|
<VIcon icon="mdi-cloud-upload-outline" />
|
||||||
|
<span v-if="display.mdAndUp.value" class="ms-2">上传新头像</span>
|
||||||
|
</VBtn>
|
||||||
|
|
||||||
<!-- 👉 Upload Photo -->
|
<input
|
||||||
<form class="d-flex flex-column justify-center gap-5">
|
ref="refInputEl"
|
||||||
<div class="d-flex flex-wrap gap-2">
|
type="file"
|
||||||
<VBtn color="primary" @click="refInputEl?.click()">
|
name="file"
|
||||||
<VIcon icon="mdi-cloud-upload-outline" />
|
accept=".jpeg,.png,.jpg,GIF"
|
||||||
<span v-if="display.mdAndUp.value" class="ms-2">上传新头像</span>
|
hidden
|
||||||
</VBtn>
|
@input="changeAvatar"
|
||||||
|
/>
|
||||||
|
|
||||||
<input ref="refInputEl" type="file" name="file" accept=".jpeg,.png,.jpg,GIF" hidden @input="changeAvatar" />
|
<VBtn type="reset" color="info" variant="tonal" @click="restoreCurrentAvatar" v-if="props.oper !== 'add'">
|
||||||
|
<VIcon icon="mdi-refresh" />
|
||||||
|
<span v-if="display.mdAndUp.value" class="ms-2">重置</span>
|
||||||
|
</VBtn>
|
||||||
|
|
||||||
<VBtn type="reset" color="info" variant="tonal" @click="restoreCurrentAvatar" v-if="props.oper !== 'add'">
|
<VBtn
|
||||||
<VIcon icon="mdi-refresh" />
|
type="reset"
|
||||||
<span v-if="display.mdAndUp.value" class="ms-2">重置</span>
|
:color="props.oper === 'add' ? 'info' : 'error'"
|
||||||
</VBtn>
|
variant="tonal"
|
||||||
|
@click="resetDefaultAvatar"
|
||||||
<VBtn
|
>
|
||||||
type="reset"
|
<VIcon icon="mdi-image-sync-outline" />
|
||||||
:color="props.oper === 'add' ? 'info' : 'error'"
|
<span v-if="display.mdAndUp.value" class="ms-2">默认</span>
|
||||||
variant="tonal"
|
</VBtn>
|
||||||
@click="resetDefaultAvatar"
|
</div>
|
||||||
>
|
<p class="text-body-1 mb-0">允许 JPG、PNG、GIF、WEBP 格式, 最大尺寸 800KB。</p>
|
||||||
<VIcon icon="mdi-image-sync-outline" />
|
|
||||||
<span v-if="display.mdAndUp.value" class="ms-2">默认</span>
|
|
||||||
</VBtn>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<p class="text-body-1 mb-0">允许 JPG、PNG、GIF、WEBP 格式, 最大尺寸 800KB。</p>
|
</VCardItem>
|
||||||
</form>
|
|
||||||
</VCardText>
|
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VForm @submit.prevent="() => {}" class="mt-3">
|
<VForm @submit.prevent="() => {}">
|
||||||
<VDivider class="my-10">
|
<VDivider class="my-10">
|
||||||
<span>用户基础设置</span>
|
<span>用户基础设置</span>
|
||||||
</VDivider>
|
</VDivider>
|
||||||
@@ -355,7 +367,7 @@ onMounted(() => {
|
|||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
<VDivider class="my-10">
|
<VDivider class="my-10">
|
||||||
<span>消息账号绑定</span>
|
<span>账号绑定</span>
|
||||||
</VDivider>
|
</VDivider>
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
@@ -388,6 +400,9 @@ onMounted(() => {
|
|||||||
label="SynologyChat用户"
|
label="SynologyChat用户"
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField v-model="userForm.settings.douban_userid" density="comfortable" clearable label="豆瓣用户" />
|
||||||
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</VForm>
|
</VForm>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
|||||||
177
src/components/dialog/UserAuthDialog.vue
Normal file
177
src/components/dialog/UserAuthDialog.vue
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||||
|
import api from '@/api'
|
||||||
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['done', 'close'])
|
||||||
|
|
||||||
|
// 提示框
|
||||||
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 是否加载中
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 用户认证表单
|
||||||
|
const authForm = ref<any>({
|
||||||
|
site: null,
|
||||||
|
params: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 所有认证站点
|
||||||
|
const authSites = ref<{
|
||||||
|
[key: string]: {
|
||||||
|
name: string
|
||||||
|
icon: string
|
||||||
|
params: { [key: string]: any }
|
||||||
|
}
|
||||||
|
}>({})
|
||||||
|
|
||||||
|
// 生成站点拉选项
|
||||||
|
const dropdownItems = computed(() => {
|
||||||
|
return Object.keys(authSites.value).map(key => {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
name: authSites.value[key].name,
|
||||||
|
prependAvatar: authSites.value[key].icon,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 读取authSites.params,生成表单配置列表
|
||||||
|
const formFields = computed(() => {
|
||||||
|
const site = authSites.value[authForm.value.site]
|
||||||
|
return Object.keys(site?.params || {})
|
||||||
|
.filter(item => {
|
||||||
|
return site.params[item].name && site.params[item].type
|
||||||
|
})
|
||||||
|
.map(key => {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
site: authForm.value.site,
|
||||||
|
name: site.params[key].name,
|
||||||
|
type: site.params[key].type,
|
||||||
|
placeholder: site.params[key].placeholder,
|
||||||
|
tooltip: site.params[key].tooltip,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 查询之前使用的认证参数
|
||||||
|
async function loadLastAuthParams() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get(`system/setting/UserSiteAuthParams`)
|
||||||
|
if (result.success) {
|
||||||
|
const ret = result.data?.value
|
||||||
|
if (ret && !isNullOrEmptyObject(ret.params)) {
|
||||||
|
authForm.value = ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载认证站点配置
|
||||||
|
async function loadAuthSites() {
|
||||||
|
try {
|
||||||
|
authSites.value = (await api.get(`site/auth`)) || {}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成
|
||||||
|
async function handleDone() {
|
||||||
|
await checkUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 认证处理
|
||||||
|
async function checkUser() {
|
||||||
|
if (!authForm.value.site) {
|
||||||
|
$toast.error('请选择认证站点!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!authSites.value[authForm.value.site]) {
|
||||||
|
$toast.error('站点配置不存在!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (formFields.value.length > 0) {
|
||||||
|
for (const field of formFields.value) {
|
||||||
|
if (!authForm.value.params[field.site.toUpperCase() + '_' + field.key.toUpperCase()]) {
|
||||||
|
$toast.error(`请输入${field.name}!`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.post(`site/auth`, authForm.value)
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success('用户认证成功,请重新登录!')
|
||||||
|
// 1秒后刷新页面
|
||||||
|
setTimeout(() => {
|
||||||
|
emit('done')
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
$toast.error(`认证失败:${result.message}`)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadAuthSites()
|
||||||
|
loadLastAuthParams()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VDialog width="40rem" max-height="85vh">
|
||||||
|
<VCard title="用户认证" class="rounded-t">
|
||||||
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VSelect
|
||||||
|
v-model="authForm.site"
|
||||||
|
:items="dropdownItems"
|
||||||
|
item-value="key"
|
||||||
|
item-title="name"
|
||||||
|
label="选择认证站点"
|
||||||
|
item-props
|
||||||
|
>
|
||||||
|
</VSelect>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol v-for="param in formFields" :key="param.key">
|
||||||
|
<VTextField
|
||||||
|
v-model="authForm.params[param.site.toUpperCase() + '_' + param.key.toUpperCase()]"
|
||||||
|
:type="param.type"
|
||||||
|
:label="param.name"
|
||||||
|
:placeholder="param.placeholder"
|
||||||
|
:hint="param.tooltip"
|
||||||
|
clearable
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
<VCardText class="text-center">
|
||||||
|
<VBtn
|
||||||
|
variant="elevated"
|
||||||
|
@click="handleDone"
|
||||||
|
prepend-icon="mdi-check"
|
||||||
|
class="px-5"
|
||||||
|
size="large"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
开始认证
|
||||||
|
</VBtn>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
</template>
|
||||||
303
src/components/dialog/WorkflowActionsDialog.vue
Normal file
303
src/components/dialog/WorkflowActionsDialog.vue
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { VueFlow, useVueFlow } from '@vue-flow/core'
|
||||||
|
import { MiniMap } from '@vue-flow/minimap'
|
||||||
|
import useDragAndDrop from '@core/utils/workflow'
|
||||||
|
import { Workflow } from '@/api/types'
|
||||||
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import api from '@/api'
|
||||||
|
import WorkflowSidebar from '@/layouts/components/WorkflowSidebar.vue'
|
||||||
|
import DropzoneBackground from '@/layouts/components/DropzoneBackground.vue'
|
||||||
|
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
|
||||||
|
|
||||||
|
const { onConnect, addEdges, nodes, edges } = useVueFlow()
|
||||||
|
|
||||||
|
const { onDragOver, onDrop, onDragLeave, isDragOver } = useDragAndDrop()
|
||||||
|
|
||||||
|
onConnect(addEdges)
|
||||||
|
|
||||||
|
// 自定义节点类型
|
||||||
|
const nodeTypes: Record<string, any> = ref({})
|
||||||
|
|
||||||
|
// 自动扫描目录下所有的 .vue 文件
|
||||||
|
const components = import.meta.glob('../workflow/*Action.vue')
|
||||||
|
|
||||||
|
// 动态加载某个组件
|
||||||
|
const loadComponent = async (componentName: string) => {
|
||||||
|
const component = components[`../workflow/${componentName}.vue`]
|
||||||
|
if (component) {
|
||||||
|
return ((await component()) as any).default
|
||||||
|
}
|
||||||
|
throw new Error(`组件 ${componentName} 未找到`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将所有components中的组件加载到nodeTypes中
|
||||||
|
for (const path in components) {
|
||||||
|
const componentName = path.match(/\.\/workflow\/(.*).vue$/)?.[1]
|
||||||
|
if (!componentName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
loadComponent(componentName).then(component => {
|
||||||
|
nodeTypes.value[componentName] = markRaw(component)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
workflow: Object as PropType<Workflow>,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['close', 'save'])
|
||||||
|
|
||||||
|
// 站点编辑表单数据
|
||||||
|
const workflowForm = ref<any>(props.workflow || {})
|
||||||
|
|
||||||
|
// 提示框
|
||||||
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 导入代码对话框
|
||||||
|
const importCodeDialog = ref(false)
|
||||||
|
|
||||||
|
// 调用API 编辑任务
|
||||||
|
async function updateWorkflow() {
|
||||||
|
// 更新节点和流程
|
||||||
|
workflowForm.value.actions = nodes
|
||||||
|
workflowForm.value.flows = edges
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: string } = await api.put(`workflow/${workflowForm.value.id}`, workflowForm.value)
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success(`保存任务流程成功!`)
|
||||||
|
emit('save')
|
||||||
|
} else {
|
||||||
|
$toast.error(`保存任务流程失败:${result.message}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存导入的代码,直接覆盖原有值
|
||||||
|
function saveCodeString(type: string, code: any) {
|
||||||
|
try {
|
||||||
|
if (code) {
|
||||||
|
const codeObject = JSON.parse(code.value)
|
||||||
|
if (type === 'workflow') {
|
||||||
|
nodes.value = codeObject.actions || []
|
||||||
|
edges.value = codeObject.flows || []
|
||||||
|
}
|
||||||
|
importCodeDialog.value = false
|
||||||
|
$toast.success('导入成功!')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
$toast.error('导入失败!')
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分享工作流程
|
||||||
|
function shareWorkflow() {
|
||||||
|
const codeString = JSON.stringify({ actions: nodes.value, flows: edges.value })
|
||||||
|
navigator.clipboard.writeText(codeString)
|
||||||
|
$toast.success('任务流程代码已复制到剪贴板!')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除选中节点或连接线
|
||||||
|
const deleteSelectedNodeOrEdge = () => {
|
||||||
|
// 删除选中的节点
|
||||||
|
const selectedNode = nodes.value.find((node: { selected: any }) => node.selected)
|
||||||
|
if (selectedNode) {
|
||||||
|
// 删除节点
|
||||||
|
nodes.value = nodes.value.filter((node: { id: any }) => node.id !== selectedNode.id)
|
||||||
|
// 删除与该节点相关的 edges
|
||||||
|
edges.value = edges.value.filter(
|
||||||
|
(edge: { source: any; target: any }) => edge.source !== selectedNode.id && edge.target !== selectedNode.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 删除选中的连接线
|
||||||
|
const selectedEdge = edges.value.find((edge: { selected: any }) => edge.selected)
|
||||||
|
if (selectedEdge) {
|
||||||
|
// 删除连接线
|
||||||
|
edges.value = edges.value.filter((edge: { id: any }) => edge.id !== selectedEdge.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 键盘按键事件处理
|
||||||
|
const handleKeyDown = (event: { key: string }) => {
|
||||||
|
if (event.key === 'Delete' || event.key === 'Backspace') {
|
||||||
|
deleteSelectedNodeOrEdge()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.workflow) {
|
||||||
|
nodes.value = props.workflow.actions ?? []
|
||||||
|
edges.value = props.workflow.flows ?? []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VDialog scrollable fullscreen :scrim="false" transition="dialog-bottom-transition">
|
||||||
|
<VCard>
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div>
|
||||||
|
<VToolbar color="primary">
|
||||||
|
<VToolbarItems>
|
||||||
|
<VBtn icon @click="emit('close')" class="ms-3">
|
||||||
|
<VIcon size="large" color="white" icon="mdi-close" />
|
||||||
|
</VBtn>
|
||||||
|
</VToolbarItems>
|
||||||
|
<VToolbarTitle> 编辑流程 - {{ workflow?.name }} </VToolbarTitle>
|
||||||
|
<VToolbarItems>
|
||||||
|
<VBtn icon @click="importCodeDialog = true">
|
||||||
|
<VIcon size="large" color="white" icon="mdi-import" />
|
||||||
|
</VBtn>
|
||||||
|
<VBtn icon @click="shareWorkflow">
|
||||||
|
<VIcon size="large" color="white" icon="mdi-share" />
|
||||||
|
</VBtn>
|
||||||
|
<VBtn icon @click="updateWorkflow" class="mx-5">
|
||||||
|
<VIcon size="large" color="white" icon="mdi-content-save" />
|
||||||
|
</VBtn>
|
||||||
|
</VToolbarItems>
|
||||||
|
</VToolbar>
|
||||||
|
</div>
|
||||||
|
<VDivider />
|
||||||
|
<VCardText class="px-0 py-0">
|
||||||
|
<div class="dnd-flow" @drop="onDrop">
|
||||||
|
<VueFlow
|
||||||
|
:nodes="nodes"
|
||||||
|
:edges="edges"
|
||||||
|
:nodeTypes="nodeTypes"
|
||||||
|
:default-edge-options="{ type: 'animation', animated: true }"
|
||||||
|
:edge-updater-radius="10"
|
||||||
|
@dragover="onDragOver"
|
||||||
|
@dragleave="onDragLeave"
|
||||||
|
@keydown="handleKeyDown"
|
||||||
|
auto-connect
|
||||||
|
>
|
||||||
|
<MiniMap />
|
||||||
|
<DropzoneBackground
|
||||||
|
:style="{
|
||||||
|
backgroundColor: isDragOver ? '#e7f3ff' : 'transparent',
|
||||||
|
transition: 'background-color 0.2s ease',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
</DropzoneBackground>
|
||||||
|
</VueFlow>
|
||||||
|
<WorkflowSidebar />
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
<ImportCodeDialog
|
||||||
|
v-if="importCodeDialog"
|
||||||
|
v-model="importCodeDialog"
|
||||||
|
title="导入任务流程"
|
||||||
|
dataType="workflow"
|
||||||
|
@close="importCodeDialog = false"
|
||||||
|
@save="saveCodeString"
|
||||||
|
/>
|
||||||
|
</VDialog>
|
||||||
|
</template>
|
||||||
|
<style>
|
||||||
|
@import '@vue-flow/core/dist/style.css';
|
||||||
|
@import '@vue-flow/core/dist/theme-default.css';
|
||||||
|
@import '@vue-flow/controls/dist/style.css';
|
||||||
|
@import '@vue-flow/minimap/dist/style.css';
|
||||||
|
@import '@vue-flow/node-resizer/dist/style.css';
|
||||||
|
|
||||||
|
.vue-flow__minimap {
|
||||||
|
transform: scale(75%);
|
||||||
|
transform-origin: bottom right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dnd-flow {
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dnd-flow aside {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
border-right: 1px solid #eee;
|
||||||
|
padding: 15px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #10b981bf;
|
||||||
|
-webkit-box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
|
||||||
|
box-shadow: 0 5px 10px #0000004d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dnd-flow aside .nodes > * {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
cursor: grab;
|
||||||
|
font-weight: 500;
|
||||||
|
-webkit-box-shadow: 5px 5px 10px 2px rgba(0, 0, 0, 0.25);
|
||||||
|
box-shadow: 5px 5px 10px 2px #00000040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dnd-flow aside .description {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.dnd-flow .vue-flow-wrapper {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 640px) {
|
||||||
|
.dnd-flow {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dnd-flow aside {
|
||||||
|
max-width: 25%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 639px) {
|
||||||
|
.dnd-flow aside .nodes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone-background {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone-background .overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__handle {
|
||||||
|
height: 24px;
|
||||||
|
width: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__edge-path,
|
||||||
|
.vue-flow__connection-path {
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__handle-left {
|
||||||
|
background-color: rgb(var(--v-theme-info));
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__handle-right {
|
||||||
|
background-color: rgb(var(--v-theme-error));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
133
src/components/dialog/WorkflowAddEditDialog.vue
Normal file
133
src/components/dialog/WorkflowAddEditDialog.vue
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import type { Workflow } from '@/api/types'
|
||||||
|
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||||
|
import { requiredValidator } from '@/@validators'
|
||||||
|
import api from '@/api'
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
// 任务信息
|
||||||
|
workflow: Object as PropType<Workflow>,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 新增或修改字样
|
||||||
|
const title = computed(() => (props.workflow ? '编辑' : '创建'))
|
||||||
|
|
||||||
|
// 显示器宽度
|
||||||
|
const display = useDisplay()
|
||||||
|
|
||||||
|
// 注册事件
|
||||||
|
const emit = defineEmits(['save', 'remove', 'close'])
|
||||||
|
|
||||||
|
// 站点编辑表单数据
|
||||||
|
const workflowForm = ref<Workflow>(
|
||||||
|
props.workflow || {
|
||||||
|
name: undefined,
|
||||||
|
timer: undefined,
|
||||||
|
description: undefined,
|
||||||
|
state: 'P',
|
||||||
|
run_count: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 提示框
|
||||||
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 调用API 新增任务
|
||||||
|
async function addWorkflow() {
|
||||||
|
if (!workflowForm.value.name || !workflowForm.value.timer) {
|
||||||
|
$toast.error('请填写完整信息!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startNProgress()
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: string } = await api.post('workflow/', workflowForm.value)
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success(`创建任务成功,请编辑流程!`)
|
||||||
|
emit('save')
|
||||||
|
} else {
|
||||||
|
$toast.error(`创建任务失败:${result.message}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
doneNProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API 编辑任务
|
||||||
|
async function editWorkflow() {
|
||||||
|
if (!workflowForm.value.name || !workflowForm.value.timer) {
|
||||||
|
$toast.error('请填写完整信息!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startNProgress()
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: string } = await api.put(`workflow/${workflowForm.value.id}`, workflowForm.value)
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success(`修改任务成功!`)
|
||||||
|
emit('save')
|
||||||
|
} else {
|
||||||
|
$toast.error(`修改任务失败:${result.message}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
doneNProgress()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VDialog scrollable :close-on-back="false" persistent eager max-width="30rem" :fullscreen="!display.mdAndUp.value">
|
||||||
|
<VCard :title="`${title}任务`" class="rounded-t">
|
||||||
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
|
<VDivider />
|
||||||
|
<VCardText>
|
||||||
|
<VForm @submit.prevent="() => {}">
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField
|
||||||
|
v-model="workflowForm.name"
|
||||||
|
label="别名"
|
||||||
|
:rules="[requiredValidator]"
|
||||||
|
persistent-hint
|
||||||
|
hint="任务名称"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VCronField
|
||||||
|
v-model="workflowForm.timer"
|
||||||
|
label="定时"
|
||||||
|
:rules="[requiredValidator]"
|
||||||
|
placeholder="5位cron表达式"
|
||||||
|
persistent-hint
|
||||||
|
hint="任务执行周期"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextarea v-model="workflowForm.description" label="任务描述" />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
<VCardActions class="pt-3">
|
||||||
|
<VSpacer />
|
||||||
|
<VBtn
|
||||||
|
v-if="workflow"
|
||||||
|
block
|
||||||
|
color="primary"
|
||||||
|
variant="elevated"
|
||||||
|
@click="editWorkflow"
|
||||||
|
prepend-icon="mdi-content-save"
|
||||||
|
class="px-5"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</VBtn>
|
||||||
|
<VBtn v-else block color="primary" variant="elevated" @click="addWorkflow" prepend-icon="mdi-plus" class="px-5">
|
||||||
|
创建
|
||||||
|
</VBtn>
|
||||||
|
</VCardActions>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
</template>
|
||||||
46
src/components/field/CronField.vue
Normal file
46
src/components/field/CronField.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import CronInput from '@/components/input/CronInput.vue'
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: '* * * * *',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const innerValue = ref(props.modelValue)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
value => {
|
||||||
|
innerValue.value = value
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const propsWithoutModelValue = computed(() => {
|
||||||
|
const { modelValue, ...rest } = props
|
||||||
|
return { ...rest, ...attrs }
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateModelValue(value: string) {
|
||||||
|
innerValue.value = value
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CronInput v-model="innerValue" @update:modelValue="updateModelValue">
|
||||||
|
<template #activator="{ menuprops }">
|
||||||
|
<VTextField
|
||||||
|
:modelValue="innerValue"
|
||||||
|
@update:modelValue="updateModelValue"
|
||||||
|
v-bind="{ ...menuprops, ...propsWithoutModelValue }"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</CronInput>
|
||||||
|
</template>
|
||||||
49
src/components/field/PathField.vue
Normal file
49
src/components/field/PathField.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import PathInput from '@/components/input/PathInput.vue'
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: '* * * * *',
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
type: String,
|
||||||
|
default: 'local',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const innerValue = ref(props.modelValue)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
value => {
|
||||||
|
innerValue.value = value
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const propsWithoutModelValue = computed(() => {
|
||||||
|
const { modelValue, ...rest } = props
|
||||||
|
return { ...rest, ...attrs }
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateModelValue(value: string) {
|
||||||
|
innerValue.value = value
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PathInput v-model="innerValue" :storage="props.storage" @update:modelValue="updateModelValue">
|
||||||
|
<template #activator="{ menuprops }">
|
||||||
|
<VTextField
|
||||||
|
:modelValue="innerValue"
|
||||||
|
@update:modelValue="updateModelValue"
|
||||||
|
v-bind="{ ...menuprops, ...propsWithoutModelValue }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</PathInput>
|
||||||
|
</template>
|
||||||
@@ -7,9 +7,9 @@ import ReorganizeDialog from '../dialog/ReorganizeDialog.vue'
|
|||||||
import { formatBytes } from '@core/utils/formatters'
|
import { formatBytes } from '@core/utils/formatters'
|
||||||
import type { Context, EndPoints, FileItem } from '@/api/types'
|
import type { Context, EndPoints, FileItem } from '@/api/types'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
|
|
||||||
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
import MediaInfoDialog from '../dialog/MediaInfoDialog.vue'
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
@@ -735,14 +735,12 @@ onMounted(() => {
|
|||||||
<!-- 进度框 -->
|
<!-- 进度框 -->
|
||||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" :value="progressValue" />
|
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" :value="progressValue" />
|
||||||
<!-- 识别结果对话框 -->
|
<!-- 识别结果对话框 -->
|
||||||
<VDialog v-if="nameTestDialog" v-model="nameTestDialog" width="50rem">
|
<MediaInfoDialog
|
||||||
<VCard>
|
v-if="nameTestDialog"
|
||||||
<DialogCloseBtn @click="nameTestDialog = false" />
|
v-model="nameTestDialog"
|
||||||
<VCardItem>
|
:context="nameTestResult"
|
||||||
<MediaInfoCard :context="nameTestResult" />
|
@close="nameTestDialog = false"
|
||||||
</VCardItem>
|
/>
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ const sortIcon = computed(() => {
|
|||||||
</IconBtn>
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
</VTooltip>
|
</VTooltip>
|
||||||
<VDialog v-model="newFolderPopper" max-width="50rem">
|
<VDialog v-if="newFolderPopper" v-model="newFolderPopper" max-width="50rem">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<IconBtn v-bind="props">
|
<IconBtn v-bind="props">
|
||||||
<VTooltip text="新建文件夹">
|
<VTooltip text="新建文件夹">
|
||||||
|
|||||||
38
src/components/input/CronInput.vue
Normal file
38
src/components/input/CronInput.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: '* * * * *',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const currentCron = ref(props.modelValue)
|
||||||
|
|
||||||
|
watch(currentCron, newVal => {
|
||||||
|
emit('update:modelValue', newVal)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
value => {
|
||||||
|
currentCron.value = value
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VMenu :close-on-content-click="false" content-class="cursor-default" persistent>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<slot name="activator" :menuprops="props" />
|
||||||
|
</template>
|
||||||
|
<VList>
|
||||||
|
<VListItem>
|
||||||
|
<VCronVuetify v-model="currentCron" locale="zh-CN" :chip-props="{ color: 'success' }" class="mt-1" />
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import api from '@/api'
|
|
||||||
import { FileItem } from '@/api/types'
|
|
||||||
import { VTreeview } from 'vuetify/labs/VTreeview'
|
|
||||||
|
|
||||||
// 输入变量为默认路径
|
|
||||||
const props = defineProps({
|
|
||||||
root: {
|
|
||||||
type: String,
|
|
||||||
default: '/',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
storage: {
|
|
||||||
type: String,
|
|
||||||
default: 'local',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// update:modelValue 事件
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
|
||||||
|
|
||||||
// 激活的目录
|
|
||||||
const activedDirs = ref<string[]>([])
|
|
||||||
|
|
||||||
// 打开的目录
|
|
||||||
const openedDirs = ref<string[]>([])
|
|
||||||
|
|
||||||
// 目录列表
|
|
||||||
const treeItems = ref<FileItem[]>([
|
|
||||||
{
|
|
||||||
name: '/',
|
|
||||||
path: props.root,
|
|
||||||
children: [],
|
|
||||||
type: 'dir',
|
|
||||||
basename: props.root,
|
|
||||||
storage: props.storage,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
// 拉取子目录
|
|
||||||
async function fetchDirs(item: any) {
|
|
||||||
return api
|
|
||||||
.post('/storage/list', item)
|
|
||||||
.then((data: any) => {
|
|
||||||
// 只添加目录到子目录
|
|
||||||
data = data.filter((i: any) => i.type === 'dir')
|
|
||||||
item.children.push(...data)
|
|
||||||
})
|
|
||||||
.catch(err => console.warn(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取选择的目录路径
|
|
||||||
const selectedPath = computed(() => {
|
|
||||||
if (activedDirs.value.length > 0) {
|
|
||||||
return activedDirs.value[0]
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听目录变化
|
|
||||||
watch(activedDirs, newVal => {
|
|
||||||
if (!newVal.length) return
|
|
||||||
emit('update:modelValue', selectedPath)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听存储变化
|
|
||||||
watch(
|
|
||||||
() => props.storage,
|
|
||||||
async newVal => {
|
|
||||||
treeItems.value = [
|
|
||||||
{
|
|
||||||
name: '/',
|
|
||||||
path: props.root,
|
|
||||||
children: [],
|
|
||||||
type: 'dir',
|
|
||||||
basename: props.root,
|
|
||||||
storage: newVal,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
openedDirs.value = []
|
|
||||||
activedDirs.value = []
|
|
||||||
},
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VMenu :close-on-content-click="false" content-class="cursor-default">
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<slot name="activator" :menuprops="props" />
|
|
||||||
</template>
|
|
||||||
<VTreeview
|
|
||||||
v-model:activated="activedDirs"
|
|
||||||
v-model:opened="openedDirs"
|
|
||||||
:items="treeItems"
|
|
||||||
:load-children="fetchDirs"
|
|
||||||
item-key="path"
|
|
||||||
item-title="name"
|
|
||||||
item-value="path"
|
|
||||||
item-type="unknown"
|
|
||||||
activatable
|
|
||||||
return-object
|
|
||||||
max-height="20rem"
|
|
||||||
expand-icon="mdi-folder"
|
|
||||||
collapse-icon="mdi-folder-open"
|
|
||||||
>
|
|
||||||
</VTreeview>
|
|
||||||
</VMenu>
|
|
||||||
</template>
|
|
||||||
174
src/components/input/PathInput.vue
Normal file
174
src/components/input/PathInput.vue
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import api from '@/api'
|
||||||
|
import { FileItem } from '@/api/types'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: '/',
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
type: String,
|
||||||
|
default: '/',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
type: String,
|
||||||
|
default: 'local',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const menuVisible = ref(false)
|
||||||
|
|
||||||
|
const treeItems = ref<FileItem[]>([
|
||||||
|
{
|
||||||
|
name: '/',
|
||||||
|
path: props.root,
|
||||||
|
children: [],
|
||||||
|
type: 'dir',
|
||||||
|
basename: props.root,
|
||||||
|
storage: props.storage,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const activedDirs = ref<FileItem[]>([])
|
||||||
|
|
||||||
|
const openedDirs = ref<FileItem[]>([])
|
||||||
|
|
||||||
|
// 调用API查询子目录
|
||||||
|
async function fetchDirs(item: any) {
|
||||||
|
return api
|
||||||
|
.post('/storage/list', item)
|
||||||
|
.then((data: any) => {
|
||||||
|
data = data.filter((i: any) => i.type === 'dir')
|
||||||
|
item.children?.push(...data)
|
||||||
|
})
|
||||||
|
.catch(err => console.warn(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归查询路径
|
||||||
|
function findPath(item: FileItem, path: string): FileItem | null {
|
||||||
|
if (item.path === path) {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
if (item.children) {
|
||||||
|
for (const child of item.children) {
|
||||||
|
const res: FileItem | null = findPath(child, path)
|
||||||
|
if (res) {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据路径展开所有子目录
|
||||||
|
async function expandDirs(path: string) {
|
||||||
|
// 分割路径
|
||||||
|
const paths = path.split('/').filter(i => i)
|
||||||
|
// 展开根目录
|
||||||
|
const root_item = treeItems.value[0]
|
||||||
|
await fetchDirs(root_item)
|
||||||
|
openedDirs.value.push(root_item)
|
||||||
|
// 逐级展开
|
||||||
|
let currentPath = '/'
|
||||||
|
for (const p of paths) {
|
||||||
|
currentPath += `${p}/`
|
||||||
|
// 查询当前目录
|
||||||
|
const item = findPath(root_item, currentPath)
|
||||||
|
if (!item) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// 加载子目录
|
||||||
|
if (item.children?.length === 0) {
|
||||||
|
await fetchDirs(item)
|
||||||
|
}
|
||||||
|
// 打开当前目录
|
||||||
|
if (!openedDirs.value.includes(item) && path != currentPath) {
|
||||||
|
openedDirs.value.push(item)
|
||||||
|
}
|
||||||
|
// 选中当前目录
|
||||||
|
if (path == currentPath) {
|
||||||
|
activedDirs.value = [item]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前选中项
|
||||||
|
const selectedPath = computed(() => {
|
||||||
|
if (activedDirs.value.length > 0) {
|
||||||
|
return activedDirs.value[0].path
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(activedDirs, newVal => {
|
||||||
|
if (!newVal.length) return
|
||||||
|
emit('update:modelValue', selectedPath.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => menuVisible.value,
|
||||||
|
async visible => {
|
||||||
|
if (visible) {
|
||||||
|
treeItems.value = [
|
||||||
|
{
|
||||||
|
name: '/',
|
||||||
|
path: props.root,
|
||||||
|
children: [],
|
||||||
|
type: 'dir',
|
||||||
|
basename: props.root,
|
||||||
|
storage: props.storage,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
openedDirs.value = []
|
||||||
|
activedDirs.value = []
|
||||||
|
await expandDirs(props.modelValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.storage,
|
||||||
|
async newVal => {
|
||||||
|
treeItems.value = [
|
||||||
|
{
|
||||||
|
name: '/',
|
||||||
|
path: props.root,
|
||||||
|
children: [],
|
||||||
|
type: 'dir',
|
||||||
|
basename: props.root,
|
||||||
|
storage: newVal,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
activedDirs.value = []
|
||||||
|
openedDirs.value = []
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VMenu v-model="menuVisible" :close-on-content-click="false" content-class="cursor-default">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<slot name="activator" :menuprops="props" />
|
||||||
|
</template>
|
||||||
|
<VTreeview
|
||||||
|
v-model:activated="activedDirs"
|
||||||
|
v-model:opened="openedDirs"
|
||||||
|
:items="treeItems"
|
||||||
|
:load-children="fetchDirs"
|
||||||
|
item-key="path"
|
||||||
|
item-title="name"
|
||||||
|
item-value="path"
|
||||||
|
activatable
|
||||||
|
return-object
|
||||||
|
max-height="20rem"
|
||||||
|
expand-icon="mdi-folder"
|
||||||
|
collapse-icon="mdi-folder-open"
|
||||||
|
/>
|
||||||
|
</VMenu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
7
src/components/misc/FilterOption.vue
Normal file
7
src/components/misc/FilterOption.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{ title: string }>()
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VListSubheader>{{ title }}</VListSubheader>
|
||||||
|
<VListItem><slot /></VListItem>
|
||||||
|
</template>
|
||||||
@@ -6,10 +6,21 @@ import { type PropType } from 'vue'
|
|||||||
const elementProps = defineProps({
|
const elementProps = defineProps({
|
||||||
config: Object as PropType<RenderProps>,
|
config: Object as PropType<RenderProps>,
|
||||||
})
|
})
|
||||||
|
// key
|
||||||
|
const componentKey = ref(0)
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
componentKey.value++
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Component :is="elementProps.config?.component" v-if="!elementProps.config?.html" v-bind="elementProps.config?.props">
|
<Component
|
||||||
|
:key="componentKey"
|
||||||
|
:is="elementProps.config?.component"
|
||||||
|
v-if="!elementProps.config?.html"
|
||||||
|
v-bind="elementProps.config?.props"
|
||||||
|
>
|
||||||
{{ elementProps.config?.text }}
|
{{ elementProps.config?.text }}
|
||||||
<template v-for="(content, name) in elementProps.config?.slots || []" :key="name" v-slot:[name]="{ _props }">
|
<template v-for="(content, name) in elementProps.config?.slots || []" :key="name" v-slot:[name]="{ _props }">
|
||||||
<slot :name="name" v-bind="_props">
|
<slot :name="name" v-bind="_props">
|
||||||
@@ -23,6 +34,7 @@ const elementProps = defineProps({
|
|||||||
/>
|
/>
|
||||||
</Component>
|
</Component>
|
||||||
<Component
|
<Component
|
||||||
|
:key="componentKey"
|
||||||
:is="elementProps.config?.component"
|
:is="elementProps.config?.component"
|
||||||
v-if="elementProps.config?.html"
|
v-if="elementProps.config?.html"
|
||||||
v-bind="elementProps.config?.props"
|
v-bind="elementProps.config?.props"
|
||||||
|
|||||||
@@ -1,57 +1,141 @@
|
|||||||
<script lang="ts" setup>
|
<script setup lang="ts">
|
||||||
import { RenderProps } from '@/api/types'
|
import { RenderProps } from '@/api/types'
|
||||||
import { type PropType, ref } from 'vue'
|
|
||||||
|
|
||||||
// 输入参数
|
// 定义 props
|
||||||
const elementProps = defineProps({
|
defineProps<{
|
||||||
config: Object as PropType<RenderProps>,
|
config: RenderProps // JSON 配置
|
||||||
form: Object as PropType<any>,
|
model: Record<string, any> // 数据模型
|
||||||
})
|
}>()
|
||||||
|
|
||||||
// 配置元素
|
/**
|
||||||
const formItem = ref<RenderProps>(
|
* 解析属性,支持 v-model 和动态绑定
|
||||||
elementProps.config ?? {
|
* @param rawProps 原始属性
|
||||||
component: 'div',
|
* @param model 数据模型
|
||||||
text: '',
|
* @returns 解析后的属性
|
||||||
html: '',
|
*/
|
||||||
props: {},
|
const parseProps = (rawProps: Record<string, any>, model: Record<string, any>) => {
|
||||||
content: [],
|
const parsedProps: Record<string, any> = {}
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 配置数据
|
const isExpression = (value: string) => value.startsWith('{{') && value.endsWith('}}')
|
||||||
const formData = ref<any>(elementProps.form || {})
|
const extractExpression = (value: string) => value.slice(2, -2).trim()
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(rawProps)) {
|
||||||
|
if (key === 'modelvalue') {
|
||||||
|
// 将 modelvalue 转换为 v-model:value 的形式
|
||||||
|
parsedProps['value'] = model[value]
|
||||||
|
parsedProps['onUpdate:value'] = (newValue: any) => {
|
||||||
|
model[value] = newValue
|
||||||
|
}
|
||||||
|
} else if (['model', 'v-model'].includes(key)) {
|
||||||
|
// 处理 v-model
|
||||||
|
parsedProps['modelValue'] = model[value]
|
||||||
|
parsedProps['onUpdate:modelValue'] = (newValue: any) => {
|
||||||
|
model[value] = newValue
|
||||||
|
}
|
||||||
|
} else if (['show', 'v-show'].includes(key)) {
|
||||||
|
// 处理 v-show,实现显示隐藏
|
||||||
|
const expression = isExpression(value) ? extractExpression(value) : value
|
||||||
|
const isVisible = new Function('model', `with(model) { return ${expression} }`)(model)
|
||||||
|
// 动态设置 style.display
|
||||||
|
if (!parsedProps.style) {
|
||||||
|
parsedProps.style = {}
|
||||||
|
}
|
||||||
|
parsedProps.style.display = isVisible ? '' : 'none'
|
||||||
|
} else if (key.startsWith('model:') || key.startsWith('v-model:')) {
|
||||||
|
// 处理 v-model:<prop>
|
||||||
|
const propName = key.split(':')[1]
|
||||||
|
parsedProps[propName] = model[value]
|
||||||
|
parsedProps[`onUpdate:${propName}`] = (newValue: any) => {
|
||||||
|
model[value] = newValue
|
||||||
|
}
|
||||||
|
} else if (key.startsWith('on')) {
|
||||||
|
// 处理事件监听,值是函数的代码
|
||||||
|
const eventName = key.replace('on', '').toLowerCase()
|
||||||
|
parsedProps[eventName] = new Function('model', `with(model) { return ${value} }`)(model)
|
||||||
|
} else {
|
||||||
|
// 如果是表达式,需要绑定
|
||||||
|
if (typeof value === 'string' && isExpression(value)) {
|
||||||
|
const expression = extractExpression(value)
|
||||||
|
parsedProps[key] = new Function('model', `with(model) { return ${expression} }`)(model)
|
||||||
|
} else if (typeof value === 'string' && value in model) {
|
||||||
|
// 如果是数据模型的属性,直接绑定
|
||||||
|
parsedProps[key] = model[value]
|
||||||
|
} else {
|
||||||
|
// 其他情况直接赋值
|
||||||
|
parsedProps[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedProps
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染插槽内容
|
||||||
|
* @param slotContent 插槽配置
|
||||||
|
* @param model 数据模型
|
||||||
|
* @param slotScope 插槽作用域
|
||||||
|
*/
|
||||||
|
const renderSlotContent = (slotContent: any, model: any, slotScope: any) => {
|
||||||
|
if (Array.isArray(slotContent)) {
|
||||||
|
// 如果插槽内容是数组,递归渲染
|
||||||
|
return slotContent.map(childConfig => renderComponent(childConfig, model, slotScope))
|
||||||
|
}
|
||||||
|
// 如果插槽内容是单个配置,递归渲染
|
||||||
|
return renderComponent(slotContent, model, slotScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染组件函数(递归支持嵌套)
|
||||||
|
* @param config JSON 配置
|
||||||
|
* @param model 数据模型
|
||||||
|
* @param slotScope 插槽作用域
|
||||||
|
* @returns 渲染的组件 VNode
|
||||||
|
*/
|
||||||
|
const renderComponent = (config: any, model: any, slotScope: any = {}) => {
|
||||||
|
const { component, props: componentProps = {}, content = [], slots = {}, html, text } = config
|
||||||
|
|
||||||
|
// 动态解析组件
|
||||||
|
const Component = resolveComponent(component)
|
||||||
|
|
||||||
|
// 解析属性
|
||||||
|
const parsedProps = parseProps(componentProps, model)
|
||||||
|
|
||||||
|
// 动态插槽解析
|
||||||
|
const slotNodes: Record<string, any> = {}
|
||||||
|
for (const [slotName, slotContent] of Object.entries(slots)) {
|
||||||
|
slotNodes[slotName] = (slotScopeData: any) =>
|
||||||
|
renderSlotContent(slotContent, model, { ...slotScope, ...slotScopeData })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染组件内容
|
||||||
|
const renderContent = () => {
|
||||||
|
// 如果配置了 `html`,直接渲染为 HTML 内容
|
||||||
|
if (html) {
|
||||||
|
return h(Component, { innerHTML: typeof html === 'string' ? html : model[html] })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果配置了 `text`,直接渲染为文本内容
|
||||||
|
if (text) {
|
||||||
|
return typeof text === 'string' ? text : model[text]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果配置了 `content`,递归渲染子组件
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
return content.map((childConfig: any) => renderComponent(childConfig, model, slotScope))
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染组件
|
||||||
|
return h(Component, parsedProps, {
|
||||||
|
...slotNodes,
|
||||||
|
default: renderContent,
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Component
|
<Component :is="renderComponent(config, model)" />
|
||||||
:is="formItem.component"
|
|
||||||
v-if="!formItem.html && !!formItem.props?.modelvalue"
|
|
||||||
v-bind="formItem.props"
|
|
||||||
v-model:value="formData[formItem.props?.modelvalue]"
|
|
||||||
>
|
|
||||||
{{ 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>
|
|
||||||
<Component :is="formItem.component" 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>
|
</template>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { RenderProps } from '@/api/types'
|
|||||||
const emit = defineEmits(['action'])
|
const emit = defineEmits(['action'])
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const elementProps = defineProps({
|
const props = defineProps({
|
||||||
config: Object as PropType<RenderProps>,
|
config: Object as PropType<RenderProps>,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -41,9 +41,9 @@ async function commonAction(api_path: string, method: string, params = {}) {
|
|||||||
// 组装事件
|
// 组装事件
|
||||||
let componentEvents = reactive<{ [key: string]: any }>({})
|
let componentEvents = reactive<{ [key: string]: any }>({})
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (!isNullOrEmptyObject(elementProps.config?.events)) {
|
if (!isNullOrEmptyObject(props.config?.events)) {
|
||||||
for (const key in elementProps.config?.events) {
|
for (const key in props.config?.events) {
|
||||||
const attr = elementProps.config?.events[key]
|
const attr = props.config?.events[key]
|
||||||
const func = async () => {
|
const func = async () => {
|
||||||
await commonAction(attr['api'], attr['method'], attr['params'])
|
await commonAction(attr['api'], attr['method'], attr['params'])
|
||||||
}
|
}
|
||||||
@@ -54,35 +54,20 @@ watchEffect(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Component
|
<Component :is="config?.component" v-if="!config?.html" v-bind="config?.props" v-on="componentEvents">
|
||||||
:is="elementProps.config?.component"
|
{{ config?.text }}
|
||||||
v-if="!elementProps.config?.html"
|
|
||||||
v-bind="elementProps.config?.props"
|
|
||||||
v-on="componentEvents"
|
|
||||||
>
|
|
||||||
{{ elementProps.config?.text }}
|
|
||||||
<template v-for="(content, name) in elementProps.config?.slots || []" :key="name" v-slot:[name]="{ _props }">
|
|
||||||
<slot :name="name" v-bind="_props">
|
|
||||||
<PageRender
|
|
||||||
v-for="(slotItem, slotIndex) in content || []"
|
|
||||||
:key="slotIndex"
|
|
||||||
:config="slotItem"
|
|
||||||
@action="emit('action')"
|
|
||||||
/>
|
|
||||||
</slot>
|
|
||||||
</template>
|
|
||||||
<PageRender
|
<PageRender
|
||||||
v-for="(innerItem, innerIndex) in elementProps.config?.content || []"
|
v-for="(innerItem, innerIndex) in config?.content || []"
|
||||||
:key="innerIndex"
|
:key="innerIndex"
|
||||||
:config="innerItem"
|
:config="innerItem"
|
||||||
@action="emit('action')"
|
@action="emit('action')"
|
||||||
/>
|
/>
|
||||||
</Component>
|
</Component>
|
||||||
<Component
|
<Component
|
||||||
:is="elementProps.config?.component"
|
:is="config?.component"
|
||||||
v-if="elementProps.config?.html"
|
v-if="config?.html"
|
||||||
v-bind="elementProps.config?.props"
|
v-bind="config?.props"
|
||||||
v-html="elementProps.config?.html"
|
v-html="config?.html"
|
||||||
v-on="componentEvents"
|
v-on="componentEvents"
|
||||||
/>
|
/>
|
||||||
<!-- 进度框 -->
|
<!-- 进度框 -->
|
||||||
|
|||||||
73
src/components/workflow/AddDownloadAction.vue
Normal file
73
src/components/workflow/AddDownloadAction.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import api from '@/api'
|
||||||
|
import { DownloaderConf } from '@/api/types'
|
||||||
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 下载器选项
|
||||||
|
const downloaderOptions = ref<{ title: string; value: string }[]>([])
|
||||||
|
|
||||||
|
// 加载所有下载器
|
||||||
|
async function loadDownloaderSetting() {
|
||||||
|
try {
|
||||||
|
const downloaders: DownloaderConf[] = await api.get('download/clients')
|
||||||
|
downloaderOptions.value = [
|
||||||
|
{ title: '默认', value: '' },
|
||||||
|
...downloaders.map((item: { name: any }) => ({
|
||||||
|
title: item.name,
|
||||||
|
value: item.name,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载下载器设置失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadDownloaderSetting()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VCard max-width="20rem">
|
||||||
|
<Handle id="edge_in" type="target" :position="Position.Left" />
|
||||||
|
<VCardItem>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<VAvatar>
|
||||||
|
<VIcon icon="mdi-download-box-outline" size="x-large"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VCardTitle>添加下载</VCardTitle>
|
||||||
|
<VCardSubtitle>根据资源列表添加下载任务</VCardSubtitle>
|
||||||
|
</VCardItem>
|
||||||
|
<VDivider />
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VSelect v-model="data.downloader" :items="downloaderOptions" label="下载器" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField v-model="data.labels" label="标签" placeholder="多个使用,分隔" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VPathField v-model="data.save_path" storage="local" label="保存路径" clearable placeholder="留空自动" />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VSwitch v-model="data.only_lack" label="仅下载缺失的资源" />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
<Handle id="edge_out" type="source" :position="Position.Right" />
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
31
src/components/workflow/AddSubscribeAction.vue
Normal file
31
src/components/workflow/AddSubscribeAction.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VCard>
|
||||||
|
<Handle id="edge_in" type="target" :position="Position.Left" />
|
||||||
|
<VCardItem>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<VAvatar>
|
||||||
|
<VIcon icon="mdi-star-check" size="x-large"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VCardTitle>添加订阅</VCardTitle>
|
||||||
|
<VCardSubtitle>根据媒体列表添加订阅</VCardSubtitle>
|
||||||
|
</VCardItem>
|
||||||
|
<Handle id="edge_out" type="source" :position="Position.Right" />
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
50
src/components/workflow/FetchDownloadsAction.vue
Normal file
50
src/components/workflow/FetchDownloadsAction.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VCard max-width="20rem">
|
||||||
|
<Handle id="edge_in" type="target" :position="Position.Left" />
|
||||||
|
<VCardItem>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<VAvatar>
|
||||||
|
<VIcon icon="mdi-progress-download" size="x-large"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VCardTitle>获取下载任务</VCardTitle>
|
||||||
|
<VCardSubtitle>获取下载队列中的任务状态</VCardSubtitle>
|
||||||
|
</VCardItem>
|
||||||
|
<VDivider />
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VSwitch v-model="data.loop" label="循环执行" />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField
|
||||||
|
v-model="data.loop_interval"
|
||||||
|
:disabled="!data.loop"
|
||||||
|
type="number"
|
||||||
|
label="循环间隔 (秒)"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
<Handle id="edge_out" type="source" :position="Position.Right" />
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
156
src/components/workflow/FetchMediasAction.vue
Normal file
156
src/components/workflow/FetchMediasAction.vue
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
|
import api from '@/api'
|
||||||
|
import { RecommendSource } from '@/api/types'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 内置榜单
|
||||||
|
const innerList = [
|
||||||
|
{
|
||||||
|
'api_path': 'recommend/tmdb_trending',
|
||||||
|
'name': '流行趋势',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'api_path': 'recommend/douban_showing',
|
||||||
|
'name': '正在热映',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'api_path': 'bangumi/calendar',
|
||||||
|
'name': 'Bangumi每日放送',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'api_path': 'recommend/tmdb_movies',
|
||||||
|
'name': 'TMDB热门电影',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'api_path': 'recommend/tmdb_tvs?with_original_language=zh|en|ja|ko',
|
||||||
|
'name': 'TMDB热门电视剧',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'api_path': 'recommend/douban_movie_hot',
|
||||||
|
'name': '豆瓣热门电影',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'api_path': 'recommend/douban_tv_hot',
|
||||||
|
'name': '豆瓣热门电视剧',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'api_path': 'recommend/douban_tv_animation',
|
||||||
|
'name': '豆瓣热门动漫',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'api_path': 'recommend/douban_movies',
|
||||||
|
'name': '豆瓣最新电影',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'api_path': 'recommend/douban_tvs',
|
||||||
|
'name': '豆瓣最新电视剧',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'api_path': 'recommend/douban_movie_top250',
|
||||||
|
'name': '豆瓣电影TOP250',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'api_path': 'recommend/douban_tv_weekly_chinese',
|
||||||
|
'name': '豆瓣国产剧集榜',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'api_path': 'recommend/douban_tv_weekly_global',
|
||||||
|
'name': '豆瓣全球剧集榜',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 额外的数据源
|
||||||
|
const extraRecommendSources = ref<RecommendSource[]>([])
|
||||||
|
|
||||||
|
// 加载额外的发现数据源
|
||||||
|
async function loadExtraRecommendSources() {
|
||||||
|
try {
|
||||||
|
extraRecommendSources.value = await api.get('recommend/source')
|
||||||
|
if (extraRecommendSources.value.length > 0) {
|
||||||
|
innerList.push(
|
||||||
|
...extraRecommendSources.value.map(source => ({
|
||||||
|
api_path: source.api_path,
|
||||||
|
name: source.name,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 来源类型下拉框
|
||||||
|
const sourceTypeOptions = [
|
||||||
|
{ value: 'ranking', title: '推荐榜单' },
|
||||||
|
{ value: 'api', title: 'API' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 计算下拉框
|
||||||
|
const sourceOptions = computed(() => innerList.map(item => item.name))
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadExtraRecommendSources()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VCard max-width="20rem">
|
||||||
|
<Handle id="edge_in" type="target" :position="Position.Left" />
|
||||||
|
<VCardItem>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<VAvatar>
|
||||||
|
<VIcon icon="mdi-multimedia" size="x-large"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VCardTitle>获取媒体数据</VCardTitle>
|
||||||
|
<VCardSubtitle>获取榜单等媒体数据列表</VCardSubtitle>
|
||||||
|
</VCardItem>
|
||||||
|
<VDivider />
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VSelect v-model="data.source_type" :items="sourceTypeOptions" label="来源" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow v-if="data.source_type === 'ranking'">
|
||||||
|
<VCol cols="12">
|
||||||
|
<VSelect
|
||||||
|
v-model="data.sources"
|
||||||
|
:items="sourceOptions"
|
||||||
|
label="选择榜单"
|
||||||
|
chips
|
||||||
|
multiple
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow v-else>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField
|
||||||
|
v-model="data.api_path"
|
||||||
|
label="API地址"
|
||||||
|
placeholder="/api/v1/plugin/xxx/xxxx"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
<Handle id="edge_out" type="source" :position="Position.Right" />
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
51
src/components/workflow/FetchRssAction.vue
Normal file
51
src/components/workflow/FetchRssAction.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VCard max-width="20rem">
|
||||||
|
<Handle id="edge_in" type="target" :position="Position.Left" />
|
||||||
|
<VCardItem>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<VAvatar>
|
||||||
|
<VIcon icon="mdi-rss" size="x-large"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VCardTitle>获取RSS资源</VCardTitle>
|
||||||
|
<VCardSubtitle>订阅RSS地址获取资源</VCardSubtitle>
|
||||||
|
</VCardItem>
|
||||||
|
<VDivider />
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField v-model="data.url" label="RSS地址" outlined dense clearable />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField v-model="data.ua" label="User-Agent" outlined dense clearable />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField v-model="data.timeout" type="number" label="超时时间" outlined dense clearable />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="6">
|
||||||
|
<VSwitch v-model="data.match_media" label="匹配媒体信息" />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="6">
|
||||||
|
<VSwitch v-model="data.proxy" label="使用代理" />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
<Handle id="edge_out" type="source" :position="Position.Right" />
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
118
src/components/workflow/FetchTorrentsAction.vue
Normal file
118
src/components/workflow/FetchTorrentsAction.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import api from '@/api'
|
||||||
|
import { Site } from '@/api/types'
|
||||||
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 电影/电视剧下拉框
|
||||||
|
const typeOptions = ref([
|
||||||
|
{
|
||||||
|
title: '电影',
|
||||||
|
value: '电影',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '电视剧',
|
||||||
|
value: '电视剧',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 搜索方式下拉框
|
||||||
|
const searchOptions = ref([
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
value: 'keyword',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '媒体列表',
|
||||||
|
value: 'media',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 站点数据列表
|
||||||
|
const siteList = ref<Site[]>([])
|
||||||
|
|
||||||
|
// 获取站点列表数据
|
||||||
|
async function loadSites() {
|
||||||
|
try {
|
||||||
|
const data: Site[] = await api.get('site/rss')
|
||||||
|
|
||||||
|
// 过滤站点,只有启用的站点才显示
|
||||||
|
siteList.value = data.filter(item => item.is_active)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 站点选项
|
||||||
|
const siteOptions = computed(() => {
|
||||||
|
return siteList.value.map(item => {
|
||||||
|
return {
|
||||||
|
title: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadSites()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VCard max-width="20rem">
|
||||||
|
<Handle id="edge_in" type="target" :position="Position.Left" />
|
||||||
|
<VCardItem>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<VAvatar>
|
||||||
|
<VIcon icon="mdi-search-web" size="x-large"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VCardTitle>搜索站点资源</VCardTitle>
|
||||||
|
<VCardSubtitle>搜索站点种子资源列表</VCardSubtitle>
|
||||||
|
</VCardItem>
|
||||||
|
<VDivider />
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VSelect v-model="data.search_type" label="搜索方式" :items="searchOptions" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow v-if="data.search_type === 'keyword'">
|
||||||
|
<VCol cols="6">
|
||||||
|
<VTextField v-model="data.name" label="名称" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="6">
|
||||||
|
<VTextField v-model="data.year" label="年份" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="6">
|
||||||
|
<VSelect v-model="data.type" label="类型" :items="typeOptions" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="6">
|
||||||
|
<VTextField v-model="data.season" type="number" label="季" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VSelect v-model="data.sites" label="站点" :items="siteOptions" chips multiple outlined dense />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow v-if="data.search_type === 'keyword'">
|
||||||
|
<VCol cols="12">
|
||||||
|
<VSwitch v-model="data.match_media" label="匹配媒体信息" />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
<Handle id="edge_out" type="source" :position="Position.Right" />
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
81
src/components/workflow/FilterMediasAction.vue
Normal file
81
src/components/workflow/FilterMediasAction.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import api from '@/api'
|
||||||
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 电影/电视剧下拉框
|
||||||
|
const typeOptions = ref([
|
||||||
|
{
|
||||||
|
title: '电影',
|
||||||
|
value: '电影',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '电视剧',
|
||||||
|
value: '电视剧',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 二级分类策略
|
||||||
|
const mediaCategories = ref<{ [key: string]: any }>({})
|
||||||
|
|
||||||
|
// 调用API查询自动分类配置
|
||||||
|
async function loadMediaCategories() {
|
||||||
|
try {
|
||||||
|
mediaCategories.value = await api.get('media/category')
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据选中的媒体类型,获取对应的媒体类别
|
||||||
|
const getCategories = computed(() => {
|
||||||
|
const default_value = [{ title: '全部', value: '' }]
|
||||||
|
if (!mediaCategories.value || !mediaCategories.value[props.data.type ?? '']) return default_value
|
||||||
|
return default_value.concat(mediaCategories.value[props.data.type ?? ''])
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadMediaCategories()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VCard max-width="20rem">
|
||||||
|
<Handle id="edge_in" type="target" :position="Position.Left" />
|
||||||
|
<VCardItem>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<VAvatar>
|
||||||
|
<VIcon icon="mdi-filter-check" size="x-large"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VCardTitle>过滤媒体数据</VCardTitle>
|
||||||
|
<VCardSubtitle>对媒体数据列表进行过滤</VCardSubtitle>
|
||||||
|
</VCardItem>
|
||||||
|
<VDivider />
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VSelect v-model="data.type" label="类型" :items="typeOptions" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="6">
|
||||||
|
<VTextField v-model="data.year" label="年份" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="6">
|
||||||
|
<VTextField v-model="data.vote" type="number" label="评分" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
<Handle id="edge_out" type="source" :position="Position.Right" />
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
176
src/components/workflow/FilterTorrentsAction.vue
Normal file
176
src/components/workflow/FilterTorrentsAction.vue
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import api from '@/api'
|
||||||
|
import { FilterRuleGroup } from '@/api/types'
|
||||||
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 质量选择框数据
|
||||||
|
const qualityOptions = ref([
|
||||||
|
{
|
||||||
|
title: '全部',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '蓝光原盘',
|
||||||
|
value: 'Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|MiniBD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Remux',
|
||||||
|
value: 'Remux',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'BluRay',
|
||||||
|
value: 'Blu-?Ray',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'UHD',
|
||||||
|
value: 'UHD|UltraHD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'WEB-DL',
|
||||||
|
value: 'WEB-?DL|WEB-?RIP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'HDTV',
|
||||||
|
value: 'HDTV',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'H265',
|
||||||
|
value: '[Hx].?265|HEVC',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'H264',
|
||||||
|
value: '[Hx].?264|AVC',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 分辨率选择框数据
|
||||||
|
const resolutionOptions = ref([
|
||||||
|
{
|
||||||
|
title: '全部',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '4k',
|
||||||
|
value: '4K|2160p|x2160',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '1080p',
|
||||||
|
value: '1080[pi]|x1080',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '720p',
|
||||||
|
value: '720[pi]|x720',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 特效选择框数据
|
||||||
|
const effectOptions = ref([
|
||||||
|
{
|
||||||
|
title: '全部',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '杜比视界',
|
||||||
|
value: 'Dolby[\\s.]+Vision|DOVI|[\\s.]+DV[\\s.]+',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '杜比全景声',
|
||||||
|
value: 'Dolby[\\s.]*\\+?Atmos|Atmos',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'HDR',
|
||||||
|
value: '[\\s.]+HDR[\\s.]+|HDR10|HDR10\\+',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'SDR',
|
||||||
|
value: '[\\s.]+SDR[\\s.]+',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 所有规则组列表
|
||||||
|
const filterRuleGroups = ref<FilterRuleGroup[]>([])
|
||||||
|
|
||||||
|
// 加载规则组
|
||||||
|
async function queryFilterRuleGroups() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get('system/setting/UserFilterRuleGroups')
|
||||||
|
filterRuleGroups.value = result.data?.value ?? []
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算过滤规则组选择框数据
|
||||||
|
const ruleGroupsOptions = computed(() => {
|
||||||
|
return filterRuleGroups.value.map(group => ({
|
||||||
|
title: group.name,
|
||||||
|
value: group.name,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
queryFilterRuleGroups()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VCard max-width="20rem">
|
||||||
|
<Handle id="edge_in" type="target" :position="Position.Left" />
|
||||||
|
<VCardItem>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<VAvatar>
|
||||||
|
<VIcon icon="mdi-filter-multiple" size="x-large"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VCardTitle>过滤资源</VCardTitle>
|
||||||
|
<VCardSubtitle>对资源列表数据进行过滤</VCardSubtitle>
|
||||||
|
</VCardItem>
|
||||||
|
<VDivider />
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="6">
|
||||||
|
<VSelect v-model="data.quality" label="质量" :items="qualityOptions" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="6">
|
||||||
|
<VSelect v-model="data.resolution" label="分辨率" :items="resolutionOptions" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="6">
|
||||||
|
<VSelect v-model="data.effect" label="特效" :items="effectOptions" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="6">
|
||||||
|
<VTextField v-model="data.size" label="大小范围" placeholder="MB" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField v-model="data.include" label="包含(关键字、正则式)" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField v-model="data.exclude" label="排除(关键字、正则式)" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VSelect
|
||||||
|
v-model="data.rule_groups"
|
||||||
|
chips
|
||||||
|
multiple
|
||||||
|
label="过滤规则组"
|
||||||
|
:items="ruleGroupsOptions"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
<Handle id="edge_out" type="source" :position="Position.Right" />
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
43
src/components/workflow/ScanFileAction.vue
Normal file
43
src/components/workflow/ScanFileAction.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
|
import { storageOptions } from '@/api/constants'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VCard max-width="20rem">
|
||||||
|
<Handle id="edge_in" type="target" :position="Position.Left" />
|
||||||
|
<VCardItem>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<VAvatar>
|
||||||
|
<VIcon icon="mdi-file-move" size="x-large"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VCardTitle>扫描目录</VCardTitle>
|
||||||
|
<VCardSubtitle>扫描目录文件到队列</VCardSubtitle>
|
||||||
|
</VCardItem>
|
||||||
|
<VDivider />
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VSelect v-model="data.storage" label="存储" :items="storageOptions" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VPathField v-model="data.directory" :storage="data.storage" label="目录" clearable />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
<Handle id="edge_out" type="source" :position="Position.Right" />
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
31
src/components/workflow/ScrapeFileAction.vue
Normal file
31
src/components/workflow/ScrapeFileAction.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VCard max-width="20rem">
|
||||||
|
<Handle id="edge_in" type="target" :position="Position.Left" />
|
||||||
|
<VCardItem>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<VAvatar>
|
||||||
|
<VIcon icon="mdi-file-find" size="x-large"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VCardTitle>刮削文件</VCardTitle>
|
||||||
|
<VCardSubtitle>刮削媒体信息和图片</VCardSubtitle>
|
||||||
|
</VCardItem>
|
||||||
|
<Handle id="edge_out" type="source" :position="Position.Right" />
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
31
src/components/workflow/SendEventAction.vue
Normal file
31
src/components/workflow/SendEventAction.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VCard max-width="20rem">
|
||||||
|
<Handle id="edge_in" type="target" :position="Position.Left" />
|
||||||
|
<VCardItem>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<VAvatar>
|
||||||
|
<VIcon icon="mdi-send-check" size="x-large"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VCardTitle>发送事件</VCardTitle>
|
||||||
|
<VCardSubtitle>发送任务执行事件</VCardSubtitle>
|
||||||
|
</VCardItem>
|
||||||
|
<Handle id="edge_out" type="source" :position="Position.Right" />
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
80
src/components/workflow/SendMessageAction.vue
Normal file
80
src/components/workflow/SendMessageAction.vue
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import api from '@/api'
|
||||||
|
import { NotificationConf } from '@/api/types'
|
||||||
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 所有消息渠道
|
||||||
|
const notifications = ref<NotificationConf[]>([])
|
||||||
|
|
||||||
|
// 调用API查询通知渠道设置
|
||||||
|
async function loadNotificationSetting() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get('system/setting/Notifications')
|
||||||
|
notifications.value = result.data?.value ?? []
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算消息渠道选项
|
||||||
|
const sourceOptions = computed(() => {
|
||||||
|
return notifications.value.map(item => {
|
||||||
|
return {
|
||||||
|
title: item.name,
|
||||||
|
value: item.name,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadNotificationSetting()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VCard max-width="20rem">
|
||||||
|
<Handle id="edge_in" type="target" :position="Position.Left" />
|
||||||
|
<VCardItem>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<VAvatar>
|
||||||
|
<VIcon icon="mdi-message-arrow-right" size="x-large"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VCardTitle>发送消息</VCardTitle>
|
||||||
|
<VCardSubtitle>发送任务执行消息</VCardSubtitle>
|
||||||
|
</VCardItem>
|
||||||
|
<VDivider />
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VSelect
|
||||||
|
v-model="data.client"
|
||||||
|
:items="sourceOptions"
|
||||||
|
label="消息渠道"
|
||||||
|
chips
|
||||||
|
multiple
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField v-model="data.userid" label="用户ID" chips multiple outlined dense clearable />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
<Handle id="edge_out" type="source" :position="Position.Right" />
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
51
src/components/workflow/TransferFileAction.vue
Normal file
51
src/components/workflow/TransferFileAction.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Handle, Position } from '@vue-flow/core'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 来源下拉框
|
||||||
|
const sourceOptions = ref([
|
||||||
|
{
|
||||||
|
title: '文件列表',
|
||||||
|
value: 'files',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '下载任务',
|
||||||
|
value: 'downloads',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VCard max-width="20rem">
|
||||||
|
<Handle id="edge_in" type="target" :position="Position.Left" />
|
||||||
|
<VCardItem>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<VAvatar>
|
||||||
|
<VIcon icon="mdi-file-move" size="x-large"></VIcon>
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VCardTitle>整理文件</VCardTitle>
|
||||||
|
<VCardSubtitle>整理重命名队列中的文件</VCardSubtitle>
|
||||||
|
</VCardItem>
|
||||||
|
<VDivider />
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VSelect v-model="data.source" label="来源" :items="sourceOptions" outlined dense />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
<Handle id="edge_out" type="source" :position="Position.Right" />
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -8,7 +8,7 @@ import UserNofification from '@/layouts/components/UserNotification.vue'
|
|||||||
import SearchBar from '@/layouts/components/SearchBar.vue'
|
import SearchBar from '@/layouts/components/SearchBar.vue'
|
||||||
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
|
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
|
||||||
import UserProfile from '@/layouts/components/UserProfile.vue'
|
import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||||
import store from '@/store'
|
import { useUserStore } from '@/stores'
|
||||||
import { SystemNavMenus } from '@/router/menu'
|
import { SystemNavMenus } from '@/router/menu'
|
||||||
import { NavMenu } from '@/@layouts/types'
|
import { NavMenu } from '@/@layouts/types'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
@@ -16,8 +16,11 @@ import { useDisplay } from 'vuetify'
|
|||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
const appMode = inject('pwaMode')
|
const appMode = inject('pwaMode')
|
||||||
|
|
||||||
|
// 用户 Store
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// 是否超级用户
|
// 是否超级用户
|
||||||
let superUser = store.state.auth.superUser
|
let superUser = userStore.superUser
|
||||||
|
|
||||||
// 开始菜单项
|
// 开始菜单项
|
||||||
const startMenus = ref<NavMenu[]>([])
|
const startMenus = ref<NavMenu[]>([])
|
||||||
@@ -64,7 +67,7 @@ onMounted(() => {
|
|||||||
<VIcon icon="mdi-menu" />
|
<VIcon icon="mdi-menu" />
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
<!-- 👉 Back Button -->
|
<!-- 👉 Back Button -->
|
||||||
<IconBtn v-if="appMode && display.mdAndDown.value" class="ms-n2" @click="goBack">
|
<IconBtn v-if="appMode" class="ms-n2" @click="goBack">
|
||||||
<VIcon icon="mdi-arrow-left" size="32" />
|
<VIcon icon="mdi-arrow-left" size="32" />
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
<!-- 👉 Search Bar -->
|
<!-- 👉 Search Bar -->
|
||||||
|
|||||||
12
src/layouts/components/DropzoneBackground.vue
Normal file
12
src/layouts/components/DropzoneBackground.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Background } from '@vue-flow/background'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="dropzone-background">
|
||||||
|
<Background :size="2" :gap="20" pattern-color="#BDBDBD" />
|
||||||
|
<div class="overlay">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,38 +1,49 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { SystemNavMenus } from '@/router/menu'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
import { VMenu } from 'vuetify/lib/components/index.mjs'
|
||||||
|
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
const appMode = inject('pwaMode') && display.mdAndDown.value
|
const appMode = inject('pwaMode') && display.mdAndDown.value
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
// 各按钮活动状态
|
const moreMenuDialog = ref(false)
|
||||||
|
|
||||||
|
const moreMemus = computed(() => SystemNavMenus.filter(menu => !menu.footer))
|
||||||
|
|
||||||
const activeState = computed(() => {
|
const activeState = computed(() => {
|
||||||
return {
|
return {
|
||||||
home: route.path === '/dashboard',
|
home: route.path === '/dashboard',
|
||||||
ranking: route.path === '/ranking',
|
recommend: route.path === '/recommend',
|
||||||
movie: route.path === '/subscribe/movie',
|
movie: route.path === '/subscribe/movie',
|
||||||
tv: route.path === '/subscribe/tv',
|
tv: route.path === '/subscribe/tv',
|
||||||
apps: route.path === '/apps',
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const moreActiveState = computed(() => {
|
||||||
|
return !Object.values(activeState.value).some(v => v)
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentPath = computed(() => route.path)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="appMode" class="w-100" style="block-size: calc(3.5rem + env(safe-area-inset-bottom))">
|
<div v-if="appMode" class="w-100">
|
||||||
<VBottomNavigation
|
<VBottomNavigation
|
||||||
grow
|
grow
|
||||||
horizontal
|
horizontal
|
||||||
color="primary"
|
color="primary"
|
||||||
class="footer-nav border-t"
|
class="footer-nav border-t"
|
||||||
style="block-size: calc(3.5rem + env(safe-area-inset-bottom))"
|
style="block-size: calc(3.5rem + env(safe-area-inset-bottom))"
|
||||||
|
:z-index="9998"
|
||||||
>
|
>
|
||||||
<VBtn to="/dashboard" :ripple="false">
|
<VBtn to="/dashboard" :ripple="false">
|
||||||
<VIcon v-if="activeState.home" size="28">mdi-home</VIcon>
|
<VIcon v-if="activeState.home" size="28">mdi-home</VIcon>
|
||||||
<VIcon v-else size="28">mdi-home-outline</VIcon>
|
<VIcon v-else size="28">mdi-home-outline</VIcon>
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn to="/ranking" :ripple="false">
|
<VBtn to="/recommend" :ripple="false">
|
||||||
<VIcon v-if="activeState.ranking" size="28">mdi-star</VIcon>
|
<VIcon v-if="activeState.recommend" size="28">mdi-star</VIcon>
|
||||||
<VIcon v-else size="28">mdi-star-outline</VIcon>
|
<VIcon v-else size="28">mdi-star-outline</VIcon>
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn to="/subscribe/movie" :ripple="false">
|
<VBtn to="/subscribe/movie" :ripple="false">
|
||||||
@@ -43,9 +54,31 @@ const activeState = computed(() => {
|
|||||||
<VIcon v-if="activeState.tv" size="28">mdi-television-play</VIcon>
|
<VIcon v-if="activeState.tv" size="28">mdi-television-play</VIcon>
|
||||||
<VIcon v-else size="28">mdi-television</VIcon>
|
<VIcon v-else size="28">mdi-television</VIcon>
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn to="/apps" :ripple="false">
|
<VBtn :ripple="false">
|
||||||
<VIcon v-if="activeState.apps" size="28">mdi-dots-horizontal-circle</VIcon>
|
<VIcon
|
||||||
<VIcon v-else size="28">mdi-dots-horizontal</VIcon>
|
size="28"
|
||||||
|
:icon="moreMenuDialog ? 'mdi-close' : 'mdi-dots-horizontal'"
|
||||||
|
:color="moreActiveState ? 'primary' : ''"
|
||||||
|
/>
|
||||||
|
<VMenu v-model="moreMenuDialog" close-on-content-click activator="parent">
|
||||||
|
<VDivider />
|
||||||
|
<VList class="font-bold" lines="one">
|
||||||
|
<VListSubheader class="bg-transparent"> 更多 </VListSubheader>
|
||||||
|
<VListItem
|
||||||
|
class="pe-20"
|
||||||
|
v-for="(menu, index) in moreMemus"
|
||||||
|
:key="index"
|
||||||
|
:prepend-icon="menu.icon"
|
||||||
|
nav
|
||||||
|
:to="menu.to"
|
||||||
|
:base-color="currentPath === menu.to ? 'primary' : undefined"
|
||||||
|
>
|
||||||
|
<VListItemTitle>
|
||||||
|
<span class="text-lg">{{ menu.title }}</span>
|
||||||
|
</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</VBottomNavigation>
|
</VBottomNavigation>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,4 +97,3 @@ const activeState = computed(() => {
|
|||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useStore } from 'vuex'
|
|
||||||
import { useConfirm } from 'vuetify-use-dialog'
|
import { useConfirm } from 'vuetify-use-dialog'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import avatar1 from '@images/avatars/avatar-1.png'
|
import avatar1 from '@images/avatars/avatar-1.png'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
|
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
|
||||||
|
import UserAuthDialog from '@/components/dialog/UserAuthDialog.vue'
|
||||||
|
import { useAuthStore, useUserStore } from '@/stores'
|
||||||
|
|
||||||
// Vuex Store
|
// 认证 Store
|
||||||
const store = useStore()
|
const authStore = useAuthStore()
|
||||||
|
// 用户 Store
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// 确认框
|
// 确认框
|
||||||
const createConfirm = useConfirm()
|
const createConfirm = useConfirm()
|
||||||
@@ -19,23 +22,24 @@ const $toast = useToast()
|
|||||||
// 进度框
|
// 进度框
|
||||||
const progressDialog = ref(false)
|
const progressDialog = ref(false)
|
||||||
|
|
||||||
|
// 站点认证对话框
|
||||||
|
const siteAuthDialog = ref(false)
|
||||||
|
|
||||||
|
// 重启确认对话框
|
||||||
|
const restartDialog = ref(false)
|
||||||
|
|
||||||
// 执行注销操作
|
// 执行注销操作
|
||||||
function logout() {
|
function logout() {
|
||||||
// 清除登录状态信息
|
// 清除登录状态信息
|
||||||
store.dispatch('auth/logout')
|
authStore.logout()
|
||||||
// 重定向到登录页面或其他适当的页面
|
// 重定向到登录页面或其他适当的页面
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行重启操作
|
// 执行重启操作
|
||||||
async function restart() {
|
async function restart() {
|
||||||
// 弹出提示
|
{
|
||||||
const confirmed = await createConfirm({
|
restartDialog.value = false
|
||||||
title: '确认',
|
|
||||||
content: '确认重启系统吗?',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (confirmed) {
|
|
||||||
// 调用API重启
|
// 调用API重启
|
||||||
try {
|
try {
|
||||||
// 显示等待框
|
// 显示等待框
|
||||||
@@ -56,10 +60,27 @@ async function restart() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从Vuex Store中获取信息
|
// 显示重启确认对话框
|
||||||
const superUser = computed(() => store.state.auth.superUser)
|
async function showRestartDialog() {
|
||||||
const userName = computed(() => store.state.auth.userName)
|
restartDialog.value = true
|
||||||
const avatar = computed(() => store.state.auth.avatar || avatar1)
|
}
|
||||||
|
|
||||||
|
// 显示站点认证对话框
|
||||||
|
function showSiteAuthDialog() {
|
||||||
|
siteAuthDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户站点认证成功
|
||||||
|
function siteAuthDone() {
|
||||||
|
siteAuthDialog.value = false
|
||||||
|
logout()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从用户 Store中获取信息
|
||||||
|
const superUser = computed(() => userStore.superUser)
|
||||||
|
const userName = computed(() => userStore.userName)
|
||||||
|
const avatar = computed(() => userStore.avatar || avatar1)
|
||||||
|
const userLevel = computed(() => userStore.level)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -94,6 +115,21 @@ const avatar = computed(() => store.state.auth.avatar || avatar1)
|
|||||||
<VListItemTitle>个人信息</VListItemTitle>
|
<VListItemTitle>个人信息</VListItemTitle>
|
||||||
</VListItem>
|
</VListItem>
|
||||||
|
|
||||||
|
<VListItem link @click="router.push('/apps')">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon class="me-2" icon="mdi-view-grid-outline" size="22" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>功能视图</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
|
||||||
|
<!-- 👉 Site Auth -->
|
||||||
|
<VListItem v-if="userLevel < 2 && superUser" link @click="showSiteAuthDialog">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon class="me-2" icon="mdi-lock-check-outline" size="22" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>用户认证</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
|
||||||
<!-- 👉 FAQ -->
|
<!-- 👉 FAQ -->
|
||||||
<VListItem href="https://wiki.movie-pilot.org" target="_blank">
|
<VListItem href="https://wiki.movie-pilot.org" target="_blank">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
@@ -106,7 +142,7 @@ const avatar = computed(() => store.state.auth.avatar || avatar1)
|
|||||||
<VDivider v-if="superUser" class="my-2" />
|
<VDivider v-if="superUser" class="my-2" />
|
||||||
|
|
||||||
<!-- 👉 restart -->
|
<!-- 👉 restart -->
|
||||||
<VListItem v-if="superUser" @click="restart">
|
<VListItem v-if="superUser" @click="showRestartDialog">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon class="me-2" icon="mdi-restart" size="22" />
|
<VIcon class="me-2" icon="mdi-restart" size="22" />
|
||||||
</template>
|
</template>
|
||||||
@@ -126,4 +162,27 @@ const avatar = computed(() => store.state.auth.avatar || avatar1)
|
|||||||
</VAvatar>
|
</VAvatar>
|
||||||
<!-- 重启进度框 -->
|
<!-- 重启进度框 -->
|
||||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" text="正在重启 ..." />
|
<ProgressDialog v-if="progressDialog" v-model="progressDialog" text="正在重启 ..." />
|
||||||
|
<!-- 用户认证对话框 -->
|
||||||
|
<UserAuthDialog v-if="siteAuthDialog" v-model="siteAuthDialog" @done="siteAuthDone" @close="siteAuthDialog = false" />
|
||||||
|
<!-- 重启确认对话框 -->
|
||||||
|
<VDialog v-if="restartDialog" v-model="restartDialog" max-width="25rem">
|
||||||
|
<VCard>
|
||||||
|
<VCardItem>
|
||||||
|
<div class="flex items-center justify-center mt-3">
|
||||||
|
<VAvatar color="warning" variant="text" size="x-large">
|
||||||
|
<VIcon size="x-large" icon="mdi-alert" />
|
||||||
|
</VAvatar>
|
||||||
|
<div class="ms-3">
|
||||||
|
<p class="font-bold text-xl text-high-emphasis">确认重启系统吗?</p>
|
||||||
|
<p>重启后,您将被注销并需要重新登录。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCardItem>
|
||||||
|
<VCardActions class="mx-auto">
|
||||||
|
<VBtn variant="elevated" color="error" @click="restart" prepend-icon="mdi-restart" class="px-5"> 确定 </VBtn>
|
||||||
|
<VBtn variant="tonal" color="secondary" class="px-5" @click="restartDialog = false">取消</VBtn>
|
||||||
|
</VCardActions>
|
||||||
|
<DialogCloseBtn @click="restartDialog = false" />
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
40
src/layouts/components/WorkflowSidebar.vue
Normal file
40
src/layouts/components/WorkflowSidebar.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import api from '@/api'
|
||||||
|
import useDragAndDrop from '@core/utils/workflow'
|
||||||
|
|
||||||
|
const { onDragStart } = useDragAndDrop()
|
||||||
|
|
||||||
|
// 组件列表
|
||||||
|
const actions = ref([])
|
||||||
|
|
||||||
|
// 加载组件列表
|
||||||
|
async function load_actions() {
|
||||||
|
try {
|
||||||
|
actions.value = await api.get('workflow/actions')
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
load_actions()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<aside>
|
||||||
|
<div class="mb-3"><VLabel>可选动作组件:</VLabel></div>
|
||||||
|
|
||||||
|
<div class="nodes flex flex-wrap justify-center">
|
||||||
|
<div
|
||||||
|
class="vue-flow__node-default cursor-grab mx-1"
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="index"
|
||||||
|
:draggable="true"
|
||||||
|
@dragstart="onDragStart($event, action)"
|
||||||
|
>
|
||||||
|
{{ action['name'] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user