Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fdbc8104c | ||
|
|
433c14679c | ||
|
|
fcaa4476f0 | ||
|
|
85c5c3058c | ||
|
|
035122a08e | ||
|
|
0a76875f8e | ||
|
|
218eac54ce | ||
|
|
84deeff4f5 | ||
|
|
0c72d026f6 | ||
|
|
aec9ea83c5 | ||
|
|
effd13aedd | ||
|
|
42b43d65d7 | ||
|
|
c501d824dd | ||
|
|
384ac2faf1 | ||
|
|
dd2c4dd24b | ||
|
|
356ffddb1c | ||
|
|
de69be7c4e | ||
|
|
e962f555ae | ||
|
|
1987246585 | ||
|
|
393264f66b | ||
|
|
9b50020b3b | ||
|
|
5e5545fe01 |
@@ -1,2 +1,2 @@
|
|||||||
VITE_API_BASE_URL=/api/v1/
|
VITE_API_BASE_URL=http://localhost:3001/api/v1/
|
||||||
VITE_PUBLIC_VAPID_KEY=BH3w49sZA6jXUnE-yt4jO6VKh73lsdsvwoJ6Hx7fmPIDKoqGiUl2GEoZzy-iJfn4SfQQcx7yQdHf9RknwrL_lSM
|
VITE_PUBLIC_VAPID_KEY=BH3w49sZA6jXUnE-yt4jO6VKh73lsdsvwoJ6Hx7fmPIDKoqGiUl2GEoZzy-iJfn4SfQQcx7yQdHf9RknwrL_lSM
|
||||||
|
|||||||
45
.github/ISSUE_TEMPLATE/rfc.yml
vendored
@@ -1,45 +0,0 @@
|
|||||||
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
|
|
||||||
16
.github/workflows/build.yml
vendored
@@ -1,12 +1,12 @@
|
|||||||
name: Build Moviepilot-Frontend v2
|
name: Build Moviepilot-Frontend
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- v2
|
- main
|
||||||
paths:
|
paths:
|
||||||
- 'package.json'
|
- package.json
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -42,21 +42,13 @@ jobs:
|
|||||||
echo "$frontend_version" > dist/version.txt
|
echo "$frontend_version" > dist/version.txt
|
||||||
zip -r dist.zip dist
|
zip -r dist.zip dist
|
||||||
|
|
||||||
- name: Delete Release
|
|
||||||
uses: dev-drprasad/delete-tag-and-release@v1.1
|
|
||||||
with:
|
|
||||||
tag_name: ${{ env.frontend_version }}
|
|
||||||
delete_release: true
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Generate Release
|
- name: Generate Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.frontend_version }}
|
tag_name: ${{ env.frontend_version }}
|
||||||
name: ${{ env.frontend_version }}
|
name: ${{ env.frontend_version }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
make_latest: false
|
|
||||||
files: |
|
files: |
|
||||||
dist.zip
|
dist.zip
|
||||||
env:
|
env:
|
||||||
|
|||||||
329
auto-imports.d.ts
vendored
@@ -3,7 +3,6 @@
|
|||||||
// @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']
|
||||||
@@ -67,7 +66,6 @@ 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']
|
||||||
@@ -79,7 +77,6 @@ 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']
|
||||||
@@ -193,7 +190,6 @@ 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']
|
||||||
@@ -213,7 +209,6 @@ 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']
|
||||||
@@ -239,7 +234,6 @@ 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']
|
||||||
@@ -248,7 +242,6 @@ 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']
|
||||||
@@ -268,7 +261,6 @@ declare global {
|
|||||||
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']
|
||||||
@@ -321,10 +313,9 @@ declare global {
|
|||||||
// for type re-export
|
// for type re-export
|
||||||
declare global {
|
declare global {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, 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' {
|
||||||
@@ -391,7 +382,6 @@ 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']>
|
||||||
@@ -403,7 +393,6 @@ 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']>
|
||||||
@@ -517,7 +506,6 @@ 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']>
|
||||||
@@ -537,7 +525,6 @@ 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']>
|
||||||
@@ -563,7 +550,6 @@ 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']>
|
||||||
@@ -572,7 +558,6 @@ 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']>
|
||||||
@@ -592,7 +577,6 @@ declare module 'vue' {
|
|||||||
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']>
|
||||||
@@ -642,4 +626,313 @@ 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
@@ -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']
|
||||||
|
|||||||
26
index.html
@@ -1,6 +1,5 @@
|
|||||||
<!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">
|
||||||
@@ -30,17 +29,10 @@
|
|||||||
<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>
|
<link rel="preload" href="index.js" as="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 style="margin: 0;">
|
<body>
|
||||||
<div id="loading-bg">
|
<div id="loading-bg">
|
||||||
<div class="loading-logo">
|
<div class="loading-logo">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
@@ -155,6 +147,16 @@
|
|||||||
</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>
|
||||||
|
|||||||
104
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "moviepilot",
|
"name": "moviepilot",
|
||||||
"version": "2.2.7",
|
"version": "1.9.17",
|
||||||
"private": true,
|
"private": true,
|
||||||
"bin": "dist/service.js",
|
"bin": "dist/service.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -19,86 +19,86 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fullcalendar/core": "^6.1.15",
|
"@fullcalendar/core": "^6.1.8",
|
||||||
"@fullcalendar/daygrid": "^6.1.15",
|
"@fullcalendar/daygrid": "^6.1.8",
|
||||||
"@fullcalendar/interaction": "^6.1.15",
|
"@fullcalendar/interaction": "^6.1.7",
|
||||||
"@fullcalendar/list": "^6.1.15",
|
"@fullcalendar/list": "^6.1.7",
|
||||||
"@fullcalendar/timegrid": "^6.1.15",
|
"@fullcalendar/timegrid": "^6.1.7",
|
||||||
"@fullcalendar/vue3": "^6.1.15",
|
"@fullcalendar/vue3": "^6.1.8",
|
||||||
"@iconify/utils": "^2.2.1",
|
"@iconify/utils": "^2.1.22",
|
||||||
"@vue-js-cron/vuetify": "^5.0.9",
|
"@vueuse/core": "^10.1.2",
|
||||||
"@vueuse/core": "^12.4.0",
|
"@vueuse/math": "^10.1.2",
|
||||||
"@vueuse/math": "^12.4.0",
|
"ace-builds": "^1.32.6",
|
||||||
"ace-builds": "^1.37.4",
|
"apexcharts-clevision": "^3.28.5",
|
||||||
"apexcharts": "^4.0.0",
|
"axios": "1.6.8",
|
||||||
"axios": "^1.7.9",
|
"colorthief": "^2.4.0",
|
||||||
"colorthief": "^2.6.0",
|
"dayjs": "^1.11.10",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"express": "^4.18.2",
|
||||||
"dayjs": "^1.11.13",
|
"express-http-proxy": "^2.0.0",
|
||||||
"express": "^4.21.2",
|
|
||||||
"express-http-proxy": "^2.1.1",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"qrcode.vue": "^3.6.0",
|
"qrcode.vue": "^3.4.1",
|
||||||
"sass": "^1.83.4",
|
"sass": "^1.59.3",
|
||||||
"tailwindcss": "^ 3.4.17",
|
"tailwindcss": "^3.3.2",
|
||||||
"vue": "^3.5.13",
|
"unplugin-vue-define-options": "^1.3.5",
|
||||||
"vue-router": "^4.5.0",
|
"vue": "^3.3.2",
|
||||||
"vue-toast-notification": "^3.1.3",
|
"vue-router": "^4.2.0",
|
||||||
|
"vue-toast-notification": "^3",
|
||||||
"vue3-ace-editor": "^2.2.4",
|
"vue3-ace-editor": "^2.2.4",
|
||||||
"vue3-apexcharts": "^1.8.0",
|
"vue3-apexcharts": "^1.4.1",
|
||||||
"vue3-perfect-scrollbar": "^2.0.0",
|
"vue3-perfect-scrollbar": "^2.0.0",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "^4.1.0",
|
||||||
"vuetify": "3.7.3",
|
"vuetify": "3.6.8",
|
||||||
"vuetify-use-dialog": "^0.6.11",
|
"vuetify-use-dialog": "^0.6.11",
|
||||||
"vuex": "^4.1.0",
|
"vuex": "^4.1.0",
|
||||||
"vuex-persistedstate": "^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.3.0",
|
"@iconify/vue": "4.1.1",
|
||||||
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||||
"@types/lodash": "^4.14.197",
|
"@types/lodash": "^4.14.197",
|
||||||
"@types/node": "^20.1.4",
|
"@types/node": "^20.1.4",
|
||||||
"@types/webfontloader": "^1.6.34",
|
"@types/webfontloader": "^1.6.34",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.20.0",
|
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||||
"@typescript-eslint/parser": "^8.20.0",
|
"@typescript-eslint/parser": "^7.5.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.0.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": "^7.2.1",
|
"eslint-plugin-promise": "^6.0.1",
|
||||||
"eslint-plugin-regex": "^1.10.0",
|
"eslint-plugin-regex": "^1.10.0",
|
||||||
"eslint-plugin-sonarjs": "^3.0.1",
|
"eslint-plugin-sonarjs": "^0.25.1",
|
||||||
"eslint-plugin-unicorn": "^56.0.1",
|
"eslint-plugin-unicorn": "^52.0.0",
|
||||||
"eslint-plugin-vue": "^9.12.0",
|
"eslint-plugin-vue": "^9.12.0",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "8",
|
||||||
"postcss-html": "^1.5.0",
|
"postcss-html": "^1.5.0",
|
||||||
"stylelint": "^16.13.2",
|
"stylelint": "16.3.1",
|
||||||
"stylelint-config-idiomatic-order": "^10.0.0",
|
"stylelint-config-idiomatic-order": "10.0.0",
|
||||||
"stylelint-config-standard-scss": "^14.0.0",
|
"stylelint-config-standard-scss": "13.1.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": "^19.0.0",
|
"unplugin-auto-import": "^0.17.5",
|
||||||
"unplugin-vue-components": "^28.0.0",
|
"unplugin-vue-components": "^0.26.0",
|
||||||
"unplugin-vue-define-options": "^1.5.3",
|
"vite": "^5.2.8",
|
||||||
"vite": "^5.4.11",
|
|
||||||
"vite-plugin-pages": "^0.32.1",
|
"vite-plugin-pages": "^0.32.1",
|
||||||
"vite-plugin-pwa": "^0.21.1",
|
"vite-plugin-pwa": "^0.20.0",
|
||||||
"vite-plugin-vue-layouts": "^0.11.0",
|
"vite-plugin-vue-layouts": "^0.11.0",
|
||||||
"vite-plugin-vuetify": "2.0.4",
|
"vite-plugin-vuetify": "2.0.3",
|
||||||
"vue-shepherd": "^4.1.0",
|
"vue-shepherd": "^3.0.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,6 +1,16 @@
|
|||||||
|
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: fixed;
|
position: absolute;
|
||||||
z-index: 9999;
|
z-index: 999;
|
||||||
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 { saveLocalTheme } from '../utils/theme'
|
import { VAceEditor } from 'vue3-ace-editor'
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
@@ -103,7 +103,8 @@ function updateTheme() {
|
|||||||
savedTheme.value = theme
|
savedTheme.value = theme
|
||||||
themeTransition()
|
themeTransition()
|
||||||
// 保存主题到本地
|
// 保存主题到本地
|
||||||
saveLocalTheme(theme, globalTheme)
|
localStorage.setItem('theme', theme)
|
||||||
|
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换主题
|
// 切换主题
|
||||||
@@ -113,8 +114,10 @@ function changeTheme(theme: string) {
|
|||||||
currentThemeName.value = nextTheme
|
currentThemeName.value = nextTheme
|
||||||
// 保存主题到服务端
|
// 保存主题到服务端
|
||||||
try {
|
try {
|
||||||
api.post('/user/config/Layout', {
|
api.post('/user/config/theme', nextTheme, {
|
||||||
theme: nextTheme,
|
headers: {
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存主题到服务端失败')
|
console.error('保存主题到服务端失败')
|
||||||
@@ -175,7 +178,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 到服务端失败')
|
||||||
}
|
}
|
||||||
@@ -209,7 +212,7 @@ onMounted(() => {
|
|||||||
</VList>
|
</VList>
|
||||||
</VMenu>
|
</VMenu>
|
||||||
<!-- 自定义 CSS -- -->
|
<!-- 自定义 CSS -- -->
|
||||||
<VDialog v-if="cssDialog" v-model="cssDialog" persistent max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
<VDialog 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 />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.auth-wrapper {
|
.auth-wrapper {
|
||||||
min-block-size: calc(var(--vh, 1vh) * 100 + env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
min-block-size: calc(var(--vh, 1vh) * 100 + env(safe-area-inset-top));
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-footer-mask {
|
.auth-footer-mask {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
@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,7 +92,8 @@
|
|||||||
.fc-header-toolbar {
|
.fc-header-toolbar {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin: 1.25rem;
|
margin: 1.25rem;
|
||||||
gap: 1rem 0.5rem;
|
column-gap: 0.5rem;
|
||||||
|
row-gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-toolbar-chunk {
|
.fc-toolbar-chunk {
|
||||||
@@ -237,7 +238,7 @@
|
|||||||
inline-size: 1.5625rem;
|
inline-size: 1.5625rem;
|
||||||
margin-inline-end: 0.25rem;
|
margin-inline-end: 0.25rem;
|
||||||
|
|
||||||
@media (width <= 1264px) {
|
@media (max-width: 1264px) {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export function useDefer(maxFrameCount = 1) {
|
|||||||
const refreshFrameCount = () => {
|
const refreshFrameCount = () => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
frameCount.value++
|
frameCount.value++
|
||||||
if (frameCount.value < maxFrameCount) refreshFrameCount()
|
if (frameCount.value < maxFrameCount)
|
||||||
|
refreshFrameCount()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
refreshFrameCount()
|
refreshFrameCount()
|
||||||
@@ -18,9 +19,3 @@ export function useDefer(maxFrameCount = 1) {
|
|||||||
return frameCount.value >= showInFrameCount
|
return frameCount.value >= showInFrameCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureRenderComplete(callback: () => void) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
requestAnimationFrame(callback)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -60,25 +60,19 @@ export const prefixWithPlus = (value: number) => (value > 0 ? `+${value}` : valu
|
|||||||
export const formatSeason = (value: string) => (value ? `S${value.padStart(2, '0')}` : '')
|
export const formatSeason = (value: string) => (value ? `S${value.padStart(2, '0')}` : '')
|
||||||
|
|
||||||
// 格式化为xx[TGMK]B
|
// 格式化为xx[TGMK]B
|
||||||
export function formatFileSize(bytes: number, decimals = 2, prefix = false) {
|
export function formatFileSize(bytes: number) {
|
||||||
// 负数标记
|
if (bytes < 0) throw new Error('字节数不能为负数。')
|
||||||
let negative = false
|
|
||||||
let size = bytes
|
|
||||||
if (bytes < 0) {
|
|
||||||
negative = true
|
|
||||||
size = Math.abs(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
let size = bytes
|
||||||
let unitIndex = 0
|
let unitIndex = 0
|
||||||
|
|
||||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
size /= 1024
|
size /= 1024
|
||||||
unitIndex++
|
unitIndex++
|
||||||
}
|
}
|
||||||
if (negative) return `-${size.toFixed(decimals)} ${units[unitIndex]}`
|
|
||||||
else
|
return `${size.toFixed(2)} ${units[unitIndex]}`
|
||||||
return prefix ? `+${size.toFixed(decimals)} ${units[unitIndex]}` : `${size.toFixed(decimals)} ${units[unitIndex]}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将时间秒格式化为时分秒
|
// 将时间秒格式化为时分秒
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import copy from 'copy-to-clipboard'
|
|
||||||
|
|
||||||
// 请求和获取剪贴板内容
|
// 请求和获取剪贴板内容
|
||||||
export async function getClipboardContent() {
|
export async function getClipboardContent() {
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
return await navigator.clipboard.readText()
|
return await navigator.clipboard.readText()
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
const input = document.createElement('textarea')
|
const input = document.createElement('textarea')
|
||||||
document.body.appendChild(input)
|
document.body.appendChild(input)
|
||||||
input.select()
|
input.select()
|
||||||
@@ -15,10 +14,19 @@ export async function getClipboardContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将内容复制到剪贴板
|
// 将内容复制到剪贴板,兼容非安全域场景
|
||||||
export async function copyToClipboard(content: string) {
|
export async function copyToClipboard(content: string) {
|
||||||
const success = copy(content)
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
return success
|
await navigator.clipboard.writeText(content)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const input = document.createElement('textarea')
|
||||||
|
input.value = content
|
||||||
|
document.body.appendChild(input)
|
||||||
|
input.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(input)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VAPID公钥转Uint8Array
|
// VAPID公钥转Uint8Array
|
||||||
@@ -34,12 +42,3 @@ export function urlBase64ToUint8Array(base64String: string) {
|
|||||||
}
|
}
|
||||||
return outputArray
|
return outputArray
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否为PWA
|
|
||||||
export const isPWA = async (): Promise<boolean> => {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations()
|
|
||||||
return registrations.length > 0
|
|
||||||
}
|
|
||||||
return (window.navigator as any).standalone === true
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"root":["./build-icons.ts"],"version":"5.7.3"}
|
|
||||||
@@ -53,8 +53,8 @@ function handleNavScroll(evt: Event) {
|
|||||||
<RouterLink to="/" class="app-logo d-flex align-center app-title-wrapper">
|
<RouterLink to="/" class="app-logo d-flex align-center app-title-wrapper">
|
||||||
<div class="d-flex" v-html="logo" />
|
<div class="d-flex" v-html="logo" />
|
||||||
|
|
||||||
<h1 class="font-weight-bold leading-normal text-xl">
|
<h1 class="font-weight-bold leading-normal text-2xl">
|
||||||
MOVIEPILOT <span class="text-sm text-gray-500">v2</span>
|
MOVIEPILOT
|
||||||
</h1>
|
</h1>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</slot>
|
</slot>
|
||||||
|
|||||||
@@ -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) + env(safe-area-inset-bottom));
|
min-height: calc(100% + env(safe-area-inset-top));
|
||||||
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) + env(safe-area-inset-bottom))
|
min-height: calc(100% + env(safe-area-inset-top))
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/@layouts/types.d.ts
vendored
@@ -122,9 +122,8 @@ export interface NavLink extends NavLinkProps, Partial<AclProperties> {
|
|||||||
|
|
||||||
export interface NavMenu extends NavLink {
|
export interface NavMenu extends NavLink {
|
||||||
header: string
|
header: string
|
||||||
|
admin: boolean
|
||||||
description?: string
|
description?: string
|
||||||
admin?: boolean
|
|
||||||
footer?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 👉 Vertical nav group
|
// 👉 Vertical nav group
|
||||||
|
|||||||
33
src/App.vue
@@ -1,16 +1,15 @@
|
|||||||
<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()
|
||||||
|
|
||||||
// 生效主题
|
// 生效主题
|
||||||
const { global: globalTheme } = useTheme()
|
async function setTheme() {
|
||||||
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 {
|
||||||
@@ -42,24 +41,14 @@ if (window.Apex) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// 页面加载时,加载当前用户数据
|
||||||
ensureRenderComplete(() => {
|
onBeforeMount(async () => {
|
||||||
nextTick(() => {
|
setTheme()
|
||||||
setTimeout(() => {
|
|
||||||
// 移除加载动画
|
|
||||||
removeEl('#loading-bg')
|
|
||||||
// 将background属性从html的style中移除
|
|
||||||
document.documentElement.style.removeProperty('background')
|
|
||||||
// 显示页面
|
|
||||||
show.value = true
|
|
||||||
}, 1500)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VApp v-show="show">
|
<VApp>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</VApp>
|
</VApp>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import modeYamlUrl from 'ace-builds/src-noconflict/mode-yaml?url'
|
|||||||
|
|
||||||
import modeCssUrl from 'ace-builds/src-noconflict/mode-css?url'
|
import modeCssUrl from 'ace-builds/src-noconflict/mode-css?url'
|
||||||
|
|
||||||
import modeIniUrl from 'ace-builds/src-noconflict/mode-ini?url'
|
import modePythonUrl from 'ace-builds/src-noconflict/mode-python?url'
|
||||||
|
|
||||||
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url'
|
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url'
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ import snippetsJsonUrl from 'ace-builds/src-noconflict/snippets/json?url'
|
|||||||
|
|
||||||
import snippertsCssUrl from 'ace-builds/src-noconflict/snippets/css?url'
|
import snippertsCssUrl from 'ace-builds/src-noconflict/snippets/css?url'
|
||||||
|
|
||||||
import snippertsIniUrl from 'ace-builds/src-noconflict/snippets/ini?url'
|
import snippetsPythonUrl from 'ace-builds/src-noconflict/snippets/python?url'
|
||||||
|
|
||||||
import 'ace-builds/src-noconflict/ext-language_tools'
|
import 'ace-builds/src-noconflict/ext-language_tools'
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ ace.config.setModuleUrl('ace/mode/javascript', modeJavascriptUrl)
|
|||||||
ace.config.setModuleUrl('ace/mode/html', modeHtmlUrl)
|
ace.config.setModuleUrl('ace/mode/html', modeHtmlUrl)
|
||||||
ace.config.setModuleUrl('ace/mode/yaml', modeYamlUrl)
|
ace.config.setModuleUrl('ace/mode/yaml', modeYamlUrl)
|
||||||
ace.config.setModuleUrl('ace/mode/css', modeCssUrl)
|
ace.config.setModuleUrl('ace/mode/css', modeCssUrl)
|
||||||
ace.config.setModuleUrl('ace/mode/ini', modeIniUrl)
|
ace.config.setModuleUrl('ace/mode/python', modePythonUrl)
|
||||||
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl)
|
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl)
|
||||||
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
|
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
|
||||||
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
|
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
|
||||||
@@ -64,6 +64,6 @@ ace.config.setModuleUrl('ace/snippets/javascript', snippetsJsUrl)
|
|||||||
ace.config.setModuleUrl('ace/snippets/javascript', snippetsYamlUrl)
|
ace.config.setModuleUrl('ace/snippets/javascript', snippetsYamlUrl)
|
||||||
ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl)
|
ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl)
|
||||||
ace.config.setModuleUrl('ace/snippets/css', snippertsCssUrl)
|
ace.config.setModuleUrl('ace/snippets/css', snippertsCssUrl)
|
||||||
ace.config.setModuleUrl('ace/snippets/ini', snippertsIniUrl)
|
ace.config.setModuleUrl('ace/snippets/python', snippetsPythonUrl)
|
||||||
|
|
||||||
ace.require('ace/ext/language_tools')
|
ace.require('ace/ext/language_tools')
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
export const storageOptions = [
|
|
||||||
{
|
|
||||||
title: '本地',
|
|
||||||
value: 'local',
|
|
||||||
icon: 'mdi-folder-multiple-outline',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '阿里云盘',
|
|
||||||
value: 'alipan',
|
|
||||||
icon: 'mdi-cloud-outline',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '115网盘',
|
|
||||||
value: 'u115',
|
|
||||||
icon: 'mdi-cloud-outline',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'RClone',
|
|
||||||
value: 'rclone',
|
|
||||||
icon: 'mdi-cloud-outline',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'AList',
|
|
||||||
value: 'alist',
|
|
||||||
icon: 'mdi-cloud-outline',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const innerFilterRules = [
|
|
||||||
{ title: '特效字幕', value: ' SPECSUB ' },
|
|
||||||
{ title: '中文字幕', value: ' CNSUB ' },
|
|
||||||
{ title: '国语配音', value: ' CNVOI ' },
|
|
||||||
{ title: '官种', value: ' GZ ' },
|
|
||||||
{ title: '排除: 国语配音', value: ' !CNVOI ' },
|
|
||||||
{ title: '粤语配音', value: ' HKVOI ' },
|
|
||||||
{ title: '排除: 粤语配音', value: ' !HKVOI ' },
|
|
||||||
{ title: '促销: 免费', value: ' FREE ' },
|
|
||||||
{ title: '分辨率: 4K', value: ' 4K ' },
|
|
||||||
{ title: '分辨率: 1080P', value: ' 1080P ' },
|
|
||||||
{ title: '分辨率: 720P', value: ' 720P ' },
|
|
||||||
{ title: '排除: 720P', value: ' !720P ' },
|
|
||||||
{ title: '质量: 蓝光原盘', value: ' BLU ' },
|
|
||||||
{ title: '排除: 蓝光原盘', value: ' !BLU ' },
|
|
||||||
{ title: '质量: BLURAY', value: ' BLURAY ' },
|
|
||||||
{ title: '排除: BLURAY', value: ' !BLURAY ' },
|
|
||||||
{ title: '质量: UHD', value: ' UHD ' },
|
|
||||||
{ title: '排除: UHD', value: ' !UHD ' },
|
|
||||||
{ title: '质量: REMUX', value: ' REMUX ' },
|
|
||||||
{ title: '排除: REMUX', value: ' !REMUX ' },
|
|
||||||
{ title: '质量: WEB-DL', value: ' WEBDL ' },
|
|
||||||
{ title: '排除: WEB-DL', value: ' !WEBDL ' },
|
|
||||||
{ title: '质量: 60fps', value: ' 60FPS ' },
|
|
||||||
{ title: '排除: 60fps', value: ' !60FPS ' },
|
|
||||||
{ title: '编码: H265', value: ' H265 ' },
|
|
||||||
{ title: '排除: H265', value: ' !H265 ' },
|
|
||||||
{ title: '编码: H264', value: ' H264 ' },
|
|
||||||
{ title: '排除: H264', value: ' !H264 ' },
|
|
||||||
{ title: '效果: 杜比视界', value: ' DOLBY ' },
|
|
||||||
{ title: '排除: 杜比视界', value: ' !DOLBY ' },
|
|
||||||
{ title: '效果: 杜比全景声', value: ' ATMOS ' },
|
|
||||||
{ title: '排除: 杜比全景声', value: ' !ATMOS ' },
|
|
||||||
{ title: '效果: HDR', value: ' HDR ' },
|
|
||||||
{ title: '排除: HDR', value: ' !HDR ' },
|
|
||||||
{ title: '效果: SDR', value: ' SDR ' },
|
|
||||||
{ title: '排除: SDR', value: ' !SDR ' },
|
|
||||||
{ title: '效果: 3D', value: ' 3D ' },
|
|
||||||
{ title: '排除: 3D', value: ' !3D ' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const storageDict = storageOptions.reduce((dict, item) => {
|
|
||||||
dict[item.value] = item.title
|
|
||||||
return dict
|
|
||||||
}, {} as Record<string, string>)
|
|
||||||
|
|
||||||
export const transferTypeOptions = [
|
|
||||||
{ title: '复制', value: 'copy' },
|
|
||||||
{ title: '移动', value: 'move' },
|
|
||||||
{ title: '硬链接', value: 'link' },
|
|
||||||
{ title: '软链接', value: 'softlink' },
|
|
||||||
]
|
|
||||||
@@ -37,13 +37,3 @@ api.interceptors.response.use(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export default api
|
export default api
|
||||||
|
|
||||||
export async function fetchGlobalSettings() {
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.get('system/global')
|
|
||||||
return result.data || {}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch global settings', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
400
src/api/types.ts
@@ -1,6 +1,5 @@
|
|||||||
// 订阅
|
// 订阅
|
||||||
export interface Subscribe {
|
export interface Subscribe {
|
||||||
// 订阅ID
|
|
||||||
id: number
|
id: number
|
||||||
// 订阅名称
|
// 订阅名称
|
||||||
name: string
|
name: string
|
||||||
@@ -14,10 +13,6 @@ export interface Subscribe {
|
|||||||
tmdbid: number
|
tmdbid: number
|
||||||
// 豆瓣ID
|
// 豆瓣ID
|
||||||
doubanid?: string
|
doubanid?: string
|
||||||
// Bangumi ID
|
|
||||||
bangumiid?: string
|
|
||||||
// 其它媒体ID
|
|
||||||
mediaid?: string
|
|
||||||
// 季号
|
// 季号
|
||||||
season?: number
|
season?: number
|
||||||
// 海报
|
// 海报
|
||||||
@@ -48,7 +43,7 @@ export interface Subscribe {
|
|||||||
lack_episode?: number
|
lack_episode?: number
|
||||||
// 附加信息
|
// 附加信息
|
||||||
note?: string
|
note?: string
|
||||||
// 状态:N-新建 R-订阅中 P-待定 S-暂停
|
// 状态:N-新建, R-订阅中
|
||||||
state: string
|
state: string
|
||||||
// 最后更新时间
|
// 最后更新时间
|
||||||
last_update: string
|
last_update: string
|
||||||
@@ -70,84 +65,12 @@ export interface Subscribe {
|
|||||||
show_edit_dialog: boolean
|
show_edit_dialog: boolean
|
||||||
// 编辑框打开状态
|
// 编辑框打开状态
|
||||||
page_open?: boolean
|
page_open?: boolean
|
||||||
// 自定义识别词
|
|
||||||
custom_words?: string
|
|
||||||
// 自定义媒体类别
|
|
||||||
media_category?: string
|
|
||||||
// 过滤规则组
|
|
||||||
filter_groups?: string[]
|
|
||||||
// 下载器
|
|
||||||
downloader: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 订阅分享
|
|
||||||
export interface SubscribeShare {
|
|
||||||
// 分享ID
|
|
||||||
id?: number
|
|
||||||
// 订阅ID
|
|
||||||
subscribe_id?: number
|
|
||||||
// 分享标题
|
|
||||||
share_title?: string
|
|
||||||
// 分享说明
|
|
||||||
share_comment?: string
|
|
||||||
// 分享人
|
|
||||||
share_user?: string
|
|
||||||
// 分享人唯一ID
|
|
||||||
share_uid?: string
|
|
||||||
// 订阅名称
|
|
||||||
name?: string
|
|
||||||
// 订阅年份
|
|
||||||
year?: string
|
|
||||||
// 订阅类型 电影/电视剧
|
|
||||||
type?: string
|
|
||||||
// 搜索关键字
|
|
||||||
keyword?: string
|
|
||||||
// TMDB ID
|
|
||||||
tmdbid?: number
|
|
||||||
// 豆瓣ID
|
|
||||||
doubanid?: string
|
|
||||||
// 季号
|
|
||||||
season?: number
|
|
||||||
// 海报
|
|
||||||
poster?: string
|
|
||||||
// 背景图
|
|
||||||
backdrop?: string
|
|
||||||
// 评分
|
|
||||||
vote?: number
|
|
||||||
// 描述
|
|
||||||
description?: string
|
|
||||||
// 过滤规则
|
|
||||||
filter?: string
|
|
||||||
// 包含
|
|
||||||
include?: string
|
|
||||||
// 排除
|
|
||||||
exclude?: string
|
|
||||||
// 质量
|
|
||||||
quality?: string
|
|
||||||
// 分辨率
|
|
||||||
resolution?: string
|
|
||||||
// 特效
|
|
||||||
effect?: string
|
|
||||||
// 总集数
|
|
||||||
total_episode?: number
|
|
||||||
// 时间
|
|
||||||
date?: string
|
|
||||||
// 自定义识别词
|
|
||||||
custom_words?: string
|
|
||||||
// 自定义媒体类别
|
|
||||||
media_category?: string
|
|
||||||
// 复用次数
|
|
||||||
count?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 历史记录
|
// 历史记录
|
||||||
export interface TransferHistory {
|
export interface TransferHistory {
|
||||||
// ID
|
// ID
|
||||||
id: number
|
id: number
|
||||||
// 源存储
|
|
||||||
src_storage?: string
|
|
||||||
// 目标存储
|
|
||||||
dest_storage?: string
|
|
||||||
// 源目录
|
// 源目录
|
||||||
src?: string
|
src?: string
|
||||||
// 目的目录
|
// 目的目录
|
||||||
@@ -190,7 +113,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
|
||||||
@@ -210,12 +133,6 @@ 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
|
||||||
// 媒体原发行标题
|
// 媒体原发行标题
|
||||||
@@ -401,8 +318,6 @@ export interface Site {
|
|||||||
pri?: number
|
pri?: number
|
||||||
// RSS地址
|
// RSS地址
|
||||||
rss?: string
|
rss?: string
|
||||||
// 下载器
|
|
||||||
downloader: string
|
|
||||||
// Cookie
|
// Cookie
|
||||||
cookie?: string
|
cookie?: string
|
||||||
// ApiKey
|
// ApiKey
|
||||||
@@ -451,48 +366,6 @@ export interface SiteStatistic {
|
|||||||
note?: string
|
note?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 站点用户数据
|
|
||||||
export interface SiteUserData {
|
|
||||||
// 站点域名
|
|
||||||
domain?: string
|
|
||||||
// 用户名
|
|
||||||
username?: string
|
|
||||||
// 用户ID
|
|
||||||
userid?: number
|
|
||||||
// 用户等级
|
|
||||||
user_level?: string
|
|
||||||
// 加入时间
|
|
||||||
join_at?: string
|
|
||||||
// 积分
|
|
||||||
bonus?: number // 默认为 0.0
|
|
||||||
// 上传量
|
|
||||||
upload?: number // 默认为 0
|
|
||||||
// 下载量
|
|
||||||
download?: number // 默认为 0
|
|
||||||
// 分享率
|
|
||||||
ratio?: number // 默认为 0
|
|
||||||
// 做种数
|
|
||||||
seeding?: number // 默认为 0
|
|
||||||
// 下载数
|
|
||||||
leeching?: number // 默认为 0
|
|
||||||
// 做种体积
|
|
||||||
seeding_size?: number // 默认为 0
|
|
||||||
// 下载体积
|
|
||||||
leeching_size?: number // 默认为 0
|
|
||||||
// 做种人数, 种子大小
|
|
||||||
seeding_info?: any[] // 默认为空数组
|
|
||||||
// 未读消息
|
|
||||||
message_unread?: number // 默认为 0
|
|
||||||
// 未读消息内容
|
|
||||||
message_unread_contents?: any[] // 默认为空数组
|
|
||||||
// 错误信息
|
|
||||||
err_msg?: string | null // 默认为 null
|
|
||||||
// 更新日期
|
|
||||||
updated_day?: string
|
|
||||||
// 更新时间
|
|
||||||
updated_time?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 正在下载
|
// 正在下载
|
||||||
export interface DownloadingInfo {
|
export interface DownloadingInfo {
|
||||||
// HASH
|
// HASH
|
||||||
@@ -521,8 +394,6 @@ export interface DownloadingInfo {
|
|||||||
userid?: string
|
userid?: string
|
||||||
// 下载用户名称
|
// 下载用户名称
|
||||||
username?: string
|
username?: string
|
||||||
// 剩余时间
|
|
||||||
left_time?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缺失剧集信息
|
// 缺失剧集信息
|
||||||
@@ -621,8 +492,6 @@ export interface TorrentInfo {
|
|||||||
site_proxy: boolean
|
site_proxy: boolean
|
||||||
// 站点优先级
|
// 站点优先级
|
||||||
site_order: number
|
site_order: number
|
||||||
// 站点下载器
|
|
||||||
site_downloader?: string
|
|
||||||
// 种子名称
|
// 种子名称
|
||||||
title?: string
|
title?: string
|
||||||
// 种子副标题
|
// 种子副标题
|
||||||
@@ -773,10 +642,6 @@ export interface User {
|
|||||||
avatar: string
|
avatar: string
|
||||||
// 是否开启双重验证
|
// 是否开启双重验证
|
||||||
is_otp: boolean
|
is_otp: boolean
|
||||||
// 用户权限 json
|
|
||||||
permissions: { [key: string]: any }
|
|
||||||
// 用户个性化设置 json
|
|
||||||
settings: { [key: string]: string | null }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储空间
|
// 存储空间
|
||||||
@@ -876,8 +741,6 @@ export interface EndPoints {
|
|||||||
|
|
||||||
// 文件浏览项目
|
// 文件浏览项目
|
||||||
export interface FileItem {
|
export interface FileItem {
|
||||||
// 存储
|
|
||||||
storage: string
|
|
||||||
// 类型 dir/file
|
// 类型 dir/file
|
||||||
type: string
|
type: string
|
||||||
// 文件名
|
// 文件名
|
||||||
@@ -980,249 +843,22 @@ export interface SystemNotification {
|
|||||||
date: string
|
date: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载器配置
|
// 下载目录/媒体库目录
|
||||||
export interface DownloaderConf {
|
export interface MediaDirectory {
|
||||||
// 名称
|
// 类型 download/library
|
||||||
name: string
|
type?: string
|
||||||
// 类型 qbittorrent/transmission
|
// 别名
|
||||||
type: string
|
name?: string
|
||||||
// 是否默认
|
// 路径
|
||||||
default: boolean
|
path?: string
|
||||||
// 配置
|
// 媒体类型 电影/电视剧
|
||||||
config: { [key: string]: any }
|
|
||||||
// 是否启用
|
|
||||||
enabled: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通知配置
|
|
||||||
export interface NotificationConf {
|
|
||||||
// 名称
|
|
||||||
name: string
|
|
||||||
// 类型 telegram/wechat/vocechat/synologychat
|
|
||||||
type: string
|
|
||||||
// 配置
|
|
||||||
config: { [key: string]: any }
|
|
||||||
// 场景开关
|
|
||||||
switchs?: string[]
|
|
||||||
// 是否启用
|
|
||||||
enabled: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通知场景开关配置
|
|
||||||
export interface NotificationSwitchConf {
|
|
||||||
// 场景名称
|
|
||||||
type: string
|
|
||||||
// 通知范围 all/user/admin
|
|
||||||
action: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储配置
|
|
||||||
export interface StorageConf {
|
|
||||||
// 名称
|
|
||||||
name: string
|
|
||||||
// 类型 local/alipan/u115/rclone
|
|
||||||
type: string
|
|
||||||
// 配置
|
|
||||||
config?: { [key: string]: any }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 媒体服务器配置
|
|
||||||
export interface MediaServerConf {
|
|
||||||
// 名称
|
|
||||||
name: string
|
|
||||||
// 类型 emby/jellyfin/plex
|
|
||||||
type: string
|
|
||||||
// 配置
|
|
||||||
config: { [key: string]: any }
|
|
||||||
// 是否启用
|
|
||||||
enabled: boolean
|
|
||||||
// 同步媒体体库列表
|
|
||||||
sync_libraries?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文件整理目录配置
|
|
||||||
export interface TransferDirectoryConf {
|
|
||||||
// 名称
|
|
||||||
name: string
|
|
||||||
// 优先级
|
|
||||||
priority: number
|
|
||||||
// 存储
|
|
||||||
storage: string
|
|
||||||
// 下载目录
|
|
||||||
download_path?: string
|
|
||||||
// 适用媒体类型
|
|
||||||
media_type?: string
|
media_type?: string
|
||||||
// 适用媒体类别
|
// 媒体类别 动画电影/国产剧
|
||||||
media_category?: string
|
|
||||||
// 下载类型子目录
|
|
||||||
download_type_folder?: boolean
|
|
||||||
// 下载类别子目录
|
|
||||||
download_category_folder?: boolean
|
|
||||||
// 监控方式 downloader/monitor,None为不监控
|
|
||||||
monitor_type?: string
|
|
||||||
// 监控模式 fast/compatibility
|
|
||||||
monitor_mode?: string
|
|
||||||
// 整理方式 move/copy/link/softlink
|
|
||||||
transfer_type: string
|
|
||||||
// 文件覆盖模式 always/size/never/latest
|
|
||||||
overwrite_mode?: string
|
|
||||||
// 整理到媒体库目录
|
|
||||||
library_path?: string
|
|
||||||
// 媒体库目录存储
|
|
||||||
library_storage?: string
|
|
||||||
// 智能重命名
|
|
||||||
renaming?: boolean
|
|
||||||
// 刮削
|
|
||||||
scraping?: boolean
|
|
||||||
// 媒体库类型子目录
|
|
||||||
library_type_folder?: boolean
|
|
||||||
// 媒体库类别子目录
|
|
||||||
library_category_folder?: boolean
|
|
||||||
// 是否发送通知
|
|
||||||
notify?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自定义规则项
|
|
||||||
export interface CustomRule {
|
|
||||||
// 规则ID
|
|
||||||
id: string
|
|
||||||
// 名称
|
|
||||||
name: string
|
|
||||||
// 包含
|
|
||||||
include?: string
|
|
||||||
// 排除
|
|
||||||
exclude?: string
|
|
||||||
// 大小范围
|
|
||||||
size_range?: string
|
|
||||||
// 最少做种人数
|
|
||||||
seeders?: string
|
|
||||||
// 发布时间
|
|
||||||
publish_time?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 过滤规则组
|
|
||||||
export interface FilterRuleGroup {
|
|
||||||
// 名称
|
|
||||||
name: string
|
|
||||||
// 规则串
|
|
||||||
rule_string?: string
|
|
||||||
// 适用类媒体类型 None-全部 电影/电视剧
|
|
||||||
media_type?: string
|
|
||||||
// # 适用媒体类别 None-全部 对应二级分类
|
|
||||||
category?: string
|
category?: string
|
||||||
}
|
// 刮削媒体信息
|
||||||
|
scrape?: boolean
|
||||||
// 订阅下载文件详情
|
// 自动二级分类,未指定类别时自动分类
|
||||||
export interface SubscribeDownloadFileInfo {
|
auto_category?: boolean
|
||||||
// 种子名称
|
// 优先级
|
||||||
torrent_title?: string
|
priority?: number
|
||||||
// 站点名称
|
|
||||||
site_name?: string
|
|
||||||
// 下载器
|
|
||||||
downloader?: string
|
|
||||||
// hash
|
|
||||||
hash?: string
|
|
||||||
// 文件路径
|
|
||||||
file_path?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 订阅媒体库文件详情
|
|
||||||
export interface SubscribeLibraryFileInfo {
|
|
||||||
// 存储
|
|
||||||
storage?: string
|
|
||||||
// 文件路径
|
|
||||||
file_path?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 订阅集详情
|
|
||||||
export interface SubscribeEpisodeInfo {
|
|
||||||
// 标题
|
|
||||||
title?: string
|
|
||||||
// 描述
|
|
||||||
description?: string
|
|
||||||
// 背景图
|
|
||||||
backdrop?: string
|
|
||||||
// 下载文件信息
|
|
||||||
download?: SubscribeDownloadFileInfo[]
|
|
||||||
// 媒体库文件信息
|
|
||||||
library?: SubscribeLibraryFileInfo[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 订阅详情
|
|
||||||
export interface SubscrbieInfo {
|
|
||||||
// 订阅信息
|
|
||||||
subscribe: Subscribe
|
|
||||||
// 集信息 {集号: {download: 文件路径,library: 文件路径, backdrop: url, title: 标题, description: 描述}}
|
|
||||||
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[]
|
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 337 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 78 KiB |
BIN
src/assets/images/logos/slack.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 20 KiB |
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
@@ -1,10 +0,0 @@
|
|||||||
<svg width="1252" height="1252" xmlns="http://www.w3.org/2000/svg" version="1.1">
|
|
||||||
<g>
|
|
||||||
<g id="#70c6beff">
|
|
||||||
<path id="svg_2" d="m634.37,138.38c11.88,-1.36 24.25,1.3 34.18,8.09c14.96,9.66 25.55,24.41 34.49,39.51c40.59,68.03 81.45,135.91 122.02,203.96c54.02,90.99 108.06,181.97 161.94,273.06c37.28,63 74.65,125.96 112.18,188.82c24.72,41.99 50.21,83.54 73.84,126.16c10.18,17.84 15.77,38.44 14.93,59.03c-0.59,15.92 -3.48,32.28 -11.84,46.08c-11.73,19.46 -31.39,33.2 -52.71,40.36c-11.37,4.09 -23.3,6.87 -35.43,6.89c-132.32,-0.05 -264.64,0.04 -396.95,0.03c-11.38,-0.29 -22.95,-1.6 -33.63,-5.72c-7.81,-3.33 -15.5,-7.43 -21.61,-13.42c-10.43,-10.32 -17.19,-24.96 -15.38,-39.83c0.94,-10.39 3.48,-20.64 7.76,-30.16c4.15,-9.77 9.99,-18.67 15.06,-27.97c22.13,-39.47 45.31,-78.35 69.42,-116.65c7.72,-12.05 14.44,-25.07 25.12,-34.87c11.35,-10.39 25.6,-18.54 41.21,-19.6c12.55,-0.52 24.89,3.82 35.35,10.55c11.8,6.92 21.09,18.44 24.2,31.88c4.49,17.01 -0.34,34.88 -7.55,50.42c-8.09,17.65 -19.62,33.67 -25.81,52.18c-1.13,4.21 -2.66,9.52 0.48,13.23c3.19,3 7.62,4.18 11.77,5.22c12,2.67 24.38,1.98 36.59,2.06c45,-0.01 90,0 135,0c8.91,-0.15 17.83,0.3 26.74,-0.22c6.43,-0.74 13.44,-1.79 18.44,-6.28c3.3,-2.92 3.71,-7.85 2.46,-11.85c-2.74,-8.86 -7.46,-16.93 -12.12,-24.89c-119.99,-204.91 -239.31,-410.22 -360.56,-614.4c-3.96,-6.56 -7.36,-13.68 -13.03,-18.98c-2.8,-2.69 -6.95,-4.22 -10.77,-3.11c-3.25,1.17 -5.45,4.03 -7.61,6.57c-5.34,6.81 -10.12,14.06 -14.51,21.52c-20.89,33.95 -40.88,68.44 -61.35,102.64c-117.9,198.43 -235.82,396.85 -353.71,595.29c-7.31,13.46 -15.09,26.67 -23.57,39.43c-7.45,10.96 -16.49,21.23 -28.14,27.83c-13.73,7.94 -30.69,11.09 -46.08,6.54c-11.23,-3.47 -22.09,-9.12 -30.13,-17.84c-10.18,-10.08 -14.69,-24.83 -14.17,-38.94c0.52,-14.86 5.49,-29.34 12.98,-42.1c71.58,-121.59 143.62,-242.92 215.93,-364.09c37.2,-62.8 74.23,-125.69 111.64,-188.36c37.84,-63.5 75.77,-126.94 113.44,-190.54c21.02,-35.82 42.19,-71.56 64.28,-106.74c6.79,-11.15 15.58,-21.15 26.16,-28.85c8.68,-5.92 18.42,-11 29.05,-11.94z" fill="#70c6be"/>
|
|
||||||
</g>
|
|
||||||
<g id="#1ba0d8ff">
|
|
||||||
<path id="svg_3" d="m628.35,608.38c17.83,-2.87 36.72,1.39 51.5,11.78c11.22,8.66 19.01,21.64 21.26,35.65c1.53,10.68 0.49,21.75 -3.44,31.84c-3.02,8.73 -7.35,16.94 -12.17,24.81c-68.76,115.58 -137.5,231.17 -206.27,346.75c-8.8,14.47 -16.82,29.47 -26.96,43.07c-7.37,9.11 -16.58,16.85 -27.21,21.89c-22.47,11.97 -51.79,4.67 -68.88,-13.33c-8.66,-8.69 -13.74,-20.63 -14.4,-32.84c-0.98,-12.64 1.81,-25.42 7.53,-36.69c5.03,-10.96 10.98,-21.45 17.19,-31.77c30.22,-50.84 60.17,-101.84 90.3,-152.73c41.24,-69.98 83.16,-139.55 124.66,-209.37c4.41,-7.94 9.91,-15.26 16.09,-21.9c8.33,-8.46 18.9,-15.3 30.8,-17.16z" fill="#1ba0d8"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 74 KiB |
@@ -1,44 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Слой_1" x="0px" y="0px" viewBox="0 0 64 64" style="enable-background:new 0 0 64 64;" xml:space="preserve">
|
|
||||||
<linearGradient id="SVGID_1__48343" gradientUnits="userSpaceOnUse" x1="39" y1="23.25" x2="39" y2="33.0008" spreadMethod="reflect">
|
|
||||||
<stop offset="0" style="stop-color:#6DC7FF"/>
|
|
||||||
<stop offset="1" style="stop-color:#E6ABFF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<circle style="fill:url(#SVGID_1__48343);" cx="39" cy="28" r="4"/>
|
|
||||||
<linearGradient id="SVGID_2__48343" gradientUnits="userSpaceOnUse" x1="32" y1="6.75" x2="32" y2="58.039" spreadMethod="reflect">
|
|
||||||
<stop offset="0" style="stop-color:#1A6DFF"/>
|
|
||||||
<stop offset="1" style="stop-color:#C822FF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path style="fill:url(#SVGID_2__48343);" d="M58,13c0-2.757-2.243-5-5-5H19c-2.757,0-5,2.243-5,5v33H6v5c0,2.757,2.243,5,5,5h34 c2.757,0,5-2.243,5-5V18h8V13z M11,54c-1.654,0-3-1.346-3-3v-3h32v3c0,1.125,0.374,2.164,1.002,3H11z M48,51c0,1.654-1.346,3-3,3 s-3-1.346-3-3v-5H16V13c0-1.654,1.346-3,3-3h30.026C48.391,10.838,48,11.87,48,13V51z M56,16h-6v-3c0-1.654,1.346-3,3-3s3,1.346,3,3 V16z"/>
|
|
||||||
<linearGradient id="SVGID_3__48343" gradientUnits="userSpaceOnUse" x1="39" y1="6.75" x2="39" y2="58.039" spreadMethod="reflect">
|
|
||||||
<stop offset="0" style="stop-color:#1A6DFF"/>
|
|
||||||
<stop offset="1" style="stop-color:#C822FF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path style="fill:url(#SVGID_3__48343);" d="M39,23c-2.757,0-5,2.243-5,5s2.243,5,5,5s5-2.243,5-5S41.757,23,39,23z M39,31 c-1.654,0-3-1.346-3-3s1.346-3,3-3s3,1.346,3,3S40.654,31,39,31z"/>
|
|
||||||
<linearGradient id="SVGID_4__48343" gradientUnits="userSpaceOnUse" x1="25" y1="6.75" x2="25" y2="58.039" spreadMethod="reflect">
|
|
||||||
<stop offset="0" style="stop-color:#1A6DFF"/>
|
|
||||||
<stop offset="1" style="stop-color:#C822FF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<rect x="20" y="23" style="fill:url(#SVGID_4__48343);" width="10" height="2"/>
|
|
||||||
<linearGradient id="SVGID_5__48343" gradientUnits="userSpaceOnUse" x1="25" y1="6.75" x2="25" y2="58.039" spreadMethod="reflect">
|
|
||||||
<stop offset="0" style="stop-color:#1A6DFF"/>
|
|
||||||
<stop offset="1" style="stop-color:#C822FF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<rect x="20" y="27" style="fill:url(#SVGID_5__48343);" width="10" height="2"/>
|
|
||||||
<linearGradient id="SVGID_6__48343" gradientUnits="userSpaceOnUse" x1="25" y1="6.75" x2="25" y2="58.039" spreadMethod="reflect">
|
|
||||||
<stop offset="0" style="stop-color:#1A6DFF"/>
|
|
||||||
<stop offset="1" style="stop-color:#C822FF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<rect x="20" y="31" style="fill:url(#SVGID_6__48343);" width="10" height="2"/>
|
|
||||||
<linearGradient id="SVGID_7__48343" gradientUnits="userSpaceOnUse" x1="25" y1="6.75" x2="25" y2="58.039" spreadMethod="reflect">
|
|
||||||
<stop offset="0" style="stop-color:#1A6DFF"/>
|
|
||||||
<stop offset="1" style="stop-color:#C822FF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<rect x="20" y="35" style="fill:url(#SVGID_7__48343);" width="10" height="2"/>
|
|
||||||
<linearGradient id="SVGID_8__48343" gradientUnits="userSpaceOnUse" x1="39" y1="6.75" x2="39" y2="58.039" spreadMethod="reflect">
|
|
||||||
<stop offset="0" style="stop-color:#1A6DFF"/>
|
|
||||||
<stop offset="1" style="stop-color:#C822FF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<rect x="34" y="35" style="fill:url(#SVGID_8__48343);" width="10" height="2"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1,24 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Слой_1" x="0px" y="0px" viewBox="0 0 64 64" style="enable-background:new 0 0 64 64;" xml:space="preserve">
|
|
||||||
<linearGradient id="SVGID_1__52535" gradientUnits="userSpaceOnUse" x1="21.9994" y1="11.6667" x2="21.9994" y2="18.5839" spreadMethod="reflect">
|
|
||||||
<stop offset="0" style="stop-color:#6DC7FF"/>
|
|
||||||
<stop offset="1" style="stop-color:#E6ABFF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<circle style="fill:url(#SVGID_1__52535);" cx="21.999" cy="14.998" r="3"/>
|
|
||||||
<linearGradient id="SVGID_2__52535" gradientUnits="userSpaceOnUse" x1="35.9994" y1="4.1667" x2="35.9994" y2="15.8334" spreadMethod="reflect">
|
|
||||||
<stop offset="0" style="stop-color:#6DC7FF"/>
|
|
||||||
<stop offset="1" style="stop-color:#E6ABFF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<circle style="fill:url(#SVGID_2__52535);" cx="35.999" cy="9.998" r="4"/>
|
|
||||||
<linearGradient id="SVGID_3__52535" gradientUnits="userSpaceOnUse" x1="32" y1="20.7501" x2="32" y2="58.7632" spreadMethod="reflect">
|
|
||||||
<stop offset="0" style="stop-color:#1A6DFF"/>
|
|
||||||
<stop offset="1" style="stop-color:#C822FF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path style="fill:url(#SVGID_3__52535);" d="M47.003,21H16.996C15.344,21,14,22.344,14,23.995V25v0.998v1.261 c0,0.717,0.257,1.41,0.722,1.95l10.556,12.315C25.743,42.068,26,42.763,26,43.479v6.964c0,0.652,0.32,1.264,0.854,1.634l8.016,5.569 c0.341,0.236,0.737,0.356,1.136,0.356c0.316,0,0.634-0.076,0.926-0.229C37.591,57.428,38,56.751,38,56.007V43.479 c0-0.716,0.257-1.409,0.722-1.953L49.277,29.21C49.743,28.668,50,27.975,50,27.259v-1.258V25v-1.005C50,22.344,48.655,21,47.003,21z M37.204,40.225c-0.447,0.521-0.762,1.129-0.963,1.775H33v2h3l0.001,2H33v2h3.003l0.002,2H34v2h2.007l0.003,4.002L28,50.442v-6.964 c0-1.193-0.428-2.35-1.205-3.255L17.176,29h29.648L37.204,40.225z M48,26.001C48,26.552,47.552,27,47,27H17.002 C16.449,27,16,26.551,16,25.998V25v-1.005C16,23.446,16.447,23,16.996,23h30.007C47.553,23,48,23.446,48,23.995V25V26.001z"/>
|
|
||||||
<linearGradient id="SVGID_4__52535" gradientUnits="userSpaceOnUse" x1="41.9994" y1="17.3333" x2="41.9994" y2="21.3333" spreadMethod="reflect">
|
|
||||||
<stop offset="0" style="stop-color:#6DC7FF"/>
|
|
||||||
<stop offset="1" style="stop-color:#E6ABFF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path style="fill:url(#SVGID_4__52535);" d="M44.999,21c0-2-1.343-3-3-3s-3,1-3,3H44.999z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -2,12 +2,15 @@
|
|||||||
import type { Axios } from 'axios'
|
import type { Axios } from 'axios'
|
||||||
import FileList from './filebrowser/FileList.vue'
|
import FileList from './filebrowser/FileList.vue'
|
||||||
import FileToolbar from './filebrowser/FileToolbar.vue'
|
import FileToolbar from './filebrowser/FileToolbar.vue'
|
||||||
import type { EndPoints, FileItem, StorageConf } from '@/api/types'
|
import type { EndPoints, FileItem } from '@/api/types'
|
||||||
import { storageOptions } from '@/api/constants'
|
import api from '@/api'
|
||||||
|
import AliyunAuthDialog from './dialog/AliyunAuthDialog.vue'
|
||||||
|
import U115AuthDialog from './dialog/U115AuthDialog.vue'
|
||||||
|
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
storages: Array as PropType<StorageConf[]>,
|
storages: String,
|
||||||
tree: Boolean,
|
tree: Boolean,
|
||||||
endpoints: Object as PropType<EndPoints>,
|
endpoints: Object as PropType<EndPoints>,
|
||||||
axios: {
|
axios: {
|
||||||
@@ -19,101 +22,49 @@ const props = defineProps({
|
|||||||
type: Object as PropType<FileItem>,
|
type: Object as PropType<FileItem>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
itemstack: {
|
itemstack: Array as PropType<FileItem[]>,
|
||||||
type: Array as PropType<FileItem[]>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 对外事件
|
// 对外事件
|
||||||
const emit = defineEmits(['pathchanged'])
|
const emit = defineEmits(['pathchanged'])
|
||||||
|
|
||||||
|
const availableStorages = [
|
||||||
|
{
|
||||||
|
name: '本地',
|
||||||
|
code: 'local',
|
||||||
|
icon: 'mdi-folder-multiple-outline',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '阿里云盘',
|
||||||
|
code: 'aliyun',
|
||||||
|
icon: 'mdi-cloud-outline',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '115网盘',
|
||||||
|
code: 'u115',
|
||||||
|
icon: 'mdi-cloud-outline',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const fileIcons = {
|
const fileIcons = {
|
||||||
// 压缩包
|
|
||||||
zip: 'mdi-folder-zip-outline',
|
zip: 'mdi-folder-zip-outline',
|
||||||
rar: 'mdi-folder-zip-outline',
|
rar: 'mdi-folder-zip-outline',
|
||||||
bak: 'mdi-folder-zip-outline',
|
|
||||||
tar: 'mdi-folder-zip-outline',
|
|
||||||
gz: 'mdi-folder-zip-outline',
|
|
||||||
bz2: 'mdi-folder-zip-outline',
|
|
||||||
// 开发
|
|
||||||
htm: 'mdi-language-html5',
|
htm: 'mdi-language-html5',
|
||||||
html: 'mdi-language-html5',
|
html: 'mdi-language-html5',
|
||||||
vue: 'mdi-vuejs',
|
|
||||||
js: 'mdi-nodejs',
|
js: 'mdi-nodejs',
|
||||||
ts: 'mdi-language-typescript',
|
|
||||||
json: 'mdi-file-document-outline',
|
json: 'mdi-file-document-outline',
|
||||||
css: 'mdi-language-css3',
|
|
||||||
scss: 'mdi-language-css3',
|
|
||||||
less: 'mdi-language-css3',
|
|
||||||
php: 'mdi-language-php',
|
|
||||||
py: 'mdi-language-python',
|
|
||||||
java: 'mdi-language-java',
|
|
||||||
go: 'mdi-language-go',
|
|
||||||
c: 'mdi-language-c',
|
|
||||||
cpp: 'mdi-language-cpp',
|
|
||||||
h: 'mdi-language-c',
|
|
||||||
cs: 'mdi-language-csharp',
|
|
||||||
sql: 'mdi-database',
|
|
||||||
sh: 'mdi-language-bash',
|
|
||||||
bat: 'mdi-language-bash',
|
|
||||||
ps1: 'mdi-language-powershell',
|
|
||||||
// markdown
|
|
||||||
md: 'mdi-language-markdown-outline',
|
md: 'mdi-language-markdown-outline',
|
||||||
markdown: 'mdi-language-markdown-outline',
|
pdf: 'mdi-file-pdf',
|
||||||
// 图片
|
png: 'mdi-file-image',
|
||||||
png: 'mdi-file-png-box',
|
jpg: 'mdi-file-image',
|
||||||
jpg: 'mdi-file-jpg-box',
|
jpeg: 'mdi-file-image',
|
||||||
jpeg: 'mdi-file-jpg-box',
|
|
||||||
gif: 'mdi-file-gif-box',
|
|
||||||
bmp: 'mdi-file-image-box',
|
|
||||||
webp: 'mdi-file-image-box',
|
|
||||||
ico: 'mdi-file-image-box',
|
|
||||||
svg: 'mdi-file-image-box',
|
|
||||||
// 视频
|
|
||||||
mp4: 'mdi-filmstrip',
|
mp4: 'mdi-filmstrip',
|
||||||
mkv: 'mdi-filmstrip',
|
mkv: 'mdi-filmstrip',
|
||||||
avi: 'mdi-filmstrip',
|
avi: 'mdi-filmstrip',
|
||||||
wmv: 'mdi-filmstrip',
|
wmv: 'mdi-filmstrip',
|
||||||
mov: 'mdi-filmstrip',
|
mov: 'mdi-filmstrip',
|
||||||
flv: 'mdi-filmstrip',
|
|
||||||
rmvb: 'mdi-filmstrip',
|
|
||||||
// 文档
|
|
||||||
txt: 'mdi-file-document-outline',
|
txt: 'mdi-file-document-outline',
|
||||||
env: 'mdi-file-cog-outline',
|
|
||||||
yml: 'mdi-file-cog-outline',
|
|
||||||
yaml: 'mdi-file-cog-outline',
|
|
||||||
conf: 'mdi-file-cog-outline',
|
|
||||||
log: 'mdi-file-document-outline',
|
|
||||||
csv: 'mdi-file-delimited',
|
|
||||||
// office
|
|
||||||
xls: 'mdi-file-excel',
|
xls: 'mdi-file-excel',
|
||||||
xlsx: 'mdi-file-excel',
|
|
||||||
doc: 'mdi-file-word',
|
|
||||||
docx: 'mdi-file-word',
|
|
||||||
ppt: 'mdi-file-powerpoint',
|
|
||||||
pptx: 'mdi-file-powerpoint',
|
|
||||||
pdf: 'mdi-file-pdf',
|
|
||||||
// 音频
|
|
||||||
mp2: 'mdi-music',
|
|
||||||
mp3: 'mdi-music',
|
|
||||||
m4a: 'mdi-music',
|
|
||||||
wma: 'mdi-music',
|
|
||||||
aac: 'mdi-music',
|
|
||||||
ogg: 'mdi-music',
|
|
||||||
flac: 'mdi-music',
|
|
||||||
wav: 'mdi-music',
|
|
||||||
// 字体
|
|
||||||
ttf: 'mdi-format-font',
|
|
||||||
otf: 'mdi-format-font',
|
|
||||||
woff: 'mdi-format-font',
|
|
||||||
woff2: 'mdi-format-font',
|
|
||||||
eot: 'mdi-format-font',
|
|
||||||
// 字幕
|
|
||||||
srt: 'mdi-subtitles-outline',
|
|
||||||
ass: 'mdi-subtitles-outline',
|
|
||||||
sub: 'mdi-subtitles-outline',
|
|
||||||
// 其他
|
|
||||||
other: 'mdi-file-outline',
|
other: 'mdi-file-outline',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,11 +76,19 @@ const activeStorage = ref('local')
|
|||||||
const refreshPending = ref(false)
|
const refreshPending = ref(false)
|
||||||
// 排序
|
// 排序
|
||||||
const sort = ref('name')
|
const sort = ref('name')
|
||||||
|
// 阿里云盘认证对话框
|
||||||
|
const aliyunAuthDialog = ref(false)
|
||||||
|
// 阿里云盘用户信息
|
||||||
|
const aliyunUserInfo = ref<{ [key: string]: any }>({})
|
||||||
|
// 115网盘认证对话框
|
||||||
|
const u115AuthDialog = ref(false)
|
||||||
|
// 115网盘用户信息
|
||||||
|
const u115UserInfo = ref<{ [key: string]: any }>({})
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const storagesArray = computed(() => {
|
const storagesArray = computed(() => {
|
||||||
const storageCodes = props.storages?.map(item => item.type)
|
const storageCodes = props.storages?.split(',')
|
||||||
return storageOptions.filter(item => storageCodes?.includes(item.value))
|
return availableStorages.filter(item => storageCodes?.includes(item.code))
|
||||||
})
|
})
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
@@ -138,10 +97,47 @@ function loadingChanged(loading: number) {
|
|||||||
else if (loading > 0) loading--
|
else if (loading > 0) loading--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询阿里云
|
||||||
|
async function loadAliyunUserInfo() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get('aliyun/userinfo')
|
||||||
|
if (result.success) {
|
||||||
|
aliyunUserInfo.value = result
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询115
|
||||||
|
async function loadU115UserInfo() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get('u115/storage')
|
||||||
|
if (result.success) {
|
||||||
|
u115UserInfo.value = result
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 存储切换
|
// 存储切换
|
||||||
async function storageChanged(storage: string) {
|
async function storageChanged(storage: string) {
|
||||||
|
if (storage == 'aliyun') {
|
||||||
|
await loadAliyunUserInfo()
|
||||||
|
if (isNullOrEmptyObject(aliyunUserInfo.value)) {
|
||||||
|
aliyunAuthDialog.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (storage == 'u115') {
|
||||||
|
await loadU115UserInfo()
|
||||||
|
if (isNullOrEmptyObject(u115UserInfo.value)) {
|
||||||
|
u115AuthDialog.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
activeStorage.value = storage
|
activeStorage.value = storage
|
||||||
emit('pathchanged', { storage: storage, path: '/', fileid: 'root' })
|
emit('pathchanged', { path: '/', fileid: 'root' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 路径变化
|
// 路径变化
|
||||||
@@ -154,6 +150,18 @@ function sortChanged(s: string) {
|
|||||||
sort.value = s
|
sort.value = s
|
||||||
refreshPending.value = true
|
refreshPending.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// aliyun认证完成
|
||||||
|
function aliyunAuthDone() {
|
||||||
|
aliyunAuthDialog.value = false
|
||||||
|
activeStorage.value = 'aliyun'
|
||||||
|
}
|
||||||
|
|
||||||
|
// u115认证完成
|
||||||
|
function u115AuthDone() {
|
||||||
|
u115AuthDialog.value = false
|
||||||
|
activeStorage.value = 'u115'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -187,4 +195,11 @@ function sortChanged(s: string) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
|
<AliyunAuthDialog
|
||||||
|
v-if="aliyunAuthDialog"
|
||||||
|
v-model="aliyunAuthDialog"
|
||||||
|
@close="aliyunAuthDialog = false"
|
||||||
|
@done="aliyunAuthDone"
|
||||||
|
/>
|
||||||
|
<U115AuthDialog v-if="u115AuthDialog" v-model="u115AuthDialog" @close="u115AuthDialog = false" @done="u115AuthDone" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -73,3 +73,9 @@ const getImgUrl = computed(() => {
|
|||||||
</template>
|
</template>
|
||||||
</VHover>
|
</VHover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.text-shadow {
|
||||||
|
text-shadow: 1px 1px #777;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,193 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { CustomRule } from '@/api/types'
|
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import filter_svg from '@images/svg/filter.svg'
|
|
||||||
import { cloneDeep } from 'lodash'
|
|
||||||
import { innerFilterRules } from '@/api/constants'
|
|
||||||
|
|
||||||
// 输入参数
|
|
||||||
const props = defineProps({
|
|
||||||
// 单条规则
|
|
||||||
rule: {
|
|
||||||
type: Object as PropType<CustomRule>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
// 所有规则
|
|
||||||
rules: {
|
|
||||||
type: Array as PropType<CustomRule[]>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 提示框
|
|
||||||
const $toast = useToast()
|
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
|
||||||
const emit = defineEmits(['close', 'change', 'done'])
|
|
||||||
|
|
||||||
// 规则详情弹窗
|
|
||||||
const ruleInfoDialog = ref(false)
|
|
||||||
|
|
||||||
// 规则详情
|
|
||||||
const ruleInfo = ref<CustomRule>({
|
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
include: '',
|
|
||||||
exclude: '',
|
|
||||||
size_range: '',
|
|
||||||
seeders: '',
|
|
||||||
publish_time: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
// 打开详情弹窗
|
|
||||||
function openRuleInfoDialog() {
|
|
||||||
// 深复制
|
|
||||||
ruleInfo.value = cloneDeep(props.rule)
|
|
||||||
ruleInfoDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存详情数据
|
|
||||||
function saveRuleInfo() {
|
|
||||||
// 有空值
|
|
||||||
if (!ruleInfo.value.id || !ruleInfo.value.name) {
|
|
||||||
if (!ruleInfo.value.id && !ruleInfo.value.name) {
|
|
||||||
$toast.error('规则ID和规则名称不能为空')
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 检查ID是否在内置的规则中
|
|
||||||
if (innerFilterRules.find(option => option.value === ruleInfo.value.id)) {
|
|
||||||
$toast.error('当前规则ID已被内置规则占用')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 检查规则名称是否在内置的规则中
|
|
||||||
if (innerFilterRules.find(option => option.title === ruleInfo.value.name)) {
|
|
||||||
$toast.error('当前规则名称已被内置规则占用')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// ID已存在
|
|
||||||
if (ruleInfo.value.id !== props.rule.id && props.rules.find(rule => rule.id === ruleInfo.value.id)) {
|
|
||||||
$toast.error(`规则ID【${ruleInfo.value.id}】已存在`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 规则名称已存在
|
|
||||||
if (ruleInfo.value.name !== props.rule.name && props.rules.find(rule => rule.name === ruleInfo.value.name)) {
|
|
||||||
$toast.error(`规则名称【${ruleInfo.value.name}】已存在`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 保存数据
|
|
||||||
ruleInfoDialog.value = false
|
|
||||||
emit('change', ruleInfo.value, props.rule.id)
|
|
||||||
emit('done')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按钮点击
|
|
||||||
function onClose() {
|
|
||||||
emit('close')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<VCard variant="tonal" @click="openRuleInfoDialog">
|
|
||||||
<span class="absolute top-3 right-12">
|
|
||||||
<IconBtn>
|
|
||||||
<VIcon class="cursor-move" icon="mdi-drag" />
|
|
||||||
</IconBtn>
|
|
||||||
</span>
|
|
||||||
<DialogCloseBtn @click="onClose" />
|
|
||||||
<VCardText class="flex justify-space-between align-center gap-3">
|
|
||||||
<div class="align-self-start">
|
|
||||||
<h5 class="text-h6 mb-1">{{ props.rule.name }}</h5>
|
|
||||||
<div class="text-body-1 mb-3">{{ props.rule.id }}</div>
|
|
||||||
</div>
|
|
||||||
<VImg :src="filter_svg" cover class="mt-7" max-width="3rem" />
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
<VDialog v-if="ruleInfoDialog" v-model="ruleInfoDialog" scrollable max-width="40rem" persistent>
|
|
||||||
<VCard :title="`${props.rule.id} - 配置`" class="rounded-t">
|
|
||||||
<DialogCloseBtn v-model="ruleInfoDialog" />
|
|
||||||
<VDivider />
|
|
||||||
<VCardText>
|
|
||||||
<VForm>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="ruleInfo.id"
|
|
||||||
label="规则ID"
|
|
||||||
placeholder="必填;不可与其他规则ID重名"
|
|
||||||
hint="字符与数字组合,不能含空格"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="ruleInfo.name"
|
|
||||||
label="规则名称"
|
|
||||||
placeholder="必填;不可与其他规则名称重名"
|
|
||||||
hint="使用别名便于区分规则"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextField
|
|
||||||
v-model="ruleInfo.include"
|
|
||||||
placeholder="关键字/正则表达式"
|
|
||||||
label="包含"
|
|
||||||
hint="必须包含的关键字或正则表达式,多个值使用|分隔"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextField
|
|
||||||
v-model="ruleInfo.exclude"
|
|
||||||
placeholder="关键字/正则表达式"
|
|
||||||
label="排除"
|
|
||||||
hint="不能包含的关键字或正则表达式,多个值使用|分隔"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="ruleInfo.size_range"
|
|
||||||
placeholder="0/1-10"
|
|
||||||
label="资源体积(MB)"
|
|
||||||
hint="最小资源文件体积或体积范围(剧集计算单集平均大小)"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="ruleInfo.seeders"
|
|
||||||
placeholder="0/1-10"
|
|
||||||
label="做种人数"
|
|
||||||
hint="最小做种人数或做种人数范围"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="ruleInfo.publish_time"
|
|
||||||
placeholder="0/1-10"
|
|
||||||
label="发布时间(分钟)"
|
|
||||||
hint="距离资源发布的最小时间间隔或时间区间"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
<VCardActions class="pt-3">
|
|
||||||
<VBtn @click="saveRuleInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 确定 </VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { TransferDirectoryConf } from '@/api/types'
|
import type { MediaDirectory } from '@/api/types'
|
||||||
import api from '@/api'
|
import { VTextField } from 'vuetify/lib/components/index.mjs'
|
||||||
import { nextTick } from 'vue'
|
|
||||||
import { storageOptions } from '@/api/constants'
|
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
type: String, // download/library
|
type: String, // download/library
|
||||||
directory: {
|
directory: {
|
||||||
type: Object as PropType<TransferDirectoryConf>,
|
type: Object as PropType<MediaDirectory>,
|
||||||
required: true, // 必填参数
|
required: true, // 必填参数
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
@@ -19,8 +17,8 @@ const props = defineProps({
|
|||||||
height: String,
|
height: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 卡版是否折叠状态
|
// 路径
|
||||||
const isCollapsed = ref(true)
|
const path = ref<string>('')
|
||||||
|
|
||||||
// 类型下拉字典
|
// 类型下拉字典
|
||||||
const typeItems = [
|
const typeItems = [
|
||||||
@@ -29,93 +27,6 @@ const typeItems = [
|
|||||||
{ title: '电视剧', value: '电视剧' },
|
{ title: '电视剧', value: '电视剧' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 自动整理方式下拉字典
|
|
||||||
const transferSourceItems = [
|
|
||||||
{ title: '不整理', value: '' },
|
|
||||||
{ title: '下载器监控', value: 'downloader' },
|
|
||||||
{ title: '目录监控', value: 'monitor' },
|
|
||||||
{ title: '手动整理', value: 'manual' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 监控模式下拉字典
|
|
||||||
const MonitorModeItems = [
|
|
||||||
{ title: '性能模式', value: 'fast' },
|
|
||||||
{ title: '兼容模式', value: 'compatibility' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 整理方式下拉字典
|
|
||||||
const transferTypeItems = ref<{ title: string; value: string }[]>([])
|
|
||||||
|
|
||||||
// 调用API查询支持的整理方式
|
|
||||||
async function loadTransferTypeItems() {
|
|
||||||
// 参数不全时不查询
|
|
||||||
if (!props.directory.library_storage || !props.directory.storage) return
|
|
||||||
try {
|
|
||||||
// 下载器储存整理方法
|
|
||||||
const storage_res = await api.get(`storage/transtype/${props.directory.storage}`)
|
|
||||||
const storage_transtype = (storage_res as any).transtype
|
|
||||||
// 媒体库储存整理方法
|
|
||||||
const library_storage_res = await api.get(`storage/transtype/${props.directory.library_storage}`)
|
|
||||||
const library_storage_transtype = (library_storage_res as any).transtype
|
|
||||||
// 为空终止
|
|
||||||
if (!library_storage_transtype || !storage_transtype) return
|
|
||||||
// 取并集
|
|
||||||
const transtype: { [key: string]: string } = {}
|
|
||||||
Object.keys(storage_transtype).forEach(key => {
|
|
||||||
if (key in library_storage_transtype) {
|
|
||||||
transtype[key] = storage_transtype[key]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 非空时设置整理方式下拉字典
|
|
||||||
if (transtype && Object.keys(transtype).length > 0) {
|
|
||||||
transferTypeItems.value = Object.keys(transtype).map(key => ({
|
|
||||||
title: transtype[key],
|
|
||||||
value: key,
|
|
||||||
}))
|
|
||||||
// 如果整理方式下拉字典不为空,且当前值不在新的transferTypeItems里,则设置整理方式为第一个
|
|
||||||
if (
|
|
||||||
transferTypeItems.value.length > 0 &&
|
|
||||||
!transferTypeItems.value.find(item => item.value === props.directory.transfer_type)
|
|
||||||
) {
|
|
||||||
nextTick(() => {
|
|
||||||
props.directory.transfer_type = transferTypeItems.value[0].value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// 如果整理方式下拉字典为空,清空整理方式
|
|
||||||
if (transferTypeItems.value.length === 0) {
|
|
||||||
props.directory.transfer_type = ''
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 无可用整理方式,清除已选值
|
|
||||||
transferTypeItems.value = []
|
|
||||||
props.directory.transfer_type = ''
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 整理方式无数据提示
|
|
||||||
const computedNoDataText = computed(() => {
|
|
||||||
if (!props.directory.library_storage && !props.directory.storage) {
|
|
||||||
return '请选择储存'
|
|
||||||
} else if (!props.directory.library_storage) {
|
|
||||||
return '请选择媒体库储存'
|
|
||||||
} else if (!props.directory.storage) {
|
|
||||||
return '请选择下载器储存'
|
|
||||||
} else {
|
|
||||||
return '选择的存储类型没有支持的整理方式'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 覆盖模式下拉字典
|
|
||||||
const overwriteModeItems = [
|
|
||||||
{ title: '从不', value: 'never' },
|
|
||||||
{ title: '总是', value: 'always' },
|
|
||||||
{ title: '按文件大小', value: 'size' },
|
|
||||||
{ title: '仅保留最新版本', value: 'latest' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
// 定义触发的自定义事件
|
||||||
const emit = defineEmits(['close', 'changed', 'update:modelValue'])
|
const emit = defineEmits(['close', 'changed', 'update:modelValue'])
|
||||||
|
|
||||||
@@ -124,38 +35,18 @@ function onClose() {
|
|||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 路径更新
|
||||||
|
function updatePath(value: string) {
|
||||||
|
path.value = value
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
|
||||||
// 根据选中的媒体类型,获取对应的媒体类别
|
// 根据选中的媒体类型,获取对应的媒体类别
|
||||||
const getCategories = computed(() => {
|
const getCategories = computed(() => {
|
||||||
const default_value = [{ title: '全部', value: '' }]
|
const default_value = [{ title: '全部', value: '' }]
|
||||||
if (!props.categories || !props.categories[props.directory?.media_type ?? '']) return default_value
|
if (!props.categories || !props.categories[props.directory?.media_type ?? '']) return default_value
|
||||||
return default_value.concat(props.categories[props.directory.media_type ?? ''])
|
return default_value.concat(props.categories[props.directory.media_type ?? ''])
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听 下载储存与媒体库储存 变化,重新加载整理方式下拉字典
|
|
||||||
watch(
|
|
||||||
[() => props.directory.library_storage, () => props.directory.storage],
|
|
||||||
([newLibraryStorage, newStorage], [oldLibraryStorage, oldStorage]) => {
|
|
||||||
if (newLibraryStorage !== oldLibraryStorage || newStorage !== oldStorage) {
|
|
||||||
loadTransferTypeItems()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
// 媒体类别和类型变更非空时,将按类型分类和按类别分类置为false
|
|
||||||
watch(
|
|
||||||
[() => props.directory.media_type, () => props.directory.media_category],
|
|
||||||
([newMediaType, newMediaCategory], [oldMediaType, oldMediaCategory]) => {
|
|
||||||
if (newMediaType && newMediaType !== oldMediaType) {
|
|
||||||
props.directory.download_type_folder = false
|
|
||||||
props.directory.library_type_folder = false
|
|
||||||
}
|
|
||||||
if (newMediaCategory && newMediaCategory !== oldMediaCategory) {
|
|
||||||
props.directory.download_category_folder = false
|
|
||||||
props.directory.library_category_folder = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -174,124 +65,40 @@ watch(
|
|||||||
</IconBtn>
|
</IconBtn>
|
||||||
</span>
|
</span>
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
<VCardText v-if="!isCollapsed">
|
<VCardText>
|
||||||
<VForm>
|
<VForm>
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="6">
|
<VCol>
|
||||||
|
<VPathField @update:modelValue="updatePath">
|
||||||
|
<template #activator="{ menuprops }">
|
||||||
|
<VTextField v-model="props.directory.path" v-bind="menuprops" variant="underlined" label="路径" />
|
||||||
|
</template>
|
||||||
|
</VPathField>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="4">
|
||||||
<VSelect
|
<VSelect
|
||||||
v-model="props.directory.media_type"
|
v-model="props.directory.media_type"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
:items="typeItems"
|
:items="typeItems"
|
||||||
label="媒体类型"
|
label="媒体类型"
|
||||||
@update:modelValue="props.directory.media_category = ''"
|
@update:modelValue="props.directory.category = ''"
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="6">
|
|
||||||
<VSelect
|
|
||||||
v-model="props.directory.media_category"
|
|
||||||
variant="underlined"
|
|
||||||
:items="getCategories"
|
|
||||||
label="媒体类别"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="4">
|
|
||||||
<VSelect
|
|
||||||
v-model="props.directory.storage"
|
|
||||||
variant="underlined"
|
|
||||||
:items="storageOptions"
|
|
||||||
label="下载存储/源存储"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="8">
|
|
||||||
<VPathField
|
|
||||||
v-model="props.directory.download_path"
|
|
||||||
:storage="props.directory.storage"
|
|
||||||
variant="underlined"
|
|
||||||
label="下载目录/源目录"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''">
|
|
||||||
<VSwitch v-model="props.directory.download_type_folder" label="按类型分类"></VSwitch>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="6" v-if="!props.directory.media_category || props.directory.media_category === ''">
|
|
||||||
<VSwitch v-model="props.directory.download_category_folder" label="按类别分类"></VSwitch>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VDivider v-if="$props.directory.monitor_type" class="my-3 bg-primary" />
|
|
||||||
<VRow>
|
|
||||||
<VCol>
|
<VCol>
|
||||||
<VSelect
|
<VSelect v-model="props.directory.category" variant="underlined" :items="getCategories" label="媒体类别" />
|
||||||
v-model="props.directory.monitor_type"
|
|
||||||
variant="underlined"
|
|
||||||
:items="transferSourceItems"
|
|
||||||
label="自动整理"
|
|
||||||
/>
|
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
<VRow v-if="$props.directory.monitor_type">
|
<VRow>
|
||||||
<VCol cols="12" v-if="$props.directory.monitor_type == 'monitor'">
|
<VCol v-if="!props.directory.category || props.directory.category === ''">
|
||||||
<VSelect
|
<VSwitch v-model="props.directory.auto_category" label="自动分类"></VSwitch>
|
||||||
v-model="props.directory.monitor_mode"
|
|
||||||
variant="underlined"
|
|
||||||
:items="MonitorModeItems"
|
|
||||||
label="监控模式"
|
|
||||||
/>
|
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="4">
|
<VCol v-if="type === 'library'">
|
||||||
<VSelect
|
<VSwitch v-model="props.directory.scrape" label="刮削元数据"></VSwitch>
|
||||||
v-model="props.directory.library_storage"
|
|
||||||
variant="underlined"
|
|
||||||
:items="storageOptions"
|
|
||||||
label="媒体库存储"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="8">
|
|
||||||
<VPathField
|
|
||||||
v-model="props.directory.library_path"
|
|
||||||
:storage="props.directory.library_storage"
|
|
||||||
variant="underlined"
|
|
||||||
label="媒体库目录"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="4">
|
|
||||||
<VSelect
|
|
||||||
v-model="props.directory.transfer_type"
|
|
||||||
variant="underlined"
|
|
||||||
:items="transferTypeItems"
|
|
||||||
label="整理方式"
|
|
||||||
:no-data-text="computedNoDataText"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="8">
|
|
||||||
<VSelect
|
|
||||||
v-model="props.directory.overwrite_mode"
|
|
||||||
variant="underlined"
|
|
||||||
:items="overwriteModeItems"
|
|
||||||
label="覆盖模式"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''">
|
|
||||||
<VSwitch v-model="props.directory.library_type_folder" label="按类型分类"></VSwitch>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="6" v-if="!props.directory.media_category || props.directory.media_category === ''">
|
|
||||||
<VSwitch v-model="props.directory.library_category_folder" label="按类别分类"></VSwitch>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="6">
|
|
||||||
<VSwitch v-model="props.directory.renaming" label="智能重命名"></VSwitch>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="6">
|
|
||||||
<VSwitch v-model="props.directory.scraping" label="刮削元数据"></VSwitch>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="6">
|
|
||||||
<VSwitch v-model="props.directory.notify" label="发送通知"></VSwitch>
|
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</VForm>
|
</VForm>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions class="text-center py-0">
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn :icon="isCollapsed ? 'mdi-chevron-down' : 'mdi-chevron-up'" @click.stop="isCollapsed = !isCollapsed" />
|
|
||||||
<VSpacer />
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
</VCard>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,317 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import api from '@/api'
|
|
||||||
import { formatFileSize } from '@/@core/utils/formatters'
|
|
||||||
import { DownloaderConf } from '@/api/types'
|
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import type { DownloaderInfo } from '@/api/types'
|
|
||||||
import qbittorrent_image from '@images/logos/qbittorrent.png'
|
|
||||||
import transmission_image from '@images/logos/transmission.png'
|
|
||||||
import { cloneDeep } from 'lodash'
|
|
||||||
|
|
||||||
// 定义输入
|
|
||||||
const props = defineProps({
|
|
||||||
// 单个下载器
|
|
||||||
downloader: {
|
|
||||||
type: Object as PropType<DownloaderConf>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
// 是否允许刷新数据
|
|
||||||
allowRefresh: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
// 所有下载器
|
|
||||||
downloaders: {
|
|
||||||
type: Array as PropType<DownloaderConf[]>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
|
||||||
const emit = defineEmits(['close', 'done', 'change'])
|
|
||||||
|
|
||||||
// 提示框
|
|
||||||
const $toast = useToast()
|
|
||||||
|
|
||||||
// timeout定时器
|
|
||||||
let timeoutTimer: NodeJS.Timeout | undefined = undefined
|
|
||||||
|
|
||||||
// 上传速率
|
|
||||||
const upload_rate = ref(0)
|
|
||||||
|
|
||||||
// 下载速度
|
|
||||||
const download_rate = ref(0)
|
|
||||||
|
|
||||||
// 下载器详情弹窗
|
|
||||||
const downloaderInfoDialog = ref(false)
|
|
||||||
|
|
||||||
// 下载器详情
|
|
||||||
const downloaderInfo = ref<DownloaderConf>({
|
|
||||||
name: '',
|
|
||||||
type: '',
|
|
||||||
default: false,
|
|
||||||
enabled: false,
|
|
||||||
config: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 调用API查询下载器数据
|
|
||||||
async function loadDownloaderInfo() {
|
|
||||||
if (!props.allowRefresh) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res: DownloaderInfo = await api.get('dashboard/downloader', {
|
|
||||||
params: {
|
|
||||||
name: props.downloader.name,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
upload_rate.value = res.upload_speed
|
|
||||||
download_rate.value = res.download_speed
|
|
||||||
// 定时查询
|
|
||||||
clearTimeout(timeoutTimer)
|
|
||||||
if (props.downloader.enabled) {
|
|
||||||
timeoutTimer = setTimeout(loadDownloaderInfo, 3000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开详情弹窗
|
|
||||||
function openDownloaderInfoDialog() {
|
|
||||||
// 深复制
|
|
||||||
downloaderInfo.value = cloneDeep(props.downloader)
|
|
||||||
downloaderInfoDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存详情数据
|
|
||||||
function saveDownloaderInfo() {
|
|
||||||
// 为空不保存,跳出警告框
|
|
||||||
if (!downloaderInfo.value.name) {
|
|
||||||
$toast.error('名称不能为空,请输入后再确定')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 重名判断
|
|
||||||
if (props.downloaders.some(item => item.name === downloaderInfo.value.name && item !== props.downloader)) {
|
|
||||||
$toast.error(`【${downloaderInfo.value.name}】已存在,请替换为其他名称`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 默认下载器去重
|
|
||||||
if (downloaderInfo.value.default) {
|
|
||||||
props.downloaders.forEach(item => {
|
|
||||||
if (item.default && item !== props.downloader) {
|
|
||||||
item.default = false
|
|
||||||
$toast.info(`【${item.name}】存在默认下载器,已替换成【${downloaderInfo.value.name}】`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// 执行保存
|
|
||||||
downloaderInfoDialog.value = false
|
|
||||||
emit('change', downloaderInfo.value, props.downloader.name)
|
|
||||||
emit('done')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据存储类型选择图标
|
|
||||||
const getIcon = computed(() => {
|
|
||||||
switch (props.downloader.type) {
|
|
||||||
case 'qbittorrent':
|
|
||||||
return qbittorrent_image
|
|
||||||
case 'transmission':
|
|
||||||
return transmission_image
|
|
||||||
default:
|
|
||||||
return qbittorrent_image
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 按钮点击
|
|
||||||
function onClose() {
|
|
||||||
emit('close')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (props.downloader.enabled) {
|
|
||||||
await loadDownloaderInfo()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (timeoutTimer) clearTimeout(timeoutTimer)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<VCard variant="tonal" @click="openDownloaderInfoDialog">
|
|
||||||
<DialogCloseBtn @click="onClose" />
|
|
||||||
<span class="absolute top-3 right-12">
|
|
||||||
<IconBtn>
|
|
||||||
<VIcon class="cursor-move" icon="mdi-drag" />
|
|
||||||
</IconBtn>
|
|
||||||
</span>
|
|
||||||
<VCardText class="flex justify-space-between align-center gap-4">
|
|
||||||
<div class="align-self-start flex-1">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<VBadge
|
|
||||||
v-if="props.downloader.default && props.downloader.enabled"
|
|
||||||
dot
|
|
||||||
inline
|
|
||||||
color="success"
|
|
||||||
class="me-1"
|
|
||||||
/>
|
|
||||||
<span class="text-h6">{{ downloader.name }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 flex flex-wrap text-sm" v-if="props.downloader.enabled">
|
|
||||||
<span class="me-2">{{ `↑ ${formatFileSize(upload_rate, 1)}/s ` }}</span>
|
|
||||||
<span>{{ `↓ ${formatFileSize(download_rate, 1)}/s` }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="h-20">
|
|
||||||
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" min-width="3rem" />
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
<VDialog v-if="downloaderInfoDialog" v-model="downloaderInfoDialog" scrollable max-width="40rem" persistent>
|
|
||||||
<VCard :title="`${props.downloader.name} - 配置`" class="rounded-t">
|
|
||||||
<DialogCloseBtn v-model="downloaderInfoDialog" />
|
|
||||||
<VDivider />
|
|
||||||
<VCardText>
|
|
||||||
<VForm>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VSwitch v-model="downloaderInfo.enabled" label="启用下载器" />
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VSwitch v-model="downloaderInfo.default" label="默认下载器" :disabled="!downloaderInfo.enabled" />
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow v-if="downloaderInfo.type == 'qbittorrent'">
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="downloaderInfo.name"
|
|
||||||
label="名称"
|
|
||||||
placeholder="必填;不可与其他名称重名"
|
|
||||||
hint="下载器的别名"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="downloaderInfo.config.host"
|
|
||||||
label="地址"
|
|
||||||
placeholder="http(s)://ip:port"
|
|
||||||
hint="服务端地址,格式:http(s)://ip:port"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="downloaderInfo.config.username"
|
|
||||||
label="用户名"
|
|
||||||
hint="登录使用的用户名"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="downloaderInfo.config.password"
|
|
||||||
type="password"
|
|
||||||
label="密码"
|
|
||||||
hint="登录使用的密码"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VSwitch
|
|
||||||
v-model="downloaderInfo.config.category"
|
|
||||||
label="自动分类管理"
|
|
||||||
hint="由下载器自动管理分类和下载目录"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VSwitch
|
|
||||||
v-model="downloaderInfo.config.sequentail"
|
|
||||||
label="顺序下载"
|
|
||||||
hint="按顺序依次下载文件"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VSwitch
|
|
||||||
v-model="downloaderInfo.config.force_resume"
|
|
||||||
label="强制继续"
|
|
||||||
hint="强制继续、强制上传模式"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VSwitch
|
|
||||||
v-model="downloaderInfo.config.first_last_piece"
|
|
||||||
label="优先首尾文件"
|
|
||||||
hint="优先下载首尾文件块"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow v-if="downloaderInfo.type == 'transmission'">
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="downloaderInfo.name"
|
|
||||||
label="名称"
|
|
||||||
placeholder="必填;不可与其他名称重名"
|
|
||||||
hint="下载器的别名"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="downloaderInfo.config.host"
|
|
||||||
label="地址"
|
|
||||||
placeholder="http(s)://ip:port"
|
|
||||||
hint="服务端地址,格式:http(s)://ip:port"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="downloaderInfo.config.username"
|
|
||||||
label="用户名"
|
|
||||||
hint="登录使用的用户名"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="downloaderInfo.config.password"
|
|
||||||
type="password"
|
|
||||||
label="密码"
|
|
||||||
hint="登录使用的密码"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
<VCardActions class="pt-3">
|
|
||||||
<VBtn @click="saveDownloaderInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
|
|
||||||
确定
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { DownloadingInfo } from '@/api/types'
|
import type { DownloadingInfo } from '@/api/types'
|
||||||
import { formatFileSize } from '@/@core/utils/formatters'
|
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -18,21 +17,16 @@ function getPercentage() {
|
|||||||
|
|
||||||
// 速度
|
// 速度
|
||||||
function getSpeedText() {
|
function getSpeedText() {
|
||||||
return `${formatFileSize(props.info?.size || 0)} ↑ ${props.info?.upspeed}/s ↓ ${props.info?.dlspeed}/s ${
|
return `↑ ${props.info?.upspeed}/s ↓ ${props.info?.dlspeed}/s ${props.info?.left_time}`
|
||||||
props.info?.left_time
|
|
||||||
}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载状态
|
// 下载状态
|
||||||
const isDownloading = ref(props.info?.state === 'downloading')
|
const isDownloading = ref(props.info?.state === 'downloading')
|
||||||
|
|
||||||
// 监听props.info?.state的变化
|
// 监听props.info?.state的变化
|
||||||
watch(
|
watch(() => props.info?.state, (newValue) => {
|
||||||
() => props.info?.state,
|
isDownloading.value = newValue === 'downloading'
|
||||||
newValue => {
|
})
|
||||||
isDownloading.value = newValue === 'downloading'
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 图片是否加载完成
|
// 图片是否加载完成
|
||||||
const imageLoaded = ref(false)
|
const imageLoaded = ref(false)
|
||||||
@@ -51,10 +45,14 @@ function getTextClass() {
|
|||||||
async function toggleDownload() {
|
async function toggleDownload() {
|
||||||
const operation = isDownloading.value ? 'stop' : 'start'
|
const operation = isDownloading.value ? 'stop' : 'start'
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get(`download/${operation}/${props.info?.hash}`)
|
const result: { [key: string]: any } = await api.get(
|
||||||
|
`download/${operation}/${props.info?.hash}`,
|
||||||
|
)
|
||||||
|
|
||||||
if (result.success) isDownloading.value = !isDownloading.value
|
if (result.success)
|
||||||
} catch (error) {
|
isDownloading.value = !isDownloading.value
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,42 +62,67 @@ async function deleteDownload() {
|
|||||||
try {
|
try {
|
||||||
await api.delete(`download/${props.info?.hash}`)
|
await api.delete(`download/${props.info?.hash}`)
|
||||||
cardState.value = false
|
cardState.value = false
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VCard v-if="cardState" :key="props.info?.hash">
|
<VCard
|
||||||
|
v-if="cardState"
|
||||||
|
:key="props.info?.hash"
|
||||||
|
>
|
||||||
<template #image>
|
<template #image>
|
||||||
<VImg :src="props.info?.media.image" aspect-ratio="2/3" cover class="brightness-50" @load="imageLoadHandler" />
|
<VImg
|
||||||
|
:src="props.info?.media.image"
|
||||||
|
aspect-ratio="2/3"
|
||||||
|
cover
|
||||||
|
class="brightness-50"
|
||||||
|
@load="imageLoadHandler"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<VCardTitle class="break-words whitespace-normal" :class="getTextClass()">
|
<VCardTitle
|
||||||
|
class="break-words whitespace-normal"
|
||||||
|
:class="getTextClass()"
|
||||||
|
>
|
||||||
{{ props.info?.media.title || props.info?.name }}
|
{{ props.info?.media.title || props.info?.name }}
|
||||||
{{
|
{{ props.info?.media.episode ? `${props.info?.media.season} ${props.info?.media.episode}` : props.info?.season_episode }}
|
||||||
props.info?.media.episode
|
|
||||||
? `${props.info?.media.season} ${props.info?.media.episode}`
|
|
||||||
: props.info?.season_episode
|
|
||||||
}}
|
|
||||||
</VCardTitle>
|
</VCardTitle>
|
||||||
|
|
||||||
<VCardSubtitle class="break-words whitespace-normal" :class="getTextClass()">
|
<VCardSubtitle
|
||||||
|
class="break-words whitespace-normal"
|
||||||
|
:class="getTextClass()"
|
||||||
|
>
|
||||||
{{ props.info?.title }}
|
{{ props.info?.title }}
|
||||||
</VCardSubtitle>
|
</VCardSubtitle>
|
||||||
|
|
||||||
<VCardText class="text-subtitle-1 pt-3 pb-1" :class="getTextClass()">
|
<VCardText
|
||||||
|
class="text-subtitle-1 pt-3 pb-1"
|
||||||
|
:class="getTextClass()"
|
||||||
|
>
|
||||||
{{ getSpeedText() }}
|
{{ getSpeedText() }}
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
|
||||||
<VCardText v-if="getPercentage() > 0" :class="getTextClass()">
|
<VCardText
|
||||||
|
v-if="getPercentage() > 0"
|
||||||
|
:class="getTextClass()"
|
||||||
|
>
|
||||||
<VProgressLinear :model-value="getPercentage()" />
|
<VProgressLinear :model-value="getPercentage()" />
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
|
||||||
<VCardActions class="justify-space-between">
|
<VCardActions class="justify-space-between">
|
||||||
<VBtn :icon="`${isDownloading ? 'mdi-pause' : 'mdi-play'}`" @click="toggleDownload" />
|
<VBtn
|
||||||
<VBtn color="error" icon="mdi-trash-can-outline" @click="deleteDownload" />
|
:icon="`${isDownloading ? 'mdi-pause' : 'mdi-play'}`"
|
||||||
|
@click="toggleDownload"
|
||||||
|
/>
|
||||||
|
<VBtn
|
||||||
|
color="error"
|
||||||
|
icon="mdi-trash-can-outline"
|
||||||
|
@click="deleteDownload"
|
||||||
|
/>
|
||||||
</VCardActions>
|
</VCardActions>
|
||||||
</VCard>
|
</VCard>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { innerFilterRules } from '@/api/constants'
|
|
||||||
import { CustomRule } from '@/api/types'
|
|
||||||
import { cloneDeep } from 'lodash'
|
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
pri: String,
|
pri: String,
|
||||||
|
maxpri: String,
|
||||||
rules: Array as PropType<string[]>,
|
rules: Array as PropType<string[]>,
|
||||||
custom_rules: Array as PropType<CustomRule[]>,
|
width: String,
|
||||||
|
height: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
// 定义触发的自定义事件
|
||||||
@@ -23,25 +21,59 @@ function filtersChanged(value: string[]) {
|
|||||||
emit('changed', props.pri, value)
|
emit('changed', props.pri, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤规则下拉框
|
// 清洗规则中的换行符和多余空格,并在前后添加空格
|
||||||
const selectFilterOptions = ref<{ [key: string]: string }[]>([])
|
const cleanedRules = computed(() => {
|
||||||
|
return props.rules.map(rule => {
|
||||||
onMounted(() => {
|
rule = rule ?? ''
|
||||||
selectFilterOptions.value = cloneDeep(innerFilterRules)
|
return ` ${rule.replace(/[\r\n]/g, '').replace(/\s+/g, '')} `
|
||||||
if (props.custom_rules) {
|
})
|
||||||
console.log(props.custom_rules)
|
|
||||||
props.custom_rules.map(rule => {
|
|
||||||
selectFilterOptions.value.push({
|
|
||||||
title: rule.name,
|
|
||||||
value: rule.id,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 过滤规则下拉框
|
||||||
|
const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
||||||
|
{ title: '特效字幕', value: ' SPECSUB ' },
|
||||||
|
{ title: '中文字幕', value: ' CNSUB ' },
|
||||||
|
{ title: '国语配音', value: ' CNVOI ' },
|
||||||
|
{ title: '官种', value: ' GZ ' },
|
||||||
|
{ title: '排除: 国语配音', value: ' !CNVOI ' },
|
||||||
|
{ title: '粤语配音', value: ' HKVOI ' },
|
||||||
|
{ title: '排除: 粤语配音', value: ' !HKVOI ' },
|
||||||
|
{ title: '促销: 免费', value: ' FREE ' },
|
||||||
|
{ title: '分辨率: 4K', value: ' 4K ' },
|
||||||
|
{ title: '分辨率: 1080P', value: ' 1080P ' },
|
||||||
|
{ title: '分辨率: 720P', value: ' 720P ' },
|
||||||
|
{ title: '排除: 720P', value: ' !720P ' },
|
||||||
|
{ title: '质量: 蓝光原盘', value: ' BLU ' },
|
||||||
|
{ title: '排除: 蓝光原盘', value: ' !BLU ' },
|
||||||
|
{ title: '质量: BLURAY', value: ' BLURAY ' },
|
||||||
|
{ title: '排除: BLURAY', value: ' !BLURAY ' },
|
||||||
|
{ title: '质量: UHD', value: ' UHD ' },
|
||||||
|
{ title: '排除: UHD', value: ' !UHD ' },
|
||||||
|
{ title: '质量: REMUX', value: ' REMUX ' },
|
||||||
|
{ title: '排除: REMUX', value: ' !REMUX ' },
|
||||||
|
{ title: '质量: WEB-DL', value: ' WEBDL ' },
|
||||||
|
{ title: '排除: WEB-DL', value: ' !WEBDL ' },
|
||||||
|
{ title: '质量: 60fps', value: ' 60FPS ' },
|
||||||
|
{ title: '排除: 60fps', value: ' !60FPS ' },
|
||||||
|
{ title: '编码: H265', value: ' H265 ' },
|
||||||
|
{ title: '排除: H265', value: ' !H265 ' },
|
||||||
|
{ title: '编码: H264', value: ' H264 ' },
|
||||||
|
{ title: '排除: H264', value: ' !H264 ' },
|
||||||
|
{ title: '效果: 杜比视界', value: ' DOLBY ' },
|
||||||
|
{ title: '排除: 杜比视界', value: ' !DOLBY ' },
|
||||||
|
{ title: '效果: 杜比全景声', value: ' ATMOS ' },
|
||||||
|
{ title: '排除: 杜比全景声', value: ' !ATMOS ' },
|
||||||
|
{ title: '效果: HDR', value: ' HDR ' },
|
||||||
|
{ title: '排除: HDR', value: ' !HDR ' },
|
||||||
|
{ title: '效果: SDR', value: ' SDR ' },
|
||||||
|
{ title: '排除: SDR', value: ' !SDR ' },
|
||||||
|
{ title: '效果: 3D', value: ' 3D ' },
|
||||||
|
{ title: '排除: 3D', value: ' !3D ' },
|
||||||
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VCard variant="tonal">
|
<VCard variant="tonal" :width="props.width" :height="props.height">
|
||||||
<span class="absolute top-3 right-12">
|
<span class="absolute top-3 right-12">
|
||||||
<IconBtn>
|
<IconBtn>
|
||||||
<VIcon class="cursor-move" icon="mdi-drag" />
|
<VIcon class="cursor-move" icon="mdi-drag" />
|
||||||
@@ -53,13 +85,12 @@ onMounted(() => {
|
|||||||
<VRow>
|
<VRow>
|
||||||
<VCol>
|
<VCol>
|
||||||
<VSelect
|
<VSelect
|
||||||
v-model="props.rules"
|
v-model="cleanedRules"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
:items="selectFilterOptions"
|
:items="selectFilterOptions"
|
||||||
chips
|
chips
|
||||||
label=""
|
label=""
|
||||||
multiple
|
multiple
|
||||||
clearable
|
|
||||||
@update:modelValue="filtersChanged"
|
@update:modelValue="filtersChanged"
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
|||||||
@@ -1,307 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import draggable from 'vuedraggable'
|
|
||||||
import { copyToClipboard } from '@/@core/utils/navigator'
|
|
||||||
import { CustomRule, FilterRuleGroup } from '@/api/types'
|
|
||||||
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import ImportCodeDialog from '@/components/dialog/ImportCodeDialog.vue'
|
|
||||||
import filter_group_svg from '@images/svg/filter-group.svg'
|
|
||||||
import { cloneDeep } from 'lodash'
|
|
||||||
|
|
||||||
// 输入参数
|
|
||||||
const props = defineProps({
|
|
||||||
// 单个规则组
|
|
||||||
group: {
|
|
||||||
type: Object as PropType<FilterRuleGroup>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
// 所有规则组
|
|
||||||
groups: {
|
|
||||||
type: Array as PropType<FilterRuleGroup[]>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
// 媒体类型字典
|
|
||||||
categories: {
|
|
||||||
type: Object as PropType<{ [key: string]: any }>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
// 自定义规则列表
|
|
||||||
custom_rules: Array as PropType<CustomRule[]>,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 规则卡片类型
|
|
||||||
interface FilterCard {
|
|
||||||
// 优先级
|
|
||||||
pri: string
|
|
||||||
// 已选规则
|
|
||||||
rules: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提示框
|
|
||||||
const $toast = useToast()
|
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
|
||||||
const emit = defineEmits(['close', 'change', 'done'])
|
|
||||||
|
|
||||||
// 规则详情弹窗
|
|
||||||
const groupInfoDialog = ref(false)
|
|
||||||
|
|
||||||
// 规则详情
|
|
||||||
const groupInfo = ref<FilterRuleGroup>({
|
|
||||||
name: props.group?.name ?? '',
|
|
||||||
rule_string: props.group?.rule_string ?? '',
|
|
||||||
media_type: props.group?.media_type ?? '',
|
|
||||||
category: props.group?.category ?? '',
|
|
||||||
})
|
|
||||||
|
|
||||||
// 媒体类型字典
|
|
||||||
const mediaTypeItems = [
|
|
||||||
{ title: '通用', value: '' },
|
|
||||||
{ title: '电影', value: '电影' },
|
|
||||||
{ title: '电视剧', value: '电视剧' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 根据选中的媒体类型,获取对应的媒体类别
|
|
||||||
const getCategories = computed(() => {
|
|
||||||
const default_value = [{ title: '全部', value: '' }]
|
|
||||||
if (!props.categories || !groupInfo.value.media_type || !props.categories[groupInfo.value.media_type]) {
|
|
||||||
return default_value
|
|
||||||
}
|
|
||||||
return default_value.concat(props.categories[groupInfo.value.media_type] || [])
|
|
||||||
})
|
|
||||||
|
|
||||||
// 规则组规则卡片列表
|
|
||||||
const filterRuleCards = ref<FilterCard[]>([])
|
|
||||||
// 规则组类型,仅用于导入判断
|
|
||||||
const filterRuleCardsType = ref<FilterCard>({
|
|
||||||
pri: '',
|
|
||||||
rules: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
// 导入代码弹窗
|
|
||||||
const importCodeDialog = ref(false)
|
|
||||||
|
|
||||||
// 导入代码类型
|
|
||||||
const importCodeType = ref('')
|
|
||||||
|
|
||||||
// 更新规则卡片的值
|
|
||||||
function updateFilterCardValue(pri: string, rules: string[]) {
|
|
||||||
const card = filterRuleCards.value.find(card => card.pri === pri)
|
|
||||||
if (card && Array.isArray(rules)) card.rules = rules
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除卡片
|
|
||||||
function filterCardClose(pri: string) {
|
|
||||||
filterRuleCards.value = filterRuleCards.value
|
|
||||||
.filter(card => card.pri !== pri)
|
|
||||||
.map((card, index) => {
|
|
||||||
card.pri = (index + 1).toString()
|
|
||||||
return card
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分享规则
|
|
||||||
async function shareRules() {
|
|
||||||
if (filterRuleCards.value.length === 0) return
|
|
||||||
|
|
||||||
const value = filterRuleCards.value
|
|
||||||
.filter(card => Array.isArray(card.rules) && card.rules.length > 0)
|
|
||||||
.map(card => card.rules.join('&'))
|
|
||||||
.join('>')
|
|
||||||
|
|
||||||
try {
|
|
||||||
let success
|
|
||||||
success = copyToClipboard(value)
|
|
||||||
if (await success) $toast.success('优先级规则已复制到剪贴板!')
|
|
||||||
else $toast.error('优先级规则复制失败:可能是浏览器不支持或被用户阻止!')
|
|
||||||
} catch (error) {
|
|
||||||
$toast.error('优先级规则复制失败!')
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导入规则
|
|
||||||
async function importRules(ruleType: string) {
|
|
||||||
importCodeType.value = ruleType
|
|
||||||
importCodeDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存导入的代码,直接覆盖原有值
|
|
||||||
function saveCodeString(type: string, code: any) {
|
|
||||||
try {
|
|
||||||
code = code.value
|
|
||||||
if (type === 'priority') {
|
|
||||||
// 解析值
|
|
||||||
if (!code) return
|
|
||||||
// 首尾增加空格
|
|
||||||
if (!code.startsWith(' ')) code = ` ${code}`
|
|
||||||
if (!code.endsWith(' ')) code = `${code} `
|
|
||||||
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() {
|
|
||||||
const pri = (filterRuleCards.value.length + 1).toString()
|
|
||||||
const newCard: FilterCard = { pri, rules: [] }
|
|
||||||
filterRuleCards.value.push(newCard)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据列表的拖动顺序更新优先级
|
|
||||||
function dragOrderEnd() {
|
|
||||||
filterRuleCards.value.forEach((card, index) => {
|
|
||||||
card.pri = (index + 1).toString()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开详情弹窗
|
|
||||||
function opengroupInfoDialog() {
|
|
||||||
groupInfo.value = cloneDeep(props.group)
|
|
||||||
if (props.group.rule_string) {
|
|
||||||
filterRuleCards.value = props.group.rule_string.split('>').map((group: string, index: number) => ({
|
|
||||||
pri: (index + 1).toString(),
|
|
||||||
rules: group.split('&').filter(rule => rule),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
groupInfoDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存详情数据
|
|
||||||
function saveGroupInfo() {
|
|
||||||
if (!groupInfo.value.name.trim()) {
|
|
||||||
$toast.error('规则组名称不能为空')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (props.groups.some(item => item.name === groupInfo.value.name && item !== props.group)) {
|
|
||||||
$toast.error(`规则组名称【${groupInfo.value.name}】已存在,请替换`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
groupInfoDialog.value = false
|
|
||||||
groupInfo.value.rule_string = filterRuleCards.value
|
|
||||||
.filter(card => Array.isArray(card.rules) && card.rules.length > 0)
|
|
||||||
.map(card => card.rules.join('&'))
|
|
||||||
.join('>')
|
|
||||||
emit('change', groupInfo.value, props.group.name)
|
|
||||||
emit('done')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按钮点击
|
|
||||||
function onClose() {
|
|
||||||
emit('close')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<VCard variant="tonal" @click="opengroupInfoDialog">
|
|
||||||
<span class="absolute top-3 right-12">
|
|
||||||
<IconBtn>
|
|
||||||
<VIcon class="cursor-move" icon="mdi-drag" />
|
|
||||||
</IconBtn>
|
|
||||||
</span>
|
|
||||||
<DialogCloseBtn @click="onClose" />
|
|
||||||
<VCardText class="flex justify-space-between align-center gap-3">
|
|
||||||
<div class="align-self-start">
|
|
||||||
<h5 class="text-h6 mb-1">{{ props.group.name }}</h5>
|
|
||||||
<div class="text-body-1 mb-3">
|
|
||||||
<span v-if="!props.group.category">{{ props.group.media_type || '通用' }}</span>
|
|
||||||
<span v-else>{{ props.group.category }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<VImg :src="filter_group_svg" cover class="mt-10" max-width="3rem" />
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
<VDialog v-if="groupInfoDialog" v-model="groupInfoDialog" scrollable max-width="80rem" persistent>
|
|
||||||
<VCard :title="`${props.group.name} - 配置`" class="rounded-t">
|
|
||||||
<DialogCloseBtn v-model="groupInfoDialog" />
|
|
||||||
<VDivider />
|
|
||||||
<VCardItem class="pt-1">
|
|
||||||
<VRow class="mt-1">
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="groupInfo.name"
|
|
||||||
label="规则组名称"
|
|
||||||
placeholder="必填;不可与其他规则组重名"
|
|
||||||
hint="自定义规则组名称"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="6" md="3">
|
|
||||||
<VSelect
|
|
||||||
v-model="groupInfo.media_type"
|
|
||||||
label="适用媒体类型"
|
|
||||||
:items="mediaTypeItems"
|
|
||||||
hint="选择规则组适用的媒体类型"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="6" md="3">
|
|
||||||
<VSelect
|
|
||||||
v-model="groupInfo.category"
|
|
||||||
:items="getCategories"
|
|
||||||
label="适用媒体类别"
|
|
||||||
hint="选择规则组适用的媒体类别"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VCardItem>
|
|
||||||
<VCardText>
|
|
||||||
<draggable
|
|
||||||
v-model="filterRuleCards"
|
|
||||||
handle=".cursor-move"
|
|
||||||
item-key="pri"
|
|
||||||
tag="div"
|
|
||||||
@end="dragOrderEnd"
|
|
||||||
:component-data="{ 'class': 'grid gap-3 grid-filterrule-card' }"
|
|
||||||
>
|
|
||||||
<template #item="{ element }">
|
|
||||||
<FilterRuleCard
|
|
||||||
:pri="element.pri"
|
|
||||||
:maxpri="filterRuleCards.length.toString()"
|
|
||||||
:rules="element.rules"
|
|
||||||
:custom_rules="props.custom_rules"
|
|
||||||
@changed="updateFilterCardValue"
|
|
||||||
@close="filterCardClose(element.pri)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</draggable>
|
|
||||||
<div class="text-center" v-if="filterRuleCards.length == 0">请添加或导入规则</div>
|
|
||||||
</VCardText>
|
|
||||||
<VCardActions class="pt-3">
|
|
||||||
<VBtn color="primary" variant="tonal" @click="addFilterCard">
|
|
||||||
<VIcon icon="mdi-plus" />
|
|
||||||
</VBtn>
|
|
||||||
<VBtn color="success" variant="tonal" @click="importRules('priority')">
|
|
||||||
<VIcon icon="mdi-import" />
|
|
||||||
</VBtn>
|
|
||||||
<VBtn color="info" variant="tonal" @click="shareRules">
|
|
||||||
<VIcon icon="mdi-share" />
|
|
||||||
</VBtn>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn @click="saveGroupInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 确定 </VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
<ImportCodeDialog
|
|
||||||
v-if="importCodeDialog"
|
|
||||||
v-model="importCodeDialog"
|
|
||||||
title="导入规则优先级"
|
|
||||||
:dataType="importCodeType"
|
|
||||||
@close="importCodeDialog = false"
|
|
||||||
@save="saveCodeString"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -91,17 +91,7 @@ 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
|
||||||
try {
|
await new Promise(resolve => (img.onload = resolve))
|
||||||
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
|
||||||
@@ -181,3 +171,9 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</VHover>
|
</VHover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.text-shadow {
|
||||||
|
text-shadow: 1px 1px #777;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { formatSeason } 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, TmdbSeason } from '@/api/types'
|
||||||
import router, { registerAbortController } from '@/router'
|
import router 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'
|
||||||
@@ -19,9 +19,6 @@ const props = defineProps({
|
|||||||
height: String,
|
height: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 从 provide 中获取全局设置
|
|
||||||
const globalSettings: any = inject('globalSettings')
|
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
@@ -67,18 +64,11 @@ const sourceIconDict: { [key: string]: any } = {
|
|||||||
bangumi: bangumiImage,
|
bangumi: bangumiImage,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定MediaCard元素
|
|
||||||
const mediaCardRef = ref<HTMLElement | null>(null)
|
|
||||||
|
|
||||||
// 创建Intersection Observer实例
|
|
||||||
const observer = ref<IntersectionObserver | null>(null)
|
|
||||||
|
|
||||||
// 获得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 if (props.media?.bangumi_id) return `bangumi:${props.media?.bangumi_id}`
|
else return `bangumi:${props.media?.bangumi_id}`
|
||||||
else return `${props.media?.mediaid_prefix}:${props.media?.media_id}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 订阅弹窗选择的多季
|
// 订阅弹窗选择的多季
|
||||||
@@ -97,6 +87,7 @@ function getChipColor(type: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 添加订阅处理
|
// 添加订阅处理
|
||||||
|
|
||||||
async function handleAddSubscribe() {
|
async function handleAddSubscribe() {
|
||||||
if (props.media?.type === '电视剧' && props.media?.tmdb_id) {
|
if (props.media?.type === '电视剧' && props.media?.tmdb_id) {
|
||||||
// TMDB电视剧
|
// TMDB电视剧
|
||||||
@@ -147,7 +138,6 @@ 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,
|
||||||
})
|
})
|
||||||
@@ -226,9 +216,6 @@ 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,
|
||||||
@@ -237,7 +224,6 @@ 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
|
||||||
@@ -249,16 +235,13 @@ 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
|
||||||
@@ -281,6 +264,7 @@ 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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -349,26 +333,13 @@ function getExistText(season: number) {
|
|||||||
// 打开详情页
|
// 打开详情页
|
||||||
function goMediaDetail(isHovering = false) {
|
function goMediaDetail(isHovering = false) {
|
||||||
if (isHovering) {
|
if (isHovering) {
|
||||||
if (props.media?.collection_id) {
|
router.push({
|
||||||
// 跳转到合集列表
|
path: '/media',
|
||||||
router.push({
|
query: {
|
||||||
path: `/browse/tmdb/collection/${props.media?.collection_id}`,
|
mediaid: getMediaId(),
|
||||||
query: {
|
type: props.media?.type,
|
||||||
title: props.media?.title,
|
},
|
||||||
},
|
})
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 跳转到媒体详情页
|
|
||||||
router.push({
|
|
||||||
path: '/media',
|
|
||||||
query: {
|
|
||||||
mediaid: getMediaId(),
|
|
||||||
title: props.media?.title,
|
|
||||||
year: props.media?.year,
|
|
||||||
type: props.media?.type,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,69 +351,32 @@ 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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 懒加载检查
|
// 装载时检查是否已订阅
|
||||||
function handleCheckLazy() {
|
onBeforeMount(() => {
|
||||||
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
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算图片地址
|
// 计算图片地址
|
||||||
const getImgUrl: Ref<string> = computed(() => {
|
const getImgUrl: Ref<string> = computed(() => {
|
||||||
if (imageLoadError.value) return noImage
|
if (imageLoadError.value) return noImage
|
||||||
const url = props.media?.poster_path?.replace('original', 'w500') ?? noImage
|
const url = props.media?.poster_path?.replace('original', 'w500') ?? noImage
|
||||||
// 使用图片缓存
|
|
||||||
if (globalSettings.GLOBAL_IMAGE_CACHE)
|
|
||||||
return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`
|
|
||||||
// 如果地址中包含douban则使用中转代理
|
// 如果地址中包含douban则使用中转代理
|
||||||
if (url.includes('doubanio.com'))
|
if (url.includes('doubanio.com'))
|
||||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
|
return `${import.meta.env.VITE_API_BASE_URL}douban/img?imgurl=${encodeURIComponent(url)}`
|
||||||
|
|
||||||
return url
|
return url
|
||||||
})
|
})
|
||||||
|
|
||||||
// 拼装季图片地址
|
// 拼装季图片地址
|
||||||
function getSeasonPoster(posterPath: string) {
|
function getSeasonPoster(posterPath: string) {
|
||||||
if (!posterPath) return ''
|
if (!posterPath) return ''
|
||||||
return `https://${globalSettings.TMDB_IMAGE_DOMAIN}/t/p/w500${posterPath}`
|
return `https://image.tmdb.org/t/p/w500${posterPath}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将yyyy-mm-dd转换为yyyy年mm月dd日
|
// 将yyyy-mm-dd转换为yyyy年mm月dd日
|
||||||
@@ -451,102 +385,92 @@ function formatAirDate(airDate: string) {
|
|||||||
const date = new Date(airDate.replaceAll(/-/g, '/'))
|
const date = new Date(airDate.replaceAll(/-/g, '/'))
|
||||||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
|
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从yyyy-mm-dd中提取年份
|
// 从yyyy-mm-dd中提取年份
|
||||||
function getYear(airDate: string) {
|
function getYear(airDate: string) {
|
||||||
if (!airDate) return ''
|
if (!airDate) return ''
|
||||||
const date = new Date(airDate.replaceAll(/-/g, '/'))
|
const date = new Date(airDate.replaceAll(/-/g, '/'))
|
||||||
return date.getFullYear()
|
return date.getFullYear()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除订阅
|
|
||||||
function onRemoveSubscribe() {
|
|
||||||
subscribeEditDialog.value = false
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VHover>
|
<VHover>
|
||||||
<template #default="hover">
|
<template #default="hover">
|
||||||
<div ref="mediaCardRef">
|
<VCard
|
||||||
<VCard
|
v-bind="hover.props"
|
||||||
v-bind="hover.props"
|
:height="props.height"
|
||||||
:height="props.height"
|
:width="props.width"
|
||||||
:width="props.width"
|
class="outline-none shadow ring-gray-500 rounded-lg"
|
||||||
class="outline-none shadow ring-gray-500 rounded-lg"
|
:class="{
|
||||||
:class="{
|
'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="goMediaDetail(hover.isHovering)"
|
||||||
@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"
|
||||||
>
|
>
|
||||||
<VImg
|
<template #placeholder>
|
||||||
aspect-ratio="2/3"
|
<div class="w-full h-full">
|
||||||
:src="getImgUrl"
|
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||||
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"
|
|
||||||
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">
|
|
||||||
<IconBtn icon="mdi-magnify" color="white" @click.stop="handleSearch" />
|
|
||||||
<IconBtn icon="mdi-heart" :color="isSubscribed ? 'error' : 'white'" @click.stop="handleSubscribe" />
|
|
||||||
</div>
|
</div>
|
||||||
</VCardText>
|
</template>
|
||||||
<!-- 类型角标 -->
|
</VImg>
|
||||||
<VChip
|
<!-- 类型角标 -->
|
||||||
v-show="isImageLoaded"
|
<VChip
|
||||||
variant="elevated"
|
v-show="isImageLoaded"
|
||||||
size="small"
|
variant="elevated"
|
||||||
:class="getChipColor(props.media?.type || '')"
|
size="small"
|
||||||
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
:class="getChipColor(props.media?.type || '')"
|
||||||
>
|
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||||
{{ props.media?.type }}
|
>
|
||||||
</VChip>
|
{{ props.media?.type }}
|
||||||
<!-- 本地存在标识 -->
|
</VChip>
|
||||||
<ExistIcon v-if="isExists && !hover.isHovering" />
|
<!-- 本地存在标识 -->
|
||||||
<!-- 评分角标 -->
|
<ExistIcon v-if="isExists && !hover.isHovering" />
|
||||||
<VChip
|
<!-- 评分角标 -->
|
||||||
v-if="isImageLoaded && props.media?.vote_average && !(isExists && !hover.isHovering)"
|
<VChip
|
||||||
variant="elevated"
|
v-if="isImageLoaded && props.media?.vote_average && !(isExists && !hover.isHovering)"
|
||||||
size="small"
|
variant="elevated"
|
||||||
:class="getChipColor('rating')"
|
size="small"
|
||||||
class="absolute right-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
:class="getChipColor('rating')"
|
||||||
>
|
class="absolute right-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||||
{{ props.media?.vote_average }}
|
>
|
||||||
</VChip>
|
{{ props.media?.vote_average }}
|
||||||
<!--来源图标-->
|
</VChip>
|
||||||
<VAvatar
|
<!-- 详情 -->
|
||||||
size="24"
|
<VCardText
|
||||||
density="compact"
|
v-show="hover.isHovering || imageLoadError"
|
||||||
class="absolute bottom-1 right-1"
|
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||||
tile
|
>
|
||||||
v-if="!hover.isHovering && isImageLoaded && props.media?.source && !imageLoadError"
|
<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 ...">
|
||||||
<VImg cover :src="sourceIconDict[props.media?.source]" class="shadow-lg" />
|
{{ props.media?.title }}
|
||||||
</VAvatar>
|
</h1>
|
||||||
</VCard>
|
<p class="leading-4 line-clamp-4 overflow-hidden text-ellipsis ...">
|
||||||
</div>
|
{{ props.media?.overview }}
|
||||||
|
</p>
|
||||||
|
<div class="flex align-center justify-between">
|
||||||
|
<IconBtn icon="mdi-magnify" color="white" @click.stop="handleSearch" />
|
||||||
|
<IconBtn icon="mdi-heart" :color="isSubscribed ? 'error' : 'white'" @click.stop="handleSubscribe" />
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
<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>
|
||||||
<!-- 订阅季弹窗 -->
|
<!-- 订阅季弹窗 -->
|
||||||
@@ -613,6 +537,17 @@ function onRemoveSubscribe() {
|
|||||||
:subid="subscribeId"
|
:subid="subscribeId"
|
||||||
@close="subscribeEditDialog = false"
|
@close="subscribeEditDialog = false"
|
||||||
@save="subscribeEditDialog = false"
|
@save="subscribeEditDialog = false"
|
||||||
@remove="onRemoveSubscribe"
|
@remove="
|
||||||
|
() => {
|
||||||
|
subscribeEditDialog = false
|
||||||
|
handleCheckSubscribe()
|
||||||
|
}
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</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'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
context: Object as PropType<Context>,
|
context: Object as PropType<Context>,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -45,7 +45,8 @@ 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 }}
|
||||||
|
|||||||
@@ -1,349 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { MediaServerConf, MediaServerLibrary, MediaStatistic } from '@/api/types'
|
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import emby_image from '@images/logos/emby.png'
|
|
||||||
import jellyfin_image from '@images/logos/jellyfin.png'
|
|
||||||
import plex_image from '@images/logos/plex.png'
|
|
||||||
import api from '@/api'
|
|
||||||
import { cloneDeep } from 'lodash'
|
|
||||||
|
|
||||||
// 定义输入
|
|
||||||
const props = defineProps({
|
|
||||||
// 单个媒体服务器
|
|
||||||
mediaserver: {
|
|
||||||
type: Object as PropType<MediaServerConf>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
// 所有媒体服务器
|
|
||||||
mediaservers: {
|
|
||||||
type: Array as PropType<MediaServerConf[]>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 提示框
|
|
||||||
const $toast = useToast()
|
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
|
||||||
const emit = defineEmits(['close', 'done', 'change'])
|
|
||||||
|
|
||||||
// 媒体统计数据
|
|
||||||
const infoItems = ref([
|
|
||||||
{
|
|
||||||
avatar: 'mdi-movie-roll',
|
|
||||||
title: '电影',
|
|
||||||
amount: '0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'mdi-television-box',
|
|
||||||
title: '电视剧',
|
|
||||||
amount: '0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'mdi-account',
|
|
||||||
title: '用户',
|
|
||||||
amount: '0',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
// 同步媒体库选项
|
|
||||||
const librariesOptions = ref<{ title: string; value: string | undefined }[]>([
|
|
||||||
{
|
|
||||||
title: '全部',
|
|
||||||
value: 'all',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
// 媒体服务器详情弹窗
|
|
||||||
const mediaServerInfoDialog = ref(false)
|
|
||||||
|
|
||||||
// 媒体服务器详情
|
|
||||||
const mediaServerInfo = ref<MediaServerConf>({
|
|
||||||
name: '',
|
|
||||||
type: '',
|
|
||||||
enabled: false,
|
|
||||||
config: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 打开详情弹窗
|
|
||||||
function openMediaServerInfoDialog() {
|
|
||||||
loadLibrary(props.mediaserver.name)
|
|
||||||
// 深复制
|
|
||||||
mediaServerInfo.value = cloneDeep(props.mediaserver)
|
|
||||||
mediaServerInfoDialog.value = true
|
|
||||||
if (!props.mediaserver.sync_libraries) {
|
|
||||||
mediaServerInfo.value.sync_libraries = ['all']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存详情数据
|
|
||||||
function saveMediaServerInfo() {
|
|
||||||
// 为空不保存,跳出警告框
|
|
||||||
if (!mediaServerInfo.value.name) {
|
|
||||||
$toast.error('名称不能为空,请输入后再确定')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 重名判断
|
|
||||||
if (props.mediaservers.some(item => item.name === mediaServerInfo.value.name && item !== props.mediaserver)) {
|
|
||||||
$toast.error(`【${mediaServerInfo.value.name}】已存在,请替换为其他名称`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 执行保存
|
|
||||||
mediaServerInfoDialog.value = false
|
|
||||||
emit('change', mediaServerInfo.value, props.mediaserver.name)
|
|
||||||
emit('done')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据存储类型选择图标
|
|
||||||
const getIcon = computed(() => {
|
|
||||||
switch (props.mediaserver.type) {
|
|
||||||
case 'emby':
|
|
||||||
return emby_image
|
|
||||||
case 'jellyfin':
|
|
||||||
return jellyfin_image
|
|
||||||
default:
|
|
||||||
return plex_image
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 按钮点击
|
|
||||||
function onClose() {
|
|
||||||
emit('close')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用API加载媒体统计数据
|
|
||||||
async function loadMediaStatistic() {
|
|
||||||
try {
|
|
||||||
const res: MediaStatistic = await api.get('dashboard/statistic', {
|
|
||||||
params: {
|
|
||||||
name: props.mediaserver.name,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
infoItems.value = [
|
|
||||||
{
|
|
||||||
avatar: 'mdi-movie-roll',
|
|
||||||
title: '电影',
|
|
||||||
amount: res.movie_count.toLocaleString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'mdi-television-box',
|
|
||||||
title: '电视剧',
|
|
||||||
amount: res.tv_count.toLocaleString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
avatar: 'mdi-account',
|
|
||||||
title: '用户',
|
|
||||||
amount: res.user_count.toLocaleString(),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用API查询媒体库
|
|
||||||
async function loadLibrary(server: string) {
|
|
||||||
try {
|
|
||||||
const result: MediaServerLibrary[] = await api.get('mediaserver/library', { params: { server } })
|
|
||||||
if (result && result.length > 0) {
|
|
||||||
librariesOptions.value = result.map(item => ({
|
|
||||||
title: item.name,
|
|
||||||
value: item.id?.toString(),
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
librariesOptions.value = []
|
|
||||||
}
|
|
||||||
librariesOptions.value.unshift({
|
|
||||||
title: '全部',
|
|
||||||
value: 'all',
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadMediaStatistic()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<VCard variant="tonal" @click="openMediaServerInfoDialog">
|
|
||||||
<DialogCloseBtn @click="onClose" />
|
|
||||||
<VCardText class="flex justify-space-between align-center gap-3">
|
|
||||||
<div class="align-self-start flex-1">
|
|
||||||
<div class="text-h6 mb-1">{{ mediaserver.name }}</div>
|
|
||||||
<div class="text-sm mt-5 flex flex-wrap">
|
|
||||||
<span v-for="item in infoItems" :key="item.title" class="me-2 mb-1">
|
|
||||||
<VIcon rounded :icon="item.avatar" class="me-1" />{{ item.amount }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" min-width="3rem" />
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
<VDialog v-if="mediaServerInfoDialog" v-model="mediaServerInfoDialog" scrollable max-width="40rem" persistent>
|
|
||||||
<VCard :title="`${props.mediaserver.name} - 配置`" class="rounded-t">
|
|
||||||
<DialogCloseBtn v-model="mediaServerInfoDialog" />
|
|
||||||
<VDivider />
|
|
||||||
<VCardText>
|
|
||||||
<VForm>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VSwitch v-model="mediaServerInfo.enabled" label="启用媒体服务器" />
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow v-if="mediaServerInfo.type == 'emby'">
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="mediaServerInfo.name"
|
|
||||||
label="名称"
|
|
||||||
placeholder="必填;不可与其他名称重名"
|
|
||||||
hint="媒体服务器的别名"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="mediaServerInfo.config.host"
|
|
||||||
label="地址"
|
|
||||||
placeholder="http(s)://ip:port"
|
|
||||||
hint="服务端地址,格式:http(s)://ip:port"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="mediaServerInfo.config.play_host"
|
|
||||||
label="外网播放地址"
|
|
||||||
placeholder="http(s)://domain:port"
|
|
||||||
hint="跳转播放页面使用的地址,格式:http(s)://domain:port"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="mediaServerInfo.config.apikey"
|
|
||||||
label="API密钥"
|
|
||||||
hint="Emby设置->高级->API密钥中生成的密钥"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow v-if="mediaServerInfo.type == 'jellyfin'">
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="mediaServerInfo.name"
|
|
||||||
label="名称"
|
|
||||||
placeholder="必填;不可与其他名称重名"
|
|
||||||
hint="媒体服务器的别名"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="mediaServerInfo.config.host"
|
|
||||||
label="地址"
|
|
||||||
placeholder="http(s)://ip:port"
|
|
||||||
hint="服务端地址,格式:http(s)://ip:port"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="mediaServerInfo.config.play_host"
|
|
||||||
label="外网播放地址"
|
|
||||||
placeholder="http(s)://domain:port"
|
|
||||||
hint="跳转播放页面使用的地址,格式:http(s)://domain:port"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="mediaServerInfo.config.apikey"
|
|
||||||
label="API密钥"
|
|
||||||
hint="Jellyfin设置->高级->API密钥中生成的密钥"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow v-if="mediaServerInfo.type == 'plex'">
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="mediaServerInfo.name"
|
|
||||||
label="名称"
|
|
||||||
placeholder="必填;不可与其他名称重名"
|
|
||||||
hint="媒体服务器的别名"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="mediaServerInfo.config.host"
|
|
||||||
label="地址"
|
|
||||||
placeholder="http(s)://ip:port"
|
|
||||||
hint="服务端地址,格式:http(s)://ip:port"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="mediaServerInfo.config.play_host"
|
|
||||||
label="外网播放地址"
|
|
||||||
placeholder="http(s)://domain:port"
|
|
||||||
hint="跳转播放页面使用的地址,格式:http(s)://domain:port"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="mediaServerInfo.config.token"
|
|
||||||
label="X-Plex-Token"
|
|
||||||
hint="浏览器F12->网络,从Plex请求URL中获取的X-Plex-Token"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VSelect
|
|
||||||
v-model="mediaServerInfo.sync_libraries"
|
|
||||||
label="同步媒体库"
|
|
||||||
:items="librariesOptions"
|
|
||||||
chips
|
|
||||||
multiple
|
|
||||||
clearable
|
|
||||||
hint="只有选中的媒体库才会被同步"
|
|
||||||
persistent-hint
|
|
||||||
active
|
|
||||||
append-inner-icon="mdi-refresh"
|
|
||||||
@click:append-inner="loadLibrary(mediaServerInfo.name)"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
<VCardActions class="pt-3">
|
|
||||||
<VBtn @click="saveMediaServerInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
|
|
||||||
确定
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<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'
|
||||||
|
|
||||||
@@ -23,7 +22,8 @@ async function imageLoaded() {
|
|||||||
|
|
||||||
// 链接打开新窗口
|
// 链接打开新窗口
|
||||||
function openLink() {
|
function openLink() {
|
||||||
if (props.message?.link) window.open(props.message.link, '_blank')
|
if (props.message?.link)
|
||||||
|
window.open(props.message.link, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将note转换为json
|
// 将note转换为json
|
||||||
@@ -31,8 +31,9 @@ function noteToJson() {
|
|||||||
if (props.message?.note) {
|
if (props.message?.note) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(props.message.note)
|
return JSON.parse(props.message.note)
|
||||||
} catch (error) {
|
}
|
||||||
return props.message.note
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
@@ -40,53 +41,58 @@ function noteToJson() {
|
|||||||
|
|
||||||
// 将\n转换为html属性的换行符
|
// 将\n转换为html属性的换行符
|
||||||
function replaceNewLine(value: string) {
|
function replaceNewLine(value: string) {
|
||||||
if (!value) return ''
|
if (!value)
|
||||||
|
return ''
|
||||||
return value.replace(/\n/g, '<br/>')
|
return value.replace(/\n/g, '<br/>')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VCard variant="tonal" :width="props.width" :height="props.height" @click="openLink" max-width="23rem">
|
<VCard
|
||||||
<div v-if="props.message?.image" class="relative text-center card-cover-blurred">
|
:width="props.width"
|
||||||
|
:height="props.height"
|
||||||
|
variant="tonal"
|
||||||
|
@click="openLink"
|
||||||
|
>
|
||||||
|
<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="3/2"
|
aspect-ratio="4/3"
|
||||||
cover
|
cover
|
||||||
position="top"
|
|
||||||
:class="{ shadow: isImageLoaded }"
|
:class="{ shadow: isImageLoaded }"
|
||||||
@load="imageLoaded"
|
@load="imageLoaded"
|
||||||
@error="imageLoadError = true"
|
@error="imageLoadError = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<VCardTitle v-if="props.message?.title" class="whitespace-break-spaces">
|
||||||
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"
|
|
||||||
>
|
|
||||||
<p class="mb-0">{{ props.message?.title }}</p>
|
|
||||||
</div>
|
|
||||||
<VCardTitle v-else-if="props.message?.title" class="break-words whitespace-break-spaces">
|
|
||||||
{{ props.message?.title }}
|
{{ props.message?.title }}
|
||||||
</VCardTitle>
|
</VCardTitle>
|
||||||
<div
|
<VAlert
|
||||||
v-if="props.message?.text && props.message?.action === 0"
|
v-if="props.message?.text && props.message?.action === 0"
|
||||||
class="rounded-md text-body-1 py-2 px-4 elevation-2 bg-primary text-white chat-right mb-1"
|
variant="tonal"
|
||||||
|
type="success"
|
||||||
>
|
>
|
||||||
<p class="mb-0">{{ props.message?.text }}</p>
|
<template #prepend />
|
||||||
</div>
|
{{ props.message?.text }}
|
||||||
<VCardText v-if="props.message?.text && props.message?.action === 1" v-html="replaceNewLine(props.message?.text)" />
|
</VAlert>
|
||||||
<VCardText v-if="!isNullOrEmptyObject(props.message?.note)">
|
<VCardText
|
||||||
|
v-if="props.message?.text && props.message?.action === 1"
|
||||||
|
v-html="replaceNewLine(props.message?.text)"
|
||||||
|
/>
|
||||||
|
<VCardText v-if="props.message?.note">
|
||||||
<VList>
|
<VList>
|
||||||
<VListItem v-for="(value, key) in noteToJson()" :key="key" two-line>
|
<VListItem
|
||||||
<VListItemTitle v-if="value.title_year" class="font-bold break-words whitespace-break-spaces">
|
v-for="(value, key) in noteToJson()"
|
||||||
|
:key="key"
|
||||||
|
two-line
|
||||||
|
>
|
||||||
|
<VListItemTitle v-if="value.title_year" class="font-bold">
|
||||||
{{ key + 1 }}. {{ value.title_year }}
|
{{ key + 1 }}. {{ value.title_year }}
|
||||||
</VListItemTitle>
|
</VListItemTitle>
|
||||||
<VListItemTitle v-if="value.enclosure" class="font-bold break-words whitespace-break-spaces">
|
<VListItemTitle v-if="value.enclosure" class="font-bold 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">
|
||||||
@@ -98,11 +104,9 @@ function replaceNewLine(value: string) {
|
|||||||
</VListItem>
|
</VListItem>
|
||||||
</VList>
|
</VList>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
<div class="text-end">
|
||||||
|
<span v-if="props.message?.action === 0" class="text-sm italic me-2">{{ props.message?.userid }}</span>
|
||||||
|
<span class="text-sm italic me-2">{{ formatDateDifference(props.message?.reg_time || props.message?.date || '') }}</span>
|
||||||
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
<div class="text-end">
|
|
||||||
<span v-if="props.message?.action === 0" class="text-sm italic me-2">{{ props.message?.userid }}</span>
|
|
||||||
<span class="text-sm italic me-2">{{
|
|
||||||
formatDateDifference(props.message?.reg_time || props.message?.date || '')
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,401 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { NotificationConf } from '@/api/types'
|
|
||||||
import wechat_image from '@images/logos/wechat.png'
|
|
||||||
import telegram_image from '@images/logos/telegram.webp'
|
|
||||||
import vocechat_image from '@images/logos/vocechat.png'
|
|
||||||
import synologychat_image from '@images/logos/synologychat.png'
|
|
||||||
import slack_image from '@images/logos/slack.webp'
|
|
||||||
import chrome_image from '@images/logos/chrome.png'
|
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import { cloneDeep } from 'lodash'
|
|
||||||
|
|
||||||
// 定义输入
|
|
||||||
const props = defineProps({
|
|
||||||
// 单个通知
|
|
||||||
notification: {
|
|
||||||
type: Object as PropType<NotificationConf>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
// 所有通知
|
|
||||||
notifications: {
|
|
||||||
type: Array as PropType<NotificationConf[]>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
|
||||||
const emit = defineEmits(['close', 'change', 'done'])
|
|
||||||
|
|
||||||
// 提示框
|
|
||||||
const $toast = useToast()
|
|
||||||
|
|
||||||
// 通知详情弹窗
|
|
||||||
const notificationInfoDialog = ref(false)
|
|
||||||
|
|
||||||
// 通知详情
|
|
||||||
const notificationInfo = ref<NotificationConf>({
|
|
||||||
name: '',
|
|
||||||
type: '',
|
|
||||||
enabled: false,
|
|
||||||
config: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 各通知类型的名称字典
|
|
||||||
const notificationTypeNames: { [key: string]: string } = {
|
|
||||||
wechat: '企业微信',
|
|
||||||
telegram: 'Telegram',
|
|
||||||
vocechat: 'VoceChat',
|
|
||||||
synologychat: 'Synology Chat',
|
|
||||||
slack: 'Slack',
|
|
||||||
webpush: 'WebPush',
|
|
||||||
}
|
|
||||||
|
|
||||||
// 消息类型下拉字典
|
|
||||||
const notificationTypes = [
|
|
||||||
{ value: '资源下载', title: '资源下载' },
|
|
||||||
{ value: '整理入库', title: '整理入库' },
|
|
||||||
{ value: '订阅', title: '订阅' },
|
|
||||||
{ value: '站点', title: '站点' },
|
|
||||||
{ value: '媒体服务器', title: '媒体服务器' },
|
|
||||||
{ value: '手动处理', title: '手动处理' },
|
|
||||||
{ value: '插件', title: '插件' },
|
|
||||||
{ value: '其它', title: '其它' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 打开详情弹窗
|
|
||||||
function openNotificationInfoDialog() {
|
|
||||||
// 替换成深复制,避免修改时影响原数据
|
|
||||||
notificationInfo.value = cloneDeep(props.notification)
|
|
||||||
console.log(`当前卡片的通知信息:${JSON.stringify(notificationInfo.value)}`)
|
|
||||||
notificationInfoDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存详情数据
|
|
||||||
function saveNotificationInfo() {
|
|
||||||
// 为空不保存,跳出警告框
|
|
||||||
if (!notificationInfo.value.name) {
|
|
||||||
$toast.error('名称不能为空,请输入后再确定')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 重名判断
|
|
||||||
if (props.notifications.some(item => item.name === notificationInfo.value.name && item !== props.notification)) {
|
|
||||||
$toast.error(`通知渠道【${notificationInfo.value.name}】已存在,请替换`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
notificationInfoDialog.value = false
|
|
||||||
emit('change', notificationInfo.value, props.notification.name)
|
|
||||||
emit('done')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据存储类型选择图标
|
|
||||||
const getIcon = computed(() => {
|
|
||||||
switch (props.notification.type) {
|
|
||||||
case 'wechat':
|
|
||||||
return wechat_image
|
|
||||||
case 'telegram':
|
|
||||||
return telegram_image
|
|
||||||
case 'vocechat':
|
|
||||||
return vocechat_image
|
|
||||||
case 'synologychat':
|
|
||||||
return synologychat_image
|
|
||||||
case 'slack':
|
|
||||||
return slack_image
|
|
||||||
case 'webpush':
|
|
||||||
return chrome_image
|
|
||||||
default:
|
|
||||||
return wechat_image
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 按钮点击
|
|
||||||
function onClose() {
|
|
||||||
emit('close')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<VCard variant="tonal" @click="openNotificationInfoDialog">
|
|
||||||
<span class="absolute top-3 right-12">
|
|
||||||
<IconBtn>
|
|
||||||
<VIcon class="cursor-move" icon="mdi-drag" />
|
|
||||||
</IconBtn>
|
|
||||||
</span>
|
|
||||||
<DialogCloseBtn @click="onClose" />
|
|
||||||
<VCardText class="flex justify-space-between align-center gap-3">
|
|
||||||
<div class="align-self-start">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<VBadge v-if="props.notification.enabled" dot inline color="success" class="me-1" />
|
|
||||||
<span class="text-h6">{{ props.notification.name }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-body-1 mb-3">{{ notificationTypeNames[notification.type] }}</div>
|
|
||||||
</div>
|
|
||||||
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" />
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
<VDialog v-if="notificationInfoDialog" v-model="notificationInfoDialog" scrollable max-width="40rem" persistent>
|
|
||||||
<VCard :title="`${props.notification.name} - 配置`" class="rounded-t">
|
|
||||||
<DialogCloseBtn v-model="notificationInfoDialog" />
|
|
||||||
<VDivider />
|
|
||||||
<VCardText>
|
|
||||||
<VForm>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VSwitch v-model="notificationInfo.enabled" label="启用通知" />
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VSelect
|
|
||||||
v-model="notificationInfo.switchs"
|
|
||||||
:items="notificationTypes"
|
|
||||||
label="消息类型"
|
|
||||||
hint="开启通知的消息类型"
|
|
||||||
multiple
|
|
||||||
clearable
|
|
||||||
chips
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow v-if="notificationInfo.type == 'wechat'">
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.name"
|
|
||||||
label="名称"
|
|
||||||
placeholder="别名"
|
|
||||||
hint="通知渠道的别名"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.WECHAT_CORPID"
|
|
||||||
label="企业ID"
|
|
||||||
hint="企业微信后台企业信息中的企业ID"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.WECHAT_APP_ID"
|
|
||||||
label="应用 AgentId"
|
|
||||||
hint="企业微信自建应用的AgentId"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.WECHAT_APP_SECRET"
|
|
||||||
label="应用 Secret"
|
|
||||||
hint="企业微信自建应用的Secret"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.WECHAT_PROXY"
|
|
||||||
label="代理地址"
|
|
||||||
hint="微信消息的转发代理地址,2022年6月20日后创建的自建应用才需要,不使用代理时需要保留默认值"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.WECHAT_TOKEN"
|
|
||||||
label="Token"
|
|
||||||
hint="微信企业自建应用->API接收消息配置中的Token"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.WECHAT_ENCODING_AESKEY"
|
|
||||||
label="EncodingAESKey"
|
|
||||||
hint="微信企业自建应用->API接收消息配置中的EncodingAESKey"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.WECHAT_ADMINS"
|
|
||||||
label="管理员白名单"
|
|
||||||
placeholder="多个用,分隔"
|
|
||||||
hint="可使用管理菜单及命令的用户ID列表,多个ID使用,分隔"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow v-if="notificationInfo.type == 'telegram'">
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.name"
|
|
||||||
label="名称"
|
|
||||||
placeholder="别名"
|
|
||||||
hint="通知渠道的别名"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.TELEGRAM_TOKEN"
|
|
||||||
label="Bot Token"
|
|
||||||
hint="Telegram机器人token,格式:123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.TELEGRAM_CHAT_ID"
|
|
||||||
label="Chat ID"
|
|
||||||
hint="接受消息通知的用户、群组或频道Chat ID"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.TELEGRAM_USERS"
|
|
||||||
label="用户白名单"
|
|
||||||
placeholder="多个用,分隔"
|
|
||||||
hint="可使用Telegram机器人的用户ID清单,多个用户用,分隔,不填写则所有用户都能使用"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.TELEGRAM_ADMINS"
|
|
||||||
label="管理员白名单"
|
|
||||||
placeholder="多个用,分隔"
|
|
||||||
hint="可使用管理菜单及命令的用户ID列表,多个ID使用,分隔"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow v-if="notificationInfo.type == 'slack'">
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.name"
|
|
||||||
label="名称"
|
|
||||||
placeholder="别名"
|
|
||||||
hint="通知渠道的别名"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.SLACK_OAUTH_TOKEN"
|
|
||||||
label="Slack Bot User OAuth Token"
|
|
||||||
placeholder="xoxb-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx"
|
|
||||||
hint="Slack应用`OAuth & Permissions`页面中的`Bot User OAuth Token`"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.SLACK_APP_TOKEN"
|
|
||||||
label="Slack App-Level Token"
|
|
||||||
placeholder="xapp-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx"
|
|
||||||
hint="Slack应用`OAuth & Permissions`页面中的`App-Level Token`"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.SLACK_CHANNEL"
|
|
||||||
label="频道名称"
|
|
||||||
placeholder="全体"
|
|
||||||
hint="消息发送频道,默认`全体`"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow v-if="notificationInfo.type == 'synologychat'">
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.name"
|
|
||||||
label="名称"
|
|
||||||
placeholder="别名"
|
|
||||||
hint="通知渠道的别名"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.SYNOLOGYCHAT_WEBHOOK"
|
|
||||||
label="机器人传入URL"
|
|
||||||
hint="Synology Chat机器人传入URL"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.SYNOLOGYCHAT_TOKEN"
|
|
||||||
label="令牌"
|
|
||||||
hint="Synology Chat机器人令牌"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow v-if="notificationInfo.type == 'vocechat'">
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.name"
|
|
||||||
label="名称"
|
|
||||||
placeholder="别名"
|
|
||||||
hint="通知渠道的别名"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.VOCECHAT_HOST"
|
|
||||||
label="地址"
|
|
||||||
hint="VoceChat服务端地址,格式:http(s)://ip:port"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.VOCECHAT_API_KEY"
|
|
||||||
label="机器人密钥"
|
|
||||||
hint="VoceChat机器人密钥"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.VOCECHAT_CHANNEL_ID"
|
|
||||||
label="频道ID"
|
|
||||||
placeholder="不包含#号"
|
|
||||||
hint="VoceChat的频道ID,不包含#号"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow v-if="notificationInfo.type == 'webpush'">
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.name"
|
|
||||||
label="名称"
|
|
||||||
placeholder="别名"
|
|
||||||
hint="通知渠道的别名"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="notificationInfo.config.WEBPUSH_USERNAME"
|
|
||||||
label="登录用户名"
|
|
||||||
hint="只有对应的用户登录后才会推送消息"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
<VCardActions class="pt-3">
|
|
||||||
<VBtn @click="saveNotificationInfo" variant="elevated" prepend-icon="mdi-content-save" class="px-5">
|
|
||||||
确定
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -9,9 +9,6 @@ const personProps = defineProps({
|
|||||||
height: String,
|
height: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 从 provide 中获取全局设置
|
|
||||||
const globalSettings: any = inject('globalSettings')
|
|
||||||
|
|
||||||
// 当前人物
|
// 当前人物
|
||||||
const personInfo = ref(personProps.person)
|
const personInfo = ref(personProps.person)
|
||||||
|
|
||||||
@@ -20,26 +17,22 @@ const isImageLoaded = ref(false)
|
|||||||
|
|
||||||
// 人物图片地址
|
// 人物图片地址
|
||||||
function getPersonImage() {
|
function getPersonImage() {
|
||||||
let url = ''
|
|
||||||
if (personProps.person?.source === 'themoviedb') {
|
if (personProps.person?.source === 'themoviedb') {
|
||||||
if (!personInfo.value?.profile_path) return personIcon
|
if (!personInfo.value?.profile_path) return personIcon
|
||||||
url = `https://${globalSettings.TMDB_IMAGE_DOMAIN}/t/p/w600_and_h900_bestv2${personInfo.value?.profile_path}`
|
return `https://image.tmdb.org/t/p/w600_and_h900_bestv2${personInfo.value?.profile_path}`
|
||||||
} else if (personProps.person?.source === 'douban') {
|
} else if (personProps.person?.source === 'douban') {
|
||||||
if (!personInfo.value?.avatar) return personIcon
|
if (!personInfo.value?.avatar) return personIcon
|
||||||
if (typeof personInfo.value?.avatar === 'object') {
|
if (typeof personInfo.value?.avatar === 'object') {
|
||||||
url = personInfo.value?.avatar?.normal
|
return personInfo.value?.avatar?.normal
|
||||||
} else {
|
} else {
|
||||||
url = personInfo.value?.avatar
|
return personInfo.value?.avatar
|
||||||
}
|
}
|
||||||
} else if (personProps.person?.source === 'bangumi') {
|
} else if (personProps.person?.source === 'bangumi') {
|
||||||
if (!personInfo.value?.images) return personIcon
|
if (!personInfo.value?.images) return personIcon
|
||||||
url = personInfo.value?.images?.medium
|
return personInfo.value?.images?.medium
|
||||||
} else {
|
} else {
|
||||||
return personIcon
|
return personIcon
|
||||||
}
|
}
|
||||||
if (globalSettings.GLOBAL_IMAGE_CACHE && url)
|
|
||||||
return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`
|
|
||||||
return url
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 人物姓名
|
// 人物姓名
|
||||||
|
|||||||
@@ -43,8 +43,11 @@ const imageLoadError = ref(false)
|
|||||||
// 更新日志弹窗
|
// 更新日志弹窗
|
||||||
const releaseDialog = ref(false)
|
const releaseDialog = ref(false)
|
||||||
|
|
||||||
// 插件详情弹窗
|
// 计算插件标签
|
||||||
const detailDialog = ref(false)
|
const pluginLabels = computed(() => {
|
||||||
|
if (!props.plugin?.plugin_label) return []
|
||||||
|
return props.plugin.plugin_label.split(',')
|
||||||
|
})
|
||||||
|
|
||||||
// 图片加载完成
|
// 图片加载完成
|
||||||
async function imageLoaded() {
|
async function imageLoaded() {
|
||||||
@@ -73,7 +76,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 {
|
||||||
@@ -146,141 +149,80 @@ const dropdownItems = ref([
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<VCard :width="props.width" :height="props.height" @click="installPlugin" class="flex flex-col">
|
||||||
<VCard :width="props.width" :height="props.height" @click="detailDialog = true" class="flex flex-col h-full">
|
<div class="me-n3 absolute bottom-0 right-3">
|
||||||
|
<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="relative flex flex-row items-start pa-3 justify-between grow"
|
class="absolute inset-0 bg-cover bg-center"
|
||||||
:style="{ background: `${backgroundColor}` }"
|
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.7)' }"
|
||||||
>
|
></div>
|
||||||
<div
|
<div class="relative flex-1 min-w-0">
|
||||||
class="absolute inset-0 bg-cover bg-center"
|
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
||||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
{{ props.plugin?.plugin_name }}
|
||||||
></div>
|
<span class="text-sm text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
||||||
<div class="relative flex-1 min-w-0">
|
</VCardTitle>
|
||||||
<VCardTitle class="text-white text-lg px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis ...">
|
<VCardText class="text-white px-2 py-1 text-shadow line-clamp-3">
|
||||||
{{ props.plugin?.plugin_name }}
|
{{ props.plugin?.plugin_desc }}
|
||||||
<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>
|
|
||||||
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
|
||||||
<span>
|
|
||||||
<VIcon icon="mdi-github" class="me-1" />
|
|
||||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
|
||||||
{{ props.plugin?.plugin_author }}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<span v-if="props.count" class="ms-3">
|
|
||||||
<VIcon icon="mdi-download" />
|
|
||||||
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
|
||||||
</span>
|
|
||||||
<div class="me-n3 absolute bottom-1 right-3">
|
|
||||||
<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>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
<!-- 安装插件进度框 -->
|
|
||||||
<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>
|
</VCardText>
|
||||||
</VCard>
|
</div>
|
||||||
</VDialog>
|
<div class="relative flex-shrink-0 self-center">
|
||||||
</div>
|
<VAvatar size="64">
|
||||||
|
<VImg
|
||||||
|
ref="imageRef"
|
||||||
|
:src="iconPath"
|
||||||
|
aspect-ratio="4/3"
|
||||||
|
cover
|
||||||
|
:class="{ shadow: isImageLoaded }"
|
||||||
|
@load="imageLoaded"
|
||||||
|
@error="imageLoadError = true"
|
||||||
|
/>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||||
|
<span>
|
||||||
|
<VIcon icon="mdi-github" class="me-1" />
|
||||||
|
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||||
|
{{ props.plugin?.plugin_author }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span v-if="props.count" class="ms-3">
|
||||||
|
<VIcon icon="mdi-download" />
|
||||||
|
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||||
|
</span>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
<!-- 安装插件进度框 -->
|
||||||
|
<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>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<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 FormRender from '@/components/render/FormRender.vue'
|
||||||
@@ -9,13 +10,12 @@ 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 store from '@/store'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
// APP
|
|
||||||
const appMode = inject('pwaMode') && display.mdAndDown.value
|
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -50,9 +50,6 @@ const pluginConfigDialog = ref(false)
|
|||||||
// 插件配置表单数据
|
// 插件配置表单数据
|
||||||
const pluginConfigForm = ref({})
|
const pluginConfigForm = ref({})
|
||||||
|
|
||||||
// 菜单显示状态
|
|
||||||
const menuVisible = ref(false)
|
|
||||||
|
|
||||||
// 进度框
|
// 进度框
|
||||||
const progressDialog = ref(false)
|
const progressDialog = ref(false)
|
||||||
|
|
||||||
@@ -65,9 +62,6 @@ const pluginInfoDialog = ref(false)
|
|||||||
// 进度框文本
|
// 进度框文本
|
||||||
const progressText = ref('正在更新插件...')
|
const progressText = ref('正在更新插件...')
|
||||||
|
|
||||||
// 用户头像是否加载完成
|
|
||||||
const isAvatarLoaded = ref(false)
|
|
||||||
|
|
||||||
// 插件数据页面配置项
|
// 插件数据页面配置项
|
||||||
let pluginPageItems = ref([])
|
let pluginPageItems = ref([])
|
||||||
|
|
||||||
@@ -222,19 +216,11 @@ const iconPath: Ref<string> = computed(() => {
|
|||||||
return `./plugin_icon/${props.plugin?.plugin_icon}`
|
return `./plugin_icon/${props.plugin?.plugin_icon}`
|
||||||
})
|
})
|
||||||
|
|
||||||
// 插件作者头像路径
|
|
||||||
const authorPath: Ref<string> = computed(() => {
|
|
||||||
// 网络图片则使用代理后返回
|
|
||||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(
|
|
||||||
props.plugin?.author_url + '.png',
|
|
||||||
)}`
|
|
||||||
})
|
|
||||||
|
|
||||||
// 重置插件
|
// 重置插件
|
||||||
async function resetPlugin() {
|
async function resetPlugin() {
|
||||||
const isConfirmed = await createConfirm({
|
const isConfirmed = await createConfirm({
|
||||||
title: '确认',
|
title: '确认',
|
||||||
content: `此操作将恢复插件 ${props.plugin?.plugin_name} 的默认设置,并清除所有相关数据,确定要继续吗?`,
|
content: `是否确认重置插件 ${props.plugin?.plugin_name} 的配置数据?`,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!isConfirmed) return
|
if (!isConfirmed) return
|
||||||
@@ -291,9 +277,10 @@ function visitAuthorPage() {
|
|||||||
|
|
||||||
// 查看日志URL
|
// 查看日志URL
|
||||||
function openLoggerWindow() {
|
function openLoggerWindow() {
|
||||||
|
const token = store.state.auth.token
|
||||||
const url = `${
|
const url = `${
|
||||||
import.meta.env.VITE_API_BASE_URL
|
import.meta.env.VITE_API_BASE_URL
|
||||||
}system/logging?length=-1&logfile=plugins/${props.plugin?.id?.toLowerCase()}.log`
|
}system/logging?token=${token}&length=-1&logfile=plugins/${props.plugin?.id?.toLowerCase()}.log`
|
||||||
window.open(url, '_blank')
|
window.open(url, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,167 +381,128 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<!-- 插件卡片 -->
|
||||||
<!-- 插件卡片 -->
|
<VCard v-if="isVisible" :width="props.width" :height="props.height" @click="openPluginDetail" class="flex flex-col">
|
||||||
<VHover>
|
<div class="me-n3 absolute bottom-0 right-3">
|
||||||
<template #default="hover">
|
<IconBtn>
|
||||||
<VCard
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
v-if="isVisible"
|
<VMenu activator="parent" close-on-content-click>
|
||||||
v-bind="hover.props"
|
<VList>
|
||||||
:width="props.width"
|
<VListItem
|
||||||
:height="props.height"
|
v-for="(item, i) in dropdownItems"
|
||||||
@click="openPluginDetail"
|
v-show="item.show"
|
||||||
class="flex flex-col h-full"
|
:key="i"
|
||||||
>
|
variant="plain"
|
||||||
<div
|
:base-color="item.props.color"
|
||||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
@click="item.props.click"
|
||||||
:style="{ background: `${backgroundColor}` }"
|
>
|
||||||
>
|
<template #prepend>
|
||||||
<div
|
<VIcon :icon="item.props.prependIcon" />
|
||||||
class="absolute inset-0 bg-cover bg-center"
|
</template>
|
||||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
<VListItemTitle v-text="item.title" />
|
||||||
/>
|
</VListItem>
|
||||||
<div class="relative flex-1 min-w-0">
|
</VList>
|
||||||
<VCardTitle class="text-white text-lg px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
</VMenu>
|
||||||
<VBadge v-if="props.plugin?.state" dot inline color="success" />
|
</IconBtn>
|
||||||
{{ props.plugin?.plugin_name }}
|
</div>
|
||||||
<span class="text-sm mt-1 text-gray-200"> v{{ props.plugin?.plugin_version }} </span>
|
<div
|
||||||
</VCardTitle>
|
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||||
<VCardText class="px-2 py-0 text-white text-sm text-shadow overflow-hidden line-clamp-3 ...">
|
:style="{ background: `${backgroundColor}` }"
|
||||||
{{ 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>
|
|
||||||
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
|
||||||
<span class="author-info">
|
|
||||||
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
|
|
||||||
<VIcon v-if="!isAvatarLoaded" icon="mdi-github" class="me-1" />
|
|
||||||
</VImg>
|
|
||||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
|
||||||
{{ props.plugin?.plugin_author }}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<span v-if="props.count" class="ms-3">
|
|
||||||
<VIcon icon="mdi-download" />
|
|
||||||
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
|
||||||
</span>
|
|
||||||
<div class="me-n3 absolute bottom-1 right-3">
|
|
||||||
<IconBtn>
|
|
||||||
<VIcon icon="mdi-dots-vertical" />
|
|
||||||
<VMenu v-model="menuVisible" activator="parent" close-on-content-click>
|
|
||||||
<VList>
|
|
||||||
<VListItem
|
|
||||||
v-for="(item, i) in dropdownItems"
|
|
||||||
v-show="item.show"
|
|
||||||
:key="i"
|
|
||||||
variant="plain"
|
|
||||||
:base-color="item.props.color"
|
|
||||||
@click="item.props.click"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon :icon="item.props.prependIcon" />
|
|
||||||
</template>
|
|
||||||
<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-if="pluginConfigDialog"
|
|
||||||
v-model="pluginConfigDialog"
|
|
||||||
scrollable
|
|
||||||
max-width="60rem"
|
|
||||||
:fullscreen="!display.mdAndUp.value"
|
|
||||||
>
|
>
|
||||||
<VCard :title="`${props.plugin?.plugin_name} - 配置`" class="rounded-t">
|
<div
|
||||||
<DialogCloseBtn v-model="pluginConfigDialog" />
|
class="absolute inset-0 bg-cover bg-center"
|
||||||
<VDivider />
|
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.7)' }"
|
||||||
<VCardText>
|
/>
|
||||||
<FormRender v-for="(item, index) in pluginFormItems" :key="index" :config="item" :model="pluginConfigForm" />
|
<div class="relative flex-1 min-w-0">
|
||||||
|
<VCardTitle class="text-white px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
||||||
|
<VBadge v-if="props.plugin?.state" dot inline color="success" />
|
||||||
|
{{ props.plugin?.plugin_name }}
|
||||||
|
<span class="text-sm mt-1 text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
||||||
|
</VCardTitle>
|
||||||
|
<VCardText class="px-2 py-1 text-white text-shadow line-clamp-3">
|
||||||
|
{{ props.plugin?.plugin_desc }}
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions class="pt-3">
|
</div>
|
||||||
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo" variant="outlined" color="info">
|
<div class="relative flex-shrink-0 self-center">
|
||||||
查看数据
|
<VAvatar size="64">
|
||||||
</VBtn>
|
<VImg
|
||||||
<VSpacer />
|
ref="imageRef"
|
||||||
<VBtn @click="savePluginConf" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 保存 </VBtn>
|
:src="iconPath"
|
||||||
</VCardActions>
|
aspect-ratio="4/3"
|
||||||
</VCard>
|
cover
|
||||||
</VDialog>
|
:class="{ shadow: isImageLoaded }"
|
||||||
|
@load="imageLoaded"
|
||||||
|
@error="imageLoadError = true"
|
||||||
|
/>
|
||||||
|
</VAvatar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||||
|
<span>
|
||||||
|
<VIcon icon="mdi-github" class="me-1" />
|
||||||
|
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||||
|
{{ props.plugin?.plugin_author }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span v-if="props.count" class="ms-3">
|
||||||
|
<VIcon icon="mdi-download" />
|
||||||
|
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||||
|
</span>
|
||||||
|
</VCardText>
|
||||||
|
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
|
||||||
|
<VIcon icon="mdi-new-box" class="text-white" />
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
|
||||||
<!-- 插件数据页面 -->
|
<!-- 插件配置页面 -->
|
||||||
<VDialog
|
<VDialog v-model="pluginConfigDialog" scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
||||||
v-if="pluginInfoDialog"
|
<VCard :title="`${props.plugin?.plugin_name} - 配置`" class="rounded-t">
|
||||||
v-model="pluginInfoDialog"
|
<DialogCloseBtn v-model="pluginConfigDialog" />
|
||||||
scrollable
|
<VDivider />
|
||||||
max-width="80rem"
|
<VCardText>
|
||||||
:fullscreen="!display.mdAndUp.value"
|
<FormRender v-for="(item, index) in pluginFormItems" :key="index" :config="item" :form="pluginConfigForm" />
|
||||||
>
|
</VCardText>
|
||||||
<VCard :title="`${props.plugin?.plugin_name}`" class="rounded-t">
|
<VCardActions class="pt-3">
|
||||||
<DialogCloseBtn v-model="pluginInfoDialog" />
|
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo" variant="outlined" color="info">
|
||||||
<VCardText class="min-h-40">
|
查看数据
|
||||||
<PageRender @action="loadPluginPage" v-for="(item, index) in pluginPageItems" :key="index" :config="item" />
|
</VBtn>
|
||||||
</VCardText>
|
<VSpacer />
|
||||||
<VFab
|
<VBtn @click="savePluginConf" variant="elevated" prepend-icon="mdi-content-save" class="px-5"> 保存 </VBtn>
|
||||||
icon="mdi-cog"
|
</VCardActions>
|
||||||
location="bottom"
|
</VCard>
|
||||||
size="x-large"
|
</VDialog>
|
||||||
fixed
|
|
||||||
app
|
|
||||||
appear
|
|
||||||
@click="showPluginConfig"
|
|
||||||
:class="{ 'mb-10': appMode }"
|
|
||||||
/>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
|
|
||||||
<!-- 进度框 -->
|
<!-- 插件数据页面 -->
|
||||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
<VDialog v-model="pluginInfoDialog" scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||||
|
<VCard :title="`${props.plugin?.plugin_name}`" class="rounded-t">
|
||||||
|
<DialogCloseBtn v-model="pluginInfoDialog" />
|
||||||
|
<VCardText 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="showPluginConfig" />
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
|
||||||
<!-- 更新日志 -->
|
<!-- 进度框 -->
|
||||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||||
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
|
||||||
<DialogCloseBtn @click="releaseDialog = false" />
|
<!-- 更新日志 -->
|
||||||
<VDivider />
|
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||||
<VersionHistory :history="props.plugin?.history" />
|
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
||||||
<VDivider />
|
<DialogCloseBtn @click="releaseDialog = false" />
|
||||||
<VCardItem>
|
<VDivider />
|
||||||
<VBtn @click="updatePlugin" block>
|
<VersionHistory :history="props.plugin?.history" />
|
||||||
<template #prepend>
|
<VDivider />
|
||||||
<VIcon icon="mdi-arrow-up-circle-outline" />
|
<VCardText>
|
||||||
</template>
|
<VBtn @click="updatePlugin" block>
|
||||||
更新到最新版本
|
<template #prepend>
|
||||||
</VBtn>
|
<VIcon icon="mdi-arrow-up-circle-outline" />
|
||||||
</VCardItem>
|
</template>
|
||||||
</VCard>
|
更新到最新版本
|
||||||
</VDialog>
|
</VBtn>
|
||||||
</div>
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -567,17 +515,4 @@ watch(
|
|||||||
content: '';
|
content: '';
|
||||||
inset: 0;
|
inset: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.author-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-avatar {
|
|
||||||
border-radius: 50%;
|
|
||||||
block-size: 24px;
|
|
||||||
inline-size: 24px;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const getImgUrl = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 跳转播放
|
// 跳转播放
|
||||||
function goPlay(isHovering: boolean | null = false) {
|
function goPlay(isHovering = 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,11 +48,13 @@ function goPlay(isHovering: boolean | null = 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"
|
||||||
@@ -76,9 +78,7 @@ function goPlay(isHovering: boolean | null = false) {
|
|||||||
<!-- 详情 -->
|
<!-- 详情 -->
|
||||||
<VCardText
|
<VCardText
|
||||||
v-show="hover.isHovering || imageLoadError"
|
v-show="hover.isHovering || imageLoadError"
|
||||||
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"
|
class="w-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%)"
|
|
||||||
@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,3 +89,9 @@ function goPlay(isHovering: boolean | null = false) {
|
|||||||
</template>
|
</template>
|
||||||
</VHover>
|
</VHover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.on-hover img {
|
||||||
|
@apply brightness-50;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,23 +2,30 @@
|
|||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import SiteAddEditDialog from '../dialog/SiteAddEditDialog.vue'
|
import SiteAddEditDialog from '../dialog/SiteAddEditDialog.vue'
|
||||||
import SiteUserDataDialog from '../dialog/SiteUserDataDialog.vue'
|
import SiteTorrentTable from '../table/SiteTorrentTable.vue'
|
||||||
import SiteResourceDialog from '../dialog/SiteResourceDialog.vue'
|
import { requiredValidator } from '@/@validators'
|
||||||
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 } from '@/api/types'
|
||||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||||
import { formatFileSize } from '@/@core/utils/formatters'
|
import { useDisplay } from 'vuetify'
|
||||||
|
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
||||||
|
|
||||||
|
// 显示器宽度
|
||||||
|
const display = useDisplay()
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const cardProps = defineProps({
|
const cardProps = defineProps({
|
||||||
site: Object as PropType<Site>,
|
site: Object as PropType<Site>,
|
||||||
data: Object as PropType<SiteUserData>,
|
width: String,
|
||||||
|
height: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
// 定义触发的自定义事件
|
||||||
const emit = defineEmits(['update', 'remove'])
|
const emit = defineEmits(['update', 'remove'])
|
||||||
|
|
||||||
|
// 密码输入
|
||||||
|
const isPasswordVisible = ref(false)
|
||||||
|
|
||||||
// 图标
|
// 图标
|
||||||
const siteIcon = ref<string>('')
|
const siteIcon = ref<string>('')
|
||||||
|
|
||||||
@@ -26,11 +33,14 @@ const siteIcon = ref<string>('')
|
|||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
// 测试按钮文字
|
// 测试按钮文字
|
||||||
const testButtonText = ref('连通性测试')
|
const testButtonText = ref('测试')
|
||||||
|
|
||||||
// 测试按钮可用性
|
// 测试按钮可用性
|
||||||
const testButtonDisable = ref(false)
|
const testButtonDisable = ref(false)
|
||||||
|
|
||||||
|
// 更新按钮可用性
|
||||||
|
const updateButtonDisable = ref(false)
|
||||||
|
|
||||||
// 更新站点Cookie UA弹窗
|
// 更新站点Cookie UA弹窗
|
||||||
const siteCookieDialog = ref(false)
|
const siteCookieDialog = ref(false)
|
||||||
|
|
||||||
@@ -40,8 +50,18 @@ const siteEditDialog = ref(false)
|
|||||||
// 资源浏览弹窗
|
// 资源浏览弹窗
|
||||||
const resourceDialog = ref(false)
|
const resourceDialog = ref(false)
|
||||||
|
|
||||||
// 用户数据弹窗
|
// 进度条
|
||||||
const siteUserDataDialog = ref(false)
|
const progressDialog = ref(false)
|
||||||
|
|
||||||
|
// 进度文本
|
||||||
|
const progressText = ref('请稍候 ...')
|
||||||
|
|
||||||
|
// 用户名密码表单
|
||||||
|
const userPwForm = ref({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
code: '',
|
||||||
|
})
|
||||||
|
|
||||||
// 站点使用统计
|
// 站点使用统计
|
||||||
const siteStats = ref<SiteStatistic>({})
|
const siteStats = ref<SiteStatistic>({})
|
||||||
@@ -65,7 +85,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()
|
||||||
@@ -93,9 +113,34 @@ async function handleResourceBrowse() {
|
|||||||
resourceDialog.value = true
|
resourceDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开站点用户数据弹窗
|
// 调用API,更新站点Cookie UA
|
||||||
async function handleSiteUserData() {
|
async function updateSiteCookie() {
|
||||||
siteUserDataDialog.value = true
|
try {
|
||||||
|
if (!userPwForm.value.username || !userPwForm.value.password) return
|
||||||
|
|
||||||
|
// 更新按钮状态
|
||||||
|
siteCookieDialog.value = false
|
||||||
|
updateButtonDisable.value = true
|
||||||
|
|
||||||
|
progressDialog.value = true
|
||||||
|
progressText.value = `正在更新 ${cardProps.site?.name} Cookie & UA ...`
|
||||||
|
|
||||||
|
const result: { [key: string]: any } = await api.get(`site/cookie/${cardProps.site?.id}`, {
|
||||||
|
params: {
|
||||||
|
username: userPwForm.value.username,
|
||||||
|
password: userPwForm.value.password,
|
||||||
|
code: userPwForm.value.code,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) $toast.success(`${cardProps.site?.name} 更新Cookie & UA 成功!`)
|
||||||
|
else $toast.error(`${cardProps.site?.name} 更新失败:${result.message}`)
|
||||||
|
|
||||||
|
progressDialog.value = false
|
||||||
|
updateButtonDisable.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开站点页面
|
// 打开站点页面
|
||||||
@@ -117,10 +162,9 @@ const statColor = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算上传量和下载量的百分比
|
// 监听resourceDialog,如果为false则重新查询站点使用统计
|
||||||
const getPercentage = computed(() => {
|
watch(resourceDialog, value => {
|
||||||
if (cardProps.data?.upload === 0) return 100
|
if (!value) getSiteStats()
|
||||||
return ((cardProps.data?.download ?? 0) / ((cardProps.data?.download ?? 0) + (cardProps.data?.upload ?? 0))) * 100
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 保存站点
|
// 保存站点
|
||||||
@@ -129,18 +173,6 @@ function saveSite() {
|
|||||||
emit('update')
|
emit('update')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新站点Cookie UA后的回调
|
|
||||||
function onSiteCookieUpdated() {
|
|
||||||
siteCookieDialog.value = false
|
|
||||||
getSiteStats()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 资源浏览弹窗关闭后的回调
|
|
||||||
function onSiteResourceDone() {
|
|
||||||
resourceDialog.value = false
|
|
||||||
getSiteStats()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 装载时查询站点图标
|
// 装载时查询站点图标
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getSiteIcon()
|
getSiteIcon()
|
||||||
@@ -151,8 +183,10 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<VCard
|
<VCard
|
||||||
|
:height="cardProps.height"
|
||||||
|
:width="cardProps.width"
|
||||||
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
|
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
|
||||||
class="overflow-hidden h-full flex flex-col"
|
class="overflow-hidden"
|
||||||
@click="siteEditDialog = true"
|
@click="siteEditDialog = true"
|
||||||
>
|
>
|
||||||
<template #image>
|
<template #image>
|
||||||
@@ -160,7 +194,7 @@ onMounted(() => {
|
|||||||
<VImg :src="siteIcon" />
|
<VImg :src="siteIcon" />
|
||||||
</VAvatar>
|
</VAvatar>
|
||||||
</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>
|
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
|
||||||
</VCardTitle>
|
</VCardTitle>
|
||||||
@@ -168,7 +202,7 @@ onMounted(() => {
|
|||||||
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
|
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
|
||||||
</VCardSubtitle>
|
</VCardSubtitle>
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
<VCardText class="py-1">
|
<VCardText class="py-2" style="block-size: 36px;">
|
||||||
<VTooltip v-if="cardProps.site?.limit_interval" text="流控">
|
<VTooltip v-if="cardProps.site?.limit_interval" text="流控">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-speedometer" />
|
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-speedometer" />
|
||||||
@@ -190,59 +224,67 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</VTooltip>
|
</VTooltip>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
<VDivider />
|
||||||
<VCardActions>
|
<VCardActions>
|
||||||
<IconBtn>
|
<VBtn v-if="!cardProps.site?.public" :disabled="updateButtonDisable" @click.stop="handleSiteUpdate">
|
||||||
<VIcon icon="mdi-chevron-down" color="primary" />
|
<template #prepend>
|
||||||
<VMenu activator="parent" close-on-content-click>
|
<VIcon icon="mdi-refresh" />
|
||||||
<VList>
|
</template>
|
||||||
<VListItem variant="plain" v-if="!cardProps.site?.public" @click="handleSiteUpdate">
|
更新
|
||||||
<template #prepend>
|
</VBtn>
|
||||||
<VIcon icon="mdi-refresh" />
|
<VBtn :disabled="testButtonDisable" @click.stop="testSite">
|
||||||
</template>
|
<template #prepend>
|
||||||
<VListItemTitle>更新 Cookie & UA</VListItemTitle>
|
<VIcon icon="mdi-link" />
|
||||||
</VListItem>
|
</template>
|
||||||
<VListItem variant="plain" :disabled="testButtonDisable" @click.stop="testSite">
|
{{ testButtonText }}
|
||||||
<template #prepend>
|
</VBtn>
|
||||||
<VIcon icon="mdi-link" />
|
<VBtn @click.stop="handleResourceBrowse">
|
||||||
</template>
|
<template #prepend>
|
||||||
<VListItemTitle>{{ testButtonText }}</VListItemTitle>
|
<VIcon icon="mdi-web" />
|
||||||
</VListItem>
|
</template>
|
||||||
<VListItem variant="plain" @click="handleResourceBrowse">
|
浏览
|
||||||
<template #prepend>
|
</VBtn>
|
||||||
<VIcon icon="mdi-web" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>资源预览</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem variant="plain" @click="handleSiteUserData">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-chart-bell-curve" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>站点数据</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
</VList>
|
|
||||||
</VMenu>
|
|
||||||
</IconBtn>
|
|
||||||
<span class="text-sm">
|
|
||||||
↑ {{ formatFileSize(cardProps.data?.upload || 0) }} / ↓ {{ formatFileSize(cardProps.data?.download || 0) }}
|
|
||||||
</span>
|
|
||||||
<VSpacer />
|
|
||||||
</VCardActions>
|
</VCardActions>
|
||||||
<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>
|
||||||
</span>
|
</span>
|
||||||
<div class="w-full absolute bottom-0" v-if="(cardProps.data?.upload || cardProps.data?.download || 0) > 0">
|
|
||||||
<VProgressLinear :model-value="getPercentage" bg-color="success" color="warning" bg-opacity="0.5" height="3" />
|
|
||||||
</div>
|
|
||||||
</VCard>
|
</VCard>
|
||||||
<!-- 更新站点Cookie & UA弹窗 -->
|
<!-- 更新站点Cookie & UA弹窗 -->
|
||||||
<SiteCookieUpdateDialog
|
<VDialog v-model="siteCookieDialog" max-width="50rem">
|
||||||
v-if="siteCookieDialog"
|
<!-- Dialog Content -->
|
||||||
v-model="siteCookieDialog"
|
<VCard title="更新站点Cookie & UA">
|
||||||
:site="cardProps.site"
|
<DialogCloseBtn @click="siteCookieDialog = false" />
|
||||||
@close="siteCookieDialog = false"
|
<VCardText>
|
||||||
@done="onSiteCookieUpdated"
|
<VForm @submit.prevent="() => {}">
|
||||||
/>
|
<VRow>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField v-model="userPwForm.username" label="用户名" :rules="[requiredValidator]" />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="userPwForm.password"
|
||||||
|
label="密码"
|
||||||
|
:type="isPasswordVisible ? 'text' : 'password'"
|
||||||
|
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||||
|
:rules="[requiredValidator]"
|
||||||
|
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||||
|
@keydown.enter="updateSiteCookie"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField v-model="userPwForm.code" label="两步验证" />
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
|
||||||
|
<VCardActions>
|
||||||
|
<VSpacer />
|
||||||
|
<VBtn variant="elevated" @click="updateSiteCookie" prepend-icon="mdi-refresh" class="px-5"> 开始更新 </VBtn>
|
||||||
|
</VCardActions>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
<!-- 站点编辑弹窗 -->
|
<!-- 站点编辑弹窗 -->
|
||||||
<SiteAddEditDialog
|
<SiteAddEditDialog
|
||||||
v-if="siteEditDialog"
|
v-if="siteEditDialog"
|
||||||
@@ -252,19 +294,30 @@ onMounted(() => {
|
|||||||
@remove="emit('remove')"
|
@remove="emit('remove')"
|
||||||
@close="siteEditDialog = false"
|
@close="siteEditDialog = false"
|
||||||
/>
|
/>
|
||||||
<!-- 站点数据弹窗 -->
|
|
||||||
<SiteUserDataDialog
|
|
||||||
v-if="siteUserDataDialog"
|
|
||||||
v-model="siteUserDataDialog"
|
|
||||||
:site="cardProps.site"
|
|
||||||
@close="siteUserDataDialog = false"
|
|
||||||
/>
|
|
||||||
<!-- 站点资源弹窗 -->
|
<!-- 站点资源弹窗 -->
|
||||||
<SiteResourceDialog
|
<VDialog
|
||||||
v-if="resourceDialog"
|
v-if="resourceDialog"
|
||||||
v-model="resourceDialog"
|
v-model="resourceDialog"
|
||||||
:site="cardProps.site"
|
max-width="80rem"
|
||||||
@close="onSiteResourceDone"
|
scrollable
|
||||||
/>
|
z-index="1010"
|
||||||
|
:fullscreen="!display.mdAndUp.value"
|
||||||
|
>
|
||||||
|
<VCard :title="`浏览站点 - ${cardProps.site?.name}`">
|
||||||
|
<DialogCloseBtn @click="resourceDialog = false" />
|
||||||
|
<VDivider />
|
||||||
|
<VCardText class="pt-2">
|
||||||
|
<SiteTorrentTable :site="cardProps.site?.id" />
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
<!-- 进度框 -->
|
||||||
|
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.v-table th {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { StorageConf } from '@/api/types'
|
|
||||||
import { formatBytes } from '@core/utils/formatters'
|
|
||||||
import storage_png from '@images/misc/storage.png'
|
|
||||||
import alipan_png from '@images/misc/alipan.webp'
|
|
||||||
import u115_png from '@images/misc/u115.png'
|
|
||||||
import rclone_png from '@images/misc/rclone.png'
|
|
||||||
import alist_png from '@images/misc/alist.svg'
|
|
||||||
import api from '@/api'
|
|
||||||
import AliyunAuthDialog from '../dialog/AliyunAuthDialog.vue'
|
|
||||||
import U115AuthDialog from '../dialog/U115AuthDialog.vue'
|
|
||||||
import RcloneConfigDialog from '../dialog/RcloneConfigDialog.vue'
|
|
||||||
import AlistConfigDialog from '../dialog/AlistConfigDialog.vue'
|
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
|
||||||
|
|
||||||
// 定义输入
|
|
||||||
const props = defineProps({
|
|
||||||
storage: {
|
|
||||||
type: Object as PropType<StorageConf>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义事件
|
|
||||||
const emit = defineEmits(['done'])
|
|
||||||
|
|
||||||
// 提示信息
|
|
||||||
const $toast = useToast()
|
|
||||||
|
|
||||||
// 存储总空间
|
|
||||||
const total = ref(0)
|
|
||||||
|
|
||||||
// 存储可用空间
|
|
||||||
const available = ref(0)
|
|
||||||
|
|
||||||
// 储存已用空间
|
|
||||||
const used = computed(() => {
|
|
||||||
return total.value - available.value
|
|
||||||
})
|
|
||||||
|
|
||||||
// 阿里云盘认证对话框
|
|
||||||
const aliyunAuthDialog = ref(false)
|
|
||||||
// 115网盘认证对话框
|
|
||||||
const u115AuthDialog = ref(false)
|
|
||||||
// Rclone配置对话框
|
|
||||||
const rcloneConfigDialog = ref(false)
|
|
||||||
// AList配置对话框
|
|
||||||
const aListConfigDialog = ref(false)
|
|
||||||
|
|
||||||
// 打开存储对话框
|
|
||||||
function openStorageDialog() {
|
|
||||||
switch (props.storage.type) {
|
|
||||||
case 'alipan':
|
|
||||||
aliyunAuthDialog.value = true
|
|
||||||
break
|
|
||||||
case 'u115':
|
|
||||||
u115AuthDialog.value = true
|
|
||||||
break
|
|
||||||
case 'rclone':
|
|
||||||
rcloneConfigDialog.value = true
|
|
||||||
break
|
|
||||||
case 'alist':
|
|
||||||
aListConfigDialog.value = true
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
$toast.info('此存储类型无需配置参数,请直接配置目录!')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据存储类型选择图标
|
|
||||||
const getIcon = computed(() => {
|
|
||||||
switch (props.storage.type) {
|
|
||||||
case 'local':
|
|
||||||
return storage_png
|
|
||||||
case 'alipan':
|
|
||||||
return alipan_png
|
|
||||||
case 'u115':
|
|
||||||
return u115_png
|
|
||||||
case 'rclone':
|
|
||||||
return rclone_png
|
|
||||||
case 'alist':
|
|
||||||
return alist_png
|
|
||||||
default:
|
|
||||||
return storage_png
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算进度条颜色
|
|
||||||
const progressColor = computed(() => {
|
|
||||||
if (usage.value > 90) {
|
|
||||||
return 'error'
|
|
||||||
} else if (usage.value > 70) {
|
|
||||||
return 'warning'
|
|
||||||
} else {
|
|
||||||
return 'success'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算存储使用率
|
|
||||||
const usage = computed(() => {
|
|
||||||
return Math.round((used.value / (total.value || 1)) * 1000) / 10
|
|
||||||
})
|
|
||||||
|
|
||||||
// 查询存储信息
|
|
||||||
async function queryStorage() {
|
|
||||||
try {
|
|
||||||
const data: { total: number; available: number } = await api.get(`storage/usage/${props.storage.type}`)
|
|
||||||
total.value = data.total
|
|
||||||
available.value = data.available
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 完成配置后的处理
|
|
||||||
function handleDone() {
|
|
||||||
aliyunAuthDialog.value = false
|
|
||||||
u115AuthDialog.value = false
|
|
||||||
rcloneConfigDialog.value = false
|
|
||||||
aListConfigDialog.value = false
|
|
||||||
emit('done')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
queryStorage()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<VCard variant="tonal" @click="openStorageDialog">
|
|
||||||
<VCardText class="flex justify-space-between align-center gap-3">
|
|
||||||
<div class="align-self-start flex-1">
|
|
||||||
<h5 class="text-h6 mb-1">{{ storage.name }}</h5>
|
|
||||||
<div class="mb-3 text-sm" v-if="total">{{ formatBytes(used, 1) }} / {{ formatBytes(total, 1) }}</div>
|
|
||||||
<div v-else-if="isNullOrEmptyObject(storage.config)">未配置</div>
|
|
||||||
</div>
|
|
||||||
<VImg :src="getIcon" cover class="mt-5" max-width="3rem" min-width="3rem" />
|
|
||||||
</VCardText>
|
|
||||||
<div class="w-full absolute bottom-0">
|
|
||||||
<VProgressLinear v-if="usage > 0" :model-value="usage" :bg-color="progressColor" :color="progressColor" />
|
|
||||||
</div>
|
|
||||||
</VCard>
|
|
||||||
<AliyunAuthDialog
|
|
||||||
v-if="aliyunAuthDialog"
|
|
||||||
v-model="aliyunAuthDialog"
|
|
||||||
:conf="props.storage.config || {}"
|
|
||||||
@close="aliyunAuthDialog = false"
|
|
||||||
@done="handleDone"
|
|
||||||
/>
|
|
||||||
<U115AuthDialog
|
|
||||||
v-if="u115AuthDialog"
|
|
||||||
v-model="u115AuthDialog"
|
|
||||||
:conf="props.storage.config || {}"
|
|
||||||
@close="u115AuthDialog = false"
|
|
||||||
@done="handleDone"
|
|
||||||
/>
|
|
||||||
<RcloneConfigDialog
|
|
||||||
v-if="rcloneConfigDialog"
|
|
||||||
v-model="rcloneConfigDialog"
|
|
||||||
:conf="props.storage.config || {}"
|
|
||||||
@close="rcloneConfigDialog = false"
|
|
||||||
@done="handleDone"
|
|
||||||
/>
|
|
||||||
<AlistConfigDialog
|
|
||||||
v-if="aListConfigDialog"
|
|
||||||
v-model="aListConfigDialog"
|
|
||||||
:conf="props.storage.config || {}"
|
|
||||||
@close="aListConfigDialog = false"
|
|
||||||
@done="handleDone"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -2,9 +2,8 @@
|
|||||||
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 SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
||||||
import SubscribeFilesDialog from '../dialog/SubscribeFilesDialog.vue'
|
import { formatDateDifference } from '@/@core/utils/formatters'
|
||||||
import SubscribeShareDialog from '../dialog/SubscribeShareDialog.vue'
|
import { formatSeason } from '@/@core/utils/formatters'
|
||||||
import { formatDateDifference, formatSeason } from '@/@core/utils/formatters'
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Subscribe } from '@/api/types'
|
import type { Subscribe } from '@/api/types'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
@@ -14,9 +13,6 @@ const props = defineProps({
|
|||||||
media: Object as PropType<Subscribe>,
|
media: Object as PropType<Subscribe>,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 从 provide 中获取全局设置
|
|
||||||
const globalSettings: any = inject('globalSettings')
|
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
// 定义触发的自定义事件
|
||||||
const emit = defineEmits(['remove', 'save'])
|
const emit = defineEmits(['remove', 'save'])
|
||||||
|
|
||||||
@@ -32,23 +28,21 @@ const imageLoaded = ref(false)
|
|||||||
// 订阅弹窗
|
// 订阅弹窗
|
||||||
const subscribeEditDialog = ref(false)
|
const subscribeEditDialog = ref(false)
|
||||||
|
|
||||||
// 订阅文件信息弹窗
|
|
||||||
const subscribeFilesDialog = ref(false)
|
|
||||||
|
|
||||||
// 分享订阅弹窗
|
|
||||||
const subscribeShareDialog = ref(false)
|
|
||||||
|
|
||||||
// 当前的订阅状态
|
|
||||||
const subscribeState = ref<string>(props.media?.state ?? 'P')
|
|
||||||
|
|
||||||
// 上一次更新时间
|
// 上一次更新时间
|
||||||
const lastUpdateText = computed(() => (props.media?.last_update ? formatDateDifference(props.media.last_update) : ''))
|
const lastUpdateText = ref(props.media && props.media.last_update ? formatDateDifference(props.media.last_update) : '')
|
||||||
|
|
||||||
// 图片加载完成响应
|
// 图片加载完成响应
|
||||||
function imageLoadHandler() {
|
function imageLoadHandler() {
|
||||||
imageLoaded.value = true
|
imageLoaded.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据 type 返回不同的图标
|
||||||
|
function getIcon() {
|
||||||
|
if (props.media?.type === '电影') return 'mdi-movie-open'
|
||||||
|
else if (props.media?.type === '电视剧') return 'mdi-television-play'
|
||||||
|
else return 'mdi-help-circle'
|
||||||
|
}
|
||||||
|
|
||||||
// 计算百分比
|
// 计算百分比
|
||||||
function getPercentage() {
|
function getPercentage() {
|
||||||
if (props.media?.total_episode === 0) return 0
|
if (props.media?.total_episode === 0) return 0
|
||||||
@@ -84,39 +78,13 @@ 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
|
||||||
// 重置
|
// 重置
|
||||||
@@ -124,7 +92,6 @@ 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) {
|
||||||
@@ -132,44 +99,24 @@ async function resetSubscribe() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分享订阅
|
|
||||||
async function shareSubscribe() {
|
|
||||||
subscribeShareDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 编辑订阅响应
|
// 编辑订阅响应
|
||||||
async function editSubscribeDialog() {
|
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: getMediaId(),
|
mediaid: `${props.media?.tmdbid ? `tmdb:${props.media?.tmdbid}` : `douban:${props.media?.doubanid}`}`,
|
||||||
title: props.media?.name,
|
|
||||||
year: props.media?.year,
|
|
||||||
type: props.media?.type,
|
type: props.media?.type,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看文件详情
|
|
||||||
async function viewSubscribeFiles() {
|
|
||||||
subscribeFilesDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 弹出菜单
|
// 弹出菜单
|
||||||
const dropdownItems = computed(() => [
|
const dropdownItems = ref([
|
||||||
{
|
{
|
||||||
title: '编辑',
|
title: '编辑',
|
||||||
value: 1,
|
value: 1,
|
||||||
@@ -187,52 +134,26 @@ const dropdownItems = computed(() => [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '详情',
|
title: '查看详情',
|
||||||
value: 3,
|
value: 3,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-information-outline',
|
prependIcon: 'mdi-open-in-new',
|
||||||
click: viewMediaDetail,
|
click: viewMediaDetail,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: '文件',
|
|
||||||
value: 4,
|
|
||||||
props: {
|
|
||||||
prependIcon: 'mdi-file-document-outline',
|
|
||||||
click: viewSubscribeFiles,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: subscribeState.value === 'S' ? '启用' : '暂停',
|
|
||||||
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: '重置',
|
title: '重置',
|
||||||
value: 6,
|
value: 4,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-restore-alert',
|
prependIcon: 'mdi-restore-alert',
|
||||||
click: resetSubscribe,
|
click: resetSubscribe,
|
||||||
color: 'warning',
|
color: 'warning',
|
||||||
},
|
},
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '分享',
|
|
||||||
value: 7,
|
|
||||||
props: {
|
|
||||||
prependIcon: 'mdi-share',
|
|
||||||
click: shareSubscribe,
|
|
||||||
color: 'success',
|
|
||||||
},
|
|
||||||
show: props.media?.type === '电视剧',
|
show: props.media?.type === '电视剧',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '取消订阅',
|
title: '取消订阅',
|
||||||
value: 8,
|
value: 5,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-trash-can-outline',
|
prependIcon: 'mdi-trash-can-outline',
|
||||||
color: 'error',
|
color: 'error',
|
||||||
@@ -248,177 +169,133 @@ watch(
|
|||||||
if (newOpenState) editSubscribeDialog()
|
if (newOpenState) editSubscribeDialog()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// 监听订阅状态
|
|
||||||
watch(
|
|
||||||
() => props.media?.state,
|
|
||||||
newState => {
|
|
||||||
subscribeState.value = newState ?? 'P'
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 计算backdrop图片地址
|
|
||||||
const backdropUrl = computed(() => {
|
|
||||||
const url = props.media?.backdrop || 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
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算海报图片地址
|
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
// 订阅编辑保存
|
|
||||||
function onSubscribeEditSave() {
|
|
||||||
subscribeEditDialog.value = false
|
|
||||||
emit('save')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 订阅编辑取消
|
|
||||||
function onSubscribeEditRemove() {
|
|
||||||
subscribeEditDialog.value = false
|
|
||||||
emit('remove')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<VHover>
|
||||||
<VHover>
|
<template #default="hover">
|
||||||
<template #default="hover">
|
<VCard
|
||||||
<VCard
|
v-bind="hover.props"
|
||||||
v-bind="hover.props"
|
:key="props.media?.id"
|
||||||
:key="props.media?.id"
|
class="flex flex-col rounded-lg"
|
||||||
class="flex flex-col rounded-lg h-full"
|
:class="{
|
||||||
:class="{
|
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
}"
|
||||||
'opacity-70': subscribeState === 'S',
|
min-height="170"
|
||||||
}"
|
@click="editSubscribeDialog"
|
||||||
min-height="170"
|
>
|
||||||
@click="editSubscribeDialog"
|
<div class="me-n3 absolute top-1 right-2">
|
||||||
>
|
<IconBtn>
|
||||||
<div class="me-n3 absolute top-1 right-2">
|
<VIcon icon="mdi-dots-vertical" color="white" />
|
||||||
<IconBtn>
|
<VMenu activator="parent" close-on-content-click>
|
||||||
<VIcon icon="mdi-dots-vertical" color="white" />
|
<VList>
|
||||||
<VMenu activator="parent" close-on-content-click>
|
<template v-for="(item, i) in dropdownItems" :key="i">
|
||||||
<VList>
|
<VListItem
|
||||||
<template v-for="(item, i) in dropdownItems" :key="i">
|
v-if="item.show !== false"
|
||||||
<VListItem
|
variant="plain"
|
||||||
v-if="item.show !== false"
|
:base-color="item.props.color"
|
||||||
variant="plain"
|
@click="item.props.click"
|
||||||
:base-color="item.props.color"
|
>
|
||||||
@click="item.props.click"
|
<template #prepend>
|
||||||
>
|
<VIcon :icon="item.props.prependIcon" />
|
||||||
<template #prepend>
|
</template>
|
||||||
<VIcon :icon="item.props.prependIcon" />
|
<VListItemTitle v-text="item.title" />
|
||||||
</template>
|
</VListItem>
|
||||||
<VListItemTitle v-text="item.title" />
|
</template>
|
||||||
</VListItem>
|
</VList>
|
||||||
</template>
|
</VMenu>
|
||||||
</VList>
|
</IconBtn>
|
||||||
</VMenu>
|
</div>
|
||||||
</IconBtn>
|
<template #image>
|
||||||
</div>
|
<VImg
|
||||||
<template #image>
|
:src="props.media?.backdrop || props.media?.poster"
|
||||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
aspect-ratio="3/2"
|
||||||
<template #placeholder>
|
cover
|
||||||
<div class="w-full h-full">
|
@load="imageLoadHandler"
|
||||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
position="top"
|
||||||
</div>
|
>
|
||||||
</template>
|
<template #placeholder>
|
||||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
<div class="w-full h-full">
|
||||||
</VImg>
|
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||||
<div v-if="subscribeState === 'P'" class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none" />
|
|
||||||
</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>
|
||||||
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
</template>
|
||||||
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||||
<div class="mr-2 min-w-0 text-lg font-bold text-white">
|
</VImg>
|
||||||
{{ props.media?.name }}
|
</template>
|
||||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
<div>
|
||||||
</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="props.media?.poster" 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>
|
</div>
|
||||||
<VCardText class="flex justify-space-between align-center flex-wrap">
|
</VCardText>
|
||||||
<div class="flex align-center">
|
<VCardText class="flex justify-space-between align-center flex-wrap">
|
||||||
<IconBtn
|
<div class="flex align-center">
|
||||||
v-if="props.media?.total_episode"
|
<IconBtn
|
||||||
v-bind="props"
|
v-if="props.media?.total_episode"
|
||||||
icon="mdi-progress-download"
|
v-bind="props"
|
||||||
color="white"
|
icon="mdi-progress-download"
|
||||||
class="me-1"
|
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 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>
|
</div>
|
||||||
<div v-if="hover.isHovering" class="me-n3 absolute top-1 right-10">
|
</VCardText>
|
||||||
<IconBtn><VIcon class="cursor-move text-white">mdi-drag</VIcon></IconBtn>
|
<VCardText v-if="lastUpdateText" class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
||||||
</div>
|
<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>
|
||||||
</VCard>
|
</div>
|
||||||
</template>
|
</VCard>
|
||||||
</VHover>
|
</template>
|
||||||
<!-- 订阅编辑弹窗 -->
|
</VHover>
|
||||||
<SubscribeEditDialog
|
<!-- 订阅编辑弹窗 -->
|
||||||
v-if="subscribeEditDialog"
|
<SubscribeEditDialog
|
||||||
v-model="subscribeEditDialog"
|
v-if="subscribeEditDialog"
|
||||||
:subid="props.media?.id"
|
v-model="subscribeEditDialog"
|
||||||
@remove="onSubscribeEditRemove"
|
:subid="props.media?.id"
|
||||||
@save="onSubscribeEditSave"
|
@remove="
|
||||||
@close="subscribeEditDialog = false"
|
() => {
|
||||||
/>
|
emit('remove')
|
||||||
|
subscribeEditDialog = false
|
||||||
<!-- 订阅文件信息弹窗 -->
|
}
|
||||||
<SubscribeFilesDialog
|
"
|
||||||
v-if="subscribeFilesDialog"
|
@save="
|
||||||
v-model="subscribeFilesDialog"
|
() => {
|
||||||
:subid="props.media?.id"
|
emit('save')
|
||||||
@close="subscribeFilesDialog = false"
|
subscribeEditDialog = false
|
||||||
/>
|
}
|
||||||
<!-- 分享订阅弹窗 -->
|
"
|
||||||
<SubscribeShareDialog
|
@close="subscribeEditDialog = false"
|
||||||
v-if="subscribeShareDialog"
|
/>
|
||||||
v-model="subscribeShareDialog"
|
|
||||||
:sub="props.media"
|
|
||||||
@close="subscribeShareDialog = false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.subscribe-card-background {
|
.subscribe-card-background {
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { formatDateDifference } from '@/@core/utils/formatters'
|
|
||||||
import type { SubscribeShare } from '@/api/types'
|
|
||||||
import router from '@/router'
|
|
||||||
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
|
||||||
import ForkSubscribeDialog from '../dialog/ForkSubscribeDialog.vue'
|
|
||||||
|
|
||||||
// 输入参数
|
|
||||||
const props = defineProps({
|
|
||||||
media: Object as PropType<SubscribeShare>,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义删除事件
|
|
||||||
const emit = defineEmits(['delete'])
|
|
||||||
|
|
||||||
// 从 provide 中获取全局设置
|
|
||||||
const globalSettings: any = inject('globalSettings')
|
|
||||||
|
|
||||||
// 图片是否加载完成
|
|
||||||
const imageLoaded = ref(false)
|
|
||||||
|
|
||||||
// 订阅编辑弹窗
|
|
||||||
const subscribeEditDialog = ref(false)
|
|
||||||
|
|
||||||
// 复用订阅弹窗
|
|
||||||
const forkSubscribeDialog = ref(false)
|
|
||||||
|
|
||||||
// 订阅ID
|
|
||||||
const subscribeId = ref<number>()
|
|
||||||
|
|
||||||
// 图片加载完成响应
|
|
||||||
function imageLoadHandler() {
|
|
||||||
imageLoaded.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分享时间
|
|
||||||
const dateText = ref(props.media && props.media?.date ? formatDateDifference(props.media.date) : '')
|
|
||||||
|
|
||||||
// 计算backdrop图片地址
|
|
||||||
const backdropUrl = computed(() => {
|
|
||||||
const url = props.media?.backdrop || 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
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算海报图片地址
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 复用订阅
|
|
||||||
function showForkSubscribe() {
|
|
||||||
forkSubscribeDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 完成复用订阅
|
|
||||||
function finishForkSubscribe(subid: number) {
|
|
||||||
subscribeId.value = subid
|
|
||||||
forkSubscribeDialog.value = false
|
|
||||||
subscribeEditDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除订阅分享时处理
|
|
||||||
function doDelete() {
|
|
||||||
forkSubscribeDialog.value = false
|
|
||||||
// 通知父组件刷新
|
|
||||||
emit('delete')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<VHover>
|
|
||||||
<template #default="hover">
|
|
||||||
<VCard
|
|
||||||
v-bind="hover.props"
|
|
||||||
:key="props.media?.id"
|
|
||||||
class="flex flex-col rounded-lg"
|
|
||||||
:class="{
|
|
||||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
|
||||||
}"
|
|
||||||
min-height="170"
|
|
||||||
@click="showForkSubscribe"
|
|
||||||
>
|
|
||||||
<template #image>
|
|
||||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
|
||||||
<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 pb-1">
|
|
||||||
<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 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 class="text-sm font-medium text-gray-200 sm:pt-1 line-clamp-3 overflow-hidden text-ellipsis ...">
|
|
||||||
{{ props.media?.share_comment }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
<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>
|
|
||||||
</VCardText>
|
|
||||||
<VCardText class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
|
||||||
<VIcon icon="mdi-calcdar" class="me-1" />
|
|
||||||
{{ dateText }}
|
|
||||||
</VCardText>
|
|
||||||
</div>
|
|
||||||
</VCard>
|
|
||||||
</template>
|
|
||||||
</VHover>
|
|
||||||
<!-- 订阅编辑弹窗 -->
|
|
||||||
<SubscribeEditDialog
|
|
||||||
v-if="subscribeEditDialog"
|
|
||||||
v-model="subscribeEditDialog"
|
|
||||||
:subid="subscribeId"
|
|
||||||
@close="subscribeEditDialog = false"
|
|
||||||
@save="subscribeEditDialog = false"
|
|
||||||
@remove="subscribeEditDialog = false"
|
|
||||||
/>
|
|
||||||
<!-- 复用订阅弹窗 -->
|
|
||||||
<ForkSubscribeDialog
|
|
||||||
v-if="forkSubscribeDialog"
|
|
||||||
v-model="forkSubscribeDialog"
|
|
||||||
:media="props.media"
|
|
||||||
@close="forkSubscribeDialog = false"
|
|
||||||
@fork="finishForkSubscribe"
|
|
||||||
@delete="doDelete"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<style lang="scss">
|
|
||||||
.subscribe-card-background {
|
|
||||||
background-image: linear-gradient(90deg, rgba(31, 41, 55, 47%) 0%, rgb(31, 41, 55) 100%);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import { useConfirm } from 'vuetify-use-dialog'
|
||||||
import { formatFileSize } from '@/@core/utils/formatters'
|
import { formatFileSize } from '@/@core/utils/formatters'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Context } from '@/api/types'
|
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||||
import AddDownloadDialog from '../dialog/AddDownloadDialog.vue'
|
import type { Context, MediaInfo, TorrentInfo } from '@/api/types'
|
||||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -14,6 +15,12 @@ const props = defineProps({
|
|||||||
height: String,
|
height: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 提示框
|
||||||
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 确认框
|
||||||
|
const createConfirm = useConfirm()
|
||||||
|
|
||||||
// 更多来源界面
|
// 更多来源界面
|
||||||
const showMoreTorrents = ref(false)
|
const showMoreTorrents = ref(false)
|
||||||
|
|
||||||
@@ -26,29 +33,11 @@ const media = ref(props.torrent?.media_info)
|
|||||||
// 识别元数据
|
// 识别元数据
|
||||||
const meta = ref(props.torrent?.meta_info)
|
const meta = ref(props.torrent?.meta_info)
|
||||||
|
|
||||||
// 当前下载项
|
|
||||||
const downloadItem = ref(props.torrent)
|
|
||||||
|
|
||||||
// 站点图标
|
// 站点图标
|
||||||
const siteIcon = ref('')
|
const siteIcon = ref('')
|
||||||
|
|
||||||
// 存储是否已经下载过的记录
|
// 存储是否已经下载过的记录
|
||||||
const downloaded = ref<string[]>([])
|
const downloaded = ref<String[]>([])
|
||||||
|
|
||||||
// 添加下载对话框
|
|
||||||
const addDownloadDialog = ref(false)
|
|
||||||
|
|
||||||
// 添加下载成功
|
|
||||||
function addDownloadSuccess(url: string) {
|
|
||||||
addDownloadDialog.value = false
|
|
||||||
// 添加下载成功
|
|
||||||
downloaded.value.push(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加下载失败
|
|
||||||
function addDownloadError(error: string) {
|
|
||||||
addDownloadDialog.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询站点图标
|
// 查询站点图标
|
||||||
async function getSiteIcon() {
|
async function getSiteIcon() {
|
||||||
@@ -60,12 +49,50 @@ async function getSiteIcon() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 询问并添加下载
|
// 询问并添加下载
|
||||||
async function handleAddDownload(item: Context | null = null) {
|
async function handleAddDownload(_site: any = undefined, _media: any = undefined, _torrent: any = undefined) {
|
||||||
if (item && !isNullOrEmptyObject(item)) {
|
if (!_media || !_torrent || !_site) {
|
||||||
downloadItem.value = item
|
_site = torrent.value?.site_name
|
||||||
|
_media = media.value
|
||||||
|
_torrent = torrent.value
|
||||||
}
|
}
|
||||||
// 打开下载对话框
|
|
||||||
addDownloadDialog.value = true
|
const isConfirmed = await createConfirm({
|
||||||
|
title: '确认',
|
||||||
|
content: `是否确认下载【${_site}】${_torrent?.title} ?`,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isConfirmed) return
|
||||||
|
|
||||||
|
addDownload(_media, _torrent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加下载
|
||||||
|
async function addDownload(_media: MediaInfo, _torrent: TorrentInfo) {
|
||||||
|
startNProgress()
|
||||||
|
try {
|
||||||
|
let result: { [key: string]: any }
|
||||||
|
|
||||||
|
if (_media) {
|
||||||
|
result = await api.post('download/', {
|
||||||
|
media_in: _media,
|
||||||
|
torrent_in: _torrent,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
result = await api.post('download/add', _torrent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result && result.success) {
|
||||||
|
// 添加下载成功
|
||||||
|
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 下载成功!`)
|
||||||
|
downloaded.value.push(_torrent?.enclosure || '')
|
||||||
|
} else {
|
||||||
|
// 添加下载失败
|
||||||
|
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 下载失败:${result?.message}!`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
doneNProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开种子详情页面
|
// 打开种子详情页面
|
||||||
@@ -93,137 +120,127 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<VCard
|
||||||
<VCard
|
:width="props.width"
|
||||||
:width="props.width"
|
:height="props.height"
|
||||||
:height="props.height"
|
:variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'elevated'"
|
||||||
:variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'elevated'"
|
@click="handleAddDownload"
|
||||||
@click="handleAddDownload(props.torrent)"
|
>
|
||||||
>
|
<template v-if="!showMoreTorrents" #image>
|
||||||
<template v-if="!showMoreTorrents" #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">
|
<VImg :src="siteIcon" />
|
||||||
<VImg :src="siteIcon" />
|
</VAvatar>
|
||||||
</VAvatar>
|
</template>
|
||||||
</template>
|
<VCardItem class="py-1">
|
||||||
<VCardItem class="py-1">
|
<VCardTitle class="break-words overflow-visible whitespace-break-spaces">
|
||||||
<VCardTitle class="break-words overflow-visible whitespace-break-spaces">
|
{{ media?.title ?? meta?.name }} {{ meta?.season_episode }}
|
||||||
{{ media?.title ?? meta?.name }} {{ meta?.season_episode }}
|
<span class="text-green-700 ms-2 text-sm">↑{{ torrent?.seeders }}</span>
|
||||||
<span class="text-green-700 ms-2 text-sm">↑{{ torrent?.seeders }}</span>
|
<span class="text-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
||||||
<span class="text-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
</VCardTitle>
|
||||||
</VCardTitle>
|
<template #append>
|
||||||
<template #append>
|
<div class="me-n3">
|
||||||
<div class="me-n3">
|
<IconBtn>
|
||||||
<IconBtn>
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
<VIcon icon="mdi-dots-vertical" />
|
<VMenu activator="parent" close-on-content-click>
|
||||||
<VMenu activator="parent" close-on-content-click>
|
<VList>
|
||||||
<VList>
|
<VListItem variant="plain" @click="openTorrentDetail()">
|
||||||
<VListItem variant="plain" @click="openTorrentDetail()">
|
<template #prepend>
|
||||||
<template #prepend>
|
<VIcon icon="mdi-information" />
|
||||||
<VIcon icon="mdi-information" />
|
</template>
|
||||||
</template>
|
<VListItemTitle>查看详情</VListItemTitle>
|
||||||
<VListItemTitle>查看详情</VListItemTitle>
|
</VListItem>
|
||||||
</VListItem>
|
<VListItem
|
||||||
<VListItem
|
v-if="props.torrent?.torrent_info?.enclosure?.startsWith('http')"
|
||||||
v-if="props.torrent?.torrent_info?.enclosure?.startsWith('http')"
|
variant="plain"
|
||||||
variant="plain"
|
@click="downloadTorrentFile()"
|
||||||
@click="downloadTorrentFile()"
|
>
|
||||||
>
|
<template #prepend>
|
||||||
<template #prepend>
|
<VIcon icon="mdi-download" />
|
||||||
<VIcon icon="mdi-download" />
|
</template>
|
||||||
</template>
|
<VListItemTitle>下载种子文件</VListItemTitle>
|
||||||
<VListItemTitle>下载种子文件</VListItemTitle>
|
</VListItem>
|
||||||
</VListItem>
|
</VList>
|
||||||
</VList>
|
</VMenu>
|
||||||
</VMenu>
|
</IconBtn>
|
||||||
</IconBtn>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</VCardItem>
|
|
||||||
<VCardText class="text-subtitle-2">
|
|
||||||
{{ torrent?.title }}
|
|
||||||
</VCardText>
|
|
||||||
<VCardText>【{{ torrent?.site_name }}】{{ torrent?.description }}</VCardText>
|
|
||||||
<VCardItem v-if="torrent?.labels" class="pb-3 pt-0 pe-12">
|
|
||||||
<VChip v-if="torrent?.hit_and_run" variant="elevated" size="small" class="me-1 mb-1 text-white bg-black">
|
|
||||||
H&R
|
|
||||||
</VChip>
|
|
||||||
<VChip v-if="torrent?.freedate_diff" variant="elevated" color="secondary" size="small" class="me-1 mb-1">
|
|
||||||
{{ torrent?.freedate_diff }}
|
|
||||||
</VChip>
|
|
||||||
<VChip
|
|
||||||
v-for="(label, index) in torrent?.labels"
|
|
||||||
:key="index"
|
|
||||||
variant="elevated"
|
|
||||||
size="small"
|
|
||||||
color="primary"
|
|
||||||
class="me-1 mb-1"
|
|
||||||
>
|
|
||||||
{{ label }}
|
|
||||||
</VChip>
|
|
||||||
<VChip v-if="meta?.edition" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
|
||||||
{{ meta?.edition }}
|
|
||||||
</VChip>
|
|
||||||
<VChip v-if="meta?.resource_pix" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
|
||||||
{{ meta?.resource_pix }}
|
|
||||||
</VChip>
|
|
||||||
<VChip v-if="meta?.video_encode" variant="elevated" size="small" class="me-1 mb-1 text-white bg-orange-500">
|
|
||||||
{{ meta?.video_encode }}
|
|
||||||
</VChip>
|
|
||||||
<VChip v-if="torrent?.size" variant="elevated" size="small" class="me-1 mb-1 text-white bg-yellow-500">
|
|
||||||
{{ formatFileSize(torrent?.size) }}
|
|
||||||
</VChip>
|
|
||||||
<VChip v-if="meta?.resource_team" variant="elevated" size="small" class="me-1 mb-1 text-white bg-cyan-500">
|
|
||||||
{{ meta?.resource_team }}
|
|
||||||
</VChip>
|
|
||||||
<VChip
|
|
||||||
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
|
|
||||||
:class="getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)"
|
|
||||||
variant="elevated"
|
|
||||||
size="small"
|
|
||||||
class="me-1 mb-1"
|
|
||||||
>
|
|
||||||
{{ torrent?.volume_factor }}
|
|
||||||
</VChip>
|
|
||||||
</VCardItem>
|
|
||||||
<VCardActions>
|
|
||||||
<VBtn v-if="props.more && props.more.length > 0" @click.stop="showMoreTorrents = !showMoreTorrents">
|
|
||||||
<template #append>
|
|
||||||
<VIcon :icon="showMoreTorrents ? 'mdi-chevron-up' : 'mdi-chevron-down'" />
|
|
||||||
</template>
|
|
||||||
更多来源
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
<VExpandTransition>
|
|
||||||
<div v-show="showMoreTorrents">
|
|
||||||
<VDivider />
|
|
||||||
<VChipGroup class="p-3" column>
|
|
||||||
<VChip v-for="(item, index) in props.more" :key="index" @click.stop="handleAddDownload(item)">
|
|
||||||
<template #append>
|
|
||||||
<VBadge color="primary" :content="`↑${item.torrent_info?.seeders}`" inline size="small" />
|
|
||||||
<VBadge
|
|
||||||
v-if="item.torrent_info?.downloadvolumefactor !== 1 || item.torrent_info?.uploadvolumefactor !== 1"
|
|
||||||
:content="item.torrent_info?.volume_factor"
|
|
||||||
inline
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
{{ item.torrent_info.site_name }}
|
|
||||||
</VChip>
|
|
||||||
</VChipGroup>
|
|
||||||
</div>
|
</div>
|
||||||
</VExpandTransition>
|
</template>
|
||||||
</VCard>
|
</VCardItem>
|
||||||
<AddDownloadDialog
|
<VCardText class="text-subtitle-2">
|
||||||
v-if="addDownloadDialog"
|
{{ torrent?.title }}
|
||||||
v-model="addDownloadDialog"
|
</VCardText>
|
||||||
:title="`${downloadItem?.media_info?.title_year || downloadItem?.meta_info?.name} ${
|
<VCardText>{{ torrent?.description }}</VCardText>
|
||||||
downloadItem?.meta_info?.season_episode
|
<VCardItem v-if="torrent?.labels" class="pb-3 pt-0 pe-12">
|
||||||
}`"
|
<VChip v-if="torrent?.hit_and_run" variant="elevated" size="small" class="me-1 mb-1 text-white bg-black">
|
||||||
:media="downloadItem?.media_info"
|
H&R
|
||||||
:torrent="downloadItem?.torrent_info"
|
</VChip>
|
||||||
@done="addDownloadSuccess"
|
<VChip v-if="torrent?.freedate_diff" variant="elevated" color="secondary" size="small" class="me-1 mb-1">
|
||||||
@error="addDownloadError"
|
{{ torrent?.freedate_diff }}
|
||||||
@close="addDownloadDialog = false"
|
</VChip>
|
||||||
/>
|
<VChip
|
||||||
</div>
|
v-for="(label, index) in torrent?.labels"
|
||||||
|
:key="index"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
class="me-1 mb-1"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</VChip>
|
||||||
|
<VChip v-if="meta?.edition" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
||||||
|
{{ meta?.edition }}
|
||||||
|
</VChip>
|
||||||
|
<VChip v-if="meta?.resource_pix" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
||||||
|
{{ meta?.resource_pix }}
|
||||||
|
</VChip>
|
||||||
|
<VChip v-if="meta?.video_encode" variant="elevated" size="small" class="me-1 mb-1 text-white bg-orange-500">
|
||||||
|
{{ meta?.video_encode }}
|
||||||
|
</VChip>
|
||||||
|
<VChip v-if="torrent?.size" variant="elevated" size="small" class="me-1 mb-1 text-white bg-yellow-500">
|
||||||
|
{{ formatFileSize(torrent?.size) }}
|
||||||
|
</VChip>
|
||||||
|
<VChip v-if="meta?.resource_team" variant="elevated" size="small" class="me-1 mb-1 text-white bg-cyan-500">
|
||||||
|
{{ meta?.resource_team }}
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
|
||||||
|
:class="getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1"
|
||||||
|
>
|
||||||
|
{{ torrent?.volume_factor }}
|
||||||
|
</VChip>
|
||||||
|
</VCardItem>
|
||||||
|
<VCardActions>
|
||||||
|
<VBtn v-if="props.more && props.more.length > 0" @click.stop="showMoreTorrents = !showMoreTorrents">
|
||||||
|
<template #append>
|
||||||
|
<VIcon :icon="showMoreTorrents ? 'mdi-chevron-up' : 'mdi-chevron-down'" />
|
||||||
|
</template>
|
||||||
|
更多来源
|
||||||
|
</VBtn>
|
||||||
|
</VCardActions>
|
||||||
|
<VExpandTransition>
|
||||||
|
<div v-show="showMoreTorrents">
|
||||||
|
<VDivider />
|
||||||
|
<VChipGroup class="p-3" column>
|
||||||
|
<VChip
|
||||||
|
v-for="(item, index) in props.more"
|
||||||
|
:key="index"
|
||||||
|
@click.stop="handleAddDownload(item.torrent_info?.site_name, item.media_info, item.torrent_info)"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<VBadge color="primary" :content="`↑${item.torrent_info?.seeders}`" inline size="small" />
|
||||||
|
<VBadge
|
||||||
|
v-if="item.torrent_info?.downloadvolumefactor !== 1 || item.torrent_info?.uploadvolumefactor !== 1"
|
||||||
|
:content="item.torrent_info?.volume_factor"
|
||||||
|
inline
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
{{ item.torrent_info.site_name }}
|
||||||
|
</VChip>
|
||||||
|
</VChipGroup>
|
||||||
|
</div>
|
||||||
|
</VExpandTransition>
|
||||||
|
</VCard>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import { useConfirm } from 'vuetify-use-dialog'
|
||||||
import { formatFileSize } from '@/@core/utils/formatters'
|
import { formatFileSize } from '@/@core/utils/formatters'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Context } from '@/api/types'
|
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||||
import AddDownloadDialog from '../dialog/AddDownloadDialog.vue'
|
import type { Context, MediaInfo, TorrentInfo } from '@/api/types'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
torrent: Object as PropType<Context>,
|
torrent: Object as PropType<Context>,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 提示框
|
||||||
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 确认框
|
||||||
|
const createConfirm = useConfirm()
|
||||||
|
|
||||||
// 更多来源界面
|
// 更多来源界面
|
||||||
const showMoreTorrents = ref(false)
|
const showMoreTorrents = ref(false)
|
||||||
|
|
||||||
@@ -26,10 +34,7 @@ const meta = ref(props.torrent?.meta_info)
|
|||||||
const siteIcon = ref('')
|
const siteIcon = ref('')
|
||||||
|
|
||||||
// 存储是否已经下载过的记录
|
// 存储是否已经下载过的记录
|
||||||
const downloaded = ref<string[]>([])
|
const downloaded = ref<String[]>([])
|
||||||
|
|
||||||
// 添加下载对话框
|
|
||||||
const addDownloadDialog = ref(false)
|
|
||||||
|
|
||||||
// 查询站点图标
|
// 查询站点图标
|
||||||
async function getSiteIcon() {
|
async function getSiteIcon() {
|
||||||
@@ -41,21 +46,50 @@ async function getSiteIcon() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 询问并添加下载
|
// 询问并添加下载
|
||||||
async function handleAddDownload() {
|
async function handleAddDownload(_site: any = undefined, _media: any = undefined, _torrent: any = undefined) {
|
||||||
// 打开下载对话框
|
if (!_media || !_torrent || !_site) {
|
||||||
addDownloadDialog.value = true
|
_site = torrent.value?.site_name
|
||||||
|
_media = media.value
|
||||||
|
_torrent = torrent.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const isConfirmed = await createConfirm({
|
||||||
|
title: '确认',
|
||||||
|
content: `是否确认下载【${_site}】${_torrent?.title} ?`,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isConfirmed) return
|
||||||
|
|
||||||
|
addDownload(_media, _torrent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加下载成功
|
// 添加下载
|
||||||
function addDownloadSuccess(url: string) {
|
async function addDownload(_media: MediaInfo, _torrent: TorrentInfo) {
|
||||||
addDownloadDialog.value = false
|
startNProgress()
|
||||||
// 添加下载成功
|
try {
|
||||||
downloaded.value.push(url)
|
let result: { [key: string]: any }
|
||||||
}
|
|
||||||
|
|
||||||
// 添加下载失败
|
if (_media) {
|
||||||
function addDownloadError(error: string) {
|
result = await api.post('download/', {
|
||||||
addDownloadDialog.value = false
|
media_in: _media,
|
||||||
|
torrent_in: _torrent,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
result = await api.post('download/add', _torrent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result && result.success) {
|
||||||
|
// 添加下载成功
|
||||||
|
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 下载成功!`)
|
||||||
|
downloaded.value.push(_torrent?.enclosure || '')
|
||||||
|
} else {
|
||||||
|
// 添加下载失败
|
||||||
|
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 下载失败:${result?.message}!`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
doneNProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开种子详情页面
|
// 打开种子详情页面
|
||||||
@@ -83,101 +117,88 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<VListItem @click="handleAddDownload" :variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'flat'">
|
||||||
<VListItem
|
<template v-if="!showMoreTorrents" #prepend>
|
||||||
@click="handleAddDownload"
|
<VAvatar class="rounded" variant="flat" @click.stop="openTorrentDetail">
|
||||||
:variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'flat'"
|
<VImg :src="siteIcon" />
|
||||||
>
|
</VAvatar>
|
||||||
<template v-if="!showMoreTorrents" #prepend>
|
</template>
|
||||||
<VAvatar class="rounded" variant="flat" @click.stop="openTorrentDetail">
|
<VListItemTitle class="break-words overflow-visible whitespace-break-spaces">
|
||||||
<VImg :src="siteIcon" />
|
{{ torrent?.title }}
|
||||||
</VAvatar>
|
<span class="text-green-700 ms-2 text-sm">↑{{ torrent?.seeders }}</span>
|
||||||
</template>
|
<span class="text-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
||||||
<VListItemTitle class="break-words overflow-visible whitespace-break-spaces">
|
</VListItemTitle>
|
||||||
{{ torrent?.title }}
|
<VListItemSubtitle>
|
||||||
<span class="text-green-700 ms-2 text-sm">↑{{ torrent?.seeders }}</span>
|
{{ torrent?.description }}
|
||||||
<span class="text-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
</VListItemSubtitle>
|
||||||
</VListItemTitle>
|
<div v-if="torrent?.labels" class="pt-2">
|
||||||
<VListItemSubtitle> 【{{ torrent?.site_name }}】{{ torrent?.description }} </VListItemSubtitle>
|
<VChip v-if="torrent?.hit_and_run" variant="elevated" size="small" class="me-1 mb-1 text-white bg-black">
|
||||||
<div v-if="torrent?.labels" class="pt-2">
|
H&R
|
||||||
<VChip v-if="torrent?.hit_and_run" variant="elevated" size="small" class="me-1 mb-1 text-white bg-black">
|
</VChip>
|
||||||
H&R
|
<VChip v-if="torrent?.freedate_diff" variant="elevated" color="secondary" size="small" class="me-1 mb-1">
|
||||||
</VChip>
|
{{ torrent?.freedate_diff }}
|
||||||
<VChip v-if="torrent?.freedate_diff" variant="elevated" color="secondary" size="small" class="me-1 mb-1">
|
</VChip>
|
||||||
{{ torrent?.freedate_diff }}
|
<VChip
|
||||||
</VChip>
|
v-for="(label, index) in torrent?.labels"
|
||||||
<VChip
|
:key="index"
|
||||||
v-for="(label, index) in torrent?.labels"
|
variant="elevated"
|
||||||
:key="index"
|
size="small"
|
||||||
variant="elevated"
|
color="primary"
|
||||||
size="small"
|
class="me-1 mb-1"
|
||||||
color="primary"
|
>
|
||||||
class="me-1 mb-1"
|
{{ label }}
|
||||||
>
|
</VChip>
|
||||||
{{ label }}
|
<VChip v-if="meta?.edition" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
||||||
</VChip>
|
{{ meta?.edition }}
|
||||||
<VChip v-if="meta?.edition" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
</VChip>
|
||||||
{{ meta?.edition }}
|
<VChip v-if="meta?.resource_pix" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
||||||
</VChip>
|
{{ meta?.resource_pix }}
|
||||||
<VChip v-if="meta?.resource_pix" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
</VChip>
|
||||||
{{ meta?.resource_pix }}
|
<VChip v-if="meta?.video_encode" variant="elevated" size="small" class="me-1 mb-1 text-white bg-orange-500">
|
||||||
</VChip>
|
{{ meta?.video_encode }}
|
||||||
<VChip v-if="meta?.video_encode" variant="elevated" size="small" class="me-1 mb-1 text-white bg-orange-500">
|
</VChip>
|
||||||
{{ meta?.video_encode }}
|
<VChip v-if="torrent?.size" variant="elevated" size="small" class="me-1 mb-1 text-white bg-yellow-500">
|
||||||
</VChip>
|
{{ formatFileSize(torrent?.size) }}
|
||||||
<VChip v-if="torrent?.size" variant="elevated" size="small" class="me-1 mb-1 text-white bg-yellow-500">
|
</VChip>
|
||||||
{{ formatFileSize(torrent?.size) }}
|
<VChip v-if="meta?.resource_team" variant="elevated" size="small" class="me-1 mb-1 text-white bg-cyan-500">
|
||||||
</VChip>
|
{{ meta?.resource_team }}
|
||||||
<VChip v-if="meta?.resource_team" variant="elevated" size="small" class="me-1 mb-1 text-white bg-cyan-500">
|
</VChip>
|
||||||
{{ meta?.resource_team }}
|
<VChip
|
||||||
</VChip>
|
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
|
||||||
<VChip
|
:class="getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)"
|
||||||
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
|
variant="elevated"
|
||||||
:class="getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)"
|
size="small"
|
||||||
variant="elevated"
|
class="me-1 mb-1"
|
||||||
size="small"
|
>
|
||||||
class="me-1 mb-1"
|
{{ torrent?.volume_factor }}
|
||||||
>
|
</VChip>
|
||||||
{{ torrent?.volume_factor }}
|
</div>
|
||||||
</VChip>
|
<template #append>
|
||||||
|
<div class="me-n3">
|
||||||
|
<IconBtn>
|
||||||
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
|
<VMenu activator="parent" close-on-content-click>
|
||||||
|
<VList>
|
||||||
|
<VListItem variant="plain" @click="openTorrentDetail()">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-information" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>查看详情</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem
|
||||||
|
v-if="props.torrent?.torrent_info?.enclosure?.startsWith('http')"
|
||||||
|
variant="plain"
|
||||||
|
@click="downloadTorrentFile()"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-download" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>下载种子文件</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
|
</IconBtn>
|
||||||
</div>
|
</div>
|
||||||
<template #append>
|
</template>
|
||||||
<div class="me-n3">
|
</VListItem>
|
||||||
<IconBtn>
|
|
||||||
<VIcon icon="mdi-dots-vertical" />
|
|
||||||
<VMenu activator="parent" close-on-content-click>
|
|
||||||
<VList>
|
|
||||||
<VListItem variant="plain" @click="openTorrentDetail()">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-information" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>查看详情</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem
|
|
||||||
v-if="props.torrent?.torrent_info?.enclosure?.startsWith('http')"
|
|
||||||
variant="plain"
|
|
||||||
@click="downloadTorrentFile()"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-download" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>下载种子文件</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
</VList>
|
|
||||||
</VMenu>
|
|
||||||
</IconBtn>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</VListItem>
|
|
||||||
<AddDownloadDialog
|
|
||||||
v-if="addDownloadDialog"
|
|
||||||
v-model="addDownloadDialog"
|
|
||||||
:title="`${media?.title_year || meta?.name} ${meta?.season_episode}`"
|
|
||||||
:media="media"
|
|
||||||
:torrent="torrent"
|
|
||||||
@done="addDownloadSuccess"
|
|
||||||
@error="addDownloadError"
|
|
||||||
@close="addDownloadDialog = false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import api from '@/api'
|
|
||||||
import { Subscribe, User } from '@/api/types'
|
|
||||||
import store from '@/store'
|
|
||||||
import avatar1 from '@images/avatars/avatar-1.png'
|
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import { useConfirm } from 'vuetify-use-dialog'
|
|
||||||
import UserAddEditDialog from '@/components/dialog/UserAddEditDialog.vue'
|
|
||||||
|
|
||||||
// 定义输入变量
|
|
||||||
const props = defineProps({
|
|
||||||
// 用户信息
|
|
||||||
user: {
|
|
||||||
type: Object as PropType<User>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
// 所有用户
|
|
||||||
users: {
|
|
||||||
type: Array as PropType<User[]>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 当前用户的ID
|
|
||||||
const currentLoginUserId = computed(() => store.state.auth.userID)
|
|
||||||
|
|
||||||
// 当前用户是否是管理员
|
|
||||||
const currentUserIsSuperuser = computed(() => store.state.auth.superUser)
|
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
|
||||||
const emit = defineEmits(['remove', 'save'])
|
|
||||||
|
|
||||||
// 确认框
|
|
||||||
const createConfirm = useConfirm()
|
|
||||||
|
|
||||||
// 用户信息弹窗
|
|
||||||
const userEditDialog = ref(false)
|
|
||||||
|
|
||||||
// 提示框
|
|
||||||
const $toast = useToast()
|
|
||||||
|
|
||||||
// 用户电影订阅数量
|
|
||||||
const movieSubscriptions = ref(0)
|
|
||||||
|
|
||||||
// 用户电视剧订阅数量
|
|
||||||
const tvShowSubscriptions = ref(0)
|
|
||||||
|
|
||||||
// 按用户查询订阅数量
|
|
||||||
async function fetchSubscriptions() {
|
|
||||||
try {
|
|
||||||
const result: Subscribe[] = await api.get(`subscribe/user/${props.user.name}`)
|
|
||||||
if (result) {
|
|
||||||
movieSubscriptions.value = result.filter(item => item.type === '电影').length
|
|
||||||
tvShowSubscriptions.value = result.filter(item => item.type === '电视剧').length
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除用户
|
|
||||||
async function removeUser() {
|
|
||||||
if (props.user.id === currentLoginUserId.value) {
|
|
||||||
$toast.error('不能删除当前登录用户!')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const isConfirmed = await createConfirm({
|
|
||||||
title: '注意',
|
|
||||||
content: `删除用户 ${props.user?.name} 的所有数据,是否确认?`,
|
|
||||||
})
|
|
||||||
if (!isConfirmed) return
|
|
||||||
const result: { [key: string]: any } = await api.delete(`user/id/${props.user.id}`)
|
|
||||||
if (result.success) {
|
|
||||||
$toast.success('用户删除成功')
|
|
||||||
emit('remove')
|
|
||||||
} else {
|
|
||||||
$toast.error('用户删除失败!')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 编辑用户
|
|
||||||
function editUser() {
|
|
||||||
userEditDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户重新完成时
|
|
||||||
function onUserUpdate() {
|
|
||||||
userEditDialog.value = false
|
|
||||||
emit('save')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchSubscriptions()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<VCard>
|
|
||||||
<VCardText class="text-center pt-10 pb-3">
|
|
||||||
<VAvatar variant="flat" size="100" rounded>
|
|
||||||
<VImg :src="user.avatar || avatar1" alt="avatar" />
|
|
||||||
</VAvatar>
|
|
||||||
<h5 class="text-h5 mt-3">{{ user.name }}</h5>
|
|
||||||
<VChip size="small" class="mt-3" :class="{ 'text-error': user.is_superuser }">
|
|
||||||
{{ user.is_superuser ? '管理员' : '普通用户' }}
|
|
||||||
</VChip>
|
|
||||||
</VCardText>
|
|
||||||
<VCardText class="flex justify-center gap-6 pb-5">
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
<VAvatar size="40" color="primary" rounded variant="tonal" class="me-4">
|
|
||||||
<VIcon size="24" icon="mdi-movie-open-outline"></VIcon>
|
|
||||||
</VAvatar>
|
|
||||||
<div>
|
|
||||||
<div class="text-h6">{{ movieSubscriptions }}</div>
|
|
||||||
<div class="text-sm text-no-wrap">电影订阅</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
<VAvatar size="40" color="primary" rounded variant="tonal" class="me-4">
|
|
||||||
<VIcon size="24" icon="mdi-television"></VIcon>
|
|
||||||
</VAvatar>
|
|
||||||
<div>
|
|
||||||
<div class="text-h6">{{ tvShowSubscriptions }}</div>
|
|
||||||
<div class="text-sm text-no-wrap">电视剧订阅</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
<VCardText class="pb-6">
|
|
||||||
<VDivider class="my-2">
|
|
||||||
<h5 class="text-h6">详情</h5>
|
|
||||||
</VDivider>
|
|
||||||
<VList lines="one">
|
|
||||||
<VListItem>
|
|
||||||
<VListItemTitle class="text-sm">
|
|
||||||
<span class="font-weight-medium">邮箱:</span><span class="text-body-1"> {{ user.email }}</span>
|
|
||||||
</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem>
|
|
||||||
<VListItemTitle class="text-sm">
|
|
||||||
<span class="font-weight-medium">状态:</span
|
|
||||||
><span class="text-body-1">
|
|
||||||
<VChip size="small" :class="{ 'text-success': user.is_active }" variant="tonal">
|
|
||||||
{{ user.is_active ? '激活' : '已停用' }}
|
|
||||||
</VChip>
|
|
||||||
</span>
|
|
||||||
</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem>
|
|
||||||
<VListItemTitle class="text-sm">
|
|
||||||
<span class="font-weight-medium">双重认证:</span
|
|
||||||
><span class="text-body-1">
|
|
||||||
<VChip size="small" :class="{ 'text-success': user.is_otp }" variant="tonal">
|
|
||||||
{{ user.is_otp ? '已启用' : '未启用' }}
|
|
||||||
</VChip>
|
|
||||||
</span>
|
|
||||||
</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
</VList>
|
|
||||||
</VCardText>
|
|
||||||
<VCardText class="flex flex-row justify-center">
|
|
||||||
<VBtn
|
|
||||||
v-if="currentUserIsSuperuser"
|
|
||||||
color="primary"
|
|
||||||
class="me-4"
|
|
||||||
@click="editUser"
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</VBtn>
|
|
||||||
<VBtn
|
|
||||||
v-if="currentUserIsSuperuser && props.user.id != currentLoginUserId"
|
|
||||||
color="error"
|
|
||||||
variant="outlined"
|
|
||||||
@click="removeUser"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</VBtn>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
<!-- 用户编辑弹窗 -->
|
|
||||||
<UserAddEditDialog
|
|
||||||
v-if="userEditDialog"
|
|
||||||
v-model="userEditDialog"
|
|
||||||
:username="props.user?.name"
|
|
||||||
:usernames="props.users.map(item => item.name)"
|
|
||||||
oper="edit"
|
|
||||||
@save="onUserUpdate"
|
|
||||||
@close="userEditDialog = false"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import api from '@/api'
|
|
||||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
|
||||||
import type { DownloaderConf, MediaInfo, TorrentInfo, TransferDirectoryConf } from '@/api/types'
|
|
||||||
import { formatFileSize } from '@/@core/utils/formatters'
|
|
||||||
import { VCardTitle, VChip } from 'vuetify/lib/components/index.mjs'
|
|
||||||
|
|
||||||
// 输入参数
|
|
||||||
const props = defineProps({
|
|
||||||
title: String,
|
|
||||||
media: Object as PropType<MediaInfo>,
|
|
||||||
torrent: Object as PropType<TorrentInfo>,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义成功和失败事件
|
|
||||||
const emit = defineEmits(['done', 'error', 'close'])
|
|
||||||
|
|
||||||
// 提示框
|
|
||||||
const $toast = useToast()
|
|
||||||
|
|
||||||
// 选择的下载器
|
|
||||||
const selectedDownloader = ref<string | null>(null)
|
|
||||||
|
|
||||||
// 选择的保存目录
|
|
||||||
const selectedDirectory = ref<string | null>(null)
|
|
||||||
|
|
||||||
// 下载器
|
|
||||||
const downloaders = ref<DownloaderConf[]>([])
|
|
||||||
|
|
||||||
// 所有目录设置
|
|
||||||
const directories = ref<TransferDirectoryConf[]>([])
|
|
||||||
|
|
||||||
// 是否正在加载
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
// 计算按钮图标
|
|
||||||
const icon = computed(() => (loading.value ? 'mdi-progress-download' : 'mdi-download'))
|
|
||||||
|
|
||||||
// 计算按钮文字
|
|
||||||
const buttonText = computed(() => (loading.value ? '下载中...' : '开始下载'))
|
|
||||||
|
|
||||||
// 加载目录设置
|
|
||||||
async function loadDirectories() {
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.get('system/setting/Directories')
|
|
||||||
directories.value = result.data?.value ?? []
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取保存目录
|
|
||||||
const targetDirectories = computed(() => {
|
|
||||||
const downloadDirectories = directories.value.map(item => item.download_path)
|
|
||||||
return [...new Set(downloadDirectories)]
|
|
||||||
})
|
|
||||||
|
|
||||||
// 调用API查询下载器设置
|
|
||||||
async function loadDownloaderSetting() {
|
|
||||||
try {
|
|
||||||
downloaders.value = await api.get('download/clients')
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下载器可选项
|
|
||||||
const downloaderOptions = computed(() => {
|
|
||||||
return downloaders.value.map(item => ({
|
|
||||||
title: item.name,
|
|
||||||
value: item.name,
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
// 添加下载
|
|
||||||
async function addDownload() {
|
|
||||||
startNProgress()
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
let result: { [key: string]: any }
|
|
||||||
|
|
||||||
const payload: any = {
|
|
||||||
torrent_in: props.torrent,
|
|
||||||
downloader: selectedDownloader.value,
|
|
||||||
save_path: selectedDirectory.value,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.media) {
|
|
||||||
payload.media_in = props.media
|
|
||||||
}
|
|
||||||
|
|
||||||
const endpoint = props.media ? 'download/' : 'download/add'
|
|
||||||
|
|
||||||
result = await api.post(endpoint, payload)
|
|
||||||
|
|
||||||
if (result && result.success) {
|
|
||||||
// 添加下载成功
|
|
||||||
$toast.success(`${props.torrent?.site_name} ${props.torrent?.title} 下载成功!`)
|
|
||||||
// 下载成功,返回链接
|
|
||||||
emit('done', props.torrent?.enclosure)
|
|
||||||
} else {
|
|
||||||
// 添加下载失败
|
|
||||||
$toast.error(`${props.torrent?.site_name} ${props.torrent?.title} 下载失败:${result?.message}!`)
|
|
||||||
// 下载失败,返回错误原因
|
|
||||||
emit('error', result?.message)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
loading.value = false
|
|
||||||
doneNProgress()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadDirectories()
|
|
||||||
loadDownloaderSetting()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<VDialog max-width="45rem" scrollable>
|
|
||||||
<VCard>
|
|
||||||
<VCardItem>
|
|
||||||
<VCardTitle v-if="title">{{ torrent?.site_name }} - {{ title }}</VCardTitle>
|
|
||||||
<VCardTitle v-else>确认下载</VCardTitle>
|
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
|
||||||
</VCardItem>
|
|
||||||
<VDivider />
|
|
||||||
<VCardText>
|
|
||||||
<VList lines="one">
|
|
||||||
<VListItem>
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-web"></VIcon>
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>
|
|
||||||
<span class="whitespace-break-spaces me-2">{{ torrent?.title }}</span>
|
|
||||||
<span class="text-green-700 ms-2 text-sm">↑{{ torrent?.seeders }}</span>
|
|
||||||
<span class="text-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
|
||||||
</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem>
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-subtitles-outline"></VIcon>
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>
|
|
||||||
<span class="text-body-1 whitespace-break-spaces">{{ torrent?.description }}</span>
|
|
||||||
</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem>
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-database"></VIcon>
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>
|
|
||||||
<span class="text-body-1">
|
|
||||||
<VChip variant="tonal" label>
|
|
||||||
{{ formatFileSize(torrent?.size || 0) }}
|
|
||||||
</VChip>
|
|
||||||
</span>
|
|
||||||
</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
</VList>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12" md="4">
|
|
||||||
<VSelect
|
|
||||||
v-model="selectedDownloader"
|
|
||||||
:items="downloaderOptions"
|
|
||||||
label="指定下载器"
|
|
||||||
variant="underlined"
|
|
||||||
placeholder="留空默认"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="8">
|
|
||||||
<VCombobox
|
|
||||||
v-model="selectedDirectory"
|
|
||||||
:items="targetDirectories"
|
|
||||||
label="指定保存目录"
|
|
||||||
placeholder="留空自动匹配"
|
|
||||||
variant="underlined"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VCardText>
|
|
||||||
<VCardText class="text-center">
|
|
||||||
<VBtn
|
|
||||||
variant="elevated"
|
|
||||||
:disabled="loading"
|
|
||||||
@click="addDownload"
|
|
||||||
:prepend-icon="icon"
|
|
||||||
class="px-5"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
{{ buttonText }}
|
|
||||||
</VBtn>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import api from '@/api'
|
|
||||||
|
|
||||||
// 定义输入
|
|
||||||
const props = defineProps({
|
|
||||||
conf: {
|
|
||||||
type: Object as PropType<{ [key: string]: any }>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义事件
|
|
||||||
const emit = defineEmits(['done', 'close'])
|
|
||||||
|
|
||||||
// 完成
|
|
||||||
async function handleDone() {
|
|
||||||
await savaAlistConfig()
|
|
||||||
emit('done')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存rclone设置
|
|
||||||
async function savaAlistConfig() {
|
|
||||||
try {
|
|
||||||
await api.post(`storage/save/alist`, props.conf)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VDialog width="50rem" scrollable max-height="85vh">
|
|
||||||
<VCard title="AList配置" class="rounded-t">
|
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
|
||||||
<VCardText>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextField v-model="props.conf.url" hint="AList服务地址" label="地址" persistent-hint />
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField v-model="props.conf.username" hint="AList登录用户名" label="用户名" persistent-hint />
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
type="password"
|
|
||||||
v-model="props.conf.password"
|
|
||||||
hint="AList登录密码"
|
|
||||||
label="密码"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VCardText>
|
|
||||||
<VCardActions>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
|
||||||
@@ -2,14 +2,6 @@
|
|||||||
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'])
|
||||||
|
|
||||||
@@ -33,17 +25,13 @@ let timeoutTimer: NodeJS.Timeout | undefined = undefined
|
|||||||
|
|
||||||
// 完成
|
// 完成
|
||||||
async function handleDone() {
|
async function handleDone() {
|
||||||
clearTimeout(timeoutTimer)
|
|
||||||
if (props.conf?.refreshToken) {
|
|
||||||
await savaAliPanConfig()
|
|
||||||
}
|
|
||||||
emit('done')
|
emit('done')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用/aliyun/qrcode api生成二维码
|
// 调用/aliyun/qrcode api生成二维码
|
||||||
async function getQrcode() {
|
async function getQrcode() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('/storage/qrcode/alipan')
|
const result: { [key: string]: any } = await api.get('/aliyun/qrcode')
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
qrCodeContent.value = result.data.codeContent
|
qrCodeContent.value = result.data.codeContent
|
||||||
ck.value = result.data.ck
|
ck.value = result.data.ck
|
||||||
@@ -59,8 +47,11 @@ async function getQrcode() {
|
|||||||
// 调用/aliyun/check api验证二维码
|
// 调用/aliyun/check api验证二维码
|
||||||
async function checkQrcode() {
|
async function checkQrcode() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('/storage/check/alipan', {
|
const result: { [key: string]: any } = await api.get('/aliyun/check', {
|
||||||
params: { ck: ck.value, t: t.value },
|
params: {
|
||||||
|
ck: ck.value,
|
||||||
|
t: t.value,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
const qrCodeStatus = result.data.qrCodeStatus
|
const qrCodeStatus = result.data.qrCodeStatus
|
||||||
@@ -87,15 +78,6 @@ 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)
|
||||||
@@ -118,13 +100,6 @@ 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>
|
||||||
|
|||||||
@@ -1,277 +0,0 @@
|
|||||||
<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,24 +2,23 @@
|
|||||||
// 输入参数
|
// 输入参数
|
||||||
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('save', props.dataType, codeString)
|
emit('update:modelValue', codeString.value)
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VDialog width="40rem" scrollable max-height="85vh" persistent>
|
<VDialog width="40rem" scrollable max-height="85vh">
|
||||||
<VCard :title="props.title" class="rounded-t">
|
<VCard :title="props.title" class="rounded-t">
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
<VCardText class="pt-2">
|
<VCardText class="pt-2">
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import api from '@/api'
|
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
|
|
||||||
// 输入参数
|
|
||||||
const props = defineProps({
|
|
||||||
title: String,
|
|
||||||
})
|
|
||||||
|
|
||||||
const $toast = useToast()
|
|
||||||
|
|
||||||
// 插件仓库设置字符串
|
|
||||||
const repoString = ref('')
|
|
||||||
|
|
||||||
// 定义事件
|
|
||||||
const emit = defineEmits(['save', 'close'])
|
|
||||||
|
|
||||||
// 查询已设置的插件仓库
|
|
||||||
async function queryMarketRepoSetting() {
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.get('system/setting/PLUGIN_MARKET')
|
|
||||||
if (result && result.data && result.data.value) repoString.value = result.data.value
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存设置
|
|
||||||
async function saveHandle() {
|
|
||||||
try {
|
|
||||||
// 用户名密码
|
|
||||||
const result: { [key: string]: any } = await api.post('system/setting/PLUGIN_MARKET', repoString.value)
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
$toast.success('插件仓库保存成功')
|
|
||||||
emit('save')
|
|
||||||
} else $toast.error(`插件仓库保存失败:${result?.message}!`)
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
queryMarketRepoSetting()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VDialog width="50rem" scrollable max-height="85vh">
|
|
||||||
<VCard title="插件仓库设置" class="rounded-t">
|
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
|
||||||
<VCardText class="pt-2">
|
|
||||||
<VTextarea
|
|
||||||
v-model="repoString"
|
|
||||||
placeholder="格式:https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/"
|
|
||||||
hint="多个地址使用逗号分隔,仅支持Github仓库"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCardText>
|
|
||||||
<VCardActions>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn variant="elevated" @click="saveHandle" prepend-icon="mdi-content-save-check" class="px-5 me-3">
|
|
||||||
保存
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import api from '@/api'
|
|
||||||
|
|
||||||
// 定义输入
|
|
||||||
const props = defineProps({
|
|
||||||
conf: {
|
|
||||||
type: Object as PropType<{ [key: string]: any }>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!props.conf.filepath) {
|
|
||||||
props.conf.filepath = '/moviepilot/.config/rclone/rclone.conf'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!props.conf.content) {
|
|
||||||
props.conf.content = '# 请在此处填写rclone配置文件内容 \n# 请参考 https://rclone.org/docs/ \n# 存储节点名必须为:MP'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义事件
|
|
||||||
const emit = defineEmits(['done', 'close'])
|
|
||||||
|
|
||||||
// 完成
|
|
||||||
async function handleDone() {
|
|
||||||
await savaRcloneConfig()
|
|
||||||
emit('done')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存rclone设置
|
|
||||||
async function savaRcloneConfig() {
|
|
||||||
try {
|
|
||||||
await api.post(`storage/save/rclone`, props.conf)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VDialog width="50rem" scrollable max-height="85vh">
|
|
||||||
<VCard title="RClone配置" class="rounded-t">
|
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
|
||||||
<VCardText>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextField v-model="props.conf.filepath" label="rclone配置文件路径" />
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VAceEditor
|
|
||||||
v-model:value="props.conf.content"
|
|
||||||
lang="ini"
|
|
||||||
theme="monokai"
|
|
||||||
style="block-size: 30rem"
|
|
||||||
class="rounded"
|
|
||||||
>
|
|
||||||
</VAceEditor>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VCardText>
|
|
||||||
<VCardActions>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
|
||||||
@@ -1,30 +1,27 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
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 store from '@/store'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
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, TransferForm } from '@/api/types'
|
import { FileItem, MediaDirectory } from '@/api/types'
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
storage: {
|
||||||
|
type: String,
|
||||||
|
default: () => 'local',
|
||||||
|
},
|
||||||
logids: Array<number>,
|
logids: Array<number>,
|
||||||
items: Array<FileItem>,
|
items: Array<FileItem>,
|
||||||
target_storage: String,
|
target: String,
|
||||||
target_path: String,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 从 provide 中获取全局设置
|
|
||||||
const globalSettings: any = inject('globalSettings')
|
|
||||||
|
|
||||||
// 当前识别类型
|
|
||||||
const mediaSource = ref(globalSettings.data?.RECOGNIZE_SOURCE || 'themoviedb')
|
|
||||||
|
|
||||||
// 定义事件
|
// 定义事件
|
||||||
const emit = defineEmits(['done', 'close'])
|
const emit = defineEmits(['done', 'close'])
|
||||||
|
|
||||||
@@ -36,6 +33,9 @@ const seasonItems = ref(
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 当前识别类型
|
||||||
|
const mediaSource = ref('themoviedb')
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
@@ -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,101 +65,56 @@ 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<TransferForm>({
|
const transferForm = reactive({
|
||||||
fileitem: {} as FileItem,
|
storage: props.storage,
|
||||||
logid: 0,
|
logid: 0,
|
||||||
target_storage: props.target_storage ?? 'local',
|
path: '',
|
||||||
|
drive_id: '',
|
||||||
|
fileid: '',
|
||||||
|
filetype: '',
|
||||||
|
target: props.target ?? null,
|
||||||
|
tmdbid: null,
|
||||||
|
doubanid: null,
|
||||||
|
season: null,
|
||||||
|
type_name: '',
|
||||||
transfer_type: '',
|
transfer_type: '',
|
||||||
target_path: '',
|
episode_format: '',
|
||||||
|
episode_detail: '',
|
||||||
|
episode_part: '',
|
||||||
|
episode_offset: null,
|
||||||
min_filesize: 0,
|
min_filesize: 0,
|
||||||
scrape: false,
|
scrape: false,
|
||||||
from_history: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 所有媒体库目录
|
// 所有媒体库目录
|
||||||
const directories = ref<TransferDirectoryConf[]>([])
|
const libraryDirectories = ref<MediaDirectory[]>([])
|
||||||
// 查询目录
|
|
||||||
async function loadDirectories() {
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.get('system/setting/Directories')
|
|
||||||
directories.value = result.data?.value ?? []
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 目的目录下拉框
|
// 目的目录下拉框
|
||||||
const targetDirectories = computed(() => {
|
const targetDirectories = computed(() => {
|
||||||
const libraryDirectories = directories.value.map(item => item.library_path)
|
const directories = libraryDirectories.value.map(item => item.path)
|
||||||
return [...new Set(libraryDirectories)]
|
return [...new Set(directories)]
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听目的路径变化,配置默认值
|
// 监听目的路径变化,自动查询目录的刮削配置
|
||||||
watch(
|
watch(transferForm, async () => {
|
||||||
() => transferForm.target_path,
|
if (transferForm.target) {
|
||||||
async newPath => {
|
const directory = libraryDirectories.value.find(item => item.path === transferForm.target)
|
||||||
if (newPath) {
|
if (directory) {
|
||||||
const directory = directories.value.find(item => item.library_path === newPath)
|
transferForm.scrape = directory.scrape ?? false
|
||||||
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() {
|
||||||
progressText.value = '请稍候 ...'
|
progressText.value = '请稍候 ...'
|
||||||
progressEventSource.value = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`)
|
|
||||||
|
const token = store.state.auth.token
|
||||||
|
|
||||||
|
progressEventSource.value = new EventSource(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer?token=${token}`,
|
||||||
|
)
|
||||||
progressEventSource.value.onmessage = event => {
|
progressEventSource.value.onmessage = event => {
|
||||||
const progress = JSON.parse(event.data)
|
const progress = JSON.parse(event.data)
|
||||||
if (progress) {
|
if (progress) {
|
||||||
@@ -175,48 +130,87 @@ function stopLoadingProgress() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 整理文件
|
// 整理文件
|
||||||
async function transfer(background: boolean = false) {
|
async function transfer() {
|
||||||
if (!props.logids && !props.items) return
|
if (!props.logids && !props.items) return
|
||||||
|
|
||||||
// 显示进度条
|
// 显示进度条
|
||||||
progressDialog.value = true
|
progressDialog.value = true
|
||||||
|
// 开始监听进度
|
||||||
if (!background) {
|
startLoadingProgress()
|
||||||
// 开始监听进度
|
|
||||||
startLoadingProgress()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文件整理
|
// 文件整理
|
||||||
if (props.items) {
|
if (props.items) {
|
||||||
for (const item of props.items) {
|
for (const item of props.items) {
|
||||||
await handleTransfer(item, background)
|
await handleTransfer(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 日志整理
|
// 日志整理
|
||||||
if (props.logids) {
|
if (props.logids) {
|
||||||
for (const logid of props.logids) {
|
for (const logid of props.logids) {
|
||||||
await handleTransferLog(logid, background)
|
await handleTransferLog(logid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!background) {
|
// 停止监听进度
|
||||||
// 停止监听进度
|
stopLoadingProgress()
|
||||||
stopLoadingProgress()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭进度条
|
// 关闭进度条
|
||||||
progressDialog.value = false
|
progressDialog.value = false
|
||||||
// 重新加载
|
// 重新加载
|
||||||
emit('done')
|
emit('done')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
// 整理文件
|
||||||
loadDirectories()
|
async function handleTransfer(item: FileItem) {
|
||||||
})
|
transferForm.path = item.path
|
||||||
|
transferForm.fileid = item.fileid || ''
|
||||||
|
transferForm.drive_id = item.drive_id || ''
|
||||||
|
transferForm.filetype = item.type || 'dir'
|
||||||
|
|
||||||
onUnmounted(() => {
|
try {
|
||||||
stopLoadingProgress()
|
const result: { [key: string]: any } = await api.post('transfer/manual', {}, { params: transferForm })
|
||||||
|
if (!result.success) $toast.error(`文件 ${item.path} 整理失败:${result.message}!`)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 整理日志
|
||||||
|
async function handleTransferLog(logid: number) {
|
||||||
|
transferForm.logid = logid
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.post('transfer/manual', {}, { params: transferForm })
|
||||||
|
if (!result.success) $toast.error(`历史记录 ${logid} 重新整理失败:${result.message}!`)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API,加载当前系统环境设置
|
||||||
|
async function loadSystemSettings() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get('system/env')
|
||||||
|
if (result) mediaSource.value = result.data?.RECOGNIZE_SOURCE || 'themoviedb'
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询媒体库目录
|
||||||
|
async function loadLibraryDirectories() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get('system/setting/LibraryDirectories')
|
||||||
|
if (result.success && result.data?.value) {
|
||||||
|
libraryDirectories.value = result.data.value
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadSystemSettings()
|
||||||
|
loadLibraryDirectories()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -228,32 +222,9 @@ onUnmounted(() => {
|
|||||||
<VCardText>
|
<VCardText>
|
||||||
<VForm @submit.prevent="() => {}">
|
<VForm @submit.prevent="() => {}">
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="12" md="6">
|
<VCol v-if="props.storage == 'local'" cols="12" md="8">
|
||||||
<VSelect
|
|
||||||
v-model="transferForm.target_storage"
|
|
||||||
:items="storageOptions"
|
|
||||||
label="目的存储"
|
|
||||||
placeholder="留空自动"
|
|
||||||
hint="整理目的存储"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VSelect
|
|
||||||
v-model="transferForm.transfer_type"
|
|
||||||
label="整理方式"
|
|
||||||
:items="transferTypeOptions"
|
|
||||||
hint="文件操作整理方式"
|
|
||||||
persistent-hint
|
|
||||||
>
|
|
||||||
<template v-slot:selection="{ item }">
|
|
||||||
{{ transferForm.transfer_type === '' ? '自动' : item.title }}
|
|
||||||
</template>
|
|
||||||
</VSelect>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VCombobox
|
<VCombobox
|
||||||
v-model="transferForm.target_path"
|
v-model="transferForm.target"
|
||||||
:items="targetDirectories"
|
:items="targetDirectories"
|
||||||
label="目的路径"
|
label="目的路径"
|
||||||
placeholder="留空自动"
|
placeholder="留空自动"
|
||||||
@@ -261,6 +232,23 @@ onUnmounted(() => {
|
|||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol v-if="props.storage == 'local'" cols="12" md="4">
|
||||||
|
<VSelect
|
||||||
|
v-model="transferForm.transfer_type"
|
||||||
|
label="整理方式"
|
||||||
|
:items="[
|
||||||
|
{ title: '默认', value: '' },
|
||||||
|
{ title: '移动', value: 'move' },
|
||||||
|
{ title: '复制', value: 'copy' },
|
||||||
|
{ title: '硬链接', value: 'link' },
|
||||||
|
{ title: '软链接', value: 'softlink' },
|
||||||
|
{ title: 'Rclone复制', value: 'rclone_copy' },
|
||||||
|
{ title: 'Rclone移动', value: 'rclone_move' },
|
||||||
|
]"
|
||||||
|
hint="文件操作整理方式"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="12" md="4">
|
<VCol cols="12" md="4">
|
||||||
@@ -326,7 +314,6 @@ onUnmounted(() => {
|
|||||||
<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"
|
||||||
@@ -344,7 +331,7 @@ onUnmounted(() => {
|
|||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12" md="4">
|
<VCol cols="12" md="4">
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="transferForm.episode_offset"
|
v-model.number="transferForm.episode_offset"
|
||||||
label="集数偏移"
|
label="集数偏移"
|
||||||
placeholder="如-10"
|
placeholder="如-10"
|
||||||
hint="集数偏移运算,如-10或EP*2"
|
hint="集数偏移运算,如-10或EP*2"
|
||||||
@@ -363,22 +350,6 @@ onUnmounted(() => {
|
|||||||
</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"
|
||||||
@@ -387,25 +358,12 @@ onUnmounted(() => {
|
|||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12" md="6" v-if="props.logids">
|
|
||||||
<VSwitch
|
|
||||||
v-model="transferForm.from_history"
|
|
||||||
label="复用历史识别信息"
|
|
||||||
hint="使用历史整理记录中已识别的媒体信息"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
</VRow>
|
||||||
</VForm>
|
</VForm>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions class="pt-3">
|
<VCardActions class="pt-3">
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
<VBtn variant="elevated" color="success" @click="transfer(true)" prepend-icon="mdi-plus" class="px-5">
|
<VBtn variant="elevated" @click="transfer" prepend-icon="mdi-arrow-right-bold" class="px-5"> 开始整理 </VBtn>
|
||||||
加入整理队列
|
|
||||||
</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 { DownloaderConf, Site } from '@/api/types'
|
import type { 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,18 +35,11 @@ const siteForm = ref<Site>({
|
|||||||
limit_seconds: 0,
|
limit_seconds: 0,
|
||||||
name: '',
|
name: '',
|
||||||
domain: '',
|
domain: '',
|
||||||
downloader: '',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
// 维护类型
|
|
||||||
const siteType = ref('cookie')
|
|
||||||
|
|
||||||
// 是否限流
|
|
||||||
const isLimit = ref(false)
|
|
||||||
|
|
||||||
// 状态下拉项
|
// 状态下拉项
|
||||||
const statusItems = [
|
const statusItems = [
|
||||||
{ title: '启用', value: true },
|
{ title: '启用', value: true },
|
||||||
@@ -61,23 +54,10 @@ const priorityItems = ref(
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 下载器选项
|
// 监控输入参数
|
||||||
const downloaderOptions = ref<{ title: string; value: string }[]>([])
|
watchEffect(async () => {
|
||||||
|
if (props.siteid) fetchSiteInfo()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询站点信息
|
// 查询站点信息
|
||||||
async function fetchSiteInfo() {
|
async function fetchSiteInfo() {
|
||||||
@@ -131,15 +111,6 @@ async function deleteSiteInfo() {
|
|||||||
async function updateSiteInfo() {
|
async function updateSiteInfo() {
|
||||||
startNProgress()
|
startNProgress()
|
||||||
try {
|
try {
|
||||||
if (isLimit.value) {
|
|
||||||
siteForm.value.limit_interval = siteForm.value.limit_interval || 0
|
|
||||||
siteForm.value.limit_count = siteForm.value.limit_count || 0
|
|
||||||
siteForm.value.limit_seconds = siteForm.value.limit_seconds || 0
|
|
||||||
} else {
|
|
||||||
siteForm.value.limit_interval = 0
|
|
||||||
siteForm.value.limit_count = 0
|
|
||||||
siteForm.value.limit_seconds = 0
|
|
||||||
}
|
|
||||||
const result: { [key: string]: any } = await api.put('site/', siteForm.value)
|
const result: { [key: string]: any } = await api.put('site/', siteForm.value)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
$toast.success(`${siteForm.value?.name} 更新成功!`)
|
$toast.success(`${siteForm.value?.name} 更新成功!`)
|
||||||
@@ -153,16 +124,6 @@ async function updateSiteInfo() {
|
|||||||
}
|
}
|
||||||
doneNProgress()
|
doneNProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (props.oper !== 'add') {
|
|
||||||
await fetchSiteInfo()
|
|
||||||
if (siteForm.value.limit_interval || siteForm.value.limit_count || siteForm.value.limit_seconds)
|
|
||||||
isLimit.value = true
|
|
||||||
if (siteForm.value.apikey) siteType.value = 'api'
|
|
||||||
}
|
|
||||||
await loadDownloaderSetting()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -206,7 +167,7 @@ onMounted(async () => {
|
|||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="9">
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="siteForm.rss"
|
v-model="siteForm.rss"
|
||||||
label="RSS地址"
|
label="RSS地址"
|
||||||
@@ -215,85 +176,37 @@ onMounted(async () => {
|
|||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12" md="3">
|
<VCol cols="12" md="3">
|
||||||
|
<VTextField v-model="siteForm.timeout" label="超时时间(秒)" hint="站点请求超时时间" persistent-hint />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextarea v-model="siteForm.cookie" label="站点Cookie" hint="站点请求头中的Cookie信息" persistent-hint />
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="siteForm.timeout"
|
v-model="siteForm.token"
|
||||||
label="超时时间(秒)"
|
label="请求头(Authorization)"
|
||||||
hint="站点请求超时时间,为0时不限制"
|
hint="站点请求头中的Authorization信息,特殊站点需要"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="6" md="3">
|
<VCol cols="12" md="6">
|
||||||
<VSelect
|
<VTextField
|
||||||
v-model="siteForm.downloader"
|
v-model="siteForm.apikey"
|
||||||
label="下载器"
|
label="令牌(API Key)"
|
||||||
:items="downloaderOptions"
|
hint="站点的访问API Key,特殊站点需要"
|
||||||
hint="此站点使用的下载器"
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField
|
||||||
|
v-model="siteForm.ua"
|
||||||
|
label="站点User-Agent"
|
||||||
|
hint="获取Cookie的浏览器对应的User-Agent"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
<VTabs v-model="siteType" show-arrows class="v-tabs-pill mt-3">
|
|
||||||
<VTab selected-class="v-tab--selected">
|
|
||||||
<div>
|
|
||||||
<VIcon size="20" start icon="mdi-cookie" value="cookie" />
|
|
||||||
Cookie
|
|
||||||
</div>
|
|
||||||
</VTab>
|
|
||||||
<VTab selected-class="v-tab--selected">
|
|
||||||
<div>
|
|
||||||
<VIcon size="20" start icon="mdi-api" value="api" />
|
|
||||||
API
|
|
||||||
</div>
|
|
||||||
</VTab>
|
|
||||||
</VTabs>
|
|
||||||
<VWindow v-model="siteType" class="my-3 disable-tab-transition" :touch="false">
|
|
||||||
<VWindowItem value="cookie">
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextarea
|
|
||||||
v-model="siteForm.cookie"
|
|
||||||
label="站点Cookie"
|
|
||||||
hint="站点请求头中的Cookie信息"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextField
|
|
||||||
v-model="siteForm.ua"
|
|
||||||
label="站点User-Agent"
|
|
||||||
hint="获取Cookie的浏览器对应的User-Agent"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VWindowItem>
|
|
||||||
<VWindowItem value="api">
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="siteForm.token"
|
|
||||||
label="请求头(Authorization)"
|
|
||||||
hint="站点请求头中的Authorization信息,特殊站点需要"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="siteForm.apikey"
|
|
||||||
label="令牌(API Key)"
|
|
||||||
hint="站点的访问API Key,特殊站点需要"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VWindowItem>
|
|
||||||
</VWindow>
|
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="12" md="4">
|
|
||||||
<VSwitch v-model="isLimit" label="限制站点访问频率" />
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow v-if="isLimit">
|
|
||||||
<VCol cols="12" md="4">
|
<VCol cols="12" md="4">
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="siteForm.limit_interval"
|
v-model="siteForm.limit_interval"
|
||||||
@@ -324,15 +237,10 @@ onMounted(async () => {
|
|||||||
</VRow>
|
</VRow>
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
<VSwitch v-model="siteForm.proxy" label="使用代理访问" hint="使用代理服务器访问该站点" persistent-hint />
|
<VSwitch v-model="siteForm.proxy" label="代理" hint="使用代理服务器访问该站点" persistent-hint />
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
<VSwitch
|
<VSwitch v-model="siteForm.render" label="仿真" hint="使用浏览器模拟真实访问该站点" persistent-hint />
|
||||||
v-model="siteForm.render"
|
|
||||||
label="浏览器仿真"
|
|
||||||
hint="使用浏览器模拟真实访问该站点"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</VForm>
|
</VForm>
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import api from '@/api'
|
|
||||||
import { Site } from '@/api/types'
|
|
||||||
import { requiredValidator } from '@/@validators'
|
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
|
||||||
|
|
||||||
// 输入参数
|
|
||||||
const cardProps = defineProps({
|
|
||||||
site: Object as PropType<Site>,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
|
||||||
const emit = defineEmits(['close', 'done'])
|
|
||||||
|
|
||||||
// 提示框
|
|
||||||
const $toast = useToast()
|
|
||||||
|
|
||||||
// 用户名密码表单
|
|
||||||
const userPwForm = ref({
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
code: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
// 密码输入
|
|
||||||
const isPasswordVisible = ref(false)
|
|
||||||
|
|
||||||
// 更新按钮可用性
|
|
||||||
const updateButtonDisable = ref(false)
|
|
||||||
|
|
||||||
// 进度条
|
|
||||||
const progressDialog = ref(false)
|
|
||||||
|
|
||||||
// 进度文本
|
|
||||||
const progressText = ref('请稍候 ...')
|
|
||||||
|
|
||||||
// 调用API,更新站点Cookie UA
|
|
||||||
async function updateSiteCookie() {
|
|
||||||
try {
|
|
||||||
if (!userPwForm.value.username || !userPwForm.value.password) return
|
|
||||||
|
|
||||||
// 更新按钮状态
|
|
||||||
updateButtonDisable.value = true
|
|
||||||
|
|
||||||
progressDialog.value = true
|
|
||||||
progressText.value = `正在更新 ${cardProps.site?.name} Cookie & UA ...`
|
|
||||||
|
|
||||||
const result: { [key: string]: any } = await api.get(`site/cookie/${cardProps.site?.id}`, {
|
|
||||||
params: {
|
|
||||||
username: userPwForm.value.username,
|
|
||||||
password: userPwForm.value.password,
|
|
||||||
code: userPwForm.value.code,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
$toast.success(`${cardProps.site?.name} 更新Cookie & UA 成功!`)
|
|
||||||
emit('done')
|
|
||||||
} else $toast.error(`${cardProps.site?.name} 更新失败:${result.message}`)
|
|
||||||
|
|
||||||
progressDialog.value = false
|
|
||||||
updateButtonDisable.value = false
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<VDialog max-width="30rem">
|
|
||||||
<!-- Dialog Content -->
|
|
||||||
<VCard title="更新站点Cookie & UA">
|
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
|
||||||
<VDivider />
|
|
||||||
<VCardText>
|
|
||||||
<VForm @submit.prevent="() => {}">
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextField v-model="userPwForm.username" label="用户名" :rules="[requiredValidator]" />
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextField
|
|
||||||
v-model="userPwForm.password"
|
|
||||||
label="密码"
|
|
||||||
:type="isPasswordVisible ? 'text' : 'password'"
|
|
||||||
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
|
||||||
:rules="[requiredValidator]"
|
|
||||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
|
||||||
@keydown.enter="updateSiteCookie"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextField v-model="userPwForm.code" label="两步验证" />
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
<VCardActions class="mx-auto">
|
|
||||||
<VBtn
|
|
||||||
size="large"
|
|
||||||
variant="elevated"
|
|
||||||
@click="updateSiteCookie"
|
|
||||||
:disabled="updateButtonDisable"
|
|
||||||
:loading="updateButtonDisable"
|
|
||||||
prepend-icon="mdi-refresh"
|
|
||||||
class="px-5"
|
|
||||||
>
|
|
||||||
开始更新
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
<!-- 进度框 -->
|
|
||||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { Site } from '@/api/types'
|
|
||||||
import { useDisplay } from 'vuetify'
|
|
||||||
import api from '@/api'
|
|
||||||
import type { TorrentInfo } from '@/api/types'
|
|
||||||
import { formatFileSize } from '@core/utils/formatters'
|
|
||||||
import AddDownloadDialog from '../dialog/AddDownloadDialog.vue'
|
|
||||||
|
|
||||||
// 显示器宽度
|
|
||||||
const display = useDisplay()
|
|
||||||
|
|
||||||
// 输入参数
|
|
||||||
const props = defineProps({
|
|
||||||
site: Object as PropType<Site>,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 注册事件
|
|
||||||
const emit = defineEmits(['close'])
|
|
||||||
|
|
||||||
// 数据列表
|
|
||||||
const resourceDataList = ref<TorrentInfo[]>([])
|
|
||||||
|
|
||||||
// 搜索
|
|
||||||
const resourceSearch = ref('')
|
|
||||||
|
|
||||||
// 总条数
|
|
||||||
const resourceTotalItems = ref(0)
|
|
||||||
|
|
||||||
// 每页条数
|
|
||||||
const resourceItemsPerPage = ref(25)
|
|
||||||
|
|
||||||
// 加载状态
|
|
||||||
const resourceLoading = ref(false)
|
|
||||||
|
|
||||||
// 种子元数据
|
|
||||||
const torrent = ref<TorrentInfo>()
|
|
||||||
|
|
||||||
// 资源浏览表头
|
|
||||||
const resourceHeaders = [
|
|
||||||
{ title: '标题', key: 'title', sortable: false },
|
|
||||||
{ title: '时间', key: 'pubdate', sortable: true },
|
|
||||||
{ title: '大小', key: 'size', sortable: true },
|
|
||||||
{ title: '做种', key: 'seeders', sortable: true },
|
|
||||||
{ title: '下载', key: 'peers', sortable: true },
|
|
||||||
{ title: '', key: 'actions', sortable: false },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 打开种子详情页面
|
|
||||||
function openTorrentDetail(page_url: string) {
|
|
||||||
window.open(page_url, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下载种子文件
|
|
||||||
async function downloadTorrentFile(enclosure: string) {
|
|
||||||
window.open(enclosure, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用API,查询站点资源
|
|
||||||
async function getResourceList() {
|
|
||||||
resourceLoading.value = true
|
|
||||||
try {
|
|
||||||
resourceDataList.value = await api.get(`site/resource/${props.site?.id}`)
|
|
||||||
resourceLoading.value = false
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 促销Chip类
|
|
||||||
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
|
||||||
if (downloadVolume === 0) return 'text-white bg-lime-500'
|
|
||||||
else if (downloadVolume < 1) return 'text-white bg-green-500'
|
|
||||||
else if (uploadVolume !== 1) return 'text-white bg-sky-500'
|
|
||||||
else return 'text-white bg-gray-500'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加下载
|
|
||||||
async function addDownload(_torrent: any) {
|
|
||||||
torrent.value = _torrent
|
|
||||||
addDownloadDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加下载对话框
|
|
||||||
const addDownloadDialog = ref(false)
|
|
||||||
|
|
||||||
// 添加下载成功
|
|
||||||
function addDownloadSuccess(url: string) {
|
|
||||||
addDownloadDialog.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加下载失败
|
|
||||||
function addDownloadError(error: string) {
|
|
||||||
addDownloadDialog.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 装载时查询站点图标
|
|
||||||
onMounted(() => {
|
|
||||||
getResourceList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<VDialog max-width="80rem" scrollable :fullscreen="!display.mdAndUp.value">
|
|
||||||
<VCard :title="`浏览 - ${props.site?.name}`">
|
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
|
||||||
<VDivider />
|
|
||||||
<VCardText class="pt-2">
|
|
||||||
<VDataTable
|
|
||||||
v-model:items-per-page="resourceItemsPerPage"
|
|
||||||
:headers="resourceHeaders"
|
|
||||||
:items="resourceDataList"
|
|
||||||
:items-length="resourceTotalItems"
|
|
||||||
:search="resourceSearch"
|
|
||||||
:loading="resourceLoading"
|
|
||||||
density="compact"
|
|
||||||
item-value="title"
|
|
||||||
return-object
|
|
||||||
fixed-header
|
|
||||||
hover
|
|
||||||
items-per-page-text="每页条数"
|
|
||||||
page-text="{0}-{1} 共 {2} 条"
|
|
||||||
loading-text="加载中..."
|
|
||||||
>
|
|
||||||
<template #item.title="{ item }">
|
|
||||||
<a href="javascript:void(0)" @click.stop="addDownload(item)">
|
|
||||||
<div class="text-high-emphasis pt-1">
|
|
||||||
{{ item.title }}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm my-1">
|
|
||||||
{{ item.description }}
|
|
||||||
</div>
|
|
||||||
<VChip v-if="item.hit_and_run" variant="elevated" size="small" class="me-1 mb-1 text-white bg-black">
|
|
||||||
H&R
|
|
||||||
</VChip>
|
|
||||||
<VChip v-if="item.freedate_diff" variant="elevated" color="secondary" size="small" class="me-1 mb-1">
|
|
||||||
{{ item.freedate_diff }}
|
|
||||||
</VChip>
|
|
||||||
<VChip
|
|
||||||
v-for="(label, index) in item.labels"
|
|
||||||
:key="index"
|
|
||||||
variant="elevated"
|
|
||||||
size="small"
|
|
||||||
color="primary"
|
|
||||||
class="me-1 mb-1"
|
|
||||||
>
|
|
||||||
{{ label }}
|
|
||||||
</VChip>
|
|
||||||
<VChip
|
|
||||||
v-if="item.downloadvolumefactor !== 1 || item.uploadvolumefactor !== 1"
|
|
||||||
:class="getVolumeFactorClass(item.downloadvolumefactor, item.uploadvolumefactor)"
|
|
||||||
variant="elevated"
|
|
||||||
size="small"
|
|
||||||
class="me-1 mb-1"
|
|
||||||
>
|
|
||||||
{{ item.volume_factor }}
|
|
||||||
</VChip>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
<template #item.pubdate="{ item }">
|
|
||||||
<div>{{ item.date_elapsed }}</div>
|
|
||||||
<div class="text-sm">
|
|
||||||
{{ item.pubdate }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #item.size="{ item }">
|
|
||||||
<div class="text-nowrap whitespace-nowrap">
|
|
||||||
{{ formatFileSize(item.size) }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #item.seeders="{ item }">
|
|
||||||
<div>{{ item.seeders }}</div>
|
|
||||||
</template>
|
|
||||||
<template #item.peers="{ item }">
|
|
||||||
<div>{{ item.peers }}</div>
|
|
||||||
</template>
|
|
||||||
<template #item.actions="{ item }">
|
|
||||||
<div class="me-n3">
|
|
||||||
<IconBtn>
|
|
||||||
<VIcon icon="mdi-dots-vertical" />
|
|
||||||
<VMenu activator="parent" close-on-content-click>
|
|
||||||
<VList>
|
|
||||||
<VListItem variant="plain" @click="openTorrentDetail(item.page_url || '')">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-information" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>查看详情</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
<VListItem
|
|
||||||
v-if="item.enclosure?.startsWith('http')"
|
|
||||||
variant="plain"
|
|
||||||
@click="downloadTorrentFile(item.enclosure)"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon icon="mdi-download" />
|
|
||||||
</template>
|
|
||||||
<VListItemTitle>下载种子文件</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
</VList>
|
|
||||||
</VMenu>
|
|
||||||
</IconBtn>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #no-data> 没有数据 </template>
|
|
||||||
</VDataTable>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
<!-- 添加下载对话框 -->
|
|
||||||
<AddDownloadDialog
|
|
||||||
v-if="addDownloadDialog"
|
|
||||||
v-model="addDownloadDialog"
|
|
||||||
:torrent="torrent"
|
|
||||||
@done="addDownloadSuccess"
|
|
||||||
@error="addDownloadError"
|
|
||||||
@close="addDownloadDialog = false"
|
|
||||||
/>
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.v-table th {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,461 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { Site, SiteUserData } from '@/api/types'
|
|
||||||
import api from '@/api'
|
|
||||||
import { useDisplay, useTheme } from 'vuetify'
|
|
||||||
import { formatFileSize } from '@/@core/utils/formatters'
|
|
||||||
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
|
|
||||||
|
|
||||||
// 显示器宽度
|
|
||||||
const display = useDisplay()
|
|
||||||
|
|
||||||
// 输入参数
|
|
||||||
const props = defineProps({
|
|
||||||
site: Object as PropType<Site>,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 注册事件
|
|
||||||
const emit = defineEmits(['close'])
|
|
||||||
|
|
||||||
// 进度框
|
|
||||||
const progressDialog = ref(false)
|
|
||||||
|
|
||||||
const vuetifyTheme = useTheme()
|
|
||||||
|
|
||||||
const currentTheme = controlledComputed(
|
|
||||||
() => vuetifyTheme.name.value,
|
|
||||||
() => vuetifyTheme.current.value.colors,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 站点数据列表
|
|
||||||
const siteDatas = ref<SiteUserData[]>([])
|
|
||||||
|
|
||||||
// 最新一天的数据
|
|
||||||
const siteData = computed(() => siteDatas.value[siteDatas.value.length - 1])
|
|
||||||
|
|
||||||
// 站点数据列表中的上传量、下载量数据生成图形使用的数据
|
|
||||||
const historySeries = computed(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: '上传量',
|
|
||||||
data: siteDatas.value.map(item => Math.round((item.upload ?? 0) / 1024 / 1024 / 1024)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '下载量',
|
|
||||||
data: siteDatas.value.map(item => Math.round((item.download ?? 0) / 1024 / 1024 / 1024)),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
// 图形选项
|
|
||||||
const historyChartOptions = computed(() => {
|
|
||||||
return {
|
|
||||||
chart: {
|
|
||||||
type: 'area',
|
|
||||||
parentHeightOffset: 0,
|
|
||||||
toolbar: { show: false },
|
|
||||||
animations: { enabled: true },
|
|
||||||
dataLabels: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
zoom: {
|
|
||||||
autoScaleYaxis: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: true,
|
|
||||||
tooltip: {
|
|
||||||
x: {
|
|
||||||
format: 'dd MMM yyyy',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
xaxis: {
|
|
||||||
lines: { show: false },
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
title: {
|
|
||||||
text: 'GB',
|
|
||||||
},
|
|
||||||
lines: { show: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stroke: {
|
|
||||||
width: 3,
|
|
||||||
lineCap: 'butt',
|
|
||||||
curve: 'smooth',
|
|
||||||
},
|
|
||||||
colors: [currentTheme.value.success, currentTheme.value.warning],
|
|
||||||
markers: {
|
|
||||||
size: 0,
|
|
||||||
style: 'hollow',
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
type: 'category',
|
|
||||||
categories: siteDatas.value.map(item => item.updated_day),
|
|
||||||
labels: {
|
|
||||||
show: true,
|
|
||||||
formatter: function (val: string) {
|
|
||||||
return new Date(val).toLocaleDateString('zh-CN')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
title: {
|
|
||||||
text: 'GB',
|
|
||||||
},
|
|
||||||
labels: {
|
|
||||||
formatter: function (val: number) {
|
|
||||||
return val.toLocaleString()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fill: {
|
|
||||||
type: 'gradient',
|
|
||||||
gradient: {
|
|
||||||
shadeIntensity: 1,
|
|
||||||
opacityFrom: 0.5,
|
|
||||||
opacityTo: 0.7,
|
|
||||||
stops: [0, 100],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 做种分布列,seeding_info的格式为[[x, y], [x, y], ...],x为做种数,y为做种体积,做种体积需要转换为GB
|
|
||||||
const seedingSeries = computed(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: '体积',
|
|
||||||
data: siteData.value?.seeding_info?.map(item => [item[0] ?? 0, Math.round((item[1] ?? 0) / 1024 / 1024 / 1024)]),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
// 做种分布图形选项
|
|
||||||
const seedingChartOptions = computed(() => {
|
|
||||||
return {
|
|
||||||
chart: {
|
|
||||||
type: 'scatter',
|
|
||||||
parentHeightOffset: 0,
|
|
||||||
toolbar: { show: false },
|
|
||||||
animations: { enabled: true },
|
|
||||||
zoom: {
|
|
||||||
autoScaleYaxis: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: true,
|
|
||||||
x: {
|
|
||||||
formatter: function (val: number) {
|
|
||||||
return '数量:' + val.toLocaleString()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
xaxis: {
|
|
||||||
lines: { show: true },
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
lines: { show: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
colors: [currentTheme.value.primary],
|
|
||||||
xaxis: {
|
|
||||||
type: 'numeric',
|
|
||||||
labels: {
|
|
||||||
show: true,
|
|
||||||
formatter: function (val: number) {
|
|
||||||
return Math.round(val).toLocaleString()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
text: '数量',
|
|
||||||
},
|
|
||||||
tickAmount: 10,
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
title: {
|
|
||||||
text: 'GB',
|
|
||||||
},
|
|
||||||
labels: {
|
|
||||||
formatter: function (val: number) {
|
|
||||||
return val.toLocaleString() + ' GB'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 根据传入属性,计算列表数据中第一条与第二条的差值,如果没有第二条则差值为全部
|
|
||||||
const diffData: { [key: string]: any } = computed(() => {
|
|
||||||
if (siteDatas.value.length < 2) {
|
|
||||||
return siteData.value
|
|
||||||
}
|
|
||||||
const first = siteDatas.value[siteDatas.value.length - 1]
|
|
||||||
const second = siteDatas.value[siteDatas.value.length - 2]
|
|
||||||
return {
|
|
||||||
bonus: (first.bonus ?? 0) - (second.bonus ?? 0),
|
|
||||||
ratio: (first.ratio ?? 0) - (second.ratio ?? 0),
|
|
||||||
upload: (first.upload ?? 0) - (second.upload ?? 0),
|
|
||||||
download: (first.download ?? 0) - (second.download ?? 0),
|
|
||||||
seeding: (first.seeding ?? 0) - (second.seeding ?? 0),
|
|
||||||
seeding_size: (first.seeding_size ?? 0) - (second.seeding_size ?? 0),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 格式化差值
|
|
||||||
function getDiffString(diff: number | undefined, format: boolean = true) {
|
|
||||||
if (diff === undefined) {
|
|
||||||
return '0'
|
|
||||||
}
|
|
||||||
if (format) {
|
|
||||||
return diff >= 0 ? `+${diff.toLocaleString()}` : diff.toLocaleString()
|
|
||||||
}
|
|
||||||
return diff >= 0 ? `+${diff}` : diff
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据差值的正负,返回不同的样式
|
|
||||||
function getDiffClass(diff: number | undefined) {
|
|
||||||
if (diff === undefined) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
if (diff == 0) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
return diff > 0 ? 'text-success' : 'text-error'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询站点用户数据
|
|
||||||
async function fetchSiteUserData() {
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.get(`site/userdata/${props.site?.id}`)
|
|
||||||
if (result.success) {
|
|
||||||
siteDatas.value = result.data.sort((a: { updated_day: any }, b: { updated_day: any }) =>
|
|
||||||
(a.updated_day || '').localeCompare(b.updated_day || ''),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新站点数据
|
|
||||||
async function refreshSiteData() {
|
|
||||||
progressDialog.value = true
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.post(`site/userdata/${props.site?.id}`)
|
|
||||||
if (result.success) {
|
|
||||||
await fetchSiteUserData()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
progressDialog.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
|
||||||
await fetchSiteUserData()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VDialog scrollable eager max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
|
||||||
<VCard class="rounded-t">
|
|
||||||
<VCardItem>
|
|
||||||
<VCardTitle
|
|
||||||
>{{ `数据 - ${props.site?.name}` }}
|
|
||||||
<IconBtn @click.stop="refreshSiteData" color="info"><VIcon icon="mdi-refresh" /></IconBtn>
|
|
||||||
</VCardTitle>
|
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
|
||||||
</VCardItem>
|
|
||||||
<VDivider />
|
|
||||||
<VCardText class="pt-5">
|
|
||||||
<VRow class="match-height">
|
|
||||||
<!-- 用户信息 -->
|
|
||||||
<VCol cols="12" md="3">
|
|
||||||
<VCard>
|
|
||||||
<VCardText class="d-flex align-center">
|
|
||||||
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
|
||||||
<div class="d-flex flex-column gap-y-1">
|
|
||||||
<span class="text-base">用户等级</span>
|
|
||||||
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
|
||||||
{{ siteData?.user_level || '无' }}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<VAvatar variant="tonal" size="42" rounded>
|
|
||||||
<VIcon icon="mdi-account"></VIcon>
|
|
||||||
</VAvatar>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</VCol>
|
|
||||||
<!-- 积分 -->
|
|
||||||
<VCol cols="12" md="3">
|
|
||||||
<VCard>
|
|
||||||
<VCardText class="d-flex align-center">
|
|
||||||
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
|
||||||
<div class="d-flex flex-column gap-y-1">
|
|
||||||
<span class="text-base">积分</span>
|
|
||||||
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
|
||||||
{{ siteData?.bonus?.toLocaleString() }}
|
|
||||||
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.bonus)">
|
|
||||||
({{ getDiffString(diffData?.bonus) }})
|
|
||||||
</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<VAvatar variant="tonal" size="42" rounded>
|
|
||||||
<VIcon icon="mdi-scoreboard"></VIcon>
|
|
||||||
</VAvatar>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</VCol>
|
|
||||||
<!-- 分享率 -->
|
|
||||||
<VCol cols="12" md="3">
|
|
||||||
<VCard>
|
|
||||||
<VCardText class="d-flex align-center">
|
|
||||||
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
|
||||||
<div class="d-flex flex-column gap-y-1">
|
|
||||||
<span class="text-base">分享率</span>
|
|
||||||
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
|
||||||
{{ siteData?.ratio }}
|
|
||||||
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.ratio)">
|
|
||||||
({{ getDiffString(diffData?.ratio) }})
|
|
||||||
</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<VAvatar variant="tonal" size="42" rounded>
|
|
||||||
<VIcon icon="mdi-percent"></VIcon>
|
|
||||||
</VAvatar>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</VCol>
|
|
||||||
<!-- 总上传量 -->
|
|
||||||
<VCol cols="12" md="3">
|
|
||||||
<VCard>
|
|
||||||
<VCardText class="d-flex align-center">
|
|
||||||
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
|
||||||
<div class="d-flex flex-column gap-y-1">
|
|
||||||
<span class="text-base">总上传量</span>
|
|
||||||
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
|
||||||
{{ formatFileSize(siteData?.upload || 0) }}
|
|
||||||
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.upload)">
|
|
||||||
({{ formatFileSize(diffData?.upload || 0, 2, true) }})
|
|
||||||
</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<VAvatar variant="tonal" size="42" rounded>
|
|
||||||
<VIcon icon="mdi-upload"></VIcon>
|
|
||||||
</VAvatar>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</VCol>
|
|
||||||
<!-- 总下载量 -->
|
|
||||||
<VCol cols="12" md="3">
|
|
||||||
<VCard>
|
|
||||||
<VCardText class="d-flex align-center">
|
|
||||||
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
|
||||||
<div class="d-flex flex-column gap-y-1">
|
|
||||||
<span class="text-base">总下载量</span>
|
|
||||||
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
|
||||||
{{ formatFileSize(siteData?.download || 0) }}
|
|
||||||
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.download)">
|
|
||||||
({{ formatFileSize(diffData?.download || 0, 2, true) }})
|
|
||||||
</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<VAvatar variant="tonal" size="42" rounded>
|
|
||||||
<VIcon icon="mdi-download"></VIcon>
|
|
||||||
</VAvatar>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</VCol>
|
|
||||||
<!-- 总做种数 -->
|
|
||||||
<VCol cols="12" md="3">
|
|
||||||
<VCard>
|
|
||||||
<VCardText class="d-flex align-center">
|
|
||||||
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
|
||||||
<div class="d-flex flex-column gap-y-1">
|
|
||||||
<span class="text-base">总做种数</span>
|
|
||||||
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
|
||||||
{{ siteData?.seeding?.toLocaleString() }}
|
|
||||||
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.seeding)">
|
|
||||||
({{ getDiffString(diffData?.seeding) }})
|
|
||||||
</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<VAvatar variant="tonal" size="42" rounded>
|
|
||||||
<VIcon icon="mdi-seed"></VIcon>
|
|
||||||
</VAvatar>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</VCol>
|
|
||||||
<!-- 总做种体积 -->
|
|
||||||
<VCol cols="12" md="3">
|
|
||||||
<VCard>
|
|
||||||
<VCardText class="d-flex align-center">
|
|
||||||
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
|
||||||
<div class="d-flex flex-column gap-y-1">
|
|
||||||
<span class="text-base">总做种体积</span>
|
|
||||||
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
|
||||||
{{ formatFileSize(siteData?.seeding_size || 0) }}
|
|
||||||
<span class="text-base font-weight-regular" :class="getDiffClass(diffData?.seeding_size)">
|
|
||||||
({{ formatFileSize(diffData?.seeding_size || 0, 2, true) }})
|
|
||||||
</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<VAvatar variant="tonal" size="42" rounded>
|
|
||||||
<VIcon icon="mdi-database"></VIcon>
|
|
||||||
</VAvatar>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</VCol>
|
|
||||||
<!-- 加入时间 -->
|
|
||||||
<VCol cols="12" md="3">
|
|
||||||
<VCard>
|
|
||||||
<VCardText class="d-flex align-center">
|
|
||||||
<div class="d-flex justify-space-between" style="inline-size: 100%">
|
|
||||||
<div class="d-flex flex-column gap-y-1">
|
|
||||||
<span class="text-base">加入时间</span>
|
|
||||||
<h5 class="text-h5 d-flex align-center gap-2 text-wrap">
|
|
||||||
{{ siteData?.join_at?.split(' ')[0] }}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<VAvatar variant="tonal" size="42" rounded>
|
|
||||||
<VIcon icon="mdi-calendar"></VIcon>
|
|
||||||
</VAvatar>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow>
|
|
||||||
<VCol>
|
|
||||||
<VCard title="历史流量">
|
|
||||||
<VCardText>
|
|
||||||
<VApexChart type="line" :options="historyChartOptions" :series="historySeries" :height="300" />
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow>
|
|
||||||
<VCol>
|
|
||||||
<VCard title="做种分布">
|
|
||||||
<VCardText>
|
|
||||||
<VApexChart type="scatter" :options="seedingChartOptions" :series="seedingSeries" :height="300" />
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
<!-- 进度框 -->
|
|
||||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" text="正在刷新站点数据..." />
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
|
||||||
@@ -2,10 +2,9 @@
|
|||||||
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 { DownloaderConf, FilterRuleGroup, Site, Subscribe, TransferDirectoryConf } from '@/api/types'
|
import type { MediaDirectory, Site, Subscribe } 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'
|
|
||||||
|
|
||||||
// 显示器宽度
|
// 显示器宽度
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
@@ -23,34 +22,38 @@ const props = defineProps({
|
|||||||
// 定义触发的自定义事件
|
// 定义触发的自定义事件
|
||||||
const emit = defineEmits(['remove', 'save', 'close'])
|
const emit = defineEmits(['remove', 'save', 'close'])
|
||||||
|
|
||||||
const activeTab = ref('basic')
|
|
||||||
|
|
||||||
// 站点数据列表
|
// 站点数据列表
|
||||||
const siteList = ref<Site[]>([])
|
const siteList = ref<Site[]>([])
|
||||||
|
|
||||||
// 下载目录列表
|
// 下载目录列表
|
||||||
const downloadDirectories = ref<TransferDirectoryConf[]>([])
|
const downloadDirectories = ref<MediaDirectory[]>([])
|
||||||
|
|
||||||
// 站点选择下载框
|
// 站点选择下载框
|
||||||
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
|
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
|
||||||
|
|
||||||
// 所有规则组列表
|
|
||||||
const filterRuleGroups = ref<FilterRuleGroup[]>([])
|
|
||||||
|
|
||||||
// 订阅编辑表单
|
// 订阅编辑表单
|
||||||
const subscribeForm = ref<Subscribe>({
|
const subscribeForm = ref<Subscribe>({
|
||||||
id: props.subid ?? 0,
|
id: props.subid ?? 0,
|
||||||
|
keyword: '',
|
||||||
|
quality: '',
|
||||||
|
resolution: '',
|
||||||
|
effect: '',
|
||||||
|
include: '',
|
||||||
|
exclude: '',
|
||||||
|
total_episode: 0,
|
||||||
|
start_episode: 0,
|
||||||
|
best_version: 0,
|
||||||
|
search_imdbid: 0,
|
||||||
|
sites: [],
|
||||||
|
type: '',
|
||||||
name: '',
|
name: '',
|
||||||
year: '',
|
year: '',
|
||||||
type: '',
|
|
||||||
tmdbid: 0,
|
tmdbid: 0,
|
||||||
state: '',
|
state: '',
|
||||||
last_update: '',
|
last_update: '',
|
||||||
username: '',
|
username: '',
|
||||||
sites: [],
|
|
||||||
best_version: undefined,
|
|
||||||
current_priority: 0,
|
current_priority: 0,
|
||||||
downloader: '',
|
save_path: undefined,
|
||||||
date: '',
|
date: '',
|
||||||
show_edit_dialog: false,
|
show_edit_dialog: false,
|
||||||
})
|
})
|
||||||
@@ -58,42 +61,6 @@ const subscribeForm = ref<Subscribe>({
|
|||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
// 下载器选项
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载规则组
|
|
||||||
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 filterRuleGroupOptions = computed(() => {
|
|
||||||
return filterRuleGroups.value.map(item => ({
|
|
||||||
title: item.name,
|
|
||||||
value: item.name,
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
// 调用API修改订阅
|
// 调用API修改订阅
|
||||||
async function updateSubscribeInfo() {
|
async function updateSubscribeInfo() {
|
||||||
try {
|
try {
|
||||||
@@ -195,7 +162,6 @@ async function removeSubscribe() {
|
|||||||
const result: { [key: string]: any } = await api.delete(`subscribe/${props.subid}`)
|
const result: { [key: string]: any } = await api.delete(`subscribe/${props.subid}`)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
$toast.success(`订阅 ${subscribeForm.value.name} 已取消!`)
|
|
||||||
// 通知父组件刷新
|
// 通知父组件刷新
|
||||||
emit('remove')
|
emit('remove')
|
||||||
}
|
}
|
||||||
@@ -207,7 +173,7 @@ async function removeSubscribe() {
|
|||||||
// 查询下载目录
|
// 查询下载目录
|
||||||
async function loadDownloadDirectories() {
|
async function loadDownloadDirectories() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('system/setting/Directories')
|
const result: { [key: string]: any } = await api.get('system/setting/DownloadDirectories')
|
||||||
if (result.success && result.data?.value) {
|
if (result.success && result.data?.value) {
|
||||||
downloadDirectories.value = result.data.value
|
downloadDirectories.value = result.data.value
|
||||||
}
|
}
|
||||||
@@ -219,7 +185,8 @@ async function loadDownloadDirectories() {
|
|||||||
// 保存目录下拉框
|
// 保存目录下拉框
|
||||||
const targetDirectories = computed(() => {
|
const targetDirectories = computed(() => {
|
||||||
// 去重后的下载目录
|
// 去重后的下载目录
|
||||||
return downloadDirectories.value.map(item => item.download_path)
|
const directories = downloadDirectories.value.map(item => item.path)
|
||||||
|
return [...new Set(directories)]
|
||||||
})
|
})
|
||||||
|
|
||||||
// 质量选择框数据
|
// 质量选择框数据
|
||||||
@@ -307,10 +274,8 @@ const effectOptions = ref([
|
|||||||
])
|
])
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
queryFilterRuleGroups()
|
|
||||||
loadDownloadDirectories()
|
loadDownloadDirectories()
|
||||||
getSiteList()
|
getSiteList()
|
||||||
loadDownloaderSetting()
|
|
||||||
if (props.subid) getSubscribeInfo()
|
if (props.subid) getSubscribeInfo()
|
||||||
if (props.default) queryDefaultSubscribeConfig()
|
if (props.default) queryDefaultSubscribeConfig()
|
||||||
})
|
})
|
||||||
@@ -326,199 +291,134 @@ onMounted(() => {
|
|||||||
}`"
|
}`"
|
||||||
class="rounded-t"
|
class="rounded-t"
|
||||||
>
|
>
|
||||||
|
<VDivider />
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
<VForm @submit.prevent="() => {}">
|
<VForm @submit.prevent="() => {}">
|
||||||
<VTabs v-model="activeTab" show-arrows>
|
<VRow>
|
||||||
<VTab value="basic">
|
<VCol cols="12" md="8">
|
||||||
<div>基础</div>
|
<VTextField
|
||||||
</VTab>
|
v-if="!props.default"
|
||||||
<VTab value="advance">
|
v-model="subscribeForm.keyword"
|
||||||
<div>进阶</div>
|
label="搜索关键词"
|
||||||
</VTab>
|
hint="指定搜索站点时使用的关键词"
|
||||||
</VTabs>
|
persistent-hint
|
||||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
/>
|
||||||
<VWindowItem value="basic">
|
</VCol>
|
||||||
<div>
|
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="2">
|
||||||
<VRow v-if="!props.default">
|
<VTextField
|
||||||
<VCol cols="12" md="4">
|
v-model="subscribeForm.total_episode"
|
||||||
<VTextField
|
label="总集数"
|
||||||
v-model="subscribeForm.keyword"
|
:rules="[numberValidator]"
|
||||||
label="搜索关键词"
|
hint="剧集总集数"
|
||||||
hint="指定搜索站点时使用的关键词"
|
persistent-hint
|
||||||
persistent-hint
|
/>
|
||||||
/>
|
</VCol>
|
||||||
</VCol>
|
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="2">
|
||||||
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="4">
|
<VTextField
|
||||||
<VTextField
|
v-model="subscribeForm.start_episode"
|
||||||
v-model="subscribeForm.total_episode"
|
label="开始集数"
|
||||||
label="总集数"
|
:rules="[numberValidator]"
|
||||||
:rules="[numberValidator]"
|
hint="开始订阅集数"
|
||||||
hint="剧集总集数"
|
persistent-hint
|
||||||
persistent-hint
|
/>
|
||||||
/>
|
</VCol>
|
||||||
</VCol>
|
</VRow>
|
||||||
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="4">
|
<VRow>
|
||||||
<VTextField
|
<VCol cols="12" md="4">
|
||||||
v-model="subscribeForm.start_episode"
|
<VSelect
|
||||||
label="开始集数"
|
v-model="subscribeForm.quality"
|
||||||
:rules="[numberValidator]"
|
label="质量"
|
||||||
hint="开始订阅集数"
|
:items="qualityOptions"
|
||||||
persistent-hint
|
hint="订阅资源质量"
|
||||||
/>
|
persistent-hint
|
||||||
</VCol>
|
/>
|
||||||
</VRow>
|
</VCol>
|
||||||
<VRow>
|
<VCol cols="12" md="4">
|
||||||
<VCol cols="12" md="4">
|
<VSelect
|
||||||
<VSelect
|
v-model="subscribeForm.resolution"
|
||||||
v-model="subscribeForm.quality"
|
label="分辨率"
|
||||||
label="质量"
|
:items="resolutionOptions"
|
||||||
:items="qualityOptions"
|
hint="订阅资源分辨率"
|
||||||
hint="订阅资源质量"
|
persistent-hint
|
||||||
persistent-hint
|
/>
|
||||||
/>
|
</VCol>
|
||||||
</VCol>
|
<VCol cols="12" md="4">
|
||||||
<VCol cols="12" md="4">
|
<VSelect
|
||||||
<VSelect
|
v-model="subscribeForm.effect"
|
||||||
v-model="subscribeForm.resolution"
|
label="特效"
|
||||||
label="分辨率"
|
:items="effectOptions"
|
||||||
:items="resolutionOptions"
|
hint="订阅资源特效"
|
||||||
hint="订阅资源分辨率"
|
persistent-hint
|
||||||
persistent-hint
|
/>
|
||||||
/>
|
</VCol>
|
||||||
</VCol>
|
</VRow>
|
||||||
<VCol cols="12" md="4">
|
<VRow>
|
||||||
<VSelect
|
<VCol cols="12" md="4">
|
||||||
v-model="subscribeForm.effect"
|
<VTextField
|
||||||
label="特效"
|
v-model="subscribeForm.include"
|
||||||
:items="effectOptions"
|
label="包含(关键字、正则式)"
|
||||||
hint="订阅资源特效"
|
hint="包含规则,支持正则表达式"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
<VCol cols="12" md="4">
|
||||||
<VRow>
|
<VTextField
|
||||||
<VCol cols="12">
|
v-model="subscribeForm.exclude"
|
||||||
<VSelect
|
label="排除(关键字、正则式)"
|
||||||
v-model="subscribeForm.sites"
|
hint="排除规则,支持正则表达式"
|
||||||
:items="selectSitesOptions"
|
persistent-hint
|
||||||
chips
|
/>
|
||||||
label="订阅站点"
|
</VCol>
|
||||||
multiple
|
<VCol cols="12" md="4">
|
||||||
clearable
|
<VSelect
|
||||||
hint="订阅的站点范围,不选使用系统设置"
|
v-model="subscribeForm.sites"
|
||||||
persistent-hint
|
:items="selectSitesOptions"
|
||||||
/>
|
chips
|
||||||
</VCol>
|
label="订阅站点"
|
||||||
</VRow>
|
multiple
|
||||||
<VRow>
|
hint="订阅的站点范围,不选使用系统设置"
|
||||||
<VCol cols="12" md="6">
|
persistent-hint
|
||||||
<VSelect
|
/>
|
||||||
v-model="subscribeForm.downloader"
|
</VCol>
|
||||||
:items="downloaderOptions"
|
</VRow>
|
||||||
label="下载器"
|
<VRow>
|
||||||
hint="指定该订阅使用的下载器"
|
<VCol cols="12">
|
||||||
persistent-hint
|
<VCombobox
|
||||||
/>
|
v-model="subscribeForm.save_path"
|
||||||
</VCol>
|
:items="targetDirectories"
|
||||||
<VCol cols="12" md="6">
|
label="保存路径"
|
||||||
<VCombobox
|
hint="指定该订阅的下载保存路径,留空自动使用设定的下载目录"
|
||||||
v-model="subscribeForm.save_path"
|
persistent-hint
|
||||||
:items="targetDirectories"
|
/>
|
||||||
label="保存路径"
|
</VCol>
|
||||||
hint="指定该订阅的下载保存路径,留空自动使用设定的下载目录"
|
</VRow>
|
||||||
persistent-hint
|
<VRow>
|
||||||
/>
|
<VCol cols="12" md="4">
|
||||||
</VCol>
|
<VSwitch
|
||||||
</VRow>
|
v-model="subscribeForm.best_version"
|
||||||
<VRow>
|
label="洗版"
|
||||||
<VCol cols="12" md="4">
|
hint="根据洗版优先级进行洗版订阅"
|
||||||
<VSwitch
|
persistent-hint
|
||||||
v-model="subscribeForm.best_version"
|
/>
|
||||||
label="洗版"
|
</VCol>
|
||||||
hint="根据洗版优先级进行洗版订阅"
|
<VCol cols="12" md="4">
|
||||||
persistent-hint
|
<VSwitch
|
||||||
/>
|
v-model="subscribeForm.search_imdbid"
|
||||||
</VCol>
|
label="使用 ImdbID 搜索"
|
||||||
<VCol cols="12" md="4">
|
hint="开使用 ImdbID 精确搜索资源"
|
||||||
<VSwitch
|
persistent-hint
|
||||||
v-model="subscribeForm.search_imdbid"
|
/>
|
||||||
label="使用 ImdbID 搜索"
|
</VCol>
|
||||||
hint="开使用 ImdbID 精确搜索资源"
|
<VCol v-if="props.default" cols="12" md="4">
|
||||||
persistent-hint
|
<VSwitch
|
||||||
/>
|
v-model="subscribeForm.show_edit_dialog"
|
||||||
</VCol>
|
label="订阅时编辑更多规则"
|
||||||
<VCol v-if="props.default" cols="12" md="4">
|
hint="添加订阅时显示此编辑订阅对话框"
|
||||||
<VSwitch
|
persistent-hint
|
||||||
v-model="subscribeForm.show_edit_dialog"
|
/>
|
||||||
label="订阅时编辑更多规则"
|
</VCol>
|
||||||
hint="添加订阅时显示此编辑订阅对话框"
|
</VRow>
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</div>
|
|
||||||
</VWindowItem>
|
|
||||||
<VWindowItem value="advance">
|
|
||||||
<div>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="subscribeForm.include"
|
|
||||||
label="包含(关键字、正则式)"
|
|
||||||
hint="包含规则,支持正则表达式"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="subscribeForm.exclude"
|
|
||||||
label="排除(关键字、正则式)"
|
|
||||||
hint="排除规则,支持正则表达式"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VSelect
|
|
||||||
v-model="subscribeForm.filter_groups"
|
|
||||||
:items="filterRuleGroupOptions"
|
|
||||||
chips
|
|
||||||
multiple
|
|
||||||
clearable
|
|
||||||
label="优先级规则组"
|
|
||||||
hint="按选定的过滤规则组对订阅进行过滤"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6" v-if="!props.default">
|
|
||||||
<VTextField
|
|
||||||
v-model="subscribeForm.media_category"
|
|
||||||
label="自定义类别"
|
|
||||||
hint="指定类别名称,留空自动识别"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow v-if="!props.default">
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextarea
|
|
||||||
v-model="subscribeForm.custom_words"
|
|
||||||
label="自定义识别词"
|
|
||||||
hint="只对该订阅使用的识别词"
|
|
||||||
persistent-hint
|
|
||||||
placeholder="屏蔽词
|
|
||||||
被替换词 => 替换词
|
|
||||||
前定位词 <> 后定位词 >> 集偏移量(EP)
|
|
||||||
被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量(EP)
|
|
||||||
其中替换词支持格式:{[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]} 直接指定TMDBID/豆瓣ID识别,其中s、e为季数和集数(可选)"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</div>
|
|
||||||
</VWindowItem>
|
|
||||||
</VWindow>
|
|
||||||
</VForm>
|
</VForm>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions class="pt-3">
|
<VCardActions class="pt-3">
|
||||||
|
|||||||
@@ -1,305 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import api from '@/api'
|
|
||||||
import { SubscrbieInfo } from '@/api/types'
|
|
||||||
import { useDisplay } from 'vuetify'
|
|
||||||
|
|
||||||
// 显示器宽度
|
|
||||||
const display = useDisplay()
|
|
||||||
|
|
||||||
//定义输入参数
|
|
||||||
const props = defineProps({
|
|
||||||
subid: Number,
|
|
||||||
})
|
|
||||||
|
|
||||||
const activeTab = ref('download')
|
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
|
||||||
const emit = defineEmits(['close'])
|
|
||||||
|
|
||||||
// 订阅文件信息
|
|
||||||
const subScribeInfo = ref<SubscrbieInfo>()
|
|
||||||
|
|
||||||
// 下载文件表头
|
|
||||||
const downloadHeaders = [
|
|
||||||
{ title: '集', key: 'episode_number', sortable: true },
|
|
||||||
{ title: '种子', key: 'torrent_title', sortable: true },
|
|
||||||
{ title: '文件', key: 'file_path', sortable: true },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 媒体库文件表头
|
|
||||||
const libraryHeaders = [
|
|
||||||
{ title: '集', key: 'episode_number', sortable: true },
|
|
||||||
{ title: '文件', key: 'file_path', sortable: true },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 调用API查询订阅文件信息
|
|
||||||
async function loadSubscribeFilesInfo() {
|
|
||||||
try {
|
|
||||||
subScribeInfo.value = await api.get(`subscribe/files/${props.subid}`)
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算下载文件列表
|
|
||||||
const downloadInfos = computed(() => {
|
|
||||||
return Object.keys(subScribeInfo.value?.episodes ?? {}).map((key: any) => {
|
|
||||||
const item = subScribeInfo.value?.episodes[key]
|
|
||||||
return {
|
|
||||||
episode_number: key,
|
|
||||||
title: item?.title,
|
|
||||||
download: item?.download ?? [],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 总集数
|
|
||||||
const totalCount = computed(() => {
|
|
||||||
return Object.keys(subScribeInfo.value?.episodes ?? {}).length
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算媒体库文件列表
|
|
||||||
const libraryInfos = computed(() => {
|
|
||||||
return Object.keys(subScribeInfo.value?.episodes ?? {}).map((key: any) => {
|
|
||||||
const item = subScribeInfo.value?.episodes[key]
|
|
||||||
return {
|
|
||||||
episode_number: key,
|
|
||||||
title: item?.title,
|
|
||||||
library: item?.library ?? [],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
loadSubscribeFilesInfo()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<VDialog scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
|
||||||
<VCard class="rounded-t">
|
|
||||||
<VCardItem class="my-2">
|
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
|
||||||
</VCardItem>
|
|
||||||
<VCardText>
|
|
||||||
<div class="media-page">
|
|
||||||
<div class="media-header">
|
|
||||||
<div class="media-poster">
|
|
||||||
<VImg
|
|
||||||
:src="subScribeInfo?.subscribe?.poster"
|
|
||||||
cover
|
|
||||||
class="object-cover aspect-w-2 aspect-h-3 ring-1 ring-gray-500"
|
|
||||||
>
|
|
||||||
<template #placeholder>
|
|
||||||
<div class="w-full h-full">
|
|
||||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</VImg>
|
|
||||||
</div>
|
|
||||||
<div class="media-title">
|
|
||||||
<h1 class="d-flex flex-column flex-lg-row align-baseline justify-center justify-lg-start">
|
|
||||||
<div class="align-self-center align-self-lg-end">
|
|
||||||
{{ subScribeInfo?.subscribe?.name }}
|
|
||||||
</div>
|
|
||||||
<div v-if="subScribeInfo?.subscribe?.season" class="text-lg align-self-center align-self-lg-end ms-3">
|
|
||||||
第 {{ subScribeInfo?.subscribe?.season }} 季
|
|
||||||
</div>
|
|
||||||
</h1>
|
|
||||||
<div>{{ subScribeInfo?.subscribe?.year }}</div>
|
|
||||||
<div class="media-overview">
|
|
||||||
<div class="media-overview-left">
|
|
||||||
<p>{{ subScribeInfo?.subscribe?.description }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-7">
|
|
||||||
<VTabs v-model="activeTab" show-arrows class="v-tabs-pill">
|
|
||||||
<VTab value="download" selected-class="v-slide-group-item--active v-tab--selected">
|
|
||||||
<div>
|
|
||||||
<VIcon size="20" start icon="mdi-download" />
|
|
||||||
下载文件
|
|
||||||
</div>
|
|
||||||
</VTab>
|
|
||||||
<VTab value="library" selected-class="v-slide-group-item--active v-tab--selected">
|
|
||||||
<div>
|
|
||||||
<VIcon size="20" start icon="mdi-filmstrip-box-multiple" />
|
|
||||||
媒体库文件
|
|
||||||
</div>
|
|
||||||
</VTab>
|
|
||||||
</VTabs>
|
|
||||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
|
||||||
<VWindowItem value="download">
|
|
||||||
<transition name="fade-slide" appear>
|
|
||||||
<div>
|
|
||||||
<VDataTable
|
|
||||||
items-per-page="50"
|
|
||||||
:headers="downloadHeaders"
|
|
||||||
:items="downloadInfos"
|
|
||||||
:items-length="totalCount"
|
|
||||||
density="compact"
|
|
||||||
item-value="title"
|
|
||||||
return-object
|
|
||||||
fixed-header
|
|
||||||
hover
|
|
||||||
items-per-page-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>
|
|
||||||
<template #item.torrent_title="{ item }">
|
|
||||||
<div class="text-xs" v-for="file in item.download">
|
|
||||||
【{{ file.site_name }}】{{ file.torrent_title }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #item.file_path="{ item }">
|
|
||||||
<div class="text-xs" v-for="file in item.download">{{ file.file_path }}</div>
|
|
||||||
</template>
|
|
||||||
<template #no-data> 没有数据 </template>
|
|
||||||
</VDataTable>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</VWindowItem>
|
|
||||||
<VWindowItem value="library">
|
|
||||||
<transition name="fade-slide" appear>
|
|
||||||
<div>
|
|
||||||
<VDataTable
|
|
||||||
items-per-page="50"
|
|
||||||
:headers="libraryHeaders"
|
|
||||||
:items="libraryInfos"
|
|
||||||
:items-length="totalCount"
|
|
||||||
density="compact"
|
|
||||||
item-value="title"
|
|
||||||
return-object
|
|
||||||
fixed-header
|
|
||||||
hover
|
|
||||||
items-per-page-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>
|
|
||||||
<template #item.file_path="{ item }">
|
|
||||||
<div class="text-xs" v-for="file in item.library">{{ file.file_path }}</div>
|
|
||||||
</template>
|
|
||||||
<template #no-data> 没有数据 </template>
|
|
||||||
</VDataTable>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</VWindowItem>
|
|
||||||
</VWindow>
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.vue-media-back {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
180deg,
|
|
||||||
rgba(var(--v-theme-background), 0) 50%,
|
|
||||||
rgba(var(--v-theme-background), 1) 100%
|
|
||||||
),
|
|
||||||
linear-gradient(90deg, rgba(var(--v-theme-background), 0) 50%, rgba(var(--v-theme-background), 1) 100%),
|
|
||||||
linear-gradient(270deg, rgba(var(--v-theme-background), 0) 50%, rgba(var(--v-theme-background), 1) 100%);
|
|
||||||
box-shadow: 0 0 0 2px rgb(var(--v-theme-background));
|
|
||||||
margin-block-start: calc(-70px - env(safe-area-inset-top));
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-page {
|
|
||||||
position: relative;
|
|
||||||
background-position: 50%;
|
|
||||||
background-size: cover;
|
|
||||||
margin-block-start: calc(-4rem - env(safe-area-inset-top));
|
|
||||||
padding-block-start: calc(4rem + env(safe-area-inset-top));
|
|
||||||
padding-inline: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding-block-start: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width >= 1280px) {
|
|
||||||
.media-header {
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-overview {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding-block: 1rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width >= 1024px) {
|
|
||||||
.media-overview {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-poster {
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
|
||||||
inline-size: 8rem;
|
|
||||||
|
|
||||||
--tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, 10%), 0 1px 2px -1px rgba(0, 0, 0, 10%);
|
|
||||||
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width >= 1280px) {
|
|
||||||
.media-poster {
|
|
||||||
inline-size: 13rem;
|
|
||||||
margin-inline-end: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width >= 768px) {
|
|
||||||
.media-poster {
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
|
||||||
inline-size: 11rem;
|
|
||||||
|
|
||||||
--tw-shadow: 0 25px 50px -12px rgba(0, 0, 0, 25%);
|
|
||||||
--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-title {
|
|
||||||
display: flex;
|
|
||||||
flex: 1 1 0%;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-block-start: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width >= 1280px) {
|
|
||||||
.media-title {
|
|
||||||
margin-block-start: 0;
|
|
||||||
margin-inline-end: 1rem;
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-title > h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width >= 1280px) {
|
|
||||||
.media-title > h1 {
|
|
||||||
font-size: 2.25rem;
|
|
||||||
line-height: 2.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -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,69 +138,72 @@ const dropdownItems = ref([
|
|||||||
<VCardTitle>{{ props.type + '订阅历史' }}</VCardTitle>
|
<VCardTitle>{{ props.type + '订阅历史' }}</VCardTitle>
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
<VDivider />
|
<VDivider />
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
<DialogCloseBtn
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
<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>
|
||||||
<LoadingBanner />
|
<LoadingBanner />
|
||||||
</template>
|
</template>
|
||||||
<template #empty />
|
<template #empty />
|
||||||
<template v-if="historyList.length > 0">
|
<template v-for="(item, i) in historyList" :key="i">
|
||||||
<template v-for="(item, i) in historyList" :key="i">
|
<VListItem>
|
||||||
<VListItem>
|
<template #prepend>
|
||||||
<template #prepend>
|
<VImg
|
||||||
<VImg
|
height="75"
|
||||||
height="75"
|
width="50"
|
||||||
width="50"
|
:src="item.poster"
|
||||||
:src="item.poster"
|
aspect-ratio="2/3"
|
||||||
aspect-ratio="2/3"
|
class="object-cover rounded shadow ring-gray-500 me-3"
|
||||||
class="object-cover rounded shadow ring-gray-500 me-3"
|
cover
|
||||||
cover
|
>
|
||||||
>
|
<template #placeholder>
|
||||||
<template #placeholder>
|
<div class="w-full h-full">
|
||||||
<div class="w-full h-full">
|
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</VImg>
|
||||||
</VImg>
|
</template>
|
||||||
</template>
|
<VListItemTitle v-if="item.type == '电视剧'">
|
||||||
<VListItemTitle v-if="item.type == '电视剧'">
|
{{ item.name }} <span class="text-sm">第 {{ item.season }} 季</span>
|
||||||
{{ item.name }} <span class="text-sm">第 {{ item.season }} 季</span>
|
</VListItemTitle>
|
||||||
</VListItemTitle>
|
<VListItemTitle v-else>
|
||||||
<VListItemTitle v-else>
|
{{ item.name }}
|
||||||
{{ item.name }}
|
</VListItemTitle>
|
||||||
</VListItemTitle>
|
<VListItemSubtitle class="mt-2">{{ formatDateDifference(item.date) }}</VListItemSubtitle>
|
||||||
<VListItemSubtitle class="mt-2">{{ formatDateDifference(item.date) }}</VListItemSubtitle>
|
<VListItemSubtitle class="mt-2">{{ item.description }}</VListItemSubtitle>
|
||||||
<VListItemSubtitle class="mt-2">{{ item.description }}</VListItemSubtitle>
|
<template #append>
|
||||||
<template #append>
|
<div class="me-n3">
|
||||||
<div class="me-n3">
|
<IconBtn>
|
||||||
<IconBtn>
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
<VIcon icon="mdi-dots-vertical" />
|
<VMenu activator="parent" close-on-content-click>
|
||||||
<VMenu activator="parent" close-on-content-click>
|
<VList>
|
||||||
<VList>
|
<VListItem
|
||||||
<VListItem
|
v-for="(menu, i) in dropdownItems"
|
||||||
v-for="(menu, i) in dropdownItems"
|
:key="i"
|
||||||
:key="i"
|
variant="plain"
|
||||||
variant="plain"
|
:base-color="menu.color"
|
||||||
:base-color="menu.color"
|
@click="menu.props.click(item)"
|
||||||
@click="menu.props.click(item)"
|
>
|
||||||
>
|
<template #prepend>
|
||||||
<template #prepend>
|
<VIcon :icon="menu.props.prependIcon" />
|
||||||
<VIcon :icon="menu.props.prependIcon" />
|
</template>
|
||||||
</template>
|
<VListItemTitle v-text="menu.title" />
|
||||||
<VListItemTitle v-text="menu.title" />
|
</VListItem>
|
||||||
</VListItem>
|
</VList>
|
||||||
</VList>
|
</VMenu>
|
||||||
</VMenu>
|
</IconBtn>
|
||||||
</IconBtn>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</VListItem>
|
||||||
</VListItem>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</VInfiniteScroll>
|
</VInfiniteScroll>
|
||||||
</VList>
|
</VList>
|
||||||
<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" />
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import { requiredValidator } from '@/@validators'
|
|
||||||
import api from '@/api'
|
|
||||||
import type { Subscribe, SubscribeShare } from '@/api/types'
|
|
||||||
import { useDisplay } from 'vuetify'
|
|
||||||
import { formatSeason } from '@/@core/utils/formatters'
|
|
||||||
|
|
||||||
// 显示器宽度
|
|
||||||
const display = useDisplay()
|
|
||||||
|
|
||||||
// 输入参数
|
|
||||||
const props = defineProps({
|
|
||||||
sub: Object as PropType<Subscribe>,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
|
||||||
const emit = defineEmits(['close'])
|
|
||||||
|
|
||||||
// 分享处理状态
|
|
||||||
const shareDoing = ref(false)
|
|
||||||
|
|
||||||
// 订阅编辑表单
|
|
||||||
const shareForm = ref<SubscribeShare>({
|
|
||||||
subscribe_id: props.sub?.id ?? 0,
|
|
||||||
share_title: `${props.sub?.name} ${formatSeason(props.sub?.season ? props.sub?.season.toString() : '')}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 分享订阅
|
|
||||||
async function doShare() {
|
|
||||||
if (!shareForm.value.share_title || !shareForm.value.share_comment || !shareForm.value.share_user) return
|
|
||||||
try {
|
|
||||||
shareDoing.value = true
|
|
||||||
const result: { [key: string]: any } = await api.post('subscribe/share', shareForm.value)
|
|
||||||
shareDoing.value = false
|
|
||||||
// 提示
|
|
||||||
if (result.success) {
|
|
||||||
$toast.success(`${props.sub?.name} 分享成功!`)
|
|
||||||
// 通知父组件刷新
|
|
||||||
emit('close')
|
|
||||||
} else {
|
|
||||||
$toast.error(`${props.sub?.name} 分享失败:${result.message}!`)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提示框
|
|
||||||
const $toast = useToast()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
|
||||||
<VCard
|
|
||||||
:title="`分享订阅 - ${props.sub?.name} ${props.sub?.season ? `第 ${props.sub?.season} 季` : ''}`"
|
|
||||||
class="rounded-t"
|
|
||||||
>
|
|
||||||
<VCardText>
|
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
|
||||||
<VForm @submit.prevent="() => {}" class="pt-2">
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextField
|
|
||||||
v-model="shareForm.share_title"
|
|
||||||
readonly
|
|
||||||
label="标题"
|
|
||||||
:rules="[requiredValidator]"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextarea
|
|
||||||
v-model="shareForm.share_comment"
|
|
||||||
label="说明"
|
|
||||||
:rules="[requiredValidator]"
|
|
||||||
hint="填写关于该订阅的说明,订阅中的搜索词、识别词等将会默认包含在分享中"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextField
|
|
||||||
v-model="shareForm.share_user"
|
|
||||||
label="分享用户"
|
|
||||||
:rules="[requiredValidator]"
|
|
||||||
hint="分享人的昵称"
|
|
||||||
persistent-hint
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
<VCardActions class="pt-3">
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn
|
|
||||||
variant="elevated"
|
|
||||||
:disabled="shareDoing"
|
|
||||||
@click="doShare"
|
|
||||||
prepend-icon="mdi-share"
|
|
||||||
class="px-5"
|
|
||||||
:loading="shareDoing"
|
|
||||||
>
|
|
||||||
确认分享
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
<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,15 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import api from '@/api'
|
|
||||||
import QrcodeVue from 'qrcode.vue'
|
import QrcodeVue from 'qrcode.vue'
|
||||||
import { VCardItem, VTextField } from 'vuetify/lib/components/index.mjs'
|
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'])
|
||||||
@@ -18,7 +9,7 @@ const emit = defineEmits(['done', 'close'])
|
|||||||
const qrCodeContent = ref('')
|
const qrCodeContent = ref('')
|
||||||
|
|
||||||
// 下方的提示信息
|
// 下方的提示信息
|
||||||
const text = ref('请使用微信或115客户端扫码,或在下方输入Cookie')
|
const text = ref('请使用微信或115客户端扫码')
|
||||||
|
|
||||||
// 提醒类型
|
// 提醒类型
|
||||||
const alertType = ref<'success' | 'info' | 'error' | 'warning' | undefined>('info')
|
const alertType = ref<'success' | 'info' | 'error' | 'warning' | undefined>('info')
|
||||||
@@ -28,17 +19,13 @@ let timeoutTimer: NodeJS.Timeout | undefined = undefined
|
|||||||
|
|
||||||
// 完成
|
// 完成
|
||||||
async function handleDone() {
|
async function handleDone() {
|
||||||
clearTimeout(timeoutTimer)
|
|
||||||
if (props.conf?.cookie) {
|
|
||||||
await savaU115Config()
|
|
||||||
}
|
|
||||||
emit('done')
|
emit('done')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用/aliyun/qrcode api生成二维码
|
// 调用/aliyun/qrcode api生成二维码
|
||||||
async function getQrcode() {
|
async function getQrcode() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('/storage/qrcode/u115')
|
const result: { [key: string]: any } = await api.get('/u115/qrcode')
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
qrCodeContent.value = result.data.codeContent
|
qrCodeContent.value = result.data.codeContent
|
||||||
} else {
|
} else {
|
||||||
@@ -52,25 +39,19 @@ async function getQrcode() {
|
|||||||
// 调用/aliyun/check api验证二维码
|
// 调用/aliyun/check api验证二维码
|
||||||
async function checkQrcode() {
|
async function checkQrcode() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('/storage/check/u115')
|
const result: { [key: string]: any } = await api.get('/u115/check')
|
||||||
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 == 0) {
|
if (status == 1) {
|
||||||
|
// 已确认完成
|
||||||
|
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'
|
||||||
@@ -84,15 +65,6 @@ 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)
|
||||||
@@ -108,20 +80,15 @@ 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 text-center p-3 border">
|
<div class="my-6 shadow-lg rounded border">
|
||||||
<QrcodeVue class="mx-auto" :value="qrCodeContent" :size="200" />
|
<VImg class="mx-auto" :src="qrCodeContent" style="block-size: 200px; inline-size: 200px">
|
||||||
|
<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>
|
||||||
|
|||||||
@@ -1,433 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import type { User } from '@/api/types'
|
|
||||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
|
||||||
import api from '@/api'
|
|
||||||
import { useDisplay } from 'vuetify'
|
|
||||||
import avatar1 from '@images/avatars/avatar-1.png'
|
|
||||||
import store from '@/store'
|
|
||||||
|
|
||||||
// 显示器宽度
|
|
||||||
const display = useDisplay()
|
|
||||||
|
|
||||||
const refInputEl = ref<HTMLElement>()
|
|
||||||
const isNewPasswordVisible = ref(false)
|
|
||||||
const isConfirmPasswordVisible = ref(false)
|
|
||||||
const newPassword = ref('')
|
|
||||||
const confirmPassword = ref('')
|
|
||||||
|
|
||||||
// 输入参数
|
|
||||||
const props = defineProps({
|
|
||||||
username: String,
|
|
||||||
usernames: Array,
|
|
||||||
oper: String,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 当前登录用户名称
|
|
||||||
const currentLoginUser = store.state.auth.userName
|
|
||||||
|
|
||||||
// 用户名
|
|
||||||
const userName = ref('')
|
|
||||||
|
|
||||||
// 当前头像缓存
|
|
||||||
const currentAvatar = ref(avatar1)
|
|
||||||
|
|
||||||
// 用户名缓存
|
|
||||||
const currentUserName = ref('')
|
|
||||||
|
|
||||||
// 注册事件
|
|
||||||
const emit = defineEmits(['save', 'close'])
|
|
||||||
|
|
||||||
// 创建新用户按钮运行状态
|
|
||||||
const isAdding = ref(false)
|
|
||||||
|
|
||||||
// 更新用户消息按钮运行状态
|
|
||||||
const isUpdating = ref(false)
|
|
||||||
|
|
||||||
// 提示框
|
|
||||||
const $toast = useToast()
|
|
||||||
|
|
||||||
// 状态下拉项
|
|
||||||
const statusItems = [
|
|
||||||
{ title: '激活', value: 1 },
|
|
||||||
{ title: '已停用', value: 0 },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 用户编辑表单数据
|
|
||||||
const userForm = ref<User>({
|
|
||||||
id: 0,
|
|
||||||
name: props.username ?? '',
|
|
||||||
password: '',
|
|
||||||
email: '',
|
|
||||||
is_active: true,
|
|
||||||
is_superuser: false,
|
|
||||||
avatar: avatar1,
|
|
||||||
is_otp: false,
|
|
||||||
permissions: {},
|
|
||||||
settings: {
|
|
||||||
wechat_userid: null,
|
|
||||||
telegram_userid: null,
|
|
||||||
slack_userid: null,
|
|
||||||
vocechat_userid: null,
|
|
||||||
synologychat_userid: null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 更新头像
|
|
||||||
function changeAvatar(file: Event) {
|
|
||||||
const fileReader = new FileReader()
|
|
||||||
const { files } = file.target as HTMLInputElement
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const selectedFile = files[0]
|
|
||||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
|
|
||||||
const maxSize = 800 * 1024
|
|
||||||
// 检查文件是否为图片
|
|
||||||
if (!allowedTypes.includes(selectedFile.type)) {
|
|
||||||
$toast.error('上传的文件不符合要求,请重新选择头像')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 检查文件大小
|
|
||||||
if (selectedFile.size > maxSize) {
|
|
||||||
$toast.error('文件大小不得大于800KB')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fileReader.readAsDataURL(selectedFile)
|
|
||||||
fileReader.onload = () => {
|
|
||||||
if (typeof fileReader.result === 'string') {
|
|
||||||
currentAvatar.value = fileReader.result
|
|
||||||
$toast.success('新头像上传成功,待保存后生效!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置默认头像
|
|
||||||
function resetDefaultAvatar() {
|
|
||||||
currentAvatar.value = avatar1
|
|
||||||
$toast.success('已重置为默认头像,待保存后生效!')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 还原当前头像
|
|
||||||
function restoreCurrentAvatar() {
|
|
||||||
currentAvatar.value = userForm.value.avatar
|
|
||||||
$toast.success('已还原当前使用头像!')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询用户信息
|
|
||||||
async function fetchUserInfo() {
|
|
||||||
try {
|
|
||||||
userForm.value = await api.get(`user/${props.username}`)
|
|
||||||
if (userForm.value) {
|
|
||||||
userForm.value.avatar = userForm.value.avatar || avatar1
|
|
||||||
currentAvatar.value = userForm.value.avatar
|
|
||||||
currentUserName.value = userForm.value.name
|
|
||||||
userName.value = userForm.value.name
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用API 新增用户
|
|
||||||
async function addUser() {
|
|
||||||
if (isAdding.value) {
|
|
||||||
$toast.error(`正在创建【${userForm.value.name}】用户,请稍后`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!currentUserName.value) {
|
|
||||||
$toast.error('用户名不能为空')
|
|
||||||
return
|
|
||||||
} else userForm.value.name = currentUserName.value
|
|
||||||
// 重名检查
|
|
||||||
if (props.usernames && props.usernames.includes(userForm.value.name)) {
|
|
||||||
$toast.error('用户名已存在')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!userForm.value?.name || !newPassword.value) return
|
|
||||||
if (newPassword.value || confirmPassword.value) {
|
|
||||||
if (newPassword.value !== confirmPassword.value) {
|
|
||||||
$toast.error('两次输入的密码不一致')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userForm.value.password = newPassword.value
|
|
||||||
}
|
|
||||||
isAdding.value = true
|
|
||||||
startNProgress()
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: string } = await api.post('user/', userForm.value)
|
|
||||||
if (result.success) {
|
|
||||||
$toast.success(`用户【${userForm.value.name}】创建成功`)
|
|
||||||
emit('save')
|
|
||||||
} else {
|
|
||||||
$toast.error(`创建用户失败:${result.message}`)
|
|
||||||
// 清除用户名
|
|
||||||
userForm.value.name = ''
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
doneNProgress()
|
|
||||||
isAdding.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用API更新用户信息
|
|
||||||
async function updateUser() {
|
|
||||||
if (isUpdating.value) {
|
|
||||||
$toast.error(`正在更新【${userForm.value.name}】用户,请稍后`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!currentUserName.value) {
|
|
||||||
$toast.error('用户名不能为空')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (newPassword.value || confirmPassword.value) {
|
|
||||||
if (newPassword.value !== confirmPassword.value) {
|
|
||||||
$toast.error('两次输入的密码不一致')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userForm.value.password = newPassword.value
|
|
||||||
}
|
|
||||||
const oldUserName = userForm.value.name
|
|
||||||
userForm.value.name = currentUserName.value
|
|
||||||
const oldAvatar = userForm.value.avatar
|
|
||||||
userForm.value.avatar = currentAvatar.value
|
|
||||||
isUpdating.value = true
|
|
||||||
startNProgress()
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.put('user/', userForm.value)
|
|
||||||
if (result.success) {
|
|
||||||
if (oldUserName !== currentUserName.value) {
|
|
||||||
$toast.success(`【${oldUserName}】更名【${currentUserName.value}】, 更新成功!`)
|
|
||||||
// 如果是当前登录用户,更新当前用户名称显示
|
|
||||||
if (isCurrentUser.value) store.commit('auth/setUserName', currentUserName.value)
|
|
||||||
} else {
|
|
||||||
$toast.success(`【${userForm.value?.name}】更新成功!`)
|
|
||||||
}
|
|
||||||
// 更新本地头像显示
|
|
||||||
if (oldAvatar !== currentAvatar.value && isCurrentUser.value) {
|
|
||||||
store.commit('auth/setAvatar', currentAvatar.value)
|
|
||||||
}
|
|
||||||
emit('save')
|
|
||||||
} else {
|
|
||||||
if (oldUserName !== currentUserName.value) {
|
|
||||||
$toast.error(`【${oldUserName}】更名【${currentUserName.value}】, 更新失败:${result.message}`)
|
|
||||||
currentUserName.value = oldUserName
|
|
||||||
} else {
|
|
||||||
$toast.error(`【${userForm.value?.name}】更新失败:${result.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//失败缓存值还原
|
|
||||||
currentUserName.value = userForm.value.name
|
|
||||||
userForm.value.name = oldUserName
|
|
||||||
currentAvatar.value = userForm.value.avatar
|
|
||||||
userForm.value.avatar = oldAvatar
|
|
||||||
userForm.value.password = ''
|
|
||||||
} catch (error) {
|
|
||||||
$toast.error(`【${userForm.value?.name}】更新失败!`)
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
doneNProgress()
|
|
||||||
isUpdating.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户状态转换,true/false转换为1/0
|
|
||||||
const userStatus = computed({
|
|
||||||
get: () => (userForm.value.is_active ? 1 : 0),
|
|
||||||
set: (value: number) => {
|
|
||||||
userForm.value.is_active = value === 1
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算是否有用户管理权限
|
|
||||||
const canControl = computed(() => {
|
|
||||||
// 新增用户时,有权限
|
|
||||||
if (props.oper === 'add') {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
// 调用isCurrentUser函数判断是否为当前用户
|
|
||||||
return !isCurrentUser.value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 检查是否为当前用户
|
|
||||||
const isCurrentUser = computed(() => {
|
|
||||||
return props.username === currentLoginUser
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (props.oper !== 'add') {
|
|
||||||
fetchUserInfo()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VDialog scrollable :close-on-back="false" persistent eager max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
|
||||||
<VCard
|
|
||||||
:title="`${props.oper === 'add' ? '新增' : '编辑'}用户${props.oper !== 'add' ? ` - ${userName}` : ''}`"
|
|
||||||
class="rounded-t"
|
|
||||||
>
|
|
||||||
<DialogCloseBtn @click="emit('close')" />
|
|
||||||
<VDivider />
|
|
||||||
<VCardItem>
|
|
||||||
<!-- 👉 Avatar -->
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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="props.oper === 'add' ? 'info' : 'error'"
|
|
||||||
variant="tonal"
|
|
||||||
@click="resetDefaultAvatar"
|
|
||||||
>
|
|
||||||
<VIcon icon="mdi-image-sync-outline" />
|
|
||||||
<span v-if="display.mdAndUp.value" class="ms-2">默认</span>
|
|
||||||
</VBtn>
|
|
||||||
</div>
|
|
||||||
<p class="text-body-1 mb-0">允许 JPG、PNG、GIF、WEBP 格式, 最大尺寸 800KB。</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VCardItem>
|
|
||||||
<VCardText>
|
|
||||||
<VForm @submit.prevent="() => {}">
|
|
||||||
<VDivider class="my-10">
|
|
||||||
<span>用户基础设置</span>
|
|
||||||
</VDivider>
|
|
||||||
<VRow>
|
|
||||||
<VCol md="6" cols="12">
|
|
||||||
<VTextField
|
|
||||||
v-model="currentUserName"
|
|
||||||
density="comfortable"
|
|
||||||
:readonly="props.oper !== 'add'"
|
|
||||||
label="用户名"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField v-model="userForm.email" density="comfortable" clearable label="邮箱" type="email" />
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="newPassword"
|
|
||||||
density="comfortable"
|
|
||||||
:type="isNewPasswordVisible ? 'text' : 'password'"
|
|
||||||
:append-inner-icon="isNewPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
|
||||||
clearable
|
|
||||||
label="密码"
|
|
||||||
autocomplete=""
|
|
||||||
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<!-- 👉 confirm password -->
|
|
||||||
<VTextField
|
|
||||||
v-model="confirmPassword"
|
|
||||||
density="comfortable"
|
|
||||||
:type="isConfirmPasswordVisible ? 'text' : 'password'"
|
|
||||||
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
|
||||||
clearable
|
|
||||||
label="确认密码"
|
|
||||||
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6" v-if="canControl">
|
|
||||||
<VSelect
|
|
||||||
v-model="userStatus"
|
|
||||||
:items="statusItems"
|
|
||||||
item-text="title"
|
|
||||||
item-value="value"
|
|
||||||
label="状态"
|
|
||||||
dense
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VDivider class="my-10">
|
|
||||||
<span>账号绑定</span>
|
|
||||||
</VDivider>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField v-model="userForm.settings.wechat_userid" density="comfortable" clearable label="微信用户" />
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="userForm.settings.telegram_userid"
|
|
||||||
density="comfortable"
|
|
||||||
clearable
|
|
||||||
label="Telegram用户"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField v-model="userForm.settings.slack_userid" density="comfortable" clearable label="Slack用户" />
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="userForm.settings.vocechat_userid"
|
|
||||||
density="comfortable"
|
|
||||||
clearable
|
|
||||||
label="VoceChat用户"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="userForm.settings.synologychat_userid"
|
|
||||||
density="comfortable"
|
|
||||||
clearable
|
|
||||||
label="SynologyChat用户"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField v-model="userForm.settings.douban_userid" density="comfortable" clearable label="豆瓣用户" />
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
<VCardActions class="pt-3">
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn
|
|
||||||
v-if="props.oper === 'add'"
|
|
||||||
:disabled="isAdding"
|
|
||||||
color="primary"
|
|
||||||
variant="elevated"
|
|
||||||
@click="addUser"
|
|
||||||
prepend-icon="mdi-plus"
|
|
||||||
class="px-5"
|
|
||||||
>
|
|
||||||
<span v-if="isAdding">创建中...</span>
|
|
||||||
<span v-else>创建</span>
|
|
||||||
</VBtn>
|
|
||||||
<VBtn
|
|
||||||
v-else
|
|
||||||
:disabled="isUpdating"
|
|
||||||
color="primary"
|
|
||||||
variant="elevated"
|
|
||||||
@click="updateUser"
|
|
||||||
prepend-icon="mdi-content-save"
|
|
||||||
class="px-5"
|
|
||||||
>
|
|
||||||
<span v-if="isUpdating">更新中...</span>
|
|
||||||
<span v-else>更新</span>
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -6,16 +6,19 @@ import { useToast } from 'vue-toast-notification'
|
|||||||
import ReorganizeDialog from '../dialog/ReorganizeDialog.vue'
|
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 store from '@/store'
|
||||||
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()
|
||||||
|
|
||||||
// APP
|
// APP
|
||||||
const appMode = inject('pwaMode') && display.mdAndDown.value
|
const appMode = computed(() => {
|
||||||
|
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
|
||||||
|
})
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const inProps = defineProps({
|
const inProps = defineProps({
|
||||||
@@ -102,19 +105,18 @@ const dirs = computed(() => items.value.filter(item => item.type === 'dir' && it
|
|||||||
|
|
||||||
// 文件过滤
|
// 文件过滤
|
||||||
const files = computed(() => items.value.filter(item => item.type === 'file' && item.name.includes(filter.value)))
|
const files = computed(() => items.value.filter(item => item.type === 'file' && item.name.includes(filter.value)))
|
||||||
|
// 是否目录
|
||||||
|
const isDir = computed(() => inProps.item.path?.endsWith('/'))
|
||||||
|
|
||||||
// 是否文件
|
// 是否文件
|
||||||
const isFile = computed(() => inProps.item.type == 'file')
|
const isFile = computed(() => !isDir.value)
|
||||||
|
|
||||||
// 需要整理的文件项
|
// 需要整理的文件项
|
||||||
const transferItems = ref<FileItem[]>([])
|
const transferItems = ref<FileItem[]>([])
|
||||||
|
|
||||||
// 当前图片地址
|
|
||||||
const currentImgLink = ref('')
|
|
||||||
|
|
||||||
// 大小控制
|
// 大小控制
|
||||||
const scrollStyle = computed(() => {
|
const scrollStyle = computed(() => {
|
||||||
return appMode
|
return appMode.value
|
||||||
? 'height: calc(100vh - 15.5rem - env(safe-area-inset-bottom) - 3.5rem)'
|
? 'height: calc(100vh - 15.5rem - env(safe-area-inset-bottom) - 3.5rem)'
|
||||||
: 'height: calc(100vh - 14.5rem - env(safe-area-inset-bottom)'
|
: 'height: calc(100vh - 14.5rem - env(safe-area-inset-bottom)'
|
||||||
})
|
})
|
||||||
@@ -122,7 +124,7 @@ const scrollStyle = computed(() => {
|
|||||||
// 是否为图片文件
|
// 是否为图片文件
|
||||||
const isImage = computed(() => {
|
const isImage = computed(() => {
|
||||||
const ext = inProps.item.path?.split('.').pop()?.toLowerCase()
|
const ext = inProps.item.path?.split('.').pop()?.toLowerCase()
|
||||||
return ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].includes(ext ?? '')
|
return ['png', 'jpg', 'jpeg', 'gif', 'bmp'].includes(ext ?? '')
|
||||||
})
|
})
|
||||||
|
|
||||||
// 调整选择模式
|
// 调整选择模式
|
||||||
@@ -137,7 +139,9 @@ async function list_files() {
|
|||||||
emit('loading', true)
|
emit('loading', true)
|
||||||
|
|
||||||
// 参数
|
// 参数
|
||||||
const url = inProps.endpoints?.list.url.replace(/{sort}/g, inProps.sort || 'name')
|
const url = inProps.endpoints?.list.url
|
||||||
|
.replace(/{storage}/g, inProps.storage)
|
||||||
|
.replace(/{sort}/g, inProps.sort || 'name')
|
||||||
|
|
||||||
const config: AxiosRequestConfig<FileItem> = {
|
const config: AxiosRequestConfig<FileItem> = {
|
||||||
url,
|
url,
|
||||||
@@ -165,7 +169,7 @@ async function deleteItem(item: FileItem, confirm: boolean = true) {
|
|||||||
emit('loading', true)
|
emit('loading', true)
|
||||||
|
|
||||||
// 请求API
|
// 请求API
|
||||||
const url = inProps.endpoints?.delete.url
|
const url = inProps.endpoints?.delete.url.replace(/{storage}/g, inProps.storage)
|
||||||
const config: AxiosRequestConfig<FileItem> = {
|
const config: AxiosRequestConfig<FileItem> = {
|
||||||
url,
|
url,
|
||||||
method: inProps.endpoints?.delete.method || 'post',
|
method: inProps.endpoints?.delete.method || 'post',
|
||||||
@@ -230,51 +234,23 @@ function listItemClick(item: FileItem) {
|
|||||||
|
|
||||||
// 新窗口中下载文件
|
// 新窗口中下载文件
|
||||||
async function download(item: FileItem) {
|
async function download(item: FileItem) {
|
||||||
const url = inProps.endpoints?.download.url
|
const url = inProps.endpoints?.download.url.replace(/{storage}/g, inProps.storage)
|
||||||
// 下载文件
|
const filterEntries = Object.entries(item).filter(([key, value]) => !['children', 'thumbnail'].includes(key) && value)
|
||||||
const config: AxiosRequestConfig<FileItem> = {
|
const queryParams = filterEntries.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&')
|
||||||
url,
|
window.open(
|
||||||
method: inProps.endpoints?.download.method || 'post',
|
`${import.meta.env.VITE_API_BASE_URL}${url.slice(1)}?${queryParams}&token=${store.state.auth.token}`,
|
||||||
data: item,
|
'_blank',
|
||||||
responseType: 'blob',
|
)
|
||||||
}
|
|
||||||
// 加载数据
|
|
||||||
const result: Blob = await inProps.axios.request(config)
|
|
||||||
if (result) {
|
|
||||||
const downloadUrl = URL.createObjectURL(result)
|
|
||||||
window.open(downloadUrl, '_blank')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取图片地址
|
// 获取图片地址
|
||||||
async function getImgLink(item: FileItem) {
|
function getImgLink(item: FileItem) {
|
||||||
let url = inProps.endpoints?.image.url
|
let url = inProps.endpoints?.image.url.replace(/{storage}/g, inProps.storage)
|
||||||
// 下载文件
|
const filterEntries = Object.entries(item).filter(([key, value]) => !['children', 'thumbnail'].includes(key) && value)
|
||||||
const config: AxiosRequestConfig<FileItem> = {
|
const queryParams = filterEntries.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&')
|
||||||
url,
|
return `${import.meta.env.VITE_API_BASE_URL}${url.slice(1)}?${queryParams}&token=${store.state.auth.token}`
|
||||||
method: inProps.endpoints?.image.method || 'post',
|
|
||||||
data: item,
|
|
||||||
responseType: 'blob',
|
|
||||||
}
|
|
||||||
// 加载二进制数据
|
|
||||||
const result: Blob = await inProps.axios.request(config)
|
|
||||||
if (result) {
|
|
||||||
// 创建图片地址
|
|
||||||
currentImgLink.value = URL.createObjectURL(result)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果当前是图片且是文件,则获取图片地址
|
|
||||||
watch(
|
|
||||||
() => inProps.item,
|
|
||||||
async () => {
|
|
||||||
if (isImage.value && isFile.value) {
|
|
||||||
await getImgLink(inProps.item)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
// 显示重命名弹窗
|
// 显示重命名弹窗
|
||||||
function showRenmae(item: FileItem) {
|
function showRenmae(item: FileItem) {
|
||||||
currentItem.value = item
|
currentItem.value = item
|
||||||
@@ -324,7 +300,9 @@ async function rename() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 调API
|
// 调API
|
||||||
let url = inProps.endpoints?.rename.url.replace(/{newname}/g, encodeURIComponent(newName.value))
|
let url = inProps.endpoints?.rename.url
|
||||||
|
.replace(/{storage}/g, inProps.storage)
|
||||||
|
.replace(/{newname}/g, encodeURIComponent(newName.value))
|
||||||
if (renameAll.value) {
|
if (renameAll.value) {
|
||||||
url += '&recursive=true'
|
url += '&recursive=true'
|
||||||
}
|
}
|
||||||
@@ -518,7 +496,12 @@ async function batchScrape() {
|
|||||||
// 使用SSE监听加载进度
|
// 使用SSE监听加载进度
|
||||||
function startLoadingProgress() {
|
function startLoadingProgress() {
|
||||||
progressText.value = '请稍候 ...'
|
progressText.value = '请稍候 ...'
|
||||||
progressEventSource.value = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/progress/batchrename`)
|
|
||||||
|
const token = store.state.auth.token
|
||||||
|
|
||||||
|
progressEventSource.value = new EventSource(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}system/progress/batchrename?token=${token}`,
|
||||||
|
)
|
||||||
progressEventSource.value.onmessage = event => {
|
progressEventSource.value.onmessage = event => {
|
||||||
const progress = JSON.parse(event.data)
|
const progress = JSON.parse(event.data)
|
||||||
if (progress) {
|
if (progress) {
|
||||||
@@ -600,7 +583,7 @@ onMounted(() => {
|
|||||||
</VCardText>
|
</VCardText>
|
||||||
<!-- 图片 -->
|
<!-- 图片 -->
|
||||||
<VCardText v-else-if="isFile && isImage && items.length > 0" class="grow d-flex justify-center align-center">
|
<VCardText v-else-if="isFile && isImage && items.length > 0" class="grow d-flex justify-center align-center">
|
||||||
<VImg :src="currentImgLink" max-width="100%" max-height="100%" />
|
<VImg :src="getImgLink(items[0])" max-width="100%" max-height="100%" />
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<!-- 目录和文件列表 -->
|
<!-- 目录和文件列表 -->
|
||||||
<VCardText v-else-if="dirs.length || files.length" class="p-0">
|
<VCardText v-else-if="dirs.length || files.length" class="p-0">
|
||||||
@@ -619,8 +602,7 @@ onMounted(() => {
|
|||||||
v-if="inProps.icons && item.extension"
|
v-if="inProps.icons && item.extension"
|
||||||
:icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other"
|
:icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other"
|
||||||
/>
|
/>
|
||||||
<VIcon v-else-if="item.type == 'dir'" icon="mdi-folder-outline" />
|
<VIcon v-else icon="mdi-folder-outline" />
|
||||||
<VIcon v-else icon="mdi-file-outline" />
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<VListItemTitle v-text="item.name" />
|
<VListItemTitle v-text="item.name" />
|
||||||
@@ -727,20 +709,22 @@ onMounted(() => {
|
|||||||
<ReorganizeDialog
|
<ReorganizeDialog
|
||||||
v-if="transferPopper"
|
v-if="transferPopper"
|
||||||
v-model="transferPopper"
|
v-model="transferPopper"
|
||||||
|
:storage="inProps.storage"
|
||||||
:items="transferItems"
|
:items="transferItems"
|
||||||
:target_storage="inProps.storage"
|
|
||||||
@done="transferDone"
|
@done="transferDone"
|
||||||
@close="transferPopper = false"
|
@close="transferPopper = false"
|
||||||
/>
|
/>
|
||||||
<!-- 进度框 -->
|
<!-- 进度框 -->
|
||||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" :value="progressValue" />
|
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" :value="progressValue" />
|
||||||
<!-- 识别结果对话框 -->
|
<!-- 识别结果对话框 -->
|
||||||
<MediaInfoDialog
|
<VDialog v-if="nameTestDialog" v-model="nameTestDialog" width="50rem">
|
||||||
v-if="nameTestDialog"
|
<VCard>
|
||||||
v-model="nameTestDialog"
|
<DialogCloseBtn @click="nameTestDialog = false" />
|
||||||
:context="nameTestResult"
|
<VCardItem>
|
||||||
@close="nameTestDialog = false"
|
<MediaInfoCard :context="nameTestResult" />
|
||||||
/>
|
</VCardItem>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const pathSegments = computed(() => {
|
|||||||
|
|
||||||
// 当前存储
|
// 当前存储
|
||||||
const storageObject = computed(() => {
|
const storageObject = computed(() => {
|
||||||
return inProps.storages?.find(item => item.value === inProps.storage)
|
return inProps.storages?.find(item => item.code === inProps.storage)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 切换存储
|
// 切换存储
|
||||||
@@ -88,7 +88,9 @@ function goUp() {
|
|||||||
// 创建目录
|
// 创建目录
|
||||||
async function mkdir() {
|
async function mkdir() {
|
||||||
emit('loading', true)
|
emit('loading', true)
|
||||||
const url = inProps.endpoints?.mkdir.url.replace(/{name}/g, newFolderName.value)
|
const url = inProps.endpoints?.mkdir.url
|
||||||
|
.replace(/{storage}/g, inProps.storage)
|
||||||
|
.replace(/{name}/g, newFolderName.value)
|
||||||
|
|
||||||
const config: AxiosRequestConfig<FileItem> = {
|
const config: AxiosRequestConfig<FileItem> = {
|
||||||
url,
|
url,
|
||||||
@@ -127,19 +129,19 @@ const sortIcon = computed(() => {
|
|||||||
<VListItem
|
<VListItem
|
||||||
v-for="(item, index) in storages"
|
v-for="(item, index) in storages"
|
||||||
:key="index"
|
:key="index"
|
||||||
:disabled="item.value === storageObject?.value"
|
:disabled="item.code === storageObject?.code"
|
||||||
@click="changeStorage(item.value)"
|
@click="changeStorage(item.code)"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<Icon :icon="item.icon" />
|
<Icon :icon="item.icon" />
|
||||||
</template>
|
</template>
|
||||||
<VListItemTitle>{{ item.title }}</VListItemTitle>
|
<VListItemTitle>{{ item.name }}</VListItemTitle>
|
||||||
</VListItem>
|
</VListItem>
|
||||||
</VList>
|
</VList>
|
||||||
</VMenu>
|
</VMenu>
|
||||||
<VBtn variant="text" :input-value="item.path === '/'" class="px-1" @click="changePath(inProps.itemstack[0])">
|
<VBtn variant="text" :input-value="item.path === '/'" class="px-1" @click="changePath(inProps.itemstack[0])">
|
||||||
<VIcon :icon="storageObject?.icon" class="mr-2" />
|
<VIcon :icon="storageObject?.icon" class="mr-2" />
|
||||||
{{ storageObject?.title }}
|
{{ storageObject?.name }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<template v-for="(segment, index) in pathSegments" :key="index">
|
<template v-for="(segment, index) in pathSegments" :key="index">
|
||||||
<VBtn
|
<VBtn
|
||||||
@@ -169,7 +171,7 @@ const sortIcon = computed(() => {
|
|||||||
</IconBtn>
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
</VTooltip>
|
</VTooltip>
|
||||||
<VDialog v-if="newFolderPopper" v-model="newFolderPopper" max-width="50rem">
|
<VDialog v-model="newFolderPopper" max-width="50rem">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<IconBtn v-bind="props">
|
<IconBtn v-bind="props">
|
||||||
<VTooltip text="新建文件夹">
|
<VTooltip text="新建文件夹">
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
<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>
|
|
||||||
90
src/components/input/PathField.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<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: '/',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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: '',
|
||||||
|
basename: props.root,
|
||||||
|
extension: '',
|
||||||
|
size: 0,
|
||||||
|
modify_time: 0,
|
||||||
|
fileid: '',
|
||||||
|
parent_fileid: '',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 拉取子目录
|
||||||
|
async function fetchDirs(item: any) {
|
||||||
|
return api
|
||||||
|
.get('/local/listdir?path=' + item.path)
|
||||||
|
.then((data: any) => {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDirs(treeItems.value[0])
|
||||||
|
})
|
||||||
|
</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>
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
defineProps<{ title: string }>()
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<VListSubheader>{{ title }}</VListSubheader>
|
|
||||||
<VListItem><slot /></VListItem>
|
|
||||||
</template>
|
|
||||||