Compare commits
140 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
9840782ce5 | ||
|
|
d18f42cd6f | ||
|
|
9372e98459 | ||
|
|
9400f4660d | ||
|
|
f0d66b8fba | ||
|
|
78abe72815 | ||
|
|
1ce75916ef | ||
|
|
46959d4baa | ||
|
|
b24cc44493 | ||
|
|
46f6c29e1d | ||
|
|
5ad75b8420 | ||
|
|
2030459f20 | ||
|
|
2855bf812b | ||
|
|
69989893d9 | ||
|
|
ffc61f4a31 | ||
|
|
dd051f28d2 | ||
|
|
a3d2def72b | ||
|
|
e8552b4385 | ||
|
|
d73e4853a8 | ||
|
|
7f991da183 | ||
|
|
046d96a012 | ||
|
|
9ee6ca43e3 | ||
|
|
43b1f7e620 | ||
|
|
ba76f79d85 | ||
|
|
ce47afa698 | ||
|
|
6da110948c | ||
|
|
533c564db5 | ||
|
|
4a65056909 | ||
|
|
c52ad73101 | ||
|
|
5a3673efc6 | ||
|
|
c03ec1d741 | ||
|
|
e62d0809b3 | ||
|
|
7f13597517 | ||
|
|
c822f1fffd | ||
|
|
14ca74a29d | ||
|
|
3ee897a350 | ||
|
|
789aac60c9 | ||
|
|
2c73a8f3e1 | ||
|
|
539bc656f8 | ||
|
|
feda0cad2d | ||
|
|
c723d89739 | ||
|
|
0a0e7a059a | ||
|
|
0263fbbee6 | ||
|
|
e205296e22 | ||
|
|
261f5a9c68 | ||
|
|
fa097651f4 | ||
|
|
c94d5f7e7d | ||
|
|
e34f18799f | ||
|
|
1681a311f7 | ||
|
|
da08d8ec19 | ||
|
|
730178c838 | ||
|
|
a04450ae98 | ||
|
|
2b2fd66a29 | ||
|
|
58fe08ad3d | ||
|
|
240d6bede0 | ||
|
|
23d808f8b1 | ||
|
|
2f293706cb | ||
|
|
9aaaf0c520 | ||
|
|
6694e7e929 | ||
|
|
d3768cb994 | ||
|
|
c59d3e28b9 | ||
|
|
914239f434 | ||
|
|
7a5d04dc53 | ||
|
|
110fe39e72 | ||
|
|
9689a86151 | ||
|
|
6462ae5956 | ||
|
|
053963d050 | ||
|
|
8a95549118 | ||
|
|
46e8fa551c | ||
|
|
be2034d75b | ||
|
|
634fa58048 | ||
|
|
cd5c093557 | ||
|
|
76cf86385e | ||
|
|
5c5ed5d7ee | ||
|
|
47e7a37667 | ||
|
|
d642ab42be | ||
|
|
b4de1c99d5 | ||
|
|
53e35eb9ff | ||
|
|
b222098ec5 | ||
|
|
bb8cf7ed78 | ||
|
|
0219ce3a9c | ||
|
|
b82e5d7cba |
9
.github/workflows/build.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Build moviepilot frontend
|
name: Build Moviepilot-Frontend
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -27,6 +27,13 @@ jobs:
|
|||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
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
|
- name: Build frontend
|
||||||
id: build_frontend
|
id: build_frontend
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -32,3 +32,4 @@ dist-ssr
|
|||||||
|
|
||||||
# iconify dist files
|
# iconify dist files
|
||||||
src/@iconify/*.js
|
src/@iconify/*.js
|
||||||
|
public/plugin_icon/**
|
||||||
|
|||||||
32
README.md
@@ -1,35 +1,39 @@
|
|||||||
# MoviePilot-Frontend
|
# 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
|
```sh
|
||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compile and Hot-Reload for Development
|
### 开发运行
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Type-Check, Compile and Minify for Production
|
### 编译打包
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn build
|
yarn build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 静态运行
|
||||||
|
|
||||||
|
1. 使用 `nginx` 等Web服务器托管 `dist` 静态文件,nginx配置参考 `public/nginx.conf`。
|
||||||
|
|
||||||
|
2. 使用 `node` 命令直接运行`service.js`,默认监听 `3000` 端口,设置环境变量 `NGINX_PORT` 来调整运行端口。
|
||||||
|
|
||||||
|
```shell
|
||||||
|
node dist/service.js
|
||||||
|
```
|
||||||
|
|||||||
14
package.json
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "moviepilot",
|
"name": "moviepilot",
|
||||||
"version": "1.2.7-2",
|
"version": "1.5.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"bin": "dist/service.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
@@ -9,7 +10,13 @@
|
|||||||
"typecheck": "vue-tsc --noEmit",
|
"typecheck": "vue-tsc --noEmit",
|
||||||
"lint": "eslint . -c .eslintrc.js --fix --ext .ts,.js,.vue,.tsx,.jsx",
|
"lint": "eslint . -c .eslintrc.js --fix --ext .ts,.js,.vue,.tsx,.jsx",
|
||||||
"build:icons": "tsc -b src/@iconify && node src/@iconify/build-icons.js",
|
"build:icons": "tsc -b src/@iconify && node src/@iconify/build-icons.js",
|
||||||
"postinstall": "npm run build:icons"
|
"postinstall": "npm run build:icons",
|
||||||
|
"pkg": "pkg . -t node18-win-x64 -o MoviePilot-Frontend.exe"
|
||||||
|
},
|
||||||
|
"pkg": {
|
||||||
|
"assets": [
|
||||||
|
"dist/**/*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@casl/ability": "^6.2.0",
|
"@casl/ability": "^6.2.0",
|
||||||
@@ -21,6 +28,9 @@
|
|||||||
"axios": "1.4.0",
|
"axios": "1.4.0",
|
||||||
"axios-mock-adapter": "^1.21.4",
|
"axios-mock-adapter": "^1.21.4",
|
||||||
"chart.js": "^4.1.2",
|
"chart.js": "^4.1.2",
|
||||||
|
"colorthief": "^2.4.0",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"express-http-proxy": "^2.0.0",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"postcss-purgecss": "^5.0.0",
|
"postcss-purgecss": "^5.0.0",
|
||||||
|
|||||||
86
public/nginx.conf
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
http {
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
|
||||||
|
keepalive_timeout 3600;
|
||||||
|
|
||||||
|
server {
|
||||||
|
|
||||||
|
include mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
listen 3000;
|
||||||
|
listen [::]:3000;
|
||||||
|
server_name moviepilot;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# 主目录
|
||||||
|
expires off;
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
root html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /assets {
|
||||||
|
# 静态资源
|
||||||
|
expires 7d;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
root html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/api/v1/system/(message|progress/) {
|
||||||
|
# SSE MIME类型设置
|
||||||
|
default_type text/event-stream;
|
||||||
|
|
||||||
|
# 禁用缓存
|
||||||
|
add_header Cache-Control no-cache;
|
||||||
|
add_header X-Accel-Buffering no;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
|
||||||
|
# 代理设置
|
||||||
|
proxy_pass http://backend_api;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
# 超时设置
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
# 后端API
|
||||||
|
proxy_pass http://backend_api;
|
||||||
|
rewrite ^.+mock-server/?(.*)$ /$1 break;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Nginx-Proxy true;
|
||||||
|
|
||||||
|
# 超时设置
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream backend_api {
|
||||||
|
# 后端API的地址和端口
|
||||||
|
server 127.0.0.1:3001;
|
||||||
|
# 可以添加更多后端服务器作为负载均衡
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
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: 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: 10 KiB |
|
Before Width: | Height: | Size: 40 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: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB |
36
public/service.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
const path = require('node:path')
|
||||||
|
const express = require('express')
|
||||||
|
const proxy = require('express-http-proxy')
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
const port = process.env.NGINX_PORT || 3000
|
||||||
|
|
||||||
|
// 后端 API 地址
|
||||||
|
const proxyConfig = {
|
||||||
|
URL: '127.0.0.1',
|
||||||
|
PORT: process.env.PORT || 3001
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静态文件服务目录
|
||||||
|
app.use(express.static(__dirname))
|
||||||
|
|
||||||
|
// 配置代理中间件将请求转发给后端API
|
||||||
|
app.use(
|
||||||
|
'/api',
|
||||||
|
proxy(`${proxyConfig.URL}:${proxyConfig.PORT}`, {
|
||||||
|
// 路径加上 /api 前缀
|
||||||
|
proxyReqPathResolver: (req) => {
|
||||||
|
return `/api${req.url}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// 处理根路径的请求
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'index.html')) // 指向你的前端入口文件
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server is running on port ${port}`)
|
||||||
|
})
|
||||||
@@ -55,7 +55,7 @@ export function formatFileSize(bytes: number) {
|
|||||||
if (bytes < 0)
|
if (bytes < 0)
|
||||||
throw new Error('字节数不能为负数。')
|
throw new Error('字节数不能为负数。')
|
||||||
|
|
||||||
const units = ['B', 'K', 'M', 'G', 'T']
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
let size = bytes
|
let size = bytes
|
||||||
let unitIndex = 0
|
let unitIndex = 0
|
||||||
|
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,15 @@ export interface Subscribe {
|
|||||||
// 排除
|
// 排除
|
||||||
exclude?: string
|
exclude?: string
|
||||||
|
|
||||||
|
// 质量
|
||||||
|
quality?: string
|
||||||
|
|
||||||
|
// 分辨率
|
||||||
|
resolution?: string
|
||||||
|
|
||||||
|
// 特效
|
||||||
|
effect?: string
|
||||||
|
|
||||||
// 总集数
|
// 总集数
|
||||||
total_episode?: number
|
total_episode?: number
|
||||||
|
|
||||||
@@ -68,8 +77,8 @@ export interface Subscribe {
|
|||||||
// 订阅站点
|
// 订阅站点
|
||||||
sites: number[]
|
sites: number[]
|
||||||
|
|
||||||
// 是否洗版
|
// 是否洗版,数字或者boolean
|
||||||
best_version: number
|
best_version: any
|
||||||
|
|
||||||
// 当前优先级
|
// 当前优先级
|
||||||
current_priority: number
|
current_priority: number
|
||||||
@@ -87,7 +96,7 @@ export interface TransferHistory {
|
|||||||
// 目的目录
|
// 目的目录
|
||||||
dest?: string
|
dest?: string
|
||||||
|
|
||||||
// 转移模式link/copy/move/softlink
|
// 转移模式link/copy/move/softlink/rclone_copy/rclone_move
|
||||||
mode?: string
|
mode?: string
|
||||||
|
|
||||||
// 类型:电影、电视剧
|
// 类型:电影、电视剧
|
||||||
@@ -328,7 +337,7 @@ export interface TmdbEpisode {
|
|||||||
guest_stars: Object[]
|
guest_stars: Object[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// TMDB人特信息
|
// TMDB人物信息
|
||||||
export interface TmdbPerson {
|
export interface TmdbPerson {
|
||||||
// ID
|
// ID
|
||||||
id?: number
|
id?: number
|
||||||
@@ -379,6 +388,34 @@ export interface TmdbPerson {
|
|||||||
biography?: string
|
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 {
|
export interface Site {
|
||||||
|
|
||||||
@@ -407,13 +444,13 @@ export interface Site {
|
|||||||
ua?: string
|
ua?: string
|
||||||
|
|
||||||
// 是否使用代理
|
// 是否使用代理
|
||||||
proxy?: number
|
proxy?: any
|
||||||
|
|
||||||
// 过滤规则
|
// 过滤规则
|
||||||
filter?: string
|
filter?: string
|
||||||
|
|
||||||
// 是否演染
|
// 是否演染
|
||||||
render?: number
|
render?: any
|
||||||
|
|
||||||
// 是否公开站点
|
// 是否公开站点
|
||||||
public?: number
|
public?: number
|
||||||
@@ -469,6 +506,9 @@ export interface DownloadingInfo {
|
|||||||
|
|
||||||
// 媒体信息
|
// 媒体信息
|
||||||
media: { [key: string]: any }
|
media: { [key: string]: any }
|
||||||
|
|
||||||
|
// 下载用户
|
||||||
|
userid?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缺失剧集信息
|
// 缺失剧集信息
|
||||||
@@ -500,9 +540,6 @@ export interface Plugin {
|
|||||||
// 插件图标
|
// 插件图标
|
||||||
plugin_icon?: string
|
plugin_icon?: string
|
||||||
|
|
||||||
// 主题色
|
|
||||||
plugin_color?: string
|
|
||||||
|
|
||||||
// 插件版本
|
// 插件版本
|
||||||
plugin_version?: string
|
plugin_version?: string
|
||||||
|
|
||||||
@@ -529,6 +566,15 @@ export interface Plugin {
|
|||||||
|
|
||||||
// 是否有详情页面
|
// 是否有详情页面
|
||||||
has_page?: boolean
|
has_page?: boolean
|
||||||
|
|
||||||
|
// 是否有新版本
|
||||||
|
has_update?: boolean
|
||||||
|
|
||||||
|
// 是否本地插件
|
||||||
|
is_local?: boolean
|
||||||
|
|
||||||
|
// 插件仓库地址
|
||||||
|
repo_url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 种子信息
|
// 种子信息
|
||||||
@@ -613,6 +659,9 @@ export interface MetaInfo {
|
|||||||
// 原字符串
|
// 原字符串
|
||||||
org_string?: string
|
org_string?: string
|
||||||
|
|
||||||
|
// 原标题(未经识别词转换)
|
||||||
|
title?: string
|
||||||
|
|
||||||
// 副标题
|
// 副标题
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
|
|
||||||
@@ -714,6 +763,9 @@ export interface MetaInfo {
|
|||||||
|
|
||||||
// 资源类型+特效
|
// 资源类型+特效
|
||||||
edition: string
|
edition: string
|
||||||
|
|
||||||
|
// 应用的自定义识别词
|
||||||
|
apply_words: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上下文信息
|
// 上下文信息
|
||||||
|
|||||||
BIN
src/assets/images/logos/plugin.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
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>
|
||||||
@@ -17,7 +17,7 @@ function getPercentage() {
|
|||||||
|
|
||||||
// 速度
|
// 速度
|
||||||
function getSpeedText() {
|
function getSpeedText() {
|
||||||
return `↑ ${props.info?.upspeed}/s ↓ ${props.info?.dlspeed}/s`
|
return `↑ ${props.info?.upspeed}/s ↓ ${props.info?.dlspeed}/s ${props.info?.left_time}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载状态
|
// 下载状态
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ function filtersChanged(value: string[]) {
|
|||||||
const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
||||||
{ title: '特效字幕', value: ' SPECSUB ' },
|
{ title: '特效字幕', value: ' SPECSUB ' },
|
||||||
{ title: '中文字幕', value: ' CNSUB ' },
|
{ title: '中文字幕', value: ' CNSUB ' },
|
||||||
|
{ title: '国语配音', value: ' CNVOI ' },
|
||||||
|
{ title: '排除: 国语配音', value: ' !CNVOI ' },
|
||||||
|
{ title: '粤语配音', value: ' HKVOI ' },
|
||||||
|
{ title: '排除: 粤语配音', value: ' !HKVOI ' },
|
||||||
|
{ title: '促销: 免费', value: ' FREE ' },
|
||||||
{ title: '分辨率: 4K', value: ' 4K ' },
|
{ title: '分辨率: 4K', value: ' 4K ' },
|
||||||
{ title: '分辨率: 1080P', value: ' 1080P ' },
|
{ title: '分辨率: 1080P', value: ' 1080P ' },
|
||||||
{ title: '分辨率: 720P', value: ' 720P ' },
|
{ title: '分辨率: 720P', value: ' 720P ' },
|
||||||
@@ -49,17 +54,22 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
|||||||
{ title: '排除: REMUX', value: ' !REMUX ' },
|
{ title: '排除: REMUX', value: ' !REMUX ' },
|
||||||
{ title: '质量: WEB-DL', value: ' WEBDL ' },
|
{ title: '质量: WEB-DL', value: ' WEBDL ' },
|
||||||
{ title: '排除: WEB-DL', value: ' !WEBDL ' },
|
{ title: '排除: WEB-DL', value: ' !WEBDL ' },
|
||||||
|
{ title: '质量: 60fps', value: ' 60FPS ' },
|
||||||
|
{ title: '排除: 60fps', value: ' !60FPS ' },
|
||||||
{ title: '编码: H265', value: ' H265 ' },
|
{ title: '编码: H265', value: ' H265 ' },
|
||||||
{ title: '排除: H265', value: ' !H265 ' },
|
{ title: '排除: H265', value: ' !H265 ' },
|
||||||
{ title: '编码: H264', value: ' H264 ' },
|
{ title: '编码: H264', value: ' H264 ' },
|
||||||
{ title: '排除: H264', value: ' !H264 ' },
|
{ title: '排除: H264', value: ' !H264 ' },
|
||||||
{ title: '效果: 杜比视界', value: ' DOLBY ' },
|
{ title: '效果: 杜比视界', value: ' DOLBY ' },
|
||||||
{ title: '排除: 杜比视界', value: ' !DOLBY ' },
|
{ title: '排除: 杜比视界', value: ' !DOLBY ' },
|
||||||
|
{ title: '效果: 杜比全景声', value: ' ATMOS ' },
|
||||||
|
{ title: '排除: 杜比全景声', value: ' !ATMOS ' },
|
||||||
{ title: '效果: HDR', value: ' HDR ' },
|
{ title: '效果: HDR', value: ' HDR ' },
|
||||||
{ title: '排除: HDR', value: ' !HDR ' },
|
{ title: '排除: HDR', value: ' !HDR ' },
|
||||||
{ title: '国语配音', value: ' CNVOI ' },
|
{ title: '效果: SDR', value: ' SDR ' },
|
||||||
{ title: '排除: 国语配音', value: ' !CNVOI ' },
|
{ title: '排除: SDR', value: ' !SDR ' },
|
||||||
{ title: '促销: 免费', value: ' FREE ' },
|
{ title: '效果: 3D', value: ' 3D ' },
|
||||||
|
{ title: '排除: 3D', value: ' !3D ' },
|
||||||
])
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { PropType, Ref } from 'vue'
|
import type { PropType, Ref } from 'vue'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
|
||||||
import { formatSeason } from '@/@core/utils/formatters'
|
import { formatSeason } from '@/@core/utils/formatters'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||||
@@ -33,12 +34,18 @@ const isSubscribed = ref(false)
|
|||||||
// 本地存在状态
|
// 本地存在状态
|
||||||
const isExists = ref(false)
|
const isExists = ref(false)
|
||||||
|
|
||||||
// 各季缺失状态:0-已存在 1-部分缺失 2-全部缺失,没有数据也是已存在
|
// 各季缺失状态:0-已入库 1-部分缺失 2-全部缺失,没有数据也是已入库
|
||||||
const seasonsNotExisted = ref<{ [key: number]: number }>({})
|
const seasonsNotExisted = ref<{ [key: number]: number }>({})
|
||||||
|
|
||||||
// 订阅季弹窗
|
// 订阅季弹窗
|
||||||
const subscribeSeasonDialog = ref(false)
|
const subscribeSeasonDialog = ref(false)
|
||||||
|
|
||||||
|
// 订阅编辑弹窗
|
||||||
|
const subscribeEditDialog = ref(false)
|
||||||
|
|
||||||
|
// 订阅ID
|
||||||
|
const subscribeId = ref<number>()
|
||||||
|
|
||||||
// 季详情
|
// 季详情
|
||||||
const seasonInfos = ref<TmdbSeason[]>([])
|
const seasonInfos = ref<TmdbSeason[]>([])
|
||||||
|
|
||||||
@@ -86,6 +93,7 @@ async function handleAddSubscribe() {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// 弹出季选择列表,支持多选
|
// 弹出季选择列表,支持多选
|
||||||
|
seasonsSelected.value = []
|
||||||
subscribeSeasonDialog.value = true
|
subscribeSeasonDialog.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,6 +144,12 @@ async function addSubscribe(season = 0) {
|
|||||||
result.message,
|
result.message,
|
||||||
best_version,
|
best_version,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 弹出订阅编辑弹窗
|
||||||
|
if (result.success && seasonsSelected.value.length <= 1) {
|
||||||
|
subscribeId.value = result.data.id
|
||||||
|
subscribeEditDialog.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -156,9 +170,9 @@ function showSubscribeAddToast(result: boolean,
|
|||||||
if (best_version > 0)
|
if (best_version > 0)
|
||||||
subname = '洗版订阅'
|
subname = '洗版订阅'
|
||||||
|
|
||||||
if (result)
|
if (result && seasonsSelected.value.length > 1)
|
||||||
$toast.success(`${title} 添加${subname}成功!`)
|
$toast.success(`${title} 添加${subname}成功!`)
|
||||||
else
|
else if (!result)
|
||||||
$toast.error(`${title} 添加${subname}失败:${message}!`)
|
$toast.error(`${title} 添加${subname}失败:${message}!`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +220,7 @@ async function handleCheckSubscribe() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询当前媒体是否已存在
|
// 查询当前媒体是否已入库
|
||||||
async function handleCheckExists() {
|
async function handleCheckExists() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('media/exists', {
|
const result: { [key: string]: any } = await api.get('media/exists', {
|
||||||
@@ -237,6 +251,7 @@ async function checkSubscribe(season = 0) {
|
|||||||
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
||||||
params: {
|
params: {
|
||||||
season,
|
season,
|
||||||
|
title: props.media?.title,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -257,7 +272,7 @@ async function checkSeasonsNotExists() {
|
|||||||
const result: NotExistMediaInfo[] = await api.post('download/notexists', props.media)
|
const result: NotExistMediaInfo[] = await api.post('download/notexists', props.media)
|
||||||
if (result) {
|
if (result) {
|
||||||
result.forEach((item) => {
|
result.forEach((item) => {
|
||||||
// 0-已存在 1-部分缺失 2-全部缺失
|
// 0-已入库 1-部分缺失 2-全部缺失
|
||||||
let state = 0
|
let state = 0
|
||||||
if (item.episodes.length === 0)
|
if (item.episodes.length === 0)
|
||||||
state = 2
|
state = 2
|
||||||
@@ -313,14 +328,14 @@ function getExistColor(season: number) {
|
|||||||
function getExistText(season: number) {
|
function getExistText(season: number) {
|
||||||
const state = seasonsNotExisted.value[season]
|
const state = seasonsNotExisted.value[season]
|
||||||
if (!state)
|
if (!state)
|
||||||
return '已存在'
|
return '已入库'
|
||||||
|
|
||||||
if (state === 1)
|
if (state === 1)
|
||||||
return '部分缺失'
|
return '部分缺失'
|
||||||
else if (state === 2)
|
else if (state === 2)
|
||||||
return '缺失'
|
return '缺失'
|
||||||
else
|
else
|
||||||
return '已存在'
|
return '已入库'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开详情页
|
// 打开详情页
|
||||||
@@ -480,7 +495,7 @@ function getYear(airDate: string) {
|
|||||||
inset
|
inset
|
||||||
scrollable
|
scrollable
|
||||||
>
|
>
|
||||||
<VCard>
|
<VCard class="rounded-t">
|
||||||
<DialogCloseBtn @click="subscribeSeasonDialog = false" />
|
<DialogCloseBtn @click="subscribeSeasonDialog = false" />
|
||||||
<VCardTitle class="pe-10">
|
<VCardTitle class="pe-10">
|
||||||
订阅 - {{ props.media?.title }}
|
订阅 - {{ props.media?.title }}
|
||||||
@@ -557,6 +572,14 @@ function getYear(airDate: string) {
|
|||||||
</div>
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VBottomSheet>
|
</VBottomSheet>
|
||||||
|
<!-- 订阅编辑弹窗 -->
|
||||||
|
<SubscribeEditForm
|
||||||
|
v-model="subscribeEditDialog"
|
||||||
|
:subid="subscribeId"
|
||||||
|
@close="subscribeEditDialog = false"
|
||||||
|
@save="subscribeEditDialog = false"
|
||||||
|
@remove="() => { subscribeEditDialog = false; handleCheckSubscribe(); }"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -143,5 +143,33 @@ function openTmdbPage(type: string, tmdbId: number) {
|
|||||||
识别失败,无法识别到有效信息!
|
识别失败,无法识别到有效信息!
|
||||||
</VAlert>
|
</VAlert>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VExpansionPanels
|
||||||
|
v-show="context?.meta_info?.title !== context?.meta_info.org_string"
|
||||||
|
>
|
||||||
|
<VExpansionPanel>
|
||||||
|
<VExpansionPanelTitle>
|
||||||
|
识别词应用详情
|
||||||
|
</VExpansionPanelTitle>
|
||||||
|
<VExpansionPanelText>
|
||||||
|
<VChip
|
||||||
|
variant="elevated"
|
||||||
|
class="me-1 mb-1 break-all"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{{ context?.meta_info.org_string }}
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-for="(val, key) in context?.meta_info.apply_words"
|
||||||
|
:key="key"
|
||||||
|
:val="val"
|
||||||
|
variant="outlined"
|
||||||
|
color="info"
|
||||||
|
class="me-1 mb-1 break-all"
|
||||||
|
>
|
||||||
|
{{ val }}
|
||||||
|
</VChip>
|
||||||
|
</VExpansionPanelText>
|
||||||
|
</VExpansionPanel>
|
||||||
|
</VExpansionPanels>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Plugin } from '@/api/types'
|
import type { Plugin } from '@/api/types'
|
||||||
|
import noImage from '@images/logos/plugin.png'
|
||||||
|
import { getDominantColor } from '@/@core/utils/image'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -13,19 +15,55 @@ const props = defineProps({
|
|||||||
// 定义触发的自定义事件
|
// 定义触发的自定义事件
|
||||||
const emit = defineEmits(['install'])
|
const emit = defineEmits(['install'])
|
||||||
|
|
||||||
|
// 背景颜色
|
||||||
|
const backgroundColor = ref('#28A9E1')
|
||||||
|
|
||||||
|
// 图片对象
|
||||||
|
const imageRef = ref<any>()
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 进度框
|
||||||
|
const progressDialog = ref(false)
|
||||||
|
|
||||||
|
// 进度框文本
|
||||||
|
const progressText = ref('正在安装插件...')
|
||||||
|
|
||||||
// 图片是否加载完成
|
// 图片是否加载完成
|
||||||
const isImageLoaded = ref(false)
|
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() {
|
async function installPlugin() {
|
||||||
try {
|
try {
|
||||||
|
// 显示等待提示框
|
||||||
|
progressDialog.value = true
|
||||||
|
progressText.value = `正在安装 ${props.plugin?.plugin_name} ${props?.plugin?.plugin_version} 插件...`
|
||||||
|
|
||||||
const result: { [key: string]: any } = await api.get(
|
const result: { [key: string]: any } = await api.get(
|
||||||
`plugin/install/${props.plugin?.id}`,
|
`plugin/install/${props.plugin?.id}`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
repo_url: props.plugin?.repo_url,
|
||||||
|
force: props.plugin?.has_update,
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 隐藏等待提示框
|
||||||
|
progressDialog.value = false
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
$toast.success(`插件 ${props.plugin?.plugin_name} 安装成功!`)
|
$toast.success(`插件 ${props.plugin?.plugin_name} 安装成功!`)
|
||||||
|
|
||||||
@@ -33,13 +71,63 @@ async function installPlugin() {
|
|||||||
emit('install')
|
emit('install')
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$toast.error(`插件 ${props.plugin?.plugin_name} 安装失败:${result.message}}`)
|
$toast.error(`插件 ${props.plugin?.plugin_name} 安装失败:${result.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error(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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -50,17 +138,51 @@ async function installPlugin() {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="relative pa-4 text-center card-cover-blurred"
|
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
|
<VAvatar
|
||||||
size="8rem"
|
size="8rem"
|
||||||
:class="{ shadow: isImageLoaded }"
|
|
||||||
>
|
>
|
||||||
<VImg
|
<VImg
|
||||||
:src="`/plugin/${props.plugin?.plugin_icon}`"
|
ref="imageRef"
|
||||||
|
:src="iconPath"
|
||||||
aspect-ratio="4/3"
|
aspect-ratio="4/3"
|
||||||
cover
|
cover
|
||||||
@load="isImageLoaded = true"
|
:class="{ shadow: isImageLoaded }"
|
||||||
|
@load="imageLoaded"
|
||||||
|
@error="imageLoadError = true"
|
||||||
/>
|
/>
|
||||||
</VAvatar>
|
</VAvatar>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,9 +198,29 @@ async function installPlugin() {
|
|||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
{{ props.plugin?.plugin_author }}
|
{{ props.plugin?.plugin_author }}
|
||||||
</a>
|
</a><br>
|
||||||
|
版本:{{ props.plugin?.plugin_version }}
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</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>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import { useConfirm } from 'vuetify-use-dialog'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Plugin } from '@/api/types'
|
import type { Plugin } from '@/api/types'
|
||||||
import FormRender from '@/components/render/FormRender.vue'
|
import FormRender from '@/components/render/FormRender.vue'
|
||||||
import PageRender from '@/components/render/PageRender.vue'
|
import PageRender from '@/components/render/PageRender.vue'
|
||||||
import { isNullOrEmptyObject } from '@core/utils'
|
import { isNullOrEmptyObject } from '@core/utils'
|
||||||
|
import noImage from '@images/logos/plugin.png'
|
||||||
|
import { getDominantColor } from '@/@core/utils/image'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -16,9 +19,18 @@ const props = defineProps({
|
|||||||
// 定义触发的自定义事件
|
// 定义触发的自定义事件
|
||||||
const emit = defineEmits(['remove', 'save'])
|
const emit = defineEmits(['remove', 'save'])
|
||||||
|
|
||||||
|
// 背景颜色
|
||||||
|
const backgroundColor = ref('#28A9E1')
|
||||||
|
|
||||||
|
// 图片对象
|
||||||
|
const imageRef = ref<any>()
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 确认框
|
||||||
|
const createConfirm = useConfirm()
|
||||||
|
|
||||||
// 本身是否可见
|
// 本身是否可见
|
||||||
const isVisible = ref(true)
|
const isVisible = ref(true)
|
||||||
|
|
||||||
@@ -31,17 +43,44 @@ const pluginConfigForm = ref({})
|
|||||||
// 插件表单配置项
|
// 插件表单配置项
|
||||||
let pluginFormItems = reactive([])
|
let pluginFormItems = reactive([])
|
||||||
|
|
||||||
// 插件详情页面
|
// 插件数据页面
|
||||||
const pluginInfoDialog = ref(false)
|
const pluginInfoDialog = ref(false)
|
||||||
|
|
||||||
// 插件详情页面配置项
|
// 插件数据页面配置项
|
||||||
let pluginPageItems = reactive([])
|
let pluginPageItems = reactive([])
|
||||||
|
|
||||||
// 图片是否加载完成
|
// 图片是否加载完成
|
||||||
const isImageLoaded = ref(false)
|
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卸载插件
|
// 调用API卸载插件
|
||||||
async function uninstallPlugin() {
|
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 {
|
try {
|
||||||
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`)
|
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -74,7 +113,7 @@ async function loadPluginForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用API读取详情页面
|
// 调用API读取数据页面
|
||||||
async function loadPluginPage() {
|
async function loadPluginPage() {
|
||||||
try {
|
try {
|
||||||
const result: [] = await api.get(`plugin/page/${props.plugin?.id}`)
|
const result: [] = await api.get(`plugin/page/${props.plugin?.id}`)
|
||||||
@@ -117,9 +156,9 @@ async function savePluginConf() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示插件详情
|
// 显示插件数据
|
||||||
async function showPluginInfo() {
|
async function showPluginInfo() {
|
||||||
// 加载详情
|
// 加载数据
|
||||||
await loadPluginPage()
|
await loadPluginPage()
|
||||||
pluginConfigDialog.value = false
|
pluginConfigDialog.value = false
|
||||||
pluginInfoDialog.value = true
|
pluginInfoDialog.value = true
|
||||||
@@ -136,10 +175,60 @@ async function showPluginConfig() {
|
|||||||
pluginConfigDialog.value = true
|
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([
|
const dropdownItems = ref([
|
||||||
{
|
{
|
||||||
title: '查看详情',
|
title: '查看数据',
|
||||||
value: 1,
|
value: 1,
|
||||||
show: props.plugin?.has_page,
|
show: props.plugin?.has_page,
|
||||||
props: {
|
props: {
|
||||||
@@ -148,7 +237,7 @@ const dropdownItems = ref([
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '配置',
|
title: '设置',
|
||||||
value: 2,
|
value: 2,
|
||||||
show: true,
|
show: true,
|
||||||
props: {
|
props: {
|
||||||
@@ -157,15 +246,34 @@ const dropdownItems = ref([
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '卸载',
|
title: '重置',
|
||||||
value: 3,
|
value: 3,
|
||||||
show: true,
|
show: true,
|
||||||
|
props: {
|
||||||
|
prependIcon: 'mdi-cancel',
|
||||||
|
color: 'warning',
|
||||||
|
click: resetPlugin,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '卸载',
|
||||||
|
value: 4,
|
||||||
|
show: true,
|
||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-trash-can-outline',
|
prependIcon: 'mdi-trash-can-outline',
|
||||||
color: 'error',
|
color: 'error',
|
||||||
click: uninstallPlugin,
|
click: uninstallPlugin,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '作者主页',
|
||||||
|
value: 4,
|
||||||
|
show: true,
|
||||||
|
props: {
|
||||||
|
prependIcon: 'mdi-home-circle-outline',
|
||||||
|
click: visitAuthorPage,
|
||||||
|
},
|
||||||
|
},
|
||||||
])
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -184,7 +292,7 @@ const dropdownItems = ref([
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="relative pa-4 text-center card-cover-blurred"
|
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">
|
<div class="me-n3 absolute top-0 right-3">
|
||||||
<IconBtn>
|
<IconBtn>
|
||||||
@@ -213,20 +321,22 @@ const dropdownItems = ref([
|
|||||||
</div>
|
</div>
|
||||||
<VAvatar
|
<VAvatar
|
||||||
size="8rem"
|
size="8rem"
|
||||||
:class="{ shadow: isImageLoaded }"
|
|
||||||
>
|
>
|
||||||
<VImg
|
<VImg
|
||||||
:src="`/plugin/${props.plugin?.plugin_icon}`"
|
ref="imageRef"
|
||||||
|
:src="iconPath"
|
||||||
aspect-ratio="4/3"
|
aspect-ratio="4/3"
|
||||||
cover
|
cover
|
||||||
:class="{ shadow: isImageLoaded }"
|
:class="{ shadow: isImageLoaded }"
|
||||||
|
@load="imageLoaded"
|
||||||
|
@error="imageLoadError = true"
|
||||||
/>
|
/>
|
||||||
</VAvatar>
|
</VAvatar>
|
||||||
</div>
|
</div>
|
||||||
<VCardItem class="py-2">
|
<VCardItem class="py-2">
|
||||||
<VCardTitle class="flex items-center flex-row">
|
<VCardTitle class="flex items-center flex-row">
|
||||||
<VBadge v-if="props.plugin?.state" dot inline color="success" class="me-1 mb-1" />
|
<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>
|
</VCardTitle>
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
<VCardText>
|
<VCardText>
|
||||||
@@ -236,11 +346,13 @@ const dropdownItems = ref([
|
|||||||
<!-- 插件配置页面 -->
|
<!-- 插件配置页面 -->
|
||||||
<VDialog
|
<VDialog
|
||||||
v-model="pluginConfigDialog"
|
v-model="pluginConfigDialog"
|
||||||
max-width="50rem"
|
|
||||||
scrollable
|
scrollable
|
||||||
persistent
|
max-width="60rem"
|
||||||
>
|
>
|
||||||
<VCard :title="`${props.plugin?.plugin_name} - 配置`">
|
<VCard
|
||||||
|
:title="`${props.plugin?.plugin_name} - 配置`"
|
||||||
|
class="rounded-t"
|
||||||
|
>
|
||||||
<DialogCloseBtn @click="pluginConfigDialog = false" />
|
<DialogCloseBtn @click="pluginConfigDialog = false" />
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<FormRender
|
<FormRender
|
||||||
@@ -252,24 +364,29 @@ const dropdownItems = ref([
|
|||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions>
|
<VCardActions>
|
||||||
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo">
|
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo">
|
||||||
查看详情
|
查看数据
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
<VBtn @click="savePluginConf">
|
<VBtn
|
||||||
|
variant="tonal"
|
||||||
|
@click="savePluginConf"
|
||||||
|
>
|
||||||
保存
|
保存
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</VCardActions>
|
</VCardActions>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
|
|
||||||
<!-- 插件详情页面 -->
|
<!-- 插件数据页面 -->
|
||||||
<VDialog
|
<VDialog
|
||||||
v-model="pluginInfoDialog"
|
v-model="pluginInfoDialog"
|
||||||
max-width="62.5rem"
|
|
||||||
scrollable
|
scrollable
|
||||||
persistent
|
max-width="80rem"
|
||||||
>
|
>
|
||||||
<VCard :title="`${props.plugin?.plugin_name}`">
|
<VCard
|
||||||
|
:title="`${props.plugin?.plugin_name}`"
|
||||||
|
class="rounded-t"
|
||||||
|
>
|
||||||
<DialogCloseBtn @click="pluginInfoDialog = false" />
|
<DialogCloseBtn @click="pluginInfoDialog = false" />
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<PageRender
|
<PageRender
|
||||||
@@ -279,11 +396,16 @@ const dropdownItems = ref([
|
|||||||
/>
|
/>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions>
|
<VCardActions>
|
||||||
<VBtn @click="showPluginConfig">
|
<VBtn
|
||||||
|
@click="showPluginConfig"
|
||||||
|
>
|
||||||
配置
|
配置
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
<VBtn @click="pluginInfoDialog = false">
|
<VBtn
|
||||||
|
variant="tonal"
|
||||||
|
@click="pluginInfoDialog = false"
|
||||||
|
>
|
||||||
关闭
|
关闭
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</VCardActions>
|
</VCardActions>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import SiteAddEditForm from '../form/SiteAddEditForm.vue'
|
||||||
import { formatFileSize } from '@core/utils/formatters'
|
import { formatFileSize } from '@core/utils/formatters'
|
||||||
import { numberValidator, requiredValidator } from '@/@validators'
|
import { requiredValidator } from '@/@validators'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Site, TorrentInfo } from '@/api/types'
|
import type { Site, TorrentInfo } from '@/api/types'
|
||||||
import ExistIcon from '@core/components/ExistIcon.vue'
|
import ExistIcon from '@core/components/ExistIcon.vue'
|
||||||
@@ -15,7 +16,7 @@ const cardProps = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 定义触发的自定义事件
|
// 定义触发的自定义事件
|
||||||
const emit = defineEmits(['remove', 'update'])
|
const emit = defineEmits(['update', 'remove'])
|
||||||
|
|
||||||
// 密码输入
|
// 密码输入
|
||||||
const isPasswordVisible = ref(false)
|
const isPasswordVisible = ref(false)
|
||||||
@@ -32,9 +33,6 @@ const testButtonText = ref('测试')
|
|||||||
// 测试按钮可用性
|
// 测试按钮可用性
|
||||||
const testButtonDisable = ref(false)
|
const testButtonDisable = ref(false)
|
||||||
|
|
||||||
// 更新按钮文字
|
|
||||||
const updateButtonText = ref('更新')
|
|
||||||
|
|
||||||
// 更新按钮可用性
|
// 更新按钮可用性
|
||||||
const updateButtonDisable = ref(false)
|
const updateButtonDisable = ref(false)
|
||||||
|
|
||||||
@@ -42,11 +40,17 @@ const updateButtonDisable = ref(false)
|
|||||||
const siteCookieDialog = ref(false)
|
const siteCookieDialog = ref(false)
|
||||||
|
|
||||||
// 站点编辑弹窗
|
// 站点编辑弹窗
|
||||||
const siteInfoDialog = ref(false)
|
const siteEditDialog = ref(false)
|
||||||
|
|
||||||
// 资源浏览弹窗
|
// 资源浏览弹窗
|
||||||
const resourceDialog = ref(false)
|
const resourceDialog = ref(false)
|
||||||
|
|
||||||
|
// 进度条
|
||||||
|
const progressDialog = ref(false)
|
||||||
|
|
||||||
|
// 进度文本
|
||||||
|
const progressText = ref('请稍候 ...')
|
||||||
|
|
||||||
// 资源浏览表头
|
// 资源浏览表头
|
||||||
const resourceHeaders = [
|
const resourceHeaders = [
|
||||||
{ title: '标题', key: 'title', sortable: false },
|
{ title: '标题', key: 'title', sortable: false },
|
||||||
@@ -78,27 +82,6 @@ const userPwForm = ref({
|
|||||||
password: '',
|
password: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 状态下拉项
|
|
||||||
const statusItems = [
|
|
||||||
{ title: '启用', value: true },
|
|
||||||
{ title: '停用', value: false },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 生成1到50的优先级下拉框选项
|
|
||||||
const priorityItems = ref(
|
|
||||||
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
|
|
||||||
title: item,
|
|
||||||
value: item,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
|
|
||||||
// 站点编辑表单数据
|
|
||||||
const siteForm = reactive<any>(cardProps.site ?? {})
|
|
||||||
|
|
||||||
// 类型转换
|
|
||||||
siteForm.proxy = siteForm.proxy === 1
|
|
||||||
siteForm.render = siteForm.render === 1
|
|
||||||
|
|
||||||
// 打开种子详情页面
|
// 打开种子详情页面
|
||||||
function openTorrentDetail(page_url: string) {
|
function openTorrentDetail(page_url: string) {
|
||||||
window.open(page_url, '_blank')
|
window.open(page_url, '_blank')
|
||||||
@@ -144,11 +127,6 @@ async function handleSiteUpdate() {
|
|||||||
siteCookieDialog.value = true
|
siteCookieDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开站点编辑弹窗
|
|
||||||
async function handleSiteInfo() {
|
|
||||||
siteInfoDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开资源浏览弹窗
|
// 打开资源浏览弹窗
|
||||||
async function handleResourceBrowse() {
|
async function handleResourceBrowse() {
|
||||||
resourceDialog.value = true
|
resourceDialog.value = true
|
||||||
@@ -163,9 +141,11 @@ async function updateSiteCookie() {
|
|||||||
|
|
||||||
// 更新按钮状态
|
// 更新按钮状态
|
||||||
siteCookieDialog.value = false
|
siteCookieDialog.value = false
|
||||||
updateButtonText.value = '更新中 ...'
|
|
||||||
updateButtonDisable.value = true
|
updateButtonDisable.value = true
|
||||||
|
|
||||||
|
progressDialog.value = true
|
||||||
|
progressText.value = `正在更新 ${cardProps.site?.name} Cookie & UA ...`
|
||||||
|
|
||||||
const result: { [key: string]: any } = await api.get(
|
const result: { [key: string]: any } = await api.get(
|
||||||
`site/cookie/${cardProps.site?.id}`,
|
`site/cookie/${cardProps.site?.id}`,
|
||||||
{
|
{
|
||||||
@@ -181,7 +161,7 @@ async function updateSiteCookie() {
|
|||||||
else
|
else
|
||||||
$toast.error(`${cardProps.site?.name} 更新失败:${result.message}`)
|
$toast.error(`${cardProps.site?.name} 更新失败:${result.message}`)
|
||||||
|
|
||||||
updateButtonText.value = '更新'
|
progressDialog.value = false
|
||||||
updateButtonDisable.value = false
|
updateButtonDisable.value = false
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -189,42 +169,6 @@ async function updateSiteCookie() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用API删除站点信息
|
|
||||||
async function deleteSiteInfo() {
|
|
||||||
try {
|
|
||||||
siteInfoDialog.value = false
|
|
||||||
const result: { [key: string]: any } = await api.delete(`site/${cardProps.site?.id}`)
|
|
||||||
if (result.success) {
|
|
||||||
$toast.success(`${cardProps.site?.name} 删除成功!`)
|
|
||||||
emit('remove')
|
|
||||||
}
|
|
||||||
else { $toast.error(`${cardProps.site?.name} 删除失败:${result.message}`) }
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
$toast.error(`${cardProps.site?.name} 删除失败!`)
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用API更新站点信息
|
|
||||||
async function updateSiteInfo() {
|
|
||||||
try {
|
|
||||||
// 更新按钮状态
|
|
||||||
siteInfoDialog.value = false
|
|
||||||
|
|
||||||
const result: { [key: string]: any } = await api.put('site/', siteForm)
|
|
||||||
if (result.success) {
|
|
||||||
$toast.success(`${cardProps.site?.name} 更新成功!`)
|
|
||||||
emit('update')
|
|
||||||
}
|
|
||||||
else { $toast.error(`${cardProps.site?.name} 更新失败:${result.message}`) }
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
$toast.error(`${cardProps.site?.name} 更新失败!`)
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 促销Chip类
|
// 促销Chip类
|
||||||
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||||
if (downloadVolume === 0)
|
if (downloadVolume === 0)
|
||||||
@@ -264,9 +208,9 @@ onMounted(() => {
|
|||||||
<VCard
|
<VCard
|
||||||
:height="cardProps.height"
|
:height="cardProps.height"
|
||||||
:width="cardProps.width"
|
:width="cardProps.width"
|
||||||
:flat="!siteForm.is_active"
|
:flat="!cardProps.site?.is_active"
|
||||||
class="overflow-hidden"
|
class="overflow-hidden"
|
||||||
@click="handleSiteInfo"
|
@click="siteEditDialog = true"
|
||||||
>
|
>
|
||||||
<template #image>
|
<template #image>
|
||||||
<VAvatar
|
<VAvatar
|
||||||
@@ -278,17 +222,19 @@ onMounted(() => {
|
|||||||
</VAvatar>
|
</VAvatar>
|
||||||
</template>
|
</template>
|
||||||
<VCardItem>
|
<VCardItem>
|
||||||
<VCardTitle class="font-bold" @click.stop="openSitePage">
|
<VCardTitle class="font-bold">
|
||||||
{{ cardProps.site?.name }}
|
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
|
||||||
</VCardTitle>
|
</VCardTitle>
|
||||||
<VCardSubtitle>{{ cardProps.site?.url }}</VCardSubtitle>
|
<VCardSubtitle>
|
||||||
|
{{ cardProps.site?.url }}
|
||||||
|
</VCardSubtitle>
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
|
|
||||||
<ExistIcon v-if="siteForm.is_active" />
|
<ExistIcon v-if="cardProps.site?.is_active" />
|
||||||
|
|
||||||
<VCardText class="py-2">
|
<VCardText class="py-2">
|
||||||
<VTooltip
|
<VTooltip
|
||||||
v-if="siteForm.render"
|
v-if="cardProps.site?.render === 1"
|
||||||
text="浏览器仿真"
|
text="浏览器仿真"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
@@ -302,7 +248,7 @@ onMounted(() => {
|
|||||||
</VTooltip>
|
</VTooltip>
|
||||||
|
|
||||||
<VTooltip
|
<VTooltip
|
||||||
v-if="siteForm.proxy"
|
v-if="cardProps.site?.proxy === 1"
|
||||||
text="代理"
|
text="代理"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
@@ -316,7 +262,7 @@ onMounted(() => {
|
|||||||
</VTooltip>
|
</VTooltip>
|
||||||
|
|
||||||
<VTooltip
|
<VTooltip
|
||||||
v-if="siteForm.limit_interval"
|
v-if="cardProps.site?.limit_interval"
|
||||||
text="流控"
|
text="流控"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
@@ -330,7 +276,7 @@ onMounted(() => {
|
|||||||
</VTooltip>
|
</VTooltip>
|
||||||
|
|
||||||
<VTooltip
|
<VTooltip
|
||||||
v-if="siteForm.filter"
|
v-if="cardProps.site?.filter"
|
||||||
text="过滤"
|
text="过滤"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
@@ -358,7 +304,7 @@ onMounted(() => {
|
|||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon icon="mdi-refresh" />
|
<VIcon icon="mdi-refresh" />
|
||||||
</template>
|
</template>
|
||||||
{{ updateButtonText }}
|
更新
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn
|
<VBtn
|
||||||
:disabled="testButtonDisable"
|
:disabled="testButtonDisable"
|
||||||
@@ -419,143 +365,22 @@ onMounted(() => {
|
|||||||
|
|
||||||
<VCardActions>
|
<VCardActions>
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
<VBtn @click="updateSiteCookie">
|
<VBtn
|
||||||
|
variant="tonal"
|
||||||
|
@click="updateSiteCookie"
|
||||||
|
>
|
||||||
开始更新
|
开始更新
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</VCardActions>
|
</VCardActions>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
<!-- 站点编辑弹窗 -->
|
<SiteAddEditForm
|
||||||
<VDialog
|
v-model="siteEditDialog"
|
||||||
v-model="siteInfoDialog"
|
:siteid="cardProps.site?.id"
|
||||||
max-width="50rem"
|
@save="siteEditDialog = false; emit('update')"
|
||||||
persistent
|
@remove="emit('remove')"
|
||||||
scrollable
|
@close="siteEditDialog = false"
|
||||||
>
|
/>
|
||||||
<!-- Dialog Content -->
|
|
||||||
<VCard :title="`编辑站点 - ${cardProps.site?.name}`">
|
|
||||||
<VCardText class="pt-2">
|
|
||||||
<DialogCloseBtn @click="siteInfoDialog = false" />
|
|
||||||
<VForm @submit.prevent="() => {}">
|
|
||||||
<VRow>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<VTextField
|
|
||||||
v-model="siteForm.url"
|
|
||||||
label="站点地址"
|
|
||||||
:rules="[requiredValidator]"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="3"
|
|
||||||
>
|
|
||||||
<VSelect
|
|
||||||
v-model="siteForm.pri"
|
|
||||||
label="优先级"
|
|
||||||
:items="priorityItems"
|
|
||||||
:rules="[requiredValidator]"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="3"
|
|
||||||
>
|
|
||||||
<VSelect
|
|
||||||
v-model="siteForm.is_active"
|
|
||||||
:items="statusItems"
|
|
||||||
label="状态"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextField
|
|
||||||
v-model="siteForm.rss"
|
|
||||||
label="RSS地址"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextarea
|
|
||||||
v-model="siteForm.cookie"
|
|
||||||
label="站点Cookie"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextField
|
|
||||||
v-model="siteForm.ua"
|
|
||||||
label="站点User-Agent"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="4"
|
|
||||||
>
|
|
||||||
<VTextField
|
|
||||||
v-model="siteForm.limit_interval"
|
|
||||||
label="单位周期(秒)"
|
|
||||||
:rules="[numberValidator]"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="4"
|
|
||||||
>
|
|
||||||
<VTextField
|
|
||||||
v-model="siteForm.limit_seconds"
|
|
||||||
label="访问次数"
|
|
||||||
:rules="[numberValidator]"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="4"
|
|
||||||
>
|
|
||||||
<VTextField
|
|
||||||
v-model="siteForm.limit_seconds"
|
|
||||||
label="访问间隔(秒)"
|
|
||||||
:rules="[numberValidator]"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<VSwitch
|
|
||||||
v-model="siteForm.proxy"
|
|
||||||
label="代理"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<VSwitch
|
|
||||||
v-model="siteForm.render"
|
|
||||||
label="仿真"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
|
|
||||||
<VCardActions>
|
|
||||||
<VBtn color="error" @click="deleteSiteInfo">
|
|
||||||
删除
|
|
||||||
</VBtn>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn @click="updateSiteInfo">
|
|
||||||
确定
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
<!-- 站点资源弹窗 -->
|
<!-- 站点资源弹窗 -->
|
||||||
<VDialog
|
<VDialog
|
||||||
v-model="resourceDialog"
|
v-model="resourceDialog"
|
||||||
@@ -654,7 +479,7 @@ onMounted(() => {
|
|||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon icon="mdi-download" />
|
<VIcon icon="mdi-download" />
|
||||||
</template>
|
</template>
|
||||||
<VListItemTitle>下载种子</VListItemTitle>
|
<VListItemTitle>下载种子文件</VListItemTitle>
|
||||||
</VListItem>
|
</VListItem>
|
||||||
</VList>
|
</VList>
|
||||||
</VMenu>
|
</VMenu>
|
||||||
@@ -668,6 +493,24 @@ onMounted(() => {
|
|||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</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>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
|
||||||
import { calculateTimeDifference } from '@/@core/utils'
|
import { calculateTimeDifference } from '@/@core/utils'
|
||||||
import { formatSeason } from '@/@core/utils/formatters'
|
import { formatSeason } from '@/@core/utils/formatters'
|
||||||
import { numberValidator } from '@/@validators'
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Site, Subscribe } from '@/api/types'
|
import type { Subscribe } from '@/api/types'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -21,19 +21,7 @@ const $toast = useToast()
|
|||||||
const imageLoaded = ref(false)
|
const imageLoaded = ref(false)
|
||||||
|
|
||||||
// 订阅弹窗
|
// 订阅弹窗
|
||||||
const subscribeInfoDialog = ref(false)
|
const subscribeEditDialog = ref(false)
|
||||||
|
|
||||||
// 站点数据列表
|
|
||||||
const siteList = ref<Site[]>([])
|
|
||||||
|
|
||||||
// 站点选择下载框
|
|
||||||
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
|
|
||||||
|
|
||||||
// 订阅编辑表单
|
|
||||||
const subscribeForm = reactive<any>(props.media ?? {})
|
|
||||||
|
|
||||||
// 类型转换
|
|
||||||
subscribeForm.best_version = subscribeForm.best_version === 1
|
|
||||||
|
|
||||||
// 上一次更新时间
|
// 上一次更新时间
|
||||||
const lastUpdateText = ref(
|
const lastUpdateText = ref(
|
||||||
@@ -114,58 +102,9 @@ async function searchSubscribe() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用API修改订阅
|
|
||||||
async function updateSubscribeInfo() {
|
|
||||||
subscribeInfoDialog.value = false
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.put('subscribe/', subscribeForm)
|
|
||||||
|
|
||||||
// 提示
|
|
||||||
if (result.success) {
|
|
||||||
$toast.success(`${props.media?.name} 更新成功!`)
|
|
||||||
// 通知父组件刷新
|
|
||||||
emit('remove')
|
|
||||||
}
|
|
||||||
else { $toast.error(`${props.media?.name} 更新失败:${result.message}!`) }
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取站点列表数据
|
|
||||||
async function loadSites() {
|
|
||||||
try {
|
|
||||||
const data: Site[] = await api.get('site/rss')
|
|
||||||
|
|
||||||
// 过滤站点,只有启用的站点才显示
|
|
||||||
siteList.value = data.filter(item => item.is_active)
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取站点列表选择框数据
|
|
||||||
async function getSiteList() {
|
|
||||||
// 加载订阅站点列表
|
|
||||||
if (!siteList.value.length)
|
|
||||||
await loadSites()
|
|
||||||
|
|
||||||
const maps = siteList.value.map((item) => {
|
|
||||||
return {
|
|
||||||
title: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
selectSitesOptions.value = maps.flat()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 编辑订阅响应
|
// 编辑订阅响应
|
||||||
async function editSubscribeDialog() {
|
async function editSubscribeDialog() {
|
||||||
await getSiteList()
|
subscribeEditDialog.value = true
|
||||||
subscribeInfoDialog.value = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 弹出菜单
|
// 弹出菜单
|
||||||
@@ -201,7 +140,7 @@ const dropdownItems = ref([
|
|||||||
<template>
|
<template>
|
||||||
<VCard
|
<VCard
|
||||||
:key="props.media?.id"
|
:key="props.media?.id"
|
||||||
:class="`${subscribeForm.best_version ? 'outline-dashed outline-1' : ''}`"
|
:class="`${props.media?.best_version ? 'outline-dashed outline-1' : ''}`"
|
||||||
@click="editSubscribeDialog"
|
@click="editSubscribeDialog"
|
||||||
>
|
>
|
||||||
<template #image>
|
<template #image>
|
||||||
@@ -323,100 +262,11 @@ const dropdownItems = ref([
|
|||||||
/>
|
/>
|
||||||
</VCard>
|
</VCard>
|
||||||
<!-- 订阅编辑弹窗 -->
|
<!-- 订阅编辑弹窗 -->
|
||||||
<VDialog
|
<SubscribeEditForm
|
||||||
v-model="subscribeInfoDialog"
|
v-model="subscribeEditDialog"
|
||||||
max-width="50rem"
|
:subid="props.media?.id"
|
||||||
persistent
|
@remove="() => { emit('remove');subscribeEditDialog = false; }"
|
||||||
scrollable
|
@save="() => { emit('save');subscribeEditDialog = false; }"
|
||||||
>
|
@close="subscribeEditDialog = false"
|
||||||
<!-- Dialog Content -->
|
/>
|
||||||
<VCard :title="`订阅 - ${props.media?.name}`">
|
|
||||||
<VCardText class="pt-2">
|
|
||||||
<VForm @submit.prevent="() => {}">
|
|
||||||
<VRow>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<VTextField
|
|
||||||
v-model="subscribeForm.keyword"
|
|
||||||
label="搜索关键词"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
v-if="props.media?.type === '电视剧'"
|
|
||||||
cols="12"
|
|
||||||
md="3"
|
|
||||||
>
|
|
||||||
<VTextField
|
|
||||||
v-model="subscribeForm.total_episode"
|
|
||||||
label="总集数"
|
|
||||||
:rules="[numberValidator]"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
v-if="props.media?.type === '电视剧'"
|
|
||||||
cols="12"
|
|
||||||
md="3"
|
|
||||||
>
|
|
||||||
<VTextField
|
|
||||||
v-model="subscribeForm.start_episode"
|
|
||||||
label="开始集数"
|
|
||||||
:rules="[numberValidator]"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<VTextField
|
|
||||||
v-model="subscribeForm.include"
|
|
||||||
label="包含(关键字、正则式)"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<VTextField
|
|
||||||
v-model="subscribeForm.exclude"
|
|
||||||
label="排除(关键字、正则式)"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VSelect
|
|
||||||
v-model="subscribeForm.sites"
|
|
||||||
:items="selectSitesOptions"
|
|
||||||
chips
|
|
||||||
label="订阅站点"
|
|
||||||
multiple
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VSwitch
|
|
||||||
v-model="subscribeForm.best_version"
|
|
||||||
label="洗版"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
|
|
||||||
<VCardActions>
|
|
||||||
<VBtn @click="subscribeInfoDialog = false">
|
|
||||||
取消
|
|
||||||
</VBtn>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn @click="updateSubscribeInfo">
|
|
||||||
确定
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ const tmdbKeyword = ref<HTMLElement | null>(null)
|
|||||||
|
|
||||||
// 选中条目
|
// 选中条目
|
||||||
function selectMedia(item: TmdbItem) {
|
function selectMedia(item: TmdbItem) {
|
||||||
console.log(item)
|
|
||||||
emit('update:modelValue', item.tmdbid)
|
emit('update:modelValue', item.tmdbid)
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ async function handleAddDownload(_site: any = undefined,
|
|||||||
dialogProps: {
|
dialogProps: {
|
||||||
maxWidth: '50rem',
|
maxWidth: '50rem',
|
||||||
},
|
},
|
||||||
|
confirmationButtonProps: {
|
||||||
|
variant: 'tonal',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!isConfirmed)
|
if (!isConfirmed)
|
||||||
@@ -176,7 +179,7 @@ onMounted(() => {
|
|||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon icon="mdi-download" />
|
<VIcon icon="mdi-download" />
|
||||||
</template>
|
</template>
|
||||||
<VListItemTitle>下载种子</VListItemTitle>
|
<VListItemTitle>下载种子文件</VListItemTitle>
|
||||||
</VListItem>
|
</VListItem>
|
||||||
</VList>
|
</VList>
|
||||||
</VMenu>
|
</VMenu>
|
||||||
|
|||||||
251
src/components/cards/TorrentItem.vue
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import { useConfirm } from 'vuetify-use-dialog'
|
||||||
|
import { formatFileSize } from '@/@core/utils/formatters'
|
||||||
|
import api from '@/api'
|
||||||
|
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||||
|
import type { Context } from '@/api/types'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
torrent: Object as PropType<Context>,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 提示框
|
||||||
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 确认框
|
||||||
|
const createConfirm = useConfirm()
|
||||||
|
|
||||||
|
// 更多来源界面
|
||||||
|
const showMoreTorrents = ref(false)
|
||||||
|
|
||||||
|
// 种子信息
|
||||||
|
const torrent = ref(props.torrent?.torrent_info)
|
||||||
|
|
||||||
|
// 媒体信息
|
||||||
|
const media = ref(props.torrent?.media_info)
|
||||||
|
|
||||||
|
// 识别元数据
|
||||||
|
const meta = ref(props.torrent?.meta_info)
|
||||||
|
|
||||||
|
// 站点图标
|
||||||
|
const siteIcon = ref('')
|
||||||
|
|
||||||
|
// 查询站点图标
|
||||||
|
async function getSiteIcon() {
|
||||||
|
try {
|
||||||
|
siteIcon.value = (await api.get(`site/icon/${torrent?.value?.site}`)).data.icon
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 询问并添加下载
|
||||||
|
async function handleAddDownload(_site: any = undefined,
|
||||||
|
_media: any = undefined,
|
||||||
|
_torrent: any = undefined) {
|
||||||
|
if (!_media || !_torrent || !_site) {
|
||||||
|
_site = torrent.value?.site_name
|
||||||
|
_media = media.value
|
||||||
|
_torrent = torrent.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const isConfirmed = await createConfirm({
|
||||||
|
title: '确认',
|
||||||
|
content: `是否确认下载【${_site}】${_torrent?.title} ?`,
|
||||||
|
confirmationText: '确认',
|
||||||
|
cancellationText: '取消',
|
||||||
|
dialogProps: {
|
||||||
|
maxWidth: '50rem',
|
||||||
|
},
|
||||||
|
confirmationButtonProps: {
|
||||||
|
variant: 'tonal',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isConfirmed)
|
||||||
|
return
|
||||||
|
|
||||||
|
addDownload(_media, _torrent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加下载
|
||||||
|
async function addDownload(_media: any, _torrent: any) {
|
||||||
|
startNProgress()
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.post('download/', {
|
||||||
|
media_in: _media,
|
||||||
|
torrent_in: _torrent,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 添加下载成功
|
||||||
|
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 添加下载失败
|
||||||
|
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
doneNProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开种子详情页面
|
||||||
|
function openTorrentDetail() {
|
||||||
|
window.open(torrent.value?.page_url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载种子文件
|
||||||
|
async function downloadTorrentFile() {
|
||||||
|
window.open(torrent.value?.enclosure, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 促销Chip类
|
||||||
|
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||||
|
if (downloadVolume === 0)
|
||||||
|
return 'text-white bg-lime-500'
|
||||||
|
else if (downloadVolume < 1)
|
||||||
|
return 'text-white bg-green-500'
|
||||||
|
else if (uploadVolume !== 1)
|
||||||
|
return 'text-white bg-sky-500'
|
||||||
|
else
|
||||||
|
return 'text-white bg-gray-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 装载时查询站点图标
|
||||||
|
onMounted(() => {
|
||||||
|
getSiteIcon()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VListItem @click="handleAddDownload">
|
||||||
|
<template
|
||||||
|
v-if="!showMoreTorrents"
|
||||||
|
#prepend
|
||||||
|
>
|
||||||
|
<VAvatar
|
||||||
|
class="rounded"
|
||||||
|
variant="flat"
|
||||||
|
@click.stop="openTorrentDetail"
|
||||||
|
>
|
||||||
|
<VImg :src="siteIcon" />
|
||||||
|
</VAvatar>
|
||||||
|
</template>
|
||||||
|
<VListItemTitle class="break-words overflow-visible whitespace-break-spaces">
|
||||||
|
{{ torrent?.title }}
|
||||||
|
<span class="text-green-700 ms-2 text-sm">↑{{ torrent?.seeders }}</span>
|
||||||
|
<span class="text-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
||||||
|
</VListItemTitle>
|
||||||
|
<VListItemSubtitle>
|
||||||
|
{{ torrent?.description }}
|
||||||
|
</VListItemSubtitle>
|
||||||
|
<div
|
||||||
|
v-if="torrent?.labels"
|
||||||
|
class="pt-2"
|
||||||
|
>
|
||||||
|
<VChip
|
||||||
|
v-for="(label, index) in torrent?.labels"
|
||||||
|
:key="index"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
class="me-1 mb-1"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="meta?.edition"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1 text-white bg-red-500"
|
||||||
|
>
|
||||||
|
{{ meta?.edition }}
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="meta?.resource_pix"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1 text-white bg-red-500"
|
||||||
|
>
|
||||||
|
{{ meta?.resource_pix }}
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="meta?.video_encode"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1 text-white bg-orange-500"
|
||||||
|
>
|
||||||
|
{{ meta?.video_encode }}
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="torrent?.size"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1 text-white bg-yellow-500"
|
||||||
|
>
|
||||||
|
{{ formatFileSize(torrent?.size) }}
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="meta?.resource_team"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1 text-white bg-cyan-500"
|
||||||
|
>
|
||||||
|
{{ meta?.resource_team }}
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
|
||||||
|
:class="
|
||||||
|
getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)
|
||||||
|
"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1"
|
||||||
|
>
|
||||||
|
{{ torrent?.volume_factor }}
|
||||||
|
</VChip>
|
||||||
|
</div>
|
||||||
|
<template #append>
|
||||||
|
<div class="me-n3">
|
||||||
|
<IconBtn>
|
||||||
|
<VIcon
|
||||||
|
icon="mdi-dots-vertical"
|
||||||
|
/>
|
||||||
|
<VMenu
|
||||||
|
activator="parent"
|
||||||
|
close-on-content-click
|
||||||
|
>
|
||||||
|
<VList>
|
||||||
|
<VListItem
|
||||||
|
variant="plain"
|
||||||
|
@click="openTorrentDetail()"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-information" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>查看详情</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
<VListItem
|
||||||
|
v-if="props.torrent?.torrent_info?.enclosure?.startsWith('http')"
|
||||||
|
variant="plain"
|
||||||
|
@click="downloadTorrentFile()"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-download" />
|
||||||
|
</template>
|
||||||
|
<VListItemTitle>下载种子文件</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VMenu>
|
||||||
|
</IconBtn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VListItem>
|
||||||
|
</template>
|
||||||
@@ -4,13 +4,12 @@ import type { PropType } from 'vue'
|
|||||||
import { useConfirm } from 'vuetify-use-dialog'
|
import { useConfirm } from 'vuetify-use-dialog'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import { numberValidator } from '@/@validators'
|
import ReorganizeForm from '../form/ReorganizeForm.vue'
|
||||||
import { formatBytes } from '@core/utils/formatters'
|
import { formatBytes } from '@core/utils/formatters'
|
||||||
import type { Context, EndPoints, FileItem } from '@/api/types'
|
import type { Context, EndPoints, FileItem } from '@/api/types'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
|
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
|
||||||
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
|
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const inProps = defineProps({
|
const inProps = defineProps({
|
||||||
@@ -32,6 +31,15 @@ const $toast = useToast()
|
|||||||
// 是否正在加载
|
// 是否正在加载
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|
||||||
|
// 识别进度条
|
||||||
|
const progressDialog = ref(false)
|
||||||
|
|
||||||
|
// 识别进度文本
|
||||||
|
const progressText = ref('请稍候 ...')
|
||||||
|
|
||||||
|
// 识别进度
|
||||||
|
const progressValue = ref(0)
|
||||||
|
|
||||||
// 确认框
|
// 确认框
|
||||||
const createConfirm = useConfirm()
|
const createConfirm = useConfirm()
|
||||||
|
|
||||||
@@ -53,57 +61,18 @@ const renamePopper = ref(false)
|
|||||||
// 整理弹窗
|
// 整理弹窗
|
||||||
const transferPopper = ref(false)
|
const transferPopper = ref(false)
|
||||||
|
|
||||||
// 整理进度条
|
|
||||||
const progressDialog = ref(false)
|
|
||||||
|
|
||||||
// 整理进度文本
|
|
||||||
const progressText = ref('请稍候 ...')
|
|
||||||
|
|
||||||
// 整理进度
|
|
||||||
const progressValue = ref(0)
|
|
||||||
|
|
||||||
// 加载进度SSE
|
|
||||||
const progressEventSource = ref<EventSource>()
|
|
||||||
|
|
||||||
// 新名称
|
// 新名称
|
||||||
const newName = ref('')
|
const newName = ref('')
|
||||||
|
|
||||||
// 当前名称
|
// 当前名称
|
||||||
const currentItem = ref<FileItem>()
|
const currentItem = ref<FileItem>()
|
||||||
|
|
||||||
// 文件转移表单
|
|
||||||
const transferForm = reactive({
|
|
||||||
path: '',
|
|
||||||
target: '',
|
|
||||||
tmdbid: null,
|
|
||||||
season: null,
|
|
||||||
type_name: '',
|
|
||||||
transfer_type: '',
|
|
||||||
episode_format: '',
|
|
||||||
episode_detail: '',
|
|
||||||
episode_part: '',
|
|
||||||
episode_offset: null,
|
|
||||||
min_filesize: 0,
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
// 识别结果
|
// 识别结果
|
||||||
const nameTestResult = ref<Context>()
|
const nameTestResult = ref<Context>()
|
||||||
|
|
||||||
// 识别结果对话框
|
// 识别结果对话框
|
||||||
const nameTestDialog = ref(false)
|
const nameTestDialog = ref(false)
|
||||||
|
|
||||||
// TMDB选择对话框
|
|
||||||
const tmdbSelectorDialog = ref(false)
|
|
||||||
|
|
||||||
// 生成1到50季的下拉框选项
|
|
||||||
const seasonItems = ref(
|
|
||||||
Array.from({ length: 51 }, (_, i) => i).map(item => ({
|
|
||||||
title: `第 ${item} 季`,
|
|
||||||
value: item,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
|
|
||||||
// 目录过滤
|
// 目录过滤
|
||||||
const dirs = computed(() =>
|
const dirs = computed(() =>
|
||||||
items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)),
|
items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)),
|
||||||
@@ -158,6 +127,9 @@ async function deleteItem(item: FileItem) {
|
|||||||
dialogProps: {
|
dialogProps: {
|
||||||
maxWidth: '50rem',
|
maxWidth: '50rem',
|
||||||
},
|
},
|
||||||
|
cancellationButtonProps: {
|
||||||
|
variant: 'tonal',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
@@ -245,41 +217,6 @@ function showTransfer(item: FileItem) {
|
|||||||
transferPopper.value = true
|
transferPopper.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 整理文件
|
|
||||||
async function transfer() {
|
|
||||||
transferForm.path = currentItem.value?.path ?? ''
|
|
||||||
// 开始整理文件
|
|
||||||
try {
|
|
||||||
// 关闭弹窗
|
|
||||||
transferPopper.value = false
|
|
||||||
// 显示进度条
|
|
||||||
progressDialog.value = true
|
|
||||||
// 开始监听进度
|
|
||||||
startLoadingProgress()
|
|
||||||
// 异步调API,结束后关闭进度条
|
|
||||||
api.post('transfer/manual', {}, {
|
|
||||||
params: transferForm,
|
|
||||||
}).then((res: any) => {
|
|
||||||
// 关闭进度条
|
|
||||||
progressDialog.value = false
|
|
||||||
// 停止监听进度
|
|
||||||
stopLoadingProgress()
|
|
||||||
// 显示结果
|
|
||||||
if (res.success) {
|
|
||||||
$toast.success(`${currentItem.value?.name} 整理完成!`)
|
|
||||||
// 重新加载
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$toast.error(`${currentItem.value?.name} 整理失败:${res.message}!`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将文件修改时间(timestape)转换为本地时间
|
// 将文件修改时间(timestape)转换为本地时间
|
||||||
function formatTime(timestape: number) {
|
function formatTime(timestape: number) {
|
||||||
return new Date(timestape * 1000).toLocaleString()
|
return new Date(timestape * 1000).toLocaleString()
|
||||||
@@ -307,29 +244,6 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// 使用SSE监听加载进度
|
|
||||||
function startLoadingProgress() {
|
|
||||||
progressText.value = '请稍候 ...'
|
|
||||||
|
|
||||||
const token = store.state.auth.token
|
|
||||||
|
|
||||||
progressEventSource.value = new EventSource(
|
|
||||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer?token=${token}`,
|
|
||||||
)
|
|
||||||
progressEventSource.value.onmessage = (event) => {
|
|
||||||
const progress = JSON.parse(event.data)
|
|
||||||
if (progress) {
|
|
||||||
progressText.value = progress.text
|
|
||||||
progressValue.value = progress.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 停止监听加载进度
|
|
||||||
function stopLoadingProgress() {
|
|
||||||
progressEventSource.value?.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用API识别
|
// 调用API识别
|
||||||
async function recognize(path: string) {
|
async function recognize(path: string) {
|
||||||
try {
|
try {
|
||||||
@@ -586,23 +500,19 @@ onMounted(() => {
|
|||||||
v-model="renamePopper"
|
v-model="renamePopper"
|
||||||
max-width="50rem"
|
max-width="50rem"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
|
||||||
<IconBtn title="重命名" v-bind="props">
|
|
||||||
<VIcon icon="mdi-rename-outline" />
|
|
||||||
</IconBtn>
|
|
||||||
</template>
|
|
||||||
<VCard title="重命名">
|
<VCard title="重命名">
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VTextField v-model="newName" label="名称" />
|
<VTextField v-model="newName" label="名称" />
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions>
|
<VCardActions>
|
||||||
<div class="flex-grow-1" />
|
|
||||||
<VBtn depressed @click="renamePopper = false">
|
<VBtn depressed @click="renamePopper = false">
|
||||||
取消
|
取消
|
||||||
</VBtn>
|
</VBtn>
|
||||||
|
<VSpacer />
|
||||||
<VBtn
|
<VBtn
|
||||||
:disabled="!newName"
|
:disabled="!newName"
|
||||||
depressed
|
depressed
|
||||||
|
variant="tonal"
|
||||||
@click="rename"
|
@click="rename"
|
||||||
>
|
>
|
||||||
重命名
|
重命名
|
||||||
@@ -611,181 +521,44 @@ onMounted(() => {
|
|||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
<!-- 文件整理弹窗 -->
|
<!-- 文件整理弹窗 -->
|
||||||
<VDialog
|
<ReorganizeForm
|
||||||
v-model="transferPopper"
|
v-model="transferPopper"
|
||||||
max-width="50rem"
|
:path="currentItem?.path"
|
||||||
scrollable
|
@done="transferPopper = false; load()"
|
||||||
>
|
@close="transferPopper = false"
|
||||||
<template #activator="{ props }">
|
/>
|
||||||
<IconBtn title="整理" v-bind="props">
|
|
||||||
<VIcon icon="mdi-folder-arrow-right-outline" />
|
|
||||||
</IconBtn>
|
|
||||||
</template>
|
|
||||||
<VCard :title="`文件整理 - ${currentItem?.name}`">
|
|
||||||
<DialogCloseBtn @click="transferPopper = false" />
|
|
||||||
<VCardText class="pt-2">
|
|
||||||
<VForm @submit.prevent="() => {}">
|
|
||||||
<VRow>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="8"
|
|
||||||
>
|
|
||||||
<VTextField
|
|
||||||
v-model="transferForm.target"
|
|
||||||
label="目的路径"
|
|
||||||
placeholder="留空自动"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="4"
|
|
||||||
>
|
|
||||||
<VSelect
|
|
||||||
v-model="transferForm.transfer_type"
|
|
||||||
label="整理方式"
|
|
||||||
:items="[
|
|
||||||
{ title: '默认', value: '' },
|
|
||||||
{ title: '移动', value: 'move' },
|
|
||||||
{ title: '复制', value: 'copy' },
|
|
||||||
{ title: '硬链接', value: 'link' },
|
|
||||||
{ title: '软链接', value: 'softlink' },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="4"
|
|
||||||
>
|
|
||||||
<VSelect
|
|
||||||
v-model="transferForm.type_name"
|
|
||||||
label="类型"
|
|
||||||
:items="[{ title: '请选择', value: '' }, { title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="4"
|
|
||||||
>
|
|
||||||
<VTextField
|
|
||||||
v-model="transferForm.tmdbid"
|
|
||||||
:disabled="transferForm.type_name === ''"
|
|
||||||
label="TMDBID"
|
|
||||||
placeholder="留空自动识别"
|
|
||||||
:rules="[numberValidator]"
|
|
||||||
append-inner-icon="mdi-magnify"
|
|
||||||
@click:append-inner="tmdbSelectorDialog = true"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="4"
|
|
||||||
>
|
|
||||||
<VSelect
|
|
||||||
v-show="transferForm.type_name === '电视剧'"
|
|
||||||
v-model.number="transferForm.season"
|
|
||||||
label="季"
|
|
||||||
:items="seasonItems"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12" md="8">
|
|
||||||
<VTextField
|
|
||||||
v-model="transferForm.episode_format"
|
|
||||||
label="集数定位"
|
|
||||||
placeholder="使用{ep}定位集数"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="4">
|
|
||||||
<VTextField
|
|
||||||
v-model="transferForm.episode_detail"
|
|
||||||
label="指定集数"
|
|
||||||
placeholder="起始集,终止集,如1或1,2"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="4">
|
|
||||||
<VTextField
|
|
||||||
v-model="transferForm.episode_part"
|
|
||||||
label="指定Part"
|
|
||||||
placeholder="如part1"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="4">
|
|
||||||
<VTextField
|
|
||||||
v-model.number="transferForm.episode_offset"
|
|
||||||
label="集数偏移"
|
|
||||||
placeholder="如-10"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="4">
|
|
||||||
<VTextField
|
|
||||||
v-model.number="transferForm.min_filesize"
|
|
||||||
label="最小文件大小(MB)"
|
|
||||||
:rules="[numberValidator]"
|
|
||||||
placeholder="0"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
<VCardActions>
|
|
||||||
<VBtn depressed @click="transferPopper = false">
|
|
||||||
取消
|
|
||||||
</VBtn>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn
|
|
||||||
@click="transfer"
|
|
||||||
>
|
|
||||||
开始整理
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
<!-- 手动整理进度框 -->
|
<!-- 手动整理进度框 -->
|
||||||
<vDialog
|
<VDialog
|
||||||
v-model="progressDialog"
|
v-model="progressDialog"
|
||||||
:scrim="false"
|
:scrim="false"
|
||||||
width="25rem"
|
width="25rem"
|
||||||
>
|
>
|
||||||
<vCard
|
<VCard
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
<vCardText class="text-center">
|
<VCardText class="text-center">
|
||||||
{{ progressText }}
|
{{ progressText }}
|
||||||
<vProgressLinear
|
<VProgressLinear
|
||||||
v-if="progressValue"
|
v-if="progressValue"
|
||||||
color="white"
|
color="white"
|
||||||
class="mb-0 mt-1"
|
class="mb-0 mt-1"
|
||||||
:model-value="progressValue"
|
:model-value="progressValue"
|
||||||
/>
|
/>
|
||||||
</vCardText>
|
</VCardText>
|
||||||
</vCard>
|
</VCard>
|
||||||
</vDialog>
|
</VDialog>
|
||||||
<!-- 识别结果对话框 -->
|
<!-- 识别结果对话框 -->
|
||||||
<vDialog
|
<VDialog
|
||||||
v-model="nameTestDialog"
|
v-model="nameTestDialog"
|
||||||
width="50rem"
|
width="50rem"
|
||||||
>
|
>
|
||||||
<vCard>
|
<VCard>
|
||||||
<DialogCloseBtn @click="nameTestDialog = false" />
|
<DialogCloseBtn @click="nameTestDialog = false" />
|
||||||
<VCardItem>
|
<VCardItem>
|
||||||
<MediaInfoCard :context="nameTestResult" />
|
<MediaInfoCard :context="nameTestResult" />
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
</vCard>
|
</VCard>
|
||||||
</vDialog>
|
</VDialog>
|
||||||
<!-- TMDB ID搜索框 -->
|
|
||||||
<vDialog
|
|
||||||
v-model="tmdbSelectorDialog"
|
|
||||||
width="40rem"
|
|
||||||
scrollable
|
|
||||||
>
|
|
||||||
<TmdbSelectorCard
|
|
||||||
v-model="transferForm.tmdbid"
|
|
||||||
@close="tmdbSelectorDialog = false"
|
|
||||||
/>
|
|
||||||
</vDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ const sortIcon = computed(() => {
|
|||||||
<VBtn
|
<VBtn
|
||||||
:disabled="!newFolderName"
|
:disabled="!newFolderName"
|
||||||
depressed
|
depressed
|
||||||
|
variant="tonal"
|
||||||
@click="mkdir"
|
@click="mkdir"
|
||||||
>
|
>
|
||||||
新建
|
新建
|
||||||
|
|||||||
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>
|
||||||
307
src/components/form/ReorganizeForm.vue
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import TmdbSelectorCard from '../cards/TmdbSelectorCard.vue'
|
||||||
|
import store from '@/store'
|
||||||
|
import api from '@/api'
|
||||||
|
import { numberValidator } from '@/@validators'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
path: String,
|
||||||
|
target: String,
|
||||||
|
logids: Array<number>,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['done', 'close'])
|
||||||
|
|
||||||
|
// 生成1到50季的下拉框选项
|
||||||
|
const seasonItems = ref(
|
||||||
|
Array.from({ length: 51 }, (_, i) => i).map(item => ({
|
||||||
|
title: `第 ${item} 季`,
|
||||||
|
value: item,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 提示框
|
||||||
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// TMDB选择对话框
|
||||||
|
const tmdbSelectorDialog = ref(false)
|
||||||
|
|
||||||
|
// 加载进度SSE
|
||||||
|
const progressEventSource = ref<EventSource>()
|
||||||
|
|
||||||
|
// 整理进度条
|
||||||
|
const progressDialog = ref(false)
|
||||||
|
|
||||||
|
// 整理进度文本
|
||||||
|
const progressText = ref('请稍候 ...')
|
||||||
|
|
||||||
|
// 整理进度
|
||||||
|
const progressValue = ref(0)
|
||||||
|
|
||||||
|
// 文件转移表单
|
||||||
|
const transferForm = reactive({
|
||||||
|
logid: 0,
|
||||||
|
path: '',
|
||||||
|
target: props.target ?? '',
|
||||||
|
tmdbid: null,
|
||||||
|
season: null,
|
||||||
|
type_name: '',
|
||||||
|
transfer_type: '',
|
||||||
|
episode_format: '',
|
||||||
|
episode_detail: '',
|
||||||
|
episode_part: '',
|
||||||
|
episode_offset: null,
|
||||||
|
min_filesize: 0,
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
transferForm.path = props.path ?? ''
|
||||||
|
transferForm.target = props.target ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用SSE监听加载进度
|
||||||
|
function startLoadingProgress() {
|
||||||
|
progressText.value = '请稍候 ...'
|
||||||
|
|
||||||
|
const token = store.state.auth.token
|
||||||
|
|
||||||
|
progressEventSource.value = new EventSource(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer?token=${token}`,
|
||||||
|
)
|
||||||
|
progressEventSource.value.onmessage = (event) => {
|
||||||
|
const progress = JSON.parse(event.data)
|
||||||
|
if (progress) {
|
||||||
|
progressText.value = progress.text
|
||||||
|
progressValue.value = progress.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止监听加载进度
|
||||||
|
function stopLoadingProgress() {
|
||||||
|
progressEventSource.value?.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 整理文件
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
async function transfer() {
|
||||||
|
if (!props.logids && !props.path)
|
||||||
|
return
|
||||||
|
|
||||||
|
// 显示进度条
|
||||||
|
progressDialog.value = true
|
||||||
|
// 开始监听进度
|
||||||
|
startLoadingProgress()
|
||||||
|
|
||||||
|
if (props.path) {
|
||||||
|
// 文件整理
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.post('transfer/manual', {}, {
|
||||||
|
params: transferForm,
|
||||||
|
})
|
||||||
|
// 显示结果
|
||||||
|
if (result.success)
|
||||||
|
$toast.success(`${props.path} 整理完成!`)
|
||||||
|
|
||||||
|
else
|
||||||
|
$toast.error(`${props.path} 整理失败:${result.message}!`)
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (props.logids) {
|
||||||
|
// 日志整理
|
||||||
|
for (const logid of props.logids) {
|
||||||
|
transferForm.logid = logid
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.post('transfer/manual', {}, {
|
||||||
|
params: transferForm,
|
||||||
|
})
|
||||||
|
if (!result.success)
|
||||||
|
$toast.error(`历史记录 ${logid} 重新整理失败:${result.message}!`)
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止监听进度
|
||||||
|
stopLoadingProgress()
|
||||||
|
// 关闭进度条
|
||||||
|
progressDialog.value = false
|
||||||
|
// 重新加载
|
||||||
|
emit('done')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VDialog
|
||||||
|
scrollable
|
||||||
|
max-width="60rem"
|
||||||
|
>
|
||||||
|
<VCard
|
||||||
|
:title="`${props.path ? `整理 - ${props.path}` : `整理 - 共 ${props.logids?.length} 条记录`}`"
|
||||||
|
class="rounded-t"
|
||||||
|
>
|
||||||
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
|
<VCardText class="pt-2">
|
||||||
|
<VForm @submit.prevent="() => {}">
|
||||||
|
<VRow>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="8"
|
||||||
|
>
|
||||||
|
<VTextField
|
||||||
|
v-model="transferForm.target"
|
||||||
|
label="目的路径"
|
||||||
|
placeholder="留空自动"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VSelect
|
||||||
|
v-model="transferForm.transfer_type"
|
||||||
|
label="整理方式"
|
||||||
|
:items="[
|
||||||
|
{ title: '默认', value: '' },
|
||||||
|
{ title: '移动', value: 'move' },
|
||||||
|
{ title: '复制', value: 'copy' },
|
||||||
|
{ title: '硬链接', value: 'link' },
|
||||||
|
{ title: '软链接', value: 'softlink' },
|
||||||
|
{ title: 'Rclone复制', value: 'rclone_copy' },
|
||||||
|
{ title: 'Rclone移动', value: 'rclone_move' },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VSelect
|
||||||
|
v-model="transferForm.type_name"
|
||||||
|
label="类型"
|
||||||
|
:items="[{ title: '自动', value: '' }, { title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VTextField
|
||||||
|
v-model="transferForm.tmdbid"
|
||||||
|
:disabled="transferForm.type_name === ''"
|
||||||
|
label="TMDBID"
|
||||||
|
placeholder="留空自动识别"
|
||||||
|
:rules="[numberValidator]"
|
||||||
|
append-inner-icon="mdi-magnify"
|
||||||
|
@click:append-inner="tmdbSelectorDialog = true"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VSelect
|
||||||
|
v-show="transferForm.type_name === '电视剧'"
|
||||||
|
v-model.number="transferForm.season"
|
||||||
|
label="季"
|
||||||
|
:items="seasonItems"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="8">
|
||||||
|
<VTextField
|
||||||
|
v-model="transferForm.episode_format"
|
||||||
|
label="集数定位"
|
||||||
|
placeholder="使用{ep}定位集数"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="transferForm.episode_detail"
|
||||||
|
label="指定集数"
|
||||||
|
placeholder="起始集,终止集,如1或1,2"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model="transferForm.episode_part"
|
||||||
|
label="指定Part"
|
||||||
|
placeholder="如part1"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model.number="transferForm.episode_offset"
|
||||||
|
label="集数偏移"
|
||||||
|
placeholder="如-10"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="4">
|
||||||
|
<VTextField
|
||||||
|
v-model.number="transferForm.min_filesize"
|
||||||
|
label="最小文件大小(MB)"
|
||||||
|
:rules="[numberValidator]"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
<VCardActions>
|
||||||
|
<VBtn depressed @click="emit('close')">
|
||||||
|
取消
|
||||||
|
</VBtn>
|
||||||
|
<VSpacer />
|
||||||
|
<VBtn
|
||||||
|
variant="tonal"
|
||||||
|
@click="transfer"
|
||||||
|
>
|
||||||
|
开始整理
|
||||||
|
</VBtn>
|
||||||
|
</VCardActions>
|
||||||
|
</VCard>
|
||||||
|
<!-- 手动整理进度框 -->
|
||||||
|
<VDialog
|
||||||
|
v-model="progressDialog"
|
||||||
|
:scrim="false"
|
||||||
|
width="25rem"
|
||||||
|
>
|
||||||
|
<VCard
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<VCardText class="text-center">
|
||||||
|
{{ progressText }}
|
||||||
|
<VProgressLinear
|
||||||
|
v-if="progressValue"
|
||||||
|
color="white"
|
||||||
|
class="mb-0 mt-1"
|
||||||
|
:model-value="progressValue"
|
||||||
|
/>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
<!-- TMDB ID搜索框 -->
|
||||||
|
<VDialog
|
||||||
|
v-model="tmdbSelectorDialog"
|
||||||
|
width="40rem"
|
||||||
|
scrollable
|
||||||
|
>
|
||||||
|
<TmdbSelectorCard
|
||||||
|
v-model="transferForm.tmdbid"
|
||||||
|
@close="tmdbSelectorDialog = false"
|
||||||
|
/>
|
||||||
|
</VDialog>
|
||||||
|
</VDialog>
|
||||||
|
</template>
|
||||||
274
src/components/form/SiteAddEditForm.vue
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import type { Site } from '@/api/types'
|
||||||
|
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||||
|
import { numberValidator, requiredValidator } from '@/@validators'
|
||||||
|
import api from '@/api'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
siteid: Number,
|
||||||
|
oper: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 注册事件
|
||||||
|
const emit = defineEmits(['save', 'remove', 'close'])
|
||||||
|
|
||||||
|
// 站点编辑表单数据
|
||||||
|
const siteForm = ref<Site>({
|
||||||
|
id: props.siteid ?? 0,
|
||||||
|
url: '',
|
||||||
|
rss: '',
|
||||||
|
cookie: '',
|
||||||
|
ua: '',
|
||||||
|
pri: 0,
|
||||||
|
is_active: true,
|
||||||
|
limit_interval: 0,
|
||||||
|
limit_seconds: 0,
|
||||||
|
name: '',
|
||||||
|
domain: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 提示框
|
||||||
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 状态下拉项
|
||||||
|
const statusItems = [
|
||||||
|
{ title: '启用', value: true },
|
||||||
|
{ title: '停用', value: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 生成1到50的优先级下拉框选项
|
||||||
|
const priorityItems = ref(
|
||||||
|
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
|
||||||
|
title: item,
|
||||||
|
value: item,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 监控输入参数
|
||||||
|
watchEffect(async () => {
|
||||||
|
if (props.siteid)
|
||||||
|
fetchSiteInfo()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 查询站点信息
|
||||||
|
async function fetchSiteInfo() {
|
||||||
|
try {
|
||||||
|
siteForm.value = await api.get(`site/${props.siteid}`)
|
||||||
|
siteForm.value.proxy = siteForm.value.proxy === 1
|
||||||
|
siteForm.value.render = siteForm.value.render === 1
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API 新增站点
|
||||||
|
async function addSite() {
|
||||||
|
if (!siteForm.value?.url)
|
||||||
|
return
|
||||||
|
startNProgress()
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: string } = await api.post('site/', siteForm.value)
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success('新增站点成功')
|
||||||
|
emit('save')
|
||||||
|
}
|
||||||
|
|
||||||
|
else { $toast.error(`新增站点失败:${result.message}`) }
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
doneNProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API删除站点信息
|
||||||
|
async function deleteSiteInfo() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.delete(`site/${siteForm.value?.id}`)
|
||||||
|
if (result.success)
|
||||||
|
emit('remove')
|
||||||
|
|
||||||
|
else $toast.error(`${siteForm.value?.name} 删除失败:${result.message}`)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
$toast.error(`${siteForm.value?.name} 删除失败!`)
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API更新站点信息
|
||||||
|
async function updateSiteInfo() {
|
||||||
|
startNProgress()
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.put('site/', siteForm.value)
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success(`${siteForm.value?.name} 更新成功!`)
|
||||||
|
emit('save')
|
||||||
|
}
|
||||||
|
else { $toast.error(`${siteForm.value?.name} 更新失败:${result.message}`) }
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
$toast.error(`${siteForm.value?.name} 更新失败!`)
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
doneNProgress()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VDialog
|
||||||
|
scrollable
|
||||||
|
max-width="60rem"
|
||||||
|
>
|
||||||
|
<VCard
|
||||||
|
:title="`${props.oper === 'add' ? '新增' : '编辑'}站点${props.oper !== 'add' ? ` - ${siteForm.name}` : ''}`"
|
||||||
|
class="rounded-t"
|
||||||
|
>
|
||||||
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
|
<VCardText class="pt-2">
|
||||||
|
<VForm @submit.prevent="() => {}">
|
||||||
|
<VRow>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<VTextField
|
||||||
|
v-model="siteForm.url"
|
||||||
|
label="站点地址"
|
||||||
|
:rules="[requiredValidator]"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="3"
|
||||||
|
>
|
||||||
|
<VSelect
|
||||||
|
v-model="siteForm.pri"
|
||||||
|
label="优先级"
|
||||||
|
:items="priorityItems"
|
||||||
|
:rules="[requiredValidator]"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="3"
|
||||||
|
>
|
||||||
|
<VSelect
|
||||||
|
v-model="siteForm.is_active"
|
||||||
|
:items="statusItems"
|
||||||
|
label="状态"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField
|
||||||
|
v-model="siteForm.rss"
|
||||||
|
label="RSS地址"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextarea
|
||||||
|
v-model="siteForm.cookie"
|
||||||
|
label="站点Cookie"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField
|
||||||
|
v-model="siteForm.ua"
|
||||||
|
label="站点User-Agent"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VTextField
|
||||||
|
v-model="siteForm.limit_interval"
|
||||||
|
label="单位周期(秒)"
|
||||||
|
:rules="[numberValidator]"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VTextField
|
||||||
|
v-model="siteForm.limit_count"
|
||||||
|
label="访问次数"
|
||||||
|
:rules="[numberValidator]"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VTextField
|
||||||
|
v-model="siteForm.limit_seconds"
|
||||||
|
label="访问间隔(秒)"
|
||||||
|
:rules="[numberValidator]"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<VSwitch
|
||||||
|
v-model="siteForm.proxy"
|
||||||
|
label="代理"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<VSwitch
|
||||||
|
v-model="siteForm.render"
|
||||||
|
label="仿真"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
<VCardActions>
|
||||||
|
<VBtn
|
||||||
|
v-if="props.oper === 'add'"
|
||||||
|
@click="emit('close')"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</VBtn>
|
||||||
|
<VBtn
|
||||||
|
v-else
|
||||||
|
color="error"
|
||||||
|
@click="deleteSiteInfo"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</VBtn>
|
||||||
|
<VSpacer />
|
||||||
|
<VBtn
|
||||||
|
v-if="props.oper === 'add'"
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
@click="addSite"
|
||||||
|
>
|
||||||
|
新增
|
||||||
|
</VBtn>
|
||||||
|
<VBtn
|
||||||
|
v-else
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
@click="updateSiteInfo"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</VBtn>
|
||||||
|
</VCardActions>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
</template>
|
||||||
365
src/components/form/SubscribeEditForm.vue
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import { numberValidator } from '@/@validators'
|
||||||
|
import api from '@/api'
|
||||||
|
import type { Site, Subscribe } from '@/api/types'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
subid: Number,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义触发的自定义事件
|
||||||
|
const emit = defineEmits(['remove', 'save', 'close'])
|
||||||
|
|
||||||
|
// 站点数据列表
|
||||||
|
const siteList = ref<Site[]>([])
|
||||||
|
|
||||||
|
// 站点选择下载框
|
||||||
|
const selectSitesOptions = ref<{ [key: number]: string }[]>([])
|
||||||
|
|
||||||
|
// 订阅编辑表单
|
||||||
|
const subscribeForm = ref<Subscribe>({
|
||||||
|
id: props.subid ?? 0,
|
||||||
|
keyword: '',
|
||||||
|
quality: '',
|
||||||
|
resolution: '',
|
||||||
|
effect: '',
|
||||||
|
include: '',
|
||||||
|
exclude: '',
|
||||||
|
total_episode: 0,
|
||||||
|
start_episode: 0,
|
||||||
|
best_version: 0,
|
||||||
|
sites: [],
|
||||||
|
type: '',
|
||||||
|
name: '',
|
||||||
|
year: '',
|
||||||
|
tmdbid: 0,
|
||||||
|
state: '',
|
||||||
|
last_update: '',
|
||||||
|
username: '',
|
||||||
|
current_priority: 0,
|
||||||
|
save_path: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 提示框
|
||||||
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 调用API修改订阅
|
||||||
|
async function updateSubscribeInfo() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.put('subscribe/', subscribeForm.value)
|
||||||
|
// 提示
|
||||||
|
if (result.success) {
|
||||||
|
$toast.success(`${subscribeForm.value.name} 更新成功!`)
|
||||||
|
// 通知父组件刷新
|
||||||
|
emit('save')
|
||||||
|
}
|
||||||
|
else { $toast.error(`${subscribeForm.value.name} 更新失败:${result.message}!`) }
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取站点列表数据
|
||||||
|
async function loadSites() {
|
||||||
|
try {
|
||||||
|
const data: Site[] = await api.get('site/rss')
|
||||||
|
|
||||||
|
// 过滤站点,只有启用的站点才显示
|
||||||
|
siteList.value = data.filter(item => item.is_active)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取站点列表选择框数据
|
||||||
|
async function getSiteList() {
|
||||||
|
// 加载订阅站点列表
|
||||||
|
if (!siteList.value.length)
|
||||||
|
await loadSites()
|
||||||
|
|
||||||
|
const maps = siteList.value.map((item) => {
|
||||||
|
return {
|
||||||
|
title: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
selectSitesOptions.value = maps.flat()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订阅信息
|
||||||
|
async function getSubscribeInfo() {
|
||||||
|
try {
|
||||||
|
const result: Subscribe = await api.get(
|
||||||
|
`subscribe/${props.subid}`,
|
||||||
|
)
|
||||||
|
subscribeForm.value = result
|
||||||
|
subscribeForm.value.best_version = subscribeForm.value.best_version === 1
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除订阅
|
||||||
|
async function removeSubscribe() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.delete(
|
||||||
|
`subscribe/${props.subid}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 通知父组件刷新
|
||||||
|
emit('remove')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 质量选择框数据
|
||||||
|
const qualityOptions = ref([
|
||||||
|
{
|
||||||
|
title: '全部',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '蓝光原盘',
|
||||||
|
value: 'Blu-?Ray.+VC-?1|Blu-?Ray.+AVC|UHD.+blu-?ray.+HEVC|MiniBD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Remux',
|
||||||
|
value: 'Remux',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'BluRay',
|
||||||
|
value: 'Blu-?Ray',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'UHD',
|
||||||
|
value: 'UHD|UltraHD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'WEB-DL',
|
||||||
|
value: 'WEB-?DL|WEB-?RIP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'HDTV',
|
||||||
|
value: 'HDTV',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'H265',
|
||||||
|
value: '[Hx].?265|HEVC',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'H264',
|
||||||
|
value: '[Hx].?264|AVC',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 分辨率选择框数据
|
||||||
|
const resolutionOptions = ref([
|
||||||
|
{
|
||||||
|
title: '全部',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '4k',
|
||||||
|
value: '4K|2160p|x2160',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '1080p',
|
||||||
|
value: '1080[pi]|x1080',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '720p',
|
||||||
|
value: '720[pi]|x720',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 特效选择框数据
|
||||||
|
const effectOptions = ref([
|
||||||
|
{
|
||||||
|
title: '全部',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '杜比视界',
|
||||||
|
value: 'Dolby[\\s.]+Vision|DOVI|[\\s.]+DV[\\s.]+',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '杜比全景声',
|
||||||
|
value: 'Dolby[\\s.]*\\+?Atmos|Atmos',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'HDR',
|
||||||
|
value: '[\\s.]+HDR[\\s.]+|HDR10|HDR10\\+',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'SDR',
|
||||||
|
value: '[\\s.]+SDR[\\s.]+',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (props.subid) {
|
||||||
|
getSiteList()
|
||||||
|
getSubscribeInfo()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VDialog
|
||||||
|
scrollable
|
||||||
|
max-width="60rem"
|
||||||
|
>
|
||||||
|
<VCard
|
||||||
|
:title="`编辑订阅 - ${subscribeForm.name} ${subscribeForm.season ? `第 ${subscribeForm.season} 季` : ''}`"
|
||||||
|
class="rounded-t"
|
||||||
|
>
|
||||||
|
<VCardText class="pt-2">
|
||||||
|
<DialogCloseBtn @click="emit('close')" />
|
||||||
|
<VForm @submit.prevent="() => {}">
|
||||||
|
<VRow>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="8"
|
||||||
|
>
|
||||||
|
<VTextField
|
||||||
|
v-model="subscribeForm.keyword"
|
||||||
|
label="搜索关键词"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
v-if="subscribeForm.type === '电视剧'"
|
||||||
|
cols="12"
|
||||||
|
md="2"
|
||||||
|
>
|
||||||
|
<VTextField
|
||||||
|
v-model="subscribeForm.total_episode"
|
||||||
|
label="总集数"
|
||||||
|
:rules="[numberValidator]"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
v-if="subscribeForm.type === '电视剧'"
|
||||||
|
cols="12"
|
||||||
|
md="2"
|
||||||
|
>
|
||||||
|
<VTextField
|
||||||
|
v-model="subscribeForm.start_episode"
|
||||||
|
label="开始集数"
|
||||||
|
:rules="[numberValidator]"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VSelect
|
||||||
|
v-model="subscribeForm.quality"
|
||||||
|
label="质量"
|
||||||
|
:items="qualityOptions"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VSelect
|
||||||
|
v-model="subscribeForm.resolution"
|
||||||
|
label="分辨率"
|
||||||
|
:items="resolutionOptions"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VSelect
|
||||||
|
v-model="subscribeForm.effect"
|
||||||
|
label="特效"
|
||||||
|
:items="effectOptions"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VTextField
|
||||||
|
v-model="subscribeForm.include"
|
||||||
|
label="包含(关键字、正则式)"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VTextField
|
||||||
|
v-model="subscribeForm.exclude"
|
||||||
|
label="排除(关键字、正则式)"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VSelect
|
||||||
|
v-model="subscribeForm.sites"
|
||||||
|
:items="selectSitesOptions"
|
||||||
|
chips
|
||||||
|
label="订阅站点"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<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="洗版"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
|
||||||
|
<VCardActions>
|
||||||
|
<VBtn color="error" @click="removeSubscribe">
|
||||||
|
取消订阅
|
||||||
|
</VBtn>
|
||||||
|
<VSpacer />
|
||||||
|
<VBtn
|
||||||
|
variant="tonal"
|
||||||
|
@click="updateSubscribeInfo"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</VBtn>
|
||||||
|
</VCardActions>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
</template>
|
||||||
@@ -91,7 +91,6 @@ const superUser = store.state.auth.superUser
|
|||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<VerticalNavLink
|
<VerticalNavLink
|
||||||
v-if="superUser"
|
|
||||||
:item="{
|
:item="{
|
||||||
title: '电影',
|
title: '电影',
|
||||||
icon: 'mdi-movie-check-outline',
|
icon: 'mdi-movie-check-outline',
|
||||||
@@ -99,7 +98,6 @@ const superUser = store.state.auth.superUser
|
|||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<VerticalNavLink
|
<VerticalNavLink
|
||||||
v-if="superUser"
|
|
||||||
:item="{
|
:item="{
|
||||||
title: '电视剧',
|
title: '电视剧',
|
||||||
icon: 'mdi-television-classic',
|
icon: 'mdi-television-classic',
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ const searchWord = ref<string>('')
|
|||||||
// 搜索弹窗
|
// 搜索弹窗
|
||||||
const searchDialog = ref(false)
|
const searchDialog = ref(false)
|
||||||
|
|
||||||
|
// ref
|
||||||
|
const searchWordInput = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
function search() {
|
function search() {
|
||||||
if (!searchWord.value)
|
if (!searchWord.value)
|
||||||
@@ -21,6 +24,14 @@ function search() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开搜索弹窗
|
||||||
|
function openSearchDialog() {
|
||||||
|
searchDialog.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
searchWordInput.value?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -34,23 +45,16 @@ function search() {
|
|||||||
max-width="50rem"
|
max-width="50rem"
|
||||||
transition="dialog-top-transition"
|
transition="dialog-top-transition"
|
||||||
>
|
>
|
||||||
<!-- Dialog Activator -->
|
|
||||||
<template #activator="{ props }">
|
|
||||||
<IconBtn
|
|
||||||
class="d-lg-none"
|
|
||||||
v-bind="props"
|
|
||||||
>
|
|
||||||
<VIcon icon="mdi-magnify" />
|
|
||||||
</IconBtn>
|
|
||||||
</template>
|
|
||||||
<!-- Dialog Content -->
|
<!-- Dialog Content -->
|
||||||
<VCard title="搜索">
|
<VCard title="搜索">
|
||||||
<VCardText>
|
<VCardText>
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
<VTextField
|
<VTextField
|
||||||
|
ref="searchWordInput"
|
||||||
v-model="searchWord"
|
v-model="searchWord"
|
||||||
label="电影、电视剧名称"
|
label="电影、电视剧名称"
|
||||||
|
@keydown.enter="search"
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
@@ -59,8 +63,8 @@ function search() {
|
|||||||
<VCardActions>
|
<VCardActions>
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
<VBtn
|
<VBtn
|
||||||
|
variant="tonal"
|
||||||
@click="search"
|
@click="search"
|
||||||
@keydown.enter="search"
|
|
||||||
>
|
>
|
||||||
搜索
|
搜索
|
||||||
</VBtn>
|
</VBtn>
|
||||||
@@ -68,7 +72,13 @@ function search() {
|
|||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 👉 Search Icon -->
|
||||||
|
<IconBtn
|
||||||
|
class="d-lg-none"
|
||||||
|
@click="openSearchDialog"
|
||||||
|
>
|
||||||
|
<VIcon icon="mdi-magnify" />
|
||||||
|
</IconBtn>
|
||||||
<!-- 👉 Search Textfield -->
|
<!-- 👉 Search Textfield -->
|
||||||
<span class="w-1/5">
|
<span class="w-1/5">
|
||||||
<VTextField
|
<VTextField
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ async function restart() {
|
|||||||
dialogProps: {
|
dialogProps: {
|
||||||
maxWidth: '30rem',
|
maxWidth: '30rem',
|
||||||
},
|
},
|
||||||
|
cancellationButtonProps: {
|
||||||
|
variant: 'tonal',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
@@ -121,6 +124,25 @@ const avatar = store.state.auth.avatar
|
|||||||
<VListItemTitle>设定</VListItemTitle>
|
<VListItemTitle>设定</VListItemTitle>
|
||||||
</VListItem>
|
</VListItem>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<VDivider class="my-2" />
|
||||||
|
|
||||||
|
<!-- 👉 restart -->
|
||||||
|
<VListItem
|
||||||
|
v-if="superUser"
|
||||||
|
@click="restart"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon
|
||||||
|
class="me-2"
|
||||||
|
icon="mdi-restart"
|
||||||
|
size="22"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<VListItemTitle>重启</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
|
||||||
<!-- 👉 FAQ -->
|
<!-- 👉 FAQ -->
|
||||||
<VListItem
|
<VListItem
|
||||||
href="https://github.com/jxxghp/MoviePilot/blob/main/README.md"
|
href="https://github.com/jxxghp/MoviePilot/blob/main/README.md"
|
||||||
@@ -137,22 +159,6 @@ const avatar = store.state.auth.avatar
|
|||||||
<VListItemTitle>帮助</VListItemTitle>
|
<VListItemTitle>帮助</VListItemTitle>
|
||||||
</VListItem>
|
</VListItem>
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<VDivider class="my-2" />
|
|
||||||
|
|
||||||
<!-- 👉 restart -->
|
|
||||||
<VListItem @click="restart">
|
|
||||||
<template #prepend>
|
|
||||||
<VIcon
|
|
||||||
class="me-2"
|
|
||||||
icon="mdi-restart"
|
|
||||||
size="22"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<VListItemTitle>重启</VListItemTitle>
|
|
||||||
</VListItem>
|
|
||||||
|
|
||||||
<!-- 👉 Logout -->
|
<!-- 👉 Logout -->
|
||||||
<VListItem @click="logout">
|
<VListItem @click="logout">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
@@ -170,21 +176,21 @@ const avatar = store.state.auth.avatar
|
|||||||
<!-- !SECTION -->
|
<!-- !SECTION -->
|
||||||
</VAvatar>
|
</VAvatar>
|
||||||
<!-- 重启进度框 -->
|
<!-- 重启进度框 -->
|
||||||
<vDialog
|
<VDialog
|
||||||
v-model="progressDialog"
|
v-model="progressDialog"
|
||||||
width="25rem"
|
width="25rem"
|
||||||
>
|
>
|
||||||
<vCard
|
<VCard
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
<vCardText class="text-center">
|
<VCardText class="text-center">
|
||||||
正在重启 ...
|
正在重启 ...
|
||||||
<vProgressLinear
|
<VProgressLinear
|
||||||
indeterminate
|
indeterminate
|
||||||
color="white"
|
color="white"
|
||||||
class="mb-0 mt-1"
|
class="mb-0 mt-1"
|
||||||
/>
|
/>
|
||||||
</vCardText>
|
</VCardText>
|
||||||
</vCard>
|
</VCard>
|
||||||
</vDialog>
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import store from '@/store'
|
|||||||
import '@core/scss/template/index.scss'
|
import '@core/scss/template/index.scss'
|
||||||
import '@layouts/styles/index.scss'
|
import '@layouts/styles/index.scss'
|
||||||
import '@styles/styles.scss'
|
import '@styles/styles.scss'
|
||||||
import 'vue-toast-notification/dist/theme-default.css'
|
import 'vue-toast-notification/dist/theme-bootstrap.css'
|
||||||
import { removeEl } from '@/util'
|
import { removeEl } from '@/util'
|
||||||
|
|
||||||
loadFonts()
|
loadFonts()
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ const route = useRoute()
|
|||||||
// 标题
|
// 标题
|
||||||
const title = route.query?.title?.toString()
|
const title = route.query?.title?.toString()
|
||||||
|
|
||||||
|
// 类型
|
||||||
|
const type = route.query?.type?.toString()
|
||||||
|
|
||||||
// 计算API路径
|
// 计算API路径
|
||||||
function getApiPath(paths: string[] | string) {
|
function getApiPath(paths: string[] | string) {
|
||||||
if (Array.isArray(paths))
|
if (Array.isArray(paths))
|
||||||
@@ -34,6 +37,7 @@ function getApiPath(paths: string[] | string) {
|
|||||||
<PersonCardListView
|
<PersonCardListView
|
||||||
:apipath="getApiPath(props.paths || '')"
|
:apipath="getApiPath(props.paths || '')"
|
||||||
:params="route.query"
|
:params="route.query"
|
||||||
|
:type="type"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const isImageLoaded = ref(false)
|
|||||||
// 获取背景图片
|
// 获取背景图片
|
||||||
async function fetchBackgroundImage() {
|
async function fetchBackgroundImage() {
|
||||||
api
|
api
|
||||||
.get('/login/tmdb')
|
.get('/login/wallpaper')
|
||||||
.then((response: any) => {
|
.then((response: any) => {
|
||||||
backgroundImageUrl.value = response.message
|
backgroundImageUrl.value = response.message
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,6 +28,24 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
|
|||||||
title="热门电视剧"
|
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=热门动漫"
|
||||||
|
title="热门动漫"
|
||||||
|
/>
|
||||||
|
|
||||||
<MediaCardSlideView
|
<MediaCardSlideView
|
||||||
apipath="douban/movies"
|
apipath="douban/movies"
|
||||||
linkurl="/browse/douban/movies?title=最新电影"
|
linkurl="/browse/douban/movies?title=最新电影"
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import NoDataFound from '@/components/NoDataFound.vue'
|
||||||
|
import api from '@/api'
|
||||||
|
import type { Context } from '@/api/types'
|
||||||
|
import store from '@/store'
|
||||||
import TorrentCardListView from '@/views/discover/TorrentCardListView.vue'
|
import TorrentCardListView from '@/views/discover/TorrentCardListView.vue'
|
||||||
|
import TorrentRowListView from '@/views/discover/TorrentRowListView.vue'
|
||||||
|
|
||||||
// 路由参数
|
// 路由参数
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -12,14 +17,130 @@ const type = route.query?.type?.toString() ?? ''
|
|||||||
|
|
||||||
// 搜索字段
|
// 搜索字段
|
||||||
const area = route.query?.area?.toString() ?? ''
|
const area = route.query?.area?.toString() ?? ''
|
||||||
|
|
||||||
|
// 视图类型,从localStorage中读取
|
||||||
|
const viewType = ref<string>(localStorage.getItem('MPTorrentsViewType') ?? 'card')
|
||||||
|
|
||||||
|
// 数据列表
|
||||||
|
const dataList = ref<Array<Context>>([])
|
||||||
|
|
||||||
|
// 是否刷新过
|
||||||
|
const isRefreshed = ref(false)
|
||||||
|
|
||||||
|
// 加载进度文本
|
||||||
|
const progressText = ref('')
|
||||||
|
|
||||||
|
// 加载进度
|
||||||
|
const progressValue = ref(0)
|
||||||
|
|
||||||
|
// 加载进度SSE
|
||||||
|
const progressEventSource = ref<EventSource>()
|
||||||
|
|
||||||
|
// 使用SSE监听加载进度
|
||||||
|
function startLoadingProgress() {
|
||||||
|
progressText.value = '正在搜索,请稍候...'
|
||||||
|
|
||||||
|
const token = store.state.auth.token
|
||||||
|
|
||||||
|
progressEventSource.value = new EventSource(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}system/progress/search?token=${token}`,
|
||||||
|
)
|
||||||
|
progressEventSource.value.onmessage = (event) => {
|
||||||
|
const progress = JSON.parse(event.data)
|
||||||
|
if (progress) {
|
||||||
|
progressText.value = progress.text
|
||||||
|
progressValue.value = progress.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止监听加载进度
|
||||||
|
function stopLoadingProgress() {
|
||||||
|
progressEventSource.value?.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置视图类型
|
||||||
|
function setViewType(type: string) {
|
||||||
|
localStorage.setItem('MPTorrentsViewType', type)
|
||||||
|
viewType.value = type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取搜索列表数据
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
if (!keyword) {
|
||||||
|
// 查询上次搜索结果
|
||||||
|
dataList.value = await api.get('search/last')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
startLoadingProgress()
|
||||||
|
// 优先按TMDBID精确查询
|
||||||
|
if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:')) {
|
||||||
|
dataList.value = await api.get(`search/media/${keyword}`, {
|
||||||
|
params: {
|
||||||
|
mtype: type,
|
||||||
|
area,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 按标题模糊查询
|
||||||
|
dataList.value = await api.get(`search/title/${keyword}`)
|
||||||
|
}
|
||||||
|
stopLoadingProgress()
|
||||||
|
}
|
||||||
|
// 标记已刷新
|
||||||
|
isRefreshed.value = true
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="!isRefreshed" class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center">
|
||||||
|
<VProgressCircular v-if="!keyword" size="48" indeterminate color="primary" />
|
||||||
|
<VProgressCircular v-if="keyword" class="mb-3" color="primary" :model-value="progressValue" size="64" />
|
||||||
|
<span>{{ progressText }}</span>
|
||||||
|
</div>
|
||||||
|
<NoDataFound
|
||||||
|
v-if="dataList.length === 0 && isRefreshed"
|
||||||
|
error-code="404"
|
||||||
|
error-title="没有资源"
|
||||||
|
error-description="没有搜索到符合条件的资源。"
|
||||||
|
/>
|
||||||
|
<div v-if="dataList.length > 0">
|
||||||
|
<TorrentRowListView
|
||||||
|
v-if="viewType === 'list'"
|
||||||
|
:items="dataList"
|
||||||
|
/>
|
||||||
<TorrentCardListView
|
<TorrentCardListView
|
||||||
:keyword="keyword"
|
v-else
|
||||||
:type="type"
|
:items="dataList"
|
||||||
:area="area"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 视图切换 -->
|
||||||
|
<span v-if="dataList.length > 0" class="fixed right-5 bottom-5">
|
||||||
|
<VBtn
|
||||||
|
v-if="viewType === 'list'"
|
||||||
|
size="x-large"
|
||||||
|
icon="mdi-view-grid"
|
||||||
|
color="primary"
|
||||||
|
@click="setViewType('card')"
|
||||||
|
/>
|
||||||
|
<VBtn
|
||||||
|
v-else
|
||||||
|
size="x-large"
|
||||||
|
icon="mdi-view-list"
|
||||||
|
color="primary"
|
||||||
|
@click="setViewType('list')"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import AccountSettingAccount from '@/views/setting/AccountSettingAccount.vue'
|
import AccountSettingAccount from '@/views/setting/AccountSettingAccount.vue'
|
||||||
import AccountSettingNotification from '@/views/setting/AccountSettingNotification.vue'
|
import AccountSettingNotification from '@/views/setting/AccountSettingNotification.vue'
|
||||||
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
|
|
||||||
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
|
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
|
||||||
import AccountSettingWords from '@/views/setting/AccountSettingWords.vue'
|
import AccountSettingWords from '@/views/setting/AccountSettingWords.vue'
|
||||||
import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
|
import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
|
||||||
@@ -41,11 +40,6 @@ const tabs = [
|
|||||||
icon: 'mdi-list-box',
|
icon: 'mdi-list-box',
|
||||||
tab: 'service',
|
tab: 'service',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: '规则',
|
|
||||||
icon: 'mdi-filter-cog',
|
|
||||||
tab: 'filter',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '通知',
|
title: '通知',
|
||||||
icon: 'mdi-bell',
|
icon: 'mdi-bell',
|
||||||
@@ -117,13 +111,6 @@ const tabs = [
|
|||||||
</transition>
|
</transition>
|
||||||
</VWindowItem>
|
</VWindowItem>
|
||||||
|
|
||||||
<!-- 规则 -->
|
|
||||||
<VWindowItem value="filter">
|
|
||||||
<transition name="fade-slide" appear>
|
|
||||||
<AccountSettingRule />
|
|
||||||
</transition>
|
|
||||||
</VWindowItem>
|
|
||||||
|
|
||||||
<!-- 通知 -->
|
<!-- 通知 -->
|
||||||
<VWindowItem value="notification">
|
<VWindowItem value="notification">
|
||||||
<transition name="fade-slide" appear>
|
<transition name="fade-slide" appear>
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ const router = createRouter({
|
|||||||
component: () => import('../pages/browse.vue'),
|
component: () => import('../pages/browse.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
meta: {
|
||||||
|
keepAlive: true,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -111,6 +112,7 @@ const router = createRouter({
|
|||||||
component: () => import('../pages/credits.vue'),
|
component: () => import('../pages/credits.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
meta: {
|
||||||
|
keepAlive: true,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import { hexToRgb } from '@layouts/utils'
|
|||||||
|
|
||||||
const vuetifyTheme = useTheme()
|
const vuetifyTheme = useTheme()
|
||||||
|
|
||||||
|
// 从Vuex Store中获取信息
|
||||||
|
const store = useStore()
|
||||||
|
const superUser = store.state.auth.superUser
|
||||||
|
|
||||||
const options = controlledComputed(
|
const options = controlledComputed(
|
||||||
() => vuetifyTheme.name.value,
|
() => vuetifyTheme.name.value,
|
||||||
() => {
|
() => {
|
||||||
@@ -129,6 +133,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VBtn
|
<VBtn
|
||||||
|
v-if="superUser"
|
||||||
block
|
block
|
||||||
to="/history"
|
to="/history"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ async function fetchData({ done }: { done: any }) {
|
|||||||
v-if="dataList.length === 0 && isRefreshed"
|
v-if="dataList.length === 0 && isRefreshed"
|
||||||
error-code="404"
|
error-code="404"
|
||||||
error-title="没有数据"
|
error-title="没有数据"
|
||||||
error-description="无法获取到TMDB媒体信息。"
|
error-description="无法获取到媒体信息。"
|
||||||
/>
|
/>
|
||||||
</VInfiniteScroll>
|
</VInfiniteScroll>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import NoDataFound from '@/components/NoDataFound.vue'
|
|||||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||||
import { formatSeason } from '@/@core/utils/formatters'
|
import { formatSeason } from '@/@core/utils/formatters'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
|
import SubscribeEditForm from '@/components/form/SubscribeEditForm.vue'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const mediaProps = defineProps({
|
const mediaProps = defineProps({
|
||||||
@@ -21,6 +22,9 @@ const $toast = useToast()
|
|||||||
// 媒体详情
|
// 媒体详情
|
||||||
const mediaDetail = ref<MediaInfo>({} as MediaInfo)
|
const mediaDetail = ref<MediaInfo>({} as MediaInfo)
|
||||||
|
|
||||||
|
// 订阅编辑弹窗
|
||||||
|
const subscribeEditDialog = ref(false)
|
||||||
|
|
||||||
// 本地是否存在
|
// 本地是否存在
|
||||||
const isExists = ref(false)
|
const isExists = ref(false)
|
||||||
|
|
||||||
@@ -33,12 +37,15 @@ const isRefreshed = ref(false)
|
|||||||
// 存储每一季的集信息
|
// 存储每一季的集信息
|
||||||
const seasonEpisodesInfo = ref({} as { [key: number]: TmdbEpisode[] })
|
const seasonEpisodesInfo = ref({} as { [key: number]: TmdbEpisode[] })
|
||||||
|
|
||||||
// 各季缺失状态:0-已存在 1-部分缺失 2-全部缺失,没有数据也是已存在
|
// 各季缺失状态:0-已入库 1-部分缺失 2-全部缺失,没有数据也是已入库
|
||||||
const seasonsNotExisted = ref<{ [key: number]: number }>({})
|
const seasonsNotExisted = ref<{ [key: number]: number }>({})
|
||||||
|
|
||||||
// 各季的订阅状态
|
// 各季的订阅状态
|
||||||
const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
|
const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
|
||||||
|
|
||||||
|
// 订阅编号
|
||||||
|
const subscribeId = ref<number>()
|
||||||
|
|
||||||
// 调用API查询详情
|
// 调用API查询详情
|
||||||
async function getMediaDetail() {
|
async function getMediaDetail() {
|
||||||
if (mediaProps.mediaid && mediaProps.type) {
|
if (mediaProps.mediaid && mediaProps.type) {
|
||||||
@@ -78,7 +85,7 @@ async function loadSeasonEpisodes(season: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询当前媒体是否已存在
|
// 查询当前媒体是否已入库
|
||||||
async function checkMovieExists() {
|
async function checkMovieExists() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('media/exists', {
|
const result: { [key: string]: any } = await api.get('media/exists', {
|
||||||
@@ -102,11 +109,12 @@ async function checkMovieExists() {
|
|||||||
// 查询当前媒体是否已订阅
|
// 查询当前媒体是否已订阅
|
||||||
async function checkSubscribe(season = 0) {
|
async function checkSubscribe(season = 0) {
|
||||||
try {
|
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}`, {
|
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
||||||
params: {
|
params: {
|
||||||
season,
|
season,
|
||||||
|
title: mediaDetail.value.title,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -131,7 +139,7 @@ async function checkSeasonsNotExists() {
|
|||||||
isExists.value = true
|
isExists.value = true
|
||||||
|
|
||||||
result.forEach((item) => {
|
result.forEach((item) => {
|
||||||
// 0-已存在 1-部分缺失 2-全部缺失
|
// 0-已入库 1-部分缺失 2-全部缺失
|
||||||
let state = 0
|
let state = 0
|
||||||
if (item.episodes.length === 0)
|
if (item.episodes.length === 0)
|
||||||
state = 2
|
state = 2
|
||||||
@@ -211,6 +219,12 @@ async function addSubscribe(season = 0) {
|
|||||||
result.message,
|
result.message,
|
||||||
best_version,
|
best_version,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 显示编辑弹窗
|
||||||
|
if (result.success) {
|
||||||
|
subscribeId.value = result.data.id
|
||||||
|
subscribeEditDialog.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -231,9 +245,7 @@ function showSubscribeAddToast(result: boolean,
|
|||||||
if (best_version > 0)
|
if (best_version > 0)
|
||||||
subname = '洗版订阅'
|
subname = '洗版订阅'
|
||||||
|
|
||||||
if (result)
|
if (!result)
|
||||||
$toast.success(`${title} 添加${subname}成功!`)
|
|
||||||
else
|
|
||||||
$toast.error(`${title} 添加${subname}失败:${message}!`)
|
$toast.error(`${title} 添加${subname}失败:${message}!`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,14 +359,14 @@ function getExistColor(season: number) {
|
|||||||
function getExistText(season: number) {
|
function getExistText(season: number) {
|
||||||
const state = seasonsNotExisted.value[season]
|
const state = seasonsNotExisted.value[season]
|
||||||
if (!state)
|
if (!state)
|
||||||
return '已存在'
|
return '已入库'
|
||||||
|
|
||||||
if (state === 1)
|
if (state === 1)
|
||||||
return '部分缺失'
|
return '部分缺失'
|
||||||
else if (state === 2)
|
else if (state === 2)
|
||||||
return '缺失'
|
return '缺失'
|
||||||
else
|
else
|
||||||
return '已存在'
|
return '已入库'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算订阅图标
|
// 计算订阅图标
|
||||||
@@ -380,10 +392,11 @@ function joinArray(arr: string[]) {
|
|||||||
|
|
||||||
// 开始搜索
|
// 开始搜索
|
||||||
function handleSearch(area: string) {
|
function handleSearch(area: string) {
|
||||||
|
const keyword = mediaDetail.value.tmdb_id ? `tmdb:${mediaDetail.value.tmdb_id}` : `douban:${mediaDetail.value.douban_id}`
|
||||||
router.push({
|
router.push({
|
||||||
path: '/resource',
|
path: '/resource',
|
||||||
query: {
|
query: {
|
||||||
keyword: `tmdb:${mediaDetail.value.tmdb_id}`,
|
keyword,
|
||||||
type: mediaDetail.value.type,
|
type: mediaDetail.value.type,
|
||||||
area,
|
area,
|
||||||
},
|
},
|
||||||
@@ -407,9 +420,9 @@ onBeforeMount(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" class="max-w-8xl mx-auto px-4">
|
<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">
|
<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>
|
||||||
<div class="vue-media-back absolute left-0 top-0 w-full h-96" />
|
<div class="vue-media-back absolute left-0 top-0 w-full h-96" />
|
||||||
</template>
|
</template>
|
||||||
@@ -445,7 +458,7 @@ onBeforeMount(() => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-actions">
|
<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">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon icon="mdi-magnify" />
|
<VIcon icon="mdi-magnify" />
|
||||||
</template>
|
</template>
|
||||||
@@ -471,7 +484,7 @@ onBeforeMount(() => {
|
|||||||
</VList>
|
</VList>
|
||||||
</VMenu>
|
</VMenu>
|
||||||
</VBtn>
|
</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" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon :icon="getSubscribeIcon" />
|
<VIcon :icon="getSubscribeIcon" />
|
||||||
</template>
|
</template>
|
||||||
@@ -499,10 +512,6 @@ onBeforeMount(() => {
|
|||||||
<span>{{ joinArray(director.roles) }}</span>
|
<span>{{ joinArray(director.roles) }}</span>
|
||||||
<a class="crew-name" :href="`${director.url}`" target="_blank">{{ director.name }}</a>
|
<a class="crew-name" :href="`${director.url}`" target="_blank">{{ director.name }}</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<a v-if="mediaDetail.tmdb_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getTheMovieDbLink()" target="_blank">
|
<a v-if="mediaDetail.tmdb_id" class="mb-2 mr-2 inline-flex last:mr-0" :href="getTheMovieDbLink()" target="_blank">
|
||||||
@@ -654,12 +663,56 @@ onBeforeMount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
<div v-if="mediaDetail.tmdb_id">
|
<div v-if="mediaDetail.tmdb_id">
|
||||||
<PersonCardSlideView
|
<PersonCardSlideView
|
||||||
:apipath="`tmdb/credits/${mediaDetail.tmdb_id}/${mediaProps.type}`"
|
: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="演员阵容"
|
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>
|
||||||
<div v-if="mediaDetail.tmdb_id">
|
<div v-if="mediaDetail.tmdb_id">
|
||||||
@@ -669,6 +722,13 @@ onBeforeMount(() => {
|
|||||||
title="推荐"
|
title="推荐"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div v-if="mediaDetail.tmdb_id">
|
||||||
<MediaCardSlideView
|
<MediaCardSlideView
|
||||||
:apipath="`tmdb/similar/${mediaDetail.tmdb_id}/${mediaProps.type}`"
|
:apipath="`tmdb/similar/${mediaDetail.tmdb_id}/${mediaProps.type}`"
|
||||||
@@ -682,7 +742,21 @@ onBeforeMount(() => {
|
|||||||
v-if="!mediaDetail.tmdb_id && !mediaDetail.douban_id && isRefreshed"
|
v-if="!mediaDetail.tmdb_id && !mediaDetail.douban_id && isRefreshed"
|
||||||
error-code="500"
|
error-code="500"
|
||||||
error-title="出错啦!"
|
error-title="出错啦!"
|
||||||
error-description="未识别到TMDB媒体信息。"
|
error-description="未识别到媒体信息。"
|
||||||
|
/>
|
||||||
|
<!-- 订阅编辑弹窗 -->
|
||||||
|
<SubscribeEditForm
|
||||||
|
v-model="subscribeEditDialog"
|
||||||
|
:subid="subscribeId"
|
||||||
|
@close="subscribeEditDialog = false"
|
||||||
|
@save="subscribeEditDialog = false"
|
||||||
|
@remove="() => {
|
||||||
|
subscribeEditDialog = false;
|
||||||
|
if (mediaDetail.type === '电影')
|
||||||
|
checkMovieSubscribed()
|
||||||
|
else
|
||||||
|
checkSeasonsSubscribed();
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { TmdbPerson } from '@/api/types'
|
import DoubanPersonCard from '@/components/cards/DoubanPersonCard.vue'
|
||||||
import PersonCard from '@/components/cards/PersonCard.vue'
|
import TmdbPersonCard from '@/components/cards/TmdbPersonCard.vue'
|
||||||
import NoDataFound from '@/components/NoDataFound.vue'
|
import NoDataFound from '@/components/NoDataFound.vue'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
apipath: String,
|
apipath: String,
|
||||||
params: Object as PropType<{ [key: string]: any }>,
|
params: Object as PropType<{ [key: string]: any }>,
|
||||||
|
type: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 判断是否有滚动条
|
// 判断是否有滚动条
|
||||||
@@ -29,8 +30,8 @@ const loading = ref(false)
|
|||||||
const isRefreshed = ref(false)
|
const isRefreshed = ref(false)
|
||||||
|
|
||||||
// 数据列表
|
// 数据列表
|
||||||
const dataList = ref<TmdbPerson[]>([])
|
const dataList = ref<any>([])
|
||||||
const currData = ref<TmdbPerson[]>([])
|
const currData = ref<any>([])
|
||||||
|
|
||||||
// 获取列表数据
|
// 获取列表数据
|
||||||
async function fetchData({ done }: { done: any }) {
|
async function fetchData({ done }: { done: any }) {
|
||||||
@@ -135,11 +136,22 @@ async function fetchData({ done }: { done: any }) {
|
|||||||
>
|
>
|
||||||
<template #loading />
|
<template #loading />
|
||||||
<div
|
<div
|
||||||
v-if="dataList.length > 0"
|
v-if="dataList.length > 0 && props.type === 'tmdb'"
|
||||||
class="grid gap-4 grid-media-card mx-3"
|
class="grid gap-4 grid-media-card mx-3"
|
||||||
tabindex="0"
|
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"
|
v-for="data in dataList"
|
||||||
:key="data.id"
|
:key="data.id"
|
||||||
:person="data"
|
:person="data"
|
||||||
@@ -149,7 +161,7 @@ async function fetchData({ done }: { done: any }) {
|
|||||||
v-if="dataList.length === 0 && isRefreshed"
|
v-if="dataList.length === 0 && isRefreshed"
|
||||||
error-code="404"
|
error-code="404"
|
||||||
error-title="没有数据"
|
error-title="没有数据"
|
||||||
error-description="无法获取到TMDB媒体信息。"
|
error-description="无法获取到媒体信息。"
|
||||||
/>
|
/>
|
||||||
</VInfiniteScroll>
|
</VInfiniteScroll>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import PersionCard from '@/components/cards/PersonCard.vue'
|
import TmdbPersonCard from '@/components/cards/TmdbPersonCard.vue'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { TmdbPerson } from '@/api/types'
|
|
||||||
import SlideView from '@/components/slide/SlideView.vue'
|
import SlideView from '@/components/slide/SlideView.vue'
|
||||||
|
import DoubanPersonCard from '@/components/cards/DoubanPersonCard.vue'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
apipath: String,
|
apipath: String,
|
||||||
linkurl: String,
|
linkurl: String,
|
||||||
title: String,
|
title: String,
|
||||||
|
type: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 组件加载完成
|
// 组件加载完成
|
||||||
const componentLoaded = ref(false)
|
const componentLoaded = ref(false)
|
||||||
|
|
||||||
// 数据列表
|
// 数据列表
|
||||||
const dataList = ref<TmdbPerson[]>([])
|
const dataList = ref<any>([])
|
||||||
|
|
||||||
// 获取订阅列表数据
|
// 获取订阅列表数据
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
@@ -46,7 +47,14 @@ onMounted(fetchData)
|
|||||||
v-for="data in dataList"
|
v-for="data in dataList"
|
||||||
:key="data.id"
|
:key="data.id"
|
||||||
>
|
>
|
||||||
<PersionCard
|
<TmdbPersonCard
|
||||||
|
v-if="props.type === 'tmdb'"
|
||||||
|
:person="data"
|
||||||
|
height="15rem"
|
||||||
|
width="10rem"
|
||||||
|
/>
|
||||||
|
<DoubanPersonCard
|
||||||
|
v-if="props.type === 'douban'"
|
||||||
:person="data"
|
:person="data"
|
||||||
height="15rem"
|
height="15rem"
|
||||||
width="10rem"
|
width="10rem"
|
||||||
|
|||||||
@@ -1,66 +1,33 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import api from '@/api'
|
|
||||||
import type { Context } from '@/api/types'
|
import type { Context } from '@/api/types'
|
||||||
import TorrentCard from '@/components/cards/TorrentCard.vue'
|
import TorrentCard from '@/components/cards/TorrentCard.vue'
|
||||||
import NoDataFound from '@/components/NoDataFound.vue'
|
|
||||||
import store from '@/store'
|
|
||||||
|
|
||||||
// 定义输入参数
|
|
||||||
const props = defineProps({
|
|
||||||
// 关键字或TMDBID
|
|
||||||
keyword: String,
|
|
||||||
|
|
||||||
// 类型
|
|
||||||
type: String,
|
|
||||||
|
|
||||||
// 搜索字段
|
|
||||||
area: String,
|
|
||||||
})
|
|
||||||
|
|
||||||
interface SearchTorrent extends Context {
|
interface SearchTorrent extends Context {
|
||||||
more?: Array<Context>
|
more?: Array<Context>
|
||||||
}
|
}
|
||||||
|
|
||||||
// 数据列表
|
// 定义输入参数
|
||||||
const dataList = ref <Array<SearchTorrent>>([])
|
const props = defineProps({
|
||||||
|
// 数据列表
|
||||||
// 分组后的数据列表
|
items: Array as PropType<SearchTorrent[]>,
|
||||||
const groupedDataList = ref<Map<string, Context[]>>()
|
})
|
||||||
|
|
||||||
// 是否刷新过
|
|
||||||
const isRefreshed = ref(false)
|
|
||||||
|
|
||||||
// 加载进度文本
|
|
||||||
const progressText = ref('')
|
|
||||||
|
|
||||||
// 加载进度
|
|
||||||
const progressValue = ref(0)
|
|
||||||
|
|
||||||
// 加载进度SSE
|
|
||||||
const progressEventSource = ref<EventSource>()
|
|
||||||
|
|
||||||
// 过滤表单
|
// 过滤表单
|
||||||
const filterForm = reactive({
|
const filterForm = reactive({
|
||||||
// 站点
|
// 站点
|
||||||
site: [] as string[],
|
site: [] as string[],
|
||||||
|
|
||||||
// 季
|
// 季
|
||||||
season: [] as string[],
|
season: [] as string[],
|
||||||
|
|
||||||
// 制作组
|
// 制作组
|
||||||
releaseGroup: [] as string[],
|
releaseGroup: [] as string[],
|
||||||
|
|
||||||
// 视频编码
|
// 视频编码
|
||||||
videoCode: [] as string[],
|
videoCode: [] as string[],
|
||||||
|
|
||||||
// 促销状态
|
// 促销状态
|
||||||
freeState: [] as string[],
|
freeState: [] as string[],
|
||||||
|
|
||||||
// 质量
|
// 质量
|
||||||
edition: [] as string[],
|
edition: [] as string[],
|
||||||
|
|
||||||
// 分辨率
|
// 分辨率
|
||||||
resolution: [] as string[],
|
resolution: [] as string[],
|
||||||
})
|
})
|
||||||
@@ -80,110 +47,13 @@ const editionFilterOptions = ref<Array<string>>([])
|
|||||||
// 获取分辨率过滤选项
|
// 获取分辨率过滤选项
|
||||||
const resolutionFilterOptions = ref<Array<string>>([])
|
const resolutionFilterOptions = ref<Array<string>>([])
|
||||||
|
|
||||||
// 按过滤项过滤卡片
|
// 数据列表
|
||||||
watchEffect(() => {
|
const dataList = ref <Array<SearchTorrent>>([])
|
||||||
// 清空数据
|
|
||||||
dataList.value.splice(0)
|
|
||||||
|
|
||||||
const match = (filter: Array<string>, value: string | undefined) =>
|
// 分组后的数据列表
|
||||||
filter.length === 0 || (value && filter.includes(value))
|
const groupedDataList = ref<Map<string, Context[]>>()
|
||||||
|
|
||||||
groupedDataList.value?.forEach((value) => {
|
|
||||||
if (value.length > 0) {
|
|
||||||
const matchData = value.filter((data) => {
|
|
||||||
const { meta_info, torrent_info } = data
|
|
||||||
// 季、制作组、视频编码
|
|
||||||
const { season_episode, resource_team, video_encode } = meta_info
|
|
||||||
return (
|
|
||||||
// 站点过滤
|
|
||||||
match(filterForm.site, torrent_info.site_name)
|
|
||||||
// 促销状态过滤
|
|
||||||
&& match(filterForm.freeState, torrent_info.volume_factor)
|
|
||||||
// 季过滤
|
|
||||||
&& match(filterForm.season, season_episode)
|
|
||||||
// 制作组过滤
|
|
||||||
&& match(filterForm.releaseGroup, resource_team)
|
|
||||||
// 视频编码过滤
|
|
||||||
&& match(filterForm.videoCode, video_encode)
|
|
||||||
// 分辨率过滤
|
|
||||||
&& match(filterForm.resolution, meta_info.resource_pix)
|
|
||||||
// 质量过滤
|
|
||||||
&& match(filterForm.edition, meta_info.edition)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
if (matchData.length > 0) {
|
|
||||||
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
|
|
||||||
if (matchData.length > 1)
|
|
||||||
firstData.more = matchData.slice(1)
|
|
||||||
|
|
||||||
dataList.value.push(firstData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取订阅列表数据
|
|
||||||
async function fetchData(): Promise<Array<Context>> {
|
|
||||||
try {
|
|
||||||
let searchData: Array<Context>
|
|
||||||
const keyword = props.keyword ?? ''
|
|
||||||
const mtype = props.type ?? ''
|
|
||||||
const area = props.area ?? ''
|
|
||||||
if (!keyword) {
|
|
||||||
// 查询上次搜索结果
|
|
||||||
searchData = await api.get('search/last')
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
startLoadingProgress()
|
|
||||||
// 优先按TMDBID精确查询
|
|
||||||
if (props.keyword?.startsWith('tmdb:') || props.keyword?.startsWith('douban:')) {
|
|
||||||
searchData = await api.get(`search/media/${props.keyword}`, {
|
|
||||||
params: {
|
|
||||||
mtype,
|
|
||||||
area,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// 按标题模糊查询
|
|
||||||
searchData = await api.get(`search/title/${props.keyword}`)
|
|
||||||
}
|
|
||||||
stopLoadingProgress()
|
|
||||||
}
|
|
||||||
isRefreshed.value = true
|
|
||||||
return Promise.resolve(searchData)
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initData() {
|
|
||||||
// load data
|
|
||||||
fetchData().then((data) => {
|
|
||||||
const groupMap = new Map<string, Context[]>()
|
|
||||||
|
|
||||||
data.forEach((item) => {
|
|
||||||
const { torrent_info } = item
|
|
||||||
// init options
|
|
||||||
initOptions(item)
|
|
||||||
// group data
|
|
||||||
const key = `${torrent_info.title}_${torrent_info.size}`
|
|
||||||
if (groupMap.has(key)) {
|
|
||||||
// 已存在相同标题和大小的分组,将当前上下文信息添加到分组中
|
|
||||||
const group = groupMap.get(key)
|
|
||||||
group?.push(item)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// 创建新的分组,并将当前上下文信息添加到分组中
|
|
||||||
groupMap.set(key, [item])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
groupedDataList.value = groupMap
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 初始化过滤选项
|
||||||
function initOptions(data: Context) {
|
function initOptions(data: Context) {
|
||||||
const { torrent_info, meta_info } = data
|
const { torrent_info, meta_info } = data
|
||||||
const optionValue = (options: Array<string>, value: string | undefined) => {
|
const optionValue = (options: Array<string>, value: string | undefined) => {
|
||||||
@@ -198,31 +68,69 @@ function initOptions(data: Context) {
|
|||||||
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
|
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用SSE监听加载进度
|
// 计算分组后的列表
|
||||||
function startLoadingProgress() {
|
watchEffect(() => {
|
||||||
progressText.value = '正在搜索,请稍候...'
|
// 数据分组
|
||||||
|
const groupMap = new Map<string, Context[]>()
|
||||||
const token = store.state.auth.token
|
// 遍历数据
|
||||||
|
props.items?.forEach((item) => {
|
||||||
progressEventSource.value = new EventSource(
|
const { torrent_info } = item
|
||||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/search?token=${token}`,
|
// init options
|
||||||
)
|
initOptions(item)
|
||||||
progressEventSource.value.onmessage = (event) => {
|
// group data
|
||||||
const progress = JSON.parse(event.data)
|
const key = `${torrent_info.title}_${torrent_info.size}`
|
||||||
if (progress) {
|
if (groupMap.has(key)) {
|
||||||
progressText.value = progress.text
|
// 已入库相同标题和大小的分组,将当前上下文信息添加到分组中
|
||||||
progressValue.value = progress.value
|
const group = groupMap.get(key)
|
||||||
|
group?.push(item)
|
||||||
}
|
}
|
||||||
}
|
else {
|
||||||
}
|
// 创建新的分组,并将当前上下文信息添加到分组中
|
||||||
|
groupMap.set(key, [item])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
groupedDataList.value = groupMap
|
||||||
|
})
|
||||||
|
|
||||||
// 停止监听加载进度
|
// 计算过滤后的列表
|
||||||
function stopLoadingProgress() {
|
watchEffect(() => {
|
||||||
progressEventSource.value?.close()
|
// 清空列表
|
||||||
}
|
dataList.value.splice(0)
|
||||||
|
// 匹配过滤函数
|
||||||
|
const match = (filter: Array<string>, value: string | undefined) =>
|
||||||
|
filter.length === 0 || (value && filter.includes(value))
|
||||||
|
|
||||||
// 加载时获取数据
|
groupedDataList.value?.forEach((value) => {
|
||||||
onMounted(initData)
|
if (value.length > 0) {
|
||||||
|
const matchData = value.filter((data) => {
|
||||||
|
const { meta_info, torrent_info } = data
|
||||||
|
// 季、制作组、视频编码
|
||||||
|
return (
|
||||||
|
// 站点过滤
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
if (matchData.length > 0) {
|
||||||
|
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
|
||||||
|
if (matchData.length > 1)
|
||||||
|
firstData.more = matchData.slice(1)
|
||||||
|
dataList.value.push(firstData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -307,20 +215,14 @@ onMounted(initData)
|
|||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</VCard>
|
</VCard>
|
||||||
<div v-if="!isRefreshed" class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center">
|
<div class="grid gap-3 grid-torrent-card items-start">
|
||||||
<VProgressCircular v-if="!props.keyword" size="48" indeterminate color="primary" />
|
<TorrentCard
|
||||||
<VProgressCircular v-if="props.keyword" class="mb-3" color="primary" :model-value="progressValue" size="64" />
|
v-for="(item, index) in dataList"
|
||||||
<span>{{ progressText }}</span>
|
:key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`"
|
||||||
|
:torrent="item"
|
||||||
|
:more="item.more"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="dataList.length > 0" class="grid gap-3 grid-torrent-card items-start">
|
|
||||||
<TorrentCard v-for="data in dataList" :key="`${data.torrent_info.title}_${data.torrent_info.site_name}_${data.torrent_info.page_url}`" :torrent="data" :more="data.more" />
|
|
||||||
</div>
|
|
||||||
<NoDataFound
|
|
||||||
v-if="dataList.length === 0 && isRefreshed"
|
|
||||||
error-code="404"
|
|
||||||
error-title="没有资源"
|
|
||||||
error-description="没有搜索到符合条件的资源。"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
245
src/views/discover/TorrentRowListView.vue
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Context } from '@/api/types'
|
||||||
|
import TorrentItem from '@/components/cards/TorrentItem.vue'
|
||||||
|
|
||||||
|
// 定义输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
// 数据列表
|
||||||
|
items: Array as PropType<Context[]>,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 过滤表单
|
||||||
|
const filterForm = reactive({
|
||||||
|
// 站点
|
||||||
|
site: [] as string[],
|
||||||
|
// 季
|
||||||
|
season: [] as string[],
|
||||||
|
// 制作组
|
||||||
|
releaseGroup: [] as string[],
|
||||||
|
// 视频编码
|
||||||
|
videoCode: [] as string[],
|
||||||
|
// 促销状态
|
||||||
|
freeState: [] as string[],
|
||||||
|
// 质量
|
||||||
|
edition: [] as string[],
|
||||||
|
// 分辨率
|
||||||
|
resolution: [] as string[],
|
||||||
|
})
|
||||||
|
|
||||||
|
// 数据列表
|
||||||
|
const dataList = ref <Array<Context>>([])
|
||||||
|
|
||||||
|
// 获取站点过滤选项
|
||||||
|
const siteFilterOptions = ref<Array<string>>([])
|
||||||
|
// 获取季过滤选项
|
||||||
|
const seasonFilterOptions = ref<Array<string>>([])
|
||||||
|
// 获取制作组过滤选项
|
||||||
|
const releaseGroupFilterOptions = ref<Array<string>>([])
|
||||||
|
// 获取视频编码过滤选项
|
||||||
|
const videoCodeFilterOptions = ref<Array<string>>([])
|
||||||
|
// 获取促销状态过滤选项
|
||||||
|
const freeStateFilterOptions = ref<Array<string>>([])
|
||||||
|
// 获取质量过滤选项
|
||||||
|
const editionFilterOptions = ref<Array<string>>([])
|
||||||
|
// 获取分辨率过滤选项
|
||||||
|
const resolutionFilterOptions = ref<Array<string>>([])
|
||||||
|
|
||||||
|
// 初始化过滤选项
|
||||||
|
function initOptions(data: Context) {
|
||||||
|
const { torrent_info, meta_info } = data
|
||||||
|
const optionValue = (options: Array<string>, value: string | undefined) => {
|
||||||
|
value && !options.includes(value) && options.push(value)
|
||||||
|
}
|
||||||
|
optionValue(siteFilterOptions.value, torrent_info?.site_name)
|
||||||
|
optionValue(seasonFilterOptions.value, meta_info?.season_episode)
|
||||||
|
optionValue(releaseGroupFilterOptions.value, meta_info?.resource_team)
|
||||||
|
optionValue(videoCodeFilterOptions.value, meta_info?.video_encode)
|
||||||
|
optionValue(freeStateFilterOptions.value, torrent_info?.volume_factor)
|
||||||
|
optionValue(editionFilterOptions.value, meta_info?.edition)
|
||||||
|
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算过滤后的列表
|
||||||
|
watchEffect(() => {
|
||||||
|
// 清空列表
|
||||||
|
dataList.value.splice(0)
|
||||||
|
// 匹配过滤函数
|
||||||
|
const match = (filter: Array<string>, value: string | undefined) =>
|
||||||
|
filter.length === 0 || (value && filter.includes(value))
|
||||||
|
|
||||||
|
props.items?.forEach((data) => {
|
||||||
|
const { meta_info, torrent_info } = data
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
dataList.value.push(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化过滤选项
|
||||||
|
onMounted(() => {
|
||||||
|
props.items?.forEach((item) => {
|
||||||
|
initOptions(item)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<VListItemTitle>没有附合当前过滤条件的资源。</VListItemTitle>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VCol>
|
||||||
|
<VCol
|
||||||
|
xl="2"
|
||||||
|
md="3"
|
||||||
|
class="d-none d-md-block"
|
||||||
|
>
|
||||||
|
<VList lines="one" class="rounded">
|
||||||
|
<VListSubheader v-if="siteFilterOptions.length > 0">
|
||||||
|
站点
|
||||||
|
</VListSubheader>
|
||||||
|
<VListItem>
|
||||||
|
<VChipGroup v-model="filterForm.site" column multiple>
|
||||||
|
<VChip
|
||||||
|
v-for="site in siteFilterOptions"
|
||||||
|
:key="site"
|
||||||
|
:color="filterForm.site.includes(site) ? 'primary' : ''"
|
||||||
|
filter
|
||||||
|
variant="outlined"
|
||||||
|
:value="site"
|
||||||
|
>
|
||||||
|
{{ site }}
|
||||||
|
</VChip>
|
||||||
|
</VChipGroup>
|
||||||
|
</VListItem>
|
||||||
|
<VListSubheader v-if="editionFilterOptions.length > 0">
|
||||||
|
质量
|
||||||
|
</VListSubheader>
|
||||||
|
<VListItem>
|
||||||
|
<VChipGroup v-model="filterForm.edition" column multiple>
|
||||||
|
<VChip
|
||||||
|
v-for="edition in editionFilterOptions"
|
||||||
|
:key="edition"
|
||||||
|
:color="filterForm.edition.includes(edition) ? 'primary' : ''"
|
||||||
|
filter
|
||||||
|
variant="outlined"
|
||||||
|
:value="edition"
|
||||||
|
>
|
||||||
|
{{ edition }}
|
||||||
|
</VChip>
|
||||||
|
</VChipGroup>
|
||||||
|
</VListItem>
|
||||||
|
<VListSubheader v-if="resolutionFilterOptions.length > 0">
|
||||||
|
分辨率
|
||||||
|
</VListSubheader>
|
||||||
|
<VListItem>
|
||||||
|
<VChipGroup v-model="filterForm.resolution" column multiple>
|
||||||
|
<VChip
|
||||||
|
v-for="resolution in resolutionFilterOptions"
|
||||||
|
:key="resolution"
|
||||||
|
:color="filterForm.resolution.includes(resolution) ? 'primary' : ''"
|
||||||
|
filter
|
||||||
|
variant="outlined"
|
||||||
|
:value="resolution"
|
||||||
|
>
|
||||||
|
{{ resolution }}
|
||||||
|
</VChip>
|
||||||
|
</VChipGroup>
|
||||||
|
</VListItem>
|
||||||
|
<VListSubheader v-if="releaseGroupFilterOptions.length > 0">
|
||||||
|
制作组
|
||||||
|
</VListSubheader>
|
||||||
|
<VListItem>
|
||||||
|
<VChipGroup v-model="filterForm.releaseGroup" column multiple>
|
||||||
|
<VChip
|
||||||
|
v-for="releaseGroup in releaseGroupFilterOptions"
|
||||||
|
:key="releaseGroup"
|
||||||
|
:color="filterForm.releaseGroup.includes(releaseGroup) ? 'primary' : ''"
|
||||||
|
filter
|
||||||
|
variant="outlined"
|
||||||
|
:value="releaseGroup"
|
||||||
|
>
|
||||||
|
{{ releaseGroup }}
|
||||||
|
</VChip>
|
||||||
|
</VChipGroup>
|
||||||
|
</VListItem>
|
||||||
|
<VListSubheader v-if="videoCodeFilterOptions.length > 0">
|
||||||
|
视频编码
|
||||||
|
</VListSubheader>
|
||||||
|
<VListItem>
|
||||||
|
<VChipGroup v-model="filterForm.videoCode" column multiple>
|
||||||
|
<VChip
|
||||||
|
v-for="videoCode in videoCodeFilterOptions"
|
||||||
|
:key="videoCode"
|
||||||
|
:color="filterForm.videoCode.includes(videoCode) ? 'primary' : ''"
|
||||||
|
filter
|
||||||
|
variant="outlined"
|
||||||
|
:value="videoCode"
|
||||||
|
>
|
||||||
|
{{ videoCode }}
|
||||||
|
</VChip>
|
||||||
|
</VChipGroup>
|
||||||
|
</VListItem>
|
||||||
|
<VListSubheader v-if="freeStateFilterOptions.length > 0">
|
||||||
|
促销状态
|
||||||
|
</VListSubheader>
|
||||||
|
<VListItem>
|
||||||
|
<VChipGroup v-model="filterForm.freeState" column multiple>
|
||||||
|
<VChip
|
||||||
|
v-for="freeState in freeStateFilterOptions"
|
||||||
|
:key="freeState"
|
||||||
|
:color="filterForm.freeState.includes(freeState) ? 'primary' : ''"
|
||||||
|
filter
|
||||||
|
variant="outlined"
|
||||||
|
:value="freeState"
|
||||||
|
>
|
||||||
|
{{ freeState }}
|
||||||
|
</VChip>
|
||||||
|
</VChipGroup>
|
||||||
|
</VListItem>
|
||||||
|
<VListSubheader v-if="seasonFilterOptions.length > 0">
|
||||||
|
季集
|
||||||
|
</VListSubheader>
|
||||||
|
<VListItem>
|
||||||
|
<VChipGroup v-model="filterForm.season" column multiple>
|
||||||
|
<VChip
|
||||||
|
v-for="season in seasonFilterOptions"
|
||||||
|
:key="season"
|
||||||
|
:color="filterForm.season.includes(season) ? 'primary' : ''"
|
||||||
|
filter
|
||||||
|
variant="outlined"
|
||||||
|
:value="season"
|
||||||
|
>
|
||||||
|
{{ season }}
|
||||||
|
</VChip>
|
||||||
|
</VChipGroup>
|
||||||
|
</VListItem>
|
||||||
|
</VList>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</template>
|
||||||
@@ -19,9 +19,9 @@ const getInstalledPluginList = computed(() => {
|
|||||||
return dataList.value.filter(item => item.installed)
|
return dataList.value.filter(item => item.installed)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取未安装的插件列表
|
// 获取未安装或者有更新的插件列表
|
||||||
const getUninstalledPluginList = computed(() => {
|
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
|
<VDialog
|
||||||
v-model="PluginAppDialog"
|
v-model="PluginAppDialog"
|
||||||
fullscreen
|
fullscreen
|
||||||
|
scrollable
|
||||||
:scrim="false"
|
:scrim="false"
|
||||||
transition="dialog-bottom-transition"
|
transition="dialog-bottom-transition"
|
||||||
>
|
>
|
||||||
<!-- Dialog Activator -->
|
<!-- Dialog Activator -->
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<VBtn
|
<VBtn
|
||||||
icon="mdi-plus"
|
icon="mdi-store-plus"
|
||||||
v-bind="props"
|
v-bind="props"
|
||||||
size="x-large"
|
size="x-large"
|
||||||
class="fixed right-5 bottom-5"
|
class="fixed right-5 bottom-5"
|
||||||
@@ -119,7 +120,7 @@ onBeforeMount(fetchData)
|
|||||||
</VToolbarItems>
|
</VToolbarItems>
|
||||||
</VToolbar>
|
</VToolbar>
|
||||||
</div>
|
</div>
|
||||||
<div class="pa-4">
|
<VCardText>
|
||||||
<div class="grid gap-4 grid-plugin-card">
|
<div class="grid gap-4 grid-plugin-card">
|
||||||
<PluginAppCard
|
<PluginAppCard
|
||||||
v-for="data in getUninstalledPluginList"
|
v-for="data in getUninstalledPluginList"
|
||||||
@@ -134,7 +135,7 @@ onBeforeMount(fetchData)
|
|||||||
error-title="没有未安装插件"
|
error-title="没有未安装插件"
|
||||||
error-description="所有可用插件均已安装。"
|
error-description="所有可用插件均已安装。"
|
||||||
/>
|
/>
|
||||||
</div>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import api from '@/api'
|
|||||||
import type { DownloadingInfo } from '@/api/types'
|
import type { DownloadingInfo } from '@/api/types'
|
||||||
import NoDataFound from '@/components/NoDataFound.vue'
|
import NoDataFound from '@/components/NoDataFound.vue'
|
||||||
import DownloadingCard from '@/components/cards/DownloadingCard.vue'
|
import DownloadingCard from '@/components/cards/DownloadingCard.vue'
|
||||||
|
import store from '@/store'
|
||||||
|
|
||||||
|
// 从Vuex Store中获取用户信息
|
||||||
|
const superUser = store.state.auth.superUser
|
||||||
|
const userName = store.state.auth.userName
|
||||||
|
|
||||||
// 定时器
|
// 定时器
|
||||||
let refreshTimer: NodeJS.Timer | null = null
|
let refreshTimer: NodeJS.Timer | null = null
|
||||||
@@ -35,6 +40,14 @@ function onRefresh() {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
|
||||||
|
const filteredDataList = computed(() => {
|
||||||
|
if (superUser)
|
||||||
|
return dataList.value
|
||||||
|
else
|
||||||
|
return dataList.value.filter(data => data.userid === userName)
|
||||||
|
})
|
||||||
|
|
||||||
// 加载时获取数据
|
// 加载时获取数据
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
@@ -71,17 +84,17 @@ onUnmounted(() => {
|
|||||||
@refresh="onRefresh"
|
@refresh="onRefresh"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="dataList.length > 0"
|
v-if="filteredDataList.length > 0"
|
||||||
class="grid gap-3 grid-downloading-card"
|
class="grid gap-3 grid-downloading-card"
|
||||||
>
|
>
|
||||||
<DownloadingCard
|
<DownloadingCard
|
||||||
v-for="data in dataList"
|
v-for="data in filteredDataList"
|
||||||
:key="data.hash"
|
:key="data.hash"
|
||||||
:info="data"
|
:info="data"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<NoDataFound
|
<NoDataFound
|
||||||
v-if="dataList.length === 0 && isRefreshed"
|
v-if="filteredDataList.length === 0 && isRefreshed"
|
||||||
error-code="404"
|
error-code="404"
|
||||||
error-title="没有任务"
|
error-title="没有任务"
|
||||||
error-description="正在下载的任务将会显示在这里。"
|
error-description="正在下载的任务将会显示在这里。"
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import { numberValidator } from '@/@validators'
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { TransferHistory } from '@/api/types'
|
import type { TransferHistory } from '@/api/types'
|
||||||
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
|
import ReorganizeForm from '@/components/form/ReorganizeForm.vue'
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
@@ -12,22 +11,15 @@ const $toast = useToast()
|
|||||||
// 重新整理对话框
|
// 重新整理对话框
|
||||||
const redoDialog = ref(false)
|
const redoDialog = ref(false)
|
||||||
|
|
||||||
// TMDB编号
|
|
||||||
const redoTmdbId = ref('')
|
|
||||||
|
|
||||||
// 类型
|
|
||||||
const redoType = ref('电影')
|
|
||||||
|
|
||||||
// 类型下拉框:电影、电视剧
|
|
||||||
const redoTypeItems = ref([
|
|
||||||
{ title: '自动', value: '' },
|
|
||||||
{ title: '电影', value: '电影' },
|
|
||||||
{ title: '电视剧', value: '电视剧' },
|
|
||||||
])
|
|
||||||
|
|
||||||
// 当前操作记录
|
// 当前操作记录
|
||||||
const currentHistory = ref<TransferHistory>()
|
const currentHistory = ref<TransferHistory>()
|
||||||
|
|
||||||
|
// 重新整理IDS
|
||||||
|
const redoIds = ref<number[]>([])
|
||||||
|
|
||||||
|
// 重新整理target
|
||||||
|
const redoTarget = ref('')
|
||||||
|
|
||||||
// 已选中的数据
|
// 已选中的数据
|
||||||
const selected = ref<TransferHistory[]>([])
|
const selected = ref<TransferHistory[]>([])
|
||||||
|
|
||||||
@@ -48,6 +40,9 @@ const dataList = ref<TransferHistory[]>([])
|
|||||||
// 搜索
|
// 搜索
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
|
|
||||||
|
// 搜索提示词列表
|
||||||
|
const searchHintList = ref<string[]>([])
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
@@ -55,7 +50,7 @@ const loading = ref(false)
|
|||||||
const totalItems = ref(0)
|
const totalItems = ref(0)
|
||||||
|
|
||||||
// 每页条数
|
// 每页条数
|
||||||
const itemsPerPage = ref(25)
|
const itemsPerPage = ref(50)
|
||||||
|
|
||||||
// 当前页码
|
// 当前页码
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
@@ -69,9 +64,6 @@ const progressText = ref('请稍候 ...')
|
|||||||
// 进度值
|
// 进度值
|
||||||
const progressValue = ref(0)
|
const progressValue = ref(0)
|
||||||
|
|
||||||
// TMDB选择对话框
|
|
||||||
const tmdbSelectorDialog = ref(false)
|
|
||||||
|
|
||||||
// 删除确认对话框
|
// 删除确认对话框
|
||||||
const deleteConfirmDialog = ref(false)
|
const deleteConfirmDialog = ref(false)
|
||||||
|
|
||||||
@@ -100,6 +92,7 @@ async function fetchData({
|
|||||||
|
|
||||||
dataList.value = result.data.list
|
dataList.value = result.data.list
|
||||||
totalItems.value = result.data.total
|
totalItems.value = result.data.total
|
||||||
|
searchHintList.value = [...new Set(dataList.value.map(item => item.title || ''))].filter(title => title !== '')
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -128,6 +121,8 @@ const TransferDict: { [key: string]: string } = {
|
|||||||
move: '移动',
|
move: '移动',
|
||||||
link: '硬链接',
|
link: '硬链接',
|
||||||
softlink: '软链接',
|
softlink: '软链接',
|
||||||
|
rclone_copy: 'Rclone复制',
|
||||||
|
rclone_move: 'Rclone移动',
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除历史记录
|
// 删除历史记录
|
||||||
@@ -219,91 +214,47 @@ async function removeHistoryBatch() {
|
|||||||
deleteConfirmDialog.value = true
|
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() {
|
async function retransferBatch() {
|
||||||
if (selected.value.length === 0)
|
if (selected.value.length === 0)
|
||||||
return
|
return
|
||||||
// 清空当前操作记录
|
// 清空当前操作记录
|
||||||
currentHistory.value = undefined
|
currentHistory.value = undefined
|
||||||
|
// 重新整理IDS
|
||||||
|
redoIds.value = selected.value.map(item => item.id)
|
||||||
|
// 重新整理target
|
||||||
|
if (selected.value.length === 1) {
|
||||||
|
// 目的目录
|
||||||
|
const dest = selected.value[0].dest ?? ''
|
||||||
|
// 类型
|
||||||
|
const mediaType = selected.value[0].type ?? ''
|
||||||
|
// 分类
|
||||||
|
const category = selected.value[0].category ?? ''
|
||||||
|
// 计算根路径
|
||||||
|
redoTarget.value = getRootPath(dest, mediaType, category)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
redoTarget.value = ''
|
||||||
|
}
|
||||||
// 打开识别弹窗
|
// 打开识别弹窗
|
||||||
redoType.value = ''
|
|
||||||
redoTmdbId.value = ''
|
|
||||||
redoDialog.value = true
|
redoDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调API重新整理
|
|
||||||
async function retransfer(item: TransferHistory, redoType = '', redoTmdbId = 0) {
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.post(
|
|
||||||
'history/transfer',
|
|
||||||
item,
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
mtype: redoType,
|
|
||||||
new_tmdbid: redoTmdbId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
fetchData({
|
|
||||||
page: currentPage.value,
|
|
||||||
itemsPerPage: itemsPerPage.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$toast.error(`重新整理失败: ${result.message}!`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重新整理
|
|
||||||
async function rehandleHistory() {
|
|
||||||
try {
|
|
||||||
// 关闭弹窗
|
|
||||||
redoDialog.value = false
|
|
||||||
|
|
||||||
let tmdbid = 0
|
|
||||||
|
|
||||||
if (redoTmdbId.value)
|
|
||||||
tmdbid = parseInt(redoTmdbId.value)
|
|
||||||
|
|
||||||
// 转移当前选中记录
|
|
||||||
if (currentHistory.value) {
|
|
||||||
$toast.info(`正在重新整理 ${currentHistory.value?.title} ...`)
|
|
||||||
await retransfer(currentHistory.value, redoType.value, tmdbid)
|
|
||||||
}
|
|
||||||
else if (selected.value.length > 0) {
|
|
||||||
// 总条数
|
|
||||||
const total = selected.value.length
|
|
||||||
if (total === 0)
|
|
||||||
return
|
|
||||||
// 已处理条数
|
|
||||||
let handled = 0
|
|
||||||
// 显示进度条
|
|
||||||
progressDialog.value = true
|
|
||||||
for (const item of selected.value) {
|
|
||||||
progressText.value = `正在重新整理 ${item.src} ...`
|
|
||||||
await retransfer(item, redoType.value, tmdbid)
|
|
||||||
handled++
|
|
||||||
progressValue.value = handled / total * 100
|
|
||||||
}
|
|
||||||
// 清空选中项
|
|
||||||
selected.value = []
|
|
||||||
// 隐藏进度条
|
|
||||||
progressDialog.value = false
|
|
||||||
}
|
|
||||||
// 批量转移
|
|
||||||
else { $toast.error('没有选中任何记录!') }
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 弹出菜单
|
// 弹出菜单
|
||||||
const dropdownItems = ref([
|
const dropdownItems = ref([
|
||||||
{
|
{
|
||||||
@@ -312,10 +263,9 @@ const dropdownItems = ref([
|
|||||||
props: {
|
props: {
|
||||||
prependIcon: 'mdi-redo-variant',
|
prependIcon: 'mdi-redo-variant',
|
||||||
click: (item: TransferHistory) => {
|
click: (item: TransferHistory) => {
|
||||||
redoTmdbId.value = ''
|
redoIds.value = [item.id]
|
||||||
redoType.value = ''
|
redoTarget.value = getRootPath(item.dest ?? '', item.type ?? '', item.category ?? '')
|
||||||
redoDialog.value = true
|
redoDialog.value = true
|
||||||
currentHistory.value = item
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -340,9 +290,10 @@ const dropdownItems = ref([
|
|||||||
<VRow>
|
<VRow>
|
||||||
<VCol> 历史记录 </VCol>
|
<VCol> 历史记录 </VCol>
|
||||||
<VCol>
|
<VCol>
|
||||||
<VTextField
|
<VCombobox
|
||||||
key="search_navbar"
|
key="search_navbar"
|
||||||
v-model="search"
|
v-model="search"
|
||||||
|
:items="searchHintList"
|
||||||
class="text-disabled"
|
class="text-disabled"
|
||||||
density="compact"
|
density="compact"
|
||||||
label="搜索"
|
label="搜索"
|
||||||
@@ -352,6 +303,7 @@ const dropdownItems = ref([
|
|||||||
hide-details
|
hide-details
|
||||||
flat
|
flat
|
||||||
rounded
|
rounded
|
||||||
|
clearable
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
@@ -442,47 +394,11 @@ const dropdownItems = ref([
|
|||||||
</template>
|
</template>
|
||||||
</VDataTableServer>
|
</VDataTableServer>
|
||||||
</VCard>
|
</VCard>
|
||||||
<VDialog
|
<!-- 底部操作按钮 -->
|
||||||
v-model="redoDialog"
|
<span
|
||||||
max-width="50rem"
|
v-if="selected.length > 0"
|
||||||
|
class="fixed right-5 bottom-5"
|
||||||
>
|
>
|
||||||
<!-- Dialog Content -->
|
|
||||||
<VCard title="重新整理">
|
|
||||||
<VCardText>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12" md="4">
|
|
||||||
<VSelect
|
|
||||||
v-model="redoType"
|
|
||||||
label="类型"
|
|
||||||
:items="redoTypeItems"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="8">
|
|
||||||
<VTextField
|
|
||||||
v-model="redoTmdbId"
|
|
||||||
label="TMDB编号"
|
|
||||||
placeholder="留空自动识别"
|
|
||||||
:disabled="redoType === ''"
|
|
||||||
:rules="[numberValidator]"
|
|
||||||
append-inner-icon="mdi-magnify"
|
|
||||||
@click:append-inner.stop="tmdbSelectorDialog = true"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VCardText>
|
|
||||||
|
|
||||||
<VCardActions>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn
|
|
||||||
@click="rehandleHistory"
|
|
||||||
@keydown.enter="rehandleHistory"
|
|
||||||
>
|
|
||||||
确定
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
<span v-if="selected.length > 0" class="fixed right-5 bottom-5">
|
|
||||||
<VBtn
|
<VBtn
|
||||||
icon="mdi-redo-variant"
|
icon="mdi-redo-variant"
|
||||||
class="me-2"
|
class="me-2"
|
||||||
@@ -497,39 +413,9 @@ const dropdownItems = ref([
|
|||||||
@click="removeHistoryBatch"
|
@click="removeHistoryBatch"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<!-- 进度框 -->
|
|
||||||
<vDialog
|
|
||||||
v-model="progressDialog"
|
|
||||||
:scrim="false"
|
|
||||||
width="25rem"
|
|
||||||
>
|
|
||||||
<vCard
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
<vCardText class="text-center">
|
|
||||||
{{ progressText }}
|
|
||||||
<vProgressLinear
|
|
||||||
color="white"
|
|
||||||
class="mb-0 mt-1"
|
|
||||||
:model-value="progressValue"
|
|
||||||
/>
|
|
||||||
</vCardText>
|
|
||||||
</vCard>
|
|
||||||
</vDialog>
|
|
||||||
<!-- TMDB ID搜索框 -->
|
|
||||||
<vDialog
|
|
||||||
v-model="tmdbSelectorDialog"
|
|
||||||
width="600"
|
|
||||||
scrollable
|
|
||||||
>
|
|
||||||
<TmdbSelectorCard
|
|
||||||
v-model="redoTmdbId"
|
|
||||||
@close="tmdbSelectorDialog = false"
|
|
||||||
/>
|
|
||||||
</vDialog>
|
|
||||||
<!-- 底部弹窗 -->
|
<!-- 底部弹窗 -->
|
||||||
<VBottomSheet v-model="deleteConfirmDialog" inset>
|
<VBottomSheet v-model="deleteConfirmDialog" inset>
|
||||||
<VCard class="text-center">
|
<VCard class="text-center rounded-t">
|
||||||
<DialogCloseBtn @click="deleteConfirmDialog = false" />
|
<DialogCloseBtn @click="deleteConfirmDialog = false" />
|
||||||
<VCardTitle class="pe-10">
|
<VCardTitle class="pe-10">
|
||||||
{{ confirmTitle }}
|
{{ confirmTitle }}
|
||||||
@@ -566,6 +452,24 @@ const dropdownItems = ref([
|
|||||||
</div>
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VBottomSheet>
|
</VBottomSheet>
|
||||||
|
<!-- 文件整理弹窗 -->
|
||||||
|
<ReorganizeForm
|
||||||
|
v-model="redoDialog"
|
||||||
|
:logids="redoIds"
|
||||||
|
:target="redoTarget"
|
||||||
|
@done="() => {
|
||||||
|
redoDialog = false
|
||||||
|
// 清空当前操作记录
|
||||||
|
currentHistory = undefined
|
||||||
|
selected = []
|
||||||
|
// 刷新
|
||||||
|
fetchData({
|
||||||
|
page: currentPage,
|
||||||
|
itemsPerPage,
|
||||||
|
})
|
||||||
|
}"
|
||||||
|
@close="redoDialog = false"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ onMounted(() => {
|
|||||||
<div>
|
<div>
|
||||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||||
<dt class="block text-sm font-bold">
|
<dt class="block text-sm font-bold">
|
||||||
当前版本
|
软件版本
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||||
<span class="flex-grow flex flex-row items-center truncate">
|
<span class="flex-grow flex flex-row items-center truncate">
|
||||||
@@ -98,6 +98,30 @@ onMounted(() => {
|
|||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||||
<dt class="block text-sm font-bold">
|
<dt class="block text-sm font-bold">
|
||||||
|
|||||||
@@ -147,6 +147,10 @@ async function deactivateUser(user: User) {
|
|||||||
|
|
||||||
// 新增用户
|
// 新增用户
|
||||||
async function addUser() {
|
async function addUser() {
|
||||||
|
if (!userForm.name || !userForm.password || !userForm.email) {
|
||||||
|
$toast.error('请填写完整信息!')
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.post('user', userForm)
|
const result: { [key: string]: any } = await api.post('user', userForm)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -447,6 +451,7 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="userForm.email"
|
v-model="userForm.email"
|
||||||
|
:rules="[requiredValidator]"
|
||||||
label="邮箱"
|
label="邮箱"
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import api from '@/api'
|
|
||||||
|
|
||||||
// 提示框
|
|
||||||
const $toast = useToast()
|
|
||||||
|
|
||||||
// 种子优先规则
|
|
||||||
const selectedTorrentPriority = ref<string>('seeder')
|
|
||||||
|
|
||||||
// 种子优先规则下拉框
|
|
||||||
const TorrentPriorityItems = [
|
|
||||||
{ title: '站点优先', value: 'site' },
|
|
||||||
{ title: '做种数优先', value: 'seeder' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 包含与排除规则
|
|
||||||
const defaultFilterRules = ref({
|
|
||||||
include: '',
|
|
||||||
exclude: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
// 查询种子优先规则
|
|
||||||
async function queryTorrentPriority() {
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.get(
|
|
||||||
'system/setting/TorrentsPriority',
|
|
||||||
)
|
|
||||||
|
|
||||||
selectedTorrentPriority.value = result.data?.value
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询包含与排除规则
|
|
||||||
async function queryDefaultFilter() {
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.get(
|
|
||||||
'system/setting/DefaultFilterRules',
|
|
||||||
)
|
|
||||||
if (result.data?.value)
|
|
||||||
defaultFilterRules.value = result.data?.value
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存种子优先规则
|
|
||||||
async function saveTorrentPriority() {
|
|
||||||
try {
|
|
||||||
// 用户名密码
|
|
||||||
const result: { [key: string]: any } = await api.post(
|
|
||||||
'system/setting/TorrentsPriority',
|
|
||||||
selectedTorrentPriority.value,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result.success)
|
|
||||||
$toast.success('优先规则保存成功')
|
|
||||||
else
|
|
||||||
$toast.error('优先规则保存失败!')
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存包含与排除规则
|
|
||||||
async function saveDefaultFilter() {
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.post(
|
|
||||||
'system/setting/DefaultFilterRules',
|
|
||||||
defaultFilterRules.value,
|
|
||||||
)
|
|
||||||
if (result.success)
|
|
||||||
$toast.success('默认包含/排除规则保存成功')
|
|
||||||
else
|
|
||||||
$toast.error('默认包含/排除规则保存失败!')
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
queryTorrentPriority()
|
|
||||||
queryDefaultFilter()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VCard title="下载优先规则">
|
|
||||||
<VCardSubtitle> 按站点优先级或资源种子数量排序和择优下载。 </VCardSubtitle>
|
|
||||||
<VCardText>
|
|
||||||
<VForm>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VSelect
|
|
||||||
v-model="selectedTorrentPriority"
|
|
||||||
:items="TorrentPriorityItems"
|
|
||||||
label="优先规则"
|
|
||||||
outlined
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</vform>
|
|
||||||
</VCardText>
|
|
||||||
<VCardItem>
|
|
||||||
<VBtn
|
|
||||||
type="submit"
|
|
||||||
@click="saveTorrentPriority"
|
|
||||||
>
|
|
||||||
保存
|
|
||||||
</VBtn>
|
|
||||||
</VCardItem>
|
|
||||||
</VCard>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VCard title="默认过滤规则">
|
|
||||||
<VCardSubtitle> 设置在搜索和订阅时默认使用的过滤规则。 </VCardSubtitle>
|
|
||||||
<VCardText>
|
|
||||||
<VForm>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="defaultFilterRules.include"
|
|
||||||
type="text"
|
|
||||||
label="包含(关键字、正则式)"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12" md="6">
|
|
||||||
<VTextField
|
|
||||||
v-model="defaultFilterRules.exclude"
|
|
||||||
type="text"
|
|
||||||
label="排除(关键字、正则式)"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
<VCardItem>
|
|
||||||
<VBtn
|
|
||||||
type="submit"
|
|
||||||
@click="saveDefaultFilter"
|
|
||||||
>
|
|
||||||
保存
|
|
||||||
</VBtn>
|
|
||||||
</VCardItem>
|
|
||||||
</VCard>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.grid-filterrule-card {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
|
||||||
padding-block-end: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -3,6 +3,8 @@ import { useToast } from 'vue-toast-notification'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
||||||
import type { Site } from '@/api/types'
|
import type { Site } from '@/api/types'
|
||||||
|
import { copyToClipboard } from '@/@core/utils/navigator'
|
||||||
|
import ImportCodeForm from '@/components/form/ImportCodeForm.vue'
|
||||||
|
|
||||||
// 规则卡片类型
|
// 规则卡片类型
|
||||||
interface FilterCard {
|
interface FilterCard {
|
||||||
@@ -24,6 +26,18 @@ const allSites = ref<Site[]>([])
|
|||||||
// 选中订阅站点
|
// 选中订阅站点
|
||||||
const selectedSites = ref<number[]>([])
|
const selectedSites = ref<number[]>([])
|
||||||
|
|
||||||
|
// 包含与排除规则
|
||||||
|
const defaultFilterRules = ref({
|
||||||
|
include: '',
|
||||||
|
exclude: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 导入代码弹窗
|
||||||
|
const importCodeDialog = ref(false)
|
||||||
|
|
||||||
|
// 导入的代码
|
||||||
|
const importCodeString = ref('')
|
||||||
|
|
||||||
// 查询已设置优先级规则
|
// 查询已设置优先级规则
|
||||||
async function queryCustomFilters() {
|
async function queryCustomFilters() {
|
||||||
try {
|
try {
|
||||||
@@ -190,9 +204,84 @@ function onLevelDown(pri: string) {
|
|||||||
filterCards.value.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
|
filterCards.value.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询包含与排除规则
|
||||||
|
async function queryDefaultFilter() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get(
|
||||||
|
'system/setting/DefaultSearchFilterRules',
|
||||||
|
)
|
||||||
|
if (result.data?.value)
|
||||||
|
defaultFilterRules.value = result.data?.value
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存包含与排除规则
|
||||||
|
async function saveDefaultFilter() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.post(
|
||||||
|
'system/setting/DefaultSearchFilterRules',
|
||||||
|
defaultFilterRules.value,
|
||||||
|
)
|
||||||
|
if (result.success)
|
||||||
|
$toast.success('默认包含/排除规则保存成功')
|
||||||
|
else
|
||||||
|
$toast.error('默认包含/排除规则保存失败!')
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分享规则
|
||||||
|
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(() => {
|
onMounted(() => {
|
||||||
queryCustomFilters()
|
queryCustomFilters()
|
||||||
querySites()
|
querySites()
|
||||||
|
queryDefaultFilter()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -226,6 +315,36 @@ onMounted(() => {
|
|||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
<VCard title="搜索优先级">
|
<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>
|
<VCardSubtitle> 设置在搜索时默认使用的优先级排序,未在优先级中的资源将不在搜索结果中显示。 </VCardSubtitle>
|
||||||
<VCardItem>
|
<VCardItem>
|
||||||
<div class="grid gap-3 grid-filterrule-card">
|
<div class="grid gap-3 grid-filterrule-card">
|
||||||
@@ -260,7 +379,51 @@ onMounted(() => {
|
|||||||
</VCardItem>
|
</VCardItem>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VCard title="默认过滤规则">
|
||||||
|
<VCardSubtitle> 设置在搜索时默认使用的过滤规则。 </VCardSubtitle>
|
||||||
|
<VCardText>
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="defaultFilterRules.include"
|
||||||
|
type="text"
|
||||||
|
label="包含(关键字、正则式)"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="defaultFilterRules.exclude"
|
||||||
|
type="text"
|
||||||
|
label="排除(关键字、正则式)"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
<VCardItem>
|
||||||
|
<VBtn
|
||||||
|
type="submit"
|
||||||
|
@click="saveDefaultFilter"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</VBtn>
|
||||||
|
</VCardItem>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
|
<VDialog
|
||||||
|
v-model="importCodeDialog"
|
||||||
|
width="60rem"
|
||||||
|
scrollable
|
||||||
|
>
|
||||||
|
<ImportCodeForm
|
||||||
|
v-model="importCodeString"
|
||||||
|
title="导入优先级规则"
|
||||||
|
@close="importCodeDialog = false"
|
||||||
|
/>
|
||||||
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ const resetSitesText = ref('重置站点数据')
|
|||||||
// 站点重置按钮可用状态
|
// 站点重置按钮可用状态
|
||||||
const resetSitesDisabled = ref(false)
|
const resetSitesDisabled = ref(false)
|
||||||
|
|
||||||
|
// 种子优先规则
|
||||||
|
const selectedTorrentPriority = ref<string>('seeder')
|
||||||
|
|
||||||
|
// 种子优先规则下拉框
|
||||||
|
const TorrentPriorityItems = [
|
||||||
|
{ title: '站点优先', value: 'site' },
|
||||||
|
{ title: '做种数优先', value: 'seeder' },
|
||||||
|
]
|
||||||
|
|
||||||
// 重置站点
|
// 重置站点
|
||||||
async function resetSites() {
|
async function resetSites() {
|
||||||
try {
|
try {
|
||||||
@@ -34,10 +43,74 @@ async function resetSites() {
|
|||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询种子优先规则
|
||||||
|
async function queryTorrentPriority() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get(
|
||||||
|
'system/setting/TorrentsPriority',
|
||||||
|
)
|
||||||
|
|
||||||
|
selectedTorrentPriority.value = result.data?.value
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存种子优先规则
|
||||||
|
async function saveTorrentPriority() {
|
||||||
|
try {
|
||||||
|
// 用户名密码
|
||||||
|
const result: { [key: string]: any } = await api.post(
|
||||||
|
'system/setting/TorrentsPriority',
|
||||||
|
selectedTorrentPriority.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.success)
|
||||||
|
$toast.success('优先规则保存成功')
|
||||||
|
else
|
||||||
|
$toast.error('优先规则保存失败!')
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
queryTorrentPriority()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VRow>
|
<VRow>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VCard title="下载优先规则">
|
||||||
|
<VCardSubtitle> 按站点或做种数量优先下载。 </VCardSubtitle>
|
||||||
|
<VCardText>
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSelect
|
||||||
|
v-model="selectedTorrentPriority"
|
||||||
|
:items="TorrentPriorityItems"
|
||||||
|
label="优先规则"
|
||||||
|
outlined
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
<VCardItem>
|
||||||
|
<VBtn
|
||||||
|
type="submit"
|
||||||
|
@click="saveTorrentPriority"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</VBtn>
|
||||||
|
</VCardItem>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
<VCard title="站点重置">
|
<VCard title="站点重置">
|
||||||
<VCardText>
|
<VCardText>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { useToast } from 'vue-toast-notification'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
|
||||||
import type { Site } from '@/api/types'
|
import type { Site } from '@/api/types'
|
||||||
|
import { copyToClipboard } from '@/@core/utils/navigator'
|
||||||
|
import ImportCodeForm from '@/components/form/ImportCodeForm.vue'
|
||||||
|
|
||||||
// 规则卡片类型
|
// 规则卡片类型
|
||||||
interface FilterCard {
|
interface FilterCard {
|
||||||
@@ -27,6 +29,21 @@ const allSites = ref<Site[]>([])
|
|||||||
// 选中订阅站点
|
// 选中订阅站点
|
||||||
const selectedRssSites = ref<number[]>([])
|
const selectedRssSites = ref<number[]>([])
|
||||||
|
|
||||||
|
// 当前规则类型
|
||||||
|
const currentRuleType = ref('SubscribeFilterRules')
|
||||||
|
|
||||||
|
// 包含与排除规则
|
||||||
|
const defaultFilterRules = ref({
|
||||||
|
include: '',
|
||||||
|
exclude: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 导入代码弹窗
|
||||||
|
const importCodeDialog = ref(false)
|
||||||
|
|
||||||
|
// 导入的代码
|
||||||
|
const importCodeString = ref('')
|
||||||
|
|
||||||
// 查询用户选中的订阅站点
|
// 查询用户选中的订阅站点
|
||||||
async function querySelectedRssSites() {
|
async function querySelectedRssSites() {
|
||||||
try {
|
try {
|
||||||
@@ -207,10 +224,102 @@ function onLevelDown(filterCards: FilterCard[], pri: string) {
|
|||||||
filterCards.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
|
filterCards.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询包含与排除规则
|
||||||
|
async function queryDefaultFilter() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get(
|
||||||
|
'system/setting/DefaultFilterRules',
|
||||||
|
)
|
||||||
|
if (result.data?.value)
|
||||||
|
defaultFilterRules.value = result.data?.value
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存包含与排除规则
|
||||||
|
async function saveDefaultFilter() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.post(
|
||||||
|
'system/setting/DefaultFilterRules',
|
||||||
|
defaultFilterRules.value,
|
||||||
|
)
|
||||||
|
if (result.success)
|
||||||
|
$toast.success('默认包含/排除规则保存成功')
|
||||||
|
else
|
||||||
|
$toast.error('默认包含/排除规则保存失败!')
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分享规则
|
||||||
|
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(() => {
|
onMounted(() => {
|
||||||
querySites()
|
querySites()
|
||||||
queryCustomFilters('SubscribeFilterRules')
|
queryCustomFilters('SubscribeFilterRules')
|
||||||
queryCustomFilters('BestVersionFilterRules')
|
queryCustomFilters('BestVersionFilterRules')
|
||||||
|
queryDefaultFilter()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -244,6 +353,36 @@ onMounted(() => {
|
|||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
<VCard title="订阅优先级">
|
<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>
|
<VCardSubtitle> 设置在正常订阅时默认使用的优先级,未在优先级中的资源将不会自动下载。 </VCardSubtitle>
|
||||||
<VCardItem>
|
<VCardItem>
|
||||||
<div class="grid gap-3 grid-filterrule-card">
|
<div class="grid gap-3 grid-filterrule-card">
|
||||||
@@ -280,6 +419,36 @@ onMounted(() => {
|
|||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
<VCard title="洗版优先级">
|
<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>
|
<VCardSubtitle> 设置在订阅洗版时使用的优先级,匹配优先级1时洗版完成。 </VCardSubtitle>
|
||||||
<VCardItem>
|
<VCardItem>
|
||||||
<div class="grid gap-3 grid-filterrule-card">
|
<div class="grid gap-3 grid-filterrule-card">
|
||||||
@@ -314,7 +483,51 @@ onMounted(() => {
|
|||||||
</VCardItem>
|
</VCardItem>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VCard title="默认过滤规则">
|
||||||
|
<VCardSubtitle> 设置在订阅时默认使用的过滤规则。 </VCardSubtitle>
|
||||||
|
<VCardText>
|
||||||
|
<VForm>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="defaultFilterRules.include"
|
||||||
|
type="text"
|
||||||
|
label="包含(关键字、正则式)"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="defaultFilterRules.exclude"
|
||||||
|
type="text"
|
||||||
|
label="排除(关键字、正则式)"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VForm>
|
||||||
|
</VCardText>
|
||||||
|
<VCardItem>
|
||||||
|
<VBtn
|
||||||
|
type="submit"
|
||||||
|
@click="saveDefaultFilter"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</VBtn>
|
||||||
|
</VCardItem>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
|
<VDialog
|
||||||
|
v-model="importCodeDialog"
|
||||||
|
width="60rem"
|
||||||
|
scrollable
|
||||||
|
>
|
||||||
|
<ImportCodeForm
|
||||||
|
v-model="importCodeString"
|
||||||
|
title="导入优先级规则"
|
||||||
|
@close="importCodeDialog = false"
|
||||||
|
/>
|
||||||
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ const customIdentifiers = ref('')
|
|||||||
// 自定义制作组
|
// 自定义制作组
|
||||||
const customReleaseGroups = ref('')
|
const customReleaseGroups = ref('')
|
||||||
|
|
||||||
|
// 自定义占位符
|
||||||
|
const customization = ref('')
|
||||||
|
|
||||||
// 文件整理屏蔽词
|
// 文件整理屏蔽词
|
||||||
const transferExcludeWords = ref('')
|
const transferExcludeWords = ref('')
|
||||||
|
|
||||||
@@ -42,6 +45,20 @@ async function queryCustomReleaseGroups() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询已设置的自定义占位符
|
||||||
|
async function queryCustomization() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get(
|
||||||
|
'system/setting/Customization',
|
||||||
|
)
|
||||||
|
|
||||||
|
customization.value = result.data?.value.join('\n')
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 查询已设置的屏蔽词
|
// 查询已设置的屏蔽词
|
||||||
async function queryTransferExcludeWords() {
|
async function queryTransferExcludeWords() {
|
||||||
try {
|
try {
|
||||||
@@ -94,6 +111,25 @@ async function saveCustomReleaseGroups() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存自定义占位符
|
||||||
|
async function saveCustomization() {
|
||||||
|
try {
|
||||||
|
// 用户名密码
|
||||||
|
const result: { [key: string]: any } = await api.post(
|
||||||
|
'system/setting/Customization',
|
||||||
|
customization.value.split('\n'),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.success)
|
||||||
|
$toast.success('自定义占位符保存成功')
|
||||||
|
else
|
||||||
|
$toast.error('自定义占位符保存失败!')
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 保存文件整理屏蔽词
|
// 保存文件整理屏蔽词
|
||||||
async function saveTransferExcludeWords() {
|
async function saveTransferExcludeWords() {
|
||||||
try {
|
try {
|
||||||
@@ -116,6 +152,7 @@ async function saveTransferExcludeWords() {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
queryCustomIdentifiers()
|
queryCustomIdentifiers()
|
||||||
queryCustomReleaseGroups()
|
queryCustomReleaseGroups()
|
||||||
|
queryCustomization()
|
||||||
queryTransferExcludeWords()
|
queryTransferExcludeWords()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -129,13 +166,26 @@ onMounted(() => {
|
|||||||
<VTextarea
|
<VTextarea
|
||||||
v-model="customIdentifiers"
|
v-model="customIdentifiers"
|
||||||
auto-grow
|
auto-grow
|
||||||
placeholder="支持正则表达式,特殊字符需要\转义,一行为一组,支持以下几种配置格式:
|
placeholder="支持正则表达式,特殊字符需要\转义,一行为一组"
|
||||||
屏蔽词
|
|
||||||
被替换词 => 替换词
|
|
||||||
前定位词 <> 后定位词 >> 集偏移量(EP)
|
|
||||||
被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量(EP)"
|
|
||||||
/>
|
/>
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
|
<VCardItem>
|
||||||
|
<VAlert
|
||||||
|
type="info"
|
||||||
|
variant="tonal"
|
||||||
|
title="支持的配置格式(注意空格):"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-html="`
|
||||||
|
屏蔽词<br>
|
||||||
|
被替换词 => 替换词<br>
|
||||||
|
前定位词 <> 后定位词 >> 集偏移量(EP)<br>
|
||||||
|
被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量(EP)<br>
|
||||||
|
其中替换词支持格式:{[tmdbid/doubanid=xxx;type=movie/tv;s=xxx;e=xxx]} 直接指定TMDBID/豆瓣ID识别,其中s、e为季数和集数(可选)<br>
|
||||||
|
`"
|
||||||
|
/>
|
||||||
|
</VAlert>
|
||||||
|
</VCardItem>
|
||||||
<VCardItem>
|
<VCardItem>
|
||||||
<VBtn
|
<VBtn
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -166,6 +216,26 @@ onMounted(() => {
|
|||||||
</VCardItem>
|
</VCardItem>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VCard title="自定义占位符">
|
||||||
|
<VCardSubtitle> 添加自定义占位符识别正则,重命名格式中添加{customization}使用。 </VCardSubtitle>
|
||||||
|
<VCardItem>
|
||||||
|
<VTextarea
|
||||||
|
v-model="customization"
|
||||||
|
auto-grow
|
||||||
|
placeholder="多个匹配对象请换行分隔,支持正则表达式,特殊字符注意转义"
|
||||||
|
/>
|
||||||
|
</VCardItem>
|
||||||
|
<VCardItem>
|
||||||
|
<VBtn
|
||||||
|
type="submit"
|
||||||
|
@click="saveCustomization"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</VBtn>
|
||||||
|
</VCardItem>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
<VCard title="文件整理屏蔽词">
|
<VCard title="文件整理屏蔽词">
|
||||||
<VCardSubtitle> 目录名或文件名中包含屏蔽词时不进行整理。 </VCardSubtitle>
|
<VCardSubtitle> 目录名或文件名中包含屏蔽词时不进行整理。 </VCardSubtitle>
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { Site } from '@/api/types'
|
import type { Site } from '@/api/types'
|
||||||
import SiteCard from '@/components/cards/SiteCard.vue'
|
import SiteCard from '@/components/cards/SiteCard.vue'
|
||||||
import NoDataFound from '@/components/NoDataFound.vue'
|
import NoDataFound from '@/components/NoDataFound.vue'
|
||||||
import { numberValidator, requiredValidator } from '@/@validators'
|
import SiteAddEditForm from '@/components/form/SiteAddEditForm.vue'
|
||||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
|
||||||
|
|
||||||
// 提示框
|
|
||||||
const $toast = useToast()
|
|
||||||
|
|
||||||
// 数据列表
|
// 数据列表
|
||||||
const dataList = ref<Site[]>([])
|
const dataList = ref<Site[]>([])
|
||||||
@@ -16,45 +11,9 @@ const dataList = ref<Site[]>([])
|
|||||||
// 是否刷新过
|
// 是否刷新过
|
||||||
const isRefreshed = ref(false)
|
const isRefreshed = ref(false)
|
||||||
|
|
||||||
// 新增按钮文本
|
|
||||||
const addBtnText = ref('新增站点')
|
|
||||||
// 新增按钮状态
|
|
||||||
const addBtnState = ref(false)
|
|
||||||
|
|
||||||
// 新增站点对话框
|
// 新增站点对话框
|
||||||
const siteAddDialog = ref(false)
|
const siteAddDialog = ref(false)
|
||||||
|
|
||||||
// 状态下拉项
|
|
||||||
const statusItems = [
|
|
||||||
{ title: '启用', value: true },
|
|
||||||
{ title: '停用', value: false },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 生成1到50的优先级下拉框选项
|
|
||||||
const priorityItems = ref(
|
|
||||||
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
|
|
||||||
title: item,
|
|
||||||
value: item,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
|
|
||||||
// 站点编辑表单数据
|
|
||||||
const siteForm = reactive<Site>({
|
|
||||||
id: 0,
|
|
||||||
url: '',
|
|
||||||
pri: 1,
|
|
||||||
is_active: true,
|
|
||||||
cookie: '',
|
|
||||||
ua: '',
|
|
||||||
limit_interval: 0,
|
|
||||||
limit_seconds: 0,
|
|
||||||
limit_count: 0,
|
|
||||||
proxy: 0,
|
|
||||||
render: 0,
|
|
||||||
name: '',
|
|
||||||
domain: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取站点列表数据
|
// 获取站点列表数据
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
try {
|
try {
|
||||||
@@ -66,38 +25,6 @@ async function fetchData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用API 新增站点
|
|
||||||
async function addSite() {
|
|
||||||
if (!siteForm.url)
|
|
||||||
return
|
|
||||||
|
|
||||||
startNProgress()
|
|
||||||
|
|
||||||
addBtnText.value = '新增中...'
|
|
||||||
addBtnState.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: string } = await api.post('site/', siteForm)
|
|
||||||
if (result.success) {
|
|
||||||
$toast.success('新增站点成功')
|
|
||||||
|
|
||||||
// 刷新数据
|
|
||||||
fetchData()
|
|
||||||
}
|
|
||||||
|
|
||||||
else { $toast.error(`新增站点失败:${result.message}`) }
|
|
||||||
siteAddDialog.value = false
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
doneNProgress()
|
|
||||||
|
|
||||||
addBtnText.value = '新增站点'
|
|
||||||
addBtnState.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载时获取数据
|
// 加载时获取数据
|
||||||
onBeforeMount(fetchData)
|
onBeforeMount(fetchData)
|
||||||
</script>
|
</script>
|
||||||
@@ -132,150 +59,20 @@ onBeforeMount(fetchData)
|
|||||||
error-title="没有站点"
|
error-title="没有站点"
|
||||||
error-description="已添加并支持的站点将会在这里显示。"
|
error-description="已添加并支持的站点将会在这里显示。"
|
||||||
/>
|
/>
|
||||||
<!-- Dialog Content -->
|
<!-- 新增站点按钮 -->
|
||||||
<VDialog
|
<VBtn
|
||||||
|
icon="mdi-plus"
|
||||||
|
size="x-large"
|
||||||
|
class="fixed right-5 bottom-5"
|
||||||
|
oper="add"
|
||||||
|
@click="siteAddDialog = true"
|
||||||
|
/>
|
||||||
|
<SiteAddEditForm
|
||||||
v-model="siteAddDialog"
|
v-model="siteAddDialog"
|
||||||
max-width="50rem"
|
oper="add"
|
||||||
persistent
|
@save="siteAddDialog = false; fetchData()"
|
||||||
scrollable
|
@close="siteAddDialog = false"
|
||||||
>
|
/>
|
||||||
<!-- Dialog Activator -->
|
|
||||||
<template #activator="{ props }">
|
|
||||||
<VBtn
|
|
||||||
icon="mdi-plus"
|
|
||||||
v-bind="props"
|
|
||||||
size="x-large"
|
|
||||||
class="fixed right-5 bottom-5"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<VCard title="新增站点">
|
|
||||||
<DialogCloseBtn @click="siteAddDialog = false" />
|
|
||||||
<VCardText class="pt-2">
|
|
||||||
<VForm @submit.prevent="() => {}">
|
|
||||||
<VRow>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<VTextField
|
|
||||||
v-model="siteForm.url"
|
|
||||||
label="站点地址"
|
|
||||||
:rules="[requiredValidator]"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="3"
|
|
||||||
>
|
|
||||||
<VSelect
|
|
||||||
v-model="siteForm.pri"
|
|
||||||
label="优先级"
|
|
||||||
:items="priorityItems"
|
|
||||||
:rules="[requiredValidator]"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="3"
|
|
||||||
>
|
|
||||||
<VSelect
|
|
||||||
v-model="siteForm.is_active"
|
|
||||||
:items="statusItems"
|
|
||||||
label="状态"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextField
|
|
||||||
v-model="siteForm.rss"
|
|
||||||
label="RSS地址"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextarea
|
|
||||||
v-model="siteForm.cookie"
|
|
||||||
label="站点Cookie"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol cols="12">
|
|
||||||
<VTextField
|
|
||||||
v-model="siteForm.ua"
|
|
||||||
label="站点User-Agent"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="4"
|
|
||||||
>
|
|
||||||
<VTextField
|
|
||||||
v-model="siteForm.limit_interval"
|
|
||||||
label="单位周期(秒)"
|
|
||||||
:rules="[numberValidator]"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="4"
|
|
||||||
>
|
|
||||||
<VTextField
|
|
||||||
v-model="siteForm.limit_seconds"
|
|
||||||
label="访问次数"
|
|
||||||
:rules="[numberValidator]"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="4"
|
|
||||||
>
|
|
||||||
<VTextField
|
|
||||||
v-model="siteForm.limit_seconds"
|
|
||||||
label="访问间隔(秒)"
|
|
||||||
:rules="[numberValidator]"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
<VRow>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<VSwitch
|
|
||||||
v-model="siteForm.proxy"
|
|
||||||
label="代理"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
<VCol
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<VSwitch
|
|
||||||
v-model="siteForm.render"
|
|
||||||
label="仿真"
|
|
||||||
/>
|
|
||||||
</VCol>
|
|
||||||
</VRow>
|
|
||||||
</VForm>
|
|
||||||
</VCardText>
|
|
||||||
<VCardActions>
|
|
||||||
<VBtn
|
|
||||||
@click="siteAddDialog = false"
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</VBtn>
|
|
||||||
<VSpacer />
|
|
||||||
<VBtn
|
|
||||||
color="primary"
|
|
||||||
:disabled="addBtnState"
|
|
||||||
@click="addSite"
|
|
||||||
>
|
|
||||||
{{ addBtnText }}
|
|
||||||
</VBtn>
|
|
||||||
</VCardActions>
|
|
||||||
</VCard>
|
|
||||||
</VDialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||