Compare commits
384 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9866a04df | ||
|
|
4f5193d602 | ||
|
|
37b92c55ba | ||
|
|
9299f1bcb6 | ||
|
|
7fe12192df | ||
|
|
1169644ab3 | ||
|
|
6f7770ed43 | ||
|
|
8059fd6f90 | ||
|
|
556dbd8d78 | ||
|
|
6695fd8c14 | ||
|
|
3ab0229275 | ||
|
|
99467127a0 | ||
|
|
90d73b7bd5 | ||
|
|
2e326e1798 | ||
|
|
251eac93c7 | ||
|
|
c74d70808c | ||
|
|
e63b2d7152 | ||
|
|
16b29b56a5 | ||
|
|
6d79c4fe2f | ||
|
|
4b1fb60ee3 | ||
|
|
1d2be54f9e | ||
|
|
83547e32db | ||
|
|
70ddb929f2 | ||
|
|
8b22961394 | ||
|
|
c15d42c179 | ||
|
|
098e473cab | ||
|
|
f6f3d9368a | ||
|
|
9558a420e9 | ||
|
|
4d3b69ca34 | ||
|
|
fdcc4a44c8 | ||
|
|
5de0494538 | ||
|
|
2045f833e4 | ||
|
|
cc4f89aac1 | ||
|
|
1c2f2c17d4 | ||
|
|
ace7a6621f | ||
|
|
d02fe55a1e | ||
|
|
9b753a8f5b | ||
|
|
11e82582b8 | ||
|
|
419358863e | ||
|
|
1d0d7f9975 | ||
|
|
c5f564372b | ||
|
|
a50f0cd727 | ||
|
|
96f6f55138 | ||
|
|
6a45c8b358 | ||
|
|
165937596e | ||
|
|
fb976f043b | ||
|
|
ecb9c4e51a | ||
|
|
9e8c3b495c | ||
|
|
24a37fc33c | ||
|
|
d09a21114d | ||
|
|
6e2b12501f | ||
|
|
2a56e116cf | ||
|
|
6de4f238d8 | ||
|
|
1b426c5957 | ||
|
|
82454a650c | ||
|
|
227b6bd7ef | ||
|
|
9554025daf | ||
|
|
0eb5d607bf | ||
|
|
750f4bc276 | ||
|
|
d0aada1d3d | ||
|
|
8a4848387c | ||
|
|
6904fc7da3 | ||
|
|
28c55a05e6 | ||
|
|
562c829267 | ||
|
|
b200ed242d | ||
|
|
815cfe55df | ||
|
|
40a1094d74 | ||
|
|
346650c091 | ||
|
|
7f74715f51 | ||
|
|
b6fcee517d | ||
|
|
4f62551f6b | ||
|
|
3980249271 | ||
|
|
e3b11b1130 | ||
|
|
f866f23af1 | ||
|
|
c793bc24f0 | ||
|
|
591a46d559 | ||
|
|
2852f26702 | ||
|
|
fc818fdfd6 | ||
|
|
5566ef87f8 | ||
|
|
366fe34d6f | ||
|
|
37a0e83124 | ||
|
|
061a3f393a | ||
|
|
1dff22aeab | ||
|
|
78e6fd4809 | ||
|
|
96bbf3d0f2 | ||
|
|
a842eaba4e | ||
|
|
37565bf8e4 | ||
|
|
beb158b387 | ||
|
|
408eb06f8d | ||
|
|
abe0e44635 | ||
|
|
cfaf414f1c | ||
|
|
f9c4dc616b | ||
|
|
bf845bab6b | ||
|
|
bae9c85990 | ||
|
|
56bbb8d0ff | ||
|
|
60d3565231 | ||
|
|
81340fd287 | ||
|
|
c10c348c73 | ||
|
|
65cb7d9674 | ||
|
|
24f1a10ff7 | ||
|
|
767d11182a | ||
|
|
cf363f667e | ||
|
|
0d1046b8c7 | ||
|
|
2c05f5779e | ||
|
|
9af200f89e | ||
|
|
7e221cfd46 | ||
|
|
640882d178 | ||
|
|
3a1436abef | ||
|
|
d431f0490d | ||
|
|
4c2a6c92a6 | ||
|
|
086c230e9e | ||
|
|
27e2ff50f2 | ||
|
|
3134e5596b | ||
|
|
315274abf9 | ||
|
|
52bbf65fa8 | ||
|
|
9c018ec63b | ||
|
|
bd7e457cdb | ||
|
|
36a0f8515b | ||
|
|
cac10a337d | ||
|
|
edb53cc58f | ||
|
|
1dceeecdad | ||
|
|
f8071ada0b | ||
|
|
21bc8edbd8 | ||
|
|
2a8aeb5041 | ||
|
|
1a7760cf6d | ||
|
|
aee4eed5ac | ||
|
|
87215fb590 | ||
|
|
5409126187 | ||
|
|
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 | ||
|
|
ccee71e638 | ||
|
|
cba0e739eb | ||
|
|
c569cb9cde | ||
|
|
fc585a3900 | ||
|
|
973f8529c2 | ||
|
|
1ff9dc50fd | ||
|
|
065c9053da | ||
|
|
6905be1bcd | ||
|
|
a550f9616c | ||
|
|
bcee3e5373 | ||
|
|
d377ced6b6 | ||
|
|
6e0ceb093c | ||
|
|
745f99e52e | ||
|
|
7197034eda | ||
|
|
264748652f | ||
|
|
48e214564a | ||
|
|
5424e7e02a | ||
|
|
0c9c70b067 | ||
|
|
0ff24f4b09 | ||
|
|
cfa75b7643 | ||
|
|
b72ad1d78d | ||
|
|
5d1f293606 | ||
|
|
2dc0eca4aa | ||
|
|
f5808c1c81 | ||
|
|
321037477f | ||
|
|
43589c66e9 | ||
|
|
435f299a8b | ||
|
|
083db80251 | ||
|
|
92bf520cf4 | ||
|
|
ab354f21c4 | ||
|
|
c7a2c045c7 | ||
|
|
d33c8942e4 | ||
|
|
5e630097b9 | ||
|
|
3b5d03c1c8 | ||
|
|
298ae2c354 | ||
|
|
d936b68597 | ||
|
|
41471b9fd6 | ||
|
|
cc071c0911 | ||
|
|
628164d2bd | ||
|
|
999af85262 | ||
|
|
07e075ad8b | ||
|
|
18098f8aef | ||
|
|
f335b4e436 | ||
|
|
ab293edf4c | ||
|
|
88917070bf | ||
|
|
5bba5cb2bc | ||
|
|
098916bfa5 | ||
|
|
bb79aaed8b | ||
|
|
bc5c5a2835 | ||
|
|
4c11199de2 | ||
|
|
2e987701a8 | ||
|
|
4f625291a5 | ||
|
|
048f2abd87 | ||
|
|
31dea532c5 | ||
|
|
3d54e5d965 | ||
|
|
aee2a5a161 | ||
|
|
198ea0104d | ||
|
|
1abdf6d15c | ||
|
|
e5b836462f | ||
|
|
552b20b5d9 | ||
|
|
7d500aedb5 | ||
|
|
751e823b8c | ||
|
|
59d47b2b15 | ||
|
|
b1635b0715 | ||
|
|
4d778e9ca9 | ||
|
|
af433286d0 | ||
|
|
2a41e8a726 | ||
|
|
6b41f3bb64 | ||
|
|
78c178b1f6 | ||
|
|
20a6dd1aeb | ||
|
|
3773dfb4a1 | ||
|
|
e156b662a3 | ||
|
|
38193a870b | ||
|
|
e3a636772a | ||
|
|
46b043fdc7 | ||
|
|
a774ae87c2 | ||
|
|
a332a7b402 | ||
|
|
2ee4d874da | ||
|
|
4f051e5251 | ||
|
|
e4a0b29162 | ||
|
|
09234296f4 | ||
|
|
679228c8a7 | ||
|
|
a752e19878 | ||
|
|
0880c0e3b3 | ||
|
|
948e65d383 | ||
|
|
7cce57496d | ||
|
|
e54e851f61 | ||
|
|
17020cf62d | ||
|
|
0c7be28eaa | ||
|
|
0d5a183f2e | ||
|
|
c222594bea | ||
|
|
3df8bdfbf2 | ||
|
|
5722547d93 | ||
|
|
dea5ebd95d | ||
|
|
048e41c1ca | ||
|
|
5078036c51 | ||
|
|
e7a128bf0d | ||
|
|
0e46936231 | ||
|
|
d91d3ef0ef | ||
|
|
1f0dd907f9 | ||
|
|
3c555cbfca | ||
|
|
9a8e4d8600 | ||
|
|
444aaa5cdc | ||
|
|
25669d18fc | ||
|
|
03d6e46eca | ||
|
|
c44c7ed0f0 | ||
|
|
47ac7437c0 | ||
|
|
37f982e0ea | ||
|
|
139646369f | ||
|
|
3d4b84dc09 | ||
|
|
02866754e0 | ||
|
|
e4e1a75d44 | ||
|
|
4e5dd03456 | ||
|
|
506b6eea09 | ||
|
|
403ee4b925 | ||
|
|
193d1f550f | ||
|
|
d2a02a830c | ||
|
|
6cc9c1ac57 | ||
|
|
fffb1c6c02 | ||
|
|
985d1baff5 | ||
|
|
a70e467b69 | ||
|
|
b07010bebd | ||
|
|
353bdc5989 | ||
|
|
f135804c4b | ||
|
|
ae6d0ead2c | ||
|
|
6db3ad4e0d | ||
|
|
09e42d5a08 | ||
|
|
b9a09fd1be | ||
|
|
b08235b9f6 | ||
|
|
43e67893b4 | ||
|
|
633b38da01 | ||
|
|
68a4818be0 | ||
|
|
be3e4a7b13 | ||
|
|
2bc616ebbb | ||
|
|
7058472784 | ||
|
|
8d9f28b3c8 | ||
|
|
0d2bba78d9 | ||
|
|
23a62d33eb | ||
|
|
93a2a4a772 | ||
|
|
38e74f0c1b | ||
|
|
d2d6ca75be | ||
|
|
a4a9f9e7c5 | ||
|
|
4d5d1094ed | ||
|
|
9f8eaa5722 | ||
|
|
79e07d2b3d | ||
|
|
822d457bff | ||
|
|
8e391af0b4 | ||
|
|
b522b4d355 | ||
|
|
9c5ef8f5b4 | ||
|
|
4dc1ad35d2 | ||
|
|
9b405d9e59 | ||
|
|
2351ec7b85 | ||
|
|
6fc3228334 | ||
|
|
81b1f5b14d | ||
|
|
b8450ad28b | ||
|
|
70371a5001 | ||
|
|
87f4cf772b | ||
|
|
970ed8ff86 | ||
|
|
3f77f1037a | ||
|
|
6cc770dced | ||
|
|
355172dbf6 | ||
|
|
3bfcd38e65 | ||
|
|
909857f146 | ||
|
|
1cc64c7d21 | ||
|
|
891db2be21 | ||
|
|
54f3451456 | ||
|
|
563471ccf5 | ||
|
|
2e70b61e60 | ||
|
|
cd0d786a4c | ||
|
|
611ae13777 | ||
|
|
026214bd3f | ||
|
|
7c29c9ad27 | ||
|
|
cba5586f05 | ||
|
|
25a8d0cc2a |
@@ -1 +1 @@
|
||||
VITE_API_BASE_URL=/api/v1/
|
||||
VITE_API_BASE_URL=api/v1/
|
||||
|
||||
9
.github/workflows/build.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build moviepilot frontend
|
||||
name: Build Moviepilot-Frontend
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -27,6 +27,13 @@ jobs:
|
||||
node-version: '18'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Download Icons
|
||||
run: |
|
||||
pwd
|
||||
curl -sL "https://github.com/jxxghp/MoviePilot-Plugins/archive/refs/heads/main.zip" | busybox unzip -d /tmp -
|
||||
mv /tmp/MoviePilot-Plugins-main/icons public/plugin_icon
|
||||
rm -rf /tmp/MoviePilot-Plugins-main
|
||||
|
||||
- name: Build frontend
|
||||
id: build_frontend
|
||||
run: |
|
||||
|
||||
1
.gitignore
vendored
@@ -32,3 +32,4 @@ dist-ssr
|
||||
|
||||
# iconify dist files
|
||||
src/@iconify/*.js
|
||||
public/plugin_icon/**
|
||||
|
||||
4
.vscode/settings.json
vendored
@@ -31,8 +31,8 @@
|
||||
"volar.preview.port": 3000,
|
||||
"volar.completion.preferredTagNameCase": "pascal",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.fixAll.stylelint": true
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.stylelint": "explicit"
|
||||
},
|
||||
"eslint.alwaysShowStatus": true,
|
||||
"eslint.format.enable": true,
|
||||
|
||||
32
README.md
@@ -1,35 +1,39 @@
|
||||
# MoviePilot-Frontend
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
[MoviePilot](https://github.com/jxxghp/MoviePilot) 的前端项目。
|
||||
|
||||
## Recommended IDE Setup
|
||||
## 推荐的IDE设置
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (and disable Vetur).
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (并禁用 Vetur).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
## 配置Vite
|
||||
|
||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates.
|
||||
请参阅 [Vite 配置参考](https://vitejs.dev/config/).
|
||||
|
||||
However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can run `Volar: Switch TS Plugin on/off` from VSCode command palette.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
## 依赖安装
|
||||
|
||||
```sh
|
||||
yarn
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
### 开发运行
|
||||
|
||||
```sh
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
### 编译打包
|
||||
|
||||
```sh
|
||||
yarn build
|
||||
```
|
||||
|
||||
### 静态运行
|
||||
|
||||
1. 使用 `nginx` 等Web服务器托管 `dist` 静态文件,nginx配置参考 `public/nginx.conf`。
|
||||
|
||||
2. 使用 `node` 命令直接运行`service.js`,默认监听 `3000` 端口,设置环境变量 `NGINX_PORT` 来调整运行端口。
|
||||
|
||||
```shell
|
||||
node dist/service.js
|
||||
```
|
||||
|
||||
222
index.html
@@ -1,143 +1,198 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width, user-scalable=no" />
|
||||
<title>MoviePilot</title>
|
||||
<meta name="Robots" content="noindex,nofollow,noarchive" />
|
||||
<meta name="referrer" content="origin" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="manifest.json" crossorigin="use-credentials" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="MoviePilot" />
|
||||
<meta name="description" content="MoviePilot" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="referrer" content="never" />
|
||||
<meta name="msapplication-TileColor" content="#7D34FD" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#28243D" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)" />
|
||||
<meta name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<link rel="stylesheet" type="text/css" href="/loader.css" />
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width, user-scalable=no" />
|
||||
<title>MoviePilot</title>
|
||||
<meta name="Robots" content="noindex,nofollow,noarchive">
|
||||
<meta name="referrer" content="origin">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg">
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="MoviePilot">
|
||||
<meta name="description" content="MoviePilot">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="referrer" content="never">
|
||||
<meta name="msapplication-TileColor" content="#7D34FD" />
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="theme-color" content="#28243D" media="(prefers-color-scheme: dark)">
|
||||
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)">
|
||||
<meta name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<link rel="stylesheet" type="text/css" href="/loader.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<body>
|
||||
<div id="loading-bg">
|
||||
<div class="loading-logo">
|
||||
<!-- Logo -->
|
||||
<svg width="100px" height="100px" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<svg
|
||||
width="100px"
|
||||
height="100px"
|
||||
viewBox="0 0 192 192"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2"
|
||||
>
|
||||
<g transform="matrix(1,0,0,1,-2606,-236)">
|
||||
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">
|
||||
<rect x="0" y="0" width="192" height="192" style="fill:none;" />
|
||||
<rect x="0" y="0" width="192" height="192" style="fill: none" />
|
||||
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)">
|
||||
<path
|
||||
d="M2241.27,-28.175C2238.86,-28.931 2236.64,-29.181 2234.48,-29.254L2159.78,-29.286L2165.01,-11.207C2167.16,-13.121 2169.64,-13.722 2172.26,-13.808L2222.12,-13.822C2223.52,-13.824 2225,-13.701 2226.78,-13.108L2241.27,-28.175Z"
|
||||
style="fill:url(#_Linear1);" />
|
||||
style="fill: url(#_Linear1)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2205.67,331.428L2205.67,332.25L2205.67,352.835C2205.67,354.263 2204.91,355.583 2203.67,356.298C2202.43,357.012 2200.91,357.013 2199.67,356.3L2190.78,351.174C2189.73,350.595 2188.83,350.083 2188.03,349.59L2187.45,349.257C2186.66,348.725 2185.91,348.142 2185.21,347.461C2185.08,347.331 2184.95,347.198 2184.82,347.061C2184.26,346.457 2183.75,345.778 2183.3,344.995C2182.16,343.05 2181.69,341.024 2181.68,338.948L2181.67,268.923L2209.77,274.425C2207.5,275.639 2205.68,278.3 2205.67,281.429L2205.67,331.428Z"
|
||||
style="fill:url(#_Linear2);" />
|
||||
style="fill: url(#_Linear2)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2295.93,363.064C2295.73,363.184 2295.53,363.301 2295.32,363.414L2295.93,363.064Z"
|
||||
style="fill:rgb(141,81,249);" />
|
||||
style="fill: rgb(141, 81, 249)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2299.79,360.238C2299.79,360.238 2320.03,348.464 2320.04,348.461C2323.1,346.372 2324.69,343.444 2325.17,339.877C2325.17,339.877 2325.17,269.846 2325.17,269.839C2325.06,267.482 2324.56,265.739 2323.61,264.133C2322.56,262.445 2321.26,261.005 2319.55,259.97L2304.42,251.217C2303.96,250.949 2303.39,250.948 2302.92,251.216C2302.46,251.484 2302.17,251.979 2302.17,252.515L2302.17,276.775L2302.17,277.879L2302.17,352.926C2302.17,352.933 2302.17,352.941 2302.17,352.948C2302.04,355.861 2301.23,358.279 2299.79,360.238Z"
|
||||
style="fill:url(#_Linear3);" />
|
||||
style="fill: url(#_Linear3)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256Z"
|
||||
style="fill:rgb(165,118,255);" />
|
||||
style="fill: rgb(165, 118, 255)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256ZM2253.68,223.756C2251.6,223.789 2249.87,224.269 2248.47,224.996L2188.17,259.754C2184.35,261.992 2182.35,265.367 2182.18,269.874C2182.18,269.874 2182.17,292.759 2182.17,292.757C2183.25,290.047 2185.13,288.051 2187.62,286.607L2249.57,250.919C2249.58,250.917 2249.58,250.915 2249.59,250.913C2250.83,250.243 2252.17,249.839 2253.67,249.847C2255.21,249.841 2256.54,250.253 2257.76,250.914C2257.76,250.916 2257.76,250.917 2257.76,250.919L2274.92,260.807C2275.38,261.075 2275.95,261.074 2276.42,260.806C2276.88,260.538 2277.17,260.043 2277.17,259.508L2277.17,237.568C2277.17,236.317 2276.5,235.16 2275.42,234.535C2275.42,234.535 2258.88,225 2258.87,224.996C2256.87,224.049 2255.2,223.746 2253.68,223.756Z"
|
||||
style="fill:url(#_Linear4);" />
|
||||
style="fill: url(#_Linear4)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(0.800798,0.462341,0.769972,-1.33363,-1677.22,-896.858)">
|
||||
<path
|
||||
d="M2241.55,-28.184C2239.1,-28.989 2236.83,-29.204 2234.68,-29.295C2234.68,-29.295 2220.82,-29.3 2215.03,-29.303C2213.48,-29.303 2212.05,-28.808 2211.28,-28.004C2208.65,-25.275 2202.56,-18.936 2199.45,-15.709C2199.07,-15.306 2199.07,-14.809 2199.46,-14.406C2199.85,-14.004 2200.57,-13.758 2201.34,-13.761C2208.36,-13.788 2222.72,-13.845 2222.72,-13.845C2223.98,-13.851 2225.44,-13.657 2227.06,-13.117L2241.55,-28.184Z"
|
||||
style="fill:rgb(141,81,249);" />
|
||||
style="fill: rgb(141, 81, 249)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-4.32309,0,0,12.4454,9610.35,-1450.35)">
|
||||
<path
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
|
||||
style="fill:rgb(104,0,197);" />
|
||||
style="fill: rgb(104, 0, 197)"
|
||||
/>
|
||||
<clipPath id="_clip5">
|
||||
<path
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z" />
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
|
||||
/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip5)">
|
||||
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
|
||||
<path
|
||||
d="M1726.17,-64.249L1708.16,-72.303L1708.05,-23.514L1721.88,-32.386C1722.96,-33.241 1723.09,-33.944 1723.15,-34.636L1723.15,-54.373C1723.19,-56.238 1724.96,-57.594 1726.87,-56.686L1726.17,-64.249Z"
|
||||
style="fill:url(#_Linear6);" />
|
||||
style="fill: url(#_Linear6)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1704.47,-40.254C1706.28,-40.527 1708.14,-40.212 1708.16,-39.416L1708.16,-18.976L1726.17,-18.976L1726.17,-45.661Z"
|
||||
style="fill:rgb(141,81,249);" />
|
||||
style="fill: rgb(141, 81, 249)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1726.17,-18.976L1708.16,-18.976L1708.16,-39.416C1707.79,-40.732 1704.5,-40.298 1702.68,-40.025L1726.17,-45.661ZM1705.49,-40.491C1706.2,-40.507 1706.87,-40.464 1707.4,-40.327C1708.01,-40.173 1708.48,-39.899 1708.62,-39.436C1708.62,-39.429 1708.62,-39.423 1708.62,-39.416L1708.62,-19.152C1708.62,-19.152 1725.72,-19.152 1725.72,-19.152L1725.72,-45.345L1705.49,-40.491Z"
|
||||
style="fill:url(#_Radial7);" />
|
||||
style="fill: url(#_Radial7)"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)">
|
||||
<stop offset="0" style="stop-color:rgb(141,81,249);stop-opacity:1" />
|
||||
<stop offset="1" style="stop-color:rgb(116,50,223);stop-opacity:1" />
|
||||
<linearGradient
|
||||
id="_Linear1"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)">
|
||||
<stop offset="0" style="stop-color:rgb(141,81,249);stop-opacity:1" />
|
||||
<stop offset="1" style="stop-color:rgb(116,50,223);stop-opacity:1" />
|
||||
<linearGradient
|
||||
id="_Linear2"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)">
|
||||
<stop offset="0" style="stop-color:rgb(211,187,255);stop-opacity:1" />
|
||||
<stop offset="1" style="stop-color:rgb(211,187,255);stop-opacity:0" />
|
||||
<linearGradient
|
||||
id="_Linear3"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)">
|
||||
<stop offset="0" style="stop-color:rgb(211,187,255);stop-opacity:1" />
|
||||
<stop offset="1" style="stop-color:rgb(211,187,255);stop-opacity:0" />
|
||||
<linearGradient
|
||||
id="_Linear4"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)">
|
||||
<stop offset="0" style="stop-color:rgb(116,50,223);stop-opacity:1" />
|
||||
<stop offset="0.51" style="stop-color:rgb(110,38,217);stop-opacity:1" />
|
||||
<stop offset="1" style="stop-color:rgb(91,0,197);stop-opacity:1" />
|
||||
<linearGradient
|
||||
id="_Linear6"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
<stop offset="0.51" style="stop-color: rgb(110, 38, 217); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(91, 0, 197); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<radialGradient id="_Radial7" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)">
|
||||
<stop offset="0" style="stop-color:rgb(211,187,255);stop-opacity:1" />
|
||||
<stop offset="1" style="stop-color:rgb(211,187,255);stop-opacity:0" />
|
||||
<radialGradient
|
||||
id="_Radial7"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
<div class="loading">
|
||||
<div class="effect-1 effects"></div>
|
||||
@@ -145,18 +200,15 @@
|
||||
<div class="effect-3 effects"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<script>
|
||||
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
|
||||
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<script>
|
||||
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
|
||||
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
|
||||
|
||||
if (loaderColor)
|
||||
document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
|
||||
if (loaderColor) document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
|
||||
|
||||
if (primaryColor)
|
||||
document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
if (primaryColor) document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
17
package.json
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "1.0.8",
|
||||
"version": "1.6.4",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
@@ -9,7 +10,13 @@
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"lint": "eslint . -c .eslintrc.js --fix --ext .ts,.js,.vue,.tsx,.jsx",
|
||||
"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": {
|
||||
"@casl/ability": "^6.2.0",
|
||||
@@ -17,10 +24,14 @@
|
||||
"@floating-ui/dom": "1.2.8",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vueuse/math": "^10.1.2",
|
||||
"ace-builds": "^1.32.6",
|
||||
"apexcharts-clevision": "^3.28.5",
|
||||
"axios": "1.4.0",
|
||||
"axios-mock-adapter": "^1.21.4",
|
||||
"chart.js": "^4.1.2",
|
||||
"colorthief": "^2.4.0",
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"postcss-purgecss": "^5.0.0",
|
||||
@@ -38,6 +49,7 @@
|
||||
"vue-prism-component": "^2.0.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"vue-toast-notification": "^3",
|
||||
"vue3-ace-editor": "^2.2.4",
|
||||
"vue3-apexcharts": "^1.4.1",
|
||||
"vue3-perfect-scrollbar": "^1.6.0",
|
||||
"vuetify": "3.3.5",
|
||||
@@ -59,6 +71,7 @@
|
||||
"@iconify/vue": "4.1.1",
|
||||
"@intlify/unplugin-vue-i18n": "^0.10.0",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/node": "^20.1.4",
|
||||
"@types/webfontloader": "^1.6.34",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.5",
|
||||
|
||||
@@ -11,10 +11,11 @@ html {
|
||||
|
||||
#loading-bg {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
display: block;
|
||||
background: var(--initial-loader-bg, #fff);
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
block-size: 100vh;
|
||||
inline-size: 100vw;
|
||||
}
|
||||
|
||||
.loading-logo {
|
||||
@@ -82,4 +83,4 @@ html {
|
||||
opacity: 1;
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: 39 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 24 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: 38 KiB |
|
Before Width: | Height: | Size: 20 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: 55 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 16 KiB |
41
public/service.js
Normal file
@@ -0,0 +1,41 @@
|
||||
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.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'index.html'))
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running on port ${port}`)
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import type { ThemeSwitcherTheme } from '@layouts/types'
|
||||
|
||||
@@ -20,26 +20,30 @@ const {
|
||||
{ initialValue: savedTheme.value },
|
||||
)
|
||||
|
||||
function changeTheme() {
|
||||
const nextTheme = getNextThemeName()
|
||||
|
||||
globalTheme.name.value = nextTheme
|
||||
savedTheme.value = nextTheme
|
||||
localStorage.setItem('theme', nextTheme)
|
||||
function updateTheme() {
|
||||
const autoTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
const theme = currentThemeName.value === 'auto' ? autoTheme : currentThemeName.value
|
||||
globalTheme.name.value = theme
|
||||
savedTheme.value = theme
|
||||
// 修改载入时背景色
|
||||
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
|
||||
|
||||
themeTransition()
|
||||
}
|
||||
|
||||
// Update icon if theme is changed from other sources
|
||||
// 监听系统主题变化
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
|
||||
|
||||
watch(
|
||||
() => globalTheme.name.value,
|
||||
(val) => {
|
||||
currentThemeName.value = val
|
||||
},
|
||||
() => currentThemeName.value,
|
||||
() => updateTheme(),
|
||||
)
|
||||
|
||||
function changeTheme() {
|
||||
const nextTheme = getNextThemeName()
|
||||
currentThemeName.value = nextTheme
|
||||
localStorage.setItem('theme', nextTheme)
|
||||
}
|
||||
|
||||
// Apply saved theme on page load
|
||||
// onMounted(() => {
|
||||
// globalTheme.name.value = savedTheme.value
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
// ℹ️ This mixin is inspired from vuetify for adding hover styles via before pseudo element
|
||||
@mixin before-pseudo() {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
background: currentcolor;
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
rgba(#{variables.$vertical-nav-background-color-rgb}, 30%) 75%,
|
||||
transparent
|
||||
);
|
||||
block-size: calc(env(safe-area-inset-top) + 64px);
|
||||
block-size: calc(env(safe-area-inset-top) + 4rem);
|
||||
inline-size: 100%;
|
||||
inset-block-start: calc(#{variables.$vertical-nav-header-height} - 2px);
|
||||
opacity: 0;
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
// ℹ️ This mixin is inspired from vuetify for adding hover styles via before pseudo element
|
||||
@mixin before-pseudo() {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
border-radius: inherit;
|
||||
|
||||
@@ -33,3 +33,11 @@ $ps-track-size: 0.5rem;
|
||||
.ps__thumb-y {
|
||||
background-color: rgb(var(--v-theme-perfect-scrollbar-thumb)) !important;
|
||||
}
|
||||
|
||||
// fix bug
|
||||
@media(hover: none) {
|
||||
.ps > .ps__rail-x,
|
||||
.ps > .ps__rail-y {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
21
src/@core/utils/dom.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export function removeEl(selector: string) {
|
||||
if (selector) {
|
||||
const el = document.querySelector(selector)
|
||||
el?.parentNode?.removeChild(el)
|
||||
}
|
||||
}
|
||||
|
||||
export function useDefer(maxFrameCount = 1) {
|
||||
const frameCount = ref(0)
|
||||
const refreshFrameCount = () => {
|
||||
requestAnimationFrame(() => {
|
||||
frameCount.value++
|
||||
if (frameCount.value < maxFrameCount)
|
||||
refreshFrameCount()
|
||||
})
|
||||
}
|
||||
refreshFrameCount()
|
||||
return function (showInFrameCount: number) {
|
||||
return frameCount.value >= showInFrameCount
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export function formatFileSize(bytes: number) {
|
||||
if (bytes < 0)
|
||||
throw new Error('字节数不能为负数。')
|
||||
|
||||
const units = ['B', 'K', 'M', 'G', 'T']
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let size = bytes
|
||||
let unitIndex = 0
|
||||
|
||||
@@ -88,9 +88,9 @@ export function formatSeconds(seconds: number) {
|
||||
}
|
||||
|
||||
// YYYY-MM-DD 转化为Date
|
||||
export function parseDate(dateString: string): Date {
|
||||
export function parseDate(dateString: string): Date | null {
|
||||
if (!dateString)
|
||||
return new Date()
|
||||
return null
|
||||
const [year, month, day] = dateString.split('-').map(Number)
|
||||
|
||||
return new Date(year, month - 1, day)
|
||||
@@ -109,3 +109,41 @@ export function formatBytes(bytes: number, decimals = 2) {
|
||||
|
||||
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
// 格式化剧集列表
|
||||
export function formatEp(nums: number[]): string {
|
||||
if (!nums.length)
|
||||
return ''
|
||||
|
||||
if (nums.length === 1)
|
||||
return nums[0].toString()
|
||||
|
||||
// 将数组升序排序
|
||||
nums.sort((a, b) => a - b)
|
||||
const formattedRanges: string[] = []
|
||||
let start = nums[0]
|
||||
let end = nums[0]
|
||||
|
||||
for (let i = 1; i < nums.length; i++) {
|
||||
if (nums[i] === end + 1) {
|
||||
end = nums[i]
|
||||
}
|
||||
else {
|
||||
if (start === end)
|
||||
formattedRanges.push(start.toString())
|
||||
|
||||
else
|
||||
formattedRanges.push(`${start.toString()}-${end.toString()}`)
|
||||
|
||||
start = end = nums[i]
|
||||
}
|
||||
}
|
||||
|
||||
if (start === end)
|
||||
formattedRanges.push(start.toString())
|
||||
|
||||
else
|
||||
formattedRanges.push(`${start.toString()}-${end.toString()}`)
|
||||
|
||||
return formattedRanges.join('、')
|
||||
}
|
||||
|
||||
23
src/@core/utils/image.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import ColorThief from 'colorthief'
|
||||
|
||||
// 将 RGB 转换为十六进制
|
||||
function rgbStringToHex(rgbArray: number[]): string {
|
||||
if (rgbArray.length !== 3 || rgbArray.some(isNaN))
|
||||
throw new Error('Invalid RGB string format')
|
||||
|
||||
const [r, g, b] = rgbArray
|
||||
|
||||
const toHex = (c: number): string => {
|
||||
const hex = c.toString(16)
|
||||
return hex.length === 1 ? `0${hex}` : hex
|
||||
}
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
// 提取主要颜色
|
||||
export async function getDominantColor(image: HTMLImageElement): Promise<string> {
|
||||
const colorThief = new ColorThief()
|
||||
const dominantColor = colorThief.getColor(image)
|
||||
return rgbStringToHex(dominantColor)
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export function isToday(date: Date) {
|
||||
)
|
||||
}
|
||||
|
||||
// 计算时间差,返回xx天xx小时xx分钟
|
||||
// 计算时间差,返回xx天/xx小时/xx分钟/xx秒
|
||||
export function calculateTimeDifference(inputTime: string): string {
|
||||
if (!inputTime)
|
||||
return ''
|
||||
@@ -64,6 +64,38 @@ export function calculateTimeDifference(inputTime: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算时间差,返回xx天xx小时xx分钟
|
||||
export function calculateTimeDiff(inputTime: string): string {
|
||||
if (!inputTime)
|
||||
return ''
|
||||
|
||||
// 使用当前时区
|
||||
const inputDate = new Date(inputTime)
|
||||
const currentDate = new Date()
|
||||
|
||||
const timeDifference = currentDate.getTime() - inputDate.getTime()
|
||||
const secondsDifference = Math.floor(timeDifference / 1000)
|
||||
|
||||
const days = Math.floor(secondsDifference / 86400)
|
||||
const hours = Math.floor(secondsDifference % 86400 / 3600)
|
||||
const minutes = Math.floor(secondsDifference % 86400 % 3600 / 60)
|
||||
const secones = Math.floor(secondsDifference % 60)
|
||||
|
||||
if (days > 0)
|
||||
return `${days}天${hours}小时${minutes}分钟`
|
||||
|
||||
else if (hours > 0)
|
||||
return `${hours}小时${minutes}分钟`
|
||||
|
||||
else if (minutes > 0)
|
||||
return `${minutes}分钟`
|
||||
|
||||
else if (secones > 0)
|
||||
return `${secones}秒`
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// 判断一个数组subArray是不是在另一个数组mainArray中
|
||||
export function isContained(subArray: any[], mainArray: any[]): boolean {
|
||||
return subArray.every(element => mainArray.includes(element))
|
||||
|
||||
30
src/@core/utils/navigator.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// 请求和获取剪贴板内容
|
||||
export async function getClipboardContent() {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
return await navigator.clipboard.readText()
|
||||
}
|
||||
else {
|
||||
const input = document.createElement('textarea')
|
||||
document.body.appendChild(input)
|
||||
input.select()
|
||||
document.execCommand('paste')
|
||||
const content = input.value
|
||||
document.body.removeChild(input)
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
// 将内容复制到剪贴板,兼容非安全域场景
|
||||
export async function copyToClipboard(content: string) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(content)
|
||||
}
|
||||
else {
|
||||
const input = document.createElement('textarea')
|
||||
input.value = content
|
||||
document.body.appendChild(input)
|
||||
input.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(input)
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export default defineComponent({
|
||||
<style scoped>
|
||||
* {
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
perspective: 62.5rem;
|
||||
transform: translateZ(0);
|
||||
will-change: block-size;
|
||||
}
|
||||
|
||||
@@ -15,13 +15,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
})
|
||||
|
||||
const { mdAndDown } = useDisplay()
|
||||
|
||||
const refNav = ref()
|
||||
|
||||
/*
|
||||
ℹ️ Close overlay side when route is changed
|
||||
Close overlay vertical nav when link is clicked
|
||||
*/
|
||||
const route = useRoute()
|
||||
|
||||
watch(
|
||||
@@ -31,9 +25,11 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
// 是否滚动
|
||||
const isVerticalNavScrolled = ref(false)
|
||||
const updateIsVerticalNavScrolled = (val: boolean) => (isVerticalNavScrolled.value = val)
|
||||
|
||||
// 滚动响应
|
||||
function handleNavScroll(evt: Event) {
|
||||
isVerticalNavScrolled.value = (evt.target as HTMLElement).scrollTop > 0
|
||||
}
|
||||
@@ -83,9 +79,12 @@ function handleNavScroll(evt: Event) {
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@configured-variables" as variables;
|
||||
@use "@layouts/styles/mixins";
|
||||
@use '@configured-variables' as variables;
|
||||
@use '@layouts/styles/mixins';
|
||||
|
||||
.visible {
|
||||
visibility: visible !important;
|
||||
}
|
||||
// 👉 Vertical Nav
|
||||
.layout-vertical-nav {
|
||||
position: fixed;
|
||||
@@ -98,6 +97,11 @@ function handleNavScroll(evt: Event) {
|
||||
inset-inline-start: 0;
|
||||
transition: transform 0.25s ease-in-out, inline-size 0.25s ease-in-out, box-shadow 0.25s ease-in-out;
|
||||
will-change: transform, inline-size;
|
||||
visibility: hidden;
|
||||
|
||||
&:not(.overlay-nav) {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.nav-header {
|
||||
display: flex;
|
||||
|
||||
@@ -111,7 +111,7 @@ export default defineComponent({
|
||||
|
||||
.layout-navbar {
|
||||
position: fixed;
|
||||
width: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
|
||||
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
|
||||
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
|
||||
inset-block-start: 0;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ body,
|
||||
overflow: hidden;
|
||||
// TODO: Use grid gutter variable here
|
||||
padding-block: 1.5rem;
|
||||
padding-top: calc(env(safe-area-inset-top) + 65px);
|
||||
padding-top: calc(env(safe-area-inset-top) + 4.25rem);
|
||||
// display: flex;
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ body,
|
||||
& > div:first-child {
|
||||
flex: auto;
|
||||
position: relative;
|
||||
width: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
|
||||
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
|
||||
// 👉 Vertical nav
|
||||
$layout-vertical-nav-z-index: 12 !default;
|
||||
$layout-vertical-nav-width: 260px !default;
|
||||
$layout-vertical-nav-collapsed-width: 80px !default;
|
||||
$layout-vertical-nav-width: 16.25rem !default;
|
||||
$layout-vertical-nav-collapsed-width: 5rem !default;
|
||||
|
||||
// 👉 Horizontal nav
|
||||
$layout-horizontal-nav-z-index: 11 !default;
|
||||
$layout-horizontal-nav-navbar-height: 64px !default;
|
||||
$layout-horizontal-nav-navbar-height: 4rem !default;
|
||||
|
||||
// 👉 Navbar
|
||||
$layout-vertical-nav-navbar-height: 64px !default;
|
||||
$layout-vertical-nav-navbar-height: 4rem !default;
|
||||
$layout-vertical-nav-navbar-is-contained: true !default;
|
||||
$layout-vertical-nav-layout-navbar-z-index: 11 !default;
|
||||
$layout-horizontal-nav-layout-navbar-z-index: 11 !default;
|
||||
|
||||
// 👉 Main content
|
||||
$layout-boxed-content-width: 1440px !default;
|
||||
$layout-boxed-content-width: 90rem !default;
|
||||
|
||||
// 👉Footer
|
||||
$layout-vertical-nav-footer-height: 56px !default;
|
||||
$layout-vertical-nav-footer-height: 3.5rem !default;
|
||||
|
||||
// 👉 Layout overlay
|
||||
$layout-overlay-z-index: 11 !default;
|
||||
|
||||
64
src/App.vue
@@ -1,17 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useTheme } from 'vuetify'
|
||||
import api from './api'
|
||||
import type { Setting, User } from './api/types'
|
||||
import store from './store'
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
|
||||
function setTheme() {
|
||||
const { global: globalTheme } = useTheme()
|
||||
let theme = localStorage.getItem('theme') || 'light'
|
||||
if (theme === 'auto')
|
||||
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
globalTheme.name.value = theme
|
||||
}
|
||||
// 第一时间应用主题
|
||||
const { global: globalTheme } = useTheme()
|
||||
globalTheme.name.value = localStorage.getItem('theme') || 'light'
|
||||
|
||||
// 路由
|
||||
const route = useRoute()
|
||||
setTheme()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
@@ -36,60 +36,14 @@ function startSSEMessager() {
|
||||
}
|
||||
}
|
||||
|
||||
// 当前用户信息
|
||||
const accountInfo = ref<User>({
|
||||
id: 0,
|
||||
name: '',
|
||||
password: '',
|
||||
email: '',
|
||||
is_active: false,
|
||||
is_superuser: false,
|
||||
avatar: avatar1,
|
||||
})
|
||||
|
||||
// 环境设置信息
|
||||
const systemEnv = ref<Setting>()
|
||||
|
||||
// 调用API,加载当前用户数据
|
||||
async function loadAccountInfo() {
|
||||
try {
|
||||
const user: User = await api.get('user/current')
|
||||
|
||||
accountInfo.value = user
|
||||
if (!accountInfo.value.avatar)
|
||||
accountInfo.value.avatar = avatar1
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API,加载当前系统环境设置
|
||||
async function loadSystemSettings() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
if (result.success)
|
||||
systemEnv.value = result.data
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时,加载当前用户数据
|
||||
onBeforeMount(async () => {
|
||||
await loadAccountInfo()
|
||||
await loadSystemSettings()
|
||||
startSSEMessager()
|
||||
})
|
||||
|
||||
// 提供给所有元素复用
|
||||
provide('accountInfo', accountInfo)
|
||||
provide('systemEnv', systemEnv)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VApp>
|
||||
<RouterView :key="route.fullPath" />
|
||||
<RouterView />
|
||||
</VApp>
|
||||
</template>
|
||||
|
||||
54
src/ace-config.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import ace from 'ace-builds'
|
||||
|
||||
import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url'
|
||||
|
||||
import modeJavascriptUrl from 'ace-builds/src-noconflict/mode-javascript?url'
|
||||
|
||||
import modeHtmlUrl from 'ace-builds/src-noconflict/mode-html?url'
|
||||
|
||||
import modeYamlUrl from 'ace-builds/src-noconflict/mode-yaml?url'
|
||||
|
||||
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url'
|
||||
|
||||
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url'
|
||||
|
||||
import themeMonokaiUrl from 'ace-builds/src-noconflict/theme-monokai?url'
|
||||
|
||||
import workerBaseUrl from 'ace-builds/src-noconflict/worker-base?url'
|
||||
|
||||
import workerJsonUrl from 'ace-builds/src-noconflict/worker-json?url'
|
||||
|
||||
import workerJavascriptUrl from 'ace-builds/src-noconflict/worker-javascript?url'
|
||||
|
||||
import workerHtmlUrl from 'ace-builds/src-noconflict/worker-html?url'
|
||||
|
||||
import workerYamlUrl from 'ace-builds/src-noconflict/worker-yaml?url'
|
||||
|
||||
import snippetsHtmlUrl from 'ace-builds/src-noconflict/snippets/html?url'
|
||||
|
||||
import snippetsJsUrl from 'ace-builds/src-noconflict/snippets/javascript?url'
|
||||
|
||||
import snippetsYamlUrl from 'ace-builds/src-noconflict/snippets/yaml?url'
|
||||
|
||||
import snippetsJsonUrl from 'ace-builds/src-noconflict/snippets/json?url'
|
||||
|
||||
import 'ace-builds/src-noconflict/ext-language_tools'
|
||||
|
||||
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl)
|
||||
ace.config.setModuleUrl('ace/mode/javascript', modeJavascriptUrl)
|
||||
ace.config.setModuleUrl('ace/mode/html', modeHtmlUrl)
|
||||
ace.config.setModuleUrl('ace/mode/yaml', modeYamlUrl)
|
||||
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl)
|
||||
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
|
||||
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
|
||||
ace.config.setModuleUrl('ace/mode/base', workerBaseUrl)
|
||||
ace.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl)
|
||||
ace.config.setModuleUrl('ace/mode/javascript_worker', workerJavascriptUrl)
|
||||
ace.config.setModuleUrl('ace/mode/html_worker', workerHtmlUrl)
|
||||
ace.config.setModuleUrl('ace/mode/yaml_worker', workerYamlUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/html', snippetsHtmlUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/javascript', snippetsJsUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/javascript', snippetsYamlUrl)
|
||||
ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl)
|
||||
|
||||
ace.require('ace/ext/language_tools')
|
||||
157
src/api/types.ts
@@ -44,6 +44,15 @@ export interface Subscribe {
|
||||
// 排除
|
||||
exclude?: string
|
||||
|
||||
// 质量
|
||||
quality?: string
|
||||
|
||||
// 分辨率
|
||||
resolution?: string
|
||||
|
||||
// 特效
|
||||
effect?: string
|
||||
|
||||
// 总集数
|
||||
total_episode?: number
|
||||
|
||||
@@ -68,11 +77,14 @@ export interface Subscribe {
|
||||
// 订阅站点
|
||||
sites: number[]
|
||||
|
||||
// 是否洗版
|
||||
best_version: number
|
||||
// 是否洗版,数字或者boolean
|
||||
best_version: any
|
||||
|
||||
// 当前优先级
|
||||
current_priority: number
|
||||
|
||||
// 保存目录
|
||||
save_path: string
|
||||
}
|
||||
|
||||
// 历史记录
|
||||
@@ -87,7 +99,7 @@ export interface TransferHistory {
|
||||
// 目的目录
|
||||
dest?: string
|
||||
|
||||
// 转移模式link/copy/move/softlink
|
||||
// 转移模式link/copy/move/softlink/rclone_copy/rclone_move
|
||||
mode?: string
|
||||
|
||||
// 类型:电影、电视剧
|
||||
@@ -328,7 +340,7 @@ export interface TmdbEpisode {
|
||||
guest_stars: Object[]
|
||||
}
|
||||
|
||||
// TMDB人特信息
|
||||
// TMDB人物信息
|
||||
export interface TmdbPerson {
|
||||
// ID
|
||||
id?: number
|
||||
@@ -379,6 +391,34 @@ export interface TmdbPerson {
|
||||
biography?: string
|
||||
}
|
||||
|
||||
// 豆瓣人物信息
|
||||
export interface DoubanPerson {
|
||||
// ID
|
||||
id?: string
|
||||
|
||||
// 名称
|
||||
name?: string
|
||||
|
||||
// 角色
|
||||
roles?: string[]
|
||||
|
||||
// 简介
|
||||
title?: string
|
||||
|
||||
// 详情页面
|
||||
url?: string
|
||||
|
||||
// 饰演
|
||||
character?: string
|
||||
|
||||
// 图片 large/normal
|
||||
avatar?: { [key: string]: string }
|
||||
|
||||
// 别名
|
||||
latin_name?: string
|
||||
|
||||
}
|
||||
|
||||
// 站点
|
||||
export interface Site {
|
||||
|
||||
@@ -407,13 +447,13 @@ export interface Site {
|
||||
ua?: string
|
||||
|
||||
// 是否使用代理
|
||||
proxy?: number
|
||||
proxy?: any
|
||||
|
||||
// 过滤规则
|
||||
filter?: string
|
||||
|
||||
// 是否演染
|
||||
render?: number
|
||||
render?: any
|
||||
|
||||
// 是否公开站点
|
||||
public?: number
|
||||
@@ -469,6 +509,9 @@ export interface DownloadingInfo {
|
||||
|
||||
// 媒体信息
|
||||
media: { [key: string]: any }
|
||||
|
||||
// 下载用户
|
||||
userid?: string
|
||||
}
|
||||
|
||||
// 缺失剧集信息
|
||||
@@ -500,9 +543,6 @@ export interface Plugin {
|
||||
// 插件图标
|
||||
plugin_icon?: string
|
||||
|
||||
// 主题色
|
||||
plugin_color?: string
|
||||
|
||||
// 插件版本
|
||||
plugin_version?: string
|
||||
|
||||
@@ -526,6 +566,18 @@ export interface Plugin {
|
||||
|
||||
// 运行状态
|
||||
state?: boolean
|
||||
|
||||
// 是否有详情页面
|
||||
has_page?: boolean
|
||||
|
||||
// 是否有新版本
|
||||
has_update?: boolean
|
||||
|
||||
// 是否本地插件
|
||||
is_local?: boolean
|
||||
|
||||
// 插件仓库地址
|
||||
repo_url?: string
|
||||
}
|
||||
|
||||
// 种子信息
|
||||
@@ -599,6 +651,13 @@ export interface TorrentInfo {
|
||||
|
||||
// 促销描述
|
||||
volume_factor: string
|
||||
|
||||
// 免费时间
|
||||
freedate: string
|
||||
|
||||
// 剩余免费时间
|
||||
freedate_diff: string
|
||||
|
||||
}
|
||||
|
||||
// 识别元数据
|
||||
@@ -610,6 +669,9 @@ export interface MetaInfo {
|
||||
// 原字符串
|
||||
org_string?: string
|
||||
|
||||
// 原标题(未经识别词转换)
|
||||
title?: string
|
||||
|
||||
// 副标题
|
||||
subtitle?: string
|
||||
|
||||
@@ -711,6 +773,9 @@ export interface MetaInfo {
|
||||
|
||||
// 资源类型+特效
|
||||
edition: string
|
||||
|
||||
// 应用的自定义识别词
|
||||
apply_words: string[]
|
||||
}
|
||||
|
||||
// 上下文信息
|
||||
@@ -829,6 +894,7 @@ export interface NotificationSwitch {
|
||||
wechat: boolean
|
||||
telegram: boolean
|
||||
slack: boolean
|
||||
synologychat: boolean
|
||||
}
|
||||
|
||||
// 环境设置
|
||||
@@ -837,55 +903,6 @@ export interface Setting {
|
||||
DOWNLOAD_PATH: string
|
||||
}
|
||||
|
||||
// 自定义订阅
|
||||
export interface Rss {
|
||||
id?: number
|
||||
// 名称
|
||||
name?: string
|
||||
// RSS地址
|
||||
url?: string
|
||||
// 类型
|
||||
type?: string
|
||||
// 标题
|
||||
title?: string
|
||||
// 年份
|
||||
year?: string
|
||||
// TMDBID
|
||||
tmdbid?: number
|
||||
// 季号
|
||||
season?: number
|
||||
// 海报
|
||||
poster?: string
|
||||
// 背景图
|
||||
backdrop?: string
|
||||
// 评分
|
||||
vote?: number
|
||||
// 简介
|
||||
description?: string
|
||||
// 总集数
|
||||
total_episode?: number
|
||||
// 包含
|
||||
include?: string
|
||||
// 排除
|
||||
exclude?: string
|
||||
// 洗版
|
||||
best_version?: number
|
||||
// 是否使用代理服务器
|
||||
proxy?: number
|
||||
// 是否使用过滤规则
|
||||
filter?: boolean
|
||||
// 保存路径
|
||||
save_path?: string
|
||||
// 已处理数量
|
||||
processed?: number
|
||||
// 附加信息
|
||||
note?: string
|
||||
// 最后更新时间
|
||||
last_update?: string
|
||||
// 状态 0-停用,1-启用
|
||||
state?: number
|
||||
}
|
||||
|
||||
// 文件浏览接口
|
||||
export interface EndPoints {
|
||||
list: any
|
||||
@@ -905,4 +922,28 @@ export interface FileItem {
|
||||
extension: string
|
||||
size: number
|
||||
children: FileItem[]
|
||||
modify_time: number
|
||||
}
|
||||
|
||||
// 媒体服务器播放条目
|
||||
export interface MediaServerPlayItem {
|
||||
id?: string | number
|
||||
title: string
|
||||
subtitle?: string
|
||||
type?: string
|
||||
image?: string
|
||||
link?: string
|
||||
percent?: number
|
||||
}
|
||||
|
||||
// 媒体服务器媒体库
|
||||
export interface MediaServerLibrary {
|
||||
server: string
|
||||
id?: string | number
|
||||
name: string
|
||||
path?: string
|
||||
type?: string
|
||||
image?: string
|
||||
image_list?: string[]
|
||||
link?: string
|
||||
}
|
||||
|
||||
BIN
src/assets/images/logos/plugin.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
src/assets/images/misc/emby.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/images/misc/jellyfin.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/images/misc/plex.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
@@ -1,10 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Axios } from 'axios'
|
||||
import axios from 'axios'
|
||||
import List from './filebrowser/List.vue'
|
||||
|
||||
import Toolbar from './filebrowser/Toolbar.vue'
|
||||
import Tree from './filebrowser/Tree.vue'
|
||||
import List from './filebrowser/List.vue'
|
||||
import type { EndPoints } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
@@ -18,6 +18,9 @@ const props = defineProps({
|
||||
axiosconfig: Object,
|
||||
})
|
||||
|
||||
// 对外事件
|
||||
const emit = defineEmits(['pathchanged'])
|
||||
|
||||
const availableStorages = [
|
||||
{
|
||||
name: '本地',
|
||||
@@ -48,14 +51,14 @@ const fileIcons = {
|
||||
other: 'mdi-file-outline',
|
||||
}
|
||||
|
||||
// 当前路径
|
||||
const path = ref(props.path)
|
||||
// 加载次数
|
||||
const loading = ref(0)
|
||||
// 当前存储
|
||||
const activeStorage = ref('local')
|
||||
// 刷新
|
||||
const refreshPending = ref(false)
|
||||
// 排序
|
||||
const sort = ref('name')
|
||||
// axios实例
|
||||
const axiosInstance = ref<Axios>()
|
||||
|
||||
@@ -67,71 +70,82 @@ const storagesArray = computed(() => {
|
||||
|
||||
// 方法
|
||||
function loadingChanged(loading: number) {
|
||||
if (loading)
|
||||
if (loading) {
|
||||
loading++
|
||||
else if (loading > 0)
|
||||
}
|
||||
else if (loading > 0) {
|
||||
loading--
|
||||
}
|
||||
}
|
||||
|
||||
function storageChanged(storage: string) {
|
||||
activeStorage.value = storage
|
||||
}
|
||||
|
||||
// 路径变化
|
||||
function pathChanged(_path: string) {
|
||||
path.value = _path
|
||||
emit('pathchanged', _path)
|
||||
}
|
||||
|
||||
// 排序变化
|
||||
function sortChanged(s: string) {
|
||||
sort.value = s
|
||||
refreshPending.value = true
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onBeforeMount(() => {
|
||||
onMounted(() => {
|
||||
activeStorage.value = props.storage ?? 'local'
|
||||
axiosInstance.value = props.axios ?? axios.create(props.axiosconfig)
|
||||
if (!path.value)
|
||||
pathChanged('/')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="mx-auto" :loading="loading > 0">
|
||||
<Toolbar
|
||||
:path="path"
|
||||
:storages="storagesArray"
|
||||
:storage="activeStorage"
|
||||
:endpoints="props.endpoints"
|
||||
:axios="axiosInstance"
|
||||
@storagechanged="storageChanged"
|
||||
@pathchanged="pathChanged"
|
||||
@foldercreated="refreshPending = true"
|
||||
/>
|
||||
<VRow no-gutters>
|
||||
<VCol v-if="tree" sm="auto" class="d-none d-md-block">
|
||||
<Tree
|
||||
:path="path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
/>
|
||||
</VCol>
|
||||
<VDivider v-if="tree" vertical />
|
||||
<VCol>
|
||||
<List
|
||||
:path="path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
@filedeleted="refreshPending = true"
|
||||
@renamed="refreshPending = true"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VCard class="mx-auto" :loading="loading > 0 || !path">
|
||||
<div v-if="path">
|
||||
<Toolbar
|
||||
:path="path"
|
||||
:storages="storagesArray"
|
||||
:storage="activeStorage"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
@storagechanged="storageChanged"
|
||||
@pathchanged="pathChanged"
|
||||
@foldercreated="refreshPending = true"
|
||||
@sortchanged="sortChanged"
|
||||
/>
|
||||
<VRow no-gutters>
|
||||
<VCol v-if="tree" sm="auto" class="d-none d-md-block">
|
||||
<Tree
|
||||
:path="path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
/>
|
||||
</VCol>
|
||||
<VDivider v-if="tree" vertical />
|
||||
<VCol>
|
||||
<List
|
||||
:path="path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
:sort="sort"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
@filedeleted="refreshPending = true"
|
||||
@renamed="refreshPending = true"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useTheme } from 'vuetify'
|
||||
import miscpose from '@images/pages/pose-fs-9.png'
|
||||
import miscMaskDark from '@images/pages/misc-mask-dark.png'
|
||||
import miscMaskLight from '@images/pages/misc-mask-light.png'
|
||||
import tree from '@images/pages/tree.png'
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const vuetifyTheme = useTheme()
|
||||
|
||||
const authThemeMask = computed(() => {
|
||||
return vuetifyTheme.global.name.value === 'light' ? miscMaskLight : miscMaskDark
|
||||
})
|
||||
|
||||
interface Props {
|
||||
errorCode?: string
|
||||
errorTitle?: string
|
||||
@@ -21,7 +11,7 @@ interface Props {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="misc-wrapper">
|
||||
<div class="flex flex-col">
|
||||
<ErrorHeader
|
||||
:error-code="props.errorCode"
|
||||
:error-title="props.errorTitle"
|
||||
@@ -29,7 +19,7 @@ interface Props {
|
||||
/>
|
||||
|
||||
<!-- 👉 Image -->
|
||||
<div class="misc-avatar text-center">
|
||||
<div class="text-center">
|
||||
<VImg
|
||||
:src="miscpose"
|
||||
class="mx-auto pt-10"
|
||||
@@ -38,40 +28,8 @@ interface Props {
|
||||
/>
|
||||
<slot name="button" />
|
||||
</div>
|
||||
|
||||
<!-- 👉 Footer -->
|
||||
<VImg
|
||||
:src="tree"
|
||||
class="misc-footer-tree d-none d-lg-block"
|
||||
/>
|
||||
|
||||
<VImg
|
||||
:src="authThemeMask"
|
||||
class="misc-footer-img d-none d-md-block"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@configured-variables" as variables;
|
||||
@use '@core/scss/pages/misc.scss';
|
||||
|
||||
.misc-wrapper {
|
||||
position: relative;
|
||||
|
||||
.misc-footer-tree {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
inline-size: 15.625rem;
|
||||
inset-block-end: 3.5rem;
|
||||
inset-inline-start: 0.375rem;
|
||||
left: variables.$layout-vertical-nav-width;
|
||||
}
|
||||
|
||||
.misc-footer-img {
|
||||
position: fixed;
|
||||
inline-size: 100%;
|
||||
inset-block-end: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
89
src/components/cards/BackdropCard.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<MediaServerPlayItem>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
// 图片加载完成响应
|
||||
function imageLoadHandler() {
|
||||
imageLoaded.value = true
|
||||
}
|
||||
|
||||
// 跳转播放
|
||||
function goPlay() {
|
||||
if (props.media?.link)
|
||||
window.open(props.media?.link, '_blank')
|
||||
}
|
||||
|
||||
// 计算图片地址
|
||||
const getImgUrl = computed(() => {
|
||||
const image = props.media?.image || ''
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(image)}/0`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover
|
||||
v-bind="props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
class="ring-gray-500"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'ring-1': imageLoaded,
|
||||
}"
|
||||
@click="goPlay"
|
||||
>
|
||||
<template #image>
|
||||
<VImg
|
||||
:src="getImgUrl"
|
||||
aspect-ratio="2/3"
|
||||
cover
|
||||
@load="imageLoadHandler"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<VCardText
|
||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
>
|
||||
<h1 class="mb-1 text-white text-shadow font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.title }}
|
||||
</h1>
|
||||
<span class="text-shadow">{{ props.media?.subtitle }}</span>
|
||||
</VCardText>
|
||||
</VImg>
|
||||
</template>
|
||||
<div class="w-full absolute bottom-0">
|
||||
<VProgressLinear
|
||||
v-if="props.media?.percent"
|
||||
:model-value="props.media?.percent"
|
||||
bg-color="success"
|
||||
color="success"
|
||||
/>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.text-shadow{
|
||||
text-shadow:1px 1px #777;
|
||||
}
|
||||
</style>
|
||||
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,12 +17,17 @@ function getPercentage() {
|
||||
|
||||
// 速度
|
||||
function getSpeedText() {
|
||||
return `↑ ${props.info?.upspeed}/s ↓ ${props.info?.dlspeed}/s`
|
||||
return `↑ ${props.info?.upspeed}/s ↓ ${props.info?.dlspeed}/s ${props.info?.left_time}`
|
||||
}
|
||||
|
||||
// 下载状态
|
||||
const isDownloading = ref(props.info?.state === 'downloading')
|
||||
|
||||
// 监听props.info?.state的变化
|
||||
watch(() => props.info?.state, (newValue) => {
|
||||
isDownloading.value = newValue === 'downloading';
|
||||
});
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
|
||||
@@ -2,19 +2,30 @@
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
pri: String,
|
||||
maxpri: String,
|
||||
rules: Array as PropType<string[]>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['close', 'changed'])
|
||||
const emit = defineEmits(['close', 'changed', 'levelup', 'leveldown'])
|
||||
|
||||
// 按钮点击
|
||||
function onClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 上升优先级
|
||||
function onLevelUp() {
|
||||
emit('levelup', props.pri)
|
||||
}
|
||||
|
||||
// 下降优先级
|
||||
function onLevelDown() {
|
||||
emit('leveldown', props.pri)
|
||||
}
|
||||
|
||||
// 选项变化
|
||||
function filtersChanged(value: string[]) {
|
||||
emit('changed', props.pri, value)
|
||||
@@ -24,6 +35,11 @@ function filtersChanged(value: string[]) {
|
||||
const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
||||
{ title: '特效字幕', value: ' SPECSUB ' },
|
||||
{ title: '中文字幕', value: ' CNSUB ' },
|
||||
{ title: '国语配音', value: ' CNVOI ' },
|
||||
{ title: '排除: 国语配音', value: ' !CNVOI ' },
|
||||
{ title: '粤语配音', value: ' HKVOI ' },
|
||||
{ title: '排除: 粤语配音', value: ' !HKVOI ' },
|
||||
{ title: '促销: 免费', value: ' FREE ' },
|
||||
{ title: '分辨率: 4K', value: ' 4K ' },
|
||||
{ title: '分辨率: 1080P', value: ' 1080P ' },
|
||||
{ title: '分辨率: 720P', value: ' 720P ' },
|
||||
@@ -38,25 +54,41 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
||||
{ title: '排除: REMUX', value: ' !REMUX ' },
|
||||
{ title: '质量: WEB-DL', value: ' WEBDL ' },
|
||||
{ title: '排除: WEB-DL', value: ' !WEBDL ' },
|
||||
{ title: '质量: 60fps', value: ' 60FPS ' },
|
||||
{ title: '排除: 60fps', value: ' !60FPS ' },
|
||||
{ title: '编码: H265', value: ' H265 ' },
|
||||
{ title: '排除: H265', value: ' !H265 ' },
|
||||
{ title: '编码: H264', value: ' H264 ' },
|
||||
{ title: '排除: H264', value: ' !H264 ' },
|
||||
{ title: '效果: 杜比视界', value: ' DOLBY ' },
|
||||
{ title: '排除: 杜比视界', value: ' !DOLBY ' },
|
||||
{ title: '效果: 杜比全景声', value: ' ATMOS ' },
|
||||
{ title: '排除: 杜比全景声', value: ' !ATMOS ' },
|
||||
{ title: '效果: HDR', value: ' HDR ' },
|
||||
{ title: '排除: HDR', value: ' !HDR ' },
|
||||
{ title: '国语配音', value: ' CNVOI ' },
|
||||
{ title: '排除: 国语配音', value: ' !CNVOI ' },
|
||||
{ title: '促销: 免费', value: ' FREE ' },
|
||||
{ title: '效果: SDR', value: ' SDR ' },
|
||||
{ title: '排除: SDR', value: ' !SDR ' },
|
||||
{ title: '效果: 3D', value: ' 3D ' },
|
||||
{ title: '排除: 3D', value: ' !3D ' },
|
||||
])
|
||||
|
||||
// 已选择的过滤规则
|
||||
const selectedFilters = ref<string[]>(props.rules ?? [])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="tonal" :width="props.width" :height="props.height">
|
||||
<span class="absolute top-3 right-14">
|
||||
<IconBtn
|
||||
v-if="props.pri !== '1'"
|
||||
@click.stop="onLevelUp"
|
||||
>
|
||||
<VIcon icon="mdi-arrow-up" />
|
||||
</IconBtn>
|
||||
<IconBtn
|
||||
v-if="props.pri !== props.maxpri"
|
||||
@click.stop="onLevelDown"
|
||||
>
|
||||
<VIcon icon="mdi-arrow-down" />
|
||||
</IconBtn>
|
||||
</span>
|
||||
<DialogCloseBtn @click="onClose" />
|
||||
<VCardItem>
|
||||
<VCardTitle>优先级 {{ props.pri }}</VCardTitle>
|
||||
@@ -64,7 +96,7 @@ const selectedFilters = ref<string[]>(props.rules ?? [])
|
||||
<VCol>
|
||||
<VSelect
|
||||
:key="props.pri"
|
||||
v-model="selectedFilters"
|
||||
v-model="props.rules"
|
||||
variant="underlined"
|
||||
:items="selectFilterOptions"
|
||||
chips
|
||||
|
||||
202
src/components/cards/LibraryCard.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MediaServerLibrary } from '@/api/types'
|
||||
import plex from '@images/misc/plex.png'
|
||||
import emby from '@images/misc/emby.png'
|
||||
import jellyfin from '@images/misc/jellyfin.png'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<MediaServerLibrary>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// canvas
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
|
||||
// 图片地址
|
||||
const imgUrl = ref('')
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
// 图片是否加载错误
|
||||
const imageError = ref(false)
|
||||
|
||||
// 图片加载完成响应
|
||||
function imageLoadHandler() {
|
||||
imageLoaded.value = true
|
||||
}
|
||||
|
||||
// 图片加载错误
|
||||
function imageErrorHandler() {
|
||||
imageError.value = true
|
||||
}
|
||||
|
||||
// 默认图片
|
||||
function getDefaultImage() {
|
||||
if (props.media?.server === 'plex')
|
||||
return plex
|
||||
else if (props.media?.server === 'emby')
|
||||
return emby
|
||||
else if (props.media?.server === 'jellyfin')
|
||||
return jellyfin
|
||||
else
|
||||
return plex
|
||||
}
|
||||
|
||||
// 跳转播放
|
||||
function goPlay() {
|
||||
if (props.media?.link)
|
||||
window.open(props.media?.link, '_blank')
|
||||
}
|
||||
|
||||
// 生成图片代理路径
|
||||
function getImgUrl(url: string) {
|
||||
if (!url)
|
||||
return getDefaultImage()
|
||||
else
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(url)}/0`
|
||||
}
|
||||
|
||||
// 根据多张图片生成媒体库封面
|
||||
async function drawImages(imageList: string[]) {
|
||||
// 图片
|
||||
const IMAGES = imageList
|
||||
if (IMAGES.length === 0)
|
||||
return getDefaultImage()
|
||||
|
||||
// 为所有图片添加system/img前缀
|
||||
for (let i = 0; i < IMAGES.length; i++)
|
||||
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(IMAGES[i])}/0`
|
||||
|
||||
// canvas
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas)
|
||||
return getDefaultImage()
|
||||
|
||||
// 画布参数
|
||||
const POSTER_WIDTH = (canvas.width - 32) / 4
|
||||
const POSTER_HEIGHT = canvas.height * 0.75 - 8
|
||||
const MARGIN_WIDTH = 4
|
||||
const MARGIN_HEIGHT = 4
|
||||
const REFLECTION_HEIGHT = POSTER_HEIGHT / 2
|
||||
const REFLECTION_SHOW_HEIGHT = canvas.height / 4
|
||||
|
||||
// 获取画布上下文
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx)
|
||||
return getDefaultImage()
|
||||
|
||||
// 设置背景色为黑色
|
||||
ctx.fillStyle = '#000000'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 绘制图片
|
||||
async function drawImageWithReflection(imgSrc: string, index: number) {
|
||||
if (!canvas)
|
||||
return
|
||||
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
const img = new Image()
|
||||
img.setAttribute('crossorigin', 'anonymous')
|
||||
img.src = imgSrc
|
||||
await new Promise(resolve => img.onload = resolve)
|
||||
|
||||
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
|
||||
const y = MARGIN_HEIGHT
|
||||
|
||||
ctx.drawImage(img, x, y, POSTER_WIDTH, POSTER_HEIGHT)
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(0, canvas.height)
|
||||
ctx.scale(1, -1)
|
||||
ctx.drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
img.width,
|
||||
img.height,
|
||||
x,
|
||||
REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT,
|
||||
POSTER_WIDTH,
|
||||
REFLECTION_HEIGHT,
|
||||
)
|
||||
|
||||
const gradient = ctx.createLinearGradient(
|
||||
0,
|
||||
REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT,
|
||||
0,
|
||||
REFLECTION_HEIGHT,
|
||||
)
|
||||
|
||||
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)')
|
||||
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.3)')
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(x, 0, POSTER_WIDTH, REFLECTION_SHOW_HEIGHT)
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
// 绘制多张图片
|
||||
const loopCount = Math.min(4, IMAGES.length)
|
||||
for (let i = 0; i < loopCount; i++)
|
||||
await drawImageWithReflection(IMAGES[i], i + 1)
|
||||
|
||||
// 转换为图片地址
|
||||
return canvas.toDataURL('image/png')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.media?.image_list && props.media?.image_list.length > 0)
|
||||
imgUrl.value = await drawImages(props.media?.image_list || [])
|
||||
else
|
||||
imgUrl.value = getImgUrl(props.media?.image || '')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover
|
||||
v-bind="props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
}"
|
||||
@click="goPlay"
|
||||
>
|
||||
<template #image>
|
||||
<canvas ref="canvasRef" class="w-full h-full hidden" />
|
||||
<VImg
|
||||
:src="imgUrl"
|
||||
aspect-ratio="2/3"
|
||||
cover
|
||||
@load="imageLoadHandler"
|
||||
@error="imageErrorHandler"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<VCardText
|
||||
class="w-full flex flex-col flex-wrap justify-end align-center text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
>
|
||||
<h1 class="mb-1 text-white font-bold line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.name }}
|
||||
</h1>
|
||||
</VCardText>
|
||||
</VImg>
|
||||
</template>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType, Ref } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
@@ -15,6 +16,11 @@ const props = defineProps({
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 订阅规则
|
||||
const subscribeRules = ref({
|
||||
show_edit_dialog: false,
|
||||
})
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -33,12 +39,18 @@ const isSubscribed = ref(false)
|
||||
// 本地存在状态
|
||||
const isExists = ref(false)
|
||||
|
||||
// 各季缺失状态:0-已存在 1-部分缺失 2-全部缺失,没有数据也是已存在
|
||||
// 各季缺失状态:0-已入库 1-部分缺失 2-全部缺失,没有数据也是已入库
|
||||
const seasonsNotExisted = ref<{ [key: number]: number }>({})
|
||||
|
||||
// 订阅季弹窗
|
||||
const subscribeSeasonDialog = ref(false)
|
||||
|
||||
// 订阅编辑弹窗
|
||||
const subscribeEditDialog = ref(false)
|
||||
|
||||
// 订阅ID
|
||||
const subscribeId = ref<number>()
|
||||
|
||||
// 季详情
|
||||
const seasonInfos = ref<TmdbSeason[]>([])
|
||||
|
||||
@@ -86,6 +98,7 @@ async function handleAddSubscribe() {
|
||||
}
|
||||
else {
|
||||
// 弹出季选择列表,支持多选
|
||||
seasonsSelected.value = []
|
||||
subscribeSeasonDialog.value = true
|
||||
}
|
||||
}
|
||||
@@ -112,7 +125,7 @@ async function addSubscribe(season = 0) {
|
||||
// 全部存在时洗版
|
||||
best_version = !seasonsNotExisted.value[season] ? 1 : 0
|
||||
// 请求API
|
||||
const result: { [key: string]: any } = await api.post('subscribe', {
|
||||
const result: { [key: string]: any } = await api.post('subscribe/', {
|
||||
name: props.media?.title,
|
||||
type: props.media?.type,
|
||||
year: props.media?.year,
|
||||
@@ -136,6 +149,12 @@ async function addSubscribe(season = 0) {
|
||||
result.message,
|
||||
best_version,
|
||||
)
|
||||
|
||||
// 弹出订阅编辑弹窗
|
||||
if (result.success && seasonsSelected.value.length <= 1 && subscribeRules.value.show_edit_dialog) {
|
||||
subscribeId.value = result.data.id
|
||||
subscribeEditDialog.value = true
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
@@ -156,9 +175,9 @@ function showSubscribeAddToast(result: boolean,
|
||||
if (best_version > 0)
|
||||
subname = '洗版订阅'
|
||||
|
||||
if (result)
|
||||
if (result && seasonsSelected.value.length > 1)
|
||||
$toast.success(`${title} 添加${subname}成功!`)
|
||||
else
|
||||
else if (!result)
|
||||
$toast.error(`${title} 添加${subname}失败:${message}!`)
|
||||
}
|
||||
|
||||
@@ -206,10 +225,10 @@ async function handleCheckSubscribe() {
|
||||
}
|
||||
}
|
||||
|
||||
// 查询当前媒体是否已存在
|
||||
// 查询当前媒体是否已入库
|
||||
async function handleCheckExists() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('media/exists', {
|
||||
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||
params: {
|
||||
tmdbid: props.media?.tmdb_id,
|
||||
title: props.media?.title,
|
||||
@@ -237,6 +256,7 @@ async function checkSubscribe(season = 0) {
|
||||
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
||||
params: {
|
||||
season,
|
||||
title: props.media?.title,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -254,10 +274,10 @@ async function checkSeasonsNotExists() {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
const result: NotExistMediaInfo[] = await api.post('download/notexists', props.media)
|
||||
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', props.media)
|
||||
if (result) {
|
||||
result.forEach((item) => {
|
||||
// 0-已存在 1-部分缺失 2-全部缺失
|
||||
// 0-已入库 1-部分缺失 2-全部缺失
|
||||
let state = 0
|
||||
if (item.episodes.length === 0)
|
||||
state = 2
|
||||
@@ -287,6 +307,20 @@ async function getMediaSeasons() {
|
||||
}
|
||||
}
|
||||
|
||||
// 查询订阅弹窗规则
|
||||
async function querySubscribeRules() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/DefaultFilterRules',
|
||||
)
|
||||
if (result.data?.value)
|
||||
subscribeRules.value = result.data?.value
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 爱心订阅按钮响应
|
||||
function handleSubscribe() {
|
||||
if (isSubscribed.value)
|
||||
@@ -313,14 +347,14 @@ function getExistColor(season: number) {
|
||||
function getExistText(season: number) {
|
||||
const state = seasonsNotExisted.value[season]
|
||||
if (!state)
|
||||
return '已存在'
|
||||
return '已入库'
|
||||
|
||||
if (state === 1)
|
||||
return '部分缺失'
|
||||
else if (state === 2)
|
||||
return '缺失'
|
||||
else
|
||||
return '已存在'
|
||||
return '已入库'
|
||||
}
|
||||
|
||||
// 打开详情页
|
||||
@@ -358,16 +392,9 @@ function handleSearch() {
|
||||
onBeforeMount(() => {
|
||||
handleCheckSubscribe()
|
||||
handleCheckExists()
|
||||
querySubscribeRules()
|
||||
})
|
||||
|
||||
// 订阅季表头
|
||||
const seasonsHeaders = [
|
||||
{ title: '季', key: 'title', sortable: false },
|
||||
{ title: '集数', key: 'episodes', sortable: false },
|
||||
{ title: '评分', key: 'vote', sortable: false },
|
||||
{ title: '状态', key: 'status', sortable: false },
|
||||
]
|
||||
|
||||
// 计算图片地址
|
||||
const getImgUrl: Ref<string> = computed(() => {
|
||||
if (imageLoadError.value)
|
||||
@@ -379,6 +406,28 @@ const getImgUrl: Ref<string> = computed(() => {
|
||||
|
||||
return url
|
||||
})
|
||||
|
||||
// 拼装季图片地址
|
||||
function getSeasonPoster(posterPath: string) {
|
||||
if (!posterPath)
|
||||
return ''
|
||||
return `https://image.tmdb.org/t/p/w500${posterPath}`
|
||||
}
|
||||
|
||||
// 将yyyy-mm-dd转换为yyyy年mm月dd日
|
||||
function formatAirDate(airDate: string) {
|
||||
if (!airDate)
|
||||
return ''
|
||||
const date = new Date(airDate)
|
||||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
|
||||
}
|
||||
// 从yyyy-mm-dd中提取年份
|
||||
function getYear(airDate: string) {
|
||||
if (!airDate)
|
||||
return ''
|
||||
const date = new Date(airDate)
|
||||
return date.getFullYear()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -460,72 +509,97 @@ const getImgUrl: Ref<string> = computed(() => {
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
<VDialog
|
||||
<!-- 订阅季弹窗 -->
|
||||
<VBottomSheet
|
||||
v-model="subscribeSeasonDialog"
|
||||
max-width="600"
|
||||
content-class="whitespace-nowrap"
|
||||
inset
|
||||
scrollable
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="选择订阅季">
|
||||
<VCardText style="padding: 0;">
|
||||
<VDataTable
|
||||
v-model="seasonsSelected"
|
||||
:headers="seasonsHeaders"
|
||||
:items="seasonInfos"
|
||||
item-value="season_number"
|
||||
return-object
|
||||
fixed-header
|
||||
show-select
|
||||
:items-per-page="100"
|
||||
density="compact"
|
||||
height="auto"
|
||||
<VCard class="rounded-t">
|
||||
<DialogCloseBtn @click="subscribeSeasonDialog = false" />
|
||||
<VCardTitle class="pe-10">
|
||||
订阅 - {{ props.media?.title }}
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<VList
|
||||
v-model:selected="seasonsSelected"
|
||||
lines="three"
|
||||
select-strategy="classic"
|
||||
>
|
||||
<template #item.title="{ item }">
|
||||
<span class="d-block whitespace-nowrap">第 {{ item.raw.season_number }} 季
|
||||
</span>
|
||||
</template>
|
||||
<template #item.episodes="{ item }">
|
||||
<VChip
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
{{ item.raw.episode_count }}
|
||||
</VChip>
|
||||
</template>
|
||||
<template #item.vote="{ item }">
|
||||
{{ item.raw.vote_average }}
|
||||
</template>
|
||||
<template #item.status="{ item }">
|
||||
<VChip
|
||||
v-if="seasonsNotExisted"
|
||||
:color="getExistColor(item.raw.season_number)"
|
||||
flat
|
||||
size="small"
|
||||
>
|
||||
{{ getExistText(item.raw.season_number) }}
|
||||
</VChip>
|
||||
</template>
|
||||
<template #no-data>
|
||||
没有数据
|
||||
</template>
|
||||
<template #bottom />
|
||||
</VDataTable>
|
||||
<VListItem
|
||||
v-for="(item, i) in seasonInfos" :key="i"
|
||||
:value="item"
|
||||
>
|
||||
<template #prepend>
|
||||
<VImg
|
||||
height="90"
|
||||
width="60"
|
||||
:src="getSeasonPoster(item.poster_path || '')"
|
||||
aspect-ratio="2/3"
|
||||
class="object-cover rounded shadow ring-gray-500 me-3"
|
||||
cover
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
第 {{ item.season_number }} 季
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle class="mt-1 me-2">
|
||||
<VChip
|
||||
v-if="item.vote_average"
|
||||
color="primary"
|
||||
size="small"
|
||||
class="mb-1"
|
||||
>
|
||||
<VIcon icon="mdi-star" /> {{ item.vote_average }}
|
||||
</VChip>
|
||||
{{ getYear(item.air_date || '') }} • {{ item.episode_count }} 集
|
||||
</VListItemSubtitle>
|
||||
<VListItemSubtitle>
|
||||
《{{ media?.title }}》第 {{ item.season_number }} 季于 {{ formatAirDate(item.air_date || '') }} 首播。
|
||||
</VListItemSubtitle>
|
||||
<VListItemSubtitle>
|
||||
<VChip
|
||||
v-if="seasonsNotExisted"
|
||||
class="mt-2"
|
||||
size="small"
|
||||
:color="getExistColor(item.season_number || 0)"
|
||||
>
|
||||
{{ getExistText(item.season_number || 0) }}
|
||||
</VChip>
|
||||
</VListItemSubtitle>
|
||||
<template #append="{ isSelected }">
|
||||
<VListItemAction start>
|
||||
<VSwitch :model-value="isSelected" />
|
||||
</VListItemAction>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn @click="subscribeSeasonDialog = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<div class="my-2 text-center">
|
||||
<VBtn
|
||||
:disabled="seasonsSelected.length === 0"
|
||||
width="30%"
|
||||
@click="subscribeSeasons"
|
||||
@keydown.enter="subscribeSeasons"
|
||||
>
|
||||
确定
|
||||
{{ seasonsSelected.length === 0 ? '请选择订阅季' : '提交订阅' }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</div>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</VBottomSheet>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<SubscribeEditForm
|
||||
v-model="subscribeEditDialog"
|
||||
:subid="subscribeId"
|
||||
@close="subscribeEditDialog = false"
|
||||
@save="subscribeEditDialog = false"
|
||||
@remove="() => { subscribeEditDialog = false; handleCheckSubscribe(); }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
175
src/components/cards/MediaInfoCard.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
import type { Context } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
context: Object as PropType<Context>,
|
||||
})
|
||||
|
||||
// TMDB图片转换为w500大小
|
||||
function getW500Image(url = '') {
|
||||
if (!url)
|
||||
return ''
|
||||
return url.replace('original', 'w500')
|
||||
}
|
||||
|
||||
// 打开TMDB详情页面
|
||||
function openTmdbPage(type: string, tmdbId: number) {
|
||||
if (!type || !tmdbId)
|
||||
return
|
||||
|
||||
const url = `https://www.themoviedb.org/${type === '电影' ? 'movie' : 'tv'}/${tmdbId}`
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-show="context">
|
||||
<VCol>
|
||||
<div
|
||||
v-if="context?.meta_info?.name"
|
||||
class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row"
|
||||
>
|
||||
<div
|
||||
v-if="context?.media_info?.poster_path"
|
||||
class="ma-auto"
|
||||
>
|
||||
<VImg
|
||||
width="10rem"
|
||||
aspect-ratio="2/3"
|
||||
class="object-cover aspect-w-2 aspect-h-3 rounded-lg ring-1 ring-gray-500"
|
||||
:src="getW500Image(context?.media_info?.poster_path)"
|
||||
cover
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<VCardItem class="pb-1">
|
||||
<VCardTitle class="text-center text-md-left">
|
||||
{{ context?.media_info?.title || context?.meta_info?.name }}
|
||||
{{ context?.meta_info?.season_episode }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle class="text-center text-md-left">
|
||||
{{ context?.media_info?.year || context?.meta_info?.year }}
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText
|
||||
v-if="context?.media_info?.overview"
|
||||
class="line-clamp-4 overflow-hidden text-ellipsis text-center text-md-left ..."
|
||||
>
|
||||
{{ context?.media_info?.overview }}
|
||||
</VCardText>
|
||||
|
||||
<VCardItem class="text-center text-md-left">
|
||||
<!-- 类型 -->
|
||||
<VChip
|
||||
v-if="context?.media_info?.type || context?.meta_info?.type"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-blue-500"
|
||||
>
|
||||
{{
|
||||
context?.media_info?.type || context?.meta_info?.type
|
||||
}}
|
||||
</VChip>
|
||||
<!-- 二级分类 -->
|
||||
<VChip
|
||||
v-if="context?.media_info?.category"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-blue-500"
|
||||
>
|
||||
{{ context?.media_info?.category }}
|
||||
</VChip>
|
||||
<!-- TMDBID -->
|
||||
<VChip
|
||||
v-if="context?.media_info?.tmdb_id"
|
||||
variant="elevated"
|
||||
color="success"
|
||||
class="me-1 mb-1"
|
||||
@click="openTmdbPage(context?.media_info?.type || '', context?.media_info?.tmdb_id)"
|
||||
>
|
||||
{{ context?.media_info?.tmdb_id }}
|
||||
</VChip>
|
||||
<!-- meta_info -->
|
||||
<VChip
|
||||
v-if="context?.meta_info?.edition"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
{{ context?.meta_info?.edition }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="context?.meta_info?.resource_pix"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
{{ context?.meta_info?.resource_pix }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="context?.meta_info?.video_encode"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-orange-500"
|
||||
>
|
||||
{{ context?.meta_info?.video_encode }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="context?.meta_info?.audio_encode"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-orange-500"
|
||||
>
|
||||
{{ context?.meta_info?.audio_encode }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="context?.meta_info?.resource_team"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-cyan-500"
|
||||
>
|
||||
{{ context?.meta_info?.resource_team }}
|
||||
</VChip>
|
||||
</VCardItem>
|
||||
</div>
|
||||
</div>
|
||||
<VAlert
|
||||
v-if="!context?.meta_info?.name"
|
||||
icon="mdi-alert-circle-outline"
|
||||
>
|
||||
识别失败,无法识别到有效信息!
|
||||
</VAlert>
|
||||
</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>
|
||||
</template>
|
||||
@@ -2,6 +2,8 @@
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { getDominantColor } from '@/@core/utils/image'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -13,19 +15,55 @@ const props = defineProps({
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['install'])
|
||||
|
||||
// 背景颜色
|
||||
const backgroundColor = ref('#28A9E1')
|
||||
|
||||
// 图片对象
|
||||
const imageRef = ref<any>()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 进度框
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 进度框文本
|
||||
const progressText = ref('正在安装插件...')
|
||||
|
||||
// 图片是否加载完成
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 图片是否加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 图片加载完成
|
||||
async function imageLoaded() {
|
||||
isImageLoaded.value = true
|
||||
const imageElement = imageRef.value?.$el.querySelector('img') as HTMLImageElement
|
||||
// 从图片中提取背景色
|
||||
backgroundColor.value = await getDominantColor(imageElement)
|
||||
}
|
||||
|
||||
// 安装插件
|
||||
async function installPlugin() {
|
||||
try {
|
||||
// 显示等待提示框
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在安装 ${props.plugin?.plugin_name} ${props?.plugin?.plugin_version} 插件...`
|
||||
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`plugin/install/${props.plugin?.id}`,
|
||||
{
|
||||
params: {
|
||||
repo_url: props.plugin?.repo_url,
|
||||
force: props.plugin?.has_update,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// 隐藏等待提示框
|
||||
progressDialog.value = false
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(`插件 ${props.plugin?.plugin_name} 安装成功!`)
|
||||
|
||||
@@ -33,13 +71,63 @@ async function installPlugin() {
|
||||
emit('install')
|
||||
}
|
||||
else {
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 安装失败:${result.message}}`)
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 安装失败:${result.message}`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算图标路径
|
||||
const iconPath: Ref<string> = computed(() => {
|
||||
if (imageLoadError.value)
|
||||
return noImage
|
||||
// 如果是网络图片则使用代理后返回
|
||||
if (props.plugin?.plugin_icon?.startsWith('http'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}/1`
|
||||
|
||||
return `./plugin_icon/${props.plugin?.plugin_icon}`
|
||||
})
|
||||
|
||||
// 访问插件页面
|
||||
function visitPluginPage() {
|
||||
// 将raw.githubusercontent.com转换为项目地址
|
||||
let repoUrl = props.plugin?.repo_url
|
||||
if (repoUrl) {
|
||||
if (repoUrl.includes('raw.githubusercontent.com')) {
|
||||
if (!repoUrl.endsWith('/'))
|
||||
repoUrl += '/'
|
||||
|
||||
if (repoUrl.split('/').length < 6)
|
||||
repoUrl = `${repoUrl}main/`
|
||||
|
||||
try {
|
||||
const [user, repo] = repoUrl.split('/').slice(-4, -2)
|
||||
repoUrl = `https://github.com/${user}/${repo}`
|
||||
}
|
||||
catch (error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
repoUrl = props.plugin?.author_url
|
||||
}
|
||||
window.open(repoUrl, '_blank')
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '查看详情',
|
||||
value: 1,
|
||||
props: {
|
||||
prependIcon: 'mdi-information-outline',
|
||||
click: visitPluginPage,
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -50,17 +138,51 @@ async function installPlugin() {
|
||||
>
|
||||
<div
|
||||
class="relative pa-4 text-center card-cover-blurred"
|
||||
:style="{ background: `${props.plugin?.plugin_color}` }"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div class="me-n3 absolute top-0 right-3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" class="text-white" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.plugin?.has_update"
|
||||
class="me-n3 absolute top-0 left-1"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-new-box"
|
||||
class="text-white"
|
||||
/>
|
||||
</div>
|
||||
<VAvatar
|
||||
size="128"
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
size="8rem"
|
||||
>
|
||||
<VImg
|
||||
:src="`/plugin/${props.plugin?.plugin_icon}`"
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
@load="isImageLoaded = true"
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
@@ -76,9 +198,29 @@ async function installPlugin() {
|
||||
@click.stop
|
||||
>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a>
|
||||
</a><br>
|
||||
版本:{{ props.plugin?.plugin_version }}
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<!-- 安装插件进度框 -->
|
||||
<VDialog
|
||||
v-model="progressDialog"
|
||||
:scrim="false"
|
||||
width="25rem"
|
||||
>
|
||||
<VCard
|
||||
color="primary"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
{{ progressText }}
|
||||
<VProgressLinear
|
||||
indeterminate
|
||||
color="white"
|
||||
class="mb-0 mt-1"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import FormRender from '@/components/render/FormRender.vue'
|
||||
import PageRender from '@/components/render/PageRender.vue'
|
||||
import { isNullOrEmptyObject } from '@core/utils'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { getDominantColor } from '@/@core/utils/image'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -16,9 +19,18 @@ const props = defineProps({
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['remove', 'save'])
|
||||
|
||||
// 背景颜色
|
||||
const backgroundColor = ref('#28A9E1')
|
||||
|
||||
// 图片对象
|
||||
const imageRef = ref<any>()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 本身是否可见
|
||||
const isVisible = ref(true)
|
||||
|
||||
@@ -31,17 +43,44 @@ const pluginConfigForm = ref({})
|
||||
// 插件表单配置项
|
||||
let pluginFormItems = reactive([])
|
||||
|
||||
// 插件详情页面
|
||||
// 插件数据页面
|
||||
const pluginInfoDialog = ref(false)
|
||||
|
||||
// 插件详情页面配置项
|
||||
// 插件数据页面配置项
|
||||
let pluginPageItems = reactive([])
|
||||
|
||||
// 图片是否加载完成
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 图片是否加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 图片加载完成
|
||||
async function imageLoaded() {
|
||||
isImageLoaded.value = true
|
||||
const imageElement = imageRef.value?.$el.querySelector('img') as HTMLImageElement
|
||||
// 从图片中提取背景色
|
||||
backgroundColor.value = await getDominantColor(imageElement)
|
||||
}
|
||||
|
||||
// 调用API卸载插件
|
||||
async function uninstallPlugin() {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认卸载插件 ${props.plugin?.plugin_name} ?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (!isConfirmed)
|
||||
return
|
||||
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(`plugin/${props.plugin?.id}`)
|
||||
if (result.success) {
|
||||
@@ -74,7 +113,7 @@ async function loadPluginForm() {
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API读取详情页面
|
||||
// 调用API读取数据页面
|
||||
async function loadPluginPage() {
|
||||
try {
|
||||
const result: [] = await api.get(`plugin/page/${props.plugin?.id}`)
|
||||
@@ -117,8 +156,10 @@ async function savePluginConf() {
|
||||
}
|
||||
}
|
||||
|
||||
// 显示插件详情
|
||||
// 显示插件数据
|
||||
async function showPluginInfo() {
|
||||
// 加载数据
|
||||
await loadPluginPage()
|
||||
pluginConfigDialog.value = false
|
||||
pluginInfoDialog.value = true
|
||||
}
|
||||
@@ -129,23 +170,110 @@ async function showPluginConfig() {
|
||||
await loadPluginForm()
|
||||
// 加载配置
|
||||
await loadPluginConf()
|
||||
// 加载详情
|
||||
await loadPluginPage()
|
||||
// 显示对话框
|
||||
pluginInfoDialog.value = false
|
||||
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)}/1`
|
||||
|
||||
return `./plugin_icon/${props.plugin?.plugin_icon}`
|
||||
})
|
||||
|
||||
// 重置插件
|
||||
async function resetPlugin() {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认重置插件 ${props.plugin?.plugin_name} 的配置数据?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
if (!isConfirmed)
|
||||
return
|
||||
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(`plugin/reset/${props.plugin?.id}`)
|
||||
if (result.success) {
|
||||
$toast.success(`插件 ${props.plugin?.plugin_name} 数据已重置`)
|
||||
// 通知父组件刷新
|
||||
emit('save')
|
||||
}
|
||||
else {
|
||||
$toast.error(`插件 ${props.plugin?.plugin_name} 重置失败:${result.message}}`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 访问作者主页
|
||||
function visitAuthorPage() {
|
||||
window.open(props.plugin?.author_url, '_blank')
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '卸载',
|
||||
title: '查看数据',
|
||||
value: 1,
|
||||
show: props.plugin?.has_page,
|
||||
props: {
|
||||
prependIcon: 'mdi-information-outline',
|
||||
click: showPluginInfo,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '设置',
|
||||
value: 2,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-cog-outline',
|
||||
click: showPluginConfig,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '重置',
|
||||
value: 3,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-cancel',
|
||||
color: 'warning',
|
||||
click: resetPlugin,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '卸载',
|
||||
value: 4,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
click: uninstallPlugin,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '作者主页',
|
||||
value: 4,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-home-circle-outline',
|
||||
click: visitAuthorPage,
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -155,11 +283,16 @@ const dropdownItems = ref([
|
||||
v-if="isVisible"
|
||||
:width="props.width"
|
||||
:height="props.height"
|
||||
@click="showPluginConfig"
|
||||
@click="() => {
|
||||
if (props.plugin?.has_page)
|
||||
showPluginInfo()
|
||||
else
|
||||
showPluginConfig()
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="relative pa-4 text-center card-cover-blurred"
|
||||
:style="{ background: `${props.plugin?.plugin_color}` }"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div class="me-n3 absolute top-0 right-3">
|
||||
<IconBtn>
|
||||
@@ -171,6 +304,7 @@ const dropdownItems = ref([
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
v-show="item.show"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="item.props.color"
|
||||
@@ -186,21 +320,23 @@ const dropdownItems = ref([
|
||||
</IconBtn>
|
||||
</div>
|
||||
<VAvatar
|
||||
size="128"
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
size="8rem"
|
||||
>
|
||||
<VImg
|
||||
:src="`/plugin/${props.plugin?.plugin_icon}`"
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
<VCardItem class="py-2">
|
||||
<VCardTitle class="flex items-center flex-row">
|
||||
<VBadge v-if="props.plugin?.state" dot inline color="success" class="me-1 mb-1" />
|
||||
{{ props.plugin?.plugin_name }}
|
||||
{{ props.plugin?.plugin_name }}<span class="text-sm ms-2 mt-1 text-gray-500">{{ props.plugin?.plugin_version }}</span>
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
@@ -210,11 +346,13 @@ const dropdownItems = ref([
|
||||
<!-- 插件配置页面 -->
|
||||
<VDialog
|
||||
v-model="pluginConfigDialog"
|
||||
max-width="800"
|
||||
scrollable
|
||||
persistent
|
||||
max-width="60rem"
|
||||
>
|
||||
<VCard :title="props.plugin?.plugin_name">
|
||||
<VCard
|
||||
:title="`${props.plugin?.plugin_name} - 配置`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<DialogCloseBtn @click="pluginConfigDialog = false" />
|
||||
<VCardText>
|
||||
<FormRender
|
||||
@@ -226,24 +364,29 @@ const dropdownItems = ref([
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo">
|
||||
详情
|
||||
查看数据
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="savePluginConf">
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="savePluginConf"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- 插件详情页面 -->
|
||||
<!-- 插件数据页面 -->
|
||||
<VDialog
|
||||
v-model="pluginInfoDialog"
|
||||
max-width="1000"
|
||||
scrollable
|
||||
persistent
|
||||
max-width="80rem"
|
||||
>
|
||||
<VCard :title="`${props.plugin?.plugin_name} - 详情`">
|
||||
<VCard
|
||||
:title="`${props.plugin?.plugin_name}`"
|
||||
class="rounded-t"
|
||||
>
|
||||
<DialogCloseBtn @click="pluginInfoDialog = false" />
|
||||
<VCardText>
|
||||
<PageRender
|
||||
@@ -253,8 +396,16 @@ const dropdownItems = ref([
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn
|
||||
@click="showPluginConfig"
|
||||
>
|
||||
配置
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="pluginInfoDialog = false">
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="pluginInfoDialog = false"
|
||||
>
|
||||
关闭
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
|
||||
102
src/components/cards/PosterCard.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
import type { MediaServerPlayItem } from '@/api/types'
|
||||
import noImage from '@images/no-image.jpeg'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<MediaServerPlayItem>,
|
||||
width: String,
|
||||
height: String,
|
||||
})
|
||||
|
||||
// 图片加载状态
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
// 图片加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// 角标颜色
|
||||
function getChipColor(type: string) {
|
||||
if (type === '电影')
|
||||
return 'border-blue-500 bg-blue-600'
|
||||
else if (type === '电视剧')
|
||||
return ' bg-indigo-500 border-indigo-600'
|
||||
else
|
||||
return 'border-purple-600 bg-purple-600'
|
||||
}
|
||||
|
||||
// 计算图片地址
|
||||
const getImgUrl = computed(() => {
|
||||
if (imageLoadError.value)
|
||||
return noImage
|
||||
const image = props.media?.image || ''
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(image)}/0`
|
||||
})
|
||||
|
||||
// 跳转播放
|
||||
function goPlay() {
|
||||
if (props.media?.link)
|
||||
window.open(props.media?.link, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VHover v-bind="props">
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
class="outline-none shadow ring-gray-500 rounded-lg"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'ring-1': isImageLoaded,
|
||||
}"
|
||||
@click.stop="goPlay"
|
||||
>
|
||||
<VImg
|
||||
aspect-ratio="2/3"
|
||||
:src="getImgUrl"
|
||||
class="object-cover aspect-w-2 aspect-h-3"
|
||||
:class="hover.isHovering ? 'on-hover' : ''"
|
||||
cover
|
||||
@load="isImageLoaded = true"
|
||||
@error="imageLoadError = true"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
<!-- 类型角标 -->
|
||||
<VChip
|
||||
v-show="isImageLoaded"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
:class="getChipColor(props.media?.type || '')"
|
||||
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||
>
|
||||
{{ props.media?.type }}
|
||||
</VChip>
|
||||
<!-- 详情 -->
|
||||
<VCardText
|
||||
v-show="hover.isHovering || imageLoadError"
|
||||
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||
>
|
||||
<span class="font-bold">{{ props.media?.subtitle }}</span>
|
||||
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.title }}
|
||||
</h1>
|
||||
</VCardText>
|
||||
</VImg>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.on-hover img {
|
||||
@apply brightness-50;
|
||||
}
|
||||
</style>
|
||||
@@ -1,590 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { calculateTimeDifference } from '@/@core/utils'
|
||||
import { formatFileSize, formatSeason } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import type { Rss, Site, TorrentInfo } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<Rss>,
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['remove', 'save'])
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
// 订阅弹窗
|
||||
const rssInfoDialog = ref(false)
|
||||
|
||||
// RSS预览窗口
|
||||
const rssPreviewDialog = ref(false)
|
||||
|
||||
// 加载状态
|
||||
const previewLoading = ref(false)
|
||||
|
||||
// 总条数
|
||||
const previewTotalItems = ref(0)
|
||||
|
||||
// 每页条数
|
||||
const previewItemsPerPage = ref(25)
|
||||
|
||||
// 预览表头
|
||||
const previewHeaders = [
|
||||
{ title: '标题', key: 'title', sortable: true },
|
||||
{ title: '时间', key: 'pubdate', sortable: true },
|
||||
{ title: '大小', key: 'size', sortable: true },
|
||||
{ title: '', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
// 预览数据
|
||||
const previewDataList = ref<TorrentInfo[]>([])
|
||||
|
||||
// 站点名称
|
||||
const siteName = ref('')
|
||||
|
||||
// 订阅编辑表单
|
||||
const rssForm = reactive<any>(props.media ?? {})
|
||||
|
||||
// 类型转换
|
||||
rssForm.best_version = rssForm.best_version === 1
|
||||
rssForm.proxy = rssForm.proxy === 1
|
||||
rssForm.filter = rssForm.filter === 1
|
||||
|
||||
// 上一次更新时间
|
||||
const lastUpdateText = ref(
|
||||
`${
|
||||
props.media?.last_update
|
||||
? `${calculateTimeDifference(props.media?.last_update || '')}前`
|
||||
: ''
|
||||
}`,
|
||||
)
|
||||
|
||||
// 图片加载完成响应
|
||||
function imageLoadHandler() {
|
||||
imageLoaded.value = true
|
||||
}
|
||||
|
||||
// 根据 type 返回不同的图标
|
||||
function getIcon() {
|
||||
if (props.media?.type === '电影')
|
||||
return 'mdi-movie'
|
||||
else if (props.media?.type === '电视剧')
|
||||
return 'mdi-television-classic'
|
||||
else
|
||||
return 'mdi-help-circle'
|
||||
}
|
||||
|
||||
// 计算文本颜色
|
||||
function getTextColor() {
|
||||
return imageLoaded.value ? 'white' : ''
|
||||
}
|
||||
|
||||
// 计算文本类
|
||||
function getTextClass() {
|
||||
return imageLoaded.value ? 'text-white' : ''
|
||||
}
|
||||
|
||||
// 删除订阅
|
||||
async function removerRss() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(
|
||||
`rss/${props.media?.id}`,
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
// 通知父组件刷新
|
||||
emit('remove')
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API修改订阅
|
||||
async function updateRssInfo() {
|
||||
rssInfoDialog.value = false
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.put('rss', rssForm)
|
||||
|
||||
// 提示
|
||||
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 querySiteName() {
|
||||
try {
|
||||
const result: Site = await api.get(
|
||||
`site/domain/${props.media?.url?.split('/')[2]}`,
|
||||
)
|
||||
|
||||
if (result)
|
||||
siteName.value = result.name
|
||||
}
|
||||
catch (e) {
|
||||
// 截取URL中的主域名作为站点名称
|
||||
siteName.value = props.media?.url?.split('/')[2] ?? '未知'
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 预览按钮响应
|
||||
async function handleRssPreview() {
|
||||
rssPreviewDialog.value = true
|
||||
previewLoading.value = true
|
||||
await previewRss()
|
||||
previewLoading.value = false
|
||||
}
|
||||
|
||||
// 预览站点RSS
|
||||
async function previewRss() {
|
||||
try {
|
||||
const result: TorrentInfo[] = await api.get(
|
||||
`rss/preview/${props.media?.id}`,
|
||||
)
|
||||
previewDataList.value = result
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑订阅响应
|
||||
async function editRssDialog() {
|
||||
rssInfoDialog.value = true
|
||||
}
|
||||
|
||||
// 刷新按钮响应
|
||||
async function refreshRss() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`rss/refresh/${props.media?.id}`,
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success(`${props.media?.name} 已提交刷新任务!`)
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成1到50季的下拉框选项
|
||||
const seasonItems = ref(
|
||||
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
|
||||
title: `第 ${item} 季`,
|
||||
value: item,
|
||||
})),
|
||||
)
|
||||
|
||||
// 打开种子详情页面
|
||||
function openTorrentDetail(page_url: string) {
|
||||
window.open(page_url, '_blank')
|
||||
}
|
||||
|
||||
// 下载种子文件
|
||||
async function downloadTorrentFile(enclosure: string) {
|
||||
window.open(enclosure, '_blank')
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '编辑',
|
||||
value: 1,
|
||||
props: {
|
||||
prependIcon: 'mdi-file-edit-outline',
|
||||
click: editRssDialog,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '预览',
|
||||
value: 2,
|
||||
props: {
|
||||
prependIcon: 'mdi-eye-outline',
|
||||
click: handleRssPreview,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '刷新',
|
||||
value: 3,
|
||||
props: {
|
||||
prependIcon: 'mdi-refresh',
|
||||
click: refreshRss,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '取消订阅',
|
||||
value: 4,
|
||||
props: {
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
click: removerRss,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
querySiteName()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
:key="props.media?.id"
|
||||
:class="`${rssForm.best_version ? 'outline-dashed outline-1' : ''}`"
|
||||
@click="editRssDialog"
|
||||
>
|
||||
<template #image>
|
||||
<VImg
|
||||
:src="props.media?.backdrop || props.media?.poster"
|
||||
aspect-ratio="2/3"
|
||||
cover
|
||||
class="brightness-50"
|
||||
@load="imageLoadHandler"
|
||||
/>
|
||||
</template>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
size="1.9rem"
|
||||
:color="getTextColor()"
|
||||
:icon="getIcon()"
|
||||
/>
|
||||
</template>
|
||||
<VCardTitle :class="getTextClass()">
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : "") }}
|
||||
</VCardTitle>
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
:color="getTextColor()"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="item.props.color"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<p
|
||||
class="clamp-text mb-0"
|
||||
:class="getTextClass()"
|
||||
>
|
||||
{{ props.media?.description }}
|
||||
</p>
|
||||
</VCardText>
|
||||
|
||||
<VCardText class="d-flex justify-space-between align-center flex-wrap">
|
||||
<div class="d-flex align-center">
|
||||
<IconBtn
|
||||
icon="mdi-star"
|
||||
:color="getTextColor()"
|
||||
class="me-1"
|
||||
/>
|
||||
<span
|
||||
class="text-subtitle-2 me-4"
|
||||
:class="getTextClass()"
|
||||
>{{
|
||||
props.media?.vote
|
||||
}}</span>
|
||||
<IconBtn
|
||||
v-bind="props"
|
||||
icon="mdi-progress-clock"
|
||||
:color="getTextColor()"
|
||||
class="me-1"
|
||||
/>
|
||||
<span
|
||||
class="text-subtitle-2 me-4"
|
||||
:class="getTextClass()"
|
||||
>{{ props.media?.processed || 0 }}</span>
|
||||
<IconBtn
|
||||
v-if="siteName"
|
||||
icon="mdi-web"
|
||||
:color="getTextColor()"
|
||||
class="me-1"
|
||||
/>
|
||||
<span
|
||||
v-if="siteName"
|
||||
class="text-subtitle-2 me-4"
|
||||
:class="getTextClass()"
|
||||
>
|
||||
{{ siteName }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText
|
||||
v-if="lastUpdateText"
|
||||
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-download"
|
||||
class="me-1"
|
||||
/> {{ lastUpdateText }}
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<VDialog
|
||||
v-model="rssInfoDialog"
|
||||
max-width="800"
|
||||
persistent
|
||||
scrollable
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard :title="`订阅 - ${props.media?.name}`">
|
||||
<VCardText class="pt-2">
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.url"
|
||||
label="RSS地址"
|
||||
placeholder="https://example.com/rss"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSelect
|
||||
v-model="rssForm.type"
|
||||
label="类型"
|
||||
:items="[{ title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
|
||||
readonly
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.title"
|
||||
label="标题"
|
||||
readonly
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.year"
|
||||
label="年份"
|
||||
readonly
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSelect
|
||||
v-show="rssForm.type === '电视剧'"
|
||||
v-model="rssForm.season"
|
||||
label="季"
|
||||
:items="seasonItems"
|
||||
readonly
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.include"
|
||||
label="包含"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.exclude"
|
||||
label="排除"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.save_path"
|
||||
label="保存路径"
|
||||
placeholder="留空自动"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSelect
|
||||
v-model="rssForm.state"
|
||||
label="状态"
|
||||
:items="[{
|
||||
title: '启用',
|
||||
value: 1,
|
||||
}, {
|
||||
title: '停用',
|
||||
value: 0,
|
||||
}]"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="rssForm.best_version"
|
||||
label="洗版"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="rssForm.proxy"
|
||||
label="代理服务器"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="rssForm.filter"
|
||||
label="过滤规则"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VBtn @click="rssInfoDialog = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="updateRssInfo">
|
||||
确定
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- RSS预览窗口 -->
|
||||
<VDialog
|
||||
v-model="rssPreviewDialog"
|
||||
max-width="1280"
|
||||
scrollable
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="RSS预览">
|
||||
<DialogCloseBtn @click="rssPreviewDialog = false" />
|
||||
<VCardText class="pt-2">
|
||||
<VDataTable
|
||||
v-model:items-per-page="previewItemsPerPage"
|
||||
:headers="previewHeaders"
|
||||
:items="previewDataList"
|
||||
:items-length="previewTotalItems"
|
||||
:loading="previewLoading"
|
||||
density="compact"
|
||||
item-value="title"
|
||||
return-object
|
||||
fixed-header
|
||||
items-per-page-text="每页条数"
|
||||
page-text="{0}-{1} 共 {2} 条"
|
||||
>
|
||||
<template #item.title="{ item }">
|
||||
<div class="text-high-emphasis">
|
||||
{{ item.raw.title }}
|
||||
</div>
|
||||
</template>
|
||||
<template #item.size="{ item }">
|
||||
<div class="text-nowrap whitespace-nowrap">
|
||||
{{ formatFileSize(item.raw.size) }}
|
||||
</div>
|
||||
</template>
|
||||
<template #item.pubdate="{ item }">
|
||||
<div class="text-sm">
|
||||
{{ item.raw.pubdate }}
|
||||
</div>
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="openTorrentDetail(item.raw.page_url)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-information" />
|
||||
</template>
|
||||
<VListItemTitle>查看详情</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="downloadTorrentFile(item.raw.enclosure)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-download" />
|
||||
</template>
|
||||
<VListItemTitle>下载种子</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
<template #no-data>
|
||||
没有数据
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import SiteAddEditForm from '../form/SiteAddEditForm.vue'
|
||||
import { formatFileSize } from '@core/utils/formatters'
|
||||
import { numberValidator, requiredValidator } from '@/@validators'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Site, TorrentInfo } from '@/api/types'
|
||||
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)
|
||||
@@ -32,9 +33,6 @@ const testButtonText = ref('测试')
|
||||
// 测试按钮可用性
|
||||
const testButtonDisable = ref(false)
|
||||
|
||||
// 更新按钮文字
|
||||
const updateButtonText = ref('更新')
|
||||
|
||||
// 更新按钮可用性
|
||||
const updateButtonDisable = ref(false)
|
||||
|
||||
@@ -42,11 +40,17 @@ const updateButtonDisable = ref(false)
|
||||
const siteCookieDialog = ref(false)
|
||||
|
||||
// 站点编辑弹窗
|
||||
const siteInfoDialog = ref(false)
|
||||
const siteEditDialog = ref(false)
|
||||
|
||||
// 资源浏览弹窗
|
||||
const resourceDialog = ref(false)
|
||||
|
||||
// 进度条
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 进度文本
|
||||
const progressText = ref('请稍候 ...')
|
||||
|
||||
// 资源浏览表头
|
||||
const resourceHeaders = [
|
||||
{ title: '标题', key: 'title', sortable: false },
|
||||
@@ -72,36 +76,13 @@ const resourceTotalItems = ref(0)
|
||||
// 每页条数
|
||||
const resourceItemsPerPage = ref(25)
|
||||
|
||||
// 当前页码
|
||||
const resourceCurrentPage = ref(0)
|
||||
|
||||
// 用户名密码表单
|
||||
const userPwForm = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
code: '',
|
||||
})
|
||||
|
||||
// 状态下拉项
|
||||
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) {
|
||||
window.open(page_url, '_blank')
|
||||
@@ -147,11 +128,6 @@ async function handleSiteUpdate() {
|
||||
siteCookieDialog.value = true
|
||||
}
|
||||
|
||||
// 打开站点编辑弹窗
|
||||
async function handleSiteInfo() {
|
||||
siteInfoDialog.value = true
|
||||
}
|
||||
|
||||
// 打开资源浏览弹窗
|
||||
async function handleResourceBrowse() {
|
||||
resourceDialog.value = true
|
||||
@@ -166,15 +142,18 @@ async function updateSiteCookie() {
|
||||
|
||||
// 更新按钮状态
|
||||
siteCookieDialog.value = false
|
||||
updateButtonText.value = '更新中 ...'
|
||||
updateButtonDisable.value = true
|
||||
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在更新 ${cardProps.site?.name} Cookie & UA ...`
|
||||
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`site/cookie/${cardProps.site?.id}`,
|
||||
{
|
||||
params: {
|
||||
username: userPwForm.value.username,
|
||||
password: userPwForm.value.password,
|
||||
code: userPwForm.value.code,
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -184,7 +163,7 @@ async function updateSiteCookie() {
|
||||
else
|
||||
$toast.error(`${cardProps.site?.name} 更新失败:${result.message}`)
|
||||
|
||||
updateButtonText.value = '更新'
|
||||
progressDialog.value = false
|
||||
updateButtonDisable.value = false
|
||||
}
|
||||
catch (error) {
|
||||
@@ -192,42 +171,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类
|
||||
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||
if (downloadVolume === 0)
|
||||
@@ -244,13 +187,7 @@ function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||
async function getResourceList() {
|
||||
resourceLoading.value = true
|
||||
try {
|
||||
resourceDataList.value = await api.get('search/title', {
|
||||
params: {
|
||||
keyword: resourceSearch.value,
|
||||
page: resourceCurrentPage.value,
|
||||
site: cardProps.site?.id,
|
||||
},
|
||||
})
|
||||
resourceDataList.value = await api.get(`site/resource/${cardProps.site?.id}`)
|
||||
resourceLoading.value = false
|
||||
}
|
||||
catch (error) {
|
||||
@@ -273,9 +210,9 @@ onMounted(() => {
|
||||
<VCard
|
||||
:height="cardProps.height"
|
||||
:width="cardProps.width"
|
||||
:flat="!siteForm.is_active"
|
||||
:flat="!cardProps.site?.is_active"
|
||||
class="overflow-hidden"
|
||||
@click="handleSiteInfo"
|
||||
@click="siteEditDialog = true"
|
||||
>
|
||||
<template #image>
|
||||
<VAvatar
|
||||
@@ -287,17 +224,19 @@ onMounted(() => {
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardItem>
|
||||
<VCardTitle class="font-bold" @click.stop="openSitePage">
|
||||
{{ cardProps.site?.name }}
|
||||
<VCardTitle class="font-bold">
|
||||
<span @click.stop="openSitePage">{{ cardProps.site?.name }}</span>
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>{{ cardProps.site?.url }}</VCardSubtitle>
|
||||
<VCardSubtitle>
|
||||
{{ cardProps.site?.url }}
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
|
||||
<ExistIcon v-if="siteForm.is_active" />
|
||||
<ExistIcon v-if="cardProps.site?.is_active" />
|
||||
|
||||
<VCardText class="py-2">
|
||||
<VTooltip
|
||||
v-if="siteForm.render"
|
||||
v-if="cardProps.site?.render === 1"
|
||||
text="浏览器仿真"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
@@ -311,7 +250,7 @@ onMounted(() => {
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip
|
||||
v-if="siteForm.proxy"
|
||||
v-if="cardProps.site?.proxy === 1"
|
||||
text="代理"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
@@ -325,7 +264,7 @@ onMounted(() => {
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip
|
||||
v-if="siteForm.limit_interval"
|
||||
v-if="cardProps.site?.limit_interval"
|
||||
text="流控"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
@@ -339,7 +278,7 @@ onMounted(() => {
|
||||
</VTooltip>
|
||||
|
||||
<VTooltip
|
||||
v-if="siteForm.filter"
|
||||
v-if="cardProps.site?.filter"
|
||||
text="过滤"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
@@ -367,7 +306,7 @@ onMounted(() => {
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-refresh" />
|
||||
</template>
|
||||
{{ updateButtonText }}
|
||||
更新
|
||||
</VBtn>
|
||||
<VBtn
|
||||
:disabled="testButtonDisable"
|
||||
@@ -389,7 +328,7 @@ onMounted(() => {
|
||||
<!-- 更新站点Cookie & UA弹窗 -->
|
||||
<VDialog
|
||||
v-model="siteCookieDialog"
|
||||
max-width="600"
|
||||
max-width="50rem"
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="更新站点Cookie & UA">
|
||||
@@ -398,7 +337,7 @@ onMounted(() => {
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="userPwForm.username"
|
||||
@@ -408,7 +347,7 @@ onMounted(() => {
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="userPwForm.password"
|
||||
@@ -422,147 +361,41 @@ onMounted(() => {
|
||||
@keydown.enter="updateSiteCookie"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="userPwForm.code"
|
||||
label="两步验证"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn @click="updateSiteCookie">
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="updateSiteCookie"
|
||||
>
|
||||
开始更新
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 站点编辑弹窗 -->
|
||||
<VDialog
|
||||
v-model="siteInfoDialog"
|
||||
max-width="1000"
|
||||
persistent
|
||||
scrollable
|
||||
>
|
||||
<!-- 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">
|
||||
<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>
|
||||
<SiteAddEditForm
|
||||
v-model="siteEditDialog"
|
||||
:siteid="cardProps.site?.id"
|
||||
@save="siteEditDialog = false; emit('update')"
|
||||
@remove="emit('remove')"
|
||||
@close="siteEditDialog = false"
|
||||
/>
|
||||
<!-- 站点资源弹窗 -->
|
||||
<VDialog
|
||||
v-model="resourceDialog"
|
||||
max-width="1280"
|
||||
max-width="80rem"
|
||||
scrollable
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
@@ -590,6 +423,23 @@ onMounted(() => {
|
||||
<div class="text-sm my-1">
|
||||
{{ item.raw.description }}
|
||||
</div>
|
||||
<VChip
|
||||
v-if="item.raw?.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="item.raw?.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ item.raw?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(label, index) in item.raw?.labels"
|
||||
:key="index"
|
||||
@@ -650,13 +500,14 @@ onMounted(() => {
|
||||
<VListItemTitle>查看详情</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
v-if="item.raw.enclosure?.startsWith('http')"
|
||||
variant="plain"
|
||||
@click="downloadTorrentFile(item.raw.enclosure)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-download" />
|
||||
</template>
|
||||
<VListItemTitle>下载种子</VListItemTitle>
|
||||
<VListItemTitle>下载种子文件</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
@@ -670,6 +521,24 @@ onMounted(() => {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<VDialog
|
||||
v-model="progressDialog"
|
||||
:scrim="false"
|
||||
width="25rem"
|
||||
>
|
||||
<VCard
|
||||
color="primary"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
{{ progressText }}
|
||||
<VProgressLinear
|
||||
indeterminate
|
||||
color="white"
|
||||
class="mb-0 mt-1"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import SubscribeEditForm from '../form/SubscribeEditForm.vue'
|
||||
import { calculateTimeDifference } from '@/@core/utils'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Site, Subscribe } from '@/api/types'
|
||||
import type { Subscribe } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -21,19 +21,7 @@ const $toast = useToast()
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
// 订阅弹窗
|
||||
const subscribeInfoDialog = 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 subscribeEditDialog = ref(false)
|
||||
|
||||
// 上一次更新时间
|
||||
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')
|
||||
|
||||
// 过滤站点,只有启用的站点才显示
|
||||
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() {
|
||||
await getSiteList()
|
||||
subscribeInfoDialog.value = true
|
||||
subscribeEditDialog.value = true
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
@@ -201,7 +140,7 @@ const dropdownItems = ref([
|
||||
<template>
|
||||
<VCard
|
||||
:key="props.media?.id"
|
||||
:class="`${subscribeForm.best_version ? 'outline-dashed outline-1' : ''}`"
|
||||
:class="`${props.media?.best_version ? 'outline-dashed outline-1' : ''}`"
|
||||
@click="editSubscribeDialog"
|
||||
>
|
||||
<template #image>
|
||||
@@ -323,100 +262,11 @@ const dropdownItems = ref([
|
||||
/>
|
||||
</VCard>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<VDialog
|
||||
v-model="subscribeInfoDialog"
|
||||
max-width="1000"
|
||||
persistent
|
||||
scrollable
|
||||
>
|
||||
<!-- 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>
|
||||
<SubscribeEditForm
|
||||
v-model="subscribeEditDialog"
|
||||
:subid="props.media?.id"
|
||||
@remove="() => { emit('remove');subscribeEditDialog = false; }"
|
||||
@save="() => { emit('save');subscribeEditDialog = false; }"
|
||||
@close="subscribeEditDialog = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
138
src/components/cards/TmdbSelectorCard.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import type { MediaInfo } from '@/api/types'
|
||||
|
||||
interface TmdbItem {
|
||||
title: string
|
||||
overview: string
|
||||
tmdbid: number
|
||||
poster: string
|
||||
}
|
||||
|
||||
// update:modelValue 事件
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
const items = ref<TmdbItem[]>([])
|
||||
|
||||
// 搜索词
|
||||
const keyword = ref('')
|
||||
|
||||
// 加载中
|
||||
const loading = ref(false)
|
||||
|
||||
// ref
|
||||
const tmdbKeyword = ref<HTMLElement | null>(null)
|
||||
|
||||
// 选中条目
|
||||
function selectMedia(item: TmdbItem) {
|
||||
emit('update:modelValue', item.tmdbid)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// TMDB图片转换为w500大小
|
||||
function getW500Image(url = '') {
|
||||
if (!url)
|
||||
return ''
|
||||
return url.replace('original', 'w500')
|
||||
}
|
||||
|
||||
// 搜索词条
|
||||
async function searchMedias() {
|
||||
if (!keyword)
|
||||
return
|
||||
|
||||
// 调用API搜索词条
|
||||
try {
|
||||
loading.value = true
|
||||
const result: MediaInfo[] = await api.get('media/search', {
|
||||
params: {
|
||||
title: keyword.value,
|
||||
page: 1,
|
||||
count: 20,
|
||||
},
|
||||
})
|
||||
|
||||
// 清空
|
||||
items.value = []
|
||||
|
||||
// 赋值
|
||||
for (const item of result) {
|
||||
items.value.push({
|
||||
tmdbid: item.tmdb_id || 0,
|
||||
poster: getW500Image(item.poster_path),
|
||||
title: `${item.title}(${item.year})`,
|
||||
overview: `<span class="text-primary">${item.type}</span> ${item.overview}`,
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时聚焦搜索框
|
||||
onMounted(() => {
|
||||
// 500ms后聚焦
|
||||
setTimeout(() => {
|
||||
tmdbKeyword.value?.focus()
|
||||
}, 500)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
class="mx-auto"
|
||||
width="100%"
|
||||
>
|
||||
<VToolbar flat class="p-0">
|
||||
<VTextField
|
||||
ref="tmdbKeyword"
|
||||
v-model="keyword"
|
||||
label="输入名称搜索"
|
||||
single-line
|
||||
placeholder="电影或电视剧名称"
|
||||
variant="solo"
|
||||
append-inner-icon="mdi-magnify"
|
||||
flat
|
||||
class="mx-1"
|
||||
:loading="loading"
|
||||
@click:append-inner="searchMedias"
|
||||
@keydown.enter="searchMedias"
|
||||
/>
|
||||
</VToolbar>
|
||||
|
||||
<VList
|
||||
v-if="items.length > 0"
|
||||
lines="three"
|
||||
>
|
||||
<template v-for="(item, i) in items" :key="i">
|
||||
<VListItem
|
||||
@click="selectMedia(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VImg
|
||||
height="75"
|
||||
width="50"
|
||||
:src="item.poster"
|
||||
aspect-ratio="2/3"
|
||||
class="object-cover rounded shadow ring-gray-500 me-3"
|
||||
cover
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
{{ item.title }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle class="mt-2" v-html="item.overview" />
|
||||
</VListItem>
|
||||
<VDivider v-if="i < items.length - 1" class="mt-1" inset />
|
||||
</template>
|
||||
</VList>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -62,7 +62,10 @@ async function handleAddDownload(_site: any = undefined,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: 600,
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
confirmationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -76,7 +79,7 @@ async function handleAddDownload(_site: any = undefined,
|
||||
async function addDownload(_media: any, _torrent: any) {
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('download', {
|
||||
const result: { [key: string]: any } = await api.post('download/', {
|
||||
media_in: _media,
|
||||
torrent_in: _torrent,
|
||||
})
|
||||
@@ -122,26 +125,6 @@ function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||
onMounted(() => {
|
||||
getSiteIcon()
|
||||
})
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '查看详情',
|
||||
value: 1,
|
||||
props: {
|
||||
prependIcon: 'mdi-information',
|
||||
click: openTorrentDetail,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '下载种子',
|
||||
value: 2,
|
||||
props: {
|
||||
prependIcon: 'mdi-download',
|
||||
click: downloadTorrentFile,
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -163,7 +146,7 @@ const dropdownItems = ref([
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardItem class="py-1">
|
||||
<VCardTitle>
|
||||
<VCardTitle class="break-words overflow-visible whitespace-break-spaces">
|
||||
{{ media?.title }} {{ meta?.season_episode }}
|
||||
<span class="text-green-700 ms-2 text-sm">↑{{ torrent?.seeders }}</span>
|
||||
<span class="text-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
||||
@@ -180,15 +163,23 @@ const dropdownItems = ref([
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
@click="item.props.click"
|
||||
@click="openTorrentDetail()"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
<VIcon icon="mdi-information" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
<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>
|
||||
@@ -204,6 +195,23 @@ const dropdownItems = ref([
|
||||
v-if="torrent?.labels"
|
||||
class="pb-3 pt-0 pe-12"
|
||||
>
|
||||
<VChip
|
||||
v-if="torrent?.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ torrent?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(label, index) in torrent?.labels"
|
||||
:key="index"
|
||||
|
||||
268
src/components/cards/TorrentItem.vue
Normal file
@@ -0,0 +1,268 @@
|
||||
<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-if="torrent?.hit_and_run"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-black"
|
||||
>
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.freedate_diff"
|
||||
variant="elevated"
|
||||
color="secondary"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ torrent?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(label, index) in torrent?.labels"
|
||||
:key="index"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
color="primary"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ label }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.edition"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
{{ meta?.edition }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.resource_pix"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
{{ meta?.resource_pix }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.video_encode"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-orange-500"
|
||||
>
|
||||
{{ meta?.video_encode }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.size"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-yellow-500"
|
||||
>
|
||||
{{ formatFileSize(torrent?.size) }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="meta?.resource_team"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1 text-white bg-cyan-500"
|
||||
>
|
||||
{{ meta?.resource_team }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
|
||||
:class="
|
||||
getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)
|
||||
"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ torrent?.volume_factor }}
|
||||
</VChip>
|
||||
</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,11 +4,12 @@ import type { PropType } from 'vue'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import axios from 'axios'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import ReorganizeForm from '../form/ReorganizeForm.vue'
|
||||
import { formatBytes } from '@core/utils/formatters'
|
||||
import type { EndPoints, FileItem } from '@/api/types'
|
||||
import type { Context, EndPoints, FileItem } from '@/api/types'
|
||||
import store from '@/store'
|
||||
import api from '@/api'
|
||||
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
|
||||
|
||||
// 输入参数
|
||||
const inProps = defineProps({
|
||||
@@ -18,6 +19,7 @@ const inProps = defineProps({
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
axios: Object as PropType<Axios>,
|
||||
refreshpending: Boolean,
|
||||
sort: String,
|
||||
})
|
||||
|
||||
// 对外事件
|
||||
@@ -29,6 +31,15 @@ const $toast = useToast()
|
||||
// 是否正在加载
|
||||
const loading = ref(true)
|
||||
|
||||
// 识别进度条
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 识别进度文本
|
||||
const progressText = ref('请稍候 ...')
|
||||
|
||||
// 识别进度
|
||||
const progressValue = ref(0)
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
@@ -56,29 +67,11 @@ const newName = ref('')
|
||||
// 当前名称
|
||||
const currentItem = ref<FileItem>()
|
||||
|
||||
// 文件转移表单
|
||||
const transferForm = reactive({
|
||||
path: '',
|
||||
target: '',
|
||||
tmdbid: 0,
|
||||
season: 0,
|
||||
type_name: '电影',
|
||||
transfer_type: '',
|
||||
episode_format: '',
|
||||
episode_detail: '',
|
||||
episode_part: '',
|
||||
episode_offset: null,
|
||||
min_filesize: 0,
|
||||
// 识别结果
|
||||
const nameTestResult = ref<Context>()
|
||||
|
||||
})
|
||||
|
||||
// 生成1到50季的下拉框选项
|
||||
const seasonItems = ref(
|
||||
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
|
||||
title: `第 ${item} 季`,
|
||||
value: item,
|
||||
})),
|
||||
)
|
||||
// 识别结果对话框
|
||||
const nameTestDialog = ref(false)
|
||||
|
||||
// 目录过滤
|
||||
const dirs = computed(() =>
|
||||
@@ -106,18 +99,18 @@ const isImage = computed(() => {
|
||||
async function load() {
|
||||
loading.value = true
|
||||
emit('loading', true)
|
||||
if (isDir.value) {
|
||||
const url = inProps.endpoints?.list.url
|
||||
.replace(/{storage}/g, storage.value)
|
||||
.replace(/{path}/g, inProps.path)
|
||||
// 参数
|
||||
const url = inProps.endpoints?.list.url
|
||||
.replace(/{storage}/g, storage.value)
|
||||
.replace(/{path}/g, encodeURIComponent(inProps.path || ''))
|
||||
.replace(/{sort}/g, inProps.sort || 'name')
|
||||
|
||||
const config = {
|
||||
url,
|
||||
method: inProps.endpoints?.list.method || 'get',
|
||||
}
|
||||
// 加载数据
|
||||
items.value = await axiosInstance.value.request(config) ?? []
|
||||
const config = {
|
||||
url,
|
||||
method: inProps.endpoints?.list.method || 'get',
|
||||
}
|
||||
// 加载数据
|
||||
items.value = await axiosInstance.value.request(config) ?? []
|
||||
emit('loading', false)
|
||||
loading.value = false
|
||||
}
|
||||
@@ -132,7 +125,10 @@ async function deleteItem(item: FileItem) {
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: 600,
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
cancellationButtonProps: {
|
||||
variant: 'tonal',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -140,7 +136,7 @@ async function deleteItem(item: FileItem) {
|
||||
emit('loading', true)
|
||||
const url = inProps.endpoints?.delete.url
|
||||
.replace(/{storage}/g, storage.value)
|
||||
.replace(/{path}/g, item.path)
|
||||
.replace(/{path}/g, encodeURIComponent(item.path))
|
||||
|
||||
const config = {
|
||||
url,
|
||||
@@ -167,7 +163,7 @@ function download(path: string) {
|
||||
const token = store.state.auth.token
|
||||
const url_path = inProps.endpoints?.download.url
|
||||
.replace(/{storage}/g, storage.value)
|
||||
.replace(/{path}/g, path)
|
||||
.replace(/{path}/g, encodeURIComponent(path))
|
||||
const url = `${import.meta.env.VITE_API_BASE_URL}${url_path.slice(1)}&token=${token}`
|
||||
// 下载文件
|
||||
window.open(url, '_blank')
|
||||
@@ -180,7 +176,7 @@ function getImgLink(path: string) {
|
||||
const token = store.state.auth.token
|
||||
const url_path = inProps.endpoints?.image.url
|
||||
.replace(/{storage}/g, storage.value)
|
||||
.replace(/{path}/g, path)
|
||||
.replace(/{path}/g, encodeURIComponent(path))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}${url_path.slice(1)}&token=${token}`
|
||||
}
|
||||
|
||||
@@ -196,8 +192,8 @@ async function rename() {
|
||||
emit('loading', true)
|
||||
const url = inProps.endpoints?.rename.url
|
||||
.replace(/{storage}/g, inProps.storage)
|
||||
.replace(/{path}/g, currentItem.value?.path)
|
||||
.replace(/{newname}/g, newName.value)
|
||||
.replace(/{path}/g, encodeURIComponent(currentItem.value?.path || ''))
|
||||
.replace(/{newname}/g, encodeURIComponent(newName.value))
|
||||
|
||||
const config = {
|
||||
url,
|
||||
@@ -221,27 +217,9 @@ function showTransfer(item: FileItem) {
|
||||
transferPopper.value = true
|
||||
}
|
||||
|
||||
// 整理文件
|
||||
async function transfer() {
|
||||
transferForm.path = currentItem.value?.path || ''
|
||||
// 开始整理文件
|
||||
try {
|
||||
transferPopper.value = false
|
||||
const result: { [key: string]: any } = await api.post('transfer/manual', {}, {
|
||||
params: transferForm,
|
||||
})
|
||||
if (result.success) {
|
||||
$toast.success(`${currentItem.value?.name} 整理成功!`)
|
||||
// 重新加载
|
||||
load()
|
||||
}
|
||||
else {
|
||||
$toast.error(`${currentItem.value?.name} 整理失败:${result.message}!`)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
// 将文件修改时间(timestape)转换为本地时间
|
||||
function formatTime(timestape: number) {
|
||||
return new Date(timestape * 1000).toLocaleString()
|
||||
}
|
||||
|
||||
// 监听path变化
|
||||
@@ -249,6 +227,8 @@ watch(
|
||||
() => inProps.path,
|
||||
async () => {
|
||||
items.value = []
|
||||
nameTestResult.value = undefined
|
||||
nameTestDialog.value = false
|
||||
await load()
|
||||
},
|
||||
)
|
||||
@@ -264,11 +244,74 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
// 调用API识别
|
||||
async function recognize(path: string) {
|
||||
try {
|
||||
// 显示进度条
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在识别 ${path} ...`
|
||||
progressValue.value = 0
|
||||
nameTestResult.value = await api.get('media/recognize_file', {
|
||||
params: {
|
||||
path,
|
||||
},
|
||||
})
|
||||
// 关闭进度条
|
||||
progressDialog.value = false
|
||||
if (!nameTestResult.value)
|
||||
$toast.error(`${path} 识别失败!`)
|
||||
nameTestDialog.value = !!nameTestResult.value?.meta_info?.name
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API刮削
|
||||
async function scrape(path: string) {
|
||||
try {
|
||||
// 显示进度条
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在刮削 ${path} ...`
|
||||
const result: { [key: string]: any } = await api.get('media/scrape', {
|
||||
params: {
|
||||
path,
|
||||
},
|
||||
})
|
||||
// 关闭进度条
|
||||
progressDialog.value = false
|
||||
if (!result.success)
|
||||
$toast.error(result.message)
|
||||
else
|
||||
$toast.success(`${path}削刮完成!`)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '重命名',
|
||||
title: '识别',
|
||||
value: 1,
|
||||
props: {
|
||||
prependIcon: 'mdi-text-recognition',
|
||||
click: (_item: FileItem) => {
|
||||
recognize(_item.path || '')
|
||||
},
|
||||
},
|
||||
}, {
|
||||
title: '刮削',
|
||||
value: 2,
|
||||
props: {
|
||||
prependIcon: 'mdi-auto-fix',
|
||||
click: (_item: FileItem) => {
|
||||
scrape(_item.path || '')
|
||||
},
|
||||
},
|
||||
}, {
|
||||
title: '重命名',
|
||||
value: 3,
|
||||
props: {
|
||||
prependIcon: 'mdi-rename',
|
||||
click: showRenmae,
|
||||
@@ -276,7 +319,7 @@ const dropdownItems = ref([
|
||||
},
|
||||
{
|
||||
title: '整理',
|
||||
value: 2,
|
||||
value: 4,
|
||||
props: {
|
||||
prependIcon: 'mdi-folder-arrow-right',
|
||||
click: showTransfer,
|
||||
@@ -284,7 +327,7 @@ const dropdownItems = ref([
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
value: 3,
|
||||
value: 5,
|
||||
props: {
|
||||
prependIcon: 'mdi-delete-outline',
|
||||
color: 'error',
|
||||
@@ -318,9 +361,11 @@ onMounted(() => {
|
||||
</VCardText>
|
||||
<VCardText
|
||||
v-else-if="isFile && !isImage"
|
||||
class="grow d-flex justify-center align-center break-all"
|
||||
class="text-center break-all"
|
||||
>
|
||||
文件: {{ path }}<br>
|
||||
<strong>{{ items[0]?.name }}</strong><br>
|
||||
大小:{{ formatBytes(items[0]?.size || 0) }}<br>
|
||||
修改时间:{{ formatTime(items[0]?.modify_time || 0) }}
|
||||
</VCardText>
|
||||
<VCardText
|
||||
v-else-if="isFile && isImage"
|
||||
@@ -331,105 +376,133 @@ onMounted(() => {
|
||||
<VCardText v-else-if="dirs.length || files.length" class="p-0">
|
||||
<VList v-if="dirs.length" subheader>
|
||||
<VListSubheader>目录</VListSubheader>
|
||||
<VListItem
|
||||
<VHover
|
||||
v-for="(item, index) in dirs"
|
||||
:key="index"
|
||||
class="px-3 pe-1"
|
||||
@click="changePath(item.path)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-folder-outline" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.name" />
|
||||
<template #append>
|
||||
<IconBtn class="d-sm-none">
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click.stop="menu.props.click(item)"
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
v-bind="hover.props"
|
||||
class="px-3 pe-1"
|
||||
@click="changePath(item.path)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-folder-outline" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.name" />
|
||||
<template #append>
|
||||
<IconBtn class="d-sm-none">
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
||||
<VIcon icon="mdi-rename" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
||||
<VIcon icon="mdi-folder-arrow-right" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
||||
<VIcon icon="mdi-delete-outline" />
|
||||
</IconBtn>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click="menu.props.click(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<span v-show="hover.isHovering" class="flex">
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
||||
<VIcon icon="mdi-text-recognition" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="scrape(item.path)">
|
||||
<VIcon icon="mdi-auto-fix" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
||||
<VIcon icon="mdi-rename" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
||||
<VIcon icon="mdi-folder-arrow-right" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
||||
<VIcon icon="mdi-delete-outline" />
|
||||
</IconBtn>
|
||||
</span>
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VHover>
|
||||
</VList>
|
||||
<VDivider v-if="dirs.length && files.length" />
|
||||
<VList v-if="files.length" subheader>
|
||||
<VListSubheader>文件</VListSubheader>
|
||||
<VListItem
|
||||
<VHover
|
||||
v-for="(item, index) in files"
|
||||
:key="index"
|
||||
class="pl-3 pe-1"
|
||||
@click="changePath(item.path)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon v-if="inProps.icons" :icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other" />
|
||||
</template>
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
v-bind="hover.props"
|
||||
class="pl-3 pe-1"
|
||||
@click="changePath(item.path)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon v-if="inProps.icons" :icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other" />
|
||||
</template>
|
||||
|
||||
<VListItemTitle v-text="item.name" />
|
||||
<VListItemSubtitle> {{ formatBytes(item.size) }}</VListItemSubtitle>
|
||||
<VListItemTitle v-text="item.name" />
|
||||
<VListItemSubtitle> {{ formatBytes(item.size) }}</VListItemSubtitle>
|
||||
|
||||
<template #append>
|
||||
<IconBtn class="d-sm-none">
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click.stop="menu.props.click(item)"
|
||||
<template #append>
|
||||
<IconBtn class="d-sm-none">
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
||||
<VIcon icon="mdi-rename" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
||||
<VIcon icon="mdi-folder-arrow-right" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
||||
<VIcon icon="mdi-delete-outline" />
|
||||
</IconBtn>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click="menu.props.click(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<span v-show="hover.isHovering" class="flex">
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
||||
<VIcon icon="mdi-text-recognition" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="scrape(item.path)">
|
||||
<VIcon icon="mdi-auto-fix" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
||||
<VIcon icon="mdi-rename" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
||||
<VIcon icon="mdi-folder-arrow-right" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
||||
<VIcon icon="mdi-delete-outline" />
|
||||
</IconBtn>
|
||||
</span>
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VHover>
|
||||
</VList>
|
||||
</VCardText>
|
||||
<VCardText
|
||||
@@ -458,36 +531,41 @@ onMounted(() => {
|
||||
class="me-2"
|
||||
/>
|
||||
<VSpacer v-if="isFile" />
|
||||
<VBtn v-if="isFile" @click="download(inProps.path || '')">
|
||||
<VIcon>mdi-download</VIcon>
|
||||
</VBtn>
|
||||
<VBtn v-if="!isFile" @click="load">
|
||||
<VIcon>mdi-refresh</VIcon>
|
||||
</VBtn>
|
||||
<IconBtn v-if="isFile" @click="recognize(inProps.path || '')">
|
||||
<VIcon color="primary">
|
||||
mdi-text-recognition
|
||||
</VIcon>
|
||||
</IconBtn>
|
||||
<IconBtn v-if="isFile" @click="download(inProps.path || '')">
|
||||
<VIcon color="primary">
|
||||
mdi-download
|
||||
</VIcon>
|
||||
</IconBtn>
|
||||
<IconBtn v-if="!isFile" @click="load">
|
||||
<VIcon color="primary">
|
||||
mdi-refresh
|
||||
</VIcon>
|
||||
</IconBtn>
|
||||
</VToolbar>
|
||||
</VCard>
|
||||
<!-- 重命名弹窗 -->
|
||||
<VDialog
|
||||
v-model="renamePopper"
|
||||
max-width="600"
|
||||
max-width="50rem"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<IconBtn title="重命名" v-bind="props">
|
||||
<VIcon icon="mdi-rename-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCard title="重命名">
|
||||
<VCardText>
|
||||
<VTextField v-model="newName" label="名称" />
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<div class="flex-grow-1" />
|
||||
<VBtn depressed @click="renamePopper = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
:disabled="!newName"
|
||||
depressed
|
||||
variant="tonal"
|
||||
@click="rename"
|
||||
>
|
||||
重命名
|
||||
@@ -496,132 +574,42 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 文件整理弹窗 -->
|
||||
<VDialog
|
||||
<ReorganizeForm
|
||||
v-model="transferPopper"
|
||||
max-width="800"
|
||||
scrollable
|
||||
:path="currentItem?.path"
|
||||
@done="transferPopper = false; load()"
|
||||
@close="transferPopper = false"
|
||||
/>
|
||||
<!-- 手动整理进度框 -->
|
||||
<VDialog
|
||||
v-model="progressDialog"
|
||||
:scrim="false"
|
||||
width="25rem"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<IconBtn title="整理" v-bind="props">
|
||||
<VIcon icon="mdi-folder-arrow-right-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCard :title="`文件整理 - ${currentItem?.name}`">
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<VTextField
|
||||
v-model="transferForm.target"
|
||||
label="目的路径"
|
||||
/>
|
||||
</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: '电视剧' }]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="transferForm.tmdbid"
|
||||
label="TMDBID"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</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>
|
||||
<VCard
|
||||
color="primary"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
{{ progressText }}
|
||||
<VProgressLinear
|
||||
v-if="progressValue"
|
||||
color="white"
|
||||
class="mb-0 mt-1"
|
||||
:model-value="progressValue"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<div class="flex-grow-1" />
|
||||
<VBtn depressed @click="transferPopper = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VBtn
|
||||
:disabled="!transferForm.tmdbid"
|
||||
depressed
|
||||
@click="transfer"
|
||||
>
|
||||
开始整理
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 识别结果对话框 -->
|
||||
<VDialog
|
||||
v-model="nameTestDialog"
|
||||
width="50rem"
|
||||
>
|
||||
<VCard>
|
||||
<DialogCloseBtn @click="nameTestDialog = false" />
|
||||
<VCardItem>
|
||||
<MediaInfoCard :context="nameTestResult" />
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -12,7 +12,7 @@ const inProps = defineProps({
|
||||
})
|
||||
|
||||
// 对外事件
|
||||
const emit = defineEmits(['storagechanged', 'pathchanged', 'loading', 'foldercreated'])
|
||||
const emit = defineEmits(['storagechanged', 'pathchanged', 'loading', 'foldercreated', 'sortchanged'])
|
||||
|
||||
// 新建文件夹名称
|
||||
const newFolderPopper = ref(false)
|
||||
@@ -20,6 +20,19 @@ const newFolderPopper = ref(false)
|
||||
// 新建文件名称
|
||||
const newFolderName = ref('')
|
||||
|
||||
// 排序方式
|
||||
const sort = ref('name')
|
||||
|
||||
// 调整排序方式
|
||||
function changeSort() {
|
||||
if (sort.value === 'name')
|
||||
sort.value = 'time'
|
||||
else
|
||||
sort.value = 'name'
|
||||
|
||||
emit('sortchanged', sort.value)
|
||||
}
|
||||
|
||||
// 计算PATH面包屑
|
||||
const pathSegments = computed(() => {
|
||||
let path_str = ''
|
||||
@@ -64,7 +77,7 @@ async function mkdir() {
|
||||
emit('loading', true)
|
||||
const url = inProps.endpoints?.mkdir.url
|
||||
.replace(/{storage}/g, inProps.storage)
|
||||
.replace(/{path}/g, inProps.path + newFolderName.value)
|
||||
.replace(/{path}/g, encodeURIComponent(inProps.path + newFolderName.value))
|
||||
|
||||
const config = {
|
||||
url,
|
||||
@@ -81,6 +94,14 @@ async function mkdir() {
|
||||
// 通知重新加载
|
||||
emit('foldercreated')
|
||||
}
|
||||
|
||||
// 计算排序图标
|
||||
const sortIcon = computed(() => {
|
||||
if (sort.value === 'time')
|
||||
return 'mdi-sort-clock-ascending-outline'
|
||||
else
|
||||
return 'mdi-sort-alphabetical-ascending'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -123,12 +144,15 @@ async function mkdir() {
|
||||
</template>
|
||||
</VToolbarItems>
|
||||
<div class="flex-grow-1" />
|
||||
<IconBtn @click="changeSort">
|
||||
<VIcon :icon="sortIcon" />
|
||||
</IconBtn>
|
||||
<IconBtn v-if="pathSegments.length > 0" @click="goUp">
|
||||
<VIcon icon="mdi-arrow-up-bold-outline" />
|
||||
</IconBtn>
|
||||
<VDialog
|
||||
v-model="newFolderPopper"
|
||||
max-width="600"
|
||||
max-width="50rem"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<IconBtn title="新建文件夹" v-bind="props">
|
||||
@@ -147,6 +171,7 @@ async function mkdir() {
|
||||
<VBtn
|
||||
:disabled="!newFolderName"
|
||||
depressed
|
||||
variant="tonal"
|
||||
@click="mkdir"
|
||||
>
|
||||
新建
|
||||
@@ -156,9 +181,3 @@ async function mkdir() {
|
||||
</VDialog>
|
||||
</VToolbar>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-toolbar{
|
||||
background: rgb(var(--v-table-header-background));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -35,6 +35,7 @@ function init() {
|
||||
name: 'root',
|
||||
children: [],
|
||||
size: 0,
|
||||
modify_time: 0,
|
||||
}]
|
||||
}
|
||||
|
||||
|
||||
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>
|
||||
364
src/components/form/SubscribeEditForm.vue
Normal file
@@ -0,0 +1,364 @@
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
@@ -5,6 +5,7 @@ import { type PropType, ref } from 'vue'
|
||||
interface RenderProps {
|
||||
component: string
|
||||
text: string
|
||||
html: string
|
||||
content?: any
|
||||
props?: any
|
||||
}
|
||||
@@ -16,9 +17,10 @@ const elementProps = defineProps({
|
||||
})
|
||||
|
||||
// 配置元素
|
||||
const formItem = ref<RenderProps>(elementProps.config || {
|
||||
const formItem = ref<RenderProps>(elementProps.config ?? {
|
||||
component: 'div',
|
||||
text: '',
|
||||
html: '',
|
||||
props: {},
|
||||
content: [],
|
||||
})
|
||||
@@ -30,16 +32,58 @@ const formData = ref<any>(elementProps.form || {})
|
||||
<template>
|
||||
<Component
|
||||
:is="formItem.component"
|
||||
v-if="!formItem.html && !!formItem.props?.modelvalue"
|
||||
v-bind="formItem.props"
|
||||
v-model="formData[formItem.props?.model || '']"
|
||||
v-model:value="formData[formItem.props?.modelvalue]"
|
||||
>
|
||||
{{ formItem.text }}
|
||||
<FormRender
|
||||
<template
|
||||
v-for="(innerItem, innerIndex) in (formItem.content || [])"
|
||||
:key="innerIndex"
|
||||
v-model="formData[innerItem.props?.model || '']"
|
||||
:config="innerItem"
|
||||
:form="formData"
|
||||
/>
|
||||
>
|
||||
<FormRender
|
||||
v-if="!!innerItem.props?.modelvalue"
|
||||
v-model:value="formData[innerItem.props?.modelvalue]"
|
||||
:config="innerItem"
|
||||
:form="formData"
|
||||
/>
|
||||
<FormRender
|
||||
v-else
|
||||
v-model="formData[innerItem.props?.model]"
|
||||
:config="innerItem"
|
||||
:form="formData"
|
||||
/>
|
||||
</template>
|
||||
</Component>
|
||||
<Component
|
||||
:is="formItem.component"
|
||||
v-else-if="formItem.html"
|
||||
v-bind="formItem.props"
|
||||
v-html="formItem.html"
|
||||
/>
|
||||
<Component
|
||||
:is="formItem.component"
|
||||
v-else
|
||||
v-bind="formItem.props"
|
||||
v-model="formData[formItem.props?.model]"
|
||||
>
|
||||
{{ formItem.text }}
|
||||
<template
|
||||
v-for="(innerItem, innerIndex) in (formItem.content || [])"
|
||||
:key="innerIndex"
|
||||
>
|
||||
<FormRender
|
||||
v-if="!!innerItem.props?.modelvalue"
|
||||
v-model:value="formData[innerItem.props?.modelvalue]"
|
||||
:config="innerItem"
|
||||
:form="formData"
|
||||
/>
|
||||
<FormRender
|
||||
v-else
|
||||
v-model="formData[innerItem.props?.model]"
|
||||
:config="innerItem"
|
||||
:form="formData"
|
||||
/>
|
||||
</template>
|
||||
</Component>
|
||||
</template>
|
||||
|
||||
@@ -13,11 +13,10 @@ interface RenderProps {
|
||||
// 输入参数
|
||||
const elementProps = defineProps({
|
||||
config: Object as PropType<RenderProps>,
|
||||
handler: Boolean,
|
||||
})
|
||||
|
||||
// 配置元素
|
||||
const formItem = ref<RenderProps>(elementProps.config || {
|
||||
const formItem = ref<RenderProps>(elementProps.config ?? {
|
||||
component: 'div',
|
||||
text: '',
|
||||
html: '',
|
||||
|
||||
@@ -11,6 +11,8 @@ const props = defineProps({
|
||||
const slideview_content = ref()
|
||||
// 分页切换状态
|
||||
const disabled = ref(0)
|
||||
// 记录滚动值
|
||||
const slideview_scrollLeft = ref(0)
|
||||
// 所有卡片数量
|
||||
let slide_card_length: number
|
||||
// 卡片间距
|
||||
@@ -58,6 +60,7 @@ function countMaxNumber() {
|
||||
|
||||
// 修改分页切换按钮状态
|
||||
function countDisabled() {
|
||||
slideview_scrollLeft.value = slideview_content.value.scrollLeft
|
||||
card_current = slideview_content.value.scrollLeft === 0 ? 0 : Math.trunc((slideview_content.value.scrollLeft + card_width / 2) / card_width)
|
||||
if (slide_card_length * card_width <= slideview_content.value.clientWidth)
|
||||
disabled.value = 3
|
||||
@@ -81,6 +84,12 @@ onUnmounted(() => {
|
||||
// 卸载事件
|
||||
window.removeEventListener('resize', countMaxNumber)
|
||||
})
|
||||
onActivated(() => {
|
||||
if (slideview_scrollLeft.value !== 0) {
|
||||
// console.log(`onActivated: to_scrollLeft, ${slideview_scrollLeft.value}`)
|
||||
slideview_content.value.scrollLeft = slideview_scrollLeft.value
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -9,6 +9,10 @@ import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
|
||||
import SearchBar from '@/layouts/components/SearchBar.vue'
|
||||
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
|
||||
import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
import store from '@/store'
|
||||
|
||||
// 从Vuex Store中获取superuser信息
|
||||
const superUser = store.state.auth.superUser
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -100,13 +104,6 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
to: '/subscribe-tv',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
:item="{
|
||||
title: '自定义',
|
||||
icon: 'mdi-rss',
|
||||
to: '/subscribe-rss',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
:item="{
|
||||
title: '日历',
|
||||
@@ -128,6 +125,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '历史记录',
|
||||
icon: 'mdi-history',
|
||||
@@ -135,6 +133,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '文件管理',
|
||||
icon: 'mdi-folder-multiple-outline',
|
||||
@@ -144,11 +143,13 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
|
||||
<!-- 👉 系统 -->
|
||||
<VerticalNavSectionTitle
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
heading: '系统',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '插件',
|
||||
icon: 'mdi-apps',
|
||||
@@ -156,6 +157,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '站点管理',
|
||||
icon: 'mdi-web',
|
||||
@@ -163,6 +165,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '设定',
|
||||
icon: 'mdi-cog',
|
||||
|
||||
@@ -10,6 +10,14 @@ const themes: ThemeSwitcherTheme[] = [
|
||||
name: 'dark',
|
||||
icon: 'mdi-weather-night',
|
||||
},
|
||||
{
|
||||
name: 'purple',
|
||||
icon: 'mdi-brightness-4',
|
||||
},
|
||||
{
|
||||
name: 'auto',
|
||||
icon: 'mdi-brightness-auto',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ const searchWord = ref<string>('')
|
||||
// 搜索弹窗
|
||||
const searchDialog = ref(false)
|
||||
|
||||
// ref
|
||||
const searchWordInput = ref<HTMLElement | null>(null)
|
||||
|
||||
// Search
|
||||
function search() {
|
||||
if (!searchWord.value)
|
||||
@@ -21,6 +24,14 @@ function search() {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 打开搜索弹窗
|
||||
function openSearchDialog() {
|
||||
searchDialog.value = true
|
||||
nextTick(() => {
|
||||
searchWordInput.value?.focus()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -31,26 +42,19 @@ function search() {
|
||||
>
|
||||
<VDialog
|
||||
v-model="searchDialog"
|
||||
max-width="600"
|
||||
max-width="50rem"
|
||||
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 -->
|
||||
<VCard title="搜索">
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
ref="searchWordInput"
|
||||
v-model="searchWord"
|
||||
label="电影、电视剧名称"
|
||||
@keydown.enter="search"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -59,8 +63,8 @@ function search() {
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
@click="search"
|
||||
@keydown.enter="search"
|
||||
>
|
||||
搜索
|
||||
</VBtn>
|
||||
@@ -68,7 +72,13 @@ function search() {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Search Icon -->
|
||||
<IconBtn
|
||||
class="d-lg-none"
|
||||
@click="openSearchDialog"
|
||||
>
|
||||
<VIcon icon="mdi-magnify" />
|
||||
</IconBtn>
|
||||
<!-- 👉 Search Textfield -->
|
||||
<span class="w-1/5">
|
||||
<VTextField
|
||||
|
||||