mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-10 17:42:50 +08:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6317277a70 | ||
|
|
a1130ec60b | ||
|
|
a1a3ccf6fb | ||
|
|
aedb8bee9c | ||
|
|
6620d1c8fe | ||
|
|
0ecc7dfead | ||
|
|
9f5859ee93 | ||
|
|
d559e1717c | ||
|
|
e649be58a2 | ||
|
|
157c37c862 | ||
|
|
da910ac670 | ||
|
|
3831363815 | ||
|
|
94a6ea13bd | ||
|
|
06c1ad0f69 | ||
|
|
d6873781e8 | ||
|
|
ab6c9647a7 | ||
|
|
59b0350993 | ||
|
|
df0be4c070 | ||
|
|
87f3ef4353 | ||
|
|
2611bbaea4 | ||
|
|
7c0d8cf792 | ||
|
|
2d17baccd2 | ||
|
|
fe31723726 | ||
|
|
bb10b22421 | ||
|
|
6445f3a634 | ||
|
|
d1f28d9c94 | ||
|
|
1e5366123c | ||
|
|
7feff7c90b | ||
|
|
429b3bc045 | ||
|
|
e76f1b89da | ||
|
|
f25e8595c3 | ||
|
|
6977ce55a3 | ||
|
|
222e0e5ff2 | ||
|
|
6996d9bbe2 | ||
|
|
f70e08adac | ||
|
|
223ecc0e6b | ||
|
|
43f36f556c | ||
|
|
4579e00283 | ||
|
|
b5e9b14048 | ||
|
|
2288e72c5f | ||
|
|
4882cc0417 | ||
|
|
499d3d0424 | ||
|
|
d6b17debb4 | ||
|
|
8f970e0008 | ||
|
|
18d778a1cc | ||
|
|
d667c4e45d | ||
|
|
b7f8ffd56f | ||
|
|
c20f9d527f | ||
|
|
b859d00cb9 | ||
|
|
a2d28ad360 | ||
|
|
c6702fbc18 | ||
|
|
5018f96786 | ||
|
|
f29f408b67 | ||
|
|
a475a3b851 | ||
|
|
9335f79c30 | ||
|
|
9dab691649 | ||
|
|
16abc65f49 | ||
|
|
23ac80886d | ||
|
|
b242e757e0 | ||
|
|
a69965a605 | ||
|
|
3321427eb4 | ||
|
|
3ffe354770 | ||
|
|
52e0d3a4bc | ||
|
|
e865a5ca62 | ||
|
|
528a4ddb03 | ||
|
|
36f3b649c6 | ||
|
|
ce91c0cc30 | ||
|
|
e31e9e3520 | ||
|
|
df313ebe7f | ||
|
|
e1cf36e952 | ||
|
|
493194652c | ||
|
|
5030e75c2c | ||
|
|
3c70eac7ca | ||
|
|
f9b22962a4 | ||
|
|
7ce0c21b0c | ||
|
|
7a7a8c923f | ||
|
|
d5d5e28f7e | ||
|
|
b22ac27075 | ||
|
|
3cb5f4bdfe | ||
|
|
d355e4575d | ||
|
|
bdbb118e55 | ||
|
|
9a174d99db | ||
|
|
9c8725066c | ||
|
|
9f0f3de864 | ||
|
|
ac84ed2d6a | ||
|
|
9d7e15f4df | ||
|
|
c3563f4501 | ||
|
|
a543202edc | ||
|
|
52cf517a91 | ||
|
|
11b649dc8c | ||
|
|
19663bacb1 | ||
|
|
41c276d0e0 | ||
|
|
6bb73add28 | ||
|
|
2c16b6c078 | ||
|
|
5ddc955805 | ||
|
|
6a3afa4240 | ||
|
|
deabd7b83c | ||
|
|
422e5858ef | ||
|
|
3c019d1376 | ||
|
|
f676e8423e | ||
|
|
f687d1de01 | ||
|
|
6fe28bc2ef | ||
|
|
86b5af3423 |
@@ -1 +1,2 @@
|
||||
VITE_API_BASE_URL=http://localhost:3001/api/v1/
|
||||
VITE_PUBLIC_VAPID_KEY=BH3w49sZA6jXUnE-yt4jO6VKh73lsdsvwoJ6Hx7fmPIDKoqGiUl2GEoZzy-iJfn4SfQQcx7yQdHf9RknwrL_lSM
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
VITE_API_BASE_URL=api/v1/
|
||||
VITE_PUBLIC_VAPID_KEY=BH3w49sZA6jXUnE-yt4jO6VKh73lsdsvwoJ6Hx7fmPIDKoqGiUl2GEoZzy-iJfn4SfQQcx7yQdHf9RknwrL_lSM
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@ node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
dev-dist
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="manifest.json" crossorigin="use-credentials" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
@@ -30,6 +29,7 @@
|
||||
<meta name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<link rel="stylesheet" type="text/css" href="/loader.css" />
|
||||
<link rel="preload" href="index.js" as="script">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -159,4 +159,4 @@
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
40
package.json
40
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "1.8.9",
|
||||
"version": "1.9.4-1",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
@@ -19,44 +19,37 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@casl/ability": "^6.2.0",
|
||||
"@casl/vue": "^2.2.0",
|
||||
"@floating-ui/dom": "1.6.3",
|
||||
"@fullcalendar/core": "^6.1.8",
|
||||
"@fullcalendar/daygrid": "^6.1.8",
|
||||
"@fullcalendar/interaction": "^6.1.7",
|
||||
"@fullcalendar/list": "^6.1.7",
|
||||
"@fullcalendar/timegrid": "^6.1.7",
|
||||
"@fullcalendar/vue3": "^6.1.8",
|
||||
"@iconify/utils": "^2.1.22",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vueuse/math": "^10.1.2",
|
||||
"ace-builds": "^1.32.6",
|
||||
"apexcharts-clevision": "^3.28.5",
|
||||
"axios": "1.6.8",
|
||||
"axios-mock-adapter": "^1.21.4",
|
||||
"chart.js": "^4.1.2",
|
||||
"colorthief": "^2.4.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nprogress": "^0.2.0",
|
||||
"postcss-purgecss": "^5.0.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"pull-refresh-vue3": "^0.3.1",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"sass": "^1.59.3",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"unplugin-vue-define-options": "^1.3.5",
|
||||
"vite-plugin-pwa": "^0.19.8",
|
||||
"vue": "^3.3.2",
|
||||
"vue-chartjs": "^5.2.0",
|
||||
"vue-flatpickr-component": "11.0.5",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-prism-component": "^2.0.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"vue-toast-notification": "^3",
|
||||
"vue-virtual-scroll-grid": "^1.11.0",
|
||||
"vue3-ace-editor": "^2.2.4",
|
||||
"vue3-apexcharts": "^1.4.1",
|
||||
"vue3-perfect-scrollbar": "^2.0.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuetify": "3.5.14",
|
||||
"vuetify": "3.6.8",
|
||||
"vuetify-use-dialog": "^0.6.11",
|
||||
"vuex": "^4.1.0",
|
||||
"vuex-persistedstate": "^4.1.0",
|
||||
@@ -64,12 +57,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config-vue": "^0.43.1",
|
||||
"@fullcalendar/core": "^6.1.8",
|
||||
"@fullcalendar/daygrid": "^6.1.8",
|
||||
"@fullcalendar/interaction": "^6.1.7",
|
||||
"@fullcalendar/list": "^6.1.7",
|
||||
"@fullcalendar/timegrid": "^6.1.7",
|
||||
"@fullcalendar/vue3": "^6.1.8",
|
||||
"@iconify-json/mdi": "^1.1.52",
|
||||
"@iconify/tools": "^4.0.4",
|
||||
"@iconify/vue": "4.1.1",
|
||||
@@ -83,7 +70,6 @@
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"dayjs": "^1.11.10",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
@@ -93,7 +79,6 @@
|
||||
"eslint-plugin-sonarjs": "^0.25.1",
|
||||
"eslint-plugin-unicorn": "^52.0.0",
|
||||
"eslint-plugin-vue": "^9.12.0",
|
||||
"lodash": "^4.17.21",
|
||||
"postcss": "8",
|
||||
"postcss-html": "^1.5.0",
|
||||
"stylelint": "16.3.1",
|
||||
@@ -106,6 +91,7 @@
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-pages": "^0.32.1",
|
||||
"vite-plugin-pwa": "^0.20.0",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vite-plugin-vuetify": "2.0.3",
|
||||
"vue-shepherd": "^3.0.0",
|
||||
@@ -115,4 +101,4 @@
|
||||
"resolutions": {
|
||||
"postcss": "8"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
{
|
||||
"name": "MoviePilot",
|
||||
"short_name": "MoviePilot",
|
||||
"start_url": "./",
|
||||
"display": "minimal-ui",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "./android-chrome-192x192_maskable.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "./android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "./android-chrome-512x512_maskable.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#28243D",
|
||||
"background_color": "#28243D",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "推荐",
|
||||
"url": "./ranking",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./sparkles-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "电影订阅",
|
||||
"url": "./subscribe-movie",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./clock-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "电视剧订阅",
|
||||
"url": "./subscribe-tv",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./clock-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "设置",
|
||||
"url": "./setting",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./cog-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,6 +11,13 @@ http {
|
||||
|
||||
keepalive_timeout 3600;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
gzip_proxied any;
|
||||
gzip_min_length 256;
|
||||
gzip_vary on;
|
||||
gzip_comp_level 6;
|
||||
|
||||
server {
|
||||
|
||||
include mime.types;
|
||||
@@ -28,9 +35,16 @@ http {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
# 静态资源
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
root html;
|
||||
}
|
||||
|
||||
location /assets {
|
||||
# 静态资源
|
||||
expires 7d;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public";
|
||||
root html;
|
||||
}
|
||||
|
||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -14,7 +14,10 @@ function onClick() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconBtn :class="props.innerClass ? props.innerClass : 'absolute right-3 top-3'" @click.stop="onClick">
|
||||
<IconBtn
|
||||
:class="props.innerClass ? props.innerClass : 'absolute right-3 top-3'"
|
||||
@click.stop="onClick"
|
||||
>
|
||||
<VIcon icon="mdi-close" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
@@ -1,28 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
// 定义输入参数
|
||||
const props = defineProps({
|
||||
progress: Number,
|
||||
text: String
|
||||
})
|
||||
// 定义输入参数
|
||||
const props = defineProps({
|
||||
progress: Number,
|
||||
text: String,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full text-center text-gray-500 text-sm flex flex-col items-center"
|
||||
>
|
||||
<VProgressCircular
|
||||
v-if="!props.text"
|
||||
size="48"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<VProgressCircular
|
||||
v-if="props.progress"
|
||||
class="mb-3"
|
||||
color="primary"
|
||||
:model-value="props.progress"
|
||||
size="64"
|
||||
/>
|
||||
<div class="w-full text-center text-gray-500 text-sm flex flex-col items-center">
|
||||
<VProgressCircular v-if="!props.text || !props.progress" class="mb-3" size="48" indeterminate color="primary" />
|
||||
<VProgressCircular v-if="props.progress" class="mb-3" color="primary" :model-value="props.progress" size="64" />
|
||||
<span>{{ props.text }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { useDisplay, useTheme } from 'vuetify'
|
||||
import type { ThemeSwitcherTheme } from '@layouts/types'
|
||||
import api from '@/api'
|
||||
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
const props = defineProps<{
|
||||
themes: ThemeSwitcherTheme[]
|
||||
@@ -18,6 +23,17 @@ const { state: currentThemeName, next: getNextThemeName } = useCycleList(
|
||||
{ initialValue: savedTheme.value },
|
||||
)
|
||||
|
||||
const $toast = useToast()
|
||||
|
||||
// 自定义CSS弹窗
|
||||
const cssDialog = ref(false)
|
||||
|
||||
// 自定义 CSS
|
||||
const customCSS = ref('')
|
||||
|
||||
// 编辑器主题
|
||||
const editorTheme = computed(() => (currentThemeName.value === 'light' ? 'github' : 'monokai'))
|
||||
|
||||
// 主题切换动画
|
||||
function themeTransition() {
|
||||
const x = performance.now()
|
||||
@@ -134,6 +150,43 @@ watch(
|
||||
() => currentThemeName.value,
|
||||
() => updateTheme(),
|
||||
)
|
||||
|
||||
// 获取自定义 CSS
|
||||
async function getCustomCSS() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/UserCustomCSS')
|
||||
if (result && result.success && result.data?.value) {
|
||||
customCSS.value = result.data?.value ?? ''
|
||||
if (customCSS.value) {
|
||||
const style = document.createElement('style')
|
||||
style.innerHTML = result.data?.value ?? ''
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存自定义 CSS
|
||||
async function saveCustomCSS() {
|
||||
cssDialog.value = false
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('system/setting/UserCustomCSS', customCSS.value, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
})
|
||||
|
||||
if (result.success) $toast.success('自定义CSS保存成功!')
|
||||
} catch (e) {
|
||||
console.error('保存自定义 CSS 到服务端失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCustomCSS()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -150,8 +203,36 @@ watch(
|
||||
</template>
|
||||
<VListItemTitle>{{ theme.title }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem @click="cssDialog = true">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-palette" />
|
||||
</template>
|
||||
<VListItemTitle>自定义</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
<!-- 自定义 CSS -- -->
|
||||
<VDialog v-model="cssDialog" persistent max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard title="自定义主题风格">
|
||||
<DialogCloseBtn @click="cssDialog = false" />
|
||||
<VDivider />
|
||||
<VAceEditor
|
||||
v-model:value="customCSS"
|
||||
lang="css"
|
||||
:theme="editorTheme"
|
||||
style="block-size: 100%; min-block-size: 30rem"
|
||||
/>
|
||||
<VDivider />
|
||||
<VCardText class="text-center">
|
||||
<VBtn @click="saveCustomCSS" class="w-1/2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-content-save" />
|
||||
</template>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
/**
|
||||
* 修复低版本Safari等浏览器数组不支持at函数的问题
|
||||
*/
|
||||
export function fixArrayAt() {
|
||||
if (!Array.prototype.at) {
|
||||
Array.prototype.at = function(index: number) {
|
||||
if (index >= 0) {
|
||||
return this[index]
|
||||
} else {
|
||||
return this[this.length + index]
|
||||
}
|
||||
}
|
||||
;(function fixArrayAt() {
|
||||
if (!Array.prototype.at) {
|
||||
Array.prototype.at = function (index: number) {
|
||||
if (index >= 0) {
|
||||
return this[index]
|
||||
} else {
|
||||
return this[this.length + index]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -8,8 +8,7 @@ dayjs.extend(relativeTime)
|
||||
dayjs.locale(ZH_CN)
|
||||
|
||||
export function avatarText(value: string) {
|
||||
if (!value)
|
||||
return ''
|
||||
if (!value) return ''
|
||||
const nameArray = value.split(' ')
|
||||
|
||||
return nameArray.map(word => word.charAt(0).toUpperCase()).join('')
|
||||
@@ -19,7 +18,9 @@ export function avatarText(value: string) {
|
||||
export function kFormatter(num: number) {
|
||||
const regex = /\B(?=(\d{3})+(?!\d))/g
|
||||
|
||||
return Math.abs(num) > 9999 ? `${Math.sign(num) * +((Math.abs(num) / 1000).toFixed(1))}k` : Math.abs(num).toFixed(0).replace(regex, ',')
|
||||
return Math.abs(num) > 9999
|
||||
? `${Math.sign(num) * +(Math.abs(num) / 1000).toFixed(1)}k`
|
||||
: Math.abs(num).toFixed(0).replace(regex, ',')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,9 +30,11 @@ export function kFormatter(num: number) {
|
||||
* @param {string} value date to format
|
||||
* @param {Intl.DateTimeFormatOptions} formatting Intl object to format with
|
||||
*/
|
||||
export function formatDate(value: string, formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' }) {
|
||||
if (!value)
|
||||
return value
|
||||
export function formatDate(
|
||||
value: string,
|
||||
formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' },
|
||||
) {
|
||||
if (!value) return value
|
||||
|
||||
return new Intl.DateTimeFormat('en-US', formatting).format(new Date(value))
|
||||
}
|
||||
@@ -46,21 +49,19 @@ export function formatDateToMonthShort(value: string, toTimeForCurrentDay = true
|
||||
const date = new Date(value)
|
||||
let formatting: Record<string, string> = { month: 'short', day: 'numeric' }
|
||||
|
||||
if (toTimeForCurrentDay && isToday(date))
|
||||
formatting = { hour: 'numeric', minute: 'numeric' }
|
||||
if (toTimeForCurrentDay && isToday(date)) formatting = { hour: 'numeric', minute: 'numeric' }
|
||||
|
||||
return new Intl.DateTimeFormat('en-US', formatting).format(new Date(value))
|
||||
}
|
||||
|
||||
export const prefixWithPlus = (value: number) => value > 0 ? `+${value}` : value
|
||||
export const prefixWithPlus = (value: number) => (value > 0 ? `+${value}` : value)
|
||||
|
||||
// 格式化为Sxx
|
||||
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
|
||||
export function formatFileSize(bytes: number) {
|
||||
if (bytes < 0)
|
||||
throw new Error('字节数不能为负数。')
|
||||
if (bytes < 0) throw new Error('字节数不能为负数。')
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let size = bytes
|
||||
@@ -82,22 +83,18 @@ export function formatSeconds(seconds: number) {
|
||||
|
||||
let formattedTime = ''
|
||||
|
||||
if (hours > 0)
|
||||
formattedTime += `${hours}小时`
|
||||
if (hours > 0) formattedTime += `${hours}小时`
|
||||
|
||||
if (minutes > 0)
|
||||
formattedTime += `${minutes}分`
|
||||
if (minutes > 0) formattedTime += `${minutes}分`
|
||||
|
||||
if ((remainingSeconds > 0 || formattedTime === '') && hours <= 0)
|
||||
formattedTime += `${remainingSeconds}秒`
|
||||
if ((remainingSeconds > 0 || formattedTime === '') && hours <= 0) formattedTime += `${remainingSeconds}秒`
|
||||
|
||||
return formattedTime
|
||||
}
|
||||
|
||||
// YYYY-MM-DD 转化为Date
|
||||
export function parseDate(dateString: string): Date | null {
|
||||
if (!dateString)
|
||||
return null
|
||||
if (!dateString) return null
|
||||
const [year, month, day] = dateString.split('-').map(Number)
|
||||
|
||||
return new Date(year, month - 1, day)
|
||||
@@ -105,8 +102,7 @@ export function parseDate(dateString: string): Date | null {
|
||||
|
||||
// 文件大小格式化
|
||||
export function formatBytes(bytes: number, decimals = 2) {
|
||||
if (bytes === 0)
|
||||
return '0 bytes'
|
||||
if (bytes === 0) return '0 bytes'
|
||||
|
||||
const k = 1024
|
||||
const dm = decimals < 0 ? 0 : decimals
|
||||
@@ -119,11 +115,9 @@ export function formatBytes(bytes: number, decimals = 2) {
|
||||
|
||||
// 格式化剧集列表
|
||||
export function formatEp(nums: number[]): string {
|
||||
if (!nums.length)
|
||||
return ''
|
||||
if (!nums.length) return ''
|
||||
|
||||
if (nums.length === 1)
|
||||
return nums[0].toString()
|
||||
if (nums.length === 1) return nums[0].toString()
|
||||
|
||||
// 将数组升序排序
|
||||
nums.sort((a, b) => a - b)
|
||||
@@ -134,44 +128,22 @@ export function formatEp(nums: number[]): string {
|
||||
for (let i = 1; i < nums.length; i++) {
|
||||
if (nums[i] === end + 1) {
|
||||
end = nums[i]
|
||||
}
|
||||
else {
|
||||
if (start === end)
|
||||
formattedRanges.push(start.toString())
|
||||
|
||||
else
|
||||
formattedRanges.push(`${start.toString()}-${end.toString()}`)
|
||||
} else {
|
||||
if (start === end) formattedRanges.push(start.toString())
|
||||
else formattedRanges.push(`${start.toString()}-${end.toString()}`)
|
||||
|
||||
start = end = nums[i]
|
||||
}
|
||||
}
|
||||
|
||||
if (start === end)
|
||||
formattedRanges.push(start.toString())
|
||||
|
||||
else
|
||||
formattedRanges.push(`${start.toString()}-${end.toString()}`)
|
||||
if (start === end) formattedRanges.push(start.toString())
|
||||
else formattedRanges.push(`${start.toString()}-${end.toString()}`)
|
||||
|
||||
return formattedRanges.join('、')
|
||||
}
|
||||
|
||||
// 将yyyy-mm-dd hh:mm:ss转换为时间差,如:1小时前,1天前
|
||||
export function formatDateDifference(dateString: string): string {
|
||||
// const timeDifference = dayjs().millisecond() - dayjs(dateString).millisecond()
|
||||
// const secondsDifference = Math.floor(timeDifference / 1000)
|
||||
// const minutesDifference = Math.floor(secondsDifference / 60)
|
||||
// const hoursDifference = Math.floor(minutesDifference / 60)
|
||||
// const daysDifference = Math.floor(hoursDifference / 24)
|
||||
|
||||
// if (daysDifference > 0)
|
||||
// return `${daysDifference}天前`
|
||||
// else if (hoursDifference > 0)
|
||||
// return `${hoursDifference}小时前`
|
||||
// else if (minutesDifference > 0)
|
||||
// return `${minutesDifference}分钟前`
|
||||
// else
|
||||
// return '刚刚'
|
||||
if (!dateString)
|
||||
return ''
|
||||
if (!dateString) return ''
|
||||
return dayjs(dateString).fromNow()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// 👉 IsEmpty
|
||||
export function isEmpty(value: unknown): boolean {
|
||||
if (value === null || value === undefined || value === '')
|
||||
return true
|
||||
if (value === null || value === undefined || value === '') return true
|
||||
|
||||
return !!(Array.isArray(value) && value.length === 0)
|
||||
}
|
||||
@@ -33,73 +32,6 @@ export function isToday(date: Date) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算时间差,返回xx天/xx小时/xx分钟/xx秒
|
||||
*
|
||||
* @deprecated 建议使用:@core/utils/formatters.ts formatDateDifference
|
||||
*/
|
||||
export function calculateTimeDifference(inputTime: string): string {
|
||||
if (!inputTime)
|
||||
return ''
|
||||
|
||||
const inputDate = new Date(inputTime.replaceAll(/-/g, '/'))
|
||||
const currentDate = new Date()
|
||||
|
||||
const timeDifference = currentDate.getTime() - inputDate.getTime()
|
||||
const secondsDifference = Math.floor(timeDifference / 1000)
|
||||
|
||||
if (secondsDifference < 60) {
|
||||
return `${secondsDifference}秒`
|
||||
}
|
||||
else if (secondsDifference < 3600) {
|
||||
const minutes = Math.floor(secondsDifference / 60)
|
||||
|
||||
return `${minutes}分钟`
|
||||
}
|
||||
else if (secondsDifference < 86400) {
|
||||
const hours = Math.floor(secondsDifference / 3600)
|
||||
|
||||
return `${hours}小时`
|
||||
}
|
||||
else {
|
||||
const days = Math.floor(secondsDifference / 86400)
|
||||
|
||||
return `${days}天`
|
||||
}
|
||||
}
|
||||
|
||||
// 计算时间差,返回xx天xx小时xx分钟
|
||||
export function calculateTimeDiff(inputTime: string): string {
|
||||
if (!inputTime)
|
||||
return ''
|
||||
|
||||
// 使用当前时区
|
||||
const inputDate = new Date(inputTime.replaceAll(/-/g, '/'))
|
||||
const currentDate = new Date()
|
||||
|
||||
const timeDifference = currentDate.getTime() - inputDate.getTime()
|
||||
const secondsDifference = Math.floor(timeDifference / 1000)
|
||||
|
||||
const days = Math.floor(secondsDifference / 86400)
|
||||
const hours = Math.floor(secondsDifference % 86400 / 3600)
|
||||
const minutes = Math.floor(secondsDifference % 86400 % 3600 / 60)
|
||||
const secones = Math.floor(secondsDifference % 60)
|
||||
|
||||
if (days > 0)
|
||||
return `${days}天${hours}小时${minutes}分钟`
|
||||
|
||||
else if (hours > 0)
|
||||
return `${hours}小时${minutes}分钟`
|
||||
|
||||
else if (minutes > 0)
|
||||
return `${minutes}分钟`
|
||||
|
||||
else if (secones > 0)
|
||||
return `${secones}秒`
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// 判断一个数组subArray是不是在另一个数组mainArray中
|
||||
export function isContained(subArray: any[], mainArray: any[]): boolean {
|
||||
return subArray.every(element => mainArray.includes(element))
|
||||
@@ -112,8 +44,7 @@ export function isIntersected(array1: any[], array2: any[]): boolean {
|
||||
|
||||
export function isNullOrEmptyObject(obj: any): boolean {
|
||||
// 首先判断是否为 null 或 undefined
|
||||
if (obj === null || obj === undefined)
|
||||
return true
|
||||
if (obj === null || obj === undefined) return true
|
||||
|
||||
// 然后判断是否为空对象
|
||||
return !!(typeof obj === 'object' && Object.keys(obj).length === 0)
|
||||
@@ -127,3 +58,10 @@ export function checkPrefersColorSchemeIsDark(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 从URL中获取参数值
|
||||
export function getQueryValue(key: string, url = window.location.href): string {
|
||||
const reg = new RegExp(`[?&]${key}=([^&#]*)`, 'i')
|
||||
const res = reg.exec(url)
|
||||
return res ? res[1] : ''
|
||||
}
|
||||
|
||||
@@ -28,3 +28,17 @@ export async function copyToClipboard(content: string) {
|
||||
document.body.removeChild(input)
|
||||
}
|
||||
}
|
||||
|
||||
// VAPID公钥转Uint8Array
|
||||
export function urlBase64ToUint8Array(base64String: string) {
|
||||
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
|
||||
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
|
||||
|
||||
const rawData = window.atob(base64)
|
||||
const outputArray = new Uint8Array(rawData.length)
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i)
|
||||
}
|
||||
return outputArray
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ defineProps<{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding-left: 1.375rem;
|
||||
padding-right: 1rem;
|
||||
margin-right: 1.125em;
|
||||
border-radius: 0 3.125rem 3.125rem 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,3 +18,12 @@ defineProps<{
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.layout-vertical-nav {
|
||||
.nav-section-title {
|
||||
padding-left: 1.375rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
6
src/@layouts/types.d.ts
vendored
6
src/@layouts/types.d.ts
vendored
@@ -120,6 +120,12 @@ export interface NavLink extends NavLinkProps, Partial<AclProperties> {
|
||||
disable?: boolean
|
||||
}
|
||||
|
||||
export interface NavMenu extends NavLink {
|
||||
header: string
|
||||
admin: boolean
|
||||
description?: string
|
||||
}
|
||||
|
||||
// 👉 Vertical nav group
|
||||
export interface NavGroup extends Partial<AclProperties> {
|
||||
title: string
|
||||
|
||||
@@ -8,6 +8,8 @@ import modeHtmlUrl from 'ace-builds/src-noconflict/mode-html?url'
|
||||
|
||||
import modeYamlUrl from 'ace-builds/src-noconflict/mode-yaml?url'
|
||||
|
||||
import modeCssUrl from 'ace-builds/src-noconflict/mode-css?url'
|
||||
|
||||
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url'
|
||||
|
||||
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url'
|
||||
@@ -24,6 +26,8 @@ import workerHtmlUrl from 'ace-builds/src-noconflict/worker-html?url'
|
||||
|
||||
import workerYamlUrl from 'ace-builds/src-noconflict/worker-yaml?url'
|
||||
|
||||
import workerCssUrl from 'ace-builds/src-noconflict/worker-css?url'
|
||||
|
||||
import snippetsHtmlUrl from 'ace-builds/src-noconflict/snippets/html?url'
|
||||
|
||||
import snippetsJsUrl from 'ace-builds/src-noconflict/snippets/javascript?url'
|
||||
@@ -32,12 +36,15 @@ import snippetsYamlUrl from 'ace-builds/src-noconflict/snippets/yaml?url'
|
||||
|
||||
import snippetsJsonUrl from 'ace-builds/src-noconflict/snippets/json?url'
|
||||
|
||||
import snippertsCssUrl from 'ace-builds/src-noconflict/snippets/css?url'
|
||||
|
||||
import 'ace-builds/src-noconflict/ext-language_tools'
|
||||
|
||||
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl)
|
||||
ace.config.setModuleUrl('ace/mode/javascript', modeJavascriptUrl)
|
||||
ace.config.setModuleUrl('ace/mode/html', modeHtmlUrl)
|
||||
ace.config.setModuleUrl('ace/mode/yaml', modeYamlUrl)
|
||||
ace.config.setModuleUrl('ace/mode/css', modeCssUrl)
|
||||
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl)
|
||||
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
|
||||
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
|
||||
@@ -46,9 +53,11 @@ ace.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl)
|
||||
ace.config.setModuleUrl('ace/mode/javascript_worker', workerJavascriptUrl)
|
||||
ace.config.setModuleUrl('ace/mode/html_worker', workerHtmlUrl)
|
||||
ace.config.setModuleUrl('ace/mode/yaml_worker', workerYamlUrl)
|
||||
ace.config.setModuleUrl('ace/mode/css_worker', workerCssUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/html', snippetsHtmlUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/javascript', snippetsJsUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/javascript', snippetsYamlUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/css', snippertsCssUrl)
|
||||
|
||||
ace.require('ace/ext/language_tools')
|
||||
|
||||
@@ -8,32 +8,33 @@ const api = axios.create({
|
||||
})
|
||||
|
||||
// 添加请求拦截器
|
||||
api.interceptors.request.use((config) => {
|
||||
api.interceptors.request.use(config => {
|
||||
// 在请求头中添加token
|
||||
const token = store.state.auth.token
|
||||
if (token)
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
// 添加响应拦截器
|
||||
api.interceptors.response.use((response) => {
|
||||
return response.data
|
||||
}, (error) => {
|
||||
if (!error.response) {
|
||||
// 请求超时
|
||||
return Promise.reject(error)
|
||||
}
|
||||
else if (error.response.status === 403) {
|
||||
// 清除登录状态信息
|
||||
store.dispatch('auth/clearToken')
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
return response.data
|
||||
},
|
||||
error => {
|
||||
if (!error.response) {
|
||||
// 请求超时
|
||||
return Promise.reject(new Error(error))
|
||||
} else if (error.response.status === 403) {
|
||||
// 清除登录状态信息
|
||||
store.dispatch('auth/clearToken')
|
||||
|
||||
// token验证失败,跳转到登录页面
|
||||
router.push('/login')
|
||||
}
|
||||
// token验证失败,跳转到登录页面
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
})
|
||||
return Promise.reject(new Error(error))
|
||||
},
|
||||
)
|
||||
|
||||
export default api
|
||||
|
||||
@@ -58,11 +58,13 @@ export interface Subscribe {
|
||||
// 当前优先级
|
||||
current_priority: number
|
||||
// 保存目录
|
||||
save_path: string
|
||||
save_path?: string
|
||||
// 时间
|
||||
date: string
|
||||
// 编辑框设置项
|
||||
show_edit_dialog: boolean
|
||||
// 编辑框打开状态
|
||||
page_open?: boolean
|
||||
}
|
||||
|
||||
// 历史记录
|
||||
@@ -445,6 +447,8 @@ export interface Plugin {
|
||||
history?: { [key: string]: string }
|
||||
// 添加时间
|
||||
add_time?: number
|
||||
// 页面打开状态
|
||||
page_open?: boolean
|
||||
}
|
||||
|
||||
// 渲染结构
|
||||
@@ -464,6 +468,8 @@ export interface DashboardItem {
|
||||
id: string
|
||||
// 名称
|
||||
name: string
|
||||
// 插件的仪表板key
|
||||
key: string
|
||||
// 全局配置
|
||||
attrs: { [key: string]: any }
|
||||
// col列数
|
||||
@@ -714,12 +720,7 @@ export interface NotificationSwitch {
|
||||
slack: boolean
|
||||
synologychat: boolean
|
||||
vocechat: boolean
|
||||
}
|
||||
|
||||
// 环境设置
|
||||
export interface Setting {
|
||||
// 下载目录
|
||||
DOWNLOAD_PATH: string
|
||||
webpush: boolean
|
||||
}
|
||||
|
||||
// 文件浏览接口
|
||||
@@ -740,7 +741,7 @@ export interface EndPoints {
|
||||
|
||||
// 文件浏览项目
|
||||
export interface FileItem {
|
||||
// 类型
|
||||
// 类型 dir/file
|
||||
type: string
|
||||
// 文件名
|
||||
name: string
|
||||
@@ -831,3 +832,23 @@ export interface SystemNotification {
|
||||
// 通知时间
|
||||
date: string
|
||||
}
|
||||
|
||||
// 下载目录/媒体库目录
|
||||
export interface MediaDirectory {
|
||||
// 类型 download/library
|
||||
type?: string
|
||||
// 别名
|
||||
name?: string
|
||||
// 路径
|
||||
path?: string
|
||||
// 媒体类型 电影/电视剧
|
||||
media_type?: string
|
||||
// 媒体类别 动画电影/国产剧
|
||||
category?: string
|
||||
// 刮削媒体信息
|
||||
scrape?: boolean
|
||||
// 自动二级分类,未指定类别时自动分类
|
||||
auto_category?: boolean
|
||||
// 优先级
|
||||
priority?: number
|
||||
}
|
||||
|
||||
104
src/components/cards/DirectoryCard.vue
Normal file
104
src/components/cards/DirectoryCard.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MediaDirectory } from '@/api/types'
|
||||
import { VTextField } from 'vuetify/lib/components/index.mjs'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
type: String, // download/library
|
||||
directory: {
|
||||
type: Object as PropType<MediaDirectory>,
|
||||
required: true, // 必填参数
|
||||
},
|
||||
categories: {
|
||||
type: Object as PropType<{ [key: string]: any }>,
|
||||
required: true,
|
||||
},
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 路径
|
||||
const path = ref<string>('')
|
||||
|
||||
// 类型下拉字典
|
||||
const typeItems = [
|
||||
{ title: '全部', value: '' },
|
||||
{ title: '电影', value: '电影' },
|
||||
{ title: '电视剧', value: '电视剧' },
|
||||
]
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['close', 'changed', 'update:modelValue'])
|
||||
|
||||
// 按钮点击
|
||||
function onClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 路径更新
|
||||
function updatePath(value: string) {
|
||||
path.value = value
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
// 根据选中的媒体类型,获取对应的媒体类别
|
||||
const getCategories = computed(() => {
|
||||
const default_value = [{ title: '全部', value: '' }]
|
||||
if (!props.categories || !props.categories[props.directory?.media_type ?? '']) return default_value
|
||||
return default_value.concat(props.categories[props.directory.media_type ?? ''])
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="tonal" :width="props.width" :height="props.height">
|
||||
<DialogCloseBtn @click="onClose" />
|
||||
<VCardItem>
|
||||
<VTextField
|
||||
v-model="props.directory.name"
|
||||
variant="underlined"
|
||||
label="别名"
|
||||
class="me-20 text-high-emphasis font-weight-bold"
|
||||
/>
|
||||
<span class="absolute top-3 right-12">
|
||||
<IconBtn>
|
||||
<VIcon class="cursor-move" icon="mdi-drag" />
|
||||
</IconBtn>
|
||||
</span>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<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
|
||||
v-model="props.directory.media_type"
|
||||
variant="underlined"
|
||||
:items="typeItems"
|
||||
label="媒体类型"
|
||||
@update:modelValue="props.directory.category = ''"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol>
|
||||
<VSelect v-model="props.directory.category" variant="underlined" :items="getCategories" label="媒体类别" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol v-if="!props.directory.category || props.directory.category === ''">
|
||||
<VSwitch v-model="props.directory.auto_category" label="自动分类"></VSwitch>
|
||||
</VCol>
|
||||
<VCol v-if="type === 'library'">
|
||||
<VSwitch v-model="props.directory.scrape" label="刮削元数据"></VSwitch>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -365,11 +365,19 @@ const dropdownItems = ref([
|
||||
// 监听插件状态变化
|
||||
watch(
|
||||
() => props.plugin?.has_update,
|
||||
(newHasUpdate, oldHasUpdate) => {
|
||||
(newHasUpdate, _) => {
|
||||
const updateItemIndex = dropdownItems.value.findIndex(item => item.value === 3)
|
||||
if (updateItemIndex !== -1) dropdownItems.value[updateItemIndex].show = newHasUpdate
|
||||
},
|
||||
)
|
||||
|
||||
// 监听插件窗口状态变化
|
||||
watch(
|
||||
() => props.plugin?.page_open,
|
||||
(newOpenState, _) => {
|
||||
if (newOpenState) openPluginDetail()
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -456,7 +464,7 @@ watch(
|
||||
<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 end" size="x-large" fixed app appear @click="showPluginConfig" />
|
||||
<VFab icon="mdi-cog" location="bottom" size="x-large" fixed app appear @click="showPluginConfig" />
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
|
||||
@@ -167,6 +167,12 @@ watch(resourceDialog, value => {
|
||||
if (!value) getSiteStats()
|
||||
})
|
||||
|
||||
// 保存站点
|
||||
function saveSite() {
|
||||
siteEditDialog.value = false
|
||||
emit('update')
|
||||
}
|
||||
|
||||
// 装载时查询站点图标
|
||||
onMounted(() => {
|
||||
getSiteIcon()
|
||||
@@ -175,150 +181,142 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
:height="cardProps.height"
|
||||
:width="cardProps.width"
|
||||
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
|
||||
class="overflow-hidden"
|
||||
@click="siteEditDialog = true"
|
||||
>
|
||||
<template #image>
|
||||
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
|
||||
<VImg :src="siteIcon" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
|
||||
<VCardItem>
|
||||
<VCardTitle class="font-bold">
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
|
||||
<StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
|
||||
|
||||
<VCardText class="py-2">
|
||||
<VTooltip v-if="cardProps.site?.render === 1" text="浏览器仿真">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-apple-safari" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip v-if="cardProps.site?.proxy === 1" text="代理">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-network-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip v-if="cardProps.site?.limit_interval" text="流控">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-speedometer" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip v-if="cardProps.site?.filter" text="过滤">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-filter-cog-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
</VCardText>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VCardActions>
|
||||
<VBtn v-if="!cardProps.site?.public" :disabled="updateButtonDisable" @click.stop="handleSiteUpdate">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-refresh" />
|
||||
</template>
|
||||
更新
|
||||
</VBtn>
|
||||
<VBtn :disabled="testButtonDisable" @click.stop="testSite">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-link" />
|
||||
</template>
|
||||
{{ testButtonText }}
|
||||
</VBtn>
|
||||
<VBtn @click.stop="handleResourceBrowse">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-web" />
|
||||
</template>
|
||||
浏览
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
<!-- 更新站点Cookie & UA弹窗 -->
|
||||
<VDialog v-model="siteCookieDialog" max-width="50rem">
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="更新站点Cookie & UA">
|
||||
<DialogCloseBtn @click="siteCookieDialog = false" />
|
||||
<VCardText>
|
||||
<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>
|
||||
<div>
|
||||
<VCard
|
||||
:height="cardProps.height"
|
||||
:width="cardProps.width"
|
||||
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
|
||||
class="overflow-hidden"
|
||||
@click="siteEditDialog = true"
|
||||
>
|
||||
<template #image>
|
||||
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
|
||||
<VImg :src="siteIcon" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardItem style="padding-block-end: 0;">
|
||||
<VCardTitle class="font-bold">
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.url }}</span>
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText class="py-2" style="block-size: 36px;">
|
||||
<VTooltip v-if="cardProps.site?.limit_interval" text="流控">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-speedometer" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip v-if="cardProps.site?.proxy === 1" text="代理">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-network-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip v-if="cardProps.site?.render === 1" text="浏览器仿真">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-apple-safari" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip v-if="cardProps.site?.filter" text="过滤">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-filter-cog-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="elevated" @click="updateSiteCookie" prepend-icon="mdi-refresh" class="px-5"> 开始更新 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<SiteAddEditDialog
|
||||
v-if="siteEditDialog"
|
||||
v-model="siteEditDialog"
|
||||
:siteid="cardProps.site?.id"
|
||||
@save="
|
||||
() => {
|
||||
siteEditDialog = false
|
||||
emit('update')
|
||||
}
|
||||
"
|
||||
@remove="emit('remove')"
|
||||
@close="siteEditDialog = false"
|
||||
/>
|
||||
<!-- 站点资源弹窗 -->
|
||||
<VDialog
|
||||
v-if="resourceDialog"
|
||||
v-model="resourceDialog"
|
||||
max-width="80rem"
|
||||
scrollable
|
||||
z-index="1010"
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard :title="`浏览站点 - ${cardProps.site?.name}`">
|
||||
<DialogCloseBtn @click="resourceDialog = false" />
|
||||
<VDivider />
|
||||
<VCardText class="pt-2">
|
||||
<SiteTorrentTable :site="cardProps.site?.id" />
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn v-if="!cardProps.site?.public" :disabled="updateButtonDisable" @click.stop="handleSiteUpdate">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-refresh" />
|
||||
</template>
|
||||
更新
|
||||
</VBtn>
|
||||
<VBtn :disabled="testButtonDisable" @click.stop="testSite">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-link" />
|
||||
</template>
|
||||
{{ testButtonText }}
|
||||
</VBtn>
|
||||
<VBtn @click.stop="handleResourceBrowse">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-web" />
|
||||
</template>
|
||||
浏览
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
<StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
|
||||
<span class="absolute top-1 right-8">
|
||||
<VIcon class="cursor-move">mdi-drag</VIcon>
|
||||
</span>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
<!-- 更新站点Cookie & UA弹窗 -->
|
||||
<VDialog v-model="siteCookieDialog" max-width="50rem">
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="更新站点Cookie & UA">
|
||||
<DialogCloseBtn @click="siteCookieDialog = false" />
|
||||
<VCardText>
|
||||
<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
|
||||
v-if="siteEditDialog"
|
||||
v-model="siteEditDialog"
|
||||
:siteid="cardProps.site?.id"
|
||||
@save="saveSite"
|
||||
@remove="emit('remove')"
|
||||
@close="siteEditDialog = false"
|
||||
/>
|
||||
<!-- 站点资源弹窗 -->
|
||||
<VDialog
|
||||
v-if="resourceDialog"
|
||||
v-model="resourceDialog"
|
||||
max-width="80rem"
|
||||
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>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.v-table th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
||||
import { formatDateDifference } from '@/@core/utils/formatters'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
@@ -15,6 +16,9 @@ const props = defineProps({
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['remove', 'save'])
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -84,6 +88,25 @@ async function searchSubscribe() {
|
||||
}
|
||||
}
|
||||
|
||||
// 重置订阅
|
||||
async function resetSubscribe() {
|
||||
// 确认
|
||||
try {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `重置后 ${props.media?.name} 已下载记录将被清除,未入库的剧集将会重新下载,是否确认?`,
|
||||
})
|
||||
if (!isConfirmed) return
|
||||
// 重置
|
||||
const result: { [key: string]: any } = await api.get(`subscribe/reset/${props.media?.id}`)
|
||||
// 提示
|
||||
if (result.success) $toast.success(`${props.media?.name} 重置成功!`)
|
||||
else $toast.error(`${props.media?.name} 重置失败:${result.message}`)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑订阅响应
|
||||
async function editSubscribeDialog() {
|
||||
subscribeEditDialog.value = true
|
||||
@@ -124,8 +147,18 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '取消订阅',
|
||||
title: '重置',
|
||||
value: 4,
|
||||
props: {
|
||||
prependIcon: 'mdi-restore-alert',
|
||||
click: resetSubscribe,
|
||||
color: 'warning',
|
||||
},
|
||||
show: props.media?.type === '电视剧',
|
||||
},
|
||||
{
|
||||
title: '取消订阅',
|
||||
value: 5,
|
||||
props: {
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
@@ -133,6 +166,14 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
// 监听插件窗口状态变化
|
||||
watch(
|
||||
() => props.media?.page_open,
|
||||
(newOpenState, _) => {
|
||||
if (newOpenState) editSubscribeDialog()
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -171,18 +212,19 @@ const dropdownItems = ref([
|
||||
<VIcon icon="mdi-dots-vertical" :color="getTextColor()" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
: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>
|
||||
<template v-for="(item, i) in dropdownItems" :key="i">
|
||||
<VListItem
|
||||
v-if="item.show !== false"
|
||||
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>
|
||||
</template>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
|
||||
@@ -70,18 +70,24 @@ async function handleAddDownload(_site: any = undefined, _media: any = undefined
|
||||
async function addDownload(_media: MediaInfo, _torrent: TorrentInfo) {
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('download/', {
|
||||
media_in: _media,
|
||||
torrent_in: _torrent,
|
||||
})
|
||||
let result: { [key: string]: any }
|
||||
|
||||
if (result.success) {
|
||||
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} 添加下载成功!`)
|
||||
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 下载成功!`)
|
||||
downloaded.value.push(_torrent?.enclosure || '')
|
||||
} else {
|
||||
// 添加下载失败
|
||||
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`)
|
||||
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 下载失败:${result?.message}!`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -127,7 +133,7 @@ onMounted(() => {
|
||||
</template>
|
||||
<VCardItem class="py-1">
|
||||
<VCardTitle class="break-words overflow-visible whitespace-break-spaces">
|
||||
{{ media?.title }} {{ 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-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
||||
</VCardTitle>
|
||||
|
||||
@@ -67,18 +67,24 @@ async function handleAddDownload(_site: any = undefined, _media: any = undefined
|
||||
async function addDownload(_media: MediaInfo, _torrent: TorrentInfo) {
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('download/', {
|
||||
media_in: _media,
|
||||
torrent_in: _torrent,
|
||||
})
|
||||
let result: { [key: string]: any }
|
||||
|
||||
if (result.success) {
|
||||
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} 添加下载成功!`)
|
||||
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 下载成功!`)
|
||||
downloaded.value.push(_torrent?.enclosure || '')
|
||||
} else {
|
||||
// 添加下载失败
|
||||
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`)
|
||||
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 下载失败:${result?.message}!`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
@@ -6,6 +6,7 @@ import api from '@/api'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import ProgressDialog from './ProgressDialog.vue'
|
||||
import { MediaDirectory } from '@/api/types'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
@@ -20,9 +21,9 @@ const props = defineProps({
|
||||
// 定义事件
|
||||
const emit = defineEmits(['done', 'close'])
|
||||
|
||||
// 生成1到50季的下拉框选项
|
||||
// 生成1到100季的下拉框选项
|
||||
const seasonItems = ref(
|
||||
Array.from({ length: 51 }, (_, i) => i).map(item => ({
|
||||
Array.from({ length: 101 }, (_, i) => i).map(item => ({
|
||||
title: `第 ${item} 季`,
|
||||
value: item,
|
||||
})),
|
||||
@@ -53,7 +54,7 @@ const progressValue = ref(0)
|
||||
const transferForm = reactive({
|
||||
logid: 0,
|
||||
path: '',
|
||||
target: props.target ?? '',
|
||||
target: props.target ?? null,
|
||||
tmdbid: null,
|
||||
doubanid: null,
|
||||
season: null,
|
||||
@@ -64,11 +65,32 @@ const transferForm = reactive({
|
||||
episode_part: '',
|
||||
episode_offset: null,
|
||||
min_filesize: 0,
|
||||
scrape: false,
|
||||
})
|
||||
|
||||
// 所有媒体库目录
|
||||
const libraryDirectories = ref<MediaDirectory[]>([])
|
||||
|
||||
// 目的目录下拉框
|
||||
const targetDirectories = computed(() => {
|
||||
const directories = libraryDirectories.value.map(item => item.path)
|
||||
return [...new Set(directories)]
|
||||
})
|
||||
|
||||
// 监听输入变化
|
||||
watchEffect(() => {
|
||||
transferForm.path = props.path ?? ''
|
||||
transferForm.target = props.target ?? ''
|
||||
transferForm.target = props.target ?? null
|
||||
})
|
||||
|
||||
// 监听目的路径变化,自动查询目录的刮削配置
|
||||
watch(transferForm, async () => {
|
||||
if (transferForm.target) {
|
||||
const directory = libraryDirectories.value.find(item => item.path === transferForm.target)
|
||||
if (directory) {
|
||||
transferForm.scrape = directory.scrape ?? false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
@@ -157,8 +179,21 @@ async function loadSystemSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
// 查询媒体库目录
|
||||
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>
|
||||
|
||||
@@ -174,11 +209,13 @@ onMounted(() => {
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol cols="12" md="8">
|
||||
<VTextField
|
||||
<VCombobox
|
||||
v-model="transferForm.target"
|
||||
:items="targetDirectories"
|
||||
label="目的路径"
|
||||
placeholder="留空自动"
|
||||
hint="留空将自动整理到媒体库目录"
|
||||
hint="整理目的路径,留空将自动匹配"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -194,6 +231,8 @@ onMounted(() => {
|
||||
{ title: 'Rclone复制', value: 'rclone_copy' },
|
||||
{ title: 'Rclone移动', value: 'rclone_move' },
|
||||
]"
|
||||
hint="文件操作整理方式"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -207,6 +246,8 @@ onMounted(() => {
|
||||
{ title: '电影', value: '电影' },
|
||||
{ title: '电视剧', value: '电视剧' },
|
||||
]"
|
||||
hint="文件的媒体类型"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -218,7 +259,8 @@ onMounted(() => {
|
||||
placeholder="留空自动识别"
|
||||
:rules="[numberValidator]"
|
||||
append-inner-icon="mdi-magnify"
|
||||
hint="点击图标按名称搜索,留空将自动重新识别"
|
||||
hint="按名称查询媒体编号,留空自动识别"
|
||||
persistent-hint
|
||||
@click:append-inner="mediaSelectorDialog = true"
|
||||
/>
|
||||
<VTextField
|
||||
@@ -229,7 +271,8 @@ onMounted(() => {
|
||||
placeholder="留空自动识别"
|
||||
:rules="[numberValidator]"
|
||||
append-inner-icon="mdi-magnify"
|
||||
hint="点击图标按名称搜索,留空将自动重新识别"
|
||||
hint="按名称查询媒体编号,留空自动识别"
|
||||
persistent-hint
|
||||
@click:append-inner="mediaSelectorDialog = true"
|
||||
/>
|
||||
</VCol>
|
||||
@@ -239,6 +282,8 @@ onMounted(() => {
|
||||
v-model.number="transferForm.season"
|
||||
label="季"
|
||||
:items="seasonItems"
|
||||
hint="指定季数"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -248,7 +293,8 @@ onMounted(() => {
|
||||
v-model="transferForm.episode_format"
|
||||
label="集数定位"
|
||||
placeholder="使用{ep}定位集数"
|
||||
hint="使用{ep}定位文件名中的集数部分,其余相同部分直接填写,不同部分使用{a}进行忽略,例如:{a}葬送的芙莉莲_Sousou no Frieren 第{ep}话{b}"
|
||||
hint="使用{ep}定位文件名中的集数部分以辅助识别"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -256,7 +302,8 @@ onMounted(() => {
|
||||
v-model="transferForm.episode_detail"
|
||||
label="指定集数"
|
||||
placeholder="起始集,终止集,如1或1,2"
|
||||
hint="直接指定集数或者范围,格式:起始集,终止集,如1或1,2"
|
||||
hint="指定集数或范围,如1或1,2"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -264,7 +311,8 @@ onMounted(() => {
|
||||
v-model="transferForm.episode_part"
|
||||
label="指定Part"
|
||||
placeholder="如part1"
|
||||
hint="指定集数的Part,如part1"
|
||||
hint="指定Part,如part1"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -272,7 +320,8 @@ onMounted(() => {
|
||||
v-model.number="transferForm.episode_offset"
|
||||
label="集数偏移"
|
||||
placeholder="如-10"
|
||||
hint="对集数进行偏移运算,如-10表示文件名中的集数减10为整理后集数"
|
||||
hint="集数偏移运算,如-10或EP*2"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -281,7 +330,18 @@ onMounted(() => {
|
||||
label="最小文件大小(MB)"
|
||||
:rules="[numberValidator]"
|
||||
placeholder="0"
|
||||
hint="最小文件大小,小于此大小的文件将被忽略不进行整理"
|
||||
hint="只整理大于最小文件大小的文件"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="transferForm.scrape"
|
||||
label="刮削元数据"
|
||||
hint="整理完成后自动刮削元数据"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
@@ -48,7 +48,7 @@ const statusItems = [
|
||||
|
||||
// 生成1到50的优先级下拉框选项
|
||||
const priorityItems = ref(
|
||||
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
|
||||
Array.from({ length: 100 }, (_, i) => i + 1).map(item => ({
|
||||
title: item,
|
||||
value: item,
|
||||
})),
|
||||
@@ -143,6 +143,7 @@ async function updateSiteInfo() {
|
||||
label="站点地址"
|
||||
:rules="[requiredValidator]"
|
||||
hint="格式:http://www.example.com/"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
@@ -151,11 +152,18 @@ async function updateSiteInfo() {
|
||||
label="优先级"
|
||||
:items="priorityItems"
|
||||
:rules="[requiredValidator]"
|
||||
hint="站点资源下载优先级,优先级数字越小越优先下载"
|
||||
hint="优先级越小越优先"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VSelect v-model="siteForm.is_active" :items="statusItems" label="状态" />
|
||||
<VSelect
|
||||
v-model="siteForm.is_active"
|
||||
:items="statusItems"
|
||||
label="状态"
|
||||
hint="站点启用/停用"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
@@ -163,34 +171,38 @@ async function updateSiteInfo() {
|
||||
<VTextField
|
||||
v-model="siteForm.rss"
|
||||
label="RSS地址"
|
||||
hint="订阅模式为站点RSS时,将会使用此地址获取站点种子资源,该地址一般会自动获取,也可手动补充"
|
||||
hint="订阅模式为`站点RSS`时使用的订阅链接,如未自动获取需手动补充"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VTextField v-model="siteForm.timeout" label="超时时间(秒)" hint="站点请求超时时间,为空将使用默认值" />
|
||||
<VTextField v-model="siteForm.timeout" label="超时时间(秒)" hint="站点请求超时时间" persistent-hint />
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="siteForm.cookie"
|
||||
label="站点Cookie"
|
||||
hint="浏览器打开站点首页,打开开发人员工具,刷新页面后在网络选项中找到首页地址,在请求头中获取Cookie信息"
|
||||
/>
|
||||
<VTextarea v-model="siteForm.cookie" label="站点Cookie" hint="站点请求头中的Cookie信息" persistent-hint />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="siteForm.token"
|
||||
label="请求头(Authorization)"
|
||||
hint="在开发人员工具,网络请求头中获取Authorization,仅个别站点需要"
|
||||
hint="站点请求头中的Authorization信息,特殊站点需要"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="siteForm.apikey" label="令牌(API Key)" hint="站点的访问API Key,仅个别站点需要" />
|
||||
<VTextField
|
||||
v-model="siteForm.apikey"
|
||||
label="令牌(API Key)"
|
||||
hint="站点的访问API Key,特殊站点需要"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="siteForm.ua"
|
||||
label="站点User-Agent"
|
||||
hint="在开发人员工具,网络请求头中获取User-Agent信息,需与站点Cookie配套使用"
|
||||
hint="获取Cookie的浏览器对应的User-Agent"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -200,7 +212,8 @@ async function updateSiteInfo() {
|
||||
v-model="siteForm.limit_interval"
|
||||
label="单位周期(秒)"
|
||||
:rules="[numberValidator]"
|
||||
hint="设定站点限流的单位周期,单位为秒,0为不限流"
|
||||
hint="限流控制的单位周期时长"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -208,7 +221,8 @@ async function updateSiteInfo() {
|
||||
v-model="siteForm.limit_count"
|
||||
label="周期内访问次数"
|
||||
:rules="[numberValidator]"
|
||||
hint="设定单位周期内站点允许的访问次数,0为不限制"
|
||||
hint="单位周期内允许的访问次数"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -216,20 +230,17 @@ async function updateSiteInfo() {
|
||||
v-model="siteForm.limit_seconds"
|
||||
label="访问间隔(秒)"
|
||||
:rules="[numberValidator]"
|
||||
hint="设定单位周期内每次站点访问需间隔时间,单位为秒,0为不限制"
|
||||
hint="每次访问需要间隔的最小时间"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch v-model="siteForm.proxy" label="代理" hint="站点是否需要代理访问,需要设置好代理服务器信息" />
|
||||
<VSwitch v-model="siteForm.proxy" label="代理" hint="使用代理服务器访问该站点" persistent-hint />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="siteForm.render"
|
||||
label="仿真"
|
||||
hint="站点是否需要使用浏览器模拟访问,开启可以一定程度上提升连通性,但会大大增加站点请求时间"
|
||||
/>
|
||||
<VSwitch v-model="siteForm.render" label="仿真" hint="使用浏览器模拟真实访问该站点" persistent-hint />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Site, Subscribe } from '@/api/types'
|
||||
import type { MediaDirectory, Site, Subscribe } from '@/api/types'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
|
||||
@@ -25,6 +25,9 @@ const emit = defineEmits(['remove', 'save', 'close'])
|
||||
// 站点数据列表
|
||||
const siteList = ref<Site[]>([])
|
||||
|
||||
// 下载目录列表
|
||||
const downloadDirectories = ref<MediaDirectory[]>([])
|
||||
|
||||
// 站点选择下载框
|
||||
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
|
||||
|
||||
@@ -50,7 +53,7 @@ const subscribeForm = ref<Subscribe>({
|
||||
last_update: '',
|
||||
username: '',
|
||||
current_priority: 0,
|
||||
save_path: '',
|
||||
save_path: undefined,
|
||||
date: '',
|
||||
show_edit_dialog: false,
|
||||
})
|
||||
@@ -167,6 +170,25 @@ async function removeSubscribe() {
|
||||
}
|
||||
}
|
||||
|
||||
// 查询下载目录
|
||||
async function loadDownloadDirectories() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/DownloadDirectories')
|
||||
if (result.success && result.data?.value) {
|
||||
downloadDirectories.value = result.data.value
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存目录下拉框
|
||||
const targetDirectories = computed(() => {
|
||||
// 去重后的下载目录
|
||||
const directories = downloadDirectories.value.map(item => item.path)
|
||||
return [...new Set(directories)]
|
||||
})
|
||||
|
||||
// 质量选择框数据
|
||||
const qualityOptions = ref([
|
||||
{
|
||||
@@ -252,9 +274,9 @@ const effectOptions = ref([
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
loadDownloadDirectories()
|
||||
getSiteList()
|
||||
if (props.subid) getSubscribeInfo()
|
||||
|
||||
if (props.default) queryDefaultSubscribeConfig()
|
||||
})
|
||||
</script>
|
||||
@@ -279,7 +301,8 @@ onMounted(() => {
|
||||
v-if="!props.default"
|
||||
v-model="subscribeForm.keyword"
|
||||
label="搜索关键词"
|
||||
hint="设定搜索关键词后,将使用此关键词搜索站点资源,否则自动使用themoviedb中的名称搜索"
|
||||
hint="指定搜索站点时使用的关键词"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="2">
|
||||
@@ -287,7 +310,8 @@ onMounted(() => {
|
||||
v-model="subscribeForm.total_episode"
|
||||
label="总集数"
|
||||
:rules="[numberValidator]"
|
||||
hint="手动设定总集数"
|
||||
hint="剧集总集数"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="subscribeForm.type === '电视剧'" cols="12" md="2">
|
||||
@@ -295,19 +319,38 @@ onMounted(() => {
|
||||
v-model="subscribeForm.start_episode"
|
||||
label="开始集数"
|
||||
:rules="[numberValidator]"
|
||||
hint="只下载此集数及之后的集"
|
||||
hint="开始订阅集数"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect v-model="subscribeForm.quality" label="质量" :items="qualityOptions" />
|
||||
<VSelect
|
||||
v-model="subscribeForm.quality"
|
||||
label="质量"
|
||||
:items="qualityOptions"
|
||||
hint="订阅资源质量"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect v-model="subscribeForm.resolution" label="分辨率" :items="resolutionOptions" />
|
||||
<VSelect
|
||||
v-model="subscribeForm.resolution"
|
||||
label="分辨率"
|
||||
:items="resolutionOptions"
|
||||
hint="订阅资源分辨率"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect v-model="subscribeForm.effect" label="特效" :items="effectOptions" />
|
||||
<VSelect
|
||||
v-model="subscribeForm.effect"
|
||||
label="特效"
|
||||
:items="effectOptions"
|
||||
hint="订阅资源特效"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
@@ -315,14 +358,16 @@ onMounted(() => {
|
||||
<VTextField
|
||||
v-model="subscribeForm.include"
|
||||
label="包含(关键字、正则式)"
|
||||
hint="支持正则表达式,多个关键字用 | 分隔表示或"
|
||||
hint="包含规则,支持正则表达式"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="subscribeForm.exclude"
|
||||
label="排除(关键字、正则式)"
|
||||
hint="支持正则表达式,多个关键字用 | 分隔表示或"
|
||||
hint="排除规则,支持正则表达式"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -332,16 +377,19 @@ onMounted(() => {
|
||||
chips
|
||||
label="订阅站点"
|
||||
multiple
|
||||
hint="只订阅选中的订阅站点,不选则订阅所有可订阅站点"
|
||||
hint="订阅的站点范围,不选使用系统设置"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
<VCombobox
|
||||
v-model="subscribeForm.save_path"
|
||||
:items="targetDirectories"
|
||||
label="保存路径"
|
||||
hint="指定该订阅的下载保存路径,留空自动使用设定的下载目录"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -350,21 +398,24 @@ onMounted(() => {
|
||||
<VSwitch
|
||||
v-model="subscribeForm.best_version"
|
||||
label="洗版"
|
||||
hint="开启后不管媒体库是否存在,均会根据洗版优先级进行过滤下载,直到下载到了最高优先级的资源为止"
|
||||
hint="根据洗版优先级进行洗版订阅"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="subscribeForm.search_imdbid"
|
||||
label="使用 ImdbID 搜索"
|
||||
hint="开启后将使用 ImdbID 搜索资源,搜索结果更精确,但不是所有站点都支持"
|
||||
hint="开使用 ImdbID 精确搜索资源"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="props.default" cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="subscribeForm.show_edit_dialog"
|
||||
label="订阅时编辑更多规则"
|
||||
hint="开启后将在添加订阅后弹出编辑订阅的对话框,方便用户编辑订阅规则"
|
||||
hint="添加订阅时显示此编辑订阅对话框"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
89
src/components/input/PathField.vue
Normal file
89
src/components/input/PathField.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import { FileItem } from '@/api/types'
|
||||
import { VTreeview } from 'vuetify/labs/VTreeview'
|
||||
|
||||
// 输入变量为默认路径
|
||||
const props = defineProps({
|
||||
root: {
|
||||
type: String,
|
||||
default: '/',
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 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,
|
||||
},
|
||||
])
|
||||
|
||||
// 拉取子目录
|
||||
async function fetchDirs(item: any) {
|
||||
return api
|
||||
.get('/filebrowser/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>
|
||||
@@ -42,7 +42,19 @@ onUnmounted(() => {
|
||||
<!-- 插件仪表板 -->
|
||||
<VHover v-else-if="!isNullOrEmptyObject(props.config)">
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props">
|
||||
<!-- 无边框 -->
|
||||
<div v-if="props.config?.attrs.border === false">
|
||||
<VCard v-bind="hover.props">
|
||||
<VCardText class="p-0">
|
||||
<DashboardRender v-for="(item, index) in props.config?.elements" :key="index" :config="item" />
|
||||
</VCardText>
|
||||
<div v-if="hover.isHovering" class="absolute right-5 top-5">
|
||||
<VIcon class="cursor-move">mdi-drag</VIcon>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
<!-- 有边框 -->
|
||||
<VCard v-else v-bind="hover.props">
|
||||
<VCardItem v-if="props.config?.attrs.border !== false">
|
||||
<template #append>
|
||||
<VIcon class="cursor-move" v-if="hover.isHovering">mdi-drag</VIcon>
|
||||
@@ -52,12 +64,9 @@ onUnmounted(() => {
|
||||
</VCardTitle>
|
||||
<VCardSubtitle v-if="props.config?.attrs?.subtitle"> {{ props.config?.attrs?.subtitle }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText :class="{ 'p-0': props.config?.attrs.border === false }">
|
||||
<VCardText>
|
||||
<DashboardRender v-for="(item, index) in props.config?.elements" :key="index" :config="item" />
|
||||
</VCardText>
|
||||
<div v-if="props.config?.attrs.border === false && hover.isHovering" class="absolute right-5 top-5">
|
||||
<VIcon class="cursor-move">mdi-drag</VIcon>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import VerticalNavSectionTitle from '@/@layouts/components/VerticalNavSectionTitle.vue'
|
||||
import VerticalNavLayout from '@layouts/components/VerticalNavLayout.vue'
|
||||
import VerticalNavLink from '@layouts/components/VerticalNavLink.vue'
|
||||
|
||||
// Components
|
||||
import Footer from '@/layouts/components/Footer.vue'
|
||||
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
|
||||
import UserNofification from '@/layouts/components/UserNotification.vue'
|
||||
@@ -11,9 +9,16 @@ import SearchBar from '@/layouts/components/SearchBar.vue'
|
||||
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
|
||||
import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
import store from '@/store'
|
||||
import { SystemNavMenus } from '@/router/menu'
|
||||
import { NavMenu } from '@/@layouts/types'
|
||||
|
||||
// 从Vuex Store中获取superuser信息
|
||||
const superUser = store.state.auth.superUser
|
||||
|
||||
// 根据分类获取菜单列表
|
||||
const getMenuList = (header: string) => {
|
||||
return SystemNavMenus.filter((item: NavMenu) => item.header === header && (!item.admin || superUser))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -25,113 +30,44 @@ const superUser = store.state.auth.superUser
|
||||
<IconBtn class="ms-n2 d-lg-none" @click="toggleVerticalOverlayNavActive(true)">
|
||||
<VIcon icon="mdi-menu" />
|
||||
</IconBtn>
|
||||
|
||||
<!-- 👉 Search Bar -->
|
||||
<SearchBar />
|
||||
|
||||
<!-- 👉 Spacer -->
|
||||
<VSpacer />
|
||||
|
||||
<!-- 👉 Shortcuts -->
|
||||
<ShortcutBar v-if="superUser" />
|
||||
|
||||
<!-- 👉 Theme -->
|
||||
<NavbarThemeSwitcher />
|
||||
|
||||
<!-- 👉 Notification -->
|
||||
<UserNofification />
|
||||
|
||||
<!-- 👉 UserProfile -->
|
||||
<UserProfile />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #vertical-nav-content>
|
||||
<VerticalNavLink
|
||||
:item="{
|
||||
title: '仪表板',
|
||||
icon: 'mdi-home-outline',
|
||||
to: '/dashboard',
|
||||
}"
|
||||
/>
|
||||
|
||||
<VerticalNavLink v-for="item in getMenuList('开始')" :item="item" />
|
||||
<!-- 👉 发现 -->
|
||||
<VerticalNavSectionTitle
|
||||
:item="{
|
||||
heading: '发现',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
:item="{
|
||||
title: '推荐',
|
||||
icon: 'mdi-table-star',
|
||||
to: '/ranking',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
:item="{
|
||||
title: '资源搜索',
|
||||
icon: 'mdi-magnify',
|
||||
to: '/resource',
|
||||
}"
|
||||
/>
|
||||
|
||||
<VerticalNavLink v-for="item in getMenuList('发现')" :item="item" />
|
||||
<!-- 👉 订阅 -->
|
||||
<VerticalNavSectionTitle
|
||||
:item="{
|
||||
heading: '订阅',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
:item="{
|
||||
title: '电影',
|
||||
icon: 'mdi-movie-check-outline',
|
||||
to: '/subscribe-movie',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
:item="{
|
||||
title: '电视剧',
|
||||
icon: 'mdi-television-classic',
|
||||
to: '/subscribe-tv',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
:item="{
|
||||
title: '日历',
|
||||
icon: 'mdi-calendar',
|
||||
to: '/calendar',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink v-for="item in getMenuList('订阅')" :item="item" />
|
||||
<!-- 👉 整理 -->
|
||||
<VerticalNavSectionTitle
|
||||
:item="{
|
||||
heading: '整理',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
:item="{
|
||||
title: '正在下载',
|
||||
icon: 'mdi-download-outline',
|
||||
to: '/downloading',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '历史记录',
|
||||
icon: 'mdi-history',
|
||||
to: '/history',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '文件管理',
|
||||
icon: 'mdi-folder-multiple-outline',
|
||||
to: '/filemanager',
|
||||
}"
|
||||
/>
|
||||
|
||||
<VerticalNavLink v-for="item in getMenuList('整理')" :item="item" />
|
||||
<!-- 👉 系统 -->
|
||||
<VerticalNavSectionTitle
|
||||
v-if="superUser"
|
||||
@@ -139,37 +75,12 @@ const superUser = store.state.auth.superUser
|
||||
heading: '系统',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '插件',
|
||||
icon: 'mdi-apps',
|
||||
to: '/plugins',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '站点管理',
|
||||
icon: 'mdi-web',
|
||||
to: '/site',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '设定',
|
||||
icon: 'mdi-cog',
|
||||
to: '/setting',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink v-for="item in getMenuList('系统')" :item="item" />
|
||||
</template>
|
||||
|
||||
<template #after-vertical-nav-items />
|
||||
|
||||
<!-- 👉 Pages -->
|
||||
<slot />
|
||||
|
||||
<!-- 👉 Footer -->
|
||||
<template #footer>
|
||||
<Footer />
|
||||
|
||||
@@ -1,109 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
// 路由
|
||||
const router = useRouter()
|
||||
import * as Mousetrap from 'mousetrap'
|
||||
import SearchBarView from '@/views/system/SearchBarView.vue'
|
||||
|
||||
// 搜索词
|
||||
const searchWord = ref(null)
|
||||
|
||||
// 搜索弹窗
|
||||
const searchDialog = ref(false)
|
||||
|
||||
// ref
|
||||
const searchWordInput = ref<HTMLElement | null>(null)
|
||||
|
||||
// 当前的搜索类型 media/person
|
||||
const searchType = ref('media')
|
||||
|
||||
// 搜索提示词列表
|
||||
const searchHintList = ref<string[]>([])
|
||||
|
||||
// Search
|
||||
function search() {
|
||||
if (!searchWord.value) return
|
||||
if (!searchHintList.value.includes(searchWord.value)) searchHintList.value.push(searchWord.value)
|
||||
searchDialog.value = false
|
||||
router.push({
|
||||
path: '/browse/media/search',
|
||||
query: {
|
||||
title: searchWord.value,
|
||||
type: searchType.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 切换搜索类型
|
||||
function switchSearchType() {
|
||||
searchType.value = searchType.value === 'media' ? 'person' : 'media'
|
||||
}
|
||||
// 注册快捷键
|
||||
Mousetrap.bind(['command+k', 'ctrl+k'], openSearchDialog)
|
||||
|
||||
// 打开搜索弹窗
|
||||
function openSearchDialog() {
|
||||
searchDialog.value = true
|
||||
nextTick(() => {
|
||||
searchWordInput.value?.focus()
|
||||
})
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 👉 Search Button -->
|
||||
<div class="d-flex align-center cursor-pointer" style="user-select: none">
|
||||
<VDialog v-model="searchDialog" max-width="50rem" transition="dialog-top-transition">
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="搜索">
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCombobox
|
||||
ref="searchWordInput"
|
||||
v-model="searchWord"
|
||||
:items="searchHintList"
|
||||
:prepend-inner-icon="searchType == 'person' ? 'mdi-account' : 'mdi-movie'"
|
||||
:label="searchType == 'person' ? '搜索演员' : '搜索电影、电视剧'"
|
||||
@keydown.enter="search"
|
||||
@click:prepend-inner="switchSearchType"
|
||||
clearable
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="tonal" @click="search"> 搜索 </VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</div>
|
||||
<!-- 👉 Search Icon -->
|
||||
<IconBtn class="d-md-none" @click="openSearchDialog">
|
||||
<VIcon icon="mdi-magnify" />
|
||||
</IconBtn>
|
||||
<!-- 👉 Search Textfield -->
|
||||
<span class="w-full me-3">
|
||||
<VCombobox
|
||||
key="search_navbar"
|
||||
v-model="searchWord"
|
||||
:items="searchHintList"
|
||||
class="d-none d-md-block text-disabled search-box"
|
||||
density="compact"
|
||||
variant="solo"
|
||||
:prepend-inner-icon="searchType == 'person' ? 'mdi-account' : 'mdi-movie'"
|
||||
:label="searchType == 'person' ? '搜索演员' : '搜索电影、电视剧'"
|
||||
append-inner-icon="mdi-magnify"
|
||||
single-line
|
||||
hide-details
|
||||
flat
|
||||
rounded
|
||||
@click:append-inner="search"
|
||||
@click:prepend-inner="switchSearchType"
|
||||
@keydown.enter="search"
|
||||
/>
|
||||
</span>
|
||||
<div class="d-flex align-center cursor-pointer ms-lg-n2" style="user-select: none">
|
||||
<IconBtn @click="openSearchDialog">
|
||||
<VIcon icon="ri-search-line" />
|
||||
</IconBtn>
|
||||
<span class="d-none d-md-flex align-center text-disabled ms-2" @click="openSearchDialog">
|
||||
<span class="me-3">搜索</span>
|
||||
<span class="meta-key">⌘K</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 搜索弹窗 -->
|
||||
<SearchBarView v-model="searchDialog" v-if="searchDialog" @close="searchDialog = false" />
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.search-box div.v-input__control div[role='textbox'] {
|
||||
border: 1px solid rgb(var(--v-theme-background));
|
||||
<style type="scss" scoped>
|
||||
.meta-key {
|
||||
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
border-radius: 6px;
|
||||
block-size: 1.75rem;
|
||||
padding-block: 0.1rem;
|
||||
padding-inline: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,7 @@ import MessageView from '@/views/system/MessageView.vue'
|
||||
import store from '@/store'
|
||||
import api from '@/api'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { getQueryValue } from '@/@core/utils'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
@@ -75,6 +76,29 @@ async function sendMessage() {
|
||||
|
||||
onMounted(() => {
|
||||
scrollMessageToEnd()
|
||||
const shortcut = getQueryValue('shortcut')
|
||||
if (shortcut) {
|
||||
switch (shortcut) {
|
||||
case 'nameTest':
|
||||
nameTestDialog.value = true
|
||||
break
|
||||
case 'netTest':
|
||||
netTestDialog.value = true
|
||||
break
|
||||
case 'logging':
|
||||
loggingDialog.value = true
|
||||
break
|
||||
case 'ruleTest':
|
||||
ruleTestDialog.value = true
|
||||
break
|
||||
case 'systemTest':
|
||||
systemTestDialog.value = true
|
||||
break
|
||||
case 'message':
|
||||
messageDialog.value = true
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ const avatar = store.state.auth.avatar
|
||||
<VDivider class="my-2" />
|
||||
|
||||
<!-- 👉 Profile -->
|
||||
<VListItem v-if="superUser" link to="setting">
|
||||
<VListItem v-if="superUser" link @click="router.push('/setting?tab=account')">
|
||||
<template #prepend>
|
||||
<VIcon class="me-2" icon="mdi-account-outline" size="22" />
|
||||
</template>
|
||||
|
||||
38
src/main.ts
38
src/main.ts
@@ -1,22 +1,19 @@
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
import { createApp } from 'vue'
|
||||
import '@/@iconify/icons-bundle'
|
||||
import ToastPlugin from 'vue-toast-notification'
|
||||
import VuetifyUseDialog from 'vuetify-use-dialog'
|
||||
import '@/@core/utils/compatibility'
|
||||
import './ace-config'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
import { removeEl } from './@core/utils/dom'
|
||||
import '@/@iconify/icons-bundle'
|
||||
import '@/plugins/webfontloader'
|
||||
import App from '@/App.vue'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
import { loadFonts } from '@/plugins/webfontloader'
|
||||
import router from '@/router'
|
||||
import store from '@/store'
|
||||
import '@core/scss/template/index.scss'
|
||||
import '@layouts/styles/index.scss'
|
||||
import '@styles/styles.scss'
|
||||
import 'vue-toast-notification/dist/theme-bootstrap.css'
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
import { createApp } from 'vue'
|
||||
import { removeEl } from './@core/utils/dom'
|
||||
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
|
||||
import 'vue3-perfect-scrollbar/style.css'
|
||||
import { VTreeview } from 'vuetify/labs/VTreeview'
|
||||
import ToastPlugin from 'vue-toast-notification'
|
||||
import VuetifyUseDialog from 'vuetify-use-dialog'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
|
||||
import MediaCard from './components/cards/MediaCard.vue'
|
||||
import PosterCard from './components/cards/PosterCard.vue'
|
||||
@@ -25,13 +22,12 @@ import PersonCard from './components/cards/PersonCard.vue'
|
||||
import MediaInfoCard from './components/cards/MediaInfoCard.vue'
|
||||
import TorrentCard from './components/cards/TorrentCard.vue'
|
||||
import MediaIdSelector from './components/misc/MediaIdSelector.vue'
|
||||
import { fixArrayAt } from '@/@core/utils/compatibility'
|
||||
|
||||
// 修复低版本Safari等浏览器数组不支持at函数的问题
|
||||
fixArrayAt()
|
||||
|
||||
// 加载字体
|
||||
loadFonts()
|
||||
import PathField from './components/input/PathField.vue'
|
||||
import '@core/scss/template/index.scss'
|
||||
import '@layouts/styles/index.scss'
|
||||
import '@styles/styles.scss'
|
||||
import 'vue-toast-notification/dist/theme-bootstrap.css'
|
||||
import 'vue3-perfect-scrollbar/style.css'
|
||||
|
||||
// 创建Vue实例
|
||||
const app = createApp(App)
|
||||
@@ -48,6 +44,8 @@ app
|
||||
.component('VMediaInfoCard', MediaInfoCard)
|
||||
.component('VTorrentCard', TorrentCard)
|
||||
.component('VMediaIdSelector', MediaIdSelector)
|
||||
.component('VTreeview', VTreeview)
|
||||
.component('VPathField', PathField)
|
||||
|
||||
// 注册插件
|
||||
app
|
||||
|
||||
@@ -9,6 +9,20 @@ import DashboardElement from '@/components/misc/DashboardElement.vue'
|
||||
// 从Vuex Store中获取superuser信息
|
||||
const superUser = store.state.auth.superUser
|
||||
|
||||
// 是否拉升高度
|
||||
const isElevated = ref(true)
|
||||
|
||||
// 计算属性,控制是否拉升高度
|
||||
const elevatedConf = controlledComputed(
|
||||
() => isElevated.value,
|
||||
() => ({
|
||||
class: { 'match-height': isElevated.value },
|
||||
}),
|
||||
)
|
||||
|
||||
// 所有组件刷新定时器的句柄
|
||||
const refreshTimers = ref<{ [key: string]: NodeJS.Timeout }>({})
|
||||
|
||||
// 仪表板启用配置
|
||||
const enableConfig = ref<{ [key: string]: boolean }>({
|
||||
mediaStatistic: true,
|
||||
@@ -24,13 +38,14 @@ const enableConfig = ref<{ [key: string]: boolean }>({
|
||||
})
|
||||
|
||||
// 仪表板顺序配置
|
||||
const orderConfig = ref<{ id: string }[]>([])
|
||||
const orderConfig = ref<{ id: string; key: string }[]>([])
|
||||
|
||||
// 仪表板配置
|
||||
const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'storage',
|
||||
name: '存储空间',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 4 },
|
||||
elements: [],
|
||||
@@ -38,6 +53,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'mediaStatistic',
|
||||
name: '媒体统计',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 8 },
|
||||
elements: [],
|
||||
@@ -45,6 +61,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'weeklyOverview',
|
||||
name: '最近入库',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 4 },
|
||||
elements: [],
|
||||
@@ -52,6 +69,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'speed',
|
||||
name: '实时速率',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 4 },
|
||||
elements: [],
|
||||
@@ -59,6 +77,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'scheduler',
|
||||
name: '后台任务',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 4 },
|
||||
elements: [],
|
||||
@@ -66,6 +85,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'cpu',
|
||||
name: 'CPU',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 6 },
|
||||
elements: [],
|
||||
@@ -73,6 +93,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'memory',
|
||||
name: '内存',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12, md: 6 },
|
||||
elements: [],
|
||||
@@ -80,6 +101,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'library',
|
||||
name: '我的媒体库',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12 },
|
||||
elements: [],
|
||||
@@ -87,6 +109,7 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'playing',
|
||||
name: '继续观看',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12 },
|
||||
elements: [],
|
||||
@@ -94,14 +117,15 @@ const dashboardConfigs = ref<DashboardItem[]>([
|
||||
{
|
||||
id: 'latest',
|
||||
name: '最近添加',
|
||||
key: '',
|
||||
attrs: {},
|
||||
cols: { cols: 12 },
|
||||
elements: [],
|
||||
},
|
||||
])
|
||||
|
||||
// 有仪表板的插件
|
||||
const dashboardPlugins = ref<any[]>([])
|
||||
// 插件的仪表板元信息
|
||||
const pluginDashboardMeta = ref<any[]>([])
|
||||
|
||||
// 插件仪表板的刷新状态
|
||||
const pluginDashboardRefreshStatus = ref<{ [key: string]: boolean }>({})
|
||||
@@ -133,6 +157,9 @@ async function loadDashboardConfig() {
|
||||
localStorage.setItem('MP_DASHBOARD_ORDER', JSON.stringify(orderConfig.value))
|
||||
}
|
||||
}
|
||||
// 是否拉升高度
|
||||
const local_elevated = localStorage.getItem('MP_DASHBOARD_ELEVATED')
|
||||
if (local_elevated) isElevated.value = local_elevated === 'true'
|
||||
// 排序
|
||||
if (orderConfig.value) {
|
||||
sortDashboardConfigs()
|
||||
@@ -142,28 +169,34 @@ async function loadDashboardConfig() {
|
||||
// 按order的顺序对dashboardConfigs进行排序
|
||||
function sortDashboardConfigs() {
|
||||
dashboardConfigs.value.sort((a, b) => {
|
||||
const aIndex = orderConfig.value.findIndex((item: { id: string }) => item.id === a.id)
|
||||
const bIndex = orderConfig.value.findIndex((item: { id: string }) => item.id === b.id)
|
||||
const aIndex = orderConfig.value.findIndex(
|
||||
(item: { id: string; key: string }) => item.id === a.id && item.key === a.key,
|
||||
)
|
||||
const bIndex = orderConfig.value.findIndex(
|
||||
(item: { id: string; key: string }) => item.id === b.id && item.key === b.key,
|
||||
)
|
||||
return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex)
|
||||
})
|
||||
}
|
||||
|
||||
// 设置项目
|
||||
function saveDashboardConfig() {
|
||||
async function saveDashboardConfig() {
|
||||
// 启用配置
|
||||
const data = JSON.stringify(enableConfig.value)
|
||||
localStorage.setItem('MP_DASHBOARD', data)
|
||||
// 顺序配置,从dashboardConfigs中提取
|
||||
const order = JSON.stringify(dashboardConfigs.value.map(item => ({ id: item.id })))
|
||||
const order = JSON.stringify(dashboardConfigs.value.map(item => ({ id: item.id, key: item.key })))
|
||||
localStorage.setItem('MP_DASHBOARD_ORDER', order)
|
||||
// 是否拉升高度
|
||||
localStorage.setItem('MP_DASHBOARD_ELEVATED', isElevated.value.toString())
|
||||
// 保存到服务端
|
||||
try {
|
||||
api.post('/user/config/Dashboard', data, {
|
||||
await api.post('/user/config/Dashboard', data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
api.post('/user/config/DashboardOrder', order, {
|
||||
await api.post('/user/config/DashboardOrder', order, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -172,22 +205,29 @@ function saveDashboardConfig() {
|
||||
console.error(error)
|
||||
}
|
||||
// 保存后重新获取插件仪表板
|
||||
getDashboardPlugins()
|
||||
getPluginDashboardMeta()
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
// 调用API获取有仪表板的插件
|
||||
async function getDashboardPlugins() {
|
||||
// 只有超级用户才能获取插件仪表板
|
||||
// 构造插件仪表板主ID
|
||||
function buildPluginDashboardId(plugin_id: string, key: string) {
|
||||
if (!key) return plugin_id
|
||||
return plugin_id + ':' + key
|
||||
}
|
||||
|
||||
// 调用API获取所有插件的仪表板元信息
|
||||
async function getPluginDashboardMeta() {
|
||||
// 只有超级用户才能获取
|
||||
if (!superUser) return
|
||||
pluginDashboardMeta.value = await api.get('/plugin/dashboard/meta')
|
||||
try {
|
||||
dashboardPlugins.value = await api.get('/plugin/dashboards')
|
||||
if (!isNullOrEmptyObject(dashboardPlugins.value)) {
|
||||
if (!isNullOrEmptyObject(pluginDashboardMeta.value)) {
|
||||
// 下载插件仪表板配置
|
||||
dashboardPlugins.value.forEach(async (plugin: { id: string }) => {
|
||||
pluginDashboardMeta.value.forEach(async (pluginDashboard: { id: string; key: string }) => {
|
||||
const pluginDashboardId = buildPluginDashboardId(pluginDashboard.id, pluginDashboard.key)
|
||||
// 初始化插件仪表板的刷新状态
|
||||
pluginDashboardRefreshStatus.value[plugin.id] = true
|
||||
await getPluginDashboard(plugin.id)
|
||||
pluginDashboardRefreshStatus.value[pluginDashboardId] = true
|
||||
await getPluginDashboard(pluginDashboard.id, pluginDashboard.key)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -196,12 +236,20 @@ async function getDashboardPlugins() {
|
||||
}
|
||||
|
||||
// 获取一个插件的仪表板配置项
|
||||
async function getPluginDashboard(id: string) {
|
||||
async function getPluginDashboard(id: string, key: string) {
|
||||
try {
|
||||
api.get(`/plugin/dashboard/${id}`).then((res: any) => {
|
||||
const url = key ? `/plugin/dashboard/${id}/${key}` : `/plugin/dashboard/${id}`
|
||||
api.get(url).then((res: any) => {
|
||||
if (res) {
|
||||
// 名称替换为元信息的名称
|
||||
const meta = pluginDashboardMeta.value.find(
|
||||
(item: { id: string; key: string }) => item.id === id && item.key === key,
|
||||
)
|
||||
if (meta) res.name = meta.name
|
||||
// 保存到仪表板配置中,如果已经存在则替换
|
||||
const index = dashboardConfigs.value.findIndex((item: { id: string }) => item.id === id)
|
||||
const index = dashboardConfigs.value.findIndex(
|
||||
(item: { id: string; key: string }) => item.id === id && item.key === key,
|
||||
)
|
||||
if (index !== -1) {
|
||||
dashboardConfigs.value[index] = res
|
||||
} else {
|
||||
@@ -209,11 +257,22 @@ async function getPluginDashboard(id: string) {
|
||||
// 排序
|
||||
sortDashboardConfigs()
|
||||
}
|
||||
const pluginDashboardId = buildPluginDashboardId(id, key)
|
||||
// 定时刷新
|
||||
if (res.attrs?.refresh) {
|
||||
setTimeout(() => {
|
||||
pluginDashboardRefreshStatus.value[id] && getPluginDashboard(id)
|
||||
if (
|
||||
res.attrs?.refresh &&
|
||||
pluginDashboardRefreshStatus.value[pluginDashboardId] &&
|
||||
enableConfig.value[pluginDashboardId]
|
||||
) {
|
||||
// 清除之前的定时器
|
||||
if (refreshTimers.value[pluginDashboardId]) {
|
||||
clearTimeout(refreshTimers.value[pluginDashboardId])
|
||||
}
|
||||
// 设置新的定时器
|
||||
let timer = setTimeout(() => {
|
||||
getPluginDashboard(id, key)
|
||||
}, res.attrs.refresh * 1000)
|
||||
refreshTimers.value[pluginDashboardId] = timer
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -230,7 +289,7 @@ function dragOrderEnd() {
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await loadDashboardConfig()
|
||||
getDashboardPlugins()
|
||||
getPluginDashboardMeta()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -242,17 +301,20 @@ onBeforeMount(async () => {
|
||||
handle=".cursor-move"
|
||||
item-key="id"
|
||||
tag="VRow"
|
||||
:component-data="{ 'class': 'match-height' }"
|
||||
:component-data="elevatedConf"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<VCol v-if="enableConfig[element.id] && element.cols" v-bind:="element.cols">
|
||||
<DashboardElement :config="element" v-model:refreshStatus="pluginDashboardRefreshStatus[element.id]" />
|
||||
<VCol v-if="enableConfig[buildPluginDashboardId(element.id, element.key)] && element.cols" v-bind:="element.cols">
|
||||
<DashboardElement
|
||||
:config="element"
|
||||
v-model:refreshStatus="pluginDashboardRefreshStatus[buildPluginDashboardId(element.id, element.key)]"
|
||||
/>
|
||||
</VCol>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<VFab icon="mdi-view-dashboard-edit" location="bottom end" size="x-large" fixed app appear @click="dialog = true" />
|
||||
<VFab icon="mdi-view-dashboard-edit" location="bottom" size="x-large" fixed app appear @click="dialog = true" />
|
||||
|
||||
<!-- 弹窗,根据配置生成选项 -->
|
||||
<VDialog v-model="dialog" max-width="35rem" scrollable>
|
||||
@@ -263,8 +325,22 @@ onBeforeMount(async () => {
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol v-for="item in dashboardConfigs" :key="item.id" cols="6" md="4" sm="4">
|
||||
<VCheckbox v-model="enableConfig[item.id]" :label="item.attrs?.title ?? item.name" />
|
||||
<VCol
|
||||
v-for="item in dashboardConfigs"
|
||||
:key="buildPluginDashboardId(item.id, item.key)"
|
||||
cols="6"
|
||||
md="4"
|
||||
sm="4"
|
||||
>
|
||||
<VCheckbox
|
||||
v-model="enableConfig[buildPluginDashboardId(item.id, item.key)]"
|
||||
:label="item.attrs?.title ?? item.name"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch v-model="isElevated" label="自适应组件高度" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
@@ -8,6 +8,7 @@ import router from '@/router'
|
||||
import logo from '@images/logo.png'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||
import { urlBase64ToUint8Array } from '@/@core/utils/navigator'
|
||||
|
||||
const { global: globalTheme } = useTheme()
|
||||
|
||||
@@ -89,11 +90,39 @@ async function setTheme() {
|
||||
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
|
||||
}
|
||||
|
||||
// 订阅推送通知
|
||||
async function subscribeForPushNotifications() {
|
||||
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||||
const registration = await navigator.serviceWorker.ready
|
||||
// 获取订阅信息
|
||||
const subscription = await registration.pushManager.getSubscription().then(function (subscription) {
|
||||
if (subscription === null) {
|
||||
const convertedVapidKey = urlBase64ToUint8Array(import.meta.env.VITE_PUBLIC_VAPID_KEY)
|
||||
return registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: convertedVapidKey,
|
||||
})
|
||||
} else {
|
||||
return subscription
|
||||
}
|
||||
})
|
||||
// 发送订阅请求
|
||||
try {
|
||||
await api.post('/message/webpush/subscribe', subscription)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 登录后处理
|
||||
async function afterLogin() {
|
||||
// 生效主题配置
|
||||
await setTheme()
|
||||
// 跳转到首页或回原始页面
|
||||
router.push(store.state.auth.originalPath ?? '/')
|
||||
// 订阅推送通知
|
||||
await subscribeForPushNotifications()
|
||||
}
|
||||
|
||||
// 登录获取token事件
|
||||
|
||||
@@ -54,7 +54,7 @@ function startLoadingProgress() {
|
||||
progressEventSource.value = new EventSource(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/search?token=${token}`,
|
||||
)
|
||||
progressEventSource.value.onmessage = (event) => {
|
||||
progressEventSource.value.onmessage = event => {
|
||||
const progress = JSON.parse(event.data)
|
||||
if (progress) {
|
||||
progressText.value = progress.text
|
||||
@@ -65,7 +65,7 @@ function startLoadingProgress() {
|
||||
|
||||
// 停止监听加载进度
|
||||
function stopLoadingProgress() {
|
||||
progressEventSource.value?.close()
|
||||
if (progressEventSource.value) progressEventSource.value?.close()
|
||||
}
|
||||
|
||||
// 设置视图类型
|
||||
@@ -80,34 +80,38 @@ async function fetchData() {
|
||||
if (!keyword) {
|
||||
// 查询上次搜索结果
|
||||
dataList.value = await api.get('search/last')
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
startLoadingProgress()
|
||||
let result: { [key: string]: any }
|
||||
// 优先按TMDBID精确查询
|
||||
if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:') || keyword?.startsWith('bangumi:')) {
|
||||
const result: {[key: string]: any} = await api.get(`search/media/${keyword}`, {
|
||||
result = await api.get(`search/media/${keyword}`, {
|
||||
params: {
|
||||
mtype: type,
|
||||
area,
|
||||
season,
|
||||
},
|
||||
})
|
||||
if (result.success){
|
||||
dataList.value = result.data
|
||||
} else {
|
||||
errorDescription.value = result.message
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// 按标题模糊查询
|
||||
dataList.value = await api.get(`search/title/${keyword}`)
|
||||
result = await api.get(`search/title`, {
|
||||
params: {
|
||||
keyword,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (result && result.success) {
|
||||
dataList.value = result.data
|
||||
} else if (result && result.message) {
|
||||
errorDescription.value = result.message
|
||||
}
|
||||
stopLoadingProgress()
|
||||
// 从浏览器历史中删除当前搜索
|
||||
window.history.replaceState(null, '', window.location.pathname)
|
||||
}
|
||||
// 标记已刷新
|
||||
isRefreshed.value = true
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
@@ -117,49 +121,34 @@ async function fetchData() {
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
// 卸载时停止加载进度
|
||||
onUnmounted(() => {
|
||||
stopLoadingProgress()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadingBanner
|
||||
v-if="!isRefreshed"
|
||||
class="mt-12"
|
||||
:text="progressText"
|
||||
:progress="progressValue"
|
||||
/>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" :text="progressText" :progress="progressValue" />
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
:error-title="errorTitle"
|
||||
:error-description="errorDescription"
|
||||
/>
|
||||
<div v-if="dataList.length > 0">
|
||||
<TorrentRowListView
|
||||
v-if="viewType === 'list'"
|
||||
:items="dataList"
|
||||
/>
|
||||
<TorrentCardListView
|
||||
v-else
|
||||
:items="dataList"
|
||||
/>
|
||||
<TorrentRowListView v-if="viewType === 'list'" :items="dataList" />
|
||||
<TorrentCardListView v-else :items="dataList" />
|
||||
</div>
|
||||
<!-- 视图切换 -->
|
||||
<VFab
|
||||
v-if="viewType === 'list'"
|
||||
icon="mdi-view-grid"
|
||||
location="bottom end"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="setViewType('card')"
|
||||
/>
|
||||
<VFab
|
||||
v-else
|
||||
icon="mdi-view-list"
|
||||
location="bottom end"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="setViewType('list')"
|
||||
/>
|
||||
<VFab v-else icon="mdi-view-list" location="bottom" size="x-large" fixed app appear @click="setViewType('list')" />
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRoute } from 'vue-router'
|
||||
import router from '@/router'
|
||||
import AccountSettingAccount from '@/views/setting/AccountSettingAccount.vue'
|
||||
import AccountSettingNotification from '@/views/setting/AccountSettingNotification.vue'
|
||||
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
|
||||
@@ -9,65 +10,28 @@ import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
|
||||
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
|
||||
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
|
||||
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
|
||||
import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue'
|
||||
import { SettingTabs } from '@/router/menu'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const activeTab = ref(route.params.tab)
|
||||
const activeTab = ref(route.query.tab)
|
||||
|
||||
// tabs
|
||||
const tabs = [
|
||||
{
|
||||
title: '用户',
|
||||
icon: 'mdi-account',
|
||||
tab: 'account',
|
||||
},
|
||||
{
|
||||
title: '系统',
|
||||
icon: 'mdi-cog',
|
||||
tab: 'system',
|
||||
},
|
||||
{
|
||||
title: '站点',
|
||||
icon: 'mdi-web',
|
||||
tab: 'site',
|
||||
},
|
||||
{
|
||||
title: '搜索',
|
||||
icon: 'mdi-magnify',
|
||||
tab: 'search',
|
||||
},
|
||||
{
|
||||
title: '订阅',
|
||||
icon: 'mdi-rss',
|
||||
tab: 'subscribe',
|
||||
},
|
||||
{
|
||||
title: '服务',
|
||||
icon: 'mdi-list-box',
|
||||
tab: 'service',
|
||||
},
|
||||
{
|
||||
title: '通知',
|
||||
icon: 'mdi-bell',
|
||||
tab: 'notification',
|
||||
},
|
||||
{
|
||||
title: '词表',
|
||||
icon: 'mdi-file-word-box',
|
||||
tab: 'words',
|
||||
},
|
||||
{
|
||||
title: '关于',
|
||||
icon: 'mdi-information',
|
||||
tab: 'about',
|
||||
},
|
||||
]
|
||||
function jumpTab(tab: string) {
|
||||
router.push('/setting?tab=' + tab)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VTabs v-model="activeTab" show-arrows class="v-tabs-pill">
|
||||
<VTab v-for="item in tabs" :key="item.icon" :value="item.tab">
|
||||
<VTab
|
||||
v-for="item in SettingTabs"
|
||||
:key="item.icon"
|
||||
:value="item.tab"
|
||||
@click="jumpTab(item.tab)"
|
||||
selected-class="v-slide-group-item--active v-tab--selected"
|
||||
>
|
||||
<VIcon size="20" start :icon="item.icon" />
|
||||
{{ item.title }}
|
||||
</VTab>
|
||||
@@ -81,13 +45,20 @@ const tabs = [
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 系统 -->
|
||||
<!-- 连接 -->
|
||||
<VWindowItem value="system">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingSystem />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 目录 -->
|
||||
<VWindowItem value="directory">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingDirectory />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 站点 -->
|
||||
<VWindowItem value="site">
|
||||
<transition name="fade-slide" appear>
|
||||
@@ -122,12 +93,14 @@ const tabs = [
|
||||
<AccountSettingNotification />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 词表 -->
|
||||
<VWindowItem value="words">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingWords />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 关于 -->
|
||||
<VWindowItem value="about">
|
||||
<transition name="fade-slide" appear>
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
|
||||
import SubscribePopularView from '@/views/subscribe/SubscribePopularView.vue'
|
||||
import router from '@/router'
|
||||
import { SubscribeMovieTabs } from '@/router/menu'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 标签页
|
||||
const tabs = [
|
||||
{
|
||||
title: '我的订阅',
|
||||
tab: 'mysub',
|
||||
},
|
||||
{
|
||||
title: '热门订阅',
|
||||
tab: 'popular',
|
||||
},
|
||||
]
|
||||
// 订阅ID参数
|
||||
const subId = ref(route.query.id as string)
|
||||
|
||||
// 当前标签
|
||||
const activeTab = ref(route.params.tab)
|
||||
const activeTab = ref(route.query.tab)
|
||||
|
||||
// 跳转tab
|
||||
function jumpTab(tab: string) {
|
||||
router.push('/subscribe-movie?tab=' + tab)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VTabs v-model="activeTab">
|
||||
<VTab v-for="item in tabs" :value="item.tab">
|
||||
<VTab v-for="item in SubscribeMovieTabs" :value="item.tab" @click="jumpTab(item.tab)">
|
||||
<span class="mx-5">{{ item.title }}</span>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
@@ -31,12 +29,12 @@ const activeTab = ref(route.params.tab)
|
||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<VWindowItem value="mysub">
|
||||
<transition name="fade-slide" appear>
|
||||
<SubscribeListView type="电影" />
|
||||
<SubscribeListView type="电影" :subid="subId" />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="popular">
|
||||
<transition name="fade-slide" appear>
|
||||
<SubscribePopularView type="电影" />
|
||||
<SubscribePopularView type="电影" :subid="subId" />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import SubscribeListView from '@/views/subscribe/SubscribeListView.vue'
|
||||
import SubscribePopularView from '@/views/subscribe/SubscribePopularView.vue'
|
||||
import router from '@/router'
|
||||
import { SubscribeTvTabs } from '@/router/menu'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 标签页
|
||||
const tabs = [
|
||||
{
|
||||
title: '我的订阅',
|
||||
tab: 'mysub',
|
||||
},
|
||||
{
|
||||
title: '热门订阅',
|
||||
tab: 'popular',
|
||||
},
|
||||
]
|
||||
const activeTab = ref(route.query.tab)
|
||||
|
||||
// 当前标签
|
||||
const activeTab = ref(route.params.tab)
|
||||
// 订阅ID参数
|
||||
const subId = ref(route.query.id as string)
|
||||
|
||||
// 跳转tab
|
||||
function jumpTab(tab: string) {
|
||||
router.push('/subscribe-tv?tab=' + tab)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VTabs v-model="activeTab">
|
||||
<VTab v-for="item in tabs" :value="item.tab">
|
||||
<VTab v-for="item in SubscribeTvTabs" :value="item.tab" @click="jumpTab(item.tab)">
|
||||
<span class="mx-5">{{ item.title }}</span>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
@@ -31,12 +28,12 @@ const activeTab = ref(route.params.tab)
|
||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<VWindowItem value="mysub">
|
||||
<transition name="fade-slide" appear>
|
||||
<SubscribeListView type="电视剧" />
|
||||
<SubscribeListView type="电视剧" :subid="subId" />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="popular">
|
||||
<transition name="fade-slide" appear>
|
||||
<SubscribePopularView type="电视剧" />
|
||||
<SubscribePopularView type="电视剧" :subid="subId" />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
* webfontloader documentation: https://github.com/typekit/webfontloader
|
||||
*/
|
||||
|
||||
export async function loadFonts() {
|
||||
const webFontLoader = await import(/* webpackChunkName: "webfontloader" */'webfontloader')
|
||||
;(async function loadFonts() {
|
||||
const webFontLoader = await import(/* webpackChunkName: "webfontloader" */ 'webfontloader')
|
||||
|
||||
webFontLoader.load({
|
||||
google: {
|
||||
families: ['Inter:100,200,300,400,500,600,700&display=swap'],
|
||||
},
|
||||
})
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -10,8 +10,7 @@ const router = createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// 如果页面有缓存那么恢复其位置, 否则始终滚动到顶部
|
||||
if (to.meta.keepAlive && savedPosition)
|
||||
return savedPosition
|
||||
if (to.meta.keepAlive && savedPosition) return savedPosition
|
||||
return { top: 0 }
|
||||
},
|
||||
routes: [
|
||||
@@ -21,14 +20,14 @@ const router = createRouter({
|
||||
component: () => import('../layouts/default.vue'),
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
path: '/dashboard',
|
||||
component: () => import('../pages/dashboard.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'ranking',
|
||||
path: '/ranking',
|
||||
component: () => import('../pages/ranking.vue'),
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
@@ -36,63 +35,63 @@ const router = createRouter({
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'resource',
|
||||
path: '/resource',
|
||||
component: () => import('../pages/resource.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'subscribe-movie',
|
||||
path: '/subscribe-movie',
|
||||
component: () => import('../pages/subscribe-movie.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'subscribe-tv',
|
||||
path: '/subscribe-tv',
|
||||
component: () => import('../pages/subscribe-tv.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'calendar',
|
||||
path: '/calendar',
|
||||
component: () => import('../pages/calendar.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'downloading',
|
||||
path: '/downloading',
|
||||
component: () => import('../pages/downloading.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'history',
|
||||
path: '/history',
|
||||
component: () => import('../pages/history.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'site',
|
||||
path: '/site',
|
||||
component: () => import('../pages/site.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'plugins',
|
||||
path: '/plugins',
|
||||
component: () => import('../pages/plugin.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'setting',
|
||||
path: '/setting',
|
||||
component: () => import('../pages/setting.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
@@ -165,8 +164,7 @@ router.beforeEach((to, from, next) => {
|
||||
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
next('/login')
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
startNProgress()
|
||||
next()
|
||||
}
|
||||
|
||||
221
src/router/menu.ts
Normal file
221
src/router/menu.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
// 导般菜单
|
||||
export const SystemNavMenus = [
|
||||
{
|
||||
title: '仪表板',
|
||||
icon: 'mdi-home-outline',
|
||||
to: '/dashboard',
|
||||
header: '开始',
|
||||
admin: false,
|
||||
},
|
||||
{
|
||||
title: '推荐',
|
||||
icon: 'mdi-table-star',
|
||||
to: '/ranking',
|
||||
header: '发现',
|
||||
admin: false,
|
||||
},
|
||||
{
|
||||
title: '资源搜索',
|
||||
icon: 'mdi-magnify',
|
||||
to: '/resource',
|
||||
header: '发现',
|
||||
admin: false,
|
||||
},
|
||||
{
|
||||
title: '电影',
|
||||
icon: 'mdi-movie-roll',
|
||||
to: '/subscribe-movie?tab=mysub',
|
||||
header: '订阅',
|
||||
admin: false,
|
||||
},
|
||||
{
|
||||
title: '电视剧',
|
||||
icon: 'mdi-television-classic',
|
||||
to: '/subscribe-tv?tab=mysub',
|
||||
header: '订阅',
|
||||
admin: false,
|
||||
},
|
||||
{
|
||||
title: '日历',
|
||||
icon: 'mdi-calendar',
|
||||
to: '/calendar',
|
||||
header: '订阅',
|
||||
admin: false,
|
||||
},
|
||||
{
|
||||
title: '正在下载',
|
||||
icon: 'mdi-download-outline',
|
||||
to: '/downloading',
|
||||
header: '整理',
|
||||
admin: false,
|
||||
},
|
||||
{
|
||||
title: '历史记录',
|
||||
icon: 'mdi-history',
|
||||
to: '/history',
|
||||
header: '整理',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: '文件管理',
|
||||
icon: 'mdi-folder-multiple-outline',
|
||||
to: '/filemanager',
|
||||
header: '整理',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: '插件',
|
||||
icon: 'mdi-apps',
|
||||
to: '/plugins?tab=installed',
|
||||
header: '系统',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: '站点管理',
|
||||
icon: 'mdi-web',
|
||||
to: '/site',
|
||||
header: '系统',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
title: '设定',
|
||||
icon: 'mdi-cog',
|
||||
to: '/setting',
|
||||
header: '系统',
|
||||
admin: true,
|
||||
},
|
||||
]
|
||||
|
||||
// 常用菜单功能
|
||||
export const UserfulMenus = [
|
||||
{
|
||||
title: '搜索设置',
|
||||
icon: 'mdi-magnify',
|
||||
to: 'setting?tab=search',
|
||||
},
|
||||
{
|
||||
title: '订阅设置',
|
||||
icon: 'mdi-rss',
|
||||
to: 'setting?tab=subscribe',
|
||||
},
|
||||
{
|
||||
title: '服务',
|
||||
icon: 'mdi-list-box',
|
||||
to: 'setting?tab=service',
|
||||
},
|
||||
{
|
||||
title: '词表',
|
||||
icon: 'mdi-file-word-box',
|
||||
to: 'setting?tab=words',
|
||||
},
|
||||
{
|
||||
title: '历史记录',
|
||||
icon: 'mdi-history',
|
||||
to: 'history',
|
||||
},
|
||||
]
|
||||
|
||||
// 设定标签页
|
||||
export const SettingTabs = [
|
||||
{
|
||||
title: '用户',
|
||||
icon: 'mdi-account',
|
||||
tab: 'account',
|
||||
description: '个人信息、用户管理、修改密码、双重认证',
|
||||
},
|
||||
{
|
||||
title: '连接',
|
||||
icon: 'mdi-server-network',
|
||||
tab: 'system',
|
||||
description: '下载器(Qbittorrent、Transmission)、媒体服务器(Emby、Jellyfin、Plex)',
|
||||
},
|
||||
{
|
||||
title: '目录',
|
||||
icon: 'mdi-folder',
|
||||
tab: 'directory',
|
||||
description: '下载目录、媒体库目录、整理模式',
|
||||
},
|
||||
{
|
||||
title: '站点',
|
||||
icon: 'mdi-web',
|
||||
tab: 'site',
|
||||
description: '站点同步、下载优先规则、站点重置',
|
||||
},
|
||||
{
|
||||
title: '搜索',
|
||||
icon: 'mdi-magnify',
|
||||
tab: 'search',
|
||||
description: '媒体数据源(TheMovieDb、豆瓣、Bangumi)、搜索站点、搜索优先级、默认过滤规则',
|
||||
},
|
||||
{
|
||||
title: '订阅',
|
||||
icon: 'mdi-rss',
|
||||
tab: 'subscribe',
|
||||
description: '订阅站点、订阅模式、订阅优先级、洗版优先级、默认过滤规则',
|
||||
},
|
||||
{
|
||||
title: '服务',
|
||||
icon: 'mdi-list-box',
|
||||
tab: 'service',
|
||||
description: '定时作业',
|
||||
},
|
||||
{
|
||||
title: '通知',
|
||||
icon: 'mdi-bell',
|
||||
tab: 'notification',
|
||||
description: '通知渠道(微信、Telegram、Slack、SynologyChat、VoceChat)、消息类型',
|
||||
},
|
||||
{
|
||||
title: '词表',
|
||||
icon: 'mdi-file-word-box',
|
||||
tab: 'words',
|
||||
description: '自定义识别词、自定义制作组/字幕组、自定义占位符、文件整理屏蔽词',
|
||||
},
|
||||
{
|
||||
title: '关于',
|
||||
icon: 'mdi-information',
|
||||
tab: 'about',
|
||||
},
|
||||
]
|
||||
|
||||
// 电影订阅标签页
|
||||
export const SubscribeMovieTabs = [
|
||||
{
|
||||
title: '我的订阅',
|
||||
tab: 'mysub',
|
||||
icon: 'mdi-movie-roll',
|
||||
},
|
||||
{
|
||||
title: '热门订阅',
|
||||
tab: 'popular',
|
||||
icon: 'mdi-movie-roll',
|
||||
},
|
||||
]
|
||||
|
||||
// 电视剧订阅标签页
|
||||
export const SubscribeTvTabs = [
|
||||
{
|
||||
title: '我的订阅',
|
||||
tab: 'mysub',
|
||||
icon: 'mdi-television-classic',
|
||||
},
|
||||
{
|
||||
title: '热门订阅',
|
||||
tab: 'popular',
|
||||
icon: 'mdi-television-classic',
|
||||
},
|
||||
]
|
||||
|
||||
// 插件标签页
|
||||
export const PluginTabs = [
|
||||
{
|
||||
title: '我的插件',
|
||||
tab: 'installed',
|
||||
icon: 'mdi-puzzle',
|
||||
},
|
||||
{
|
||||
title: '插件市场',
|
||||
tab: 'market',
|
||||
icon: 'mdi-store',
|
||||
},
|
||||
]
|
||||
74
src/service-worker.ts
Normal file
74
src/service-worker.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { createHandlerBoundToURL, cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
|
||||
import { NavigationRoute, registerRoute } from 'workbox-routing'
|
||||
import { clientsClaim } from 'workbox-core'
|
||||
|
||||
declare let self: ServiceWorkerGlobalScope
|
||||
|
||||
cleanupOutdatedCaches()
|
||||
|
||||
// self.__WB_MANIFEST is default injection point
|
||||
precacheAndRoute(self.__WB_MANIFEST)
|
||||
|
||||
// to allow work offline
|
||||
registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'), { denylist: [/^\/api/] }))
|
||||
|
||||
// 通知选项
|
||||
const options = {
|
||||
icon: '/logo.png',
|
||||
vibrate: [100, 50, 100],
|
||||
actions: [{ action: 'close', title: '关闭' }],
|
||||
}
|
||||
|
||||
// 监听 push 事件,显示通知
|
||||
self.addEventListener('push', function (event) {
|
||||
console.log('notification push')
|
||||
if (!event.data) {
|
||||
return
|
||||
}
|
||||
// 解析获取推送消息
|
||||
let payload
|
||||
try {
|
||||
payload = event.data?.json()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
payload = {
|
||||
title: event.data?.text(),
|
||||
}
|
||||
}
|
||||
// 根据推送消息生成桌面通知并展现出来
|
||||
try {
|
||||
const content = {
|
||||
body: payload.body || '',
|
||||
icon: payload.icon || options.icon,
|
||||
vibrate: [100, 50, 100],
|
||||
data: { url: payload.url },
|
||||
actions: options.actions,
|
||||
}
|
||||
event.waitUntil(self.registration.showNotification(payload.title, content))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
// 安装
|
||||
self.addEventListener('install', function (e) {
|
||||
console.log('worker install')
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
// 激活
|
||||
self.addEventListener('activate', function (e) {
|
||||
console.log('worker activate')
|
||||
e.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
// 监听通知点击事件
|
||||
self.addEventListener('notificationclick', function (event) {
|
||||
console.log('notification click')
|
||||
const info = event.notification
|
||||
if (event.action === 'close') {
|
||||
info.close()
|
||||
} else if (info.data?.url) {
|
||||
event.waitUntil(self.clients.openWindow(info.data?.url))
|
||||
}
|
||||
})
|
||||
@@ -139,3 +139,52 @@
|
||||
.apexcharts-title-text {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
|
||||
}
|
||||
|
||||
.grid-site-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
|
||||
.grid-backdrop-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-torrent-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-plugin-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-downloading-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-directory-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.grid-filterrule-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.grid-subscribe-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.v-tabs:not(.v-tabs-pill).v-tabs--horizontal {
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const variableTheme = controlledComputed(
|
||||
)
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 时间序列
|
||||
const series = ref([
|
||||
|
||||
@@ -17,7 +17,7 @@ const variableTheme = controlledComputed(
|
||||
)
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 时间序列
|
||||
const series = ref([
|
||||
|
||||
@@ -10,7 +10,7 @@ const headers = ['进程ID', '进程名称', '运行时间', '内存占用']
|
||||
const processList = ref<Process[]>([])
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 调用API加载数据
|
||||
async function loadProcessList() {
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ScheduleInfo } from '@/api/types'
|
||||
const schedulerList = ref<ScheduleInfo[]>([])
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 调用API加载定时服务列表
|
||||
async function loadSchedulerList() {
|
||||
|
||||
@@ -4,7 +4,7 @@ import api from '@/api'
|
||||
import type { DownloaderInfo } from '@/api/types'
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 下载器信息
|
||||
const downloadInfo = ref<DownloaderInfo>({
|
||||
|
||||
@@ -38,9 +38,3 @@ onMounted(() => {
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -38,10 +38,3 @@ onMounted(() => {
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-backdrop-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -38,10 +38,3 @@ onMounted(() => {
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-backdrop-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -125,7 +125,5 @@ async function fetchData({ done }: { done: any }) {
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -124,9 +124,3 @@ async function fetchData({ done }: { done: any }) {
|
||||
/>
|
||||
</VInfiniteScroll>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import _ from 'lodash'
|
||||
import type { Context } from '@/api/types'
|
||||
import TorrentCard from '@/components/cards/TorrentCard.vue'
|
||||
import { useDefer } from '@/@core/utils/dom'
|
||||
|
||||
interface SearchTorrent extends Context {
|
||||
more?: Array<Context>
|
||||
@@ -71,8 +70,35 @@ function initOptions(data: Context) {
|
||||
// 对季过滤选项进行排序
|
||||
const sortSeasonFilterOptions = computed(() => {
|
||||
return seasonFilterOptions.value.sort((a, b) => {
|
||||
// 按字符串升序排序
|
||||
return a.localeCompare(b, 'zh-Hans-CN', { sensitivity: 'accent' })
|
||||
// 按季,集降序排序
|
||||
const parseSeasonEpisode = (str: string) => {
|
||||
const seasonRangeMatch = str.match(/S(\d+)(?:-S(\d+))?/)
|
||||
const episodeRangeMatch = str.match(/E(\d+)(?:-E(\d+))?/)
|
||||
return {
|
||||
seasonStart: seasonRangeMatch?.[1] ? parseInt(seasonRangeMatch[1]) : 0,
|
||||
seasonEnd: seasonRangeMatch?.[2] ? parseInt(seasonRangeMatch[2]) : 0,
|
||||
episodeStart: episodeRangeMatch?.[1] ? parseInt(episodeRangeMatch[1]) : 0,
|
||||
episodeEnd: episodeRangeMatch?.[2] ? parseInt(episodeRangeMatch[2]) : 0,
|
||||
}
|
||||
}
|
||||
const parsedA = parseSeasonEpisode(a)
|
||||
const parsedB = parseSeasonEpisode(b)
|
||||
// 先按季降序排序
|
||||
if (parsedB.seasonStart !== parsedA.seasonStart) {
|
||||
return parsedB.seasonStart - parsedA.seasonStart
|
||||
}
|
||||
if (parsedB.seasonEnd !== parsedA.seasonEnd) {
|
||||
return parsedB.seasonEnd - parsedA.seasonEnd
|
||||
}
|
||||
// 按集降序排序
|
||||
if (parsedB.episodeStart !== parsedA.episodeStart) {
|
||||
return parsedB.episodeStart - parsedA.episodeStart
|
||||
}
|
||||
if (parsedB.episodeEnd !== parsedA.episodeEnd) {
|
||||
return parsedB.episodeEnd - parsedA.episodeEnd
|
||||
}
|
||||
// 兜底
|
||||
return b.localeCompare(a)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -99,15 +125,13 @@ onMounted(() => {
|
||||
groupedDataList.value = groupMap
|
||||
})
|
||||
|
||||
let defer = (_: number) => true
|
||||
|
||||
// 计算过滤后的列表
|
||||
watchEffect(() => {
|
||||
// 清空列表
|
||||
dataList.value = []
|
||||
// 匹配过滤函数
|
||||
const match = (filter: Array<string>, value: string | undefined) =>
|
||||
filter.length === 0 || (value && filter.includes(value))
|
||||
// 匹配过滤函数,filter中有任一值包含value则返回true
|
||||
const match = (filter: Array<string>, value: string | undefined): boolean =>
|
||||
filter.length === 0 || filter.includes(value ?? '') || filter.some(v => value?.includes(v) ?? false)
|
||||
|
||||
groupedDataList.value?.forEach(value => {
|
||||
if (value.length > 0) {
|
||||
@@ -139,7 +163,6 @@ watchEffect(() => {
|
||||
}
|
||||
}
|
||||
})
|
||||
defer = useDefer(dataList.value.length)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -227,14 +250,7 @@ watchEffect(() => {
|
||||
</VCard>
|
||||
<div class="grid gap-3 grid-torrent-card items-start">
|
||||
<div v-for="(item, index) in dataList" :key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`">
|
||||
<TorrentCard v-if="defer(index)" :torrent="item" :more="item.more" />
|
||||
<TorrentCard :torrent="item" :more="item.more" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-torrent-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Context } from '@/api/types'
|
||||
import TorrentItem from '@/components/cards/TorrentItem.vue'
|
||||
import { useDefer } from '@/@core/utils/dom'
|
||||
|
||||
// 定义输入参数
|
||||
const props = defineProps({
|
||||
@@ -118,12 +117,12 @@ onMounted(() => {
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VList v-if="dataList.length === 0" lines="three" class="rounded p-0">
|
||||
<VList v-if="dataList.length === 0" lines="three" class="rounded p-0 shadow-lg">
|
||||
<VListItem>
|
||||
<VListItemTitle>没有附合当前过滤条件的资源。</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<VList v-if="dataList.length !== 0" lines="three" class="rounded p-0 torrent-list-vscroll">
|
||||
<VList v-if="dataList.length !== 0" lines="three" class="rounded p-0 torrent-list-vscroll shadow-lg">
|
||||
<VVirtualScroll :items="dataList">
|
||||
<template #default="{ item }">
|
||||
<TorrentItem :torrent="item" :key="`${item.torrent_info.title}_${item.torrent_info.site}`" />
|
||||
@@ -132,7 +131,7 @@ onMounted(() => {
|
||||
</VList>
|
||||
</VCol>
|
||||
<VCol xl="2" md="3" class="d-none d-md-block">
|
||||
<VList lines="one" class="rounded torrent-list-vscroll">
|
||||
<VList lines="one" class="rounded torrent-list-vscroll shadow-lg">
|
||||
<VListSubheader> 排序 </VListSubheader>
|
||||
<VListItem>
|
||||
<VChipGroup column v-model="sortField">
|
||||
@@ -265,7 +264,7 @@ onMounted(() => {
|
||||
|
||||
@media (width <= 768px) {
|
||||
.orrent-list-vscroll {
|
||||
block-size: calc(100vh - 9rem);
|
||||
block-size: calc(100vh - 10rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,30 +8,19 @@ import PluginCard from '@/components/cards/PluginCard.vue'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { useDefer } from '@/@core/utils/dom'
|
||||
import router from '@/router'
|
||||
import { PluginTabs } from '@/router/menu'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 延迟加载
|
||||
let deferApp = (_: number) => true
|
||||
|
||||
// 当前标签
|
||||
const activeTab = ref(route.params.tab)
|
||||
const activeTab = ref(route.query.tab)
|
||||
|
||||
// 标签页
|
||||
const tabs = [
|
||||
{
|
||||
title: '我的插件',
|
||||
tab: 'myplugin',
|
||||
},
|
||||
{
|
||||
title: '插件市场',
|
||||
tab: 'pluginmarket',
|
||||
},
|
||||
]
|
||||
// 插件ID参数
|
||||
const pluginId = ref(route.query.id)
|
||||
|
||||
// 当前排序字段
|
||||
const activeSort = ref(null)
|
||||
@@ -287,8 +276,6 @@ const sortedUninstalledList = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
deferApp = useDefer(ret_list.length)
|
||||
|
||||
if (isNullOrEmptyObject(PluginStatistics.value)) return ret_list
|
||||
// 数据排序
|
||||
if (!activeSort.value || activeSort.value === 'count') {
|
||||
@@ -320,24 +307,36 @@ function handleRepoUrl(url: string | undefined) {
|
||||
return url.replace('https://github.com/', '').replace('https://raw.githubusercontent.com/', '')
|
||||
}
|
||||
|
||||
// 跳转tab
|
||||
function jumpTab(tab: string) {
|
||||
router.push('/plugins?tab=' + tab)
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(async () => {
|
||||
await refreshData()
|
||||
getPluginStatistics()
|
||||
if (activeTab.value != 'market' && pluginId.value) {
|
||||
// 找到这个插件
|
||||
const plugin = dataList.value.find(item => item.id === pluginId.value)
|
||||
if (plugin) {
|
||||
plugin.page_open = true
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VTabs v-model="activeTab">
|
||||
<VTab v-for="item in tabs" :value="item.tab">
|
||||
<VTab v-for="item in PluginTabs" :value="item.tab" @click="jumpTab(item.tab)">
|
||||
<span class="mx-5">{{ item.title }}</span>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<!-- 我的插件 -->
|
||||
<VWindowItem value="myplugin">
|
||||
<VWindowItem value="installed">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
@@ -363,7 +362,7 @@ onBeforeMount(async () => {
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<!-- 插件市场 -->
|
||||
<VWindowItem value="pluginmarket">
|
||||
<VWindowItem value="market">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<LoadingBanner v-if="!isAppMarketLoaded" class="mt-12" />
|
||||
@@ -413,12 +412,7 @@ onBeforeMount(async () => {
|
||||
</div>
|
||||
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card items-start">
|
||||
<template v-for="(data, index) in sortedUninstalledList" :key="`${data.id}_v${data.plugin_version}`">
|
||||
<PluginAppCard
|
||||
v-if="deferApp(index)"
|
||||
:plugin="data"
|
||||
:count="PluginStatistics[data.id || '0']"
|
||||
@install="pluginInstalled"
|
||||
/>
|
||||
<PluginAppCard :plugin="data" :count="PluginStatistics[data.id || '0']" @install="pluginInstalled" />
|
||||
</template>
|
||||
</div>
|
||||
<NoDataFound
|
||||
@@ -437,7 +431,7 @@ onBeforeMount(async () => {
|
||||
<VFab
|
||||
icon="mdi-magnify"
|
||||
color="info"
|
||||
location="bottom end"
|
||||
location="bottom"
|
||||
class="mb-2"
|
||||
size="x-large"
|
||||
fixed
|
||||
@@ -516,14 +510,3 @@ onBeforeMount(async () => {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-plugin-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.v-tabs:not(.v-tabs-pill).v-tabs--horizontal {
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import PullRefresh from 'pull-refresh-vue3'
|
||||
import { VPullToRefresh } from 'vuetify/labs/VPullToRefresh'
|
||||
import api from '@/api'
|
||||
import type { DownloadingInfo } from '@/api/types'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
@@ -7,7 +7,7 @@ import DownloadingCard from '@/components/cards/DownloadingCard.vue'
|
||||
import store from '@/store'
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<DownloadingInfo[]>([])
|
||||
@@ -20,8 +20,7 @@ async function fetchData() {
|
||||
try {
|
||||
dataList.value = await api.get('download/')
|
||||
isRefreshed.value = true
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
@@ -41,10 +40,8 @@ const filteredDataList = computed(() => {
|
||||
// 从Vuex Store中获取用户信息
|
||||
const superUser = store.state.auth.superUser
|
||||
const userName = store.state.auth.userName
|
||||
if (superUser)
|
||||
return dataList.value
|
||||
else
|
||||
return dataList.value.filter(data => data.userid === userName || data.username === userName)
|
||||
if (superUser) return dataList.value
|
||||
else return dataList.value.filter(data => data.userid === userName || data.username === userName)
|
||||
})
|
||||
|
||||
// 加载时获取数据
|
||||
@@ -67,23 +64,10 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadingBanner
|
||||
v-if="!isRefreshed"
|
||||
class="mt-12"
|
||||
/>
|
||||
<PullRefresh
|
||||
v-model="loading"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<div
|
||||
v-if="filteredDataList.length > 0"
|
||||
class="grid gap-3 grid-downloading-card"
|
||||
>
|
||||
<DownloadingCard
|
||||
v-for="data in filteredDataList"
|
||||
:key="data.hash"
|
||||
:info="data"
|
||||
/>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<VPullToRefresh v-model="loading" @load="onRefresh" :pull-down-threshold="64">
|
||||
<div v-if="filteredDataList.length > 0" class="grid gap-3 grid-downloading-card">
|
||||
<DownloadingCard v-for="data in filteredDataList" :key="data.hash" :info="data" />
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="filteredDataList.length === 0 && isRefreshed"
|
||||
@@ -91,12 +75,5 @@ onUnmounted(() => {
|
||||
error-title="没有任务"
|
||||
error-description="正在下载的任务将会显示在这里。"
|
||||
/>
|
||||
</PullRefresh>
|
||||
</VPullToRefresh>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-downloading-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import { MediaDirectory } from '@/api/types'
|
||||
import FileBrowser from '@/components/FileBrowser.vue'
|
||||
|
||||
const endpoints = {
|
||||
@@ -29,42 +30,65 @@ const endpoints = {
|
||||
},
|
||||
}
|
||||
|
||||
// 读取下载目录
|
||||
// 当前目录
|
||||
const path: Ref<string | undefined> = ref()
|
||||
|
||||
// 调用API,加载当前系统环境设置
|
||||
function loadSystemSettings(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
api
|
||||
.get('system/env')
|
||||
.then((result: any) => {
|
||||
let path = '/'
|
||||
if (result.success)
|
||||
path = result.data?.DOWNLOAD_PATH || '/'
|
||||
// 下载目录列表
|
||||
const downloadDirectories = ref<MediaDirectory[]>([])
|
||||
|
||||
if (!path.endsWith('/'))
|
||||
path += '/'
|
||||
// 计算公共路径
|
||||
function findCommonPath(paths: string[]): string {
|
||||
let commonPath = '/'
|
||||
if (!paths || paths.length === 0) {
|
||||
commonPath = '/'
|
||||
} else if (paths.length === 1) {
|
||||
commonPath = paths[0]
|
||||
commonPath = commonPath.replace(/\\/g, '/')
|
||||
} else {
|
||||
const normalizedPaths = paths.map(path => path.replace(/\\/g, '/'))
|
||||
const splitPaths = normalizedPaths.map(path => path.split('/'))
|
||||
let commonParts: string[] = []
|
||||
for (let i = 0; i < splitPaths[0].length; i++) {
|
||||
const part = splitPaths[0][i]
|
||||
if (splitPaths.every(pathParts => pathParts[i] === part)) {
|
||||
commonParts.push(part)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
commonPath = commonParts.join('/')
|
||||
}
|
||||
|
||||
resolve(path)
|
||||
})
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
if (!commonPath.endsWith('/')) {
|
||||
commonPath += '/'
|
||||
}
|
||||
|
||||
if (commonPath.includes(':')) {
|
||||
commonPath = commonPath.replace('/', '\\')
|
||||
}
|
||||
|
||||
return commonPath
|
||||
}
|
||||
|
||||
// 查询下载目录
|
||||
async function loadDownloadDirectories() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/DownloadDirectories')
|
||||
if (result.success && result.data?.value) {
|
||||
downloadDirectories.value = result.data.value
|
||||
path.value = findCommonPath(downloadDirectories.value.map(item => item.path) as string[])
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 目录变化
|
||||
function pathChanged(_path: string) {
|
||||
path.value = _path
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSystemSettings()
|
||||
.then((res) => {
|
||||
path.value = res
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
path.value = '/'
|
||||
})
|
||||
})
|
||||
onBeforeMount(loadDownloadDirectories)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'lodash'
|
||||
import { ref, unref } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { TransferHistory } from '@/api/types'
|
||||
import ReorganizeDialog from '@/components/dialog/ReorganizeDialog.vue'
|
||||
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import router from '@/router'
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 路由
|
||||
const route = useRoute()
|
||||
|
||||
// 重新整理对话框
|
||||
const redoDialog = ref(false)
|
||||
|
||||
@@ -19,9 +23,6 @@ const currentHistory = ref<TransferHistory>()
|
||||
// 重新整理IDS
|
||||
const redoIds = ref<number[]>([])
|
||||
|
||||
// 重新整理target
|
||||
const redoTarget = ref('')
|
||||
|
||||
// 已选中的数据
|
||||
const selected = ref<TransferHistory[]>([])
|
||||
|
||||
@@ -72,7 +73,7 @@ const pageRange = [
|
||||
const dataList = ref<TransferHistory[]>([])
|
||||
|
||||
// 搜索
|
||||
const search = ref()
|
||||
const search = ref(route.query.search as string)
|
||||
|
||||
// 搜索提示词列表
|
||||
const searchHintList = ref<string[]>([])
|
||||
@@ -84,10 +85,10 @@ const loading = ref(false)
|
||||
const totalItems = ref(0)
|
||||
|
||||
// 每页条数
|
||||
const itemsPerPage = ref(50)
|
||||
const itemsPerPage = ref<number>(ensureNumber(route.query.itemsPerPage, 50))
|
||||
|
||||
// 当前页码
|
||||
const currentPage = ref(1)
|
||||
const currentPage = ref<number>(ensureNumber(route.query.currentPage, 1))
|
||||
|
||||
// 进度条
|
||||
const progressDialog = ref(false)
|
||||
@@ -116,8 +117,8 @@ const TransferDict: { [key: string]: string } = {
|
||||
|
||||
// 分页提示
|
||||
const pageTip = computed(() => {
|
||||
const begin = unref(itemsPerPage) * (unref(currentPage) - 1) + 1
|
||||
const end = unref(itemsPerPage) * unref(currentPage) === -1 ? 'ALL' : unref(itemsPerPage) * unref(currentPage)
|
||||
const begin = itemsPerPage.value * (currentPage.value - 1) + 1
|
||||
const end = itemsPerPage.value * currentPage.value === -1 ? 'ALL' : itemsPerPage.value * currentPage.value
|
||||
return {
|
||||
begin,
|
||||
end,
|
||||
@@ -126,7 +127,7 @@ const pageTip = computed(() => {
|
||||
|
||||
// 分页总数
|
||||
const totalPage = computed(() => {
|
||||
const total = Math.ceil(unref(totalItems) / unref(itemsPerPage))
|
||||
const total = Math.ceil(totalItems.value / itemsPerPage.value)
|
||||
return total
|
||||
})
|
||||
|
||||
@@ -134,7 +135,7 @@ const totalPage = computed(() => {
|
||||
watch(
|
||||
[() => currentPage.value, () => itemsPerPage.value, () => search.value],
|
||||
debounce(async () => {
|
||||
await fetchData()
|
||||
reloadPage()
|
||||
}, 1000),
|
||||
)
|
||||
|
||||
@@ -271,23 +272,20 @@ async function retransferBatch() {
|
||||
currentHistory.value = undefined
|
||||
// 重新整理IDS
|
||||
redoIds.value = selected.value.map(item => item.id)
|
||||
// 重新整理target
|
||||
if (selected.value.length === 1) {
|
||||
// 目的目录
|
||||
const dest = selected.value[0].dest ?? ''
|
||||
// 类型
|
||||
const mediaType = selected.value[0].type ?? ''
|
||||
// 分类
|
||||
const category = selected.value[0].category ?? ''
|
||||
// 计算根路径
|
||||
redoTarget.value = getRootPath(dest, mediaType, category)
|
||||
} else {
|
||||
redoTarget.value = ''
|
||||
}
|
||||
// 打开识别弹窗
|
||||
redoDialog.value = true
|
||||
}
|
||||
|
||||
// 整理完成
|
||||
function transferDone() {
|
||||
redoDialog.value = false
|
||||
// 清空当前操作记录
|
||||
currentHistory.value = undefined
|
||||
selected.value = []
|
||||
// 刷新
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
@@ -297,7 +295,6 @@ const dropdownItems = ref([
|
||||
prependIcon: 'mdi-redo-variant',
|
||||
click: (item: TransferHistory) => {
|
||||
redoIds.value = [item.id]
|
||||
redoTarget.value = getRootPath(item.dest ?? '', item.type ?? '', item.category ?? '')
|
||||
redoDialog.value = true
|
||||
},
|
||||
},
|
||||
@@ -315,6 +312,38 @@ const dropdownItems = ref([
|
||||
},
|
||||
])
|
||||
|
||||
// 添加url参数
|
||||
function addUrlQuery(url: string, name: string, value: any) {
|
||||
if (!url || !name || !value) return url
|
||||
const separator = url.includes('?') ? '&' : '?'
|
||||
return url + separator + name + '=' + encodeURIComponent(value)
|
||||
}
|
||||
|
||||
// 重载页面
|
||||
function reloadPage() {
|
||||
let url = '/history'
|
||||
if (search.value) {
|
||||
url = addUrlQuery(url, 'search', search.value)
|
||||
}
|
||||
if (itemsPerPage.value) {
|
||||
url = addUrlQuery(url, 'itemsPerPage', itemsPerPage.value)
|
||||
}
|
||||
if (currentPage.value) {
|
||||
url = addUrlQuery(url, 'currentPage', currentPage.value)
|
||||
}
|
||||
router.push(url)
|
||||
}
|
||||
|
||||
// 确保值为number类型
|
||||
function ensureNumber(value: any, defaultValue: number = 0) {
|
||||
value = Number(value)
|
||||
// 如果不是数字
|
||||
if (value !== value) {
|
||||
value = defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// 初始加载数据
|
||||
onMounted(fetchData)
|
||||
</script>
|
||||
@@ -356,6 +385,7 @@ onMounted(fetchData)
|
||||
show-select
|
||||
loading-text="加载中..."
|
||||
class="data-table-div"
|
||||
hover
|
||||
>
|
||||
<template #item.title="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
@@ -430,6 +460,32 @@ onMounted(fetchData)
|
||||
</VPagination>
|
||||
</div>
|
||||
</VCard>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<span>
|
||||
<VFab
|
||||
v-if="selected.length > 0"
|
||||
icon="mdi-trash-can-outline"
|
||||
color="error"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="removeHistoryBatch"
|
||||
/>
|
||||
<VFab
|
||||
v-if="selected.length > 0"
|
||||
class="mb-16"
|
||||
icon="mdi-redo-variant"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="retransferBatch"
|
||||
/>
|
||||
</span>
|
||||
<!-- 底部弹窗 -->
|
||||
<VBottomSheet v-model="deleteConfirmDialog" inset>
|
||||
<VCard class="text-center rounded-t">
|
||||
@@ -456,44 +512,9 @@ onMounted(fetchData)
|
||||
v-if="redoDialog"
|
||||
v-model="redoDialog"
|
||||
:logids="redoIds"
|
||||
:target="redoTarget"
|
||||
@done="
|
||||
() => {
|
||||
redoDialog = false
|
||||
// 清空当前操作记录
|
||||
currentHistory = undefined
|
||||
selected = []
|
||||
// 刷新
|
||||
fetchData()
|
||||
}
|
||||
"
|
||||
@done="transferDone"
|
||||
@close="redoDialog = false"
|
||||
/>
|
||||
<!-- 底部操作按钮 -->
|
||||
<span>
|
||||
<VFab
|
||||
v-if="selected.length > 0"
|
||||
icon="mdi-trash-can-outline"
|
||||
color="error"
|
||||
location="bottom end"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="removeHistoryBatch"
|
||||
/>
|
||||
<VFab
|
||||
v-if="selected.length > 0"
|
||||
class="mb-2"
|
||||
icon="mdi-redo-variant"
|
||||
location="bottom end"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
appear
|
||||
@click="retransferBatch"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
326
src/views/setting/AccountSettingDirectory.vue
Normal file
326
src/views/setting/AccountSettingDirectory.vue
Normal file
@@ -0,0 +1,326 @@
|
||||
<!-- eslint-disable sonarjs/no-duplicate-string -->
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import draggable from 'vuedraggable'
|
||||
import { VRow } from 'vuetify/lib/components/index.mjs'
|
||||
import api from '@/api'
|
||||
import { MediaDirectory } from '@/api/types'
|
||||
import DirectoryCard from '@/components/cards/DirectoryCard.vue'
|
||||
|
||||
// 媒体库设置项
|
||||
const transferSettings = ref({
|
||||
TRANSFER_TYPE: 'copy',
|
||||
OVERWRITE_MODE: 'size',
|
||||
TRANSFER_SAME_DISK: true,
|
||||
})
|
||||
|
||||
// 转移方式字典
|
||||
const transferTypeItems = [
|
||||
{ title: '硬链接', value: 'link' },
|
||||
{ title: '复制', value: 'copy' },
|
||||
{ title: '移动', value: 'move' },
|
||||
{ title: '软链接', value: 'softlink' },
|
||||
{ title: 'rclone复制', value: 'rclone_copy' },
|
||||
{ title: 'rclone移动', value: 'rclone_move' },
|
||||
]
|
||||
|
||||
// 覆盖模式字典
|
||||
const overwriteModeItems = [
|
||||
{ title: '从不覆盖', value: 'never' },
|
||||
{ title: '按大小覆盖', value: 'size' },
|
||||
{ title: '总是覆盖', value: 'always' },
|
||||
{ title: '仅保留最新版本', value: 'latest' },
|
||||
]
|
||||
|
||||
// 所有下载目录
|
||||
const downloadDirectories = ref<MediaDirectory[]>([])
|
||||
|
||||
// 所有媒体库目录
|
||||
const libraryDirectories = ref<MediaDirectory[]>([])
|
||||
|
||||
// 二级分类策略
|
||||
const mediaCategories = ref<{ [key: string]: any }>({})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 加载媒体库设置
|
||||
async function loadTransferSettings() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
if (result.success) {
|
||||
const { TRANSFER_TYPE, OVERWRITE_MODE, TRANSFER_SAME_DISK } = result.data
|
||||
transferSettings.value = {
|
||||
TRANSFER_TYPE,
|
||||
OVERWRITE_MODE,
|
||||
TRANSFER_SAME_DISK,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API保存媒体设置
|
||||
async function saveTransferSetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('system/env', transferSettings.value)
|
||||
|
||||
if (result.success) $toast.success('保存媒体库设置成功')
|
||||
else $toast.error('保存媒体库设置失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 移动结束
|
||||
function orderDownloadCards() {
|
||||
// 更新所有目录的优先级
|
||||
downloadDirectories.value.forEach((item, index) => {
|
||||
item.priority = index
|
||||
})
|
||||
}
|
||||
|
||||
// 移动结束
|
||||
function orderLibraryCards() {
|
||||
// 更新所有目录的优先级
|
||||
libraryDirectories.value.forEach((item, index) => {
|
||||
item.priority = index
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭目录卡片
|
||||
function libraryCardClose(name: string) {
|
||||
libraryDirectories.value = libraryDirectories.value.filter(item => item.name !== name)
|
||||
}
|
||||
|
||||
// 关闭下载卡片
|
||||
function downloadCardClose(name: string) {
|
||||
downloadDirectories.value = downloadDirectories.value.filter(item => item.name !== name)
|
||||
}
|
||||
|
||||
// 查询下载目录
|
||||
async function loadDownloadDirectories() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/DownloadDirectories')
|
||||
if (result.success && result.data?.value) {
|
||||
downloadDirectories.value = result.data.value
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存下载目录
|
||||
async function saveDownloadDirectories() {
|
||||
orderDownloadCards()
|
||||
try {
|
||||
const value = downloadDirectories.value.map(item => {
|
||||
return {
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
media_type: item.media_type,
|
||||
category: item.category,
|
||||
auto_category: item.auto_category,
|
||||
priority: item.priority,
|
||||
}
|
||||
})
|
||||
const result: { [key: string]: any } = await api.post('system/setting/DownloadDirectories', value)
|
||||
if (result.success) $toast.success('下载目录设置保存成功!')
|
||||
} catch (e) {
|
||||
console.error('保存下载目录设置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 添加下载目录
|
||||
function addDownloadDirectory() {
|
||||
downloadDirectories.value.push({
|
||||
name: `下载目录${downloadDirectories.value.length + 1}`,
|
||||
path: '',
|
||||
media_type: '全部',
|
||||
category: '',
|
||||
})
|
||||
}
|
||||
|
||||
// 查询媒体库目录
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存媒体库目录
|
||||
async function saveLibraryDirectories() {
|
||||
orderLibraryCards()
|
||||
try {
|
||||
const value = libraryDirectories.value.map(item => {
|
||||
return {
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
media_type: item.media_type,
|
||||
category: item.category,
|
||||
auto_category: item.auto_category,
|
||||
scrape: item.scrape,
|
||||
priority: item.priority,
|
||||
}
|
||||
})
|
||||
const result: { [key: string]: any } = await api.post('system/setting/LibraryDirectories', value)
|
||||
if (result.success) $toast.success('媒体库目录设置保存成功!')
|
||||
} catch (e) {
|
||||
console.error('保存媒体库目录设置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 添加媒体库目录
|
||||
function addLibraryDirectory() {
|
||||
libraryDirectories.value.push({
|
||||
name: `媒体库目录${libraryDirectories.value.length + 1}`,
|
||||
path: '',
|
||||
media_type: '全部',
|
||||
category: '',
|
||||
scrape: true,
|
||||
})
|
||||
}
|
||||
|
||||
// 调用API查询自动分类配置
|
||||
async function loadMediaCategories() {
|
||||
try {
|
||||
mediaCategories.value = await api.get('media/category')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
loadTransferSettings()
|
||||
loadMediaCategories()
|
||||
loadDownloadDirectories()
|
||||
loadLibraryDirectories()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>下载目录</VCardTitle>
|
||||
<VCardSubtitle>设置下载目录路径和分类,按顺序依次匹配使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<draggable
|
||||
v-model="downloadDirectories"
|
||||
handle=".cursor-move"
|
||||
item-key="pri"
|
||||
tag="div"
|
||||
@end="orderDownloadCards"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-directory-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<DirectoryCard
|
||||
type="download"
|
||||
:directory="element"
|
||||
:categories="mediaCategories"
|
||||
@update:modelValue="(value: string) => (element.path = value)"
|
||||
@close="downloadCardClose(element.name)"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" class="me-2" @click="saveDownloadDirectories"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addDownloadDirectory">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>媒体库目录</VCardTitle>
|
||||
<VCardSubtitle>设置媒体文件整理后存储目录和分类,按顺序依次匹配使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<draggable
|
||||
v-model="libraryDirectories"
|
||||
handle=".cursor-move"
|
||||
item-key="pri"
|
||||
tag="div"
|
||||
@end="orderLibraryCards"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-directory-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<DirectoryCard
|
||||
type="library"
|
||||
:directory="element"
|
||||
:categories="mediaCategories"
|
||||
@update:modelValue="(value: string) => (element.path = value)"
|
||||
@close="libraryCardClose(element.name)"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" class="me-2" @click="saveLibraryDirectories"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addLibraryDirectory">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>整理模式</VCardTitle>
|
||||
<VCardSubtitle>设置文件整理方式和偏好。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="transferSettings.TRANSFER_TYPE"
|
||||
:items="transferTypeItems"
|
||||
label="整理方式"
|
||||
hint="文件从下载目录整理到媒体库目录的操作方式"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="transferSettings.OVERWRITE_MODE"
|
||||
:items="overwriteModeItems"
|
||||
label="覆盖模式"
|
||||
hint="媒体库中同名文件已存在时的覆盖方式"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="transferSettings.TRANSFER_SAME_DISK"
|
||||
label="同盘/同根目录优先"
|
||||
hint="优先整理到与下载目录同一磁盘/同一根路径的媒体库目录中"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn mtype="submit" @click="saveTransferSetting"> 保存 </VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
@@ -56,6 +56,10 @@ const NotificationChannels = [
|
||||
title: 'VoceChat',
|
||||
value: 'vocechat',
|
||||
},
|
||||
{
|
||||
title: 'WebPush',
|
||||
value: 'webpush',
|
||||
},
|
||||
]
|
||||
|
||||
// 提示框
|
||||
@@ -67,8 +71,7 @@ async function loadNotificationSwitchs() {
|
||||
const result: NotificationSwitch[] = await api.get('message/switchs')
|
||||
|
||||
messagemTypes.value = result
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -76,17 +79,11 @@ async function loadNotificationSwitchs() {
|
||||
// 调用API保存消息开关
|
||||
async function saveNotificationSwitchs() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'message/switchs',
|
||||
messagemTypes.value,
|
||||
)
|
||||
const result: { [key: string]: any } = await api.post('message/switchs', messagemTypes.value)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('保存通知消息设置成功')
|
||||
else
|
||||
$toast.error('保存通知消息设置失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('保存通知消息设置成功')
|
||||
else $toast.error('保存通知消息设置失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -143,8 +140,7 @@ async function loadNotificationSettings() {
|
||||
VOCECHAT_CHANNEL_ID,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -152,23 +148,17 @@ async function loadNotificationSettings() {
|
||||
// 调用API保存消息渠道设置
|
||||
async function saveNotificationSettings() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.post(
|
||||
'system/setting/MESSAGER',
|
||||
selectedChannels.value.join(','),
|
||||
)
|
||||
const result1: { [key: string]: any } = await api.post('system/setting/MESSAGER', selectedChannels.value.join(','))
|
||||
|
||||
const result2: { [key: string]: any } = await api.post(
|
||||
'system/env',
|
||||
notificationSettings.value,
|
||||
)
|
||||
const result2: { [key: string]: any } = await api.post('system/env', notificationSettings.value)
|
||||
|
||||
if (result1.success && result2.success) {
|
||||
$toast.success('保存通知渠道设置成功')
|
||||
reloadModule()
|
||||
} else {
|
||||
$toast.error('保存通知渠道设置失败!')
|
||||
}
|
||||
else { $toast.error('保存通知渠道设置失败!') }
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -177,12 +167,9 @@ async function saveNotificationSettings() {
|
||||
async function reloadModule() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success)
|
||||
$toast.success('重新加载模块成功')
|
||||
else
|
||||
$toast.error('重新加载模块失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('重新加载模块成功')
|
||||
else $toast.error('重新加载模块失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -197,8 +184,11 @@ onMounted(() => {
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="通知渠道">
|
||||
<VCardSubtitle>只有选中的渠道才会发送消息。</VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>通知渠道</VCardTitle>
|
||||
<VCardSubtitle>只有选中的渠道才会发送消息。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -209,37 +199,21 @@ onMounted(() => {
|
||||
chips
|
||||
:items="NotificationChannels"
|
||||
label="当前使用通知渠道"
|
||||
hint="选中的渠道才会按消息类型的设定发送消息"
|
||||
hint="消息通知渠道总开关"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTabs
|
||||
v-model="messagerTab"
|
||||
stacked
|
||||
>
|
||||
<VTab value="wechat">
|
||||
微信
|
||||
</VTab>
|
||||
<VTab value="telegram">
|
||||
Telegram
|
||||
</VTab>
|
||||
<VTab value="slack">
|
||||
Slack
|
||||
</VTab>
|
||||
<VTab value="synologychat">
|
||||
SynologyChat
|
||||
</VTab>
|
||||
<VTab value="vocechat">
|
||||
VoceChat
|
||||
</VTab>
|
||||
<VTabs v-model="messagerTab" stacked>
|
||||
<VTab value="wechat"> 微信 </VTab>
|
||||
<VTab value="telegram"> Telegram </VTab>
|
||||
<VTab value="slack"> Slack </VTab>
|
||||
<VTab value="synologychat"> SynologyChat </VTab>
|
||||
<VTab value="vocechat"> VoceChat </VTab>
|
||||
</VTabs>
|
||||
<VWindow
|
||||
v-model="messagerTab"
|
||||
class="mt-5 disable-tab-transition"
|
||||
:touch="false"
|
||||
>
|
||||
<VWindow v-model="messagerTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<VWindowItem value="wechat">
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -247,42 +221,48 @@ onMounted(() => {
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_CORPID"
|
||||
label="企业ID"
|
||||
hint="登录企业微信后台,在 https://work.weixin.qq.com/wework_admin/frame#profile 中查看"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_APP_SECRET"
|
||||
label="应用Secret"
|
||||
hint="在企业微信中创建应用,查看应用的Secret"
|
||||
hint="企业微信后台企业信息中的企业ID"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_APP_ID"
|
||||
label="应用 AgentId"
|
||||
hint="在企业微信中创建应用,查看应用的AgentId"
|
||||
hint="企业微信自建应用的AgentId"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_APP_SECRET"
|
||||
label="应用Secret"
|
||||
hint="企业微信自建应用的Secret"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_PROXY"
|
||||
label="代理地址"
|
||||
hint="由于微信官方限制,2022年6月20日后创建的企业微信应用需要有固定的公网IP地址并加入IP白名单后才能接收消息,使用有固定公网IP的代理服务器转发可解决该问题;代理服务器需自行搭建,搭建方法参考项目主页说明,不使用代理需保留默认值"
|
||||
hint="微信消息的转发代理地址,2022年6月20日后创建的自建应用才需要,不使用代理时需要保留默认值"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_TOKEN"
|
||||
label="Token"
|
||||
hint="在微信企业应用管理后台-接收消息设置页面生成"
|
||||
hint="微信企业自建应用->API接收消息配置中的Token"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.WECHAT_ENCODING_AESKEY"
|
||||
label="EncodingAESKey"
|
||||
hint="在微信企业应用管理后台-接收消息设置页面生成,所有信息填入完成后保存,然后再在企业微信应用消息接收服务中输入回调地址:http(s)://domain:port/api/v1/message/"
|
||||
hint="微信企业自建应用->API接收消息配置中的EncodingAESKey"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -290,7 +270,8 @@ onMounted(() => {
|
||||
v-model="notificationSettings.WECHAT_ADMINS"
|
||||
label="管理员白名单"
|
||||
placeholder="多个用,分隔"
|
||||
hint="只有在白名单中的用户才能使用菜单管理功能,不填写则所有用户都能使用,菜单会自动生成,不需要手动创建"
|
||||
hint="可使用管理菜单及命令的用户ID列表,多个ID使用,分隔"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -303,14 +284,16 @@ onMounted(() => {
|
||||
<VTextField
|
||||
v-model="notificationSettings.TELEGRAM_TOKEN"
|
||||
label="Bot Token"
|
||||
hint="Telegram机器人的token,关注BotFather创建机器人并获取token,格式为:123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
|
||||
hint="Telegram机器人token,格式:123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationSettings.TELEGRAM_CHAT_ID"
|
||||
label="Chat ID"
|
||||
hint="接受消息通知的用户、群组或频道Chat ID,关注@getidsbot获取"
|
||||
hint="接受消息通知的用户、群组或频道Chat ID"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -318,7 +301,8 @@ onMounted(() => {
|
||||
v-model="notificationSettings.TELEGRAM_USERS"
|
||||
label="用户白名单"
|
||||
placeholder="多个用,分隔"
|
||||
hint="只有在白名单中的用户才能使用Telegram机器人,不填写则所有用户都能使用,多个用户用英文,分隔"
|
||||
hint="可使用Telegram机器人的用户ID清单,多个用户用,分隔,不填写则所有用户都能使用"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -326,7 +310,8 @@ onMounted(() => {
|
||||
v-model="notificationSettings.TELEGRAM_ADMINS"
|
||||
label="管理员白名单"
|
||||
placeholder="多个用,分隔"
|
||||
hint="只有在白名单中的用户才能使用管理功能,不填写则所有用户都能使用,多个用户用英文,分隔。菜单会自动生成,不需要手动创建"
|
||||
hint="可使用管理菜单及命令的用户ID列表,多个ID使用,分隔"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -340,7 +325,8 @@ onMounted(() => {
|
||||
v-model="notificationSettings.SLACK_OAUTH_TOKEN"
|
||||
label="Slack Bot User OAuth Token"
|
||||
placeholder="xoxb-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
hint="在 https://api.slack.com/apps 中创建应用,查看OAuth & Permissions页面中的Bot User OAuth Token"
|
||||
hint="Slack应用`OAuth & Permissions`页面中的`Bot User OAuth Token`"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="5">
|
||||
@@ -348,7 +334,8 @@ onMounted(() => {
|
||||
v-model="notificationSettings.SLACK_APP_TOKEN"
|
||||
label="Slack App-Level Token"
|
||||
placeholder="xapp-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
hint="在 https://api.slack.com/apps 中创建应用,查看OAuth & Permissions页面中的App-Level Token"
|
||||
hint="Slack应用`OAuth & Permissions`页面中的`App-Level Token`"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="2">
|
||||
@@ -356,7 +343,8 @@ onMounted(() => {
|
||||
v-model="notificationSettings.SLACK_CHANNEL"
|
||||
label="频道名称"
|
||||
placeholder="全体"
|
||||
hint="消息发送到的频道名称,不填写则发送到全体频道"
|
||||
hint="消息发送频道,默认`全体`"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -369,14 +357,16 @@ onMounted(() => {
|
||||
<VTextField
|
||||
v-model="notificationSettings.SYNOLOGYCHAT_WEBHOOK"
|
||||
label="机器人传入URL"
|
||||
hint="在Synology Chat中创建机器人,获取机器人传入URL"
|
||||
hint="Synology Chat机器人传入URL"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationSettings.SYNOLOGYCHAT_TOKEN"
|
||||
label="令牌"
|
||||
hint="在Synology Chat中创建机器人,获取机器人令牌"
|
||||
hint="Synology Chat机器人令牌"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -389,13 +379,16 @@ onMounted(() => {
|
||||
<VTextField
|
||||
v-model="notificationSettings.VOCECHAT_HOST"
|
||||
label="地址"
|
||||
hint="VoceChat服务端地址,格式:http(s)://ip:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="notificationSettings.VOCECHAT_API_KEY"
|
||||
label="机器人密钥"
|
||||
hint="在VoceChat中创建机器人,获取机器人密钥"
|
||||
hint="VoceChat机器人密钥"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -403,7 +396,8 @@ onMounted(() => {
|
||||
v-model="notificationSettings.VOCECHAT_CHANNEL_ID"
|
||||
label="频道ID"
|
||||
placeholder="不包含#号"
|
||||
hint="在VoceChat中创建频道,获取频道ID,不包含#号"
|
||||
hint="VoceChat的频道ID,不包含#号"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -417,12 +411,7 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn
|
||||
mtype="submit"
|
||||
@click="saveNotificationSettings"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
<VBtn mtype="submit" @click="saveNotificationSettings"> 保存 </VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -431,36 +420,25 @@ onMounted(() => {
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="消息类型">
|
||||
<VCardSubtitle> 对应消息类型只会发送给选中的消息渠道。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>消息类型</VCardTitle>
|
||||
<VCardSubtitle>对应消息类型只会发送给选中的消息渠道。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VTable class="text-no-wrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
消息类型
|
||||
</th>
|
||||
<th scope="col">
|
||||
微信
|
||||
</th>
|
||||
<th scope="col">
|
||||
Telegram
|
||||
</th>
|
||||
<th scope="col">
|
||||
Slack
|
||||
</th>
|
||||
<th scope="col">
|
||||
SynologyChat
|
||||
</th>
|
||||
<th scope="col">
|
||||
VoceChat
|
||||
</th>
|
||||
<th scope="col">消息类型</th>
|
||||
<th scope="col">微信</th>
|
||||
<th scope="col">Telegram</th>
|
||||
<th scope="col">Slack</th>
|
||||
<th scope="col">SynologyChat</th>
|
||||
<th scope="col">VoceChat</th>
|
||||
<th scope="col">WebPush</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="message in messagemTypes"
|
||||
:key="message.mtype"
|
||||
>
|
||||
<tr v-for="message in messagemTypes" :key="message.mtype">
|
||||
<td>
|
||||
{{ message.mtype }}
|
||||
</td>
|
||||
@@ -479,28 +457,20 @@ onMounted(() => {
|
||||
<td>
|
||||
<VCheckbox v-model="message.vocechat" />
|
||||
</td>
|
||||
<td>
|
||||
<VCheckbox v-model="message.webpush" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="messagemTypes.length === 0">
|
||||
<td
|
||||
colspan="6"
|
||||
class="text-center"
|
||||
>
|
||||
没有设置任何通知渠道
|
||||
</td>
|
||||
<td colspan="6" class="text-center">没有设置任何通知渠道</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
<VDivider />
|
||||
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn
|
||||
mtype="submit"
|
||||
@click="saveNotificationSwitchs"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
<VBtn mtype="submit" @click="saveNotificationSwitchs"> 保存 </VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
@@ -280,8 +280,11 @@ onMounted(() => {
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="媒体数据源">
|
||||
<VCardSubtitle> 设定搜索时展示哪些源的媒体信息。</VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>媒体数据源</VCardTitle>
|
||||
<VCardSubtitle>设定搜索时展示哪些源的媒体信息。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -291,22 +294,24 @@ onMounted(() => {
|
||||
chips
|
||||
:items="mediaSourcesDict"
|
||||
label="当前使用数据源"
|
||||
hint="选中多项时会同时展示来自不同数据源的搜索结果,选择的数据源顺序将会影响搜索结果的排序"
|
||||
hint="搜索媒体信息时使用的数据源以及排序"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<VCardItem>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveMediaSourceSetting"> 保存 </VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="搜索站点">
|
||||
<VCardSubtitle> 只有选中的站点才会在搜索中使用。</VCardSubtitle>
|
||||
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>搜索站点</VCardTitle>
|
||||
<VCardSubtitle> 只有选中的站点才会在搜索中使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VChipGroup v-model="selectedSites" column multiple>
|
||||
<VChip
|
||||
v-for="site in allSites"
|
||||
@@ -319,38 +324,40 @@ onMounted(() => {
|
||||
{{ site.name }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VCardItem>
|
||||
|
||||
<VCardItem>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveSelectedSites"> 保存 </VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="搜索优先级">
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem variant="plain" @click="shareRules">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" @click="importCodeDialog = true">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardSubtitle> 设置在搜索时默认使用的优先级排序,未在优先级中的资源将不在搜索结果中显示。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem variant="plain" @click="shareRules">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" @click="importCodeDialog = true">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardTitle>搜索优先级</VCardTitle>
|
||||
<VCardSubtitle>设置在搜索时默认使用的优先级排序,未在优先级中的资源将不在搜索结果中显示。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<draggable
|
||||
v-model="filterCards"
|
||||
handle=".cursor-move"
|
||||
@@ -369,18 +376,21 @@ onMounted(() => {
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" class="me-2" @click="saveCustomFilters()"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addFilterCard()">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="默认过滤规则">
|
||||
<VCardSubtitle> 设置在搜索时默认使用的过滤规则。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>默认过滤规则</VCardTitle>
|
||||
<VCardSubtitle>设置在搜索时默认使用的过滤规则。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -389,7 +399,8 @@ onMounted(() => {
|
||||
v-model="defaultFilterRules.include"
|
||||
type="text"
|
||||
label="包含(关键字、正则式)"
|
||||
hint="支持正式表达式,多个关键字用 | 分隔表示或"
|
||||
hint="包含规则,支持正式表达式,多个关键字用 | 分隔表示或"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -397,7 +408,8 @@ onMounted(() => {
|
||||
v-model="defaultFilterRules.exclude"
|
||||
type="text"
|
||||
label="排除(关键字、正则式)"
|
||||
hint="支持正式表达式,多个关键字用 | 分隔表示或"
|
||||
hint="排除规则,支持正式表达式,多个关键字用 | 分隔表示或"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -407,23 +419,25 @@ onMounted(() => {
|
||||
label="最小做种数"
|
||||
placeholder="0"
|
||||
hint="小于该值的资源将被过滤掉,0表示不过滤"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.min_seeders_time"
|
||||
type="text"
|
||||
label="最少做种人数生效发布时间(分钟)"
|
||||
label="最少做种数生效发布时间(分钟)"
|
||||
placeholder="0"
|
||||
hint="发布时间距现在大于该值的资源将生效最小做种数规则,0表示不生效"
|
||||
hint="发布时间距当前时间大于该值的资源将生效最小做种数规则,0表示不生效"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveDefaultFilter"> 保存 </VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -431,10 +445,3 @@ onMounted(() => {
|
||||
<ImportCodeDialog v-model="importCodeString" title="导入优先级规则" @close="importCodeDialog = false" />
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-filterrule-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@ const $toast = useToast()
|
||||
const schedulerList = ref<ScheduleInfo[]>([])
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
let refreshTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 调用API加载定时服务列表
|
||||
async function loadSchedulerList() {
|
||||
@@ -18,8 +18,7 @@ async function loadSchedulerList() {
|
||||
const res: ScheduleInfo[] = await api.get('dashboard/schedule')
|
||||
|
||||
schedulerList.value = res
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -52,8 +51,7 @@ function runCommand(id: string) {
|
||||
setTimeout(() => {
|
||||
loadSchedulerList()
|
||||
}, 1000)
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
@@ -77,32 +75,23 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard title="定时作业">
|
||||
<VCardSubtitle> 包含系统内置服务以及插件提供的服务,手动执行不会影响作业正常的时间表。 </VCardSubtitle>
|
||||
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>定时作业</VCardTitle>
|
||||
<VCardSubtitle>包含系统内置服务以及插件提供的服务,手动执行不会影响作业正常的时间表。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VTable class="text-no-wrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
提供者
|
||||
</th>
|
||||
<th scope="col">
|
||||
任务名称
|
||||
</th>
|
||||
<th scope="col">
|
||||
任务状态
|
||||
</th>
|
||||
<th scope="col">
|
||||
下一次执行时间
|
||||
</th>
|
||||
<th scope="col">提供者</th>
|
||||
<th scope="col">任务名称</th>
|
||||
<th scope="col">任务状态</th>
|
||||
<th scope="col">下一次执行时间</th>
|
||||
<th scope="col" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="scheduler in schedulerList"
|
||||
:key="scheduler.id"
|
||||
>
|
||||
<tr v-for="scheduler in schedulerList" :key="scheduler.id">
|
||||
<td>
|
||||
{{ scheduler.provider }}
|
||||
</td>
|
||||
@@ -118,11 +107,7 @@ onUnmounted(() => {
|
||||
{{ scheduler.next_run }}
|
||||
</td>
|
||||
<td>
|
||||
<VBtn
|
||||
size="small"
|
||||
:disabled="scheduler.status === '正在运行'"
|
||||
@click="runCommand(scheduler.id)"
|
||||
>
|
||||
<VBtn size="small" :disabled="scheduler.status === '正在运行'" @click="runCommand(scheduler.id)">
|
||||
<template #prepend>
|
||||
<VIcon>mdi-play</VIcon>
|
||||
</template>
|
||||
@@ -131,12 +116,7 @@ onUnmounted(() => {
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="schedulerList.length === 0">
|
||||
<td
|
||||
colspan="4"
|
||||
class="text-center"
|
||||
>
|
||||
没有后台服务
|
||||
</td>
|
||||
<td colspan="4" class="text-center">没有后台服务</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
|
||||
@@ -51,16 +51,12 @@ async function resetSites() {
|
||||
resetSitesText.value = '正在重置...'
|
||||
|
||||
const result: { [key: string]: any } = await api.get('site/reset')
|
||||
if (result.success)
|
||||
$toast.success('站点重置成功,请等待CookieCloud同步完成!')
|
||||
|
||||
else
|
||||
$toast.error('站点重置失败!')
|
||||
if (result.success) $toast.success('站点重置成功,请等待CookieCloud同步完成!')
|
||||
else $toast.error('站点重置失败!')
|
||||
|
||||
resetSitesDisabled.value = false
|
||||
resetSitesText.value = '重置站点数据'
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -68,13 +64,10 @@ async function resetSites() {
|
||||
// 查询种子优先规则
|
||||
async function queryTorrentPriority() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/TorrentsPriority',
|
||||
)
|
||||
const result: { [key: string]: any } = await api.get('system/setting/TorrentsPriority')
|
||||
|
||||
selectedTorrentPriority.value = result.data?.value
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -88,12 +81,9 @@ async function saveTorrentPriority() {
|
||||
selectedTorrentPriority.value,
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('优先规则保存成功')
|
||||
else
|
||||
$toast.error('优先规则保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('优先规则保存成功')
|
||||
else $toast.error('优先规则保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -120,8 +110,7 @@ async function loadCookieCloudSettings() {
|
||||
COOKIECLOUD_ENABLE_LOCAL,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -129,17 +118,11 @@ async function loadCookieCloudSettings() {
|
||||
// 调用API保存CookieCloud设置
|
||||
async function saveCookieCloudetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/env',
|
||||
cookieCloudSetting.value,
|
||||
)
|
||||
const result: { [key: string]: any } = await api.post('system/env', cookieCloudSetting.value)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('保存站点同步设置成功')
|
||||
else
|
||||
$toast.error('保存站点同步设置失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('保存站点同步设置成功')
|
||||
else $toast.error('保存站点同步设置失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -154,8 +137,11 @@ onMounted(() => {
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="站点同步">
|
||||
<VCardSubtitle> 从CookieCloud快速同步站点数据。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>站点同步</VCardTitle>
|
||||
<VCardSubtitle>从CookieCloud快速同步站点数据。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -163,7 +149,8 @@ onMounted(() => {
|
||||
<VCheckbox
|
||||
v-model="cookieCloudSetting.COOKIECLOUD_ENABLE_LOCAL"
|
||||
label="启用本地CookieCloud服务器"
|
||||
hint="启用后,将使用内建CookieCloud服务同步站点数据,服务地址为:http://localhost:3000/cookiecloud"
|
||||
hint="使用内建CookieCloud服务同步站点数据,服务地址为:http://localhost:3000/cookiecloud"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -171,17 +158,19 @@ onMounted(() => {
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="cookieCloudSetting.COOKIECLOUD_HOST"
|
||||
label="远程CookieCloud服务器地址"
|
||||
label="服务地址"
|
||||
placeholder="https://movie-pilot.org/cookiecloud"
|
||||
:disabled="!!cookieCloudSetting.COOKIECLOUD_ENABLE_LOCAL"
|
||||
hint="格式:https://movie-pilot.org/cookiecloud"
|
||||
hint="远端CookieCloud服务地址,格式:https://movie-pilot.org/cookiecloud"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="cookieCloudSetting.COOKIECLOUD_KEY"
|
||||
label="用户KEY"
|
||||
hint="在CookieCloud浏览器插件中生成"
|
||||
hint="CookieCloud浏览器插件生成的用户KEY"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -189,7 +178,8 @@ onMounted(() => {
|
||||
v-model="cookieCloudSetting.COOKIECLOUD_PASSWORD"
|
||||
type="password"
|
||||
label="端对端加密密码"
|
||||
hint="在CookieCloud浏览器插件中生成"
|
||||
hint="CookieCloud浏览器插件生成的端对端加密密码"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -197,32 +187,32 @@ onMounted(() => {
|
||||
v-model="cookieCloudSetting.COOKIECLOUD_INTERVAL"
|
||||
label="自动同步间隔"
|
||||
:items="CookieCloudIntervalItems"
|
||||
hint="设置定时从CookieCloud服务器同步站点Cookie到MoviePilot的时间周期"
|
||||
hint="从CookieCloud服务器自动同步站点Cookie到MoviePilot的时间间隔"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="cookieCloudSetting.USER_AGENT"
|
||||
label="浏览器User-Agent"
|
||||
hint="设置为CookieCloud插件所在的浏览器的User-Agent,用于模拟浏览器请求,正确填写后有助于提升站点访问成功率"
|
||||
hint="CookieCloud插件所在的浏览器的User-Agent"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveCookieCloudetting"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveCookieCloudetting"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="下载优先规则">
|
||||
<VCardSubtitle> 按站点或做种数量优先下载。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>下载优先规则</VCardTitle>
|
||||
<VCardSubtitle>按站点或做种数量优先下载。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -231,20 +221,16 @@ onMounted(() => {
|
||||
v-model="selectedTorrentPriority"
|
||||
:items="TorrentPriorityItems"
|
||||
label="当前使用下载优先规则"
|
||||
hint="站点优先:优先下载站点优先级最高的站点的种子;做种数优先:优先下载做种数量最多的种子。注意下载优先级仍然低于搜索和订阅中设定的优先级规则"
|
||||
hint="同时命中多个站点的多个资源时下载的优先规则"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveTorrentPriority"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveTorrentPriority"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
@@ -254,7 +240,8 @@ onMounted(() => {
|
||||
<VCheckbox
|
||||
v-model="isConfirmResetSites"
|
||||
label="确认删除所有站点数据并重新同步。"
|
||||
hint="删除所有站点数据并重新同步,站点图标短时间内会因数缓存而混乱,重启或者等待2两时自动恢复。"
|
||||
hint="删除所有站点数据并重新从CookieCloud同步,操作请先清空涉及站点的相关设置。"
|
||||
persistent-hint
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -311,10 +311,12 @@ onMounted(() => {
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="订阅站点">
|
||||
<VCardSubtitle> 只有选中的站点才会在订阅中使用。</VCardSubtitle>
|
||||
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>订阅站点</VCardTitle>
|
||||
<VCardSubtitle>只有选中的站点才会在订阅中使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VChipGroup v-model="selectedRssSites" column multiple>
|
||||
<VChip
|
||||
v-for="site in allSites"
|
||||
@@ -327,7 +329,7 @@ onMounted(() => {
|
||||
{{ site.name }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -336,7 +338,8 @@ onMounted(() => {
|
||||
v-model="selectedSubscribeMode"
|
||||
:items="subscribeModeItems"
|
||||
label="订阅模式"
|
||||
hint="自动:系统自动爬取站点首页资源;站点RSS:使用站点RSS订阅资源,站点RSS会自动获取,也可手动在站点管理中补全"
|
||||
hint="自动:自动爬取站点首页,站点RSS:通过站点RSS链接订阅"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -344,7 +347,8 @@ onMounted(() => {
|
||||
v-model="selectedRssInterval"
|
||||
:items="rssIntervalItems"
|
||||
label="站点RSS周期"
|
||||
hint="设置站点RSS运行周期,在订阅模式为站点RSS时生效"
|
||||
hint="设置站点RSS运行周期,在订阅模式为`站点RSS`时生效"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -353,42 +357,46 @@ onMounted(() => {
|
||||
<VSwitch
|
||||
v-model="enableIntervalSearch"
|
||||
label="开启订阅定时搜索"
|
||||
hint="开启后,系统每隔24小时将按名称搜索全站,补全订阅可能漏掉的资源"
|
||||
hint="每隔24小时全站搜索,以补全订阅可能漏掉的资源"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveSelectedRssSites"> 保存 </VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="订阅优先级">
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem variant="plain" @click="shareRules('SubscribeFilterRules')">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" @click="importRules('SubscribeFilterRules')">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardSubtitle> 设置在正常订阅时默认使用的优先级,未在优先级中的资源将不会自动下载。</VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem variant="plain" @click="shareRules('SubscribeFilterRules')">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" @click="importRules('SubscribeFilterRules')">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardTitle>订阅优先级</VCardTitle>
|
||||
<VCardSubtitle> 设置在正常订阅时默认使用的优先级,未在优先级中的资源将不会自动下载。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<draggable
|
||||
v-model="subscribeFilterCards"
|
||||
handle=".cursor-move"
|
||||
@@ -407,40 +415,43 @@ onMounted(() => {
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" class="me-2" @click="saveCustomFilters('SubscribeFilterRules')"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addFilterCard('SubscribeFilterRules')">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="洗版优先级">
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem variant="plain" @click="shareRules('BestVersionFilterRules')">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" @click="importRules('BestVersionFilterRules')">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardSubtitle> 设置在订阅洗版时使用的优先级,匹配优先级1时洗版完成。</VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>洗版优先级</VCardTitle>
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem variant="plain" @click="shareRules('BestVersionFilterRules')">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" @click="importRules('BestVersionFilterRules')">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardSubtitle> 设置在订阅洗版时使用的优先级,匹配优先级1时洗版完成。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<draggable
|
||||
v-model="bestVersionFilterCards"
|
||||
handle=".cursor-move"
|
||||
@@ -459,18 +470,21 @@ onMounted(() => {
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" class="me-2" @click="saveCustomFilters('BestVersionFilterRules')"> 保存 </VBtn>
|
||||
<VBtn color="success" variant="tonal" @click="addFilterCard('BestVersionFilterRules')">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="默认过滤规则">
|
||||
<VCardSubtitle> 设置在订阅时默认使用的过滤规则。</VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>默认过滤规则</VCardTitle>
|
||||
<VCardSubtitle> 设置在订阅时默认使用的过滤规则。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -479,7 +493,8 @@ onMounted(() => {
|
||||
v-model="defaultFilterRules.include"
|
||||
type="text"
|
||||
label="包含(关键字、正则式)"
|
||||
hint="支持正式表达式,多个关键字用 | 分隔表示或"
|
||||
hint="包含规则,支持正式表达式,多个关键字用 | 分隔表示或"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -487,7 +502,8 @@ onMounted(() => {
|
||||
v-model="defaultFilterRules.exclude"
|
||||
type="text"
|
||||
label="排除(关键字、正则式)"
|
||||
hint="支持正式表达式,多个关键字用 | 分隔表示或"
|
||||
hint="排除规则,支持正式表达式,多个关键字用 | 分隔表示或"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -496,7 +512,8 @@ onMounted(() => {
|
||||
type="text"
|
||||
label="电影文件大小(GB)"
|
||||
placeholder="0-30"
|
||||
hint="格式:0-30,表示0到30GB之间的资源"
|
||||
hint="文件大小范围,格式:0-30,表示0-30GB之间的资源"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -505,7 +522,8 @@ onMounted(() => {
|
||||
type="text"
|
||||
label="剧集单集文件大小(GB)"
|
||||
placeholder="0-10"
|
||||
hint="格式:0-10,表示0到10GB之间的资源"
|
||||
hint="单集文件大小范围,格式:0-10,表示0-10GB之间的资源"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
@@ -515,23 +533,25 @@ onMounted(() => {
|
||||
label="最小做种数"
|
||||
placeholder="0"
|
||||
hint="小于该值的资源将被过滤掉,0表示不过滤"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="defaultFilterRules.min_seeders_time"
|
||||
type="text"
|
||||
label="最少做种人数生效发布时间(分钟)"
|
||||
label="最少做种数生效发布时间(分钟)"
|
||||
placeholder="0"
|
||||
hint="发布时间距现在大于该值的资源将生效最小做种数规则,0表示不生效"
|
||||
hint="发布时间距当前时间大于该值的资源将生效最小做种数规则,0表示不生效"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveDefaultFilter"> 保存 </VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -539,10 +559,3 @@ onMounted(() => {
|
||||
<ImportCodeDialog v-model="importCodeString" title="导入优先级规则" @close="importCodeDialog = false" />
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-filterrule-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { VRow } from 'vuetify/lib/components/index.mjs'
|
||||
import api from '@/api'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
|
||||
// 选中的媒体服务器
|
||||
const selectedMediaServers = ref([])
|
||||
@@ -17,23 +16,6 @@ const downloaderTab = ref('qbittorrent')
|
||||
// 媒体服务器选中标签页
|
||||
const mediaserverTab = ref('emby')
|
||||
|
||||
// 媒体库设置项
|
||||
const mediaSettings = ref({
|
||||
SCRAP_METADATA: true,
|
||||
DOWNLOAD_PATH: '',
|
||||
DOWNLOAD_MOVIE_PATH: '',
|
||||
DOWNLOAD_TV_PATH: '',
|
||||
DOWNLOAD_ANIME_PATH: '',
|
||||
DOWNLOAD_CATEGORY: false,
|
||||
TRANSFER_TYPE: 'copy',
|
||||
OVERWRITE_MODE: 'size',
|
||||
LIBRARY_PATH: '',
|
||||
LIBRARY_MOVIE_NAME: '',
|
||||
LIBRARY_TV_NAME: '',
|
||||
LIBRARY_ANIME_NAME: '',
|
||||
LIBRARY_CATEGORY: false,
|
||||
})
|
||||
|
||||
// 下载器设置项
|
||||
const downloaderSettings = ref({
|
||||
DOWNLOADER_MONITOR: true,
|
||||
@@ -92,24 +74,6 @@ const MediaServers = [
|
||||
},
|
||||
]
|
||||
|
||||
// 转移方式字典
|
||||
const transferTypeItems = [
|
||||
{ title: '硬链接', value: 'link' },
|
||||
{ title: '复制', value: 'copy' },
|
||||
{ title: '移动', value: 'move' },
|
||||
{ title: '软链接', value: 'softlink' },
|
||||
{ title: 'rclone复制', value: 'rclone_copy' },
|
||||
{ title: 'rclone移动', value: 'rclone_move' },
|
||||
]
|
||||
|
||||
// 覆盖模式字典
|
||||
const overwriteModeItems = [
|
||||
{ title: '从不覆盖', value: 'never' },
|
||||
{ title: '按大小覆盖', value: 'size' },
|
||||
{ title: '总是覆盖', value: 'always' },
|
||||
{ title: '仅保留最新版本', value: 'latest' },
|
||||
]
|
||||
|
||||
// 媒体库同步周期字典
|
||||
const syncIntervalItems = [
|
||||
{ title: '从不', value: 0 },
|
||||
@@ -123,72 +87,11 @@ const syncIntervalItems = [
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 加载媒体库设置
|
||||
async function loadMediaSettings() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
if (result.success) {
|
||||
const {
|
||||
SCRAP_METADATA,
|
||||
DOWNLOAD_PATH,
|
||||
DOWNLOAD_MOVIE_PATH,
|
||||
DOWNLOAD_TV_PATH,
|
||||
DOWNLOAD_ANIME_PATH,
|
||||
DOWNLOAD_CATEGORY,
|
||||
TRANSFER_TYPE,
|
||||
OVERWRITE_MODE,
|
||||
LIBRARY_PATH,
|
||||
LIBRARY_MOVIE_NAME,
|
||||
LIBRARY_TV_NAME,
|
||||
LIBRARY_ANIME_NAME,
|
||||
LIBRARY_CATEGORY,
|
||||
} = result.data
|
||||
mediaSettings.value = {
|
||||
SCRAP_METADATA,
|
||||
DOWNLOAD_PATH,
|
||||
DOWNLOAD_MOVIE_PATH,
|
||||
DOWNLOAD_TV_PATH,
|
||||
DOWNLOAD_ANIME_PATH,
|
||||
DOWNLOAD_CATEGORY,
|
||||
TRANSFER_TYPE,
|
||||
OVERWRITE_MODE,
|
||||
LIBRARY_PATH,
|
||||
LIBRARY_MOVIE_NAME,
|
||||
LIBRARY_TV_NAME,
|
||||
LIBRARY_ANIME_NAME,
|
||||
LIBRARY_CATEGORY,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API保存媒体设置
|
||||
async function saveMediaSetting() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/env',
|
||||
mediaSettings.value,
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('保存媒体库设置成功')
|
||||
else
|
||||
$toast.error('保存媒体库设置失败!')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API查询下载器设置
|
||||
async function loadDownloaderSetting() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.get('system/setting/DOWNLOADER')
|
||||
if (result1.success)
|
||||
selectedDownloaders.value = result1.data?.value?.split(',')
|
||||
if (result1.success) selectedDownloaders.value = result1.data?.value?.split(',')
|
||||
|
||||
const result2: { [key: string]: any } = await api.get('system/env')
|
||||
if (result2.success) {
|
||||
@@ -219,8 +122,7 @@ async function loadDownloaderSetting() {
|
||||
TR_PASSWORD,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -232,18 +134,15 @@ async function saveDownloaderSetting() {
|
||||
'system/setting/DOWNLOADER',
|
||||
selectedDownloaders.value.join(','),
|
||||
)
|
||||
const result2: { [key: string]: any } = await api.post(
|
||||
'system/env',
|
||||
downloaderSettings.value,
|
||||
)
|
||||
const result2: { [key: string]: any } = await api.post('system/env', downloaderSettings.value)
|
||||
|
||||
if (result1.success && result2.success) {
|
||||
$toast.success('保存下载器设置成功')
|
||||
reloadModule()
|
||||
} else {
|
||||
$toast.error('保存下载器设置失败!')
|
||||
}
|
||||
else { $toast.error('保存下载器设置失败!') }
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -252,8 +151,7 @@ async function saveDownloaderSetting() {
|
||||
async function loadMediaServerSetting() {
|
||||
try {
|
||||
const result1: { [key: string]: any } = await api.get('system/setting/MEDIASERVER')
|
||||
if (result1.success)
|
||||
selectedMediaServers.value = result1.data?.value?.split(',')
|
||||
if (result1.success) selectedMediaServers.value = result1.data?.value?.split(',')
|
||||
|
||||
const result2: { [key: string]: any } = await api.get('system/env')
|
||||
if (result2.success) {
|
||||
@@ -284,8 +182,7 @@ async function loadMediaServerSetting() {
|
||||
PLEX_TOKEN,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -298,18 +195,15 @@ async function saveMediaServerSetting() {
|
||||
selectedMediaServers.value.join(','),
|
||||
)
|
||||
|
||||
const result2: { [key: string]: any } = await api.post(
|
||||
'system/env',
|
||||
mediaServerSettings.value,
|
||||
)
|
||||
const result2: { [key: string]: any } = await api.post('system/env', mediaServerSettings.value)
|
||||
|
||||
if (result1.success && result2.success) {
|
||||
$toast.success('保存媒体服务器设置成功')
|
||||
reloadModule()
|
||||
} else {
|
||||
$toast.error('保存媒体服务器设置失败!')
|
||||
}
|
||||
else { $toast.error('保存媒体服务器设置失败!') }
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -318,12 +212,9 @@ async function saveMediaServerSetting() {
|
||||
async function reloadModule() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success)
|
||||
$toast.success('重新加载模块成功')
|
||||
else
|
||||
$toast.error('重新加载模块失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('重新加载模块成功')
|
||||
else $toast.error('重新加载模块失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -332,15 +223,17 @@ async function reloadModule() {
|
||||
onMounted(() => {
|
||||
loadDownloaderSetting()
|
||||
loadMediaServerSetting()
|
||||
loadMediaSettings()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="下载器">
|
||||
<VCardSubtitle>只有选中的第1个下载器才会被默认使用。</VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>下载器</VCardTitle>
|
||||
<VCardSubtitle>只有选中的第1个下载器才会被默认使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -351,14 +244,16 @@ onMounted(() => {
|
||||
chips
|
||||
:items="Downloaders"
|
||||
label="当前使用下载器"
|
||||
hint="MoviePilot自动添加的下载任务将使用选中的第1个下载器"
|
||||
hint="启用下载器,只有第1个会被默认下载使用"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="downloaderSettings.TORRENT_TAG"
|
||||
label="下载器种子标签"
|
||||
hint="设置种子标签用于区分MoviePilot添加的下载任务,默认标签为`MOVIEPILOT`"
|
||||
hint="MoviePilot添加的下载任务标签"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -366,29 +261,19 @@ onMounted(() => {
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="downloaderSettings.DOWNLOADER_MONITOR"
|
||||
label="监控默认下载器"
|
||||
hint="监控选中的第1个下载器,当任务下载完成时自动整理文件到媒体库"
|
||||
label="下载文件自动整理"
|
||||
hint="任务下载完成时自动整理文件到媒体库"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTabs
|
||||
v-model="downloaderTab"
|
||||
stacked
|
||||
>
|
||||
<VTab value="qbittorrent">
|
||||
Qbittorrent
|
||||
</VTab>
|
||||
<VTab value="transmission">
|
||||
Transmission
|
||||
</VTab>
|
||||
<VTabs v-model="downloaderTab" stacked>
|
||||
<VTab value="qbittorrent"> Qbittorrent </VTab>
|
||||
<VTab value="transmission"> Transmission </VTab>
|
||||
</VTabs>
|
||||
<VWindow
|
||||
v-model="downloaderTab"
|
||||
class="mt-5 disable-tab-transition"
|
||||
:touch="false"
|
||||
>
|
||||
<VWindow v-model="downloaderTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<VWindowItem value="qbittorrent">
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -396,8 +281,9 @@ onMounted(() => {
|
||||
<VTextField
|
||||
v-model="downloaderSettings.QB_HOST"
|
||||
label="地址"
|
||||
placeholder="IP:PORT"
|
||||
hint="格式:IP:PORT,如启用了HTTPS,请使用https://IP:PORT"
|
||||
placeholder="http(s)://ip:port"
|
||||
hint="服务端地址,格式:http(s)://ip:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -405,7 +291,8 @@ onMounted(() => {
|
||||
v-model="downloaderSettings.QB_USER"
|
||||
label="用户名"
|
||||
placeholder="admin"
|
||||
hint="QB的登录用户名"
|
||||
hint="登录使用的用户名"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -413,28 +300,32 @@ onMounted(() => {
|
||||
v-model="downloaderSettings.QB_PASSWORD"
|
||||
type="password"
|
||||
label="密码"
|
||||
hint="QB的登录密码"
|
||||
hint="登录使用的密码"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="downloaderSettings.QB_CATEGORY"
|
||||
label="自动分类管理"
|
||||
hint="开启后,下载目录将由QB控制自动下载到分类到目录,此时MoviePilot的下载目录设定无效,需在QB中提前创建分类"
|
||||
hint="由下载器自动管理分类和下载目录"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="downloaderSettings.QB_SEQUENTIAL"
|
||||
label="顺序下载"
|
||||
hint="开启后QB将按照文件顺序依次下载"
|
||||
hint="按顺序依次下载文件"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSwitch
|
||||
v-model="downloaderSettings.QB_FORCE_RESUME"
|
||||
label="强制继续"
|
||||
hint="开启后,QB将设置为强制继续、强制上传模式(带[F]标识)"
|
||||
hint="强制继续、强制上传模式"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -447,8 +338,9 @@ onMounted(() => {
|
||||
<VTextField
|
||||
v-model="downloaderSettings.TR_HOST"
|
||||
label="地址"
|
||||
placeholder="IP:PORT"
|
||||
hint="格式:IP:PORT,如启用了HTTPS,请使用https://IP:PORT"
|
||||
placeholder="http(s)://ip:port"
|
||||
hint="服务端地址,格式:http(s)://ip:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -456,7 +348,8 @@ onMounted(() => {
|
||||
v-model="downloaderSettings.TR_USER"
|
||||
label="用户名"
|
||||
placeholder="admin"
|
||||
hint="TR的登录用户名"
|
||||
hint="登录使用的用户名"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -464,7 +357,8 @@ onMounted(() => {
|
||||
v-model="downloaderSettings.TR_PASSWORD"
|
||||
type="password"
|
||||
label="密码"
|
||||
hint="TR的登录密码"
|
||||
hint="登录使用的密码"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -478,12 +372,7 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn
|
||||
mtype="submit"
|
||||
@click="saveDownloaderSetting"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
<VBtn mtype="submit" @click="saveDownloaderSetting"> 保存 </VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -492,8 +381,11 @@ onMounted(() => {
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="媒体服务器">
|
||||
<VCardSubtitle>只有选中的媒体服务器才会被默认使用。</VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>媒体服务器</VCardTitle>
|
||||
<VCardSubtitle>只有选中的媒体服务器才会被默认使用。</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -504,7 +396,8 @@ onMounted(() => {
|
||||
chips
|
||||
:items="MediaServers"
|
||||
label="当前使用媒体服务器"
|
||||
hint="媒体服务器用于搜索下载等判断库中是否已存在,以避免重复下载"
|
||||
hint="启用媒体服务器,入库展示、下载控重等将使用"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -512,7 +405,8 @@ onMounted(() => {
|
||||
v-model="mediaServerSettings.MEDIASERVER_SYNC_INTERVAL"
|
||||
:items="syncIntervalItems"
|
||||
label="同步周期"
|
||||
hint="设置后数据将定时同步到MoviePilot数据库,以便展示媒体库是否存在标识"
|
||||
hint="同步媒体库数据到MoviePilot的时间间隔"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -520,31 +414,19 @@ onMounted(() => {
|
||||
v-model="mediaServerSettings.MEDIASERVER_SYNC_BLACKLIST"
|
||||
label="媒体库同步黑名单"
|
||||
placeholder="使用,分隔"
|
||||
hint="设置不同步数据的媒体库名称,使用,分隔,如:电影,电视剧"
|
||||
hint="不同步数据的媒体库名称,多个使用,分隔"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTabs
|
||||
v-model="mediaserverTab"
|
||||
stacked
|
||||
>
|
||||
<VTab value="emby">
|
||||
Emby
|
||||
</VTab>
|
||||
<VTab value="jellyfin">
|
||||
Jellyfin
|
||||
</VTab>
|
||||
<VTab value="plex">
|
||||
Plex
|
||||
</vtab>
|
||||
<VTabs v-model="mediaserverTab" stacked>
|
||||
<VTab value="emby"> Emby </VTab>
|
||||
<VTab value="jellyfin"> Jellyfin </VTab>
|
||||
<VTab value="plex"> Plex </VTab>
|
||||
</VTabs>
|
||||
<VWindow
|
||||
v-model="mediaserverTab"
|
||||
class="mt-5 disable-tab-transition"
|
||||
:touch="false"
|
||||
>
|
||||
<VWindow v-model="mediaserverTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<VWindowItem value="emby">
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -552,8 +434,9 @@ onMounted(() => {
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.EMBY_HOST"
|
||||
label="地址"
|
||||
placeholder="IP:PORT"
|
||||
hint="格式:IP:PORT 或 http(s)://IP:PORT/"
|
||||
placeholder="http(s)://ip:port"
|
||||
hint="服务端地址,格式:http(s)://ip:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -561,14 +444,16 @@ onMounted(() => {
|
||||
v-model="mediaServerSettings.EMBY_PLAY_HOST"
|
||||
label="外网播放地址"
|
||||
placeholder="http(s)://domain:port"
|
||||
hint="格式:http(s)://domain:port,设置后跳转Emby时将优先使用此地址"
|
||||
hint="跳转播放页面使用的地址,格式:http(s)://domain:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.EMBY_API_KEY"
|
||||
label="API密钥"
|
||||
hint="Emby的API密钥,在 Emby设置->高级->API 密钥 中生成"
|
||||
hint="Emby设置->高级->API密钥中生成的密钥"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -581,8 +466,9 @@ onMounted(() => {
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.JELLYFIN_HOST"
|
||||
label="地址"
|
||||
placeholder="IP:PORT"
|
||||
hint="格式:IP:PORT 或 http(s)://IP:PORT/"
|
||||
placeholder="http(s)://ip:port"
|
||||
hint="服务端地址,格式:http(s)://ip:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -590,14 +476,16 @@ onMounted(() => {
|
||||
v-model="mediaServerSettings.JELLYFIN_PLAY_HOST"
|
||||
label="外网播放地址"
|
||||
placeholder="http(s)://domain:port"
|
||||
hint="格式:http(s)://domain:port,设置后跳转Jellyfin时将优先使用此地址"
|
||||
hint="跳转播放页面使用的地址,格式:http(s)://domain:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.JELLYFIN_API_KEY"
|
||||
label="API密钥"
|
||||
hint="Jellyfin的API密钥,在 Jellyfin设置->高级->API 密钥 中生成"
|
||||
hint="Jellyfin设置->高级->API密钥中生成的密钥"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -610,8 +498,9 @@ onMounted(() => {
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.PLEX_HOST"
|
||||
label="地址"
|
||||
placeholder="IP:PORT"
|
||||
hint="格式:IP:PORT 或 http(s)://IP:PORT/"
|
||||
placeholder="http(s)://ip:port"
|
||||
hint="服务端地址,格式:http(s)://ip:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
@@ -619,14 +508,16 @@ onMounted(() => {
|
||||
v-model="mediaServerSettings.PLEX_PLAY_HOST"
|
||||
label="外网播放地址"
|
||||
placeholder="http(s)://domain:port"
|
||||
hint="格式:http(s)://domain:port,设置后跳转Plex时将优先使用此地址"
|
||||
hint="跳转播放页面使用的地址,格式:http(s)://domain:port"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="mediaServerSettings.PLEX_TOKEN"
|
||||
label="API密钥"
|
||||
hint="Plex网页Url中的X-Plex-Token,通过浏览器F12->网络从请求URL中获取"
|
||||
label="X-Plex-Token"
|
||||
hint="浏览器F12->网络,从Plex请求URL中获取的X-Plex-Token"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -640,140 +531,7 @@ onMounted(() => {
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn
|
||||
mtype="submit"
|
||||
@click="saveMediaServerSetting"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="媒体库">
|
||||
<VCardSubtitle>设置下载目录、媒体库目录以及整理方式。</VCardSubtitle>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.DOWNLOAD_PATH"
|
||||
label="下载目录"
|
||||
:rules="[requiredValidator]"
|
||||
hint="MoviePilot添加的下载任务的默认保存目录,必须设置"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.DOWNLOAD_MOVIE_PATH"
|
||||
label="电影下载目录"
|
||||
hint="为电影设置单独的下载保存目录,不设置则使用下载目录"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.DOWNLOAD_TV_PATH"
|
||||
label="电视剧下载目录"
|
||||
hint="为电视剧设置单独的下载保存目录,不设置则使用下载目录"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.DOWNLOAD_ANIME_PATH"
|
||||
label="动漫下载目录"
|
||||
hint="为动漫设置单独的下载保存目录,不设置则使用下载目录"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="mediaSettings.DOWNLOAD_CATEGORY"
|
||||
label="下载目录自动分类"
|
||||
hint="开启后,下载任务保存目录将根据二级分类策略自动分类存放到下载目录的二级子目录中,二级分类策略需要编辑配置文件目录下的`category.yml`文件,插件市场有提供文件编辑插件"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="mediaSettings.TRANSFER_TYPE"
|
||||
:items="transferTypeItems"
|
||||
label="整理方式"
|
||||
hint="硬链接需要确保下载目录和媒体库目录不跨盘、不跨共享目录、不分别映射;rclone需要手动在容器中完成配置,且配置名为:`MP`"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="mediaSettings.OVERWRITE_MODE"
|
||||
:items="overwriteModeItems"
|
||||
label="覆盖模式"
|
||||
hint="从不覆盖:不覆盖已存在的文件;按大小覆盖:大文件将覆盖小文件;总是覆盖:总是覆盖已存在的文件;仅保留最新版本:保留最新版本的文件,删除其它版本的文件"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="mediaSettings.SCRAP_METADATA"
|
||||
label="自动刮削媒体信息"
|
||||
hint="开启后,整理完成后将自动刮削媒体信息,如海报、简介等"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.LIBRARY_PATH"
|
||||
label="媒体库目录"
|
||||
placeholder="多个目录使用,分隔"
|
||||
:rules="[requiredValidator]"
|
||||
hint="整理完成后的媒体文件存放的根目录,所有整理场景下未设定目的目录时都将整理到该目录下,必须设置"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.LIBRARY_MOVIE_NAME"
|
||||
label="电影目录名称"
|
||||
placeholder="电影"
|
||||
hint="设置电影的存放一级目录名称,不设置则使用使用`电影`做为目录名称"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.LIBRARY_TV_NAME"
|
||||
label="电视剧目录名称"
|
||||
placeholder="电视剧"
|
||||
hint="设置电视剧的存放一级目录名称,不设置则使用使用`电视剧`做为目录名称"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaSettings.LIBRARY_ANIME_NAME"
|
||||
label="动漫目录名称"
|
||||
placeholder="动漫"
|
||||
hint="设置动漫的存放一级目录名称,不设置则使用使用`动漫`做为目录名称"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="mediaSettings.LIBRARY_CATEGORY"
|
||||
label="媒体库目录自动分类"
|
||||
hint="开启后,整理完成后的媒体文件将根据二级分类策略自动分类存放到媒体库一级目录的二级子目录中,二级分类策略需要编辑配置文件目录下的`category.yml`文件,插件市场有提供文件编辑插件"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||
<VBtn
|
||||
mtype="submit"
|
||||
@click="saveMediaSetting"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
<VBtn mtype="submit" @click="saveMediaServerSetting"> 保存 </VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
@@ -20,13 +20,9 @@ const transferExcludeWords = ref('')
|
||||
// 查询已设置的识别词
|
||||
async function queryCustomIdentifiers() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/CustomIdentifiers',
|
||||
)
|
||||
|
||||
customIdentifiers.value = result.data?.value.join('\n')
|
||||
}
|
||||
catch (error) {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/CustomIdentifiers')
|
||||
if (result && result.data && result.data.value) customIdentifiers.value = result.data.value.join('\n')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -34,13 +30,9 @@ async function queryCustomIdentifiers() {
|
||||
// 查询已设置的制作组
|
||||
async function queryCustomReleaseGroups() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/CustomReleaseGroups',
|
||||
)
|
||||
|
||||
customReleaseGroups.value = result.data?.value.join('\n')
|
||||
}
|
||||
catch (error) {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/CustomReleaseGroups')
|
||||
if (result && result.data && result.data.value) customReleaseGroups.value = result.data.value.join('\n')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -48,13 +40,9 @@ async function queryCustomReleaseGroups() {
|
||||
// 查询已设置的自定义占位符
|
||||
async function queryCustomization() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/Customization',
|
||||
)
|
||||
|
||||
customization.value = result.data?.value.join('\n')
|
||||
}
|
||||
catch (error) {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/Customization')
|
||||
if (result && result.data && result.data.value) customization.value = result.data?.value.join('\n')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -62,13 +50,9 @@ async function queryCustomization() {
|
||||
// 查询已设置的屏蔽词
|
||||
async function queryTransferExcludeWords() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/TransferExcludeWords',
|
||||
)
|
||||
|
||||
transferExcludeWords.value = result.data?.value.join('\n')
|
||||
}
|
||||
catch (error) {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/TransferExcludeWords')
|
||||
if (result && result.data && result.data.value) transferExcludeWords.value = result.data?.value.join('\n')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -82,12 +66,9 @@ async function saveCustomIdentifiers() {
|
||||
customIdentifiers.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('自定义识别词保存成功')
|
||||
else
|
||||
$toast.error('自定义识别词保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('自定义识别词保存成功')
|
||||
else $toast.error('自定义识别词保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -101,12 +82,9 @@ async function saveCustomReleaseGroups() {
|
||||
customReleaseGroups.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('自定义制作组/字幕组保存成功')
|
||||
else
|
||||
$toast.error('自定义制作组/字幕组保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('自定义制作组/字幕组保存成功')
|
||||
else $toast.error('自定义制作组/字幕组保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -120,12 +98,9 @@ async function saveCustomization() {
|
||||
customization.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('自定义占位符保存成功')
|
||||
else
|
||||
$toast.error('自定义占位符保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('自定义占位符保存成功')
|
||||
else $toast.error('自定义占位符保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -139,12 +114,9 @@ async function saveTransferExcludeWords() {
|
||||
transferExcludeWords.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('文件整理屏蔽词保存成功')
|
||||
else
|
||||
$toast.error('文件整理屏蔽词保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
if (result.success) $toast.success('文件整理屏蔽词保存成功')
|
||||
else $toast.error('文件整理屏蔽词保存失败!')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
@@ -160,104 +132,98 @@ onMounted(() => {
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="自定义识别词">
|
||||
<VCardSubtitle> 添加规则对种子名或者文件名进行预处理以校正识别。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>自定义识别词</VCardTitle>
|
||||
<VCardSubtitle> 添加规则对种子名或者文件名进行预处理以校正识别。 </VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextarea
|
||||
v-model="customIdentifiers"
|
||||
auto-grow
|
||||
placeholder="支持正则表达式,特殊字符需要\转义,一行为一组"
|
||||
hint="支持正则表达式,特殊字符需要\转义,一行为一组"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VAlert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
title="支持的配置格式(注意空格):"
|
||||
>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VAlert type="info" variant="tonal" title="支持的配置格式(注意空格):">
|
||||
<span
|
||||
v-html="`
|
||||
v-html="
|
||||
`
|
||||
屏蔽词<br>
|
||||
被替换词 => 替换词<br>
|
||||
前定位词 <> 后定位词 >> 集偏移量(EP)<br>
|
||||
被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量(EP)<br>
|
||||
其中替换词支持格式:{[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]} 直接指定TMDBID/豆瓣ID识别,其中s、e为季数和集数(可选)<br>
|
||||
`"
|
||||
`
|
||||
"
|
||||
/>
|
||||
</VAlert>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveCustomIdentifiers"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveCustomIdentifiers"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="自定义制作组/字幕组">
|
||||
<VCardSubtitle> 添加无法识别的制作组/字幕组。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>自定义制作组/字幕组</VCardTitle>
|
||||
<VCardSubtitle> 添加无法识别的制作组/字幕组。 </VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextarea
|
||||
v-model="customReleaseGroups"
|
||||
auto-grow
|
||||
placeholder="支持正则表达式,特殊字符需要\转义,一行代表一个制作组/字幕组"
|
||||
hint="支持正则表达式,特殊字符需要\转义,一行代表一个制作组/字幕组"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveCustomReleaseGroups"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveCustomReleaseGroups"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="自定义占位符">
|
||||
<VCardSubtitle> 添加自定义占位符识别正则,重命名格式中添加{customization}使用。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>自定义占位符</VCardTitle>
|
||||
<VCardSubtitle> 添加自定义占位符识别正则,重命名格式中添加{customization}使用。 </VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextarea
|
||||
v-model="customization"
|
||||
auto-grow
|
||||
placeholder="多个匹配对象请换行分隔,支持正则表达式,特殊字符注意转义"
|
||||
hint="多个匹配对象请换行分隔,支持正则表达式,特殊字符注意转义"
|
||||
placeholder="支持正则表达式,特殊字符需要\转义,多个匹配对象请换行分隔"
|
||||
hint="支持正则表达式,特殊字符需要\转义,多个匹配对象请换行分隔"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveCustomization"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveCustomization"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="文件整理屏蔽词">
|
||||
<VCardSubtitle> 目录名或文件名中包含屏蔽词时不进行整理。 </VCardSubtitle>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>文件整理屏蔽词</VCardTitle>
|
||||
<VCardSubtitle> 目录名或文件名中包含屏蔽词时不进行整理。 </VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextarea
|
||||
v-model="transferExcludeWords"
|
||||
auto-grow
|
||||
placeholder="支持正则表达式,特殊字符需要\转义,一行代表一个屏蔽词"
|
||||
hint="支持正则表达式,特殊字符需要\转义,一行代表一个屏蔽词"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveTransferExcludeWords"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VBtn type="submit" @click="saveTransferExcludeWords"> 保存 </VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import draggable from 'vuedraggable'
|
||||
import api from '@/api'
|
||||
import type { Site } from '@/api/types'
|
||||
import SiteCard from '@/components/cards/SiteCard.vue'
|
||||
@@ -24,16 +25,40 @@ async function fetchData() {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存站点排序
|
||||
async function savaSitesPriority() {
|
||||
// 重新排序
|
||||
const priorities = dataList.value.map((site, index) => ({ id: site.id, pri: index + 1 }))
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('site/priorities', priorities)
|
||||
if (result.success) {
|
||||
fetchData()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(fetchData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<div v-if="dataList.length > 0" class="grid gap-3 grid-site-card">
|
||||
<div v-for="(data, index) in dataList" :key="index">
|
||||
<SiteCard :key="data.id" :site="data" @remove="fetchData" @update="fetchData" />
|
||||
</div>
|
||||
<div>
|
||||
<draggable
|
||||
v-if="dataList.length > 0"
|
||||
v-model="dataList"
|
||||
@end="savaSitesPriority"
|
||||
handle=".cursor-move"
|
||||
item-key="id"
|
||||
tag="div"
|
||||
:component-data="{ 'class': 'grid gap-3 grid-site-card' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<SiteCard :site="element" @remove="fetchData" @update="fetchData" />
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
@@ -42,7 +67,7 @@ onBeforeMount(fetchData)
|
||||
error-description="已添加并支持的站点将会在这里显示。"
|
||||
/>
|
||||
<!-- 新增站点按钮 -->
|
||||
<VFab icon="mdi-plus" location="bottom end" size="x-large" fixed app appear @click="siteAddDialog = true" />
|
||||
<VFab icon="mdi-plus" location="bottom" size="x-large" fixed app appear @click="siteAddDialog = true" />
|
||||
<!-- 新增站点弹窗 -->
|
||||
<SiteAddEditDialog
|
||||
v-if="siteAddDialog"
|
||||
@@ -57,10 +82,3 @@ onBeforeMount(fetchData)
|
||||
@close="siteAddDialog = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-site-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import PullRefresh from 'pull-refresh-vue3'
|
||||
import { VPullToRefresh } from 'vuetify/labs/VPullToRefresh'
|
||||
import api from '@/api'
|
||||
import type { Subscribe } from '@/api/types'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
@@ -11,6 +11,7 @@ import store from '@/store'
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
type: String,
|
||||
subid: String,
|
||||
})
|
||||
|
||||
// 是否刷新过
|
||||
@@ -35,9 +36,6 @@ async function fetchData() {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(fetchData)
|
||||
|
||||
// 刷新状态
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -56,11 +54,23 @@ const filteredDataList = computed(() => {
|
||||
if (superUser) return dataList.value.filter(data => data.type === props.type)
|
||||
else return dataList.value.filter(data => data.type === props.type && data.username === userName)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData()
|
||||
if (props.subid) {
|
||||
// 找到这个订阅
|
||||
const sub = dataList.value.find(sub => sub.id.toString() == props.subid?.toString())
|
||||
if (sub) {
|
||||
// 打开编辑弹窗
|
||||
sub.page_open = true
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<PullRefresh v-model="loading" @refresh="onRefresh">
|
||||
<VPullToRefresh v-model="loading" @load="onRefresh">
|
||||
<div v-if="filteredDataList.length > 0" class="mx-3 grid gap-4 grid-subscribe-card p-1">
|
||||
<SubscribeCard
|
||||
v-for="data in filteredDataList"
|
||||
@@ -76,12 +86,12 @@ const filteredDataList = computed(() => {
|
||||
error-title="没有订阅"
|
||||
error-description="请通过搜索添加电影、电视剧订阅。"
|
||||
/>
|
||||
</PullRefresh>
|
||||
</VPullToRefresh>
|
||||
<!-- 底部操作按钮 -->
|
||||
<VFab
|
||||
v-if="store.state.auth.superUser"
|
||||
icon="mdi-clipboard-edit"
|
||||
location="bottom end"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
@@ -92,8 +102,8 @@ const filteredDataList = computed(() => {
|
||||
v-if="store.state.auth.superUser"
|
||||
icon="mdi-history"
|
||||
color="info"
|
||||
location="bottom end"
|
||||
class="mb-2"
|
||||
location="bottom"
|
||||
class="mb-16"
|
||||
size="x-large"
|
||||
fixed
|
||||
app
|
||||
@@ -123,10 +133,3 @@ const filteredDataList = computed(() => {
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-subscribe-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -129,13 +129,3 @@ async function fetchData({ done }: { done: any }) {
|
||||
/>
|
||||
</VInfiniteScroll>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
|
||||
.v-tabs:not(.v-tabs-pill).v-tabs--horizontal {
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
</style>
|
||||
|
||||
478
src/views/system/SearchBarView.vue
Normal file
478
src/views/system/SearchBarView.vue
Normal file
@@ -0,0 +1,478 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { Plugin, Subscribe } from '@/api/types'
|
||||
import {
|
||||
SystemNavMenus,
|
||||
UserfulMenus,
|
||||
SubscribeMovieTabs,
|
||||
SubscribeTvTabs,
|
||||
PluginTabs,
|
||||
SettingTabs,
|
||||
} from '@/router/menu'
|
||||
import { NavMenu } from '@/@layouts/types'
|
||||
|
||||
// 路由
|
||||
const router = useRouter()
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
// 搜索词
|
||||
const searchWord = ref<string | null>(null)
|
||||
|
||||
// ref
|
||||
const searchWordInput = ref<HTMLElement | null>(null)
|
||||
|
||||
// 近期搜索词条
|
||||
const recentSearches = ref<string[]>([])
|
||||
|
||||
// 保存近期搜索到本地
|
||||
function saveRecentSearches(keyword: string) {
|
||||
if (!keyword) return
|
||||
if (recentSearches.value.includes(keyword)) return
|
||||
recentSearches.value.unshift(keyword)
|
||||
localStorage.setItem('MP_RecentSearches', JSON.stringify(recentSearches.value))
|
||||
}
|
||||
|
||||
// 从本地加载近期搜索
|
||||
function loadRecentSearches() {
|
||||
const recentSearchesStr = localStorage.getItem('MP_RecentSearches')
|
||||
if (recentSearchesStr) {
|
||||
recentSearches.value = JSON.parse(recentSearchesStr)
|
||||
// 只保留最近的 5 条
|
||||
if (recentSearches.value.length > 5) {
|
||||
recentSearches.value = recentSearches.value.slice(0, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有菜单功能
|
||||
function getMenus(): NavMenu[] {
|
||||
let menus: NavMenu[] = []
|
||||
// 导航菜单
|
||||
SystemNavMenus.forEach(
|
||||
item =>
|
||||
item &&
|
||||
menus.push({
|
||||
title: item.title,
|
||||
icon: item.icon,
|
||||
to: item.to,
|
||||
header: item.header,
|
||||
admin: item.admin,
|
||||
}),
|
||||
)
|
||||
// 各类标签页
|
||||
SettingTabs.forEach(
|
||||
item =>
|
||||
item &&
|
||||
menus.push({
|
||||
title: '设定 -> ' + item.title,
|
||||
icon: item.icon,
|
||||
to: `/setting?tab=${item.tab}`,
|
||||
header: '',
|
||||
admin: true,
|
||||
description: item.description,
|
||||
}),
|
||||
)
|
||||
SubscribeMovieTabs.forEach(
|
||||
item =>
|
||||
item &&
|
||||
menus.push({
|
||||
title: '电影 -> ' + item.title,
|
||||
icon: item.icon,
|
||||
to: `/subscribe-movie?tab=${item.tab}`,
|
||||
header: '',
|
||||
admin: false,
|
||||
}),
|
||||
)
|
||||
SubscribeTvTabs.forEach(
|
||||
item =>
|
||||
item &&
|
||||
menus.push({
|
||||
title: '电视剧 -> ' + item.title,
|
||||
icon: item.icon,
|
||||
to: `/subscribe-tv?tab=${item.tab}`,
|
||||
header: '',
|
||||
admin: false,
|
||||
}),
|
||||
)
|
||||
PluginTabs.forEach(
|
||||
item =>
|
||||
item &&
|
||||
menus.push({
|
||||
title: '插件 -> ' + item.title,
|
||||
icon: item.icon,
|
||||
to: `/plugins?tab=${item.tab}`,
|
||||
header: '',
|
||||
admin: true,
|
||||
}),
|
||||
)
|
||||
|
||||
return menus
|
||||
}
|
||||
|
||||
// 匹配的菜单列表
|
||||
const matchedMenuItems = computed(() => {
|
||||
if (!searchWord.value) return []
|
||||
const lowerWord = (searchWord.value as string).toLowerCase()
|
||||
const menuItems = getMenus()
|
||||
if (menuItems)
|
||||
return menuItems.filter(
|
||||
item =>
|
||||
item.title.toLowerCase().includes(lowerWord) ||
|
||||
(item.description && item.description.toLowerCase().includes(lowerWord)),
|
||||
)
|
||||
return []
|
||||
})
|
||||
|
||||
// 所有插件(已安装)
|
||||
const pluginItems = ref<Plugin[]>([])
|
||||
|
||||
// 获取插件列表数据
|
||||
async function fetchInstalledPlugins() {
|
||||
try {
|
||||
pluginItems.value = await api.get('plugin/', {
|
||||
params: {
|
||||
state: 'installed',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 区配的插件列表
|
||||
const matchedPluginItems = computed(() => {
|
||||
if (!searchWord.value) return []
|
||||
const lowerWord = (searchWord.value as string).toLowerCase()
|
||||
return pluginItems.value.filter((item: Plugin) => {
|
||||
if (!item.plugin_name && !item.plugin_desc) return false
|
||||
return item.plugin_name?.toLowerCase().includes(lowerWord) || item.plugin_desc?.toLowerCase().includes(lowerWord)
|
||||
})
|
||||
})
|
||||
|
||||
// 所有订阅数据
|
||||
const SubscribeItems = ref<Subscribe[]>([])
|
||||
|
||||
// 获取电影订阅列表数据
|
||||
async function fetchSubscribes() {
|
||||
try {
|
||||
SubscribeItems.value = await api.get('subscribe/')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 匹配的订阅列表
|
||||
const matchedSubscribeItems = computed(() => {
|
||||
if (!searchWord.value) return []
|
||||
const lowerWord = (searchWord.value as string).toLowerCase()
|
||||
return SubscribeItems.value.filter((item: Subscribe) => {
|
||||
return item.name.toLowerCase().includes(lowerWord)
|
||||
})
|
||||
})
|
||||
|
||||
// 跳转媒体搜索页面
|
||||
function searchMedia(searchType: string) {
|
||||
// 搜索类型 media/person
|
||||
if (!searchWord.value) return
|
||||
saveRecentSearches(searchWord.value)
|
||||
router.push({
|
||||
path: '/browse/media/search',
|
||||
query: {
|
||||
title: searchWord.value,
|
||||
type: searchType,
|
||||
},
|
||||
})
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 跳转到种子搜索页面
|
||||
function searchTorrent() {
|
||||
if (!searchWord.value) return
|
||||
saveRecentSearches(searchWord.value)
|
||||
router.push({
|
||||
path: '/resource',
|
||||
query: {
|
||||
keyword: searchWord.value,
|
||||
area: 'title',
|
||||
},
|
||||
})
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 跳转到历史记录页面
|
||||
function searchHistory() {
|
||||
if (!searchWord.value) return
|
||||
saveRecentSearches(searchWord.value)
|
||||
router.push({
|
||||
path: '/history',
|
||||
query: {
|
||||
search: searchWord.value,
|
||||
},
|
||||
})
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 跳转插件页面
|
||||
function showPlugin(pluginId: string) {
|
||||
router.push({
|
||||
path: `/plugins/`,
|
||||
query: {
|
||||
tab: 'installed',
|
||||
id: pluginId,
|
||||
},
|
||||
})
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 跳转菜单页面
|
||||
function goPage(to: string) {
|
||||
router.push(to)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 跳转订阅页面
|
||||
function goSubscribe(subscribe: Subscribe) {
|
||||
if (subscribe.type === '电影') {
|
||||
router.push({
|
||||
path: '/subscribe-movie',
|
||||
query: {
|
||||
id: subscribe.id,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
router.push({
|
||||
path: '/subscribe-tv',
|
||||
query: {
|
||||
id: subscribe.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
emit('close')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
searchWordInput.value?.focus()
|
||||
}, 500)
|
||||
fetchInstalledPlugins()
|
||||
fetchSubscribes()
|
||||
loadRecentSearches()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VDialog max-width="40rem" scrollable>
|
||||
<VCard>
|
||||
<VCardItem class="pe-12">
|
||||
<VCombobox
|
||||
ref="searchWordInput"
|
||||
v-model="searchWord"
|
||||
density="compact"
|
||||
variant="plain"
|
||||
class="text-high-emphasis"
|
||||
placeholder="搜索 ..."
|
||||
@keydown.enter="searchMedia('media')"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="ri-search-line" style="opacity: 1" />
|
||||
</template>
|
||||
</VCombobox>
|
||||
</VCardItem>
|
||||
<DialogCloseBtn inner-class="absolute right-3 top-5 text-high-emphasis" @click="emit('close')" />
|
||||
<VDivider />
|
||||
<VCardText class="p-0">
|
||||
<VList lines="one" v-if="searchWord">
|
||||
<!-- 搜索结果 -->
|
||||
<VListSubheader v-if="searchWord"> 媒体 & 资源 </VListSubheader>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
prepend-icon="mdi-movie-search"
|
||||
density="compact"
|
||||
link
|
||||
v-bind="hover.props"
|
||||
@click="searchMedia('media')"
|
||||
>
|
||||
<VListItemTitle>
|
||||
搜索 <span class="font-bold">{{ searchWord }} </span> 相关的【电影、电视剧】 ...
|
||||
</VListItemTitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VListItem prepend-icon="mdi-account-search" link v-bind="hover.props" @click="searchMedia('person')">
|
||||
<VListItemTitle>
|
||||
搜索 <span class="font-bold">{{ searchWord }}</span> 相关的【演职人员】 ...
|
||||
</VListItemTitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VListItem prepend-icon="mdi-search-web" link v-bind="hover.props" @click="searchTorrent">
|
||||
<VListItemTitle>
|
||||
搜索 <span class="font-bold">{{ searchWord }}</span> 相关的【站点资源】 ...
|
||||
</VListItemTitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VListItem prepend-icon="mdi-history" link v-bind="hover.props" @click="searchHistory">
|
||||
<VListItemTitle>
|
||||
搜索 <span class="font-bold">{{ searchWord }}</span> 相关的【历史记录】 ...
|
||||
</VListItemTitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
<VListSubheader v-if="matchedSubscribeItems.length > 0"> 订阅 </VListSubheader>
|
||||
<VHover
|
||||
v-if="matchedSubscribeItems.length > 0"
|
||||
v-for="subscribe in matchedSubscribeItems"
|
||||
:key="subscribe.id"
|
||||
>
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
:prepend-icon="`${subscribe.type === '电影' ? 'mdi-movie-roll' : 'mdi-television-classic'}`"
|
||||
density="compact"
|
||||
link
|
||||
v-bind="hover.props"
|
||||
@click="goSubscribe(subscribe)"
|
||||
>
|
||||
<VListItemTitle>
|
||||
{{ subscribe.name }}<span v-if="subscribe.season"> 第 {{ subscribe.season }} 季</span>
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle> {{ subscribe.type }}</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
<VListSubheader v-if="matchedMenuItems.length > 0"> 功能 </VListSubheader>
|
||||
<VHover v-if="matchedMenuItems.length > 0" v-for="menu in matchedMenuItems" :key="menu.title">
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
:prepend-icon="menu.icon as string"
|
||||
density="compact"
|
||||
link
|
||||
v-bind="hover.props"
|
||||
@click="goPage(menu.to as string)"
|
||||
>
|
||||
<VListItemTitle>
|
||||
{{ menu.title }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle v-if="menu.description"> {{ menu.description }} </VListItemSubtitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
<VListSubheader v-if="matchedPluginItems.length > 0"> 插件 </VListSubheader>
|
||||
<VHover v-if="matchedPluginItems.length > 0" v-for="plugin in matchedPluginItems" :key="plugin.id">
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
prepend-icon="mdi-puzzle"
|
||||
density="compact"
|
||||
link
|
||||
v-bind="hover.props"
|
||||
@click="showPlugin(plugin.id ?? '')"
|
||||
>
|
||||
<VListItemTitle> {{ plugin.plugin_name }} </VListItemTitle>
|
||||
<VListItemSubtitle> {{ plugin.plugin_desc }} </VListItemSubtitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
</VList>
|
||||
<div v-else>
|
||||
<!-- 默认 -->
|
||||
<VCardText>
|
||||
<VRow v-if="recentSearches.length > 0">
|
||||
<VCol cols="12">
|
||||
<p class="custom-letter-spacing text-sm text-disabled text-uppercase py-2 px-4 mb-0">最近搜索</p>
|
||||
<div class="px-3">
|
||||
<VChip
|
||||
v-for="(word, index) in recentSearches"
|
||||
:key="index"
|
||||
class="me-2"
|
||||
variant="tonal"
|
||||
@click="searchWord = word"
|
||||
label
|
||||
>
|
||||
{{ word }}
|
||||
</VChip>
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<p class="custom-letter-spacing text-sm text-disabled text-uppercase py-2 px-4 mb-0">常用功能</p>
|
||||
<VList lines="one">
|
||||
<VHover v-for="(menu, index) in UserfulMenus" :key="index">
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
:prepend-icon="menu.icon"
|
||||
density="compact"
|
||||
link
|
||||
v-bind="hover.props"
|
||||
@click="goPage(menu.to)"
|
||||
>
|
||||
<VListItemTitle>
|
||||
{{ menu.title }}
|
||||
</VListItemTitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
</VList>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<p class="custom-letter-spacing text-sm text-disabled text-uppercase py-2 px-4 mb-0">常用插件</p>
|
||||
<VList lines="one">
|
||||
<VHover v-for="plugin in pluginItems.slice(0, 5)" :key="plugin.id">
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
prepend-icon="mdi-puzzle"
|
||||
density="compact"
|
||||
link
|
||||
v-bind="hover.props"
|
||||
@click="showPlugin(plugin.id ?? '')"
|
||||
>
|
||||
<VListItemTitle> {{ plugin.plugin_name }} </VListItemTitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="ri-corner-down-left-line" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
</VList>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6"> </VCol>
|
||||
<VCol cols="12" md="6"> </VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
@@ -46,7 +46,8 @@
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
"scripthost",
|
||||
"WebWorker"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
@@ -63,11 +64,13 @@
|
||||
"src/**/*.vue",
|
||||
"themeConfig.ts",
|
||||
"auto-imports.d.ts",
|
||||
"components.d.ts"
|
||||
"components.d.ts",
|
||||
"src/service-worker.ts",
|
||||
"public/service.js"
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules",
|
||||
"src/@iconify/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
104
vite.config.ts
104
vite.config.ts
@@ -4,8 +4,8 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import vuetify from 'vite-plugin-vuetify'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
@@ -13,8 +13,6 @@ export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
|
||||
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
|
||||
vuetify({
|
||||
styles: {
|
||||
configFile: 'src/styles/variables/_vuetify.scss',
|
||||
@@ -29,12 +27,100 @@ export default defineConfig({
|
||||
vueTemplate: true,
|
||||
}),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
injectRegister: 'script',
|
||||
manifest: false,
|
||||
registerType: 'autoUpdate',
|
||||
strategies: 'injectManifest',
|
||||
srcDir: 'src',
|
||||
filename: 'service-worker.ts',
|
||||
workbox: {
|
||||
navigateFallbackDenylist: [
|
||||
/.*\/api\/v\d+\/system\/logging.*/,
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,jpg,jpeg}'],
|
||||
navigateFallbackDenylist: [/.*\/api\/v\d+\/system\/logging.*/],
|
||||
},
|
||||
injectManifest: {
|
||||
rollupFormat: 'iife',
|
||||
},
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
type: 'module',
|
||||
},
|
||||
manifest: {
|
||||
'name': 'MoviePilot',
|
||||
'short_name': 'MoviePilot',
|
||||
'start_url': './',
|
||||
'display': 'standalone',
|
||||
'icons': [
|
||||
{
|
||||
'src': './android-chrome-192x192.png',
|
||||
'sizes': '192x192',
|
||||
'type': 'image/png',
|
||||
'purpose': 'any',
|
||||
},
|
||||
{
|
||||
'src': './android-chrome-192x192_maskable.png',
|
||||
'sizes': '192x192',
|
||||
'type': 'image/png',
|
||||
'purpose': 'maskable',
|
||||
},
|
||||
{
|
||||
'src': './android-chrome-512x512.png',
|
||||
'sizes': '512x512',
|
||||
'type': 'image/png',
|
||||
'purpose': 'any',
|
||||
},
|
||||
{
|
||||
'src': './android-chrome-512x512_maskable.png',
|
||||
'sizes': '512x512',
|
||||
'type': 'image/png',
|
||||
'purpose': 'maskable',
|
||||
},
|
||||
],
|
||||
'theme_color': '#28243D',
|
||||
'background_color': '#28243D',
|
||||
'shortcuts': [
|
||||
{
|
||||
'name': '推荐',
|
||||
'url': './ranking',
|
||||
'icons': [
|
||||
{
|
||||
'src': './sparkles-icon-192x192.png',
|
||||
'sizes': '192x192',
|
||||
'type': 'image/png',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'name': '电影订阅',
|
||||
'url': './subscribe-movie?tab=mysub',
|
||||
'icons': [
|
||||
{
|
||||
'src': './clock-icon-192x192.png',
|
||||
'sizes': '192x192',
|
||||
'type': 'image/png',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'name': '电视剧订阅',
|
||||
'url': './subscribe-tv?tab=mysub',
|
||||
'icons': [
|
||||
{
|
||||
'src': './clock-icon-192x192.png',
|
||||
'sizes': '192x192',
|
||||
'type': 'image/png',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'name': '设置',
|
||||
'url': './setting',
|
||||
'icons': [
|
||||
{
|
||||
'src': './cog-icon-192x192.png',
|
||||
'sizes': '192x192',
|
||||
'type': 'image/png',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
@@ -63,8 +149,6 @@ export default defineConfig({
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['vuetify'],
|
||||
entries: [
|
||||
'./src/**/*.vue',
|
||||
],
|
||||
entries: ['./src/**/*.vue'],
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user