Compare commits

...

142 Commits

Author SHA1 Message Date
jxxghp
7f991da183 fix ui 2023-10-18 21:07:26 +08:00
jxxghp
046d96a012 Merge pull request #52 from thsrite/main 2023-10-17 17:04:33 +08:00
thsrite
9ee6ca43e3 feat MoviePilot更新推送插件 2023-10-17 16:17:05 +08:00
jxxghp
43b1f7e620 v1.3.4-2 2023-10-17 14:04:29 +08:00
jxxghp
ba76f79d85 fix ui 2023-10-17 14:02:54 +08:00
jxxghp
ce47afa698 fix ui 2023-10-16 19:56:49 +08:00
jxxghp
6da110948c v1.3.4 2023-10-16 17:35:23 +08:00
jxxghp
533c564db5 feat 普通用户下载中只显示自己添加的下载 2023-10-16 08:32:02 +08:00
jxxghp
4a65056909 fix 普通用户订阅权限 2023-10-16 08:23:38 +08:00
jxxghp
c52ad73101 fix dialogs 2023-10-16 06:58:09 +08:00
jxxghp
5a3673efc6 更新 package.json 2023-10-14 14:34:16 +08:00
jxxghp
c03ec1d741 fix ui 2023-10-14 14:29:57 +08:00
jxxghp
e62d0809b3 fix ui 2023-10-14 14:15:38 +08:00
jxxghp
7f13597517 feat merge form 2023-10-14 13:48:02 +08:00
jxxghp
c822f1fffd feat 整合站点编辑组件 2023-10-14 09:13:38 +08:00
jxxghp
14ca74a29d Merge branch 'main' of https://github.com/jxxghp/MoviePilot-Frontend 2023-10-13 22:56:07 +08:00
jxxghp
3ee897a350 fix 2023-10-13 22:56:02 +08:00
jxxghp
789aac60c9 更新 package.json 2023-10-13 22:39:49 +08:00
jxxghp
2c73a8f3e1 fix ui 2023-10-13 22:37:04 +08:00
jxxghp
539bc656f8 fix 2023-10-13 22:33:58 +08:00
jxxghp
feda0cad2d feat 默认过滤规则拆分 2023-10-13 21:28:34 +08:00
jxxghp
c723d89739 fix 2023-10-13 17:30:14 +08:00
jxxghp
0a0e7a059a fix 2023-10-13 17:29:23 +08:00
jxxghp
0263fbbee6 fix 2023-10-13 17:26:12 +08:00
jxxghp
e205296e22 feat 订阅实时编辑 2023-10-13 17:24:18 +08:00
jxxghp
261f5a9c68 fix #822 2023-10-13 15:14:26 +08:00
jxxghp
fa097651f4 fix rules 2023-10-13 11:41:27 +08:00
jxxghp
c94d5f7e7d fix bug 2023-10-12 22:45:59 +08:00
jxxghp
e34f18799f fix 2023-10-12 22:25:15 +08:00
jxxghp
1681a311f7 fix 2023-10-12 21:44:48 +08:00
jxxghp
da08d8ec19 fix ui 2023-10-12 21:36:08 +08:00
jxxghp
730178c838 fix 2023-10-12 20:02:24 +08:00
jxxghp
a04450ae98 feat 60fps 2023-10-12 17:06:37 +08:00
jxxghp
2b2fd66a29 fix bug 2023-10-12 16:31:17 +08:00
jxxghp
58fe08ad3d build ui 2023-10-12 16:30:27 +08:00
jxxghp
240d6bede0 fix ui 2023-10-12 16:26:05 +08:00
jxxghp
23d808f8b1 feat 资源页面视图切换 2023-10-12 16:09:15 +08:00
jxxghp
2f293706cb fix 2023-10-12 09:46:04 +08:00
jxxghp
9aaaf0c520 feat 更多订阅设置项 2023-10-12 09:43:55 +08:00
jxxghp
6694e7e929 feat 搜索框聚焦、发现页缓存 2023-10-12 08:13:05 +08:00
jxxghp
d3768cb994 feat switch view button 2023-10-11 21:30:59 +08:00
jxxghp
c59d3e28b9 feat torrents page 2023-10-11 21:21:49 +08:00
jxxghp
914239f434 feat 热门动漫 2023-10-11 16:12:38 +08:00
jxxghp
7a5d04dc53 add rclone 2023-10-11 11:47:45 +08:00
jxxghp
110fe39e72 Merge pull request #50 from thsrite/main
fix customization
2023-10-10 13:57:14 +08:00
thsrite
9689a86151 fix customization 2023-10-10 13:54:57 +08:00
jxxghp
6462ae5956 fix 2023-10-10 09:15:01 +08:00
jxxghp
053963d050 nginx conf 2023-10-10 08:36:09 +08:00
jxxghp
8a95549118 remove win builder 2023-10-10 08:09:29 +08:00
jxxghp
46e8fa551c fix plugin uri 2023-10-10 08:06:34 +08:00
jxxghp
be2034d75b fix build 2023-10-09 21:28:39 +08:00
jxxghp
634fa58048 fix build 2023-10-09 19:21:51 +08:00
jxxghp
cd5c093557 fix build 2023-10-09 19:16:14 +08:00
jxxghp
76cf86385e add pkg 2023-10-09 19:13:31 +08:00
jxxghp
5c5ed5d7ee add express 2023-10-09 18:40:55 +08:00
jxxghp
47e7a37667 Merge pull request #48 from thsrite/customization 2023-10-08 12:22:40 +08:00
thsrite
d642ab42be feat 自定义占位符 2023-10-08 12:13:21 +08:00
jxxghp
b4de1c99d5 repack 2023-10-08 09:19:23 +08:00
jxxghp
53e35eb9ff fix ui 2023-10-08 09:05:37 +08:00
jxxghp
b222098ec5 Update package.json 2023-10-07 05:55:15 -07:00
jxxghp
bb8cf7ed78 Merge pull request #47 from thsrite/main
feat 药丸论坛签到
2023-10-07 20:54:37 +08:00
thsrite
0219ce3a9c Merge remote-tracking branch 'origin/main' into main 2023-10-07 20:50:07 +08:00
thsrite
b82e5d7cba feat 药丸论坛签到 2023-10-07 20:49:59 +08:00
jxxghp
ccee71e638 fix ui 2023-09-29 08:55:33 +08:00
jxxghp
cba0e739eb 更新 package.json 2023-09-28 17:55:17 +08:00
jxxghp
c569cb9cde add icon 2023-09-28 17:11:04 +08:00
jxxghp
fc585a3900 v1.2.7 2023-09-28 16:16:04 +08:00
jxxghp
973f8529c2 Merge pull request #46 from thsrite/main
feat 自动清理媒体库插件icon
2023-09-28 15:46:36 +08:00
thsrite
1ff9dc50fd feat 自动清理媒体库插件icon 2023-09-28 15:12:15 +08:00
jxxghp
065c9053da fix ui 2023-09-28 12:52:44 +08:00
jxxghp
6905be1bcd fix #681 2023-09-28 09:58:39 +08:00
jxxghp
a550f9616c feat 批量整理进度条 2023-09-27 14:26:59 +08:00
jxxghp
bcee3e5373 v1.2.6 2023-09-27 10:19:05 +08:00
jxxghp
d377ced6b6 feat 优先级规则支持动态调整 2023-09-27 09:42:28 +08:00
jxxghp
6e0ceb093c feat 批量重新整理 2023-09-27 08:59:09 +08:00
jxxghp
745f99e52e feat 历史记录批量重新整理 2023-09-27 08:45:55 +08:00
jxxghp
7197034eda fix ui 2023-09-24 19:52:55 +08:00
jxxghp
264748652f fix 2023-09-24 19:47:28 +08:00
jxxghp
48e214564a v1.2.5 2023-09-24 19:30:37 +08:00
jxxghp
5424e7e02a fix ui 2023-09-24 12:26:42 +08:00
jxxghp
0c9c70b067 feat 服务设置 2023-09-24 11:14:27 +08:00
jxxghp
0ff24f4b09 fix torrent ui 2023-09-23 11:55:49 +08:00
jxxghp
cfa75b7643 rename 2023-09-23 08:32:24 +08:00
jxxghp
b72ad1d78d Merge pull request #45 from jianxcao/feature-url-307
feat: 修改url地址,防止307成错误的url地址
2023-09-22 15:34:14 +08:00
jxxghp
5d1f293606 feat SynologyChat 2023-09-22 15:32:51 +08:00
jianxiong.cao
2dc0eca4aa feat: 修改url地址,防止307成错误的url地址 2023-09-22 15:11:52 +08:00
jxxghp
f5808c1c81 fix 光标聚焦 2023-09-22 13:49:00 +08:00
jxxghp
321037477f fix placeholder 2023-09-22 11:03:58 +08:00
jxxghp
43589c66e9 fix bug 2023-09-22 09:20:41 +08:00
jxxghp
435f299a8b fix ui 2023-09-22 07:27:27 +08:00
jxxghp
083db80251 更新 MediaCard.vue 2023-09-21 23:21:04 +08:00
jxxghp
92bf520cf4 fix 2023-09-21 23:06:51 +08:00
jxxghp
ab354f21c4 fix ui 2023-09-21 22:57:54 +08:00
jxxghp
c7a2c045c7 fix ui 2023-09-21 22:50:35 +08:00
jxxghp
d33c8942e4 fix ui 2023-09-21 22:28:18 +08:00
jxxghp
5e630097b9 更新 MediaCard.vue 2023-09-21 21:52:56 +08:00
jxxghp
3b5d03c1c8 更新 package.json 2023-09-21 21:26:34 +08:00
jxxghp
298ae2c354 fix ui 2023-09-21 21:24:10 +08:00
jxxghp
d936b68597 fix ui 2023-09-21 20:31:24 +08:00
jxxghp
41471b9fd6 feat 历史记录删除UI调整 2023-09-21 20:01:25 +08:00
jxxghp
cc071c0911 v1.2.3-1 2023-09-20 15:36:16 +08:00
jxxghp
628164d2bd fix cssCodeSplit 2023-09-20 15:35:49 +08:00
jxxghp
999af85262 fix text 2023-09-20 07:10:15 +08:00
jxxghp
07e075ad8b fix text 2023-09-20 07:08:45 +08:00
jxxghp
18098f8aef fix 2023-09-20 07:02:10 +08:00
jxxghp
f335b4e436 v1.2.3 2023-09-20 06:56:25 +08:00
jxxghp
ab293edf4c 更新 AccountSettingRule.vue 2023-09-19 23:44:17 +08:00
jxxghp
88917070bf Merge pull request #43 from thofx/fix_bug
try fix bug
2023-09-19 22:46:46 +08:00
thofx
5bba5cb2bc try fix bug 2023-09-19 22:44:18 +08:00
jxxghp
098916bfa5 Merge pull request #42 from thofx/fix_bug 2023-09-19 22:16:37 +08:00
thofx
bb79aaed8b fix bug 2023-09-19 22:11:58 +08:00
jxxghp
bc5c5a2835 fix ui 2023-09-19 21:43:39 +08:00
jxxghp
4c11199de2 feat 规则页面拆分 2023-09-19 21:41:07 +08:00
jxxghp
2e987701a8 fix router 2023-09-19 18:02:44 +08:00
jxxghp
4f625291a5 roolback 2023-09-19 17:24:05 +08:00
jxxghp
048f2abd87 fix 2023-09-19 17:20:51 +08:00
jxxghp
31dea532c5 更新 App.vue 2023-09-19 14:57:38 +08:00
jxxghp
3d54e5d965 更新 VerticalNav.vue 2023-09-19 14:56:52 +08:00
jxxghp
aee2a5a161 Merge pull request #41 from cikezhu/main 2023-09-19 14:55:38 +08:00
叮叮当
198ea0104d 优化首次载入流程 2023-09-19 14:35:43 +08:00
叮叮当
1abdf6d15c 优化首次载入方式 2023-09-19 14:30:54 +08:00
叮叮当
e5b836462f 优化首次载入流程 2023-09-19 13:57:37 +08:00
jxxghp
552b20b5d9 fix ui 2023-09-19 13:41:05 +08:00
jxxghp
7d500aedb5 fix menu layout 2023-09-19 13:04:35 +08:00
jxxghp
751e823b8c test remove transition 2023-09-19 12:51:44 +08:00
jxxghp
59d47b2b15 Merge pull request #40 from cikezhu/main
去除多余的代码/修改记录位置的方式/修复IOS14登录页显示bug
2023-09-19 12:07:54 +08:00
叮叮当
b1635b0715 修复IOS14登录页显示bug 2023-09-19 10:42:46 +08:00
叮叮当
4d778e9ca9 修改记录位置的方式 2023-09-19 10:36:59 +08:00
叮叮当
af433286d0 Merge branch 'main' of https://github.com/jxxghp/MoviePilot-Frontend 2023-09-19 10:35:02 +08:00
叮叮当
2a41e8a726 修改记录位置的方式 2023-09-19 10:34:49 +08:00
叮叮当
6b41f3bb64 去除多余的代码 2023-09-19 10:33:52 +08:00
jxxghp
78c178b1f6 fix ui 2023-09-19 08:17:18 +08:00
jxxghp
20a6dd1aeb fix ui 2023-09-18 17:51:04 +08:00
jxxghp
3773dfb4a1 fix ui 2023-09-18 17:39:05 +08:00
jxxghp
e156b662a3 fix ui 2023-09-18 17:36:12 +08:00
jxxghp
38193a870b fix 2023-09-18 17:05:40 +08:00
jxxghp
e3a636772a fix 2023-09-17 20:02:26 +08:00
jxxghp
46b043fdc7 fix ui 2023-09-17 20:02:04 +08:00
jxxghp
a774ae87c2 Merge pull request #39 from WithdewHua/dev 2023-09-17 18:58:52 +08:00
WithdewHua
a332a7b402 feat: 订阅搜索支持默认包含与排除规则 2023-09-17 18:32:41 +08:00
jxxghp
2ee4d874da Merge pull request #38 from cikezhu/main 2023-09-16 16:54:07 +08:00
叮叮当
4f051e5251 增加滚动位置记录, 返回时恢复 2023-09-16 16:51:11 +08:00
106 changed files with 4004 additions and 1964 deletions

View File

@@ -1,4 +1,4 @@
name: Build moviepilot frontend
name: Build Moviepilot-Frontend
on:
workflow_dispatch:

View File

@@ -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>

View File

@@ -1,7 +1,8 @@
{
"name": "moviepilot",
"version": "1.2.2",
"version": "1.3.4-3",
"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",
@@ -21,6 +28,8 @@
"axios": "1.4.0",
"axios-mock-adapter": "^1.21.4",
"chart.js": "^4.1.2",
"express": "^4.18.2",
"express-http-proxy": "^2.0.0",
"jwt-decode": "^3.1.2",
"nprogress": "^0.2.0",
"postcss-purgecss": "^5.0.0",

View File

@@ -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
View 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;
# 可以添加更多后端服务器作为负载均衡
}
}

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

View File

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 186 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

36
public/service.js Normal file
View File

@@ -0,0 +1,36 @@
const path = require('node:path')
const express = require('express')
const proxy = require('express-http-proxy')
const app = express()
const port = process.env.NGINX_PORT || 3000
// 后端 API 地址
const proxyConfig = {
URL: '127.0.0.1',
PORT: process.env.PORT || 3001
}
// 静态文件服务目录
app.use(express.static(__dirname))
// 配置代理中间件将请求转发给后端API
app.use(
'/api',
proxy(`${proxyConfig.URL}:${proxyConfig.PORT}`, {
// 路径加上 /api 前缀
proxyReqPathResolver: (req) => {
return `/api${req.url}`
}
})
);
// 处理根路径的请求
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html')) // 指向你的前端入口文件
})
app.listen(port, () => {
console.log(`Server is running on port ${port}`)
})

View File

@@ -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;
}
}

View File

@@ -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
}
@@ -70,15 +66,12 @@ function handleNavScroll(evt: Event) {
<slot name="nav-items" :update-is-vertical-nav-scrolled="updateIsVerticalNavScrolled">
<PerfectScrollbar
tag="ul"
class="nav-items d-none d-lg-block"
class="nav-items"
:options="{ wheelPropagation: false }"
@ps-scroll-y="handleNavScroll"
>
<slot />
</PerfectScrollbar>
<ul class="nav-items d-lg-none overflow-auto">
<slot />
</ul>
</slot>
<slot name="after-nav-items" />
@@ -86,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;
@@ -101,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;

View File

@@ -7,9 +7,6 @@ import store from './store'
const { global: globalTheme } = useTheme()
globalTheme.name.value = localStorage.getItem('theme') || 'light'
// 路由
const route = useRoute()
// 提示框
const $toast = useToast()

View File

@@ -44,6 +44,15 @@ export interface Subscribe {
// 排除
exclude?: string
// 质量
quality?: string
// 分辨率
resolution?: string
// 特效
effect?: string
// 总集数
total_episode?: number
@@ -68,8 +77,8 @@ export interface Subscribe {
// 订阅站点
sites: number[]
// 是否洗版
best_version: number
// 是否洗版数字或者boolean
best_version: any
// 当前优先级
current_priority: number
@@ -87,7 +96,7 @@ export interface TransferHistory {
// 目的目录
dest?: string
// 转移模式link/copy/move/softlink
// 转移模式link/copy/move/softlink/rclone_copy/rclone_move
mode?: string
// 类型:电影、电视剧
@@ -407,13 +416,13 @@ export interface Site {
ua?: string
// 是否使用代理
proxy?: number
proxy?: any
// 过滤规则
filter?: string
// 是否演染
render?: number
render?: any
// 是否公开站点
public?: number
@@ -469,6 +478,9 @@ export interface DownloadingInfo {
// 媒体信息
media: { [key: string]: any }
// 下载用户
userid?: string
}
// 缺失剧集信息
@@ -832,6 +844,7 @@ export interface NotificationSwitch {
wechat: boolean
telegram: boolean
slack: boolean
synologychat: boolean
}
// 环境设置

View File

@@ -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,22 +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 ' },
])
</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>

View File

@@ -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'
@@ -39,6 +40,12 @@ const seasonsNotExisted = ref<{ [key: number]: number }>({})
// 订阅季弹窗
const subscribeSeasonDialog = ref(false)
// 订阅编辑弹窗
const subscribeEditDialog = ref(false)
// 订阅ID
const subscribeId = ref(0)
// 季详情
const seasonInfos = ref<TmdbSeason[]>([])
@@ -86,6 +93,7 @@ async function handleAddSubscribe() {
}
else {
// 弹出季选择列表,支持多选
seasonsSelected.value = []
subscribeSeasonDialog.value = true
}
}
@@ -112,7 +120,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 +144,12 @@ async function addSubscribe(season = 0) {
result.message,
best_version,
)
// 弹出订阅编辑弹窗
if (result.success && seasonsSelected.value.length <= 1) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
}
}
catch (error) {
console.error(error)
@@ -156,9 +170,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}`)
}
@@ -360,14 +374,6 @@ onBeforeMount(() => {
handleCheckExists()
})
// 订阅季表头
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 +385,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 +488,97 @@ const getImgUrl: Ref<string> = computed(() => {
</VCard>
</template>
</VHover>
<VDialog
<!-- 订阅季弹窗 -->
<VBottomSheet
v-model="subscribeSeasonDialog"
max-width="50rem"
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">

View File

@@ -57,7 +57,7 @@ async function installPlugin() {
:class="{ shadow: isImageLoaded }"
>
<VImg
:src="`/plugin/${props.plugin?.plugin_icon}`"
:src="`/plugin_icon/${props.plugin?.plugin_icon}`"
aspect-ratio="4/3"
cover
@load="isImageLoaded = true"

View File

@@ -216,7 +216,7 @@ const dropdownItems = ref([
:class="{ shadow: isImageLoaded }"
>
<VImg
:src="`/plugin/${props.plugin?.plugin_icon}`"
:src="`/plugin_icon/${props.plugin?.plugin_icon}`"
aspect-ratio="4/3"
cover
:class="{ shadow: isImageLoaded }"
@@ -236,11 +236,13 @@ const dropdownItems = ref([
<!-- 插件配置页面 -->
<VDialog
v-model="pluginConfigDialog"
max-width="50rem"
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
@@ -255,7 +257,10 @@ const dropdownItems = ref([
查看详情
</VBtn>
<VSpacer />
<VBtn @click="savePluginConf">
<VBtn
variant="tonal"
@click="savePluginConf"
>
保存
</VBtn>
</VCardActions>
@@ -265,11 +270,13 @@ const dropdownItems = ref([
<!-- 插件详情页面 -->
<VDialog
v-model="pluginInfoDialog"
max-width="62.5rem"
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
@@ -279,11 +286,16 @@ const dropdownItems = ref([
/>
</VCardText>
<VCardActions>
<VBtn @click="showPluginConfig">
<VBtn
@click="showPluginConfig"
>
配置
</VBtn>
<VSpacer />
<VBtn @click="pluginInfoDialog = false">
<VBtn
variant="tonal"
@click="pluginInfoDialog = false"
>
关闭
</VBtn>
</VCardActions>

View File

@@ -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)
@@ -42,7 +43,7 @@ const updateButtonDisable = ref(false)
const siteCookieDialog = ref(false)
// 站点编辑弹窗
const siteInfoDialog = ref(false)
const siteEditDialog = ref(false)
// 资源浏览弹窗
const resourceDialog = ref(false)
@@ -78,27 +79,6 @@ const userPwForm = ref({
password: '',
})
// 状态下拉项
const statusItems = [
{ title: '启用', value: true },
{ title: '停用', value: false },
]
// 生成1到50的优先级下拉框选项
const priorityItems = ref(
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
title: item,
value: item,
})),
)
// 站点编辑表单数据
const siteForm = reactive<any>(cardProps.site ?? {})
// 类型转换
siteForm.proxy = siteForm.proxy === 1
siteForm.render = siteForm.render === 1
// 打开种子详情页面
function openTorrentDetail(page_url: string) {
window.open(page_url, '_blank')
@@ -144,11 +124,6 @@ async function handleSiteUpdate() {
siteCookieDialog.value = true
}
// 打开站点编辑弹窗
async function handleSiteInfo() {
siteInfoDialog.value = true
}
// 打开资源浏览弹窗
async function handleResourceBrowse() {
resourceDialog.value = true
@@ -189,42 +164,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)
@@ -264,9 +203,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
@@ -278,17 +217,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 }">
@@ -302,7 +243,7 @@ onMounted(() => {
</VTooltip>
<VTooltip
v-if="siteForm.proxy"
v-if="cardProps.site?.proxy === 1"
text="代理"
>
<template #activator="{ props }">
@@ -316,7 +257,7 @@ onMounted(() => {
</VTooltip>
<VTooltip
v-if="siteForm.limit_interval"
v-if="cardProps.site?.limit_interval"
text="流控"
>
<template #activator="{ props }">
@@ -330,7 +271,7 @@ onMounted(() => {
</VTooltip>
<VTooltip
v-if="siteForm.filter"
v-if="cardProps.site?.filter"
text="过滤"
>
<template #activator="{ props }">
@@ -419,143 +360,22 @@ onMounted(() => {
<VCardActions>
<VSpacer />
<VBtn @click="updateSiteCookie">
<VBtn
variant="tonal"
@click="updateSiteCookie"
>
开始更新
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 站点编辑弹窗 -->
<VDialog
v-model="siteInfoDialog"
max-width="50rem"
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">
<VTextField
v-model="siteForm.rss"
label="RSS地址"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="siteForm.cookie"
label="站点Cookie"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="siteForm.ua"
label="站点User-Agent"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_interval"
label="单位周期(秒)"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问次数"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问间隔(秒)"
:rules="[numberValidator]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.proxy"
label="代理"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.render"
label="仿真"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn color="error" @click="deleteSiteInfo">
删除
</VBtn>
<VSpacer />
<VBtn @click="updateSiteInfo">
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<SiteAddEditForm
v-model="siteEditDialog"
:siteid="cardProps.site?.id"
@save="siteEditDialog = false; emit('update')"
@remove="emit('remove')"
@close="siteEditDialog = false"
/>
<!-- 站点资源弹窗 -->
<VDialog
v-model="resourceDialog"
@@ -647,13 +467,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>

View File

@@ -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/rss')
// 过滤站点,只有启用的站点才显示
siteList.value = data.filter(item => item.is_active)
}
catch (error) {
console.error(error)
}
}
// 获取站点列表选择框数据
async function getSiteList() {
// 加载订阅站点列表
if (!siteList.value.length)
await loadSites()
const maps = siteList.value.map((item) => {
return {
title: item.name,
value: item.id,
}
})
selectSitesOptions.value = maps.flat()
}
// 编辑订阅响应
async function editSubscribeDialog() {
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="50rem"
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>

View File

@@ -20,6 +20,9 @@ const keyword = ref('')
// 加载中
const loading = ref(false)
// ref
const tmdbKeyword = ref<HTMLElement | null>(null)
// 选中条目
function selectMedia(item: TmdbItem) {
console.log(item)
@@ -68,6 +71,14 @@ async function searchMedias() {
console.error(e)
}
}
// 加载时聚焦搜索框
onMounted(() => {
// 500ms后聚焦
setTimeout(() => {
tmdbKeyword.value?.focus()
}, 500)
})
</script>
<template>
@@ -75,16 +86,17 @@ async function searchMedias() {
class="mx-auto"
width="100%"
>
<VToolbar flat dense>
<VToolbar flat class="p-0">
<VTextField
ref="tmdbKeyword"
v-model="keyword"
density="compact"
label="输入名称搜索"
single-line
hide-details
flat
class="mx-3"
placeholder="电影或电视剧名称"
variant="solo"
append-inner-icon="mdi-magnify"
flat
class="mx-1"
:loading="loading"
@click:append-inner="searchMedias"
@keydown.enter="searchMedias"
@@ -97,7 +109,6 @@ async function searchMedias() {
>
<template v-for="(item, i) in items" :key="i">
<VListItem
density="compact"
@click="selectMedia(item)"
>
<template #prepend>
@@ -119,7 +130,7 @@ async function searchMedias() {
<VListItemTitle>
{{ item.title }}
</VListItemTitle>
<VListItemSubtitle v-html="item.overview" />
<VListItemSubtitle class="mt-2" v-html="item.overview" />
</VListItem>
<VDivider v-if="i < items.length - 1" class="mt-1" inset />
</template>

View File

@@ -64,6 +64,9 @@ async function handleAddDownload(_site: any = undefined,
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
variant: 'tonal',
},
})
if (!isConfirmed)
@@ -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>
@@ -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>

View File

@@ -0,0 +1,251 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import { formatFileSize } from '@/@core/utils/formatters'
import api from '@/api'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import type { Context } from '@/api/types'
// 输入参数
const props = defineProps({
torrent: Object as PropType<Context>,
})
// 提示框
const $toast = useToast()
// 确认框
const createConfirm = useConfirm()
// 更多来源界面
const showMoreTorrents = ref(false)
// 种子信息
const torrent = ref(props.torrent?.torrent_info)
// 媒体信息
const media = ref(props.torrent?.media_info)
// 识别元数据
const meta = ref(props.torrent?.meta_info)
// 站点图标
const siteIcon = ref('')
// 查询站点图标
async function getSiteIcon() {
try {
siteIcon.value = (await api.get(`site/icon/${torrent?.value?.site}`)).data.icon
}
catch (error) {
console.error(error)
}
}
// 询问并添加下载
async function handleAddDownload(_site: any = undefined,
_media: any = undefined,
_torrent: any = undefined) {
if (!_media || !_torrent || !_site) {
_site = torrent.value?.site_name
_media = media.value
_torrent = torrent.value
}
const isConfirmed = await createConfirm({
title: '确认',
content: `是否确认下载【${_site}${_torrent?.title} ?`,
confirmationText: '确认',
cancellationText: '取消',
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
variant: 'tonal',
},
})
if (!isConfirmed)
return
addDownload(_media, _torrent)
}
// 添加下载
async function addDownload(_media: any, _torrent: any) {
startNProgress()
try {
const result: { [key: string]: any } = await api.post('download/', {
media_in: _media,
torrent_in: _torrent,
})
if (result.success) {
// 添加下载成功
$toast.success(`${_torrent?.site_name} ${_torrent?.title} 添加下载成功!`)
}
else {
// 添加下载失败
$toast.error(`${_torrent?.site_name} ${_torrent?.title} 添加下载失败!`)
}
}
catch (error) {
console.error(error)
}
doneNProgress()
}
// 打开种子详情页面
function openTorrentDetail() {
window.open(torrent.value?.page_url, '_blank')
}
// 下载种子文件
async function downloadTorrentFile() {
window.open(torrent.value?.enclosure, '_blank')
}
// 促销Chip类
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
if (downloadVolume === 0)
return 'text-white bg-lime-500'
else if (downloadVolume < 1)
return 'text-white bg-green-500'
else if (uploadVolume !== 1)
return 'text-white bg-sky-500'
else
return 'text-white bg-gray-500'
}
// 装载时查询站点图标
onMounted(() => {
getSiteIcon()
})
</script>
<template>
<VListItem @click="handleAddDownload">
<template
v-if="!showMoreTorrents"
#prepend
>
<VAvatar
class="rounded"
variant="flat"
@click.stop="openTorrentDetail"
>
<VImg :src="siteIcon" />
</VAvatar>
</template>
<VListItemTitle class="break-words overflow-visible whitespace-break-spaces">
{{ torrent?.title }}
<span class="text-green-700 ms-2 text-sm">{{ torrent?.seeders }}</span>
<span class="text-orange-700 ms-2 text-sm">{{ torrent?.peers }}</span>
</VListItemTitle>
<VListItemSubtitle>
{{ torrent?.description }}
</VListItemSubtitle>
<div
v-if="torrent?.labels"
class="pt-2"
>
<VChip
v-for="(label, index) in torrent?.labels"
:key="index"
variant="elevated"
size="small"
color="primary"
class="me-1 mb-1"
>
{{ label }}
</VChip>
<VChip
v-if="meta?.edition"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-red-500"
>
{{ meta?.edition }}
</VChip>
<VChip
v-if="meta?.resource_pix"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-red-500"
>
{{ meta?.resource_pix }}
</VChip>
<VChip
v-if="meta?.video_encode"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-orange-500"
>
{{ meta?.video_encode }}
</VChip>
<VChip
v-if="torrent?.size"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-yellow-500"
>
{{ formatFileSize(torrent?.size) }}
</VChip>
<VChip
v-if="meta?.resource_team"
variant="elevated"
size="small"
class="me-1 mb-1 text-white bg-cyan-500"
>
{{ meta?.resource_team }}
</VChip>
<VChip
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
:class="
getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)
"
variant="elevated"
size="small"
class="me-1 mb-1"
>
{{ torrent?.volume_factor }}
</VChip>
</div>
<template #append>
<div class="me-n3">
<IconBtn>
<VIcon
icon="mdi-dots-vertical"
/>
<VMenu
activator="parent"
close-on-content-click
>
<VList>
<VListItem
variant="plain"
@click="openTorrentDetail()"
>
<template #prepend>
<VIcon icon="mdi-information" />
</template>
<VListItemTitle>查看详情</VListItemTitle>
</VListItem>
<VListItem
v-if="props.torrent?.torrent_info?.enclosure?.startsWith('http')"
variant="plain"
@click="downloadTorrentFile()"
>
<template #prepend>
<VIcon icon="mdi-download" />
</template>
<VListItemTitle>下载种子文件</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</IconBtn>
</div>
</template>
</VListItem>
</template>

View File

@@ -4,13 +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 { Context, EndPoints, FileItem } from '@/api/types'
import store from '@/store'
import api from '@/api'
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
// 输入参数
const inProps = defineProps({
@@ -32,6 +31,15 @@ const $toast = useToast()
// 是否正在加载
const loading = ref(true)
// 识别进度条
const progressDialog = ref(false)
// 识别进度文本
const progressText = ref('请稍候 ...')
// 识别进度
const progressValue = ref(0)
// 确认框
const createConfirm = useConfirm()
@@ -53,57 +61,18 @@ const renamePopper = ref(false)
// 整理弹窗
const transferPopper = ref(false)
// 整理进度条
const progressDialog = ref(false)
// 整理进度文本
const progressText = ref('请稍候 ...')
// 整理进度
const progressValue = ref(0)
// 加载进度SSE
const progressEventSource = ref<EventSource>()
// 新名称
const newName = ref('')
// 当前名称
const currentItem = ref<FileItem>()
// 文件转移表单
const transferForm = reactive({
path: '',
target: '',
tmdbid: null,
season: null,
type_name: '',
transfer_type: '',
episode_format: '',
episode_detail: '',
episode_part: '',
episode_offset: null,
min_filesize: 0,
})
// 识别结果
const nameTestResult = ref<Context>()
// 识别结果对话框
const nameTestDialog = ref(false)
// TMDB选择对话框
const tmdbSelectorDialog = ref(false)
// 生成1到50季的下拉框选项
const seasonItems = ref(
Array.from({ length: 51 }, (_, i) => i).map(item => ({
title: `${item}`,
value: item,
})),
)
// 目录过滤
const dirs = computed(() =>
items.value.filter(item => item.type === 'dir' && item.basename.includes(filter.value)),
@@ -158,6 +127,9 @@ async function deleteItem(item: FileItem) {
dialogProps: {
maxWidth: '50rem',
},
cancellationButtonProps: {
variant: 'tonal',
},
})
if (confirmed) {
@@ -245,41 +217,6 @@ function showTransfer(item: FileItem) {
transferPopper.value = true
}
// 整理文件
async function transfer() {
transferForm.path = currentItem.value?.path ?? ''
// 开始整理文件
try {
// 关闭弹窗
transferPopper.value = false
// 显示进度条
progressDialog.value = true
// 开始监听进度
startLoadingProgress()
// 异步调API结束后关闭进度条
api.post('transfer/manual', {}, {
params: transferForm,
}).then((res: any) => {
// 关闭进度条
progressDialog.value = false
// 停止监听进度
stopLoadingProgress()
// 显示结果
if (res.success) {
$toast.success(`${currentItem.value?.name} 整理完成!`)
// 重新加载
load()
}
else {
$toast.error(`${currentItem.value?.name} 整理失败:${res.message}`)
}
})
}
catch (e) {
console.log(e)
}
}
// 将文件修改时间timestape转换为本地时间
function formatTime(timestape: number) {
return new Date(timestape * 1000).toLocaleString()
@@ -307,29 +244,6 @@ watch(
},
)
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '请稍候 ...'
const token = store.state.auth.token
progressEventSource.value = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer?token=${token}`,
)
progressEventSource.value.onmessage = (event) => {
const progress = JSON.parse(event.data)
if (progress) {
progressText.value = progress.text
progressValue.value = progress.value
}
}
}
// 停止监听加载进度
function stopLoadingProgress() {
progressEventSource.value?.close()
}
// 调用API识别
async function recognize(path: string) {
try {
@@ -586,23 +500,19 @@ onMounted(() => {
v-model="renamePopper"
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"
>
重命名
@@ -611,178 +521,44 @@ onMounted(() => {
</VCard>
</VDialog>
<!-- 文件整理弹窗 -->
<VDialog
<ReorganizeForm
v-model="transferPopper"
max-width="50rem"
scrollable
>
<template #activator="{ props }">
<IconBtn title="整理" v-bind="props">
<VIcon icon="mdi-folder-arrow-right-outline" />
</IconBtn>
</template>
<VCard :title="`文件整理 - ${currentItem?.name}`">
<VCardText class="pt-2">
<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: '电影' }, { title: '电视剧', value: '电视剧' }]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="transferForm.tmdbid"
label="TMDBID"
placeholder="留空自动识别"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
@click:append-inner="tmdbSelectorDialog = true"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VSelect
v-show="transferForm.type_name === '电视剧'"
v-model.number="transferForm.season"
label="季"
:items="seasonItems"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="8">
<VTextField
v-model="transferForm.episode_format"
label="集数定位"
placeholder="使用{ep}定位集数"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.episode_detail"
label="指定集数"
placeholder="起始集,终止集如1或1,2"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model="transferForm.episode_part"
label="指定Part"
placeholder="如part1"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model.number="transferForm.episode_offset"
label="集数偏移"
placeholder="如-10"
/>
</VCol>
<VCol cols="12" md="4">
<VTextField
v-model.number="transferForm.min_filesize"
label="最小文件大小MB"
:rules="[numberValidator]"
placeholder="0"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn depressed @click="transferPopper = false">
取消
</VBtn>
<VSpacer />
<VBtn
@click="transfer"
>
开始整理
</VBtn>
</VCardActions>
</VCard>
</VDialog>
:path="currentItem?.path"
@done="transferPopper = false; load()"
@close="transferPopper = false"
/>
<!-- 手动整理进度框 -->
<vDialog
<VDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<vCard
<VCard
color="primary"
>
<vCardText class="text-center">
<VCardText class="text-center">
{{ progressText }}
<vProgressLinear
<VProgressLinear
v-if="progressValue"
color="white"
class="mb-0 mt-1"
:model-value="progressValue"
/>
</vCardText>
</vCard>
</vDialog>
</VCardText>
</VCard>
</VDialog>
<!-- 识别结果对话框 -->
<vDialog
<VDialog
v-model="nameTestDialog"
width="50rem"
>
<vCard>
<VCard>
<DialogCloseBtn @click="nameTestDialog = false" />
<VCardItem>
<MediaInfoCard :context="nameTestResult" />
</VCardItem>
</vCard>
</vDialog>
<!-- TMDB ID搜索框 -->
<vDialog
v-model="tmdbSelectorDialog"
width="40rem"
scrollable
>
<TmdbSelectorCard
v-model="transferForm.tmdbid"
@close="tmdbSelectorDialog = false"
/>
</vDialog>
</VCard>
</VDialog>
</template>
<style lang="scss" scoped>

View File

@@ -171,6 +171,7 @@ const sortIcon = computed(() => {
<VBtn
:disabled="!newFolderName"
depressed
variant="tonal"
@click="mkdir"
>
新建

View 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>

View 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_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
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>

View File

@@ -0,0 +1,349 @@
<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,
})
// 提示框
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.]+',
},
])
// 初始化
onMounted(async () => {
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">
<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>

View File

@@ -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,6 +32,7 @@ const formData = ref<any>(elementProps.form || {})
<template>
<Component
:is="formItem.component"
v-if="!formItem.html"
v-bind="formItem.props"
v-model="formData[formItem.props?.model || '']"
>
@@ -42,4 +45,10 @@ const formData = ref<any>(elementProps.form || {})
:form="formData"
/>
</Component>
<Component
:is="formItem.component"
v-if="formItem.html"
v-bind="formItem.props"
v-html="formItem.html"
/>
</template>

View File

@@ -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: '',

View File

@@ -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>

View File

@@ -91,7 +91,6 @@ const superUser = store.state.auth.superUser
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '电影',
icon: 'mdi-movie-check-outline',
@@ -99,7 +98,6 @@ const superUser = store.state.auth.superUser
}"
/>
<VerticalNavLink
v-if="superUser"
:item="{
title: '电视剧',
icon: 'mdi-television-classic',

View File

@@ -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>
@@ -34,23 +45,16 @@ function search() {
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

View File

@@ -132,9 +132,9 @@ const ruleTestDialog = ref(false)
<VIcon icon="mdi-filter-cog-outline" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">
规则
优先级
</h6>
<span class="text-sm">过滤规则测试</span>
<span class="text-sm">优先级规则测试</span>
</VListItem>
</VCol>
</VRow>
@@ -184,7 +184,7 @@ const ruleTestDialog = ref(false)
max-width="50rem"
scrollable
>
<VCard title="过滤规则测试">
<VCard title="优先级测试">
<DialogCloseBtn @click="ruleTestDialog = false" />
<VCardText>
<RuleTestView />

View File

@@ -38,6 +38,9 @@ async function restart() {
dialogProps: {
maxWidth: '30rem',
},
cancellationButtonProps: {
variant: 'tonal',
},
})
if (confirmed) {
@@ -121,6 +124,25 @@ const avatar = store.state.auth.avatar
<VListItemTitle>设定</VListItemTitle>
</VListItem>
<!-- Divider -->
<VDivider class="my-2" />
<!-- 👉 restart -->
<VListItem
v-if="superUser"
@click="restart"
>
<template #prepend>
<VIcon
class="me-2"
icon="mdi-restart"
size="22"
/>
</template>
<VListItemTitle>重启</VListItemTitle>
</VListItem>
<!-- 👉 FAQ -->
<VListItem
href="https://github.com/jxxghp/MoviePilot/blob/main/README.md"
@@ -137,22 +159,6 @@ const avatar = store.state.auth.avatar
<VListItemTitle>帮助</VListItemTitle>
</VListItem>
<!-- Divider -->
<VDivider class="my-2" />
<!-- 👉 restart -->
<VListItem @click="restart">
<template #prepend>
<VIcon
class="me-2"
icon="mdi-restart"
size="22"
/>
</template>
<VListItemTitle>重启</VListItemTitle>
</VListItem>
<!-- 👉 Logout -->
<VListItem @click="logout">
<template #prepend>
@@ -170,21 +176,21 @@ const avatar = store.state.auth.avatar
<!-- !SECTION -->
</VAvatar>
<!-- 重启进度框 -->
<vDialog
<VDialog
v-model="progressDialog"
width="25rem"
>
<vCard
<VCard
color="primary"
>
<vCardText class="text-center">
<VCardText class="text-center">
正在重启 ...
<vProgressLinear
<VProgressLinear
indeterminate
color="white"
class="mb-0 mt-1"
/>
</vCardText>
</vCard>
</vDialog>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -2,7 +2,6 @@ import { createApp } from 'vue'
import '@/@iconify/icons-bundle'
import ToastPlugin from 'vue-toast-notification'
import VuetifyUseDialog from 'vuetify-use-dialog'
import { configureNProgress, doneNProgress, startNProgress } from '@/api/nprogress'
import App from '@/App.vue'
import vuetify from '@/plugins/vuetify'
import { loadFonts } from '@/plugins/webfontloader'
@@ -11,54 +10,22 @@ import store from '@/store'
import '@core/scss/template/index.scss'
import '@layouts/styles/index.scss'
import '@styles/styles.scss'
import 'vue-toast-notification/dist/theme-default.css'
import 'vue-toast-notification/dist/theme-bootstrap.css'
import { removeEl } from '@/util'
loadFonts()
// Nprogress
configureNProgress()
// Create vue app
const app = createApp(App)
// Use plugins Mount vue app
app.use(vuetify)
app
.use(vuetify)
.use(router)
.use(store)
.use(ToastPlugin, {
position: 'bottom-right',
})
.use(VuetifyUseDialog).mount('#app')
// 记录和恢复滚动位置
const scrollPositions: { [key: string]: number } = {} // 用于存储每个路由的滚动位置
// 路由导航守卫
router.beforeEach((to, from, next) => {
const isAuthenticated = store.state.auth.token !== null
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
}
else {
// 只有 meta 中 keepAlive 为 true 的情况下才记录滚动位置
if (from.meta.keepAlive)
scrollPositions[from.fullPath] = window.scrollY
startNProgress()
next()
}
})
router.afterEach((to) => {
// 只有 meta 中 keepAlive 为 true 的情况下才恢复滚动位置
if (to.meta.keepAlive) {
const savedPosition = scrollPositions[to.fullPath]
if (savedPosition !== undefined) {
setTimeout(() => {
window.scrollTo(0, savedPosition)
}, 0)
}
}
doneNProgress()
})
.use(VuetifyUseDialog)
.mount('#app')
.$nextTick(() => removeEl('#loading-bg'))

View File

@@ -130,7 +130,7 @@ onMounted(() => {
<VCardItem class="justify-center mb-7">
<template #prepend>
<div class="d-flex pe-0">
<VImg :src="logo" width="64" />
<VImg :src="logo" width="64" height="64" />
</div>
</template>

View File

@@ -28,6 +28,12 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
title="热门电视剧"
/>
<MediaCardSlideView
apipath="douban/tv_animation"
linkurl="/browse/douban/tv_animation?title=热门动漫"
title="热门动漫"
/>
<MediaCardSlideView
apipath="douban/movies"
linkurl="/browse/douban/movies?title=最新电影"

View File

@@ -1,5 +1,9 @@
<script setup lang="ts">
import api from '@/api'
import type { Context } from '@/api/types'
import store from '@/store'
import TorrentCardListView from '@/views/discover/TorrentCardListView.vue'
import TorrentRowListView from '@/views/discover/TorrentRowListView.vue'
// 路由参数
const route = useRoute()
@@ -12,14 +16,130 @@ const type = route.query?.type?.toString() ?? ''
// 搜索字段
const area = route.query?.area?.toString() ?? ''
// 视图类型从localStorage中读取
const viewType = ref<string>(localStorage.getItem('MPTorrentsViewType') ?? 'card')
// 数据列表
const dataList = ref<Array<Context>>([])
// 是否刷新过
const isRefreshed = ref(false)
// 加载进度文本
const progressText = ref('')
// 加载进度
const progressValue = ref(0)
// 加载进度SSE
const progressEventSource = ref<EventSource>()
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '正在搜索,请稍候...'
const token = store.state.auth.token
progressEventSource.value = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/progress/search?token=${token}`,
)
progressEventSource.value.onmessage = (event) => {
const progress = JSON.parse(event.data)
if (progress) {
progressText.value = progress.text
progressValue.value = progress.value
}
}
}
// 停止监听加载进度
function stopLoadingProgress() {
progressEventSource.value?.close()
}
// 设置视图类型
function setViewType(type: string) {
localStorage.setItem('MPTorrentsViewType', type)
viewType.value = type
}
// 获取搜索列表数据
async function fetchData() {
try {
if (!keyword) {
// 查询上次搜索结果
dataList.value = await api.get('search/last')
}
else {
startLoadingProgress()
// 优先按TMDBID精确查询
if (keyword?.startsWith('tmdb:') || keyword?.startsWith('douban:')) {
dataList.value = await api.get(`search/media/${keyword}`, {
params: {
mtype: type,
area,
},
})
}
else {
// 按标题模糊查询
dataList.value = await api.get(`search/title/${keyword}`)
}
stopLoadingProgress()
}
// 标记已刷新
isRefreshed.value = true
}
catch (error) {
console.error(error)
return Promise.reject(error)
}
}
// 加载数据
onMounted(() => {
fetchData()
})
</script>
<template>
<div>
<div v-if="!isRefreshed" class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center">
<VProgressCircular v-if="!keyword" size="48" indeterminate color="primary" />
<VProgressCircular v-if="keyword" class="mb-3" color="primary" :model-value="progressValue" size="64" />
<span>{{ progressText }}</span>
</div>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
error-code="404"
error-title="没有资源"
error-description="没有搜索到符合条件的资源"
/>
<div v-if="dataList.length > 0">
<TorrentRowListView
v-if="viewType === 'list'"
:items="dataList"
/>
<TorrentCardListView
:keyword="keyword"
:type="type"
:area="area"
v-else
:items="dataList"
/>
</div>
<!-- 视图切换 -->
<span v-if="dataList.length > 0" class="fixed right-5 bottom-5">
<VBtn
v-if="viewType === 'list'"
size="x-large"
icon="mdi-view-grid"
color="primary"
@click="setViewType('card')"
/>
<VBtn
v-else
size="x-large"
icon="mdi-view-list"
color="primary"
@click="setViewType('list')"
/>
</span>
</template>

View File

@@ -2,10 +2,12 @@
import { useRoute } from 'vue-router'
import AccountSettingAccount from '@/views/setting/AccountSettingAccount.vue'
import AccountSettingNotification from '@/views/setting/AccountSettingNotification.vue'
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
import AccountSettingWords from '@/views/setting/AccountSettingWords.vue'
import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
const route = useRoute()
@@ -24,9 +26,19 @@ const tabs = [
tab: 'site',
},
{
title: '规则',
icon: 'mdi-filter-cog',
tab: 'filter',
title: '搜索',
icon: 'mdi-magnify',
tab: 'search',
},
{
title: '订阅',
icon: 'mdi-rss',
tab: 'subscribe',
},
{
title: '服务',
icon: 'mdi-list-box',
tab: 'service',
},
{
title: '通知',
@@ -48,7 +60,10 @@ const tabs = [
<template>
<div>
<VTabs v-model="activeTab" show-arrows>
<VTabs
v-model="activeTab"
show-arrows
>
<VTab v-for="item in tabs" :key="item.icon" :value="item.tab">
<VIcon size="20" start :icon="item.icon" />
{{ item.title }}
@@ -56,41 +71,59 @@ const tabs = [
</VTabs>
<VDivider />
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition">
<!-- Account -->
<VWindow
v-model="activeTab"
class="mt-5 disable-tab-transition"
:touch="false"
>
<!-- 用户 -->
<VWindowItem value="account">
<transition name="fade-slide" appear>
<AccountSettingAccount />
</transition>
</VWindowItem>
<!-- System -->
<!-- 站点 -->
<VWindowItem value="site">
<transition name="fade-slide" appear>
<AccountSettingSite />
</transition>
</VWindowItem>
<!-- Notification -->
<VWindowItem value="filter">
<!-- 搜索 -->
<VWindowItem value="search">
<transition name="fade-slide" appear>
<AccountSettingRule />
<AccountSettingSearch />
</transition>
</VWindowItem>
<!-- Notification -->
<!-- 订阅 -->
<VWindowItem value="subscribe">
<transition name="fade-slide" appear>
<AccountSettingSubscribe />
</transition>
</VWindowItem>
<!-- 服务 -->
<VWindowItem value="service">
<transition name="fade-slide" appear>
<AccountSettingService />
</transition>
</VWindowItem>
<!-- 通知 -->
<VWindowItem value="notification">
<transition name="fade-slide" appear>
<AccountSettingNotification />
</transition>
</VWindowItem>
<!-- Words -->
<!-- 词表 -->
<VWindowItem value="words">
<transition name="fade-slide" appear>
<AccountSettingWords />
</transition>
</VWindowItem>
<!-- About -->
<!-- 关于 -->
<VWindowItem value="about">
<transition name="fade-slide" appear>
<AccountSettingAbout />

View File

@@ -1,9 +1,17 @@
import { createRouter, createWebHistory } from 'vue-router'
import { configureNProgress, doneNProgress, startNProgress } from '@/api/nprogress'
import store from '@/store'
// Nprogress
configureNProgress()
// Router
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
scrollBehavior() {
// 始终滚动到顶部
scrollBehavior(to, from, savedPosition) {
// 如果页面有缓存那么恢复其位置, 否则始终滚动到顶部
if (to.meta.keepAlive && savedPosition)
return savedPosition
return { top: 0 }
},
routes: [
@@ -95,6 +103,7 @@ const router = createRouter({
component: () => import('../pages/browse.vue'),
props: true,
meta: {
keepAlive: true,
requiresAuth: true,
},
},
@@ -103,6 +112,7 @@ const router = createRouter({
component: () => import('../pages/credits.vue'),
props: true,
meta: {
keepAlive: true,
requiresAuth: true,
},
},
@@ -147,4 +157,21 @@ const router = createRouter({
],
})
// 路由导航守卫
router.beforeEach((to, from, next) => {
const isAuthenticated = store.state.auth.token !== null
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
}
else {
startNProgress()
next()
}
})
router.afterEach(() => {
doneNProgress()
})
export default router

View File

@@ -40,7 +40,6 @@
width: 100%;
}
/* router view transition fade-slide */
.fade-slide-leave-active,
.fade-slide-enter-active {
@@ -107,8 +106,10 @@
border-radius: 3px;
background: rgb(var(--v-theme-perfect-scrollbar-thumb));
-webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.2);
&:hover{
background: #a1a1a1;
@media(hover){
&:hover{
background: #a1a1a1;
}
}
}

6
src/util/dom.ts Normal file
View File

@@ -0,0 +1,6 @@
export function removeEl(selector: string) {
if (selector) {
const el = document.querySelector(selector)
el?.parentNode?.removeChild(el)
}
}

1
src/util/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './dom'

View File

@@ -8,6 +8,7 @@ import NoDataFound from '@/components/NoDataFound.vue'
import { doneNProgress, startNProgress } from '@/api/nprogress'
import { formatSeason } from '@/@core/utils/formatters'
import router from '@/router'
import SubscribeEditForm from '@/components/form/SubscribeEditForm.vue'
// 输入参数
const mediaProps = defineProps({
@@ -21,6 +22,9 @@ const $toast = useToast()
// 媒体详情
const mediaDetail = ref<MediaInfo>({} as MediaInfo)
// 订阅编辑弹窗
const subscribeEditDialog = ref(false)
// 本地是否存在
const isExists = ref(false)
@@ -39,6 +43,9 @@ const seasonsNotExisted = ref<{ [key: number]: number }>({})
// 各季的订阅状态
const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
// 订阅编号
const subscribeId = ref(0)
// 调用API查询详情
async function getMediaDetail() {
if (mediaProps.mediaid && mediaProps.type) {
@@ -185,7 +192,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: mediaDetail.value?.title,
type: mediaDetail.value?.type,
year: mediaDetail.value?.year,
@@ -211,6 +218,12 @@ async function addSubscribe(season = 0) {
result.message,
best_version,
)
// 显示编辑弹窗
if (result.success) {
subscribeId.value = result.data.id
subscribeEditDialog.value = true
}
}
catch (error) {
console.error(error)
@@ -231,9 +244,7 @@ function showSubscribeAddToast(result: boolean,
if (best_version > 0)
subname = '洗版订阅'
if (result)
$toast.success(`${title} 添加${subname}成功!`)
else
if (!result)
$toast.error(`${title} 添加${subname}失败:${message}`)
}
@@ -684,6 +695,20 @@ onBeforeMount(() => {
error-title="出错啦"
error-description="未识别到TMDB媒体信息"
/>
<!-- 订阅编辑弹窗 -->
<SubscribeEditForm
v-model="subscribeEditDialog"
:subid="subscribeId"
@close="subscribeEditDialog = false"
@save="subscribeEditDialog = false"
@remove="() => {
subscribeEditDialog = false;
if (mediaDetail.type === '电影')
checkMovieSubscribed()
else
checkSeasonsSubscribed();
}"
/>
</template>
<style lang="scss">

View File

@@ -1,66 +1,33 @@
<script lang="ts" setup>
import { ref } from 'vue'
import _ from 'lodash'
import api from '@/api'
import type { Context } from '@/api/types'
import TorrentCard from '@/components/cards/TorrentCard.vue'
import NoDataFound from '@/components/NoDataFound.vue'
import store from '@/store'
// 定义输入参数
const props = defineProps({
// 关键字或TMDBID
keyword: String,
// 类型
type: String,
// 搜索字段
area: String,
})
interface SearchTorrent extends Context {
more?: Array<Context>
}
// 数据列表
const dataList = ref <Array<SearchTorrent>>([])
// 分组后的数据列表
const groupedDataList = ref<Map<string, Context[]>>()
// 是否刷新过
const isRefreshed = ref(false)
// 加载进度文本
const progressText = ref('')
// 加载进度
const progressValue = ref(0)
// 加载进度SSE
const progressEventSource = ref<EventSource>()
// 定义输入参
const props = defineProps({
// 数据列表
items: Array as PropType<SearchTorrent[]>,
})
// 过滤表单
const filterForm = reactive({
// 站点
site: [] as string[],
// 季
season: [] as string[],
// 制作组
releaseGroup: [] as string[],
// 视频编码
videoCode: [] as string[],
// 促销状态
freeState: [] as string[],
// 质量
edition: [] as string[],
// 分辨率
resolution: [] as string[],
})
@@ -80,110 +47,13 @@ const editionFilterOptions = ref<Array<string>>([])
// 获取分辨率过滤选项
const resolutionFilterOptions = ref<Array<string>>([])
// 按过滤项过滤卡片
watchEffect(() => {
// 清空数据
dataList.value.splice(0)
// 数据列表
const dataList = ref <Array<SearchTorrent>>([])
const match = (filter: Array<string>, value: string | undefined) =>
filter.length === 0 || (value && filter.includes(value))
groupedDataList.value?.forEach((value) => {
if (value.length > 0) {
const matchData = value.filter((data) => {
const { meta_info, torrent_info } = data
// 季、制作组、视频编码
const { season_episode, resource_team, video_encode } = meta_info
return (
// 站点过滤
match(filterForm.site, torrent_info.site_name)
// 促销状态过滤
&& match(filterForm.freeState, torrent_info.volume_factor)
// 季过滤
&& match(filterForm.season, season_episode)
// 制作组过滤
&& match(filterForm.releaseGroup, resource_team)
// 视频编码过滤
&& match(filterForm.videoCode, video_encode)
// 分辨率过滤
&& match(filterForm.resolution, meta_info.resource_pix)
// 质量过滤
&& match(filterForm.edition, meta_info.edition)
)
})
if (matchData.length > 0) {
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
if (matchData.length > 1)
firstData.more = matchData.slice(1)
dataList.value.push(firstData)
}
}
})
})
// 获取订阅列表数据
async function fetchData(): Promise<Array<Context>> {
try {
let searchData: Array<Context>
const keyword = props.keyword ?? ''
const mtype = props.type ?? ''
const area = props.area ?? ''
if (!keyword) {
// 查询上次搜索结果
searchData = await api.get('search/last')
}
else {
startLoadingProgress()
// 优先按TMDBID精确查询
if (props.keyword?.startsWith('tmdb:') || props.keyword?.startsWith('douban:')) {
searchData = await api.get(`search/media/${props.keyword}`, {
params: {
mtype,
area,
},
})
}
else {
// 按标题模糊查询
searchData = await api.get(`search/title/${props.keyword}`)
}
stopLoadingProgress()
}
isRefreshed.value = true
return Promise.resolve(searchData)
}
catch (error) {
console.error(error)
return Promise.reject(error)
}
}
function initData() {
// load data
fetchData().then((data) => {
const groupMap = new Map<string, Context[]>()
data.forEach((item) => {
const { torrent_info } = item
// init options
initOptions(item)
// group data
const key = `${torrent_info.title}_${torrent_info.size}`
if (groupMap.has(key)) {
// 已存在相同标题和大小的分组,将当前上下文信息添加到分组中
const group = groupMap.get(key)
group?.push(item)
}
else {
// 创建新的分组,并将当前上下文信息添加到分组中
groupMap.set(key, [item])
}
})
groupedDataList.value = groupMap
})
}
// 分组后的数据列表
const groupedDataList = ref<Map<string, Context[]>>()
// 初始化过滤选项
function initOptions(data: Context) {
const { torrent_info, meta_info } = data
const optionValue = (options: Array<string>, value: string | undefined) => {
@@ -198,31 +68,69 @@ function initOptions(data: Context) {
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
}
// 使用SSE监听加载进度
function startLoadingProgress() {
progressText.value = '正在搜索,请稍候...'
const token = store.state.auth.token
progressEventSource.value = new EventSource(
`${import.meta.env.VITE_API_BASE_URL}system/progress/search?token=${token}`,
)
progressEventSource.value.onmessage = (event) => {
const progress = JSON.parse(event.data)
if (progress) {
progressText.value = progress.text
progressValue.value = progress.value
// 计算分组后的列表
watchEffect(() => {
// 数据分组
const groupMap = new Map<string, Context[]>()
// 遍历数据
props.items?.forEach((item) => {
const { torrent_info } = item
// init options
initOptions(item)
// group data
const key = `${torrent_info.title}_${torrent_info.size}`
if (groupMap.has(key)) {
// 已存在相同标题和大小的分组,将当前上下文信息添加到分组中
const group = groupMap.get(key)
group?.push(item)
}
}
}
else {
// 创建新的分组,并将当前上下文信息添加到分组中
groupMap.set(key, [item])
}
})
groupedDataList.value = groupMap
})
// 停止监听加载进度
function stopLoadingProgress() {
progressEventSource.value?.close()
}
// 计算过滤后的列表
watchEffect(() => {
// 清空列表
dataList.value.splice(0)
// 匹配过滤函数
const match = (filter: Array<string>, value: string | undefined) =>
filter.length === 0 || (value && filter.includes(value))
// 加载时获取数据
onMounted(initData)
groupedDataList.value?.forEach((value) => {
if (value.length > 0) {
const matchData = value.filter((data) => {
const { meta_info, torrent_info } = data
// 季、制作组、视频编码
return (
// 站点过滤
match(filterForm.site, torrent_info.site_name)
// 促销状态过滤
&& match(filterForm.freeState, torrent_info.volume_factor)
// 季过滤
&& match(filterForm.season, meta_info.season_episode)
// 制作组过滤
&& match(filterForm.releaseGroup, meta_info.resource_team)
// 视频编码过滤
&& match(filterForm.videoCode, meta_info.video_encode)
// 分辨率过滤
&& match(filterForm.resolution, meta_info.resource_pix)
// 质量过滤
&& match(filterForm.edition, meta_info.edition)
)
})
if (matchData.length > 0) {
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
if (matchData.length > 1)
firstData.more = matchData.slice(1)
dataList.value.push(firstData)
}
}
})
})
</script>
<template>
@@ -307,20 +215,14 @@ onMounted(initData)
</VCol>
</VRow>
</VCard>
<div v-if="!isRefreshed" class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center">
<VProgressCircular v-if="!props.keyword" size="48" indeterminate color="primary" />
<VProgressCircular v-if="props.keyword" class="mb-3" color="primary" :model-value="progressValue" size="64" />
<span>{{ progressText }}</span>
<div class="grid gap-3 grid-torrent-card items-start">
<TorrentCard
v-for="(item, index) in dataList"
:key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`"
:torrent="item"
:more="item.more"
/>
</div>
<div v-if="dataList.length > 0" class="grid gap-3 grid-torrent-card items-start">
<TorrentCard v-for="data in dataList" :key="`${data.torrent_info.title}_${data.torrent_info.site_name}_${data.torrent_info.page_url}`" :torrent="data" :more="data.more" />
</div>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
error-code="404"
error-title="没有资源"
error-description="没有搜索到符合条件的资源"
/>
</template>
<style lang="scss">

View File

@@ -0,0 +1,245 @@
<script lang="ts" setup>
import type { Context } from '@/api/types'
import TorrentItem from '@/components/cards/TorrentItem.vue'
// 定义输入参数
const props = defineProps({
// 数据列表
items: Array as PropType<Context[]>,
})
// 过滤表单
const filterForm = reactive({
// 站点
site: [] as string[],
// 季
season: [] as string[],
// 制作组
releaseGroup: [] as string[],
// 视频编码
videoCode: [] as string[],
// 促销状态
freeState: [] as string[],
// 质量
edition: [] as string[],
// 分辨率
resolution: [] as string[],
})
// 数据列表
const dataList = ref <Array<Context>>([])
// 获取站点过滤选项
const siteFilterOptions = ref<Array<string>>([])
// 获取季过滤选项
const seasonFilterOptions = ref<Array<string>>([])
// 获取制作组过滤选项
const releaseGroupFilterOptions = ref<Array<string>>([])
// 获取视频编码过滤选项
const videoCodeFilterOptions = ref<Array<string>>([])
// 获取促销状态过滤选项
const freeStateFilterOptions = ref<Array<string>>([])
// 获取质量过滤选项
const editionFilterOptions = ref<Array<string>>([])
// 获取分辨率过滤选项
const resolutionFilterOptions = ref<Array<string>>([])
// 初始化过滤选项
function initOptions(data: Context) {
const { torrent_info, meta_info } = data
const optionValue = (options: Array<string>, value: string | undefined) => {
value && !options.includes(value) && options.push(value)
}
optionValue(siteFilterOptions.value, torrent_info?.site_name)
optionValue(seasonFilterOptions.value, meta_info?.season_episode)
optionValue(releaseGroupFilterOptions.value, meta_info?.resource_team)
optionValue(videoCodeFilterOptions.value, meta_info?.video_encode)
optionValue(freeStateFilterOptions.value, torrent_info?.volume_factor)
optionValue(editionFilterOptions.value, meta_info?.edition)
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
}
// 计算过滤后的列表
watchEffect(() => {
// 清空列表
dataList.value.splice(0)
// 匹配过滤函数
const match = (filter: Array<string>, value: string | undefined) =>
filter.length === 0 || (value && filter.includes(value))
props.items?.forEach((data) => {
const { meta_info, torrent_info } = data
if (
// 站点过滤
match(filterForm.site, torrent_info.site_name)
// 促销状态过滤
&& match(filterForm.freeState, torrent_info.volume_factor)
// 季过滤
&& match(filterForm.season, meta_info.season_episode)
// 制作组过滤
&& match(filterForm.releaseGroup, meta_info.resource_team)
// 视频编码过滤
&& match(filterForm.videoCode, meta_info.video_encode)
// 分辨率过滤
&& match(filterForm.resolution, meta_info.resource_pix)
// 质量过滤
&& match(filterForm.edition, meta_info.edition)
)
dataList.value.push(data)
})
})
// 初始化过滤选项
onMounted(() => {
props.items?.forEach((item) => {
initOptions(item)
})
})
</script>
<template>
<VRow>
<VCol>
<VList
lines="three"
class="rounded"
>
<TorrentItem
v-for="(item, index) in dataList"
:key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`"
:torrent="item"
/>
<VListItem v-if="dataList.length === 0">
<VListItemTitle>没有附合当前过滤条件的资源</VListItemTitle>
</VListItem>
</VList>
</VCol>
<VCol
xl="2"
md="3"
class="d-none d-md-block"
>
<VList lines="one" class="rounded">
<VListSubheader v-if="siteFilterOptions.length > 0">
站点
</VListSubheader>
<VListItem>
<VChipGroup v-model="filterForm.site" column multiple>
<VChip
v-for="site in siteFilterOptions"
:key="site"
:color="filterForm.site.includes(site) ? 'primary' : ''"
filter
variant="outlined"
:value="site"
>
{{ site }}
</VChip>
</VChipGroup>
</VListItem>
<VListSubheader v-if="editionFilterOptions.length > 0">
质量
</VListSubheader>
<VListItem>
<VChipGroup v-model="filterForm.edition" column multiple>
<VChip
v-for="edition in editionFilterOptions"
:key="edition"
:color="filterForm.edition.includes(edition) ? 'primary' : ''"
filter
variant="outlined"
:value="edition"
>
{{ edition }}
</VChip>
</VChipGroup>
</VListItem>
<VListSubheader v-if="resolutionFilterOptions.length > 0">
分辨率
</VListSubheader>
<VListItem>
<VChipGroup v-model="filterForm.resolution" column multiple>
<VChip
v-for="resolution in resolutionFilterOptions"
:key="resolution"
:color="filterForm.resolution.includes(resolution) ? 'primary' : ''"
filter
variant="outlined"
:value="resolution"
>
{{ resolution }}
</VChip>
</VChipGroup>
</VListItem>
<VListSubheader v-if="releaseGroupFilterOptions.length > 0">
制作组
</VListSubheader>
<VListItem>
<VChipGroup v-model="filterForm.releaseGroup" column multiple>
<VChip
v-for="releaseGroup in releaseGroupFilterOptions"
:key="releaseGroup"
:color="filterForm.releaseGroup.includes(releaseGroup) ? 'primary' : ''"
filter
variant="outlined"
:value="releaseGroup"
>
{{ releaseGroup }}
</VChip>
</VChipGroup>
</VListItem>
<VListSubheader v-if="videoCodeFilterOptions.length > 0">
视频编码
</VListSubheader>
<VListItem>
<VChipGroup v-model="filterForm.videoCode" column multiple>
<VChip
v-for="videoCode in videoCodeFilterOptions"
:key="videoCode"
:color="filterForm.videoCode.includes(videoCode) ? 'primary' : ''"
filter
variant="outlined"
:value="videoCode"
>
{{ videoCode }}
</VChip>
</VChipGroup>
</VListItem>
<VListSubheader v-if="freeStateFilterOptions.length > 0">
促销状态
</VListSubheader>
<VListItem>
<VChipGroup v-model="filterForm.freeState" column multiple>
<VChip
v-for="freeState in freeStateFilterOptions"
:key="freeState"
:color="filterForm.freeState.includes(freeState) ? 'primary' : ''"
filter
variant="outlined"
:value="freeState"
>
{{ freeState }}
</VChip>
</VChipGroup>
</VListItem>
<VListSubheader v-if="seasonFilterOptions.length > 0">
季集
</VListSubheader>
<VListItem>
<VChipGroup v-model="filterForm.season" column multiple>
<VChip
v-for="season in seasonFilterOptions"
:key="season"
:color="filterForm.season.includes(season) ? 'primary' : ''"
filter
variant="outlined"
:value="season"
>
{{ season }}
</VChip>
</VChipGroup>
</VListItem>
</VList>
</VCol>
</VRow>
</template>

View File

@@ -38,7 +38,7 @@ function pluginInstalled() {
// 获取插件列表数据
async function fetchData() {
try {
dataList.value = await api.get('plugin')
dataList.value = await api.get('plugin/')
isRefreshed.value = true
}
catch (error) {

View File

@@ -4,6 +4,11 @@ import api from '@/api'
import type { DownloadingInfo } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import DownloadingCard from '@/components/cards/DownloadingCard.vue'
import store from '@/store'
// 从Vuex Store中获取用户信息
const superUser = store.state.auth.superUser
const userName = store.state.auth.userName
// 定时器
let refreshTimer: NodeJS.Timer | null = null
@@ -17,7 +22,7 @@ const isRefreshed = ref(false)
// 获取订阅列表数据
async function fetchData() {
try {
dataList.value = await api.get('download')
dataList.value = await api.get('download/')
isRefreshed.value = true
}
catch (error) {
@@ -35,6 +40,14 @@ function onRefresh() {
loading.value = false
}
// 过滤数据,管理员用户显示全部,非管理员只显示自己的订阅
const filteredDataList = computed(() => {
if (superUser)
return dataList.value
else
return dataList.value.filter(data => data.userid === userName)
})
// 加载时获取数据
onBeforeMount(() => {
fetchData()
@@ -71,17 +84,17 @@ onUnmounted(() => {
@refresh="onRefresh"
>
<div
v-if="dataList.length > 0"
v-if="filteredDataList.length > 0"
class="grid gap-3 grid-downloading-card"
>
<DownloadingCard
v-for="data in dataList"
v-for="data in filteredDataList"
:key="data.hash"
:info="data"
/>
</div>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
v-if="filteredDataList.length === 0 && isRefreshed"
error-code="404"
error-title="没有任务"
error-description="正在下载的任务将会显示在这里"

View File

@@ -1,14 +1,9 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import { numberValidator, requiredValidator } from '@/@validators'
import api from '@/api'
import type { TransferHistory } from '@/api/types'
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
// 确认框
const createConfirm = useConfirm()
import ReorganizeForm from '@/components/form/ReorganizeForm.vue'
// 提示框
const $toast = useToast()
@@ -16,21 +11,15 @@ const $toast = useToast()
// 重新整理对话框
const redoDialog = ref(false)
// TMDB编号
const redoTmdbId = ref('')
// 类型
const redoType = ref('电影')
// 类型下拉框:电影、电视剧
const redoTypeItems = ref([
{ title: '电影', value: '电影' },
{ title: '电视剧', value: '电视剧' },
])
// 当前操作记录
const currentHistory = ref<TransferHistory>()
// 重新整理IDS
const redoIds = ref<number[]>([])
// 重新整理target
const redoTarget = ref('')
// 已选中的数据
const selected = ref<TransferHistory[]>([])
@@ -72,8 +61,11 @@ const progressText = ref('请稍候 ...')
// 进度值
const progressValue = ref(0)
// TMDB选择对话框
const tmdbSelectorDialog = ref(false)
// 删除确认对话框
const deleteConfirmDialog = ref(false)
// 确认框标题
const confirmTitle = ref('')
// 获取订阅列表数据
async function fetchData({
@@ -125,78 +117,56 @@ const TransferDict: { [key: string]: string } = {
move: '移动',
link: '硬链接',
softlink: '软链接',
rclone_copy: 'Rclone复制',
rclone_move: 'Rclone移动',
}
// 删除历史记录
async function removeHistory(item: TransferHistory) {
const isConfirmed = await createConfirm({
title: '确认',
content: `同步删除 ${item.title} 对应的媒体库文件 ?`,
confirmationText: '同步删除文件',
cancellationText: '仅删除历史记录',
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
color: 'error',
},
})
if (isConfirmed === undefined)
return
// 执行删除
remove(item, isConfirmed || false)
// 清空选中项
selected.value = []
currentHistory.value = item
confirmTitle.value = `确认删除 ${item.title} ${item.seasons}${item.episodes} ?`
deleteConfirmDialog.value = true
}
// 调用API删除记录
async function remove(item: TransferHistory, deleteFile: boolean) {
async function remove(item: TransferHistory, deleteSrc: boolean, deleteDest: boolean) {
try {
// 调用删除API
const result: { [key: string]: any } = await api.delete(`history/transfer?delete_file=${deleteFile}`, {
const result: { [key: string]: any } = await api.delete(`history/transfer?deletesrc=${deleteSrc}&deletedest=${deleteDest}`, {
data: item,
})
if (result.success) {
fetchData({
page: currentPage.value,
itemsPerPage: itemsPerPage.value,
})
}
else {
if (!result.success)
$toast.error(`删除失败: ${result.msg}`)
}
}
catch (error) {
console.error(error)
}
}
// 批量删除历史记录
async function removeHistoryBatch() {
if (selected.value.length === 0)
// 删除单条记录
async function removeSingle(deleteSrc: boolean, deleteDest: boolean) {
// 关闭弹窗
deleteConfirmDialog.value = false
if (!currentHistory.value)
return
// 确认
const isConfirmed = await createConfirm({
title: '确认',
content: `同步删除 ${selected.value.length} 条记录对应的媒体库文件 ?`,
confirmationText: '同步删除文件',
cancellationText: '仅删除历史记录',
dialogProps: {
maxWidth: '50rem',
},
confirmationButtonProps: {
color: 'error',
},
// 删除
await remove(currentHistory.value, deleteSrc, deleteDest)
// 刷新
fetchData({
page: currentPage.value,
itemsPerPage: itemsPerPage.value,
})
if (isConfirmed === undefined)
return
console.log(selected.value)
}
// 批量删除记录
async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
// 关闭弹窗
deleteConfirmDialog.value = false
// 总条数
const total = selected.value.length
if (total === 0)
return
// 已处理条数
let handled = 0
// 显示进度条
@@ -205,7 +175,7 @@ async function removeHistoryBatch() {
for (const item of selected.value) {
// 开始删除
progressText.value = `正在删除 ${item.title} ${item.seasons}${item.episodes} ...`
await remove(item, isConfirmed || false)
await remove(item, deleteSrc, deleteDest)
// 删除完成
handled++
progressValue.value = handled / total * 100
@@ -221,44 +191,40 @@ async function removeHistoryBatch() {
})
}
// 重新整理
async function rehandleHistory() {
try {
if (!redoTmdbId.value || !redoType.value)
return
// 响应删除操作
async function deleteConfirmHandler(deleteSrc: boolean, deleteDest: boolean) {
if (currentHistory.value)
await removeSingle(deleteSrc, deleteDest)
else
await removeBatch(deleteSrc, deleteDest)
}
redoDialog.value = false
$toast.info(`正在重新整理 ${currentHistory.value?.title} ...`)
// 批量删除历史记录
async function removeHistoryBatch() {
if (selected.value.length === 0)
return
// 清空当前操作记录
currentHistory.value = undefined
confirmTitle.value = `确认删除 ${selected.value.length} 条记录 ?`
// 打开确认弹窗
deleteConfirmDialog.value = true
}
// 调用API接口重新转移
const requestData = {
...currentHistory.value,
}
const result: { [key: string]: any } = await api.post(
'history/transfer',
requestData,
{
params: {
mtype: redoType.value,
new_tmdbid: parseInt(redoTmdbId.value),
},
},
)
if (result.success) {
fetchData({
page: currentPage.value,
itemsPerPage: itemsPerPage.value,
})
}
else {
$toast.error(`重新整理失败: ${result.message}`)
}
}
catch (e) {
console.log(e)
}
// 批量重新整理
async function retransferBatch() {
if (selected.value.length === 0)
return
// 清空当前操作记录
currentHistory.value = undefined
// 重新整理IDS
redoIds.value = selected.value.map(item => item.id)
// 重新整理target
if (selected.value.length === 1)
redoTarget.value = selected.value[0].dest ?? ''
else
redoTarget.value = ''
// 打开识别弹窗
redoDialog.value = true
}
// 弹出菜单
@@ -269,8 +235,9 @@ const dropdownItems = ref([
props: {
prependIcon: 'mdi-redo-variant',
click: (item: TransferHistory) => {
redoIds.value = [item.id]
redoTarget.value = item.dest ?? ''
redoDialog.value = true
currentHistory.value = item
},
},
},
@@ -280,7 +247,9 @@ const dropdownItems = ref([
props: {
prependIcon: 'mdi-trash-can-outline',
color: 'error',
click: removeHistory,
click: (item: TransferHistory) => {
removeHistory(item)
},
},
},
])
@@ -395,46 +364,18 @@ const dropdownItems = ref([
</template>
</VDataTableServer>
</VCard>
<VDialog
v-model="redoDialog"
max-width="50rem"
<!-- 底部操作按钮 -->
<span
v-if="selected.length > 0"
class="fixed right-5 bottom-5"
>
<!-- Dialog Content -->
<VCard title="重新整理">
<VCardText>
<VRow>
<VCol cols="12" md="4">
<VSelect
v-model="redoType"
label="类型"
:rules="[requiredValidator]"
:items="redoTypeItems"
/>
</VCol>
<VCol cols="12" md="8">
<VTextField
v-model="redoTmdbId"
label="TMDB编号"
:rules="[requiredValidator, numberValidator]"
append-inner-icon="mdi-magnify"
@click:append-inner="tmdbSelectorDialog = true"
/>
</VCol>
</VRow>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
@click="rehandleHistory"
@keydown.enter="rehandleHistory"
>
确定
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<span v-if="selected.length > 0" class="fixed right-5 bottom-5">
<VBtn
icon="mdi-redo-variant"
class="me-2"
color="primary"
size="x-large"
@click="retransferBatch"
/>
<VBtn
icon="mdi-trash-can-outline"
color="error"
@@ -442,36 +383,63 @@ const dropdownItems = ref([
@click="removeHistoryBatch"
/>
</span>
<!-- 进度框 -->
<vDialog
v-model="progressDialog"
:scrim="false"
width="25rem"
>
<vCard
color="primary"
>
<vCardText class="text-center">
{{ progressText }}
<vProgressLinear
color="white"
class="mb-0 mt-1"
:model-value="progressValue"
/>
</vCardText>
</vCard>
</vDialog>
<!-- TMDB ID搜索框 -->
<vDialog
v-model="tmdbSelectorDialog"
width="600"
scrollable
>
<TmdbSelectorCard
v-model="redoTmdbId"
@close="tmdbSelectorDialog = false"
/>
</vDialog>
<!-- 底部弹窗 -->
<VBottomSheet v-model="deleteConfirmDialog" inset>
<VCard class="text-center rounded-t">
<DialogCloseBtn @click="deleteConfirmDialog = false" />
<VCardTitle class="pe-10">
{{ confirmTitle }}
</VCardTitle>
<div class="d-flex flex-column flex-lg-row justify-center my-3">
<VBtn
color="primary"
class="mb-2 mx-2"
@click="deleteConfirmHandler(false, false)"
>
仅删除历史记录
</VBtn>
<VBtn
color="warning"
class="mb-2 mx-2"
@click="deleteConfirmHandler(true, false)"
>
删除历史记录和源文件
</VBtn>
<VBtn
color="info"
class="mb-2 mx-2"
@click="deleteConfirmHandler(false, true)"
>
删除历史记录和媒体库文件
</VBtn>
<VBtn
color="error"
class="mb-2 mx-2"
@click="deleteConfirmHandler(true, true)"
>
删除历史记录源文件和媒体库文件
</VBtn>
</div>
</VCard>
</VBottomSheet>
<!-- 文件整理弹窗 -->
<ReorganizeForm
v-model="redoDialog"
:logids="redoIds"
:target="redoTarget"
@done="() => {
redoDialog = false
// 清空当前操作记录
currentHistory = undefined
selected = []
// 刷新
fetchData({
page: currentPage,
itemsPerPage,
})
}"
@close="redoDialog = false"
/>
</template>
<style lang="scss">

View File

@@ -86,7 +86,7 @@ async function saveAccountInfo() {
accountInfo.value.password = newPassword.value
}
try {
const result: { [key: string]: any } = await api.put('user', accountInfo.value)
const result: { [key: string]: any } = await api.put('user/', accountInfo.value)
if (result.success)
$toast.success('用户信息保存成功!')
else
@@ -100,7 +100,7 @@ async function saveAccountInfo() {
// 调用API查询所有用户
async function loadAllUsers() {
try {
const result: User[] = await api.get('/user')
const result: User[] = await api.get('/user/')
allUsers.value = result
}
@@ -131,7 +131,7 @@ async function deactivateUser(user: User) {
try {
user.is_active = !user.is_active
const result: { [key: string]: any } = await api.put('user', user)
const result: { [key: string]: any } = await api.put('user/', user)
if (result.success) {
$toast.success('用户冻结成功!')
loadAllUsers()

View File

@@ -47,7 +47,7 @@ onMounted(() => {
<template>
<VCard title="消息通知">
<VCardText> 对应消息类型只会发送给选中的消息渠道 </VCardText>
<VCardText> 对应消息类型只会发送给选中的消息渠道 </VCardText>
<VTable class="text-no-wrap">
<thead>
@@ -64,6 +64,9 @@ onMounted(() => {
<th scope="col">
Slack
</th>
<th scope="col">
SynologyChat
</th>
</tr>
</thead>
<tbody>
@@ -83,6 +86,9 @@ onMounted(() => {
<td>
<VCheckbox v-model="message.slack" />
</td>
<td>
<VCheckbox v-model="message.synologychat" />
</td>
</tr>
<tr v-if="messagemTypes.length === 0">
<td

View File

@@ -1,267 +0,0 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
// 规则卡片类型
interface FilterCard {
// 优先级
pri: string
// 已选规则
rules: string[]
}
// 提示框
const $toast = useToast()
// 种子优先规则
const selectedTorrentPriority = ref<string>('seeder')
// 种子优先规则下拉框
const TorrentPriorityItems = [
{ title: '站点优先', value: 'site' },
{ title: '做种数优先', value: 'seeder' },
]
// 规则卡片列表
const filterCards = ref<FilterCard[]>([])
// 洗版规则卡片列表
const filterCards2 = ref<FilterCard[]>([])
// 查询已设置过滤规则
async function queryCustomFilters(ruleType: string) {
try {
const result: { [key: string]: any } = await api.get(`system/setting/${ruleType}`)
if (result.success) {
// 保存的是个字符串,需要分割成数组
const groups = result.data?.value?.split('>') ?? []
// 生成规则卡片
const cards = ruleType === 'FilterRules' ? filterCards : filterCards2
cards.value = groups?.map((group: string, index: number) => {
return {
pri: (index + 1).toString(),
rules: group.split('&'),
}
})
}
}
catch (error) {
console.log(error)
}
}
// 查询种子优先规则
async function queryTorrentPriority() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/TorrentsPriority',
)
selectedTorrentPriority.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 保存用户设置的规则
async function saveCustomFilters(ruleType: string) {
try {
// 有值才处理
let value = ''
if (filterCards.value.length !== 0) {
// 将卡片规则接装为字符串
const cards = ruleType === 'FilterRules' ? filterCards : filterCards2
value = cards.value
.filter(card => card.rules.length > 0)
.map(card => card.rules.join('&'))
.join('>')
}
// 保存
const result: { [key: string]: any } = await api.post(
`system/setting/${ruleType}`,
value,
)
const msg = ruleType === 'FilterRules' ? '过滤规则' : '洗版规则'
if (result.success)
$toast.success(`${msg}保存成功`)
else
$toast.error(`${msg}保存失败!`)
}
catch (error) {
console.log(error)
}
}
// 保存种子优先规则
async function saveTorrentPriority() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post(
'system/setting/TorrentsPriority',
selectedTorrentPriority.value,
)
if (result.success)
$toast.success('优先规则保存成功')
else
$toast.error('优先规则保存失败!')
}
catch (error) {
console.log(error)
}
}
// 更新规则卡片的值
function updateFilterCardValue(pri: string, rules: string[]) {
const card = filterCards.value.find(card => card.pri === pri)
if (card)
card.rules = rules
}
// 更新洗版规则卡片的值
function updateFilterCardValue2(pri: string, rules: string[]) {
const card = filterCards2.value.find(card => card.pri === pri)
if (card)
card.rules = rules
}
// 移除卡片
function filterCardClose(ruleType: string, pri: string) {
// 将pri对应的卡片从列表中删除并更新剩余卡片的序号
const updatedCards = (ruleType === 'FilterRules' ? filterCards.value : filterCards2.value)
.filter(card => card.pri !== pri)
.map((card, index) => {
card.pri = (index + 1).toString()
return card
})
// 更新 filterCards.value
if (ruleType === 'FilterRules')
filterCards.value = updatedCards
else
filterCards2.value = updatedCards
}
// 增加卡片
function addFilterCard(ruleType: string) {
const cards = ruleType === 'FilterRules' ? filterCards : filterCards2
// 优先级
const pri = (cards.value.length + 1).toString()
// 新卡片
const newCard: FilterCard = { pri, rules: [] }
// 添加到列表
cards.value.push(newCard)
}
onMounted(() => {
queryTorrentPriority()
queryCustomFilters('FilterRules')
queryCustomFilters('FilterRules2')
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="过滤规则">
<VCardSubtitle> 设置在搜索和订阅时默认使用的过滤规则 </VCardSubtitle>
<VCardItem>
<div class="grid gap-3 grid-filterrule-card">
<FilterRuleCard
v-for="(card, index) in filterCards"
:key="index"
:pri="card.pri"
:rules="card.rules"
@changed="updateFilterCardValue"
@close="filterCardClose('FilterRules', card.pri)"
/>
</div>
</VCardItem>
<VCardItem>
<VBtn
type="submit"
class="me-2"
@click="saveCustomFilters('FilterRules')"
>
保存
</VBtn>
<VBtn
color="success"
variant="tonal"
@click="addFilterCard('FilterRules')"
>
<VIcon icon="mdi-plus" />
<span class="d-none d-sm-block">增加规则</span>
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="洗版规则">
<VCardSubtitle> 设置在订阅洗版时使用的过滤规则匹配优先级1时洗版完成 </VCardSubtitle>
<VCardItem>
<div class="grid gap-3 grid-filterrule-card">
<FilterRuleCard
v-for="(card, index) in filterCards2"
:key="index"
:pri="card.pri"
:rules="card.rules"
@changed="updateFilterCardValue2"
@close="filterCardClose('FilterRules2', card.pri)"
/>
</div>
</VCardItem>
<VCardItem>
<VBtn
type="submit"
class="me-2"
@click="saveCustomFilters('FilterRules2')"
>
保存
</VBtn>
<VBtn
color="success"
variant="tonal"
@click="addFilterCard('FilterRules2')"
>
<VIcon icon="mdi-plus" />
<span class="d-none d-sm-block">增加规则</span>
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="下载优先规则">
<VCardText>
<VSelect
v-model="selectedTorrentPriority"
:items="TorrentPriorityItems"
label="优先规则"
outlined
/>
</VCardText>
<VCardItem>
<VBtn
type="submit"
@click="saveTorrentPriority"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
</VRow>
</template>
<style lang="scss">
.grid-filterrule-card {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
padding-block-end: 1rem;
}
</style>

View File

@@ -0,0 +1,342 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
import type { Site } from '@/api/types'
// 规则卡片类型
interface FilterCard {
// 优先级
pri: string
// 已选规则
rules: string[]
}
// 提示框
const $toast = useToast()
// 规则卡片列表
const filterCards = ref<FilterCard[]>([])
// 所有站点
const allSites = ref<Site[]>([])
// 选中订阅站点
const selectedSites = ref<number[]>([])
// 包含与排除规则
const defaultFilterRules = ref({
include: '',
exclude: '',
})
// 查询已设置优先级规则
async function queryCustomFilters() {
try {
const result: { [key: string]: any } = await api.get('system/setting/SearchFilterRules')
if (result.success) {
// 保存的是个字符串,需要分割成数组
const groups = result.data?.value?.split('>') ?? []
// 生成规则卡片
filterCards.value = groups?.map((group: string, index: number) => {
return {
pri: (index + 1).toString(),
rules: group.split('&'),
}
})
}
}
catch (error) {
console.log(error)
}
}
// 保存用户设置的规则
async function saveCustomFilters() {
try {
// 有值才处理
let value = ''
if (filterCards.value.length !== 0) {
// 将卡片规则接装为字符串
value = filterCards.value
.filter(card => card.rules.length > 0)
.map(card => card.rules.join('&'))
.join('>')
}
// 保存
const result: { [key: string]: any } = await api.post(
'system/setting/SearchFilterRules',
value,
)
if (result.success)
$toast.success('搜索优先级保存成功')
else
$toast.error('搜索优先级保存失败!')
}
catch (error) {
console.log(error)
}
}
// 更新规则卡片的值
function updateFilterCardValue(pri: string, rules: string[]) {
const card = filterCards.value.find(card => card.pri === pri)
if (card)
card.rules = rules
}
// 移除卡片
function filterCardClose(pri: string) {
// 将pri对应的卡片从列表中删除并更新剩余卡片的序号
const updatedCards = filterCards.value
.filter(card => card.pri !== pri)
.map((card, index) => {
card.pri = (index + 1).toString()
return card
})
// 更新 filterCards.value
filterCards.value = updatedCards
}
// 增加卡片
function addFilterCard() {
// 优先级
const pri = (filterCards.value.length + 1).toString()
// 新卡片
const newCard: FilterCard = { pri, rules: [] }
// 添加到列表
filterCards.value.push(newCard)
}
// 查询所有站点
async function querySites() {
try {
const data: Site[] = await api.get('site/')
// 过滤站点,只有启用的站点才显示
allSites.value = data.filter(item => item.is_active)
querySelectedSites()
}
catch (error) {
console.log(error)
}
}
// 查询用户选中的站点
async function querySelectedSites() {
try {
const result: { [key: string]: any } = await api.get('system/setting/IndexerSites')
selectedSites.value = result.data?.value ?? []
}
catch (error) {
console.log(error)
}
}
// 保存用户选中的站点
async function saveSelectedSites() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post('system/setting/IndexerSites', selectedSites.value)
if (result.success)
$toast.success('搜索站点保存成功')
else
$toast.error('搜索站点保存失败!')
}
catch (error) {
console.log(error)
}
}
// 上调优先级
function onLevelUp(pri: string) {
// 找到当前卡片
const card = filterCards.value.find(card => card.pri === pri)
if (!card)
return
// 找到当前卡片的上一张卡片
const prevCard = filterCards.value.find(card => card.pri === (parseInt(pri) - 1).toString())
if (!prevCard)
return
// 交换两张卡片的优先级
const temp = card.pri
card.pri = prevCard.pri
prevCard.pri = temp
// 卡片重新按优先级排序
filterCards.value.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
}
// 下调优先级
function onLevelDown(pri: string) {
// 找到当前卡片
const card = filterCards.value.find(card => card.pri === pri)
if (!card)
return
// 找到当前卡片的下一张卡片
const nextCard = filterCards.value.find(card => card.pri === (parseInt(pri) + 1).toString())
if (!nextCard)
return
// 交换两张卡片的优先级
const temp = card.pri
card.pri = nextCard.pri
nextCard.pri = temp
// 卡片重新按优先级排序
filterCards.value.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
}
// 查询包含与排除规则
async function queryDefaultFilter() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/DefaultSearchFilterRules',
)
if (result.data?.value)
defaultFilterRules.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 保存包含与排除规则
async function saveDefaultFilter() {
try {
const result: { [key: string]: any } = await api.post(
'system/setting/DefaultSearchFilterRules',
defaultFilterRules.value,
)
if (result.success)
$toast.success('默认包含/排除规则保存成功')
else
$toast.error('默认包含/排除规则保存失败!')
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
queryCustomFilters()
querySites()
queryDefaultFilter()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="搜索站点">
<VCardSubtitle> 只有选中的站点才会在搜索中使用</VCardSubtitle>
<VCardItem>
<VChipGroup v-model="selectedSites" column multiple>
<VChip
v-for="site in allSites"
:key="site.id"
:color="selectedSites.includes(site.id) ? 'primary' : ''"
filter
variant="outlined"
:value="site.id"
>
{{ site.name }}
</VChip>
</VChipGroup>
</VCardItem>
<VCardItem>
<VBtn type="submit" @click="saveSelectedSites">
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="搜索优先级">
<VCardSubtitle> 设置在搜索时默认使用的优先级排序未在优先级中的资源将不在搜索结果中显示 </VCardSubtitle>
<VCardItem>
<div class="grid gap-3 grid-filterrule-card">
<FilterRuleCard
v-for="(card, index) in filterCards"
:key="index"
:pri="card.pri"
:maxpri="filterCards.length.toString()"
:rules="card.rules"
@changed="updateFilterCardValue"
@close="filterCardClose(card.pri)"
@leveldown="onLevelDown"
@levelup="onLevelUp"
/>
</div>
</VCardItem>
<VCardItem>
<VBtn
type="submit"
class="me-2"
@click="saveCustomFilters()"
>
保存
</VBtn>
<VBtn
color="success"
variant="tonal"
@click="addFilterCard()"
>
<VIcon icon="mdi-plus" />
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="默认过滤规则">
<VCardSubtitle> 设置在搜索时默认使用的过滤规则 </VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.include"
type="text"
label="包含(关键字、正则式)"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.exclude"
type="text"
label="排除(关键字、正则式)"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardItem>
<VBtn
type="submit"
@click="saveDefaultFilter"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
</VRow>
</template>
<style lang="scss">
.grid-filterrule-card {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
padding-block-end: 1rem;
}
</style>

View File

@@ -0,0 +1,138 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { ScheduleInfo } from '@/api/types'
// 提示框
const $toast = useToast()
// 定时服务列表
const schedulerList = ref<ScheduleInfo[]>([])
// 定时器
let refreshTimer: NodeJS.Timer | null = null
// 调用API加载定时服务列表
async function loadSchedulerList() {
try {
const res: ScheduleInfo[] = await api.get('dashboard/schedule')
schedulerList.value = res
}
catch (e) {
console.log(e)
}
}
// 任务状态颜色
function getSchedulerColor(status: string) {
switch (status) {
case '正在运行':
return 'success'
case '已停止':
return 'error'
case '等待':
return ''
default:
return ''
}
}
// 执行命令
function runCommand(id: string) {
try {
// 异步提交
api.get('system/runscheduler', {
params: {
jobid: id,
},
})
$toast.success('定时作业执行请求提交成功!')
// 1秒后刷新数据
setTimeout(() => {
loadSchedulerList()
}, 1000)
}
catch (e) {
console.log(e)
}
}
onMounted(() => {
loadSchedulerList()
// 启动定时器
refreshTimer = setInterval(() => {
loadSchedulerList()
}, 5000)
})
// 组件卸载时停止定时器
onUnmounted(() => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
})
</script>
<template>
<VCard title="定时作业">
<VCardText> 手动执行不会影响作业正常的时间表 </VCardText>
<VTable class="text-no-wrap">
<thead>
<tr>
<th scope="col">
任务名称
</th>
<th scope="col">
任务状态
</th>
<th scope="col">
下一次执行时间
</th>
<th scope="col" />
</tr>
</thead>
<tbody>
<tr
v-for="scheduler in schedulerList"
:key="scheduler.id"
>
<td>
{{ scheduler.name }}
</td>
<td>
<VChip :color="getSchedulerColor(scheduler.status)">
{{ scheduler.status }}
</VChip>
</td>
<td>
{{ scheduler.next_run }}
</td>
<td>
<VBtn
size="small"
:disabled="scheduler.status === '正在运行'"
@click="runCommand(scheduler.id)"
>
<template #prepend>
<VIcon>mdi-play</VIcon>
</template>
执行
</VBtn>
</td>
</tr>
<tr v-if="schedulerList.length === 0">
<td
colspan="4"
class="text-center"
>
没有后台服务
</td>
</tr>
</tbody>
</VTable>
</VCard>
</template>

View File

@@ -1,19 +1,10 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { Site } from '@/api/types'
// 提示框
const $toast = useToast()
// 选中搜索站点
const selectedSites = ref<number[]>([])
// 选中订阅站点
const selectedRssSites = ref<number[]>([])
// 所有站点
const allSites = ref<Site[]>([])
// 站点重置
const isConfirmResetSites = ref(false)
@@ -23,75 +14,14 @@ const resetSitesText = ref('重置站点数据')
// 站点重置按钮可用状态
const resetSitesDisabled = ref(false)
// 查询所有站点
async function querySites() {
try {
const data: Site[] = await api.get('site')
// 种子优先规则
const selectedTorrentPriority = ref<string>('seeder')
// 过滤站点,只有启用的站点才显示
allSites.value = data.filter(item => item.is_active)
querySelectedSites()
querySelectedRssSites()
}
catch (error) {
console.log(error)
}
}
// 查询用户选中的站点
async function querySelectedSites() {
try {
const result: { [key: string]: any } = await api.get('system/setting/IndexerSites')
selectedSites.value = result.data?.value ?? []
}
catch (error) {
console.log(error)
}
}
// 保存用户选中的站点
async function saveSelectedSites() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post('system/setting/IndexerSites', selectedSites.value)
if (result.success)
$toast.success('搜索站点保存成功')
else
$toast.error('搜索站点保存失败!')
}
catch (error) {
console.log(error)
}
}
// 查询用户选中的订阅站点
async function querySelectedRssSites() {
try {
const result: { [key: string]: any } = await api.get('system/setting/RssSites')
selectedRssSites.value = result.data?.value ?? []
}
catch (error) {
console.log(error)
}
}
// 保存用户选中的订阅站点
async function saveSelectedRssSites() {
try {
const result: { [key: string]: any } = await api.post('system/setting/RssSites', selectedRssSites.value)
if (result.success)
$toast.success('订阅站点保存成功')
else
$toast.error('订阅站点保存失败!')
}
catch (error) {
console.log(error)
}
}
// 种子优先规则下拉框
const TorrentPriorityItems = [
{ title: '站点优先', value: 'site' },
{ title: '做种数优先', value: 'seeder' },
]
// 重置站点
async function resetSites() {
@@ -100,13 +30,12 @@ async function resetSites() {
resetSitesText.value = '正在重置...'
const result: { [key: string]: any } = await api.get('site/reset')
if (result.success) {
if (result.success)
$toast.success('站点重置成功请等待CookieCloud同步完成')
querySites()
}
else {
else
$toast.error('站点重置失败!')
}
resetSitesDisabled.value = false
resetSitesText.value = '重置站点数据'
}
@@ -115,60 +44,68 @@ async function resetSites() {
}
}
// 查询种子优先规则
async function queryTorrentPriority() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/TorrentsPriority',
)
selectedTorrentPriority.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 保存种子优先规则
async function saveTorrentPriority() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post(
'system/setting/TorrentsPriority',
selectedTorrentPriority.value,
)
if (result.success)
$toast.success('优先规则保存成功')
else
$toast.error('优先规则保存失败!')
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
querySites()
queryTorrentPriority()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="搜索站点">
<VCardSubtitle> 只有选中的站点才会在搜索中使用</VCardSubtitle>
<VCard title="下载优先规则">
<VCardSubtitle> 按站点或做种数量优先下载 </VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VSelect
v-model="selectedTorrentPriority"
:items="TorrentPriorityItems"
label="优先规则"
outlined
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardItem>
<VChipGroup v-model="selectedSites" column multiple>
<VChip
v-for="site in allSites"
:key="site.id"
:color="selectedSites.includes(site.id) ? 'primary' : ''"
filter
variant="outlined"
:value="site.id"
>
{{ site.name }}
</VChip>
</VChipGroup>
</VCardItem>
<VCardItem>
<VBtn type="submit" @click="saveSelectedSites">
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="订阅站点">
<VCardSubtitle> 只有选中的站点才会在订阅中使用</VCardSubtitle>
<VCardItem>
<VChipGroup v-model="selectedRssSites" column multiple>
<VChip
v-for="site in allSites"
:key="site.id"
:color="selectedRssSites.includes(site.id) ? 'primary' : ''"
filter
variant="outlined"
:value="site.id"
>
{{ site.name }}
</VChip>
</VChipGroup>
</VCardItem>
<VCardItem>
<VBtn type="submit" @click="saveSelectedRssSites">
<VBtn
type="submit"
@click="saveTorrentPriority"
>
保存
</VBtn>
</VCardItem>
@@ -178,7 +115,7 @@ onMounted(() => {
<VCard title="站点重置">
<VCardText>
<div>
<VCheckbox v-model="isConfirmResetSites" label="确认删除所有站点数据并重新同步" />
<VCheckbox v-model="isConfirmResetSites" label="确认删除所有站点数据并重新同步" />
</div>
<VBtn :disabled="!isConfirmResetSites || resetSitesDisabled" color="error" class="mt-3" @click="resetSites">

View File

@@ -0,0 +1,396 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import FilterRuleCard from '@/components/cards/FilterRuleCard.vue'
import type { Site } from '@/api/types'
// 规则卡片类型
interface FilterCard {
// 优先级
pri: string
// 已选规则
rules: string[]
}
// 提示框
const $toast = useToast()
// 订阅规则卡片列表
const subscribeFilterCards = ref<FilterCard[]>([])
// 洗版规则卡片列表
const bestVersionFilterCards = ref<FilterCard[]>([])
// 所有站点
const allSites = ref<Site[]>([])
// 选中订阅站点
const selectedRssSites = ref<number[]>([])
// 包含与排除规则
const defaultFilterRules = ref({
include: '',
exclude: '',
})
// 查询用户选中的订阅站点
async function querySelectedRssSites() {
try {
const result: { [key: string]: any } = await api.get('system/setting/RssSites')
selectedRssSites.value = result.data?.value ?? []
}
catch (error) {
console.log(error)
}
}
// 保存用户选中的订阅站点
async function saveSelectedRssSites() {
try {
const result: { [key: string]: any } = await api.post('system/setting/RssSites', selectedRssSites.value)
if (result.success)
$toast.success('订阅站点保存成功')
else
$toast.error('订阅站点保存失败!')
}
catch (error) {
console.log(error)
}
}
// 查询所有站点
async function querySites() {
try {
const data: Site[] = await api.get('site/')
// 过滤站点,只有启用的站点才显示
allSites.value = data.filter(item => item.is_active)
querySelectedRssSites()
}
catch (error) {
console.log(error)
}
}
// 查询已设置优先级规则
async function queryCustomFilters(ruleType: string) {
try {
const result: { [key: string]: any } = await api.get(`system/setting/${ruleType}`)
if (result.success) {
// 保存的是个字符串,需要分割成数组
const groups = result.data?.value?.split('>') ?? []
// 生成规则卡片
const cards = ruleType === 'SubscribeFilterRules' ? subscribeFilterCards : bestVersionFilterCards
cards.value = groups?.map((group: string, index: number) => {
return {
pri: (index + 1).toString(),
rules: group.split('&'),
}
})
}
}
catch (error) {
console.log(error)
}
}
// 保存用户设置的规则
async function saveCustomFilters(ruleType: string) {
try {
// 有值才处理
let value = ''
const cards = ruleType === 'SubscribeFilterRules' ? subscribeFilterCards : bestVersionFilterCards
if (cards.value.length !== 0) {
// 将卡片规则接装为字符串
value = cards.value
.filter(card => card.rules.length > 0)
.map(card => card.rules.join('&'))
.join('>')
}
// 保存
const result: { [key: string]: any } = await api.post(
`system/setting/${ruleType}`,
value,
)
const msg = ruleType === 'SubscribeFilterRules' ? '订阅优先级' : '洗版优先级'
if (result.success)
$toast.success(`${msg}保存成功`)
else
$toast.error(`${msg}保存失败!`)
}
catch (error) {
console.log(error)
}
}
// 更新规则卡片的值
function updateFilterCardValue(pri: string, rules: string[]) {
const card = subscribeFilterCards.value.find(card => card.pri === pri)
if (card)
card.rules = rules
}
// 更新洗版规则卡片的值
function updateFilterCardValue2(pri: string, rules: string[]) {
const card = bestVersionFilterCards.value.find(card => card.pri === pri)
if (card)
card.rules = rules
}
// 移除卡片
function filterCardClose(ruleType: string, pri: string) {
// 将pri对应的卡片从列表中删除并更新剩余卡片的序号
const updatedCards = (ruleType === 'SubscribeFilterRules' ? subscribeFilterCards.value : bestVersionFilterCards.value)
.filter(card => card.pri !== pri)
.map((card, index) => {
card.pri = (index + 1).toString()
return card
})
// 更新 subscribeFilterCards.value
if (ruleType === 'SubscribeFilterRules')
subscribeFilterCards.value = updatedCards
else
bestVersionFilterCards.value = updatedCards
}
// 增加卡片
function addFilterCard(ruleType: string) {
const cards = ruleType === 'SubscribeFilterRules' ? subscribeFilterCards : bestVersionFilterCards
// 优先级
const pri = (cards.value.length + 1).toString()
// 新卡片
const newCard: FilterCard = { pri, rules: [] }
// 添加到列表
cards.value.push(newCard)
}
// 上调优先级
function onLevelUp(filterCards: FilterCard[], pri: string) {
// 找到当前卡片
const card = filterCards.find(card => card.pri === pri)
if (!card)
return
// 找到当前卡片的上一张卡片
const prevCard = filterCards.find(card => card.pri === (parseInt(pri) - 1).toString())
if (!prevCard)
return
// 交换两张卡片的优先级
const temp = card.pri
card.pri = prevCard.pri
prevCard.pri = temp
// 卡片重新按优先级排序
filterCards.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
}
// 下调优先级
function onLevelDown(filterCards: FilterCard[], pri: string) {
// 找到当前卡片
const card = filterCards.find(card => card.pri === pri)
if (!card)
return
// 找到当前卡片的下一张卡片
const nextCard = filterCards.find(card => card.pri === (parseInt(pri) + 1).toString())
if (!nextCard)
return
// 交换两张卡片的优先级
const temp = card.pri
card.pri = nextCard.pri
nextCard.pri = temp
// 卡片重新按优先级排序
filterCards.sort((a, b) => parseInt(a.pri) - parseInt(b.pri))
}
// 查询包含与排除规则
async function queryDefaultFilter() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/DefaultFilterRules',
)
if (result.data?.value)
defaultFilterRules.value = result.data?.value
}
catch (error) {
console.log(error)
}
}
// 保存包含与排除规则
async function saveDefaultFilter() {
try {
const result: { [key: string]: any } = await api.post(
'system/setting/DefaultFilterRules',
defaultFilterRules.value,
)
if (result.success)
$toast.success('默认包含/排除规则保存成功')
else
$toast.error('默认包含/排除规则保存失败!')
}
catch (error) {
console.log(error)
}
}
onMounted(() => {
querySites()
queryCustomFilters('SubscribeFilterRules')
queryCustomFilters('BestVersionFilterRules')
queryDefaultFilter()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard title="订阅站点">
<VCardSubtitle> 只有选中的站点才会在订阅中使用</VCardSubtitle>
<VCardItem>
<VChipGroup v-model="selectedRssSites" column multiple>
<VChip
v-for="site in allSites"
:key="site.id"
:color="selectedRssSites.includes(site.id) ? 'primary' : ''"
filter
variant="outlined"
:value="site.id"
>
{{ site.name }}
</VChip>
</VChipGroup>
</VCardItem>
<VCardItem>
<VBtn type="submit" @click="saveSelectedRssSites">
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="订阅优先级">
<VCardSubtitle> 设置在正常订阅时默认使用的优先级未在优先级中的资源将不会自动下载 </VCardSubtitle>
<VCardItem>
<div class="grid gap-3 grid-filterrule-card">
<FilterRuleCard
v-for="(card, index) in subscribeFilterCards"
:key="index"
:pri="card.pri"
:maxpri="subscribeFilterCards.length.toString()"
:rules="card.rules"
@changed="updateFilterCardValue"
@close="filterCardClose('SubscribeFilterRules', card.pri)"
@leveldown="onLevelDown(subscribeFilterCards, card.pri)"
@levelup="onLevelUp(subscribeFilterCards, card.pri)"
/>
</div>
</VCardItem>
<VCardItem>
<VBtn
type="submit"
class="me-2"
@click="saveCustomFilters('SubscribeFilterRules')"
>
保存
</VBtn>
<VBtn
color="success"
variant="tonal"
@click="addFilterCard('SubscribeFilterRules')"
>
<VIcon icon="mdi-plus" />
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="洗版优先级">
<VCardSubtitle> 设置在订阅洗版时使用的优先级匹配优先级1时洗版完成 </VCardSubtitle>
<VCardItem>
<div class="grid gap-3 grid-filterrule-card">
<FilterRuleCard
v-for="(card, index) in bestVersionFilterCards"
:key="index"
:pri="card.pri"
:maxpri="bestVersionFilterCards.length.toString()"
:rules="card.rules"
@changed="updateFilterCardValue2"
@close="filterCardClose('BestVersionFilterRules', card.pri)"
@leveldown="onLevelDown(bestVersionFilterCards, card.pri)"
@levelup="onLevelUp(bestVersionFilterCards, card.pri)"
/>
</div>
</VCardItem>
<VCardItem>
<VBtn
type="submit"
class="me-2"
@click="saveCustomFilters('BestVersionFilterRules')"
>
保存
</VBtn>
<VBtn
color="success"
variant="tonal"
@click="addFilterCard('BestVersionFilterRules')"
>
<VIcon icon="mdi-plus" />
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="默认过滤规则">
<VCardSubtitle> 设置在订阅时默认使用的过滤规则 </VCardSubtitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.include"
type="text"
label="包含(关键字、正则式)"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="defaultFilterRules.exclude"
type="text"
label="排除(关键字、正则式)"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardItem>
<VBtn
type="submit"
@click="saveDefaultFilter"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
</VRow>
</template>
<style lang="scss">
.grid-filterrule-card {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
padding-block-end: 1rem;
}
</style>

View File

@@ -11,6 +11,9 @@ const customIdentifiers = ref('')
// 自定义制作组
const customReleaseGroups = ref('')
// 自定义占位符
const customization = ref('')
// 文件整理屏蔽词
const transferExcludeWords = ref('')
@@ -42,6 +45,20 @@ async function queryCustomReleaseGroups() {
}
}
// 查询已设置的自定义占位符
async function queryCustomization() {
try {
const result: { [key: string]: any } = await api.get(
'system/setting/Customization',
)
customization.value = result.data?.value.join('\n')
}
catch (error) {
console.log(error)
}
}
// 查询已设置的屏蔽词
async function queryTransferExcludeWords() {
try {
@@ -94,6 +111,25 @@ async function saveCustomReleaseGroups() {
}
}
// 保存自定义占位符
async function saveCustomization() {
try {
// 用户名密码
const result: { [key: string]: any } = await api.post(
'system/setting/Customization',
customization.value.split('\n'),
)
if (result.success)
$toast.success('自定义占位符保存成功')
else
$toast.error('自定义占位符保存失败!')
}
catch (error) {
console.log(error)
}
}
// 保存文件整理屏蔽词
async function saveTransferExcludeWords() {
try {
@@ -116,6 +152,7 @@ async function saveTransferExcludeWords() {
onMounted(() => {
queryCustomIdentifiers()
queryCustomReleaseGroups()
queryCustomization()
queryTransferExcludeWords()
})
</script>
@@ -124,7 +161,7 @@ onMounted(() => {
<VRow>
<VCol cols="12">
<VCard title="自定义识别词">
<VCardSubtitle> 添加规则对种子名或者文件名进行预处理以校正识别 </VCardSubtitle>
<VCardSubtitle> 添加规则对种子名或者文件名进行预处理以校正识别 </VCardSubtitle>
<VCardItem>
<VTextarea
v-model="customIdentifiers"
@@ -148,7 +185,7 @@ onMounted(() => {
</VCol>
<VCol cols="12">
<VCard title="自定义制作组/字幕组">
<VCardSubtitle> 添加无法识别的制作组/字幕组 </VCardSubtitle>
<VCardSubtitle> 添加无法识别的制作组/字幕组 </VCardSubtitle>
<VCardItem>
<VTextarea
v-model="customReleaseGroups"
@@ -166,9 +203,29 @@ onMounted(() => {
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="自定义占位符">
<VCardSubtitle> 添加自定义占位符识别正则重命名格式中添加{customization}使用 </VCardSubtitle>
<VCardItem>
<VTextarea
v-model="customization"
auto-grow
placeholder="多个匹配对象请换行分隔,支持正则表达式,特殊字符注意转义"
/>
</VCardItem>
<VCardItem>
<VBtn
type="submit"
@click="saveCustomization"
>
保存
</VBtn>
</VCardItem>
</VCard>
</VCol>
<VCol cols="12">
<VCard title="文件整理屏蔽词">
<VCardSubtitle> 目录名或文件名中包含屏蔽词时不进行整理 </VCardSubtitle>
<VCardSubtitle> 目录名或文件名中包含屏蔽词时不进行整理 </VCardSubtitle>
<VCardItem>
<VTextarea
v-model="transferExcludeWords"

View File

@@ -1,14 +1,9 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import api from '@/api'
import type { Site } from '@/api/types'
import SiteCard from '@/components/cards/SiteCard.vue'
import NoDataFound from '@/components/NoDataFound.vue'
import { numberValidator, requiredValidator } from '@/@validators'
import { doneNProgress, startNProgress } from '@/api/nprogress'
// 提示框
const $toast = useToast()
import SiteAddEditForm from '@/components/form/SiteAddEditForm.vue'
// 数据列表
const dataList = ref<Site[]>([])
@@ -16,49 +11,13 @@ const dataList = ref<Site[]>([])
// 是否刷新过
const isRefreshed = ref(false)
// 新增按钮文本
const addBtnText = ref('新增站点')
// 新增按钮状态
const addBtnState = ref(false)
// 新增站点对话框
const siteAddDialog = ref(false)
// 状态下拉项
const statusItems = [
{ title: '启用', value: true },
{ title: '停用', value: false },
]
// 生成1到50的优先级下拉框选项
const priorityItems = ref(
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
title: item,
value: item,
})),
)
// 站点编辑表单数据
const siteForm = reactive<Site>({
id: 0,
url: '',
pri: 1,
is_active: true,
cookie: '',
ua: '',
limit_interval: 0,
limit_seconds: 0,
limit_count: 0,
proxy: 0,
render: 0,
name: '',
domain: '',
})
// 获取站点列表数据
async function fetchData() {
try {
dataList.value = await api.get('site')
dataList.value = await api.get('site/')
isRefreshed.value = true
}
catch (error) {
@@ -66,38 +25,6 @@ async function fetchData() {
}
}
// 调用API 新增站点
async function addSite() {
if (!siteForm.url)
return
startNProgress()
addBtnText.value = '新增中...'
addBtnState.value = true
try {
const result: { [key: string]: string } = await api.post('site', siteForm)
if (result.success) {
$toast.success('新增站点成功')
// 刷新数据
fetchData()
}
else { $toast.error(`新增站点失败:${result.message}`) }
siteAddDialog.value = false
}
catch (error) {
console.error(error)
}
doneNProgress()
addBtnText.value = '新增站点'
addBtnState.value = false
}
// 加载时获取数据
onBeforeMount(fetchData)
</script>
@@ -132,150 +59,20 @@ onBeforeMount(fetchData)
error-title="没有站点"
error-description="已添加并支持的站点将会在这里显示"
/>
<!-- Dialog Content -->
<VDialog
<!-- 新增站点按钮 -->
<VBtn
icon="mdi-plus"
size="x-large"
class="fixed right-5 bottom-5"
oper="add"
@click="siteAddDialog = true"
/>
<SiteAddEditForm
v-model="siteAddDialog"
max-width="50rem"
persistent
scrollable
>
<!-- Dialog Activator -->
<template #activator="{ props }">
<VBtn
icon="mdi-plus"
v-bind="props"
size="x-large"
class="fixed right-5 bottom-5"
/>
</template>
<VCard title="新增站点">
<DialogCloseBtn @click="siteAddDialog = false" />
<VCardText class="pt-2">
<VForm @submit.prevent="() => {}">
<VRow>
<VCol
cols="12"
md="6"
>
<VTextField
v-model="siteForm.url"
label="站点地址"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.pri"
label="优先级"
:items="priorityItems"
:rules="[requiredValidator]"
/>
</VCol>
<VCol
cols="12"
md="3"
>
<VSelect
v-model="siteForm.is_active"
:items="statusItems"
label="状态"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VTextField
v-model="siteForm.rss"
label="RSS地址"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="siteForm.cookie"
label="站点Cookie"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="siteForm.ua"
label="站点User-Agent"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_interval"
label="单位周期(秒)"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问次数"
:rules="[numberValidator]"
/>
</VCol>
<VCol
cols="12"
md="4"
>
<VTextField
v-model="siteForm.limit_seconds"
label="访问间隔(秒)"
:rules="[numberValidator]"
/>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.proxy"
label="代理"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<VSwitch
v-model="siteForm.render"
label="仿真"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardActions>
<VBtn
@click="siteAddDialog = false"
>
取消
</VBtn>
<VSpacer />
<VBtn
color="primary"
:disabled="addBtnState"
@click="addSite"
>
{{ addBtnText }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
oper="add"
@save="siteAddDialog = false; fetchData()"
@close="siteAddDialog = false"
/>
</template>
<style lang="scss">

Some files were not shown because too many files have changed in this diff Show More