mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-09 21:02:39 +08:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
229b7b0c12 | ||
|
|
4b7b5ff8a4 | ||
|
|
4906bde746 | ||
|
|
a87a1a8988 | ||
|
|
e05f45e681 | ||
|
|
b4acacea81 | ||
|
|
fa9645b05b | ||
|
|
1ed4052814 | ||
|
|
7dc814461f | ||
|
|
9154ec0e8c | ||
|
|
3a2ea60583 | ||
|
|
b36bff3a1e | ||
|
|
b3d8cbf280 | ||
|
|
38fb02d112 | ||
|
|
2597f893cd | ||
|
|
ebdd036654 | ||
|
|
5032f0e6a9 | ||
|
|
ad963d718d | ||
|
|
69d314bce3 | ||
|
|
4a7425a947 | ||
|
|
c172ac0d5c | ||
|
|
01a66493a8 | ||
|
|
188f8b3faa | ||
|
|
ebcf5fad71 | ||
|
|
d1a656db82 | ||
|
|
4f6a11fd7c | ||
|
|
1d09a946bb | ||
|
|
6c4eb7edbd | ||
|
|
4f9f669ac6 | ||
|
|
f9e0e78473 | ||
|
|
b004facfca | ||
|
|
fb6ee2910f | ||
|
|
3fedc9b730 | ||
|
|
b260427312 | ||
|
|
dd1447e93c | ||
|
|
dbcc213562 | ||
|
|
1c019cd5c8 | ||
|
|
e37bde77a1 | ||
|
|
57bf0d2021 | ||
|
|
88b00f7069 | ||
|
|
7b08cbb2f7 | ||
|
|
97c0ec184d | ||
|
|
d18c845088 | ||
|
|
a64d97774d | ||
|
|
2ddc51aa4f | ||
|
|
28afe2a922 | ||
|
|
c2e97bf191 | ||
|
|
c922752a1f | ||
|
|
08f36a74ca | ||
|
|
d7809dd00c | ||
|
|
27582004da | ||
|
|
3d6a176cde | ||
|
|
4a2073a038 | ||
|
|
c8a65ecbe4 | ||
|
|
3750d5cba0 | ||
|
|
55b383780e | ||
|
|
6aec0ddf88 | ||
|
|
7c8e94d1df | ||
|
|
5ecbf626c8 | ||
|
|
584f580e3b | ||
|
|
280de47dac | ||
|
|
c7c05f5897 | ||
|
|
bb86180582 | ||
|
|
aff228edd3 | ||
|
|
f65ae6d703 | ||
|
|
0fccc06883 | ||
|
|
8652966645 | ||
|
|
6d84eb9f09 | ||
|
|
1a3dccac29 | ||
|
|
fa8de34fc5 | ||
|
|
10cfd6be80 | ||
|
|
a390b36e7c | ||
|
|
d6b5994e22 | ||
|
|
08611a97e7 | ||
|
|
35bbb44ce3 | ||
|
|
8ff879661a | ||
|
|
a8f01f099d | ||
|
|
040ab1096b | ||
|
|
0cbdf24315 | ||
|
|
164ea79bd1 | ||
|
|
97f3435bb3 | ||
|
|
63b108ff6b | ||
|
|
b0880cb369 | ||
|
|
5f70ee8e18 |
@@ -245,13 +245,21 @@ const props = defineProps({
|
||||
|
||||
<template>
|
||||
<div class="dashboard-widget">
|
||||
<!-- 仪表板内容 -->
|
||||
<v-card>
|
||||
<v-card-title>{{ config.title || '仪表板组件' }}</v-card-title>
|
||||
<v-card-text>
|
||||
<!-- 组件内容 -->
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-hover>
|
||||
<!-- 仪表板内容 -->
|
||||
<template #default="{ isHovering, props: hoverProps }">
|
||||
<v-card v-bind="hoverProps">
|
||||
<v-card-title>{{ config.title || '仪表板组件' }}</v-card-title>
|
||||
<v-card-text>
|
||||
<!-- 组件内容 -->
|
||||
</v-card-text>
|
||||
<!-- 只在悬停时显示拖拽图标 -->
|
||||
<div v-show="isHovering" class="absolute right-5 top-5">
|
||||
<v-icon class="cursor-move">mdi-drag</v-icon>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-hover>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
17
index.html
17
index.html
@@ -13,7 +13,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<!-- 核心viewport设置 - 针对PWA优化 -->
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=no" />
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=no, interactive-widget=resizes-content" />
|
||||
|
||||
<!-- 防止缩放和选择,提供原生应用体验 -->
|
||||
<meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
|
||||
@@ -95,9 +95,14 @@
|
||||
<link rel="preload" href="/logo.png" as="image" />
|
||||
<link rel="modulepreload" href="/src/main.ts" />
|
||||
|
||||
<!-- 内联关键CSS -->
|
||||
<style>
|
||||
/* 关键路径CSS - 从loader.css内联 */
|
||||
#app {
|
||||
block-size: 100%;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
#loading-bg {
|
||||
position: fixed;
|
||||
z-index: 99999;
|
||||
@@ -115,14 +120,12 @@
|
||||
transition: opacity 0.8s ease, transform 0.8s ease, filter 0.8s ease;
|
||||
}
|
||||
|
||||
/* 添加logo完成动画 - 放大虚化效果 */
|
||||
.loading-complete .loading-logo {
|
||||
filter: blur(10px);
|
||||
opacity: 0;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
/* 添加加载背景消失动画 - 放大虚化效果 */
|
||||
.loading-complete {
|
||||
filter: blur(15px);
|
||||
opacity: 0;
|
||||
@@ -141,7 +144,6 @@
|
||||
transition: opacity 0.6s ease;
|
||||
}
|
||||
|
||||
/* 完成时隐藏加载动画 */
|
||||
.loading-complete .loading {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -197,7 +199,6 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 初始化脚本 -->
|
||||
<script>
|
||||
// 检测系统主题是否为深色模式
|
||||
function checkPrefersColorSchemeIsDark() {
|
||||
@@ -366,4 +367,4 @@
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.7.6",
|
||||
"version": "2.8.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": "dist/service.js",
|
||||
@@ -41,6 +41,7 @@
|
||||
"ace-builds": "^1.37.4",
|
||||
"apexcharts": "^4.0.0",
|
||||
"axios": "^1.7.9",
|
||||
"body-scroll-lock": "^3.1.5",
|
||||
"colorthief": "^2.6.0",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
@@ -74,6 +75,7 @@
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
||||
"@originjs/vite-plugin-federation": "^1.4.1",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@types/body-scroll-lock": "^3.1.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^20.1.4",
|
||||
|
||||
53
public/logo.svg
Normal file
53
public/logo.svg
Normal file
@@ -0,0 +1,53 @@
|
||||
<svg width="3em" height="3em" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<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">
|
||||
<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)">
|
||||
<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>
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 10 KiB |
@@ -1,5 +1,7 @@
|
||||
.auth-wrapper {
|
||||
min-block-size: calc(var(--vh, 1vh) * 100 + env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
||||
min-block-size: 100%;
|
||||
min-block-size: 100vh;
|
||||
min-block-size: 100dvh;
|
||||
}
|
||||
|
||||
.auth-footer-mask {
|
||||
|
||||
@@ -23,6 +23,13 @@ export function kFormatter(num: number) {
|
||||
: Math.abs(num).toFixed(0).replace(regex, ',')
|
||||
}
|
||||
|
||||
// 格式化下载量显示,超过1000显示为x.xk格式
|
||||
export function formatDownloadCount(num: number): string {
|
||||
if (!num || num < 1000) return num?.toLocaleString() || '0'
|
||||
|
||||
return `${(num / 1000).toFixed(1)}k`
|
||||
}
|
||||
|
||||
/**
|
||||
* Format and return date in Humanize format
|
||||
* Intl docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format
|
||||
|
||||
@@ -8,7 +8,6 @@ html {
|
||||
background: rgb(var(--v-theme-background));
|
||||
min-block-size: 100vh;
|
||||
min-block-size: 100dvh;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -7,5 +7,7 @@
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
min-height: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom))
|
||||
min-block-size: 100%;
|
||||
min-block-size: 100vh;
|
||||
min-block-size: 100dvh;
|
||||
}
|
||||
|
||||
@@ -314,6 +314,8 @@ export interface MediaInfo {
|
||||
production_countries?: any[]
|
||||
// 语种
|
||||
spoken_languages?: string[]
|
||||
// 数字/实体发行日期
|
||||
release_dates?: MediaRelease[]
|
||||
// 状态
|
||||
status?: string
|
||||
// 标签
|
||||
@@ -368,6 +370,18 @@ export interface TmdbSeason {
|
||||
vote_average?: number
|
||||
}
|
||||
|
||||
// 发行信息
|
||||
export interface MediaRelease {
|
||||
// 发行日期
|
||||
date: string
|
||||
// 发行地区
|
||||
iso_code: string
|
||||
// 备注
|
||||
note?: string
|
||||
// 发行类型
|
||||
type: number
|
||||
}
|
||||
|
||||
// TMDB集信息
|
||||
export interface TmdbEpisode {
|
||||
// 上映日期
|
||||
@@ -520,7 +534,7 @@ export interface SiteUserData {
|
||||
// 用户名
|
||||
username?: string
|
||||
// 用户ID
|
||||
userid?: number
|
||||
userid?: string
|
||||
// 用户等级
|
||||
user_level?: string
|
||||
// 加入时间
|
||||
@@ -992,6 +1006,8 @@ export interface MediaServerPlayItem {
|
||||
percent?: number
|
||||
// 媒体服务器类型
|
||||
server_type?: string
|
||||
// 图片是否需要Cookies
|
||||
use_cookies?: boolean
|
||||
}
|
||||
|
||||
// 媒体服务器媒体库
|
||||
@@ -1014,6 +1030,8 @@ export interface MediaServerLibrary {
|
||||
link?: string
|
||||
// 媒体服务器类型
|
||||
server_type?: string
|
||||
// 图片是否需要Cookies
|
||||
use_cookies?: boolean
|
||||
}
|
||||
|
||||
// 消息通知
|
||||
|
||||
@@ -26,7 +26,12 @@ async function goPlay() {
|
||||
// 计算图片地址
|
||||
const getImgUrl = computed(() => {
|
||||
const image = props.media?.image || ''
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
|
||||
let url = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
|
||||
const use_cookies = props.media?.use_cookies
|
||||
if (use_cookies) {
|
||||
url += `&use_cookies=${encodeURIComponent(use_cookies)}`
|
||||
}
|
||||
return url
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,9 +4,7 @@ import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import { DownloaderConf } from '@/api/types'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import type { DownloaderInfo } from '@/api/types'
|
||||
import qbittorrent_image from '@images/logos/qbittorrent.png'
|
||||
import transmission_image from '@images/logos/transmission.png'
|
||||
import custom_image from '@images/logos/downloader.png'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { downloaderDict } from '@/api/constants'
|
||||
@@ -128,11 +126,11 @@ function saveDownloaderInfo() {
|
||||
const getIcon = computed(() => {
|
||||
switch (props.downloader.type) {
|
||||
case 'qbittorrent':
|
||||
return qbittorrent_image
|
||||
return getLogoUrl('qbittorrent')
|
||||
case 'transmission':
|
||||
return transmission_image
|
||||
return getLogoUrl('transmission')
|
||||
default:
|
||||
return custom_image
|
||||
return getLogoUrl('downloader')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { MediaServerLibrary } from '@/api/types'
|
||||
import plex from '@images/misc/plex.png'
|
||||
import emby from '@images/misc/emby.png'
|
||||
import jellyfin from '@images/misc/jellyfin.png'
|
||||
import trimemedia from '@images/logos/trimemedia.png'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
import { openMediaServerWithAutoDetect } from '@/utils/appDeepLink'
|
||||
|
||||
// 输入参数
|
||||
@@ -40,7 +40,7 @@ function getDefaultImage() {
|
||||
if (props.media?.server_type === 'plex') return plex
|
||||
else if (props.media?.server_type === 'emby') return emby
|
||||
else if (props.media?.server_type === 'jellyfin') return jellyfin
|
||||
else if (props.media?.server_type === 'trimemedia') return trimemedia
|
||||
else if (props.media?.server_type === 'trimemedia') return getLogoUrl('trimemedia')
|
||||
else return plex
|
||||
}
|
||||
|
||||
@@ -52,31 +52,39 @@ async function goPlay() {
|
||||
}
|
||||
|
||||
// 生成图片代理路径
|
||||
function getImgUrl(url: string) {
|
||||
function getImgUrl(url: string, use_cookies?: boolean) {
|
||||
if (!url) return getDefaultImage()
|
||||
else return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
|
||||
let imgurl = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
|
||||
if (use_cookies) {
|
||||
imgurl += `&use_cookies=${encodeURIComponent(use_cookies)}`
|
||||
}
|
||||
return imgurl
|
||||
}
|
||||
|
||||
// 根据多张图片生成媒体库封面
|
||||
async function drawImages(imageList: string[]) {
|
||||
async function drawImages(imageList: string[], use_cookies?: boolean) {
|
||||
// 图片
|
||||
const IMAGES = imageList
|
||||
if (IMAGES.length === 0) return getDefaultImage()
|
||||
|
||||
// 为所有图片添加system/img前缀
|
||||
for (let i = 0; i < IMAGES.length; i++)
|
||||
for (let i = 0; i < IMAGES.length; i++) {
|
||||
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(IMAGES[i])}`
|
||||
if (use_cookies) {
|
||||
IMAGES[i] += `&use_cookies=${encodeURIComponent(use_cookies)}`
|
||||
}
|
||||
}
|
||||
|
||||
// canvas
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return getDefaultImage()
|
||||
|
||||
// 画布参数
|
||||
const POSTER_WIDTH = (canvas.width - 40) / 4 // 左右边框8px + 3个间隔24px = 40px
|
||||
const POSTER_HEIGHT = 256 // 上方海报高256
|
||||
const MARGIN_WIDTH = 8 // 左右间隔为8
|
||||
const MARGIN_HEIGHT = 4 // 海报和倒影之间的间隔为4
|
||||
const REFLECTION_HEIGHT = canvas.height - POSTER_HEIGHT - MARGIN_HEIGHT // 下方倒影使用剩余全部高度
|
||||
const POSTER_WIDTH = (canvas.width - 40) / 4 // 左右边框8px + 3个间隔24px = 40px
|
||||
const POSTER_HEIGHT = 256 // 上方海报高256
|
||||
const MARGIN_WIDTH = 8 // 左右间隔为8
|
||||
const MARGIN_HEIGHT = 4 // 海报和倒影之间的间隔为4
|
||||
const REFLECTION_HEIGHT = canvas.height - POSTER_HEIGHT - MARGIN_HEIGHT // 下方倒影使用剩余全部高度
|
||||
|
||||
// 获取画布上下文
|
||||
const ctx = canvas.getContext('2d')
|
||||
@@ -107,30 +115,20 @@ async function drawImages(imageList: string[]) {
|
||||
}
|
||||
|
||||
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
|
||||
const y = 0 // 海报紧贴顶部
|
||||
const y = 0 // 海报紧贴顶部
|
||||
|
||||
ctx.drawImage(img, x, y, POSTER_WIDTH, POSTER_HEIGHT)
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(0, canvas.height)
|
||||
ctx.scale(1, -1)
|
||||
ctx.drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
img.width,
|
||||
img.height,
|
||||
x,
|
||||
0,
|
||||
POSTER_WIDTH,
|
||||
REFLECTION_HEIGHT,
|
||||
)
|
||||
ctx.drawImage(img, 0, 0, img.width, img.height, x, 0, POSTER_WIDTH, REFLECTION_HEIGHT)
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height - (POSTER_HEIGHT + MARGIN_HEIGHT))
|
||||
|
||||
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)')
|
||||
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.7)')
|
||||
ctx.globalCompositeOperation = 'destination-out';
|
||||
ctx.globalCompositeOperation = 'destination-out'
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(x, 0, POSTER_WIDTH, REFLECTION_HEIGHT)
|
||||
|
||||
@@ -147,8 +145,8 @@ async function drawImages(imageList: string[]) {
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.media?.image_list && props.media?.image_list.length > 0)
|
||||
imgUrl.value = await drawImages(props.media?.image_list || [])
|
||||
else imgUrl.value = getImgUrl(props.media?.image || '')
|
||||
imgUrl.value = await drawImages(props.media?.image_list || [], props.media?.use_cookies)
|
||||
else imgUrl.value = getImgUrl(props.media?.image || '', props.media?.use_cookies)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import noImage from '@images/no-image.jpeg'
|
||||
import tmdbImage from '@images/logos/tmdb.png'
|
||||
import doubanImage from '@images/logos/douban-black.png'
|
||||
import bangumiImage from '@images/logos/bangumi.png'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
import api from '@/api'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { formatSeason, formatRating } from '@/@core/utils/formatters'
|
||||
@@ -64,9 +62,9 @@ const seasonsSelected = ref<MediaSeason[]>([])
|
||||
|
||||
// 来源角标字典
|
||||
const sourceIconDict: { [key: string]: any } = {
|
||||
themoviedb: tmdbImage,
|
||||
douban: doubanImage,
|
||||
bangumi: bangumiImage,
|
||||
themoviedb: getLogoUrl('tmdb'),
|
||||
douban: getLogoUrl('douban-black'),
|
||||
bangumi: getLogoUrl('bangumi'),
|
||||
}
|
||||
|
||||
// 绑定MediaCard元素
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { MediaServerConf, MediaServerLibrary, MediaStatistic } from '@/api/types'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import emby_image from '@images/logos/emby.png'
|
||||
import jellyfin_image from '@images/logos/jellyfin.png'
|
||||
import plex_image from '@images/logos/plex.png'
|
||||
import trimemedia_image from '@images/logos/trimemedia.png'
|
||||
import custom_image from '@images/logos/mediaserver.png'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
import api from '@/api'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -109,15 +105,15 @@ function saveMediaServerInfo() {
|
||||
const getIcon = computed(() => {
|
||||
switch (props.mediaserver.type) {
|
||||
case 'emby':
|
||||
return emby_image
|
||||
return getLogoUrl('emby')
|
||||
case 'jellyfin':
|
||||
return jellyfin_image
|
||||
return getLogoUrl('jellyfin')
|
||||
case 'trimemedia':
|
||||
return trimemedia_image
|
||||
return getLogoUrl('trimemedia')
|
||||
case 'plex':
|
||||
return plex_image
|
||||
return getLogoUrl('plex')
|
||||
default:
|
||||
return custom_image
|
||||
return getLogoUrl('mediaserver')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -262,6 +258,16 @@ onMounted(() => {
|
||||
prepend-inner-icon="mdi-play-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaServerInfo.config.username"
|
||||
:label="t('mediaserver.username')"
|
||||
:hint="t('mediaserver.usernameHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaServerInfo.config.apikey"
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { NotificationConf } from '@/api/types'
|
||||
import wechat_image from '@images/logos/wechat.png'
|
||||
import telegram_image from '@images/logos/telegram.webp'
|
||||
import vocechat_image from '@images/logos/vocechat.png'
|
||||
import synologychat_image from '@images/logos/synologychat.png'
|
||||
import slack_image from '@images/logos/slack.webp'
|
||||
import chrome_image from '@images/logos/chrome.png'
|
||||
import custom_image from '@images/logos/notification.png'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -99,19 +93,19 @@ function saveNotificationInfo() {
|
||||
const getIcon = computed(() => {
|
||||
switch (props.notification.type) {
|
||||
case 'wechat':
|
||||
return wechat_image
|
||||
return getLogoUrl('wechat')
|
||||
case 'telegram':
|
||||
return telegram_image
|
||||
return getLogoUrl('telegram')
|
||||
case 'vocechat':
|
||||
return vocechat_image
|
||||
return getLogoUrl('vocechat')
|
||||
case 'synologychat':
|
||||
return synologychat_image
|
||||
return getLogoUrl('synologychat')
|
||||
case 'slack':
|
||||
return slack_image
|
||||
return getLogoUrl('slack')
|
||||
case 'webpush':
|
||||
return chrome_image
|
||||
return getLogoUrl('chrome')
|
||||
default:
|
||||
return custom_image
|
||||
return getLogoUrl('notification')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ import { useToast } from 'vue-toastification'
|
||||
import VersionHistory from '../misc/VersionHistory.vue'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
import { getDominantColor } from '@/@core/utils/image'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { formatDownloadCount } from '@/@core/utils/formatters'
|
||||
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -103,7 +104,7 @@ async function installPlugin() {
|
||||
|
||||
// 计算图标路径
|
||||
const iconPath: Ref<string> = computed(() => {
|
||||
if (imageLoadError.value) return noImage
|
||||
if (imageLoadError.value) return getLogoUrl('plugin')
|
||||
// 如果是网络图片则使用代理后返回
|
||||
if (props.plugin?.plugin_icon?.startsWith('http'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(
|
||||
@@ -244,7 +245,7 @@ const dropdownItems = ref([
|
||||
</div>
|
||||
<div v-if="props.count" class="ms-2 flex-shrink-0 download-count align-middle items-center">
|
||||
<VIcon size="small" icon="mdi-download" />
|
||||
<span class="text-sm">{{ props.count?.toLocaleString() }}</span>
|
||||
<span class="text-sm">{{ formatDownloadCount(props.count) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 right-0">
|
||||
@@ -327,7 +328,7 @@ const dropdownItems = ref([
|
||||
}}</VBtn>
|
||||
<div class="text-xs mt-2" v-if="props.count">
|
||||
<VIcon icon="mdi-fire" />{{
|
||||
t('plugin.totalDownloads', { count: props.count?.toLocaleString() })
|
||||
t('plugin.totalDownloads', { count: formatDownloadCount(props.count) })
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,9 @@ import { useConfirm } from '@/composables/useConfirm'
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import { isNullOrEmptyObject } from '@core/utils'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
import { getDominantColor } from '@/@core/utils/image'
|
||||
import { formatDownloadCount } from '@/@core/utils/formatters'
|
||||
import VersionHistory from '@/components/misc/VersionHistory.vue'
|
||||
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
||||
import PluginConfigDialog from '../dialog/PluginConfigDialog.vue'
|
||||
@@ -167,7 +168,7 @@ async function showPluginConfig() {
|
||||
|
||||
// 计算图标路径
|
||||
const iconPath: Ref<string> = computed(() => {
|
||||
if (imageLoadError.value) return noImage
|
||||
if (imageLoadError.value) return getLogoUrl('plugin')
|
||||
// 如果是网络图片则使用代理后返回
|
||||
if (props.plugin?.plugin_icon?.startsWith('http'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(
|
||||
@@ -492,7 +493,7 @@ watch(
|
||||
</div>
|
||||
<span v-if="props.count" class="ms-2 flex-shrink-0 download-count items-center align-middle">
|
||||
<VIcon size="small" icon="mdi-download" />
|
||||
<span class="text-sm">{{ props.count?.toLocaleString() }}</span>
|
||||
<span class="text-sm">{{ formatDownloadCount(props.count) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="absolute bottom-0 right-0">
|
||||
|
||||
@@ -28,7 +28,12 @@ function getChipColor(type: string) {
|
||||
const getImgUrl = computed(() => {
|
||||
if (imageLoadError.value) return noImage
|
||||
const image = props.media?.image || ''
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
|
||||
let url = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
|
||||
const use_cookies = props.media?.use_cookies
|
||||
if (use_cookies) {
|
||||
url += `&use_cookies=${encodeURIComponent(use_cookies)}`
|
||||
}
|
||||
return url
|
||||
})
|
||||
|
||||
// 跳转播放
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
import noImage from '@images/logos/site.webp'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import SiteAddEditDialog from '../dialog/SiteAddEditDialog.vue'
|
||||
@@ -62,7 +62,7 @@ async function getSiteIcon() {
|
||||
try {
|
||||
siteIcon.value = (await api.get(`site/icon/${cardProps.site?.id}`)).data.icon
|
||||
if (!siteIcon.value) {
|
||||
siteIcon.value = noImage
|
||||
siteIcon.value = getLogoUrl('site')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
382
src/components/dialog/AboutDialog.vue
Normal file
382
src/components/dialog/AboutDialog.vue
Normal file
@@ -0,0 +1,382 @@
|
||||
<script lang="ts" setup>
|
||||
import { formatDateDifference } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
// 显示器
|
||||
const display = useDisplay()
|
||||
|
||||
// 系统环境变量
|
||||
const systemEnv = ref<any>({})
|
||||
|
||||
// 所有Release
|
||||
const allRelease = ref<any>([])
|
||||
|
||||
// 支持站点
|
||||
const supportingSites = ref<any>({})
|
||||
|
||||
// 支持站点折叠状态
|
||||
const sitesExpanded = ref(false)
|
||||
|
||||
// 去重后的支持站点
|
||||
const uniqueSupportingSites = computed(() => {
|
||||
const sitesMap = new Map()
|
||||
|
||||
Object.entries(supportingSites.value).forEach(([domain, site]: [string, any]) => {
|
||||
if (!sitesMap.has(site.name)) {
|
||||
sitesMap.set(site.name, {
|
||||
name: site.name,
|
||||
urls: [{ domain, url: site.url }],
|
||||
})
|
||||
} else {
|
||||
sitesMap.get(site.name).urls.push({ domain, url: site.url })
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(sitesMap.values())
|
||||
})
|
||||
|
||||
// 显示的支持站点(折叠时只显示前5个)
|
||||
const displayedSites = computed(() => {
|
||||
if (sitesExpanded.value) {
|
||||
return uniqueSupportingSites.value
|
||||
}
|
||||
return uniqueSupportingSites.value.slice(0, 5)
|
||||
})
|
||||
|
||||
// 变更日志对话框
|
||||
const releaseDialog = ref(false)
|
||||
|
||||
// 最新版本
|
||||
const latestRelease = ref('')
|
||||
|
||||
// 变更日志对话框标题
|
||||
const releaseDialogTitle = ref('')
|
||||
|
||||
// 变更日志对话框内容
|
||||
const releaseDialogBody = ref('')
|
||||
|
||||
// 打开日志对话框
|
||||
function showReleaseDialog(title: string, body: string) {
|
||||
releaseDialogTitle.value = title
|
||||
releaseDialogBody.value = body.replaceAll('\r\n', '<br />')
|
||||
releaseDialog.value = true
|
||||
}
|
||||
|
||||
// 查询系统环境变量
|
||||
async function querySystemEnv() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
|
||||
systemEnv.value = result.data
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询所有Release
|
||||
async function queryAllRelease() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/versions')
|
||||
|
||||
allRelease.value = result.data ?? []
|
||||
|
||||
// 最新版本
|
||||
if (allRelease.value.length > 0) latestRelease.value = allRelease.value[0].tag_name
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询支持站点
|
||||
async function querySupportingSites() {
|
||||
try {
|
||||
supportingSites.value = await api.get('site/supporting')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换站点列表展开状态
|
||||
function toggleSitesExpanded() {
|
||||
sitesExpanded.value = !sitesExpanded.value
|
||||
}
|
||||
|
||||
// 计算发布时间
|
||||
function releaseTime(releaseDate: string) {
|
||||
// 上一次更新时间
|
||||
return formatDateDifference(releaseDate)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
querySystemEnv()
|
||||
queryAllRelease()
|
||||
querySupportingSites()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-information" class="me-2" />
|
||||
{{ t('setting.about.title') }}
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<div class="px-3">
|
||||
<div class="section">
|
||||
<div class="section border-gray-800">
|
||||
<dl>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.softwareVersion') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow flex flex-row items-center truncate">
|
||||
<code class="truncate">{{ systemEnv.VERSION }}</code>
|
||||
<a
|
||||
v-if="latestRelease === systemEnv.VERSION"
|
||||
href="https://github.com/jxxghp/MoviePilot/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 ml-2 !cursor-pointer transition hover:bg-green-400"
|
||||
>
|
||||
{{ t('setting.about.latest') }}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="systemEnv.FRONTEND_VERSION">
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.frontendVersion') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow flex flex-row items-center truncate">
|
||||
<code class="truncate">{{ systemEnv.FRONTEND_VERSION }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.authVersion') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow flex flex-row items-center truncate">
|
||||
<code class="truncate">{{ systemEnv.AUTH_VERSION }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.indexerVersion') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow flex flex-row items-center truncate">
|
||||
<code class="truncate">{{ systemEnv.INDEXER_VERSION }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.configDir') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined">
|
||||
<code>{{ systemEnv.CONFIG_DIR }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.dataDir') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined"
|
||||
><code>{{ t('setting.about.dataDirectory') }}</code></span
|
||||
>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.timezone') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined">
|
||||
<code>{{ systemEnv.TZ }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.supportingSites') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap gap-2 mt-1 ms-1">
|
||||
<VChip v-for="site in displayedSites" :key="site.name" variant="outlined" size="small">
|
||||
<span class="truncate max-w-32">{{ site.name }}</span>
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="!sitesExpanded && uniqueSupportingSites.length > 5"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
@click="toggleSitesExpanded"
|
||||
>
|
||||
<span> {{ uniqueSupportingSites.length }}+ ...</span>
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="sitesExpanded && uniqueSupportingSites.length > 5"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
@click="toggleSitesExpanded"
|
||||
>
|
||||
<span>< {{ t('setting.about.collapse') }}</span>
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div>
|
||||
<h3 class="heading">{{ t('setting.about.support') }}</h3>
|
||||
</div>
|
||||
<div class="section border-t border-gray-800">
|
||||
<dl>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.documentation') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined">
|
||||
<a
|
||||
href="https://movie-pilot.org"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="text-indigo-500 transition duration-300 hover:underline"
|
||||
>
|
||||
https://movie-pilot.org
|
||||
</a>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.feedback') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined">
|
||||
<a
|
||||
href="https://github.com/jxxghp/MoviePilot/issues/new/choose"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="text-indigo-500 transition duration-300 hover:underline"
|
||||
>
|
||||
https://github.com/jxxghp/MoviePilot/issues/new/choose
|
||||
</a>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.channel') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined">
|
||||
<a
|
||||
href="https://t.me/moviepilot_channel"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="text-indigo-500 transition duration-300 hover:underline"
|
||||
>
|
||||
https://t.me/moviepilot_channel
|
||||
</a>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div>
|
||||
<h3 class="heading">{{ t('setting.about.versions') }}</h3>
|
||||
<div class="section space-y-3">
|
||||
<div>
|
||||
<div
|
||||
v-for="release in allRelease"
|
||||
:key="release.tag_name"
|
||||
class="mb-3 flex w-full flex-col space-y-3 rounded-md px-4 py-2 ring-1 ring-gray-400 sm:flex-row sm:space-y-0 sm:space-x-3"
|
||||
>
|
||||
<div class="flex w-full flex-grow items-center justify-start space-x-2 truncate sm:justify-start">
|
||||
<span class="truncate text-lg font-bold">
|
||||
<span class="mr-2 whitespace-nowrap text-xs font-normal">{{
|
||||
releaseTime(release.published_at)
|
||||
}}</span>
|
||||
{{ release.tag_name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="release.tag_name === latestRelease"
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap cursor-default bg-green-500 bg-opacity-80 border border-green-500 !text-green-100"
|
||||
>
|
||||
{{ t('setting.about.latestVersion') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="release.tag_name === systemEnv.VERSION"
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap cursor-default bg-indigo-500 bg-opacity-80 border border-indigo-500 !text-indigo-100"
|
||||
>
|
||||
{{ t('setting.about.currentVersion') }}
|
||||
</span>
|
||||
</div>
|
||||
<VBtn @click.stop="showReleaseDialog(release.tag_name, release.body)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-text-box-outline" />
|
||||
</template>
|
||||
{{ t('setting.about.viewChangelog') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VDialogCloseBtn @click="releaseDialog = false" />
|
||||
<VCardTitle>{{ releaseDialogTitle }} {{ t('setting.about.changelog') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText v-html="releaseDialogBody" />
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style type="scss" scoped>
|
||||
.heading {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
line-height: 2rem;
|
||||
|
||||
--tw-text-opacity: 1;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-block: 0.5rem 2.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -6,10 +6,20 @@ import type { DownloaderConf, MediaInfo, TorrentInfo, TransferDirectoryConf } fr
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import { VCardTitle, VChip } from 'vuetify/lib/components/index.mjs'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import MediaIdSelector from '../misc/MediaIdSelector.vue'
|
||||
import { numberValidator } from '@/@validators'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
|
||||
// 从 provide 中获取全局设置
|
||||
const globalSettingsStore = useGlobalSettingsStore()
|
||||
const globalSettings = globalSettingsStore.globalSettings
|
||||
|
||||
// 当前识别类型
|
||||
const mediaSource = ref(globalSettings.RECOGNIZE_SOURCE || 'themoviedb')
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
@@ -38,6 +48,18 @@ const directories = ref<TransferDirectoryConf[]>([])
|
||||
// 是否正在加载
|
||||
const loading = ref(false)
|
||||
|
||||
// 是否显示高级选项
|
||||
const showAdvancedOptions = ref(false)
|
||||
|
||||
// TMDB ID
|
||||
const tmdbid = ref<number | undefined>(undefined)
|
||||
|
||||
// 豆瓣ID
|
||||
const doubanId = ref<string | undefined>(undefined)
|
||||
|
||||
// TMDB选择对话框
|
||||
const mediaSelectorDialog = ref(false)
|
||||
|
||||
// 计算按钮图标
|
||||
const icon = computed(() => (loading.value ? 'mdi-progress-download' : 'mdi-download'))
|
||||
|
||||
@@ -96,6 +118,14 @@ async function addDownload() {
|
||||
payload.media_in = props.media
|
||||
}
|
||||
|
||||
// 添加媒体ID辅助识别
|
||||
if (tmdbid.value) {
|
||||
payload.tmdbid = tmdbid.value
|
||||
}
|
||||
if (doubanId.value) {
|
||||
payload.doubanid = doubanId.value
|
||||
}
|
||||
|
||||
const endpoint = props.media ? 'download/' : 'download/add'
|
||||
|
||||
result = await api.post(endpoint, payload)
|
||||
@@ -202,6 +232,56 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow class="px-5 mt-2">
|
||||
<VCol cols="12">
|
||||
<VBtn
|
||||
variant="text"
|
||||
size="small"
|
||||
:prepend-icon="showAdvancedOptions ? 'mdi-chevron-up' : 'mdi-chevron-down'"
|
||||
@click="showAdvancedOptions = !showAdvancedOptions"
|
||||
>
|
||||
{{
|
||||
showAdvancedOptions
|
||||
? t('dialog.addDownload.hideAdvancedOptions')
|
||||
: t('dialog.addDownload.showAdvancedOptions')
|
||||
}}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-show="showAdvancedOptions" class="px-5">
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-if="mediaSource === 'themoviedb'"
|
||||
v-model="tmdbid"
|
||||
:label="t('dialog.reorganize.tmdbId')"
|
||||
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
|
||||
:rules="[numberValidator]"
|
||||
append-inner-icon="mdi-magnify"
|
||||
:hint="t('dialog.reorganize.mediaIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-identifier"
|
||||
size="small"
|
||||
variant="underlined"
|
||||
density="comfortable"
|
||||
@click:append-inner="mediaSelectorDialog = true"
|
||||
/>
|
||||
<VTextField
|
||||
v-else
|
||||
v-model="doubanId"
|
||||
:label="t('dialog.reorganize.doubanId')"
|
||||
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
|
||||
:rules="[numberValidator]"
|
||||
append-inner-icon="mdi-magnify"
|
||||
:hint="t('dialog.reorganize.mediaIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-identifier"
|
||||
size="small"
|
||||
variant="underlined"
|
||||
density="comfortable"
|
||||
@click:append-inner="mediaSelectorDialog = true"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardText class="text-center">
|
||||
<VBtn variant="elevated" :disabled="loading" @click="addDownload" :prepend-icon="icon" class="px-5">
|
||||
@@ -209,5 +289,15 @@ onMounted(() => {
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<!-- 媒体ID选择器 -->
|
||||
<VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh">
|
||||
<MediaIdSelector
|
||||
v-if="mediaSource === 'themoviedb'"
|
||||
v-model="tmdbid"
|
||||
@close="mediaSelectorDialog = false"
|
||||
:type="mediaSource"
|
||||
/>
|
||||
<MediaIdSelector v-else v-model="doubanId" @close="mediaSelectorDialog = false" :type="mediaSource" />
|
||||
</VDialog>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -82,6 +82,9 @@ const items = ref<FileItem[]>([])
|
||||
// 过滤条件
|
||||
const filter = ref('')
|
||||
|
||||
// 是否忽略大小写
|
||||
const ignoreCase = ref(true)
|
||||
|
||||
// 重命名弹窗
|
||||
const renamePopper = ref(false)
|
||||
|
||||
@@ -112,12 +115,26 @@ const dropdownItems = ref<{ [key: string]: any }[]>([])
|
||||
// 进度是否激活
|
||||
const progressActive = ref(false)
|
||||
|
||||
// 通用过滤
|
||||
const getFilteredItems = (type: 'dir' | 'file') => {
|
||||
const filterValue = filter.value
|
||||
if (!filterValue) {
|
||||
return items.value.filter(item => item.type === type)
|
||||
}
|
||||
|
||||
if (ignoreCase.value) {
|
||||
const lowerCaseFilter = filterValue.toLowerCase()
|
||||
return items.value.filter(item => item.type === type && item.name.toLowerCase().includes(lowerCaseFilter))
|
||||
} else {
|
||||
return items.value.filter(item => item.type === type && item.name.includes(filterValue))
|
||||
}
|
||||
}
|
||||
|
||||
// 目录过滤
|
||||
const dirs = computed(() => items.value.filter(item => item.type === 'dir' && item.name.includes(filter.value)))
|
||||
const dirs = computed(() => getFilteredItems('dir'))
|
||||
|
||||
// 文件过滤
|
||||
const files = computed(() => items.value.filter(item => item.type === 'file' && item.name.includes(filter.value)))
|
||||
|
||||
const files = computed(() => getFilteredItems('file'))
|
||||
// 是否文件
|
||||
const isFile = computed(() => inProps.item.type == 'file')
|
||||
|
||||
@@ -622,9 +639,11 @@ onMounted(() => {
|
||||
rounded
|
||||
/>
|
||||
<VSpacer v-if="isFile" />
|
||||
<IconBtn v-if="!isFile" @click="ignoreCase = !ignoreCase">
|
||||
<VIcon :color="ignoreCase ? 'primary' : 'error'" icon="mdi-format-letter-case" />
|
||||
</IconBtn>
|
||||
<IconBtn v-if="!isFile" @click="changeSelectMode">
|
||||
<VIcon color="primary" v-if="selectMode"> mdi-selection-remove </VIcon>
|
||||
<VIcon color="primary" v-else>mdi-select</VIcon>
|
||||
<VIcon color="primary" :icon="selectMode ? 'mdi-selection-remove' : 'mdi-select'" />
|
||||
</IconBtn>
|
||||
<IconBtn v-if="isFile" @click="recognize(inProps.item.path || '')">
|
||||
<VIcon color="primary"> mdi-text-recognition </VIcon>
|
||||
|
||||
@@ -10,7 +10,6 @@ const props = defineProps({
|
||||
root: {
|
||||
type: String,
|
||||
default: '/',
|
||||
required: true,
|
||||
},
|
||||
storage: {
|
||||
type: String,
|
||||
|
||||
@@ -91,10 +91,6 @@ onUnmounted(() => {
|
||||
<!-- Vue 渲染模式 -->
|
||||
<div v-if="pluginRenderMode === 'vue'">
|
||||
<component :is="dynamicPluginComponent" :config="props.config" :allow-refresh="props.allowRefresh" :api="api" />
|
||||
<!-- Vue 模式下也可以显示拖拽句柄 -->
|
||||
<div class="absolute right-5 top-5">
|
||||
<VIcon class="cursor-move">mdi-drag</VIcon>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Vuetify 渲染模式 -->
|
||||
<VHover v-else-if="pluginRenderMode === 'vuetify'">
|
||||
|
||||
1233
src/composables/useSetupWizard.ts
Normal file
1233
src/composables/useSetupWizard.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -386,7 +386,7 @@ onMounted(() => {
|
||||
|
||||
<!-- 👉 Footer -->
|
||||
<template #footer>
|
||||
<Footer />
|
||||
<Footer :show-nav="!showPluginQuickAccess" />
|
||||
</template>
|
||||
</VerticalNavLayout>
|
||||
|
||||
|
||||
@@ -7,6 +7,15 @@ import { useUserStore } from '@/stores'
|
||||
import { filterMenusByPermission } from '@/utils/permission'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
|
||||
// 是否显示的输入参数
|
||||
defineProps({
|
||||
showNav: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const display = useDisplay()
|
||||
// PWA模式检测
|
||||
const { appMode } = usePWA()
|
||||
@@ -160,7 +169,7 @@ const showDynamicButton = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport v-if="appMode" to="body">
|
||||
<Teleport v-if="appMode && showNav" to="body">
|
||||
<div class="footer-nav-container">
|
||||
<VCard elevation="3" class="footer-nav-card border" rounded="pill" :class="{ 'shift-left': showDynamicButton }">
|
||||
<VCardText class="footer-card-content">
|
||||
|
||||
@@ -57,159 +57,78 @@ const statusIcon = computed(() => {
|
||||
const colorTheme = computed(() => {
|
||||
return props.type === 'online' ? 'success' : 'error'
|
||||
})
|
||||
|
||||
// 动画时长
|
||||
const ENTER_DURATION = 600
|
||||
const LEAVE_DURATION = 400
|
||||
|
||||
// 进入动画
|
||||
function onEnter(el: HTMLElement, done: () => void) {
|
||||
// 初始状态
|
||||
el.style.opacity = '0'
|
||||
el.style.transform = 'scale(0.9)'
|
||||
el.style.filter = 'blur(10px)'
|
||||
|
||||
// 强制重绘
|
||||
el.offsetHeight
|
||||
|
||||
// 应用过渡
|
||||
el.style.transition = `all ${ENTER_DURATION}ms cubic-bezier(0.4, 0, 0.2, 1)`
|
||||
|
||||
// 目标状态
|
||||
requestAnimationFrame(() => {
|
||||
el.style.opacity = '1'
|
||||
el.style.transform = 'scale(1)'
|
||||
el.style.filter = 'blur(0)'
|
||||
})
|
||||
|
||||
// 动画完成
|
||||
setTimeout(done, ENTER_DURATION)
|
||||
}
|
||||
|
||||
// 离开动画
|
||||
function onLeave(el: HTMLElement, done: () => void) {
|
||||
// 应用过渡
|
||||
el.style.transition = `all ${LEAVE_DURATION}ms cubic-bezier(0.4, 0, 1, 1)`
|
||||
|
||||
// 目标状态
|
||||
requestAnimationFrame(() => {
|
||||
el.style.opacity = '0'
|
||||
el.style.transform = 'scale(1.1)'
|
||||
el.style.filter = 'blur(20px)'
|
||||
})
|
||||
|
||||
// 动画完成
|
||||
setTimeout(done, LEAVE_DURATION)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition
|
||||
:css="false"
|
||||
@enter="onEnter"
|
||||
@leave="onLeave"
|
||||
>
|
||||
<div v-if="shouldShow" class="offline-page" ref="offlinePage">
|
||||
<div class="offline-container" :class="{ 'container-animate': shouldShow }">
|
||||
<!-- 状态图标 -->
|
||||
<div class="status-icon-wrapper">
|
||||
<div class="status-icon-bg">
|
||||
<VIcon :icon="statusIcon" size="64" :color="colorTheme" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要信息 -->
|
||||
<div class="content-section">
|
||||
<h1 class="offline-title">
|
||||
{{ props.type === 'online' ? t('app.online') : t('app.offline') }}
|
||||
</h1>
|
||||
|
||||
<p class="offline-message">
|
||||
{{ statusText }}
|
||||
</p>
|
||||
|
||||
<!-- 重试按钮 -->
|
||||
<div class="action-section">
|
||||
<VBtn
|
||||
v-if="props.type === 'offline'"
|
||||
:loading="retrying"
|
||||
:color="colorTheme"
|
||||
size="large"
|
||||
variant="flat"
|
||||
@click="handleRetry"
|
||||
>
|
||||
<VIcon icon="mdi-refresh" class="me-2" />
|
||||
{{ retrying ? t('common.checking') : t('common.retry') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<!-- 状态指示器 -->
|
||||
<div class="status-indicators">
|
||||
<VChip
|
||||
:color="isOnline ? 'success' : 'error'"
|
||||
:prepend-icon="isOnline ? 'mdi-wifi' : 'mdi-wifi-off'"
|
||||
variant="tonal"
|
||||
class="me-2"
|
||||
>
|
||||
{{ isOnline ? t('common.networkOnline') : t('common.networkOffline') }}
|
||||
</VChip>
|
||||
|
||||
<VChip
|
||||
:color="canPerformNetworkAction ? 'success' : 'warning'"
|
||||
:prepend-icon="canPerformNetworkAction ? 'mdi-check-circle' : 'mdi-alert-circle'"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ canPerformNetworkAction ? t('common.serviceAvailable') : t('common.serviceUnavailable') }}
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<div class="footer-section">
|
||||
<p class="app-info">{{ t('app.moviepilot') }}</p>
|
||||
<VDialog :model-value="shouldShow" persistent max-width="420" scrollable>
|
||||
<VCard class="offline-dialog">
|
||||
<!-- 状态图标 -->
|
||||
<div class="status-icon-wrapper">
|
||||
<div class="status-icon-bg">
|
||||
<VIcon :icon="statusIcon" size="48" :color="colorTheme" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<!-- 主要信息 -->
|
||||
<VCardText class="text-center">
|
||||
<h2 class="offline-title mb-4">
|
||||
{{ props.type === 'online' ? t('app.online') : t('app.offline') }}
|
||||
</h2>
|
||||
|
||||
<p class="offline-message mb-6">
|
||||
{{ statusText }}
|
||||
</p>
|
||||
|
||||
<!-- 重试按钮 -->
|
||||
<div class="action-section mb-6">
|
||||
<VBtn
|
||||
v-if="props.type === 'offline'"
|
||||
:loading="retrying"
|
||||
:color="colorTheme"
|
||||
size="default"
|
||||
variant="flat"
|
||||
@click="handleRetry"
|
||||
>
|
||||
<VIcon icon="mdi-refresh" class="me-2" />
|
||||
{{ retrying ? t('common.checking') : t('common.retry') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<!-- 状态指示器 -->
|
||||
<div class="status-indicators">
|
||||
<VChip
|
||||
:color="isOnline ? 'success' : 'error'"
|
||||
:prepend-icon="isOnline ? 'mdi-wifi' : 'mdi-wifi-off'"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
class="me-2"
|
||||
>
|
||||
{{ isOnline ? t('common.networkOnline') : t('common.networkOffline') }}
|
||||
</VChip>
|
||||
|
||||
<VChip
|
||||
:color="canPerformNetworkAction ? 'success' : 'warning'"
|
||||
:prepend-icon="canPerformNetworkAction ? 'mdi-check-circle' : 'mdi-alert-circle'"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
>
|
||||
{{ canPerformNetworkAction ? t('common.serviceAvailable') : t('common.serviceUnavailable') }}
|
||||
</VChip>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.offline-page {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
background: linear-gradient(135deg, rgb(var(--v-theme-surface)) 0%, rgb(var(--v-theme-surface-variant)) 100%);
|
||||
inset: 0;
|
||||
will-change: transform, opacity, filter;
|
||||
}
|
||||
|
||||
.offline-container {
|
||||
padding: 40px;
|
||||
border-radius: 24px;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 10%), 0 0 0 1px rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
inline-size: 100%;
|
||||
max-inline-size: 500px;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.offline-page .offline-container.container-animate {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition-delay: 0.2s;
|
||||
.offline-dialog {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.status-icon-wrapper {
|
||||
margin-block-end: 32px;
|
||||
padding-block: 24px 0;
|
||||
padding-inline: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-icon-bg {
|
||||
@@ -218,71 +137,61 @@ function onLeave(el: HTMLElement, done: () => void) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
animation: icon-pulse 3s ease-in-out infinite;
|
||||
background: rgba(var(--v-theme-surface-variant), 0.5);
|
||||
block-size: 120px;
|
||||
inline-size: 120px;
|
||||
block-size: 80px;
|
||||
inline-size: 80px;
|
||||
margin-block: 0;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.status-icon-bg {
|
||||
animation: iconPulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-icon-bg::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
border-radius: 50%;
|
||||
animation: icon-glow 2s ease-in-out infinite alternate;
|
||||
background: linear-gradient(45deg, rgb(var(--v-theme-primary)), rgb(var(--v-theme-secondary)));
|
||||
content: '';
|
||||
inset: -4px;
|
||||
inset: -3px;
|
||||
opacity: 0.1;
|
||||
animation: iconGlow 2s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes iconPulse {
|
||||
0%, 100% {
|
||||
@keyframes icon-pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iconGlow {
|
||||
@keyframes icon-glow {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-block-end: 32px;
|
||||
}
|
||||
|
||||
.offline-title {
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
font-size: 2rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-block-end: 16px;
|
||||
}
|
||||
|
||||
.offline-message {
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
margin-block-end: 32px;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.action-section {
|
||||
margin-block-end: 32px;
|
||||
}
|
||||
|
||||
.status-indicators {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -290,41 +199,19 @@ function onLeave(el: HTMLElement, done: () => void) {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.help-section {
|
||||
margin-block-end: 32px;
|
||||
}
|
||||
|
||||
.help-panels {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.footer-section {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.app-info {
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (width <= 600px) {
|
||||
.offline-container {
|
||||
padding: 24px;
|
||||
margin: 16px;
|
||||
.status-icon-bg {
|
||||
block-size: 70px;
|
||||
inline-size: 70px;
|
||||
}
|
||||
|
||||
.offline-title {
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.offline-message {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.status-icon-bg {
|
||||
block-size: 100px;
|
||||
inline-size: 100px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.status-indicators {
|
||||
@@ -332,13 +219,4 @@ function onLeave(el: HTMLElement, done: () => void) {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* 暗黑模式优化 */
|
||||
.v-theme--dark .offline-page {
|
||||
background: linear-gradient(135deg, rgb(var(--v-theme-surface)) 0%, rgba(var(--v-theme-surface-variant), 0.8) 100%);
|
||||
}
|
||||
|
||||
.v-theme--dark .offline-container {
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 30%), 0 0 0 1px rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRecentPlugins } from '@/composables/useRecentPlugins'
|
||||
import PluginDataDialog from '@/components/dialog/PluginDataDialog.vue'
|
||||
import { VCard } from 'vuetify/components'
|
||||
import { getDominantColor } from '@/@core/utils/image'
|
||||
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -136,8 +137,8 @@ const componentOpacity = computed(() => {
|
||||
|
||||
// 计算插件图标路径
|
||||
function getPluginIcon(plugin: Plugin): string {
|
||||
if (!plugin.plugin_icon) return noImage
|
||||
if (pluginIconLoadError.value[plugin.id]) return noImage
|
||||
if (!plugin.plugin_icon) return getLogoUrl('plugin')
|
||||
if (pluginIconLoadError.value[plugin.id]) return getLogoUrl('plugin')
|
||||
|
||||
// 如果是网络图片则使用代理后返回
|
||||
if (plugin?.plugin_icon?.startsWith('http'))
|
||||
@@ -205,6 +206,29 @@ function handleClosePluginDataDialog() {
|
||||
currentPlugin.value = null
|
||||
}
|
||||
|
||||
// 管理滚动状态
|
||||
function manageScrollLock() {
|
||||
if (isVisible.value) {
|
||||
// 使用 nextTick 确保 DOM 已经更新
|
||||
nextTick(() => {
|
||||
// 先恢复之前的锁定状态,避免重复锁定
|
||||
const scrollableElement = document.querySelector('.all-plugins-grid')
|
||||
if (scrollableElement) {
|
||||
// 确保元素存在且可见
|
||||
if ((scrollableElement as HTMLElement).offsetHeight > 0) {
|
||||
disableBodyScroll(scrollableElement as HTMLElement)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 恢复背景滚动
|
||||
const scrollableElement = document.querySelector('.all-plugins-grid')
|
||||
if (scrollableElement) {
|
||||
enableBodyScroll(scrollableElement as HTMLElement)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听可见性变化,加载数据
|
||||
watch(
|
||||
() => isVisible.value,
|
||||
@@ -212,6 +236,9 @@ watch(
|
||||
if (visible) {
|
||||
fetchPluginsWithPage()
|
||||
loadRecentPlugins()
|
||||
manageScrollLock()
|
||||
} else {
|
||||
manageScrollLock()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -221,6 +248,15 @@ onMounted(() => {
|
||||
if (isVisible.value) {
|
||||
fetchPluginsWithPage()
|
||||
loadRecentPlugins()
|
||||
manageScrollLock()
|
||||
}
|
||||
})
|
||||
|
||||
// 组件卸载时确保恢复背景滚动
|
||||
onUnmounted(() => {
|
||||
const scrollableElement = document.querySelector('.all-plugins-grid')
|
||||
if (scrollableElement) {
|
||||
enableBodyScroll(scrollableElement as HTMLElement)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -420,40 +456,41 @@ function handleBackdropClick(event: MouseEvent) {
|
||||
<div class="section-title">{{ t('plugin.allPlugins') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="pluginsWithPage.length > 0" class="all-plugins-grid">
|
||||
<div
|
||||
v-for="plugin in pluginsWithPage"
|
||||
:key="plugin.id"
|
||||
class="plugin-item"
|
||||
@click="handlePluginClick(plugin)"
|
||||
>
|
||||
<VBadge
|
||||
dot
|
||||
:color="plugin.state ? 'success' : 'secondary'"
|
||||
location="top end"
|
||||
:offset-x="-1"
|
||||
:offset-y="-1"
|
||||
<div v-if="pluginsWithPage.length > 0" class="all-plugins-container">
|
||||
<div class="all-plugins-grid">
|
||||
<div
|
||||
v-for="plugin in pluginsWithPage"
|
||||
:key="plugin.id"
|
||||
class="plugin-item"
|
||||
@click="handlePluginClick(plugin)"
|
||||
>
|
||||
<div
|
||||
class="plugin-icon"
|
||||
:style="{
|
||||
background: `${getPluginBackgroundColor(plugin)}`,
|
||||
}"
|
||||
<VBadge
|
||||
dot
|
||||
:color="plugin.state ? 'success' : 'secondary'"
|
||||
location="top end"
|
||||
:offset-x="-1"
|
||||
:offset-y="-1"
|
||||
>
|
||||
<VImg
|
||||
:src="getPluginIcon(plugin)"
|
||||
:alt="plugin.plugin_name"
|
||||
cover
|
||||
@load="src => handleIconLoaded(src, plugin)"
|
||||
@error="handleIconError(plugin)"
|
||||
class="rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</VBadge>
|
||||
<div class="plugin-name">{{ plugin.plugin_name }}</div>
|
||||
<div
|
||||
class="plugin-icon"
|
||||
:style="{
|
||||
background: `${getPluginBackgroundColor(plugin)}`,
|
||||
}"
|
||||
>
|
||||
<VImg
|
||||
:src="getPluginIcon(plugin)"
|
||||
:alt="plugin.plugin_name"
|
||||
cover
|
||||
@load="src => handleIconLoaded(src, plugin)"
|
||||
@error="handleIconError(plugin)"
|
||||
class="rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</VBadge>
|
||||
<div class="plugin-name">{{ plugin.plugin_name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态(只有在没有插件时显示) -->
|
||||
<div v-else-if="pluginsWithPage.length === 0" class="empty-state">
|
||||
<VIcon icon="mdi-puzzle-outline" size="48" color="grey" />
|
||||
@@ -622,10 +659,34 @@ function handleBackdropClick(event: MouseEvent) {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.all-plugins-container {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.all-plugins-grid {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
|
||||
max-block-size: 100%;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-ms-overflow-style: none; // IE/Edge
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-block: 8px;
|
||||
padding-inline: 0;
|
||||
|
||||
// 隐藏滚动条
|
||||
scrollbar-width: none; // Firefox
|
||||
touch-action: pan-y;
|
||||
will-change: scroll-position;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none; // WebKit 浏览器
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-item {
|
||||
@@ -677,6 +738,7 @@ function handleBackdropClick(event: MouseEvent) {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
line-height: 1.2;
|
||||
max-block-size: 2.4em;
|
||||
text-align: center;
|
||||
|
||||
@@ -5,6 +5,8 @@ import LoggingView from '@/views/system/LoggingView.vue'
|
||||
import RuleTestView from '@/views/system/RuleTestView.vue'
|
||||
import ModuleTestView from '@/views/system/ModuleTestView.vue'
|
||||
import MessageView from '@/views/system/MessageView.vue'
|
||||
import WordsView from '@/views/system/WordsView.vue'
|
||||
import CacheView from '@/views/system/CacheView.vue'
|
||||
import api from '@/api'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { getQueryValue } from '@/@core/utils'
|
||||
@@ -41,6 +43,12 @@ const systemTestDialog = ref(false)
|
||||
// 消息中心弹窗
|
||||
const messageDialog = ref(false)
|
||||
|
||||
// 词表设置弹窗
|
||||
const wordsDialog = ref(false)
|
||||
|
||||
// 缓存管理弹窗
|
||||
const cacheDialog = ref(false)
|
||||
|
||||
// 输入消息
|
||||
const user_message = ref('')
|
||||
|
||||
@@ -86,6 +94,20 @@ const shortcuts = [
|
||||
dialog: 'netTest',
|
||||
dialogRef: netTestDialog,
|
||||
},
|
||||
{
|
||||
title: t('shortcut.words.title'),
|
||||
subtitle: t('shortcut.words.subtitle'),
|
||||
icon: 'mdi-file-word-box',
|
||||
dialog: 'words',
|
||||
dialogRef: wordsDialog,
|
||||
},
|
||||
{
|
||||
title: t('shortcut.cache.title'),
|
||||
subtitle: t('shortcut.cache.subtitle'),
|
||||
icon: 'mdi-database',
|
||||
dialog: 'cache',
|
||||
dialogRef: cacheDialog,
|
||||
},
|
||||
{
|
||||
title: t('shortcut.system.title'),
|
||||
subtitle: t('shortcut.system.subtitle'),
|
||||
@@ -249,7 +271,15 @@ onMounted(() => {
|
||||
flat
|
||||
class="pa-2 d-flex align-center cursor-pointer transition-transform duration-300 hover:-translate-y-1 border h-full"
|
||||
hover
|
||||
@click="item.dialog === 'message' ? openMessageDialog() : openDialog(item.dialogRef)"
|
||||
@click="
|
||||
item.dialog === 'message'
|
||||
? openMessageDialog()
|
||||
: item.dialog === 'words'
|
||||
? openDialog(item.dialogRef)
|
||||
: item.dialog === 'cache'
|
||||
? openDialog(item.dialogRef)
|
||||
: openDialog(item.dialogRef)
|
||||
"
|
||||
>
|
||||
<VAvatar variant="text" size="48" rounded="lg">
|
||||
<VIcon color="primary" :icon="item.icon" size="24" />
|
||||
@@ -358,6 +388,38 @@ onMounted(() => {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 词表设置弹窗 -->
|
||||
<VDialog v-if="wordsDialog" v-model="wordsDialog" max-width="60rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-file-word-box" class="me-2" />
|
||||
{{ t('shortcut.words.subtitle') }}
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="wordsDialog = false" />
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<WordsView />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 缓存管理弹窗 -->
|
||||
<VDialog v-if="cacheDialog" v-model="cacheDialog" max-width="90rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-database" class="me-2" />
|
||||
{{ t('shortcut.cache.subtitle') }}
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="cacheDialog = false" />
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<CacheView />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
<!-- 系统健康检查弹窗 -->
|
||||
<VDialog
|
||||
v-if="systemTestDialog"
|
||||
|
||||
@@ -5,7 +5,8 @@ import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import api from '@/api'
|
||||
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
|
||||
import UserAuthDialog from '@/components/dialog/UserAuthDialog.vue'
|
||||
import { useAuthStore, useUserStore } from '@/stores'
|
||||
import AboutDialog from '@/components/dialog/AboutDialog.vue'
|
||||
import { useAuthStore, useUserStore, useGlobalSettingsStore } from '@/stores'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay, useTheme } from 'vuetify'
|
||||
import { SUPPORTED_LOCALES, SupportedLocale } from '@/types/i18n'
|
||||
@@ -20,6 +21,8 @@ import { themeManager } from '@/utils/themeManager'
|
||||
const authStore = useAuthStore()
|
||||
// 用户 Store
|
||||
const userStore = useUserStore()
|
||||
// 全局设置 Store
|
||||
const globalSettingsStore = useGlobalSettingsStore()
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
// 显示器
|
||||
@@ -53,6 +56,9 @@ const transparencyLevel = ref(localStorage.getItem('transparency-level') || 'med
|
||||
const isTransparentTheme = computed(() => currentThemeName.value === 'transparent')
|
||||
const showTransparencyDialog = ref(false)
|
||||
|
||||
// 关于对话框
|
||||
const aboutDialog = ref(false)
|
||||
|
||||
// 预设值配置
|
||||
const transparencyPresets = {
|
||||
low: { opacity: 0.1, blur: 5 },
|
||||
@@ -205,6 +211,11 @@ function showSiteAuthDialog() {
|
||||
siteAuthDialog.value = true
|
||||
}
|
||||
|
||||
// 显示关于对话框
|
||||
function showAboutDialog() {
|
||||
aboutDialog.value = true
|
||||
}
|
||||
|
||||
// 用户站点认证成功
|
||||
function siteAuthDone() {
|
||||
siteAuthDialog.value = false
|
||||
@@ -217,6 +228,11 @@ const userName = computed(() => userStore.userName)
|
||||
const avatar = computed(() => userStore.avatar || avatar1)
|
||||
const userLevel = computed(() => userStore.level)
|
||||
|
||||
// 检查是否为高级模式
|
||||
const isAdvancedMode = computed(() => {
|
||||
return globalSettingsStore.get('ADVANCED_MODE') !== false
|
||||
})
|
||||
|
||||
// 主题相关功能
|
||||
const { name: themeName, global: globalTheme } = useTheme()
|
||||
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
|
||||
@@ -509,11 +525,17 @@ onUnmounted(() => {
|
||||
<VListItemTitle>{{ t('user.profile') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem v-if="superUser" link @click="router.push('/setting')" class="mb-1 rounded-lg" hover>
|
||||
<VListItem
|
||||
v-if="superUser"
|
||||
link
|
||||
@click="isAdvancedMode ? router.push('/setting') : router.push('/setup-wizard')"
|
||||
class="mb-1 rounded-lg"
|
||||
hover
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-cog-outline" />
|
||||
<VIcon :icon="isAdvancedMode ? 'mdi-cog-outline' : 'mdi-wizard-hat'" />
|
||||
</template>
|
||||
<VListItemTitle>{{ t('user.systemSettings') }}</VListItemTitle>
|
||||
<VListItemTitle>{{ isAdvancedMode ? t('user.systemSettings') : t('user.wizardSettings') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<!-- 👉 Site Auth -->
|
||||
@@ -620,6 +642,14 @@ onUnmounted(() => {
|
||||
<VListItemTitle>{{ t('user.helpDocs') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<!-- 👉 About -->
|
||||
<VListItem @click="showAboutDialog" class="mb-1 rounded-lg" hover>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-information-outline" />
|
||||
</template>
|
||||
<VListItemTitle>{{ t('setting.about.title') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<!-- Divider -->
|
||||
<VDivider v-if="superUser" class="my-3" />
|
||||
|
||||
@@ -764,6 +794,9 @@ onUnmounted(() => {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- 关于对话框 -->
|
||||
<AboutDialog v-if="aboutDialog" v-model="aboutDialog" @close="aboutDialog = false" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -49,6 +49,9 @@ export default {
|
||||
itemsPerPage: 'Items per page',
|
||||
pageText: '{0}-{1} of {2}',
|
||||
noDataText: 'No data',
|
||||
next: 'Next',
|
||||
previous: 'Previous',
|
||||
skip: 'Skip',
|
||||
loadingText: 'Loading...',
|
||||
networkRequired: 'This feature requires network connection',
|
||||
networkDisconnected: 'Network connection lost',
|
||||
@@ -321,11 +324,6 @@ export default {
|
||||
title: 'Notifications',
|
||||
description: 'Notification channels (WeChat, Telegram, Slack, SynologyChat, VoceChat, WebPush), message scope',
|
||||
},
|
||||
words: {
|
||||
title: 'Word Lists',
|
||||
description:
|
||||
'Custom recognition words, custom production/subtitle groups, custom placeholders, file organization block words',
|
||||
},
|
||||
about: {
|
||||
title: 'About',
|
||||
description: 'Software version',
|
||||
@@ -369,8 +367,10 @@ export default {
|
||||
deleteFailed: 'Failed to delete user!',
|
||||
profile: 'Profile',
|
||||
systemSettings: 'System Settings',
|
||||
wizardSettings: 'Setup Wizard',
|
||||
siteAuth: 'User Authentication',
|
||||
helpDocs: 'Help Documents',
|
||||
about: 'About',
|
||||
restart: 'Restart',
|
||||
management: 'User Management',
|
||||
noUsers: 'No Users',
|
||||
@@ -378,8 +378,11 @@ export default {
|
||||
addUser: 'Add User',
|
||||
editUser: 'Edit User',
|
||||
username: 'Username',
|
||||
usernameHint: 'Username for system login',
|
||||
password: 'Password',
|
||||
passwordHint: 'Password for system login',
|
||||
confirmPassword: 'Confirm Password',
|
||||
confirmPasswordHint: 'Please enter the password again to confirm',
|
||||
role: 'Role',
|
||||
email: 'Email',
|
||||
enabled: 'Enabled',
|
||||
@@ -408,10 +411,13 @@ export default {
|
||||
name: 'WeChat Work',
|
||||
corpId: 'Corp ID',
|
||||
corpIdHint: 'Corp ID in WeChat Work backend enterprise information',
|
||||
corpIdRequired: 'Corp ID cannot be empty',
|
||||
appId: 'App AgentId',
|
||||
appIdHint: 'AgentId of self-built app in WeChat Work',
|
||||
appIdRequired: 'App AgentId cannot be empty',
|
||||
appSecret: 'App Secret',
|
||||
appSecretHint: 'Secret of self-built app in WeChat Work',
|
||||
appSecretRequired: 'App Secret cannot be empty',
|
||||
proxy: 'Proxy Address',
|
||||
proxyHint:
|
||||
'Proxy address for WeChat message forwarding, required for self-built apps created after June 20, 2022',
|
||||
@@ -427,8 +433,10 @@ export default {
|
||||
name: 'Telegram',
|
||||
token: 'Bot Token',
|
||||
tokenHint: 'Telegram bot token, format: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11',
|
||||
tokenRequired: 'Bot Token cannot be empty',
|
||||
chatId: 'Chat ID',
|
||||
chatIdHint: 'Chat ID of user, group or channel that receives notifications',
|
||||
chatIdRequired: 'Chat ID cannot be empty',
|
||||
users: 'User Whitelist',
|
||||
usersHint: 'User IDs that can use Telegram bot, separated by commas. Leave empty to allow all users',
|
||||
admins: 'Admin Whitelist',
|
||||
@@ -443,15 +451,18 @@ export default {
|
||||
name: 'Slack',
|
||||
oauthToken: 'Slack Bot User OAuth Token',
|
||||
oauthTokenHint: 'Bot User OAuth Token in Slack app OAuth & Permissions page',
|
||||
oauthTokenRequired: 'OAuth Token cannot be empty',
|
||||
appToken: 'Slack App-Level Token',
|
||||
appTokenHint: 'App-Level Token in Slack app OAuth & Permissions page',
|
||||
channel: 'Channel Name',
|
||||
channelHint: 'Channel to send messages, default is "all"',
|
||||
channelRequired: 'Channel Name cannot be empty',
|
||||
},
|
||||
synologychat: {
|
||||
name: 'Synology Chat',
|
||||
webhook: 'Webhook URL',
|
||||
webhookHint: 'Synology Chat bot webhook URL',
|
||||
webhookRequired: 'Webhook URL cannot be empty',
|
||||
token: 'Token',
|
||||
tokenHint: 'Synology Chat bot token',
|
||||
},
|
||||
@@ -459,8 +470,10 @@ export default {
|
||||
name: 'VoceChat',
|
||||
host: 'Address',
|
||||
hostHint: 'VoceChat server address, format: http(s)://ip:port',
|
||||
hostRequired: 'Address cannot be empty',
|
||||
apiKey: 'Bot API Key',
|
||||
apiKeyHint: 'VoceChat bot API key',
|
||||
apiKeyRequired: 'API Key cannot be empty',
|
||||
channelId: 'Channel ID',
|
||||
channelIdHint: 'VoceChat channel ID, without #',
|
||||
},
|
||||
@@ -468,6 +481,7 @@ export default {
|
||||
name: 'WebPush',
|
||||
username: 'Login Username',
|
||||
usernameHint: 'Only push messages to the corresponding logged-in user',
|
||||
usernameRequired: 'Username cannot be empty',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
@@ -496,6 +510,14 @@ export default {
|
||||
title: 'Messages',
|
||||
subtitle: 'Message Center',
|
||||
},
|
||||
words: {
|
||||
title: 'Words',
|
||||
subtitle: 'Word Settings',
|
||||
},
|
||||
cache: {
|
||||
title: 'Cache',
|
||||
subtitle: 'Manage Cache',
|
||||
},
|
||||
},
|
||||
workflow: {
|
||||
components: 'Action Components',
|
||||
@@ -766,6 +788,8 @@ export default {
|
||||
originalTitle: 'Original Title',
|
||||
status: 'Status',
|
||||
releaseDate: 'Release Date',
|
||||
digitalRelease: 'Digital Release',
|
||||
physicalRelease: 'Physical Release',
|
||||
originalLanguage: 'Original Language',
|
||||
productionCountries: 'Production Countries',
|
||||
productionCompanies: 'Production Companies',
|
||||
@@ -850,6 +874,7 @@ export default {
|
||||
batchEnableError: 'Batch enable operation failed',
|
||||
batchPauseError: 'Batch pause operation failed',
|
||||
batchDeleteError: 'Batch delete operation failed',
|
||||
minSubscribers: 'Minimum Subscribers',
|
||||
},
|
||||
recommend: {
|
||||
all: 'All',
|
||||
@@ -1215,9 +1240,22 @@ export default {
|
||||
apiTokenLength: 'API Token must be at least 16 characters',
|
||||
githubToken: 'Github Token',
|
||||
githubTokenFormat: 'ghp_**** or github_pat_****',
|
||||
githubTokenHint: 'Used to increase the rate limit threshold when plugins access Github API',
|
||||
githubTokenHint:
|
||||
'Used to increase the rate limit threshold when plugins access Github API,it is recommended to configure, otherwise plugins may not work properly',
|
||||
ocrHost: 'OCR Server',
|
||||
ocrHostHint: 'Used for site check-in, updating site cookies and other captcha recognition',
|
||||
aiAgent: 'Enable AI Assistant',
|
||||
aiAgentEnable: 'Enable AI Assistant',
|
||||
aiAgentEnableHint: 'Enable AI assistant functionality, requires LLM configuration',
|
||||
llmProvider: 'LLM Provider',
|
||||
llmProviderHint: 'Select the LLM service provider to use',
|
||||
llmModel: 'LLM Model Name',
|
||||
llmModelHint: 'Specify the LLM model to use, such as gpt-3.5-turbo, deepseek-chat, etc.',
|
||||
llmApiKey: 'LLM API Key',
|
||||
llmApiKeyHint: 'API key from the LLM service provider for authentication',
|
||||
llmApiKeyPlaceholder: 'Please enter API key',
|
||||
llmBaseUrl: 'LLM Base URL',
|
||||
llmBaseUrlHint: 'Base URL for LLM API, used for custom API endpoints',
|
||||
advancedSettings: 'Advanced Settings',
|
||||
advancedSettingsDesc: 'System advanced settings, only need to be adjusted in special cases',
|
||||
downloaders: 'Downloaders',
|
||||
@@ -1669,7 +1707,11 @@ export default {
|
||||
bestVersionRuleGroup: 'Version Upgrade Priority Rule Group',
|
||||
bestVersionRuleGroupHint: 'Filter version upgrade subscriptions based on selected filter rule groups',
|
||||
timedSearch: 'Subscription Scheduled Search',
|
||||
timedSearchHint: 'Search all sites every 24 hours to supplement resources that may be missed by subscription',
|
||||
timedSearchHint:
|
||||
'Search all sites at specified intervals to supplement resources that may be missed by subscription',
|
||||
searchInterval: 'Subscription Search Interval',
|
||||
searchIntervalHint:
|
||||
'Set the time interval for subscription search, only effective when subscription scheduled search is enabled',
|
||||
checkLocalMedia: 'Check File System Resources',
|
||||
checkLocalMediaHint:
|
||||
'Scan the storage directory for existing resource files to avoid duplicate downloads; regardless of whether it is enabled, the media server will be checked',
|
||||
@@ -1685,6 +1727,8 @@ export default {
|
||||
hour1: '1 hour',
|
||||
hour12: '12 hours',
|
||||
day1: '1 day',
|
||||
day3: '3 days',
|
||||
week1: '1 week',
|
||||
},
|
||||
saveSuccess: 'Subscription sites saved successfully',
|
||||
saveFailed: 'Failed to save subscription sites!',
|
||||
@@ -1694,6 +1738,8 @@ export default {
|
||||
cache: {
|
||||
title: 'Cache Management',
|
||||
subtitle: 'Manage torrent cache data',
|
||||
totalCount: 'Total Count',
|
||||
siteCount: 'Site Count',
|
||||
filterByTitle: 'Filter by Title',
|
||||
filterBySite: 'Filter by Site',
|
||||
selectSite: 'Select Site',
|
||||
@@ -1766,8 +1812,12 @@ export default {
|
||||
add: 'Add User',
|
||||
edit: 'Edit User',
|
||||
username: 'Username',
|
||||
usernameRequired: 'Username cannot be empty',
|
||||
password: 'Password',
|
||||
passwordMinLength: 'Password must be at least 6 characters',
|
||||
confirmPassword: 'Confirm Password',
|
||||
confirmPasswordRequired: 'Please confirm password',
|
||||
passwordMismatch: 'Passwords do not match',
|
||||
email: 'Email',
|
||||
nickname: 'Nickname',
|
||||
status: 'Status',
|
||||
@@ -1788,9 +1838,7 @@ export default {
|
||||
webPush: 'WebPush',
|
||||
creatingUser: 'Creating user [{name}], please wait',
|
||||
updatingUser: 'Updating user [{name}], please wait',
|
||||
usernameRequired: 'Username cannot be empty',
|
||||
usernameExists: 'Username already exists',
|
||||
passwordMismatch: 'The two passwords do not match',
|
||||
userCreated: 'User [{name}] created successfully',
|
||||
userCreateFailed: 'Failed to create user: {message}',
|
||||
userUpdateSuccess: 'User [{name}] updated successfully',
|
||||
@@ -1866,6 +1914,8 @@ export default {
|
||||
startDownload: 'Start Download',
|
||||
downloadSuccess: '{site} {title} downloaded successfully!',
|
||||
downloadFailed: '{site} {title} download failed: {message}!',
|
||||
showAdvancedOptions: 'Show Advanced Options',
|
||||
hideAdvancedOptions: 'Hide Advanced Options',
|
||||
},
|
||||
subscribeShare: {
|
||||
shareSubscription: 'Share Subscription',
|
||||
@@ -2621,6 +2671,9 @@ export default {
|
||||
nameRequired: 'Name cannot be empty',
|
||||
nameDuplicate: 'Name already exists',
|
||||
defaultChanged: 'Default downloader exists, has been replaced',
|
||||
hostRequired: 'Host cannot be empty',
|
||||
usernameRequired: 'Username cannot be empty',
|
||||
passwordRequired: 'Password cannot be empty',
|
||||
},
|
||||
filterRule: {
|
||||
title: 'Filter Rule',
|
||||
@@ -2665,9 +2718,15 @@ export default {
|
||||
plexToken: 'X-Plex-Token',
|
||||
plexTokenHint: 'X-Plex-Token obtained from Plex request URL in browser F12 -> Network',
|
||||
username: 'Username',
|
||||
usernameHint: 'Login username',
|
||||
password: 'Password',
|
||||
syncLibraries: 'Sync Libraries',
|
||||
syncLibrariesHint: 'Only selected libraries will be synchronized',
|
||||
hostRequired: 'Host cannot be empty',
|
||||
apiKeyRequired: 'API Key cannot be empty',
|
||||
tokenRequired: 'Token cannot be empty',
|
||||
usernameRequired: 'Username cannot be empty',
|
||||
passwordRequired: 'Password cannot be empty',
|
||||
nameExists: '【{name}】 already exists, please use a different name',
|
||||
},
|
||||
bangumi: {
|
||||
@@ -2701,6 +2760,9 @@ export default {
|
||||
firstAirDateAsc: 'First Air Date Ascending',
|
||||
voteAverageDesc: 'Vote Average Descending',
|
||||
voteAverageAsc: 'Vote Average Ascending',
|
||||
time: 'Latest',
|
||||
count: 'Popular',
|
||||
rating: 'Rating',
|
||||
},
|
||||
genreType: {
|
||||
action: 'Action',
|
||||
@@ -2830,7 +2892,9 @@ export default {
|
||||
libraryStorage: 'Library Storage',
|
||||
libraryDirectory: 'Library Directory',
|
||||
transferType: 'Transfer Type',
|
||||
transferTypeHint: 'File operation organization method, hard link saves space, copy is safer',
|
||||
overwriteMode: 'Overwrite Mode',
|
||||
overwriteModeHint: 'How to handle when target file already exists',
|
||||
smartRename: 'Smart Rename',
|
||||
scrapingMetadata: 'Scrape Metadata',
|
||||
sendNotification: 'Send Notification',
|
||||
@@ -2872,4 +2936,150 @@ export default {
|
||||
customBackgroundImageHint: 'Supports web image URLs, leave blank for gradient background',
|
||||
pluginCount: '{count} Plugins',
|
||||
},
|
||||
setupWizard: {
|
||||
title: 'Welcome to MoviePilot!',
|
||||
subtitle: 'Complete the configuration by the wizard, and start using it immediately.',
|
||||
completed: 'Setup Wizard completed!',
|
||||
failed: 'Setup Wizard failed, please try again',
|
||||
complete: 'Complete Configuration',
|
||||
loading: 'Loading configuration data...',
|
||||
testing: 'Testing',
|
||||
connectivityTestSuccess: 'Connectivity test passed',
|
||||
connectivityTestFailed: 'Connectivity test failed',
|
||||
testingStorage: 'Testing storage',
|
||||
checkingStorage: 'Checking storage connectivity',
|
||||
testingDownloader: 'Testing downloader',
|
||||
checkingDownloader: 'Checking downloader connectivity',
|
||||
testingMediaServer: 'Testing media server',
|
||||
checkingMediaServer: 'Checking media server connectivity',
|
||||
testingNotification: 'Testing notification',
|
||||
checkingNotification: 'Checking notification connectivity',
|
||||
testFailedHint: 'Please check if the configuration is correct, you can retest after modification',
|
||||
unsupportedDownloaderType: 'Unsupported downloader type: {type}',
|
||||
unsupportedMediaServerType: 'Unsupported media server type: {type}',
|
||||
unsupportedNotificationType: 'Unsupported notification type: {type}',
|
||||
passwordUpdateSuccess: 'Password updated successfully',
|
||||
userCreateSuccess: 'User created successfully',
|
||||
passwordUpdateFailed: 'Failed to update password',
|
||||
basic: {
|
||||
title: 'Basic Settings',
|
||||
description: 'Set access domain, username/password and network configuration',
|
||||
appDomain: 'App Domain',
|
||||
appDomainHint: 'Used to add quick jump links when sending notifications',
|
||||
wallpaper: 'Background Wallpaper',
|
||||
wallpaperHint: 'Choose the source of the login page background',
|
||||
recognizeSource: 'Recognize Source',
|
||||
recognizeSourceHint: 'Set the default media info recognition data source',
|
||||
apiToken: 'API Token',
|
||||
apiTokenHint: 'API Token required for accessing MoviePilot API, please record it for subsequent use',
|
||||
currentUserHint: 'Current user, cannot be modified',
|
||||
passwordOptionalHint: 'Leave blank to keep current password',
|
||||
confirmPasswordHint: 'Confirm new password',
|
||||
apiTokenRequired: 'API Token is required',
|
||||
},
|
||||
storage: {
|
||||
title: 'Storage',
|
||||
description: 'Configure download directory and media library directory',
|
||||
info: 'Storage Configuration',
|
||||
infoDesc: 'Configure local storage directories for download and media library management',
|
||||
downloadPath: 'Download Directory',
|
||||
downloadPathHint: 'Set the storage path for downloaded files',
|
||||
libraryPath: 'Media Library Directory',
|
||||
libraryPathHint: 'Set the storage path for media files',
|
||||
downloadPathRequired: 'Download directory is required',
|
||||
libraryPathRequired: 'Media library directory is required',
|
||||
},
|
||||
downloader: {
|
||||
title: 'Downloader',
|
||||
description: 'Configure downloader',
|
||||
info: 'Downloader Configuration',
|
||||
infoDesc: 'Configure downloader for resource download, can choose qBittorrent or Transmission',
|
||||
type: 'Downloader Type',
|
||||
typeHint: 'Select the type of downloader to use',
|
||||
name: 'Downloader Name',
|
||||
nameHint: 'Set a name for the downloader',
|
||||
qbittorrentConfig: 'qBittorrent Configuration',
|
||||
transmissionConfig: 'Transmission Configuration',
|
||||
host: 'Server Address',
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
downloadPath: 'Download Path',
|
||||
},
|
||||
mediaServer: {
|
||||
title: 'Media Server',
|
||||
description: 'Configure media server',
|
||||
info: 'Media Server Configuration',
|
||||
infoDesc: 'Configure media server for media library management, can choose Emby, Jellyfin or Plex etc.',
|
||||
type: 'Media Server Type',
|
||||
typeHint: 'Select the type of media server to use',
|
||||
name: 'Server Name',
|
||||
nameHint: 'Set a name for the media server',
|
||||
embyConfig: 'Emby Configuration',
|
||||
jellyfinConfig: 'Jellyfin Configuration',
|
||||
plexConfig: 'Plex Configuration',
|
||||
host: 'Server Address',
|
||||
apiKey: 'API Key',
|
||||
token: 'Access Token',
|
||||
},
|
||||
notification: {
|
||||
title: 'Notification',
|
||||
description: 'Configure notification channels',
|
||||
info: 'Notification Configuration',
|
||||
infoDesc: 'Configure notification channels for receiving system messages (optional)',
|
||||
type: 'Notification Type',
|
||||
typeHint: 'Select the type of notification channel to use',
|
||||
name: 'Notification Name',
|
||||
nameHint: 'Set a name for the notification channel',
|
||||
telegramConfig: 'Telegram Configuration',
|
||||
emailConfig: 'Email Configuration',
|
||||
botToken: 'Bot Token',
|
||||
chatId: 'Chat ID',
|
||||
smtpServer: 'SMTP Server',
|
||||
smtpPort: 'SMTP Port',
|
||||
senderEmail: 'Sender Email',
|
||||
senderPassword: 'Sender Password',
|
||||
receiverEmail: 'Receiver Email',
|
||||
},
|
||||
preferences: {
|
||||
title: 'Resource Preferences',
|
||||
description: 'Set resource download preferences',
|
||||
info: 'Resource Preferences',
|
||||
infoDesc:
|
||||
'Set resource download preferences, the system will automatically select the best resources based on these preferences',
|
||||
quality: 'Quality Preference',
|
||||
qualityHint: 'Select preferred video quality',
|
||||
subtitle: 'Subtitle Preference',
|
||||
subtitleHint: 'Select preferred subtitle type',
|
||||
resolution: 'Resolution Preference',
|
||||
resolutionHint: 'Select preferred video resolution',
|
||||
presetRules: 'Preset Rules',
|
||||
detailedConfig: 'Detailed Configuration',
|
||||
quickPresets: 'Quick Presets',
|
||||
quickPresetsDesc: 'Select preset configuration, system will automatically apply corresponding rules',
|
||||
personalizationOptions: 'Personalization Options',
|
||||
personalizationOptionsDesc: 'Adjust rules according to your needs',
|
||||
excludeDolbyVision: 'Exclude Dolby Vision',
|
||||
excludeDolbyVisionHint: 'Exclude Dolby Vision resources from rules when selected',
|
||||
excludeBluray: 'Exclude Blu-ray',
|
||||
excludeBlurayHint: 'Exclude Blu-ray resources from rules when selected',
|
||||
presets: {
|
||||
'4k-enthusiast': {
|
||||
name: '4K Enthusiast',
|
||||
description: 'Pursue the highest quality, prioritize 4K',
|
||||
},
|
||||
'balanced': {
|
||||
name: 'Balanced Mode',
|
||||
description: 'Balance between quality and storage space',
|
||||
},
|
||||
'space-saver': {
|
||||
name: 'Space Saver',
|
||||
description: 'Prioritize smaller files to save storage space',
|
||||
},
|
||||
'free-priority': {
|
||||
name: 'Free Priority',
|
||||
description: 'Prioritize free resources, no other requirements',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ export default {
|
||||
itemsPerPage: '每页条数',
|
||||
pageText: '{0}-{1} 共 {2} 条',
|
||||
noDataText: '没有数据',
|
||||
next: '下一步',
|
||||
previous: '上一步',
|
||||
skip: '跳过',
|
||||
loadingText: '加载中...',
|
||||
networkRequired: '此功能需要网络连接',
|
||||
networkDisconnected: '网络连接已断开',
|
||||
@@ -320,10 +323,6 @@ export default {
|
||||
title: '通知',
|
||||
description: '通知渠道(微信、Telegram、Slack、SynologyChat、VoceChat、WebPush)、消息发送范围',
|
||||
},
|
||||
words: {
|
||||
title: '词表',
|
||||
description: '自定义识别词、自定义制作组/字幕组、自定义占位符、文件整理屏蔽词',
|
||||
},
|
||||
about: {
|
||||
title: '关于',
|
||||
description: '软件版本',
|
||||
@@ -367,8 +366,10 @@ export default {
|
||||
deleteFailed: '用户删除失败!',
|
||||
profile: '个人信息',
|
||||
systemSettings: '系统设定',
|
||||
wizardSettings: '设置向导',
|
||||
siteAuth: '用户认证',
|
||||
helpDocs: '帮助文档',
|
||||
about: '关于',
|
||||
restart: '重启',
|
||||
management: '用户管理',
|
||||
noUsers: '没有用户',
|
||||
@@ -376,8 +377,11 @@ export default {
|
||||
addUser: '添加用户',
|
||||
editUser: '编辑用户',
|
||||
username: '用户名',
|
||||
usernameHint: '用于登录系统的用户名',
|
||||
password: '密码',
|
||||
passwordHint: '用于登录系统的密码',
|
||||
confirmPassword: '确认密码',
|
||||
confirmPasswordHint: '请再次输入密码以确认',
|
||||
role: '角色',
|
||||
email: '邮箱',
|
||||
enabled: '启用',
|
||||
@@ -406,10 +410,13 @@ export default {
|
||||
name: '企业微信',
|
||||
corpId: '企业ID',
|
||||
corpIdHint: '企业微信后台企业信息中的企业ID',
|
||||
corpIdRequired: '企业ID不能为空',
|
||||
appId: '应用 AgentId',
|
||||
appIdHint: '企业微信自建应用的AgentId',
|
||||
appIdRequired: '应用AgentId不能为空',
|
||||
appSecret: '应用 Secret',
|
||||
appSecretHint: '企业微信自建应用的Secret',
|
||||
appSecretRequired: '应用Secret不能为空',
|
||||
proxy: '代理地址',
|
||||
proxyHint: '微信消息的转发代理地址,2022年6月20日后创建的自建应用才需要,不使用代理时需要保留默认值',
|
||||
token: 'Token',
|
||||
@@ -424,8 +431,10 @@ export default {
|
||||
name: 'Telegram',
|
||||
token: 'Bot Token',
|
||||
tokenHint: 'Telegram机器人token,格式:123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11',
|
||||
tokenRequired: 'Bot Token不能为空',
|
||||
chatId: 'Chat ID',
|
||||
chatIdHint: '接受消息通知的用户、群组或频道Chat ID',
|
||||
chatIdRequired: 'Chat ID不能为空',
|
||||
users: '用户白名单',
|
||||
usersHint: '可使用Telegram机器人的用户ID清单,多个用户用,分隔,不填写则所有用户都能使用',
|
||||
admins: '管理员白名单',
|
||||
@@ -440,15 +449,18 @@ export default {
|
||||
name: 'Slack',
|
||||
oauthToken: 'Slack Bot User OAuth Token',
|
||||
oauthTokenHint: 'Slack应用`OAuth & Permissions`页面中的`Bot User OAuth Token`',
|
||||
oauthTokenRequired: 'OAuth Token不能为空',
|
||||
appToken: 'Slack App-Level Token',
|
||||
appTokenHint: 'Slack应用`OAuth & Permissions`页面中的`App-Level Token`',
|
||||
channel: '频道名称',
|
||||
channelHint: '消息发送频道,默认`全体`',
|
||||
channelRequired: '频道名称不能为空',
|
||||
},
|
||||
synologychat: {
|
||||
name: 'Synology Chat',
|
||||
webhook: '机器人传入URL',
|
||||
webhookHint: 'Synology Chat机器人传入URL',
|
||||
webhookRequired: 'Webhook URL不能为空',
|
||||
token: '令牌',
|
||||
tokenHint: 'Synology Chat机器人令牌',
|
||||
},
|
||||
@@ -456,8 +468,10 @@ export default {
|
||||
name: 'VoceChat',
|
||||
host: '地址',
|
||||
hostHint: 'VoceChat服务端地址,格式:http(s)://ip:port',
|
||||
hostRequired: '地址不能为空',
|
||||
apiKey: '机器人密钥',
|
||||
apiKeyHint: 'VoceChat机器人密钥',
|
||||
apiKeyRequired: 'API密钥不能为空',
|
||||
channelId: '频道ID',
|
||||
channelIdHint: 'VoceChat的频道ID,不包含#号',
|
||||
},
|
||||
@@ -465,6 +479,7 @@ export default {
|
||||
name: 'WebPush',
|
||||
username: '登录用户名',
|
||||
usernameHint: '只有对应的用户登录后才会推送消息',
|
||||
usernameRequired: '用户名不能为空',
|
||||
},
|
||||
},
|
||||
shortcut: {
|
||||
@@ -493,6 +508,14 @@ export default {
|
||||
title: '消息',
|
||||
subtitle: '消息中心',
|
||||
},
|
||||
words: {
|
||||
title: '词表',
|
||||
subtitle: '词表设置',
|
||||
},
|
||||
cache: {
|
||||
title: '缓存',
|
||||
subtitle: '管理缓存',
|
||||
},
|
||||
},
|
||||
workflow: {
|
||||
components: '动作组件',
|
||||
@@ -763,6 +786,8 @@ export default {
|
||||
originalTitle: '原始标题',
|
||||
status: '状态',
|
||||
releaseDate: '上映日期',
|
||||
digitalRelease: '数字发行',
|
||||
physicalRelease: '实体发行',
|
||||
originalLanguage: '原始语言',
|
||||
productionCountries: '出品国家',
|
||||
productionCompanies: '制作公司',
|
||||
@@ -846,6 +871,7 @@ export default {
|
||||
batchEnableError: '批量启用操作失败',
|
||||
batchPauseError: '批量暂停操作失败',
|
||||
batchDeleteError: '批量删除操作失败',
|
||||
minSubscribers: '最小订阅人数',
|
||||
},
|
||||
recommend: {
|
||||
all: '全部',
|
||||
@@ -1211,9 +1237,21 @@ export default {
|
||||
apiTokenLength: 'API Token不得低于16位',
|
||||
githubToken: 'Github Token',
|
||||
githubTokenFormat: 'ghp_**** 或 github_pat_****',
|
||||
githubTokenHint: '用于提高插件等访问Github API时的限流阈值',
|
||||
githubTokenHint: '用于提高插件等访问Github API时的限流阈值,建议配置,否则插件可能无法正常使用',
|
||||
ocrHost: '验证码识别服务器',
|
||||
ocrHostHint: '用于站点签到、更新站点Cookie等识别验证码',
|
||||
aiAgent: '启用智能助手',
|
||||
aiAgentEnable: '启用智能助手',
|
||||
aiAgentEnableHint: '启用后可使用智能助手功能,需要配置LLM相关参数',
|
||||
llmProvider: 'LLM提供商',
|
||||
llmProviderHint: '选择使用的LLM服务提供商',
|
||||
llmModel: 'LLM模型名称',
|
||||
llmModelHint: '指定使用的LLM模型,如gpt-3.5-turbo、deepseek-chat等',
|
||||
llmApiKey: 'LLM API密钥',
|
||||
llmApiKeyHint: 'LLM服务提供商的API密钥,用于身份验证',
|
||||
llmApiKeyPlaceholder: '请输入API密钥',
|
||||
llmBaseUrl: 'LLM基础URL',
|
||||
llmBaseUrlHint: 'LLM API的基础URL地址,用于自定义API端点',
|
||||
advancedSettings: '高级设置',
|
||||
advancedSettingsDesc: '系统进阶设置,特殊情况下才需要调整',
|
||||
downloaders: '下载器',
|
||||
@@ -1648,7 +1686,9 @@ export default {
|
||||
bestVersionRuleGroup: '洗版优先级规则组',
|
||||
bestVersionRuleGroupHint: '按选定的过滤规则组对洗版订阅进行过滤',
|
||||
timedSearch: '订阅定时搜索',
|
||||
timedSearchHint: '每隔24小时全站搜索,以补全订阅可能漏掉的资源',
|
||||
timedSearchHint: '每隔指定时间全站搜索,以补全订阅可能漏掉的资源',
|
||||
searchInterval: '订阅搜索时间间隔',
|
||||
searchIntervalHint: '设置订阅搜索的时间间隔,仅在开启订阅定时搜索时生效',
|
||||
checkLocalMedia: '检查文件系统资源',
|
||||
checkLocalMediaHint: '扫描存储目录中是否已存在相应资源文件,以避免重复下载;不管是否开启都会检查媒体服务器',
|
||||
modes: {
|
||||
@@ -1663,6 +1703,8 @@ export default {
|
||||
hour1: '1小时',
|
||||
hour12: '12小时',
|
||||
day1: '1天',
|
||||
day3: '3天',
|
||||
week1: '一周',
|
||||
},
|
||||
saveSuccess: '订阅站点保存成功',
|
||||
saveFailed: '订阅站点保存失败!',
|
||||
@@ -1672,6 +1714,8 @@ export default {
|
||||
cache: {
|
||||
title: '缓存管理',
|
||||
subtitle: '管理缓存的站点资源',
|
||||
totalCount: '总条数',
|
||||
siteCount: '站点数',
|
||||
filterByTitle: '按标题筛选',
|
||||
filterBySite: '按站点筛选',
|
||||
selectSite: '选择站点',
|
||||
@@ -1744,8 +1788,12 @@ export default {
|
||||
add: '添加用户',
|
||||
edit: '编辑用户',
|
||||
username: '用户名',
|
||||
usernameRequired: '用户名不能为空',
|
||||
password: '密码',
|
||||
passwordMinLength: '密码长度不能少于6位',
|
||||
confirmPassword: '确认密码',
|
||||
confirmPasswordRequired: '请确认密码',
|
||||
passwordMismatch: '两次输入的密码不一致',
|
||||
email: '邮箱',
|
||||
nickname: '昵称',
|
||||
status: '状态',
|
||||
@@ -1766,9 +1814,7 @@ export default {
|
||||
webPush: 'WebPush',
|
||||
creatingUser: '正在创建【{name}】用户,请稍后',
|
||||
updatingUser: '正在更新【{name}】用户,请稍后',
|
||||
usernameRequired: '用户名不能为空',
|
||||
usernameExists: '用户名已存在',
|
||||
passwordMismatch: '两次输入的密码不一致',
|
||||
userCreated: '用户【{name}】创建成功',
|
||||
userCreateFailed: '创建用户失败:{message}',
|
||||
userUpdateSuccess: '用户【{name}】更新成功',
|
||||
@@ -1844,6 +1890,8 @@ export default {
|
||||
startDownload: '开始下载',
|
||||
downloadSuccess: '{site} {title} 下载成功!',
|
||||
downloadFailed: '{site} {title} 下载失败:{message}!',
|
||||
showAdvancedOptions: '显示高级选项',
|
||||
hideAdvancedOptions: '隐藏高级选项',
|
||||
},
|
||||
subscribeShare: {
|
||||
shareSubscription: '分享订阅',
|
||||
@@ -2591,6 +2639,9 @@ export default {
|
||||
nameRequired: '不能为空,且不能重名',
|
||||
nameDuplicate: '名称已存在',
|
||||
defaultChanged: '存在默认下载器,已替换',
|
||||
hostRequired: '地址不能为空',
|
||||
usernameRequired: '用户名不能为空',
|
||||
passwordRequired: '密码不能为空',
|
||||
},
|
||||
filterRule: {
|
||||
title: '过滤规则',
|
||||
@@ -2635,10 +2686,16 @@ export default {
|
||||
plexToken: 'X-Plex-Token',
|
||||
plexTokenHint: '浏览器F12->网络,从Plex请求URL中获取的X-Plex-Token',
|
||||
username: '用户名',
|
||||
usernameHint: '登录用户名',
|
||||
password: '密码',
|
||||
syncLibraries: '同步媒体库',
|
||||
syncLibrariesHint: '只有选中的媒体库才会被同步',
|
||||
nameExists: '【{name}】已存在,请替换为其他名称',
|
||||
hostRequired: '地址不能为空',
|
||||
apiKeyRequired: 'API密钥不能为空',
|
||||
tokenRequired: 'Token不能为空',
|
||||
usernameRequired: '用户名不能为空',
|
||||
passwordRequired: '密码不能为空',
|
||||
},
|
||||
bangumi: {
|
||||
category: '类别',
|
||||
@@ -2671,6 +2728,9 @@ export default {
|
||||
firstAirDateAsc: '首播日期升序',
|
||||
voteAverageDesc: '评分降序',
|
||||
voteAverageAsc: '评分升序',
|
||||
time: '最新',
|
||||
count: '热门',
|
||||
rating: '评分',
|
||||
},
|
||||
genreType: {
|
||||
action: '动作',
|
||||
@@ -2800,7 +2860,9 @@ export default {
|
||||
libraryStorage: '媒体库存储',
|
||||
libraryDirectory: '媒体库目录',
|
||||
transferType: '整理方式',
|
||||
transferTypeHint: '文件操作整理方式,硬链接节省空间,复制更安全',
|
||||
overwriteMode: '覆盖模式',
|
||||
overwriteModeHint: '当目标文件已存在时的处理方式',
|
||||
smartRename: '智能重命名',
|
||||
scrapingMetadata: '刮削元数据',
|
||||
sendNotification: '发送通知',
|
||||
@@ -2841,4 +2903,169 @@ export default {
|
||||
customBackgroundImageHint: '支持网络图片URL,留空则使用渐变背景',
|
||||
pluginCount: '{count} 个插件',
|
||||
},
|
||||
setupWizard: {
|
||||
title: '欢迎使用 MoviePilot !',
|
||||
subtitle: '按向导完成配置,即刻开始使用。',
|
||||
completed: '配置向导完成!',
|
||||
failed: '配置向导失败,请重试',
|
||||
complete: '完成配置',
|
||||
loading: '正在加载配置数据...',
|
||||
testing: '正在测试',
|
||||
connectivityTestSuccess: '连通性测试通过',
|
||||
connectivityTestFailed: '连通性测试失败',
|
||||
testingStorage: '正在测试存储目录',
|
||||
checkingStorage: '检查存储目录连通性',
|
||||
storageTestFailed: '存储目录测试失败',
|
||||
testingDownloader: '正在测试下载器',
|
||||
checkingDownloader: '检查下载器连通性',
|
||||
downloaderTestFailed: '下载器测试失败',
|
||||
downloaderNotSelected: '未选择下载器',
|
||||
unsupportedDownloaderType: '不支持的下载器类型: {type}',
|
||||
testingMediaServer: '正在测试媒体服务器',
|
||||
checkingMediaServer: '检查媒体服务器连通性',
|
||||
mediaServerTestFailed: '媒体服务器测试失败',
|
||||
mediaServerNotSelected: '未选择媒体服务器',
|
||||
unsupportedMediaServerType: '不支持的媒体服务器类型: {type}',
|
||||
testingNotification: '正在测试消息通知',
|
||||
checkingNotification: '检查消息通知连通性',
|
||||
notificationTestFailed: '消息通知测试失败',
|
||||
notificationNotSelected: '未选择通知类型',
|
||||
unsupportedNotificationType: '不支持的通知类型: {type}',
|
||||
testFailedHint: '请检查配置是否正确,修改后可以重新测试',
|
||||
saveStepFailed: '保存步骤设置失败',
|
||||
basicSettingsSaved: '基础设置保存成功',
|
||||
saveBasicSettingsFailed: '保存基础设置失败',
|
||||
storageSettingsSaved: '存储设置保存成功',
|
||||
saveStorageSettingsFailed: '保存存储设置失败',
|
||||
downloaderSettingsSaved: '下载器设置保存成功',
|
||||
saveDownloaderSettingsFailed: '保存下载器设置失败',
|
||||
mediaServerSettingsSaved: '媒体服务器设置保存成功',
|
||||
saveMediaServerSettingsFailed: '保存媒体服务器设置失败',
|
||||
notificationSettingsSaved: '通知设置保存成功',
|
||||
saveNotificationSettingsFailed: '保存通知设置失败',
|
||||
preferenceSettingsSaved: '偏好设置保存成功',
|
||||
savePreferenceSettingsFailed: '保存偏好设置失败',
|
||||
passwordUpdateSuccess: '密码更新成功',
|
||||
passwordUpdateFailed: '密码更新失败',
|
||||
userCreateSuccess: '用户创建成功',
|
||||
basic: {
|
||||
title: '基础设置',
|
||||
description: '设置访问域名、用户名密码和网络配置',
|
||||
appDomain: '访问域名',
|
||||
appDomainHint: '用于发送通知时,添加快捷跳转地址',
|
||||
wallpaper: '背景壁纸',
|
||||
wallpaperHint: '选择登录页面背景来源',
|
||||
recognizeSource: '识别数据源',
|
||||
recognizeSourceHint: '设置默认媒体信息识别数据源',
|
||||
apiToken: 'API 令牌',
|
||||
apiTokenHint: '访问MoviePilot API 需要的访问令牌,请记录下来以便后续使用',
|
||||
currentUserHint: '当前用户,不可修改',
|
||||
passwordOptionalHint: '留空表示不修改密码',
|
||||
confirmPasswordHint: '确认新密码',
|
||||
apiTokenRequired: 'API Token不能为空',
|
||||
},
|
||||
storage: {
|
||||
title: '存储',
|
||||
description: '配置下载目录和媒体库目录',
|
||||
info: '存储配置说明',
|
||||
infoDesc: '配置本地存储目录,用于下载和媒体库管理',
|
||||
downloadPath: '下载目录',
|
||||
downloadPathHint: '设置下载文件的存储路径',
|
||||
libraryPath: '媒体库目录',
|
||||
libraryPathHint: '设置媒体文件的存储路径',
|
||||
downloadPathRequired: '下载目录不能为空',
|
||||
libraryPathRequired: '媒体库目录不能为空',
|
||||
},
|
||||
downloader: {
|
||||
title: '下载器',
|
||||
description: '配置下载器',
|
||||
info: '下载器配置说明',
|
||||
infoDesc: '配置下载器用于下载资源,可选择qBittorrent或Transmission',
|
||||
type: '下载器类型',
|
||||
typeHint: '选择要使用的下载器类型',
|
||||
name: '下载器名称',
|
||||
nameHint: '为下载器设置一个名称',
|
||||
qbittorrentConfig: 'qBittorrent 配置',
|
||||
transmissionConfig: 'Transmission 配置',
|
||||
host: '服务器地址',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
downloadPath: '下载路径',
|
||||
},
|
||||
mediaServer: {
|
||||
title: '媒体服务器',
|
||||
description: '配置媒体服务器',
|
||||
info: '媒体服务器配置说明',
|
||||
infoDesc: '配置媒体服务器用于媒体库管理,可选择Emby、Jellyfin或Plex等',
|
||||
type: '媒体服务器类型',
|
||||
typeHint: '选择要使用的媒体服务器类型',
|
||||
name: '服务器名称',
|
||||
nameHint: '为媒体服务器设置一个名称',
|
||||
embyConfig: 'Emby 配置',
|
||||
jellyfinConfig: 'Jellyfin 配置',
|
||||
plexConfig: 'Plex 配置',
|
||||
host: '服务器地址',
|
||||
apiKey: 'API 密钥',
|
||||
token: '访问令牌',
|
||||
},
|
||||
notification: {
|
||||
title: '通知',
|
||||
description: '配置通知渠道',
|
||||
info: '通知配置说明',
|
||||
infoDesc: '配置通知渠道用于接收系统消息(可选)',
|
||||
type: '通知类型',
|
||||
typeHint: '选择要使用的通知渠道类型',
|
||||
name: '通知名称',
|
||||
nameHint: '为通知渠道设置一个名称',
|
||||
telegramConfig: 'Telegram 配置',
|
||||
emailConfig: '邮件配置',
|
||||
botToken: '机器人令牌',
|
||||
chatId: '聊天ID',
|
||||
smtpServer: 'SMTP 服务器',
|
||||
smtpPort: 'SMTP 端口',
|
||||
senderEmail: '发送邮箱',
|
||||
senderPassword: '发送密码',
|
||||
receiverEmail: '接收邮箱',
|
||||
},
|
||||
preferences: {
|
||||
title: '资源偏好',
|
||||
description: '设置资源下载偏好',
|
||||
info: '资源偏好说明',
|
||||
infoDesc: '设置资源下载的偏好,系统将根据这些偏好自动选择最佳资源',
|
||||
quality: '质量偏好',
|
||||
qualityHint: '选择偏好的视频质量',
|
||||
subtitle: '字幕偏好',
|
||||
subtitleHint: '选择偏好的字幕类型',
|
||||
resolution: '分辨率偏好',
|
||||
resolutionHint: '选择偏好的视频分辨率',
|
||||
presetRules: '预设规则',
|
||||
detailedConfig: '详细配置',
|
||||
quickPresets: '快速预设',
|
||||
quickPresetsDesc: '选择预设配置,系统将自动应用对应的规则',
|
||||
personalizationOptions: '个性化选项',
|
||||
personalizationOptionsDesc: '根据您的需求调整规则',
|
||||
excludeDolbyVision: '排除杜比视界',
|
||||
excludeDolbyVisionHint: '选中后规则中将排除杜比视界资源',
|
||||
excludeBluray: '排除蓝光原盘',
|
||||
excludeBlurayHint: '选中后规则中将排除蓝光原盘资源',
|
||||
presets: {
|
||||
'4k-enthusiast': {
|
||||
name: '4K发烧友',
|
||||
description: '追求最高画质,优先4K',
|
||||
},
|
||||
'balanced': {
|
||||
name: '平衡模式',
|
||||
description: '画质与存储空间的平衡选择',
|
||||
},
|
||||
'space-saver': {
|
||||
name: '节省空间',
|
||||
description: '优先较小文件,节省存储空间',
|
||||
},
|
||||
'free-priority': {
|
||||
name: '免费优先',
|
||||
description: '优先免费资源,其它的没有要求',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ export default {
|
||||
itemsPerPage: '每頁條數',
|
||||
pageText: '{0}-{1} 共 {2} 條',
|
||||
noDataText: '沒有數據',
|
||||
next: '下一步',
|
||||
previous: '上一步',
|
||||
skip: '跳過',
|
||||
loadingText: '加載中...',
|
||||
networkRequired: '此功能需要網絡連接',
|
||||
networkDisconnected: '網絡連接已斷開',
|
||||
@@ -321,10 +324,6 @@ export default {
|
||||
title: '通知',
|
||||
description: '通知渠道(微信、Telegram、Slack、SynologyChat、VoceChat、WebPush)、消息發送範圍',
|
||||
},
|
||||
words: {
|
||||
title: '詞表',
|
||||
description: '自定義識別詞、自定義製作組/字幕組、自定義占位符、文件整理屏蔽詞',
|
||||
},
|
||||
about: {
|
||||
title: '關於',
|
||||
description: '軟件版本',
|
||||
@@ -368,8 +367,10 @@ export default {
|
||||
deleteFailed: '用戶刪除失敗!',
|
||||
profile: '個人信息',
|
||||
systemSettings: '系統設定',
|
||||
wizardSettings: '設定向導',
|
||||
siteAuth: '用戶認證',
|
||||
helpDocs: '幫助文檔',
|
||||
about: '關於',
|
||||
restart: '重啟',
|
||||
management: '用戶管理',
|
||||
noUsers: '沒有用戶',
|
||||
@@ -377,8 +378,11 @@ export default {
|
||||
addUser: '添加用戶',
|
||||
editUser: '編輯用戶',
|
||||
username: '用戶名',
|
||||
usernameHint: '用於登入系統的用戶名',
|
||||
password: '密碼',
|
||||
passwordHint: '用於登入系統的密碼',
|
||||
confirmPassword: '確認密碼',
|
||||
confirmPasswordHint: '請再次輸入密碼以確認',
|
||||
role: '角色',
|
||||
email: '郵箱',
|
||||
enabled: '啟用',
|
||||
@@ -491,6 +495,14 @@ export default {
|
||||
title: '消息',
|
||||
subtitle: '消息中心',
|
||||
},
|
||||
words: {
|
||||
title: '詞表',
|
||||
subtitle: '詞表設置',
|
||||
},
|
||||
cache: {
|
||||
title: '緩存',
|
||||
subtitle: '管理緩存',
|
||||
},
|
||||
},
|
||||
workflow: {
|
||||
components: '動作組件',
|
||||
@@ -761,6 +773,8 @@ export default {
|
||||
originalTitle: '原始標題',
|
||||
status: '狀態',
|
||||
releaseDate: '上映日期',
|
||||
digitalRelease: '數位發行',
|
||||
physicalRelease: '實體發行',
|
||||
originalLanguage: '原始語言',
|
||||
productionCountries: '出品國家',
|
||||
productionCompanies: '製作公司',
|
||||
@@ -844,6 +858,7 @@ export default {
|
||||
batchEnableError: '批量啟用操作失敗',
|
||||
batchPauseError: '批量暫停操作失敗',
|
||||
batchDeleteError: '批量刪除操作失敗',
|
||||
minSubscribers: '最小訂閱人數',
|
||||
},
|
||||
recommend: {
|
||||
all: '全部',
|
||||
@@ -1210,9 +1225,21 @@ export default {
|
||||
apiTokenLength: 'API Token不得低於16位',
|
||||
githubToken: 'Github Token',
|
||||
githubTokenFormat: 'ghp_**** 或 github_pat_****',
|
||||
githubTokenHint: '用於提高插件等訪問Github API時的限流閾值',
|
||||
githubTokenHint: '用於提高插件等訪問Github API時的限流閾值,建議配置,否則插件可能無法正常使用',
|
||||
ocrHost: '驗證碼識別服務器',
|
||||
ocrHostHint: '用於站點簽到、更新站點Cookie等識別驗證碼',
|
||||
aiAgent: '啟用智能助手',
|
||||
aiAgentEnable: '啟用智能助手',
|
||||
aiAgentEnableHint: '啟用後可使用智能助手功能,需要配置LLM相關參數',
|
||||
llmProvider: 'LLM提供商',
|
||||
llmProviderHint: '選擇使用的LLM服務提供商',
|
||||
llmModel: 'LLM模型名稱',
|
||||
llmModelHint: '指定使用的LLM模型,如gpt-3.5-turbo、deepseek-chat等',
|
||||
llmApiKey: 'LLM API密鑰',
|
||||
llmApiKeyHint: 'LLM服務提供商的API密鑰,用於身份驗證',
|
||||
llmApiKeyPlaceholder: '請輸入API密鑰',
|
||||
llmBaseUrl: 'LLM基礎URL',
|
||||
llmBaseUrlHint: 'LLM API的基礎URL地址,用於自定義API端點',
|
||||
advancedSettings: '高級設置',
|
||||
advancedSettingsDesc: '系統進階設置,特殊情況下才需要調整',
|
||||
downloaders: '下載器',
|
||||
@@ -1646,7 +1673,9 @@ export default {
|
||||
bestVersionRuleGroup: '洗版優先級規則組',
|
||||
bestVersionRuleGroupHint: '按選定的過濾規則組對洗版訂閱進行過濾',
|
||||
timedSearch: '訂閱定時搜索',
|
||||
timedSearchHint: '每隔24小時全站搜索,以補全訂閱可能漏掉的資源',
|
||||
timedSearchHint: '每隔指定時間全站搜索,以補全訂閱可能漏掉的資源',
|
||||
searchInterval: '訂閱搜索時間間隔',
|
||||
searchIntervalHint: '設置訂閱搜索的時間間隔,僅在開啟訂閱定時搜索時生效',
|
||||
checkLocalMedia: '檢查文件系統資源',
|
||||
checkLocalMediaHint: '掃描存儲目錄中是否已存在相應資源文件,以避免重複下載;不管是否開啟都會檢查媒體伺服器',
|
||||
modes: {
|
||||
@@ -1661,6 +1690,8 @@ export default {
|
||||
hour1: '1小時',
|
||||
hour12: '12小時',
|
||||
day1: '1天',
|
||||
day3: '3天',
|
||||
week1: '一週',
|
||||
},
|
||||
saveSuccess: '訂閱站點保存成功',
|
||||
saveFailed: '訂閱站點保存失敗!',
|
||||
@@ -1743,8 +1774,12 @@ export default {
|
||||
add: '添加用戶',
|
||||
edit: '編輯用戶',
|
||||
username: '用戶名',
|
||||
usernameRequired: '用戶名不能為空',
|
||||
password: '密碼',
|
||||
passwordMinLength: '密碼長度不能少於6位',
|
||||
confirmPassword: '確認密碼',
|
||||
confirmPasswordRequired: '請確認密碼',
|
||||
passwordMismatch: '兩次輸入的密碼不一致',
|
||||
email: '郵箱',
|
||||
nickname: '暱稱',
|
||||
status: '狀態',
|
||||
@@ -1765,9 +1800,7 @@ export default {
|
||||
webPush: 'WebPush',
|
||||
creatingUser: '正在創建【{name}】用戶,請稍後',
|
||||
updatingUser: '正在更新【{name}】用戶,請稍後',
|
||||
usernameRequired: '用戶名不能為空',
|
||||
usernameExists: '用戶名已存在',
|
||||
passwordMismatch: '兩次輸入的密碼不一致',
|
||||
userCreated: '用戶【{name}】創建成功',
|
||||
userCreateFailed: '創建用戶失敗:{message}',
|
||||
userUpdateSuccess: '用戶【{name}】更新成功',
|
||||
@@ -1843,6 +1876,8 @@ export default {
|
||||
startDownload: '開始下載',
|
||||
downloadSuccess: '{site} {title} 下載成功!',
|
||||
downloadFailed: '{site} {title} 下載失敗:{message}!',
|
||||
showAdvancedOptions: '顯示高級選項',
|
||||
hideAdvancedOptions: '隱藏高級選項',
|
||||
},
|
||||
subscribeShare: {
|
||||
shareSubscription: '分享訂閱',
|
||||
@@ -2590,6 +2625,9 @@ export default {
|
||||
nameRequired: '名稱不能為空',
|
||||
nameDuplicate: '名稱已存在',
|
||||
defaultChanged: '存在預設下載器,已替換',
|
||||
hostRequired: '地址不能為空',
|
||||
usernameRequired: '用戶名不能為空',
|
||||
passwordRequired: '密碼不能為空',
|
||||
},
|
||||
filterRule: {
|
||||
title: '過濾規則',
|
||||
@@ -2625,15 +2663,21 @@ export default {
|
||||
host: '地址',
|
||||
hostPlaceholder: 'http(s)://ip:port',
|
||||
hostHint: '服務端地址,格式:http(s)://ip:port',
|
||||
hostRequired: '地址不能為空',
|
||||
playHost: '外網播放地址',
|
||||
playHostPlaceholder: 'http(s)://domain:port',
|
||||
playHostHint: '跳轉播放頁面使用的地址,格式:http(s)://domain:port',
|
||||
apiKey: 'API密鑰',
|
||||
apiKeyRequired: 'API密鑰不能為空',
|
||||
embyApiKeyHint: 'Emby設置->高級->API密鑰中生成的密鑰',
|
||||
jellyfinApiKeyHint: 'Jellyfin設置->高級->API密鑰中生成的密鑰',
|
||||
plexToken: 'X-Plex-Token',
|
||||
tokenRequired: 'Token不能為空',
|
||||
usernameRequired: '用戶名不能為空',
|
||||
passwordRequired: '密碼不能為空',
|
||||
plexTokenHint: '瀏覽器F12->網絡,從Plex請求URL中獲取的X-Plex-Token',
|
||||
username: '用戶名',
|
||||
usernameHint: '登錄用戶名',
|
||||
password: '密碼',
|
||||
syncLibraries: '同步媒體庫',
|
||||
syncLibrariesHint: '只有選中的媒體庫才會被同步',
|
||||
@@ -2670,6 +2714,9 @@ export default {
|
||||
firstAirDateAsc: '首播日期升序',
|
||||
voteAverageDesc: '評分降序',
|
||||
voteAverageAsc: '評分升序',
|
||||
time: '最新',
|
||||
count: '熱門',
|
||||
rating: '評分',
|
||||
},
|
||||
genreType: {
|
||||
action: '動作',
|
||||
@@ -2799,7 +2846,9 @@ export default {
|
||||
libraryStorage: '媒體庫存儲',
|
||||
libraryDirectory: '媒體庫目錄',
|
||||
transferType: '轉移方式',
|
||||
transferTypeHint: '文件操作整理方式,硬連結節省空間,複製更安全',
|
||||
overwriteMode: '覆蓋模式',
|
||||
overwriteModeHint: '當目標文件已存在時的處理方式',
|
||||
smartRename: '智能重命名',
|
||||
scrapingMetadata: '刮削元數據',
|
||||
sendNotification: '發送通知',
|
||||
@@ -2840,4 +2889,149 @@ export default {
|
||||
customBackgroundImageHint: '支援網路圖片URL,留空則使用漸變背景',
|
||||
pluginCount: '{count} 個插件',
|
||||
},
|
||||
setupWizard: {
|
||||
title: '歡迎使用 MoviePilot !',
|
||||
subtitle: '按向導完成配置,即刻開始使用。',
|
||||
completed: '設定精靈完成!',
|
||||
failed: '設定精靈失敗,請重試',
|
||||
complete: '完成設定',
|
||||
loading: '正在載入配置資料...',
|
||||
testing: '正在測試',
|
||||
connectivityTestSuccess: '連通性測試通過',
|
||||
connectivityTestFailed: '連通性測試失敗',
|
||||
testingStorage: '正在測試存儲目錄',
|
||||
checkingStorage: '檢查存儲目錄連通性',
|
||||
testingDownloader: '正在測試下載器',
|
||||
checkingDownloader: '檢查下載器連通性',
|
||||
testingMediaServer: '正在測試媒體服務器',
|
||||
checkingMediaServer: '檢查媒體服務器連通性',
|
||||
testingNotification: '正在測試消息通知',
|
||||
checkingNotification: '檢查消息通知連通性',
|
||||
testFailedHint: '請檢查配置是否正確,修改後可以重新測試',
|
||||
unsupportedDownloaderType: '不支援的下載器類型: {type}',
|
||||
unsupportedMediaServerType: '不支援的媒體服務器類型: {type}',
|
||||
unsupportedNotificationType: '不支援的通知類型: {type}',
|
||||
passwordUpdateSuccess: '密碼更新成功',
|
||||
userCreateSuccess: '使用者建立成功',
|
||||
passwordUpdateFailed: '密碼更新失敗',
|
||||
basic: {
|
||||
title: '基礎設定',
|
||||
description: '設定存取網域、用戶名密碼和網路配置',
|
||||
appDomain: '存取網域',
|
||||
appDomainHint: '用於發送通知時,新增快速跳轉位址',
|
||||
wallpaper: '背景桌布',
|
||||
wallpaperHint: '選擇登入頁面背景來源',
|
||||
recognizeSource: '識別資料來源',
|
||||
recognizeSourceHint: '設定預設媒體資訊識別資料來源',
|
||||
apiToken: 'API 權杖',
|
||||
apiTokenHint: '訪問MoviePilot API 需要的訪問令牌,請記錄下來以便後續使用',
|
||||
currentUserHint: '目前使用者,不可修改',
|
||||
passwordOptionalHint: '留空表示不修改密碼',
|
||||
confirmPasswordHint: '確認新密碼',
|
||||
apiTokenRequired: 'API Token 不能為空',
|
||||
},
|
||||
storage: {
|
||||
title: '儲存',
|
||||
description: '設定下載目錄和媒體庫目錄',
|
||||
info: '儲存設定說明',
|
||||
infoDesc: '設定本機儲存目錄,用於下載和媒體庫管理',
|
||||
downloadPath: '下載目錄',
|
||||
downloadPathHint: '設定下載檔案的儲存路徑',
|
||||
libraryPath: '媒體庫目錄',
|
||||
libraryPathHint: '設定媒體檔案的儲存路徑',
|
||||
downloadPathRequired: '下載目錄不能為空',
|
||||
libraryPathRequired: '媒體庫目錄不能為空',
|
||||
},
|
||||
downloader: {
|
||||
title: '下載器',
|
||||
description: '設定下載器',
|
||||
info: '下載器設定說明',
|
||||
infoDesc: '設定下載器用於下載資源,可選擇qBittorrent或Transmission',
|
||||
type: '下載器類型',
|
||||
typeHint: '選擇要使用的下載器類型',
|
||||
name: '下載器名稱',
|
||||
nameHint: '為下載器設定一個名稱',
|
||||
qbittorrentConfig: 'qBittorrent 設定',
|
||||
transmissionConfig: 'Transmission 設定',
|
||||
host: '伺服器位址',
|
||||
username: '使用者名稱',
|
||||
password: '密碼',
|
||||
downloadPath: '下載路徑',
|
||||
},
|
||||
mediaServer: {
|
||||
title: '媒體伺服器',
|
||||
description: '設定媒體伺服器',
|
||||
info: '媒體伺服器設定說明',
|
||||
infoDesc: '設定媒體伺服器用於媒體庫管理,可選擇Emby、Jellyfin或Plex等',
|
||||
type: '媒體伺服器類型',
|
||||
typeHint: '選擇要使用的媒體伺服器類型',
|
||||
name: '伺服器名稱',
|
||||
nameHint: '為媒體伺服器設定一個名稱',
|
||||
embyConfig: 'Emby 設定',
|
||||
jellyfinConfig: 'Jellyfin 設定',
|
||||
plexConfig: 'Plex 設定',
|
||||
host: '伺服器位址',
|
||||
apiKey: 'API 金鑰',
|
||||
token: '存取權杖',
|
||||
},
|
||||
notification: {
|
||||
title: '通知',
|
||||
description: '設定通知管道',
|
||||
info: '通知設定說明',
|
||||
infoDesc: '設定通知管道用於接收系統訊息(可選)',
|
||||
type: '通知類型',
|
||||
typeHint: '選擇要使用的通知管道類型',
|
||||
name: '通知名稱',
|
||||
nameHint: '為通知管道設定一個名稱',
|
||||
telegramConfig: 'Telegram 設定',
|
||||
emailConfig: '郵件設定',
|
||||
botToken: '機器人權杖',
|
||||
chatId: '聊天ID',
|
||||
smtpServer: 'SMTP 伺服器',
|
||||
smtpPort: 'SMTP 連接埠',
|
||||
senderEmail: '發送信箱',
|
||||
senderPassword: '發送密碼',
|
||||
receiverEmail: '接收信箱',
|
||||
},
|
||||
preferences: {
|
||||
title: '資源偏好',
|
||||
description: '設定資源下載偏好',
|
||||
info: '資源偏好說明',
|
||||
infoDesc: '設定資源下載的偏好,系統將根據這些偏好自動選擇最佳資源',
|
||||
quality: '品質偏好',
|
||||
qualityHint: '選擇偏好的影片品質',
|
||||
subtitle: '字幕偏好',
|
||||
subtitleHint: '選擇偏好的字幕類型',
|
||||
resolution: '解析度偏好',
|
||||
resolutionHint: '選擇偏好的影片解析度',
|
||||
presetRules: '預設規則',
|
||||
detailedConfig: '詳細設定',
|
||||
quickPresets: '快速預設',
|
||||
quickPresetsDesc: '選擇預設配置,系統將自動應用對應的規則',
|
||||
personalizationOptions: '個性化選項',
|
||||
personalizationOptionsDesc: '根據您的需求調整規則',
|
||||
excludeDolbyVision: '排除杜比視界',
|
||||
excludeDolbyVisionHint: '選中後規則中將排除杜比視界資源',
|
||||
excludeBluray: '排除藍光原盤',
|
||||
excludeBlurayHint: '選中後規則中將排除藍光原盤資源',
|
||||
presets: {
|
||||
'4k-enthusiast': {
|
||||
name: '4K發燒友',
|
||||
description: '追求最高畫質,優先4K',
|
||||
},
|
||||
'balanced': {
|
||||
name: '平衡模式',
|
||||
description: '畫質與儲存空間的平衡選擇',
|
||||
},
|
||||
'space-saver': {
|
||||
name: '節省空間',
|
||||
description: '優先較小檔案,節省儲存空間',
|
||||
},
|
||||
'free-priority': {
|
||||
name: '免費優先',
|
||||
description: '優先免費資源,其它的沒有要求',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -117,12 +117,17 @@ async function subscribeForPushNotifications() {
|
||||
|
||||
// 登录后处理
|
||||
async function afterLogin(superuser: boolean, userPayload: userState, filteredMenus: any[]) {
|
||||
// 如果有原始路径,优先跳转到原始路径
|
||||
if (authStore.originalPath && authStore.originalPath !== '/') {
|
||||
router.push(authStore.originalPath)
|
||||
// 如果需要显示设置向导,跳转到设置向导页面
|
||||
if (userPayload.wizard) {
|
||||
router.push('/setup-wizard')
|
||||
} else {
|
||||
// 跳转到第一个有权限的菜单
|
||||
router.push(filteredMenus[0].to)
|
||||
// 如果有原始路径,优先跳转到原始路径
|
||||
if (authStore.originalPath && authStore.originalPath !== '/') {
|
||||
router.push(authStore.originalPath)
|
||||
} else {
|
||||
// 跳转到第一个有权限的菜单
|
||||
router.push(filteredMenus[0].to)
|
||||
}
|
||||
}
|
||||
|
||||
// 订阅推送通知
|
||||
@@ -165,6 +170,7 @@ function login() {
|
||||
avatar: response.avatar,
|
||||
level: response.level,
|
||||
permissions: response.permissions,
|
||||
wizard: response.widzard,
|
||||
}
|
||||
|
||||
// 在保存用户信息之前检查权限
|
||||
|
||||
@@ -49,7 +49,7 @@ const dataList = ref<Array<Context>>([])
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 加载进度文本
|
||||
const progressText = ref('')
|
||||
const progressText = ref(t('common.pleaseWait'))
|
||||
|
||||
// 加载进度
|
||||
const progressValue = ref(0)
|
||||
|
||||
@@ -3,15 +3,12 @@ import { useRoute } from 'vue-router'
|
||||
import router from '@/router'
|
||||
import AccountSettingNotification from '@/views/setting/AccountSettingNotification.vue'
|
||||
import AccountSettingSite from '@/views/setting/AccountSettingSite.vue'
|
||||
import AccountSettingWords from '@/views/setting/AccountSettingWords.vue'
|
||||
import AccountSettingAbout from '@/views/setting/AccountSettingAbout.vue'
|
||||
import AccountSettingSearch from '@/views/setting/AccountSettingSearch.vue'
|
||||
import AccountSettingSubscribe from '@/views/setting/AccountSettingSubscribe.vue'
|
||||
import AccountSettingSystem from '@/views/setting/AccountSettingSystem.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'
|
||||
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
||||
|
||||
@@ -104,15 +101,6 @@ onMounted(() => {
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 缓存 -->
|
||||
<VWindowItem value="cache">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingCache />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 通知 -->
|
||||
<VWindowItem value="notification">
|
||||
<transition name="fade-slide" appear>
|
||||
@@ -121,24 +109,6 @@ onMounted(() => {
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 词表 -->
|
||||
<VWindowItem value="words">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingWords />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 关于 -->
|
||||
<VWindowItem value="about">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingAbout />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
190
src/pages/setup.vue
Normal file
190
src/pages/setup.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
import BasicSettingsStep from '@/views/setup/BasicSettingsStep.vue'
|
||||
import StorageSettingsStep from '@/views/setup/StorageSettingsStep.vue'
|
||||
import DownloaderSettingsStep from '@/views/setup/DownloaderSettingsStep.vue'
|
||||
import MediaServerSettingsStep from '@/views/setup/MediaServerSettingsStep.vue'
|
||||
import NotificationSettingsStep from '@/views/setup/NotificationSettingsStep.vue'
|
||||
import PreferencesSettingsStep from '@/views/setup/PreferencesSettingsStep.vue'
|
||||
import ConnectivityTest from '@/views/setup/ConnectivityTest.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
const {
|
||||
currentStep,
|
||||
totalSteps,
|
||||
stepTitles,
|
||||
connectivityTest,
|
||||
nextStep,
|
||||
prevStep,
|
||||
completeWizard,
|
||||
initialize,
|
||||
isLoading,
|
||||
} = useSetupWizard()
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
await initialize()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="setup-wizard-fullscreen">
|
||||
<!-- 全屏头部 -->
|
||||
<div class="setup-wizard-header">
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<!-- 左侧占位 -->
|
||||
<div v-if="display.mdAndUp.value" style="inline-size: 96px"></div>
|
||||
|
||||
<!-- 中间标题 -->
|
||||
<div class="d-flex align-center text-center">
|
||||
<div>
|
||||
<h1 class="text-h3 font-weight-bold text-moviepilot mb-3">{{ t('setupWizard.title') }}</h1>
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.subtitle') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧按钮组 -->
|
||||
<div v-if="display.mdAndUp.value" class="d-flex gap-2 px-3">
|
||||
<VBtn
|
||||
variant="text"
|
||||
icon="mdi-cog"
|
||||
@click="router.push('/setting')"
|
||||
size="small"
|
||||
class="text-medium-emphasis"
|
||||
/>
|
||||
<VBtn variant="text" icon="mdi-close" @click="router.push('/')" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 向导内容 -->
|
||||
<VCard max-width="800px" class="mx-auto my-5">
|
||||
<VCardText class="px-1">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="isLoading" class="d-flex flex-column align-center justify-center py-16">
|
||||
<VProgressCircular indeterminate color="primary" size="64" class="mb-4" />
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.loading') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 使用 VStepper 组件 -->
|
||||
<VStepper v-else v-model="currentStep" class="elevation-0" flat alt-labels :mobile="display.smAndDown.value">
|
||||
<!-- 步骤标题 -->
|
||||
<VStepperHeader class="elevation-0">
|
||||
<template v-for="(step, index) in stepTitles" :key="index">
|
||||
<VStepperItem
|
||||
:value="index + 1"
|
||||
:complete="currentStep > index + 1"
|
||||
:color="currentStep >= index + 1 ? 'primary' : 'default'"
|
||||
complete-icon="mdi-check-circle"
|
||||
>
|
||||
<template #title>
|
||||
<span class="text-caption">{{ step }}</span>
|
||||
</template>
|
||||
</VStepperItem>
|
||||
<VDivider v-if="index < stepTitles.length - 1" />
|
||||
</template>
|
||||
</VStepperHeader>
|
||||
|
||||
<!-- 步骤内容 -->
|
||||
<VStepperWindow>
|
||||
<!-- 步骤1:基础参数 -->
|
||||
<VStepperWindowItem :value="1">
|
||||
<BasicSettingsStep />
|
||||
</VStepperWindowItem>
|
||||
|
||||
<!-- 步骤2:存储目录 -->
|
||||
<VStepperWindowItem :value="2">
|
||||
<StorageSettingsStep />
|
||||
</VStepperWindowItem>
|
||||
|
||||
<!-- 步骤3:下载器 -->
|
||||
<VStepperWindowItem :value="3">
|
||||
<DownloaderSettingsStep />
|
||||
</VStepperWindowItem>
|
||||
|
||||
<!-- 步骤4:媒体服务器 -->
|
||||
<VStepperWindowItem :value="4">
|
||||
<MediaServerSettingsStep />
|
||||
</VStepperWindowItem>
|
||||
|
||||
<!-- 步骤5:通知 -->
|
||||
<VStepperWindowItem :value="5">
|
||||
<NotificationSettingsStep />
|
||||
</VStepperWindowItem>
|
||||
|
||||
<!-- 步骤6:资源偏好 -->
|
||||
<VStepperWindowItem :value="6">
|
||||
<PreferencesSettingsStep />
|
||||
</VStepperWindowItem>
|
||||
</VStepperWindow>
|
||||
|
||||
<!-- 连通性测试进度条 -->
|
||||
<ConnectivityTest />
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<VCardActions class="justify-space-between">
|
||||
<div class="d-flex gap-2">
|
||||
<VBtn
|
||||
v-if="currentStep !== 1"
|
||||
prepend-icon="mdi-chevron-left"
|
||||
@click="prevStep"
|
||||
:disabled="connectivityTest.isTesting"
|
||||
>
|
||||
{{ t('common.previous') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<VBtn
|
||||
v-if="currentStep < totalSteps"
|
||||
color="primary"
|
||||
append-icon="mdi-chevron-right"
|
||||
@click="nextStep"
|
||||
:disabled="connectivityTest.isTesting"
|
||||
>
|
||||
{{ connectivityTest.isTesting ? t('setupWizard.testing') : t('common.next') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
v-else
|
||||
color="success"
|
||||
prepend-icon="mdi-check"
|
||||
@click="completeWizard"
|
||||
:disabled="connectivityTest.isTesting"
|
||||
>
|
||||
{{ t('setupWizard.complete') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardActions>
|
||||
</VStepper>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.setup-wizard-fullscreen {
|
||||
position: fixed;
|
||||
background-color: rgb(var(--v-theme-background));
|
||||
inset: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.setup-wizard-header {
|
||||
position: sticky;
|
||||
z-index: 2000;
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
border-block-end: 1px solid rgb(var(--v-theme-outline-variant));
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 4%);
|
||||
inset-block-start: 0;
|
||||
padding-block: calc(16px + env(safe-area-inset-top)) 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,13 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
|
||||
// 构建路由菜单,每次调用时使用当前的语言环境
|
||||
export function getNavMenus() {
|
||||
const { t } = useI18n()
|
||||
const globalSettingsStore = useGlobalSettingsStore()
|
||||
|
||||
// 检查是否为高级模式
|
||||
const isAdvancedMode = globalSettingsStore.get('ADVANCED_MODE') !== false
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -127,14 +132,18 @@ export function getNavMenus() {
|
||||
admin: true,
|
||||
permission: 'admin',
|
||||
},
|
||||
{
|
||||
title: t('navItems.settings'),
|
||||
icon: 'mdi-cog-outline',
|
||||
to: '/setting',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
permission: 'admin',
|
||||
},
|
||||
...(isAdvancedMode
|
||||
? [
|
||||
{
|
||||
title: t('navItems.settings'),
|
||||
icon: 'mdi-cog-outline',
|
||||
to: '/setting',
|
||||
header: t('menu.system'),
|
||||
admin: true,
|
||||
permission: 'admin',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -185,30 +194,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',
|
||||
tab: 'notification',
|
||||
description: t('settingTabs.notification.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.words.title'),
|
||||
icon: 'mdi-file-word-box',
|
||||
tab: 'words',
|
||||
description: t('settingTabs.words.description'),
|
||||
},
|
||||
{
|
||||
title: t('settingTabs.about.title'),
|
||||
icon: 'mdi-information',
|
||||
tab: 'about',
|
||||
description: t('settingTabs.about.description'),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -208,6 +208,13 @@ const router = createRouter({
|
||||
path: 'login',
|
||||
component: () => import('../pages/login.vue'),
|
||||
},
|
||||
{
|
||||
path: 'setup-wizard',
|
||||
component: () => import('../pages/setup.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
component: () => import('../pages/[...all].vue'),
|
||||
|
||||
@@ -6,7 +6,7 @@ declare let self: ServiceWorkerGlobalScope & {
|
||||
}
|
||||
|
||||
// 缓存版本控制
|
||||
const CACHE_VERSION = 'v1.0.9'
|
||||
const CACHE_VERSION = 'v13'
|
||||
const CACHE_NAMES = {
|
||||
appShell: `app-shell-${CACHE_VERSION}`,
|
||||
static: `static-resources-${CACHE_VERSION}`,
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface userState {
|
||||
level: number
|
||||
// 权限
|
||||
permissions: { [key: string]: any }
|
||||
// 是否需要显示设置向导
|
||||
wizard: boolean
|
||||
}
|
||||
|
||||
export interface globalSettingsState {
|
||||
|
||||
@@ -10,6 +10,7 @@ export const useUserStore = defineStore('user', {
|
||||
avatar: '',
|
||||
level: 1,
|
||||
permissions: DEFAULT_PERMISSIONS,
|
||||
wizard: false,
|
||||
}),
|
||||
|
||||
// 全局持久化
|
||||
@@ -34,6 +35,9 @@ export const useUserStore = defineStore('user', {
|
||||
setPermissions(permissions: object) {
|
||||
this.permissions = { ...DEFAULT_PERMISSIONS, ...permissions }
|
||||
},
|
||||
setWizard(wizard: boolean) {
|
||||
this.wizard = wizard
|
||||
},
|
||||
loginUser(payload: userState) {
|
||||
this.setSuperUser(payload.superUser)
|
||||
this.setUserID(payload.userID)
|
||||
@@ -41,6 +45,7 @@ export const useUserStore = defineStore('user', {
|
||||
this.setAvatar(payload.avatar)
|
||||
this.setLevel(payload.level)
|
||||
this.setPermissions(payload.permissions)
|
||||
this.setWizard(payload.wizard)
|
||||
},
|
||||
reset() {
|
||||
this.setSuperUser(false)
|
||||
@@ -49,6 +54,7 @@ export const useUserStore = defineStore('user', {
|
||||
this.setAvatar('')
|
||||
this.setLevel(1)
|
||||
this.setPermissions(DEFAULT_PERMISSIONS)
|
||||
this.setWizard(false)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -59,5 +65,6 @@ export const useUserStore = defineStore('user', {
|
||||
getAvatar: state => state.avatar,
|
||||
getLevel: state => state.level,
|
||||
getPermissions: state => state.permissions,
|
||||
getWizard: state => state.wizard,
|
||||
},
|
||||
})
|
||||
|
||||
84
src/utils/imageUtils.ts
Normal file
84
src/utils/imageUtils.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 静态资源导入工具函数
|
||||
* 用于在生产环境中正确引用静态资源
|
||||
*/
|
||||
|
||||
// 导入所有 logo 图标
|
||||
import qbittorrentLogo from '@/assets/images/logos/qbittorrent.png'
|
||||
import transmissionLogo from '@/assets/images/logos/transmission.png'
|
||||
import embyLogo from '@/assets/images/logos/emby.png'
|
||||
import jellyfinLogo from '@/assets/images/logos/jellyfin.png'
|
||||
import plexLogo from '@/assets/images/logos/plex.png'
|
||||
import trimemediaLogo from '@/assets/images/logos/trimemedia.png'
|
||||
import wechatLogo from '@/assets/images/logos/wechat.png'
|
||||
import telegramLogo from '@/assets/images/logos/telegram.webp'
|
||||
import slackLogo from '@/assets/images/logos/slack.webp'
|
||||
import synologychatLogo from '@/assets/images/logos/synologychat.png'
|
||||
import vocechatLogo from '@/assets/images/logos/vocechat.png'
|
||||
import downloaderLogo from '@/assets/images/logos/downloader.png'
|
||||
import mediaserverLogo from '@/assets/images/logos/mediaserver.png'
|
||||
import notificationLogo from '@/assets/images/logos/notification.png'
|
||||
import chromeLogo from '@/assets/images/logos/chrome.png'
|
||||
import doubanLogo from '@/assets/images/logos/douban.png'
|
||||
import githubLogo from '@/assets/images/logos/github.png'
|
||||
import tmdbLogo from '@/assets/images/logos/tmdb.png'
|
||||
import fanartLogo from '@/assets/images/logos/fanart.webp'
|
||||
import pythonLogo from '@/assets/images/logos/python.png'
|
||||
import pluginLogo from '@/assets/images/logos/plugin.png'
|
||||
import siteLogo from '@/assets/images/logos/site.webp'
|
||||
import bangumiLogo from '@/assets/images/logos/bangumi.png'
|
||||
import doubanBlackLogo from '@/assets/images/logos/douban-black.png'
|
||||
|
||||
// 图标映射表
|
||||
const logoMap: Record<string, string> = {
|
||||
qbittorrent: qbittorrentLogo,
|
||||
transmission: transmissionLogo,
|
||||
emby: embyLogo,
|
||||
jellyfin: jellyfinLogo,
|
||||
plex: plexLogo,
|
||||
trimemedia: trimemediaLogo,
|
||||
wechat: wechatLogo,
|
||||
telegram: telegramLogo,
|
||||
slack: slackLogo,
|
||||
synologychat: synologychatLogo,
|
||||
vocechat: vocechatLogo,
|
||||
downloader: downloaderLogo,
|
||||
mediaserver: mediaserverLogo,
|
||||
notification: notificationLogo,
|
||||
chrome: chromeLogo,
|
||||
douban: doubanLogo,
|
||||
github: githubLogo,
|
||||
tmdb: tmdbLogo,
|
||||
fanart: fanartLogo,
|
||||
python: pythonLogo,
|
||||
plugin: pluginLogo,
|
||||
site: siteLogo,
|
||||
bangumi: bangumiLogo,
|
||||
'douban-black': doubanBlackLogo,
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图标 URL
|
||||
* @param logoName 图标名称
|
||||
* @returns 图标的 URL
|
||||
*/
|
||||
export function getLogoUrl(logoName: string): string {
|
||||
return logoMap[logoName] || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的图标名称
|
||||
* @returns 图标名称数组
|
||||
*/
|
||||
export function getAvailableLogos(): string[] {
|
||||
return Object.keys(logoMap)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查图标是否存在
|
||||
* @param logoName 图标名称
|
||||
* @returns 是否存在
|
||||
*/
|
||||
export function hasLogo(logoName: string): boolean {
|
||||
return logoName in logoMap
|
||||
}
|
||||
@@ -428,6 +428,17 @@ const getProductionCompanies = computed(() => {
|
||||
return mediaDetail.value.production_companies?.map(company => company.name)
|
||||
})
|
||||
|
||||
// 获取最早实体/数字发行日期
|
||||
const getEarliestReleaseDate = computed(() => {
|
||||
const filteredDates = mediaDetail.value.release_dates?.filter(date => [4, 5].includes(date.type))
|
||||
if (!filteredDates || filteredDates.length === 0)
|
||||
return null
|
||||
|
||||
return filteredDates.reduce((earliest, current) =>
|
||||
new Date(current.date) < new Date(earliest.date) ? current : earliest,
|
||||
)
|
||||
})
|
||||
|
||||
// 计算存在状态的颜色
|
||||
function getExistColor(season: number) {
|
||||
const state = seasonsNotExisted.value[season]
|
||||
@@ -840,6 +851,17 @@ onBeforeMount(() => {
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="mediaDetail.type === '电影' && getEarliestReleaseDate" class="media-fact">
|
||||
<span>{{ t(getEarliestReleaseDate.type === 4 ? 'media.info.digitalRelease' : 'media.info.physicalRelease') }}</span>
|
||||
<span class="media-fact-value">
|
||||
<span class="flex items-center justify-end">
|
||||
<span class="inline-flex items-center justify-center h-4 w-4 text-[0.6rem] font-bold text-current border border-current leading-none">
|
||||
{{ getEarliestReleaseDate.iso_code }}
|
||||
</span>
|
||||
<span class="ml-1.5">{{ getEarliestReleaseDate.date.slice(0, 10) }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="mediaDetail.original_language" class="media-fact">
|
||||
<span>{{ t('media.info.originalLanguage') }}</span>
|
||||
<span class="media-fact-value">{{ mediaDetail.original_language }}</span>
|
||||
|
||||
@@ -5,7 +5,7 @@ import api from '@/api'
|
||||
import type { Plugin } from '@/api/types'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import PluginAppCard from '@/components/cards/PluginAppCard.vue'
|
||||
import noImage from '@images/logos/plugin.png'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { getPluginTabs } from '@/router/i18n-menu'
|
||||
@@ -675,7 +675,7 @@ function pluginIconError(item: Plugin) {
|
||||
// 插件图标地址
|
||||
function pluginIcon(item: Plugin) {
|
||||
// 如果图片加载错误
|
||||
if (pluginIconLoaded.value[item.id || '0'] === false) return noImage
|
||||
if (pluginIconLoaded.value[item.id || '0'] === false) return getLogoUrl('plugin')
|
||||
// 如果是网络图片则使用代理后返回
|
||||
if (item?.plugin_icon?.startsWith('http'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(item?.plugin_icon)}&cache=true`
|
||||
|
||||
@@ -1,364 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { formatDateDifference } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 系统环境变量
|
||||
const systemEnv = ref<any>({})
|
||||
|
||||
// 所有Release
|
||||
const allRelease = ref<any>([])
|
||||
|
||||
// 支持站点
|
||||
const supportingSites = ref<any>({})
|
||||
|
||||
// 支持站点折叠状态
|
||||
const sitesExpanded = ref(false)
|
||||
|
||||
// 去重后的支持站点
|
||||
const uniqueSupportingSites = computed(() => {
|
||||
const sitesMap = new Map()
|
||||
|
||||
Object.entries(supportingSites.value).forEach(([domain, site]: [string, any]) => {
|
||||
if (!sitesMap.has(site.name)) {
|
||||
sitesMap.set(site.name, {
|
||||
name: site.name,
|
||||
urls: [{ domain, url: site.url }],
|
||||
})
|
||||
} else {
|
||||
sitesMap.get(site.name).urls.push({ domain, url: site.url })
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(sitesMap.values())
|
||||
})
|
||||
|
||||
// 显示的支持站点(折叠时只显示前5个)
|
||||
const displayedSites = computed(() => {
|
||||
if (sitesExpanded.value) {
|
||||
return uniqueSupportingSites.value
|
||||
}
|
||||
return uniqueSupportingSites.value.slice(0, 5)
|
||||
})
|
||||
|
||||
// 变更日志对话框
|
||||
const releaseDialog = ref(false)
|
||||
|
||||
// 最新版本
|
||||
const latestRelease = ref('')
|
||||
|
||||
// 变更日志对话框标题
|
||||
const releaseDialogTitle = ref('')
|
||||
|
||||
// 变更日志对话框内容
|
||||
const releaseDialogBody = ref('')
|
||||
|
||||
// 打开日志对话框
|
||||
function showReleaseDialog(title: string, body: string) {
|
||||
releaseDialogTitle.value = title
|
||||
releaseDialogBody.value = body.replaceAll('\r\n', '<br />')
|
||||
releaseDialog.value = true
|
||||
}
|
||||
|
||||
// 查询系统环境变量
|
||||
async function querySystemEnv() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/env')
|
||||
|
||||
systemEnv.value = result.data
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询所有Release
|
||||
async function queryAllRelease() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/versions')
|
||||
|
||||
allRelease.value = result.data ?? []
|
||||
|
||||
// 最新版本
|
||||
if (allRelease.value.length > 0) latestRelease.value = allRelease.value[0].tag_name
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询支持站点
|
||||
async function querySupportingSites() {
|
||||
try {
|
||||
supportingSites.value = await api.get('site/supporting')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换站点列表展开状态
|
||||
function toggleSitesExpanded() {
|
||||
sitesExpanded.value = !sitesExpanded.value
|
||||
}
|
||||
|
||||
// 计算发布时间
|
||||
function releaseTime(releaseDate: string) {
|
||||
// 上一次更新时间
|
||||
return formatDateDifference(releaseDate)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
querySystemEnv()
|
||||
queryAllRelease()
|
||||
querySupportingSites()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-3">
|
||||
<div class="section">
|
||||
<div>
|
||||
<h3 class="heading">{{ t('setting.about.title') }}</h3>
|
||||
</div>
|
||||
<div class="section border-t border-gray-800">
|
||||
<dl>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.softwareVersion') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow flex flex-row items-center truncate">
|
||||
<code class="truncate">{{ systemEnv.VERSION }}</code>
|
||||
<a
|
||||
v-if="latestRelease === systemEnv.VERSION"
|
||||
href="https://github.com/jxxghp/MoviePilot/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 ml-2 !cursor-pointer transition hover:bg-green-400"
|
||||
>
|
||||
{{ t('setting.about.latest') }}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="systemEnv.FRONTEND_VERSION">
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.frontendVersion') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow flex flex-row items-center truncate">
|
||||
<code class="truncate">{{ systemEnv.FRONTEND_VERSION }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.authVersion') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow flex flex-row items-center truncate">
|
||||
<code class="truncate">{{ systemEnv.AUTH_VERSION }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.indexerVersion') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow flex flex-row items-center truncate">
|
||||
<code class="truncate">{{ systemEnv.INDEXER_VERSION }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.configDir') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined">
|
||||
<code>{{ systemEnv.CONFIG_DIR }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.dataDir') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined"
|
||||
><code>{{ t('setting.about.dataDirectory') }}</code></span
|
||||
>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.timezone') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined">
|
||||
<code>{{ systemEnv.TZ }}</code>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.supportingSites') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap gap-2 mt-1 ms-1">
|
||||
<VChip v-for="site in displayedSites" :key="site.name" variant="outlined" size="small">
|
||||
<span class="truncate max-w-32">{{ site.name }}</span>
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="!sitesExpanded && uniqueSupportingSites.length > 5"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
@click="toggleSitesExpanded"
|
||||
>
|
||||
<span> {{ uniqueSupportingSites.length }}+ ...</span>
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="sitesExpanded && uniqueSupportingSites.length > 5"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
@click="toggleSitesExpanded"
|
||||
>
|
||||
<span>< {{ t('setting.about.collapse') }}</span>
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div>
|
||||
<h3 class="heading">{{ t('setting.about.support') }}</h3>
|
||||
</div>
|
||||
<div class="section border-t border-gray-800">
|
||||
<dl>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.documentation') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined">
|
||||
<a
|
||||
href="https://movie-pilot.org"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="text-indigo-500 transition duration-300 hover:underline"
|
||||
>
|
||||
https://movie-pilot.org
|
||||
</a>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.feedback') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined">
|
||||
<a
|
||||
href="https://github.com/jxxghp/MoviePilot/issues/new/choose"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="text-indigo-500 transition duration-300 hover:underline"
|
||||
>
|
||||
https://github.com/jxxghp/MoviePilot/issues/new/choose
|
||||
</a>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt class="block text-sm font-bold">{{ t('setting.about.channel') }}</dt>
|
||||
<dd class="flex text-sm sm:col-span-2 sm:mt-0">
|
||||
<span class="flex-grow undefined">
|
||||
<a
|
||||
href="https://t.me/moviepilot_channel"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="text-indigo-500 transition duration-300 hover:underline"
|
||||
>
|
||||
https://t.me/moviepilot_channel
|
||||
</a>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div>
|
||||
<h3 class="heading">{{ t('setting.about.versions') }}</h3>
|
||||
<div class="section space-y-3">
|
||||
<div>
|
||||
<div
|
||||
v-for="release in allRelease"
|
||||
:key="release.tag_name"
|
||||
class="mb-3 flex w-full flex-col space-y-3 rounded-md px-4 py-2 ring-1 ring-gray-400 sm:flex-row sm:space-y-0 sm:space-x-3"
|
||||
>
|
||||
<div class="flex w-full flex-grow items-center justify-start space-x-2 truncate sm:justify-start">
|
||||
<span class="truncate text-lg font-bold">
|
||||
<span class="mr-2 whitespace-nowrap text-xs font-normal">{{
|
||||
releaseTime(release.published_at)
|
||||
}}</span>
|
||||
{{ release.tag_name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="release.tag_name === latestRelease"
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap cursor-default bg-green-500 bg-opacity-80 border border-green-500 !text-green-100"
|
||||
>
|
||||
{{ t('setting.about.latestVersion') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="release.tag_name === systemEnv.VERSION"
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap cursor-default bg-indigo-500 bg-opacity-80 border border-indigo-500 !text-indigo-100"
|
||||
>
|
||||
{{ t('setting.about.currentVersion') }}
|
||||
</span>
|
||||
</div>
|
||||
<VBtn @click.stop="showReleaseDialog(release.tag_name, release.body)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-text-box-outline" />
|
||||
</template>
|
||||
{{ t('setting.about.viewChangelog') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VDialogCloseBtn @click="releaseDialog = false" />
|
||||
<VCardTitle>{{ releaseDialogTitle }} {{ t('setting.about.changelog') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText v-html="releaseDialogBody" />
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style type="scss" scoped>
|
||||
.heading {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
line-height: 2rem;
|
||||
|
||||
--tw-text-opacity: 1;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-block: 0.5rem 2.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -54,12 +54,20 @@ const rssIntervalItems = [
|
||||
{ title: t('setting.subscribe.intervals.day1'), value: 1440 },
|
||||
]
|
||||
|
||||
// 订阅搜索时间间隔选择项(小时)
|
||||
const subscribeSearchIntervalItems = [
|
||||
{ title: t('setting.subscribe.intervals.day1'), value: 24 },
|
||||
{ title: t('setting.subscribe.intervals.day3'), value: 72 },
|
||||
{ title: t('setting.subscribe.intervals.week1'), value: 168 },
|
||||
]
|
||||
|
||||
// 系统设置项
|
||||
const SystemSettings = ref<any>({
|
||||
// 基础设置
|
||||
Basic: {
|
||||
SUBSCRIBE_MODE: 'auto',
|
||||
SUBSCRIBE_SEARCH: false,
|
||||
SUBSCRIBE_SEARCH_INTERVAL: 24,
|
||||
SUBSCRIBE_RSS_INTERVAL: 30,
|
||||
LOCAL_EXISTS_SEARCH: false,
|
||||
},
|
||||
@@ -252,6 +260,16 @@ onMounted(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="SystemSettings.Basic.SUBSCRIBE_SEARCH" cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="SystemSettings.Basic.SUBSCRIBE_SEARCH_INTERVAL"
|
||||
:items="subscribeSearchIntervalItems"
|
||||
:label="t('setting.subscribe.searchInterval')"
|
||||
:hint="t('setting.subscribe.searchIntervalHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-timer"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Basic.LOCAL_EXISTS_SEARCH"
|
||||
|
||||
@@ -31,6 +31,11 @@ const SystemSettings = ref<any>({
|
||||
GITHUB_TOKEN: null,
|
||||
OCR_HOST: null,
|
||||
CUSTOMIZE_WALLPAPER_API_URL: null,
|
||||
AI_AGENT_ENABLE: false,
|
||||
LLM_PROVIDER: 'deepseek',
|
||||
LLM_MODEL: 'deepseek-chat',
|
||||
LLM_API_KEY: null,
|
||||
LLM_BASE_URL: 'https://api.deepseek.com',
|
||||
},
|
||||
// 高级系统设置
|
||||
Advanced: {
|
||||
@@ -114,6 +119,10 @@ const progressDialog = ref(false)
|
||||
// 高级设置对话框
|
||||
const advancedDialog = ref(false)
|
||||
|
||||
// LLM 模型列表
|
||||
const llmModels = ref<string[]>([])
|
||||
const loadingModels = ref(false)
|
||||
|
||||
const activeTab = ref('system')
|
||||
|
||||
// 元数据语言
|
||||
@@ -149,6 +158,30 @@ const logLevelItems = [
|
||||
// 安全域名添加变量
|
||||
const newSecurityDomain = ref('')
|
||||
|
||||
// 加载LLM模型列表
|
||||
async function loadLlmModels() {
|
||||
loadingModels.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/llm-models', {
|
||||
params: {
|
||||
provider: SystemSettings.value.Basic.LLM_PROVIDER,
|
||||
api_key: SystemSettings.value.Basic.LLM_API_KEY,
|
||||
base_url: SystemSettings.value.Basic.LLM_BASE_URL,
|
||||
},
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
llmModels.value = result.data
|
||||
if (llmModels.value.length > 0) SystemSettings.value.Basic.LLM_MODEL = llmModels.value[0]
|
||||
} else {
|
||||
$toast.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
loadingModels.value = false
|
||||
}
|
||||
|
||||
// 添加安全域名
|
||||
function addSecurityDomain() {
|
||||
if (
|
||||
@@ -607,6 +640,74 @@ onDeactivated(() => {
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VDivider class="my-4" />
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Basic.AI_AGENT_ENABLE"
|
||||
:label="t('setting.system.aiAgentEnable')"
|
||||
:hint="t('setting.system.aiAgentEnableHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="SystemSettings.Basic.LLM_PROVIDER"
|
||||
:label="t('setting.system.llmProvider')"
|
||||
:hint="t('setting.system.llmProviderHint')"
|
||||
persistent-hint
|
||||
:items="[
|
||||
{ title: 'OpenAI', value: 'openai' },
|
||||
{ title: 'Google', value: 'google' },
|
||||
{ title: 'DeepSeek', value: 'deepseek' },
|
||||
]"
|
||||
prepend-inner-icon="mdi-robot"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="SystemSettings.Basic.LLM_BASE_URL"
|
||||
:label="t('setting.system.llmBaseUrl')"
|
||||
:hint="t('setting.system.llmBaseUrlHint')"
|
||||
placeholder="https://api.deepseek.com"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-link"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="SystemSettings.Basic.LLM_API_KEY"
|
||||
:label="t('setting.system.llmApiKey')"
|
||||
:hint="t('setting.system.llmApiKeyHint')"
|
||||
:placeholder="t('setting.system.llmApiKeyPlaceholder')"
|
||||
persistent-hint
|
||||
type="password"
|
||||
prepend-inner-icon="mdi-key-variant"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
||||
<VCombobox
|
||||
v-model="SystemSettings.Basic.LLM_MODEL"
|
||||
:label="t('setting.system.llmModel')"
|
||||
:hint="t('setting.system.llmModelHint')"
|
||||
:placeholder="t('setting.system.llmModelHint')"
|
||||
persistent-hint
|
||||
:items="llmModels"
|
||||
:loading="loadingModels"
|
||||
prepend-inner-icon="mdi-brain"
|
||||
>
|
||||
<template #append-inner>
|
||||
<VBtn
|
||||
variant="text"
|
||||
icon="mdi-refresh"
|
||||
size="small"
|
||||
@click="loadLlmModels"
|
||||
:disabled="!SystemSettings.Basic.LLM_API_KEY"
|
||||
/>
|
||||
</template>
|
||||
</VCombobox>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
|
||||
160
src/views/setup/BasicSettingsStep.vue
Normal file
160
src/views/setup/BasicSettingsStep.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { wizardData, createRandomString, copyValue, validateCurrentStep } = useSetupWizard()
|
||||
|
||||
// 密码可见性控制
|
||||
const isPasswordVisible = ref(false)
|
||||
const isConfirmPasswordVisible = ref(false)
|
||||
|
||||
// 验证状态
|
||||
const validation = computed(() => validateCurrentStep())
|
||||
const hasErrors = computed(() => !validation.value.isValid)
|
||||
|
||||
// 密码相关验证
|
||||
const passwordError = computed(() => {
|
||||
if (!wizardData.value.basic.password) return false
|
||||
return wizardData.value.basic.password.length < 6
|
||||
})
|
||||
|
||||
const confirmPasswordError = computed(() => {
|
||||
if (!wizardData.value.basic.password) return false
|
||||
if (!wizardData.value.basic.confirmPassword) return true
|
||||
return wizardData.value.basic.password !== wizardData.value.basic.confirmPassword
|
||||
})
|
||||
|
||||
const passwordErrorMessage = computed(() => {
|
||||
if (passwordError.value) return t('dialog.userAddEdit.passwordMinLength')
|
||||
return ''
|
||||
})
|
||||
|
||||
const confirmPasswordErrorMessage = computed(() => {
|
||||
if (!wizardData.value.basic.password) return ''
|
||||
if (!wizardData.value.basic.confirmPassword) return t('dialog.userAddEdit.confirmPasswordRequired')
|
||||
if (confirmPasswordError.value) return t('dialog.userAddEdit.passwordMismatch')
|
||||
return ''
|
||||
})
|
||||
|
||||
// API Token验证
|
||||
const apiTokenError = computed(() => {
|
||||
return !wizardData.value.basic.apiToken && hasErrors.value
|
||||
})
|
||||
|
||||
const apiTokenErrorMessage = computed(() => {
|
||||
if (apiTokenError.value) return t('setupWizard.basic.apiTokenRequired')
|
||||
return ''
|
||||
})
|
||||
|
||||
// 用户名验证(虽然是只读的,但为了完整性)
|
||||
const usernameError = computed(() => {
|
||||
return !wizardData.value.basic.username && hasErrors.value
|
||||
})
|
||||
|
||||
const usernameErrorMessage = computed(() => {
|
||||
if (usernameError.value) return t('dialog.userAddEdit.usernameRequired')
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="outlined">
|
||||
<VCardText>
|
||||
<div class="text-center mb-6">
|
||||
<h3 class="text-h4 mb-2">{{ t('setupWizard.basic.title') }}</h3>
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.basic.description') }}</p>
|
||||
</div>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.basic.appDomain"
|
||||
:label="t('setupWizard.basic.appDomain')"
|
||||
:hint="t('setupWizard.basic.appDomainHint')"
|
||||
placeholder="http://localhost:3000"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-web"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.basic.username"
|
||||
:label="t('user.username')"
|
||||
:hint="t('setupWizard.basic.currentUserHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account"
|
||||
readonly
|
||||
:error="usernameError"
|
||||
:error-messages="usernameError ? [usernameErrorMessage] : []"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.basic.password"
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
:label="t('user.password')"
|
||||
:hint="t('setupWizard.basic.passwordOptionalHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-lock"
|
||||
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
:error="passwordError"
|
||||
:error-messages="passwordError ? [passwordErrorMessage] : []"
|
||||
clearable
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.basic.confirmPassword"
|
||||
:type="isConfirmPasswordVisible ? 'text' : 'password'"
|
||||
:label="t('user.confirmPassword')"
|
||||
:hint="t('setupWizard.basic.confirmPasswordHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-lock-check"
|
||||
:append-inner-icon="isConfirmPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
|
||||
:disabled="!wizardData.basic.password"
|
||||
:error="confirmPasswordError"
|
||||
:error-messages="confirmPasswordError ? [confirmPasswordErrorMessage] : []"
|
||||
clearable
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.basic.proxyHost"
|
||||
:label="t('setting.system.proxyHost')"
|
||||
:hint="t('setting.system.proxyHostHint')"
|
||||
placeholder="http://127.0.0.1:7890"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-server-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.basic.githubToken"
|
||||
:label="t('setting.system.githubToken')"
|
||||
:placeholder="t('setting.system.githubTokenFormat')"
|
||||
:hint="t('setting.system.githubTokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-github"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.basic.apiToken"
|
||||
:label="t('setupWizard.basic.apiToken')"
|
||||
:hint="t('setupWizard.basic.apiTokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
:append-inner-icon="wizardData.basic.apiToken ? 'mdi-content-copy' : 'mdi-reload'"
|
||||
@click:append-inner="
|
||||
wizardData.basic.apiToken ? copyValue(wizardData.basic.apiToken) : createRandomString()
|
||||
"
|
||||
:error="apiTokenError"
|
||||
:error-messages="apiTokenError ? [apiTokenErrorMessage] : []"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
64
src/views/setup/ConnectivityTest.vue
Normal file
64
src/views/setup/ConnectivityTest.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { connectivityTest } = useSetupWizard()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 连通性测试进度条 -->
|
||||
<VCard v-if="connectivityTest.isTesting || connectivityTest.showResult" variant="outlined" class="mx-4 mb-4">
|
||||
<VCardText class="text-center py-4">
|
||||
<!-- 测试中 -->
|
||||
<div v-if="connectivityTest.isTesting">
|
||||
<VIcon icon="mdi-cog-sync" class="rotating mb-2" color="primary" size="24" />
|
||||
<div class="text-body-2 mb-2">{{ connectivityTest.testMessage }}</div>
|
||||
<VProgressLinear
|
||||
v-model="connectivityTest.testProgress"
|
||||
color="primary"
|
||||
height="6"
|
||||
rounded
|
||||
class="mb-2"
|
||||
/>
|
||||
<div class="text-caption text-medium-emphasis">{{ Math.round(connectivityTest.testProgress) }}%</div>
|
||||
</div>
|
||||
|
||||
<!-- 测试结果 -->
|
||||
<div v-else-if="connectivityTest.showResult">
|
||||
<VIcon
|
||||
:icon="connectivityTest.testResult === 'success' ? 'mdi-check-circle' : 'mdi-alert-circle'"
|
||||
:color="connectivityTest.testResult === 'success' ? 'success' : 'error'"
|
||||
size="24"
|
||||
class="mb-2"
|
||||
/>
|
||||
<div
|
||||
:class="connectivityTest.testResult === 'success' ? 'text-success' : 'text-error'"
|
||||
class="text-body-2 mb-2 font-weight-medium"
|
||||
>
|
||||
{{ connectivityTest.testMessage }}
|
||||
</div>
|
||||
<div v-if="connectivityTest.testResult === 'error'" class="text-caption text-medium-emphasis">
|
||||
{{ t('setupWizard.testFailedHint') }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 旋转动画 */
|
||||
.rotating {
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
262
src/views/setup/DownloaderSettingsStep.vue
Normal file
262
src/views/setup/DownloaderSettingsStep.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { wizardData, selectDownloader, validationErrors } = useSetupWizard()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="outlined">
|
||||
<VCardText>
|
||||
<div class="text-center mb-6">
|
||||
<h3 class="text-h4 mb-2">{{ t('setupWizard.downloader.title') }}</h3>
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.downloader.description') }}</p>
|
||||
</div>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VAlert type="info" variant="tonal" class="mb-4">
|
||||
<VAlertTitle>{{ t('setupWizard.downloader.info') }}</VAlertTitle>
|
||||
{{ t('setupWizard.downloader.infoDesc') }}
|
||||
</VAlert>
|
||||
</VCol>
|
||||
|
||||
<!-- 下载器选择 -->
|
||||
<VCol cols="12">
|
||||
<div class="mb-4">
|
||||
<h4 class="text-h6 mb-4">{{ t('setupWizard.downloader.type') }}</h4>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VCard
|
||||
:color="wizardData.downloader.type === 'qbittorrent' ? 'primary' : 'default'"
|
||||
:variant="wizardData.downloader.type === 'qbittorrent' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectDownloader('qbittorrent')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg :src="getLogoUrl('qbittorrent')" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">qBittorrent</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCard
|
||||
:color="wizardData.downloader.type === 'transmission' ? 'primary' : 'default'"
|
||||
:variant="wizardData.downloader.type === 'transmission' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectDownloader('transmission')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg :src="getLogoUrl('transmission')" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">Transmission</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<!-- 下载器配置 -->
|
||||
<VCol v-if="wizardData.downloader.type" cols="12">
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow v-if="wizardData.downloader.type === 'qbittorrent'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.name"
|
||||
:label="t('downloader.name')"
|
||||
:placeholder="t('downloader.nameRequired')"
|
||||
:hint="t('downloader.name')"
|
||||
:error="validationErrors.downloader.name"
|
||||
:error-messages="validationErrors.downloader.name ? [t('downloader.nameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.config.host"
|
||||
:label="t('downloader.host')"
|
||||
placeholder="http(s)://ip:port"
|
||||
:hint="t('downloader.host')"
|
||||
:error="validationErrors.downloader.host"
|
||||
:error-messages="validationErrors.downloader.host ? [t('downloader.hostRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.config.username"
|
||||
:label="t('downloader.username')"
|
||||
:hint="t('downloader.username')"
|
||||
:error="validationErrors.downloader.username"
|
||||
:error-messages="validationErrors.downloader.username ? [t('downloader.usernameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.config.password"
|
||||
type="password"
|
||||
:label="t('downloader.password')"
|
||||
:hint="t('downloader.password')"
|
||||
:error="validationErrors.downloader.password"
|
||||
:error-messages="validationErrors.downloader.password ? [t('downloader.passwordRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="wizardData.downloader.config.sequentail"
|
||||
:label="t('downloader.sequentail')"
|
||||
:hint="t('downloader.sequentail')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="wizardData.downloader.config.force_resume"
|
||||
:label="t('downloader.force_resume')"
|
||||
:hint="t('downloader.force_resume')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="wizardData.downloader.config.first_last_piece"
|
||||
:label="t('downloader.first_last_piece')"
|
||||
:hint="t('downloader.first_last_piece')"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.downloader.type === 'transmission'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.name"
|
||||
:label="t('downloader.name')"
|
||||
:placeholder="t('downloader.nameRequired')"
|
||||
:hint="t('downloader.name')"
|
||||
:error="validationErrors.downloader.name"
|
||||
:error-messages="validationErrors.downloader.name ? [t('downloader.nameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.config.host"
|
||||
:label="t('downloader.host')"
|
||||
placeholder="http(s)://ip:port"
|
||||
:hint="t('downloader.host')"
|
||||
:error="validationErrors.downloader.host"
|
||||
:error-messages="validationErrors.downloader.host ? [t('downloader.hostRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.config.username"
|
||||
:label="t('downloader.username')"
|
||||
:hint="t('downloader.username')"
|
||||
:error="validationErrors.downloader.username"
|
||||
:error-messages="validationErrors.downloader.username ? [t('downloader.usernameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.config.password"
|
||||
type="password"
|
||||
:label="t('downloader.password')"
|
||||
:hint="t('downloader.password')"
|
||||
:error="validationErrors.downloader.password"
|
||||
:error-messages="validationErrors.downloader.password ? [t('downloader.passwordRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.type"
|
||||
:label="t('downloader.type')"
|
||||
:hint="t('downloader.customTypeHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-cog"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.downloader.name"
|
||||
:label="t('downloader.name')"
|
||||
:hint="t('downloader.nameRequired')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cursor-pointer:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 15%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.cursor-pointer:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 选中状态的样式 */
|
||||
.v-card--variant-tonal.v-theme--light {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.12);
|
||||
}
|
||||
|
||||
.v-card--variant-tonal.v-theme--dark {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.2);
|
||||
}
|
||||
</style>
|
||||
507
src/views/setup/MediaServerSettingsStep.vue
Normal file
507
src/views/setup/MediaServerSettingsStep.vue
Normal file
@@ -0,0 +1,507 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
import api from '@/api'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { wizardData, selectMediaServer, validationErrors } = useSetupWizard()
|
||||
|
||||
// 同步媒体库选项
|
||||
const librariesOptions = ref<{ title: string; value: string | undefined }[]>([
|
||||
{
|
||||
title: t('common.all'),
|
||||
value: 'all',
|
||||
},
|
||||
])
|
||||
|
||||
// 调用API查询媒体库
|
||||
async function loadLibrary(server: string) {
|
||||
try {
|
||||
console.log('Loading library for server:', server)
|
||||
const result: any[] = await api.get('mediaserver/library', { params: { server } })
|
||||
if (result && result.length > 0) {
|
||||
librariesOptions.value = result.map(item => ({
|
||||
title: item.name,
|
||||
value: item.id?.toString(),
|
||||
}))
|
||||
console.log('Loaded libraries:', librariesOptions.value)
|
||||
} else {
|
||||
librariesOptions.value = []
|
||||
console.log('No libraries found')
|
||||
}
|
||||
librariesOptions.value.unshift({
|
||||
title: t('common.all'),
|
||||
value: 'all',
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('Error loading library:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择媒体服务器并自动加载媒体库
|
||||
async function selectMediaServerWithLibrary(type: string) {
|
||||
selectMediaServer(type)
|
||||
// 如果选择了媒体服务器类型,自动加载媒体库
|
||||
if (type && wizardData.value.mediaServer.name) {
|
||||
await loadLibrary(wizardData.value.mediaServer.name)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时检查是否需要加载媒体库
|
||||
onMounted(async () => {
|
||||
// 如果已经有媒体服务器配置,自动加载媒体库
|
||||
if (wizardData.value.mediaServer.type && wizardData.value.mediaServer.name) {
|
||||
await loadLibrary(wizardData.value.mediaServer.name)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听媒体服务器配置变化,自动加载媒体库
|
||||
watch(
|
||||
() => [wizardData.value.mediaServer.type, wizardData.value.mediaServer.name],
|
||||
async ([type, name]) => {
|
||||
console.log('Media server changed:', { type, name })
|
||||
if (type && name) {
|
||||
await loadLibrary(name)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="outlined">
|
||||
<VCardText>
|
||||
<div class="text-center mb-6">
|
||||
<h3 class="text-h4 mb-2">{{ t('setupWizard.mediaServer.title') }}</h3>
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.mediaServer.description') }}</p>
|
||||
</div>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VAlert type="info" variant="tonal" class="mb-4">
|
||||
<VAlertTitle>{{ t('setupWizard.mediaServer.info') }}</VAlertTitle>
|
||||
{{ t('setupWizard.mediaServer.infoDesc') }}
|
||||
</VAlert>
|
||||
</VCol>
|
||||
|
||||
<!-- 媒体服务器选择 -->
|
||||
<VCol cols="12">
|
||||
<div class="mb-4">
|
||||
<h4 class="text-h6 mb-4">{{ t('setupWizard.mediaServer.type') }}</h4>
|
||||
<VRow>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.mediaServer.type === 'emby' ? 'primary' : 'default'"
|
||||
:variant="wizardData.mediaServer.type === 'emby' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectMediaServerWithLibrary('emby')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg :src="getLogoUrl('emby')" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">Emby</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.mediaServer.type === 'jellyfin' ? 'primary' : 'default'"
|
||||
:variant="wizardData.mediaServer.type === 'jellyfin' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectMediaServerWithLibrary('jellyfin')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg :src="getLogoUrl('jellyfin')" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">Jellyfin</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.mediaServer.type === 'plex' ? 'primary' : 'default'"
|
||||
:variant="wizardData.mediaServer.type === 'plex' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectMediaServerWithLibrary('plex')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg :src="getLogoUrl('plex')" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">Plex</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.mediaServer.type === 'trimemedia' ? 'primary' : 'default'"
|
||||
:variant="wizardData.mediaServer.type === 'trimemedia' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectMediaServerWithLibrary('trimemedia')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg :src="getLogoUrl('trimemedia')" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">飞牛影视</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<!-- 媒体服务器配置 -->
|
||||
<VCol v-if="wizardData.mediaServer.type" cols="12">
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow v-if="wizardData.mediaServer.type === 'emby'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.name"
|
||||
:label="t('common.name')"
|
||||
:placeholder="t('mediaserver.nameRequired')"
|
||||
:hint="t('mediaserver.serverAlias')"
|
||||
:error="validationErrors.mediaServer.name"
|
||||
:error-messages="validationErrors.mediaServer.name ? [t('mediaserver.nameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.host"
|
||||
:label="t('mediaserver.host')"
|
||||
:placeholder="t('mediaserver.hostPlaceholder')"
|
||||
:hint="t('mediaserver.hostHint')"
|
||||
:error="validationErrors.mediaServer.host"
|
||||
:error-messages="validationErrors.mediaServer.host ? [t('mediaserver.hostRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.play_host"
|
||||
:label="t('mediaserver.playHost')"
|
||||
:placeholder="t('mediaserver.playHostPlaceholder')"
|
||||
:hint="t('mediaserver.playHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-play-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.username"
|
||||
:label="t('mediaserver.username')"
|
||||
:hint="t('mediaserver.usernameHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.apikey"
|
||||
:label="t('mediaserver.apiKey')"
|
||||
:hint="t('mediaserver.embyApiKeyHint')"
|
||||
:error="validationErrors.mediaServer.apikey"
|
||||
:error-messages="validationErrors.mediaServer.apikey ? [t('mediaserver.apiKeyRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-key"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VAutocomplete
|
||||
v-model="wizardData.mediaServer.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
chips
|
||||
multiple
|
||||
clearable
|
||||
:hint="t('mediaserver.syncLibrariesHint')"
|
||||
persistent-hint
|
||||
active
|
||||
append-inner-icon="mdi-refresh"
|
||||
prepend-inner-icon="mdi-library"
|
||||
@click:append-inner="loadLibrary(wizardData.mediaServer.name)"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.mediaServer.type === 'jellyfin'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.name"
|
||||
:label="t('common.name')"
|
||||
:placeholder="t('mediaserver.nameRequired')"
|
||||
:hint="t('mediaserver.serverAlias')"
|
||||
:error="validationErrors.mediaServer.name"
|
||||
:error-messages="validationErrors.mediaServer.name ? [t('mediaserver.nameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.host"
|
||||
:label="t('mediaserver.host')"
|
||||
:placeholder="t('mediaserver.hostPlaceholder')"
|
||||
:hint="t('mediaserver.hostHint')"
|
||||
:error="validationErrors.mediaServer.host"
|
||||
:error-messages="validationErrors.mediaServer.host ? [t('mediaserver.hostRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.play_host"
|
||||
:label="t('mediaserver.playHost')"
|
||||
:placeholder="t('mediaserver.playHostPlaceholder')"
|
||||
:hint="t('mediaserver.playHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-play-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.apikey"
|
||||
:label="t('mediaserver.apiKey')"
|
||||
:hint="t('mediaserver.jellyfinApiKeyHint')"
|
||||
:error="validationErrors.mediaServer.apikey"
|
||||
:error-messages="validationErrors.mediaServer.apikey ? [t('mediaserver.apiKeyRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-key"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VAutocomplete
|
||||
v-model="wizardData.mediaServer.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
chips
|
||||
multiple
|
||||
clearable
|
||||
:hint="t('mediaserver.syncLibrariesHint')"
|
||||
persistent-hint
|
||||
active
|
||||
append-inner-icon="mdi-refresh"
|
||||
prepend-inner-icon="mdi-library"
|
||||
@click:append-inner="loadLibrary(wizardData.mediaServer.name)"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.mediaServer.type === 'trimemedia'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.name"
|
||||
:label="t('common.name')"
|
||||
:placeholder="t('mediaserver.nameRequired')"
|
||||
:hint="t('mediaserver.serverAlias')"
|
||||
:error="validationErrors.mediaServer.name"
|
||||
:error-messages="validationErrors.mediaServer.name ? [t('mediaserver.nameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.host"
|
||||
:label="t('mediaserver.host')"
|
||||
:placeholder="t('mediaserver.hostPlaceholder')"
|
||||
:hint="t('mediaserver.hostHint')"
|
||||
:error="validationErrors.mediaServer.host"
|
||||
:error-messages="validationErrors.mediaServer.host ? [t('mediaserver.hostRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.play_host"
|
||||
:label="t('mediaserver.playHost')"
|
||||
:placeholder="t('mediaserver.playHostPlaceholder')"
|
||||
:hint="t('mediaserver.playHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-play-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.username"
|
||||
:label="t('mediaserver.username')"
|
||||
:error="validationErrors.mediaServer.username"
|
||||
:error-messages="validationErrors.mediaServer.username ? [t('mediaserver.usernameRequired')] : []"
|
||||
active
|
||||
prepend-inner-icon="mdi-account"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
type="password"
|
||||
v-model="wizardData.mediaServer.config.password"
|
||||
:label="t('mediaserver.password')"
|
||||
:error="validationErrors.mediaServer.password"
|
||||
:error-messages="validationErrors.mediaServer.password ? [t('mediaserver.passwordRequired')] : []"
|
||||
active
|
||||
prepend-inner-icon="mdi-lock"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VAutocomplete
|
||||
v-model="wizardData.mediaServer.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
chips
|
||||
multiple
|
||||
clearable
|
||||
:hint="t('mediaserver.syncLibrariesHint')"
|
||||
persistent-hint
|
||||
active
|
||||
append-inner-icon="mdi-refresh"
|
||||
prepend-inner-icon="mdi-library"
|
||||
@click:append-inner="loadLibrary(wizardData.mediaServer.name)"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.mediaServer.type === 'plex'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.name"
|
||||
:label="t('common.name')"
|
||||
:placeholder="t('mediaserver.nameRequired')"
|
||||
:hint="t('mediaserver.serverAlias')"
|
||||
:error="validationErrors.mediaServer.name"
|
||||
:error-messages="validationErrors.mediaServer.name ? [t('mediaserver.nameRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.host"
|
||||
:label="t('mediaserver.host')"
|
||||
:placeholder="t('mediaserver.hostPlaceholder')"
|
||||
:hint="t('mediaserver.hostHint')"
|
||||
:error="validationErrors.mediaServer.host"
|
||||
:error-messages="validationErrors.mediaServer.host ? [t('mediaserver.hostRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.play_host"
|
||||
:label="t('mediaserver.playHost')"
|
||||
:placeholder="t('mediaserver.playHostPlaceholder')"
|
||||
:hint="t('mediaserver.playHostHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-play-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.config.token"
|
||||
:label="t('mediaserver.plexToken')"
|
||||
:hint="t('mediaserver.plexTokenHint')"
|
||||
:error="validationErrors.mediaServer.token"
|
||||
:error-messages="validationErrors.mediaServer.token ? [t('mediaserver.tokenRequired')] : []"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-key"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VAutocomplete
|
||||
v-model="wizardData.mediaServer.sync_libraries"
|
||||
:label="t('mediaserver.syncLibraries')"
|
||||
:items="librariesOptions"
|
||||
chips
|
||||
multiple
|
||||
clearable
|
||||
:hint="t('mediaserver.syncLibrariesHint')"
|
||||
persistent-hint
|
||||
active
|
||||
append-inner-icon="mdi-refresh"
|
||||
prepend-inner-icon="mdi-library"
|
||||
@click:append-inner="loadLibrary(wizardData.mediaServer.name)"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.type"
|
||||
:label="t('mediaserver.type')"
|
||||
:hint="t('mediaserver.customTypeHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-cog"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.mediaServer.name"
|
||||
:label="t('common.name')"
|
||||
:hint="t('mediaserver.nameRequired')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cursor-pointer:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 15%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.cursor-pointer:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 选中状态的样式 */
|
||||
.v-card--variant-tonal.v-theme--light {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.12);
|
||||
}
|
||||
|
||||
.v-card--variant-tonal.v-theme--dark {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.2);
|
||||
}
|
||||
</style>
|
||||
553
src/views/setup/NotificationSettingsStep.vue
Normal file
553
src/views/setup/NotificationSettingsStep.vue
Normal file
@@ -0,0 +1,553 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { wizardData, selectNotification, validationErrors } = useSetupWizard()
|
||||
|
||||
// 消息类型下拉字典
|
||||
const notificationTypes = [
|
||||
{ value: '资源下载', title: t('notificationSwitch.resourceDownload') },
|
||||
{ value: '整理入库', title: t('notificationSwitch.organize') },
|
||||
{ value: '订阅', title: t('notificationSwitch.subscribe') },
|
||||
{ value: '站点', title: t('notificationSwitch.site') },
|
||||
{ value: '媒体服务器', title: t('notificationSwitch.mediaServer') },
|
||||
{ value: '手动处理', title: t('notificationSwitch.manual') },
|
||||
{ value: '插件', title: t('notificationSwitch.plugin') },
|
||||
{ value: '其它', title: t('notificationSwitch.other') },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="outlined">
|
||||
<VCardText>
|
||||
<div class="text-center mb-6">
|
||||
<h3 class="text-h4 mb-2">{{ t('setupWizard.notification.title') }}</h3>
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.notification.description') }}</p>
|
||||
</div>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VAlert type="info" variant="tonal" class="mb-4">
|
||||
<VAlertTitle>{{ t('setupWizard.notification.info') }}</VAlertTitle>
|
||||
{{ t('setupWizard.notification.infoDesc') }}
|
||||
</VAlert>
|
||||
</VCol>
|
||||
|
||||
<!-- 通知选择 -->
|
||||
<VCol cols="12">
|
||||
<div class="mb-4">
|
||||
<h4 class="text-h6 mb-4">{{ t('setupWizard.notification.type') }}</h4>
|
||||
<VRow>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.notification.type === 'wechat' ? 'primary' : 'default'"
|
||||
:variant="wizardData.notification.type === 'wechat' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectNotification('wechat')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg :src="getLogoUrl('wechat')" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">微信</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.notification.type === 'telegram' ? 'primary' : 'default'"
|
||||
:variant="wizardData.notification.type === 'telegram' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectNotification('telegram')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg :src="getLogoUrl('telegram')" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">Telegram</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.notification.type === 'slack' ? 'primary' : 'default'"
|
||||
:variant="wizardData.notification.type === 'slack' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectNotification('slack')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg :src="getLogoUrl('slack')" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">Slack</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.notification.type === 'synologychat' ? 'primary' : 'default'"
|
||||
:variant="wizardData.notification.type === 'synologychat' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectNotification('synologychat')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg :src="getLogoUrl('synologychat')" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">Synology Chat</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.notification.type === 'vocechat' ? 'primary' : 'default'"
|
||||
:variant="wizardData.notification.type === 'vocechat' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectNotification('vocechat')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VImg :src="getLogoUrl('vocechat')" height="48" width="48" class="mx-auto mb-2" />
|
||||
<div class="text-h6">VoceChat</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCard
|
||||
:color="wizardData.notification.type === 'webpush' ? 'primary' : 'default'"
|
||||
:variant="wizardData.notification.type === 'webpush' ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer"
|
||||
@click="selectNotification('webpush')"
|
||||
>
|
||||
<VCardText class="text-center">
|
||||
<VIcon icon="mdi-apple-safari" size="48" class="mb-2" />
|
||||
<div class="text-h6">WebPush</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<!-- 通知配置 -->
|
||||
<VCol v-if="wizardData.notification.type" cols="12">
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VAutocomplete
|
||||
v-model="wizardData.notification.switchs"
|
||||
:items="notificationTypes"
|
||||
:label="t('notification.type')"
|
||||
:hint="t('notification.typeHint')"
|
||||
multiple
|
||||
clearable
|
||||
chips
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-bell-outline"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-if="wizardData.notification.type === 'wechat'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.name"
|
||||
:label="t('notification.name')"
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
:error="validationErrors.notification.name"
|
||||
:error-messages="validationErrors.notification.name ? [t('notification.nameRequired')] : []"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WECHAT_CORPID"
|
||||
:label="t('notification.wechat.corpId')"
|
||||
:hint="t('notification.wechat.corpIdHint')"
|
||||
:error="validationErrors.notification.WECHAT_CORPID"
|
||||
:error-messages="
|
||||
validationErrors.notification.WECHAT_CORPID ? [t('notification.wechat.corpIdRequired')] : []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-domain"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WECHAT_APP_ID"
|
||||
:label="t('notification.wechat.appId')"
|
||||
:hint="t('notification.wechat.appIdHint')"
|
||||
:error="validationErrors.notification.WECHAT_APP_ID"
|
||||
:error-messages="
|
||||
validationErrors.notification.WECHAT_APP_ID ? [t('notification.wechat.appIdRequired')] : []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-application"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WECHAT_APP_SECRET"
|
||||
:label="t('notification.wechat.appSecret')"
|
||||
:hint="t('notification.wechat.appSecretHint')"
|
||||
:error="validationErrors.notification.WECHAT_APP_SECRET"
|
||||
:error-messages="
|
||||
validationErrors.notification.WECHAT_APP_SECRET
|
||||
? [t('notification.wechat.appSecretRequired')]
|
||||
: []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WECHAT_PROXY"
|
||||
:label="t('notification.wechat.proxy')"
|
||||
:hint="t('notification.wechat.proxyHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-server-network"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WECHAT_TOKEN"
|
||||
:label="t('notification.wechat.token')"
|
||||
:hint="t('notification.wechat.tokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key-variant"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WECHAT_ENCODING_AESKEY"
|
||||
:label="t('notification.wechat.encodingAesKey')"
|
||||
:hint="t('notification.wechat.encodingAesKeyHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-lock"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WECHAT_ADMINS"
|
||||
:label="t('notification.wechat.admins')"
|
||||
:placeholder="t('notification.wechat.adminsPlaceholder')"
|
||||
:hint="t('notification.wechat.adminsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-supervisor"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.notification.type === 'telegram'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.name"
|
||||
:label="t('notification.name')"
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
:error="validationErrors.notification.name"
|
||||
:error-messages="validationErrors.notification.name ? [t('notification.nameRequired')] : []"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.TELEGRAM_TOKEN"
|
||||
:label="t('notification.telegram.token')"
|
||||
:hint="t('notification.telegram.tokenHint')"
|
||||
:error="validationErrors.notification.TELEGRAM_TOKEN"
|
||||
:error-messages="
|
||||
validationErrors.notification.TELEGRAM_TOKEN ? [t('notification.telegram.tokenRequired')] : []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.TELEGRAM_CHAT_ID"
|
||||
:label="t('notification.telegram.chatId')"
|
||||
:hint="t('notification.telegram.chatIdHint')"
|
||||
:error="validationErrors.notification.TELEGRAM_CHAT_ID"
|
||||
:error-messages="
|
||||
validationErrors.notification.TELEGRAM_CHAT_ID
|
||||
? [t('notification.telegram.chatIdRequired')]
|
||||
: []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-chat"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.TELEGRAM_USERS"
|
||||
:label="t('notification.telegram.users')"
|
||||
:placeholder="t('notification.telegram.usersPlaceholder')"
|
||||
:hint="t('notification.telegram.usersHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-group"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.TELEGRAM_ADMINS"
|
||||
:label="t('notification.telegram.admins')"
|
||||
:placeholder="t('notification.telegram.adminsPlaceholder')"
|
||||
:hint="t('notification.telegram.adminsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-supervisor"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.API_URL"
|
||||
:label="t('notification.telegram.apiUrl')"
|
||||
:placeholder="t('notification.telegram.apiUrlPlaceholder')"
|
||||
:hint="t('notification.telegram.apiUrlHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-web"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.notification.type === 'slack'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.name"
|
||||
:label="t('notification.name')"
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
:error="validationErrors.notification.name"
|
||||
:error-messages="validationErrors.notification.name ? [t('notification.nameRequired')] : []"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.SLACK_OAUTH_TOKEN"
|
||||
:label="t('notification.slack.oauthToken')"
|
||||
:placeholder="t('notification.slack.oauthTokenPlaceholder')"
|
||||
:hint="t('notification.slack.oauthTokenHint')"
|
||||
:error="validationErrors.notification.SLACK_OAUTH_TOKEN"
|
||||
:error-messages="
|
||||
validationErrors.notification.SLACK_OAUTH_TOKEN
|
||||
? [t('notification.slack.oauthTokenRequired')]
|
||||
: []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.SLACK_APP_TOKEN"
|
||||
:label="t('notification.slack.appToken')"
|
||||
:placeholder="t('notification.slack.appTokenPlaceholder')"
|
||||
:hint="t('notification.slack.appTokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-application"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.SLACK_CHANNEL"
|
||||
:label="t('notification.slack.channel')"
|
||||
:placeholder="t('notification.slack.channelPlaceholder')"
|
||||
:hint="t('notification.slack.channelHint')"
|
||||
:error="validationErrors.notification.SLACK_CHANNEL"
|
||||
:error-messages="
|
||||
validationErrors.notification.SLACK_CHANNEL ? [t('notification.slack.channelRequired')] : []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-pound"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.notification.type === 'synologychat'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.name"
|
||||
:label="t('notification.name')"
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
:error="validationErrors.notification.name"
|
||||
:error-messages="validationErrors.notification.name ? [t('notification.nameRequired')] : []"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.SYNOLOGYCHAT_WEBHOOK"
|
||||
:label="t('notification.synologychat.webhook')"
|
||||
:hint="t('notification.synologychat.webhookHint')"
|
||||
:error="validationErrors.notification.SYNOLOGYCHAT_WEBHOOK"
|
||||
:error-messages="
|
||||
validationErrors.notification.SYNOLOGYCHAT_WEBHOOK
|
||||
? [t('notification.synologychat.webhookRequired')]
|
||||
: []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-webhook"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.SYNOLOGYCHAT_TOKEN"
|
||||
:label="t('notification.synologychat.token')"
|
||||
:hint="t('notification.synologychat.tokenHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.notification.type === 'vocechat'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.name"
|
||||
:label="t('notification.name')"
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
:error="validationErrors.notification.name"
|
||||
:error-messages="validationErrors.notification.name ? [t('notification.nameRequired')] : []"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.VOCECHAT_HOST"
|
||||
:label="t('notification.vocechat.host')"
|
||||
:hint="t('notification.vocechat.hostHint')"
|
||||
:error="validationErrors.notification.VOCECHAT_HOST"
|
||||
:error-messages="
|
||||
validationErrors.notification.VOCECHAT_HOST ? [t('notification.vocechat.hostRequired')] : []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-server"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.VOCECHAT_API_KEY"
|
||||
:label="t('notification.vocechat.apiKey')"
|
||||
:hint="t('notification.vocechat.apiKeyHint')"
|
||||
:error="validationErrors.notification.VOCECHAT_API_KEY"
|
||||
:error-messages="
|
||||
validationErrors.notification.VOCECHAT_API_KEY
|
||||
? [t('notification.vocechat.apiKeyRequired')]
|
||||
: []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-key"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.VOCECHAT_CHANNEL_ID"
|
||||
:label="t('notification.vocechat.channelId')"
|
||||
:placeholder="t('notification.vocechat.channelIdPlaceholder')"
|
||||
:hint="t('notification.vocechat.channelIdHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-pound"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="wizardData.notification.type === 'webpush'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.name"
|
||||
:label="t('notification.name')"
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
:error="validationErrors.notification.name"
|
||||
:error-messages="validationErrors.notification.name ? [t('notification.nameRequired')] : []"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.config.WEBPUSH_USERNAME"
|
||||
:label="t('notification.webpush.username')"
|
||||
:hint="t('notification.webpush.usernameHint')"
|
||||
:error="validationErrors.notification.WEBPUSH_USERNAME"
|
||||
:error-messages="
|
||||
validationErrors.notification.WEBPUSH_USERNAME
|
||||
? [t('notification.webpush.usernameRequired')]
|
||||
: []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.type"
|
||||
:label="t('notification.type')"
|
||||
:hint="t('notification.customTypeHint')"
|
||||
persistent-hint
|
||||
active
|
||||
prepend-inner-icon="mdi-cog"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="wizardData.notification.name"
|
||||
:label="t('notification.name')"
|
||||
:hint="t('notification.nameRequired')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cursor-pointer:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 15%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.cursor-pointer:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 选中状态的样式 */
|
||||
.v-card--variant-tonal.v-theme--light {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.12);
|
||||
}
|
||||
|
||||
.v-card--variant-tonal.v-theme--dark {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.2);
|
||||
}
|
||||
</style>
|
||||
279
src/views/setup/PreferencesSettingsStep.vue
Normal file
279
src/views/setup/PreferencesSettingsStep.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
import api from '@/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { updatePreferences } = useSetupWizard()
|
||||
|
||||
// 个性化选项
|
||||
const personalizationOptions = ref({
|
||||
excludeDolbyVision: true, // 排除杜比视界
|
||||
excludeBluray: true, // 排除蓝光原盘
|
||||
})
|
||||
|
||||
// 预设配置 - 使用多语言
|
||||
const presetConfigs = computed(() => ({
|
||||
'4k-enthusiast': {
|
||||
name: t('setupWizard.preferences.presets.4k-enthusiast.name'),
|
||||
description: t('setupWizard.preferences.presets.4k-enthusiast.description'),
|
||||
icon: 'mdi-4k',
|
||||
color: 'primary',
|
||||
ruleString:
|
||||
' SPECSUB & 4K & 60FPS & UHD & !BLU & !DOLBY > CNSUB & 4K & 60FPS & UHD & !BLU & !DOLBY > 4K & 60FPS & UHD & !BLU & !DOLBY > SPECSUB & 4K & UHD & !BLU & !DOLBY > CNSUB & 4K & UHD & !BLU & !DOLBY > 4K & UHD & !BLU & !DOLBY > SPECSUB & 4K & !BLU & !DOLBY > CNSUB & 4K & !BLU & !DOLBY > 4K & !BLU & !DOLBY ',
|
||||
},
|
||||
'balanced': {
|
||||
name: t('setupWizard.preferences.presets.balanced.name'),
|
||||
description: t('setupWizard.preferences.presets.balanced.description'),
|
||||
icon: 'mdi-scale-unbalanced',
|
||||
color: 'success',
|
||||
ruleString:
|
||||
' SPECSUB & 4K & !BLU & !DOLBY & !UHD & !60FPS > CNSUB & 4K & !BLU & !DOLBY & !REMUX & !60FPS > SPECSUB & 1080P & !BLU & !DOLBY & !60FPS & !UHD > CNSUB & 1080P & !BLU & !DOLBY & !UHD & !60FPS > 4K & BLU & !DOLBY & !UHD & !60FPS > 1080P & !BLU & !DOLBY & !UHD & !60FPS ',
|
||||
},
|
||||
'space-saver': {
|
||||
name: t('setupWizard.preferences.presets.space-saver.name'),
|
||||
description: t('setupWizard.preferences.presets.space-saver.description'),
|
||||
icon: 'mdi-harddisk',
|
||||
color: 'warning',
|
||||
ruleString:
|
||||
' SPECSUB & 1080P & !BLU & !UHD & !60FPS & !DOLBY > CNSUB & 1080P & !BLU & !UHD & !60FPS & !DOLBY > 1080P & !BLU & !UHD & !60FPS & !DOLBY > !BLU & !UHD & !60FPS & !DOLBY ',
|
||||
},
|
||||
'free-priority': {
|
||||
name: t('setupWizard.preferences.presets.free-priority.name'),
|
||||
description: t('setupWizard.preferences.presets.free-priority.description'),
|
||||
icon: 'mdi-gift',
|
||||
color: 'info',
|
||||
ruleString:
|
||||
' SPECSUB & FREE & !BLU & !DOLBY > CNSUB & FREE & !BLU & !DOLBY > FREE & !BLU & !DOLBY > !BLU & !DOLBY ',
|
||||
},
|
||||
}))
|
||||
|
||||
// 当前选中的预设
|
||||
const selectedPreset = ref('')
|
||||
|
||||
// 加载用户当前的规则组设置
|
||||
async function loadUserFilterRuleGroups() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/UserFilterRuleGroups')
|
||||
if (result.success && result.data?.value && result.data.value.length > 0) {
|
||||
const userRuleGroups = result.data.value
|
||||
|
||||
// 查找匹配的预设
|
||||
for (const [presetKey, preset] of Object.entries(presetConfigs.value)) {
|
||||
const matchingRule = userRuleGroups.find((rule: any) => rule.name === preset.name)
|
||||
if (matchingRule) {
|
||||
selectedPreset.value = presetKey
|
||||
|
||||
// 分析规则字符串,判断个性化选项
|
||||
const ruleString = matchingRule.rule_string || ''
|
||||
personalizationOptions.value.excludeDolbyVision = ruleString.includes('!DOLBY')
|
||||
personalizationOptions.value.excludeBluray = ruleString.includes('!BLU')
|
||||
|
||||
// 更新向导数据
|
||||
updateWizardData()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Load user filter rule groups failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择预设
|
||||
function selectPreset(presetKey: string) {
|
||||
if (selectedPreset.value === presetKey) {
|
||||
// 如果再次点击同一个预设,则取消选择
|
||||
selectedPreset.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
selectedPreset.value = presetKey
|
||||
updateWizardData()
|
||||
}
|
||||
|
||||
// 生成规则序列的逻辑
|
||||
const generateRuleSequences = computed(() => {
|
||||
if (!selectedPreset.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
const preset = presetConfigs.value[selectedPreset.value as keyof typeof presetConfigs.value]
|
||||
if (!preset) {
|
||||
return []
|
||||
}
|
||||
|
||||
let ruleString = preset.ruleString
|
||||
|
||||
// 根据个性化选项调整规则
|
||||
if (!personalizationOptions.value.excludeDolbyVision) {
|
||||
// 移除所有 !DOLBY 条件
|
||||
ruleString = ruleString.replace(/ & !DOLBY/g, '').replace(/!DOLBY & /g, '')
|
||||
}
|
||||
|
||||
if (!personalizationOptions.value.excludeBluray) {
|
||||
// 移除所有 !BLU 条件
|
||||
ruleString = ruleString.replace(/ & !BLU/g, '').replace(/!BLU & /g, '')
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
name: preset.name,
|
||||
rule_string: ruleString,
|
||||
media_type: '',
|
||||
category: '',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// 监听偏好变化,更新到wizardData
|
||||
function updateWizardData() {
|
||||
if (updatePreferences) {
|
||||
updatePreferences(personalizationOptions.value, generateRuleSequences.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载用户设置
|
||||
onMounted(() => {
|
||||
loadUserFilterRuleGroups()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="outlined">
|
||||
<VCardText>
|
||||
<div class="text-center mb-6">
|
||||
<h3 class="text-h4 mb-2">{{ t('setupWizard.preferences.title') }}</h3>
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.preferences.description') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 快速预设 -->
|
||||
<VCard class="mb-6">
|
||||
<VCardTitle class="text-h6 d-flex align-center">
|
||||
<VIcon icon="mdi-flash" class="me-2" />
|
||||
{{ t('setupWizard.preferences.quickPresets') }}
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">{{ t('setupWizard.preferences.quickPresetsDesc') }}</p>
|
||||
<VRow>
|
||||
<VCol v-for="(preset, key) in presetConfigs" :key="key" cols="12" sm="6" md="3">
|
||||
<VCard
|
||||
:color="selectedPreset === key ? preset.color : 'default'"
|
||||
:variant="selectedPreset === key ? 'tonal' : 'outlined'"
|
||||
class="cursor-pointer preset-card"
|
||||
@click="selectPreset(key)"
|
||||
>
|
||||
<VCardText class="text-center pa-4">
|
||||
<VIcon :icon="preset.icon" size="40" class="mb-3" />
|
||||
<div class="text-h6 mb-2">{{ preset.name }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">{{ preset.description }}</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 个性化选项 -->
|
||||
<VCard class="mb-6">
|
||||
<VCardTitle class="text-h6 d-flex align-center">
|
||||
<VIcon icon="mdi-cog" class="me-2" />
|
||||
{{ t('setupWizard.preferences.personalizationOptions') }}
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
{{ t('setupWizard.preferences.personalizationOptionsDesc') }}
|
||||
</p>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="personalizationOptions.excludeDolbyVision"
|
||||
:label="t('setupWizard.preferences.excludeDolbyVision')"
|
||||
color="primary"
|
||||
hide-details
|
||||
@change="updateWizardData"
|
||||
/>
|
||||
<p class="text-caption text-medium-emphasis mt-1">
|
||||
{{ t('setupWizard.preferences.excludeDolbyVisionHint') }}
|
||||
</p>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="personalizationOptions.excludeBluray"
|
||||
:label="t('setupWizard.preferences.excludeBluray')"
|
||||
color="primary"
|
||||
hide-details
|
||||
@change="updateWizardData"
|
||||
/>
|
||||
<p class="text-caption text-medium-emphasis mt-1">{{ t('setupWizard.preferences.excludeBlurayHint') }}</p>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.preset-card:hover {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 15%);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.preset-card:active {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 预设卡片选中状态的样式 */
|
||||
.v-card--variant-tonal.v-theme--light {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.12);
|
||||
}
|
||||
|
||||
.v-card--variant-tonal.v-theme--dark {
|
||||
border: 2px solid rgb(var(--v-theme-primary));
|
||||
background-color: rgb(var(--v-theme-primary), 0.2);
|
||||
}
|
||||
|
||||
/* 规则代码样式 */
|
||||
.v-code {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(var(--v-theme-surface-variant), 0.3);
|
||||
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 展开面板样式 */
|
||||
.v-expansion-panel-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.v-expansion-panel-text {
|
||||
padding-block-start: 16px;
|
||||
}
|
||||
|
||||
/* 开关组件样式优化 */
|
||||
.v-switch {
|
||||
margin-block-end: 8px;
|
||||
}
|
||||
|
||||
/* 芯片组样式 */
|
||||
.v-chip-group {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.v-chip {
|
||||
margin-block: 4px;
|
||||
margin-inline: 0;
|
||||
}
|
||||
</style>
|
||||
94
src/views/setup/StorageSettingsStep.vue
Normal file
94
src/views/setup/StorageSettingsStep.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSetupWizard } from '@/composables/useSetupWizard'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { wizardData, validateCurrentStep } = useSetupWizard()
|
||||
|
||||
// 验证状态
|
||||
const validation = computed(() => validateCurrentStep())
|
||||
const hasErrors = computed(() => !validation.value.isValid)
|
||||
|
||||
// 整理方式选项
|
||||
const transferTypeItems = [
|
||||
{ title: '硬链接', value: 'link' },
|
||||
{ title: '软链接', value: 'softlink' },
|
||||
{ title: '复制', value: 'copy' },
|
||||
{ title: '移动', value: 'move' },
|
||||
]
|
||||
|
||||
// 覆盖模式选项
|
||||
const overwriteModeItems = [
|
||||
{ title: '从不覆盖', value: 'never' },
|
||||
{ title: '总是覆盖', value: 'always' },
|
||||
{ title: '按文件大小', value: 'size' },
|
||||
{ title: '仅保留最新', value: 'latest' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="outlined">
|
||||
<VCardText>
|
||||
<div class="text-center mb-6">
|
||||
<h3 class="text-h4 mb-2">{{ t('setupWizard.storage.title') }}</h3>
|
||||
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.storage.description') }}</p>
|
||||
</div>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VAlert type="info" variant="tonal" class="mb-4">
|
||||
<VAlertTitle>{{ t('setupWizard.storage.info') }}</VAlertTitle>
|
||||
{{ t('setupWizard.storage.infoDesc') }}
|
||||
</VAlert>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VPathField
|
||||
v-model="wizardData.storage.downloadPath"
|
||||
:label="t('setupWizard.storage.downloadPath')"
|
||||
:hint="t('setupWizard.storage.downloadPathHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-download"
|
||||
placeholder="/downloads"
|
||||
:error="!wizardData.storage.downloadPath && hasErrors"
|
||||
:error-messages="
|
||||
!wizardData.storage.downloadPath && hasErrors ? [t('setupWizard.storage.downloadPathRequired')] : []
|
||||
"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VPathField
|
||||
v-model="wizardData.storage.libraryPath"
|
||||
:label="t('setupWizard.storage.libraryPath')"
|
||||
:hint="t('setupWizard.storage.libraryPathHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-folder-multiple"
|
||||
placeholder="/media"
|
||||
:error="!wizardData.storage.libraryPath && hasErrors"
|
||||
:error-messages="
|
||||
!wizardData.storage.libraryPath && hasErrors ? [t('setupWizard.storage.libraryPathRequired')] : []
|
||||
"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="wizardData.storage.transferType"
|
||||
:label="t('directory.transferType')"
|
||||
:hint="t('directory.transferTypeHint')"
|
||||
persistent-hint
|
||||
:items="transferTypeItems"
|
||||
prepend-inner-icon="mdi-swap-horizontal"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="wizardData.storage.overwriteMode"
|
||||
:label="t('directory.overwriteMode')"
|
||||
:hint="t('directory.overwriteModeHint')"
|
||||
persistent-hint
|
||||
:items="overwriteModeItems"
|
||||
prepend-inner-icon="mdi-file-replace"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -120,8 +120,9 @@ async function getSubscribes() {
|
||||
loading.value = true
|
||||
const subscribes: Subscribe[] = await api.get('subscribe/')
|
||||
loading.value = false
|
||||
const subEvents = await Promise.all(subscribes.map(async sub => eventsHander(sub)))
|
||||
calendarOptions.value.events = subEvents.flat().filter(event => event.start) as EventSourceInput
|
||||
const subEvents = await Promise.allSettled(subscribes.map(async sub => eventsHander(sub)))
|
||||
const succEvents = subEvents.filter(result => result.status === 'fulfilled').map(result => result.value)
|
||||
calendarOptions.value.events = succEvents.flat().filter(event => event.start) as EventSourceInput
|
||||
isLoaded.value = true
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
@@ -34,13 +34,104 @@ const isRefreshed = ref(false)
|
||||
const dataList = ref<MediaInfo[]>([])
|
||||
const currData = ref<MediaInfo[]>([])
|
||||
|
||||
// 筛选参数
|
||||
const filterParams = reactive({
|
||||
genre_id: '', // 空字符串表示选中"全部"
|
||||
min_rating: 0,
|
||||
max_rating: 10,
|
||||
min_sub: 1,
|
||||
sort_type: 'count', // 默认按热度排序
|
||||
})
|
||||
|
||||
// 当前Key(用于重新加载数据)
|
||||
const currentKey = ref(0)
|
||||
|
||||
// TMDB电影风格字典
|
||||
const tmdbMovieGenreDict: Record<string, string> = {
|
||||
'28': t('tmdb.genreType.action'),
|
||||
'12': t('tmdb.genreType.adventure'),
|
||||
'16': t('tmdb.genreType.animation'),
|
||||
'35': t('tmdb.genreType.comedy'),
|
||||
'80': t('tmdb.genreType.crime'),
|
||||
'99': t('tmdb.genreType.documentary'),
|
||||
'18': t('tmdb.genreType.drama'),
|
||||
'10751': t('tmdb.genreType.family'),
|
||||
'14': t('tmdb.genreType.fantasy'),
|
||||
'36': t('tmdb.genreType.history'),
|
||||
'27': t('tmdb.genreType.horror'),
|
||||
'10402': t('tmdb.genreType.music'),
|
||||
'9648': t('tmdb.genreType.mystery'),
|
||||
'10749': t('tmdb.genreType.romance'),
|
||||
'878': t('tmdb.genreType.scienceFiction'),
|
||||
'10770': t('tmdb.genreType.tvMovie'),
|
||||
'53': t('tmdb.genreType.thriller'),
|
||||
'10752': t('tmdb.genreType.war'),
|
||||
'37': t('tmdb.genreType.western'),
|
||||
}
|
||||
|
||||
// TMDB电视剧风格字典
|
||||
const tmdbTvGenreDict: Record<string, string> = {
|
||||
'10759': t('tmdb.genreType.actionAdventure'),
|
||||
'16': t('tmdb.genreType.animation'),
|
||||
'35': t('tmdb.genreType.comedy'),
|
||||
'80': t('tmdb.genreType.crime'),
|
||||
'99': t('tmdb.genreType.documentary'),
|
||||
'18': t('tmdb.genreType.drama'),
|
||||
'10751': t('tmdb.genreType.family'),
|
||||
'10762': t('tmdb.genreType.kids'),
|
||||
'9648': t('tmdb.genreType.mystery'),
|
||||
'10763': t('tmdb.genreType.news'),
|
||||
'10764': t('tmdb.genreType.reality'),
|
||||
'10765': t('tmdb.genreType.sciFiFantasy'),
|
||||
'10766': t('tmdb.genreType.soap'),
|
||||
'10767': t('tmdb.genreType.talk'),
|
||||
'10768': t('tmdb.genreType.warPolitics'),
|
||||
'37': t('tmdb.genreType.western'),
|
||||
}
|
||||
|
||||
// 获取当前类型对应的风格字典
|
||||
const currentGenreDict = computed(() => {
|
||||
return props.type === '电影' ? tmdbMovieGenreDict : tmdbTvGenreDict
|
||||
})
|
||||
|
||||
// 监听筛选参数变化
|
||||
watch(
|
||||
filterParams,
|
||||
() => {
|
||||
// 重置数据
|
||||
dataList.value = []
|
||||
page.value = 1
|
||||
isRefreshed.value = false
|
||||
currentKey.value++
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// 拼装参数
|
||||
function getParams() {
|
||||
let params = {
|
||||
let params: { [key: string]: any } = {
|
||||
stype: props.type,
|
||||
page: page.value,
|
||||
count: 30,
|
||||
}
|
||||
|
||||
// 添加筛选参数
|
||||
if (filterParams.genre_id) {
|
||||
params.genre_id = parseInt(filterParams.genre_id)
|
||||
}
|
||||
if (filterParams.min_rating > 0) {
|
||||
params.min_rating = filterParams.min_rating
|
||||
}
|
||||
if (filterParams.max_rating < 10) {
|
||||
params.max_rating = filterParams.max_rating
|
||||
}
|
||||
if (filterParams.min_sub > 1) {
|
||||
params.min_sub = filterParams.min_sub
|
||||
}
|
||||
if (filterParams.sort_type) {
|
||||
params.sort_type = filterParams.sort_type
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
@@ -110,8 +201,77 @@ async function fetchData({ done }: { done: any }) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 筛选器 -->
|
||||
<div class="px-3 mb-4">
|
||||
<div class="flex justify-start align-center mb-3">
|
||||
<div class="mr-5">
|
||||
<VLabel>{{ t('tmdb.sort') }}</VLabel>
|
||||
</div>
|
||||
<VChipGroup v-model="filterParams.sort_type">
|
||||
<VChip :color="filterParams.sort_type == 'time' ? 'primary' : ''" filter tile value="time">
|
||||
{{ t('tmdb.sortType.time') }}
|
||||
</VChip>
|
||||
<VChip :color="filterParams.sort_type == 'count' ? 'primary' : ''" filter tile value="count">
|
||||
{{ t('tmdb.sortType.count') }}
|
||||
</VChip>
|
||||
<VChip :color="filterParams.sort_type == 'rating' ? 'primary' : ''" filter tile value="rating">
|
||||
{{ t('tmdb.sortType.rating') }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-start align-center mb-3">
|
||||
<div class="mr-5">
|
||||
<VLabel>{{ t('tmdb.genre') }}</VLabel>
|
||||
</div>
|
||||
<VChipGroup v-model="filterParams.genre_id">
|
||||
<VChip
|
||||
:color="filterParams.genre_id == '' ? 'primary' : ''"
|
||||
filter
|
||||
tile
|
||||
value=""
|
||||
>
|
||||
{{ t('common.all') }}
|
||||
</VChip>
|
||||
<VChip
|
||||
:color="filterParams.genre_id == key ? 'primary' : ''"
|
||||
filter
|
||||
tile
|
||||
:value="key"
|
||||
v-for="(value, key) in currentGenreDict"
|
||||
:key="key"
|
||||
>
|
||||
{{ value }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-start align-center mb-3">
|
||||
<div class="mr-5">
|
||||
<VLabel>{{ t('tmdb.rating') }}</VLabel>
|
||||
</div>
|
||||
<VSlider
|
||||
v-model="filterParams.min_rating"
|
||||
thumb-label
|
||||
max="10"
|
||||
min="0"
|
||||
:step="1"
|
||||
class="align-center"
|
||||
hide-details
|
||||
>
|
||||
</VSlider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible px-2" @load="fetchData">
|
||||
<VInfiniteScroll
|
||||
mode="intersect"
|
||||
side="end"
|
||||
:items="dataList"
|
||||
class="overflow-visible px-2"
|
||||
@load="fetchData"
|
||||
:key="currentKey"
|
||||
>
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-media-card" tabindex="0">
|
||||
|
||||
@@ -28,6 +28,66 @@ const page = ref(1)
|
||||
// 搜索关键字
|
||||
const keyword = ref(props.keyword)
|
||||
|
||||
// 筛选参数
|
||||
const filterParams = reactive({
|
||||
genre_id: '', // 空字符串表示选中"全部"
|
||||
min_rating: 0,
|
||||
max_rating: 10,
|
||||
sort_type: 'time', // 默认按时间排序
|
||||
})
|
||||
|
||||
// 当前Key(用于重新加载数据)
|
||||
const currentKey = ref(0)
|
||||
|
||||
// TMDB电影风格字典
|
||||
const tmdbMovieGenreDict: Record<string, string> = {
|
||||
'28': t('tmdb.genreType.action'),
|
||||
'12': t('tmdb.genreType.adventure'),
|
||||
'16': t('tmdb.genreType.animation'),
|
||||
'35': t('tmdb.genreType.comedy'),
|
||||
'80': t('tmdb.genreType.crime'),
|
||||
'99': t('tmdb.genreType.documentary'),
|
||||
'18': t('tmdb.genreType.drama'),
|
||||
'10751': t('tmdb.genreType.family'),
|
||||
'14': t('tmdb.genreType.fantasy'),
|
||||
'36': t('tmdb.genreType.history'),
|
||||
'27': t('tmdb.genreType.horror'),
|
||||
'10402': t('tmdb.genreType.music'),
|
||||
'9648': t('tmdb.genreType.mystery'),
|
||||
'10749': t('tmdb.genreType.romance'),
|
||||
'878': t('tmdb.genreType.scienceFiction'),
|
||||
'10770': t('tmdb.genreType.tvMovie'),
|
||||
'53': t('tmdb.genreType.thriller'),
|
||||
'10752': t('tmdb.genreType.war'),
|
||||
'37': t('tmdb.genreType.western'),
|
||||
}
|
||||
|
||||
// TMDB电视剧风格字典
|
||||
const tmdbTvGenreDict: Record<string, string> = {
|
||||
'10759': t('tmdb.genreType.actionAdventure'),
|
||||
'16': t('tmdb.genreType.animation'),
|
||||
'35': t('tmdb.genreType.comedy'),
|
||||
'80': t('tmdb.genreType.crime'),
|
||||
'99': t('tmdb.genreType.documentary'),
|
||||
'18': t('tmdb.genreType.drama'),
|
||||
'10751': t('tmdb.genreType.family'),
|
||||
'10762': t('tmdb.genreType.kids'),
|
||||
'9648': t('tmdb.genreType.mystery'),
|
||||
'10763': t('tmdb.genreType.news'),
|
||||
'10764': t('tmdb.genreType.reality'),
|
||||
'10765': t('tmdb.genreType.sciFiFantasy'),
|
||||
'10766': t('tmdb.genreType.soap'),
|
||||
'10767': t('tmdb.genreType.talk'),
|
||||
'10768': t('tmdb.genreType.warPolitics'),
|
||||
'37': t('tmdb.genreType.western'),
|
||||
}
|
||||
|
||||
// 获取当前类型对应的风格字典(订阅分享包含电影和电视剧,所以显示所有风格)
|
||||
const currentGenreDict = computed(() => {
|
||||
// 合并电影和电视剧风格字典
|
||||
return { ...tmdbMovieGenreDict, ...tmdbTvGenreDict }
|
||||
})
|
||||
|
||||
// 监听 props.keyword 变化
|
||||
watch(
|
||||
() => props.keyword,
|
||||
@@ -37,9 +97,23 @@ watch(
|
||||
page.value = 1
|
||||
dataList.value = []
|
||||
isRefreshed.value = false
|
||||
currentKey.value++
|
||||
},
|
||||
)
|
||||
|
||||
// 监听筛选参数变化
|
||||
watch(
|
||||
filterParams,
|
||||
() => {
|
||||
// 重置数据
|
||||
dataList.value = []
|
||||
page.value = 1
|
||||
isRefreshed.value = false
|
||||
currentKey.value++
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// 是否加载中
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -52,11 +126,26 @@ const currData = ref<SubscribeShare[]>([])
|
||||
|
||||
// 拼装参数
|
||||
function getParams() {
|
||||
let params = {
|
||||
let params: { [key: string]: any } = {
|
||||
page: page.value,
|
||||
count: 30,
|
||||
name: keyword.value,
|
||||
}
|
||||
|
||||
// 添加筛选参数
|
||||
if (filterParams.genre_id) {
|
||||
params.genre_id = parseInt(filterParams.genre_id)
|
||||
}
|
||||
if (filterParams.min_rating > 0) {
|
||||
params.min_rating = filterParams.min_rating
|
||||
}
|
||||
if (filterParams.max_rating < 10) {
|
||||
params.max_rating = filterParams.max_rating
|
||||
}
|
||||
if (filterParams.sort_type) {
|
||||
params.sort_type = filterParams.sort_type
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
@@ -131,9 +220,78 @@ function removeData(id: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 筛选器 -->
|
||||
<div class="px-3 mb-4">
|
||||
<div class="flex justify-start align-center mb-3">
|
||||
<div class="mr-5">
|
||||
<VLabel>{{ t('tmdb.sort') }}</VLabel>
|
||||
</div>
|
||||
<VChipGroup v-model="filterParams.sort_type">
|
||||
<VChip :color="filterParams.sort_type == 'time' ? 'primary' : ''" filter tile value="time">
|
||||
{{ t('tmdb.sortType.time') }}
|
||||
</VChip>
|
||||
<VChip :color="filterParams.sort_type == 'count' ? 'primary' : ''" filter tile value="count">
|
||||
{{ t('tmdb.sortType.count') }}
|
||||
</VChip>
|
||||
<VChip :color="filterParams.sort_type == 'rating' ? 'primary' : ''" filter tile value="rating">
|
||||
{{ t('tmdb.sortType.rating') }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-start align-center mb-3">
|
||||
<div class="mr-5">
|
||||
<VLabel>{{ t('tmdb.genre') }}</VLabel>
|
||||
</div>
|
||||
<VChipGroup v-model="filterParams.genre_id">
|
||||
<VChip
|
||||
:color="filterParams.genre_id == '' ? 'primary' : ''"
|
||||
filter
|
||||
tile
|
||||
value=""
|
||||
>
|
||||
{{ t('common.all') }}
|
||||
</VChip>
|
||||
<VChip
|
||||
:color="filterParams.genre_id == key ? 'primary' : ''"
|
||||
filter
|
||||
tile
|
||||
:value="key"
|
||||
v-for="(value, key) in currentGenreDict"
|
||||
:key="key"
|
||||
>
|
||||
{{ value }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-start align-center mb-3">
|
||||
<div class="mr-5">
|
||||
<VLabel>{{ t('tmdb.rating') }}</VLabel>
|
||||
</div>
|
||||
<VSlider
|
||||
v-model="filterParams.min_rating"
|
||||
thumb-label
|
||||
max="10"
|
||||
min="0"
|
||||
:step="1"
|
||||
class="align-center"
|
||||
hide-details
|
||||
>
|
||||
</VSlider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VPageContentTitle v-if="keyword" :title="`${t('common.search')}:${keyword}`" />
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible px-2" @load="fetchData">
|
||||
<VInfiniteScroll
|
||||
mode="intersect"
|
||||
side="end"
|
||||
:items="dataList"
|
||||
class="overflow-visible px-2"
|
||||
@load="fetchData"
|
||||
:key="currentKey"
|
||||
>
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-subscribe-card" tabindex="0">
|
||||
|
||||
@@ -232,64 +232,91 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('setting.cache.title') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('setting.cache.subtitle') }}</VCardSubtitle>
|
||||
<div>
|
||||
<!-- 工具栏统计信息和操作按钮 -->
|
||||
<VCard class="mb-4">
|
||||
<VCardItem>
|
||||
<!-- 移动端垂直布局,桌面端水平布局 -->
|
||||
<div class="d-flex flex-column flex-md-row align-center justify-space-between w-100 gap-4">
|
||||
<!-- 左侧统计信息 -->
|
||||
<div class="d-flex align-center justify-center justify-md-start gap-2 gap-md-6 w-100 w-md-auto">
|
||||
<!-- 统计信息卡片 -->
|
||||
<div class="d-flex gap-2 gap-md-4 flex-wrap justify-center justify-md-start">
|
||||
<VCard variant="tonal" color="primary" class="pa-2 pa-md-3 flex-grow-1 flex-md-grow-0" style="min-width: 120px;">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<VIcon color="primary" size="small">mdi-database</VIcon>
|
||||
<div>
|
||||
<div class="text-h6 text-md-h6 font-weight-bold">{{ cacheData.count }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ t('setting.cache.totalCount') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
|
||||
<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>
|
||||
<VCard variant="tonal" color="success" class="pa-2 pa-md-3 flex-grow-1 flex-md-grow-0" style="min-width: 120px;">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<VIcon color="success" size="small">mdi-web</VIcon>
|
||||
<div>
|
||||
<div class="text-h6 text-md-h6 font-weight-bold">{{ cacheData.sites }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ t('setting.cache.siteCount') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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
|
||||
<!-- 右侧操作按钮 -->
|
||||
<div class="d-flex gap-1 gap-md-2 flex-wrap justify-center justify-md-end">
|
||||
<VBtn icon color="primary" :loading="loading" @click="refreshCache" size="small">
|
||||
<VIcon size="small">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"
|
||||
size="small"
|
||||
>
|
||||
</VBtn>
|
||||
<VIcon size="small">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>
|
||||
<VBtn icon color="error" :loading="loading" @click="clearAllCache" size="small">
|
||||
<VIcon size="small">mdi-delete-variant</VIcon>
|
||||
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.clearAll') }}</VTooltip>
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
|
||||
<!-- 筛选框 -->
|
||||
<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>
|
||||
<VRow class="mb-4">
|
||||
<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>
|
||||
|
||||
<!-- 缓存列表 -->
|
||||
<VDataTable
|
||||
@@ -419,54 +446,54 @@ onMounted(() => {
|
||||
</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>
|
||||
<!-- 重新识别对话框 -->
|
||||
<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>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn color="primary" :loading="loading" prepend-icon="mdi-check" @click="performReidentify">
|
||||
{{ t('setting.cache.reidentifyDialog.confirm') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,14 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import douban from '@images/logos/douban.png'
|
||||
import github from '@images/logos/github.png'
|
||||
import slack from '@images/logos/slack.webp'
|
||||
import telegram from '@images/logos/telegram.webp'
|
||||
import tmdb from '@images/logos/tmdb.png'
|
||||
import wechat from '@images/logos/wechat.png'
|
||||
import fanart from '@images/logos/fanart.webp'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
import tvdb from '@images/logos/thetvdb.jpeg'
|
||||
import python from '@images/logos/python.png'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 国际化
|
||||
@@ -36,7 +29,7 @@ interface Address {
|
||||
// 测试集
|
||||
const targets = ref<Address[]>([
|
||||
{
|
||||
image: tmdb,
|
||||
image: getLogoUrl('tmdb'),
|
||||
name: 'api.themoviedb.org',
|
||||
url: 'https://api.themoviedb.org/3/movie/550?api_key={TMDBAPIKEY}',
|
||||
proxy: true,
|
||||
@@ -46,7 +39,7 @@ const targets = ref<Address[]>([
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: tmdb,
|
||||
image: getLogoUrl('tmdb'),
|
||||
name: 'api.tmdb.org',
|
||||
url: 'https://api.tmdb.org/3/movie/550?api_key={TMDBAPIKEY}',
|
||||
proxy: true,
|
||||
@@ -56,7 +49,7 @@ const targets = ref<Address[]>([
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: tmdb,
|
||||
image: getLogoUrl('tmdb'),
|
||||
name: 'www.themoviedb.org',
|
||||
url: 'https://www.themoviedb.org',
|
||||
proxy: true,
|
||||
@@ -76,7 +69,7 @@ const targets = ref<Address[]>([
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: fanart,
|
||||
image: getLogoUrl('fanart'),
|
||||
name: 'webservice.fanart.tv',
|
||||
url: 'https://webservice.fanart.tv',
|
||||
proxy: true,
|
||||
@@ -86,7 +79,7 @@ const targets = ref<Address[]>([
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: telegram,
|
||||
image: getLogoUrl('telegram'),
|
||||
name: 'api.telegram.org',
|
||||
url: 'https://api.telegram.org',
|
||||
proxy: true,
|
||||
@@ -96,7 +89,7 @@ const targets = ref<Address[]>([
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: wechat,
|
||||
image: getLogoUrl('wechat'),
|
||||
name: 'qyapi.weixin.qq.com',
|
||||
url: 'https://qyapi.weixin.qq.com/cgi-bin/gettoken',
|
||||
proxy: false,
|
||||
@@ -106,7 +99,7 @@ const targets = ref<Address[]>([
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: douban,
|
||||
image: getLogoUrl('douban'),
|
||||
name: 'frodo.douban.com',
|
||||
url: 'https://frodo.douban.com',
|
||||
proxy: false,
|
||||
@@ -116,7 +109,7 @@ const targets = ref<Address[]>([
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: slack,
|
||||
image: getLogoUrl('slack'),
|
||||
name: 'slack.com',
|
||||
url: 'https://slack.com',
|
||||
proxy: false,
|
||||
@@ -126,7 +119,7 @@ const targets = ref<Address[]>([
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: python,
|
||||
image: getLogoUrl('python'),
|
||||
name: 'pypi.org',
|
||||
url: '{PIP_PROXY}rsa/',
|
||||
proxy: true,
|
||||
@@ -137,7 +130,7 @@ const targets = ref<Address[]>([
|
||||
include: 'pypi:repository-version',
|
||||
},
|
||||
{
|
||||
image: github,
|
||||
image: getLogoUrl('github'),
|
||||
name: 'github.com',
|
||||
url: '{GITHUB_PROXY}https://github.com/jxxghp/MoviePilot/blob/v2/README.md',
|
||||
proxy: true,
|
||||
@@ -148,7 +141,7 @@ const targets = ref<Address[]>([
|
||||
include: 'MoviePilot',
|
||||
},
|
||||
{
|
||||
image: github,
|
||||
image: getLogoUrl('github'),
|
||||
name: 'codeload.github.com',
|
||||
url: 'https://codeload.github.com',
|
||||
proxy: true,
|
||||
@@ -158,7 +151,7 @@ const targets = ref<Address[]>([
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: github,
|
||||
image: getLogoUrl('github'),
|
||||
name: 'api.github.com',
|
||||
url: 'https://api.github.com',
|
||||
proxy: true,
|
||||
@@ -168,7 +161,7 @@ const targets = ref<Address[]>([
|
||||
btndisable: false,
|
||||
},
|
||||
{
|
||||
image: github,
|
||||
image: getLogoUrl('github'),
|
||||
name: 'raw.githubusercontent.com',
|
||||
url: '{GITHUB_PROXY}https://raw.githubusercontent.com/jxxghp/MoviePilot/v2/README.md',
|
||||
proxy: true,
|
||||
@@ -188,7 +181,7 @@ const resolveStatusColor: Status = {
|
||||
}
|
||||
|
||||
const abortControllers = new Set<AbortController>()
|
||||
const isUnmounting = ref(false);
|
||||
const isUnmounting = ref(false)
|
||||
|
||||
// 调用API测试网络连接
|
||||
async function netTest(index: number) {
|
||||
@@ -229,17 +222,16 @@ async function netTest(index: number) {
|
||||
|
||||
// 加载时测试所有连接
|
||||
onMounted(async () => {
|
||||
isUnmounting.value = false;
|
||||
for (let i = 0; !isUnmounting.value && i < targets.value.length; i++)
|
||||
await netTest(i)
|
||||
isUnmounting.value = false
|
||||
for (let i = 0; !isUnmounting.value && i < targets.value.length; i++) await netTest(i)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
isUnmounting.value = true;
|
||||
isUnmounting.value = true
|
||||
for (const controller of abortControllers) {
|
||||
controller.abort()
|
||||
}
|
||||
abortControllers.clear()
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -149,7 +149,7 @@ export default defineConfig({
|
||||
},
|
||||
injectManifest: {
|
||||
rollupFormat: 'iife',
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
|
||||
},
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
|
||||
30
yarn.lock
30
yarn.lock
@@ -1927,6 +1927,11 @@
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@types/body-scroll-lock@^3.1.2":
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-scroll-lock/-/body-scroll-lock-3.1.2.tgz#1ae7857d98180dbe6c3b05abbe7ec1fa67b614e3"
|
||||
integrity sha512-ELhtuphE/YbhEcpBf/rIV9Tl3/O0A0gpCVD+oYFSS8bWstHFJUgA4nNw1ZakVlRC38XaQEIsBogUZKWIPBvpfQ==
|
||||
|
||||
"@types/crypto-js@^4.2.2":
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea"
|
||||
@@ -2879,6 +2884,11 @@ body-parser@1.20.3:
|
||||
type-is "~1.6.18"
|
||||
unpipe "1.0.0"
|
||||
|
||||
body-scroll-lock@^3.1.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz#c1392d9217ed2c3e237fee1e910f6cdd80b7aaec"
|
||||
integrity sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg==
|
||||
|
||||
boolbase@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz"
|
||||
@@ -6845,16 +6855,7 @@ std-env@^3.9.0:
|
||||
resolved "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz"
|
||||
integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -6939,14 +6940,7 @@ stringify-object@^3.3.0:
|
||||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
|
||||
Reference in New Issue
Block a user