Compare commits
198 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b222098ec5 | ||
|
|
bb8cf7ed78 | ||
|
|
0219ce3a9c | ||
|
|
b82e5d7cba | ||
|
|
ccee71e638 | ||
|
|
cba0e739eb | ||
|
|
c569cb9cde | ||
|
|
fc585a3900 | ||
|
|
973f8529c2 | ||
|
|
1ff9dc50fd | ||
|
|
065c9053da | ||
|
|
6905be1bcd | ||
|
|
a550f9616c | ||
|
|
bcee3e5373 | ||
|
|
d377ced6b6 | ||
|
|
6e0ceb093c | ||
|
|
745f99e52e | ||
|
|
7197034eda | ||
|
|
264748652f | ||
|
|
48e214564a | ||
|
|
5424e7e02a | ||
|
|
0c9c70b067 | ||
|
|
0ff24f4b09 | ||
|
|
cfa75b7643 | ||
|
|
b72ad1d78d | ||
|
|
5d1f293606 | ||
|
|
2dc0eca4aa | ||
|
|
f5808c1c81 | ||
|
|
321037477f | ||
|
|
43589c66e9 | ||
|
|
435f299a8b | ||
|
|
083db80251 | ||
|
|
92bf520cf4 | ||
|
|
ab354f21c4 | ||
|
|
c7a2c045c7 | ||
|
|
d33c8942e4 | ||
|
|
5e630097b9 | ||
|
|
3b5d03c1c8 | ||
|
|
298ae2c354 | ||
|
|
d936b68597 | ||
|
|
41471b9fd6 | ||
|
|
cc071c0911 | ||
|
|
628164d2bd | ||
|
|
999af85262 | ||
|
|
07e075ad8b | ||
|
|
18098f8aef | ||
|
|
f335b4e436 | ||
|
|
ab293edf4c | ||
|
|
88917070bf | ||
|
|
5bba5cb2bc | ||
|
|
098916bfa5 | ||
|
|
bb79aaed8b | ||
|
|
bc5c5a2835 | ||
|
|
4c11199de2 | ||
|
|
2e987701a8 | ||
|
|
4f625291a5 | ||
|
|
048f2abd87 | ||
|
|
31dea532c5 | ||
|
|
3d54e5d965 | ||
|
|
aee2a5a161 | ||
|
|
198ea0104d | ||
|
|
1abdf6d15c | ||
|
|
e5b836462f | ||
|
|
552b20b5d9 | ||
|
|
7d500aedb5 | ||
|
|
751e823b8c | ||
|
|
59d47b2b15 | ||
|
|
b1635b0715 | ||
|
|
4d778e9ca9 | ||
|
|
af433286d0 | ||
|
|
2a41e8a726 | ||
|
|
6b41f3bb64 | ||
|
|
78c178b1f6 | ||
|
|
20a6dd1aeb | ||
|
|
3773dfb4a1 | ||
|
|
e156b662a3 | ||
|
|
38193a870b | ||
|
|
e3a636772a | ||
|
|
46b043fdc7 | ||
|
|
a774ae87c2 | ||
|
|
a332a7b402 | ||
|
|
2ee4d874da | ||
|
|
4f051e5251 | ||
|
|
e4a0b29162 | ||
|
|
09234296f4 | ||
|
|
679228c8a7 | ||
|
|
a752e19878 | ||
|
|
0880c0e3b3 | ||
|
|
948e65d383 | ||
|
|
7cce57496d | ||
|
|
e54e851f61 | ||
|
|
17020cf62d | ||
|
|
0c7be28eaa | ||
|
|
0d5a183f2e | ||
|
|
c222594bea | ||
|
|
3df8bdfbf2 | ||
|
|
5722547d93 | ||
|
|
dea5ebd95d | ||
|
|
048e41c1ca | ||
|
|
5078036c51 | ||
|
|
e7a128bf0d | ||
|
|
0e46936231 | ||
|
|
d91d3ef0ef | ||
|
|
1f0dd907f9 | ||
|
|
3c555cbfca | ||
|
|
9a8e4d8600 | ||
|
|
444aaa5cdc | ||
|
|
25669d18fc | ||
|
|
03d6e46eca | ||
|
|
c44c7ed0f0 | ||
|
|
47ac7437c0 | ||
|
|
37f982e0ea | ||
|
|
139646369f | ||
|
|
3d4b84dc09 | ||
|
|
02866754e0 | ||
|
|
e4e1a75d44 | ||
|
|
4e5dd03456 | ||
|
|
506b6eea09 | ||
|
|
403ee4b925 | ||
|
|
193d1f550f | ||
|
|
d2a02a830c | ||
|
|
6cc9c1ac57 | ||
|
|
fffb1c6c02 | ||
|
|
985d1baff5 | ||
|
|
a70e467b69 | ||
|
|
b07010bebd | ||
|
|
353bdc5989 | ||
|
|
f135804c4b | ||
|
|
ae6d0ead2c | ||
|
|
6db3ad4e0d | ||
|
|
09e42d5a08 | ||
|
|
b9a09fd1be | ||
|
|
b08235b9f6 | ||
|
|
43e67893b4 | ||
|
|
633b38da01 | ||
|
|
68a4818be0 | ||
|
|
be3e4a7b13 | ||
|
|
2bc616ebbb | ||
|
|
7058472784 | ||
|
|
8d9f28b3c8 | ||
|
|
0d2bba78d9 | ||
|
|
23a62d33eb | ||
|
|
93a2a4a772 | ||
|
|
38e74f0c1b | ||
|
|
d2d6ca75be | ||
|
|
a4a9f9e7c5 | ||
|
|
4d5d1094ed | ||
|
|
9f8eaa5722 | ||
|
|
79e07d2b3d | ||
|
|
822d457bff | ||
|
|
8e391af0b4 | ||
|
|
b522b4d355 | ||
|
|
9c5ef8f5b4 | ||
|
|
4dc1ad35d2 | ||
|
|
9b405d9e59 | ||
|
|
2351ec7b85 | ||
|
|
6fc3228334 | ||
|
|
81b1f5b14d | ||
|
|
b8450ad28b | ||
|
|
70371a5001 | ||
|
|
87f4cf772b | ||
|
|
970ed8ff86 | ||
|
|
3f77f1037a | ||
|
|
6cc770dced | ||
|
|
355172dbf6 | ||
|
|
3bfcd38e65 | ||
|
|
909857f146 | ||
|
|
1cc64c7d21 | ||
|
|
891db2be21 | ||
|
|
54f3451456 | ||
|
|
563471ccf5 | ||
|
|
2e70b61e60 | ||
|
|
cd0d786a4c | ||
|
|
611ae13777 | ||
|
|
026214bd3f | ||
|
|
7c29c9ad27 | ||
|
|
cba5586f05 | ||
|
|
25a8d0cc2a | ||
|
|
2396a6b5fc | ||
|
|
c94add8d6b | ||
|
|
8b893dc6f2 | ||
|
|
51816da3d3 | ||
|
|
5e2d144828 | ||
|
|
04f56692a5 | ||
|
|
d3fb71c289 | ||
|
|
e565ec5b62 | ||
|
|
e5f7467d5b | ||
|
|
3a9e85c821 | ||
|
|
f2b51107fa | ||
|
|
e3df4000bb | ||
|
|
f4aef8f9dd | ||
|
|
476bd4bd19 | ||
|
|
4681c947c7 | ||
|
|
5db4d97568 | ||
|
|
235942157e | ||
|
|
8ef2be1c81 | ||
|
|
ed45f3438f | ||
|
|
2ca0920131 |
222
index.html
@@ -1,143 +1,198 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width, user-scalable=no" />
|
||||
<title>MoviePilot</title>
|
||||
<meta name="Robots" content="noindex,nofollow,noarchive" />
|
||||
<meta name="referrer" content="origin" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="manifest.json" crossorigin="use-credentials" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="MoviePilot" />
|
||||
<meta name="description" content="MoviePilot" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="referrer" content="never" />
|
||||
<meta name="msapplication-TileColor" content="#7D34FD" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#28243D" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)" />
|
||||
<meta name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<link rel="stylesheet" type="text/css" href="/loader.css" />
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width, user-scalable=no" />
|
||||
<title>MoviePilot</title>
|
||||
<meta name="Robots" content="noindex,nofollow,noarchive">
|
||||
<meta name="referrer" content="origin">
|
||||
<link rel="icon" type="image/png" href="/logo.png">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg">
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="MoviePilot">
|
||||
<meta name="description" content="MoviePilot">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="referrer" content="never">
|
||||
<meta name="msapplication-TileColor" content="#7D34FD" />
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="theme-color" content="#28243D" media="(prefers-color-scheme: dark)">
|
||||
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)">
|
||||
<meta name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<link rel="stylesheet" type="text/css" href="/loader.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<body>
|
||||
<div id="loading-bg">
|
||||
<div class="loading-logo">
|
||||
<!-- Logo -->
|
||||
<svg width="100px" height="100px" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<svg
|
||||
width="100px"
|
||||
height="100px"
|
||||
viewBox="0 0 192 192"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2"
|
||||
>
|
||||
<g transform="matrix(1,0,0,1,-2606,-236)">
|
||||
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">
|
||||
<rect x="0" y="0" width="192" height="192" style="fill:none;" />
|
||||
<rect x="0" y="0" width="192" height="192" style="fill: none" />
|
||||
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)">
|
||||
<path
|
||||
d="M2241.27,-28.175C2238.86,-28.931 2236.64,-29.181 2234.48,-29.254L2159.78,-29.286L2165.01,-11.207C2167.16,-13.121 2169.64,-13.722 2172.26,-13.808L2222.12,-13.822C2223.52,-13.824 2225,-13.701 2226.78,-13.108L2241.27,-28.175Z"
|
||||
style="fill:url(#_Linear1);" />
|
||||
style="fill: url(#_Linear1)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2205.67,331.428L2205.67,332.25L2205.67,352.835C2205.67,354.263 2204.91,355.583 2203.67,356.298C2202.43,357.012 2200.91,357.013 2199.67,356.3L2190.78,351.174C2189.73,350.595 2188.83,350.083 2188.03,349.59L2187.45,349.257C2186.66,348.725 2185.91,348.142 2185.21,347.461C2185.08,347.331 2184.95,347.198 2184.82,347.061C2184.26,346.457 2183.75,345.778 2183.3,344.995C2182.16,343.05 2181.69,341.024 2181.68,338.948L2181.67,268.923L2209.77,274.425C2207.5,275.639 2205.68,278.3 2205.67,281.429L2205.67,331.428Z"
|
||||
style="fill:url(#_Linear2);" />
|
||||
style="fill: url(#_Linear2)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2295.93,363.064C2295.73,363.184 2295.53,363.301 2295.32,363.414L2295.93,363.064Z"
|
||||
style="fill:rgb(141,81,249);" />
|
||||
style="fill: rgb(141, 81, 249)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2299.79,360.238C2299.79,360.238 2320.03,348.464 2320.04,348.461C2323.1,346.372 2324.69,343.444 2325.17,339.877C2325.17,339.877 2325.17,269.846 2325.17,269.839C2325.06,267.482 2324.56,265.739 2323.61,264.133C2322.56,262.445 2321.26,261.005 2319.55,259.97L2304.42,251.217C2303.96,250.949 2303.39,250.948 2302.92,251.216C2302.46,251.484 2302.17,251.979 2302.17,252.515L2302.17,276.775L2302.17,277.879L2302.17,352.926C2302.17,352.933 2302.17,352.941 2302.17,352.948C2302.04,355.861 2301.23,358.279 2299.79,360.238Z"
|
||||
style="fill:url(#_Linear3);" />
|
||||
style="fill: url(#_Linear3)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256Z"
|
||||
style="fill:rgb(165,118,255);" />
|
||||
style="fill: rgb(165, 118, 255)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256ZM2253.68,223.756C2251.6,223.789 2249.87,224.269 2248.47,224.996L2188.17,259.754C2184.35,261.992 2182.35,265.367 2182.18,269.874C2182.18,269.874 2182.17,292.759 2182.17,292.757C2183.25,290.047 2185.13,288.051 2187.62,286.607L2249.57,250.919C2249.58,250.917 2249.58,250.915 2249.59,250.913C2250.83,250.243 2252.17,249.839 2253.67,249.847C2255.21,249.841 2256.54,250.253 2257.76,250.914C2257.76,250.916 2257.76,250.917 2257.76,250.919L2274.92,260.807C2275.38,261.075 2275.95,261.074 2276.42,260.806C2276.88,260.538 2277.17,260.043 2277.17,259.508L2277.17,237.568C2277.17,236.317 2276.5,235.16 2275.42,234.535C2275.42,234.535 2258.88,225 2258.87,224.996C2256.87,224.049 2255.2,223.746 2253.68,223.756Z"
|
||||
style="fill:url(#_Linear4);" />
|
||||
style="fill: url(#_Linear4)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(0.800798,0.462341,0.769972,-1.33363,-1677.22,-896.858)">
|
||||
<path
|
||||
d="M2241.55,-28.184C2239.1,-28.989 2236.83,-29.204 2234.68,-29.295C2234.68,-29.295 2220.82,-29.3 2215.03,-29.303C2213.48,-29.303 2212.05,-28.808 2211.28,-28.004C2208.65,-25.275 2202.56,-18.936 2199.45,-15.709C2199.07,-15.306 2199.07,-14.809 2199.46,-14.406C2199.85,-14.004 2200.57,-13.758 2201.34,-13.761C2208.36,-13.788 2222.72,-13.845 2222.72,-13.845C2223.98,-13.851 2225.44,-13.657 2227.06,-13.117L2241.55,-28.184Z"
|
||||
style="fill:rgb(141,81,249);" />
|
||||
style="fill: rgb(141, 81, 249)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-4.32309,0,0,12.4454,9610.35,-1450.35)">
|
||||
<path
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
|
||||
style="fill:rgb(104,0,197);" />
|
||||
style="fill: rgb(104, 0, 197)"
|
||||
/>
|
||||
<clipPath id="_clip5">
|
||||
<path
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z" />
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
|
||||
/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip5)">
|
||||
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
|
||||
<path
|
||||
d="M1726.17,-64.249L1708.16,-72.303L1708.05,-23.514L1721.88,-32.386C1722.96,-33.241 1723.09,-33.944 1723.15,-34.636L1723.15,-54.373C1723.19,-56.238 1724.96,-57.594 1726.87,-56.686L1726.17,-64.249Z"
|
||||
style="fill:url(#_Linear6);" />
|
||||
style="fill: url(#_Linear6)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1704.47,-40.254C1706.28,-40.527 1708.14,-40.212 1708.16,-39.416L1708.16,-18.976L1726.17,-18.976L1726.17,-45.661Z"
|
||||
style="fill:rgb(141,81,249);" />
|
||||
style="fill: rgb(141, 81, 249)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1726.17,-18.976L1708.16,-18.976L1708.16,-39.416C1707.79,-40.732 1704.5,-40.298 1702.68,-40.025L1726.17,-45.661ZM1705.49,-40.491C1706.2,-40.507 1706.87,-40.464 1707.4,-40.327C1708.01,-40.173 1708.48,-39.899 1708.62,-39.436C1708.62,-39.429 1708.62,-39.423 1708.62,-39.416L1708.62,-19.152C1708.62,-19.152 1725.72,-19.152 1725.72,-19.152L1725.72,-45.345L1705.49,-40.491Z"
|
||||
style="fill:url(#_Radial7);" />
|
||||
style="fill: url(#_Radial7)"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)">
|
||||
<stop offset="0" style="stop-color:rgb(141,81,249);stop-opacity:1" />
|
||||
<stop offset="1" style="stop-color:rgb(116,50,223);stop-opacity:1" />
|
||||
<linearGradient
|
||||
id="_Linear1"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)">
|
||||
<stop offset="0" style="stop-color:rgb(141,81,249);stop-opacity:1" />
|
||||
<stop offset="1" style="stop-color:rgb(116,50,223);stop-opacity:1" />
|
||||
<linearGradient
|
||||
id="_Linear2"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)">
|
||||
<stop offset="0" style="stop-color:rgb(211,187,255);stop-opacity:1" />
|
||||
<stop offset="1" style="stop-color:rgb(211,187,255);stop-opacity:0" />
|
||||
<linearGradient
|
||||
id="_Linear3"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)">
|
||||
<stop offset="0" style="stop-color:rgb(211,187,255);stop-opacity:1" />
|
||||
<stop offset="1" style="stop-color:rgb(211,187,255);stop-opacity:0" />
|
||||
<linearGradient
|
||||
id="_Linear4"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)">
|
||||
<stop offset="0" style="stop-color:rgb(116,50,223);stop-opacity:1" />
|
||||
<stop offset="0.51" style="stop-color:rgb(110,38,217);stop-opacity:1" />
|
||||
<stop offset="1" style="stop-color:rgb(91,0,197);stop-opacity:1" />
|
||||
<linearGradient
|
||||
id="_Linear6"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
<stop offset="0.51" style="stop-color: rgb(110, 38, 217); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(91, 0, 197); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<radialGradient id="_Radial7" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)">
|
||||
<stop offset="0" style="stop-color:rgb(211,187,255);stop-opacity:1" />
|
||||
<stop offset="1" style="stop-color:rgb(211,187,255);stop-opacity:0" />
|
||||
<radialGradient
|
||||
id="_Radial7"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
<div class="loading">
|
||||
<div class="effect-1 effects"></div>
|
||||
@@ -145,18 +200,15 @@
|
||||
<div class="effect-3 effects"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<script>
|
||||
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
|
||||
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<script>
|
||||
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
|
||||
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
|
||||
|
||||
if (loaderColor)
|
||||
document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
|
||||
if (loaderColor) document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
|
||||
|
||||
if (primaryColor)
|
||||
document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
if (primaryColor) document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "1.0.6",
|
||||
"version": "1.2.9",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
@@ -59,6 +59,7 @@
|
||||
"@iconify/vue": "4.1.1",
|
||||
"@intlify/unplugin-vue-i18n": "^0.10.0",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/node": "^20.1.4",
|
||||
"@types/webfontloader": "^1.6.34",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.5",
|
||||
@@ -96,4 +97,4 @@
|
||||
"resolutions": {
|
||||
"postcss": "8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/plugin/actor.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
public/plugin/brush.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
public/plugin/clean.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
public/plugin/database.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/plugin/delete.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 14 KiB |
BIN
public/plugin/downloadmsg.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/plugin/fileupload.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/plugin/invites.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 28 KiB |
BIN
public/plugin/sync_file.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 16 KiB |
@@ -17,7 +17,6 @@
|
||||
// ℹ️ This mixin is inspired from vuetify for adding hover styles via before pseudo element
|
||||
@mixin before-pseudo() {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
background: currentcolor;
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
rgba(#{variables.$vertical-nav-background-color-rgb}, 30%) 75%,
|
||||
transparent
|
||||
);
|
||||
block-size: calc(env(safe-area-inset-top) + 64px);
|
||||
block-size: calc(env(safe-area-inset-top) + 4rem);
|
||||
inline-size: 100%;
|
||||
inset-block-start: calc(#{variables.$vertical-nav-header-height} - 2px);
|
||||
opacity: 0;
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
// ℹ️ This mixin is inspired from vuetify for adding hover styles via before pseudo element
|
||||
@mixin before-pseudo() {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
border-radius: inherit;
|
||||
|
||||
@@ -33,3 +33,11 @@ $ps-track-size: 0.5rem;
|
||||
.ps__thumb-y {
|
||||
background-color: rgb(var(--v-theme-perfect-scrollbar-thumb)) !important;
|
||||
}
|
||||
|
||||
// fix bug
|
||||
@media(hover: none) {
|
||||
.ps > .ps__rail-x,
|
||||
.ps > .ps__rail-y {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,10 +88,24 @@ export function formatSeconds(seconds: number) {
|
||||
}
|
||||
|
||||
// YYYY-MM-DD 转化为Date
|
||||
export function parseDate(dateString: string): Date {
|
||||
export function parseDate(dateString: string): Date | null {
|
||||
if (!dateString)
|
||||
return new Date()
|
||||
return null
|
||||
const [year, month, day] = dateString.split('-').map(Number)
|
||||
|
||||
return new Date(year, month - 1, day)
|
||||
}
|
||||
|
||||
// 文件大小格式化
|
||||
export function formatBytes(bytes: number, decimals = 2) {
|
||||
if (bytes === 0)
|
||||
return '0 bytes'
|
||||
|
||||
const k = 1024
|
||||
const dm = decimals < 0 ? 0 : decimals
|
||||
const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export default defineComponent({
|
||||
<style scoped>
|
||||
* {
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
perspective: 62.5rem;
|
||||
transform: translateZ(0);
|
||||
will-change: block-size;
|
||||
}
|
||||
|
||||
@@ -15,13 +15,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
})
|
||||
|
||||
const { mdAndDown } = useDisplay()
|
||||
|
||||
const refNav = ref()
|
||||
|
||||
/*
|
||||
ℹ️ Close overlay side when route is changed
|
||||
Close overlay vertical nav when link is clicked
|
||||
*/
|
||||
const route = useRoute()
|
||||
|
||||
watch(
|
||||
@@ -31,9 +25,11 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
// 是否滚动
|
||||
const isVerticalNavScrolled = ref(false)
|
||||
const updateIsVerticalNavScrolled = (val: boolean) => (isVerticalNavScrolled.value = val)
|
||||
|
||||
// 滚动响应
|
||||
function handleNavScroll(evt: Event) {
|
||||
isVerticalNavScrolled.value = (evt.target as HTMLElement).scrollTop > 0
|
||||
}
|
||||
@@ -83,9 +79,12 @@ function handleNavScroll(evt: Event) {
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@configured-variables" as variables;
|
||||
@use "@layouts/styles/mixins";
|
||||
@use '@configured-variables' as variables;
|
||||
@use '@layouts/styles/mixins';
|
||||
|
||||
.visible {
|
||||
visibility: visible !important;
|
||||
}
|
||||
// 👉 Vertical Nav
|
||||
.layout-vertical-nav {
|
||||
position: fixed;
|
||||
@@ -98,6 +97,11 @@ function handleNavScroll(evt: Event) {
|
||||
inset-inline-start: 0;
|
||||
transition: transform 0.25s ease-in-out, inline-size 0.25s ease-in-out, box-shadow 0.25s ease-in-out;
|
||||
will-change: transform, inline-size;
|
||||
visibility: hidden;
|
||||
|
||||
&:not(.overlay-nav) {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.nav-header {
|
||||
display: flex;
|
||||
|
||||
@@ -111,7 +111,7 @@ export default defineComponent({
|
||||
|
||||
.layout-navbar {
|
||||
position: fixed;
|
||||
width: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
|
||||
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
|
||||
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
|
||||
inset-block-start: 0;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ body,
|
||||
overflow: hidden;
|
||||
// TODO: Use grid gutter variable here
|
||||
padding-block: 1.5rem;
|
||||
padding-top: calc(env(safe-area-inset-top) + 65px);
|
||||
padding-top: calc(env(safe-area-inset-top) + 4.25rem);
|
||||
// display: flex;
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ body,
|
||||
& > div:first-child {
|
||||
flex: auto;
|
||||
position: relative;
|
||||
width: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
|
||||
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
|
||||
// 👉 Vertical nav
|
||||
$layout-vertical-nav-z-index: 12 !default;
|
||||
$layout-vertical-nav-width: 260px !default;
|
||||
$layout-vertical-nav-collapsed-width: 80px !default;
|
||||
$layout-vertical-nav-width: 16.25rem !default;
|
||||
$layout-vertical-nav-collapsed-width: 5rem !default;
|
||||
|
||||
// 👉 Horizontal nav
|
||||
$layout-horizontal-nav-z-index: 11 !default;
|
||||
$layout-horizontal-nav-navbar-height: 64px !default;
|
||||
$layout-horizontal-nav-navbar-height: 4rem !default;
|
||||
|
||||
// 👉 Navbar
|
||||
$layout-vertical-nav-navbar-height: 64px !default;
|
||||
$layout-vertical-nav-navbar-height: 4rem !default;
|
||||
$layout-vertical-nav-navbar-is-contained: true !default;
|
||||
$layout-vertical-nav-layout-navbar-z-index: 11 !default;
|
||||
$layout-horizontal-nav-layout-navbar-z-index: 11 !default;
|
||||
|
||||
// 👉 Main content
|
||||
$layout-boxed-content-width: 1440px !default;
|
||||
$layout-boxed-content-width: 90rem !default;
|
||||
|
||||
// 👉Footer
|
||||
$layout-vertical-nav-footer-height: 56px !default;
|
||||
$layout-vertical-nav-footer-height: 3.5rem !default;
|
||||
|
||||
// 👉 Layout overlay
|
||||
$layout-overlay-z-index: 11 !default;
|
||||
|
||||
39
src/App.vue
@@ -1,18 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useTheme } from 'vuetify'
|
||||
import api from './api'
|
||||
import type { User } from './api/types'
|
||||
import store from './store'
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
|
||||
// 第一时间应用主题
|
||||
const { global: globalTheme } = useTheme()
|
||||
globalTheme.name.value = localStorage.getItem('theme') || 'light'
|
||||
|
||||
// 路由
|
||||
const route = useRoute()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -36,43 +30,14 @@ function startSSEMessager() {
|
||||
}
|
||||
}
|
||||
|
||||
// 当前用户信息
|
||||
const accountInfo = ref<User>({
|
||||
id: 0,
|
||||
name: '',
|
||||
password: '',
|
||||
email: '',
|
||||
is_active: false,
|
||||
is_superuser: false,
|
||||
avatar: avatar1,
|
||||
})
|
||||
|
||||
// 调用API,加载当前用户数据
|
||||
async function loadAccountInfo() {
|
||||
try {
|
||||
const user: User = await api.get('user/current')
|
||||
|
||||
accountInfo.value = user
|
||||
if (!accountInfo.value.avatar)
|
||||
accountInfo.value.avatar = avatar1
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时,加载当前用户数据
|
||||
onMounted(() => {
|
||||
loadAccountInfo()
|
||||
onBeforeMount(async () => {
|
||||
startSSEMessager()
|
||||
})
|
||||
|
||||
// 提供给所有元素复用
|
||||
provide('accountInfo', accountInfo)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VApp>
|
||||
<RouterView :key="route.fullPath" />
|
||||
<RouterView />
|
||||
</VApp>
|
||||
</template>
|
||||
|
||||
@@ -526,6 +526,9 @@ export interface Plugin {
|
||||
|
||||
// 运行状态
|
||||
state?: boolean
|
||||
|
||||
// 是否有详情页面
|
||||
has_page?: boolean
|
||||
}
|
||||
|
||||
// 种子信息
|
||||
@@ -829,71 +832,33 @@ export interface NotificationSwitch {
|
||||
wechat: boolean
|
||||
telegram: boolean
|
||||
slack: boolean
|
||||
synologychat: boolean
|
||||
}
|
||||
|
||||
// 环境设置
|
||||
export interface Setting {
|
||||
// 媒体服务器 emby/jellyfin/plex
|
||||
MEDIASERVER: string
|
||||
// EMBY服务器地址,IP:PORT
|
||||
EMBY_HOST: string
|
||||
// EMBY Api Key
|
||||
EMBY_API_KEY: string
|
||||
// Jellyfin服务器地址,IP:PORT
|
||||
JELLYFIN_HOST: string
|
||||
// Jellyfin Api Key
|
||||
JELLYFIN_API_KEY: string
|
||||
// Plex服务器地址,IP:PORT
|
||||
PLEX_HOST: string
|
||||
// Plex Token
|
||||
PLEX_TOKEN: string
|
||||
// 下载目录
|
||||
DOWNLOAD_PATH: string
|
||||
}
|
||||
|
||||
// 自定义订阅
|
||||
export interface Rss {
|
||||
id?: number
|
||||
// 名称
|
||||
name?: string
|
||||
// RSS地址
|
||||
url?: string
|
||||
// 类型
|
||||
type?: string
|
||||
// 标题
|
||||
title?: string
|
||||
// 年份
|
||||
year?: string
|
||||
// TMDBID
|
||||
tmdbid?: number
|
||||
// 季号
|
||||
season?: number
|
||||
// 海报
|
||||
poster?: string
|
||||
// 背景图
|
||||
backdrop?: string
|
||||
// 评分
|
||||
vote?: number
|
||||
// 简介
|
||||
description?: string
|
||||
// 总集数
|
||||
total_episode?: number
|
||||
// 包含
|
||||
include?: string
|
||||
// 排除
|
||||
exclude?: string
|
||||
// 洗版
|
||||
best_version?: number
|
||||
// 是否使用代理服务器
|
||||
proxy?: number
|
||||
// 是否使用过滤规则
|
||||
filter?: boolean
|
||||
// 保存路径
|
||||
save_path?: string
|
||||
// 已处理数量
|
||||
processed?: number
|
||||
// 附加信息
|
||||
note?: string
|
||||
// 最后更新时间
|
||||
last_update?: string
|
||||
// 状态 0-停用,1-启用
|
||||
state?: number
|
||||
// 文件浏览接口
|
||||
export interface EndPoints {
|
||||
list: any
|
||||
mkdir: any
|
||||
delete: any
|
||||
download: any
|
||||
image: any
|
||||
rename: any
|
||||
}
|
||||
|
||||
// 文件浏览项目
|
||||
export interface FileItem {
|
||||
type: string
|
||||
name: string
|
||||
basename: string
|
||||
path: string
|
||||
extension: string
|
||||
size: number
|
||||
children: FileItem[]
|
||||
modify_time: number
|
||||
}
|
||||
|
||||
147
src/components/FileBrowser.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Axios } from 'axios'
|
||||
import axios from 'axios'
|
||||
|
||||
import Toolbar from './filebrowser/Toolbar.vue'
|
||||
import Tree from './filebrowser/Tree.vue'
|
||||
import List from './filebrowser/List.vue'
|
||||
import type { EndPoints } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
storages: String,
|
||||
storage: String,
|
||||
path: String,
|
||||
tree: Boolean,
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
axios: Object as PropType<Axios>,
|
||||
axiosconfig: Object,
|
||||
})
|
||||
|
||||
// 对外事件
|
||||
const emit = defineEmits(['pathchanged'])
|
||||
|
||||
const availableStorages = [
|
||||
{
|
||||
name: '本地',
|
||||
code: 'local',
|
||||
icon: 'mdi-folder-multiple-outline',
|
||||
},
|
||||
]
|
||||
|
||||
const fileIcons = {
|
||||
zip: 'mdi-folder-zip-outline',
|
||||
rar: 'mdi-folder-zip-outline',
|
||||
htm: 'mdi-language-html5',
|
||||
html: 'mdi-language-html5',
|
||||
js: 'mdi-nodejs',
|
||||
json: 'mdi-file-document-outline',
|
||||
md: 'mdi-language-markdown-outline',
|
||||
pdf: 'mdi-file-pdf',
|
||||
png: 'mdi-file-image',
|
||||
jpg: 'mdi-file-image',
|
||||
jpeg: 'mdi-file-image',
|
||||
mp4: 'mdi-filmstrip',
|
||||
mkv: 'mdi-filmstrip',
|
||||
avi: 'mdi-filmstrip',
|
||||
wmv: 'mdi-filmstrip',
|
||||
mov: 'mdi-filmstrip',
|
||||
txt: 'mdi-file-document-outline',
|
||||
xls: 'mdi-file-excel',
|
||||
other: 'mdi-file-outline',
|
||||
}
|
||||
|
||||
// 加载次数
|
||||
const loading = ref(0)
|
||||
// 当前存储
|
||||
const activeStorage = ref('local')
|
||||
// 刷新
|
||||
const refreshPending = ref(false)
|
||||
// 排序
|
||||
const sort = ref('name')
|
||||
// axios实例
|
||||
const axiosInstance = ref<Axios>()
|
||||
|
||||
// 计算属性
|
||||
const storagesArray = computed(() => {
|
||||
const storageCodes = props.storages?.split(',')
|
||||
return availableStorages.filter(item => storageCodes?.includes(item.code))
|
||||
})
|
||||
|
||||
// 方法
|
||||
function loadingChanged(loading: number) {
|
||||
if (loading)
|
||||
loading++
|
||||
else if (loading > 0)
|
||||
loading--
|
||||
}
|
||||
|
||||
function storageChanged(storage: string) {
|
||||
activeStorage.value = storage
|
||||
}
|
||||
|
||||
// 路径变化
|
||||
function pathChanged(_path: string) {
|
||||
emit('pathchanged', _path)
|
||||
}
|
||||
|
||||
// 排序变化
|
||||
function sortChanged(s: string) {
|
||||
sort.value = s
|
||||
refreshPending.value = true
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onBeforeMount(() => {
|
||||
activeStorage.value = props.storage ?? 'local'
|
||||
axiosInstance.value = props.axios ?? axios.create(props.axiosconfig)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="mx-auto" :loading="loading > 0">
|
||||
<Toolbar
|
||||
:path="props.path"
|
||||
:storages="storagesArray"
|
||||
:storage="activeStorage"
|
||||
:endpoints="props.endpoints"
|
||||
:axios="axiosInstance"
|
||||
@storagechanged="storageChanged"
|
||||
@pathchanged="pathChanged"
|
||||
@foldercreated="refreshPending = true"
|
||||
@sortchanged="sortChanged"
|
||||
/>
|
||||
<VRow no-gutters>
|
||||
<VCol v-if="tree" sm="auto" class="d-none d-md-block">
|
||||
<Tree
|
||||
:path="props.path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
/>
|
||||
</VCol>
|
||||
<VDivider v-if="tree" vertical />
|
||||
<VCol>
|
||||
<List
|
||||
:path="props.path"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axiosInstance"
|
||||
:refreshpending="refreshPending"
|
||||
:sort="sort"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
@filedeleted="refreshPending = true"
|
||||
@renamed="refreshPending = true"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -1,18 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useTheme } from 'vuetify'
|
||||
import miscpose from '@images/pages/pose-fs-9.png'
|
||||
import miscMaskDark from '@images/pages/misc-mask-dark.png'
|
||||
import miscMaskLight from '@images/pages/misc-mask-light.png'
|
||||
import tree from '@images/pages/tree.png'
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const vuetifyTheme = useTheme()
|
||||
|
||||
const authThemeMask = computed(() => {
|
||||
return vuetifyTheme.global.name.value === 'light' ? miscMaskLight : miscMaskDark
|
||||
})
|
||||
|
||||
interface Props {
|
||||
errorCode?: string
|
||||
errorTitle?: string
|
||||
@@ -21,7 +11,7 @@ interface Props {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="misc-wrapper">
|
||||
<div class="flex flex-col">
|
||||
<ErrorHeader
|
||||
:error-code="props.errorCode"
|
||||
:error-title="props.errorTitle"
|
||||
@@ -29,7 +19,7 @@ interface Props {
|
||||
/>
|
||||
|
||||
<!-- 👉 Image -->
|
||||
<div class="misc-avatar text-center">
|
||||
<div class="text-center">
|
||||
<VImg
|
||||
:src="miscpose"
|
||||
class="mx-auto pt-10"
|
||||
@@ -38,40 +28,8 @@ interface Props {
|
||||
/>
|
||||
<slot name="button" />
|
||||
</div>
|
||||
|
||||
<!-- 👉 Footer -->
|
||||
<VImg
|
||||
:src="tree"
|
||||
class="misc-footer-tree d-none d-lg-block"
|
||||
/>
|
||||
|
||||
<VImg
|
||||
:src="authThemeMask"
|
||||
class="misc-footer-img d-none d-md-block"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@configured-variables" as variables;
|
||||
@use '@core/scss/pages/misc.scss';
|
||||
|
||||
.misc-wrapper {
|
||||
position: relative;
|
||||
|
||||
.misc-footer-tree {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
inline-size: 15.625rem;
|
||||
inset-block-end: 3.5rem;
|
||||
inset-inline-start: 0.375rem;
|
||||
left: variables.$layout-vertical-nav-width;
|
||||
}
|
||||
|
||||
.misc-footer-img {
|
||||
position: fixed;
|
||||
inline-size: 100%;
|
||||
inset-block-end: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -84,7 +84,7 @@ async function deleteDownload() {
|
||||
:class="getTextClass()"
|
||||
>
|
||||
{{ props.info?.media.title || props.info?.name }}
|
||||
{{ props.info?.season_episode }}
|
||||
{{ props.info?.media.episode ? `${props.info?.media.season} ${props.info?.media.episode}` : props.info?.season_episode }}
|
||||
</VCardTitle>
|
||||
|
||||
<VCardSubtitle
|
||||
|
||||
@@ -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)
|
||||
@@ -50,13 +61,24 @@ const selectFilterOptions = ref<{ [key: string]: string }[]>([
|
||||
{ title: '排除: 国语配音', value: ' !CNVOI ' },
|
||||
{ title: '促销: 免费', value: ' FREE ' },
|
||||
])
|
||||
|
||||
// 已选择的过滤规则
|
||||
const selectedFilters = ref<string[]>(props.rules ?? [])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="tonal" :width="props.width" :height="props.height">
|
||||
<span class="absolute top-3 right-14">
|
||||
<IconBtn
|
||||
v-if="props.pri !== '1'"
|
||||
@click.stop="onLevelUp"
|
||||
>
|
||||
<VIcon icon="mdi-arrow-up" />
|
||||
</IconBtn>
|
||||
<IconBtn
|
||||
v-if="props.pri !== props.maxpri"
|
||||
@click.stop="onLevelDown"
|
||||
>
|
||||
<VIcon icon="mdi-arrow-down" />
|
||||
</IconBtn>
|
||||
</span>
|
||||
<DialogCloseBtn @click="onClose" />
|
||||
<VCardItem>
|
||||
<VCardTitle>优先级 {{ props.pri }}</VCardTitle>
|
||||
@@ -64,7 +86,7 @@ const selectedFilters = ref<string[]>(props.rules ?? [])
|
||||
<VCol>
|
||||
<VSelect
|
||||
:key="props.pri"
|
||||
v-model="selectedFilters"
|
||||
v-model="props.rules"
|
||||
variant="underlined"
|
||||
:items="selectFilterOptions"
|
||||
chips
|
||||
|
||||
@@ -112,7 +112,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,
|
||||
@@ -360,14 +360,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 +371,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 +474,89 @@ const getImgUrl: Ref<string> = computed(() => {
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
<VDialog
|
||||
<!-- 订阅季弹窗 -->
|
||||
<VBottomSheet
|
||||
v-model="subscribeSeasonDialog"
|
||||
max-width="600"
|
||||
content-class="whitespace-nowrap"
|
||||
inset
|
||||
scrollable
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="选择订阅季">
|
||||
<VCardText style="padding: 0;">
|
||||
<VDataTable
|
||||
v-model="seasonsSelected"
|
||||
:headers="seasonsHeaders"
|
||||
:items="seasonInfos"
|
||||
item-value="season_number"
|
||||
return-object
|
||||
fixed-header
|
||||
show-select
|
||||
:items-per-page="100"
|
||||
density="compact"
|
||||
height="auto"
|
||||
<VCard>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
147
src/components/cards/MediaInfoCard.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
import type { Context } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
context: Object as PropType<Context>,
|
||||
})
|
||||
|
||||
// TMDB图片转换为w500大小
|
||||
function getW500Image(url = '') {
|
||||
if (!url)
|
||||
return ''
|
||||
return url.replace('original', 'w500')
|
||||
}
|
||||
|
||||
// 打开TMDB详情页面
|
||||
function openTmdbPage(type: string, tmdbId: number) {
|
||||
if (!type || !tmdbId)
|
||||
return
|
||||
|
||||
const url = `https://www.themoviedb.org/${type === '电影' ? 'movie' : 'tv'}/${tmdbId}`
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-show="context">
|
||||
<VCol>
|
||||
<div
|
||||
v-if="context?.meta_info?.name"
|
||||
class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row"
|
||||
>
|
||||
<div
|
||||
v-if="context?.media_info?.poster_path"
|
||||
class="ma-auto"
|
||||
>
|
||||
<VImg
|
||||
width="10rem"
|
||||
aspect-ratio="2/3"
|
||||
class="object-cover aspect-w-2 aspect-h-3 rounded-lg ring-1 ring-gray-500"
|
||||
:src="getW500Image(context?.media_info?.poster_path)"
|
||||
cover
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<VCardItem class="pb-1">
|
||||
<VCardTitle class="text-center text-md-left">
|
||||
{{ context?.media_info?.title || context?.meta_info?.name }}
|
||||
{{ context?.meta_info?.season_episode }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle class="text-center text-md-left">
|
||||
{{ context?.media_info?.year || context?.meta_info?.year }}
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText
|
||||
v-if="context?.media_info?.overview"
|
||||
class="line-clamp-4 overflow-hidden text-ellipsis text-center text-md-left ..."
|
||||
>
|
||||
{{ context?.media_info?.overview }}
|
||||
</VCardText>
|
||||
|
||||
<VCardItem class="text-center text-md-left">
|
||||
<!-- 类型 -->
|
||||
<VChip
|
||||
v-if="context?.media_info?.type || context?.meta_info?.type"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-blue-500"
|
||||
>
|
||||
{{
|
||||
context?.media_info?.type || context?.meta_info?.type
|
||||
}}
|
||||
</VChip>
|
||||
<!-- 二级分类 -->
|
||||
<VChip
|
||||
v-if="context?.media_info?.category"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-blue-500"
|
||||
>
|
||||
{{ context?.media_info?.category }}
|
||||
</VChip>
|
||||
<!-- TMDBID -->
|
||||
<VChip
|
||||
v-if="context?.media_info?.tmdb_id"
|
||||
variant="elevated"
|
||||
color="success"
|
||||
class="me-1 mb-1"
|
||||
@click="openTmdbPage(context?.media_info?.type || '', context?.media_info?.tmdb_id)"
|
||||
>
|
||||
{{ context?.media_info?.tmdb_id }}
|
||||
</VChip>
|
||||
<!-- meta_info -->
|
||||
<VChip
|
||||
v-if="context?.meta_info?.edition"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
{{ context?.meta_info?.edition }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="context?.meta_info?.resource_pix"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
{{ context?.meta_info?.resource_pix }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="context?.meta_info?.video_encode"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-orange-500"
|
||||
>
|
||||
{{ context?.meta_info?.video_encode }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="context?.meta_info?.audio_encode"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-orange-500"
|
||||
>
|
||||
{{ context?.meta_info?.audio_encode }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="context?.meta_info?.resource_team"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-cyan-500"
|
||||
>
|
||||
{{ context?.meta_info?.resource_team }}
|
||||
</VChip>
|
||||
</VCardItem>
|
||||
</div>
|
||||
</div>
|
||||
<VAlert
|
||||
v-if="!context?.meta_info?.name"
|
||||
icon="mdi-alert-circle-outline"
|
||||
>
|
||||
识别失败,无法识别到有效信息!
|
||||
</VAlert>
|
||||
</VCol>
|
||||
</div>
|
||||
</template>
|
||||
@@ -53,7 +53,7 @@ async function installPlugin() {
|
||||
:style="{ background: `${props.plugin?.plugin_color}` }"
|
||||
>
|
||||
<VAvatar
|
||||
size="128"
|
||||
size="8rem"
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
>
|
||||
<VImg
|
||||
|
||||
@@ -119,6 +119,8 @@ async function savePluginConf() {
|
||||
|
||||
// 显示插件详情
|
||||
async function showPluginInfo() {
|
||||
// 加载详情
|
||||
await loadPluginPage()
|
||||
pluginConfigDialog.value = false
|
||||
pluginInfoDialog.value = true
|
||||
}
|
||||
@@ -129,17 +131,35 @@ async function showPluginConfig() {
|
||||
await loadPluginForm()
|
||||
// 加载配置
|
||||
await loadPluginConf()
|
||||
// 加载详情
|
||||
await loadPluginPage()
|
||||
// 显示对话框
|
||||
pluginInfoDialog.value = false
|
||||
pluginConfigDialog.value = true
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '卸载',
|
||||
title: '查看详情',
|
||||
value: 1,
|
||||
show: props.plugin?.has_page,
|
||||
props: {
|
||||
prependIcon: 'mdi-information-outline',
|
||||
click: showPluginInfo,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '配置',
|
||||
value: 2,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-cog-outline',
|
||||
click: showPluginConfig,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '卸载',
|
||||
value: 3,
|
||||
show: true,
|
||||
props: {
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
@@ -155,7 +175,12 @@ const dropdownItems = ref([
|
||||
v-if="isVisible"
|
||||
:width="props.width"
|
||||
:height="props.height"
|
||||
@click="showPluginConfig"
|
||||
@click="() => {
|
||||
if (props.plugin?.has_page)
|
||||
showPluginInfo()
|
||||
else
|
||||
showPluginConfig()
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="relative pa-4 text-center card-cover-blurred"
|
||||
@@ -171,6 +196,7 @@ const dropdownItems = ref([
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
v-show="item.show"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="item.props.color"
|
||||
@@ -186,7 +212,7 @@ const dropdownItems = ref([
|
||||
</IconBtn>
|
||||
</div>
|
||||
<VAvatar
|
||||
size="128"
|
||||
size="8rem"
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
>
|
||||
<VImg
|
||||
@@ -210,11 +236,11 @@ const dropdownItems = ref([
|
||||
<!-- 插件配置页面 -->
|
||||
<VDialog
|
||||
v-model="pluginConfigDialog"
|
||||
max-width="800"
|
||||
max-width="50rem"
|
||||
scrollable
|
||||
persistent
|
||||
>
|
||||
<VCard :title="props.plugin?.plugin_name">
|
||||
<VCard :title="`${props.plugin?.plugin_name} - 配置`">
|
||||
<DialogCloseBtn @click="pluginConfigDialog = false" />
|
||||
<VCardText>
|
||||
<FormRender
|
||||
@@ -226,7 +252,7 @@ const dropdownItems = ref([
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn v-if="pluginPageItems.length > 0" @click="showPluginInfo">
|
||||
详情
|
||||
查看详情
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="savePluginConf">
|
||||
@@ -239,11 +265,11 @@ const dropdownItems = ref([
|
||||
<!-- 插件详情页面 -->
|
||||
<VDialog
|
||||
v-model="pluginInfoDialog"
|
||||
max-width="1000"
|
||||
max-width="62.5rem"
|
||||
scrollable
|
||||
persistent
|
||||
>
|
||||
<VCard :title="`${props.plugin?.plugin_name} - 详情`">
|
||||
<VCard :title="`${props.plugin?.plugin_name}`">
|
||||
<DialogCloseBtn @click="pluginInfoDialog = false" />
|
||||
<VCardText>
|
||||
<PageRender
|
||||
@@ -253,6 +279,9 @@ const dropdownItems = ref([
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn @click="showPluginConfig">
|
||||
配置
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="pluginInfoDialog = false">
|
||||
关闭
|
||||
|
||||
@@ -1,590 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { calculateTimeDifference } from '@/@core/utils'
|
||||
import { formatFileSize, formatSeason } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import type { Rss, Site, TorrentInfo } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<Rss>,
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['remove', 'save'])
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 图片是否加载完成
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
// 订阅弹窗
|
||||
const rssInfoDialog = ref(false)
|
||||
|
||||
// RSS预览窗口
|
||||
const rssPreviewDialog = ref(false)
|
||||
|
||||
// 加载状态
|
||||
const previewLoading = ref(false)
|
||||
|
||||
// 总条数
|
||||
const previewTotalItems = ref(0)
|
||||
|
||||
// 每页条数
|
||||
const previewItemsPerPage = ref(25)
|
||||
|
||||
// 预览表头
|
||||
const previewHeaders = [
|
||||
{ title: '标题', key: 'title', sortable: true },
|
||||
{ title: '时间', key: 'pubdate', sortable: true },
|
||||
{ title: '大小', key: 'size', sortable: true },
|
||||
{ title: '', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
// 预览数据
|
||||
const previewDataList = ref<TorrentInfo[]>([])
|
||||
|
||||
// 站点名称
|
||||
const siteName = ref('')
|
||||
|
||||
// 订阅编辑表单
|
||||
const rssForm = reactive<any>(props.media ?? {})
|
||||
|
||||
// 类型转换
|
||||
rssForm.best_version = rssForm.best_version === 1
|
||||
rssForm.proxy = rssForm.proxy === 1
|
||||
rssForm.filter = rssForm.filter === 1
|
||||
|
||||
// 上一次更新时间
|
||||
const lastUpdateText = ref(
|
||||
`${
|
||||
props.media?.last_update
|
||||
? `${calculateTimeDifference(props.media?.last_update || '')}前`
|
||||
: ''
|
||||
}`,
|
||||
)
|
||||
|
||||
// 图片加载完成响应
|
||||
function imageLoadHandler() {
|
||||
imageLoaded.value = true
|
||||
}
|
||||
|
||||
// 根据 type 返回不同的图标
|
||||
function getIcon() {
|
||||
if (props.media?.type === '电影')
|
||||
return 'mdi-movie'
|
||||
else if (props.media?.type === '电视剧')
|
||||
return 'mdi-television-classic'
|
||||
else
|
||||
return 'mdi-help-circle'
|
||||
}
|
||||
|
||||
// 计算文本颜色
|
||||
function getTextColor() {
|
||||
return imageLoaded.value ? 'white' : ''
|
||||
}
|
||||
|
||||
// 计算文本类
|
||||
function getTextClass() {
|
||||
return imageLoaded.value ? 'text-white' : ''
|
||||
}
|
||||
|
||||
// 删除订阅
|
||||
async function removerRss() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(
|
||||
`rss/${props.media?.id}`,
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
// 通知父组件刷新
|
||||
emit('remove')
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API修改订阅
|
||||
async function updateRssInfo() {
|
||||
rssInfoDialog.value = false
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.put('rss', rssForm)
|
||||
|
||||
// 提示
|
||||
if (result.success) {
|
||||
$toast.success(`${props.media?.name} 更新成功!`)
|
||||
// 通知父组件刷新
|
||||
emit('remove')
|
||||
}
|
||||
else { $toast.error(`${props.media?.name} 更新失败:${result.message}!`) }
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询站点名称
|
||||
async function querySiteName() {
|
||||
try {
|
||||
const result: Site = await api.get(
|
||||
`site/domain/${props.media?.url?.split('/')[2]}`,
|
||||
)
|
||||
|
||||
if (result)
|
||||
siteName.value = result.name
|
||||
}
|
||||
catch (e) {
|
||||
// 截取URL中的主域名作为站点名称
|
||||
siteName.value = props.media?.url?.split('/')[2] ?? '未知'
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 预览按钮响应
|
||||
async function handleRssPreview() {
|
||||
rssPreviewDialog.value = true
|
||||
previewLoading.value = true
|
||||
await previewRss()
|
||||
previewLoading.value = false
|
||||
}
|
||||
|
||||
// 预览站点RSS
|
||||
async function previewRss() {
|
||||
try {
|
||||
const result: TorrentInfo[] = await api.get(
|
||||
`rss/preview/${props.media?.id}`,
|
||||
)
|
||||
previewDataList.value = result
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑订阅响应
|
||||
async function editRssDialog() {
|
||||
rssInfoDialog.value = true
|
||||
}
|
||||
|
||||
// 刷新按钮响应
|
||||
async function refreshRss() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
`rss/refresh/${props.media?.id}`,
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success(`${props.media?.name} 已提交刷新任务!`)
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成1到50季的下拉框选项
|
||||
const seasonItems = ref(
|
||||
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
|
||||
title: `第 ${item} 季`,
|
||||
value: item,
|
||||
})),
|
||||
)
|
||||
|
||||
// 打开种子详情页面
|
||||
function openTorrentDetail(page_url: string) {
|
||||
window.open(page_url, '_blank')
|
||||
}
|
||||
|
||||
// 下载种子文件
|
||||
async function downloadTorrentFile(enclosure: string) {
|
||||
window.open(enclosure, '_blank')
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '编辑',
|
||||
value: 1,
|
||||
props: {
|
||||
prependIcon: 'mdi-file-edit-outline',
|
||||
click: editRssDialog,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '预览',
|
||||
value: 2,
|
||||
props: {
|
||||
prependIcon: 'mdi-eye-outline',
|
||||
click: handleRssPreview,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '刷新',
|
||||
value: 3,
|
||||
props: {
|
||||
prependIcon: 'mdi-refresh',
|
||||
click: refreshRss,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '取消订阅',
|
||||
value: 4,
|
||||
props: {
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
click: removerRss,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
querySiteName()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
:key="props.media?.id"
|
||||
:class="`${rssForm.best_version ? 'outline-dashed outline-1' : ''}`"
|
||||
@click="editRssDialog"
|
||||
>
|
||||
<template #image>
|
||||
<VImg
|
||||
:src="props.media?.backdrop || props.media?.poster"
|
||||
aspect-ratio="2/3"
|
||||
cover
|
||||
class="brightness-50"
|
||||
@load="imageLoadHandler"
|
||||
/>
|
||||
</template>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
size="1.9rem"
|
||||
:color="getTextColor()"
|
||||
:icon="getIcon()"
|
||||
/>
|
||||
</template>
|
||||
<VCardTitle :class="getTextClass()">
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : "") }}
|
||||
</VCardTitle>
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
:color="getTextColor()"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="item.props.color"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<p
|
||||
class="clamp-text mb-0"
|
||||
:class="getTextClass()"
|
||||
>
|
||||
{{ props.media?.description }}
|
||||
</p>
|
||||
</VCardText>
|
||||
|
||||
<VCardText class="d-flex justify-space-between align-center flex-wrap">
|
||||
<div class="d-flex align-center">
|
||||
<IconBtn
|
||||
icon="mdi-star"
|
||||
:color="getTextColor()"
|
||||
class="me-1"
|
||||
/>
|
||||
<span
|
||||
class="text-subtitle-2 me-4"
|
||||
:class="getTextClass()"
|
||||
>{{
|
||||
props.media?.vote
|
||||
}}</span>
|
||||
<IconBtn
|
||||
v-bind="props"
|
||||
icon="mdi-progress-clock"
|
||||
:color="getTextColor()"
|
||||
class="me-1"
|
||||
/>
|
||||
<span
|
||||
class="text-subtitle-2 me-4"
|
||||
:class="getTextClass()"
|
||||
>{{ props.media?.processed || 0 }}</span>
|
||||
<IconBtn
|
||||
v-if="siteName"
|
||||
icon="mdi-web"
|
||||
:color="getTextColor()"
|
||||
class="me-1"
|
||||
/>
|
||||
<span
|
||||
v-if="siteName"
|
||||
class="text-subtitle-2 me-4"
|
||||
:class="getTextClass()"
|
||||
>
|
||||
{{ siteName }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText
|
||||
v-if="lastUpdateText"
|
||||
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300"
|
||||
>
|
||||
<VIcon
|
||||
icon="mdi-download"
|
||||
class="me-1"
|
||||
/> {{ lastUpdateText }}
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<VDialog
|
||||
v-model="rssInfoDialog"
|
||||
max-width="800"
|
||||
persistent
|
||||
scrollable
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard :title="`订阅 - ${props.media?.name}`">
|
||||
<VCardText class="pt-2">
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.url"
|
||||
label="RSS地址"
|
||||
placeholder="https://example.com/rss"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSelect
|
||||
v-model="rssForm.type"
|
||||
label="类型"
|
||||
:items="[{ title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
|
||||
readonly
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.title"
|
||||
label="标题"
|
||||
readonly
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.year"
|
||||
label="年份"
|
||||
readonly
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSelect
|
||||
v-show="rssForm.type === '电视剧'"
|
||||
v-model="rssForm.season"
|
||||
label="季"
|
||||
:items="seasonItems"
|
||||
readonly
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.include"
|
||||
label="包含"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.exclude"
|
||||
label="排除"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.save_path"
|
||||
label="保存路径"
|
||||
placeholder="留空自动"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSelect
|
||||
v-model="rssForm.state"
|
||||
label="状态"
|
||||
:items="[{
|
||||
title: '启用',
|
||||
value: 1,
|
||||
}, {
|
||||
title: '停用',
|
||||
value: 0,
|
||||
}]"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="rssForm.best_version"
|
||||
label="洗版"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="rssForm.proxy"
|
||||
label="代理服务器"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="rssForm.filter"
|
||||
label="过滤规则"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VBtn @click="rssInfoDialog = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn @click="updateRssInfo">
|
||||
确定
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- RSS预览窗口 -->
|
||||
<VDialog
|
||||
v-model="rssPreviewDialog"
|
||||
max-width="1280"
|
||||
scrollable
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="RSS预览">
|
||||
<DialogCloseBtn @click="rssPreviewDialog = false" />
|
||||
<VCardText class="pt-2">
|
||||
<VDataTable
|
||||
v-model:items-per-page="previewItemsPerPage"
|
||||
:headers="previewHeaders"
|
||||
:items="previewDataList"
|
||||
:items-length="previewTotalItems"
|
||||
:loading="previewLoading"
|
||||
density="compact"
|
||||
item-value="title"
|
||||
return-object
|
||||
fixed-header
|
||||
items-per-page-text="每页条数"
|
||||
page-text="{0}-{1} 共 {2} 条"
|
||||
>
|
||||
<template #item.title="{ item }">
|
||||
<div class="text-high-emphasis">
|
||||
{{ item.raw.title }}
|
||||
</div>
|
||||
</template>
|
||||
<template #item.size="{ item }">
|
||||
<div class="text-nowrap whitespace-nowrap">
|
||||
{{ formatFileSize(item.raw.size) }}
|
||||
</div>
|
||||
</template>
|
||||
<template #item.pubdate="{ item }">
|
||||
<div class="text-sm">
|
||||
{{ item.raw.pubdate }}
|
||||
</div>
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="openTorrentDetail(item.raw.page_url)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-information" />
|
||||
</template>
|
||||
<VListItemTitle>查看详情</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
variant="plain"
|
||||
@click="downloadTorrentFile(item.raw.enclosure)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-download" />
|
||||
</template>
|
||||
<VListItemTitle>下载种子</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
<template #no-data>
|
||||
没有数据
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
@@ -72,9 +72,6 @@ const resourceTotalItems = ref(0)
|
||||
// 每页条数
|
||||
const resourceItemsPerPage = ref(25)
|
||||
|
||||
// 当前页码
|
||||
const resourceCurrentPage = ref(0)
|
||||
|
||||
// 用户名密码表单
|
||||
const userPwForm = ref({
|
||||
username: '',
|
||||
@@ -215,7 +212,7 @@ async function updateSiteInfo() {
|
||||
// 更新按钮状态
|
||||
siteInfoDialog.value = false
|
||||
|
||||
const result: { [key: string]: any } = await api.put('site', siteForm)
|
||||
const result: { [key: string]: any } = await api.put('site/', siteForm)
|
||||
if (result.success) {
|
||||
$toast.success(`${cardProps.site?.name} 更新成功!`)
|
||||
emit('update')
|
||||
@@ -244,13 +241,7 @@ function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||
async function getResourceList() {
|
||||
resourceLoading.value = true
|
||||
try {
|
||||
resourceDataList.value = await api.get('search/title', {
|
||||
params: {
|
||||
keyword: resourceSearch.value,
|
||||
page: resourceCurrentPage.value,
|
||||
site: cardProps.site?.id,
|
||||
},
|
||||
})
|
||||
resourceDataList.value = await api.get(`site/resource/${cardProps.site?.id}`)
|
||||
resourceLoading.value = false
|
||||
}
|
||||
catch (error) {
|
||||
@@ -389,7 +380,7 @@ onMounted(() => {
|
||||
<!-- 更新站点Cookie & UA弹窗 -->
|
||||
<VDialog
|
||||
v-model="siteCookieDialog"
|
||||
max-width="600"
|
||||
max-width="50rem"
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="更新站点Cookie & UA">
|
||||
@@ -437,7 +428,7 @@ onMounted(() => {
|
||||
<!-- 站点编辑弹窗 -->
|
||||
<VDialog
|
||||
v-model="siteInfoDialog"
|
||||
max-width="1000"
|
||||
max-width="50rem"
|
||||
persistent
|
||||
scrollable
|
||||
>
|
||||
@@ -480,6 +471,12 @@ onMounted(() => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="siteForm.rss"
|
||||
label="RSS地址"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="siteForm.cookie"
|
||||
@@ -562,7 +559,7 @@ onMounted(() => {
|
||||
<!-- 站点资源弹窗 -->
|
||||
<VDialog
|
||||
v-model="resourceDialog"
|
||||
max-width="1280"
|
||||
max-width="80rem"
|
||||
scrollable
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
@@ -650,6 +647,7 @@ onMounted(() => {
|
||||
<VListItemTitle>查看详情</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
v-if="item.raw.enclosure?.startsWith('http')"
|
||||
variant="plain"
|
||||
@click="downloadTorrentFile(item.raw.enclosure)"
|
||||
>
|
||||
|
||||
@@ -65,8 +65,8 @@ function getPercentage() {
|
||||
return 0
|
||||
|
||||
return Math.round(
|
||||
(((props.media?.total_episode || 0) - (props.media?.lack_episode || 0))
|
||||
/ (props.media?.total_episode || 1))
|
||||
(((props.media?.total_episode ?? 0) - (props.media?.lack_episode ?? 0))
|
||||
/ (props.media?.total_episode ?? 1))
|
||||
* 100,
|
||||
)
|
||||
}
|
||||
@@ -118,7 +118,7 @@ async function searchSubscribe() {
|
||||
async function updateSubscribeInfo() {
|
||||
subscribeInfoDialog.value = false
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.put('subscribe', subscribeForm)
|
||||
const result: { [key: string]: any } = await api.put('subscribe/', subscribeForm)
|
||||
|
||||
// 提示
|
||||
if (result.success) {
|
||||
@@ -136,7 +136,7 @@ async function updateSubscribeInfo() {
|
||||
// 获取站点列表数据
|
||||
async function loadSites() {
|
||||
try {
|
||||
const data: Site[] = await api.get('site')
|
||||
const data: Site[] = await api.get('site/rss')
|
||||
|
||||
// 过滤站点,只有启用的站点才显示
|
||||
siteList.value = data.filter(item => item.is_active)
|
||||
@@ -325,7 +325,7 @@ const dropdownItems = ref([
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<VDialog
|
||||
v-model="subscribeInfoDialog"
|
||||
max-width="1000"
|
||||
max-width="50rem"
|
||||
persistent
|
||||
scrollable
|
||||
>
|
||||
|
||||
139
src/components/cards/TmdbSelectorCard.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import type { MediaInfo } from '@/api/types'
|
||||
|
||||
interface TmdbItem {
|
||||
title: string
|
||||
overview: string
|
||||
tmdbid: number
|
||||
poster: string
|
||||
}
|
||||
|
||||
// update:modelValue 事件
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
const items = ref<TmdbItem[]>([])
|
||||
|
||||
// 搜索词
|
||||
const keyword = ref('')
|
||||
|
||||
// 加载中
|
||||
const loading = ref(false)
|
||||
|
||||
// ref
|
||||
const tmdbKeyword = ref<HTMLElement | null>(null)
|
||||
|
||||
// 选中条目
|
||||
function selectMedia(item: TmdbItem) {
|
||||
console.log(item)
|
||||
emit('update:modelValue', item.tmdbid)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// TMDB图片转换为w500大小
|
||||
function getW500Image(url = '') {
|
||||
if (!url)
|
||||
return ''
|
||||
return url.replace('original', 'w500')
|
||||
}
|
||||
|
||||
// 搜索词条
|
||||
async function searchMedias() {
|
||||
if (!keyword)
|
||||
return
|
||||
|
||||
// 调用API搜索词条
|
||||
try {
|
||||
loading.value = true
|
||||
const result: MediaInfo[] = await api.get('media/search', {
|
||||
params: {
|
||||
title: keyword.value,
|
||||
page: 1,
|
||||
count: 20,
|
||||
},
|
||||
})
|
||||
|
||||
// 清空
|
||||
items.value = []
|
||||
|
||||
// 赋值
|
||||
for (const item of result) {
|
||||
items.value.push({
|
||||
tmdbid: item.tmdb_id || 0,
|
||||
poster: getW500Image(item.poster_path),
|
||||
title: `${item.title}(${item.year})`,
|
||||
overview: `<span class="text-primary">${item.type}</span> ${item.overview}`,
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时聚焦搜索框
|
||||
onMounted(() => {
|
||||
// 500ms后聚焦
|
||||
setTimeout(() => {
|
||||
tmdbKeyword.value?.focus()
|
||||
}, 500)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
class="mx-auto"
|
||||
width="100%"
|
||||
>
|
||||
<VToolbar flat class="p-0">
|
||||
<VTextField
|
||||
ref="tmdbKeyword"
|
||||
v-model="keyword"
|
||||
label="输入名称搜索"
|
||||
single-line
|
||||
placeholder="电影或电视剧名称"
|
||||
variant="solo"
|
||||
append-inner-icon="mdi-magnify"
|
||||
flat
|
||||
class="mx-1"
|
||||
:loading="loading"
|
||||
@click:append-inner="searchMedias"
|
||||
@keydown.enter="searchMedias"
|
||||
/>
|
||||
</VToolbar>
|
||||
|
||||
<VList
|
||||
v-if="items.length > 0"
|
||||
lines="three"
|
||||
>
|
||||
<template v-for="(item, i) in items" :key="i">
|
||||
<VListItem
|
||||
@click="selectMedia(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VImg
|
||||
height="75"
|
||||
width="50"
|
||||
:src="item.poster"
|
||||
aspect-ratio="2/3"
|
||||
class="object-cover rounded shadow ring-gray-500 me-3"
|
||||
cover
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
{{ item.title }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle class="mt-2" v-html="item.overview" />
|
||||
</VListItem>
|
||||
<VDivider v-if="i < items.length - 1" class="mt-1" inset />
|
||||
</template>
|
||||
</VList>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -62,7 +62,7 @@ async function handleAddDownload(_site: any = undefined,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: 600,
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -76,7 +76,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 +122,6 @@ function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||
onMounted(() => {
|
||||
getSiteIcon()
|
||||
})
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '查看详情',
|
||||
value: 1,
|
||||
props: {
|
||||
prependIcon: 'mdi-information',
|
||||
click: openTorrentDetail,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '下载种子',
|
||||
value: 2,
|
||||
props: {
|
||||
prependIcon: 'mdi-download',
|
||||
click: downloadTorrentFile,
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -163,7 +143,7 @@ const dropdownItems = ref([
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardItem class="py-1">
|
||||
<VCardTitle>
|
||||
<VCardTitle class="break-words overflow-visible whitespace-break-spaces">
|
||||
{{ media?.title }} {{ meta?.season_episode }}
|
||||
<span class="text-green-700 ms-2 text-sm">↑{{ torrent?.seeders }}</span>
|
||||
<span class="text-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
||||
@@ -180,15 +160,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>
|
||||
|
||||
798
src/components/filebrowser/List.vue
Normal file
@@ -0,0 +1,798 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Axios } from 'axios'
|
||||
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 { 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({
|
||||
icons: Object,
|
||||
storage: String,
|
||||
path: String,
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
axios: Object as PropType<Axios>,
|
||||
refreshpending: Boolean,
|
||||
sort: String,
|
||||
})
|
||||
|
||||
// 对外事件
|
||||
const emit = defineEmits(['loading', 'pathchanged', 'refreshed', 'filedeleted', 'renamed'])
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 是否正在加载
|
||||
const loading = ref(true)
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 存储空间类型
|
||||
const storage = ref(inProps.storage ?? '')
|
||||
|
||||
// axios实例
|
||||
const axiosInstance = ref<Axios>(inProps.axios ?? axios)
|
||||
|
||||
// 内容列表
|
||||
const items = ref<FileItem[]>([])
|
||||
|
||||
// 过滤条件
|
||||
const filter = ref('')
|
||||
|
||||
// 重命名弹窗
|
||||
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)),
|
||||
)
|
||||
|
||||
// 文件过滤
|
||||
const files = computed(() =>
|
||||
items.value.filter(item => item.type === 'file' && item.basename.includes(filter.value)),
|
||||
)
|
||||
|
||||
// 是否目录
|
||||
const isDir = computed(() => inProps.path?.endsWith('/'))
|
||||
|
||||
// 是否文件
|
||||
const isFile = computed(() => !isDir.value)
|
||||
|
||||
// 是否为图片文件
|
||||
const isImage = computed(() => {
|
||||
const ext = inProps.path?.split('.').pop()?.toLowerCase()
|
||||
return ['png', 'jpg', 'jpeg', 'gif', 'bmp'].includes(ext ?? '')
|
||||
})
|
||||
|
||||
// 调API加载内容
|
||||
async function load() {
|
||||
loading.value = true
|
||||
emit('loading', true)
|
||||
// 参数
|
||||
const url = inProps.endpoints?.list.url
|
||||
.replace(/{storage}/g, storage.value)
|
||||
.replace(/{path}/g, encodeURIComponent(inProps.path || ''))
|
||||
.replace(/{sort}/g, inProps.sort || 'name')
|
||||
|
||||
const config = {
|
||||
url,
|
||||
method: inProps.endpoints?.list.method || 'get',
|
||||
}
|
||||
// 加载数据
|
||||
items.value = await axiosInstance.value.request(config) ?? []
|
||||
emit('loading', false)
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 删除项目
|
||||
async function deleteItem(item: FileItem) {
|
||||
const confirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认删除${
|
||||
item.type === 'dir' ? '目录' : '文件'
|
||||
} ${item.basename}?`,
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '50rem',
|
||||
},
|
||||
})
|
||||
|
||||
if (confirmed) {
|
||||
emit('loading', true)
|
||||
const url = inProps.endpoints?.delete.url
|
||||
.replace(/{storage}/g, storage.value)
|
||||
.replace(/{path}/g, encodeURIComponent(item.path))
|
||||
|
||||
const config = {
|
||||
url,
|
||||
method: inProps.endpoints?.delete.method || 'post',
|
||||
}
|
||||
|
||||
await axiosInstance.value.request(config)
|
||||
emit('filedeleted')
|
||||
emit('loading', false)
|
||||
// 重新加载
|
||||
load()
|
||||
}
|
||||
}
|
||||
|
||||
// 切换路径
|
||||
function changePath(_path: string) {
|
||||
emit('pathchanged', _path)
|
||||
}
|
||||
|
||||
// 新窗口中下载文件
|
||||
function download(path: string) {
|
||||
if (!path)
|
||||
return
|
||||
const token = store.state.auth.token
|
||||
const url_path = inProps.endpoints?.download.url
|
||||
.replace(/{storage}/g, storage.value)
|
||||
.replace(/{path}/g, encodeURIComponent(path))
|
||||
const url = `${import.meta.env.VITE_API_BASE_URL}${url_path.slice(1)}&token=${token}`
|
||||
// 下载文件
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// 显示图片
|
||||
function getImgLink(path: string) {
|
||||
if (!path)
|
||||
return ''
|
||||
const token = store.state.auth.token
|
||||
const url_path = inProps.endpoints?.image.url
|
||||
.replace(/{storage}/g, storage.value)
|
||||
.replace(/{path}/g, encodeURIComponent(path))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}${url_path.slice(1)}&token=${token}`
|
||||
}
|
||||
|
||||
// 显示重命名弹窗
|
||||
function showRenmae(item: FileItem) {
|
||||
currentItem.value = item
|
||||
newName.value = item.name
|
||||
renamePopper.value = true
|
||||
}
|
||||
|
||||
// 重命名
|
||||
async function rename() {
|
||||
emit('loading', true)
|
||||
const url = inProps.endpoints?.rename.url
|
||||
.replace(/{storage}/g, inProps.storage)
|
||||
.replace(/{path}/g, encodeURIComponent(currentItem.value?.path || ''))
|
||||
.replace(/{newname}/g, encodeURIComponent(newName.value))
|
||||
|
||||
const config = {
|
||||
url,
|
||||
method: inProps.endpoints?.mkdir.method || 'post',
|
||||
}
|
||||
|
||||
// 调API
|
||||
await inProps.axios?.request(config)
|
||||
|
||||
renamePopper.value = false
|
||||
newName.value = ''
|
||||
emit('loading', false)
|
||||
|
||||
// 通知重新加载
|
||||
emit('renamed')
|
||||
}
|
||||
|
||||
// 显示整理对话框
|
||||
function showTransfer(item: FileItem) {
|
||||
currentItem.value = item
|
||||
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()
|
||||
}
|
||||
|
||||
// 监听path变化
|
||||
watch(
|
||||
() => inProps.path,
|
||||
async () => {
|
||||
items.value = []
|
||||
nameTestResult.value = undefined
|
||||
nameTestDialog.value = false
|
||||
await load()
|
||||
},
|
||||
)
|
||||
|
||||
// 监听refreshPending变化
|
||||
watch(
|
||||
() => inProps.refreshpending,
|
||||
async () => {
|
||||
if (inProps.refreshpending) {
|
||||
await load()
|
||||
emit('refreshed')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 使用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 {
|
||||
// 显示进度条
|
||||
progressDialog.value = true
|
||||
progressText.value = `正在识别 ${path} ...`
|
||||
progressValue.value = 0
|
||||
nameTestResult.value = await api.get('media/recognize_file', {
|
||||
params: {
|
||||
path,
|
||||
},
|
||||
})
|
||||
// 关闭进度条
|
||||
progressDialog.value = false
|
||||
if (!nameTestResult.value)
|
||||
$toast.error(`${path} 识别失败!`)
|
||||
nameTestDialog.value = !!nameTestResult.value?.meta_info?.name
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
title: '识别',
|
||||
value: 1,
|
||||
props: {
|
||||
prependIcon: 'mdi-text-recognition',
|
||||
click: (_item: FileItem) => {
|
||||
recognize(_item.path || '')
|
||||
},
|
||||
},
|
||||
}, {
|
||||
title: '重命名',
|
||||
value: 2,
|
||||
props: {
|
||||
prependIcon: 'mdi-rename',
|
||||
click: showRenmae,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '整理',
|
||||
value: 3,
|
||||
props: {
|
||||
prependIcon: 'mdi-folder-arrow-right',
|
||||
click: showTransfer,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '删除',
|
||||
value: 4,
|
||||
props: {
|
||||
prependIcon: 'mdi-delete-outline',
|
||||
color: 'error',
|
||||
click: deleteItem,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="d-flex flex-column">
|
||||
<VCardText
|
||||
v-if="loading"
|
||||
class="text-center flex flex-col items-center"
|
||||
>
|
||||
<VProgressCircular
|
||||
size="48"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</VCardText>
|
||||
<VCardText
|
||||
v-if="!path"
|
||||
class="grow d-flex justify-center align-center grey--text"
|
||||
>
|
||||
选择目录或文件
|
||||
</VCardText>
|
||||
<VCardText
|
||||
v-else-if="isFile && !isImage"
|
||||
class="text-center break-all"
|
||||
>
|
||||
<strong>{{ items[0]?.name }}</strong><br>
|
||||
大小:{{ formatBytes(items[0]?.size || 0) }}<br>
|
||||
修改时间:{{ formatTime(items[0]?.modify_time || 0) }}
|
||||
</VCardText>
|
||||
<VCardText
|
||||
v-else-if="isFile && isImage"
|
||||
class="grow d-flex justify-center align-center"
|
||||
>
|
||||
<VImg :src="getImgLink(path)" max-width="100%" max-height="100%" />
|
||||
</VCardText>
|
||||
<VCardText v-else-if="dirs.length || files.length" class="p-0">
|
||||
<VList v-if="dirs.length" subheader>
|
||||
<VListSubheader>目录</VListSubheader>
|
||||
<VListItem
|
||||
v-for="(item, index) in dirs"
|
||||
:key="index"
|
||||
class="px-3 pe-1"
|
||||
@click="changePath(item.path)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-folder-outline" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.name" />
|
||||
<template #append>
|
||||
<IconBtn class="d-sm-none">
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click="menu.props.click(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
||||
<VIcon icon="mdi-text-recognition" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
||||
<VIcon icon="mdi-rename" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
||||
<VIcon icon="mdi-folder-arrow-right" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
||||
<VIcon icon="mdi-delete-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<VDivider v-if="dirs.length && files.length" />
|
||||
<VList v-if="files.length" subheader>
|
||||
<VListSubheader>文件</VListSubheader>
|
||||
<VListItem
|
||||
v-for="(item, index) in files"
|
||||
:key="index"
|
||||
class="pl-3 pe-1"
|
||||
@click="changePath(item.path)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon v-if="inProps.icons" :icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other" />
|
||||
</template>
|
||||
|
||||
<VListItemTitle v-text="item.name" />
|
||||
<VListItemSubtitle> {{ formatBytes(item.size) }}</VListItemSubtitle>
|
||||
|
||||
<template #append>
|
||||
<IconBtn class="d-sm-none">
|
||||
<VIcon
|
||||
icon="mdi-dots-vertical"
|
||||
/>
|
||||
<VMenu
|
||||
activator="parent"
|
||||
close-on-content-click
|
||||
>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="menu.props.color"
|
||||
@click="menu.props.click(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="recognize(item.path)">
|
||||
<VIcon icon="mdi-text-recognition" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showRenmae(item)">
|
||||
<VIcon icon="mdi-rename" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="showTransfer(item)">
|
||||
<VIcon icon="mdi-folder-arrow-right" />
|
||||
</IconBtn>
|
||||
<IconBtn class="d-none d-sm-block" @click.stop="deleteItem(item)">
|
||||
<VIcon icon="mdi-delete-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
<VCardText
|
||||
v-else-if="filter"
|
||||
class="grow d-flex justify-center align-center grey--text py-5"
|
||||
>
|
||||
没有目录或文件
|
||||
</VCardText>
|
||||
<VCardText
|
||||
v-else-if="!loading"
|
||||
class="grow d-flex justify-center align-center grey--text py-5"
|
||||
>
|
||||
空目录
|
||||
</VCardText>
|
||||
<VDivider v-if="path" />
|
||||
<VToolbar v-if="!loading" density="compact" flat color="gray">
|
||||
<VTextField
|
||||
v-if="!isFile"
|
||||
v-model="filter"
|
||||
hide-details
|
||||
flat
|
||||
density="compact"
|
||||
variant="solo-filled"
|
||||
placeholder="搜索 ..."
|
||||
prepend-inner-icon="mdi-filter-outline"
|
||||
class="me-2"
|
||||
/>
|
||||
<VSpacer v-if="isFile" />
|
||||
<IconBtn v-if="isFile" @click="recognize(inProps.path || '')">
|
||||
<VIcon color="primary">
|
||||
mdi-text-recognition
|
||||
</VIcon>
|
||||
</IconBtn>
|
||||
<IconBtn v-if="isFile" @click="download(inProps.path || '')">
|
||||
<VIcon color="primary">
|
||||
mdi-download
|
||||
</VIcon>
|
||||
</IconBtn>
|
||||
<IconBtn v-if="!isFile" @click="load">
|
||||
<VIcon color="primary">
|
||||
mdi-refresh
|
||||
</VIcon>
|
||||
</IconBtn>
|
||||
</VToolbar>
|
||||
</VCard>
|
||||
<!-- 重命名弹窗 -->
|
||||
<VDialog
|
||||
v-model="renamePopper"
|
||||
max-width="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>
|
||||
<VBtn
|
||||
:disabled="!newName"
|
||||
depressed
|
||||
@click="rename"
|
||||
>
|
||||
重命名
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 文件整理弹窗 -->
|
||||
<VDialog
|
||||
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}`">
|
||||
<DialogCloseBtn @click="transferPopper = false" />
|
||||
<VCardText class="pt-2">
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
>
|
||||
<VTextField
|
||||
v-model="transferForm.target"
|
||||
label="目的路径"
|
||||
placeholder="留空自动"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="transferForm.transfer_type"
|
||||
label="整理方式"
|
||||
:items="[
|
||||
{ title: '默认', value: '' },
|
||||
{ title: '移动', value: 'move' },
|
||||
{ title: '复制', value: 'copy' },
|
||||
{ title: '硬链接', value: 'link' },
|
||||
{ title: '软链接', value: 'softlink' },
|
||||
]"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSelect
|
||||
v-model="transferForm.type_name"
|
||||
label="类型"
|
||||
:items="[{ title: '请选择', value: '' }, { title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VTextField
|
||||
v-model="transferForm.tmdbid"
|
||||
:disabled="transferForm.type_name === ''"
|
||||
label="TMDBID"
|
||||
placeholder="留空自动识别"
|
||||
:rules="[numberValidator]"
|
||||
append-inner-icon="mdi-magnify"
|
||||
@click:append-inner="tmdbSelectorDialog = true"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSelect
|
||||
v-show="transferForm.type_name === '电视剧'"
|
||||
v-model.number="transferForm.season"
|
||||
label="季"
|
||||
:items="seasonItems"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="8">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_format"
|
||||
label="集数定位"
|
||||
placeholder="使用{ep}定位集数"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_detail"
|
||||
label="指定集数"
|
||||
placeholder="起始集,终止集,如1或1,2"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_part"
|
||||
label="指定Part"
|
||||
placeholder="如part1"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model.number="transferForm.episode_offset"
|
||||
label="集数偏移"
|
||||
placeholder="如-10"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model.number="transferForm.min_filesize"
|
||||
label="最小文件大小(MB)"
|
||||
:rules="[numberValidator]"
|
||||
placeholder="0"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn depressed @click="transferPopper = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
@click="transfer"
|
||||
>
|
||||
开始整理
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 手动整理进度框 -->
|
||||
<vDialog
|
||||
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>
|
||||
<!-- 识别结果对话框 -->
|
||||
<vDialog
|
||||
v-model="nameTestDialog"
|
||||
width="50rem"
|
||||
>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-card {
|
||||
height: 100%;
|
||||
}
|
||||
.v-toolbar{
|
||||
background: rgb(var(--v-table-header-background));
|
||||
}
|
||||
</style>
|
||||
182
src/components/filebrowser/Toolbar.vue
Normal file
@@ -0,0 +1,182 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Axios } from 'axios'
|
||||
import type { EndPoints } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const inProps = defineProps({
|
||||
storages: Array as PropType<any[]>,
|
||||
storage: String,
|
||||
path: String,
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
axios: Object as PropType<Axios>,
|
||||
})
|
||||
|
||||
// 对外事件
|
||||
const emit = defineEmits(['storagechanged', 'pathchanged', 'loading', 'foldercreated', 'sortchanged'])
|
||||
|
||||
// 新建文件夹名称
|
||||
const newFolderPopper = ref(false)
|
||||
|
||||
// 新建文件名称
|
||||
const newFolderName = ref('')
|
||||
|
||||
// 排序方式
|
||||
const sort = ref('name')
|
||||
|
||||
// 调整排序方式
|
||||
function changeSort() {
|
||||
if (sort.value === 'name')
|
||||
sort.value = 'time'
|
||||
else
|
||||
sort.value = 'name'
|
||||
|
||||
emit('sortchanged', sort.value)
|
||||
}
|
||||
|
||||
// 计算PATH面包屑
|
||||
const pathSegments = computed(() => {
|
||||
let path_str = ''
|
||||
const isFolder = inProps.path?.endsWith('/')
|
||||
const segments = inProps.path?.split('/').filter(item => item)
|
||||
|
||||
return segments?.map((item, index) => {
|
||||
path_str += item + ((index < segments.length - 1 || isFolder) ? '/' : '')
|
||||
return {
|
||||
name: item,
|
||||
path: path_str,
|
||||
}
|
||||
}) ?? []
|
||||
})
|
||||
|
||||
const storageObject = computed(() => {
|
||||
return inProps.storages?.find(item => item.code === inProps.storage)
|
||||
})
|
||||
|
||||
// 切换存储
|
||||
function changeStorage(code: string) {
|
||||
if (inProps.storage !== code) {
|
||||
emit('storagechanged', code)
|
||||
emit('pathchanged', '')
|
||||
}
|
||||
}
|
||||
|
||||
// 路径变化
|
||||
function changePath(_path: string) {
|
||||
emit('pathchanged', _path)
|
||||
}
|
||||
|
||||
// 返回上一级
|
||||
function goUp() {
|
||||
const segments = pathSegments.value ?? []
|
||||
const path = segments?.length === 1 ? '/' : segments[segments.length - 2].path
|
||||
changePath(path)
|
||||
}
|
||||
|
||||
// 创建目录
|
||||
async function mkdir() {
|
||||
emit('loading', true)
|
||||
const url = inProps.endpoints?.mkdir.url
|
||||
.replace(/{storage}/g, inProps.storage)
|
||||
.replace(/{path}/g, encodeURIComponent(inProps.path + newFolderName.value))
|
||||
|
||||
const config = {
|
||||
url,
|
||||
method: inProps.endpoints?.mkdir.method || 'post',
|
||||
}
|
||||
|
||||
// 调API
|
||||
await inProps.axios?.request(config)
|
||||
|
||||
newFolderPopper.value = false
|
||||
newFolderName.value = ''
|
||||
emit('loading', false)
|
||||
|
||||
// 通知重新加载
|
||||
emit('foldercreated')
|
||||
}
|
||||
|
||||
// 计算排序图标
|
||||
const sortIcon = computed(() => {
|
||||
if (sort.value === 'time')
|
||||
return 'mdi-sort-clock-ascending-outline'
|
||||
else
|
||||
return 'mdi-sort-alphabetical-ascending'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VToolbar flat dense>
|
||||
<VToolbarItems class="overflow-hidden">
|
||||
<VMenu v-if="inProps.storages?.length || 0 > 1" offset-y>
|
||||
<template #activator="{ props }">
|
||||
<VBtn v-bind="props">
|
||||
<VIcon icon="mdi-arrow-down-drop-circle-outline" />
|
||||
</VBtn>
|
||||
</template>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, index) in storages"
|
||||
:key="index"
|
||||
:disabled="item.code === storageObject?.code"
|
||||
@click="changeStorage(item.code)"
|
||||
>
|
||||
<template #prepend>
|
||||
<Icon :icon="item.icon" />
|
||||
</template>
|
||||
<VListItemTitle>{{ item.name }}</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
<VBtn variant="text" :input-value="path === '/'" class="px-1" @click="changePath('/')">
|
||||
<VIcon :icon="storageObject?.icon" class="mr-2" />
|
||||
{{ storageObject?.name }}
|
||||
</VBtn>
|
||||
<template v-for="(segment, index) in pathSegments" :key="index">
|
||||
<VBtn
|
||||
variant="text"
|
||||
:input-value="index === pathSegments.length - 1"
|
||||
class="px-1 d-none d-md-block"
|
||||
@click="changePath(segment.path)"
|
||||
>
|
||||
<VIcon icon=" mdi-chevron-right" />
|
||||
{{ segment.name }}
|
||||
</VBtn>
|
||||
</template>
|
||||
</VToolbarItems>
|
||||
<div class="flex-grow-1" />
|
||||
<IconBtn @click="changeSort">
|
||||
<VIcon :icon="sortIcon" />
|
||||
</IconBtn>
|
||||
<IconBtn v-if="pathSegments.length > 0" @click="goUp">
|
||||
<VIcon icon="mdi-arrow-up-bold-outline" />
|
||||
</IconBtn>
|
||||
<VDialog
|
||||
v-model="newFolderPopper"
|
||||
max-width="50rem"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<IconBtn title="新建文件夹" v-bind="props">
|
||||
<VIcon icon="mdi-folder-plus-outline" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VCard title="新建文件夹">
|
||||
<VCardText>
|
||||
<VTextField v-model="newFolderName" label="名称" />
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<div class="flex-grow-1" />
|
||||
<VBtn depressed @click="newFolderPopper = false">
|
||||
取消
|
||||
</VBtn>
|
||||
<VBtn
|
||||
:disabled="!newFolderName"
|
||||
depressed
|
||||
@click="mkdir"
|
||||
>
|
||||
新建
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</VToolbar>
|
||||
</template>
|
||||
203
src/components/filebrowser/Tree.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Axios } from 'axios'
|
||||
import type { EndPoints, FileItem } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
icons: Object,
|
||||
storage: String,
|
||||
path: String,
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
axios: Object as PropType<Axios>,
|
||||
refreshpending: Boolean,
|
||||
})
|
||||
|
||||
// 对外事件
|
||||
const emit = defineEmits(['pathchanged', 'loading', 'refreshed'])
|
||||
|
||||
// 变量
|
||||
const open = ref<string[]>([])
|
||||
// 活跃的文件夹
|
||||
const active = ref<string[]>([])
|
||||
// 内容
|
||||
const items = ref<FileItem[]>([])
|
||||
// 过滤
|
||||
const filter = ref('')
|
||||
|
||||
// 方法
|
||||
function init() {
|
||||
open.value = []
|
||||
items.value = [{
|
||||
type: 'dir',
|
||||
path: '/',
|
||||
basename: 'root',
|
||||
extension: '',
|
||||
name: 'root',
|
||||
children: [],
|
||||
size: 0,
|
||||
modify_time: 0,
|
||||
}]
|
||||
}
|
||||
|
||||
// 调用API读取文件夹
|
||||
async function readFolder(item: FileItem) {
|
||||
emit('loading', true)
|
||||
const url = props.endpoints?.list.url
|
||||
.replace(/{storage}/g, props.storage)
|
||||
.replace(/{path}/g, item.path)
|
||||
|
||||
const config = {
|
||||
url,
|
||||
method: props.endpoints?.list.method || 'get',
|
||||
}
|
||||
|
||||
const response: FileItem[] = await props.axios?.request(config) ?? []
|
||||
|
||||
item.children = response.map((item: FileItem) => {
|
||||
if (item.type === 'dir')
|
||||
item.children = []
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
emit('loading', false)
|
||||
}
|
||||
|
||||
// 选中变化
|
||||
function activeChanged(_active: string[]) {
|
||||
let path = ''
|
||||
if (active.value.length)
|
||||
path = active.value[0]
|
||||
|
||||
if (props.path !== path)
|
||||
emit('pathchanged', path)
|
||||
}
|
||||
|
||||
// 查找文件
|
||||
function findItem(path: string) {
|
||||
const stack: FileItem[] = []
|
||||
stack.push(items.value[0])
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()
|
||||
if (node?.path === path) {
|
||||
return node
|
||||
}
|
||||
else if (node?.children && node.children.length) {
|
||||
for (const element of node.children)
|
||||
stack.push(element)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 监听存储空间变量
|
||||
watch(() => props.storage, () => {
|
||||
init()
|
||||
})
|
||||
|
||||
// 监听路径变化
|
||||
watch(
|
||||
() => props.path,
|
||||
() => {
|
||||
if (props.path) {
|
||||
active.value = [props.path]
|
||||
if (!open.value.includes(props.path))
|
||||
open.value.push(props.path)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 refreshPending
|
||||
watch(
|
||||
() => props.refreshpending,
|
||||
async () => {
|
||||
if (props.refreshpending && props.path) {
|
||||
const item = findItem(props.path)
|
||||
if (item) {
|
||||
await readFolder(item)
|
||||
emit('refreshed')
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard flat width="250" min-height="500" class="d-flex flex-column folders-tree-card">
|
||||
<div class="grow scroll-x">
|
||||
<VTreeview
|
||||
:open="open"
|
||||
:active="active"
|
||||
:items="items"
|
||||
:search="filter"
|
||||
:load-children="readFolder"
|
||||
item-key="path"
|
||||
item-text="basename"
|
||||
dense
|
||||
activatable
|
||||
transition
|
||||
class="folders-tree"
|
||||
@update:active="activeChanged"
|
||||
>
|
||||
<template #prepend="{ item, open }">
|
||||
<VIcon
|
||||
v-if="item.type === 'dir'"
|
||||
>
|
||||
{{ open ? 'mdi-folder-open-outline' : 'mdi-folder-outline' }}
|
||||
</VIcon>
|
||||
<VIcon v-else-if="props.icons" :icon="props.icons[item.extension.toLowerCase()] || props.icons.other" />
|
||||
</template>
|
||||
<template #label="{ item }">
|
||||
{{ item.basename }}
|
||||
<VBtn
|
||||
v-if="item.type === 'dir'"
|
||||
icon
|
||||
class="ml-1"
|
||||
@click.stop="readFolder(item)"
|
||||
>
|
||||
<VIcon class="pa-0 mdi-18px" color="grey lighten-1">
|
||||
mdi-refresh
|
||||
</VIcon>
|
||||
</VBtn>
|
||||
</template>
|
||||
</VTreeview>
|
||||
</div>
|
||||
<VDivider />
|
||||
<VToolbar
|
||||
density="compact"
|
||||
>
|
||||
<VBtn icon @click="init">
|
||||
<VIcon icon="mdi-collapse-all-outline" />
|
||||
</VBtn>
|
||||
</VToolbar>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.folders-tree-card {
|
||||
height: 100%;
|
||||
|
||||
.scroll-x {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
::v-deep .folders-tree {
|
||||
width: fit-content;
|
||||
min-width: 250px;
|
||||
|
||||
.v-treeview-node {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.v-toolbar{
|
||||
background: rgb(var(--v-table-header-background));
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
|
||||
@@ -13,11 +13,10 @@ interface RenderProps {
|
||||
// 输入参数
|
||||
const elementProps = defineProps({
|
||||
config: Object as PropType<RenderProps>,
|
||||
handler: Boolean,
|
||||
})
|
||||
|
||||
// 配置元素
|
||||
const formItem = ref<RenderProps>(elementProps.config || {
|
||||
const formItem = ref<RenderProps>(elementProps.config ?? {
|
||||
component: 'div',
|
||||
text: '',
|
||||
html: '',
|
||||
|
||||
@@ -11,6 +11,8 @@ const props = defineProps({
|
||||
const slideview_content = ref()
|
||||
// 分页切换状态
|
||||
const disabled = ref(0)
|
||||
// 记录滚动值
|
||||
const slideview_scrollLeft = ref(0)
|
||||
// 所有卡片数量
|
||||
let slide_card_length: number
|
||||
// 卡片间距
|
||||
@@ -58,6 +60,7 @@ function countMaxNumber() {
|
||||
|
||||
// 修改分页切换按钮状态
|
||||
function countDisabled() {
|
||||
slideview_scrollLeft.value = slideview_content.value.scrollLeft
|
||||
card_current = slideview_content.value.scrollLeft === 0 ? 0 : Math.trunc((slideview_content.value.scrollLeft + card_width / 2) / card_width)
|
||||
if (slide_card_length * card_width <= slideview_content.value.clientWidth)
|
||||
disabled.value = 3
|
||||
@@ -81,6 +84,12 @@ onUnmounted(() => {
|
||||
// 卸载事件
|
||||
window.removeEventListener('resize', countMaxNumber)
|
||||
})
|
||||
onActivated(() => {
|
||||
if (slideview_scrollLeft.value !== 0) {
|
||||
// console.log(`onActivated: to_scrollLeft, ${slideview_scrollLeft.value}`)
|
||||
slideview_content.value.scrollLeft = slideview_scrollLeft.value
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -9,6 +9,10 @@ import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
|
||||
import SearchBar from '@/layouts/components/SearchBar.vue'
|
||||
import ShortcutBar from '@/layouts/components/ShortcutBar.vue'
|
||||
import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
import store from '@/store'
|
||||
|
||||
// 从Vuex Store中获取superuser信息
|
||||
const superUser = store.state.auth.superUser
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -87,6 +91,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '电影',
|
||||
icon: 'mdi-movie-check-outline',
|
||||
@@ -94,19 +99,13 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '电视剧',
|
||||
icon: 'mdi-television-classic',
|
||||
to: '/subscribe-tv',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
:item="{
|
||||
title: '自定义',
|
||||
icon: 'mdi-rss',
|
||||
to: '/subscribe-rss',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
:item="{
|
||||
title: '日历',
|
||||
@@ -128,20 +127,31 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '历史记录',
|
||||
icon: 'mdi-history',
|
||||
to: '/history',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '文件管理',
|
||||
icon: 'mdi-folder-multiple-outline',
|
||||
to: '/filemanager',
|
||||
}"
|
||||
/>
|
||||
|
||||
<!-- 👉 系统 -->
|
||||
<VerticalNavSectionTitle
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
heading: '系统',
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '插件',
|
||||
icon: 'mdi-apps',
|
||||
@@ -149,6 +159,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '站点管理',
|
||||
icon: 'mdi-web',
|
||||
@@ -156,6 +167,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
|
||||
}"
|
||||
/>
|
||||
<VerticalNavLink
|
||||
v-if="superUser"
|
||||
:item="{
|
||||
title: '设定',
|
||||
icon: 'mdi-cog',
|
||||
|
||||
@@ -10,6 +10,10 @@ const themes: ThemeSwitcherTheme[] = [
|
||||
name: 'dark',
|
||||
icon: 'mdi-weather-night',
|
||||
},
|
||||
{
|
||||
name: 'purple',
|
||||
icon: 'mdi-brightness-4',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ function search() {
|
||||
>
|
||||
<VDialog
|
||||
v-model="searchDialog"
|
||||
max-width="600"
|
||||
max-width="50rem"
|
||||
transition="dialog-top-transition"
|
||||
>
|
||||
<!-- Dialog Activator -->
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import NameTestView from '@/views/system/NameTestView.vue'
|
||||
import NetTestView from '@/views/system/NetTestView.vue'
|
||||
import LoggingView from '@/views/system/LoggingView.vue'
|
||||
import RuleTestView from '@/views/system/RuleTestView.vue'
|
||||
|
||||
// App捷径
|
||||
const appsMenu = ref(false)
|
||||
@@ -10,6 +12,12 @@ const nameTestDialog = ref(false)
|
||||
|
||||
// 网络测试弹窗
|
||||
const netTestDialog = ref(false)
|
||||
|
||||
// 实时日志弹窗
|
||||
const loggingDialog = ref(false)
|
||||
|
||||
// 过滤规则弹窗
|
||||
const ruleTestDialog = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -86,13 +94,57 @@ const netTestDialog = ref(false)
|
||||
</VListItem>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow class="ma-0 mt-n1 border-t">
|
||||
<VCol
|
||||
cols="6"
|
||||
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
|
||||
@click="() => {}"
|
||||
>
|
||||
<VListItem
|
||||
class="pa-4"
|
||||
@click="loggingDialog = true"
|
||||
>
|
||||
<VAvatar
|
||||
size="48"
|
||||
variant="tonal"
|
||||
>
|
||||
<VIcon icon="mdi-file-document-outline" />
|
||||
</VAvatar>
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||
日志
|
||||
</h6>
|
||||
<span class="text-sm">系统实时日志</span>
|
||||
</VListItem>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="6"
|
||||
class="text-center cursor-pointer pa-0 shortcut-icon border-e"
|
||||
@click="() => {}"
|
||||
>
|
||||
<VListItem
|
||||
class="pa-4"
|
||||
@click="ruleTestDialog = true"
|
||||
>
|
||||
<VAvatar
|
||||
size="48"
|
||||
variant="tonal"
|
||||
>
|
||||
<VIcon icon="mdi-filter-cog-outline" />
|
||||
</VAvatar>
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">
|
||||
优先级
|
||||
</h6>
|
||||
<span class="text-sm">优先级规则测试</span>
|
||||
</VListItem>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VCard>
|
||||
</VMenu>
|
||||
<!-- 名称测试弹窗 -->
|
||||
<VDialog
|
||||
v-model="nameTestDialog"
|
||||
max-width="800"
|
||||
max-width="50rem"
|
||||
>
|
||||
<VCard title="名称识别测试">
|
||||
<DialogCloseBtn @click="nameTestDialog = false" />
|
||||
@@ -104,7 +156,7 @@ const netTestDialog = ref(false)
|
||||
<!-- 网络测试弹窗 -->
|
||||
<VDialog
|
||||
v-model="netTestDialog"
|
||||
max-width="600"
|
||||
max-width="35rem"
|
||||
>
|
||||
<VCard title="网络测试">
|
||||
<DialogCloseBtn @click="netTestDialog = false" />
|
||||
@@ -113,4 +165,30 @@ const netTestDialog = ref(false)
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 实时日志弹窗 -->
|
||||
<VDialog
|
||||
v-model="loggingDialog"
|
||||
class="w-full lg:w-4/5"
|
||||
scrollable
|
||||
>
|
||||
<VCard title="实时日志">
|
||||
<DialogCloseBtn @click="loggingDialog = false" />
|
||||
<VCardText>
|
||||
<LoggingView />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 规则测试弹窗 -->
|
||||
<VDialog
|
||||
v-model="ruleTestDialog"
|
||||
max-width="50rem"
|
||||
scrollable
|
||||
>
|
||||
<VCard title="优先级测试">
|
||||
<DialogCloseBtn @click="ruleTestDialog = false" />
|
||||
<VCardText>
|
||||
<RuleTestView />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { useStore } from 'vuex'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import router from '@/router'
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import api from '@/api'
|
||||
|
||||
// Vuex Store
|
||||
const store = useStore()
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 进度框
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 执行注销操作
|
||||
function logout() {
|
||||
// 清除登录状态信息
|
||||
@@ -14,8 +27,45 @@ function logout() {
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
const accountInfo: any = inject('accountInfo')
|
||||
// 执行重启操作
|
||||
async function restart() {
|
||||
// 弹出提示
|
||||
const confirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: '确认重启系统吗?',
|
||||
confirmationText: '确认',
|
||||
cancellationText: '取消',
|
||||
dialogProps: {
|
||||
maxWidth: '30rem',
|
||||
},
|
||||
})
|
||||
|
||||
if (confirmed) {
|
||||
// 调用API重启
|
||||
try {
|
||||
// 显示等待框
|
||||
progressDialog.value = true
|
||||
const result: { [key: string]: any } = await api.get('system/restart')
|
||||
if (!result?.success) {
|
||||
// 隐藏等待框
|
||||
progressDialog.value = false
|
||||
// 重启不成功
|
||||
$toast.error(result.message)
|
||||
return
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
// 注销
|
||||
logout()
|
||||
}
|
||||
}
|
||||
|
||||
// 从Vuex Store中获取信息
|
||||
const superUser = store.state.auth.superUser
|
||||
const userName = store.state.auth.userName
|
||||
const avatar = store.state.auth.avatar
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -24,7 +74,7 @@ const accountInfo: any = inject('accountInfo')
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
>
|
||||
<VImg :src="accountInfo.avatar" />
|
||||
<VImg :src="avatar ?? avatar1" />
|
||||
|
||||
<!-- SECTION Menu -->
|
||||
<VMenu
|
||||
@@ -42,20 +92,21 @@ const accountInfo: any = inject('accountInfo')
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
>
|
||||
<VImg :src="accountInfo.avatar" />
|
||||
<VImg :src="avatar ?? avatar1" />
|
||||
</VAvatar>
|
||||
</VListItemAction>
|
||||
</template>
|
||||
|
||||
<VListItemTitle class="font-weight-semibold">
|
||||
{{ accountInfo.is_superuser ? "管理员" : "普通用户" }}
|
||||
{{ superUser ? "管理员" : "普通用户" }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle>{{ accountInfo.name }}</VListItemSubtitle>
|
||||
<VListItemSubtitle>{{ userName }}</VListItemSubtitle>
|
||||
</VListItem>
|
||||
<VDivider class="my-2" />
|
||||
|
||||
<!-- 👉 Profile -->
|
||||
<VListItem
|
||||
v-if="superUser"
|
||||
link
|
||||
to="setting"
|
||||
>
|
||||
@@ -89,6 +140,19 @@ const accountInfo: any = inject('accountInfo')
|
||||
<!-- 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>
|
||||
@@ -105,4 +169,22 @@ const accountInfo: any = inject('accountInfo')
|
||||
</VMenu>
|
||||
<!-- !SECTION -->
|
||||
</VAvatar>
|
||||
<!-- 重启进度框 -->
|
||||
<vDialog
|
||||
v-model="progressDialog"
|
||||
width="25rem"
|
||||
>
|
||||
<vCard
|
||||
color="primary"
|
||||
>
|
||||
<vCardText class="text-center">
|
||||
正在重启 ...
|
||||
<vProgressLinear
|
||||
indeterminate
|
||||
color="white"
|
||||
class="mb-0 mt-1"
|
||||
/>
|
||||
</vCardText>
|
||||
</vCard>
|
||||
</vDialog>
|
||||
</template>
|
||||
|
||||
@@ -6,14 +6,14 @@ import DefaultLayoutWithVerticalNav from './components/DefaultLayoutWithVertical
|
||||
<DefaultLayoutWithVerticalNav>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<component :is="Component" v-if="$route.meta.keepAlive" :key="$route.name" />
|
||||
<component :is="Component" v-if="$route.meta.keepAlive" :key="$route.fullPath" />
|
||||
</keep-alive>
|
||||
<component :is="Component" v-if="!$route.meta.keepAlive" :key="$route.name" />
|
||||
<component :is="Component" v-if="!$route.meta.keepAlive" :key="$route.fullPath" />
|
||||
</router-view>
|
||||
</DefaultLayoutWithVerticalNav>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
// As we are using `layouts` plugin we need its styles to be imported
|
||||
@use '@layouts/styles/default-layout';
|
||||
@use "@layouts/styles/default-layout";
|
||||
</style>
|
||||
|
||||
45
src/main.ts
@@ -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'
|
||||
@@ -12,53 +11,21 @@ import '@core/scss/template/index.scss'
|
||||
import '@layouts/styles/index.scss'
|
||||
import '@styles/styles.scss'
|
||||
import 'vue-toast-notification/dist/theme-default.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'))
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import AnalyticsMediaStatistic from '@/views/dashboard/AnalyticsMediaStatistic.vue'
|
||||
import AnalyticsProcesses from '@/views/dashboard/AnalyticsProcesses.vue'
|
||||
import AnalyticsScheduler from '@/views/dashboard/AnalyticsScheduler.vue'
|
||||
import AnalyticsSpeed from '@/views/dashboard/AnalyticsSpeed.vue'
|
||||
import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue'
|
||||
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
|
||||
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
|
||||
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -44,8 +45,18 @@ import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.v
|
||||
<AnalyticsScheduler />
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<AnalyticsProcesses />
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AnalyticsCpu />
|
||||
</VCol>
|
||||
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AnalyticsMemory />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
7
src/pages/filemanager.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import FileBrowserView from '@/views/reorganize/FileBrowserView.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FileBrowserView />
|
||||
</template>
|
||||
@@ -66,10 +66,16 @@ function login() {
|
||||
.then((response: any) => {
|
||||
// 获取token
|
||||
const token = response.access_token
|
||||
const superuser = response.super_user
|
||||
const username = response.user_name
|
||||
const avatar = response.avatar
|
||||
|
||||
// 更新token和remember状态到Vuex Store
|
||||
store.dispatch('auth/updateToken', token)
|
||||
store.dispatch('auth/updateRemember', form.value.remember)
|
||||
store.dispatch('auth/updateSuperUser', superuser)
|
||||
store.dispatch('auth/updateUserName', username)
|
||||
store.dispatch('auth/updateAvatar', avatar)
|
||||
|
||||
// 跳转到首页
|
||||
router.push('/')
|
||||
@@ -124,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>
|
||||
|
||||
|
||||
@@ -10,6 +10,12 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
|
||||
title="流行趋势"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="douban/showing"
|
||||
linkurl="/browse/douban/showing?title=正在热映"
|
||||
title="正在热映"
|
||||
/>
|
||||
|
||||
<MediaCardSlideView
|
||||
apipath="tmdb/movies"
|
||||
linkurl="/browse/tmdb/movies?title=热门电影"
|
||||
|
||||
@@ -5,8 +5,10 @@ import AccountSettingNotification from '@/views/setting/AccountSettingNotificati
|
||||
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
|
||||
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
|
||||
import AccountSettingWords from '@/views/setting/AccountSettingWords.vue'
|
||||
import AccountSettingLogging from '@/views/setting/AccountSettingLogging.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,6 +26,21 @@ const tabs = [
|
||||
icon: 'mdi-web',
|
||||
tab: 'site',
|
||||
},
|
||||
{
|
||||
title: '搜索',
|
||||
icon: 'mdi-magnify',
|
||||
tab: 'search',
|
||||
},
|
||||
{
|
||||
title: '订阅',
|
||||
icon: 'mdi-rss',
|
||||
tab: 'subscribe',
|
||||
},
|
||||
{
|
||||
title: '服务',
|
||||
icon: 'mdi-list-box',
|
||||
tab: 'service',
|
||||
},
|
||||
{
|
||||
title: '规则',
|
||||
icon: 'mdi-filter-cog',
|
||||
@@ -39,11 +56,6 @@ const tabs = [
|
||||
icon: 'mdi-file-word-box',
|
||||
tab: 'words',
|
||||
},
|
||||
{
|
||||
title: '日志',
|
||||
icon: 'mdi-text-box',
|
||||
tab: 'logging',
|
||||
},
|
||||
{
|
||||
title: '关于',
|
||||
icon: 'mdi-information',
|
||||
@@ -54,7 +66,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 }}
|
||||
@@ -62,47 +77,66 @@ 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="search">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingSearch />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 订阅 -->
|
||||
<VWindowItem value="subscribe">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingSubscribe />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 服务 -->
|
||||
<VWindowItem value="service">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingService />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 规则 -->
|
||||
<VWindowItem value="filter">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingRule />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- Notification -->
|
||||
<!-- 通知 -->
|
||||
<VWindowItem value="notification">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingNotification />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<!-- Words -->
|
||||
<!-- 词表 -->
|
||||
<VWindowItem value="words">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingWords />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<!-- Logging -->
|
||||
<VWindowItem value="logging">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingLogging />
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
<!-- About -->
|
||||
<!-- 关于 -->
|
||||
<VWindowItem value="about">
|
||||
<transition name="fade-slide" appear>
|
||||
<AccountSettingAbout />
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import RssListView from '@/views/subscribe/RssListView.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<RssListView />
|
||||
</div>
|
||||
</template>
|
||||
@@ -55,6 +55,56 @@ const theme: VuetifyOptions['theme'] = {
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
dark: true,
|
||||
colors: {
|
||||
'primary': '#6E66ED',
|
||||
'secondary': '#8A8D93',
|
||||
'on-secondary': '#fff',
|
||||
'success': '#56CA00',
|
||||
'info': '#16B1FF',
|
||||
'warning': '#FFB400',
|
||||
'error': '#FF4C51',
|
||||
'on-primary': '#FFFFFF',
|
||||
'on-success': '#FFFFFF',
|
||||
'on-warning': '#FFFFFF',
|
||||
'background': '#111827',
|
||||
'on-background': '#E7E3FC',
|
||||
'surface': '#161D2C',
|
||||
'on-surface': '#E7E3FC',
|
||||
'grey-50': '#2A2E42',
|
||||
'grey-100': '#474360',
|
||||
'grey-200': '#4A5072',
|
||||
'grey-300': '#5E6692',
|
||||
'grey-400': '#7983BB',
|
||||
'grey-500': '#8692D0',
|
||||
'grey-600': '#AAB3DE',
|
||||
'grey-700': '#B6BEE3',
|
||||
'grey-800': '#CFD3EC',
|
||||
'grey-900': '#E7E9F6',
|
||||
'perfect-scrollbar-thumb': '#4A5072',
|
||||
'skin-bordered-background': '#312d4b',
|
||||
'skin-bordered-surface': '#312d4b',
|
||||
},
|
||||
variables: {
|
||||
'code-color': '#d400ff',
|
||||
'overlay-scrim-background': '#1F2937',
|
||||
'overlay-scrim-opacity': 0.6,
|
||||
'hover-opacity': 0.04,
|
||||
'focus-opacity': 0.1,
|
||||
'selected-opacity': 0.12,
|
||||
'activated-opacity': 0.1,
|
||||
'pressed-opacity': 0.14,
|
||||
'dragged-opacity': 0.1,
|
||||
'border-color': '#E7E3FC',
|
||||
'table-header-background': '#1F2937',
|
||||
'custom-background': '#373452',
|
||||
// Shadows
|
||||
'shadow-key-umbra-opacity': 'rgba(20, 18, 33, 0.08)',
|
||||
'shadow-key-penumbra-opacity': 'rgba(20, 18, 33, 0.12)',
|
||||
'shadow-key-ambient-opacity': 'rgba(20, 18, 33, 0.04)',
|
||||
},
|
||||
},
|
||||
purple: {
|
||||
dark: true,
|
||||
colors: {
|
||||
'primary': '#9155FD',
|
||||
|
||||
@@ -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: [
|
||||
@@ -48,13 +56,6 @@ const router = createRouter({
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'subscribe-rss',
|
||||
component: () => import('../pages/subscribe-rss.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'calendar',
|
||||
component: () => import('../pages/calendar.vue'),
|
||||
@@ -128,6 +129,13 @@ const router = createRouter({
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/filemanager',
|
||||
component: () => import('../pages/filemanager.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -147,4 +155,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
|
||||
|
||||
@@ -4,6 +4,9 @@ import type { Module } from 'vuex'
|
||||
interface AuthState {
|
||||
token: string | null
|
||||
remember: boolean
|
||||
superUser: boolean
|
||||
userName: string
|
||||
avatar: string
|
||||
}
|
||||
|
||||
// 定义根状态类型
|
||||
@@ -17,6 +20,9 @@ const authModule: Module<AuthState, RootState> = {
|
||||
state: {
|
||||
token: null,
|
||||
remember: false,
|
||||
superUser: false,
|
||||
userName: '',
|
||||
avatar: '',
|
||||
},
|
||||
mutations: {
|
||||
setToken(state, token: string) {
|
||||
@@ -28,6 +34,15 @@ const authModule: Module<AuthState, RootState> = {
|
||||
setRemember(state, remember: boolean) {
|
||||
state.remember = remember
|
||||
},
|
||||
setSuperUser(state, superUser: boolean) {
|
||||
state.superUser = superUser
|
||||
},
|
||||
setUserName(state, userName: string) {
|
||||
state.userName = userName
|
||||
},
|
||||
setAvatar(state, avatar: string) {
|
||||
state.avatar = avatar
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
updateToken({ commit }, token: string) {
|
||||
@@ -39,10 +54,22 @@ const authModule: Module<AuthState, RootState> = {
|
||||
updateRemember({ commit }, remember: boolean) {
|
||||
commit('setRemember', remember)
|
||||
},
|
||||
updateSuperUser({ commit }, superUser: boolean) {
|
||||
commit('setSuperUser', superUser)
|
||||
},
|
||||
updateUserName({ commit }, userName: string) {
|
||||
commit('setUserName', userName)
|
||||
},
|
||||
updateAvatar({ commit }, avatar: string) {
|
||||
commit('setAvatar', avatar)
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
getToken: state => state.token,
|
||||
getRemember: state => state.remember,
|
||||
getSuperUser: state => state.superUser,
|
||||
getUserName: state => state.userName,
|
||||
getAvatar: state => state.avatar,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
#nprogress .bar {
|
||||
background: #7D34FD !important;
|
||||
background: rgb(var(--v-theme-primary)) !important;
|
||||
top: env(safe-area-inset-top) !important;
|
||||
}
|
||||
|
||||
#nprogress .peg {
|
||||
box-shadow: 0 0 10px #7D34FD, 0 0 5px #7D34FD !important;
|
||||
box-shadow: 0 0 10px rgb(var(--v-theme-primary)), 0 0 5px rgb(var(--v-theme-primary)) !important;
|
||||
-webkit-transform: rotate(0deg) translate(0px, -1px);
|
||||
-ms-transform: rotate(0deg) translate(0px, -1px);
|
||||
transform: rotate(0deg) translate(0px, -1px);
|
||||
@@ -41,7 +40,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
/* router view transition fade-slide */
|
||||
.fade-slide-leave-active,
|
||||
.fade-slide-enter-active {
|
||||
@@ -108,7 +106,24 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-alert--variant-elevated, .v-alert--variant-flat {
|
||||
background: rgb(var(--v-table-header-background));
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
.backdrop-blur {
|
||||
--tw-backdrop-blur: blur(8px)!important;
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important;
|
||||
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important;
|
||||
}
|
||||
|
||||
.v-toolbar{
|
||||
background: rgb(var(--v-table-header-background));
|
||||
}
|
||||
|
||||
6
src/util/dom.ts
Normal 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
@@ -0,0 +1 @@
|
||||
export * from './dom'
|
||||
134
src/views/dashboard/AnalyticsCpu.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { hexToRgb } from '@layouts/utils'
|
||||
import api from '@/api'
|
||||
|
||||
const vuetifyTheme = useTheme()
|
||||
|
||||
const currentTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.colors)
|
||||
const variableTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.variables)
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
|
||||
// 时间序列
|
||||
const series = ref([
|
||||
{
|
||||
data: [0],
|
||||
},
|
||||
])
|
||||
|
||||
// 当前值
|
||||
const current = ref(0)
|
||||
|
||||
const chartOptions = controlledComputed(() => vuetifyTheme.name.value, () => {
|
||||
return {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
animations: { enabled: false },
|
||||
},
|
||||
tooltip: { enabled: false },
|
||||
grid: {
|
||||
borderColor: `rgba(${hexToRgb(String(variableTheme.value['border-color']))},${variableTheme.value['border-opacity']})`,
|
||||
strokeDashArray: 6,
|
||||
xaxis: {
|
||||
lines: { show: false },
|
||||
},
|
||||
yaxis: {
|
||||
lines: { show: true },
|
||||
},
|
||||
padding: {
|
||||
top: -10,
|
||||
left: -7,
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
width: 3,
|
||||
lineCap: 'butt',
|
||||
curve: 'smooth',
|
||||
},
|
||||
colors: [currentTheme.value.primary],
|
||||
markers: {
|
||||
size: 6,
|
||||
offsetY: 4,
|
||||
offsetX: -2,
|
||||
strokeWidth: 3,
|
||||
colors: ['transparent'],
|
||||
strokeColors: 'transparent',
|
||||
discrete: [
|
||||
{
|
||||
size: 5.5,
|
||||
seriesIndex: 0,
|
||||
strokeColor: currentTheme.value.primary,
|
||||
fillColor: currentTheme.value.surface,
|
||||
},
|
||||
],
|
||||
hover: { size: 7 },
|
||||
},
|
||||
xaxis: {
|
||||
labels: { show: false },
|
||||
axisTicks: { show: false },
|
||||
axisBorder: { show: false },
|
||||
},
|
||||
yaxis: {
|
||||
labels: { show: false },
|
||||
max: 100,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// 调用API接口获取最新CPU使用率
|
||||
async function getCpuUsage() {
|
||||
try {
|
||||
// 请求数据
|
||||
current.value = await api.get('dashboard/cpu') ?? 0
|
||||
// 添加到序列
|
||||
series.value[0].data.push(current.value)
|
||||
// 序列超过30条记录时,清掉前面的
|
||||
if (series.value[0].data.length > 30)
|
||||
series.value[0].data.shift()
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCpuUsage()// 启动定时器
|
||||
refreshTimer = setInterval(() => {
|
||||
getCpuUsage()
|
||||
}, 2000)
|
||||
})
|
||||
|
||||
// 组件卸载时停止定时器
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<h6 class="text-h6">
|
||||
CPU
|
||||
</h6>
|
||||
<VueApexCharts
|
||||
type="line"
|
||||
:options="chartOptions"
|
||||
:series="series"
|
||||
:height="150"
|
||||
/>
|
||||
|
||||
<p class="text-center font-weight-medium mb-0">
|
||||
当前:{{ current }}%
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
140
src/views/dashboard/AnalyticsMemory.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { hexToRgb } from '@layouts/utils'
|
||||
import api from '@/api'
|
||||
import { formatBytes } from '@/@core/utils/formatters'
|
||||
|
||||
const vuetifyTheme = useTheme()
|
||||
|
||||
const currentTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.colors)
|
||||
const variableTheme = controlledComputed(() => vuetifyTheme.name.value, () => vuetifyTheme.current.value.variables)
|
||||
|
||||
// 定时器
|
||||
let refreshTimer: NodeJS.Timer | null = null
|
||||
|
||||
// 时间序列
|
||||
const series = ref([
|
||||
{
|
||||
data: [0],
|
||||
},
|
||||
])
|
||||
|
||||
// 占用的内存
|
||||
const usedMemory = ref(0)
|
||||
// 内存使用百分比
|
||||
const memoryUsage = ref(0)
|
||||
|
||||
const chartOptions = controlledComputed(() => vuetifyTheme.name.value, () => {
|
||||
return {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
animations: { enabled: false },
|
||||
},
|
||||
tooltip: { enabled: false },
|
||||
grid: {
|
||||
borderColor: `rgba(${hexToRgb(String(variableTheme.value['border-color']))},${variableTheme.value['border-opacity']})`,
|
||||
strokeDashArray: 6,
|
||||
xaxis: {
|
||||
lines: { show: false },
|
||||
},
|
||||
yaxis: {
|
||||
lines: { show: true },
|
||||
},
|
||||
padding: {
|
||||
top: -10,
|
||||
left: -7,
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
width: 3,
|
||||
lineCap: 'butt',
|
||||
curve: 'smooth',
|
||||
},
|
||||
colors: [currentTheme.value.primary],
|
||||
markers: {
|
||||
size: 6,
|
||||
offsetY: 4,
|
||||
offsetX: -2,
|
||||
strokeWidth: 3,
|
||||
colors: ['transparent'],
|
||||
strokeColors: 'transparent',
|
||||
discrete: [
|
||||
{
|
||||
size: 5.5,
|
||||
seriesIndex: 0,
|
||||
strokeColor: currentTheme.value.primary,
|
||||
fillColor: currentTheme.value.surface,
|
||||
},
|
||||
],
|
||||
hover: { size: 7 },
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
xaxis: {
|
||||
labels: { show: false },
|
||||
axisTicks: { show: false },
|
||||
axisBorder: { show: false },
|
||||
},
|
||||
yaxis: {
|
||||
labels: { show: false },
|
||||
max: 100,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// 调用API接口获取最新内存使用量
|
||||
async function getMemorgUsage() {
|
||||
try {
|
||||
// 请求数据
|
||||
[usedMemory.value, memoryUsage.value] = await api.get('dashboard/memory')
|
||||
series.value[0].data.push(memoryUsage.value)
|
||||
// 序列超过30条记录时,清掉前面的
|
||||
if (series.value[0].data.length > 30)
|
||||
series.value[0].data.shift()
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getMemorgUsage()
|
||||
// 启动定时器
|
||||
refreshTimer = setInterval(() => {
|
||||
getMemorgUsage()
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
// 组件卸载时停止定时器
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<h6 class="text-h6">
|
||||
内存
|
||||
</h6>
|
||||
<VueApexCharts
|
||||
type="area"
|
||||
:options="chartOptions"
|
||||
:series="series"
|
||||
:height="150"
|
||||
/>
|
||||
|
||||
<p class="text-center font-weight-medium mb-0">
|
||||
当前:{{ formatBytes(usedMemory) }}
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -185,7 +185,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,
|
||||
@@ -430,9 +430,13 @@ onBeforeMount(() => {
|
||||
<div class="relative z-20 flex items-center false"><span>已入库</span></div>
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="flex flex-row items-baseline justify-start lg:justify-center">
|
||||
<span>{{ mediaDetail.title }}</span>
|
||||
<span v-if="mediaDetail.year" class="text-lg">({{ mediaDetail.year }})</span>
|
||||
<h1 class="d-flex flex-column flex-lg-row align-baseline justify-center justify-lg-start">
|
||||
<div class="align-self-center align-self-lg-end">
|
||||
{{ mediaDetail.title }}
|
||||
</div>
|
||||
<div v-if="mediaDetail.year" class="text-lg align-self-center align-self-lg-end">
|
||||
({{ mediaDetail.year }})
|
||||
</div>
|
||||
</h1>
|
||||
<span class="media-attributes">
|
||||
<span v-if="mediaDetail.runtime || mediaDetail.episode_run_time[0]">{{ mediaDetail.runtime || mediaDetail.episode_run_time[0] }} 分钟</span>
|
||||
@@ -608,6 +612,10 @@ onBeforeMount(() => {
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id" class="media-fact">
|
||||
<span>ID</span>
|
||||
<span class="media-fact-value">{{ mediaDetail.tmdb_id }}</span>
|
||||
</div>
|
||||
<div v-if="mediaDetail.original_title || mediaDetail.original_name" class="media-fact">
|
||||
<span>原始标题</span>
|
||||
<span class="media-fact-value">{{ mediaDetail.original_title || mediaDetail.original_name }}</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<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'
|
||||
@@ -111,7 +112,7 @@ watchEffect(() => {
|
||||
)
|
||||
})
|
||||
if (matchData.length > 0) {
|
||||
const firstData = matchData[0] as SearchTorrent
|
||||
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
|
||||
if (matchData.length > 1)
|
||||
firstData.more = matchData.slice(1)
|
||||
|
||||
@@ -134,9 +135,8 @@ async function fetchData(): Promise<Array<Context>> {
|
||||
}
|
||||
else {
|
||||
startLoadingProgress()
|
||||
const qualify = props.keyword?.startsWith('tmdb:') || props.keyword?.startsWith('douban:')
|
||||
// 优先按TMDBID精确查询
|
||||
if (qualify) {
|
||||
if (props.keyword?.startsWith('tmdb:') || props.keyword?.startsWith('douban:')) {
|
||||
searchData = await api.get(`search/media/${props.keyword}`, {
|
||||
params: {
|
||||
mtype,
|
||||
@@ -313,7 +313,7 @@ onMounted(initData)
|
||||
<span>{{ progressText }}</span>
|
||||
</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}`" :torrent="data" :more="data.more" />
|
||||
<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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -17,7 +17,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) {
|
||||
|
||||
44
src/views/reorganize/FileBrowserView.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import FileBrowser from '@/components/FileBrowser.vue'
|
||||
|
||||
const endpoints = {
|
||||
list: { url: '/filebrowser/list?path={path}&sort={sort}', method: 'get' },
|
||||
mkdir: { url: '/filebrowser/mkdir?path={path}', method: 'get' },
|
||||
delete: { url: '/filebrowser/delete?path={path}', method: 'get' },
|
||||
download: { url: '/filebrowser/download?path={path}', method: 'get' },
|
||||
image: { url: '/filebrowser/image?path={path}', method: 'get' },
|
||||
rename: { url: '/filebrowser/rename?path={path}&new_name={newname}', method: 'get' },
|
||||
}
|
||||
|
||||
// 读取下载目录
|
||||
const path = ref('/')
|
||||
|
||||
// 调用API,加载当前系统环境设置
|
||||
async function loadSystemSettings() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
if (result.success)
|
||||
path.value = result.data?.DOWNLOAD_PATH || '/'
|
||||
if (path.value && !path.value.endsWith('/'))
|
||||
path.value += '/'
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
function pathChanged(_path: string) {
|
||||
path.value = _path
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await loadSystemSettings()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FileBrowser storages="local" :tree="false" :path="path" :endpoints="endpoints" :axios="api" @pathchanged="pathChanged" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,13 +1,10 @@
|
||||
<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 { numberValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { TransferHistory } from '@/api/types'
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
import TmdbSelectorCard from '@/components/cards/TmdbSelectorCard.vue'
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
@@ -23,6 +20,7 @@ const redoType = ref('电影')
|
||||
|
||||
// 类型下拉框:电影、电视剧
|
||||
const redoTypeItems = ref([
|
||||
{ title: '自动', value: '' },
|
||||
{ title: '电影', value: '电影' },
|
||||
{ title: '电视剧', value: '电视剧' },
|
||||
])
|
||||
@@ -30,6 +28,9 @@ const redoTypeItems = ref([
|
||||
// 当前操作记录
|
||||
const currentHistory = ref<TransferHistory>()
|
||||
|
||||
// 已选中的数据
|
||||
const selected = ref<TransferHistory[]>([])
|
||||
|
||||
// 表头
|
||||
const headers = [
|
||||
{ title: '标题', key: 'title', sortable: false },
|
||||
@@ -59,6 +60,24 @@ const itemsPerPage = ref(25)
|
||||
// 当前页码
|
||||
const currentPage = ref(1)
|
||||
|
||||
// 进度条
|
||||
const progressDialog = ref(false)
|
||||
|
||||
// 进度文本
|
||||
const progressText = ref('请稍候 ...')
|
||||
|
||||
// 进度值
|
||||
const progressValue = ref(0)
|
||||
|
||||
// TMDB选择对话框
|
||||
const tmdbSelectorDialog = ref(false)
|
||||
|
||||
// 删除确认对话框
|
||||
const deleteConfirmDialog = ref(false)
|
||||
|
||||
// 确认框标题
|
||||
const confirmTitle = ref('')
|
||||
|
||||
// 获取订阅列表数据
|
||||
async function fetchData({
|
||||
page,
|
||||
@@ -113,62 +132,115 @@ const TransferDict: { [key: string]: string } = {
|
||||
|
||||
// 删除历史记录
|
||||
async function removeHistory(item: TransferHistory) {
|
||||
currentHistory.value = item
|
||||
confirmTitle.value = `确认删除 ${item.title} ${item.seasons}${item.episodes} ?`
|
||||
deleteConfirmDialog.value = true
|
||||
}
|
||||
|
||||
// 调用API删除记录
|
||||
async function remove(item: TransferHistory, deleteSrc: boolean, deleteDest: boolean) {
|
||||
try {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `同步删除 ${item.title} 对应的媒体库文件 ?`,
|
||||
confirmationText: '同步删除文件',
|
||||
cancellationText: '仅删除历史记录',
|
||||
dialogProps: {
|
||||
maxWidth: 600,
|
||||
},
|
||||
})
|
||||
|
||||
let deleteFile = false
|
||||
if (isConfirmed)
|
||||
deleteFile = true
|
||||
|
||||
// 调用删除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 rehandleHistory() {
|
||||
// 删除单条记录
|
||||
async function removeSingle(deleteSrc: boolean, deleteDest: boolean) {
|
||||
// 关闭弹窗
|
||||
deleteConfirmDialog.value = false
|
||||
if (!currentHistory.value)
|
||||
return
|
||||
// 删除
|
||||
await remove(currentHistory.value, deleteSrc, deleteDest)
|
||||
// 刷新
|
||||
fetchData({
|
||||
page: currentPage.value,
|
||||
itemsPerPage: itemsPerPage.value,
|
||||
})
|
||||
}
|
||||
|
||||
// 批量删除记录
|
||||
async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
|
||||
// 关闭弹窗
|
||||
deleteConfirmDialog.value = false
|
||||
// 总条数
|
||||
const total = selected.value.length
|
||||
if (total === 0)
|
||||
return
|
||||
// 已处理条数
|
||||
let handled = 0
|
||||
// 显示进度条
|
||||
progressDialog.value = true
|
||||
// 循环调用removeHistory
|
||||
for (const item of selected.value) {
|
||||
// 开始删除
|
||||
progressText.value = `正在删除 ${item.title} ${item.seasons}${item.episodes} ...`
|
||||
await remove(item, deleteSrc, deleteDest)
|
||||
// 删除完成
|
||||
handled++
|
||||
progressValue.value = handled / total * 100
|
||||
}
|
||||
// 清空选中项
|
||||
selected.value = []
|
||||
// 隐藏进度条
|
||||
progressDialog.value = false
|
||||
// 重新获取数据
|
||||
fetchData({
|
||||
page: currentPage.value,
|
||||
itemsPerPage: itemsPerPage.value,
|
||||
})
|
||||
}
|
||||
|
||||
// 响应删除操作
|
||||
async function deleteConfirmHandler(deleteSrc: boolean, deleteDest: boolean) {
|
||||
if (currentHistory.value)
|
||||
await removeSingle(deleteSrc, deleteDest)
|
||||
else
|
||||
await removeBatch(deleteSrc, deleteDest)
|
||||
}
|
||||
|
||||
// 批量删除历史记录
|
||||
async function removeHistoryBatch() {
|
||||
if (selected.value.length === 0)
|
||||
return
|
||||
// 清空当前操作记录
|
||||
currentHistory.value = undefined
|
||||
confirmTitle.value = `确认删除 ${selected.value.length} 条记录 ?`
|
||||
// 打开确认弹窗
|
||||
deleteConfirmDialog.value = true
|
||||
}
|
||||
|
||||
// 批量重新整理
|
||||
async function retransferBatch() {
|
||||
if (selected.value.length === 0)
|
||||
return
|
||||
// 清空当前操作记录
|
||||
currentHistory.value = undefined
|
||||
// 打开识别弹窗
|
||||
redoType.value = ''
|
||||
redoTmdbId.value = ''
|
||||
redoDialog.value = true
|
||||
}
|
||||
|
||||
// 调API重新整理
|
||||
async function retransfer(item: TransferHistory, redoType = '', redoTmdbId = 0) {
|
||||
try {
|
||||
if (!redoTmdbId.value || !redoType.value)
|
||||
return
|
||||
|
||||
redoDialog.value = false
|
||||
$toast.info(`正在重新整理 ${currentHistory.value?.title} ...`)
|
||||
|
||||
// 调用API接口重新转移
|
||||
const requestData = {
|
||||
...currentHistory.value,
|
||||
}
|
||||
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'history/transfer',
|
||||
requestData,
|
||||
item,
|
||||
{
|
||||
params: {
|
||||
mtype: redoType.value,
|
||||
new_tmdbid: parseInt(redoTmdbId.value),
|
||||
mtype: redoType,
|
||||
new_tmdbid: redoTmdbId,
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -188,6 +260,50 @@ async function rehandleHistory() {
|
||||
}
|
||||
}
|
||||
|
||||
// 重新整理
|
||||
async function rehandleHistory() {
|
||||
try {
|
||||
// 关闭弹窗
|
||||
redoDialog.value = false
|
||||
|
||||
let tmdbid = 0
|
||||
|
||||
if (redoTmdbId.value)
|
||||
tmdbid = parseInt(redoTmdbId.value)
|
||||
|
||||
// 转移当前选中记录
|
||||
if (currentHistory.value) {
|
||||
$toast.info(`正在重新整理 ${currentHistory.value?.title} ...`)
|
||||
await retransfer(currentHistory.value, redoType.value, tmdbid)
|
||||
}
|
||||
else if (selected.value.length > 0) {
|
||||
// 总条数
|
||||
const total = selected.value.length
|
||||
if (total === 0)
|
||||
return
|
||||
// 已处理条数
|
||||
let handled = 0
|
||||
// 显示进度条
|
||||
progressDialog.value = true
|
||||
for (const item of selected.value) {
|
||||
progressText.value = `正在重新整理 ${item.src} ...`
|
||||
await retransfer(item, redoType.value, tmdbid)
|
||||
handled++
|
||||
progressValue.value = handled / total * 100
|
||||
}
|
||||
// 清空选中项
|
||||
selected.value = []
|
||||
// 隐藏进度条
|
||||
progressDialog.value = false
|
||||
}
|
||||
// 批量转移
|
||||
else { $toast.error('没有选中任何记录!') }
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出菜单
|
||||
const dropdownItems = ref([
|
||||
{
|
||||
@@ -196,6 +312,8 @@ const dropdownItems = ref([
|
||||
props: {
|
||||
prependIcon: 'mdi-redo-variant',
|
||||
click: (item: TransferHistory) => {
|
||||
redoTmdbId.value = ''
|
||||
redoType.value = ''
|
||||
redoDialog.value = true
|
||||
currentHistory.value = item
|
||||
},
|
||||
@@ -207,7 +325,9 @@ const dropdownItems = ref([
|
||||
props: {
|
||||
prependIcon: 'mdi-trash-can-outline',
|
||||
color: 'error',
|
||||
click: removeHistory,
|
||||
click: (item: TransferHistory) => {
|
||||
removeHistory(item)
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -238,6 +358,7 @@ const dropdownItems = ref([
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDataTableServer
|
||||
v-model="selected"
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
:headers="headers"
|
||||
:items="dataList"
|
||||
@@ -248,6 +369,7 @@ const dropdownItems = ref([
|
||||
item-value="id"
|
||||
return-object
|
||||
fixed-header
|
||||
show-select
|
||||
items-per-page-text="每页条数"
|
||||
page-text="{0}-{1} 共 {2} 条"
|
||||
@update:options="fetchData"
|
||||
@@ -322,7 +444,7 @@ const dropdownItems = ref([
|
||||
</VCard>
|
||||
<VDialog
|
||||
v-model="redoDialog"
|
||||
max-width="600"
|
||||
max-width="50rem"
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="重新整理">
|
||||
@@ -332,7 +454,6 @@ const dropdownItems = ref([
|
||||
<VSelect
|
||||
v-model="redoType"
|
||||
label="类型"
|
||||
:rules="[requiredValidator]"
|
||||
:items="redoTypeItems"
|
||||
/>
|
||||
</VCol>
|
||||
@@ -340,7 +461,11 @@ const dropdownItems = ref([
|
||||
<VTextField
|
||||
v-model="redoTmdbId"
|
||||
label="TMDB编号"
|
||||
:rules="[requiredValidator, numberValidator]"
|
||||
placeholder="留空自动识别"
|
||||
:disabled="redoType === ''"
|
||||
:rules="[numberValidator]"
|
||||
append-inner-icon="mdi-magnify"
|
||||
@click:append-inner.stop="tmdbSelectorDialog = true"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -357,6 +482,90 @@ const dropdownItems = ref([
|
||||
</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"
|
||||
size="x-large"
|
||||
@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">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -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()
|
||||
@@ -365,7 +365,7 @@ onMounted(() => {
|
||||
</td>
|
||||
<td>{{ user.is_superuser ? "是" : "否" }}</td>
|
||||
<td>
|
||||
<IconBtn v-show="!user.is_superuser">
|
||||
<IconBtn v-show="accountInfo.is_superuser && accountInfo.name != user.name">
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu
|
||||
activator="parent"
|
||||
@@ -408,7 +408,7 @@ onMounted(() => {
|
||||
<!-- 站点编辑弹窗 -->
|
||||
<VDialog
|
||||
v-model="addUserDialog"
|
||||
max-width="800"
|
||||
max-width="50rem"
|
||||
persistent
|
||||
>
|
||||
<!-- Dialog Content -->
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import store from '@/store'
|
||||
|
||||
// 日志列表
|
||||
const logs = ref<string[]>([])
|
||||
|
||||
// SSE持续获取日志
|
||||
function startSSELogging() {
|
||||
const token = store.state.auth.token
|
||||
if (token) {
|
||||
const eventSource = new EventSource(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}`,
|
||||
)
|
||||
|
||||
eventSource.addEventListener('message', (event) => {
|
||||
const message = event.data
|
||||
if (message)
|
||||
logs.value.push(message)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
eventSource.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startSSELogging()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="实时日志">
|
||||
<VCardText>
|
||||
<div
|
||||
v-if="logs.length === 0"
|
||||
class="mt-5 w-full text-center text-gray-500 text-sm flex flex-col items-center"
|
||||
>
|
||||
<VProgressCircular
|
||||
size="48"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<span class="mt-3">正在刷新 ...</span>
|
||||
</div>
|
||||
<div v-for="(log, i) in logs" :key="i">
|
||||
{{ log }}
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
@@ -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
|
||||
|
||||
@@ -1,20 +1,6 @@
|
||||
<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[]
|
||||
|
||||
// 是否可见
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
@@ -28,35 +14,11 @@ const TorrentPriorityItems = [
|
||||
{ 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('&'),
|
||||
visible: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
// 包含与排除规则
|
||||
const defaultFilterRules = ref({
|
||||
include: '',
|
||||
exclude: '',
|
||||
})
|
||||
|
||||
// 查询种子优先规则
|
||||
async function queryTorrentPriority() {
|
||||
@@ -72,31 +34,14 @@ async function queryTorrentPriority() {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户设置的规则
|
||||
async function saveCustomFilters(ruleType: string) {
|
||||
// 查询包含与排除规则
|
||||
async function queryDefaultFilter() {
|
||||
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 result: { [key: string]: any } = await api.get(
|
||||
'system/setting/DefaultFilterRules',
|
||||
)
|
||||
|
||||
const msg = ruleType === 'FilterRules' ? '过滤规则' : '洗版规则'
|
||||
|
||||
if (result.success)
|
||||
$toast.success(`${msg}保存成功`)
|
||||
else
|
||||
$toast.error(`${msg}保存失败!`)
|
||||
if (result.data?.value)
|
||||
defaultFilterRules.value = result.data?.value
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
@@ -122,142 +67,47 @@ async function saveTorrentPriority() {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新规则卡片的值
|
||||
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) {
|
||||
// 将卡片从列表中删除,并更新剩余卡片的序号
|
||||
const cards = ruleType === 'FilterRules' ? filterCards : filterCards2
|
||||
const index = cards.value.findIndex(card => card.pri === pri)
|
||||
if (index !== -1) {
|
||||
// 创建新的数组,然后使用 splice 方法来删除元素
|
||||
const updatedCards = [...cards.value]
|
||||
|
||||
updatedCards.splice(index, 1)
|
||||
|
||||
// 更新剩余卡片的序号
|
||||
updatedCards.forEach((card, i) => {
|
||||
card.pri = (i + 1).toString()
|
||||
})
|
||||
|
||||
// 更新 filterCards.value
|
||||
cards.value = updatedCards
|
||||
// 保存包含与排除规则
|
||||
async function saveDefaultFilter() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/DefaultFilterRules',
|
||||
defaultFilterRules.value,
|
||||
)
|
||||
if (result.success)
|
||||
$toast.success('默认包含/排除规则保存成功')
|
||||
else
|
||||
$toast.error('默认包含/排除规则保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 增加卡片
|
||||
function addFilterCard(ruleType: string) {
|
||||
const cards = ruleType === 'FilterRules' ? filterCards : filterCards2
|
||||
// 优先级
|
||||
const pri = (cards.value.length + 1).toString()
|
||||
|
||||
// 新卡片
|
||||
const newCard: FilterCard = { pri, rules: [], visible: true }
|
||||
|
||||
// 添加到列表
|
||||
cards.value.push(newCard)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
queryTorrentPriority()
|
||||
queryCustomFilters('FilterRules')
|
||||
queryCustomFilters('FilterRules2')
|
||||
queryDefaultFilter()
|
||||
})
|
||||
</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"
|
||||
:visible="card.visible"
|
||||
@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"
|
||||
:visible="card.visible"
|
||||
@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="下载优先规则">
|
||||
<VCardSubtitle> 按站点优先级或资源种子数量排序和择优下载。 </VCardSubtitle>
|
||||
<VCardText>
|
||||
<VSelect
|
||||
v-model="selectedTorrentPriority"
|
||||
:items="TorrentPriorityItems"
|
||||
label="优先规则"
|
||||
outlined
|
||||
/>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="selectedTorrentPriority"
|
||||
:items="TorrentPriorityItems"
|
||||
label="优先规则"
|
||||
outlined
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</vform>
|
||||
</VCardText>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
@@ -269,6 +119,39 @@ onMounted(() => {
|
||||
</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>
|
||||
|
||||
|
||||
271
src/views/setting/AccountSettingSearch.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<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[]>([])
|
||||
|
||||
// 查询已设置优先级规则
|
||||
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))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
queryCustomFilters()
|
||||
querySites()
|
||||
})
|
||||
</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>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-filterrule-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
138
src/views/setting/AccountSettingService.vue
Normal 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>
|
||||
@@ -1,17 +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 allSites = ref<Site[]>([])
|
||||
|
||||
// 站点重置
|
||||
const isConfirmResetSites = ref(false)
|
||||
|
||||
@@ -21,48 +14,6 @@ const resetSitesText = ref('重置站点数据')
|
||||
// 站点重置按钮可用状态
|
||||
const resetSitesDisabled = ref(false)
|
||||
|
||||
// 查询所有站点
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置站点
|
||||
async function resetSites() {
|
||||
try {
|
||||
@@ -70,13 +21,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 = '重置站点数据'
|
||||
}
|
||||
@@ -84,45 +34,15 @@ async function resetSites() {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
querySites()
|
||||
})
|
||||
</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="站点重置">
|
||||
<VCardText>
|
||||
<div>
|
||||
<VCheckbox v-model="isConfirmResetSites" label="确认删除所有站点数据并重新同步" />
|
||||
<VCheckbox v-model="isConfirmResetSites" label="确认删除所有站点数据并重新同步。" />
|
||||
</div>
|
||||
|
||||
<VBtn :disabled="!isConfirmResetSites || resetSitesDisabled" color="error" class="mt-3" @click="resetSites">
|
||||
|
||||
325
src/views/setting/AccountSettingSubscribe.vue
Normal file
@@ -0,0 +1,325 @@
|
||||
<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[]>([])
|
||||
|
||||
// 查询用户选中的订阅站点
|
||||
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))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
querySites()
|
||||
queryCustomFilters('SubscribeFilterRules')
|
||||
queryCustomFilters('BestVersionFilterRules')
|
||||
})
|
||||
</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>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-filterrule-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -11,6 +11,9 @@ const customIdentifiers = ref('')
|
||||
// 自定义制作组
|
||||
const customReleaseGroups = ref('')
|
||||
|
||||
// 文件整理屏蔽词
|
||||
const transferExcludeWords = ref('')
|
||||
|
||||
// 查询已设置的识别词
|
||||
async function queryCustomIdentifiers() {
|
||||
try {
|
||||
@@ -39,6 +42,20 @@ async function queryCustomReleaseGroups() {
|
||||
}
|
||||
}
|
||||
|
||||
// 查询已设置的屏蔽词
|
||||
async function queryTransferExcludeWords() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(
|
||||
'system/setting/TransferExcludeWords',
|
||||
)
|
||||
|
||||
transferExcludeWords.value = result.data?.value.join('\n')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户设置的识别词
|
||||
async function saveCustomIdentifiers() {
|
||||
try {
|
||||
@@ -77,9 +94,29 @@ async function saveCustomReleaseGroups() {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件整理屏蔽词
|
||||
async function saveTransferExcludeWords() {
|
||||
try {
|
||||
// 用户名密码
|
||||
const result: { [key: string]: any } = await api.post(
|
||||
'system/setting/TransferExcludeWords',
|
||||
transferExcludeWords.value.split('\n'),
|
||||
)
|
||||
|
||||
if (result.success)
|
||||
$toast.success('文件整理屏蔽词保存成功')
|
||||
else
|
||||
$toast.error('文件整理屏蔽词保存失败!')
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
queryCustomIdentifiers()
|
||||
queryCustomReleaseGroups()
|
||||
queryTransferExcludeWords()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -87,15 +124,16 @@ onMounted(() => {
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard title="自定义识别词">
|
||||
<VCardSubtitle> 添加规则对种子名或者文件名进行预处理以校正识别 </VCardSubtitle>
|
||||
<VCardSubtitle> 添加规则对种子名或者文件名进行预处理以校正识别。 </VCardSubtitle>
|
||||
<VCardItem>
|
||||
<VTextarea
|
||||
v-model="customIdentifiers"
|
||||
auto-grow
|
||||
placeholder="支持正则表达式,特殊字符需要\转义,一行为一组,支持三种配置格式:
|
||||
placeholder="支持正则表达式,特殊字符需要\转义,一行为一组,支持以下几种配置格式:
|
||||
屏蔽词
|
||||
被替换词 => 替换词
|
||||
前定位词 <> 后定位词 >> 偏移量(EP)"
|
||||
前定位词 <> 后定位词 >> 集偏移量(EP)
|
||||
被替换词 => 替换词 && 前定位词 <> 后定位词 >> 集偏移量(EP)"
|
||||
/>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
@@ -110,7 +148,7 @@ onMounted(() => {
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="自定义制作组/字幕组">
|
||||
<VCardSubtitle> 添加无法识别的制作组/字幕组 </VCardSubtitle>
|
||||
<VCardSubtitle> 添加无法识别的制作组/字幕组。 </VCardSubtitle>
|
||||
<VCardItem>
|
||||
<VTextarea
|
||||
v-model="customReleaseGroups"
|
||||
@@ -128,5 +166,25 @@ onMounted(() => {
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard title="文件整理屏蔽词">
|
||||
<VCardSubtitle> 目录名或文件名中包含屏蔽词时不进行整理。 </VCardSubtitle>
|
||||
<VCardItem>
|
||||
<VTextarea
|
||||
v-model="transferExcludeWords"
|
||||
auto-grow
|
||||
placeholder="支持正则表达式,特殊字符需要\转义,一行代表一个屏蔽词"
|
||||
/>
|
||||
</VCardItem>
|
||||
<VCardItem>
|
||||
<VBtn
|
||||
type="submit"
|
||||
@click="saveTransferExcludeWords"
|
||||
>
|
||||
保存
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
@@ -58,7 +58,7 @@ const siteForm = reactive<Site>({
|
||||
// 获取站点列表数据
|
||||
async function fetchData() {
|
||||
try {
|
||||
dataList.value = await api.get('site')
|
||||
dataList.value = await api.get('site/')
|
||||
isRefreshed.value = true
|
||||
}
|
||||
catch (error) {
|
||||
@@ -77,7 +77,7 @@ async function addSite() {
|
||||
addBtnState.value = true
|
||||
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.post('site', siteForm)
|
||||
const result: { [key: string]: string } = await api.post('site/', siteForm)
|
||||
if (result.success) {
|
||||
$toast.success('新增站点成功')
|
||||
|
||||
@@ -135,7 +135,7 @@ onBeforeMount(fetchData)
|
||||
<!-- Dialog Content -->
|
||||
<VDialog
|
||||
v-model="siteAddDialog"
|
||||
max-width="800"
|
||||
max-width="50rem"
|
||||
persistent
|
||||
scrollable
|
||||
>
|
||||
@@ -149,6 +149,7 @@ onBeforeMount(fetchData)
|
||||
/>
|
||||
</template>
|
||||
<VCard title="新增站点">
|
||||
<DialogCloseBtn @click="siteAddDialog = false" />
|
||||
<VCardText class="pt-2">
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
@@ -185,6 +186,12 @@ onBeforeMount(fetchData)
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="siteForm.rss"
|
||||
label="RSS地址"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="siteForm.cookie"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CalendarOptions } from '@fullcalendar/core'
|
||||
import type { CalendarOptions, EventSourceInput } from '@fullcalendar/core'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
@@ -73,23 +73,13 @@ async function eventsHander(subscribe: Subscribe | Rss) {
|
||||
async function getSubscribes() {
|
||||
try {
|
||||
// 订阅
|
||||
const subscribes: Subscribe[] = await api.get('subscribe')
|
||||
const subscribes: Subscribe[] = await api.get('subscribe/')
|
||||
|
||||
const subEvents = await Promise.all(
|
||||
subscribes.map(async sub => eventsHander(sub)),
|
||||
)
|
||||
|
||||
// 自定义订阅
|
||||
const rsses: Rss[] = await api.get('rss')
|
||||
|
||||
const rssEvents = await Promise.all(
|
||||
rsses.map(async rss => eventsHander(rss)),
|
||||
)
|
||||
|
||||
// 合并事件
|
||||
const events = [...subEvents, ...rssEvents]
|
||||
|
||||
calendarOptions.value.events = events.flat()
|
||||
calendarOptions.value.events = subEvents.flat().filter(event => event.start) as EventSourceInput
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import PullRefresh from 'pull-refresh-vue3'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { Rss } from '@/api/types'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import RssCard from '@/components/cards/RssCard.vue'
|
||||
import { numberValidator, requiredValidator } from '@/@validators'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 是否刷新过
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 新增按钮文本
|
||||
const addBtnText = ref('新增订阅')
|
||||
// 新增按钮状态
|
||||
const addBtnState = ref(false)
|
||||
|
||||
// 新增自定义订阅对话框
|
||||
const rssAddDialog = ref(false)
|
||||
|
||||
// 新增订阅表单
|
||||
const rssForm = reactive({
|
||||
// RSS地址
|
||||
url: '',
|
||||
// 类型
|
||||
type: '电影',
|
||||
// 标题
|
||||
title: '',
|
||||
// 年份
|
||||
year: '',
|
||||
// 季号
|
||||
season: 1,
|
||||
// 包含
|
||||
include: '',
|
||||
// 排除
|
||||
exclude: '',
|
||||
// 洗版
|
||||
best_version: false,
|
||||
// 是否使用代理服务器
|
||||
proxy: false,
|
||||
// 是否使用过滤规则
|
||||
filter: true,
|
||||
// 保存路径
|
||||
save_path: '',
|
||||
// 状态 0-停用,1-启用
|
||||
state: 1,
|
||||
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<Rss[]>([])
|
||||
|
||||
// 获取订阅列表数据
|
||||
async function fetchData() {
|
||||
try {
|
||||
dataList.value = await api.get('rss')
|
||||
isRefreshed.value = true
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API 新增自定义订阅
|
||||
async function addRss() {
|
||||
if (!rssForm.url || !rssForm.title)
|
||||
return
|
||||
|
||||
startNProgress()
|
||||
|
||||
addBtnText.value = '新增中...'
|
||||
addBtnState.value = true
|
||||
|
||||
if (rssForm.type === '电影')
|
||||
rssForm.season = 0
|
||||
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.post('rss', rssForm)
|
||||
if (result.success) {
|
||||
$toast.success('新增自定义订阅成功')
|
||||
|
||||
// 刷新数据
|
||||
fetchData()
|
||||
}
|
||||
else { $toast.error(`新增自定义订阅失败:${result.message}`) }
|
||||
rssAddDialog.value = false
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
doneNProgress()
|
||||
|
||||
addBtnText.value = '新增订阅'
|
||||
addBtnState.value = false
|
||||
}
|
||||
|
||||
// 生成1到50季的下拉框选项
|
||||
const seasonItems = ref(
|
||||
Array.from({ length: 50 }, (_, i) => i + 1).map(item => ({
|
||||
title: `第 ${item} 季`,
|
||||
value: item,
|
||||
})),
|
||||
)
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(fetchData)
|
||||
|
||||
// 刷新状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 下拉刷新
|
||||
function onRefresh() {
|
||||
loading.value = true
|
||||
fetchData()
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="!isRefreshed"
|
||||
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
|
||||
>
|
||||
<VProgressCircular
|
||||
v-if="!isRefreshed"
|
||||
size="48"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
<PullRefresh
|
||||
v-model="loading"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<div
|
||||
v-if="dataList.length > 0"
|
||||
class="grid gap-3 grid-rss-card p-1"
|
||||
>
|
||||
<RssCard
|
||||
v-for="data in dataList"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
@remove="fetchData"
|
||||
@save="fetchData"
|
||||
/>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
error-title="没有自定义订阅"
|
||||
error-description="点击右下角按钮新增订阅。"
|
||||
/>
|
||||
</PullRefresh>
|
||||
|
||||
<!-- 新增订阅 -->
|
||||
<VDialog
|
||||
v-model="rssAddDialog"
|
||||
max-width="800"
|
||||
persistent
|
||||
scrollable
|
||||
>
|
||||
<!-- Dialog Activator -->
|
||||
<template #activator="{ props }">
|
||||
<VBtn
|
||||
icon="mdi-plus"
|
||||
v-bind="props"
|
||||
size="x-large"
|
||||
class="fixed right-5 bottom-5"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Dialog Content -->
|
||||
<VCard title="新增自定义订阅">
|
||||
<VCardText class="pt-2">
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.url"
|
||||
label="RSS地址"
|
||||
placeholder="https://example.com/rss"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSelect
|
||||
v-model="rssForm.type"
|
||||
label="类型"
|
||||
:items="[{ title: '电影', value: '电影' }, { title: '电视剧', value: '电视剧' }]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.title"
|
||||
label="标题"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.year"
|
||||
label="年份"
|
||||
:rules="[numberValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
v-if="rssForm.type === '电视剧'"
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSelect
|
||||
v-model="rssForm.season"
|
||||
label="季"
|
||||
:items="seasonItems"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.include"
|
||||
label="包含"
|
||||
placeholder="支持正则表达式"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.exclude"
|
||||
label="排除"
|
||||
placeholder="支持正则表达式"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
>
|
||||
<VTextField
|
||||
v-model="rssForm.save_path"
|
||||
label="保存路径"
|
||||
placeholder="留空自动"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<VSelect
|
||||
v-model="rssForm.state"
|
||||
label="状态"
|
||||
:items="[{
|
||||
title: '启用',
|
||||
value: 1,
|
||||
}, {
|
||||
title: '停用',
|
||||
value: 0,
|
||||
}]"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="rssForm.best_version"
|
||||
label="洗版"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="rssForm.proxy"
|
||||
label="代理服务器"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<VSwitch
|
||||
v-model="rssForm.filter"
|
||||
label="过滤规则"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VBtn
|
||||
@click="rssAddDialog = false"
|
||||
>
|
||||
取消
|
||||
</VBtn>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
color="primary"
|
||||
:disabled="addBtnState"
|
||||
@click="addRss"
|
||||
>
|
||||
{{ addBtnText }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-rss-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -19,7 +19,7 @@ const dataList = ref<Subscribe[]>([])
|
||||
// 获取订阅列表数据
|
||||
async function fetchData() {
|
||||
try {
|
||||
dataList.value = await api.get('subscribe')
|
||||
dataList.value = await api.get('subscribe/')
|
||||
isRefreshed.value = true
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
121
src/views/system/LoggingView.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script lang="ts" setup>
|
||||
import store from '@/store'
|
||||
|
||||
// 日志列表
|
||||
const logs = ref<string[]>([])
|
||||
|
||||
// SSE持续获取日志
|
||||
function startSSELogging() {
|
||||
const token = store.state.auth.token
|
||||
if (token) {
|
||||
const eventSource = new EventSource(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/logging?token=${token}`,
|
||||
)
|
||||
|
||||
eventSource.addEventListener('message', (event) => {
|
||||
const message = event.data
|
||||
if (message)
|
||||
logs.value.push(message)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
eventSource.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 从日志中提取日志详情
|
||||
function extractLogDetailsFromLogs(logs: string[]): { level: string; time: string; program: string; content: string }[] {
|
||||
const logDetails: { level: string; time: string; program: string; content: string }[] = []
|
||||
|
||||
const logPattern = /^【(.*?)】[0-9\-:]*\s(.*?)\s-\s(.*?)\s-\s(.*)$/
|
||||
|
||||
for (const log of logs) {
|
||||
const matches = RegExp(logPattern).exec(log)
|
||||
if (matches && matches.length === 5) {
|
||||
const [_, level, time, program, content] = matches
|
||||
logDetails.push({ level, time, program, content })
|
||||
}
|
||||
}
|
||||
|
||||
return logDetails
|
||||
}
|
||||
|
||||
// 计算日志颜色
|
||||
function getLogColor(level: string): string {
|
||||
switch (level) {
|
||||
case 'DEBUG':
|
||||
return 'primary'
|
||||
case 'INFO':
|
||||
return 'secondary'
|
||||
case 'WARNING':
|
||||
return 'warning'
|
||||
case 'ERROR':
|
||||
return 'error'
|
||||
default:
|
||||
return 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
// 拆分日志数据计算属性
|
||||
const extractLogDetails = computed(() => {
|
||||
return extractLogDetailsFromLogs(logs.value)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
startSSELogging()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="logs.length === 0"
|
||||
class="mt-5 w-full text-center flex flex-col items-center"
|
||||
>
|
||||
<VProgressCircular
|
||||
size="48"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<span class="mt-3">正在刷新 ...</span>
|
||||
</div>
|
||||
<div>
|
||||
<VTable
|
||||
class="table-rounded"
|
||||
hide-default-footer
|
||||
disable-sort
|
||||
>
|
||||
<tbody>
|
||||
<tr v-for="(log, i) in extractLogDetails" :key="i" class="text-sm">
|
||||
<td
|
||||
class="text-sm"
|
||||
>
|
||||
<VChip
|
||||
size="small"
|
||||
:color="getLogColor(log.level)"
|
||||
variant="elevated"
|
||||
v-text="log.level"
|
||||
/>
|
||||
</td>
|
||||
<!-- name -->
|
||||
<td
|
||||
class="text-sm"
|
||||
>
|
||||
{{ log.time }}
|
||||
</td>
|
||||
<td
|
||||
class="text-sm"
|
||||
>
|
||||
<h6 class="text-sm font-weight-medium">
|
||||
{{ log.program }}
|
||||
</h6>
|
||||
</td>
|
||||
<td
|
||||
class="text-sm"
|
||||
v-text="log.content"
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,6 +3,7 @@ import { reactive, ref } from 'vue'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Context } from '@/api/types'
|
||||
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
|
||||
|
||||
// 识别结果
|
||||
const nameTestResult = ref<Context>()
|
||||
@@ -45,22 +46,6 @@ async function nameTest() {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开TMDB详情页面
|
||||
function openTmdbPage(type: string, tmdbId: number) {
|
||||
if (!type || !tmdbId)
|
||||
return
|
||||
|
||||
const url = `https://www.themoviedb.org/${type === '电影' ? 'movie' : 'tv'}/${tmdbId}`
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// TMDB图片转换为w500大小
|
||||
function getW500Image(url = '') {
|
||||
if (!url)
|
||||
return ''
|
||||
return url.replace('original', 'w500')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -101,123 +86,7 @@ function getW500Image(url = '') {
|
||||
</VForm>
|
||||
<VExpandTransition>
|
||||
<div v-show="showResult">
|
||||
<VCol>
|
||||
<div
|
||||
v-if="nameTestResult?.meta_info?.name"
|
||||
class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row"
|
||||
>
|
||||
<div
|
||||
v-if="nameTestResult?.media_info?.poster_path"
|
||||
class="ma-auto"
|
||||
>
|
||||
<VImg
|
||||
width="10rem"
|
||||
aspect-ratio="2/3"
|
||||
class="object-cover aspect-w-2 aspect-h-3 rounded-lg ring-1 ring-gray-500"
|
||||
:src="getW500Image(nameTestResult?.media_info?.poster_path)"
|
||||
cover
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<VCardItem class="pb-1">
|
||||
<VCardTitle>
|
||||
{{ nameTestResult?.media_info?.title || nameTestResult?.meta_info?.name }}
|
||||
{{ nameTestResult?.meta_info?.season_episode }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
{{ nameTestResult?.media_info?.year || nameTestResult?.meta_info?.year }}
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText
|
||||
v-if="nameTestResult?.media_info?.overview"
|
||||
class="line-clamp-4 overflow-hidden text-ellipsis ..."
|
||||
>
|
||||
{{ nameTestResult?.media_info?.overview }}
|
||||
</VCardText>
|
||||
|
||||
<VCardItem>
|
||||
<!-- 类型 -->
|
||||
<VChip
|
||||
v-if="nameTestResult?.media_info?.type || nameTestResult?.meta_info?.type"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-blue-500"
|
||||
>
|
||||
{{
|
||||
nameTestResult?.media_info?.type || nameTestResult?.meta_info?.type
|
||||
}}
|
||||
</VChip>
|
||||
<!-- 二级分类 -->
|
||||
<VChip
|
||||
v-if="nameTestResult?.media_info?.category"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-blue-500"
|
||||
>
|
||||
{{ nameTestResult?.media_info?.category }}
|
||||
</VChip>
|
||||
<!-- TMDBID -->
|
||||
<VChip
|
||||
v-if="nameTestResult?.media_info?.tmdb_id"
|
||||
variant="elevated"
|
||||
color="success"
|
||||
class="me-1 mb-1"
|
||||
@click="openTmdbPage(nameTestResult?.media_info?.type || '', nameTestResult?.media_info?.tmdb_id)"
|
||||
>
|
||||
{{ nameTestResult?.media_info?.tmdb_id }}
|
||||
</VChip>
|
||||
<!-- meta_info -->
|
||||
<VChip
|
||||
v-if="nameTestResult?.meta_info?.edition"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
{{ nameTestResult?.meta_info?.edition }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="nameTestResult?.meta_info?.resource_pix"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-red-500"
|
||||
>
|
||||
{{ nameTestResult?.meta_info?.resource_pix }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="nameTestResult?.meta_info?.video_encode"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-orange-500"
|
||||
>
|
||||
{{ nameTestResult?.meta_info?.video_encode }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="nameTestResult?.meta_info?.audio_encode"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-orange-500"
|
||||
>
|
||||
{{ nameTestResult?.meta_info?.audio_encode }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="nameTestResult?.meta_info?.resource_team"
|
||||
variant="elevated"
|
||||
class="me-1 mb-1 text-white bg-cyan-500"
|
||||
>
|
||||
{{ nameTestResult?.meta_info?.resource_team }}
|
||||
</VChip>
|
||||
</VCardItem>
|
||||
</div>
|
||||
</div>
|
||||
<VAlert
|
||||
v-if="!nameTestResult?.meta_info?.name"
|
||||
icon="mdi-alert-circle-outline"
|
||||
>
|
||||
识别失败,无法识别到有效信息!
|
||||
</VAlert>
|
||||
</VCol>
|
||||
<MediaInfoCard :context="nameTestResult" />
|
||||
</div>
|
||||
</VExpandTransition>
|
||||
</template>
|
||||
|
||||
120
src/views/system/RuleTestView.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
|
||||
// 识别结果
|
||||
const ruleTestResult = ref('')
|
||||
|
||||
// 名称识别表单
|
||||
const ruleTestForm = reactive({
|
||||
title: '',
|
||||
subtitle: '',
|
||||
ruletype: '1',
|
||||
})
|
||||
|
||||
// 识别按钮状态
|
||||
const ruleTestLoading = ref(false)
|
||||
|
||||
// 识别按钮文本
|
||||
const ruleTestText = ref('测试')
|
||||
|
||||
// 是否显示结果
|
||||
const showResult = ref(false)
|
||||
|
||||
// 调用API识别
|
||||
async function ruleTest() {
|
||||
if (!ruleTestForm.title)
|
||||
return
|
||||
|
||||
try {
|
||||
ruleTestLoading.value = true
|
||||
ruleTestText.value = '正在测试...'
|
||||
showResult.value = false
|
||||
const result: { [key: string]: any } = await api.get('system/ruletest', {
|
||||
params: {
|
||||
title: ruleTestForm.title,
|
||||
subtitle: ruleTestForm.subtitle,
|
||||
ruletype: ruleTestForm.ruletype,
|
||||
},
|
||||
})
|
||||
if (result.success)
|
||||
ruleTestResult.value = `优先级:${result.data.priority}`
|
||||
|
||||
else
|
||||
ruleTestResult.value = '未命中任何优先级规则!'
|
||||
|
||||
ruleTestLoading.value = false
|
||||
ruleTestText.value = '重新测试'
|
||||
showResult.value = true
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow class="pt-2">
|
||||
<VCol cols="12" md="8">
|
||||
<VTextField
|
||||
v-model="ruleTestForm.title"
|
||||
label="标题"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
v-model="ruleTestForm.ruletype"
|
||||
label="规则类型"
|
||||
:items="[{
|
||||
title: '订阅优先级',
|
||||
value: '1',
|
||||
}, {
|
||||
title: '洗版优先级',
|
||||
value: '2',
|
||||
}, {
|
||||
title: '搜索优先级',
|
||||
value: '3',
|
||||
}]"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="ruleTestForm.subtitle"
|
||||
label="副标题"
|
||||
rows="2"
|
||||
auto-grow
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="12"
|
||||
class="text-center"
|
||||
>
|
||||
<VBtn
|
||||
:disabled="ruleTestLoading"
|
||||
@click="ruleTest"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-filter-check-outline" />
|
||||
</template>
|
||||
{{ ruleTestText }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
<VExpandTransition>
|
||||
<div v-show="showResult">
|
||||
<VCol>
|
||||
<VAlert
|
||||
icon="mdi-alert-circle-outline"
|
||||
>
|
||||
{{ ruleTestResult }}
|
||||
</VAlert>
|
||||
</VCol>
|
||||
</div>
|
||||
</VExpandTransition>
|
||||
</template>
|
||||
@@ -4,6 +4,9 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
future: {
|
||||
hoverOnlyWhenSupported: true,
|
||||
},
|
||||
corePlugins: {
|
||||
aspectRatio: false,
|
||||
},
|
||||
|
||||
@@ -43,6 +43,7 @@ export default defineConfig({
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 5000,
|
||||
cssCodeSplit: false,
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['vuetify'],
|
||||
|
||||
@@ -1836,6 +1836,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||
|
||||
"@types/lodash@^4.14.197":
|
||||
version "4.14.198"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.198.tgz#4d27465257011aedc741a809f1269941fa2c5d4c"
|
||||
integrity sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg==
|
||||
|
||||
"@types/mdast@^3.0.0":
|
||||
version "3.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0"
|
||||
|
||||