mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-11 10:00:08 +08:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44ba2dff78 | ||
|
|
0954e4bde2 | ||
|
|
5b183d31e2 | ||
|
|
b2017764eb | ||
|
|
f27cd796b6 | ||
|
|
3c051b8698 | ||
|
|
052d6edd13 | ||
|
|
e7dc61e3d9 | ||
|
|
f0aefdfdf8 | ||
|
|
0beec368b8 | ||
|
|
3f1d03a127 | ||
|
|
eb143c28e3 | ||
|
|
1631951a24 | ||
|
|
31bdd89373 | ||
|
|
ad5ae12d44 | ||
|
|
c838db262c | ||
|
|
623b807a11 | ||
|
|
ce9335a842 | ||
|
|
1c62465c3e | ||
|
|
a2c176bdee | ||
|
|
bff8c0f86b | ||
|
|
1065973e07 | ||
|
|
8e042d5691 | ||
|
|
d9a6b32e5f | ||
|
|
eed3f97fbf | ||
|
|
6b9a8ed108 | ||
|
|
adc718b751 | ||
|
|
df9981d0c9 | ||
|
|
f58b661b1b | ||
|
|
ec1926ba60 | ||
|
|
e853851933 | ||
|
|
3705ce3b90 | ||
|
|
7ad73ff251 | ||
|
|
6c23e8892a | ||
|
|
58efafac71 | ||
|
|
abf2364bf6 | ||
|
|
0650f35dbb | ||
|
|
cc593634d2 | ||
|
|
79a3b9de8a | ||
|
|
ceb46ec974 | ||
|
|
a7e2893a57 | ||
|
|
2efe8efde0 | ||
|
|
31047b0d44 | ||
|
|
7c2b724d10 | ||
|
|
ca5670f06b | ||
|
|
427e05871d | ||
|
|
bef56bdb56 | ||
|
|
d450d02e18 | ||
|
|
85a766cc7b | ||
|
|
a473f356c9 | ||
|
|
52b5fdf383 | ||
|
|
b886f02043 | ||
|
|
61963ea497 | ||
|
|
2f9b27ad9e | ||
|
|
9334109767 | ||
|
|
103bdb32c8 |
@@ -264,7 +264,10 @@ const props = defineProps({
|
||||
yarn build
|
||||
```
|
||||
|
||||
将生成的dist文件夹上传到插件后端目录下(默认为`dist/assets`)
|
||||
- 将生成的dist文件夹上传到插件后端目录下(默认为`dist/assets`)
|
||||
|
||||
**注意: `__federation_shared_vuetify` 目录以及 `index-`、`date-`、`runtime-` 开头的文件不需要上传**,只需要上传以下命名格式文件:`__federation_*`、`_plugin-vue_export-helper-*`、`remoteEntry.js`
|
||||
|
||||
|
||||
- 在插件的后端python代码中,实现以下方法来集成远程组件:
|
||||
|
||||
|
||||
439
index.html
439
index.html
@@ -1,214 +1,273 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" style="
|
||||
<html
|
||||
lang="en"
|
||||
style="
|
||||
overflow: hidden auto;
|
||||
min-block-size: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
||||
background: var(--initial-loader-bg, #fff);
|
||||
">
|
||||
"
|
||||
>
|
||||
<head>
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="expires" content="0" />
|
||||
<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-icon-precomposed" href="/apple-touch-icon-precomposed.png" />
|
||||
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<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="#0E1116" 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" />
|
||||
<script>
|
||||
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
|
||||
if (loaderColor) document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
|
||||
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
|
||||
if (primaryColor) document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="expires" content="0" />
|
||||
<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" />
|
||||
<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="#0E1116" 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" />
|
||||
<script>
|
||||
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
|
||||
if (loaderColor) document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
|
||||
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
|
||||
if (primaryColor) document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
||||
</script>
|
||||
</head>
|
||||
<body style="margin: 0">
|
||||
<div id="loading-bg">
|
||||
<div class="loading-logo">
|
||||
<!-- Logo -->
|
||||
<svg
|
||||
width="160px"
|
||||
height="160px"
|
||||
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"
|
||||
>
|
||||
<style>
|
||||
/* 添加SVG内部的动画样式 */
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
<body style="margin: 0">
|
||||
<div id="loading-bg">
|
||||
<div class="loading-logo">
|
||||
<!-- Logo -->
|
||||
<svg width="160px" height="160px" 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">
|
||||
<style>
|
||||
/* 添加SVG内部的动画样式 */
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.8;
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes glow {
|
||||
0%,
|
||||
100% {
|
||||
filter: drop-shadow(0 0 3px rgba(141, 81, 249, 0.3));
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
filter: drop-shadow(0 0 3px rgba(141, 81, 249, 0.3));
|
||||
50% {
|
||||
filter: drop-shadow(0 0 6px rgba(141, 81, 249, 0.6));
|
||||
}
|
||||
}
|
||||
|
||||
50% {
|
||||
filter: drop-shadow(0 0 6px rgba(141, 81, 249, 0.6));
|
||||
/* 为各个元素添加动画 */
|
||||
#a2-c {
|
||||
filter: drop-shadow(0 0 5px rgba(141, 81, 249, 0.3));
|
||||
animation: glow 3s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
/* 为各个元素添加动画 */
|
||||
#a2-c {
|
||||
filter: drop-shadow(0 0 5px rgba(141, 81, 249, 0.3));
|
||||
animation: glow 3s ease-in-out infinite;
|
||||
}
|
||||
path {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
path {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
/* 错开不同元素的动画开始时间 */
|
||||
g:nth-child(2) path {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
/* 错开不同元素的动画开始时间 */
|
||||
g:nth-child(2) path {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
g:nth-child(3) path {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
g:nth-child(3) path {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
g:nth-child(4) path {
|
||||
animation-delay: 0.9s;
|
||||
}
|
||||
|
||||
g:nth-child(4) path {
|
||||
animation-delay: 0.9s;
|
||||
}
|
||||
|
||||
g:nth-child(5) path {
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
</style>
|
||||
<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" />
|
||||
<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)" />
|
||||
</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)" />
|
||||
</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)" />
|
||||
</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)" />
|
||||
</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)" />
|
||||
</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)" />
|
||||
</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)" />
|
||||
</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)" />
|
||||
<clipPath id="_clip5">
|
||||
g:nth-child(5) path {
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
</style>
|
||||
<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" />
|
||||
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)">
|
||||
<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" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip5)">
|
||||
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
|
||||
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)"
|
||||
/>
|
||||
</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)"
|
||||
/>
|
||||
</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)"
|
||||
/>
|
||||
</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)"
|
||||
/>
|
||||
</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)"
|
||||
/>
|
||||
</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)"
|
||||
/>
|
||||
</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)"
|
||||
/>
|
||||
</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)"
|
||||
/>
|
||||
<clipPath id="_clip5">
|
||||
<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)" />
|
||||
</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)" />
|
||||
</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)" />
|
||||
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)"
|
||||
/>
|
||||
</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)"
|
||||
/>
|
||||
</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)"
|
||||
/>
|
||||
</g>
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<div class="effect-2 effects"></div>
|
||||
<div class="effect-3 effects"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading">
|
||||
<div class="effect-1 effects"></div>
|
||||
<div class="effect-2 effects"></div>
|
||||
<div class="effect-3 effects"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.5.1-1",
|
||||
"version": "2.5.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": "dist/service.js",
|
||||
|
||||
BIN
public/apple-touch-icon-precomposed.png
Normal file
BIN
public/apple-touch-icon-precomposed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@@ -1,15 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
// 定义输入参数
|
||||
const props = defineProps({
|
||||
progress: Number,
|
||||
text: String,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full text-center text-gray-500 text-sm flex flex-col items-center mb-5">
|
||||
<VProgressCircular v-if="!props.text || !props.progress" class="mb-3" size="64" indeterminate color="primary" />
|
||||
<VProgressCircular v-if="props.progress" class="mb-3" color="primary" :model-value="props.progress" size="64" />
|
||||
<span>{{ props.text }}</span>
|
||||
<div class="w-full text-center text-gray-500 text-sm flex flex-col items-center my-5">
|
||||
<div class="initial-loading-container">
|
||||
<div class="initial-loading-content">
|
||||
<div class="wave-loader">
|
||||
<div class="wave-dot"></div>
|
||||
<div class="wave-dot"></div>
|
||||
<div class="wave-dot"></div>
|
||||
<div class="wave-dot"></div>
|
||||
</div>
|
||||
<div class="initial-loading-text" v-if="props.text">{{ props.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 初始的加载状态 */
|
||||
.initial-loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-block-size: 20vh;
|
||||
}
|
||||
|
||||
.initial-loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.wave-loader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
block-size: 40px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wave-dot {
|
||||
border-radius: 50%;
|
||||
animation: wave 1.5s ease-in-out infinite;
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
block-size: 8px;
|
||||
inline-size: 8px;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(4) {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
.initial-loading-text {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1305,3 +1305,49 @@ export interface Workflow {
|
||||
// 最后执行时间
|
||||
last_time?: string
|
||||
}
|
||||
|
||||
// 种子缓存项
|
||||
export interface TorrentCacheItem {
|
||||
// 种子hash(用于操作标识)
|
||||
hash: string
|
||||
// 站点域名
|
||||
domain: string
|
||||
// 种子标题
|
||||
title: string
|
||||
// 种子描述
|
||||
description?: string
|
||||
// 种子大小
|
||||
size: number
|
||||
// 发布时间
|
||||
pubdate?: string
|
||||
// 站点名称
|
||||
site_name?: string
|
||||
// 识别的媒体名称
|
||||
media_name?: string
|
||||
// 识别的媒体年份
|
||||
media_year?: string
|
||||
// 识别的媒体类型
|
||||
media_type?: string
|
||||
// 季集信息
|
||||
season_episode?: string
|
||||
// 资源信息
|
||||
resource_term?: string
|
||||
// 种子链接
|
||||
enclosure?: string
|
||||
// 详情页面
|
||||
page_url?: string
|
||||
// 海报图片
|
||||
poster_path?: string
|
||||
// 背景图片
|
||||
backdrop_path?: string
|
||||
}
|
||||
|
||||
// 种子缓存数据
|
||||
export interface TorrentCacheData {
|
||||
// 缓存数量
|
||||
count: number
|
||||
// 站点数量
|
||||
sites: number
|
||||
// 缓存数据
|
||||
data: TorrentCacheItem[]
|
||||
}
|
||||
|
||||
@@ -136,6 +136,12 @@ const sort = ref('name')
|
||||
// 是否显示目录树
|
||||
const showDirTree = ref(false)
|
||||
|
||||
// 拖动分隔条相关
|
||||
const navigatorWidth = ref(280) // 初始宽度
|
||||
const isDragging = ref(false)
|
||||
const dragStartX = ref(0)
|
||||
const dragStartWidth = ref(0)
|
||||
|
||||
// 计算属性
|
||||
const storagesArray = computed(() => {
|
||||
return props.storages?.map(item => ({
|
||||
@@ -181,10 +187,62 @@ function fileListUpdated(items: FileItem[]) {
|
||||
fileListItems.value = items
|
||||
}
|
||||
|
||||
// 阻止选择事件
|
||||
function preventSelect(event: Event) {
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
|
||||
// 拖动分隔条相关方法
|
||||
function startDrag(event: MouseEvent) {
|
||||
event.preventDefault() // 阻止默认行为
|
||||
event.stopPropagation() // 阻止事件冒泡
|
||||
|
||||
isDragging.value = true
|
||||
dragStartX.value = event.clientX
|
||||
dragStartWidth.value = navigatorWidth.value
|
||||
|
||||
document.addEventListener('mousemove', handleDrag, { passive: false })
|
||||
document.addEventListener('mouseup', stopDrag, { passive: false })
|
||||
document.addEventListener('selectstart', preventSelect) // 阻止选择开始
|
||||
|
||||
document.body.style.cursor = 'col-resize'
|
||||
document.body.style.userSelect = 'none'
|
||||
;(document.body.style as any).webkitUserSelect = 'none' // Safari兼容
|
||||
;(document.body.style as any).mozUserSelect = 'none' // Firefox兼容
|
||||
}
|
||||
|
||||
function handleDrag(event: MouseEvent) {
|
||||
if (!isDragging.value) return
|
||||
|
||||
event.preventDefault() // 阻止默认行为
|
||||
|
||||
const deltaX = event.clientX - dragStartX.value
|
||||
const newWidth = dragStartWidth.value + deltaX
|
||||
|
||||
// 设置最小和最大宽度限制
|
||||
const minWidth = 200
|
||||
const maxWidth = window.innerWidth * 0.6
|
||||
|
||||
navigatorWidth.value = Math.max(minWidth, Math.min(maxWidth, newWidth))
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('mousemove', handleDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
document.removeEventListener('selectstart', preventSelect)
|
||||
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
;(document.body.style as any).webkitUserSelect = ''
|
||||
;(document.body.style as any).mozUserSelect = ''
|
||||
}
|
||||
|
||||
// 外层DIV大小控制
|
||||
const scrollStyle = computed(() => {
|
||||
return appMode
|
||||
? 'height: calc(100vh - 10.5rem - env(safe-area-inset-bottom) - 6.5rem)'
|
||||
? 'height: calc(100vh - 10.5rem - env(safe-area-inset-bottom) - 7rem)'
|
||||
: 'height: calc(100vh - 10.5rem - env(safe-area-inset-bottom)'
|
||||
})
|
||||
|
||||
@@ -219,8 +277,14 @@ const fileListStyle = computed(() => {
|
||||
:items="fileListItems"
|
||||
:endpoints="endpoints"
|
||||
:axios="axios"
|
||||
:style="{ width: `${navigatorWidth}px`, minWidth: `${navigatorWidth}px` }"
|
||||
@navigate="pathChanged"
|
||||
/>
|
||||
<!-- 拖动分隔条 -->
|
||||
<div v-if="showDirTree" class="divider" :class="{ 'divider-dragging': isDragging }" @mousedown="startDrag">
|
||||
<div class="divider-line"></div>
|
||||
<VIcon class="divider-icon" size="small">mdi-drag-vertical</VIcon>
|
||||
</div>
|
||||
<FileList
|
||||
:item="item"
|
||||
:storage="activeStorage"
|
||||
@@ -231,6 +295,7 @@ const fileListStyle = computed(() => {
|
||||
:sort="sort"
|
||||
:listStyle="fileListStyle"
|
||||
:showTree="showDirTree"
|
||||
:style="{ flex: 1 }"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
@@ -243,3 +308,64 @@ const fileListStyle = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.divider {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
cursor: col-resize;
|
||||
inline-size: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.divider:hover {
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.08);
|
||||
}
|
||||
|
||||
.divider-dragging {
|
||||
background-color: rgba(var(--v-theme-primary), 0.12) !important;
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
background-color: rgba(var(--v-theme-outline), 0.3);
|
||||
block-size: 100%;
|
||||
inline-size: 1px;
|
||||
transition: background-color 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.divider-dragging .divider-line {
|
||||
background-color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
|
||||
.divider:hover .divider-line {
|
||||
background-color: rgba(var(--v-theme-primary), 0.8);
|
||||
}
|
||||
|
||||
.divider-icon {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
background-color: rgba(var(--v-theme-surface), 0.9);
|
||||
color: rgba(var(--v-theme-on-surface-variant), 0.6);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.divider-dragging .divider-icon {
|
||||
background-color: rgba(var(--v-theme-surface), 0.95);
|
||||
color: rgb(var(--v-theme-primary));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.divider:hover .divider-icon {
|
||||
color: rgba(var(--v-theme-primary), 0.9);
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -214,7 +214,7 @@ watch(
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="props.directory.media_type"
|
||||
variant="underlined"
|
||||
:items="typeItems"
|
||||
@@ -223,7 +223,7 @@ watch(
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="props.directory.media_category"
|
||||
variant="underlined"
|
||||
:items="getCategories"
|
||||
@@ -231,7 +231,7 @@ watch(
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="4">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="props.directory.storage"
|
||||
variant="underlined"
|
||||
:items="resourceStorageOptions"
|
||||
@@ -277,7 +277,7 @@ watch(
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="4">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="props.directory.library_storage"
|
||||
variant="underlined"
|
||||
:items="libraryStorageOptions"
|
||||
|
||||
@@ -56,7 +56,7 @@ onMounted(() => {
|
||||
<VCardTitle>{{ t('filterRule.priority') }} {{ props.pri }}</VCardTitle>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="props.rules"
|
||||
variant="underlined"
|
||||
:items="selectFilterOptions"
|
||||
|
||||
@@ -247,7 +247,7 @@ function onClose() {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="groupInfo.media_type"
|
||||
:label="t('filterRule.mediaType')"
|
||||
:items="mediaTypeItems"
|
||||
@@ -258,7 +258,7 @@ function onClose() {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="groupInfo.category"
|
||||
:items="getCategories"
|
||||
:label="t('filterRule.category')"
|
||||
|
||||
@@ -15,6 +15,7 @@ import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
||||
import SubscribeSeasonDialog from '../dialog/SubscribeSeasonDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { mediaTypeDict } from '@/api/constants'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -481,7 +482,13 @@ onBeforeUnmount(() => {
|
||||
</p>
|
||||
<div v-if="props.media?.collection_id" class="mb-3" @click.stop=""></div>
|
||||
<div v-else class="flex align-center justify-between">
|
||||
<IconBtn icon="mdi-magnify" color="white" @click.stop="clickSearch" />
|
||||
<IconBtn
|
||||
v-if="hasPermission({ is_superuser: userStore.superUser, ...userStore.permissions }, 'search')"
|
||||
icon="mdi-magnify"
|
||||
color="white"
|
||||
@click.stop="clickSearch"
|
||||
/>
|
||||
<VSpacer />
|
||||
<IconBtn icon="mdi-heart" :color="isSubscribed ? 'error' : 'white'" @click.stop="handleSubscribe" />
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
@@ -273,7 +273,7 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="mediaServerInfo.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
@@ -334,7 +334,7 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="mediaServerInfo.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
@@ -402,7 +402,7 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="mediaServerInfo.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
@@ -463,7 +463,7 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="mediaServerInfo.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
|
||||
@@ -165,7 +165,7 @@ function onClose() {
|
||||
<VSwitch v-model="notificationInfo.enabled" :label="t('notification.enabled')" />
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="notificationInfo.switchs"
|
||||
:items="notificationTypes"
|
||||
:label="t('notification.type')"
|
||||
|
||||
@@ -229,7 +229,7 @@ const dropdownItems = ref([
|
||||
class="flex flex-col align-self-baseline justify-between px-2 py-2 w-full overflow-hidden max-h-10 min-h-10"
|
||||
>
|
||||
<div class="flex flex-nowrap items-center w-full pe-10">
|
||||
<div class="flex flex-nowrap max-w-32 items-center align-middle">
|
||||
<div class="flex flex-nowrap max-w-40 items-center align-middle">
|
||||
<VIcon icon="mdi-github" class="me-1" />
|
||||
<a
|
||||
class="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
|
||||
@@ -473,7 +473,7 @@ watch(
|
||||
class="flex flex-col align-self-baseline justify-between px-2 py-2 w-full overflow-hidden max-h-10 min-h-10"
|
||||
>
|
||||
<div class="flex flex-nowrap items-center w-full pe-10">
|
||||
<div class="flex flex-nowrap max-w-32 items-center align-middle">
|
||||
<div class="flex flex-nowrap max-w-40 items-center align-middle">
|
||||
<VImg :src="authorPath" class="author-avatar" @load="isAvatarLoaded = true">
|
||||
<VIcon v-if="!isAvatarLoaded" size="small" icon="mdi-github" class="me-1" />
|
||||
</VImg>
|
||||
|
||||
@@ -218,7 +218,7 @@ onMounted(() => {
|
||||
elevation="0"
|
||||
rounded="lg"
|
||||
hover
|
||||
@click="siteEditDialog = true"
|
||||
@click="handleResourceBrowse"
|
||||
>
|
||||
<!-- 装饰性状态指示器 -->
|
||||
<div v-if="cardProps.site?.is_active" class="site-status-indicator" :class="statColor"></div>
|
||||
@@ -339,11 +339,11 @@ onMounted(() => {
|
||||
<VIcon icon="mdi-dots-vertical" size="20" />
|
||||
<VMenu :activator="'parent'" :close-on-content-click="true" :location="'left'">
|
||||
<VList>
|
||||
<VListItem @click="handleResourceBrowse" base-color="info">
|
||||
<VListItem @click="siteEditDialog = true" base-color="info">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-web" size="20" />
|
||||
<VIcon icon="mdi-file-edit-outline" size="20" />
|
||||
</template>
|
||||
<VListItemTitle>{{ t('site.browseResources') }}</VListItemTitle>
|
||||
<VListItemTitle>{{ t('site.actions.edit') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem @click="deleteSiteInfo">
|
||||
<template #prepend>
|
||||
|
||||
@@ -353,7 +353,7 @@ function onSubscribeEditRemove() {
|
||||
<div>
|
||||
<VCardText class="flex items-center pt-3 pb-2">
|
||||
<div
|
||||
class="h-auto w-14 flex-shrink-0 overflow-hidden rounded-md"
|
||||
class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md"
|
||||
v-if="imageLoaded"
|
||||
:class="{ 'cursor-move': display.mdAndUp.value }"
|
||||
>
|
||||
@@ -367,7 +367,7 @@ function onSubscribeEditRemove() {
|
||||
</div>
|
||||
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
||||
<div class="mr-2 min-w-0 text-lg font-bold text-white">
|
||||
<div class="mr-2 min-w-0 text-lg font-bold text-white text-ellipsis overflow-hidden line-clamp-2 ...">
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||
</div>
|
||||
|
||||
@@ -123,157 +123,176 @@ onMounted(() => {
|
||||
'transition-transform duration-300 hover:-translate-y-1',
|
||||
!props.user.is_active ? 'opacity-85 bg-surface-lighten-1' : '',
|
||||
]"
|
||||
class="flex flex-column"
|
||||
@click="userEditDialog = true"
|
||||
>
|
||||
<!-- 用户头像和基本信息 -->
|
||||
<VCardItem :class="[user.is_superuser ? 'admin-header' : '']">
|
||||
<template v-slot:prepend>
|
||||
<div class="position-relative mr-4">
|
||||
<VAvatar
|
||||
size="72"
|
||||
rounded="lg"
|
||||
:class="[
|
||||
user.is_superuser ? 'admin-avatar' : 'border-4 bg-surface',
|
||||
!user.is_active ? 'grayscale-50 opacity-90' : '',
|
||||
]"
|
||||
:style="user.is_superuser ? 'border: 4px solid rgba(var(--v-theme-warning), 0.3);' : ''"
|
||||
>
|
||||
<VImg :src="user.avatar || avatar1" :alt="user.name" />
|
||||
<div
|
||||
v-if="!user.is_active"
|
||||
class="position-absolute d-flex align-center justify-center rounded-lg bg-surface-variant opacity-20"
|
||||
style="inset: 0"
|
||||
>
|
||||
<VIcon icon="mdi-account-lock" color="white" />
|
||||
</div>
|
||||
</VAvatar>
|
||||
<div v-if="user.is_superuser" class="admin-crown">
|
||||
<VIcon icon="mdi-crown" color="warning" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<VCardTitle class="pa-0 d-flex flex-column">
|
||||
<div class="d-flex flex-column mb-1">
|
||||
<div class="d-flex align-center">
|
||||
<span
|
||||
<div class="flex-grow">
|
||||
<!-- 用户头像和基本信息 -->
|
||||
<VCardItem :class="[user.is_superuser ? 'admin-header' : '']">
|
||||
<template v-slot:prepend>
|
||||
<div class="position-relative mr-4">
|
||||
<VAvatar
|
||||
size="72"
|
||||
rounded="lg"
|
||||
:class="[
|
||||
'text-h6 font-weight-bold truncate',
|
||||
user.is_superuser ? 'text-warning' : '',
|
||||
!user.is_active ? 'text-medium-emphasis' : '',
|
||||
user.is_superuser ? 'admin-avatar' : 'border-4 bg-surface',
|
||||
!user.is_active ? 'grayscale-50 opacity-90' : '',
|
||||
]"
|
||||
:style="user.is_superuser ? 'border: 4px solid rgba(var(--v-theme-warning), 0.3);' : ''"
|
||||
>
|
||||
{{ displayName }}
|
||||
<VIcon
|
||||
v-if="user.nickname || user.settings?.nickname"
|
||||
icon="mdi-format-quote-close"
|
||||
size="x-small"
|
||||
color="info"
|
||||
class="animate-pulse"
|
||||
/>
|
||||
</span>
|
||||
<VImg :src="user.avatar || avatar1" :alt="user.name" />
|
||||
<div
|
||||
v-if="!user.is_active"
|
||||
class="position-absolute d-flex align-center justify-center rounded-lg bg-surface-variant opacity-20"
|
||||
style="inset: 0"
|
||||
>
|
||||
<VIcon icon="mdi-account-lock" color="white" />
|
||||
</div>
|
||||
</VAvatar>
|
||||
<div v-if="user.is_superuser" class="admin-crown">
|
||||
<VIcon icon="mdi-crown" color="warning" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1 overflow-auto">
|
||||
<VChip v-if="user.is_superuser" size="x-small" color="error" variant="outlined" label>{{
|
||||
t('user.admin')
|
||||
}}</VChip>
|
||||
<VChip v-else size="x-small" label>{{ t('user.normal') }}</VChip>
|
||||
<VChip size="x-small" :color="user.is_active ? 'success' : 'grey'" variant="tonal" label>
|
||||
{{ user.is_active ? t('user.active') : t('user.inactive') }}
|
||||
</VChip>
|
||||
<VChip v-if="user.is_otp" size="x-small" color="info" variant="tonal" label>2FA</VChip>
|
||||
</template>
|
||||
|
||||
<VCardTitle class="pa-0 d-flex flex-column">
|
||||
<div class="d-flex flex-column mb-1">
|
||||
<div class="d-flex align-center">
|
||||
<span
|
||||
:class="[
|
||||
'text-h6 font-weight-bold truncate',
|
||||
user.is_superuser ? 'text-warning' : '',
|
||||
!user.is_active ? 'text-medium-emphasis' : '',
|
||||
]"
|
||||
>
|
||||
{{ displayName }}
|
||||
<VIcon
|
||||
v-if="user.nickname || user.settings?.nickname"
|
||||
icon="mdi-format-quote-close"
|
||||
size="x-small"
|
||||
color="info"
|
||||
class="animate-pulse"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1 overflow-auto">
|
||||
<VChip v-if="user.is_superuser" size="x-small" color="error" variant="outlined" label>{{
|
||||
t('user.admin')
|
||||
}}</VChip>
|
||||
<VChip v-else size="x-small" label>{{ t('user.normal') }}</VChip>
|
||||
<VChip size="x-small" :color="user.is_active ? 'success' : 'grey'" variant="tonal" label>
|
||||
{{ user.is_active ? t('user.active') : t('user.inactive') }}
|
||||
</VChip>
|
||||
<VChip v-if="user.is_otp" size="x-small" color="info" variant="tonal" label>2FA</VChip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端订阅数据信息 -->
|
||||
<div v-if="isMobile" class="d-flex gap-5 mt-2">
|
||||
<div class="d-flex align-center">
|
||||
<VIcon size="x-small" icon="mdi-movie-outline" color="primary" class="mr-1" />
|
||||
<span class="text-body-2">{{ movieSubscriptions }}</span>
|
||||
<!-- 移动端订阅数据信息 -->
|
||||
<div v-if="isMobile" class="d-flex gap-5 mt-2">
|
||||
<div class="d-flex align-center">
|
||||
<VIcon size="x-small" icon="mdi-movie-outline" color="primary" class="mr-1" />
|
||||
<span class="text-body-2">{{ movieSubscriptions }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<VIcon size="x-small" icon="mdi-television-classic" color="primary" class="mr-1" />
|
||||
<span class="text-body-2">{{ tvShowSubscriptions }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<VIcon size="x-small" icon="mdi-television-classic" color="primary" class="mr-1" />
|
||||
<span class="text-body-2">{{ tvShowSubscriptions }}</span>
|
||||
</VCardTitle>
|
||||
|
||||
<!-- 头部操作按钮 -->
|
||||
<template v-slot:append>
|
||||
<div :class="['d-flex', isMobile ? 'position-absolute top-2 right-2' : '']">
|
||||
<VBtn
|
||||
icon
|
||||
size="small"
|
||||
:color="user.is_superuser ? 'warning' : 'primary'"
|
||||
variant="text"
|
||||
class="opacity-70 hover:opacity-100 transition-opacity"
|
||||
@click.stop="editUser"
|
||||
>
|
||||
<VIcon icon="mdi-pencil" />
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
v-if="props.user.id != currentLoginUserId && currentUserIsSuperuser"
|
||||
icon
|
||||
size="small"
|
||||
color="error"
|
||||
variant="text"
|
||||
class="opacity-70 hover:opacity-100 transition-opacity"
|
||||
@click.stop="removeUser"
|
||||
>
|
||||
<VIcon icon="mdi-delete" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCardTitle>
|
||||
|
||||
<!-- 头部操作按钮 -->
|
||||
<template v-slot:append>
|
||||
<div :class="['d-flex', isMobile ? 'position-absolute top-2 right-2' : '']">
|
||||
<VBtn
|
||||
icon
|
||||
size="small"
|
||||
:color="user.is_superuser ? 'warning' : 'primary'"
|
||||
variant="text"
|
||||
class="opacity-70 hover:opacity-100 transition-opacity"
|
||||
@click.stop="editUser"
|
||||
>
|
||||
<VIcon icon="mdi-pencil" />
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
v-if="props.user.id != currentLoginUserId && currentUserIsSuperuser"
|
||||
icon
|
||||
size="small"
|
||||
color="error"
|
||||
variant="text"
|
||||
class="opacity-70 hover:opacity-100 transition-opacity"
|
||||
@click.stop="removeUser"
|
||||
>
|
||||
<VIcon icon="mdi-delete" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<!-- 权限显示 -->
|
||||
<div v-if="!user.is_superuser && user.permissions" class="d-flex flex-wrap gap-1 px-7 pb-3">
|
||||
<VChip v-if="user.permissions.discovery" size="x-small" color="purple" variant="outlined" label>
|
||||
{{ t('dialog.userAddEdit.permissions.discovery') }}
|
||||
</VChip>
|
||||
<VChip v-if="user.permissions.search" size="x-small" color="blue" variant="outlined" label>
|
||||
{{ t('dialog.userAddEdit.permissions.search') }}
|
||||
</VChip>
|
||||
<VChip v-if="user.permissions.subscribe" size="x-small" color="green" variant="outlined" label>
|
||||
{{ t('dialog.userAddEdit.permissions.subscribe') }}
|
||||
</VChip>
|
||||
<VChip v-if="user.permissions.manage" size="x-small" color="orange" variant="outlined" label>
|
||||
{{ t('dialog.userAddEdit.permissions.manage') }}
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 独立的邮箱显示 -->
|
||||
<VDivider class="mx-4" />
|
||||
<div>
|
||||
<VCardText class="d-flex align-center py-2 px-4 text-medium-emphasis">
|
||||
<VIcon icon="mdi-email-outline" size="small" color="primary" class="mr-2 opacity-70" />
|
||||
<span class="text-body-2 truncate">{{ user.email || t('user.noEmail') }}</span>
|
||||
</VCardText>
|
||||
|
||||
<VCardText class="d-flex align-center py-2 px-4 text-medium-emphasis">
|
||||
<VIcon icon="mdi-email-outline" size="small" color="primary" class="mr-2 opacity-70" />
|
||||
<span class="text-body-2 truncate">{{ user.email || t('user.noEmail') }}</span>
|
||||
</VCardText>
|
||||
|
||||
<!-- PC端显示订阅统计信息 -->
|
||||
<VCardText v-if="!isMobile" class="px-4 pt-0 pb-4">
|
||||
<div rounded="lg" class="d-flex justify-space-around pa-3">
|
||||
<div class="d-flex align-center gap-3">
|
||||
<VAvatar
|
||||
tile
|
||||
rounded="lg"
|
||||
size="large"
|
||||
class="mr-1"
|
||||
:class="user.is_superuser ? 'admin-stats-container' : 'user-stats-container'"
|
||||
>
|
||||
<div :class="['d-flex align-center justify-center rounded-lg w-10 h-10']">
|
||||
<VIcon :color="user.is_superuser ? 'warning' : 'primary'" icon="mdi-movie-outline" size="20" />
|
||||
<!-- PC端显示订阅统计信息 -->
|
||||
<VCardText v-if="!isMobile" class="px-4 pt-0 pb-4">
|
||||
<div rounded="lg" class="d-flex justify-space-around">
|
||||
<div class="d-flex align-center gap-3">
|
||||
<VAvatar
|
||||
tile
|
||||
rounded="lg"
|
||||
size="large"
|
||||
class="mr-1"
|
||||
:class="user.is_superuser ? 'admin-stats-container' : 'user-stats-container'"
|
||||
>
|
||||
<div :class="['d-flex align-center justify-center rounded-lg w-10 h-10']">
|
||||
<VIcon :color="user.is_superuser ? 'warning' : 'primary'" icon="mdi-movie-outline" size="20" />
|
||||
</div>
|
||||
</VAvatar>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-lg text-medium-emphasis font-weight-bold">{{ movieSubscriptions }}</span>
|
||||
<span class="text-caption text-medium-emphasis">{{ t('user.movieSubscriptions') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-center gap-3">
|
||||
<VAvatar
|
||||
tile
|
||||
rounded="lg"
|
||||
size="large"
|
||||
class="mr-1"
|
||||
:class="user.is_superuser ? 'admin-stats-container' : 'user-stats-container'"
|
||||
>
|
||||
<div :class="['d-flex align-center justify-center rounded-lg w-10 h-10']">
|
||||
<VIcon :color="user.is_superuser ? 'warning' : 'primary'" icon="mdi-television-classic" />
|
||||
</div>
|
||||
</VAvatar>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-lg text-medium-emphasis">{{ tvShowSubscriptions }}</span>
|
||||
<span class="text-caption text-medium-emphasis">{{ t('user.tvSubscriptions') }}</span>
|
||||
</div>
|
||||
</VAvatar>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-lg text-medium-emphasis font-weight-bold">{{ movieSubscriptions }}</span>
|
||||
<span class="text-caption text-medium-emphasis">{{ t('user.movieSubscriptions') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-center gap-3">
|
||||
<VAvatar
|
||||
tile
|
||||
rounded="lg"
|
||||
size="large"
|
||||
class="mr-1"
|
||||
:class="user.is_superuser ? 'admin-stats-container' : 'user-stats-container'"
|
||||
>
|
||||
<div :class="['d-flex align-center justify-center rounded-lg w-10 h-10']">
|
||||
<VIcon :color="user.is_superuser ? 'warning' : 'primary'" icon="mdi-television-classic" />
|
||||
</div>
|
||||
</VAvatar>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-lg text-medium-emphasis">{{ tvShowSubscriptions }}</span>
|
||||
<span class="text-caption text-medium-emphasis">{{ t('user.tvSubscriptions') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCardText>
|
||||
</div>
|
||||
</VCard>
|
||||
|
||||
<!-- 用户编辑弹窗 -->
|
||||
@@ -294,9 +313,10 @@ onMounted(() => {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
padding: 8px 12px;
|
||||
inline-size: 100%;
|
||||
inset-block-start: 0;
|
||||
padding-block: 8px;
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
@@ -326,10 +346,12 @@ onMounted(() => {
|
||||
opacity: 0.6;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 0.2;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
transform: scale(0.95);
|
||||
@@ -340,19 +362,21 @@ onMounted(() => {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
top: -10px;
|
||||
left: -6px;
|
||||
transform: rotate(-25deg);
|
||||
filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 40%));
|
||||
inset-block-start: -10px;
|
||||
inset-inline-start: -6px;
|
||||
transform: rotate(-25deg);
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% {
|
||||
transform: rotate(-25deg) translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(-25deg) translateY(-3px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(-25deg) translateY(0);
|
||||
}
|
||||
@@ -368,6 +392,7 @@ onMounted(() => {
|
||||
opacity: 0.9;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
|
||||
@@ -179,7 +179,13 @@ const resolveProgress = (item: Workflow) => {
|
||||
:loading="loading"
|
||||
:class="{ 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering }"
|
||||
>
|
||||
<VCardItem class="py-3" :class="`bg-${resolveStatusVariant(workflow?.state).color}`">
|
||||
<VCardItem
|
||||
:class="{
|
||||
'py-1': workflow?.description,
|
||||
'py-3': !workflow?.description,
|
||||
[`bg-${resolveStatusVariant(workflow?.state).color}`]: true,
|
||||
}"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar variant="text" class="me-2">
|
||||
<VIcon
|
||||
|
||||
@@ -172,7 +172,7 @@ onMounted(async () => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="siteForm.pri"
|
||||
:label="t('site.fields.priority')"
|
||||
:items="priorityItems"
|
||||
@@ -213,7 +213,7 @@ onMounted(async () => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="siteForm.downloader"
|
||||
:label="t('site.fields.downloader')"
|
||||
:items="downloaderOptions"
|
||||
|
||||
@@ -134,7 +134,7 @@ onMounted(() => {
|
||||
<VCard>
|
||||
<!-- Toolbar -->
|
||||
<div>
|
||||
<VToolbar color="primary">
|
||||
<VToolbar color="primary" density="comfortable">
|
||||
<VToolbarTitle>{{ t('dialog.siteResource.browseTitle', { name: props.site?.name }) }}</VToolbarTitle>
|
||||
<VSpacer />
|
||||
<VToolbarItems>
|
||||
|
||||
@@ -346,7 +346,7 @@ onMounted(() => {
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.quality"
|
||||
:label="t('dialog.subscribeEdit.quality')"
|
||||
:items="qualityOptions"
|
||||
@@ -356,7 +356,7 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.resolution"
|
||||
:label="t('dialog.subscribeEdit.resolution')"
|
||||
:items="resolutionOptions"
|
||||
@@ -366,7 +366,7 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.effect"
|
||||
:label="t('dialog.subscribeEdit.effect')"
|
||||
:items="effectOptions"
|
||||
@@ -378,7 +378,7 @@ onMounted(() => {
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.sites"
|
||||
:items="selectSitesOptions"
|
||||
chips
|
||||
@@ -393,7 +393,7 @@ onMounted(() => {
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.downloader"
|
||||
:items="downloaderOptions"
|
||||
:label="t('dialog.subscribeEdit.downloader')"
|
||||
@@ -465,7 +465,7 @@ onMounted(() => {
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.filter_groups"
|
||||
:items="filterRuleGroupOptions"
|
||||
chips
|
||||
@@ -478,7 +478,7 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="!props.default && subscribeForm.type === '电视剧'" cols="12" md="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.episode_group"
|
||||
:items="episodeGroupOptions"
|
||||
:item-props="episodeGroupItemProps"
|
||||
@@ -489,7 +489,7 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="!props.default && subscribeForm.type === '电视剧'" cols="12" md="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="subscribeForm.season"
|
||||
:items="seasonItems"
|
||||
:label="t('dialog.subscribeEdit.season')"
|
||||
|
||||
@@ -67,6 +67,7 @@ const $toast = useToast()
|
||||
{{ props.sub?.season ? t('dialog.subscribeShare.season', { number: props.sub?.season }) : '' }}
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VForm @submit.prevent="() => {}" class="pt-2">
|
||||
|
||||
@@ -65,6 +65,14 @@ interface ExtendedUser extends User {
|
||||
nickname?: string
|
||||
}
|
||||
|
||||
// 权限类型定义
|
||||
interface UserPermissions {
|
||||
discovery: boolean // 发现权限
|
||||
search: boolean // 搜索权限
|
||||
subscribe: boolean // 订阅权限
|
||||
manage: boolean // 管理权限
|
||||
}
|
||||
|
||||
// 用户编辑表单数据
|
||||
const userForm = ref<ExtendedUser>({
|
||||
id: 0,
|
||||
@@ -75,7 +83,12 @@ const userForm = ref<ExtendedUser>({
|
||||
is_superuser: false,
|
||||
avatar: avatar1,
|
||||
is_otp: false,
|
||||
permissions: {},
|
||||
permissions: {
|
||||
discovery: true,
|
||||
search: true,
|
||||
subscribe: true,
|
||||
manage: false,
|
||||
},
|
||||
settings: {
|
||||
wechat_userid: null,
|
||||
telegram_userid: null,
|
||||
@@ -86,6 +99,59 @@ const userForm = ref<ExtendedUser>({
|
||||
nickname: '', // 昵称字段
|
||||
})
|
||||
|
||||
// 权限选项
|
||||
const permissionOptions = [
|
||||
{
|
||||
key: 'discovery',
|
||||
title: t('dialog.userAddEdit.permissions.discovery'),
|
||||
description: t('dialog.userAddEdit.permissions.discoveryDesc'),
|
||||
icon: 'mdi-star-outline',
|
||||
},
|
||||
{
|
||||
key: 'search',
|
||||
title: t('dialog.userAddEdit.permissions.search'),
|
||||
description: t('dialog.userAddEdit.permissions.searchDesc'),
|
||||
icon: 'mdi-magnify',
|
||||
},
|
||||
{
|
||||
key: 'subscribe',
|
||||
title: t('dialog.userAddEdit.permissions.subscribe'),
|
||||
description: t('dialog.userAddEdit.permissions.subscribeDesc'),
|
||||
icon: 'mdi-rss',
|
||||
},
|
||||
{
|
||||
key: 'manage',
|
||||
title: t('dialog.userAddEdit.permissions.manage'),
|
||||
description: t('dialog.userAddEdit.permissions.manageDesc'),
|
||||
icon: 'mdi-cog-outline',
|
||||
},
|
||||
]
|
||||
|
||||
// 权限状态计算属性
|
||||
const userPermissions = computed({
|
||||
get: () => {
|
||||
const permissions = userForm.value.permissions as UserPermissions
|
||||
return {
|
||||
discovery: permissions?.discovery ?? true,
|
||||
search: permissions?.search ?? true,
|
||||
subscribe: permissions?.subscribe ?? true,
|
||||
manage: permissions?.manage ?? false,
|
||||
}
|
||||
},
|
||||
set: (value: UserPermissions) => {
|
||||
userForm.value.permissions = value
|
||||
},
|
||||
})
|
||||
|
||||
// 切换权限状态
|
||||
function togglePermission(key: keyof UserPermissions) {
|
||||
const currentPermissions = userPermissions.value
|
||||
userPermissions.value = {
|
||||
...currentPermissions,
|
||||
[key]: !currentPermissions[key],
|
||||
}
|
||||
}
|
||||
|
||||
// 更新头像
|
||||
function changeAvatar(file: Event) {
|
||||
const fileReader = new FileReader()
|
||||
@@ -164,6 +230,10 @@ async function addUser() {
|
||||
}
|
||||
userForm.value.password = newPassword.value
|
||||
}
|
||||
|
||||
// 设置权限数据
|
||||
userForm.value.permissions = userPermissions.value
|
||||
|
||||
isAdding.value = true
|
||||
startNProgress()
|
||||
try {
|
||||
@@ -216,8 +286,10 @@ async function updateUser() {
|
||||
isUpdating.value = true
|
||||
startNProgress()
|
||||
try {
|
||||
// 确保昵称保存,使用一个临时变量存储完整数据
|
||||
// 确保昵称和权限保存,使用一个临时变量存储完整数据
|
||||
const userData = { ...userForm.value }
|
||||
// 确保权限数据正确传递
|
||||
userData.permissions = userPermissions.value
|
||||
|
||||
const result: { [key: string]: any } = await api.put('user/', userData)
|
||||
|
||||
@@ -235,6 +307,10 @@ async function updateUser() {
|
||||
if (oldAvatar !== currentAvatar.value && isCurrentUser.value) {
|
||||
userStore.setAvatar(currentAvatar.value)
|
||||
}
|
||||
// 如果是当前登录用户,更新权限信息
|
||||
if (isCurrentUser.value) {
|
||||
userStore.setPermissions(userPermissions.value)
|
||||
}
|
||||
emit('save')
|
||||
} else {
|
||||
if (oldUserName !== currentUserName.value) {
|
||||
@@ -473,6 +549,48 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VDivider class="my-10" v-if="canControl">
|
||||
<span>{{ t('dialog.userAddEdit.permissions.title') }}</span>
|
||||
</VDivider>
|
||||
<!-- 权限设置 -->
|
||||
<div v-if="canControl">
|
||||
<VRow>
|
||||
<VCol v-for="option in permissionOptions" :key="option.key" cols="6">
|
||||
<VCard
|
||||
:color="userPermissions[option.key as keyof UserPermissions] ? 'primary' : 'surface'"
|
||||
:variant="userPermissions[option.key as keyof UserPermissions] ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer transition-all h-full"
|
||||
@click="togglePermission(option.key as keyof UserPermissions)"
|
||||
hover
|
||||
>
|
||||
<VCardText class="d-flex align-center pa-4">
|
||||
<VAvatar
|
||||
:color="userPermissions[option.key as keyof UserPermissions] ? 'primary' : 'surface-variant'"
|
||||
size="40"
|
||||
class="me-3"
|
||||
>
|
||||
<VIcon :icon="option.icon" />
|
||||
</VAvatar>
|
||||
<div class="flex-grow-1">
|
||||
<div class="text-subtitle-1 font-weight-medium d-flex align-center">
|
||||
{{ option.title }}
|
||||
<VIcon
|
||||
v-if="userPermissions[option.key as keyof UserPermissions]"
|
||||
icon="mdi-check-circle"
|
||||
color="primary"
|
||||
size="small"
|
||||
class="ms-2"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ option.description }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardActions class="pt-3">
|
||||
|
||||
@@ -3,10 +3,7 @@ import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import api from '@/api'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
@@ -137,7 +134,7 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog width="40rem" scrollable>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
|
||||
@@ -200,7 +200,7 @@ const isMacOS = computed(() => {
|
||||
<VDialog scrollable fullscreen :scrim="false" transition="dialog-bottom-transition">
|
||||
<VCard class="workflow-dialog">
|
||||
<!-- Toolbar -->
|
||||
<VToolbar color="primary">
|
||||
<VToolbar color="primary" density="comfortable">
|
||||
<VToolbarItems>
|
||||
<VBtn icon @click="emit('close')" class="ms-3">
|
||||
<VIcon size="large" color="white" icon="mdi-close" />
|
||||
|
||||
@@ -599,9 +599,7 @@ onMounted(() => {
|
||||
</IconBtn>
|
||||
</span>
|
||||
</div>
|
||||
<VCardText v-if="loading" class="text-center flex flex-col items-center">
|
||||
<VProgressCircular size="48" indeterminate color="primary" />
|
||||
</VCardText>
|
||||
<LoadingBanner v-if="loading" />
|
||||
<!-- 文件详情 -->
|
||||
<VCardText v-else-if="isFile && !isImage && items.length > 0" class="text-center break-all">
|
||||
<div v-if="items[0]?.thumbnail" class="flex justify-center">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getNavMenus } from '@/router/i18n-menu'
|
||||
import { NavMenu } from '@/@layouts/types'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { filterMenusByPermission } from '@/utils/permission'
|
||||
|
||||
const display = useDisplay()
|
||||
const appMode = inject('pwaMode')
|
||||
@@ -23,6 +24,12 @@ const userStore = useUserStore()
|
||||
// 是否超级用户
|
||||
let superUser = userStore.superUser
|
||||
|
||||
// 获取用户权限信息
|
||||
const userPermissions = computed(() => ({
|
||||
is_superuser: userStore.superUser,
|
||||
...userStore.permissions,
|
||||
}))
|
||||
|
||||
// 开始菜单项
|
||||
const startMenus = ref<NavMenu[]>([])
|
||||
|
||||
@@ -42,7 +49,8 @@ const systemMenus = ref<NavMenu[]>([])
|
||||
const getMenuList = (header: string) => {
|
||||
// 使用国际化菜单
|
||||
const menus = getNavMenus()
|
||||
return menus.filter((item: NavMenu) => item.header === header && (superUser || !item.admin))
|
||||
const filteredMenus = filterMenusByPermission(menus, userPermissions.value)
|
||||
return filteredMenus.filter((item: NavMenu) => item.header === header)
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
|
||||
@@ -3,6 +3,8 @@ import { getNavMenus } from '@/router/i18n-menu'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { NavMenu } from '@/@layouts/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { filterMenusByPermission } from '@/utils/permission'
|
||||
|
||||
const display = useDisplay()
|
||||
const appMode = inject('pwaMode') && display.mdAndDown.value
|
||||
@@ -13,8 +15,33 @@ const isEnglish = computed(() => locale.value === 'en-US')
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 用户Store
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 获取用户权限信息
|
||||
const userPermissions = computed(() => {
|
||||
// 确保用户已认证且信息已加载
|
||||
if (!userStore || userStore.userID === -1) {
|
||||
return {
|
||||
is_superuser: false,
|
||||
discovery: false,
|
||||
search: false,
|
||||
subscribe: false,
|
||||
manage: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
is_superuser: userStore.superUser,
|
||||
...userStore.permissions,
|
||||
}
|
||||
})
|
||||
|
||||
// 获取导航菜单
|
||||
const navMenus = computed(() => getNavMenus())
|
||||
const navMenus = computed(() => {
|
||||
const allMenus = getNavMenus()
|
||||
return filterMenusByPermission(allMenus, userPermissions.value)
|
||||
})
|
||||
|
||||
// 根据当前路径获取匹配的菜单路径
|
||||
function getMenuPathFromRoute(path: string): string {
|
||||
@@ -27,7 +54,42 @@ const currentMenu = ref<string>(getMenuPathFromRoute(route.path))
|
||||
|
||||
// 过滤出底部菜单项
|
||||
const footerMenus = computed(() => {
|
||||
return navMenus.value.filter((menu: NavMenu) => menu.footer === true)
|
||||
// 获取所有有权限的菜单
|
||||
const allAuthorizedMenus = navMenus.value
|
||||
|
||||
// 优先获取有 footer: true 属性的菜单
|
||||
const footerMenusWithProperty = allAuthorizedMenus.filter((menu: NavMenu) => menu.footer === true)
|
||||
|
||||
// 设置期望的底部菜单数量(不包括"更多"按钮)
|
||||
// 一般来说,底部导航栏显示 3-4 个主要功能比较合适
|
||||
const expectedFooterMenuCount = 3
|
||||
|
||||
// 如果有 footer 属性的菜单已经足够,优先显示它们
|
||||
if (footerMenusWithProperty.length >= expectedFooterMenuCount) {
|
||||
return footerMenusWithProperty.slice(0, expectedFooterMenuCount)
|
||||
}
|
||||
|
||||
// 如果不够,从没有 footer 属性或 footer 为 false 的菜单中补充
|
||||
// 优先选择一些常用的功能菜单
|
||||
const nonFooterMenus = allAuthorizedMenus.filter(
|
||||
(menu: NavMenu) =>
|
||||
menu.footer !== true &&
|
||||
// 排除已经在 footerMenusWithProperty 中的菜单
|
||||
!footerMenusWithProperty.some(footerMenu => footerMenu.to === menu.to),
|
||||
)
|
||||
|
||||
// 计算还需要多少个菜单
|
||||
const needCount = expectedFooterMenuCount - footerMenusWithProperty.length
|
||||
|
||||
// 合并菜单:优先显示有 footer 属性的,然后按菜单定义顺序添加其他菜单
|
||||
let finalMenus = [...footerMenusWithProperty, ...nonFooterMenus.slice(0, needCount)]
|
||||
|
||||
// 确保至少有一个菜单显示,如果都没有权限,则显示第一个有权限的菜单
|
||||
if (finalMenus.length === 0 && allAuthorizedMenus.length > 0) {
|
||||
finalMenus = [allAuthorizedMenus[0]]
|
||||
}
|
||||
|
||||
return finalMenus
|
||||
})
|
||||
|
||||
// 监听路由变化来更新currentMenu
|
||||
@@ -117,7 +179,7 @@ const showDynamicButton = computed(() => {
|
||||
:value="menu.to"
|
||||
>
|
||||
<div class="btn-content">
|
||||
<VIcon :icon="menu.icon" :size="isEnglish ? 32 : 24"></VIcon>
|
||||
<VIcon :icon="menu.icon" size="32"></VIcon>
|
||||
<span v-if="!isEnglish" class="text-xs">{{ menu.title }}</span>
|
||||
</div>
|
||||
</VBtn>
|
||||
@@ -134,8 +196,8 @@ const showDynamicButton = computed(() => {
|
||||
value="/apps"
|
||||
>
|
||||
<div class="btn-content">
|
||||
<VIcon icon="mdi-dots-horizontal" :size="isEnglish ? 32 : 24"></VIcon>
|
||||
<span v-if="!isEnglish" class="btn-text">{{ t('nav.more') }}</span>
|
||||
<VIcon icon="mdi-dots-horizontal" size="32"></VIcon>
|
||||
<span v-if="!isEnglish" class="text-xs">{{ t('nav.more') }}</span>
|
||||
</div>
|
||||
</VBtn>
|
||||
</VBtnToggle>
|
||||
@@ -153,7 +215,7 @@ const showDynamicButton = computed(() => {
|
||||
rounded="pill"
|
||||
class="footer-nav-btn"
|
||||
>
|
||||
<VIcon color="secondary" :icon="dynamicButton?.icon || 'mdi-plus'" size="24"></VIcon>
|
||||
<VIcon color="secondary" :icon="dynamicButton?.icon || 'mdi-plus'" size="28"></VIcon>
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
@@ -191,12 +253,17 @@ const showDynamicButton = computed(() => {
|
||||
&.shift-left {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.v-btn-toggle {
|
||||
block-size: auto;
|
||||
min-block-size: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card-content {
|
||||
position: relative;
|
||||
padding-block: 6px;
|
||||
padding-inline: 8px;
|
||||
padding-block: 4px;
|
||||
padding-inline: 6px;
|
||||
}
|
||||
|
||||
.footer-btn-group {
|
||||
@@ -212,8 +279,11 @@ const showDynamicButton = computed(() => {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 0;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
block-size: 48px;
|
||||
|
||||
&.v-btn--active {
|
||||
background-color: transparent;
|
||||
@@ -229,12 +299,8 @@ const showDynamicButton = computed(() => {
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
font-size: 0.75rem;
|
||||
max-inline-size: 100%;
|
||||
scale: var(--text-scale, 1);
|
||||
text-overflow: ellipsis;
|
||||
transform-origin: center;
|
||||
transition: scale 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
@@ -252,9 +318,9 @@ const showDynamicButton = computed(() => {
|
||||
|
||||
.footer-nav-btn {
|
||||
padding: 0;
|
||||
block-size: 36px;
|
||||
inline-size: 36px;
|
||||
min-inline-size: 36px;
|
||||
block-size: 40px;
|
||||
inline-size: 40px;
|
||||
min-inline-size: 40px;
|
||||
|
||||
.btn-content {
|
||||
margin: 0;
|
||||
|
||||
@@ -3,12 +3,28 @@ import * as Mousetrap from 'mousetrap'
|
||||
import SearchBarDialog from '@/components/dialog/SearchBarDialog.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
const display = useDisplay()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 用户Store
|
||||
const userStore = useUserStore()
|
||||
|
||||
const searchDialog = ref(false)
|
||||
|
||||
// 检查是否有搜索权限
|
||||
const hasSearchPermission = computed(() => {
|
||||
return hasPermission(
|
||||
{
|
||||
is_superuser: userStore.superUser,
|
||||
...userStore.permissions,
|
||||
},
|
||||
'search',
|
||||
)
|
||||
})
|
||||
|
||||
// 注册快捷键
|
||||
Mousetrap.bind(['command+k', 'ctrl+k'], openSearchDialog)
|
||||
|
||||
@@ -28,7 +44,7 @@ const metaKey = computed(() => (isMac() ? '⌘+K' : 'Ctrl+K'))
|
||||
|
||||
<template>
|
||||
<!-- 👉 Search Icon -->
|
||||
<div class="d-flex align-center cursor-pointer ms-lg-n2" style="user-select: none">
|
||||
<div v-if="hasSearchPermission" class="d-flex align-center cursor-pointer ms-lg-n2" style="user-select: none">
|
||||
<IconBtn @click="openSearchDialog">
|
||||
<VIcon icon="ri-search-line" />
|
||||
</IconBtn>
|
||||
@@ -38,7 +54,7 @@ const metaKey = computed(() => (isMac() ? '⌘+K' : 'Ctrl+K'))
|
||||
</span>
|
||||
</div>
|
||||
<!-- 搜索弹窗 -->
|
||||
<SearchBarDialog v-model="searchDialog" v-if="searchDialog" @close="searchDialog = false" />
|
||||
<SearchBarDialog v-model="searchDialog" v-if="searchDialog && hasSearchPermission" @close="searchDialog = false" />
|
||||
</template>
|
||||
<style type="scss" scoped>
|
||||
.meta-key {
|
||||
|
||||
@@ -45,36 +45,118 @@ const showLanguageMenu = ref(false)
|
||||
// 自定义CSS
|
||||
const customCSS = ref('')
|
||||
|
||||
// 重启轮询控制标识
|
||||
const restartPollingId = ref<number | null>(null)
|
||||
const isRestarting = ref(false)
|
||||
|
||||
// 确认框
|
||||
const { createConfirm } = useConfirm()
|
||||
|
||||
// 执行注销操作
|
||||
function logout() {
|
||||
// 清理重启相关状态
|
||||
isRestarting.value = false
|
||||
if (restartPollingId.value) {
|
||||
clearTimeout(restartPollingId.value)
|
||||
restartPollingId.value = null
|
||||
}
|
||||
|
||||
// 清除登录状态信息
|
||||
authStore.logout()
|
||||
userStore.reset()
|
||||
// 重定向到登录页面或其他适当的页面
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// 检测服务状态
|
||||
async function checkServiceStatus(): Promise<boolean> {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env', { timeout: 3000 })
|
||||
return result?.success === true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 轮询检测服务恢复状态
|
||||
async function pollServiceStatus() {
|
||||
// 如果已经有轮询在运行,先清除
|
||||
if (restartPollingId.value) {
|
||||
clearTimeout(restartPollingId.value)
|
||||
restartPollingId.value = null
|
||||
}
|
||||
|
||||
// 最大重试次数(约3分钟)
|
||||
const maxRetries = 60
|
||||
let retryCount = 0
|
||||
|
||||
const poll = async () => {
|
||||
// 如果不在重启状态,停止轮询
|
||||
if (!isRestarting.value) {
|
||||
return
|
||||
}
|
||||
|
||||
retryCount++
|
||||
const isServiceUp = await checkServiceStatus()
|
||||
|
||||
if (isServiceUp) {
|
||||
// 服务已恢复,清理状态并执行注销
|
||||
isRestarting.value = false
|
||||
progressDialog.value = false
|
||||
restartPollingId.value = null
|
||||
|
||||
setTimeout(() => {
|
||||
logout()
|
||||
}, 1000)
|
||||
return
|
||||
}
|
||||
|
||||
if (retryCount >= maxRetries) {
|
||||
// 超时未恢复,清理状态并提示用户
|
||||
isRestarting.value = false
|
||||
progressDialog.value = false
|
||||
restartPollingId.value = null
|
||||
$toast.error(t('app.restartTimeout'))
|
||||
return
|
||||
}
|
||||
|
||||
// 继续轮询,每3秒检测一次
|
||||
restartPollingId.value = setTimeout(poll, 3000) as unknown as number
|
||||
}
|
||||
|
||||
// 开始轮询
|
||||
poll()
|
||||
}
|
||||
|
||||
// 执行重启操作
|
||||
async function restart() {
|
||||
// 设置重启状态
|
||||
isRestarting.value = true
|
||||
|
||||
// 调用API重启
|
||||
try {
|
||||
// 显示等待框
|
||||
progressDialog.value = true
|
||||
const result: { [key: string]: any } = await api.get('system/restart')
|
||||
if (!result?.success) {
|
||||
// 隐藏等待框
|
||||
// 重启失败,清理状态
|
||||
isRestarting.value = false
|
||||
progressDialog.value = false
|
||||
// 重启不成功
|
||||
$toast.error(result.message)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
// 重启失败,清理状态
|
||||
isRestarting.value = false
|
||||
progressDialog.value = false
|
||||
console.error(error)
|
||||
return
|
||||
}
|
||||
// 注销
|
||||
logout()
|
||||
|
||||
// 重启请求成功,开始轮询检测服务状态
|
||||
setTimeout(() => {
|
||||
pollServiceStatus()
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
// 显示重启确认对话框
|
||||
@@ -257,6 +339,16 @@ const getThemeIcon = computed(() => {
|
||||
onMounted(() => {
|
||||
getCustomCSS()
|
||||
})
|
||||
|
||||
// 组件卸载时清理轮询
|
||||
onUnmounted(() => {
|
||||
// 清理重启轮询
|
||||
if (restartPollingId.value) {
|
||||
clearTimeout(restartPollingId.value)
|
||||
restartPollingId.value = null
|
||||
}
|
||||
isRestarting.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -282,7 +374,7 @@ onMounted(() => {
|
||||
</template>
|
||||
<div>
|
||||
<span class="text-primary text-sm font-medium d-block">
|
||||
{{ superUser ? t('user.admin') : t('user.normalUser') }}
|
||||
{{ superUser ? t('user.admin') : t('user.normal') }}
|
||||
</span>
|
||||
<span class="text-high-emphasis text-lg font-weight-bold">
|
||||
{{ userName }}
|
||||
|
||||
@@ -40,6 +40,10 @@ export default {
|
||||
media: 'Media',
|
||||
unknown: 'Unknown',
|
||||
notice: 'Notice',
|
||||
itemsPerPage: 'Items per page',
|
||||
pageText: '{0}-{1} of {2}',
|
||||
noDataText: 'No data',
|
||||
loadingText: 'Loading...',
|
||||
},
|
||||
mediaType: {
|
||||
movie: 'Movie',
|
||||
@@ -120,6 +124,8 @@ export default {
|
||||
restarting: 'Restarting...',
|
||||
confirmRestart: 'Confirm restart system?',
|
||||
restartTip: 'After restart, you will be logged out and need to log in again.',
|
||||
restartTimeout: 'Restart timeout, the system may need more time to recover, please refresh the page manually later',
|
||||
restartFailed: 'Restart failed, please check system status',
|
||||
},
|
||||
login: {
|
||||
wallpapers: 'Wallpapers',
|
||||
@@ -131,6 +137,7 @@ export default {
|
||||
networkError: 'Login failed, please check your network connection!',
|
||||
authFailure: 'Login failed, please check your username, password or two-factor authentication!',
|
||||
permissionDenied: 'Login failed, you do not have permission to access!',
|
||||
noPermission: 'Login failed, you have no functional permissions, please contact the administrator!',
|
||||
serverError: 'Login failed, server error!',
|
||||
loginFailed: 'Login Failed',
|
||||
checkCredentials: 'Please check your username, password or two-factor authentication code!',
|
||||
@@ -202,6 +209,10 @@ export default {
|
||||
title: 'Services',
|
||||
description: 'Scheduled jobs',
|
||||
},
|
||||
cache: {
|
||||
title: 'Cache',
|
||||
description: 'Torrent cache, media recognition data cache, image file cache management',
|
||||
},
|
||||
notification: {
|
||||
title: 'Notifications',
|
||||
description: 'Notification channels (WeChat, Telegram, Slack, SynologyChat, VoceChat, WebPush), message scope',
|
||||
@@ -1102,10 +1113,38 @@ export default {
|
||||
moviePilotAutoUpdateHint: 'Automatically update MoviePilot to the latest release version when restarting',
|
||||
autoUpdateResource: 'Auto Update Resource',
|
||||
autoUpdateResourceHint: 'Automatically detect and update site resource package when restarting',
|
||||
// Scraping Switch Settings
|
||||
scrapingSwitchSettings: 'Scraping Switch Settings',
|
||||
scrapingSwitchSettingsDesc: 'Control various media file scraping function switches',
|
||||
movie: 'Movie',
|
||||
tv: 'TV Show',
|
||||
season: 'Season',
|
||||
episode: 'Episode',
|
||||
movieNfo: 'NFO',
|
||||
seasonNfo: 'NFO',
|
||||
moviePoster: 'Poster',
|
||||
movieBackdrop: 'Backdrop',
|
||||
movieLogo: 'Logo',
|
||||
movieDisc: 'Disc',
|
||||
movieBanner: 'Banner',
|
||||
movieThumb: 'Thumb',
|
||||
tvNfo: 'NFO',
|
||||
tvPoster: 'Poster',
|
||||
tvBackdrop: 'Backdrop',
|
||||
tvBanner: 'Banner',
|
||||
tvLogo: 'Logo',
|
||||
tvThumb: 'Thumb',
|
||||
seasonPoster: 'Poster',
|
||||
seasonBanner: 'Banner',
|
||||
seasonThumb: 'Thumb',
|
||||
episodeNfo: 'NFO',
|
||||
episodeThumb: 'Thumb',
|
||||
scrapingSwitchSaveFailed: 'Scraping switch settings save failed: {message}',
|
||||
scrapingSwitchSaveError: 'Scraping switch settings save failed',
|
||||
},
|
||||
site: {
|
||||
siteSync: 'Site Synchronization',
|
||||
siteSyncDesc: 'Quickly sync site data from CookieCloud.',
|
||||
siteSyncDesc: 'Quickly sync site data from CookieCloud',
|
||||
enableLocalCookieCloud: 'Enable Local CookieCloud Server',
|
||||
enableLocalCookieCloudHint:
|
||||
'Use built-in CookieCloud service to sync site data, service address: http://localhost:3000/cookiecloud',
|
||||
@@ -1151,7 +1190,7 @@ export default {
|
||||
},
|
||||
notification: {
|
||||
channels: 'Notification Channels',
|
||||
channelsDesc: 'Set message sending channel parameters.',
|
||||
channelsDesc: 'Set message sending channel parameters',
|
||||
organizeSuccess: 'Media Import',
|
||||
downloadAdded: 'Download Added',
|
||||
subscribeAdded: 'Subscribe Added',
|
||||
@@ -1198,7 +1237,7 @@ export default {
|
||||
},
|
||||
words: {
|
||||
customIdentifiers: 'Custom Identifiers',
|
||||
identifiersDesc: 'Add rules to preprocess torrent names or file names to correct identification.',
|
||||
identifiersDesc: 'Add rules to preprocess torrent names or file names to correct identification',
|
||||
identifiersPlaceholder: 'Support regular expressions, special characters need \\ escape, one line for each rule',
|
||||
identifiersHint: 'Support regular expressions, special characters need \\ escape, one line for each rule',
|
||||
formatTitle: 'Supported configuration formats (mind the spaces):',
|
||||
@@ -1238,7 +1277,7 @@ export default {
|
||||
},
|
||||
search: {
|
||||
basicSettings: 'Basic Settings',
|
||||
basicSettingsDesc: 'Set data sources, rule groups and other basic information.',
|
||||
basicSettingsDesc: 'Set data sources, rule groups and other basic information',
|
||||
recognizeSource: 'Recognition Data Source',
|
||||
recognizeSourceDesc:
|
||||
'Default is TMDB. Douban is usually more friendly for Chinese works, but some foreign works have incomplete information.',
|
||||
@@ -1339,8 +1378,7 @@ export default {
|
||||
},
|
||||
scheduler: {
|
||||
title: 'Scheduled Jobs',
|
||||
subtitle:
|
||||
"Includes built-in system services and plugin services. Manual execution will not affect the job's normal schedule.",
|
||||
subtitle: 'Includes built-in system services and plugin services',
|
||||
provider: 'Provider',
|
||||
taskName: 'Task Name',
|
||||
taskStatus: 'Task Status',
|
||||
@@ -1387,6 +1425,55 @@ export default {
|
||||
settingsSaveSuccess: 'Subscription basic settings saved successfully',
|
||||
settingsSaveFailed: 'Failed to save subscription basic settings!',
|
||||
},
|
||||
cache: {
|
||||
title: 'Cache Management',
|
||||
subtitle: 'Manage torrent cache data',
|
||||
filterByTitle: 'Filter by Title',
|
||||
filterBySite: 'Filter by Site',
|
||||
selectSite: 'Select Site',
|
||||
refresh: 'Refresh Cache',
|
||||
deleteSelected: 'Delete Selected',
|
||||
clearAll: 'Clear All Cache',
|
||||
refreshSuccess: 'Cache refresh completed',
|
||||
refreshFailed: 'Failed to refresh cache',
|
||||
clearSuccess: 'Cache clear completed',
|
||||
clearFailed: 'Failed to clear cache',
|
||||
deleteSuccess: 'Cache item deleted successfully',
|
||||
deleteFailed: 'Failed to delete cache item',
|
||||
deleteSelectedSuccess: 'Successfully deleted {count} cache items',
|
||||
deleteSelectedFailed: 'Failed to delete cache items',
|
||||
loadFailed: 'Failed to load cache data',
|
||||
selectDeleteWarning: 'Please select cache items to delete',
|
||||
reidentify: 'Re-identify',
|
||||
reidentifySuccess: 'Re-identification completed',
|
||||
reidentifyFailed: 'Re-identification failed',
|
||||
poster: 'Poster',
|
||||
torrentTitle: 'Title',
|
||||
site: 'Site',
|
||||
size: 'Size',
|
||||
publishTime: 'Publish Time',
|
||||
recognitionResult: 'Recognition Result',
|
||||
actions: 'Actions',
|
||||
unrecognized: 'Unrecognized',
|
||||
noData: 'No cache data',
|
||||
noDataHint: 'Click "Refresh Cache" button to get the latest torrent cache',
|
||||
reidentifyDialog: {
|
||||
title: 'Re-identify',
|
||||
torrentInfo: 'Torrent Info',
|
||||
tmdbId: 'TMDB ID',
|
||||
tmdbIdHint: 'Optional, manually specify TMDB ID for recognition',
|
||||
doubanId: 'Douban ID',
|
||||
doubanIdHint: 'Optional, manually specify Douban ID for recognition',
|
||||
autoHint: 'If no ID is specified, the torrent will be automatically re-identified',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Re-identify',
|
||||
},
|
||||
mediaType: {
|
||||
movie: 'Movie',
|
||||
tv: 'TV Show',
|
||||
},
|
||||
clearConfirm: 'Are you sure you want to clear all cache?',
|
||||
},
|
||||
},
|
||||
dialog: {
|
||||
progress: {
|
||||
@@ -1453,6 +1540,19 @@ export default {
|
||||
saveUserInfo: 'Save User Information',
|
||||
cannotDeleteCurrentUser: 'Cannot delete current logged-in user',
|
||||
deleteUser: 'Delete User',
|
||||
permissions: {
|
||||
title: 'Permission Settings',
|
||||
presetNormal: 'Normal User',
|
||||
presetAdmin: 'Administrator',
|
||||
discovery: 'Discovery',
|
||||
discoveryDesc: 'Access recommendation and exploration features',
|
||||
search: 'Search',
|
||||
searchDesc: 'Use search functionality and view search results',
|
||||
subscribe: 'Subscribe',
|
||||
subscribeDesc: 'Manage movie and TV show subscriptions',
|
||||
manage: 'Manage',
|
||||
manageDesc: 'Access download management and site management etc.',
|
||||
},
|
||||
},
|
||||
searchBar: {
|
||||
search: 'Search',
|
||||
@@ -1964,6 +2064,7 @@ export default {
|
||||
filterPlugins: 'Filter Plugins',
|
||||
name: 'Name',
|
||||
hasNewVersion: 'Has New Version',
|
||||
running: 'Running',
|
||||
author: 'Author',
|
||||
label: 'Label',
|
||||
repository: 'Repository',
|
||||
|
||||
@@ -40,6 +40,10 @@ export default {
|
||||
media: '媒体',
|
||||
unknown: '未知',
|
||||
notice: '注意',
|
||||
itemsPerPage: '每页条数',
|
||||
pageText: '{0}-{1} 共 {2} 条',
|
||||
noDataText: '没有数据',
|
||||
loadingText: '加载中...',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '电影',
|
||||
@@ -120,6 +124,8 @@ export default {
|
||||
restarting: '正在重启...',
|
||||
confirmRestart: '确认重启系统吗?',
|
||||
restartTip: '重启后,您将被注销并需要重新登录。',
|
||||
restartTimeout: '重启超时,系统可能需要更长时间恢复,请稍后手动刷新页面',
|
||||
restartFailed: '重启失败,请检查系统状态',
|
||||
},
|
||||
login: {
|
||||
wallpapers: '壁纸',
|
||||
@@ -131,6 +137,7 @@ export default {
|
||||
networkError: '登录失败,请检查网络连接!',
|
||||
authFailure: '登录失败,请检查用户名、密码或双重验证是否正确!',
|
||||
permissionDenied: '登录失败,您没有权限访问!',
|
||||
noPermission: '登录失败,您没有任何功能权限,请联系管理员!',
|
||||
serverError: '登录失败,服务器错误!',
|
||||
loginFailed: '登录失败',
|
||||
checkCredentials: '请检查用户名、密码或双重验证码是否正确!',
|
||||
@@ -202,6 +209,10 @@ export default {
|
||||
title: '服务',
|
||||
description: '定时作业',
|
||||
},
|
||||
cache: {
|
||||
title: '缓存',
|
||||
description: '种子缓存、图片文件缓存管理',
|
||||
},
|
||||
notification: {
|
||||
title: '通知',
|
||||
description: '通知渠道(微信、Telegram、Slack、SynologyChat、VoceChat、WebPush)、消息发送范围',
|
||||
@@ -937,7 +948,7 @@ export default {
|
||||
system: {
|
||||
custom: '自定义',
|
||||
basicSettings: '基础设置',
|
||||
basicSettingsDesc: '设置服务器的全局功能。',
|
||||
basicSettingsDesc: '设置服务器的全局功能',
|
||||
appDomain: '访问域名',
|
||||
appDomainHint: '用于发送通知时,添加快捷跳转地址',
|
||||
wallpaper: '背景壁纸',
|
||||
@@ -1092,10 +1103,38 @@ export default {
|
||||
moviePilotAutoUpdateHint: '重启时自动更新MoviePilot到最新发行版本',
|
||||
autoUpdateResource: '自动更新站点资源',
|
||||
autoUpdateResourceHint: '重启时自动检测和更新站点资源包',
|
||||
// 刮削开关设置
|
||||
scrapingSwitchSettings: '刮削开关设置',
|
||||
scrapingSwitchSettingsDesc: '控制各类媒体文件的刮削功能开关',
|
||||
movie: '电影',
|
||||
tv: '电视剧',
|
||||
season: '季',
|
||||
episode: '集',
|
||||
movieNfo: 'NFO',
|
||||
moviePoster: '海报',
|
||||
movieBackdrop: '背景图',
|
||||
movieLogo: 'Logo',
|
||||
movieDisc: '光盘图',
|
||||
movieBanner: '横幅图',
|
||||
movieThumb: '缩略图',
|
||||
tvNfo: 'NFO',
|
||||
seasonNfo: 'NFO',
|
||||
tvPoster: '海报',
|
||||
tvBackdrop: '背景图',
|
||||
tvBanner: '横幅图',
|
||||
tvLogo: 'Logo',
|
||||
tvThumb: '缩略图',
|
||||
seasonPoster: '海报',
|
||||
seasonBanner: '横幅图',
|
||||
seasonThumb: '缩略图',
|
||||
episodeNfo: 'NFO',
|
||||
episodeThumb: '缩略图',
|
||||
scrapingSwitchSaveFailed: '刮削开关设置保存失败:{message}',
|
||||
scrapingSwitchSaveError: '刮削开关设置保存失败',
|
||||
},
|
||||
site: {
|
||||
siteSync: '站点同步',
|
||||
siteSyncDesc: '从CookieCloud快速同步站点数据。',
|
||||
siteSyncDesc: '从CookieCloud快速同步站点数据',
|
||||
enableLocalCookieCloud: '启用本地CookieCloud服务器',
|
||||
enableLocalCookieCloudHint: '使用内建CookieCloud服务同步站点数据,服务地址为:http://localhost:3000/cookiecloud',
|
||||
serviceAddress: '服务地址',
|
||||
@@ -1138,7 +1177,7 @@ export default {
|
||||
},
|
||||
notification: {
|
||||
channels: '通知渠道',
|
||||
channelsDesc: '设置消息发送渠道参数。',
|
||||
channelsDesc: '设置消息发送渠道参数',
|
||||
organizeSuccess: '资源入库',
|
||||
downloadAdded: '资源下载',
|
||||
subscribeAdded: '添加订阅',
|
||||
@@ -1185,7 +1224,7 @@ export default {
|
||||
},
|
||||
words: {
|
||||
customIdentifiers: '自定义识别词',
|
||||
identifiersDesc: '添加规则对种子名或者文件名进行预处理以校正识别。',
|
||||
identifiersDesc: '添加规则对种子名或者文件名进行预处理以校正识别',
|
||||
identifiersPlaceholder: '支持正则表达式,特殊字符需要\\转义,一行为一组',
|
||||
identifiersHint: '支持正则表达式,特殊字符需要\\转义,一行为一组',
|
||||
formatTitle: '支持的配置格式(注意空格):',
|
||||
@@ -1221,7 +1260,7 @@ export default {
|
||||
},
|
||||
search: {
|
||||
basicSettings: '基础设置',
|
||||
basicSettingsDesc: '设定数据源、规则组等基础信息。',
|
||||
basicSettingsDesc: '设定数据源、规则组等基础信息',
|
||||
recognizeSource: '识别数据源',
|
||||
recognizeSourceDesc: '默认使用TMDB。豆瓣识别中文作品通常更友好,但有些国外作品信息不完整。',
|
||||
themoviedb: 'TheMovieDb',
|
||||
@@ -1257,7 +1296,7 @@ export default {
|
||||
},
|
||||
directory: {
|
||||
storage: '存储',
|
||||
storageDesc: '设置本地或网盘存储。',
|
||||
storageDesc: '设置本地或网盘存储',
|
||||
directory: '目录',
|
||||
mediaType: '媒体类型',
|
||||
directoryDesc: '设置媒体文件整理目录结构,按先后顺序依次匹配。',
|
||||
@@ -1319,7 +1358,7 @@ export default {
|
||||
},
|
||||
scheduler: {
|
||||
title: '定时作业',
|
||||
subtitle: '包含系统内置服务以及插件提供的服务,手动执行不会影响作业正常的时间表。',
|
||||
subtitle: '包含系统内置服务以及插件提供的服务',
|
||||
provider: '提供者',
|
||||
taskName: '任务名称',
|
||||
taskStatus: '任务状态',
|
||||
@@ -1366,6 +1405,55 @@ export default {
|
||||
settingsSaveSuccess: '订阅基础设置保存成功',
|
||||
settingsSaveFailed: '订阅基础设置保存失败!',
|
||||
},
|
||||
cache: {
|
||||
title: '缓存管理',
|
||||
subtitle: '管理缓存的站点资源',
|
||||
filterByTitle: '按标题筛选',
|
||||
filterBySite: '按站点筛选',
|
||||
selectSite: '选择站点',
|
||||
refresh: '刷新缓存',
|
||||
deleteSelected: '删除选中',
|
||||
clearAll: '清空缓存',
|
||||
refreshSuccess: '缓存刷新完成',
|
||||
refreshFailed: '刷新缓存失败',
|
||||
clearSuccess: '缓存清理完成',
|
||||
clearFailed: '清理缓存失败',
|
||||
deleteSuccess: '缓存项删除成功',
|
||||
deleteFailed: '删除缓存项失败',
|
||||
deleteSelectedSuccess: '成功删除 {count} 个缓存项',
|
||||
deleteSelectedFailed: '删除缓存项失败',
|
||||
loadFailed: '加载缓存数据失败',
|
||||
selectDeleteWarning: '请选择要删除的缓存项',
|
||||
reidentify: '重新识别',
|
||||
reidentifySuccess: '重新识别完成',
|
||||
reidentifyFailed: '重新识别失败',
|
||||
poster: '海报',
|
||||
torrentTitle: '标题',
|
||||
site: '站点',
|
||||
size: '大小',
|
||||
publishTime: '发布时间',
|
||||
recognitionResult: '识别结果',
|
||||
actions: '操作',
|
||||
unrecognized: '未识别',
|
||||
noData: '暂无缓存数据',
|
||||
noDataHint: '点击"刷新缓存"按钮获取最新的种子缓存',
|
||||
reidentifyDialog: {
|
||||
title: '重新识别',
|
||||
torrentInfo: '种子信息',
|
||||
tmdbId: 'TMDB ID',
|
||||
tmdbIdHint: '可选,手动指定TMDB ID进行识别',
|
||||
doubanId: '豆瓣 ID',
|
||||
doubanIdHint: '可选,手动指定豆瓣ID进行识别',
|
||||
autoHint: '如果不指定ID,将自动重新识别该种子',
|
||||
cancel: '取消',
|
||||
confirm: '重新识别',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '电影',
|
||||
tv: '电视剧',
|
||||
},
|
||||
clearConfirm: '确认清空所有缓存吗?',
|
||||
},
|
||||
},
|
||||
dialog: {
|
||||
progress: {
|
||||
@@ -1432,6 +1520,19 @@ export default {
|
||||
saveUserInfo: '保存用户信息',
|
||||
cannotDeleteCurrentUser: '不能删除当前登录用户',
|
||||
deleteUser: '删除用户',
|
||||
permissions: {
|
||||
title: '权限设置',
|
||||
presetNormal: '普通用户',
|
||||
presetAdmin: '管理员',
|
||||
discovery: '发现',
|
||||
discoveryDesc: '访问推荐和探索功能',
|
||||
search: '搜索',
|
||||
searchDesc: '使用搜索功能和查看搜索结果',
|
||||
subscribe: '订阅',
|
||||
subscribeDesc: '管理电影和电视剧订阅',
|
||||
manage: '管理',
|
||||
manageDesc: '访问下载管理和站点管理等功能',
|
||||
},
|
||||
},
|
||||
searchBar: {
|
||||
search: '搜索',
|
||||
@@ -1941,6 +2042,7 @@ export default {
|
||||
filterPlugins: '过滤插件',
|
||||
name: '名称',
|
||||
hasNewVersion: '有新版本',
|
||||
running: '运行中',
|
||||
author: '作者',
|
||||
label: '标签',
|
||||
repository: '仓库',
|
||||
|
||||
@@ -40,6 +40,10 @@ export default {
|
||||
media: '媒體',
|
||||
unknown: '未知',
|
||||
notice: '注意',
|
||||
itemsPerPage: '每頁條數',
|
||||
pageText: '{0}-{1} 共 {2} 條',
|
||||
noDataText: '沒有數據',
|
||||
loadingText: '加載中...',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '電影',
|
||||
@@ -121,6 +125,8 @@ export default {
|
||||
restarting: '正在重啟...',
|
||||
confirmRestart: '確認重啟系統嗎?',
|
||||
restartTip: '重啟後,您將被註銷並需要重新登錄。',
|
||||
restartTimeout: '重啟超時,系統可能需要更長時間恢復,請稍後手動刷新頁面',
|
||||
restartFailed: '重啟失敗,請檢查系統狀態',
|
||||
},
|
||||
login: {
|
||||
wallpapers: '壁紙',
|
||||
@@ -133,6 +139,7 @@ export default {
|
||||
authFailure: '登錄失敗,請檢查用戶名、密碼或雙重驗證是否正確!',
|
||||
permissionDenied: '登錄失敗,您沒有權限訪問!',
|
||||
serverError: '登錄失敗,服務器錯誤!',
|
||||
noPermission: '登錄失敗,您沒有任何功能權限,請聯繫管理員!',
|
||||
loginFailed: '登錄失敗',
|
||||
checkCredentials: '請檢查用戶名、密碼或雙重驗證碼是否正確!',
|
||||
},
|
||||
@@ -203,6 +210,10 @@ export default {
|
||||
title: '服務',
|
||||
description: '定時作業',
|
||||
},
|
||||
cache: {
|
||||
title: '緩存',
|
||||
description: '種子緩存、識別媒體數據緩存、圖片文件緩存管理',
|
||||
},
|
||||
notification: {
|
||||
title: '通知',
|
||||
description: '通知渠道(微信、Telegram、Slack、SynologyChat、VoceChat、WebPush)、消息發送範圍',
|
||||
@@ -939,7 +950,7 @@ export default {
|
||||
system: {
|
||||
custom: '自定義',
|
||||
basicSettings: '基礎設置',
|
||||
basicSettingsDesc: '設置服務器的全局功能。',
|
||||
basicSettingsDesc: '設置服務器的全局功能',
|
||||
appDomain: '訪問域名',
|
||||
appDomainHint: '用於發送通知時,添加快捷跳轉地址',
|
||||
wallpaper: '背景壁紙',
|
||||
@@ -1094,10 +1105,38 @@ export default {
|
||||
moviePilotAutoUpdateHint: '重啟時自動更新MoviePilot到最新發行版本',
|
||||
autoUpdateResource: '自動更新站點資源',
|
||||
autoUpdateResourceHint: '重啟時自動檢測和更新站點資源包',
|
||||
// 刮削開關設定
|
||||
scrapingSwitchSettings: '刮削開關設定',
|
||||
scrapingSwitchSettingsDesc: '控制各類媒體檔案的刮削功能開關',
|
||||
movie: '電影',
|
||||
tv: '電視劇',
|
||||
season: '季',
|
||||
episode: '集',
|
||||
movieNfo: 'NFO',
|
||||
moviePoster: '海報',
|
||||
movieBackdrop: '背景圖',
|
||||
movieLogo: 'Logo',
|
||||
movieDisc: '光碟圖',
|
||||
movieBanner: '橫幅圖',
|
||||
movieThumb: '縮略圖',
|
||||
tvNfo: 'NFO',
|
||||
seasonNfo: 'NFO',
|
||||
tvPoster: '海報',
|
||||
tvBackdrop: '背景圖',
|
||||
tvBanner: '橫幅圖',
|
||||
tvLogo: 'Logo',
|
||||
tvThumb: '縮略圖',
|
||||
seasonPoster: '海報',
|
||||
seasonBanner: '橫幅圖',
|
||||
seasonThumb: '縮略圖',
|
||||
episodeNfo: 'NFO',
|
||||
episodeThumb: '縮略圖',
|
||||
scrapingSwitchSaveFailed: '刮削開關設定保存失敗:{message}',
|
||||
scrapingSwitchSaveError: '刮削開關設定保存失敗',
|
||||
},
|
||||
site: {
|
||||
siteSync: '站點同步',
|
||||
siteSyncDesc: '從CookieCloud快速同步站點數據。',
|
||||
siteSyncDesc: '從CookieCloud快速同步站點數據',
|
||||
enableLocalCookieCloud: '啟用本地CookieCloud服務器',
|
||||
enableLocalCookieCloudHint: '使用內建CookieCloud服務同步站點數據,服務地址為:http://localhost:3000/cookiecloud',
|
||||
serviceAddress: '服務地址',
|
||||
@@ -1140,7 +1179,7 @@ export default {
|
||||
},
|
||||
notification: {
|
||||
channels: '通知渠道',
|
||||
channelsDesc: '設置消息發送渠道參數。',
|
||||
channelsDesc: '設置消息發送渠道參數',
|
||||
organizeSuccess: '資源入庫',
|
||||
downloadAdded: '資源下載',
|
||||
subscribeAdded: '添加訂閱',
|
||||
@@ -1187,7 +1226,7 @@ export default {
|
||||
},
|
||||
words: {
|
||||
customIdentifiers: '自定義識別詞',
|
||||
identifiersDesc: '添加規則對種子名或者文件名進行預處理以校正識別。',
|
||||
identifiersDesc: '添加規則對種子名或者文件名進行預處理以校正識別',
|
||||
identifiersPlaceholder: '支持正則表達式,特殊字符需要\\轉義,一行為一組',
|
||||
identifiersHint: '支持正則表達式,特殊字符需要\\轉義,一行為一組',
|
||||
formatTitle: '支持的配置格式(注意空格):',
|
||||
@@ -1223,7 +1262,7 @@ export default {
|
||||
},
|
||||
search: {
|
||||
basicSettings: '基礎設置',
|
||||
basicSettingsDesc: '設定數據源、規則組等基礎信息。',
|
||||
basicSettingsDesc: '設定數據源、規則組等基礎信息',
|
||||
recognizeSource: '識別數據源',
|
||||
recognizeSourceDesc: '默認使用TMDB。豆瓣識別中文作品通常更友好,但有些國外作品信息不完整。',
|
||||
themoviedb: 'TheMovieDb',
|
||||
@@ -1259,7 +1298,7 @@ export default {
|
||||
},
|
||||
directory: {
|
||||
storage: '存儲',
|
||||
storageDesc: '設置本地或網盤存儲。',
|
||||
storageDesc: '設置本地或網盤存儲',
|
||||
directory: '目錄',
|
||||
directoryDesc: '設置媒體文件整理目錄結構,按先後順序依次匹配。',
|
||||
organizeAndScrap: '整理 & 刮削',
|
||||
@@ -1320,7 +1359,7 @@ export default {
|
||||
},
|
||||
scheduler: {
|
||||
scheduledTasks: '定時作業',
|
||||
scheduledTasksDesc: '包含系統內置服務以及插件提供的服務,手動執行不會影響作業正常的時間表。',
|
||||
scheduledTasksDesc: '包含系統內置服務以及插件提供的服務',
|
||||
provider: '提供者',
|
||||
taskName: '任務名稱',
|
||||
taskStatus: '任務狀態',
|
||||
@@ -1367,6 +1406,56 @@ export default {
|
||||
settingsSaveSuccess: '訂閱基礎設置保存成功',
|
||||
settingsSaveFailed: '訂閱基礎設置保存失敗!',
|
||||
},
|
||||
cache: {
|
||||
title: '緩存',
|
||||
description: '種子緩存、圖片文件緩存管理',
|
||||
subtitle: '管理緩存的站點資源',
|
||||
filterByTitle: '按標題篩選',
|
||||
filterBySite: '按站點篩選',
|
||||
selectSite: '選擇站點',
|
||||
refresh: '刷新緩存',
|
||||
deleteSelected: '刪除選中',
|
||||
clearAll: '清空緩存',
|
||||
refreshSuccess: '緩存刷新完成',
|
||||
refreshFailed: '刷新緩存失敗',
|
||||
clearSuccess: '緩存清理完成',
|
||||
clearFailed: '清理緩存失敗',
|
||||
deleteSuccess: '緩存項刪除成功',
|
||||
deleteFailed: '刪除緩存項失敗',
|
||||
deleteSelectedSuccess: '成功刪除 {count} 個緩存項',
|
||||
deleteSelectedFailed: '刪除緩存項失敗',
|
||||
loadFailed: '加載緩存數據失敗',
|
||||
selectDeleteWarning: '請選擇要刪除的緩存項',
|
||||
reidentify: '重新識別',
|
||||
reidentifySuccess: '重新識別完成',
|
||||
reidentifyFailed: '重新識別失敗',
|
||||
poster: '海報',
|
||||
torrentTitle: '標題',
|
||||
site: '站點',
|
||||
size: '大小',
|
||||
publishTime: '發布時間',
|
||||
recognitionResult: '識別結果',
|
||||
actions: '操作',
|
||||
unrecognized: '未識別',
|
||||
noData: '暫無緩存數據',
|
||||
noDataHint: '點擊"刷新緩存"按鈕獲取最新的種子緩存',
|
||||
reidentifyDialog: {
|
||||
title: '重新識別',
|
||||
torrentInfo: '種子信息',
|
||||
tmdbId: 'TMDB ID',
|
||||
tmdbIdHint: '可選,手動指定TMDB ID進行識別',
|
||||
doubanId: '豆瓣 ID',
|
||||
doubanIdHint: '可選,手動指定豆瓣ID進行識別',
|
||||
autoHint: '如果不指定ID,將自動重新識別該種子',
|
||||
cancel: '取消',
|
||||
confirm: '重新識別',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '電影',
|
||||
tv: '電視劇',
|
||||
},
|
||||
clearConfirm: '確認清空所有緩存嗎?',
|
||||
},
|
||||
},
|
||||
dialog: {
|
||||
progress: {
|
||||
@@ -1433,6 +1522,19 @@ export default {
|
||||
saveUserInfo: '保存用戶信息',
|
||||
cannotDeleteCurrentUser: '不能刪除當前登錄用戶',
|
||||
deleteUser: '刪除用戶',
|
||||
permissions: {
|
||||
title: '權限設置',
|
||||
presetNormal: '普通用戶',
|
||||
presetAdmin: '管理員',
|
||||
discovery: '發現',
|
||||
discoveryDesc: '存取推薦和探索功能',
|
||||
search: '搜索',
|
||||
searchDesc: '使用搜索功能和查看搜索結果',
|
||||
subscribe: '訂閱',
|
||||
subscribeDesc: '管理電影和電視劇訂閱',
|
||||
manage: '管理',
|
||||
manageDesc: '存取下載管理和站點管理等功能',
|
||||
},
|
||||
},
|
||||
searchBar: {
|
||||
search: '搜索',
|
||||
@@ -1942,6 +2044,7 @@ export default {
|
||||
filterPlugins: '過濾插件',
|
||||
name: '名稱',
|
||||
hasNewVersion: '有新版本',
|
||||
running: '運行中',
|
||||
author: '作者',
|
||||
label: '標籤',
|
||||
repository: '倉庫',
|
||||
|
||||
@@ -3,20 +3,30 @@ import { NavMenu } from '@/@layouts/types'
|
||||
import { getNavMenus } from '@/router/i18n-menu'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { filterMenusByPermission } from '@/utils/permission'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 从 Store 中获取superuser信息
|
||||
const superUser = useUserStore().superUser
|
||||
// 从 Store 中获取用户信息
|
||||
const userStore = useUserStore()
|
||||
const superUser = userStore.superUser
|
||||
|
||||
// 获取用户权限信息
|
||||
const userPermissions = computed(() => ({
|
||||
is_superuser: userStore.superUser,
|
||||
...userStore.permissions,
|
||||
}))
|
||||
|
||||
// 应用分组(以header分组)
|
||||
const appGroups = ref<Record<string, NavMenu[]>>({})
|
||||
|
||||
// 根据header属性对应用进行分类
|
||||
function categorizeApps() {
|
||||
// 获取可见的菜单项
|
||||
const menus = getNavMenus().filter((item: NavMenu) => (!item.admin || superUser) && !item.footer)
|
||||
// 获取所有菜单并根据权限过滤
|
||||
const allMenus = getNavMenus()
|
||||
const filteredMenus = filterMenusByPermission(allMenus, userPermissions.value)
|
||||
const menus = filteredMenus.filter((item: NavMenu) => !item.footer)
|
||||
|
||||
// 按header属性分组
|
||||
const groupedMenus: Record<string, NavMenu[]> = {}
|
||||
|
||||
@@ -11,6 +11,8 @@ import { urlBase64ToUint8Array } from '@/@core/utils/navigator'
|
||||
import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n'
|
||||
import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { getNavMenus } from '@/router/i18n-menu'
|
||||
import { filterMenusByPermission } from '@/utils/permission'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -19,6 +21,9 @@ const authStore = useAuthStore()
|
||||
//用户 Store
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 获取有权限的菜单
|
||||
const navMenus = getNavMenus()
|
||||
|
||||
// 表单
|
||||
const form = ref({
|
||||
username: '',
|
||||
@@ -111,9 +116,15 @@ async function subscribeForPushNotifications() {
|
||||
}
|
||||
|
||||
// 登录后处理
|
||||
async function afterLogin(superuser: boolean) {
|
||||
// 跳转到首页或回原始页面
|
||||
router.push(authStore.originalPath ?? '/')
|
||||
async function afterLogin(superuser: boolean, userPayload: userState, filteredMenus: any[]) {
|
||||
// 如果有原始路径,优先跳转到原始路径
|
||||
if (authStore.originalPath && authStore.originalPath !== '/') {
|
||||
router.push(authStore.originalPath)
|
||||
} else {
|
||||
// 跳转到第一个有权限的菜单
|
||||
router.push(filteredMenus[0].to)
|
||||
}
|
||||
|
||||
// 订阅推送通知
|
||||
if (superuser) await subscribeForPushNotifications()
|
||||
// 登录按钮 loading
|
||||
@@ -147,11 +158,6 @@ function login() {
|
||||
},
|
||||
})
|
||||
.then((response: any) => {
|
||||
const authPayLoad: authState = {
|
||||
token: response.access_token,
|
||||
remember: form.value.remember,
|
||||
}
|
||||
|
||||
const userPayload: userState = {
|
||||
superUser: response.super_user,
|
||||
userID: response.user_id,
|
||||
@@ -161,11 +167,32 @@ function login() {
|
||||
permissions: response.permissions,
|
||||
}
|
||||
|
||||
// 在保存用户信息之前检查权限
|
||||
const userPermissions = {
|
||||
is_superuser: userPayload.superUser,
|
||||
...userPayload.permissions,
|
||||
}
|
||||
|
||||
const filteredMenus = filterMenusByPermission(navMenus, userPermissions)
|
||||
// 如果用户没有任何可用菜单,拒绝登录
|
||||
if (filteredMenus.length === 0) {
|
||||
// 显示错误信息
|
||||
errorMessage.value = t('login.noPermission')
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 权限检查通过,保存用户信息
|
||||
const authPayLoad: authState = {
|
||||
token: response.access_token,
|
||||
remember: form.value.remember,
|
||||
}
|
||||
|
||||
authStore.login(authPayLoad)
|
||||
userStore.loginUser(userPayload)
|
||||
|
||||
// 登录后处理
|
||||
afterLogin(userPayload.superUser)
|
||||
afterLogin(userPayload.superUser, userPayload, filteredMenus)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
// 登录失败,显示错误提示
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'lodash-es'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import api from '@/api'
|
||||
import type { Context } from '@/api/types'
|
||||
@@ -51,6 +52,9 @@ const progressText = ref('')
|
||||
// 加载进度
|
||||
const progressValue = ref(0)
|
||||
|
||||
// 进度是否有效
|
||||
const progressEnabled = ref(false)
|
||||
|
||||
// 加载进度SSE
|
||||
const progressEventSource = ref<EventSource>()
|
||||
|
||||
@@ -60,23 +64,30 @@ const errorTitle = ref(t('resource.noData'))
|
||||
// 错误描述
|
||||
const errorDescription = ref(t('resource.noResourceFound'))
|
||||
|
||||
// 添加安全超时,确保进度条不会永远卡住
|
||||
const watchProgressValue = watch(
|
||||
progressValue,
|
||||
debounce(async () => {
|
||||
if (progressEventSource.value && progressValue.value < 100) {
|
||||
console.warn('卡进度超时 关闭进度条')
|
||||
stopLoadingProgress()
|
||||
}
|
||||
}, 60_000),
|
||||
)
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
function startLoadingProgress() {
|
||||
watchProgressValue.resume()
|
||||
progressText.value = t('resource.searching')
|
||||
progressValue.value = 10 // 初始进度设为10%,确保进度条显示
|
||||
progressValue.value = 0
|
||||
progressEnabled.value = false
|
||||
progressEventSource.value = new EventSource(`${import.meta.env.VITE_API_BASE_URL}system/progress/search`)
|
||||
progressEventSource.value.onmessage = event => {
|
||||
const progress = JSON.parse(event.data)
|
||||
if (progress) {
|
||||
progressText.value = progress.text
|
||||
progressValue.value = progress.value
|
||||
|
||||
// 搜索完成条件调整:只有明确完成时才关闭
|
||||
if (progress.text.includes('完成') && progress.value >= 99) {
|
||||
setTimeout(() => {
|
||||
stopLoadingProgress()
|
||||
}, 1000) // 延迟1秒关闭,确保用户能看到100%
|
||||
}
|
||||
progressEnabled.value = progress.enable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,26 +97,22 @@ function startLoadingProgress() {
|
||||
stopLoadingProgress()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 添加安全超时,确保不会永远卡住
|
||||
setTimeout(() => {
|
||||
if (progressEventSource.value && progressValue.value < 100) {
|
||||
stopLoadingProgress()
|
||||
}
|
||||
}, 60000) // 60秒超时
|
||||
}
|
||||
|
||||
// 停止监听加载进度
|
||||
function stopLoadingProgress() {
|
||||
watchProgressValue.pause()
|
||||
if (progressEventSource.value) {
|
||||
progressEventSource.value.close()
|
||||
progressEventSource.value = undefined
|
||||
|
||||
// 确保进度显示100%,然后再渐进清零
|
||||
progressValue.value = 100
|
||||
setTimeout(() => {
|
||||
progressValue.value = 0
|
||||
progressEnabled.value = false
|
||||
}, 1500) // 延长到1.5秒,让用户有足够时间看到完成状态
|
||||
}
|
||||
// 确保进度显示100%,然后再渐进清零
|
||||
progressValue.value = 100
|
||||
setTimeout(() => {
|
||||
progressValue.value = 0
|
||||
}, 1500) // 延长到1.5秒,让用户有足够时间看到完成状态
|
||||
}
|
||||
|
||||
// 设置视图类型
|
||||
@@ -186,7 +193,7 @@ onUnmounted(() => {
|
||||
<div>
|
||||
<!-- 加载进度条 -->
|
||||
<VFadeTransition>
|
||||
<div v-if="progressValue > 0" class="search-progress-container">
|
||||
<div v-if="progressValue > 0 || progressEnabled" class="search-progress-container">
|
||||
<VCard elevation="3" class="search-progress-card">
|
||||
<div class="progress-header">
|
||||
<VIcon icon="mdi-movie-search" color="primary" size="small" class="me-2" />
|
||||
@@ -273,17 +280,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- 初始加载状态 -->
|
||||
<div v-else-if="!isRefreshed && !progressValue" class="initial-loading-container">
|
||||
<div class="initial-loading-content">
|
||||
<div class="wave-loader">
|
||||
<div class="wave-dot"></div>
|
||||
<div class="wave-dot"></div>
|
||||
<div class="wave-dot"></div>
|
||||
<div class="wave-dot"></div>
|
||||
</div>
|
||||
<div class="initial-loading-text">{{ t('resource.searching') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<LoadingBanner v-else-if="!isRefreshed && !(progressEnabled || progressValue > 0)" />
|
||||
<!-- 滚动到顶部按钮 -->
|
||||
<VScrollToTopBtn />
|
||||
</div>
|
||||
@@ -452,70 +449,6 @@ onUnmounted(() => {
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* 初始的加载状态 */
|
||||
.initial-loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-block-size: 50vh;
|
||||
}
|
||||
|
||||
.initial-loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.wave-loader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
block-size: 40px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wave-dot {
|
||||
border-radius: 50%;
|
||||
animation: wave 1.5s ease-in-out infinite;
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
block-size: 8px;
|
||||
inline-size: 8px;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.wave-dot:nth-child(4) {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
.initial-loading-text {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.search-results-container {
|
||||
position: relative;
|
||||
min-block-size: 50vh;
|
||||
|
||||
@@ -8,9 +8,10 @@ import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
|
||||
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
|
||||
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
|
||||
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.vue'
|
||||
import AccountSettingScheduler from '@/views/setting/AccountSettingScheduler.vue'
|
||||
import AccountSettingService from '@/views/setting/AccountSettingService.vue'
|
||||
import AccountSettingDirectory from '@/views/setting/AccountSettingDirectory.vue'
|
||||
import AccountSettingRule from '@/views/setting/AccountSettingRule.vue'
|
||||
import AccountSettingCache from '@/views/setting/AccountSettingCache.vue'
|
||||
import { getSettingTabs } from '@/router/i18n-menu'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -81,7 +82,16 @@ const settingTabs = computed(() => getSettingTabs())
|
||||
<VWindowItem value="scheduler">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingScheduler />
|
||||
<AccountSettingService />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 缓存 -->
|
||||
<VWindowItem value="cache">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingCache />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
@@ -12,6 +12,7 @@ export function getNavMenus() {
|
||||
header: t('menu.start'),
|
||||
admin: false,
|
||||
footer: true,
|
||||
permission: 'manage',
|
||||
},
|
||||
{
|
||||
title: t('navItems.searchResult'),
|
||||
@@ -19,6 +20,7 @@ export function getNavMenus() {
|
||||
to: '/resource',
|
||||
header: t('menu.start'),
|
||||
admin: false,
|
||||
permission: 'search',
|
||||
},
|
||||
{
|
||||
title: t('navItems.recommend'),
|
||||
@@ -27,6 +29,7 @@ export function getNavMenus() {
|
||||
header: t('menu.discovery'),
|
||||
admin: false,
|
||||
footer: true,
|
||||
permission: 'discovery',
|
||||
},
|
||||
{
|
||||
title: t('navItems.explore'),
|
||||
@@ -35,6 +38,7 @@ export function getNavMenus() {
|
||||
header: t('menu.discovery'),
|
||||
admin: false,
|
||||
footer: true,
|
||||
permission: 'discovery',
|
||||
},
|
||||
{
|
||||
title: t('navItems.movie'),
|
||||
@@ -44,6 +48,7 @@ export function getNavMenus() {
|
||||
header: t('menu.subscribe'),
|
||||
admin: false,
|
||||
footer: false,
|
||||
permission: 'subscribe',
|
||||
},
|
||||
{
|
||||
title: t('navItems.tv'),
|
||||
@@ -53,6 +58,7 @@ export function getNavMenus() {
|
||||
header: t('menu.subscribe'),
|
||||
admin: false,
|
||||
footer: false,
|
||||
permission: 'subscribe',
|
||||
},
|
||||
{
|
||||
title: t('navItems.workflow'),
|
||||
@@ -62,6 +68,7 @@ export function getNavMenus() {
|
||||
header: t('menu.subscribe'),
|
||||
admin: true,
|
||||
footer: false,
|
||||
permission: 'manage',
|
||||
},
|
||||
{
|
||||
title: t('navItems.calendar'),
|
||||
@@ -70,6 +77,7 @@ export function getNavMenus() {
|
||||
to: '/calendar',
|
||||
header: t('menu.subscribe'),
|
||||
admin: false,
|
||||
permission: 'subscribe',
|
||||
},
|
||||
{
|
||||
title: t('navItems.downloadManager'),
|
||||
@@ -77,6 +85,7 @@ export function getNavMenus() {
|
||||
to: '/downloading',
|
||||
header: t('menu.organize'),
|
||||
admin: false,
|
||||
permission: 'manage',
|
||||
},
|
||||
{
|
||||
title: t('navItems.mediaOrganize'),
|
||||
@@ -84,6 +93,7 @@ export function getNavMenus() {
|
||||
to: '/history',
|
||||
header: t('menu.organize'),
|
||||
admin: true,
|
||||
permission: 'manage',
|
||||
},
|
||||
{
|
||||
title: t('navItems.fileManager'),
|
||||
@@ -91,6 +101,7 @@ export function getNavMenus() {
|
||||
to: '/filemanager',
|
||||
header: t('menu.organize'),
|
||||
admin: true,
|
||||
permission: 'manage',
|
||||
},
|
||||
{
|
||||
title: t('navItems.pluginManager'),
|
||||
@@ -98,6 +109,7 @@ export function getNavMenus() {
|
||||
to: '/plugins',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
permission: 'manage',
|
||||
},
|
||||
{
|
||||
title: t('navItems.siteManager'),
|
||||
@@ -105,6 +117,7 @@ export function getNavMenus() {
|
||||
to: '/site',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
permission: 'manage',
|
||||
},
|
||||
{
|
||||
title: t('navItems.userManager'),
|
||||
@@ -112,6 +125,7 @@ export function getNavMenus() {
|
||||
to: '/user',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
permission: 'admin',
|
||||
},
|
||||
{
|
||||
title: t('navItems.settings'),
|
||||
@@ -119,6 +133,7 @@ export function getNavMenus() {
|
||||
to: '/setting',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
permission: 'admin',
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -170,6 +185,12 @@ export function getSettingTabs() {
|
||||
tab: 'scheduler',
|
||||
description: t('settingTabs.scheduler.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.cache.title'),
|
||||
icon: 'mdi-database',
|
||||
tab: 'cache',
|
||||
description: t('settingTabs.cache.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.notification.title'),
|
||||
icon: 'mdi-bell',
|
||||
|
||||
@@ -224,15 +224,18 @@ function abortAllControllers() {
|
||||
}
|
||||
|
||||
// 路由导航守卫
|
||||
router.beforeEach((to: any, from: any, next: any) => {
|
||||
router.beforeEach(async (to: any, from: any, next: any) => {
|
||||
// 认证 Store
|
||||
const authStore = useAuthStore()
|
||||
// 总是记录非login路由
|
||||
if (to.fullPath != '/login') authStore.originalPath = to.fullPath
|
||||
const isAuthenticated = authStore.token !== null
|
||||
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
// 用户未登录,重定向到登录页
|
||||
next('/login')
|
||||
} else {
|
||||
// 清理所有中止控制器
|
||||
abortAllControllers()
|
||||
next()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { userState } from '@/stores/types'
|
||||
import { DEFAULT_PERMISSIONS } from '@/utils/permission'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: (): userState => ({
|
||||
@@ -8,7 +9,7 @@ export const useUserStore = defineStore('user', {
|
||||
userName: '',
|
||||
avatar: '',
|
||||
level: 1,
|
||||
permissions: {},
|
||||
permissions: DEFAULT_PERMISSIONS,
|
||||
}),
|
||||
|
||||
// 全局持久化
|
||||
@@ -31,7 +32,7 @@ export const useUserStore = defineStore('user', {
|
||||
this.level = level
|
||||
},
|
||||
setPermissions(permissions: object) {
|
||||
this.permissions = permissions
|
||||
this.permissions = { ...DEFAULT_PERMISSIONS, ...permissions }
|
||||
},
|
||||
loginUser(payload: userState) {
|
||||
this.setSuperUser(payload.superUser)
|
||||
@@ -47,7 +48,7 @@ export const useUserStore = defineStore('user', {
|
||||
this.setUserName('')
|
||||
this.setAvatar('')
|
||||
this.setLevel(1)
|
||||
this.setPermissions({})
|
||||
this.setPermissions(DEFAULT_PERMISSIONS)
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
63
src/utils/permission.ts
Normal file
63
src/utils/permission.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
// 权限类型定义
|
||||
export interface UserPermissions {
|
||||
discovery: boolean // 发现权限
|
||||
search: boolean // 搜索权限
|
||||
subscribe: boolean // 订阅权限
|
||||
manage: boolean // 管理权限
|
||||
}
|
||||
|
||||
// 默认权限配置
|
||||
export const DEFAULT_PERMISSIONS: UserPermissions = {
|
||||
discovery: true,
|
||||
search: true,
|
||||
subscribe: true,
|
||||
manage: false,
|
||||
}
|
||||
|
||||
// 管理员权限配置
|
||||
export const ADMIN_PERMISSIONS: UserPermissions = {
|
||||
discovery: true,
|
||||
search: true,
|
||||
subscribe: true,
|
||||
manage: true,
|
||||
}
|
||||
|
||||
// 权限检查函数
|
||||
export function hasPermission(userPermissions: any, permission: keyof UserPermissions): boolean {
|
||||
// 如果用户是超级用户,拥有所有权限
|
||||
if (userPermissions?.is_superuser === true) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查具体权限
|
||||
const permissions = userPermissions || {}
|
||||
return permissions[permission] === true
|
||||
}
|
||||
|
||||
// 批量权限检查
|
||||
export function hasAnyPermission(userPermissions: any, permissionList: (keyof UserPermissions)[]): boolean {
|
||||
return permissionList.some(permission => hasPermission(userPermissions, permission))
|
||||
}
|
||||
|
||||
// 检查是否有所有权限
|
||||
export function hasAllPermissions(userPermissions: any, permissionList: (keyof UserPermissions)[]): boolean {
|
||||
return permissionList.every(permission => hasPermission(userPermissions, permission))
|
||||
}
|
||||
|
||||
// 根据权限过滤菜单项
|
||||
export function filterMenusByPermission(menus: any[], userPermissions: any): any[] {
|
||||
return menus.filter(menu => {
|
||||
// 如果是超级用户,拥有所有权限
|
||||
if (userPermissions?.is_superuser) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 如果菜单没有权限要求,默认显示
|
||||
if (!menu.permission) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查用户是否拥有所需权限
|
||||
return hasPermission(userPermissions, menu.permission)
|
||||
})
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
|
||||
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -587,7 +588,10 @@ onBeforeMount(() => {
|
||||
</div>
|
||||
<div class="media-actions">
|
||||
<VBtn
|
||||
v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id"
|
||||
v-if="
|
||||
(mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id) &&
|
||||
hasPermission({ is_superuser: userStore.superUser, ...userStore.permissions }, 'search')
|
||||
"
|
||||
variant="tonal"
|
||||
color="info"
|
||||
class="mb-2"
|
||||
|
||||
@@ -93,6 +93,9 @@ const SearchDialog = ref(false)
|
||||
// 插件市场设置窗口
|
||||
const MarketSettingDialog = ref(false)
|
||||
|
||||
// 插件市场刷新状态
|
||||
const isMarketRefreshing = ref(false)
|
||||
|
||||
// 搜索关键字
|
||||
const keyword = ref('')
|
||||
|
||||
@@ -147,6 +150,9 @@ const installedFilter = ref(null)
|
||||
// 有新版本过滤条件
|
||||
const hasUpdateFilter = ref(false)
|
||||
|
||||
// 已启用过滤条件
|
||||
const enabledFilter = ref(false)
|
||||
|
||||
// 已安装插件过滤窗口
|
||||
const filterInstalledPluginDialog = ref(false)
|
||||
|
||||
@@ -191,9 +197,17 @@ const getFilteredFolderPlugins = (folderName: string) => {
|
||||
|
||||
// 应用筛选条件
|
||||
return folderPlugins.filter(plugin => {
|
||||
if (!installedFilter.value && !hasUpdateFilter.value) return true
|
||||
if (hasUpdateFilter.value) {
|
||||
return plugin.has_update
|
||||
if (!installedFilter.value && !hasUpdateFilter.value && !enabledFilter.value) return true
|
||||
if (hasUpdateFilter.value && enabledFilter.value) {
|
||||
return plugin.has_update && plugin.state
|
||||
}
|
||||
if (hasUpdateFilter.value) return plugin.has_update
|
||||
if (enabledFilter.value) return plugin.state
|
||||
if (installedFilter.value) {
|
||||
return plugin.plugin_name?.toLowerCase().includes((installedFilter.value as string).toLowerCase())
|
||||
}
|
||||
if (installedFilter.value) {
|
||||
return plugin.plugin_name?.toLowerCase().includes((installedFilter.value as string).toLowerCase())
|
||||
}
|
||||
if (installedFilter.value) {
|
||||
return plugin.plugin_name?.toLowerCase().includes((installedFilter.value as string).toLowerCase())
|
||||
@@ -263,7 +277,7 @@ const displayedFolders = computed(() => {
|
||||
})
|
||||
.filter(folder => {
|
||||
// 当有筛选条件时,只显示包含筛选后插件的文件夹
|
||||
if (installedFilter.value || hasUpdateFilter.value) {
|
||||
if (installedFilter.value || hasUpdateFilter.value || enabledFilter.value) {
|
||||
return folder.pluginCount > 0
|
||||
}
|
||||
return true
|
||||
@@ -278,9 +292,6 @@ function updateMixedSortList() {
|
||||
// 主列表:创建混合列表
|
||||
const items: MixedSortItem[] = []
|
||||
|
||||
// 创建统一的排序索引
|
||||
let globalOrder = 0
|
||||
|
||||
// 始终使用全局排序配置来创建混合列表
|
||||
const allItems: { type: 'folder' | 'plugin'; id: string; data: any; order: number }[] = []
|
||||
|
||||
@@ -330,7 +341,7 @@ function updateMixedSortList() {
|
||||
|
||||
// 监听相关数据变化,更新混合排序列表
|
||||
watch(
|
||||
[displayedPlugins, displayedFolders, orderConfig, folderOrder, installedFilter, hasUpdateFilter],
|
||||
[displayedPlugins, displayedFolders, orderConfig, folderOrder, installedFilter, hasUpdateFilter, enabledFilter],
|
||||
() => {
|
||||
// 只有在非拖拽状态下才更新
|
||||
if (!isDraggingSortMode.value) {
|
||||
@@ -566,7 +577,10 @@ async function installPlugin(item: Plugin) {
|
||||
|
||||
if (result.success) {
|
||||
$toast.success(t('plugin.installSuccess', { name: item?.plugin_name }))
|
||||
|
||||
// 清空过滤条件
|
||||
hasUpdateFilter.value = false
|
||||
enabledFilter.value = false
|
||||
installedFilter.value = null
|
||||
// 刷新
|
||||
refreshData()
|
||||
} else {
|
||||
@@ -643,12 +657,13 @@ async function fetchInstalledPlugins() {
|
||||
}
|
||||
|
||||
// 获取未安装插件列表数据
|
||||
async function fetchUninstalledPlugins() {
|
||||
async function fetchUninstalledPlugins(force: boolean = false) {
|
||||
try {
|
||||
loading.value = true
|
||||
uninstalledList.value = await api.get('plugin/', {
|
||||
params: {
|
||||
state: 'market',
|
||||
force: force,
|
||||
},
|
||||
})
|
||||
// 设置更新状态
|
||||
@@ -754,6 +769,19 @@ function marketSettingDone() {
|
||||
refreshData()
|
||||
}
|
||||
|
||||
// 手动刷新插件市场
|
||||
async function refreshMarket() {
|
||||
isMarketRefreshing.value = true
|
||||
try {
|
||||
await fetchUninstalledPlugins(true)
|
||||
await getPluginStatistics()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
isMarketRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理掉github地址的前缀
|
||||
function handleRepoUrl(url: string | undefined) {
|
||||
if (!url) return ''
|
||||
@@ -761,12 +789,14 @@ function handleRepoUrl(url: string | undefined) {
|
||||
}
|
||||
|
||||
// 监测dataList变化或installedFilter、hasUpdateFilter变化时更新filteredDataList
|
||||
watch([dataList, installedFilter, hasUpdateFilter], () => {
|
||||
watch([dataList, installedFilter, hasUpdateFilter, enabledFilter], () => {
|
||||
filteredDataList.value = dataList.value.filter(item => {
|
||||
if (!installedFilter.value && !hasUpdateFilter.value) return true
|
||||
if (hasUpdateFilter.value) {
|
||||
return item.has_update
|
||||
if (!installedFilter.value && !hasUpdateFilter.value && !enabledFilter.value) return true
|
||||
if (hasUpdateFilter.value && enabledFilter.value) {
|
||||
return item.has_update && item.state
|
||||
}
|
||||
if (hasUpdateFilter.value) return item.has_update
|
||||
if (enabledFilter.value) return item.state
|
||||
if (installedFilter.value) {
|
||||
return item.plugin_name?.toLowerCase().includes((installedFilter.value as string).toLowerCase())
|
||||
}
|
||||
@@ -1213,7 +1243,7 @@ function onDragStartPlugin(evt: any) {
|
||||
<VBtn
|
||||
icon="mdi-filter-multiple-outline"
|
||||
variant="text"
|
||||
:color="installedFilter ? 'primary' : 'gray'"
|
||||
:color="installedFilter || hasUpdateFilter || enabledFilter ? 'primary' : 'gray'"
|
||||
size="default"
|
||||
class="settings-icon-button"
|
||||
v-bind="props"
|
||||
@@ -1238,7 +1268,10 @@ function onDragStartPlugin(evt: any) {
|
||||
clearable
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCol cols="6">
|
||||
<VSwitch v-model="enabledFilter" :label="t('plugin.running')" />
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<VSwitch v-model="hasUpdateFilter" :label="t('plugin.hasNewVersion')" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -1323,6 +1356,16 @@ function onDragStartPlugin(evt: any) {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VMenu>
|
||||
<VBtn
|
||||
v-if="activeTab === 'market'"
|
||||
icon="mdi-refresh"
|
||||
variant="text"
|
||||
color="gray"
|
||||
size="default"
|
||||
class="settings-icon-button"
|
||||
:loading="isMarketRefreshing"
|
||||
@click="refreshMarket"
|
||||
/>
|
||||
<VBtn
|
||||
v-if="activeTab === 'market'"
|
||||
icon="mdi-store-cog"
|
||||
@@ -1443,10 +1486,10 @@ function onDragStartPlugin(evt: any) {
|
||||
<VWindowItem value="market">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<LoadingBanner v-if="!isAppMarketLoaded" class="mt-12" />
|
||||
<LoadingBanner v-if="!isAppMarketLoaded || isMarketRefreshing" class="mt-12" />
|
||||
<!-- 资源列表 -->
|
||||
<VInfiniteScroll
|
||||
v-if="isAppMarketLoaded"
|
||||
v-if="isAppMarketLoaded && !isMarketRefreshing"
|
||||
mode="intersect"
|
||||
side="end"
|
||||
:items="displayUninstalledList"
|
||||
|
||||
@@ -11,7 +11,6 @@ import router from '@/router'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { storageAttributes } from '@/api/constants'
|
||||
|
||||
// i18n
|
||||
const { t } = useI18n()
|
||||
@@ -215,7 +214,7 @@ const TransferDict: { [key: string]: string } = {
|
||||
|
||||
const tableStyle = computed(() => {
|
||||
return appMode
|
||||
? 'height: calc(100vh - 15rem - env(safe-area-inset-bottom) - 6.5rem)'
|
||||
? 'height: calc(100vh - 15rem - env(safe-area-inset-bottom) - 7rem)'
|
||||
: 'height: calc(100vh - 15rem - env(safe-area-inset-bottom)'
|
||||
})
|
||||
|
||||
|
||||
470
src/views/setting/AccountSettingCache.vue
Normal file
470
src/views/setting/AccountSettingCache.vue
Normal file
@@ -0,0 +1,470 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { TorrentCacheData, TorrentCacheItem } from '@/api/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { formatFileSize, formatDateDifference } from '@core/utils/formatters'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
const display = useDisplay()
|
||||
const appMode = inject('pwaMode') && display.mdAndDown.value
|
||||
|
||||
// 从 provide 中获取全局设置
|
||||
const globalSettings: any = inject('globalSettings')
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 缓存数据
|
||||
const cacheData = ref<TorrentCacheData>({
|
||||
count: 0,
|
||||
sites: 0,
|
||||
data: [],
|
||||
})
|
||||
|
||||
// 筛选条件
|
||||
const titleFilter = ref<string | null>(null)
|
||||
const siteFilter = ref<string | null>(null)
|
||||
|
||||
// 获取所有站点选项
|
||||
const siteOptions = computed(() => {
|
||||
const sites = new Set<string>()
|
||||
cacheData.value.data.forEach(item => {
|
||||
if (item.site_name) {
|
||||
sites.add(item.site_name)
|
||||
}
|
||||
})
|
||||
return Array.from(sites).sort()
|
||||
})
|
||||
|
||||
// 筛选后的数据
|
||||
const filteredData = computed(() => {
|
||||
return cacheData.value.data.filter(item => {
|
||||
const titleMatch = !titleFilter.value || item.title?.toLowerCase().includes(titleFilter.value?.toLowerCase())
|
||||
const siteMatch = !siteFilter.value || item.site_name === siteFilter.value
|
||||
return titleMatch && siteMatch
|
||||
})
|
||||
})
|
||||
|
||||
// 选中的缓存项
|
||||
const selectedItems = ref<string[]>([])
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 重新识别对话框
|
||||
const reidentifyDialog = ref(false)
|
||||
const currentReidentifyItem = ref<TorrentCacheItem | null>(null)
|
||||
const tmdbId = ref<number | undefined>()
|
||||
const doubanId = ref<string | undefined>()
|
||||
|
||||
const tableStyle = computed(() => {
|
||||
return appMode ? '' : 'height: calc(100vh - 21rem - env(safe-area-inset-bottom)'
|
||||
})
|
||||
|
||||
// 调用API加载缓存数据
|
||||
async function loadCacheData() {
|
||||
try {
|
||||
loading.value = true
|
||||
const res: any = await api.get('torrent/cache')
|
||||
cacheData.value = res.data
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.loadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 清空所有缓存
|
||||
async function clearAllCache() {
|
||||
const isConfirmed = await createConfirm({
|
||||
type: 'warn',
|
||||
title: t('common.confirm'),
|
||||
content: t('setting.cache.clearConfirm'),
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
try {
|
||||
loading.value = true
|
||||
await api.delete('torrent/cache')
|
||||
$toast.success(t('setting.cache.clearSuccess'))
|
||||
await loadCacheData()
|
||||
selectedItems.value = []
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.clearFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新缓存
|
||||
async function refreshCache() {
|
||||
try {
|
||||
loading.value = true
|
||||
const res: any = await api.post('torrent/cache/refresh')
|
||||
$toast.success(res.message || t('setting.cache.refreshSuccess'))
|
||||
await loadCacheData()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.refreshFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除选中的缓存项
|
||||
async function deleteSelectedItems() {
|
||||
if (selectedItems.value.length === 0) {
|
||||
$toast.warning(t('setting.cache.selectDeleteWarning'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const deletePromises = selectedItems.value.map(hash => {
|
||||
const item = cacheData.value.data.find(d => d.hash === hash)
|
||||
if (item) {
|
||||
return api.delete(`torrent/cache/${item.domain}/${hash}`)
|
||||
}
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
await Promise.all(deletePromises)
|
||||
$toast.success(t('setting.cache.deleteSelectedSuccess', { count: selectedItems.value.length }))
|
||||
await loadCacheData()
|
||||
selectedItems.value = []
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.deleteSelectedFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除单个缓存项
|
||||
async function deleteSingleItem(item: TorrentCacheItem) {
|
||||
try {
|
||||
loading.value = true
|
||||
await api.delete(`torrent/cache/${item.domain}/${item.hash}`)
|
||||
$toast.success(t('setting.cache.deleteSuccess'))
|
||||
await loadCacheData()
|
||||
// 从选中列表中移除
|
||||
const index = selectedItems.value.indexOf(item.hash)
|
||||
if (index > -1) {
|
||||
selectedItems.value.splice(index, 1)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.deleteFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 打开重新识别对话框
|
||||
function openReidentifyDialog(item: TorrentCacheItem) {
|
||||
currentReidentifyItem.value = item
|
||||
tmdbId.value = undefined
|
||||
doubanId.value = undefined
|
||||
reidentifyDialog.value = true
|
||||
}
|
||||
|
||||
// 重新识别
|
||||
async function performReidentify() {
|
||||
if (!currentReidentifyItem.value) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const params: any = {}
|
||||
if (tmdbId.value) params.tmdbid = tmdbId.value
|
||||
if (doubanId.value) params.doubanid = doubanId.value
|
||||
|
||||
const res: any = await api.post(
|
||||
`torrent/cache/reidentify/${currentReidentifyItem.value.domain}/${currentReidentifyItem.value.hash}`,
|
||||
null,
|
||||
{
|
||||
params,
|
||||
},
|
||||
)
|
||||
|
||||
$toast.success(res.message || t('setting.cache.reidentifySuccess'))
|
||||
await loadCacheData()
|
||||
reidentifyDialog.value = false
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.reidentifyFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取媒体类型颜色
|
||||
function getMediaTypeColor(type: string): string {
|
||||
switch (type) {
|
||||
case t('setting.cache.mediaType.movie'):
|
||||
return 'primary'
|
||||
case t('setting.cache.mediaType.tv'):
|
||||
return 'success'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
// 打开详情页面
|
||||
function openPageUrl(url: string) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCacheData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('setting.cache.title') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('setting.cache.subtitle') }}</VCardSubtitle>
|
||||
|
||||
<template #append>
|
||||
<div class="d-flex gap-2">
|
||||
<VBtn icon color="primary" :loading="loading" @click="refreshCache">
|
||||
<VIcon>mdi-refresh</VIcon>
|
||||
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.refresh') }}</VTooltip>
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
icon
|
||||
color="warning"
|
||||
:loading="loading"
|
||||
:disabled="selectedItems.length === 0"
|
||||
@click="deleteSelectedItems"
|
||||
>
|
||||
<VIcon>mdi-delete-sweep</VIcon>
|
||||
<VTooltip activator="parent" location="bottom"
|
||||
>{{ t('setting.cache.deleteSelected') }} ({{ selectedItems.length }})</VTooltip
|
||||
>
|
||||
</VBtn>
|
||||
|
||||
<VBtn icon color="error" :loading="loading" @click="clearAllCache">
|
||||
<VIcon>mdi-delete-variant</VIcon>
|
||||
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.clearAll') }}</VTooltip>
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<!-- 筛选框 -->
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="6">
|
||||
<VTextField
|
||||
v-model="titleFilter"
|
||||
:label="t('setting.cache.filterByTitle')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
clearable
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6">
|
||||
<VAutocomplete
|
||||
v-model="siteFilter"
|
||||
:label="t('setting.cache.filterBySite')"
|
||||
:items="siteOptions"
|
||||
prepend-inner-icon="mdi-web"
|
||||
clearable
|
||||
density="compact"
|
||||
:placeholder="t('setting.cache.selectSite')"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
|
||||
<!-- 缓存列表 -->
|
||||
<VDataTable
|
||||
v-model="selectedItems"
|
||||
:headers="[
|
||||
{ title: '', key: 'data-table-select', sortable: false, width: '48px' },
|
||||
{ title: t('setting.cache.poster'), key: 'poster', sortable: false, width: '80px' },
|
||||
{ title: t('setting.cache.torrentTitle'), key: 'title', sortable: true },
|
||||
{ title: t('setting.cache.site'), key: 'site_name', sortable: true, width: '120px' },
|
||||
{ title: t('setting.cache.size'), key: 'size', sortable: true, width: '100px' },
|
||||
{ title: t('setting.cache.publishTime'), key: 'pubdate', sortable: true, width: '150px' },
|
||||
{ title: t('setting.cache.recognitionResult'), key: 'media_info', sortable: false, width: '200px' },
|
||||
{ title: t('setting.cache.actions'), key: 'actions', sortable: false, width: '150px' },
|
||||
]"
|
||||
:items="filteredData"
|
||||
:loading="loading"
|
||||
item-value="hash"
|
||||
show-select
|
||||
hover
|
||||
fixed-header
|
||||
:items-per-page-text="t('common.itemsPerPage')"
|
||||
:no-data-text="t('common.noDataText')"
|
||||
:loading-text="t('common.loadingText')"
|
||||
:style="tableStyle"
|
||||
>
|
||||
<!-- 全选复选框 -->
|
||||
<template #header.data-table-select="{ allSelected, selectAll, someSelected }">
|
||||
<VCheckbox
|
||||
:indeterminate="someSelected && !allSelected"
|
||||
:model-value="allSelected"
|
||||
@update:model-value="(value: boolean | null) => selectAll(value as boolean)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 海报列 -->
|
||||
<template #item.poster="{ item }">
|
||||
<div class="text-center">
|
||||
<VImg
|
||||
v-if="item.poster_path"
|
||||
:src="item.poster_path"
|
||||
:alt="item.media_name || item.title"
|
||||
cover
|
||||
rounded="md"
|
||||
class="w-12 my-1 ms-auto"
|
||||
/>
|
||||
<VIcon v-else size="x-large" color="grey-lighten-1">
|
||||
{{ item.media_type === 'movie' ? 'mdi-movie-open' : 'mdi-television-play' }}
|
||||
</VIcon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 标题列 -->
|
||||
<template #item.title="{ item }">
|
||||
<div class="d-flex flex-column min-w-40">
|
||||
<div class="text-subtitle-2 font-weight-bold">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div v-if="item.description" class="text-caption text-grey">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
<div v-if="item.season_episode || item.resource_term" class="text-caption text-primary mt-1">
|
||||
{{ item.season_episode }} {{ item.resource_term }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 大小列 -->
|
||||
<template #item.size="{ item }">
|
||||
{{ formatFileSize(item.size) }}
|
||||
</template>
|
||||
|
||||
<!-- 发布时间列 -->
|
||||
<template #item.pubdate="{ item }">
|
||||
{{ formatDateDifference(item.pubdate || '') }}
|
||||
</template>
|
||||
|
||||
<!-- 识别结果列 -->
|
||||
<template #item.media_info="{ item }">
|
||||
<div v-if="item.media_name" class="d-flex flex-column">
|
||||
<div class="text-subtitle-2">
|
||||
{{ item.media_name }}
|
||||
<span v-if="item.media_year" class="text-caption text-grey"> ({{ item.media_year }}) </span>
|
||||
</div>
|
||||
<div>
|
||||
<VChip v-if="item.media_type" :color="getMediaTypeColor(item.media_type)" size="x-small">
|
||||
{{ item.media_type }}
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-caption text-grey">
|
||||
{{ t('setting.cache.unrecognized') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex gap-1">
|
||||
<VBtn icon size="small" color="primary" variant="text" @click="openReidentifyDialog(item)">
|
||||
<VIcon size="16">mdi-text-recognition</VIcon>
|
||||
</VBtn>
|
||||
|
||||
<VBtn icon size="small" color="error" variant="text" @click="deleteSingleItem(item)">
|
||||
<VIcon size="16">mdi-delete</VIcon>
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
v-if="item.page_url"
|
||||
icon
|
||||
size="small"
|
||||
color="info"
|
||||
variant="text"
|
||||
@click="openPageUrl(item.page_url || '')"
|
||||
target="_blank"
|
||||
>
|
||||
<VIcon size="16">mdi-open-in-new</VIcon>
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<template #no-data>
|
||||
<div class="text-center pa-4">
|
||||
<VIcon size="64" class="mb-4"> mdi-database-off </VIcon>
|
||||
<div class="text-body-2 text-grey">
|
||||
{{ t('setting.cache.noData') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCard>
|
||||
|
||||
<!-- 重新识别对话框 -->
|
||||
<VDialog v-model="reidentifyDialog" scrollable max-width="35rem">
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon>mdi-text-recognition</VIcon>
|
||||
</template>
|
||||
<VCardTitle>{{ t('setting.cache.reidentifyDialog.title') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ currentReidentifyItem?.title }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="reidentifyDialog = false" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-if="globalSettings.RECOGNIZE_SOURCE === 'themoviedb'"
|
||||
v-model="tmdbId"
|
||||
:label="t('setting.cache.reidentifyDialog.tmdbId')"
|
||||
:hint="t('setting.cache.reidentifyDialog.tmdbIdHint')"
|
||||
clearable
|
||||
prepend-inner-icon="mdi-id-card"
|
||||
persistent-hint
|
||||
/>
|
||||
<VTextField
|
||||
v-else
|
||||
v-model="doubanId"
|
||||
:label="t('setting.cache.reidentifyDialog.doubanId')"
|
||||
:hint="t('setting.cache.reidentifyDialog.doubanIdHint')"
|
||||
clearable
|
||||
prepend-inner-icon="mdi-id-card"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VAlert type="info" variant="tonal" class="mt-4">
|
||||
{{ t('setting.cache.reidentifyDialog.autoHint') }}
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn color="primary" :loading="loading" prepend-icon="mdi-check" @click="performReidentify">
|
||||
{{ t('setting.cache.reidentifyDialog.confirm') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
@@ -59,19 +59,6 @@ async function loadSystemSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
// 重载系统生效配置
|
||||
async function reloadSystem() {
|
||||
progressDialog.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success) $toast.success(t('setting.system.reloadSuccess'))
|
||||
else $toast.error(t('setting.system.reloadFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
progressDialog.value = false
|
||||
}
|
||||
|
||||
// 移动结束
|
||||
function orderDirectoryCards() {
|
||||
// 更新所有目录的优先级
|
||||
@@ -124,7 +111,6 @@ async function saveDirectories() {
|
||||
const result: { [key: string]: any } = await api.post('system/setting/Directories', directories.value)
|
||||
if (result.success) {
|
||||
$toast.success(t('setting.directory.directorySaveSuccess'))
|
||||
await reloadSystem()
|
||||
} else $toast.error(t('setting.directory.directorySaveFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
|
||||
@@ -103,19 +103,6 @@ const notificationTime = ref({
|
||||
end: '23:59',
|
||||
})
|
||||
|
||||
// 重载系统生效配置
|
||||
async function reloadSystem() {
|
||||
progressDialog.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success) $toast.success(t('setting.system.reloadSuccess'))
|
||||
else $toast.error(t('setting.system.reloadFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
progressDialog.value = false
|
||||
}
|
||||
|
||||
// 添加通知渠道
|
||||
function addNotification(notification: string) {
|
||||
let name = `${t('setting.notification.channel')}${notifications.value.length + 1}`
|
||||
@@ -199,7 +186,6 @@ async function saveNotificationSetting() {
|
||||
const result: { [key: string]: any } = await api.post('system/setting/Notifications', notifications.value)
|
||||
if (result.success) {
|
||||
$toast.success(t('setting.notification.saveSuccess'))
|
||||
await reloadSystem()
|
||||
} else $toast.error(t('setting.notification.saveFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
@@ -212,7 +198,6 @@ async function saveNotificationTime() {
|
||||
const result: { [key: string]: any } = await api.post('system/setting/NotificationSendTime', notificationTime.value)
|
||||
if (result.success) {
|
||||
$toast.success(t('setting.notification.timeSaveSuccess'))
|
||||
await reloadSystem()
|
||||
} else $toast.error(t('setting.notification.timeSaveFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
|
||||
@@ -209,7 +209,7 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="selectedFilterGroup"
|
||||
multiple
|
||||
clearable
|
||||
|
||||
@@ -96,27 +96,12 @@ async function loadSiteSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
// 重载系统生效配置
|
||||
async function reloadSystem() {
|
||||
progressDialog.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success) $toast.success(t('setting.system.reloadSuccess'))
|
||||
else $toast.error(t('setting.system.reloadFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
progressDialog.value = false
|
||||
}
|
||||
|
||||
// 调用API保存设置
|
||||
async function saveSiteSetting(value: { [key: string]: any }) {
|
||||
console.log(`正在保存设置:${JSON.stringify(value)}`)
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('system/env', value)
|
||||
if (result.success) {
|
||||
$toast.success(t('setting.site.saveSuccess'))
|
||||
await reloadSystem()
|
||||
} else {
|
||||
$toast.error(t('setting.site.saveFailed'))
|
||||
}
|
||||
|
||||
@@ -153,19 +153,6 @@ async function querySubscribeRules() {
|
||||
}
|
||||
}
|
||||
|
||||
// 重载系统生效配置
|
||||
async function reloadSystem() {
|
||||
progressDialog.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success) $toast.success(t('setting.system.reloadSuccess'))
|
||||
else $toast.error(t('setting.system.reloadFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
progressDialog.value = false
|
||||
}
|
||||
|
||||
// 保存订阅设置
|
||||
async function saveSubscribeSetting() {
|
||||
try {
|
||||
@@ -183,7 +170,6 @@ async function saveSubscribeSetting() {
|
||||
|
||||
if (result1.success && result2.success && result3) {
|
||||
$toast.success(t('setting.subscribe.settingsSaveSuccess'))
|
||||
await reloadSystem()
|
||||
} else $toast.error(t('setting.subscribe.settingsSaveFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
@@ -231,7 +217,7 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="selectedFilterRuleGroup"
|
||||
:items="filterRuleGroupOptions"
|
||||
chips
|
||||
@@ -244,7 +230,7 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
<VAutocomplete
|
||||
v-model="selectedBestVersionRuleGroup"
|
||||
:items="filterRuleGroupOptions"
|
||||
chips
|
||||
|
||||
@@ -67,10 +67,32 @@ const SystemSettings = ref<any>({
|
||||
// 实验室
|
||||
PLUGIN_AUTO_RELOAD: false,
|
||||
ENCODING_DETECTION_PERFORMANCE_MODE: true,
|
||||
TOKENIZED_SEARCH: false,
|
||||
},
|
||||
})
|
||||
|
||||
// 刮削开关设置
|
||||
const ScrapingSwitchs = ref<any>({
|
||||
movie_nfo: true, // 电影NFO
|
||||
movie_poster: true, // 电影海报
|
||||
movie_backdrop: true, // 电影背景图
|
||||
movie_logo: true, // 电影Logo
|
||||
movie_disc: true, // 电影光盘图
|
||||
movie_banner: true, // 电影横幅图
|
||||
movie_thumb: true, // 电影缩略图
|
||||
tv_nfo: true, // 电视剧NFO
|
||||
tv_poster: true, // 电视剧海报
|
||||
tv_backdrop: true, // 电视剧背景图
|
||||
tv_banner: true, // 电视剧横幅图
|
||||
tv_logo: true, // 电视剧Logo
|
||||
tv_thumb: true, // 电视剧缩略图
|
||||
season_nfo: true, // 季NFO
|
||||
season_poster: true, // 季海报
|
||||
season_banner: true, // 季横幅图
|
||||
season_thumb: true, // 季缩略图
|
||||
episode_nfo: true, // 集NFO
|
||||
episode_thumb: true, // 集缩略图
|
||||
})
|
||||
|
||||
// 是否发送请求的总开关
|
||||
const isRequest = ref(true)
|
||||
|
||||
@@ -131,19 +153,6 @@ async function loadDownloaderSetting() {
|
||||
}
|
||||
}
|
||||
|
||||
// 重载系统生效配置
|
||||
async function reloadSystem() {
|
||||
progressDialog.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/reload')
|
||||
if (result.success) $toast.success(t('setting.system.reloadSuccess'))
|
||||
else $toast.error(t('setting.system.reloadFailed'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
progressDialog.value = false
|
||||
}
|
||||
|
||||
// 调用API保存下载器设置
|
||||
async function saveDownloaderSetting() {
|
||||
try {
|
||||
@@ -158,7 +167,6 @@ async function saveDownloaderSetting() {
|
||||
else $toast.error(t('setting.system.downloaderSaveFailed'))
|
||||
|
||||
await loadDownloaderSetting()
|
||||
await reloadSystem()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -198,7 +206,6 @@ async function saveMediaServerSetting() {
|
||||
else $toast.error(t('setting.system.mediaServerSaveFailed'))
|
||||
|
||||
await loadMediaServerSetting()
|
||||
await reloadSystem()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
@@ -241,7 +248,6 @@ async function saveSystemSetting(value: { [key: string]: any }) {
|
||||
async function saveBasicSettings() {
|
||||
if (await saveSystemSetting(SystemSettings.value.Basic)) {
|
||||
$toast.success(t('setting.system.basicSaveSuccess'))
|
||||
await reloadSystem()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,10 +255,13 @@ async function saveBasicSettings() {
|
||||
async function saveAdvancedSettings() {
|
||||
cleanEmptyFields(SystemSettings.value.Advanced, ['LOG_FILE_FORMAT'])
|
||||
|
||||
if (await saveSystemSetting(SystemSettings.value.Advanced)) {
|
||||
// 同时保存高级设置和刮削开关设置
|
||||
const advancedResult = await saveSystemSetting(SystemSettings.value.Advanced)
|
||||
const scrapingResult = await saveScrapingSwitchs()
|
||||
|
||||
if (advancedResult && scrapingResult) {
|
||||
advancedDialog.value = false
|
||||
$toast.success(t('setting.system.advancedSaveSuccess'))
|
||||
await reloadSystem()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,11 +406,41 @@ const moviePilotAutoUpdate = computed({
|
||||
},
|
||||
})
|
||||
|
||||
// 加载刮削开关设置
|
||||
async function loadScrapingSwitchs() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/ScrapingSwitchs')
|
||||
if (result.success && result.data?.value) {
|
||||
ScrapingSwitchs.value = { ...ScrapingSwitchs.value, ...result.data.value }
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存刮削开关设置
|
||||
async function saveScrapingSwitchs() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('system/setting/ScrapingSwitchs', ScrapingSwitchs.value)
|
||||
if (result.success) {
|
||||
return true
|
||||
} else {
|
||||
$toast.error(t('setting.system.scrapingSwitchSaveFailed', { message: result?.message }))
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
$toast.error(t('setting.system.scrapingSwitchSaveError'))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
loadDownloaderSetting()
|
||||
loadMediaServerSetting()
|
||||
loadSystemSettings()
|
||||
loadScrapingSwitchs()
|
||||
})
|
||||
|
||||
onActivated(async () => {
|
||||
@@ -672,11 +711,14 @@ onDeactivated(() => {
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VDialogCloseBtn @click="advancedDialog = false" />
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-cog" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('setting.system.advancedSettings') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('setting.system.advancedSettingsDesc') }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="advancedDialog = false" />
|
||||
<VCardText>
|
||||
<VTabs v-model="activeTab" show-arrows>
|
||||
<VTab value="system">
|
||||
@@ -703,64 +745,42 @@ onDeactivated(() => {
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.AUXILIARY_AUTH_ENABLE"
|
||||
:label="t('setting.system.auxAuthEnable')"
|
||||
:hint="t('setting.system.auxAuthEnableHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.GLOBAL_IMAGE_CACHE"
|
||||
:label="t('setting.system.globalImageCache')"
|
||||
:hint="t('setting.system.globalImageCacheHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.SUBSCRIBE_STATISTIC_SHARE"
|
||||
:label="t('setting.system.subscribeStatisticShare')"
|
||||
:hint="t('setting.system.subscribeStatisticShareHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.PLUGIN_STATISTIC_SHARE"
|
||||
:label="t('setting.system.pluginStatisticShare')"
|
||||
:hint="t('setting.system.pluginStatisticShareHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.BIG_MEMORY_MODE"
|
||||
:label="t('setting.system.bigMemoryMode')"
|
||||
:hint="t('setting.system.bigMemoryModeHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.DB_WAL_ENABLE"
|
||||
:label="t('setting.system.dbWalEnable')"
|
||||
:hint="t('setting.system.dbWalEnableHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
<VSwitch v-model="SystemSettings.Advanced.DB_WAL_ENABLE" :label="t('setting.system.dbWalEnable')" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="moviePilotAutoUpdate"
|
||||
:label="t('setting.system.moviePilotAutoUpdate')"
|
||||
:hint="t('setting.system.moviePilotAutoUpdateHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
<VSwitch v-model="moviePilotAutoUpdate" :label="t('setting.system.moviePilotAutoUpdate')" />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.AUTO_UPDATE_RESOURCE"
|
||||
:label="t('setting.system.autoUpdateResource')"
|
||||
:hint="t('setting.system.autoUpdateResourceHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
@@ -774,8 +794,6 @@ onDeactivated(() => {
|
||||
v-model="SystemSettings.Advanced.TMDB_API_DOMAIN"
|
||||
:label="t('setting.system.tmdbApiDomain')"
|
||||
:placeholder="t('setting.system.tmdbApiDomainPlaceholder')"
|
||||
:hint="t('setting.system.tmdbApiDomainHint')"
|
||||
persistent-hint
|
||||
:items="['api.themoviedb.org', 'api.tmdb.org']"
|
||||
:rules="[(v: string) => !!v || t('setting.system.tmdbApiDomainRequired')]"
|
||||
prepend-inner-icon="mdi-api"
|
||||
@@ -786,8 +804,6 @@ onDeactivated(() => {
|
||||
v-model="SystemSettings.Advanced.TMDB_IMAGE_DOMAIN"
|
||||
:label="t('setting.system.tmdbImageDomain')"
|
||||
:placeholder="t('setting.system.tmdbImageDomainPlaceholder')"
|
||||
:hint="t('setting.system.tmdbImageDomainHint')"
|
||||
persistent-hint
|
||||
:items="['image.tmdb.org', 'static-mdb.v.geilijiasu.com']"
|
||||
:rules="[(v: string) => !!v || t('setting.system.tmdbImageDomainRequired')]"
|
||||
prepend-inner-icon="mdi-image"
|
||||
@@ -798,8 +814,6 @@ onDeactivated(() => {
|
||||
v-model="SystemSettings.Advanced.TMDB_LOCALE"
|
||||
:label="t('setting.system.tmdbLocale')"
|
||||
:placeholder="t('setting.system.tmdbLocalePlaceholder')"
|
||||
:hint="t('setting.system.tmdbLocaleHint')"
|
||||
persistent-hint
|
||||
:items="tmdbLanguageItems"
|
||||
prepend-inner-icon="mdi-translate"
|
||||
/>
|
||||
@@ -808,8 +822,6 @@ onDeactivated(() => {
|
||||
<VTextField
|
||||
v-model="SystemSettings.Advanced.META_CACHE_EXPIRE"
|
||||
:label="t('setting.system.metaCacheExpire')"
|
||||
:hint="t('setting.system.metaCacheExpireHint')"
|
||||
persistent-hint
|
||||
min="0"
|
||||
type="number"
|
||||
:suffix="t('setting.system.hour')"
|
||||
@@ -826,25 +838,196 @@ onDeactivated(() => {
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.SCRAP_FOLLOW_TMDB"
|
||||
:label="t('setting.system.scrapFollowTmdb')"
|
||||
:hint="t('setting.system.scrapFollowTmdbHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.TMDB_SCRAP_ORIGINAL_IMAGE"
|
||||
:label="t('setting.system.scrapOriginalImage')"
|
||||
:hint="t('setting.system.scrapOriginalImageHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.FANART_ENABLE"
|
||||
:label="t('setting.system.fanartEnable')"
|
||||
:hint="t('setting.system.fanartEnableHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
<VSwitch v-model="SystemSettings.Advanced.FANART_ENABLE" :label="t('setting.system.fanartEnable')" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- 刮削开关设置 -->
|
||||
<VRow class="mt-4">
|
||||
<VCol cols="12">
|
||||
<VExpansionPanels>
|
||||
<VExpansionPanel>
|
||||
<VExpansionPanelTitle class="text-lg">
|
||||
<template #default>
|
||||
<VIcon icon="mdi-checkbox-multiple-outline" class="me-2" />
|
||||
{{ t('setting.system.scrapingSwitchSettings') }}
|
||||
</template>
|
||||
</VExpansionPanelTitle>
|
||||
<VExpansionPanelText>
|
||||
<VRow>
|
||||
<VCol cols="12" class="pb-2">
|
||||
<VListSubheader class="text-lg">{{ t('setting.system.movie') }}</VListSubheader>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.movie_nfo"
|
||||
:label="t('setting.system.movieNfo')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.movie_poster"
|
||||
:label="t('setting.system.moviePoster')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.movie_backdrop"
|
||||
:label="t('setting.system.movieBackdrop')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.movie_logo"
|
||||
:label="t('setting.system.movieLogo')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.movie_disc"
|
||||
:label="t('setting.system.movieDisc')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.movie_banner"
|
||||
:label="t('setting.system.movieBanner')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.movie_thumb"
|
||||
:label="t('setting.system.movieThumb')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<VDivider class="my-4" />
|
||||
|
||||
<VRow>
|
||||
<VCol cols="12" class="pb-2">
|
||||
<VListSubheader class="text-lg">{{ t('setting.system.tv') }}</VListSubheader>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.tv_nfo"
|
||||
:label="t('setting.system.tvNfo')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.tv_poster"
|
||||
:label="t('setting.system.tvPoster')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.tv_backdrop"
|
||||
:label="t('setting.system.tvBackdrop')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.tv_banner"
|
||||
:label="t('setting.system.tvBanner')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.tv_logo"
|
||||
:label="t('setting.system.tvLogo')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.tv_thumb"
|
||||
:label="t('setting.system.tvThumb')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<VDivider class="my-4" />
|
||||
|
||||
<VRow>
|
||||
<VCol cols="12" class="pb-2">
|
||||
<VListSubheader class="text-lg">{{ t('setting.system.season') }}</VListSubheader>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.season_nfo"
|
||||
:label="t('setting.system.seasonNfo')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.season_poster"
|
||||
:label="t('setting.system.seasonPoster')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.season_banner"
|
||||
:label="t('setting.system.seasonBanner')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.season_thumb"
|
||||
:label="t('setting.system.seasonThumb')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<VDivider class="my-4" />
|
||||
|
||||
<VRow>
|
||||
<VCol cols="12" class="pb-2">
|
||||
<VListSubheader class="text-lg">{{ t('setting.system.episode') }}</VListSubheader>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.episode_nfo"
|
||||
:label="t('setting.system.episodeNfo')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<VCheckbox
|
||||
v-model="ScrapingSwitchs.episode_thumb"
|
||||
:label="t('setting.system.episodeThumb')"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VExpansionPanelText>
|
||||
</VExpansionPanel>
|
||||
</VExpansionPanels>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
@@ -919,43 +1102,46 @@ onDeactivated(() => {
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<!-- 安全域名 -->
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('setting.system.securityImageDomains') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('setting.system.securityImageDomainsHint') }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<VChip
|
||||
v-for="(domain, index) in SystemSettings.Advanced.SECURITY_IMAGE_DOMAINS"
|
||||
:key="index"
|
||||
closable
|
||||
@click:close="SystemSettings.Advanced.SECURITY_IMAGE_DOMAINS.splice(index, 1)"
|
||||
>
|
||||
{{ domain }}
|
||||
</VChip>
|
||||
<VChip v-if="SystemSettings.Advanced.SECURITY_IMAGE_DOMAINS.length === 0" color="warning">
|
||||
{{ t('setting.system.noSecurityImageDomains') }}
|
||||
</VChip>
|
||||
</div>
|
||||
<div class="d-flex align-center gap-2">
|
||||
<VTextField
|
||||
v-model="newSecurityDomain"
|
||||
:placeholder="t('setting.system.securityImageDomainAdd')"
|
||||
hide-details
|
||||
density="compact"
|
||||
prepend-inner-icon="mdi-shield-check"
|
||||
>
|
||||
<template #append>
|
||||
<VBtn icon color="primary" @click="addSecurityDomain" :disabled="!newSecurityDomain">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</template>
|
||||
</VTextField>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VExpansionPanels>
|
||||
<VExpansionPanel>
|
||||
<VExpansionPanelTitle class="text-lg">
|
||||
<template #default>
|
||||
<VIcon icon="mdi-shield-check" class="me-2" />
|
||||
{{ t('setting.system.securityImageDomains') }}
|
||||
</template>
|
||||
</VExpansionPanelTitle>
|
||||
<VExpansionPanelText>
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<VChip
|
||||
v-for="(domain, index) in SystemSettings.Advanced.SECURITY_IMAGE_DOMAINS"
|
||||
:key="index"
|
||||
closable
|
||||
@click:close="SystemSettings.Advanced.SECURITY_IMAGE_DOMAINS.splice(index, 1)"
|
||||
>
|
||||
{{ domain }}
|
||||
</VChip>
|
||||
<VChip v-if="SystemSettings.Advanced.SECURITY_IMAGE_DOMAINS.length === 0" color="warning">
|
||||
{{ t('setting.system.noSecurityImageDomains') }}
|
||||
</VChip>
|
||||
</div>
|
||||
<div class="d-flex align-center gap-2">
|
||||
<VTextField
|
||||
v-model="newSecurityDomain"
|
||||
:placeholder="t('setting.system.securityImageDomainAdd')"
|
||||
hide-details
|
||||
density="compact"
|
||||
prepend-inner-icon="mdi-shield-check"
|
||||
>
|
||||
<template #append>
|
||||
<VBtn icon color="primary" @click="addSecurityDomain" :disabled="!newSecurityDomain">
|
||||
<VIcon icon="mdi-plus" />
|
||||
</VBtn>
|
||||
</template>
|
||||
</VTextField>
|
||||
</div>
|
||||
</VExpansionPanelText>
|
||||
</VExpansionPanel>
|
||||
</VExpansionPanels>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
@@ -1038,14 +1224,6 @@ onDeactivated(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.TOKENIZED_SEARCH"
|
||||
:label="t('setting.system.tokenizedSearch')"
|
||||
:hint="t('setting.system.tokenizedSearchHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VWindowItem>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { isToday } from '@/@core/utils/index'
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// 定义输入变量
|
||||
const props = defineProps<{
|
||||
@@ -10,7 +12,7 @@ const props = defineProps<{
|
||||
const { t } = useI18n()
|
||||
|
||||
// 已解析的日志列表
|
||||
const parsedLogs = ref<{ level: string; time: string; program: string; content: string }[]>([])
|
||||
const parsedLogs = ref<{ level: string; date: string; time: string; program: string; content: string }[]>([])
|
||||
|
||||
// 表头
|
||||
const headers = [
|
||||
@@ -56,11 +58,11 @@ function startSSELogging() {
|
||||
// 解析新日志
|
||||
const newParsedLogs = buffer
|
||||
.map(log => {
|
||||
const logPattern = /^【(.*?)】[0-9\-:]*\s(.*?)\s-\s(.*?)\s-\s(.*)$/
|
||||
const logPattern = /^【(.*?)】\s*([\d]{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2})?)\s+(.*?)\s*-\s*(.*?)\s*-\s*(.*)$/
|
||||
const matches = log.match(logPattern)
|
||||
if (matches) {
|
||||
const [, level, time, program, content] = matches
|
||||
return { level, time, program, content }
|
||||
const [, level, date, time, program, content] = matches
|
||||
return { level, date, time, program, content }
|
||||
}
|
||||
return null
|
||||
})
|
||||
@@ -104,7 +106,8 @@ onBeforeUnmount(() => {
|
||||
<VChip size="small" :color="getLogColor(item.level)" variant="elevated" v-text="item.level" />
|
||||
</template>
|
||||
<template #item.time="{ item }">
|
||||
<span class="text-sm">{{ item.time }}</span>
|
||||
<span class="text-sm">{{ isToday(dayjs(item.date).toDate()) ? item.time : `${item.date}
|
||||
${item.time}` }}</span>
|
||||
</template>
|
||||
<template #item.program="{ item }">
|
||||
<h6 class="text-sm font-weight-medium">{{ item.program }}</h6>
|
||||
|
||||
Reference in New Issue
Block a user