Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b753a8f5b | ||
|
|
11e82582b8 | ||
|
|
419358863e | ||
|
|
1d0d7f9975 | ||
|
|
c5f564372b | ||
|
|
a50f0cd727 | ||
|
|
96f6f55138 | ||
|
|
6a45c8b358 | ||
|
|
165937596e | ||
|
|
fb976f043b | ||
|
|
ecb9c4e51a | ||
|
|
9e8c3b495c | ||
|
|
24a37fc33c | ||
|
|
d09a21114d | ||
|
|
6e2b12501f | ||
|
|
2a56e116cf | ||
|
|
6de4f238d8 | ||
|
|
1b426c5957 | ||
|
|
82454a650c | ||
|
|
227b6bd7ef | ||
|
|
9554025daf | ||
|
|
0eb5d607bf | ||
|
|
750f4bc276 | ||
|
|
d0aada1d3d | ||
|
|
8a4848387c | ||
|
|
6904fc7da3 | ||
|
|
28c55a05e6 | ||
|
|
562c829267 | ||
|
|
b200ed242d | ||
|
|
815cfe55df | ||
|
|
40a1094d74 | ||
|
|
346650c091 | ||
|
|
7f74715f51 | ||
|
|
b6fcee517d | ||
|
|
4f62551f6b | ||
|
|
3980249271 | ||
|
|
e3b11b1130 | ||
|
|
f866f23af1 | ||
|
|
c793bc24f0 | ||
|
|
591a46d559 | ||
|
|
2852f26702 | ||
|
|
fc818fdfd6 | ||
|
|
5566ef87f8 | ||
|
|
366fe34d6f | ||
|
|
37a0e83124 | ||
|
|
061a3f393a | ||
|
|
1dff22aeab | ||
|
|
78e6fd4809 | ||
|
|
96bbf3d0f2 | ||
|
|
a842eaba4e | ||
|
|
37565bf8e4 | ||
|
|
beb158b387 | ||
|
|
408eb06f8d | ||
|
|
abe0e44635 | ||
|
|
cfaf414f1c | ||
|
|
f9c4dc616b | ||
|
|
bf845bab6b | ||
|
|
bae9c85990 | ||
|
|
56bbb8d0ff | ||
|
|
60d3565231 | ||
|
|
81340fd287 | ||
|
|
c10c348c73 | ||
|
|
65cb7d9674 | ||
|
|
24f1a10ff7 | ||
|
|
767d11182a | ||
|
|
cf363f667e | ||
|
|
0d1046b8c7 | ||
|
|
2c05f5779e | ||
|
|
9af200f89e | ||
|
|
7e221cfd46 | ||
|
|
640882d178 | ||
|
|
3a1436abef | ||
|
|
d431f0490d | ||
|
|
4c2a6c92a6 | ||
|
|
086c230e9e | ||
|
|
27e2ff50f2 | ||
|
|
3134e5596b | ||
|
|
315274abf9 | ||
|
|
52bbf65fa8 | ||
|
|
9c018ec63b | ||
|
|
bd7e457cdb | ||
|
|
36a0f8515b | ||
|
|
cac10a337d | ||
|
|
edb53cc58f | ||
|
|
1dceeecdad | ||
|
|
f8071ada0b | ||
|
|
21bc8edbd8 | ||
|
|
2a8aeb5041 | ||
|
|
1a7760cf6d | ||
|
|
aee4eed5ac | ||
|
|
87215fb590 | ||
|
|
5409126187 |
7
.github/workflows/build.yml
vendored
@@ -27,6 +27,13 @@ jobs:
|
||||
node-version: '18'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Download Icons
|
||||
run: |
|
||||
pwd
|
||||
curl -sL "https://github.com/jxxghp/MoviePilot-Plugins/archive/refs/heads/main.zip" | busybox unzip -d /tmp -
|
||||
mv /tmp/MoviePilot-Plugins-main/icons public/plugin_icon
|
||||
rm -rf /tmp/MoviePilot-Plugins-main
|
||||
|
||||
- name: Build frontend
|
||||
id: build_frontend
|
||||
run: |
|
||||
|
||||
1
.gitignore
vendored
@@ -32,3 +32,4 @@ dist-ssr
|
||||
|
||||
# iconify dist files
|
||||
src/@iconify/*.js
|
||||
public/plugin_icon/**
|
||||
|
||||
4
.vscode/settings.json
vendored
@@ -31,8 +31,8 @@
|
||||
"volar.preview.port": 3000,
|
||||
"volar.completion.preferredTagNameCase": "pascal",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.fixAll.stylelint": true
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.stylelint": "explicit"
|
||||
},
|
||||
"eslint.alwaysShowStatus": true,
|
||||
"eslint.format.enable": true,
|
||||
|
||||
32
README.md
@@ -1,35 +1,39 @@
|
||||
# MoviePilot-Frontend
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
[MoviePilot](https://github.com/jxxghp/MoviePilot) 的前端项目。
|
||||
|
||||
## Recommended IDE Setup
|
||||
## 推荐的IDE设置
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (and disable Vetur).
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (并禁用 Vetur).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
## 配置Vite
|
||||
|
||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates.
|
||||
请参阅 [Vite 配置参考](https://vitejs.dev/config/).
|
||||
|
||||
However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can run `Volar: Switch TS Plugin on/off` from VSCode command palette.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
## 依赖安装
|
||||
|
||||
```sh
|
||||
yarn
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
### 开发运行
|
||||
|
||||
```sh
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
### 编译打包
|
||||
|
||||
```sh
|
||||
yarn build
|
||||
```
|
||||
|
||||
### 静态运行
|
||||
|
||||
1. 使用 `nginx` 等Web服务器托管 `dist` 静态文件,nginx配置参考 `public/nginx.conf`。
|
||||
|
||||
2. 使用 `node` 命令直接运行`service.js`,默认监听 `3000` 端口,设置环境变量 `NGINX_PORT` 来调整运行端口。
|
||||
|
||||
```shell
|
||||
node dist/service.js
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "1.3.8",
|
||||
"version": "1.5.7-1",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
@@ -28,6 +28,7 @@
|
||||
"axios": "1.4.0",
|
||||
"axios-mock-adapter": "^1.21.4",
|
||||
"chart.js": "^4.1.2",
|
||||
"colorthief": "^2.4.0",
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
|
||||
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 81 KiB |
@@ -28,7 +28,12 @@ app.use(
|
||||
|
||||
// 处理根路径的请求
|
||||
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, () => {
|
||||
|
||||
21
src/@core/utils/dom.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export function removeEl(selector: string) {
|
||||
if (selector) {
|
||||
const el = document.querySelector(selector)
|
||||
el?.parentNode?.removeChild(el)
|
||||
}
|
||||
}
|
||||
|
||||
export function useDefer(maxFrameCount = 1) {
|
||||
const frameCount = ref(0)
|
||||
const refreshFrameCount = () => {
|
||||
requestAnimationFrame(() => {
|
||||
frameCount.value++
|
||||
if (frameCount.value < maxFrameCount)
|
||||
refreshFrameCount()
|
||||
})
|
||||
}
|
||||
refreshFrameCount()
|
||||
return function (showInFrameCount: number) {
|
||||
return frameCount.value >= showInFrameCount
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export function formatFileSize(bytes: number) {
|
||||
if (bytes < 0)
|
||||
throw new Error('字节数不能为负数。')
|
||||
|
||||
const units = ['B', 'K', 'M', 'G', 'T']
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let size = bytes
|
||||
let unitIndex = 0
|
||||
|
||||
@@ -109,3 +109,41 @@ export function formatBytes(bytes: number, decimals = 2) {
|
||||
|
||||
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('、')
|
||||
}
|
||||
|
||||
23
src/@core/utils/image.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import ColorThief from 'colorthief'
|
||||
|
||||
// 将 RGB 转换为十六进制
|
||||
function rgbStringToHex(rgbArray: number[]): string {
|
||||
if (rgbArray.length !== 3 || rgbArray.some(isNaN))
|
||||
throw new Error('Invalid RGB string format')
|
||||
|
||||
const [r, g, b] = rgbArray
|
||||
|
||||
const toHex = (c: number): string => {
|
||||
const hex = c.toString(16)
|
||||
return hex.length === 1 ? `0${hex}` : hex
|
||||
}
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
// 提取主要颜色
|
||||
export async function getDominantColor(image: HTMLImageElement): Promise<string> {
|
||||
const colorThief = new ColorThief()
|
||||
const dominantColor = colorThief.getColor(image)
|
||||
return rgbStringToHex(dominantColor)
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export function isToday(date: Date) {
|
||||
)
|
||||
}
|
||||
|
||||
// 计算时间差,返回xx天xx小时xx分钟
|
||||
// 计算时间差,返回xx天/xx小时/xx分钟/xx秒
|
||||
export function calculateTimeDifference(inputTime: string): string {
|
||||
if (!inputTime)
|
||||
return ''
|
||||
@@ -64,6 +64,38 @@ export function calculateTimeDifference(inputTime: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算时间差,返回xx天xx小时xx分钟
|
||||
export function calculateTimeDiff(inputTime: string): string {
|
||||
if (!inputTime)
|
||||
return ''
|
||||
|
||||
// 使用当前时区
|
||||
const inputDate = new Date(inputTime)
|
||||
const currentDate = new Date()
|
||||
|
||||
const timeDifference = currentDate.getTime() - inputDate.getTime()
|
||||
const secondsDifference = Math.floor(timeDifference / 1000)
|
||||
|
||||
const days = Math.floor(secondsDifference / 86400)
|
||||
const hours = Math.floor(secondsDifference % 86400 / 3600)
|
||||
const minutes = Math.floor(secondsDifference % 86400 % 3600 / 60)
|
||||
const secones = Math.floor(secondsDifference % 60)
|
||||
|
||||
if (days > 0)
|
||||
return `${days}天${hours}小时${minutes}分钟`
|
||||
|
||||
else if (hours > 0)
|
||||
return `${hours}小时${minutes}分钟`
|
||||
|
||||
else if (minutes > 0)
|
||||
return `${minutes}分钟`
|
||||
|
||||
else if (secones > 0)
|
||||
return `${secones}秒`
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// 判断一个数组subArray是不是在另一个数组mainArray中
|
||||
export function isContained(subArray: any[], mainArray: any[]): boolean {
|
||||
return subArray.every(element => mainArray.includes(element))
|
||||
|
||||
30
src/@core/utils/navigator.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// 请求和获取剪贴板内容
|
||||
export async function getClipboardContent() {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
return await navigator.clipboard.readText()
|
||||
}
|
||||
else {
|
||||
const input = document.createElement('textarea')
|
||||
document.body.appendChild(input)
|
||||
input.select()
|
||||
document.execCommand('paste')
|
||||
const content = input.value
|
||||
document.body.removeChild(input)
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
// 将内容复制到剪贴板,兼容非安全域场景
|
||||
export async function copyToClipboard(content: string) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(content)
|
||||
}
|
||||
else {
|
||||
const input = document.createElement('textarea')
|
||||
input.value = content
|
||||
document.body.appendChild(input)
|
||||
input.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(input)
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,9 @@ export interface Subscribe {
|
||||
|
||||
// 当前优先级
|
||||
current_priority: number
|
||||
|
||||
// 保存目录
|
||||
save_path: string
|
||||
}
|
||||
|
||||
// 历史记录
|
||||
@@ -337,7 +340,7 @@ export interface TmdbEpisode {
|
||||
guest_stars: Object[]
|
||||
}
|
||||
|
||||
// TMDB人特信息
|
||||
// TMDB人物信息
|
||||
export interface TmdbPerson {
|
||||
// ID
|
||||
id?: number
|
||||
@@ -388,6 +391,34 @@ export interface TmdbPerson {
|
||||
biography?: string
|
||||
}
|
||||
|
||||
// 豆瓣人物信息
|
||||
export interface DoubanPerson {
|
||||
// ID
|
||||
id?: string
|
||||
|
||||
// 名称
|
||||
name?: string
|
||||
|
||||
// 角色
|
||||
roles?: string[]
|
||||
|
||||
// 简介
|
||||
title?: string
|
||||
|
||||
// 详情页面
|
||||
url?: string
|
||||
|
||||
// 饰演
|
||||
character?: string
|
||||
|
||||
// 图片 large/normal
|
||||
avatar?: { [key: string]: string }
|
||||
|
||||
// 别名
|
||||
latin_name?: string
|
||||
|
||||
}
|
||||
|
||||
// 站点
|
||||
export interface Site {
|
||||
|
||||
@@ -512,9 +543,6 @@ export interface Plugin {
|
||||
// 插件图标
|
||||
plugin_icon?: string
|
||||
|
||||
// 主题色
|
||||
plugin_color?: string
|
||||
|
||||
// 插件版本
|
||||
plugin_version?: string
|
||||
|
||||
@@ -541,6 +569,15 @@ export interface Plugin {
|
||||
|
||||
// 是否有详情页面
|
||||
has_page?: boolean
|
||||
|
||||
// 是否有新版本
|
||||
has_update?: boolean
|
||||
|
||||
// 是否本地插件
|
||||
is_local?: boolean
|
||||
|
||||
// 插件仓库地址
|
||||
repo_url?: string
|
||||
}
|
||||
|
||||
// 种子信息
|
||||
@@ -614,6 +651,13 @@ export interface TorrentInfo {
|
||||
|
||||
// 促销描述
|
||||
volume_factor: string
|
||||
|
||||
// 免费时间
|
||||
freedate: string
|
||||
|
||||
// 剩余免费时间
|
||||
freedate_diff: string
|
||||
|
||||
}
|
||||
|
||||
// 识别元数据
|
||||
@@ -880,3 +924,26 @@ export interface FileItem {
|
||||
children: FileItem[]
|
||||
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/logos/plugin.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
@@ -1,10 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Axios } from 'axios'
|
||||
import axios from 'axios'
|
||||
import List from './filebrowser/List.vue'
|
||||
|
||||
import Toolbar from './filebrowser/Toolbar.vue'
|
||||
import Tree from './filebrowser/Tree.vue'
|
||||
import List from './filebrowser/List.vue'
|
||||
import type { EndPoints } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
@@ -70,10 +70,12 @@ const storagesArray = computed(() => {
|
||||
|
||||
// 方法
|
||||
function loadingChanged(loading: number) {
|
||||
if (loading)
|
||||
if (loading) {
|
||||
loading++
|
||||
else if (loading > 0)
|
||||
}
|
||||
else if (loading > 0) {
|
||||
loading--
|
||||
}
|
||||
}
|
||||
|
||||
function storageChanged(storage: string) {
|
||||
@@ -92,56 +94,58 @@ function sortChanged(s: string) {
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onBeforeMount(() => {
|
||||
onMounted(() => {
|
||||
activeStorage.value = props.storage ?? 'local'
|
||||
axiosInstance.value = props.axios ?? axios.create(props.axiosconfig)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="mx-auto" :loading="loading > 0">
|
||||
<Toolbar
|
||||
:path="props.path"
|
||||
:storages="storagesArray"
|
||||
:storage="activeStorage"
|
||||
:endpoints="props.endpoints"
|
||||
:axios="axiosInstance"
|
||||
@storagechanged="storageChanged"
|
||||
@pathchanged="pathChanged"
|
||||
@foldercreated="refreshPending = true"
|
||||
@sortchanged="sortChanged"
|
||||
/>
|
||||
<VRow no-gutters>
|
||||
<VCol v-if="tree" sm="auto" class="d-none d-md-block">
|
||||
<Tree
|
||||
:path="props.path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
/>
|
||||
</VCol>
|
||||
<VDivider v-if="tree" vertical />
|
||||
<VCol>
|
||||
<List
|
||||
:path="props.path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
:sort="sort"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
@filedeleted="refreshPending = true"
|
||||
@renamed="refreshPending = true"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VCard class="mx-auto" :loading="loading > 0 || !path">
|
||||
<div v-if="path">
|
||||
<Toolbar
|
||||
:path="path"
|
||||
:storages="storagesArray"
|
||||
:storage="activeStorage"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
@storagechanged="storageChanged"
|
||||
@pathchanged="pathChanged"
|
||||
@foldercreated="refreshPending = true"
|
||||
@sortchanged="sortChanged"
|
||||
/>
|
||||
<VRow no-gutters>
|
||||
<VCol v-if="tree" sm="auto" class="d-none d-md-block">
|
||||
<Tree
|
||||
:path="path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
/>
|
||||
</VCol>
|
||||
<VDivider v-if="tree" vertical />
|
||||
<VCol>
|
||||
<List
|
||||
:path="path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
:sort="sort"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
@filedeleted="refreshPending = true"
|
||||
@renamed="refreshPending = true"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
77
src/components/cards/BackdropCard.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<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')
|
||||
}
|
||||
</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="props.media?.image"
|
||||
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 font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.title }}
|
||||
</h1>
|
||||
<span>{{ 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>
|
||||
88
src/components/cards/DoubanPersonCard.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
import personIcon from '@images/misc/person-icon.png'
|
||||
import type { DoubanPerson } from '@/api/types'
|
||||
|
||||
const personProps = defineProps({
|
||||
person: Object as PropType<DoubanPerson>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 当前人物
|
||||
const personInfo = ref(personProps.person)
|
||||
|
||||
// 人物图片是否加载
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 人物图片地址
|
||||
function getPersonImage() {
|
||||
if (!personInfo.value?.avatar)
|
||||
return personIcon
|
||||
return personInfo.value?.avatar?.large
|
||||
}
|
||||
|
||||
// 打开人物详情
|
||||
function goPersonDetail() {
|
||||
if (!personInfo.value?.id)
|
||||
return
|
||||
window.open(`https://movie.douban.com/celebrity/${personInfo.value?.id}/`, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover v-bind="personProps">
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:height="personProps.height"
|
||||
:width="personProps.width"
|
||||
class="rounded-lg"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105': hover.isHovering,
|
||||
}"
|
||||
@click.stop="goPersonDetail"
|
||||
>
|
||||
<div
|
||||
class="person-card relative transform-gpu cursor-pointer rounded shadow ring-1 transition duration-150 ease-in-out scale-100 ring-gray-700"
|
||||
>
|
||||
<div style="padding-bottom: 150%;">
|
||||
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2">
|
||||
<div class="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
|
||||
<VAvatar
|
||||
size="120"
|
||||
:class="{
|
||||
'ring-1 ring-gray-700': isImageLoaded,
|
||||
}"
|
||||
>
|
||||
<VImg
|
||||
v-img
|
||||
:src="getPersonImage()"
|
||||
cover
|
||||
@load="isImageLoaded = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
<div class="w-full truncate text-center font-bold">
|
||||
{{ personInfo?.name }}
|
||||
</div>
|
||||
<div class="overflow-hidden whitespace-normal text-center text-sm" style=" display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical;-webkit-line-clamp: 2;">
|
||||
{{ personInfo?.character }}
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 right-0 h-12 rounded-b" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.person-card {
|
||||
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-theme-surface)) 60%);
|
||||
}
|
||||
|
||||
.person-card:hover {
|
||||
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-custom-background)) 60%);
|
||||
}
|
||||
</style>
|
||||
71
src/components/cards/LibraryCard.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MediaServerLibrary } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<MediaServerLibrary>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
// 图片加载完成响应
|
||||
function imageLoadHandler() {
|
||||
imageLoaded.value = true
|
||||
}
|
||||
|
||||
// 计算图片地址
|
||||
const getImgUrl = computed(() => {
|
||||
return props.media?.image || props.media?.image_list?.[0]
|
||||
})
|
||||
|
||||
// 跳转播放
|
||||
function goPlay() {
|
||||
if (props.media?.link)
|
||||
window.open(props.media?.link, '_blank')
|
||||
}
|
||||
</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>
|
||||
<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-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,
|
||||
})
|
||||
|
||||
// 订阅规则
|
||||
const subscribeRules = ref({
|
||||
show_edit_dialog: false,
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -34,7 +39,7 @@ const isSubscribed = ref(false)
|
||||
// 本地存在状态
|
||||
const isExists = ref(false)
|
||||
|
||||
// 各季缺失状态:0-已存在 1-部分缺失 2-全部缺失,没有数据也是已存在
|
||||
// 各季缺失状态:0-已入库 1-部分缺失 2-全部缺失,没有数据也是已入库
|
||||
const seasonsNotExisted = ref<{ [key: number]: number }>({})
|
||||
|
||||
// 订阅季弹窗
|
||||
@@ -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
|
||||
subscribeEditDialog.value = true
|
||||
}
|
||||
@@ -220,10 +225,10 @@ async function handleCheckSubscribe() {
|
||||
}
|
||||
}
|
||||
|
||||
// 查询当前媒体是否已存在
|
||||
// 查询当前媒体是否已入库
|
||||
async function handleCheckExists() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('media/exists', {
|
||||
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||
params: {
|
||||
tmdbid: props.media?.tmdb_id,
|
||||
title: props.media?.title,
|
||||
@@ -251,6 +256,7 @@ async function checkSubscribe(season = 0) {
|
||||
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
||||
params: {
|
||||
season,
|
||||
title: props.media?.title,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -268,10 +274,10 @@ async function checkSeasonsNotExists() {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
const result: NotExistMediaInfo[] = await api.post('download/notexists', props.media)
|
||||
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', props.media)
|
||||
if (result) {
|
||||
result.forEach((item) => {
|
||||
// 0-已存在 1-部分缺失 2-全部缺失
|
||||
// 0-已入库 1-部分缺失 2-全部缺失
|
||||
let state = 0
|
||||
if (item.episodes.length === 0)
|
||||
state = 2
|
||||
@@ -301,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() {
|
||||
if (isSubscribed.value)
|
||||
@@ -327,14 +347,14 @@ function getExistColor(season: number) {
|
||||
function getExistText(season: number) {
|
||||
const state = seasonsNotExisted.value[season]
|
||||
if (!state)
|
||||
return '已存在'
|
||||
return '已入库'
|
||||
|
||||
if (state === 1)
|
||||
return '部分缺失'
|
||||
else if (state === 2)
|
||||
return '缺失'
|
||||
else
|
||||
return '已存在'
|
||||
return '已入库'
|
||||
}
|
||||
|
||||
// 打开详情页
|
||||
@@ -372,6 +392,7 @@ function handleSearch() {
|
||||
onBeforeMount(() => {
|
||||
handleCheckSubscribe()
|
||||
handleCheckExists()
|
||||
querySubscribeRules()
|
||||
})
|
||||
|
||||
// 计算图片地址
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { getDominantColor } from '@/@core/utils/image'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -13,19 +15,55 @@ const props = defineProps({
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['install'])
|
||||
|
||||
// 背景颜色
|
||||
const backgroundColor = ref('#28A9E1')
|
||||
|
||||
// 图片对象
|
||||
const imageRef = ref<any>()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 进度框
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 进度框文本
|
||||
const progressText = ref('正在安装插件...')
|
||||
|
||||
// 图片是否加载完成
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 图片是否加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 图片加载完成
|
||||
async function imageLoaded() {
|
||||
isImageLoaded.value = true
|
||||
const imageElement = imageRef.value?.$el.querySelector('img') as HTMLImageElement
|
||||
// 从图片中提取背景色
|
||||
backgroundColor.value = await getDominantColor(imageElement)
|
||||
}
|
||||
|
||||
// 安装插件
|
||||
async function installPlugin() {
|
||||
try {
|
||||
// 显示等待提示框
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在安装 ${props.plugin?.plugin_name} ${props?.plugin?.plugin_version} 插件...`
|
||||
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`plugin/install/${props.plugin?.id}`,
|
||||
{
|
||||
params: {
|
||||
repo_url: props.plugin?.repo_url,
|
||||
force: props.plugin?.has_update,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// 隐藏等待提示框
|
||||
progressDialog.value = false
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(`插件 ${props.plugin?.plugin_name} 安装成功!`)
|
||||
|
||||
@@ -33,13 +71,63 @@ async function installPlugin() {
|
||||
emit('install')
|
||||
}
|
||||
else {
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 安装失败:${result.message}}`)
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 安装失败:${result.message}`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算图标路径
|
||||
const iconPath: Ref<string> = computed(() => {
|
||||
if (imageLoadError.value)
|
||||
return noImage
|
||||
// 如果是网络图片则使用代理后返回
|
||||
if (props.plugin?.plugin_icon?.startsWith('http'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}`
|
||||
|
||||
return `/plugin_icon/${props.plugin?.plugin_icon}`
|
||||
})
|
||||
|
||||
// 访问插件页面
|
||||
function visitPluginPage() {
|
||||
// 将raw.githubusercontent.com转换为项目地址
|
||||
let repoUrl = props.plugin?.repo_url
|
||||
if (repoUrl) {
|
||||
if (repoUrl.includes('raw.githubusercontent.com')) {
|
||||
if (!repoUrl.endsWith('/'))
|
||||
repoUrl += '/'
|
||||
|
||||
if (repoUrl.split('/').length < 6)
|
||||
repoUrl = `${repoUrl}main/`
|
||||
|
||||
try {
|
||||
const [user, repo] = repoUrl.split('/').slice(-4, -2)
|
||||
repoUrl = `https://github.com/${user}/${repo}`
|
||||
}
|
||||
catch (error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
repoUrl = props.plugin?.author_url
|
||||
}
|
||||
window.open(repoUrl, '_blank')
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '查看详情',
|
||||
value: 1,
|
||||
props: {
|
||||
prependIcon: 'mdi-information-outline',
|
||||
click: visitPluginPage,
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -50,17 +138,51 @@ async function installPlugin() {
|
||||
>
|
||||
<div
|
||||
class="relative pa-4 text-center card-cover-blurred"
|
||||
:style="{ background: `${props.plugin?.plugin_color}` }"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div class="me-n3 absolute top-0 right-3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" class="text-white" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.plugin?.has_update"
|
||||
class="me-n3 absolute top-0 left-1"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-new-box"
|
||||
class="text-white"
|
||||
/>
|
||||
</div>
|
||||
<VAvatar
|
||||
size="8rem"
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
>
|
||||
<VImg
|
||||
:src="`/plugin_icon/${props.plugin?.plugin_icon}`"
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
@load="isImageLoaded = true"
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
@@ -76,9 +198,29 @@ async function installPlugin() {
|
||||
@click.stop
|
||||
>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a>
|
||||
</a><br>
|
||||
版本:{{ props.plugin?.plugin_version }}
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<!-- 安装插件进度框 -->
|
||||
<VDialog
|
||||
v-model="progressDialog"
|
||||
:scrim="false"
|
||||
width="25rem"
|
||||
>
|
||||
<VCard
|
||||
color="primary"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
{{ progressText }}
|
||||
<VProgressLinear
|
||||
indeterminate
|
||||
color="white"
|
||||
class="mb-0 mt-1"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import FormRender from '@/components/render/FormRender.vue'
|
||||
import PageRender from '@/components/render/PageRender.vue'
|
||||
import { isNullOrEmptyObject } from '@core/utils'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { getDominantColor } from '@/@core/utils/image'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -16,9 +19,18 @@ const props = defineProps({
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['remove', 'save'])
|
||||
|
||||
// 背景颜色
|
||||
const backgroundColor = ref('#28A9E1')
|
||||
|
||||
// 图片对象
|
||||
const imageRef = ref<any>()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 本身是否可见
|
||||
const isVisible = ref(true)
|
||||
|
||||
@@ -31,17 +43,44 @@ const pluginConfigForm = ref({})
|
||||
// 插件表单配置项
|
||||
let pluginFormItems = reactive([])
|
||||
|
||||
// 插件详情页面
|
||||
// 插件数据页面
|
||||
const pluginInfoDialog = ref(false)
|
||||
|
||||
// 插件详情页面配置项
|
||||
// 插件数据页面配置项
|
||||
let pluginPageItems = reactive([])
|
||||
|
||||
// 图片是否加载完成
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 图片是否加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 图片加载完成
|
||||
async function imageLoaded() {
|
||||
isImageLoaded.value = true
|
||||
const imageElement = imageRef.value?.$el.querySelector('img') as HTMLImageElement
|
||||
// 从图片中提取背景色
|
||||
backgroundColor.value = await getDominantColor(imageElement)
|
||||
}
|
||||
|
||||
// 调用API卸载插件
|
||||
async function uninstallPlugin() {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认卸载插件 ${props.plugin?.plugin_name} ?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (!isConfirmed)
|
||||
return
|
||||
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`)
|
||||
if (result.success) {
|
||||
@@ -74,7 +113,7 @@ async function loadPluginForm() {
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API读取详情页面
|
||||
// 调用API读取数据页面
|
||||
async function loadPluginPage() {
|
||||
try {
|
||||
const result: [] = await api.get(`plugin/page/${props.plugin?.id}`)
|
||||
@@ -117,9 +156,9 @@ async function savePluginConf() {
|
||||
}
|
||||
}
|
||||
|
||||
// 显示插件详情
|
||||
// 显示插件数据
|
||||
async function showPluginInfo() {
|
||||
// 加载详情
|
||||
// 加载数据
|
||||
await loadPluginPage()
|
||||
pluginConfigDialog.value = false
|
||||
pluginInfoDialog.value = true
|
||||
@@ -136,10 +175,60 @@ async function showPluginConfig() {
|
||||
pluginConfigDialog.value = true
|
||||
}
|
||||
|
||||
// 计算图标路径
|
||||
const iconPath: Ref<string> = computed(() => {
|
||||
if (imageLoadError.value)
|
||||
return noImage
|
||||
// 如果是网络图片则使用代理后返回
|
||||
if (props.plugin?.plugin_icon?.startsWith('http'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}`
|
||||
|
||||
return `/plugin_icon/${props.plugin?.plugin_icon}`
|
||||
})
|
||||
|
||||
// 重置插件
|
||||
async function resetPlugin() {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认重置插件 ${props.plugin?.plugin_name} 的配置数据?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (!isConfirmed)
|
||||
return
|
||||
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(`plugin/reset/${props.plugin?.id}`)
|
||||
if (result.success) {
|
||||
$toast.success(`插件 ${props.plugin?.plugin_name} 数据已重置`)
|
||||
// 通知父组件刷新
|
||||
emit('save')
|
||||
}
|
||||
else {
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 重置失败:${result.message}}`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 访问作者主页
|
||||
function visitAuthorPage() {
|
||||
window.open(props.plugin?.author_url, '_blank')
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '查看详情',
|
||||
title: '查看数据',
|
||||
value: 1,
|
||||
show: props.plugin?.has_page,
|
||||
props: {
|
||||
@@ -148,7 +237,7 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '配置',
|
||||
title: '设置',
|
||||
value: 2,
|
||||
show: true,
|
||||
props: {
|
||||
@@ -157,15 +246,34 @@ const dropdownItems = ref([
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '卸载',
|
||||
title: '重置',
|
||||
value: 3,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-cancel',
|
||||
color: 'warning',
|
||||
click: resetPlugin,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '卸载',
|
||||
value: 4,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
click: uninstallPlugin,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '作者主页',
|
||||
value: 4,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-home-circle-outline',
|
||||
click: visitAuthorPage,
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -184,7 +292,7 @@ const dropdownItems = ref([
|
||||
>
|
||||
<div
|
||||
class="relative pa-4 text-center card-cover-blurred"
|
||||
:style="{ background: `${props.plugin?.plugin_color}` }"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div class="me-n3 absolute top-0 right-3">
|
||||
<IconBtn>
|
||||
@@ -213,20 +321,22 @@ const dropdownItems = ref([
|
||||
</div>
|
||||
<VAvatar
|
||||
size="8rem"
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
>
|
||||
<VImg
|
||||
:src="`/plugin_icon/${props.plugin?.plugin_icon}`"
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
<VCardItem class="py-2">
|
||||
<VCardTitle class="flex items-center flex-row">
|
||||
<VBadge v-if="props.plugin?.state" dot inline color="success" class="me-1 mb-1" />
|
||||
{{ props.plugin?.plugin_name }}
|
||||
{{ props.plugin?.plugin_name }}<span class="text-sm ms-2 mt-1 text-gray-500">{{ props.plugin?.plugin_version }}</span>
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
@@ -254,7 +364,7 @@ const dropdownItems = ref([
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo">
|
||||
查看详情
|
||||
查看数据
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
@@ -267,7 +377,7 @@ const dropdownItems = ref([
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- 插件详情页面 -->
|
||||
<!-- 插件数据页面 -->
|
||||
<VDialog
|
||||
v-model="pluginInfoDialog"
|
||||
scrollable
|
||||
|
||||
101
src/components/cards/PosterCard.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<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
|
||||
return props.media?.image
|
||||
})
|
||||
|
||||
// 跳转播放
|
||||
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>
|
||||
@@ -33,9 +33,6 @@ const testButtonText = ref('测试')
|
||||
// 测试按钮可用性
|
||||
const testButtonDisable = ref(false)
|
||||
|
||||
// 更新按钮文字
|
||||
const updateButtonText = ref('更新')
|
||||
|
||||
// 更新按钮可用性
|
||||
const updateButtonDisable = ref(false)
|
||||
|
||||
@@ -48,6 +45,12 @@ const siteEditDialog = ref(false)
|
||||
// 资源浏览弹窗
|
||||
const resourceDialog = ref(false)
|
||||
|
||||
// 进度条
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 进度文本
|
||||
const progressText = ref('请稍候 ...')
|
||||
|
||||
// 资源浏览表头
|
||||
const resourceHeaders = [
|
||||
{ title: '标题', key: 'title', sortable: false },
|
||||
@@ -138,9 +141,11 @@ async function updateSiteCookie() {
|
||||
|
||||
// 更新按钮状态
|
||||
siteCookieDialog.value = false
|
||||
updateButtonText.value = '更新中 ...'
|
||||
updateButtonDisable.value = true
|
||||
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在更新 ${cardProps.site?.name} Cookie & UA ...`
|
||||
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`site/cookie/${cardProps.site?.id}`,
|
||||
{
|
||||
@@ -156,7 +161,7 @@ async function updateSiteCookie() {
|
||||
else
|
||||
$toast.error(`${cardProps.site?.name} 更新失败:${result.message}`)
|
||||
|
||||
updateButtonText.value = '更新'
|
||||
progressDialog.value = false
|
||||
updateButtonDisable.value = false
|
||||
}
|
||||
catch (error) {
|
||||
@@ -299,7 +304,7 @@ onMounted(() => {
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-refresh" />
|
||||
</template>
|
||||
{{ updateButtonText }}
|
||||
更新
|
||||
</VBtn>
|
||||
<VBtn
|
||||
:disabled="testButtonDisable"
|
||||
@@ -407,6 +412,23 @@ onMounted(() => {
|
||||
<div class="text-sm my-1">
|
||||
{{ item.raw.description }}
|
||||
</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
|
||||
v-for="(label, index) in item.raw?.labels"
|
||||
:key="index"
|
||||
@@ -488,6 +510,24 @@ onMounted(() => {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<VDialog
|
||||
v-model="progressDialog"
|
||||
:scrim="false"
|
||||
width="25rem"
|
||||
>
|
||||
<VCard
|
||||
color="primary"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
{{ progressText }}
|
||||
<VProgressLinear
|
||||
indeterminate
|
||||
color="white"
|
||||
class="mb-0 mt-1"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -25,7 +25,6 @@ const tmdbKeyword = ref<HTMLElement | null>(null)
|
||||
|
||||
// 选中条目
|
||||
function selectMedia(item: TmdbItem) {
|
||||
console.log(item)
|
||||
emit('update:modelValue', item.tmdbid)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
@@ -195,6 +195,23 @@ onMounted(() => {
|
||||
v-if="torrent?.labels"
|
||||
class="pb-3 pt-0 pe-12"
|
||||
>
|
||||
<VChip
|
||||
v-if="torrent?.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ torrent?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(label, index) in torrent?.labels"
|
||||
:key="index"
|
||||
|
||||
@@ -150,6 +150,23 @@ onMounted(() => {
|
||||
v-if="torrent?.labels"
|
||||
class="pt-2"
|
||||
>
|
||||
<VChip
|
||||
v-if="torrent?.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ torrent?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(label, index) in torrent?.labels"
|
||||
:key="index"
|
||||
|
||||
39
src/components/form/ImportCodeForm.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
// 代码
|
||||
const codeString = ref('')
|
||||
|
||||
// 导入
|
||||
function handleImport() {
|
||||
emit('update:modelValue', codeString.value)
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
:title="props.title"
|
||||
class="rounded-t"
|
||||
>
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VCardText class="pt-2">
|
||||
<VTextarea v-model="codeString" />
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="handleImport"
|
||||
>
|
||||
导入
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -199,7 +199,7 @@ async function updateSiteInfo() {
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="siteForm.limit_seconds"
|
||||
v-model="siteForm.limit_count"
|
||||
label="访问次数"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
|
||||
@@ -39,6 +39,7 @@ const subscribeForm = ref<Subscribe>({
|
||||
last_update: '',
|
||||
username: '',
|
||||
current_priority: 0,
|
||||
save_path: '',
|
||||
})
|
||||
|
||||
// 提示框
|
||||
@@ -323,7 +324,21 @@ watchEffect(() => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="subscribeForm.save_path"
|
||||
label="保存路径"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="subscribeForm.best_version"
|
||||
label="洗版"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createApp } from 'vue'
|
||||
import '@/@iconify/icons-bundle'
|
||||
import ToastPlugin from 'vue-toast-notification'
|
||||
import VuetifyUseDialog from 'vuetify-use-dialog'
|
||||
import { removeEl } from './@core/utils/dom'
|
||||
import App from '@/App.vue'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
import { loadFonts } from '@/plugins/webfontloader'
|
||||
@@ -11,7 +12,6 @@ import '@core/scss/template/index.scss'
|
||||
import '@layouts/styles/index.scss'
|
||||
import '@styles/styles.scss'
|
||||
import 'vue-toast-notification/dist/theme-bootstrap.css'
|
||||
import { removeEl } from '@/util'
|
||||
|
||||
loadFonts()
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ const route = useRoute()
|
||||
// 标题
|
||||
const title = route.query?.title?.toString()
|
||||
|
||||
// 类型
|
||||
const type = route.query?.type?.toString()
|
||||
|
||||
// 计算API路径
|
||||
function getApiPath(paths: string[] | string) {
|
||||
if (Array.isArray(paths))
|
||||
@@ -34,6 +37,7 @@ function getApiPath(paths: string[] | string) {
|
||||
<PersonCardListView
|
||||
:apipath="getApiPath(props.paths || '')"
|
||||
:params="route.query"
|
||||
:type="type"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -6,11 +6,57 @@ import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue'
|
||||
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
|
||||
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
|
||||
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue'
|
||||
import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
|
||||
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
|
||||
|
||||
// 仪表盘配置
|
||||
const dashboard_names = {
|
||||
storage: '存储空间',
|
||||
mediaStatistic: '媒体统计',
|
||||
weeklyOverview: '最近入库',
|
||||
speed: '实时速率',
|
||||
scheduler: '后台任务',
|
||||
cpu: 'CPU',
|
||||
memory: '内存',
|
||||
library: '我的媒体库',
|
||||
playing: '继续观看',
|
||||
latest: '最近添加',
|
||||
}
|
||||
|
||||
// 弹窗
|
||||
const dialog = ref(false)
|
||||
|
||||
// 从localStorage中获取数据
|
||||
const default_config = {
|
||||
mediaStatistic: true,
|
||||
scheduler: false,
|
||||
speed: false,
|
||||
storage: true,
|
||||
weeklyOverview: false,
|
||||
cpu: false,
|
||||
memory: false,
|
||||
library: true,
|
||||
playing: true,
|
||||
latest: true,
|
||||
}
|
||||
const config = ref(JSON.parse(localStorage.getItem('MP_DASHBOARD') || '{}'))
|
||||
if (Object.keys(config.value).length === 0) {
|
||||
config.value = default_config
|
||||
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value))
|
||||
}
|
||||
|
||||
// 设置项目
|
||||
function setDashboardConfig() {
|
||||
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value))
|
||||
dialog.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow class="match-height">
|
||||
<VCol
|
||||
v-if="config.storage"
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
@@ -18,6 +64,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.mediaStatistic"
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
@@ -25,6 +72,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.weeklyOverview"
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
@@ -32,6 +80,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.speed"
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
@@ -39,6 +88,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.scheduler"
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
@@ -46,6 +96,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.cpu"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
@@ -53,10 +104,75 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.memory"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AnalyticsMemory />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.library"
|
||||
cols="12"
|
||||
>
|
||||
<MediaServerLibrary />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.playing"
|
||||
cols="12"
|
||||
>
|
||||
<MediaServerPlaying />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
v-if="config.latest"
|
||||
cols="12"
|
||||
>
|
||||
<MediaServerLatest />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<!-- 底部操作按钮 -->
|
||||
<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>
|
||||
|
||||
@@ -28,6 +28,18 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
|
||||
title="热门电视剧"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="douban/movie_hot"
|
||||
linkurl="/browse/douban/movie_hot?title=热门电影"
|
||||
title="热门电影"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="douban/tv_hot"
|
||||
linkurl="/browse/douban/tv_hot?title=热门电视剧"
|
||||
title="热门电视剧"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="douban/tv_animation"
|
||||
linkurl="/browse/douban/tv_animation?title=热门动漫"
|
||||
|
||||
@@ -31,7 +31,7 @@ export default {
|
||||
elevation: 0,
|
||||
},
|
||||
VList: {
|
||||
activeColor: 'primary',
|
||||
color: 'primary',
|
||||
},
|
||||
VPagination: {
|
||||
activeColor: 'primary',
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export function removeEl(selector: string) {
|
||||
if (selector) {
|
||||
const el = document.querySelector(selector)
|
||||
el?.parentNode?.removeChild(el)
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './dom'
|
||||
48
src/views/dashboard/MediaServerLatest.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
import PosterCard from '@/components/cards/PosterCard.vue'
|
||||
|
||||
// 最近入库列表
|
||||
const latestList = ref<MediaServerPlayItem[]>([])
|
||||
|
||||
// 调用API查询
|
||||
async function loadLatest() {
|
||||
try {
|
||||
latestList.value = await api.get('mediaserver/latest')
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLatest()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>最近添加</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<div
|
||||
v-if="latestList.length > 0"
|
||||
class="grid gap-4 grid-media-card mx-3 mb-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<PosterCard
|
||||
v-for="data in latestList"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
</style>
|
||||
50
src/views/dashboard/MediaServerLibrary.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
import LibraryCard from '@/components/cards/LibraryCard.vue'
|
||||
|
||||
// 媒体库列表
|
||||
const libraryList = ref<MediaServerPlayItem[]>([])
|
||||
|
||||
// 调用API查询
|
||||
async function loadLibrary() {
|
||||
try {
|
||||
libraryList.value = await api.get('mediaserver/library')
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLibrary()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>我的媒体库</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<div
|
||||
v-if="libraryList.length > 0"
|
||||
class="grid gap-4 grid-backdrop-card mx-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<LibraryCard
|
||||
v-for="data in libraryList"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
height="10rem"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-backdrop-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
50
src/views/dashboard/MediaServerPlaying.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
import BackdropCard from '@/components/cards/BackdropCard.vue'
|
||||
|
||||
// 继续播放列表
|
||||
const playingList = ref<MediaServerPlayItem[]>([])
|
||||
|
||||
// 调用API查询
|
||||
async function loadPlayingList() {
|
||||
try {
|
||||
playingList.value = await api.get('mediaserver/playing')
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPlayingList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>继续观看</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<div
|
||||
v-if="playingList.length > 0"
|
||||
class="grid gap-4 grid-backdrop-card mx-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<BackdropCard
|
||||
v-for="data in playingList"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
height="10rem"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-backdrop-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -156,7 +156,7 @@ async function fetchData({ done }: { done: any }) {
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
error-title="没有数据"
|
||||
error-description="无法获取到TMDB媒体信息。"
|
||||
error-description="无法获取到媒体信息。"
|
||||
/>
|
||||
</VInfiniteScroll>
|
||||
</template>
|
||||
|
||||
@@ -25,8 +25,8 @@ const mediaDetail = ref<MediaInfo>({} as MediaInfo)
|
||||
// 订阅编辑弹窗
|
||||
const subscribeEditDialog = ref(false)
|
||||
|
||||
// 本地是否存在
|
||||
const isExists = ref(false)
|
||||
// 本地是否存在,存在则包括Item信息
|
||||
const existsItemId = ref('')
|
||||
|
||||
// 是否已订阅
|
||||
const isSubscribed = ref(false)
|
||||
@@ -37,7 +37,7 @@ const isRefreshed = ref(false)
|
||||
// 存储每一季的集信息
|
||||
const seasonEpisodesInfo = ref({} as { [key: number]: TmdbEpisode[] })
|
||||
|
||||
// 各季缺失状态:0-已存在 1-部分缺失 2-全部缺失,没有数据也是已存在
|
||||
// 各季缺失状态:0-已入库 1-部分缺失 2-全部缺失,没有数据也是已入库
|
||||
const seasonsNotExisted = ref<{ [key: number]: number }>({})
|
||||
|
||||
// 各季的订阅状态
|
||||
@@ -46,6 +46,11 @@ const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
|
||||
// 订阅编号
|
||||
const subscribeId = ref<number>()
|
||||
|
||||
// 订阅规则
|
||||
const subscribeRules = ref({
|
||||
show_edit_dialog: false,
|
||||
})
|
||||
|
||||
// 调用API查询详情
|
||||
async function getMediaDetail() {
|
||||
if (mediaProps.mediaid && mediaProps.type) {
|
||||
@@ -59,9 +64,8 @@ async function getMediaDetail() {
|
||||
return
|
||||
|
||||
// 检查存在状态
|
||||
if (mediaDetail.value.type === '电影')
|
||||
checkMovieExists()
|
||||
else
|
||||
checkExists()
|
||||
if (mediaDetail.value.type === '电视剧')
|
||||
checkSeasonsNotExists()
|
||||
// 检查订阅状态
|
||||
if (mediaDetail.value.type === '电影')
|
||||
@@ -85,10 +89,10 @@ async function loadSeasonEpisodes(season: number) {
|
||||
}
|
||||
}
|
||||
|
||||
// 查询当前媒体是否已存在
|
||||
async function checkMovieExists() {
|
||||
// 查询当前媒体是否已入库
|
||||
async function checkExists() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('media/exists', {
|
||||
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||
params: {
|
||||
tmdbid: mediaDetail.value.tmdb_id,
|
||||
title: mediaDetail.value.title,
|
||||
@@ -99,7 +103,7 @@ async function checkMovieExists() {
|
||||
})
|
||||
|
||||
if (result.success)
|
||||
isExists.value = true
|
||||
existsItemId.value = result.data.item.id
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
@@ -109,11 +113,12 @@ async function checkMovieExists() {
|
||||
// 查询当前媒体是否已订阅
|
||||
async function checkSubscribe(season = 0) {
|
||||
try {
|
||||
const mediaid = `tmdb:${mediaDetail.value.tmdb_id}`
|
||||
const mediaid = mediaDetail.value.tmdb_id ? `tmdb:${mediaDetail.value.tmdb_id}` : `douban:${mediaDetail.value.douban_id}`
|
||||
|
||||
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
||||
params: {
|
||||
season,
|
||||
title: mediaDetail.value.title,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -132,20 +137,15 @@ async function checkSeasonsNotExists() {
|
||||
if (mediaDetail.value.type !== '电视剧')
|
||||
return
|
||||
try {
|
||||
const result: NotExistMediaInfo[] = await api.post('download/notexists', mediaDetail.value)
|
||||
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', mediaDetail.value)
|
||||
if (result) {
|
||||
if (result.length === 0)
|
||||
isExists.value = true
|
||||
|
||||
result.forEach((item) => {
|
||||
// 0-已存在 1-部分缺失 2-全部缺失
|
||||
// 0-已入库 1-部分缺失 2-全部缺失
|
||||
let state = 0
|
||||
if (item.episodes.length === 0)
|
||||
state = 2
|
||||
else if (item.episodes.length < item.total_episode)
|
||||
state = 1
|
||||
if (state !== 2)
|
||||
isExists.value = true
|
||||
seasonsNotExisted.value[item.season] = state
|
||||
})
|
||||
}
|
||||
@@ -187,7 +187,7 @@ async function addSubscribe(season = 0) {
|
||||
startNProgress()
|
||||
try {
|
||||
// 是否洗版
|
||||
let best_version = isExists.value ? 1 : 0
|
||||
let best_version = existsItemId.value ? 1 : 0
|
||||
if (season)
|
||||
// 全部存在时洗版
|
||||
best_version = !seasonsNotExisted.value[season] ? 1 : 0
|
||||
@@ -220,7 +220,7 @@ async function addSubscribe(season = 0) {
|
||||
)
|
||||
|
||||
// 显示编辑弹窗
|
||||
if (result.success) {
|
||||
if (result.success && subscribeRules.value.show_edit_dialog) {
|
||||
subscribeId.value = result.data.id
|
||||
subscribeEditDialog.value = true
|
||||
}
|
||||
@@ -282,6 +282,20 @@ async function removeSubscribe(season: number) {
|
||||
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) {
|
||||
if (isSubscribed.value)
|
||||
@@ -358,14 +372,14 @@ function getExistColor(season: number) {
|
||||
function getExistText(season: number) {
|
||||
const state = seasonsNotExisted.value[season]
|
||||
if (!state)
|
||||
return '已存在'
|
||||
return '已入库'
|
||||
|
||||
if (state === 1)
|
||||
return '部分缺失'
|
||||
else if (state === 2)
|
||||
return '缺失'
|
||||
else
|
||||
return '已存在'
|
||||
return '已入库'
|
||||
}
|
||||
|
||||
// 计算订阅图标
|
||||
@@ -391,18 +405,40 @@ function joinArray(arr: string[]) {
|
||||
|
||||
// 开始搜索
|
||||
function handleSearch(area: string) {
|
||||
const keyword = mediaDetail.value.tmdb_id ? `tmdb:${mediaDetail.value.tmdb_id}` : `douban:${mediaDetail.value.douban_id}`
|
||||
router.push({
|
||||
path: '/resource',
|
||||
query: {
|
||||
keyword: `tmdb:${mediaDetail.value.tmdb_id}`,
|
||||
keyword,
|
||||
type: mediaDetail.value.type,
|
||||
area,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转播放页面
|
||||
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(() => {
|
||||
getMediaDetail()
|
||||
querySubscribeRules()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -418,9 +454,9 @@ onBeforeMount(() => {
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" class="max-w-8xl mx-auto px-4">
|
||||
<template v-if="mediaDetail.backdrop_path">
|
||||
<template v-if="mediaDetail.backdrop_path || mediaDetail.poster_path">
|
||||
<div class="vue-media-back absolute left-0 top-0 w-full h-96">
|
||||
<VImg class="h-96" :src="mediaDetail.backdrop_path" cover />
|
||||
<VImg class="h-96" :src="mediaDetail.backdrop_path || mediaDetail.poster_path" cover />
|
||||
</div>
|
||||
<div class="vue-media-back absolute left-0 top-0 w-full h-96" />
|
||||
</template>
|
||||
@@ -436,7 +472,7 @@ onBeforeMount(() => {
|
||||
</VImg>
|
||||
</div>
|
||||
<div class="media-title">
|
||||
<div v-if="isExists" class="media-status">
|
||||
<div v-if="existsItemId" class="media-status">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap transition !no-underline bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 hover:bg-green-500 hover:bg-opacity-100 false overflow-hidden">
|
||||
<div class="relative z-20 flex items-center false"><span>已入库</span></div>
|
||||
</span>
|
||||
@@ -456,7 +492,7 @@ onBeforeMount(() => {
|
||||
</span>
|
||||
</div>
|
||||
<div class="media-actions">
|
||||
<VBtn v-if="mediaDetail.tmdb_id" variant="tonal" color="info">
|
||||
<VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" variant="tonal" color="info" class="mb-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-magnify" />
|
||||
</template>
|
||||
@@ -482,12 +518,18 @@ onBeforeMount(() => {
|
||||
</VList>
|
||||
</VMenu>
|
||||
</VBtn>
|
||||
<VBtn v-if="mediaDetail.type === '电影'" 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>
|
||||
<VIcon :icon="getSubscribeIcon" />
|
||||
</template>
|
||||
{{ isSubscribed ? '已订阅' : '订阅' }}
|
||||
</VBtn>
|
||||
<VBtn v-if="existsItemId" class="ms-2 mb-2" variant="tonal" @click="handlePlay()">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-play" />
|
||||
</template>
|
||||
在线播放
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-overview">
|
||||
@@ -510,10 +552,6 @@ onBeforeMount(() => {
|
||||
<span>{{ joinArray(director.roles) }}</span>
|
||||
<a class="crew-name" :href="`${director.url}`" target="_blank">{{ director.name }}</a>
|
||||
</li>
|
||||
<li v-for="director in mediaDetail.actors" :key="director.id">
|
||||
<span>{{ joinArray(director.roles) }}</span>
|
||||
<a class="crew-name" :href="`${director.url}`" target="_blank">{{ director.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mt-6">
|
||||
<a v-if="mediaDetail.tmdb_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getTheMovieDbLink()" target="_blank">
|
||||
@@ -665,12 +703,56 @@ onBeforeMount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="mediaDetail.douban_id" class="media-overview-right">
|
||||
<div class="media-facts">
|
||||
<div v-if="mediaDetail.vote_average" class="media-ratings">
|
||||
<VRating
|
||||
v-model="mediaDetail.vote_average"
|
||||
density="compact"
|
||||
length="10"
|
||||
class="ma-2"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mediaDetail.douban_id" class="media-fact">
|
||||
<span>豆瓣ID</span>
|
||||
<span class="media-fact-value">{{ mediaDetail.douban_id }}</span>
|
||||
</div>
|
||||
<div v-if="mediaDetail.original_title" class="media-fact">
|
||||
<span>原始标题</span>
|
||||
<span class="media-fact-value">{{ mediaDetail.original_title }}</span>
|
||||
</div>
|
||||
<div v-if="mediaDetail.release_date" class="media-fact">
|
||||
<span>上映日期</span>
|
||||
<span class="media-fact-value">
|
||||
{{ mediaDetail.release_date }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="mediaDetail.production_countries" class="media-fact border-b-0">
|
||||
<span>出品国家</span>
|
||||
<span class="media-fact-value">
|
||||
<span v-for="country in getProductionCountries" :key="country" class="flex items-center justify-end text-end">
|
||||
{{ country }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id">
|
||||
<PersonCardSlideView
|
||||
:apipath="`tmdb/credits/${mediaDetail.tmdb_id}/${mediaProps.type}`"
|
||||
:linkurl="`/credits/tmdb/credits/${mediaDetail.tmdb_id}/${mediaProps.type}?title=演员阵容`"
|
||||
:linkurl="`/credits/tmdb/credits/${mediaDetail.tmdb_id}/${mediaProps.type}?title=演员阵容&type=tmdb`"
|
||||
title="演员阵容"
|
||||
type="tmdb"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="mediaDetail.douban_id">
|
||||
<PersonCardSlideView
|
||||
:apipath="`douban/credits/${mediaDetail.douban_id}/${mediaProps.type}`"
|
||||
:linkurl="`/credits/douban/credits/${mediaDetail.douban_id}/${mediaProps.type}?title=演员阵容&type=douban`"
|
||||
title="演员阵容"
|
||||
type="douban"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id">
|
||||
@@ -680,6 +762,13 @@ onBeforeMount(() => {
|
||||
title="推荐"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="mediaDetail.douban_id">
|
||||
<MediaCardSlideView
|
||||
:apipath="`douban/recommend/${mediaDetail.douban_id}/${mediaProps.type}`"
|
||||
:linkurl="`/browse/douban/recommend/${mediaDetail.douban_id}/${mediaProps.type}?title=推荐`"
|
||||
title="推荐"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id">
|
||||
<MediaCardSlideView
|
||||
:apipath="`tmdb/similar/${mediaDetail.tmdb_id}/${mediaProps.type}`"
|
||||
@@ -693,7 +782,7 @@ onBeforeMount(() => {
|
||||
v-if="!mediaDetail.tmdb_id && !mediaDetail.douban_id && isRefreshed"
|
||||
error-code="500"
|
||||
error-title="出错啦!"
|
||||
error-description="未识别到TMDB媒体信息。"
|
||||
error-description="未识别到媒体信息。"
|
||||
/>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<SubscribeEditForm
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import type { TmdbPerson } from '@/api/types'
|
||||
import PersonCard from '@/components/cards/PersonCard.vue'
|
||||
import DoubanPersonCard from '@/components/cards/DoubanPersonCard.vue'
|
||||
import TmdbPersonCard from '@/components/cards/TmdbPersonCard.vue'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
apipath: String,
|
||||
params: Object as PropType<{ [key: string]: any }>,
|
||||
type: String,
|
||||
})
|
||||
|
||||
// 判断是否有滚动条
|
||||
@@ -29,8 +30,8 @@ const loading = ref(false)
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<TmdbPerson[]>([])
|
||||
const currData = ref<TmdbPerson[]>([])
|
||||
const dataList = ref<any>([])
|
||||
const currData = ref<any>([])
|
||||
|
||||
// 获取列表数据
|
||||
async function fetchData({ done }: { done: any }) {
|
||||
@@ -135,11 +136,22 @@ async function fetchData({ done }: { done: any }) {
|
||||
>
|
||||
<template #loading />
|
||||
<div
|
||||
v-if="dataList.length > 0"
|
||||
v-if="dataList.length > 0 && props.type === 'tmdb'"
|
||||
class="grid gap-4 grid-media-card mx-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<PersonCard
|
||||
<TmdbPersonCard
|
||||
v-for="data in dataList"
|
||||
:key="data.id"
|
||||
:person="data"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="dataList.length > 0 && props.type === 'douban'"
|
||||
class="grid gap-4 grid-media-card mx-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<DoubanPersonCard
|
||||
v-for="data in dataList"
|
||||
:key="data.id"
|
||||
:person="data"
|
||||
@@ -149,7 +161,7 @@ async function fetchData({ done }: { done: any }) {
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
error-title="没有数据"
|
||||
error-description="无法获取到TMDB媒体信息。"
|
||||
error-description="无法获取到媒体信息。"
|
||||
/>
|
||||
</VInfiniteScroll>
|
||||
</template>
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import PersionCard from '@/components/cards/PersonCard.vue'
|
||||
import TmdbPersonCard from '@/components/cards/TmdbPersonCard.vue'
|
||||
import api from '@/api'
|
||||
import type { TmdbPerson } from '@/api/types'
|
||||
import SlideView from '@/components/slide/SlideView.vue'
|
||||
import DoubanPersonCard from '@/components/cards/DoubanPersonCard.vue'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
apipath: String,
|
||||
linkurl: String,
|
||||
title: String,
|
||||
type: String,
|
||||
})
|
||||
|
||||
// 组件加载完成
|
||||
const componentLoaded = ref(false)
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<TmdbPerson[]>([])
|
||||
const dataList = ref<any>([])
|
||||
|
||||
// 获取订阅列表数据
|
||||
async function fetchData() {
|
||||
@@ -46,7 +47,14 @@ onMounted(fetchData)
|
||||
v-for="data in dataList"
|
||||
:key="data.id"
|
||||
>
|
||||
<PersionCard
|
||||
<TmdbPersonCard
|
||||
v-if="props.type === 'tmdb'"
|
||||
:person="data"
|
||||
height="15rem"
|
||||
width="10rem"
|
||||
/>
|
||||
<DoubanPersonCard
|
||||
v-if="props.type === 'douban'"
|
||||
:person="data"
|
||||
height="15rem"
|
||||
width="10rem"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import _ from 'lodash'
|
||||
import type { Context } from '@/api/types'
|
||||
import TorrentCard from '@/components/cards/TorrentCard.vue'
|
||||
import { useDefer } from '@/@core/utils/dom'
|
||||
|
||||
interface SearchTorrent extends Context {
|
||||
more?: Array<Context>
|
||||
@@ -48,7 +48,7 @@ const editionFilterOptions = ref<Array<string>>([])
|
||||
const resolutionFilterOptions = ref<Array<string>>([])
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref <Array<SearchTorrent>>([])
|
||||
const dataList = ref<Array<SearchTorrent>>([])
|
||||
|
||||
// 分组后的数据列表
|
||||
const groupedDataList = ref<Map<string, Context[]>>()
|
||||
@@ -69,7 +69,7 @@ function initOptions(data: Context) {
|
||||
}
|
||||
|
||||
// 计算分组后的列表
|
||||
watchEffect(() => {
|
||||
onMounted(() => {
|
||||
// 数据分组
|
||||
const groupMap = new Map<string, Context[]>()
|
||||
// 遍历数据
|
||||
@@ -80,7 +80,7 @@ watchEffect(() => {
|
||||
// group data
|
||||
const key = `${torrent_info.title}_${torrent_info.size}`
|
||||
if (groupMap.has(key)) {
|
||||
// 已存在相同标题和大小的分组,将当前上下文信息添加到分组中
|
||||
// 已入库相同标题和大小的分组,将当前上下文信息添加到分组中
|
||||
const group = groupMap.get(key)
|
||||
group?.push(item)
|
||||
}
|
||||
@@ -92,10 +92,12 @@ watchEffect(() => {
|
||||
groupedDataList.value = groupMap
|
||||
})
|
||||
|
||||
let defer = (_: number) => true
|
||||
|
||||
// 计算过滤后的列表
|
||||
watchEffect(() => {
|
||||
// 清空列表
|
||||
dataList.value.splice(0)
|
||||
dataList.value = []
|
||||
// 匹配过滤函数
|
||||
const match = (filter: Array<string>, value: string | undefined) =>
|
||||
filter.length === 0 || (value && filter.includes(value))
|
||||
@@ -126,10 +128,12 @@ watchEffect(() => {
|
||||
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
|
||||
if (matchData.length > 1)
|
||||
firstData.more = matchData.slice(1)
|
||||
|
||||
dataList.value.push(firstData)
|
||||
}
|
||||
}
|
||||
})
|
||||
defer = useDefer(dataList.value.length)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -216,12 +220,9 @@ watchEffect(() => {
|
||||
</VRow>
|
||||
</VCard>
|
||||
<div class="grid gap-3 grid-torrent-card items-start">
|
||||
<TorrentCard
|
||||
v-for="(item, index) in dataList"
|
||||
:key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`"
|
||||
:torrent="item"
|
||||
:more="item.more"
|
||||
/>
|
||||
<div v-for="(item, index) in dataList" :key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`">
|
||||
<TorrentCard v-if="defer(index)" :torrent="item" :more="item.more" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Context } from '@/api/types'
|
||||
import TorrentItem from '@/components/cards/TorrentItem.vue'
|
||||
import { useDefer } from '@/@core/utils/dom'
|
||||
|
||||
// 定义输入参数
|
||||
const props = defineProps({
|
||||
@@ -27,7 +28,7 @@ const filterForm = reactive({
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref <Array<Context>>([])
|
||||
const dataList = ref<Array<Context>>([])
|
||||
|
||||
// 获取站点过滤选项
|
||||
const siteFilterOptions = ref<Array<string>>([])
|
||||
@@ -59,10 +60,12 @@ function initOptions(data: Context) {
|
||||
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
|
||||
}
|
||||
|
||||
let defer = (_: number) => true
|
||||
|
||||
// 计算过滤后的列表
|
||||
watchEffect(() => {
|
||||
// 清空列表
|
||||
dataList.value.splice(0)
|
||||
dataList.value = []
|
||||
// 匹配过滤函数
|
||||
const match = (filter: Array<string>, value: string | undefined) =>
|
||||
filter.length === 0 || (value && filter.includes(value))
|
||||
@@ -72,21 +75,22 @@ watchEffect(() => {
|
||||
if (
|
||||
// 站点过滤
|
||||
match(filterForm.site, torrent_info.site_name)
|
||||
// 促销状态过滤
|
||||
&& match(filterForm.freeState, torrent_info.volume_factor)
|
||||
// 季过滤
|
||||
&& match(filterForm.season, meta_info.season_episode)
|
||||
// 制作组过滤
|
||||
&& match(filterForm.releaseGroup, meta_info.resource_team)
|
||||
// 视频编码过滤
|
||||
&& match(filterForm.videoCode, meta_info.video_encode)
|
||||
// 分辨率过滤
|
||||
&& match(filterForm.resolution, meta_info.resource_pix)
|
||||
// 质量过滤
|
||||
&& match(filterForm.edition, meta_info.edition)
|
||||
// 促销状态过滤
|
||||
&& match(filterForm.freeState, torrent_info.volume_factor)
|
||||
// 季过滤
|
||||
&& match(filterForm.season, meta_info.season_episode)
|
||||
// 制作组过滤
|
||||
&& match(filterForm.releaseGroup, meta_info.resource_team)
|
||||
// 视频编码过滤
|
||||
&& match(filterForm.videoCode, meta_info.video_encode)
|
||||
// 分辨率过滤
|
||||
&& match(filterForm.resolution, meta_info.resource_pix)
|
||||
// 质量过滤
|
||||
&& match(filterForm.edition, meta_info.edition)
|
||||
)
|
||||
dataList.value.push(data)
|
||||
})
|
||||
defer = useDefer(dataList.value.length)
|
||||
})
|
||||
|
||||
// 初始化过滤选项
|
||||
@@ -100,25 +104,18 @@ onMounted(() => {
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VList
|
||||
lines="three"
|
||||
class="rounded"
|
||||
>
|
||||
<TorrentItem
|
||||
v-for="(item, index) in dataList"
|
||||
:key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`"
|
||||
:torrent="item"
|
||||
/>
|
||||
<VListItem v-if="dataList.length === 0">
|
||||
<VList v-if="dataList.length === 0" lines="three" class="rounded">
|
||||
<VListItem>
|
||||
<VListItemTitle>没有附合当前过滤条件的资源。</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<div>
|
||||
<div v-for="(item, index) in dataList" :key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`">
|
||||
<TorrentItem v-if="defer(index)" :torrent="item" />
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<VListSubheader v-if="siteFilterOptions.length > 0">
|
||||
站点
|
||||
|
||||
@@ -19,9 +19,9 @@ const getInstalledPluginList = computed(() => {
|
||||
return dataList.value.filter(item => item.installed)
|
||||
})
|
||||
|
||||
// 获取未安装的插件列表
|
||||
// 获取未安装或者有更新的插件列表
|
||||
const getUninstalledPluginList = computed(() => {
|
||||
return dataList.value.filter(item => !item.installed)
|
||||
return dataList.value.filter(item => !item.installed || item.has_update)
|
||||
})
|
||||
|
||||
// 关闭插件市场窗口
|
||||
@@ -84,13 +84,14 @@ onBeforeMount(fetchData)
|
||||
<VDialog
|
||||
v-model="PluginAppDialog"
|
||||
fullscreen
|
||||
scrollable
|
||||
:scrim="false"
|
||||
transition="dialog-bottom-transition"
|
||||
>
|
||||
<!-- Dialog Activator -->
|
||||
<template #activator="{ props }">
|
||||
<VBtn
|
||||
icon="mdi-plus"
|
||||
icon="mdi-store-plus"
|
||||
v-bind="props"
|
||||
size="x-large"
|
||||
class="fixed right-5 bottom-5"
|
||||
@@ -119,7 +120,7 @@ onBeforeMount(fetchData)
|
||||
</VToolbarItems>
|
||||
</VToolbar>
|
||||
</div>
|
||||
<div class="pa-4">
|
||||
<VCardText>
|
||||
<div class="grid gap-4 grid-plugin-card">
|
||||
<PluginAppCard
|
||||
v-for="data in getUninstalledPluginList"
|
||||
@@ -134,7 +135,7 @@ onBeforeMount(fetchData)
|
||||
error-title="没有未安装插件"
|
||||
error-description="所有可用插件均已安装。"
|
||||
/>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -3,42 +3,79 @@ import api from '@/api'
|
||||
import FileBrowser from '@/components/FileBrowser.vue'
|
||||
|
||||
const endpoints = {
|
||||
list: { url: '/filebrowser/list?path={path}&sort={sort}', method: 'get' },
|
||||
mkdir: { url: '/filebrowser/mkdir?path={path}', method: 'get' },
|
||||
delete: { url: '/filebrowser/delete?path={path}', method: 'get' },
|
||||
download: { url: '/filebrowser/download?path={path}', method: 'get' },
|
||||
image: { url: '/filebrowser/image?path={path}', method: 'get' },
|
||||
rename: { url: '/filebrowser/rename?path={path}&new_name={newname}', method: 'get' },
|
||||
list: {
|
||||
url: '/filebrowser/list?path={path}&sort={sort}',
|
||||
method: 'get',
|
||||
},
|
||||
mkdir: {
|
||||
url: '/filebrowser/mkdir?path={path}',
|
||||
method: 'get',
|
||||
},
|
||||
delete: {
|
||||
url: '/filebrowser/delete?path={path}',
|
||||
method: 'get',
|
||||
},
|
||||
download: {
|
||||
url: '/filebrowser/download?path={path}',
|
||||
method: 'get',
|
||||
},
|
||||
image: {
|
||||
url: '/filebrowser/image?path={path}',
|
||||
method: 'get',
|
||||
},
|
||||
rename: {
|
||||
url: '/filebrowser/rename?path={path}&new_name={newname}',
|
||||
method: 'get',
|
||||
},
|
||||
}
|
||||
|
||||
// 读取下载目录
|
||||
const path = ref('/')
|
||||
const path: Ref<string | undefined> = ref()
|
||||
|
||||
// 调用API,加载当前系统环境设置
|
||||
async function loadSystemSettings() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
if (result.success)
|
||||
path.value = result.data?.DOWNLOAD_PATH || '/'
|
||||
if (path.value && !path.value.endsWith('/'))
|
||||
path.value += '/'
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
function loadSystemSettings(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
api
|
||||
.get('system/env')
|
||||
.then((result: any) => {
|
||||
let path = '/'
|
||||
if (result.success)
|
||||
path = result.data?.DOWNLOAD_PATH || '/'
|
||||
|
||||
if (!path.endsWith('/'))
|
||||
path += '/'
|
||||
|
||||
resolve(path)
|
||||
})
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function pathChanged(_path: string) {
|
||||
path.value = _path
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await loadSystemSettings()
|
||||
onMounted(() => {
|
||||
loadSystemSettings()
|
||||
.then((res) => {
|
||||
path.value = res
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
path.value = '/'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FileBrowser storages="local" :tree="false" :path="path" :endpoints="endpoints" :axios="api" @pathchanged="pathChanged" />
|
||||
<FileBrowser
|
||||
storages="local"
|
||||
:tree="false"
|
||||
:path="path"
|
||||
:endpoints="endpoints"
|
||||
:axios="api"
|
||||
@pathchanged="pathChanged"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -25,20 +25,46 @@ const selected = ref<TransferHistory[]>([])
|
||||
|
||||
// 表头
|
||||
const headers = [
|
||||
{ title: '标题', key: 'title', sortable: false },
|
||||
{ title: '目录', key: 'src', sortable: false },
|
||||
{ title: '转移方式', key: 'mode', sortable: false },
|
||||
{ title: '时间', key: 'date', sortable: false },
|
||||
{ title: '状态', key: 'status', sortable: false },
|
||||
{ title: '失败原因', key: 'errmsg', sortable: false },
|
||||
{ title: '', key: 'actions', sortable: false },
|
||||
{
|
||||
title: '标题',
|
||||
key: 'title',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: '目录',
|
||||
key: 'src',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: '转移方式',
|
||||
key: 'mode',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
key: 'date',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<TransferHistory[]>([])
|
||||
|
||||
// 搜索
|
||||
const search = ref('')
|
||||
const search = ref()
|
||||
|
||||
// 搜索提示词列表
|
||||
const searchHintList = ref<string[]>([])
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
@@ -47,7 +73,7 @@ const loading = ref(false)
|
||||
const totalItems = ref(0)
|
||||
|
||||
// 每页条数
|
||||
const itemsPerPage = ref(25)
|
||||
const itemsPerPage = ref(50)
|
||||
|
||||
// 当前页码
|
||||
const currentPage = ref(1)
|
||||
@@ -68,13 +94,7 @@ const deleteConfirmDialog = ref(false)
|
||||
const confirmTitle = ref('')
|
||||
|
||||
// 获取订阅列表数据
|
||||
async function fetchData({
|
||||
page,
|
||||
itemsPerPage,
|
||||
}: {
|
||||
page: number
|
||||
itemsPerPage: number
|
||||
}) {
|
||||
async function fetchData({ page, itemsPerPage }: { page: number; itemsPerPage: number }) {
|
||||
loading.value = true
|
||||
try {
|
||||
currentPage.value = page
|
||||
@@ -89,6 +109,9 @@ async function fetchData({
|
||||
|
||||
dataList.value = result.data.list
|
||||
totalItems.value = result.data.total
|
||||
searchHintList.value = ['失败', '成功', ...new Set(dataList.value.map(item => item.title || ''))].filter(
|
||||
title => title !== '',
|
||||
)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
@@ -106,11 +129,6 @@ function getIcon(type: string) {
|
||||
return 'mdi-help-circle'
|
||||
}
|
||||
|
||||
// 计算颜色
|
||||
function getStatusColor(status: boolean) {
|
||||
return status ? 'success' : 'error'
|
||||
}
|
||||
|
||||
// 转移方式字典
|
||||
const TransferDict: { [key: string]: string } = {
|
||||
copy: '复制',
|
||||
@@ -132,7 +150,9 @@ async function removeHistory(item: TransferHistory) {
|
||||
async function remove(item: TransferHistory, deleteSrc: boolean, deleteDest: boolean) {
|
||||
try {
|
||||
// 调用删除API
|
||||
const result: { [key: string]: any } = await api.delete(`history/transfer?deletesrc=${deleteSrc}&deletedest=${deleteDest}`, {
|
||||
const result: {
|
||||
[key: string]: any
|
||||
} = await api.delete(`history/transfer?deletesrc=${deleteSrc}&deletedest=${deleteDest}`, {
|
||||
data: item,
|
||||
})
|
||||
|
||||
@@ -150,6 +170,7 @@ async function removeSingle(deleteSrc: boolean, deleteDest: boolean) {
|
||||
deleteConfirmDialog.value = false
|
||||
if (!currentHistory.value)
|
||||
return
|
||||
|
||||
// 删除
|
||||
await remove(currentHistory.value, deleteSrc, deleteDest)
|
||||
// 刷新
|
||||
@@ -167,6 +188,7 @@ async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
|
||||
const total = selected.value.length
|
||||
if (total === 0)
|
||||
return
|
||||
|
||||
// 已处理条数
|
||||
let handled = 0
|
||||
// 显示进度条
|
||||
@@ -178,7 +200,7 @@ async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
|
||||
await remove(item, deleteSrc, deleteDest)
|
||||
// 删除完成
|
||||
handled++
|
||||
progressValue.value = handled / total * 100
|
||||
progressValue.value = (handled / total) * 100
|
||||
}
|
||||
// 清空选中项
|
||||
selected.value = []
|
||||
@@ -203,6 +225,7 @@ async function deleteConfirmHandler(deleteSrc: boolean, deleteDest: boolean) {
|
||||
async function removeHistoryBatch() {
|
||||
if (selected.value.length === 0)
|
||||
return
|
||||
|
||||
// 清空当前操作记录
|
||||
currentHistory.value = undefined
|
||||
confirmTitle.value = `确认删除 ${selected.value.length} 条记录 ?`
|
||||
@@ -210,19 +233,47 @@ async function removeHistoryBatch() {
|
||||
deleteConfirmDialog.value = true
|
||||
}
|
||||
|
||||
// 计算根路径
|
||||
function getRootPath(path: string, type: string, category: string) {
|
||||
if (!path)
|
||||
return ''
|
||||
|
||||
let index = -2
|
||||
if (type !== '电影')
|
||||
index = -3
|
||||
|
||||
if (category)
|
||||
index -= 1
|
||||
|
||||
if (path.includes('/'))
|
||||
return path.split('/').slice(0, index).join('/')
|
||||
else
|
||||
return path.split('\\').slice(0, index).join('\\')
|
||||
}
|
||||
|
||||
// 批量重新整理
|
||||
async function retransferBatch() {
|
||||
if (selected.value.length === 0)
|
||||
return
|
||||
|
||||
// 清空当前操作记录
|
||||
currentHistory.value = undefined
|
||||
// 重新整理IDS
|
||||
redoIds.value = selected.value.map(item => item.id)
|
||||
// 重新整理target
|
||||
if (selected.value.length === 1)
|
||||
redoTarget.value = selected.value[0].dest ?? ''
|
||||
else
|
||||
if (selected.value.length === 1) {
|
||||
// 目的目录
|
||||
const dest = selected.value[0].dest ?? ''
|
||||
// 类型
|
||||
const mediaType = selected.value[0].type ?? ''
|
||||
// 分类
|
||||
const category = selected.value[0].category ?? ''
|
||||
// 计算根路径
|
||||
redoTarget.value = getRootPath(dest, mediaType, category)
|
||||
}
|
||||
else {
|
||||
redoTarget.value = ''
|
||||
}
|
||||
// 打开识别弹窗
|
||||
redoDialog.value = true
|
||||
}
|
||||
@@ -236,7 +287,7 @@ const dropdownItems = ref([
|
||||
prependIcon: 'mdi-redo-variant',
|
||||
click: (item: TransferHistory) => {
|
||||
redoIds.value = [item.id]
|
||||
redoTarget.value = item.dest ?? ''
|
||||
redoTarget.value = getRootPath(item.dest ?? '', item.type ?? '', item.category ?? '')
|
||||
redoDialog.value = true
|
||||
},
|
||||
},
|
||||
@@ -260,20 +311,24 @@ const dropdownItems = ref([
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VRow>
|
||||
<VCol> 历史记录 </VCol>
|
||||
<VCol>
|
||||
<VTextField
|
||||
<VCol cols="4" md="6">
|
||||
历史记录
|
||||
</VCol>
|
||||
<VCol cols="8" md="6">
|
||||
<VCombobox
|
||||
key="search_navbar"
|
||||
v-model="search"
|
||||
:items="searchHintList"
|
||||
class="text-disabled"
|
||||
density="compact"
|
||||
label="搜索"
|
||||
label="搜索标题、状态"
|
||||
append-inner-icon="mdi-magnify"
|
||||
variant="solo-filled"
|
||||
single-line
|
||||
hide-details
|
||||
flat
|
||||
rounded
|
||||
clearable
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -297,58 +352,52 @@ const dropdownItems = ref([
|
||||
@update:options="fetchData"
|
||||
>
|
||||
<template #item.title="{ item }">
|
||||
<div class="d-flex">
|
||||
<VAvatar><VIcon :icon="getIcon(item.raw.type || '')" /></VAvatar>
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar>
|
||||
<VIcon :icon="getIcon(item.value.type || '')" />
|
||||
</VAvatar>
|
||||
<div class="d-flex flex-column ms-1">
|
||||
<span class="d-block whitespace-nowrap text-high-emphasis">
|
||||
{{ item.raw.title }} {{ item.raw.seasons }}{{ item.raw.episodes }}
|
||||
{{ item.value.title }} {{ item.value.seasons }}{{ item.value.episodes }}
|
||||
</span>
|
||||
<small>{{ item.raw.category }}</small>
|
||||
<small>{{ item.value.category }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #item.src="{ item }">
|
||||
<small>{{ item.raw.src }} <br>=> {{ item.raw.dest }}</small>
|
||||
<small>{{ item.value.src }} <br>=> {{ item.value.dest }}</small>
|
||||
</template>
|
||||
<template #item.mode="{ item }">
|
||||
<VChip
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="small"
|
||||
>
|
||||
{{
|
||||
TransferDict[item.raw.mode]
|
||||
}}
|
||||
<VChip variant="outlined" color="primary" size="small">
|
||||
{{ TransferDict[item.value.mode] }}
|
||||
</VChip>
|
||||
</template>
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
:color="getStatusColor(item.raw.status)"
|
||||
size="small"
|
||||
>
|
||||
{{ item.raw.status ? "成功" : "失败" }}
|
||||
<VChip v-if="item.value.status" color="success" size="small">
|
||||
成功
|
||||
</VChip>
|
||||
<v-tooltip v-else :text="item.value.errmsg">
|
||||
<template #activator="{ props }">
|
||||
<VChip v-bind="props" color="error" size="small">
|
||||
失败
|
||||
</VChip>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<template #item.date="{ item }">
|
||||
<small>{{ item.raw.date }}</small>
|
||||
</template>
|
||||
<template #item.errmsg="{ item }">
|
||||
<small class="text-error">{{ item.raw.errmsg }}</small>
|
||||
<small>{{ item.value.date }}</small>
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click="menu.props.click(item.raw)"
|
||||
@click="menu.props.click(item.value)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
@@ -365,23 +414,9 @@ const dropdownItems = ref([
|
||||
</VDataTableServer>
|
||||
</VCard>
|
||||
<!-- 底部操作按钮 -->
|
||||
<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"
|
||||
/>
|
||||
<VBtn
|
||||
icon="mdi-trash-can-outline"
|
||||
color="error"
|
||||
size="x-large"
|
||||
@click="removeHistoryBatch"
|
||||
/>
|
||||
<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" />
|
||||
<VBtn icon="mdi-trash-can-outline" color="error" size="x-large" @click="removeHistoryBatch" />
|
||||
</span>
|
||||
<!-- 底部弹窗 -->
|
||||
<VBottomSheet v-model="deleteConfirmDialog" inset>
|
||||
@@ -390,33 +425,17 @@ const dropdownItems = ref([
|
||||
<VCardTitle class="pe-10">
|
||||
{{ confirmTitle }}
|
||||
</VCardTitle>
|
||||
<div class="d-flex flex-column flex-lg-row justify-center my-3">
|
||||
<VBtn
|
||||
color="primary"
|
||||
class="mb-2 mx-2"
|
||||
@click="deleteConfirmHandler(false, false)"
|
||||
>
|
||||
<div class="d-flex flex-column flex-lg-row justify-center my-3">
|
||||
<VBtn color="primary" class="mb-2 mx-2" @click="deleteConfirmHandler(false, false)">
|
||||
仅删除历史记录
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="warning"
|
||||
class="mb-2 mx-2"
|
||||
@click="deleteConfirmHandler(true, false)"
|
||||
>
|
||||
<VBtn color="warning" class="mb-2 mx-2" @click="deleteConfirmHandler(true, false)">
|
||||
删除历史记录和源文件
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="info"
|
||||
class="mb-2 mx-2"
|
||||
@click="deleteConfirmHandler(false, true)"
|
||||
>
|
||||
<VBtn color="info" class="mb-2 mx-2" @click="deleteConfirmHandler(false, true)">
|
||||
删除历史记录和媒体库文件
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
class="mb-2 mx-2"
|
||||
@click="deleteConfirmHandler(true, true)"
|
||||
>
|
||||
<VBtn color="error" class="mb-2 mx-2" @click="deleteConfirmHandler(true, true)">
|
||||
删除历史记录、源文件和媒体库文件
|
||||
</VBtn>
|
||||
</div>
|
||||
@@ -427,17 +446,19 @@ const dropdownItems = ref([
|
||||
v-model="redoDialog"
|
||||
:logids="redoIds"
|
||||
:target="redoTarget"
|
||||
@done="() => {
|
||||
redoDialog = false
|
||||
// 清空当前操作记录
|
||||
currentHistory = undefined
|
||||
selected = []
|
||||
// 刷新
|
||||
fetchData({
|
||||
page: currentPage,
|
||||
itemsPerPage,
|
||||
})
|
||||
}"
|
||||
@done="
|
||||
() => {
|
||||
redoDialog = false
|
||||
// 清空当前操作记录
|
||||
currentHistory = undefined
|
||||
selected = []
|
||||
// 刷新
|
||||
fetchData({
|
||||
page: currentPage,
|
||||
itemsPerPage,
|
||||
})
|
||||
}
|
||||
"
|
||||
@close="redoDialog = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -84,7 +84,7 @@ onMounted(() => {
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">
|
||||
当前版本
|
||||
软件版本
|
||||
</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow flex flex-row items-center truncate">
|
||||
@@ -98,6 +98,30 @@ onMounted(() => {
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">
|
||||
认证资源版本
|
||||
</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow flex flex-row items-center truncate">
|
||||
<code class="truncate">{{ systemEnv.AUTH_VERSION }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">
|
||||
站点资源版本
|
||||
</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow flex flex-row items-center truncate">
|
||||
<code class="truncate">{{ systemEnv.INDEXER_VERSION }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">
|
||||
|
||||
@@ -147,6 +147,10 @@ async function deactivateUser(user: User) {
|
||||
|
||||
// 新增用户
|
||||
async function addUser() {
|
||||
if (!userForm.name || !userForm.password || !userForm.email) {
|
||||
$toast.error('请填写完整信息!')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('user', userForm)
|
||||
if (result.success) {
|
||||
@@ -447,6 +451,7 @@ onMounted(() => {
|
||||
>
|
||||
<VTextField
|
||||
v-model="userForm.email"
|
||||
:rules="[requiredValidator]"
|
||||
label="邮箱"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
||||
import type { Site } from '@/api/types'
|
||||
import { copyToClipboard } from '@/@core/utils/navigator'
|
||||
import ImportCodeForm from '@/components/form/ImportCodeForm.vue'
|
||||
|
||||
// 规则卡片类型
|
||||
interface FilterCard {
|
||||
@@ -30,6 +32,12 @@ const defaultFilterRules = ref({
|
||||
exclude: '',
|
||||
})
|
||||
|
||||
// 导入代码弹窗
|
||||
const importCodeDialog = ref(false)
|
||||
|
||||
// 导入的代码
|
||||
const importCodeString = ref('')
|
||||
|
||||
// 查询已设置优先级规则
|
||||
async function queryCustomFilters() {
|
||||
try {
|
||||
@@ -227,6 +235,49 @@ async function saveDefaultFilter() {
|
||||
}
|
||||
}
|
||||
|
||||
// 分享规则
|
||||
function shareRules() {
|
||||
// 有值才处理
|
||||
if (filterCards.value.length === 0)
|
||||
return
|
||||
|
||||
// 将卡片规则接装为字符串
|
||||
const value = filterCards.value
|
||||
.filter(card => card.rules.length > 0)
|
||||
.map(card => card.rules.join('&'))
|
||||
.join('>')
|
||||
|
||||
// 复制到剪贴板
|
||||
try {
|
||||
copyToClipboard(value)
|
||||
$toast.success('优先级规则已复制到剪贴板')
|
||||
}
|
||||
catch (error) {
|
||||
$toast.error('优先级规则复制失败!')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听导入代码变化
|
||||
watchEffect(() => {
|
||||
if (!importCodeString.value)
|
||||
return
|
||||
|
||||
// 导入代码需要以空格开头和结束,没有则拼接
|
||||
if (!importCodeString.value.startsWith(' '))
|
||||
importCodeString.value = ` ${importCodeString.value}`
|
||||
if (!importCodeString.value.endsWith(' '))
|
||||
importCodeString.value = `${importCodeString.value} `
|
||||
|
||||
// 将导入的代码转换为规则卡片
|
||||
const groups = importCodeString.value.split('>')
|
||||
filterCards.value = groups.map((group: string, index: number) => {
|
||||
return {
|
||||
pri: (index + 1).toString(),
|
||||
rules: group.split('&'),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
queryCustomFilters()
|
||||
querySites()
|
||||
@@ -264,6 +315,36 @@ onMounted(() => {
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="搜索优先级">
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="shareRules"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="importCodeDialog = true"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardSubtitle> 设置在搜索时默认使用的优先级排序,未在优先级中的资源将不在搜索结果中显示。 </VCardSubtitle>
|
||||
<VCardItem>
|
||||
<div class="grid gap-3 grid-filterrule-card">
|
||||
@@ -332,6 +413,17 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VDialog
|
||||
v-model="importCodeDialog"
|
||||
width="60rem"
|
||||
scrollable
|
||||
>
|
||||
<ImportCodeForm
|
||||
v-model="importCodeString"
|
||||
title="导入优先级规则"
|
||||
@close="importCodeDialog = false"
|
||||
/>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
||||
import type { Site } from '@/api/types'
|
||||
import { copyToClipboard } from '@/@core/utils/navigator'
|
||||
import ImportCodeForm from '@/components/form/ImportCodeForm.vue'
|
||||
|
||||
// 规则卡片类型
|
||||
interface FilterCard {
|
||||
@@ -27,12 +29,24 @@ const allSites = ref<Site[]>([])
|
||||
// 选中订阅站点
|
||||
const selectedRssSites = ref<number[]>([])
|
||||
|
||||
// 当前规则类型
|
||||
const currentRuleType = ref('SubscribeFilterRules')
|
||||
|
||||
// 包含与排除规则
|
||||
const defaultFilterRules = ref({
|
||||
include: '',
|
||||
exclude: '',
|
||||
movie_size: '',
|
||||
tv_size: '',
|
||||
show_edit_dialog: false,
|
||||
})
|
||||
|
||||
// 导入代码弹窗
|
||||
const importCodeDialog = ref(false)
|
||||
|
||||
// 导入的代码
|
||||
const importCodeString = ref('')
|
||||
|
||||
// 查询用户选中的订阅站点
|
||||
async function querySelectedRssSites() {
|
||||
try {
|
||||
@@ -244,6 +258,66 @@ async function saveDefaultFilter() {
|
||||
}
|
||||
}
|
||||
|
||||
// 分享规则
|
||||
function shareRules(ruleType: string) {
|
||||
let filterCards: Ref<FilterCard[]>
|
||||
if (ruleType === 'SubscribeFilterRules')
|
||||
filterCards = subscribeFilterCards
|
||||
else
|
||||
filterCards = bestVersionFilterCards
|
||||
// 有值才处理
|
||||
if (filterCards.value.length === 0)
|
||||
return
|
||||
|
||||
// 将卡片规则接装为字符串
|
||||
const value = filterCards.value
|
||||
.filter(card => card.rules.length > 0)
|
||||
.map(card => card.rules.join('&'))
|
||||
.join('>')
|
||||
|
||||
// 复制到剪贴板
|
||||
try {
|
||||
copyToClipboard(value)
|
||||
$toast.success('优先级规则已复制到剪贴板')
|
||||
}
|
||||
catch (error) {
|
||||
$toast.error('优先级规则复制失败!')
|
||||
}
|
||||
}
|
||||
|
||||
// 导入规则
|
||||
async function importRules(ruleType: string) {
|
||||
currentRuleType.value = ruleType
|
||||
importCodeString.value = ''
|
||||
importCodeDialog.value = true
|
||||
}
|
||||
|
||||
// 监听导入代码变化
|
||||
watchEffect(() => {
|
||||
if (!importCodeString.value)
|
||||
return
|
||||
if (!currentRuleType.value)
|
||||
return
|
||||
// 导入代码需要以空格开头和结束,没有则拼接
|
||||
if (!importCodeString.value.startsWith(' '))
|
||||
importCodeString.value = ` ${importCodeString.value}`
|
||||
if (!importCodeString.value.endsWith(' '))
|
||||
importCodeString.value = `${importCodeString.value} `
|
||||
let filterCards: Ref<FilterCard[]>
|
||||
if (currentRuleType.value === 'SubscribeFilterRules')
|
||||
filterCards = subscribeFilterCards
|
||||
else
|
||||
filterCards = bestVersionFilterCards
|
||||
// 将导入的代码转换为规则卡片
|
||||
const groups = importCodeString.value.split('>')
|
||||
filterCards.value = groups.map((group: string, index: number) => {
|
||||
return {
|
||||
pri: (index + 1).toString(),
|
||||
rules: group.split('&'),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
querySites()
|
||||
queryCustomFilters('SubscribeFilterRules')
|
||||
@@ -282,6 +356,36 @@ onMounted(() => {
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="订阅优先级">
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="shareRules('SubscribeFilterRules')"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="importRules('SubscribeFilterRules')"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardSubtitle> 设置在正常订阅时默认使用的优先级,未在优先级中的资源将不会自动下载。 </VCardSubtitle>
|
||||
<VCardItem>
|
||||
<div class="grid gap-3 grid-filterrule-card">
|
||||
@@ -318,6 +422,36 @@ onMounted(() => {
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="洗版优先级">
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="shareRules('BestVersionFilterRules')"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-share" />
|
||||
</template>
|
||||
<VListItemTitle>分享</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="importRules('BestVersionFilterRules')"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-import" />
|
||||
</template>
|
||||
<VListItemTitle>导入</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCardSubtitle> 设置在订阅洗版时使用的优先级,匹配优先级1时洗版完成。 </VCardSubtitle>
|
||||
<VCardItem>
|
||||
<div class="grid gap-3 grid-filterrule-card">
|
||||
@@ -372,6 +506,28 @@ onMounted(() => {
|
||||
label="排除(关键字、正则式)"
|
||||
/>
|
||||
</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>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
@@ -386,6 +542,17 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VDialog
|
||||
v-model="importCodeDialog"
|
||||
width="60rem"
|
||||
scrollable
|
||||
>
|
||||
<ImportCodeForm
|
||||
v-model="importCodeString"
|
||||
title="导入优先级规则"
|
||||
@close="importCodeDialog = false"
|
||||
/>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -173,7 +173,7 @@ onMounted(() => {
|
||||
<VAlert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
title="支持的配置格式:"
|
||||
title="支持的配置格式(注意空格):"
|
||||
>
|
||||
<span
|
||||
v-html="`
|
||||
@@ -181,7 +181,7 @@ onMounted(() => {
|
||||
被替换词 => 替换词<br>
|
||||
前定位词 <> 后定位词 >> 集偏移量(EP)<br>
|
||||
被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量(EP)<br>
|
||||
其中替换词支持格式:{[tmdbid=xxx;type=movie/tv;s=xxx;e=xxx]} 直接指定TMDBID识别,其中s、e为季数和集数(可选)<br>
|
||||
其中替换词支持格式:{[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]} 直接指定TMDBID/豆瓣ID识别,其中s、e为季数和集数(可选)<br>
|
||||
`"
|
||||
/>
|
||||
</VAlert>
|
||||
|
||||