mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-11 18:10:49 +08:00
Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef92cdc183 | ||
|
|
08f4a6cf2c | ||
|
|
c0517cd29a | ||
|
|
084449ccf3 | ||
|
|
0e8203ae03 | ||
|
|
236440be52 | ||
|
|
6f7e4bb272 | ||
|
|
38dcd3635a | ||
|
|
a3f3330dad | ||
|
|
bbc6c57c08 | ||
|
|
2f36a8edef | ||
|
|
df637fb887 | ||
|
|
be74c92a35 | ||
|
|
a219a64e20 | ||
|
|
25c22a276a | ||
|
|
6e6be057ca | ||
|
|
af69efa48b | ||
|
|
c551083fa4 | ||
|
|
9767feed29 | ||
|
|
4392818e92 | ||
|
|
8d22bafeb6 | ||
|
|
89ddd1fb78 | ||
|
|
24513fa22b | ||
|
|
cddde0c2a0 | ||
|
|
9c674e0018 | ||
|
|
0c6476d283 | ||
|
|
bf0c529a59 | ||
|
|
877bb4d4a2 | ||
|
|
dc4db0b2b3 | ||
|
|
a738d4a3b9 | ||
|
|
e9866a04df | ||
|
|
4f5193d602 | ||
|
|
37b92c55ba | ||
|
|
9299f1bcb6 | ||
|
|
7fe12192df | ||
|
|
1169644ab3 | ||
|
|
6f7770ed43 | ||
|
|
8059fd6f90 | ||
|
|
556dbd8d78 | ||
|
|
6695fd8c14 | ||
|
|
3ab0229275 | ||
|
|
99467127a0 | ||
|
|
90d73b7bd5 | ||
|
|
2e326e1798 | ||
|
|
251eac93c7 | ||
|
|
c74d70808c | ||
|
|
e63b2d7152 | ||
|
|
16b29b56a5 | ||
|
|
6d79c4fe2f | ||
|
|
4b1fb60ee3 | ||
|
|
1d2be54f9e | ||
|
|
83547e32db | ||
|
|
70ddb929f2 | ||
|
|
8b22961394 | ||
|
|
c15d42c179 | ||
|
|
098e473cab | ||
|
|
f6f3d9368a | ||
|
|
9558a420e9 | ||
|
|
4d3b69ca34 | ||
|
|
fdcc4a44c8 | ||
|
|
5de0494538 | ||
|
|
2045f833e4 | ||
|
|
cc4f89aac1 | ||
|
|
1c2f2c17d4 | ||
|
|
ace7a6621f | ||
|
|
d02fe55a1e | ||
|
|
9b753a8f5b | ||
|
|
11e82582b8 | ||
|
|
419358863e | ||
|
|
1d0d7f9975 | ||
|
|
c5f564372b | ||
|
|
a50f0cd727 | ||
|
|
96f6f55138 | ||
|
|
6a45c8b358 | ||
|
|
165937596e | ||
|
|
fb976f043b | ||
|
|
ecb9c4e51a | ||
|
|
9e8c3b495c | ||
|
|
24a37fc33c | ||
|
|
d09a21114d | ||
|
|
6e2b12501f | ||
|
|
2a56e116cf | ||
|
|
6de4f238d8 | ||
|
|
1b426c5957 | ||
|
|
82454a650c | ||
|
|
227b6bd7ef | ||
|
|
9554025daf | ||
|
|
0eb5d607bf | ||
|
|
750f4bc276 |
@@ -1 +1 @@
|
|||||||
VITE_API_BASE_URL=/api/v1/
|
VITE_API_BASE_URL=api/v1/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "moviepilot",
|
"name": "moviepilot",
|
||||||
"version": "1.5.5",
|
"version": "1.6.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"bin": "dist/service.js",
|
"bin": "dist/service.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"@floating-ui/dom": "1.2.8",
|
"@floating-ui/dom": "1.2.8",
|
||||||
"@vueuse/core": "^10.1.2",
|
"@vueuse/core": "^10.1.2",
|
||||||
"@vueuse/math": "^10.1.2",
|
"@vueuse/math": "^10.1.2",
|
||||||
|
"ace-builds": "^1.32.6",
|
||||||
"apexcharts-clevision": "^3.28.5",
|
"apexcharts-clevision": "^3.28.5",
|
||||||
"axios": "1.4.0",
|
"axios": "1.4.0",
|
||||||
"axios-mock-adapter": "^1.21.4",
|
"axios-mock-adapter": "^1.21.4",
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
"vue-prism-component": "^2.0.0",
|
"vue-prism-component": "^2.0.0",
|
||||||
"vue-router": "^4.2.0",
|
"vue-router": "^4.2.0",
|
||||||
"vue-toast-notification": "^3",
|
"vue-toast-notification": "^3",
|
||||||
|
"vue3-ace-editor": "^2.2.4",
|
||||||
"vue3-apexcharts": "^1.4.1",
|
"vue3-apexcharts": "^1.4.1",
|
||||||
"vue3-perfect-scrollbar": "^1.6.0",
|
"vue3-perfect-scrollbar": "^1.6.0",
|
||||||
"vuetify": "3.3.5",
|
"vuetify": "3.3.5",
|
||||||
@@ -107,4 +109,4 @@
|
|||||||
"resolutions": {
|
"resolutions": {
|
||||||
"postcss": "8"
|
"postcss": "8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,12 @@ app.use(
|
|||||||
|
|
||||||
// 处理根路径的请求
|
// 处理根路径的请求
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'index.html')) // 指向你的前端入口文件
|
res.sendFile(path.join(__dirname, 'index.html'))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理所有其他请求,重定向到前端入口文件
|
||||||
|
app.get('*', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'index.html'))
|
||||||
})
|
})
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useTheme } from 'vuetify'
|
import { useTheme } from 'vuetify'
|
||||||
import type { ThemeSwitcherTheme } from '@layouts/types'
|
import type { ThemeSwitcherTheme } from '@layouts/types'
|
||||||
|
|
||||||
@@ -20,26 +20,30 @@ const {
|
|||||||
{ initialValue: savedTheme.value },
|
{ initialValue: savedTheme.value },
|
||||||
)
|
)
|
||||||
|
|
||||||
function changeTheme() {
|
function updateTheme() {
|
||||||
const nextTheme = getNextThemeName()
|
const autoTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||||
|
const theme = currentThemeName.value === 'auto' ? autoTheme : currentThemeName.value
|
||||||
globalTheme.name.value = nextTheme
|
globalTheme.name.value = theme
|
||||||
savedTheme.value = nextTheme
|
savedTheme.value = theme
|
||||||
localStorage.setItem('theme', nextTheme)
|
|
||||||
// 修改载入时背景色
|
// 修改载入时背景色
|
||||||
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
|
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
|
||||||
|
|
||||||
themeTransition()
|
themeTransition()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update icon if theme is changed from other sources
|
// 监听系统主题变化
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => globalTheme.name.value,
|
() => currentThemeName.value,
|
||||||
(val) => {
|
() => updateTheme(),
|
||||||
currentThemeName.value = val
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function changeTheme() {
|
||||||
|
const nextTheme = getNextThemeName()
|
||||||
|
currentThemeName.value = nextTheme
|
||||||
|
localStorage.setItem('theme', nextTheme)
|
||||||
|
}
|
||||||
|
|
||||||
// Apply saved theme on page load
|
// Apply saved theme on page load
|
||||||
// onMounted(() => {
|
// onMounted(() => {
|
||||||
// globalTheme.name.value = savedTheme.value
|
// globalTheme.name.value = savedTheme.value
|
||||||
@@ -117,7 +121,11 @@ function themeTransition() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<IconBtn @click="changeTheme">
|
<IconBtn @click="changeTheme">
|
||||||
<VIcon :icon="props.themes[currentThemeIndex].icon" />
|
<VTooltip text="切换主题">
|
||||||
|
<template #activator="{ props: _props }">
|
||||||
|
<VIcon v-bind="_props" :icon="props.themes[currentThemeIndex].icon" />
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -109,3 +109,41 @@ export function formatBytes(bytes: number, decimals = 2) {
|
|||||||
|
|
||||||
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
|
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化剧集列表
|
||||||
|
export function formatEp(nums: number[]): string {
|
||||||
|
if (!nums.length)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
if (nums.length === 1)
|
||||||
|
return nums[0].toString()
|
||||||
|
|
||||||
|
// 将数组升序排序
|
||||||
|
nums.sort((a, b) => a - b)
|
||||||
|
const formattedRanges: string[] = []
|
||||||
|
let start = nums[0]
|
||||||
|
let end = nums[0]
|
||||||
|
|
||||||
|
for (let i = 1; i < nums.length; i++) {
|
||||||
|
if (nums[i] === end + 1) {
|
||||||
|
end = nums[i]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (start === end)
|
||||||
|
formattedRanges.push(start.toString())
|
||||||
|
|
||||||
|
else
|
||||||
|
formattedRanges.push(`${start.toString()}-${end.toString()}`)
|
||||||
|
|
||||||
|
start = end = nums[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start === end)
|
||||||
|
formattedRanges.push(start.toString())
|
||||||
|
|
||||||
|
else
|
||||||
|
formattedRanges.push(`${start.toString()}-${end.toString()}`)
|
||||||
|
|
||||||
|
return formattedRanges.join('、')
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function isToday(date: Date) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算时间差,返回xx天xx小时xx分钟
|
// 计算时间差,返回xx天/xx小时/xx分钟/xx秒
|
||||||
export function calculateTimeDifference(inputTime: string): string {
|
export function calculateTimeDifference(inputTime: string): string {
|
||||||
if (!inputTime)
|
if (!inputTime)
|
||||||
return ''
|
return ''
|
||||||
@@ -64,6 +64,38 @@ export function calculateTimeDifference(inputTime: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算时间差,返回xx天xx小时xx分钟
|
||||||
|
export function calculateTimeDiff(inputTime: string): string {
|
||||||
|
if (!inputTime)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
// 使用当前时区
|
||||||
|
const inputDate = new Date(inputTime)
|
||||||
|
const currentDate = new Date()
|
||||||
|
|
||||||
|
const timeDifference = currentDate.getTime() - inputDate.getTime()
|
||||||
|
const secondsDifference = Math.floor(timeDifference / 1000)
|
||||||
|
|
||||||
|
const days = Math.floor(secondsDifference / 86400)
|
||||||
|
const hours = Math.floor(secondsDifference % 86400 / 3600)
|
||||||
|
const minutes = Math.floor(secondsDifference % 86400 % 3600 / 60)
|
||||||
|
const secones = Math.floor(secondsDifference % 60)
|
||||||
|
|
||||||
|
if (days > 0)
|
||||||
|
return `${days}天${hours}小时${minutes}分钟`
|
||||||
|
|
||||||
|
else if (hours > 0)
|
||||||
|
return `${hours}小时${minutes}分钟`
|
||||||
|
|
||||||
|
else if (minutes > 0)
|
||||||
|
return `${minutes}分钟`
|
||||||
|
|
||||||
|
else if (secones > 0)
|
||||||
|
return `${secones}秒`
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
// 判断一个数组subArray是不是在另一个数组mainArray中
|
// 判断一个数组subArray是不是在另一个数组mainArray中
|
||||||
export function isContained(subArray: any[], mainArray: any[]): boolean {
|
export function isContained(subArray: any[], mainArray: any[]): boolean {
|
||||||
return subArray.every(element => mainArray.includes(element))
|
return subArray.every(element => mainArray.includes(element))
|
||||||
|
|||||||
15
src/App.vue
15
src/App.vue
@@ -1,15 +1,21 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import { useTheme } from 'vuetify'
|
import { useTheme } from 'vuetify'
|
||||||
import store from './store'
|
|
||||||
|
|
||||||
// 第一时间应用主题
|
import store from './store'
|
||||||
const { global: globalTheme } = useTheme()
|
|
||||||
globalTheme.name.value = localStorage.getItem('theme') || 'light'
|
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 设置主题
|
||||||
|
function setTheme() {
|
||||||
|
const { global: globalTheme } = useTheme()
|
||||||
|
let theme = localStorage.getItem('theme') || 'light'
|
||||||
|
if (theme === 'auto')
|
||||||
|
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||||
|
globalTheme.name.value = theme
|
||||||
|
}
|
||||||
|
|
||||||
// SSE持续接收消息
|
// SSE持续接收消息
|
||||||
function startSSEMessager() {
|
function startSSEMessager() {
|
||||||
const token = store.state.auth.token
|
const token = store.state.auth.token
|
||||||
@@ -32,6 +38,7 @@ function startSSEMessager() {
|
|||||||
|
|
||||||
// 页面加载时,加载当前用户数据
|
// 页面加载时,加载当前用户数据
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
|
setTheme()
|
||||||
startSSEMessager()
|
startSSEMessager()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
54
src/ace-config.ts
Normal file
54
src/ace-config.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import ace from 'ace-builds'
|
||||||
|
|
||||||
|
import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url'
|
||||||
|
|
||||||
|
import modeJavascriptUrl from 'ace-builds/src-noconflict/mode-javascript?url'
|
||||||
|
|
||||||
|
import modeHtmlUrl from 'ace-builds/src-noconflict/mode-html?url'
|
||||||
|
|
||||||
|
import modeYamlUrl from 'ace-builds/src-noconflict/mode-yaml?url'
|
||||||
|
|
||||||
|
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url'
|
||||||
|
|
||||||
|
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url'
|
||||||
|
|
||||||
|
import themeMonokaiUrl from 'ace-builds/src-noconflict/theme-monokai?url'
|
||||||
|
|
||||||
|
import workerBaseUrl from 'ace-builds/src-noconflict/worker-base?url'
|
||||||
|
|
||||||
|
import workerJsonUrl from 'ace-builds/src-noconflict/worker-json?url'
|
||||||
|
|
||||||
|
import workerJavascriptUrl from 'ace-builds/src-noconflict/worker-javascript?url'
|
||||||
|
|
||||||
|
import workerHtmlUrl from 'ace-builds/src-noconflict/worker-html?url'
|
||||||
|
|
||||||
|
import workerYamlUrl from 'ace-builds/src-noconflict/worker-yaml?url'
|
||||||
|
|
||||||
|
import snippetsHtmlUrl from 'ace-builds/src-noconflict/snippets/html?url'
|
||||||
|
|
||||||
|
import snippetsJsUrl from 'ace-builds/src-noconflict/snippets/javascript?url'
|
||||||
|
|
||||||
|
import snippetsYamlUrl from 'ace-builds/src-noconflict/snippets/yaml?url'
|
||||||
|
|
||||||
|
import snippetsJsonUrl from 'ace-builds/src-noconflict/snippets/json?url'
|
||||||
|
|
||||||
|
import 'ace-builds/src-noconflict/ext-language_tools'
|
||||||
|
|
||||||
|
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl)
|
||||||
|
ace.config.setModuleUrl('ace/mode/javascript', modeJavascriptUrl)
|
||||||
|
ace.config.setModuleUrl('ace/mode/html', modeHtmlUrl)
|
||||||
|
ace.config.setModuleUrl('ace/mode/yaml', modeYamlUrl)
|
||||||
|
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl)
|
||||||
|
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
|
||||||
|
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
|
||||||
|
ace.config.setModuleUrl('ace/mode/base', workerBaseUrl)
|
||||||
|
ace.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl)
|
||||||
|
ace.config.setModuleUrl('ace/mode/javascript_worker', workerJavascriptUrl)
|
||||||
|
ace.config.setModuleUrl('ace/mode/html_worker', workerHtmlUrl)
|
||||||
|
ace.config.setModuleUrl('ace/mode/yaml_worker', workerYamlUrl)
|
||||||
|
ace.config.setModuleUrl('ace/snippets/html', snippetsHtmlUrl)
|
||||||
|
ace.config.setModuleUrl('ace/snippets/javascript', snippetsJsUrl)
|
||||||
|
ace.config.setModuleUrl('ace/snippets/javascript', snippetsYamlUrl)
|
||||||
|
ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl)
|
||||||
|
|
||||||
|
ace.require('ace/ext/language_tools')
|
||||||
@@ -651,6 +651,16 @@ export interface TorrentInfo {
|
|||||||
|
|
||||||
// 促销描述
|
// 促销描述
|
||||||
volume_factor: string
|
volume_factor: string
|
||||||
|
|
||||||
|
// 免费时间
|
||||||
|
freedate: string
|
||||||
|
|
||||||
|
// 剩余免费时间
|
||||||
|
freedate_diff: string
|
||||||
|
|
||||||
|
// 种子类型
|
||||||
|
category: string
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 识别元数据
|
// 识别元数据
|
||||||
@@ -870,6 +880,9 @@ export interface ScheduleInfo {
|
|||||||
// 名称
|
// 名称
|
||||||
name: string
|
name: string
|
||||||
|
|
||||||
|
// 提供者
|
||||||
|
provider: string
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
status: string
|
status: string
|
||||||
|
|
||||||
@@ -917,3 +930,26 @@ export interface FileItem {
|
|||||||
children: FileItem[]
|
children: FileItem[]
|
||||||
modify_time: number
|
modify_time: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 媒体服务器播放条目
|
||||||
|
export interface MediaServerPlayItem {
|
||||||
|
id?: string | number
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
type?: string
|
||||||
|
image?: string
|
||||||
|
link?: string
|
||||||
|
percent?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 媒体服务器媒体库
|
||||||
|
export interface MediaServerLibrary {
|
||||||
|
server: string
|
||||||
|
id?: string | number
|
||||||
|
name: string
|
||||||
|
path?: string
|
||||||
|
type?: string
|
||||||
|
image?: string
|
||||||
|
image_list?: string[]
|
||||||
|
link?: string
|
||||||
|
}
|
||||||
|
|||||||
BIN
src/assets/images/misc/emby.png
Normal file
BIN
src/assets/images/misc/emby.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/images/misc/jellyfin.png
Normal file
BIN
src/assets/images/misc/jellyfin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/images/misc/plex.png
Normal file
BIN
src/assets/images/misc/plex.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
89
src/components/cards/BackdropCard.vue
Normal file
89
src/components/cards/BackdropCard.vue
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
media: Object as PropType<MediaServerPlayItem>,
|
||||||
|
width: String,
|
||||||
|
height: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图片是否加载完成
|
||||||
|
const imageLoaded = ref(false)
|
||||||
|
|
||||||
|
// 图片加载完成响应
|
||||||
|
function imageLoadHandler() {
|
||||||
|
imageLoaded.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转播放
|
||||||
|
function goPlay() {
|
||||||
|
if (props.media?.link)
|
||||||
|
window.open(props.media?.link, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算图片地址
|
||||||
|
const getImgUrl = computed(() => {
|
||||||
|
const image = props.media?.image || ''
|
||||||
|
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(image)}/0`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VHover
|
||||||
|
v-bind="props"
|
||||||
|
:height="props.height"
|
||||||
|
:width="props.width"
|
||||||
|
>
|
||||||
|
<template #default="hover">
|
||||||
|
<VCard
|
||||||
|
v-bind="hover.props"
|
||||||
|
:height="props.height"
|
||||||
|
:width="props.width"
|
||||||
|
class="ring-gray-500"
|
||||||
|
:class="{
|
||||||
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
|
'ring-1': imageLoaded,
|
||||||
|
}"
|
||||||
|
@click="goPlay"
|
||||||
|
>
|
||||||
|
<template #image>
|
||||||
|
<VImg
|
||||||
|
:src="getImgUrl"
|
||||||
|
aspect-ratio="2/3"
|
||||||
|
cover
|
||||||
|
@load="imageLoadHandler"
|
||||||
|
>
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<VCardText
|
||||||
|
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||||
|
>
|
||||||
|
<h1 class="mb-1 text-white text-shadow font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||||
|
{{ props.media?.title }}
|
||||||
|
</h1>
|
||||||
|
<span class="text-shadow">{{ props.media?.subtitle }}</span>
|
||||||
|
</VCardText>
|
||||||
|
</VImg>
|
||||||
|
</template>
|
||||||
|
<div class="w-full absolute bottom-0">
|
||||||
|
<VProgressLinear
|
||||||
|
v-if="props.media?.percent"
|
||||||
|
:model-value="props.media?.percent"
|
||||||
|
bg-color="success"
|
||||||
|
color="success"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
</VHover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.text-shadow{
|
||||||
|
text-shadow:1px 1px #777;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -23,6 +23,11 @@ function getSpeedText() {
|
|||||||
// 下载状态
|
// 下载状态
|
||||||
const isDownloading = ref(props.info?.state === 'downloading')
|
const isDownloading = ref(props.info?.state === 'downloading')
|
||||||
|
|
||||||
|
// 监听props.info?.state的变化
|
||||||
|
watch(() => props.info?.state, (newValue) => {
|
||||||
|
isDownloading.value = newValue === 'downloading';
|
||||||
|
});
|
||||||
|
|
||||||
// 图片是否加载完成
|
// 图片是否加载完成
|
||||||
const imageLoaded = ref(false)
|
const imageLoaded = ref(false)
|
||||||
|
|
||||||
|
|||||||
202
src/components/cards/LibraryCard.vue
Normal file
202
src/components/cards/LibraryCard.vue
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MediaServerLibrary } from '@/api/types'
|
||||||
|
import plex from '@images/misc/plex.png'
|
||||||
|
import emby from '@images/misc/emby.png'
|
||||||
|
import jellyfin from '@images/misc/jellyfin.png'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
media: Object as PropType<MediaServerLibrary>,
|
||||||
|
width: String,
|
||||||
|
height: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
// canvas
|
||||||
|
const canvasRef = ref<HTMLCanvasElement>()
|
||||||
|
|
||||||
|
// 图片地址
|
||||||
|
const imgUrl = ref('')
|
||||||
|
|
||||||
|
// 图片是否加载完成
|
||||||
|
const imageLoaded = ref(false)
|
||||||
|
|
||||||
|
// 图片是否加载错误
|
||||||
|
const imageError = ref(false)
|
||||||
|
|
||||||
|
// 图片加载完成响应
|
||||||
|
function imageLoadHandler() {
|
||||||
|
imageLoaded.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片加载错误
|
||||||
|
function imageErrorHandler() {
|
||||||
|
imageError.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认图片
|
||||||
|
function getDefaultImage() {
|
||||||
|
if (props.media?.server === 'plex')
|
||||||
|
return plex
|
||||||
|
else if (props.media?.server === 'emby')
|
||||||
|
return emby
|
||||||
|
else if (props.media?.server === 'jellyfin')
|
||||||
|
return jellyfin
|
||||||
|
else
|
||||||
|
return plex
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转播放
|
||||||
|
function goPlay() {
|
||||||
|
if (props.media?.link)
|
||||||
|
window.open(props.media?.link, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成图片代理路径
|
||||||
|
function getImgUrl(url: string) {
|
||||||
|
if (!url)
|
||||||
|
return getDefaultImage()
|
||||||
|
else
|
||||||
|
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(url)}/0`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据多张图片生成媒体库封面
|
||||||
|
async function drawImages(imageList: string[]) {
|
||||||
|
// 图片
|
||||||
|
const IMAGES = imageList
|
||||||
|
if (IMAGES.length === 0)
|
||||||
|
return getDefaultImage()
|
||||||
|
|
||||||
|
// 为所有图片添加system/img前缀
|
||||||
|
for (let i = 0; i < IMAGES.length; i++)
|
||||||
|
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(IMAGES[i])}/0`
|
||||||
|
|
||||||
|
// canvas
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
if (!canvas)
|
||||||
|
return getDefaultImage()
|
||||||
|
|
||||||
|
// 画布参数
|
||||||
|
const POSTER_WIDTH = (canvas.width - 32) / 4
|
||||||
|
const POSTER_HEIGHT = canvas.height * 0.75 - 8
|
||||||
|
const MARGIN_WIDTH = 4
|
||||||
|
const MARGIN_HEIGHT = 4
|
||||||
|
const REFLECTION_HEIGHT = POSTER_HEIGHT / 2
|
||||||
|
const REFLECTION_SHOW_HEIGHT = canvas.height / 4
|
||||||
|
|
||||||
|
// 获取画布上下文
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx)
|
||||||
|
return getDefaultImage()
|
||||||
|
|
||||||
|
// 设置背景色为黑色
|
||||||
|
ctx.fillStyle = '#000000'
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
// 绘制图片
|
||||||
|
async function drawImageWithReflection(imgSrc: string, index: number) {
|
||||||
|
if (!canvas)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (!ctx)
|
||||||
|
return
|
||||||
|
|
||||||
|
const img = new Image()
|
||||||
|
img.setAttribute('crossorigin', 'anonymous')
|
||||||
|
img.src = imgSrc
|
||||||
|
await new Promise(resolve => img.onload = resolve)
|
||||||
|
|
||||||
|
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
|
||||||
|
const y = MARGIN_HEIGHT
|
||||||
|
|
||||||
|
ctx.drawImage(img, x, y, POSTER_WIDTH, POSTER_HEIGHT)
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(0, canvas.height)
|
||||||
|
ctx.scale(1, -1)
|
||||||
|
ctx.drawImage(
|
||||||
|
img,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
img.width,
|
||||||
|
img.height,
|
||||||
|
x,
|
||||||
|
REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT,
|
||||||
|
POSTER_WIDTH,
|
||||||
|
REFLECTION_HEIGHT,
|
||||||
|
)
|
||||||
|
|
||||||
|
const gradient = ctx.createLinearGradient(
|
||||||
|
0,
|
||||||
|
REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT,
|
||||||
|
0,
|
||||||
|
REFLECTION_HEIGHT,
|
||||||
|
)
|
||||||
|
|
||||||
|
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)')
|
||||||
|
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.3)')
|
||||||
|
ctx.fillStyle = gradient
|
||||||
|
ctx.fillRect(x, 0, POSTER_WIDTH, REFLECTION_SHOW_HEIGHT)
|
||||||
|
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制多张图片
|
||||||
|
const loopCount = Math.min(4, IMAGES.length)
|
||||||
|
for (let i = 0; i < loopCount; i++)
|
||||||
|
await drawImageWithReflection(IMAGES[i], i + 1)
|
||||||
|
|
||||||
|
// 转换为图片地址
|
||||||
|
return canvas.toDataURL('image/png')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (props.media?.image_list && props.media?.image_list.length > 0)
|
||||||
|
imgUrl.value = await drawImages(props.media?.image_list || [])
|
||||||
|
else
|
||||||
|
imgUrl.value = getImgUrl(props.media?.image || '')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VHover
|
||||||
|
v-bind="props"
|
||||||
|
:height="props.height"
|
||||||
|
:width="props.width"
|
||||||
|
>
|
||||||
|
<template #default="hover">
|
||||||
|
<VCard
|
||||||
|
v-bind="hover.props"
|
||||||
|
:height="props.height"
|
||||||
|
:width="props.width"
|
||||||
|
:class="{
|
||||||
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
|
}"
|
||||||
|
@click="goPlay"
|
||||||
|
>
|
||||||
|
<template #image>
|
||||||
|
<canvas ref="canvasRef" class="w-full h-full hidden" />
|
||||||
|
<VImg
|
||||||
|
:src="imgUrl"
|
||||||
|
aspect-ratio="2/3"
|
||||||
|
cover
|
||||||
|
@load="imageLoadHandler"
|
||||||
|
@error="imageErrorHandler"
|
||||||
|
>
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<VCardText
|
||||||
|
class="w-full flex flex-col flex-wrap justify-end align-center text-white absolute bottom-0 cursor-pointer pa-2"
|
||||||
|
>
|
||||||
|
<h1 class="mb-1 text-white font-bold line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||||
|
{{ props.media?.name }}
|
||||||
|
</h1>
|
||||||
|
</VCardText>
|
||||||
|
</VImg>
|
||||||
|
</template>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
</VHover>
|
||||||
|
</template>
|
||||||
@@ -16,6 +16,11 @@ const props = defineProps({
|
|||||||
height: String,
|
height: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 订阅规则
|
||||||
|
const subscribeRules = ref({
|
||||||
|
show_edit_dialog: false,
|
||||||
|
})
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
@@ -146,7 +151,7 @@ async function addSubscribe(season = 0) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 弹出订阅编辑弹窗
|
// 弹出订阅编辑弹窗
|
||||||
if (result.success && seasonsSelected.value.length <= 1) {
|
if (result.success && seasonsSelected.value.length <= 1 && subscribeRules.value.show_edit_dialog) {
|
||||||
subscribeId.value = result.data.id
|
subscribeId.value = result.data.id
|
||||||
subscribeEditDialog.value = true
|
subscribeEditDialog.value = true
|
||||||
}
|
}
|
||||||
@@ -223,7 +228,7 @@ async function handleCheckSubscribe() {
|
|||||||
// 查询当前媒体是否已入库
|
// 查询当前媒体是否已入库
|
||||||
async function handleCheckExists() {
|
async function handleCheckExists() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('media/exists', {
|
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||||
params: {
|
params: {
|
||||||
tmdbid: props.media?.tmdb_id,
|
tmdbid: props.media?.tmdb_id,
|
||||||
title: props.media?.title,
|
title: props.media?.title,
|
||||||
@@ -269,7 +274,7 @@ async function checkSeasonsNotExists() {
|
|||||||
// 开始处理
|
// 开始处理
|
||||||
startNProgress()
|
startNProgress()
|
||||||
try {
|
try {
|
||||||
const result: NotExistMediaInfo[] = await api.post('download/notexists', props.media)
|
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', props.media)
|
||||||
if (result) {
|
if (result) {
|
||||||
result.forEach((item) => {
|
result.forEach((item) => {
|
||||||
// 0-已入库 1-部分缺失 2-全部缺失
|
// 0-已入库 1-部分缺失 2-全部缺失
|
||||||
@@ -302,6 +307,20 @@ async function getMediaSeasons() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询订阅弹窗规则
|
||||||
|
async function querySubscribeRules() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get(
|
||||||
|
'system/setting/DefaultFilterRules',
|
||||||
|
)
|
||||||
|
if (result.data?.value)
|
||||||
|
subscribeRules.value = result.data?.value
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 爱心订阅按钮响应
|
// 爱心订阅按钮响应
|
||||||
function handleSubscribe() {
|
function handleSubscribe() {
|
||||||
if (isSubscribed.value)
|
if (isSubscribed.value)
|
||||||
@@ -373,6 +392,7 @@ function handleSearch() {
|
|||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
handleCheckSubscribe()
|
handleCheckSubscribe()
|
||||||
handleCheckExists()
|
handleCheckExists()
|
||||||
|
querySubscribeRules()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算图片地址
|
// 计算图片地址
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ const iconPath: Ref<string> = computed(() => {
|
|||||||
return noImage
|
return noImage
|
||||||
// 如果是网络图片则使用代理后返回
|
// 如果是网络图片则使用代理后返回
|
||||||
if (props.plugin?.plugin_icon?.startsWith('http'))
|
if (props.plugin?.plugin_icon?.startsWith('http'))
|
||||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}`
|
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}/1`
|
||||||
|
|
||||||
return `/plugin_icon/${props.plugin?.plugin_icon}`
|
return `./plugin_icon/${props.plugin?.plugin_icon}`
|
||||||
})
|
})
|
||||||
|
|
||||||
// 访问插件页面
|
// 访问插件页面
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import PageRender from '@/components/render/PageRender.vue'
|
|||||||
import { isNullOrEmptyObject } from '@core/utils'
|
import { isNullOrEmptyObject } from '@core/utils'
|
||||||
import noImage from '@images/logos/plugin.png'
|
import noImage from '@images/logos/plugin.png'
|
||||||
import { getDominantColor } from '@/@core/utils/image'
|
import { getDominantColor } from '@/@core/utils/image'
|
||||||
|
import store from '@/store'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -181,9 +182,9 @@ const iconPath: Ref<string> = computed(() => {
|
|||||||
return noImage
|
return noImage
|
||||||
// 如果是网络图片则使用代理后返回
|
// 如果是网络图片则使用代理后返回
|
||||||
if (props.plugin?.plugin_icon?.startsWith('http'))
|
if (props.plugin?.plugin_icon?.startsWith('http'))
|
||||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}`
|
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}/1`
|
||||||
|
|
||||||
return `/plugin_icon/${props.plugin?.plugin_icon}`
|
return `./plugin_icon/${props.plugin?.plugin_icon}`
|
||||||
})
|
})
|
||||||
|
|
||||||
// 重置插件
|
// 重置插件
|
||||||
@@ -225,6 +226,13 @@ function visitAuthorPage() {
|
|||||||
window.open(props.plugin?.author_url, '_blank')
|
window.open(props.plugin?.author_url, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查看日志URL
|
||||||
|
function openLoggerWindow() {
|
||||||
|
const token = store.state.auth.token
|
||||||
|
const url = `${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}&length=-1&logfile=plugins/${props.plugin?.id?.toLowerCase()}.log`
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
// 弹出菜单
|
// 弹出菜单
|
||||||
const dropdownItems = ref([
|
const dropdownItems = ref([
|
||||||
{
|
{
|
||||||
@@ -265,9 +273,20 @@ const dropdownItems = ref([
|
|||||||
click: uninstallPlugin,
|
click: uninstallPlugin,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '查看日志',
|
||||||
|
value: 5,
|
||||||
|
show: true,
|
||||||
|
props: {
|
||||||
|
prependIcon: 'mdi-file-document-outline',
|
||||||
|
click: () => {
|
||||||
|
openLoggerWindow()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '作者主页',
|
title: '作者主页',
|
||||||
value: 4,
|
value: 5,
|
||||||
show: true,
|
show: true,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-home-circle-outline',
|
prependIcon: 'mdi-home-circle-outline',
|
||||||
|
|||||||
102
src/components/cards/PosterCard.vue
Normal file
102
src/components/cards/PosterCard.vue
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
import noImage from '@images/no-image.jpeg'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
media: Object as PropType<MediaServerPlayItem>,
|
||||||
|
width: String,
|
||||||
|
height: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图片加载状态
|
||||||
|
const isImageLoaded = ref(false)
|
||||||
|
|
||||||
|
// 图片加载失败
|
||||||
|
const imageLoadError = ref(false)
|
||||||
|
|
||||||
|
// 角标颜色
|
||||||
|
function getChipColor(type: string) {
|
||||||
|
if (type === '电影')
|
||||||
|
return 'border-blue-500 bg-blue-600'
|
||||||
|
else if (type === '电视剧')
|
||||||
|
return ' bg-indigo-500 border-indigo-600'
|
||||||
|
else
|
||||||
|
return 'border-purple-600 bg-purple-600'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算图片地址
|
||||||
|
const getImgUrl = computed(() => {
|
||||||
|
if (imageLoadError.value)
|
||||||
|
return noImage
|
||||||
|
const image = props.media?.image || ''
|
||||||
|
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(image)}/0`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 跳转播放
|
||||||
|
function goPlay() {
|
||||||
|
if (props.media?.link)
|
||||||
|
window.open(props.media?.link, '_blank')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VHover v-bind="props">
|
||||||
|
<template #default="hover">
|
||||||
|
<VCard
|
||||||
|
v-bind="hover.props"
|
||||||
|
:height="props.height"
|
||||||
|
:width="props.width"
|
||||||
|
class="outline-none shadow ring-gray-500 rounded-lg"
|
||||||
|
:class="{
|
||||||
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
|
'ring-1': isImageLoaded,
|
||||||
|
}"
|
||||||
|
@click.stop="goPlay"
|
||||||
|
>
|
||||||
|
<VImg
|
||||||
|
aspect-ratio="2/3"
|
||||||
|
:src="getImgUrl"
|
||||||
|
class="object-cover aspect-w-2 aspect-h-3"
|
||||||
|
:class="hover.isHovering ? 'on-hover' : ''"
|
||||||
|
cover
|
||||||
|
@load="isImageLoaded = true"
|
||||||
|
@error="imageLoadError = true"
|
||||||
|
>
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- 类型角标 -->
|
||||||
|
<VChip
|
||||||
|
v-show="isImageLoaded"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
:class="getChipColor(props.media?.type || '')"
|
||||||
|
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||||
|
>
|
||||||
|
{{ props.media?.type }}
|
||||||
|
</VChip>
|
||||||
|
<!-- 详情 -->
|
||||||
|
<VCardText
|
||||||
|
v-show="hover.isHovering || imageLoadError"
|
||||||
|
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||||
|
>
|
||||||
|
<span class="font-bold">{{ props.media?.subtitle }}</span>
|
||||||
|
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||||
|
{{ props.media?.title }}
|
||||||
|
</h1>
|
||||||
|
</VCardText>
|
||||||
|
</VImg>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
</VHover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.on-hover img {
|
||||||
|
@apply brightness-50;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -80,6 +80,7 @@ const resourceItemsPerPage = ref(25)
|
|||||||
const userPwForm = ref({
|
const userPwForm = ref({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
code: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 打开种子详情页面
|
// 打开种子详情页面
|
||||||
@@ -152,6 +153,7 @@ async function updateSiteCookie() {
|
|||||||
params: {
|
params: {
|
||||||
username: userPwForm.value.username,
|
username: userPwForm.value.username,
|
||||||
password: userPwForm.value.password,
|
password: userPwForm.value.password,
|
||||||
|
code: userPwForm.value.code,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -335,7 +337,7 @@ onMounted(() => {
|
|||||||
<VRow>
|
<VRow>
|
||||||
<VCol
|
<VCol
|
||||||
cols="12"
|
cols="12"
|
||||||
md="6"
|
md="4"
|
||||||
>
|
>
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="userPwForm.username"
|
v-model="userPwForm.username"
|
||||||
@@ -345,7 +347,7 @@ onMounted(() => {
|
|||||||
</VCol>
|
</VCol>
|
||||||
<VCol
|
<VCol
|
||||||
cols="12"
|
cols="12"
|
||||||
md="6"
|
md="4"
|
||||||
>
|
>
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="userPwForm.password"
|
v-model="userPwForm.password"
|
||||||
@@ -359,6 +361,15 @@ onMounted(() => {
|
|||||||
@keydown.enter="updateSiteCookie"
|
@keydown.enter="updateSiteCookie"
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VTextField
|
||||||
|
v-model="userPwForm.code"
|
||||||
|
label="两步验证"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</VForm>
|
</VForm>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
@@ -412,6 +423,23 @@ onMounted(() => {
|
|||||||
<div class="text-sm my-1">
|
<div class="text-sm my-1">
|
||||||
{{ item.raw.description }}
|
{{ item.raw.description }}
|
||||||
</div>
|
</div>
|
||||||
|
<VChip
|
||||||
|
v-if="item.raw?.hit_and_run"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1 text-white bg-black"
|
||||||
|
>
|
||||||
|
H&R
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="item.raw?.freedate_diff"
|
||||||
|
variant="elevated"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1"
|
||||||
|
>
|
||||||
|
{{ item.raw?.freedate_diff }}
|
||||||
|
</VChip>
|
||||||
<VChip
|
<VChip
|
||||||
v-for="(label, index) in item.raw?.labels"
|
v-for="(label, index) in item.raw?.labels"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script lang="ts" setup>
|
<script lang='ts' setup>
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
|
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
|
||||||
import { calculateTimeDifference } from '@/@core/utils'
|
import { calculateTimeDifference } from '@/@core/utils'
|
||||||
import { formatSeason } from '@/@core/utils/formatters'
|
import { formatSeason } from '@/@core/utils/formatters'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Subscribe } from '@/api/types'
|
import type { Subscribe } from '@/api/types'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -55,7 +56,7 @@ function getPercentage() {
|
|||||||
return Math.round(
|
return Math.round(
|
||||||
(((props.media?.total_episode ?? 0) - (props.media?.lack_episode ?? 0))
|
(((props.media?.total_episode ?? 0) - (props.media?.lack_episode ?? 0))
|
||||||
/ (props.media?.total_episode ?? 1))
|
/ (props.media?.total_episode ?? 1))
|
||||||
* 100,
|
* 100,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,8 +127,28 @@ const dropdownItems = ref([
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '取消订阅',
|
title: '查看详情',
|
||||||
value: 3,
|
value: 3,
|
||||||
|
props: {
|
||||||
|
prependIcon: 'mdi-open-in-new',
|
||||||
|
click: () => {
|
||||||
|
router.push({
|
||||||
|
path: '/media',
|
||||||
|
query: {
|
||||||
|
mediaid: `${
|
||||||
|
props.media?.tmdbid
|
||||||
|
? `tmdb:${props.media?.tmdbid}`
|
||||||
|
: `douban:${props.media?.doubanid}`
|
||||||
|
}`,
|
||||||
|
type: props.media?.type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '取消订阅',
|
||||||
|
value: 4,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-trash-can-outline',
|
prependIcon: 'mdi-trash-can-outline',
|
||||||
color: 'error',
|
color: 'error',
|
||||||
@@ -162,7 +183,7 @@ const dropdownItems = ref([
|
|||||||
</template>
|
</template>
|
||||||
<VCardTitle :class="getTextClass()">
|
<VCardTitle :class="getTextClass()">
|
||||||
{{ props.media?.name }}
|
{{ props.media?.name }}
|
||||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : "") }}
|
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||||
</VCardTitle>
|
</VCardTitle>
|
||||||
<template #append>
|
<template #append>
|
||||||
<div class="me-n3">
|
<div class="me-n3">
|
||||||
@@ -252,7 +273,8 @@ const dropdownItems = ref([
|
|||||||
<VIcon
|
<VIcon
|
||||||
icon="mdi-download"
|
icon="mdi-download"
|
||||||
class="me-1"
|
class="me-1"
|
||||||
/> {{ lastUpdateText }}
|
/>
|
||||||
|
{{ lastUpdateText }}
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VProgressLinear
|
<VProgressLinear
|
||||||
v-if="getPercentage() > 0"
|
v-if="getPercentage() > 0"
|
||||||
|
|||||||
@@ -195,6 +195,23 @@ onMounted(() => {
|
|||||||
v-if="torrent?.labels"
|
v-if="torrent?.labels"
|
||||||
class="pb-3 pt-0 pe-12"
|
class="pb-3 pt-0 pe-12"
|
||||||
>
|
>
|
||||||
|
<VChip
|
||||||
|
v-if="torrent?.hit_and_run"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1 text-white bg-black"
|
||||||
|
>
|
||||||
|
H&R
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="torrent?.freedate_diff"
|
||||||
|
variant="elevated"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1"
|
||||||
|
>
|
||||||
|
{{ torrent?.freedate_diff }}
|
||||||
|
</VChip>
|
||||||
<VChip
|
<VChip
|
||||||
v-for="(label, index) in torrent?.labels"
|
v-for="(label, index) in torrent?.labels"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
|||||||
@@ -150,6 +150,23 @@ onMounted(() => {
|
|||||||
v-if="torrent?.labels"
|
v-if="torrent?.labels"
|
||||||
class="pt-2"
|
class="pt-2"
|
||||||
>
|
>
|
||||||
|
<VChip
|
||||||
|
v-if="torrent?.hit_and_run"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1 text-white bg-black"
|
||||||
|
>
|
||||||
|
H&R
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="torrent?.freedate_diff"
|
||||||
|
variant="elevated"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1"
|
||||||
|
>
|
||||||
|
{{ torrent?.freedate_diff }}
|
||||||
|
</VChip>
|
||||||
<VChip
|
<VChip
|
||||||
v-for="(label, index) in torrent?.labels"
|
v-for="(label, index) in torrent?.labels"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
|||||||
@@ -267,6 +267,28 @@ async function recognize(path: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 调用API刮削
|
||||||
|
async function scrape(path: string) {
|
||||||
|
try {
|
||||||
|
// 显示进度条
|
||||||
|
progressDialog.value = true
|
||||||
|
progressText.value = `正在刮削 ${path} ...`
|
||||||
|
const result: { [key: string]: any } = await api.get('media/scrape', {
|
||||||
|
params: {
|
||||||
|
path,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// 关闭进度条
|
||||||
|
progressDialog.value = false
|
||||||
|
if (!result.success)
|
||||||
|
$toast.error(result.message)
|
||||||
|
else
|
||||||
|
$toast.success(`${path}削刮完成!`)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
// 弹出菜单
|
// 弹出菜单
|
||||||
const dropdownItems = ref([
|
const dropdownItems = ref([
|
||||||
{
|
{
|
||||||
@@ -279,8 +301,17 @@ const dropdownItems = ref([
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
title: '重命名',
|
title: '刮削',
|
||||||
value: 2,
|
value: 2,
|
||||||
|
props: {
|
||||||
|
prependIcon: 'mdi-auto-fix',
|
||||||
|
click: (_item: FileItem) => {
|
||||||
|
scrape(_item.path || '')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
title: '重命名',
|
||||||
|
value: 3,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-rename',
|
prependIcon: 'mdi-rename',
|
||||||
click: showRenmae,
|
click: showRenmae,
|
||||||
@@ -288,7 +319,7 @@ const dropdownItems = ref([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '整理',
|
title: '整理',
|
||||||
value: 3,
|
value: 4,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-folder-arrow-right',
|
prependIcon: 'mdi-folder-arrow-right',
|
||||||
click: showTransfer,
|
click: showTransfer,
|
||||||
@@ -296,7 +327,7 @@ const dropdownItems = ref([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '删除',
|
title: '删除',
|
||||||
value: 4,
|
value: 5,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-delete-outline',
|
prependIcon: 'mdi-delete-outline',
|
||||||
color: 'error',
|
color: 'error',
|
||||||
@@ -345,111 +376,173 @@ onMounted(() => {
|
|||||||
<VCardText v-else-if="dirs.length || files.length" class="p-0">
|
<VCardText v-else-if="dirs.length || files.length" class="p-0">
|
||||||
<VList v-if="dirs.length" subheader>
|
<VList v-if="dirs.length" subheader>
|
||||||
<VListSubheader>目录</VListSubheader>
|
<VListSubheader>目录</VListSubheader>
|
||||||
<VListItem
|
<VHover
|
||||||
v-for="(item, index) in dirs"
|
v-for="(item, index) in dirs"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="px-3 pe-1"
|
|
||||||
@click="changePath(item.path)"
|
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #default="hover">
|
||||||
<VIcon icon="mdi-folder-outline" />
|
<VListItem
|
||||||
</template>
|
v-bind="hover.props"
|
||||||
<VListItemTitle v-text="item.name" />
|
class="px-3 pe-1"
|
||||||
<template #append>
|
@click="changePath(item.path)"
|
||||||
<IconBtn class="d-sm-none">
|
>
|
||||||
<VIcon
|
<template #prepend>
|
||||||
icon="mdi-dots-vertical"
|
<VIcon icon="mdi-folder-outline" />
|
||||||
/>
|
</template>
|
||||||
<VMenu
|
<VListItemTitle v-text="item.name" />
|
||||||
activator="parent"
|
<template #append>
|
||||||
close-on-content-click
|
<IconBtn class="d-sm-none">
|
||||||
>
|
<VIcon
|
||||||
<VList>
|
icon="mdi-dots-vertical"
|
||||||
<VListItem
|
/>
|
||||||
v-for="(menu, i) in dropdownItems"
|
<VMenu
|
||||||
:key="i"
|
activator="parent"
|
||||||
variant="plain"
|
close-on-content-click
|
||||||
:base-color="menu.props.color"
|
|
||||||
@click="menu.props.click(item)"
|
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<VList>
|
||||||
<VIcon :icon="menu.props.prependIcon" />
|
<VListItem
|
||||||
|
v-for="(menu, i) in dropdownItems"
|
||||||
|
:key="i"
|
||||||
|
variant="plain"
|
||||||
|
:base-color="menu.props.color"
|
||||||
|
@click="menu.props.click(item)"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon :icon="menu.props.prependIcon" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle v-text="menu.title" />
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
|
</IconBtn>
|
||||||
|
<span v-show="hover.isHovering" class="flex">
|
||||||
|
<VTooltip text="识别">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
||||||
|
<VIcon icon="mdi-text-recognition" />
|
||||||
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
<VListItemTitle v-text="menu.title" />
|
</VTooltip>
|
||||||
</VListItem>
|
<VTooltip text="刮削">
|
||||||
</VList>
|
<template #activator="{ props }">
|
||||||
</VMenu>
|
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="scrape(item.path)">
|
||||||
</IconBtn>
|
<VIcon icon="mdi-auto-fix" />
|
||||||
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
</IconBtn>
|
||||||
<VIcon icon="mdi-text-recognition" />
|
</template>
|
||||||
</IconBtn>
|
</VTooltip>
|
||||||
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
<VTooltip text="重命名">
|
||||||
<VIcon icon="mdi-rename" />
|
<template #activator="{ props }">
|
||||||
</IconBtn>
|
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
||||||
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
<VIcon icon="mdi-rename" />
|
||||||
<VIcon icon="mdi-folder-arrow-right" />
|
</IconBtn>
|
||||||
</IconBtn>
|
</template>
|
||||||
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
</VTooltip>
|
||||||
<VIcon icon="mdi-delete-outline" />
|
<VTooltip text="整理">
|
||||||
</IconBtn>
|
<template #activator="{ props }">
|
||||||
|
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
||||||
|
<VIcon icon="mdi-folder-arrow-right" />
|
||||||
|
</IconBtn>
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
|
<VTooltip text="删除">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
||||||
|
<VIcon icon="mdi-delete-outline" color="error" />
|
||||||
|
</IconBtn>
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VListItem>
|
||||||
</template>
|
</template>
|
||||||
</VListItem>
|
</VHover>
|
||||||
</VList>
|
</VList>
|
||||||
<VDivider v-if="dirs.length && files.length" />
|
<VDivider v-if="dirs.length && files.length" />
|
||||||
<VList v-if="files.length" subheader>
|
<VList v-if="files.length" subheader>
|
||||||
<VListSubheader>文件</VListSubheader>
|
<VListSubheader>文件</VListSubheader>
|
||||||
<VListItem
|
<VHover
|
||||||
v-for="(item, index) in files"
|
v-for="(item, index) in files"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="pl-3 pe-1"
|
|
||||||
@click="changePath(item.path)"
|
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #default="hover">
|
||||||
<VIcon v-if="inProps.icons" :icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other" />
|
<VListItem
|
||||||
</template>
|
v-bind="hover.props"
|
||||||
|
class="pl-3 pe-1"
|
||||||
|
@click="changePath(item.path)"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon v-if="inProps.icons" :icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<VListItemTitle v-text="item.name" />
|
<VListItemTitle v-text="item.name" />
|
||||||
<VListItemSubtitle> {{ formatBytes(item.size) }}</VListItemSubtitle>
|
<VListItemSubtitle> {{ formatBytes(item.size) }}</VListItemSubtitle>
|
||||||
|
|
||||||
<template #append>
|
<template #append>
|
||||||
<IconBtn class="d-sm-none">
|
<IconBtn class="d-sm-none">
|
||||||
<VIcon
|
<VIcon
|
||||||
icon="mdi-dots-vertical"
|
icon="mdi-dots-vertical"
|
||||||
/>
|
/>
|
||||||
<VMenu
|
<VMenu
|
||||||
activator="parent"
|
activator="parent"
|
||||||
close-on-content-click
|
close-on-content-click
|
||||||
>
|
|
||||||
<VList>
|
|
||||||
<VListItem
|
|
||||||
v-for="(menu, i) in dropdownItems"
|
|
||||||
:key="i"
|
|
||||||
variant="plain"
|
|
||||||
:base-color="menu.props.color"
|
|
||||||
@click="menu.props.click(item)"
|
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<VList>
|
||||||
<VIcon :icon="menu.props.prependIcon" />
|
<VListItem
|
||||||
|
v-for="(menu, i) in dropdownItems"
|
||||||
|
:key="i"
|
||||||
|
variant="plain"
|
||||||
|
:base-color="menu.props.color"
|
||||||
|
@click="menu.props.click(item)"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon :icon="menu.props.prependIcon" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle v-text="menu.title" />
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
|
</IconBtn>
|
||||||
|
<span v-show="hover.isHovering" class="flex">
|
||||||
|
<VTooltip text="识别">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
||||||
|
<VIcon icon="mdi-text-recognition" />
|
||||||
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
<VListItemTitle v-text="menu.title" />
|
</VTooltip>
|
||||||
</VListItem>
|
<VTooltip text="刮削">
|
||||||
</VList>
|
<template #activator="{ props }">
|
||||||
</VMenu>
|
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="scrape(item.path)">
|
||||||
</IconBtn>
|
<VIcon icon="mdi-auto-fix" />
|
||||||
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
</IconBtn>
|
||||||
<VIcon icon="mdi-text-recognition" />
|
</template>
|
||||||
</IconBtn>
|
</VTooltip>
|
||||||
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
<VTooltip text="重命名">
|
||||||
<VIcon icon="mdi-rename" />
|
<template #activator="{ props }">
|
||||||
</IconBtn>
|
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
||||||
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
<VIcon icon="mdi-rename" />
|
||||||
<VIcon icon="mdi-folder-arrow-right" />
|
</IconBtn>
|
||||||
</IconBtn>
|
</template>
|
||||||
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
</VTooltip>
|
||||||
<VIcon icon="mdi-delete-outline" />
|
<VTooltip text="整理">
|
||||||
</IconBtn>
|
<template #activator="{ props }">
|
||||||
|
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
||||||
|
<VIcon icon="mdi-folder-arrow-right" />
|
||||||
|
</IconBtn>
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
|
<VTooltip text="删除">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<IconBtn v-bind="props" class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
||||||
|
<VIcon icon="mdi-delete-outline" color="error" />
|
||||||
|
</IconBtn>
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VListItem>
|
||||||
</template>
|
</template>
|
||||||
</VListItem>
|
</VHover>
|
||||||
</VList>
|
</VList>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardText
|
<VCardText
|
||||||
|
|||||||
@@ -144,19 +144,31 @@ const sortIcon = computed(() => {
|
|||||||
</template>
|
</template>
|
||||||
</VToolbarItems>
|
</VToolbarItems>
|
||||||
<div class="flex-grow-1" />
|
<div class="flex-grow-1" />
|
||||||
<IconBtn @click="changeSort">
|
<VTooltip text="调整排序">
|
||||||
<VIcon :icon="sortIcon" />
|
<template #activator="{ props }">
|
||||||
</IconBtn>
|
<IconBtn v-bind="props" @click="changeSort">
|
||||||
<IconBtn v-if="pathSegments.length > 0" @click="goUp">
|
<VIcon :icon="sortIcon" />
|
||||||
<VIcon icon="mdi-arrow-up-bold-outline" />
|
</IconBtn>
|
||||||
</IconBtn>
|
</template>
|
||||||
|
</VTooltip>
|
||||||
|
<VTooltip text="返回上一级" v-if="pathSegments.length > 0">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<IconBtn v-bind="props" @click="goUp">
|
||||||
|
<VIcon icon="mdi-arrow-up-bold-outline" />
|
||||||
|
</IconBtn>
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
<VDialog
|
<VDialog
|
||||||
v-model="newFolderPopper"
|
v-model="newFolderPopper"
|
||||||
max-width="50rem"
|
max-width="50rem"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<IconBtn title="新建文件夹" v-bind="props">
|
<IconBtn v-bind="props">
|
||||||
<VIcon icon="mdi-folder-plus-outline" />
|
<VTooltip text="新建文件夹">
|
||||||
|
<template #activator="{ props: _props }">
|
||||||
|
<VIcon v-bind="_props" icon="mdi-folder-plus-outline" />
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
<VCard title="新建文件夹">
|
<VCard title="新建文件夹">
|
||||||
|
|||||||
@@ -326,7 +326,6 @@ watchEffect(() => {
|
|||||||
<VRow>
|
<VRow>
|
||||||
<VCol
|
<VCol
|
||||||
cols="12"
|
cols="12"
|
||||||
md="4"
|
|
||||||
>
|
>
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="subscribeForm.save_path"
|
v-model="subscribeForm.save_path"
|
||||||
|
|||||||
@@ -32,23 +32,58 @@ const formData = ref<any>(elementProps.form || {})
|
|||||||
<template>
|
<template>
|
||||||
<Component
|
<Component
|
||||||
:is="formItem.component"
|
:is="formItem.component"
|
||||||
v-if="!formItem.html"
|
v-if="!formItem.html && !!formItem.props?.modelvalue"
|
||||||
v-bind="formItem.props"
|
v-bind="formItem.props"
|
||||||
v-model="formData[formItem.props?.model || '']"
|
v-model:value="formData[formItem.props?.modelvalue]"
|
||||||
>
|
>
|
||||||
{{ formItem.text }}
|
{{ formItem.text }}
|
||||||
<FormRender
|
<template
|
||||||
v-for="(innerItem, innerIndex) in (formItem.content || [])"
|
v-for="(innerItem, innerIndex) in (formItem.content || [])"
|
||||||
:key="innerIndex"
|
:key="innerIndex"
|
||||||
v-model="formData[innerItem.props?.model || '']"
|
>
|
||||||
:config="innerItem"
|
<FormRender
|
||||||
:form="formData"
|
v-if="!!innerItem.props?.modelvalue"
|
||||||
/>
|
v-model:value="formData[innerItem.props?.modelvalue]"
|
||||||
|
:config="innerItem"
|
||||||
|
:form="formData"
|
||||||
|
/>
|
||||||
|
<FormRender
|
||||||
|
v-else
|
||||||
|
v-model="formData[innerItem.props?.model]"
|
||||||
|
:config="innerItem"
|
||||||
|
:form="formData"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</Component>
|
</Component>
|
||||||
<Component
|
<Component
|
||||||
:is="formItem.component"
|
:is="formItem.component"
|
||||||
v-if="formItem.html"
|
v-else-if="formItem.html"
|
||||||
v-bind="formItem.props"
|
v-bind="formItem.props"
|
||||||
v-html="formItem.html"
|
v-html="formItem.html"
|
||||||
/>
|
/>
|
||||||
|
<Component
|
||||||
|
:is="formItem.component"
|
||||||
|
v-else
|
||||||
|
v-bind="formItem.props"
|
||||||
|
v-model="formData[formItem.props?.model]"
|
||||||
|
>
|
||||||
|
{{ formItem.text }}
|
||||||
|
<template
|
||||||
|
v-for="(innerItem, innerIndex) in (formItem.content || [])"
|
||||||
|
:key="innerIndex"
|
||||||
|
>
|
||||||
|
<FormRender
|
||||||
|
v-if="!!innerItem.props?.modelvalue"
|
||||||
|
v-model:value="formData[innerItem.props?.modelvalue]"
|
||||||
|
:config="innerItem"
|
||||||
|
:form="formData"
|
||||||
|
/>
|
||||||
|
<FormRender
|
||||||
|
v-else
|
||||||
|
v-model="formData[innerItem.props?.model]"
|
||||||
|
:config="innerItem"
|
||||||
|
:form="formData"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Component>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ const themes: ThemeSwitcherTheme[] = [
|
|||||||
name: 'purple',
|
name: 'purple',
|
||||||
icon: 'mdi-brightness-4',
|
icon: 'mdi-brightness-4',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'auto',
|
||||||
|
icon: 'mdi-brightness-auto',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import NameTestView from '@/views/system/NameTestView.vue'
|
|||||||
import NetTestView from '@/views/system/NetTestView.vue'
|
import NetTestView from '@/views/system/NetTestView.vue'
|
||||||
import LoggingView from '@/views/system/LoggingView.vue'
|
import LoggingView from '@/views/system/LoggingView.vue'
|
||||||
import RuleTestView from '@/views/system/RuleTestView.vue'
|
import RuleTestView from '@/views/system/RuleTestView.vue'
|
||||||
|
import store from '@/store'
|
||||||
|
|
||||||
// App捷径
|
// App捷径
|
||||||
const appsMenu = ref(false)
|
const appsMenu = ref(false)
|
||||||
@@ -18,6 +19,12 @@ const loggingDialog = ref(false)
|
|||||||
|
|
||||||
// 过滤规则弹窗
|
// 过滤规则弹窗
|
||||||
const ruleTestDialog = ref(false)
|
const ruleTestDialog = ref(false)
|
||||||
|
|
||||||
|
// 拼接全部日志url
|
||||||
|
function allLoggingUrl() {
|
||||||
|
const token = store.state.auth.token
|
||||||
|
return `${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}&length=-1`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -37,7 +44,11 @@ const ruleTestDialog = ref(false)
|
|||||||
class="me-2"
|
class="me-2"
|
||||||
v-bind="props"
|
v-bind="props"
|
||||||
>
|
>
|
||||||
<VIcon icon="mdi-checkbox-multiple-blank-outline" />
|
<VTooltip text="捷径">
|
||||||
|
<template #activator="{ props: _props }">
|
||||||
|
<VIcon v-bind="_props" icon="mdi-checkbox-multiple-blank-outline" />
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
<!-- Menu Content -->
|
<!-- Menu Content -->
|
||||||
@@ -171,8 +182,19 @@ const ruleTestDialog = ref(false)
|
|||||||
class="w-full lg:w-4/5"
|
class="w-full lg:w-4/5"
|
||||||
scrollable
|
scrollable
|
||||||
>
|
>
|
||||||
<VCard title="实时日志">
|
<VCard>
|
||||||
<DialogCloseBtn @click="loggingDialog = false" />
|
<DialogCloseBtn @click="loggingDialog = false" />
|
||||||
|
<VCardItem>
|
||||||
|
<VCardTitle class="inline-flex">
|
||||||
|
实时日志
|
||||||
|
<a class="mx-2 inline-flex items-center justify-center" :href="allLoggingUrl()" target="_blank">
|
||||||
|
<div class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700">
|
||||||
|
<VIcon icon="mdi-open-in-new" />
|
||||||
|
<span class="ms-1">在新窗口中打开</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</VCardTitle>
|
||||||
|
</VCardItem>
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<LoggingView />
|
<LoggingView />
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
|||||||
13
src/main.ts
13
src/main.ts
@@ -1,7 +1,11 @@
|
|||||||
|
import { VAceEditor } from 'vue3-ace-editor'
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import '@/@iconify/icons-bundle'
|
import '@/@iconify/icons-bundle'
|
||||||
import ToastPlugin from 'vue-toast-notification'
|
import ToastPlugin from 'vue-toast-notification'
|
||||||
import VuetifyUseDialog from 'vuetify-use-dialog'
|
import VuetifyUseDialog from 'vuetify-use-dialog'
|
||||||
|
import './ace-config'
|
||||||
|
import VueApexCharts from 'vue3-apexcharts'
|
||||||
|
import { removeEl } from './@core/utils/dom'
|
||||||
import App from '@/App.vue'
|
import App from '@/App.vue'
|
||||||
import vuetify from '@/plugins/vuetify'
|
import vuetify from '@/plugins/vuetify'
|
||||||
import { loadFonts } from '@/plugins/webfontloader'
|
import { loadFonts } from '@/plugins/webfontloader'
|
||||||
@@ -11,14 +15,17 @@ import '@core/scss/template/index.scss'
|
|||||||
import '@layouts/styles/index.scss'
|
import '@layouts/styles/index.scss'
|
||||||
import '@styles/styles.scss'
|
import '@styles/styles.scss'
|
||||||
import 'vue-toast-notification/dist/theme-bootstrap.css'
|
import 'vue-toast-notification/dist/theme-bootstrap.css'
|
||||||
import { removeEl } from '@/util'
|
|
||||||
|
|
||||||
loadFonts()
|
loadFonts()
|
||||||
|
|
||||||
// Create vue app
|
// 创建Vue实例
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
// Use plugins Mount vue app
|
// 注册全局组件
|
||||||
|
app.component('VAceEditor', VAceEditor)
|
||||||
|
.component('VApexChart', VueApexCharts)
|
||||||
|
|
||||||
|
// 注册插件
|
||||||
app
|
app
|
||||||
.use(vuetify)
|
.use(vuetify)
|
||||||
.use(router)
|
.use(router)
|
||||||
|
|||||||
@@ -6,11 +6,57 @@ import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue'
|
|||||||
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
|
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
|
||||||
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
|
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
|
||||||
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||||
|
import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue'
|
||||||
|
import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
|
||||||
|
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
|
||||||
|
|
||||||
|
// 仪表盘配置
|
||||||
|
const dashboard_names = {
|
||||||
|
storage: '存储空间',
|
||||||
|
mediaStatistic: '媒体统计',
|
||||||
|
weeklyOverview: '最近入库',
|
||||||
|
speed: '实时速率',
|
||||||
|
scheduler: '后台任务',
|
||||||
|
cpu: 'CPU',
|
||||||
|
memory: '内存',
|
||||||
|
library: '我的媒体库',
|
||||||
|
playing: '继续观看',
|
||||||
|
latest: '最近添加',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹窗
|
||||||
|
const dialog = ref(false)
|
||||||
|
|
||||||
|
// 从localStorage中获取数据
|
||||||
|
const default_config = {
|
||||||
|
mediaStatistic: true,
|
||||||
|
scheduler: false,
|
||||||
|
speed: false,
|
||||||
|
storage: true,
|
||||||
|
weeklyOverview: false,
|
||||||
|
cpu: false,
|
||||||
|
memory: false,
|
||||||
|
library: true,
|
||||||
|
playing: true,
|
||||||
|
latest: true,
|
||||||
|
}
|
||||||
|
const config = ref(JSON.parse(localStorage.getItem('MP_DASHBOARD') || '{}'))
|
||||||
|
if (Object.keys(config.value).length === 0) {
|
||||||
|
config.value = default_config
|
||||||
|
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置项目
|
||||||
|
function setDashboardConfig() {
|
||||||
|
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value))
|
||||||
|
dialog.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VRow class="match-height">
|
<VRow class="match-height">
|
||||||
<VCol
|
<VCol
|
||||||
|
v-if="config.storage"
|
||||||
cols="12"
|
cols="12"
|
||||||
md="4"
|
md="4"
|
||||||
>
|
>
|
||||||
@@ -18,6 +64,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
|||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
<VCol
|
<VCol
|
||||||
|
v-if="config.mediaStatistic"
|
||||||
cols="12"
|
cols="12"
|
||||||
md="8"
|
md="8"
|
||||||
>
|
>
|
||||||
@@ -25,6 +72,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
|||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
<VCol
|
<VCol
|
||||||
|
v-if="config.weeklyOverview"
|
||||||
cols="12"
|
cols="12"
|
||||||
md="4"
|
md="4"
|
||||||
>
|
>
|
||||||
@@ -32,6 +80,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
|||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
<VCol
|
<VCol
|
||||||
|
v-if="config.speed"
|
||||||
cols="12"
|
cols="12"
|
||||||
md="4"
|
md="4"
|
||||||
>
|
>
|
||||||
@@ -39,6 +88,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
|||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
<VCol
|
<VCol
|
||||||
|
v-if="config.scheduler"
|
||||||
cols="12"
|
cols="12"
|
||||||
md="4"
|
md="4"
|
||||||
>
|
>
|
||||||
@@ -46,6 +96,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
|||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
<VCol
|
<VCol
|
||||||
|
v-if="config.cpu"
|
||||||
cols="12"
|
cols="12"
|
||||||
md="6"
|
md="6"
|
||||||
>
|
>
|
||||||
@@ -53,10 +104,75 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
|||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
<VCol
|
<VCol
|
||||||
|
v-if="config.memory"
|
||||||
cols="12"
|
cols="12"
|
||||||
md="6"
|
md="6"
|
||||||
>
|
>
|
||||||
<AnalyticsMemory />
|
<AnalyticsMemory />
|
||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
|
<VCol
|
||||||
|
v-if="config.library"
|
||||||
|
cols="12"
|
||||||
|
>
|
||||||
|
<MediaServerLibrary />
|
||||||
|
</VCol>
|
||||||
|
|
||||||
|
<VCol
|
||||||
|
v-if="config.playing"
|
||||||
|
cols="12"
|
||||||
|
>
|
||||||
|
<MediaServerPlaying />
|
||||||
|
</VCol>
|
||||||
|
|
||||||
|
<VCol
|
||||||
|
v-if="config.latest"
|
||||||
|
cols="12"
|
||||||
|
>
|
||||||
|
<MediaServerLatest />
|
||||||
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
|
<!-- 底部操作按钮 -->
|
||||||
|
<span class="fixed right-5 bottom-5">
|
||||||
|
<VBtn icon="mdi-view-dashboard-edit" class="me-2" color="primary" size="x-large" @click="dialog = true" />
|
||||||
|
</span>
|
||||||
|
<!-- 弹窗,根据配置生成选项 -->
|
||||||
|
<VDialog
|
||||||
|
v-model="dialog"
|
||||||
|
max-width="600"
|
||||||
|
scrollable
|
||||||
|
>
|
||||||
|
<VCard title="设置仪表板">
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol
|
||||||
|
v-for="(item, key) in dashboard_names"
|
||||||
|
:key="key"
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VCheckbox
|
||||||
|
v-model="config[key]"
|
||||||
|
:label="dashboard_names[key]"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
<VCardActions>
|
||||||
|
<VBtn
|
||||||
|
color="primary"
|
||||||
|
@click="dialog = false"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</VBtn>
|
||||||
|
<VSpacer />
|
||||||
|
<VBtn
|
||||||
|
color="primary"
|
||||||
|
@click="setDashboardConfig"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</VBtn>
|
||||||
|
</VCardActions>
|
||||||
|
</VCard>
|
||||||
|
</vdialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
|
|||||||
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
|
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
|
||||||
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
|
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
|
||||||
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
|
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
|
||||||
|
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@@ -20,6 +21,11 @@ const tabs = [
|
|||||||
icon: 'mdi-account',
|
icon: 'mdi-account',
|
||||||
tab: 'account',
|
tab: 'account',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '系统',
|
||||||
|
icon: 'mdi-cog',
|
||||||
|
tab: 'system',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '站点',
|
title: '站点',
|
||||||
icon: 'mdi-web',
|
icon: 'mdi-web',
|
||||||
@@ -83,6 +89,13 @@ const tabs = [
|
|||||||
</transition>
|
</transition>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
|
<!-- 系统 -->
|
||||||
|
<VWindowItem value="system">
|
||||||
|
<transition name="fade-slide" appear>
|
||||||
|
<AccountSettingSystem />
|
||||||
|
</transition>
|
||||||
|
</VWindowItem>
|
||||||
|
|
||||||
<!-- 站点 -->
|
<!-- 站点 -->
|
||||||
<VWindowItem value="site">
|
<VWindowItem value="site">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
import { configureNProgress, doneNProgress, startNProgress } from '@/api/nprogress'
|
import { configureNProgress, doneNProgress, startNProgress } from '@/api/nprogress'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ configureNProgress()
|
|||||||
|
|
||||||
// Router
|
// Router
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
scrollBehavior(to, from, savedPosition) {
|
scrollBehavior(to, from, savedPosition) {
|
||||||
// 如果页面有缓存那么恢复其位置, 否则始终滚动到顶部
|
// 如果页面有缓存那么恢复其位置, 否则始终滚动到顶部
|
||||||
if (to.meta.keepAlive && savedPosition)
|
if (to.meta.keepAlive && savedPosition)
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root{
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei UI", "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nprogress .bar {
|
#nprogress .bar {
|
||||||
background: rgb(var(--v-theme-primary)) !important;
|
background: rgb(var(--v-theme-primary)) !important;
|
||||||
top: env(safe-area-inset-top) !important;
|
top: env(safe-area-inset-top) !important;
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from './dom'
|
|
||||||
48
src/views/dashboard/MediaServerLatest.vue
Normal file
48
src/views/dashboard/MediaServerLatest.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import api from '@/api'
|
||||||
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
import PosterCard from '@/components/cards/PosterCard.vue'
|
||||||
|
|
||||||
|
// 最近入库列表
|
||||||
|
const latestList = ref<MediaServerPlayItem[]>([])
|
||||||
|
|
||||||
|
// 调用API查询
|
||||||
|
async function loadLatest() {
|
||||||
|
try {
|
||||||
|
latestList.value = await api.get('mediaserver/latest')
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadLatest()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VCard>
|
||||||
|
<VCardItem>
|
||||||
|
<VCardTitle>最近添加</VCardTitle>
|
||||||
|
</VCardItem>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="latestList.length > 0"
|
||||||
|
class="grid gap-4 grid-media-card mx-3 mb-3"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<PosterCard
|
||||||
|
v-for="data in latestList"
|
||||||
|
:key="data.id"
|
||||||
|
:media="data"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.grid-media-card {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
src/views/dashboard/MediaServerLibrary.vue
Normal file
50
src/views/dashboard/MediaServerLibrary.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import api from '@/api'
|
||||||
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
import LibraryCard from '@/components/cards/LibraryCard.vue'
|
||||||
|
|
||||||
|
// 媒体库列表
|
||||||
|
const libraryList = ref<MediaServerPlayItem[]>([])
|
||||||
|
|
||||||
|
// 调用API查询
|
||||||
|
async function loadLibrary() {
|
||||||
|
try {
|
||||||
|
libraryList.value = await api.get('mediaserver/library')
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadLibrary()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VCard>
|
||||||
|
<VCardItem>
|
||||||
|
<VCardTitle>我的媒体库</VCardTitle>
|
||||||
|
</VCardItem>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="libraryList.length > 0"
|
||||||
|
class="grid gap-4 grid-backdrop-card mx-3"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<LibraryCard
|
||||||
|
v-for="data in libraryList"
|
||||||
|
:key="data.id"
|
||||||
|
:media="data"
|
||||||
|
height="10rem"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.grid-backdrop-card {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||||
|
padding-block-end: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
src/views/dashboard/MediaServerPlaying.vue
Normal file
50
src/views/dashboard/MediaServerPlaying.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import api from '@/api'
|
||||||
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
import BackdropCard from '@/components/cards/BackdropCard.vue'
|
||||||
|
|
||||||
|
// 继续播放列表
|
||||||
|
const playingList = ref<MediaServerPlayItem[]>([])
|
||||||
|
|
||||||
|
// 调用API查询
|
||||||
|
async function loadPlayingList() {
|
||||||
|
try {
|
||||||
|
playingList.value = await api.get('mediaserver/playing')
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadPlayingList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VCard>
|
||||||
|
<VCardItem>
|
||||||
|
<VCardTitle>继续观看</VCardTitle>
|
||||||
|
</VCardItem>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="playingList.length > 0"
|
||||||
|
class="grid gap-4 grid-backdrop-card mx-3"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<BackdropCard
|
||||||
|
v-for="data in playingList"
|
||||||
|
:key="data.id"
|
||||||
|
:media="data"
|
||||||
|
height="10rem"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.grid-backdrop-card {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||||
|
padding-block-end: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -25,8 +25,8 @@ const mediaDetail = ref<MediaInfo>({} as MediaInfo)
|
|||||||
// 订阅编辑弹窗
|
// 订阅编辑弹窗
|
||||||
const subscribeEditDialog = ref(false)
|
const subscribeEditDialog = ref(false)
|
||||||
|
|
||||||
// 本地是否存在
|
// 本地是否存在,存在则包括Item信息
|
||||||
const isExists = ref(false)
|
const existsItemId = ref('')
|
||||||
|
|
||||||
// 是否已订阅
|
// 是否已订阅
|
||||||
const isSubscribed = ref(false)
|
const isSubscribed = ref(false)
|
||||||
@@ -46,6 +46,11 @@ const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
|
|||||||
// 订阅编号
|
// 订阅编号
|
||||||
const subscribeId = ref<number>()
|
const subscribeId = ref<number>()
|
||||||
|
|
||||||
|
// 订阅规则
|
||||||
|
const subscribeRules = ref({
|
||||||
|
show_edit_dialog: false,
|
||||||
|
})
|
||||||
|
|
||||||
// 调用API查询详情
|
// 调用API查询详情
|
||||||
async function getMediaDetail() {
|
async function getMediaDetail() {
|
||||||
if (mediaProps.mediaid && mediaProps.type) {
|
if (mediaProps.mediaid && mediaProps.type) {
|
||||||
@@ -59,9 +64,8 @@ async function getMediaDetail() {
|
|||||||
return
|
return
|
||||||
|
|
||||||
// 检查存在状态
|
// 检查存在状态
|
||||||
if (mediaDetail.value.type === '电影')
|
checkExists()
|
||||||
checkMovieExists()
|
if (mediaDetail.value.type === '电视剧')
|
||||||
else
|
|
||||||
checkSeasonsNotExists()
|
checkSeasonsNotExists()
|
||||||
// 检查订阅状态
|
// 检查订阅状态
|
||||||
if (mediaDetail.value.type === '电影')
|
if (mediaDetail.value.type === '电影')
|
||||||
@@ -86,9 +90,9 @@ async function loadSeasonEpisodes(season: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查询当前媒体是否已入库
|
// 查询当前媒体是否已入库
|
||||||
async function checkMovieExists() {
|
async function checkExists() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('media/exists', {
|
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||||
params: {
|
params: {
|
||||||
tmdbid: mediaDetail.value.tmdb_id,
|
tmdbid: mediaDetail.value.tmdb_id,
|
||||||
title: mediaDetail.value.title,
|
title: mediaDetail.value.title,
|
||||||
@@ -99,7 +103,7 @@ async function checkMovieExists() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (result.success)
|
if (result.success)
|
||||||
isExists.value = true
|
existsItemId.value = result.data.item.id
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -133,11 +137,8 @@ async function checkSeasonsNotExists() {
|
|||||||
if (mediaDetail.value.type !== '电视剧')
|
if (mediaDetail.value.type !== '电视剧')
|
||||||
return
|
return
|
||||||
try {
|
try {
|
||||||
const result: NotExistMediaInfo[] = await api.post('download/notexists', mediaDetail.value)
|
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', mediaDetail.value)
|
||||||
if (result) {
|
if (result) {
|
||||||
if (result.length === 0)
|
|
||||||
isExists.value = true
|
|
||||||
|
|
||||||
result.forEach((item) => {
|
result.forEach((item) => {
|
||||||
// 0-已入库 1-部分缺失 2-全部缺失
|
// 0-已入库 1-部分缺失 2-全部缺失
|
||||||
let state = 0
|
let state = 0
|
||||||
@@ -145,8 +146,6 @@ async function checkSeasonsNotExists() {
|
|||||||
state = 2
|
state = 2
|
||||||
else if (item.episodes.length < item.total_episode)
|
else if (item.episodes.length < item.total_episode)
|
||||||
state = 1
|
state = 1
|
||||||
if (state !== 2)
|
|
||||||
isExists.value = true
|
|
||||||
seasonsNotExisted.value[item.season] = state
|
seasonsNotExisted.value[item.season] = state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -188,7 +187,7 @@ async function addSubscribe(season = 0) {
|
|||||||
startNProgress()
|
startNProgress()
|
||||||
try {
|
try {
|
||||||
// 是否洗版
|
// 是否洗版
|
||||||
let best_version = isExists.value ? 1 : 0
|
let best_version = existsItemId.value ? 1 : 0
|
||||||
if (season)
|
if (season)
|
||||||
// 全部存在时洗版
|
// 全部存在时洗版
|
||||||
best_version = !seasonsNotExisted.value[season] ? 1 : 0
|
best_version = !seasonsNotExisted.value[season] ? 1 : 0
|
||||||
@@ -221,7 +220,7 @@ async function addSubscribe(season = 0) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 显示编辑弹窗
|
// 显示编辑弹窗
|
||||||
if (result.success) {
|
if (result.success && subscribeRules.value.show_edit_dialog) {
|
||||||
subscribeId.value = result.data.id
|
subscribeId.value = result.data.id
|
||||||
subscribeEditDialog.value = true
|
subscribeEditDialog.value = true
|
||||||
}
|
}
|
||||||
@@ -283,6 +282,20 @@ async function removeSubscribe(season: number) {
|
|||||||
doneNProgress()
|
doneNProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询订阅弹窗规则
|
||||||
|
async function querySubscribeRules() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get(
|
||||||
|
'system/setting/DefaultFilterRules',
|
||||||
|
)
|
||||||
|
if (result.data?.value)
|
||||||
|
subscribeRules.value = result.data?.value
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 订阅按钮响应
|
// 订阅按钮响应
|
||||||
function handleSubscribe(season = 0) {
|
function handleSubscribe(season = 0) {
|
||||||
if (isSubscribed.value)
|
if (isSubscribed.value)
|
||||||
@@ -403,8 +416,29 @@ function handleSearch(area: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 跳转播放页面
|
||||||
|
async function handlePlay() {
|
||||||
|
// 获取播放链接地址
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get(
|
||||||
|
`mediaserver/play/${existsItemId.value}`,
|
||||||
|
)
|
||||||
|
if (result?.success) {
|
||||||
|
// 打开链接地址
|
||||||
|
setTimeout(() => {
|
||||||
|
window.open(result.data.url, '_blank')
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
else { $toast.error(`获取播放链接失败:${result.message}!`) }
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
getMediaDetail()
|
getMediaDetail()
|
||||||
|
querySubscribeRules()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -438,7 +472,7 @@ onBeforeMount(() => {
|
|||||||
</VImg>
|
</VImg>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-title">
|
<div class="media-title">
|
||||||
<div v-if="isExists" class="media-status">
|
<div v-if="existsItemId" class="media-status">
|
||||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap transition !no-underline bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 hover:bg-green-500 hover:bg-opacity-100 false overflow-hidden">
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap transition !no-underline bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 hover:bg-green-500 hover:bg-opacity-100 false overflow-hidden">
|
||||||
<div class="relative z-20 flex items-center false"><span>已入库</span></div>
|
<div class="relative z-20 flex items-center false"><span>已入库</span></div>
|
||||||
</span>
|
</span>
|
||||||
@@ -458,7 +492,7 @@ onBeforeMount(() => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-actions">
|
<div class="media-actions">
|
||||||
<VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" variant="tonal" color="info">
|
<VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" variant="tonal" color="info" class="mb-2">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon icon="mdi-magnify" />
|
<VIcon icon="mdi-magnify" />
|
||||||
</template>
|
</template>
|
||||||
@@ -484,12 +518,18 @@ onBeforeMount(() => {
|
|||||||
</VList>
|
</VList>
|
||||||
</VMenu>
|
</VMenu>
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id" class="ms-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
|
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id" class="ms-2 mb-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon :icon="getSubscribeIcon" />
|
<VIcon :icon="getSubscribeIcon" />
|
||||||
</template>
|
</template>
|
||||||
{{ isSubscribed ? '已订阅' : '订阅' }}
|
{{ isSubscribed ? '已订阅' : '订阅' }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
|
<VBtn v-if="existsItemId" class="ms-2 mb-2" variant="tonal" @click="handlePlay()">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-play" />
|
||||||
|
</template>
|
||||||
|
在线播放
|
||||||
|
</VBtn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-overview">
|
<div class="media-overview">
|
||||||
@@ -504,7 +544,9 @@ onBeforeMount(() => {
|
|||||||
<ul v-if="mediaDetail.tmdb_id" class="media-crew">
|
<ul v-if="mediaDetail.tmdb_id" class="media-crew">
|
||||||
<li v-for="director in mediaDetail.directors" :key="director.id">
|
<li v-for="director in mediaDetail.directors" :key="director.id">
|
||||||
<span>{{ director.job }}</span>
|
<span>{{ director.job }}</span>
|
||||||
<a class="crew-name" :href="`person?personid=${director.id}`" target="_blank">{{ director.name }}</a>
|
<RouterLink :to="`/person?personid=${director.id}`" class="crew-name" target="_blank">
|
||||||
|
{{ director.name }}
|
||||||
|
</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul v-if="!mediaDetail.tmdb_id && mediaDetail.douban_id" class="media-crew">
|
<ul v-if="!mediaDetail.tmdb_id && mediaDetail.douban_id" class="media-crew">
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import type { Ref } from 'vue'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { useDefer } from '@/util'
|
|
||||||
import type { Context } from '@/api/types'
|
import type { Context } from '@/api/types'
|
||||||
import TorrentCard from '@/components/cards/TorrentCard.vue'
|
import TorrentCard from '@/components/cards/TorrentCard.vue'
|
||||||
|
import { useDefer } from '@/@core/utils/dom'
|
||||||
|
|
||||||
interface SearchTorrent extends Context {
|
interface SearchTorrent extends Context {
|
||||||
more?: Array<Context>
|
more?: Array<Context>
|
||||||
@@ -94,7 +92,7 @@ onMounted(() => {
|
|||||||
groupedDataList.value = groupMap
|
groupedDataList.value = groupMap
|
||||||
})
|
})
|
||||||
|
|
||||||
const defer: Ref<Function> = ref(() => true)
|
let defer = (_: number) => true
|
||||||
|
|
||||||
// 计算过滤后的列表
|
// 计算过滤后的列表
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
@@ -135,7 +133,7 @@ watchEffect(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
defer.value = useDefer(dataList.value.length)
|
defer = useDefer(dataList.value.length)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
|
||||||
import type { Context } from '@/api/types'
|
import type { Context } from '@/api/types'
|
||||||
import TorrentItem from '@/components/cards/TorrentItem.vue'
|
import TorrentItem from '@/components/cards/TorrentItem.vue'
|
||||||
|
import { useDefer } from '@/@core/utils/dom'
|
||||||
|
|
||||||
// 定义输入参数
|
// 定义输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -60,6 +60,8 @@ function initOptions(data: Context) {
|
|||||||
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
|
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let defer = (_: number) => true
|
||||||
|
|
||||||
// 计算过滤后的列表
|
// 计算过滤后的列表
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
// 清空列表
|
// 清空列表
|
||||||
@@ -88,6 +90,7 @@ watchEffect(() => {
|
|||||||
)
|
)
|
||||||
dataList.value.push(data)
|
dataList.value.push(data)
|
||||||
})
|
})
|
||||||
|
defer = useDefer(dataList.value.length)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 初始化过滤选项
|
// 初始化过滤选项
|
||||||
@@ -106,14 +109,14 @@ onMounted(() => {
|
|||||||
<VListItemTitle>没有附合当前过滤条件的资源。</VListItemTitle>
|
<VListItemTitle>没有附合当前过滤条件的资源。</VListItemTitle>
|
||||||
</VListItem>
|
</VListItem>
|
||||||
</VList>
|
</VList>
|
||||||
<v-virtual-scroll lines="three" class="rounded" :items="dataList" height="calc(100vh - 156px)">
|
<div>
|
||||||
<template #default="{ item }">
|
<div v-for="(item, index) in dataList" :key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`">
|
||||||
<TorrentItem :torrent="item" />
|
<TorrentItem v-if="defer(index)" :torrent="item" />
|
||||||
</template>
|
</div>
|
||||||
</v-virtual-scroll>
|
</div>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol xl="2" md="3" class="d-none d-md-block">
|
<VCol xl="2" md="3" class="d-none d-md-block">
|
||||||
<VList lines="one" class="rounded" height="calc(100vh - 156px)">
|
<VList lines="one" class="rounded">
|
||||||
<VListSubheader v-if="siteFilterOptions.length > 0">
|
<VListSubheader v-if="siteFilterOptions.length > 0">
|
||||||
站点
|
站点
|
||||||
</VListSubheader>
|
</VListSubheader>
|
||||||
|
|||||||
@@ -5,25 +5,21 @@ import NoDataFound from '@/components/NoDataFound.vue'
|
|||||||
import PluginAppCard from '@/components/cards/PluginAppCard.vue'
|
import PluginAppCard from '@/components/cards/PluginAppCard.vue'
|
||||||
import PluginCard from '@/components/cards/PluginCard.vue'
|
import PluginCard from '@/components/cards/PluginCard.vue'
|
||||||
|
|
||||||
// 数据列表
|
// 已安装插件列表
|
||||||
const dataList = ref<Plugin[]>([])
|
const dataList = ref<Plugin[]>([])
|
||||||
|
|
||||||
|
// 未安装插件列表
|
||||||
|
const uninstalledList = ref<Plugin[]>([])
|
||||||
|
|
||||||
// 是否刷新过
|
// 是否刷新过
|
||||||
const isRefreshed = ref(false)
|
const isRefreshed = ref(false)
|
||||||
|
|
||||||
|
// APP市场是否加载完成
|
||||||
|
const isAppMarketLoaded = ref(false)
|
||||||
|
|
||||||
// APP市场窗口
|
// APP市场窗口
|
||||||
const PluginAppDialog = ref(false)
|
const PluginAppDialog = ref(false)
|
||||||
|
|
||||||
// 获取已安装的插件列表
|
|
||||||
const getInstalledPluginList = computed(() => {
|
|
||||||
return dataList.value.filter(item => item.installed)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取未安装或者有更新的插件列表
|
|
||||||
const getUninstalledPluginList = computed(() => {
|
|
||||||
return dataList.value.filter(item => !item.installed || item.has_update)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 关闭插件市场窗口
|
// 关闭插件市场窗口
|
||||||
function pluginDialogClose() {
|
function pluginDialogClose() {
|
||||||
PluginAppDialog.value = false
|
PluginAppDialog.value = false
|
||||||
@@ -31,14 +27,19 @@ function pluginDialogClose() {
|
|||||||
|
|
||||||
// 新安装了插件
|
// 新安装了插件
|
||||||
function pluginInstalled() {
|
function pluginInstalled() {
|
||||||
fetchData()
|
fetchInstalledPlugins()
|
||||||
pluginDialogClose()
|
pluginDialogClose()
|
||||||
|
fetchUninstalledPlugins()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取插件列表数据
|
// 获取插件列表数据
|
||||||
async function fetchData() {
|
async function fetchInstalledPlugins() {
|
||||||
try {
|
try {
|
||||||
dataList.value = await api.get('plugin/')
|
dataList.value = await api.get('plugin/', {
|
||||||
|
params: {
|
||||||
|
state: 'installed',
|
||||||
|
},
|
||||||
|
})
|
||||||
isRefreshed.value = true
|
isRefreshed.value = true
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -46,8 +47,26 @@ async function fetchData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取未安装插件列表数据
|
||||||
|
async function fetchUninstalledPlugins() {
|
||||||
|
try {
|
||||||
|
uninstalledList.value = await api.get('plugin/', {
|
||||||
|
params: {
|
||||||
|
state: 'market',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
isAppMarketLoaded.value = true
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载时获取数据
|
// 加载时获取数据
|
||||||
onBeforeMount(fetchData)
|
onBeforeMount(() => {
|
||||||
|
fetchInstalledPlugins()
|
||||||
|
fetchUninstalledPlugins()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -63,19 +82,19 @@ onBeforeMount(fetchData)
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="getInstalledPluginList.length > 0"
|
v-if="dataList.length > 0"
|
||||||
class="grid gap-4 grid-plugin-card"
|
class="grid gap-4 grid-plugin-card"
|
||||||
>
|
>
|
||||||
<PluginCard
|
<PluginCard
|
||||||
v-for="data in getInstalledPluginList"
|
v-for="data in dataList"
|
||||||
:key="data.id"
|
:key="data.id"
|
||||||
:plugin="data"
|
:plugin="data"
|
||||||
@remove="fetchData"
|
@remove="fetchInstalledPlugins"
|
||||||
@save="fetchData"
|
@save="fetchInstalledPlugins"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<NoDataFound
|
<NoDataFound
|
||||||
v-if="getInstalledPluginList.length === 0 && isRefreshed"
|
v-if="dataList.length === 0 && isRefreshed"
|
||||||
error-code="404"
|
error-code="404"
|
||||||
error-title="没有安装插件"
|
error-title="没有安装插件"
|
||||||
error-description="点击右下角按钮,前往插件市场安装插件。"
|
error-description="点击右下角按钮,前往插件市场安装插件。"
|
||||||
@@ -86,6 +105,7 @@ onBeforeMount(fetchData)
|
|||||||
fullscreen
|
fullscreen
|
||||||
scrollable
|
scrollable
|
||||||
:scrim="false"
|
:scrim="false"
|
||||||
|
:z-index="1010"
|
||||||
transition="dialog-bottom-transition"
|
transition="dialog-bottom-transition"
|
||||||
>
|
>
|
||||||
<!-- Dialog Activator -->
|
<!-- Dialog Activator -->
|
||||||
@@ -121,16 +141,27 @@ onBeforeMount(fetchData)
|
|||||||
</VToolbar>
|
</VToolbar>
|
||||||
</div>
|
</div>
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<div class="grid gap-4 grid-plugin-card">
|
<div
|
||||||
|
v-if="!isAppMarketLoaded"
|
||||||
|
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
|
||||||
|
>
|
||||||
|
<VProgressCircular
|
||||||
|
v-if="!isAppMarketLoaded"
|
||||||
|
size="48"
|
||||||
|
indeterminate
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card">
|
||||||
<PluginAppCard
|
<PluginAppCard
|
||||||
v-for="data in getUninstalledPluginList"
|
v-for="data in uninstalledList"
|
||||||
:key="data.id"
|
:key="data.id"
|
||||||
:plugin="data"
|
:plugin="data"
|
||||||
@install="pluginInstalled"
|
@install="pluginInstalled"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<NoDataFound
|
<NoDataFound
|
||||||
v-if="getUninstalledPluginList.length === 0 && isRefreshed"
|
v-if="uninstalledList.length === 0 && isAppMarketLoaded"
|
||||||
error-code="404"
|
error-code="404"
|
||||||
error-title="没有未安装插件"
|
error-title="没有未安装插件"
|
||||||
error-description="所有可用插件均已安装。"
|
error-description="所有可用插件均已安装。"
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ const dropdownItems = ref([
|
|||||||
<VIcon :icon="getIcon(item.value.type || '')" />
|
<VIcon :icon="getIcon(item.value.type || '')" />
|
||||||
</VAvatar>
|
</VAvatar>
|
||||||
<div class="d-flex flex-column ms-1">
|
<div class="d-flex flex-column ms-1">
|
||||||
<span class="d-block whitespace-nowrap text-high-emphasis">
|
<span class="d-block text-high-emphasis">
|
||||||
{{ item.value.title }} {{ item.value.seasons }}{{ item.value.episodes }}
|
{{ item.value.title }} {{ item.value.seasons }}{{ item.value.episodes }}
|
||||||
</span>
|
</span>
|
||||||
<small>{{ item.value.category }}</small>
|
<small>{{ item.value.category }}</small>
|
||||||
@@ -415,8 +415,16 @@ const dropdownItems = ref([
|
|||||||
</VCard>
|
</VCard>
|
||||||
<!-- 底部操作按钮 -->
|
<!-- 底部操作按钮 -->
|
||||||
<span v-if="selected.length > 0" class="fixed right-5 bottom-5">
|
<span v-if="selected.length > 0" class="fixed right-5 bottom-5">
|
||||||
<VBtn icon="mdi-redo-variant" class="me-2" color="primary" size="x-large" @click="retransferBatch" />
|
<VTooltip text="批量重新整理">
|
||||||
<VBtn icon="mdi-trash-can-outline" color="error" size="x-large" @click="removeHistoryBatch" />
|
<template #activator="{ props }">
|
||||||
|
<VBtn v-bind="props" icon="mdi-redo-variant" class="me-2" color="primary" size="x-large" @click="retransferBatch" />
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
|
<VTooltip text="批量删除">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<VBtn v-bind="props" icon="mdi-trash-can-outline" color="error" size="x-large" @click="removeHistoryBatch" />
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
</span>
|
</span>
|
||||||
<!-- 底部弹窗 -->
|
<!-- 底部弹窗 -->
|
||||||
<VBottomSheet v-model="deleteConfirmDialog" inset>
|
<VBottomSheet v-model="deleteConfirmDialog" inset>
|
||||||
|
|||||||
@@ -5,6 +5,52 @@ import type { NotificationSwitch } from '@/api/types'
|
|||||||
|
|
||||||
const messagemTypes = ref<NotificationSwitch[]>([])
|
const messagemTypes = ref<NotificationSwitch[]>([])
|
||||||
|
|
||||||
|
// 选中的消息渠道
|
||||||
|
const selectedChannels = ref([])
|
||||||
|
|
||||||
|
// 消息渠道标签页
|
||||||
|
const messagerTab = ref('wechat')
|
||||||
|
|
||||||
|
// 消息设置
|
||||||
|
const notificationSettings = ref({
|
||||||
|
WECHAT_CORPID: '',
|
||||||
|
WECHAT_APP_SECRET: '',
|
||||||
|
WECHAT_APP_ID: '',
|
||||||
|
WECHAT_PROXY: '',
|
||||||
|
WECHAT_TOKEN: '',
|
||||||
|
WECHAT_ENCODING_AESKEY: '',
|
||||||
|
WECHAT_ADMINS: '',
|
||||||
|
TELEGRAM_TOKEN: '',
|
||||||
|
TELEGRAM_CHAT_ID: '',
|
||||||
|
TELEGRAM_USERS: '',
|
||||||
|
TELEGRAM_ADMINS: '',
|
||||||
|
SLACK_OAUTH_TOKEN: '',
|
||||||
|
SLACK_APP_TOKEN: '',
|
||||||
|
SLACK_CHANNEL: '',
|
||||||
|
SYNOLOGYCHAT_WEBHOOK: '',
|
||||||
|
SYNOLOGYCHAT_TOKEN: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 消息渠道
|
||||||
|
const NotificationChannels = [
|
||||||
|
{
|
||||||
|
title: '微信',
|
||||||
|
value: 'wechat',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Telegram',
|
||||||
|
value: 'telegram',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Slack',
|
||||||
|
value: 'slack',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'SynologyChat',
|
||||||
|
value: 'synologychat',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
@@ -32,87 +78,365 @@ async function saveNotificationSwitchs() {
|
|||||||
$toast.success('保存通知消息设置成功')
|
$toast.success('保存通知消息设置成功')
|
||||||
else
|
else
|
||||||
$toast.error('保存通知消息设置失败!')
|
$toast.error('保存通知消息设置失败!')
|
||||||
|
|
||||||
// messagemTypes.value = messagemTypes.value
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 调用API查询消息渠道设置
|
||||||
|
async function loadNotificationSettings() {
|
||||||
|
try {
|
||||||
|
const result1: { [key: string]: any } = await api.get('system/setting/MESSAGER')
|
||||||
|
if (result1.success)
|
||||||
|
selectedChannels.value = result1.data?.value?.split(',')
|
||||||
|
|
||||||
|
const result2: { [key: string]: any } = await api.get('system/env')
|
||||||
|
if (result2.success) {
|
||||||
|
const {
|
||||||
|
WECHAT_CORPID,
|
||||||
|
WECHAT_APP_SECRET,
|
||||||
|
WECHAT_APP_ID,
|
||||||
|
WECHAT_PROXY,
|
||||||
|
WECHAT_TOKEN,
|
||||||
|
WECHAT_ENCODING_AESKEY,
|
||||||
|
WECHAT_ADMINS,
|
||||||
|
TELEGRAM_TOKEN,
|
||||||
|
TELEGRAM_CHAT_ID,
|
||||||
|
TELEGRAM_USERS,
|
||||||
|
TELEGRAM_ADMINS,
|
||||||
|
SLACK_OAUTH_TOKEN,
|
||||||
|
SLACK_APP_TOKEN,
|
||||||
|
SLACK_CHANNEL,
|
||||||
|
SYNOLOGYCHAT_WEBHOOK,
|
||||||
|
SYNOLOGYCHAT_TOKEN,
|
||||||
|
} = result2.data
|
||||||
|
notificationSettings.value = {
|
||||||
|
WECHAT_CORPID,
|
||||||
|
WECHAT_APP_SECRET,
|
||||||
|
WECHAT_APP_ID,
|
||||||
|
WECHAT_PROXY,
|
||||||
|
WECHAT_TOKEN,
|
||||||
|
WECHAT_ENCODING_AESKEY,
|
||||||
|
WECHAT_ADMINS,
|
||||||
|
TELEGRAM_TOKEN,
|
||||||
|
TELEGRAM_CHAT_ID,
|
||||||
|
TELEGRAM_USERS,
|
||||||
|
TELEGRAM_ADMINS,
|
||||||
|
SLACK_OAUTH_TOKEN,
|
||||||
|
SLACK_APP_TOKEN,
|
||||||
|
SLACK_CHANNEL,
|
||||||
|
SYNOLOGYCHAT_WEBHOOK,
|
||||||
|
SYNOLOGYCHAT_TOKEN,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API保存消息渠道设置
|
||||||
|
async function saveNotificationSettings() {
|
||||||
|
try {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result1.success && result2.success) {
|
||||||
|
$toast.success('保存通知渠道设置成功')
|
||||||
|
reloadModule()
|
||||||
|
}
|
||||||
|
else { $toast.error('保存通知渠道设置失败!') }
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API接口重新加载模块
|
||||||
|
async function reloadModule() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get('system/reload')
|
||||||
|
if (result.success)
|
||||||
|
$toast.success('重新加载模块成功')
|
||||||
|
else
|
||||||
|
$toast.error('重新加载模块失败!')
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadNotificationSwitchs()
|
loadNotificationSwitchs()
|
||||||
|
loadNotificationSettings()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VCard title="消息通知">
|
<VRow>
|
||||||
<VCardText> 对应消息类型只会发送给选中的消息渠道。 </VCardText>
|
<VCol cols="12">
|
||||||
|
<VCard title="通知渠道">
|
||||||
|
<VCardSubtitle>只有选中的渠道才会发送消息。</VCardSubtitle>
|
||||||
|
<VCardText>
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSelect
|
||||||
|
v-model="selectedChannels"
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
:items="NotificationChannels"
|
||||||
|
label="当前使用通知渠道"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
</VTabs>
|
||||||
|
<VWindow
|
||||||
|
v-model="messagerTab"
|
||||||
|
class="mt-5 disable-tab-transition"
|
||||||
|
:touch="false"
|
||||||
|
>
|
||||||
|
<VWindowItem value="wechat">
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.WECHAT_CORPID"
|
||||||
|
label="企业ID"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.WECHAT_APP_SECRET"
|
||||||
|
label="应用密钥"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.WECHAT_APP_ID"
|
||||||
|
label="应用ID"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.WECHAT_PROXY"
|
||||||
|
label="代理地址"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.WECHAT_TOKEN"
|
||||||
|
label="Token"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.WECHAT_ENCODING_AESKEY"
|
||||||
|
label="EncodingAESKey"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.WECHAT_ADMINS"
|
||||||
|
label="管理员白名单"
|
||||||
|
placeholder="多个用,分隔"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VWindowItem>
|
||||||
|
<VWindowItem value="telegram">
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.TELEGRAM_TOKEN"
|
||||||
|
label="Bot Token"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.TELEGRAM_CHAT_ID"
|
||||||
|
label="Chat ID"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.TELEGRAM_USERS"
|
||||||
|
label="用户白名单"
|
||||||
|
placeholder="多个用,分隔"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.TELEGRAM_ADMINS"
|
||||||
|
label="管理员白名单"
|
||||||
|
placeholder="多个用,分隔"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VWindowItem>
|
||||||
|
<VWindowItem value="slack">
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="5">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.SLACK_OAUTH_TOKEN"
|
||||||
|
label="Slack Bot User OAuth Token"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="5">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.SLACK_APP_TOKEN"
|
||||||
|
label="Slack App-Level Token"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="2">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.SLACK_CHANNEL"
|
||||||
|
label="频道名称"
|
||||||
|
placeholder="全体"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VWindowItem>
|
||||||
|
<VWindowItem value="synologychat">
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.SYNOLOGYCHAT_WEBHOOK"
|
||||||
|
label="Webhook"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="notificationSettings.SYNOLOGYCHAT_TOKEN"
|
||||||
|
label="Token"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VWindowItem>
|
||||||
|
</VWindow>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
<VCardText>
|
||||||
|
<VForm @submit.prevent="() => {}">
|
||||||
|
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||||
|
<VBtn
|
||||||
|
mtype="submit"
|
||||||
|
@click="saveNotificationSettings"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VCard title="消息类型">
|
||||||
|
<VCardSubtitle> 对应消息类型只会发送给选中的消息渠道。 </VCardSubtitle>
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="message in messagemTypes"
|
||||||
|
:key="message.mtype"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
{{ message.mtype }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<VCheckbox v-model="message.wechat" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<VCheckbox v-model="message.telegram" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<VCheckbox v-model="message.slack" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<VCheckbox v-model="message.synologychat" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="messagemTypes.length === 0">
|
||||||
|
<td
|
||||||
|
colspan="5"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
没有设置任何通知渠道
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</VTable>
|
||||||
|
<VDivider />
|
||||||
|
|
||||||
<VTable class="text-no-wrap">
|
<VCardText>
|
||||||
<thead>
|
<VForm @submit.prevent="() => {}">
|
||||||
<tr>
|
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||||
<th scope="col">
|
<VBtn
|
||||||
消息类型
|
mtype="submit"
|
||||||
</th>
|
@click="saveNotificationSwitchs"
|
||||||
<th scope="col">
|
>
|
||||||
微信
|
保存
|
||||||
</th>
|
</VBtn>
|
||||||
<th scope="col">
|
</div>
|
||||||
Telegram
|
</VForm>
|
||||||
</th>
|
</VCardText>
|
||||||
<th scope="col">
|
</VCard>
|
||||||
Slack
|
</VCol>
|
||||||
</th>
|
</VRow>
|
||||||
<th scope="col">
|
|
||||||
SynologyChat
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
v-for="message in messagemTypes"
|
|
||||||
:key="message.mtype"
|
|
||||||
>
|
|
||||||
<td>
|
|
||||||
{{ message.mtype }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<VCheckbox v-model="message.wechat" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<VCheckbox v-model="message.telegram" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<VCheckbox v-model="message.slack" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<VCheckbox v-model="message.synologychat" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="messagemTypes.length === 0">
|
|
||||||
<td
|
|
||||||
colspan="4"
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
</VCard>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -78,11 +78,14 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VCard title="定时作业">
|
<VCard title="定时作业">
|
||||||
<VCardText> 手动执行不会影响作业正常的时间表。 </VCardText>
|
<VCardSubtitle> 包含系统内置服务以及插件提供的服务,手动执行不会影响作业正常的时间表。 </VCardSubtitle>
|
||||||
|
|
||||||
<VTable class="text-no-wrap">
|
<VTable class="text-no-wrap">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th scope="col">
|
||||||
|
提供者
|
||||||
|
</th>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
任务名称
|
任务名称
|
||||||
</th>
|
</th>
|
||||||
@@ -100,6 +103,9 @@ onUnmounted(() => {
|
|||||||
v-for="scheduler in schedulerList"
|
v-for="scheduler in schedulerList"
|
||||||
:key="scheduler.id"
|
:key="scheduler.id"
|
||||||
>
|
>
|
||||||
|
<td>
|
||||||
|
{{ scheduler.provider }}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ scheduler.name }}
|
{{ scheduler.name }}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -17,12 +17,32 @@ const resetSitesDisabled = ref(false)
|
|||||||
// 种子优先规则
|
// 种子优先规则
|
||||||
const selectedTorrentPriority = ref<string>('seeder')
|
const selectedTorrentPriority = ref<string>('seeder')
|
||||||
|
|
||||||
|
// CookieCloud设置项
|
||||||
|
const cookieCloudSetting = ref({
|
||||||
|
COOKIECLOUD_HOST: '',
|
||||||
|
COOKIECLOUD_KEY: '',
|
||||||
|
COOKIECLOUD_PASSWORD: '',
|
||||||
|
COOKIECLOUD_INTERVAL: 0,
|
||||||
|
USER_AGENT: '',
|
||||||
|
})
|
||||||
|
|
||||||
// 种子优先规则下拉框
|
// 种子优先规则下拉框
|
||||||
const TorrentPriorityItems = [
|
const TorrentPriorityItems = [
|
||||||
{ title: '站点优先', value: 'site' },
|
{ title: '站点优先', value: 'site' },
|
||||||
{ title: '做种数优先', value: 'seeder' },
|
{ title: '做种数优先', value: 'seeder' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 同步间隔下拉框
|
||||||
|
const CookieCloudIntervalItems = [
|
||||||
|
{ title: '每小时', value: 60 },
|
||||||
|
{ title: '每6小时', value: 360 },
|
||||||
|
{ title: '每12小时', value: 720 },
|
||||||
|
{ title: '每天', value: 1440 },
|
||||||
|
{ title: '每周', value: 10080 },
|
||||||
|
{ title: '每月', value: 43200 },
|
||||||
|
{ title: '永不', value: 0 },
|
||||||
|
]
|
||||||
|
|
||||||
// 重置站点
|
// 重置站点
|
||||||
async function resetSites() {
|
async function resetSites() {
|
||||||
try {
|
try {
|
||||||
@@ -77,13 +97,111 @@ async function saveTorrentPriority() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载CookieCloud设置
|
||||||
|
async function loadCookieCloudSettings() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get('system/env')
|
||||||
|
if (result.success) {
|
||||||
|
const {
|
||||||
|
COOKIECLOUD_HOST,
|
||||||
|
COOKIECLOUD_KEY,
|
||||||
|
COOKIECLOUD_PASSWORD,
|
||||||
|
COOKIECLOUD_INTERVAL,
|
||||||
|
USER_AGENT,
|
||||||
|
} = result.data
|
||||||
|
cookieCloudSetting.value = {
|
||||||
|
COOKIECLOUD_HOST,
|
||||||
|
COOKIECLOUD_KEY,
|
||||||
|
COOKIECLOUD_PASSWORD,
|
||||||
|
COOKIECLOUD_INTERVAL,
|
||||||
|
USER_AGENT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API保存CookieCloud设置
|
||||||
|
async function saveCookieCloudetting() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.post(
|
||||||
|
'system/env',
|
||||||
|
cookieCloudSetting.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.success)
|
||||||
|
$toast.success('保存站点同步设置成功')
|
||||||
|
else
|
||||||
|
$toast.error('保存站点同步设置失败!')
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
queryTorrentPriority()
|
queryTorrentPriority()
|
||||||
|
loadCookieCloudSettings()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VRow>
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VCard title="站点同步">
|
||||||
|
<VCardSubtitle> 从CookieCloud快速同步站点数据。 </VCardSubtitle>
|
||||||
|
<VCardText>
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="cookieCloudSetting.COOKIECLOUD_HOST"
|
||||||
|
label="CookieCloud服务器地址"
|
||||||
|
placeholder="https://movie-pilot.org/cookiecloud"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="cookieCloudSetting.COOKIECLOUD_KEY"
|
||||||
|
label="用户KEY"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="cookieCloudSetting.COOKIECLOUD_PASSWORD"
|
||||||
|
type="password"
|
||||||
|
label="端对端加密密码"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSelect
|
||||||
|
v-model="cookieCloudSetting.COOKIECLOUD_INTERVAL"
|
||||||
|
label="自动同步间隔"
|
||||||
|
:items="CookieCloudIntervalItems"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField
|
||||||
|
v-model="cookieCloudSetting.USER_AGENT"
|
||||||
|
label="浏览器User-Agent"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
<VCardItem>
|
||||||
|
<VBtn
|
||||||
|
type="submit"
|
||||||
|
@click="saveCookieCloudetting"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</VBtn>
|
||||||
|
</VCardItem>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
<VCard title="下载优先规则">
|
<VCard title="下载优先规则">
|
||||||
<VCardSubtitle> 按站点或做种数量优先下载。 </VCardSubtitle>
|
<VCardSubtitle> 按站点或做种数量优先下载。 </VCardSubtitle>
|
||||||
@@ -94,8 +212,7 @@ onMounted(() => {
|
|||||||
<VSelect
|
<VSelect
|
||||||
v-model="selectedTorrentPriority"
|
v-model="selectedTorrentPriority"
|
||||||
:items="TorrentPriorityItems"
|
:items="TorrentPriorityItems"
|
||||||
label="优先规则"
|
label="当前使用下载优先规则"
|
||||||
outlined
|
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script lang='ts' setup>
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
||||||
@@ -32,12 +32,41 @@ const selectedRssSites = ref<number[]>([])
|
|||||||
// 当前规则类型
|
// 当前规则类型
|
||||||
const currentRuleType = ref('SubscribeFilterRules')
|
const currentRuleType = ref('SubscribeFilterRules')
|
||||||
|
|
||||||
|
// 是否开启订阅定时搜索
|
||||||
|
const enableIntervalSearch = ref(false)
|
||||||
|
|
||||||
// 包含与排除规则
|
// 包含与排除规则
|
||||||
const defaultFilterRules = ref({
|
const defaultFilterRules = ref({
|
||||||
include: '',
|
include: '',
|
||||||
exclude: '',
|
exclude: '',
|
||||||
|
movie_size: '',
|
||||||
|
tv_size: '',
|
||||||
|
show_edit_dialog: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 订阅模式选择项
|
||||||
|
const subscribeModeItems = [
|
||||||
|
{ title: '自动', value: 'spider' },
|
||||||
|
{ title: '站点RSS', value: 'rss' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 选择的订阅模式
|
||||||
|
const selectedSubscribeMode = ref('spider')
|
||||||
|
|
||||||
|
// RSS运行周期选择项
|
||||||
|
const rssIntervalItems = [
|
||||||
|
{ title: '5分钟', value: 5 },
|
||||||
|
{ title: '10分钟', value: 10 },
|
||||||
|
{ title: '20分钟', value: 20 },
|
||||||
|
{ title: '半小时', value: 30 },
|
||||||
|
{ title: '1小时', value: 60 },
|
||||||
|
{ title: '12小时', value: 720 },
|
||||||
|
{ title: '1天', value: 1440 },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 选择的RSS运行周期
|
||||||
|
const selectedRssInterval = ref<number>(5)
|
||||||
|
|
||||||
// 导入代码弹窗
|
// 导入代码弹窗
|
||||||
const importCodeDialog = ref(false)
|
const importCodeDialog = ref(false)
|
||||||
|
|
||||||
@@ -59,9 +88,26 @@ async function querySelectedRssSites() {
|
|||||||
// 保存用户选中的订阅站点
|
// 保存用户选中的订阅站点
|
||||||
async function saveSelectedRssSites() {
|
async function saveSelectedRssSites() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.post('system/setting/RssSites', selectedRssSites.value)
|
const result1: { [key: string]: any } = await api.post(
|
||||||
|
'system/setting/RssSites',
|
||||||
|
selectedRssSites.value)
|
||||||
|
|
||||||
if (result.success)
|
const result2: { [key: string]: any } = await api.post(
|
||||||
|
'system/setting/SUBSCRIBE_SEARCH',
|
||||||
|
enableIntervalSearch.value ? 'True' : 'False',
|
||||||
|
)
|
||||||
|
|
||||||
|
const result3: { [key: string]: any } = await api.post(
|
||||||
|
'system/setting/SUBSCRIBE_MODE',
|
||||||
|
selectedSubscribeMode.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
const result4: { [key: string]: any } = await api.post(
|
||||||
|
'system/setting/SUBSCRIBE_RSS_INTERVAL',
|
||||||
|
selectedRssInterval.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result1.success && result2.success && result3.success && result4.success)
|
||||||
$toast.success('订阅站点保存成功')
|
$toast.success('订阅站点保存成功')
|
||||||
else
|
else
|
||||||
$toast.error('订阅站点保存失败!')
|
$toast.error('订阅站点保存失败!')
|
||||||
@@ -79,6 +125,19 @@ async function querySites() {
|
|||||||
// 过滤站点,只有启用的站点才显示
|
// 过滤站点,只有启用的站点才显示
|
||||||
allSites.value = data.filter(item => item.is_active)
|
allSites.value = data.filter(item => item.is_active)
|
||||||
querySelectedRssSites()
|
querySelectedRssSites()
|
||||||
|
|
||||||
|
// 查询订阅搜索开关
|
||||||
|
const result: { [key: string]: any } = await api.get('system/setting/SUBSCRIBE_SEARCH')
|
||||||
|
if (result.success)
|
||||||
|
enableIntervalSearch.value = result.data?.value
|
||||||
|
// 查询订阅模式
|
||||||
|
const result2: { [key: string]: any } = await api.get('system/setting/SUBSCRIBE_MODE')
|
||||||
|
if (result2.success)
|
||||||
|
selectedSubscribeMode.value = result2.data?.value
|
||||||
|
// 查询站点RSS周期
|
||||||
|
const result3: { [key: string]: any } = await api.get('system/setting/SUBSCRIBE_RSS_INTERVAL')
|
||||||
|
if (result3.success)
|
||||||
|
selectedRssInterval.value = result3.data?.value
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
@@ -343,7 +402,34 @@ onMounted(() => {
|
|||||||
</VChip>
|
</VChip>
|
||||||
</VChipGroup>
|
</VChipGroup>
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
|
<VCardText>
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSelect
|
||||||
|
v-model="selectedSubscribeMode"
|
||||||
|
:items="subscribeModeItems"
|
||||||
|
label="订阅模式"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSelect
|
||||||
|
v-model="selectedRssInterval"
|
||||||
|
:items="rssIntervalItems"
|
||||||
|
label="站点RSS周期"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSwitch
|
||||||
|
v-model="enableIntervalSearch"
|
||||||
|
label="开启订阅定时搜索"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
<VCardItem>
|
<VCardItem>
|
||||||
<VBtn type="submit" @click="saveSelectedRssSites">
|
<VBtn type="submit" @click="saveSelectedRssSites">
|
||||||
保存
|
保存
|
||||||
@@ -383,7 +469,7 @@ onMounted(() => {
|
|||||||
</VMenu>
|
</VMenu>
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
<VCardSubtitle> 设置在正常订阅时默认使用的优先级,未在优先级中的资源将不会自动下载。 </VCardSubtitle>
|
<VCardSubtitle> 设置在正常订阅时默认使用的优先级,未在优先级中的资源将不会自动下载。</VCardSubtitle>
|
||||||
<VCardItem>
|
<VCardItem>
|
||||||
<div class="grid gap-3 grid-filterrule-card">
|
<div class="grid gap-3 grid-filterrule-card">
|
||||||
<FilterRuleCard
|
<FilterRuleCard
|
||||||
@@ -449,7 +535,7 @@ onMounted(() => {
|
|||||||
</VMenu>
|
</VMenu>
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
<VCardSubtitle> 设置在订阅洗版时使用的优先级,匹配优先级1时洗版完成。 </VCardSubtitle>
|
<VCardSubtitle> 设置在订阅洗版时使用的优先级,匹配优先级1时洗版完成。</VCardSubtitle>
|
||||||
<VCardItem>
|
<VCardItem>
|
||||||
<div class="grid gap-3 grid-filterrule-card">
|
<div class="grid gap-3 grid-filterrule-card">
|
||||||
<FilterRuleCard
|
<FilterRuleCard
|
||||||
@@ -485,7 +571,7 @@ onMounted(() => {
|
|||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
<VCard title="默认过滤规则">
|
<VCard title="默认过滤规则">
|
||||||
<VCardSubtitle> 设置在订阅时默认使用的过滤规则。 </VCardSubtitle>
|
<VCardSubtitle> 设置在订阅时默认使用的过滤规则。</VCardSubtitle>
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VForm>
|
<VForm>
|
||||||
<VRow>
|
<VRow>
|
||||||
@@ -503,6 +589,28 @@ onMounted(() => {
|
|||||||
label="排除(关键字、正则式)"
|
label="排除(关键字、正则式)"
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="defaultFilterRules.movie_size"
|
||||||
|
type="text"
|
||||||
|
label="电影文件大小(GB)"
|
||||||
|
placeholder="0-30"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="defaultFilterRules.tv_size"
|
||||||
|
type="text"
|
||||||
|
label="剧集单集文件大小(GB)"
|
||||||
|
placeholder="0-10"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSwitch
|
||||||
|
v-model="defaultFilterRules.show_edit_dialog"
|
||||||
|
label="订阅时编辑更多规则"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</VForm>
|
</VForm>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
@@ -530,7 +638,7 @@ onMounted(() => {
|
|||||||
</VDialog>
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang='scss'>
|
||||||
.grid-filterrule-card {
|
.grid-filterrule-card {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||||
padding-block-end: 1rem;
|
padding-block-end: 1rem;
|
||||||
|
|||||||
736
src/views/setting/AccountSettingSystem.vue
Normal file
736
src/views/setting/AccountSettingSystem.vue
Normal file
@@ -0,0 +1,736 @@
|
|||||||
|
<!-- eslint-disable sonarjs/no-duplicate-string -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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([])
|
||||||
|
|
||||||
|
// 下载器选中标签页
|
||||||
|
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: '',
|
||||||
|
DOWNLOADER_MONITOR: true,
|
||||||
|
TORRENT_TAG: '',
|
||||||
|
QB_HOST: '',
|
||||||
|
QB_USER: '',
|
||||||
|
QB_PASSWORD: '',
|
||||||
|
QB_CATEGORY: false,
|
||||||
|
QB_SEQUENTIAL: false,
|
||||||
|
QB_FORCE_RESUME: false,
|
||||||
|
TR_HOST: '',
|
||||||
|
TR_USER: '',
|
||||||
|
TR_PASSWORD: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 媒体服务器设置项
|
||||||
|
const mediaServerSettings = ref({
|
||||||
|
MEDIASERVER_SYNC_INTERVAL: 6,
|
||||||
|
MEDIASERVER_SYNC_BLACKLIST: '',
|
||||||
|
EMBY_HOST: '',
|
||||||
|
EMBY_PLAY_HOST: '',
|
||||||
|
EMBY_API_KEY: '',
|
||||||
|
JELLYFIN_HOST: '',
|
||||||
|
JELLYFIN_PLAY_HOST: '',
|
||||||
|
JELLYFIN_API_KEY: '',
|
||||||
|
PLEX_HOST: '',
|
||||||
|
PLEX_PLAY_HOST: '',
|
||||||
|
PLEX_TOKEN: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 下载器字典项
|
||||||
|
const Downloaders = [
|
||||||
|
{
|
||||||
|
title: 'Qbittorrent',
|
||||||
|
value: 'qbittorrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Transmission',
|
||||||
|
value: 'transmission',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 媒体服务器字典项
|
||||||
|
const MediaServers = [
|
||||||
|
{
|
||||||
|
title: 'Emby',
|
||||||
|
value: 'emby',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Jellyfin',
|
||||||
|
value: 'jellyfin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Plex',
|
||||||
|
value: 'plex',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 转移方式字典
|
||||||
|
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 },
|
||||||
|
{ title: '每小时', value: 1 },
|
||||||
|
{ title: '每6小时', value: 6 },
|
||||||
|
{ title: '每12小时', value: 12 },
|
||||||
|
{ title: '每天', value: 24 },
|
||||||
|
{ title: '每周', value: 168 },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 提示框
|
||||||
|
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 loadDownladerSetting() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get('system/env')
|
||||||
|
if (result.success) {
|
||||||
|
const {
|
||||||
|
DOWNLOADER,
|
||||||
|
DOWNLOADER_MONITOR,
|
||||||
|
TORRENT_TAG,
|
||||||
|
QB_HOST,
|
||||||
|
QB_USER,
|
||||||
|
QB_PASSWORD,
|
||||||
|
QB_CATEGORY,
|
||||||
|
QB_SEQUENTIAL,
|
||||||
|
QB_FORCE_RESUME,
|
||||||
|
TR_HOST,
|
||||||
|
TR_USER,
|
||||||
|
TR_PASSWORD,
|
||||||
|
} = result.data
|
||||||
|
downloaderSettings.value = {
|
||||||
|
DOWNLOADER,
|
||||||
|
DOWNLOADER_MONITOR,
|
||||||
|
TORRENT_TAG,
|
||||||
|
QB_HOST,
|
||||||
|
QB_USER,
|
||||||
|
QB_PASSWORD,
|
||||||
|
QB_CATEGORY,
|
||||||
|
QB_SEQUENTIAL,
|
||||||
|
QB_FORCE_RESUME,
|
||||||
|
TR_HOST,
|
||||||
|
TR_USER,
|
||||||
|
TR_PASSWORD,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API保存下载器设置
|
||||||
|
async function saveDownloaderSetting() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.post(
|
||||||
|
'system/env',
|
||||||
|
downloaderSettings.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success('保存下载器设置成功')
|
||||||
|
reloadModule()
|
||||||
|
}
|
||||||
|
else { $toast.error('保存下载器设置失败!') }
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API查询媒体服务器设置
|
||||||
|
async function loadMediaServerSetting() {
|
||||||
|
try {
|
||||||
|
const result1: { [key: string]: any } = await api.get('system/setting/MEDIASERVER')
|
||||||
|
if (result1.success)
|
||||||
|
selectedMediaServers.value = result1.data?.value?.split(',')
|
||||||
|
|
||||||
|
const result2: { [key: string]: any } = await api.get('system/env')
|
||||||
|
if (result2.success) {
|
||||||
|
const {
|
||||||
|
MEDIASERVER_SYNC_INTERVAL,
|
||||||
|
MEDIASERVER_SYNC_BLACKLIST,
|
||||||
|
EMBY_HOST,
|
||||||
|
EMBY_PLAY_HOST,
|
||||||
|
EMBY_API_KEY,
|
||||||
|
JELLYFIN_HOST,
|
||||||
|
JELLYFIN_PLAY_HOST,
|
||||||
|
JELLYFIN_API_KEY,
|
||||||
|
PLEX_HOST,
|
||||||
|
PLEX_PLAY_HOST,
|
||||||
|
PLEX_TOKEN,
|
||||||
|
} = result2.data
|
||||||
|
mediaServerSettings.value = {
|
||||||
|
MEDIASERVER_SYNC_INTERVAL,
|
||||||
|
MEDIASERVER_SYNC_BLACKLIST,
|
||||||
|
EMBY_HOST,
|
||||||
|
EMBY_PLAY_HOST,
|
||||||
|
EMBY_API_KEY,
|
||||||
|
JELLYFIN_HOST,
|
||||||
|
JELLYFIN_PLAY_HOST,
|
||||||
|
JELLYFIN_API_KEY,
|
||||||
|
PLEX_HOST,
|
||||||
|
PLEX_PLAY_HOST,
|
||||||
|
PLEX_TOKEN,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API保存媒体服务器设置
|
||||||
|
async function saveMediaServerSetting() {
|
||||||
|
try {
|
||||||
|
const result1: { [key: string]: any } = await api.post(
|
||||||
|
'system/setting/MEDIASERVER',
|
||||||
|
selectedMediaServers.value.join(','),
|
||||||
|
)
|
||||||
|
|
||||||
|
const result2: { [key: string]: any } = await api.post(
|
||||||
|
'system/env',
|
||||||
|
mediaServerSettings.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result1.success && result2.success) {
|
||||||
|
$toast.success('保存媒体服务器设置成功')
|
||||||
|
reloadModule()
|
||||||
|
}
|
||||||
|
else { $toast.error('保存媒体服务器设置失败!') }
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API接口重新加载模块
|
||||||
|
async function reloadModule() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get('system/reload')
|
||||||
|
if (result.success)
|
||||||
|
$toast.success('重新加载模块成功')
|
||||||
|
else
|
||||||
|
$toast.error('重新加载模块失败!')
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadDownladerSetting()
|
||||||
|
loadMediaServerSetting()
|
||||||
|
loadMediaSettings()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VCard title="下载器">
|
||||||
|
<VCardSubtitle>只有选中的下载器才会被默认使用。</VCardSubtitle>
|
||||||
|
<VCardText>
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSelect
|
||||||
|
v-model="downloaderSettings.DOWNLOADER"
|
||||||
|
:items="Downloaders"
|
||||||
|
label="当前使用下载器"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="downloaderSettings.TORRENT_TAG"
|
||||||
|
label="下载器种子标签"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSwitch
|
||||||
|
v-model="downloaderSettings.DOWNLOADER_MONITOR"
|
||||||
|
label="监控下载器"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<VWindowItem value="qbittorrent">
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="downloaderSettings.QB_HOST"
|
||||||
|
label="地址"
|
||||||
|
placeholder="IP:PORT"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="downloaderSettings.QB_USER"
|
||||||
|
label="用户名"
|
||||||
|
placeholder="admin"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="downloaderSettings.QB_PASSWORD"
|
||||||
|
type="password"
|
||||||
|
label="密码"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VSwitch
|
||||||
|
v-model="downloaderSettings.QB_CATEGORY"
|
||||||
|
label="自动分类管理"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VSwitch
|
||||||
|
v-model="downloaderSettings.QB_SEQUENTIAL"
|
||||||
|
label="顺序下载"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VSwitch
|
||||||
|
v-model="downloaderSettings.QB_FORCE_RESUME"
|
||||||
|
label="强制继续"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VWindowItem>
|
||||||
|
<VWindowItem value="transmission">
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="downloaderSettings.TR_HOST"
|
||||||
|
label="地址"
|
||||||
|
placeholder="IP:PORT"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="downloaderSettings.TR_USER"
|
||||||
|
label="用户名"
|
||||||
|
placeholder="admin"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="downloaderSettings.TR_PASSWORD"
|
||||||
|
type="password"
|
||||||
|
label="密码"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VWindowItem>
|
||||||
|
</VWindow>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
<VCardText>
|
||||||
|
<VForm @submit.prevent="() => {}">
|
||||||
|
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||||
|
<VBtn
|
||||||
|
mtype="submit"
|
||||||
|
@click="saveDownloaderSetting"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VCard title="媒体服务器">
|
||||||
|
<VCardSubtitle>只有选中的媒体服务器才会被默认使用。</VCardSubtitle>
|
||||||
|
<VCardText>
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VSelect
|
||||||
|
v-model="selectedMediaServers"
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
:items="MediaServers"
|
||||||
|
label="当前使用媒体服务器"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VSelect
|
||||||
|
v-model="mediaServerSettings.MEDIASERVER_SYNC_INTERVAL"
|
||||||
|
:items="syncIntervalItems"
|
||||||
|
label="同步周期"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaServerSettings.MEDIASERVER_SYNC_BLACKLIST"
|
||||||
|
label="媒体库同步黑名单"
|
||||||
|
placeholder="使用,分隔"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
<VWindow
|
||||||
|
v-model="mediaserverTab"
|
||||||
|
class="mt-5 disable-tab-transition"
|
||||||
|
:touch="false"
|
||||||
|
>
|
||||||
|
<VWindowItem value="emby">
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaServerSettings.EMBY_HOST"
|
||||||
|
label="地址"
|
||||||
|
placeholder="IP:PORT"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaServerSettings.EMBY_PLAY_HOST"
|
||||||
|
label="外网播放地址"
|
||||||
|
placeholder="http(s)://domain:port"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaServerSettings.EMBY_API_KEY"
|
||||||
|
label="API密钥"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VWindowItem>
|
||||||
|
<VWindowItem value="jellyfin">
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaServerSettings.JELLYFIN_HOST"
|
||||||
|
label="地址"
|
||||||
|
placeholder="IP:PORT"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaServerSettings.JELLYFIN_PLAY_HOST"
|
||||||
|
label="外网播放地址"
|
||||||
|
placeholder="http(s)://domain:port"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaServerSettings.JELLYFIN_API_KEY"
|
||||||
|
label="API密钥"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VWindowItem>
|
||||||
|
<VWindowItem value="plex">
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaServerSettings.PLEX_HOST"
|
||||||
|
label="地址"
|
||||||
|
placeholder="IP:PORT"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaServerSettings.PLEX_PLAY_HOST"
|
||||||
|
label="外网播放地址"
|
||||||
|
placeholder="http(s)://domain:port"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaServerSettings.PLEX_TOKEN"
|
||||||
|
label="API密钥"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VWindowItem>
|
||||||
|
</VWindow>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
<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]"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaSettings.DOWNLOAD_MOVIE_PATH"
|
||||||
|
label="电影下载目录"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaSettings.DOWNLOAD_TV_PATH"
|
||||||
|
label="电视剧下载目录"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaSettings.DOWNLOAD_ANIME_PATH"
|
||||||
|
label="动漫下载目录"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSwitch
|
||||||
|
v-model="mediaSettings.DOWNLOAD_CATEGORY"
|
||||||
|
label="下载目录自动分类"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSelect
|
||||||
|
v-model="mediaSettings.TRANSFER_TYPE"
|
||||||
|
:items="transferTypeItems"
|
||||||
|
label="整理方式"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSelect
|
||||||
|
v-model="mediaSettings.OVERWRITE_MODE"
|
||||||
|
:items="overwriteModeItems"
|
||||||
|
label="覆盖模式"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSwitch
|
||||||
|
v-model="mediaSettings.SCRAP_METADATA"
|
||||||
|
label="自动刮削媒体信息"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaSettings.LIBRARY_PATH"
|
||||||
|
label="媒体库目录"
|
||||||
|
placeholder="多个目录使用,分隔"
|
||||||
|
:rules="[requiredValidator]"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaSettings.LIBRARY_MOVIE_NAME"
|
||||||
|
label="电影目录名称"
|
||||||
|
placeholder="电影"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaSettings.LIBRARY_TV_NAME"
|
||||||
|
label="电视剧目录名称"
|
||||||
|
placeholder="电视剧"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="mediaSettings.LIBRARY_ANIME_NAME"
|
||||||
|
label="动漫目录名称"
|
||||||
|
placeholder="动漫"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSwitch
|
||||||
|
v-model="mediaSettings.LIBRARY_CATEGORY"
|
||||||
|
label="媒体库目录自动分类"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
<VCardText>
|
||||||
|
<VForm @submit.prevent="() => {}">
|
||||||
|
<div class="d-flex flex-wrap gap-4 mt-4">
|
||||||
|
<VBtn
|
||||||
|
mtype="submit"
|
||||||
|
@click="saveMediaSetting"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</template>
|
||||||
@@ -5,9 +5,9 @@ import interactionPlugin from '@fullcalendar/interaction'
|
|||||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||||
import FullCalendar from '@fullcalendar/vue3'
|
import FullCalendar from '@fullcalendar/vue3'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import type { MediaInfo, Rss, Subscribe, TmdbEpisode } from '@/api/types'
|
import type { MediaInfo, Subscribe, TmdbEpisode } from '@/api/types'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { parseDate } from '@/@core/utils/formatters'
|
import { formatEp, parseDate } from '@/@core/utils/formatters'
|
||||||
|
|
||||||
// 日历属性
|
// 日历属性
|
||||||
const calendarOptions: Ref<CalendarOptions> = ref({
|
const calendarOptions: Ref<CalendarOptions> = ref({
|
||||||
@@ -33,7 +33,7 @@ const calendarOptions: Ref<CalendarOptions> = ref({
|
|||||||
events: [],
|
events: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
async function eventsHander(subscribe: Subscribe | Rss) {
|
async function eventsHander(subscribe: Subscribe) {
|
||||||
// 如果是电影直接返回
|
// 如果是电影直接返回
|
||||||
if (subscribe.type === '电影') {
|
if (subscribe.type === '电影') {
|
||||||
// 调用API查询TMDB详情
|
// 调用API查询TMDB详情
|
||||||
@@ -62,7 +62,7 @@ async function eventsHander(subscribe: Subscribe | Rss) {
|
|||||||
subtitle: string
|
subtitle: string
|
||||||
start: Date | null
|
start: Date | null
|
||||||
allDay: boolean
|
allDay: boolean
|
||||||
posterPath: string
|
posterPath: string | undefined
|
||||||
mediaType: string
|
mediaType: string
|
||||||
len: number
|
len: number
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ async function eventsHander(subscribe: Subscribe | Rss) {
|
|||||||
else {
|
else {
|
||||||
dictEpisode[air_date] = {
|
dictEpisode[air_date] = {
|
||||||
title: subscribe.name,
|
title: subscribe.name,
|
||||||
subtitle: `第${episode.episode_number}`,
|
subtitle: `${episode.episode_number}`,
|
||||||
start: parseDate(episode.air_date || ''),
|
start: parseDate(episode.air_date || ''),
|
||||||
allDay: false,
|
allDay: false,
|
||||||
posterPath: subscribe.poster,
|
posterPath: subscribe.poster,
|
||||||
@@ -90,10 +90,8 @@ async function eventsHander(subscribe: Subscribe | Rss) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
for (const key in dictEpisode) {
|
for (const key in dictEpisode)
|
||||||
if (dictEpisode.hasOwnProperty(key))
|
dictEpisode[key].subtitle = formatEp(dictEpisode[key].subtitle.split(',').map(Number))
|
||||||
dictEpisode[key].subtitle += '集'
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.values(dictEpisode)
|
return Object.values(dictEpisode)
|
||||||
}
|
}
|
||||||
@@ -148,11 +146,8 @@ onMounted(() => {
|
|||||||
<VCardSubtitle class="pa-1 px-2 font-bold break-words whitespace-break-spaces">
|
<VCardSubtitle class="pa-1 px-2 font-bold break-words whitespace-break-spaces">
|
||||||
{{ arg.event.title }}
|
{{ arg.event.title }}
|
||||||
</VCardSubtitle>
|
</VCardSubtitle>
|
||||||
<VCardText class="pa-0 px-2">
|
<VCardText v-if="arg.event.extendedProps.subtitle" class="pa-0 px-2 break-words">
|
||||||
{{ arg.event.extendedProps.len }}集
|
第{{ arg.event.extendedProps.subtitle }}集
|
||||||
</VCardText>
|
|
||||||
<VCardText class="pa-0 px-2 break-words">
|
|
||||||
{{ arg.event.extendedProps.subtitle }}
|
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -178,8 +173,9 @@ onMounted(() => {
|
|||||||
<VChip
|
<VChip
|
||||||
v-if="arg.event.extendedProps.len > 1"
|
v-if="arg.event.extendedProps.len > 1"
|
||||||
variant="elevated"
|
variant="elevated"
|
||||||
size="mini"
|
color="primary"
|
||||||
class="absolute right-0.5 top-0.5 bg-opacity-80 shadow-md text-white font-bold border-purple-600 bg-purple-600"
|
size="x-small"
|
||||||
|
class="absolute right-0 top-0"
|
||||||
>
|
>
|
||||||
{{ arg.event.extendedProps.len }}
|
{{ arg.event.extendedProps.len }}
|
||||||
</VChip>
|
</VChip>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import vuetify from 'vite-plugin-vuetify'
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: './',
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
vueJsx(),
|
vueJsx(),
|
||||||
@@ -27,7 +28,16 @@ export default defineConfig({
|
|||||||
imports: ['vue', 'vue-router', '@vueuse/core', '@vueuse/math', 'vuex'],
|
imports: ['vue', 'vue-router', '@vueuse/core', '@vueuse/math', 'vuex'],
|
||||||
vueTemplate: true,
|
vueTemplate: true,
|
||||||
}),
|
}),
|
||||||
VitePWA({ registerType: 'autoUpdate', injectRegister: 'script', manifest: false }),
|
VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
injectRegister: 'script',
|
||||||
|
manifest: false,
|
||||||
|
workbox: {
|
||||||
|
navigateFallbackDenylist: [
|
||||||
|
/.*\/api\/v\d+\/system\/logging.*/,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
define: { 'process.env': {} },
|
define: { 'process.env': {} },
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
17
yarn.lock
17
yarn.lock
@@ -2422,6 +2422,11 @@ accepts@~1.3.8:
|
|||||||
mime-types "~2.1.34"
|
mime-types "~2.1.34"
|
||||||
negotiator "0.6.3"
|
negotiator "0.6.3"
|
||||||
|
|
||||||
|
ace-builds@^1.32.6:
|
||||||
|
version "1.32.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.32.6.tgz#454ec8bc9235fbb960b8d8b86e698f941c104de2"
|
||||||
|
integrity sha512-dO5BnyDOhCnznhOpILzXq4jqkbhRXxNkf3BuVTmyxGyRLrhddfdyk6xXgy+7A8LENrcYoFi/sIxMuH3qjNUN4w==
|
||||||
|
|
||||||
acorn-jsx@^5.2.0, acorn-jsx@^5.3.2:
|
acorn-jsx@^5.2.0, acorn-jsx@^5.3.2:
|
||||||
version "5.3.2"
|
version "5.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||||
@@ -6651,6 +6656,11 @@ require-from-string@^2.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
||||||
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
||||||
|
|
||||||
|
resize-observer-polyfill@^1.5.1:
|
||||||
|
version "1.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||||
|
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
|
||||||
|
|
||||||
resolve-from@^4.0.0:
|
resolve-from@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||||
@@ -7912,6 +7922,13 @@ vue-tsc@^1.6.5:
|
|||||||
"@volar/vue-typescript" "1.6.5"
|
"@volar/vue-typescript" "1.6.5"
|
||||||
semver "^7.3.8"
|
semver "^7.3.8"
|
||||||
|
|
||||||
|
vue3-ace-editor@^2.2.4:
|
||||||
|
version "2.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue3-ace-editor/-/vue3-ace-editor-2.2.4.tgz#1f2a787f91cf7979f27fab29e0e0604bb3ee1c17"
|
||||||
|
integrity sha512-FZkEyfpbH068BwjhMyNROxfEI8135Sc+x8ouxkMdCNkuj/Tuw83VP/gStFQqZHqljyX9/VfMTCdTqtOnJZGN8g==
|
||||||
|
dependencies:
|
||||||
|
resize-observer-polyfill "^1.5.1"
|
||||||
|
|
||||||
vue3-apexcharts@^1.4.1:
|
vue3-apexcharts@^1.4.1:
|
||||||
version "1.4.1"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/vue3-apexcharts/-/vue3-apexcharts-1.4.1.tgz#ea561308430a1c5213b7f17c44ba3c845f6c490d"
|
resolved "https://registry.yarnpkg.com/vue3-apexcharts/-/vue3-apexcharts-1.4.1.tgz#ea561308430a1c5213b7f17c44ba3c845f6c490d"
|
||||||
|
|||||||
Reference in New Issue
Block a user