mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-10 06:32:45 +08:00
Compare commits
201 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3af1013c34 | ||
|
|
c2bca6fc3f | ||
|
|
226a12df40 | ||
|
|
ab7286a87a | ||
|
|
c43fd88c7c | ||
|
|
21871626f3 | ||
|
|
8ce09ecf79 | ||
|
|
ef2df85faf | ||
|
|
77ec8c7a81 | ||
|
|
06f6ab355e | ||
|
|
5e1761c47a | ||
|
|
ed63297814 | ||
|
|
3db7d6ce63 | ||
|
|
aa5f31ee70 | ||
|
|
c3379e9737 | ||
|
|
ef32172359 | ||
|
|
c2381deb9f | ||
|
|
5753d4ff07 | ||
|
|
71437a2122 | ||
|
|
93005518d2 | ||
|
|
da04cfc683 | ||
|
|
c60eea73fc | ||
|
|
bdb092bda9 | ||
|
|
84317a4217 | ||
|
|
6dd9d94e86 | ||
|
|
1ffcfe643c | ||
|
|
87c11eda46 | ||
|
|
9613141527 | ||
|
|
a820d9129b | ||
|
|
fd7279b528 | ||
|
|
8e5ffa81a1 | ||
|
|
95f6635591 | ||
|
|
7a1208a04f | ||
|
|
06668d9415 | ||
|
|
708928ab26 | ||
|
|
78f04c4b4b | ||
|
|
af20a6c821 | ||
|
|
3c4ee302e7 | ||
|
|
0987ba3575 | ||
|
|
2b0564211d | ||
|
|
174b2b9fa3 | ||
|
|
dc9c08ed30 | ||
|
|
2abbace470 | ||
|
|
c3511fe27e | ||
|
|
913e1728e0 | ||
|
|
d0ea7f3fd9 | ||
|
|
c1201fbd96 | ||
|
|
f862a3d8a1 | ||
|
|
120a12edde | ||
|
|
a484fc2d39 | ||
|
|
229264f2d0 | ||
|
|
06f4898ce8 | ||
|
|
476d2f7e81 | ||
|
|
01c8304c8b | ||
|
|
e002588949 | ||
|
|
017656f592 | ||
|
|
526d2c7085 | ||
|
|
86e90cfe7e | ||
|
|
e97d815dc3 | ||
|
|
65ebdb61d0 | ||
|
|
cb5bccc945 | ||
|
|
8b53cd0a09 | ||
|
|
3d7a0d9b0d | ||
|
|
114844ad48 | ||
|
|
f83f709080 | ||
|
|
999a6e7c6e | ||
|
|
2fd4c0b2ea | ||
|
|
16d62642f6 | ||
|
|
96a0ce8c5f | ||
|
|
7703d8157c | ||
|
|
87aa4e902c | ||
|
|
c86f32fab5 | ||
|
|
f85ac34753 | ||
|
|
f58d4fcb7e | ||
|
|
675c32cee3 | ||
|
|
de011b35db | ||
|
|
288a7ebc20 | ||
|
|
d7c3167ecd | ||
|
|
3205ae3ebe | ||
|
|
2ba609fb78 | ||
|
|
7e70b1b7ab | ||
|
|
561bdf4137 | ||
|
|
22a2bb65c8 | ||
|
|
c4f6db9f9f | ||
|
|
e5d2140ea3 | ||
|
|
83e57deec3 | ||
|
|
fc357a03e5 | ||
|
|
f031077fbd | ||
|
|
02de63210d | ||
|
|
98610e3e0d | ||
|
|
bb6cfd9d0e | ||
|
|
57c6d7e8f3 | ||
|
|
6d8b850b15 | ||
|
|
db6c3ea36c | ||
|
|
0ddf7ab070 | ||
|
|
93686bd354 | ||
|
|
89e4a68a03 | ||
|
|
204719caf8 | ||
|
|
267f53942b | ||
|
|
7ae9bbc4f0 | ||
|
|
717b460246 | ||
|
|
d52a814d77 | ||
|
|
6e1503334e | ||
|
|
34a33f87b2 | ||
|
|
03d885d391 | ||
|
|
c50b25997d | ||
|
|
3adcc894b7 | ||
|
|
8a73ad63ee | ||
|
|
69b5b2b900 | ||
|
|
97075fc167 | ||
|
|
bde70a2e26 | ||
|
|
2e05c8079b | ||
|
|
357191afcf | ||
|
|
8352ba335b | ||
|
|
7ae3e402cf | ||
|
|
c5e62cc8e4 | ||
|
|
ddb0befa4d | ||
|
|
82c19f3512 | ||
|
|
e95552b47a | ||
|
|
50ffcc6e92 | ||
|
|
43beb1df95 | ||
|
|
3cef928b75 | ||
|
|
6f5d62f1f9 | ||
|
|
e3a385c989 | ||
|
|
12356abf00 | ||
|
|
50e76496a2 | ||
|
|
a98bf08b2d | ||
|
|
697fd57bc7 | ||
|
|
7a691fe4e7 | ||
|
|
3822ab20d5 | ||
|
|
88f261584f | ||
|
|
62db4508da | ||
|
|
122acc7ad3 | ||
|
|
c15927cca0 | ||
|
|
b5b2de30a2 | ||
|
|
aebce53450 | ||
|
|
3c261a2c29 | ||
|
|
6a6a3bd463 | ||
|
|
ae62847ded | ||
|
|
c873787a89 | ||
|
|
410ff78ef5 | ||
|
|
ed53fbae93 | ||
|
|
a3d8aa6a33 | ||
|
|
24d03431c4 | ||
|
|
1d40d4a329 | ||
|
|
564896d99d | ||
|
|
2b2e25202d | ||
|
|
9055b95d00 | ||
|
|
5a8eb5b10e | ||
|
|
3e36cb6e31 | ||
|
|
6b4b44aec6 | ||
|
|
91a10c9d28 | ||
|
|
d7fbbd2d28 | ||
|
|
7b171e2c6f | ||
|
|
90ecaa1891 | ||
|
|
842f7401a0 | ||
|
|
77a6c591ff | ||
|
|
9bd3aebd73 | ||
|
|
b70d03e86b | ||
|
|
7d1ff9876f | ||
|
|
2cd8303191 | ||
|
|
21dbaf6db5 | ||
|
|
f9f45d9e32 | ||
|
|
ef5db9ee4b | ||
|
|
a909cdc21c | ||
|
|
b8e546a584 | ||
|
|
c4f54dcddc | ||
|
|
59b5e4a330 | ||
|
|
f8f7275438 | ||
|
|
6eec2e97f9 | ||
|
|
9020494f65 | ||
|
|
43fbc7abd7 | ||
|
|
d65a4b747d | ||
|
|
849fad8a8a | ||
|
|
f0b2d14502 | ||
|
|
fa169fb785 | ||
|
|
49acf7fba3 | ||
|
|
80d55dae8d | ||
|
|
76aa5407a2 | ||
|
|
d70789934f | ||
|
|
398e8b6afc | ||
|
|
593fede47c | ||
|
|
40c7e9c126 | ||
|
|
54e2f70ee0 | ||
|
|
81f85b9e46 | ||
|
|
60a5476e59 | ||
|
|
4271b63530 | ||
|
|
8aca17f0c6 | ||
|
|
4f238dc1a3 | ||
|
|
d4777fde70 | ||
|
|
b6c823c386 | ||
|
|
b7488214fc | ||
|
|
06b6c3f3cb | ||
|
|
abfaf926c4 | ||
|
|
6eabeb09c9 | ||
|
|
a15afabfa7 | ||
|
|
30276d5022 | ||
|
|
683ddc3fce | ||
|
|
f00f79279b | ||
|
|
7989965b1a | ||
|
|
5b84ce307b |
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -11,7 +11,8 @@
|
||||
},
|
||||
// SCSS
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "stylelint.vscode-stylelint"
|
||||
"editor.defaultFormatter": "stylelint.vscode-stylelint",
|
||||
"editor.formatOnSave": false
|
||||
},
|
||||
// JSON
|
||||
"[json]": {
|
||||
@@ -106,4 +107,4 @@
|
||||
]
|
||||
},
|
||||
"vue3snippets.enable-compile-vue-file-on-did-save-code": false
|
||||
}
|
||||
}
|
||||
|
||||
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -12,8 +12,10 @@ declare module 'vue' {
|
||||
ExistIcon: typeof import('./src/@core/components/ExistIcon.vue')['default']
|
||||
LoadingBanner: typeof import('./src/@core/components/LoadingBanner.vue')['default']
|
||||
MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default']
|
||||
PageContentTitle: typeof import('./src/@core/components/PageContentTitle.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
ScrollToTopBtn: typeof import('./src/@core/components/ScrollToTopBtn.vue')['default']
|
||||
StatIcon: typeof import('./src/@core/components/StatIcon.vue')['default']
|
||||
ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default']
|
||||
}
|
||||
|
||||
358
index.html
358
index.html
@@ -1,221 +1,161 @@
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
lang="en"
|
||||
style="
|
||||
<html lang="en" style="
|
||||
overflow: hidden auto;
|
||||
min-block-size: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
||||
background: var(--initial-loader-bg, #fff);
|
||||
"
|
||||
>
|
||||
<head>
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="expires" content="0" />
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width, user-scalable=no" />
|
||||
<title>MoviePilot</title>
|
||||
<meta name="Robots" content="noindex,nofollow,noarchive" />
|
||||
<meta name="referrer" content="origin" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="MoviePilot" />
|
||||
<meta name="description" content="MoviePilot" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="referrer" content="never" />
|
||||
<meta name="msapplication-TileColor" content="#7D34FD" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#28243D" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)" />
|
||||
<meta name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<link rel="stylesheet" type="text/css" href="/loader.css" />
|
||||
<script>
|
||||
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
|
||||
if (loaderColor) document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
|
||||
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
|
||||
if (primaryColor) document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
||||
</script>
|
||||
</head>
|
||||
">
|
||||
|
||||
<body style="margin: 0">
|
||||
<div id="loading-bg">
|
||||
<div class="loading-logo">
|
||||
<!-- Logo -->
|
||||
<svg
|
||||
width="10rem"
|
||||
height="10rem"
|
||||
viewBox="0 0 192 192"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2"
|
||||
>
|
||||
<g transform="matrix(1,0,0,1,-2606,-236)">
|
||||
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">
|
||||
<rect x="0" y="0" width="192" height="192" style="fill: none" />
|
||||
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)">
|
||||
<head>
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="expires" content="0" />
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="initial-scale=1, viewport-fit=cover, width=device-width, user-scalable=no" />
|
||||
<title>MoviePilot</title>
|
||||
<meta name="Robots" content="noindex,nofollow,noarchive" />
|
||||
<meta name="referrer" content="origin" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="MoviePilot" />
|
||||
<meta name="description" content="MoviePilot" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="referrer" content="never" />
|
||||
<meta name="msapplication-TileColor" content="#7D34FD" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#0E1116" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)" />
|
||||
<meta name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<link rel="stylesheet" type="text/css" href="/loader.css" />
|
||||
<script>
|
||||
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
|
||||
if (loaderColor) document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
|
||||
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
|
||||
if (primaryColor) document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body style="margin: 0">
|
||||
<div id="loading-bg">
|
||||
<div class="loading-logo">
|
||||
<!-- Logo -->
|
||||
<svg width="160px" height="160px" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2">
|
||||
<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="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">
|
||||
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="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>
|
||||
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>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="_Linear1"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear2"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear3"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear4"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear6"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
<stop offset="0.51" style="stop-color: rgb(110, 38, 217); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(91, 0, 197); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="_Radial7"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="loading">
|
||||
<div class="effect-1 effects"></div>
|
||||
<div class="effect-2 effects"></div>
|
||||
<div class="effect-3 effects"></div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
<div class="loading">
|
||||
<div class="effect-1 effects"></div>
|
||||
<div class="effect-2 effects"></div>
|
||||
<div class="effect-3 effects"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
14609
package-lock.json
generated
Normal file
14609
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.3.4",
|
||||
"version": "2.4.1",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
@@ -26,6 +26,7 @@
|
||||
"@fullcalendar/timegrid": "^6.1.15",
|
||||
"@fullcalendar/vue3": "^6.1.15",
|
||||
"@iconify/utils": "^2.2.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@vue-flow/background": "^1.3.2",
|
||||
"@vue-flow/controls": "^1.1.2",
|
||||
"@vue-flow/core": "^1.42.1",
|
||||
@@ -43,6 +44,7 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"express": "^4.21.2",
|
||||
"express-http-proxy": "^2.1.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nprogress": "^0.2.0",
|
||||
@@ -71,6 +73,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^20.1.4",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/webfontloader": "^1.6.34",
|
||||
"@typescript-eslint/eslint-plugin": "^8.20.0",
|
||||
"@typescript-eslint/parser": "^8.20.0",
|
||||
@@ -108,4 +111,4 @@
|
||||
"workbox-window": "^7.3.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<template>
|
||||
<div class="absolute top-0 right-0 flex items-center justify-between p-2">
|
||||
<div class="pointer-events-none z-40 flex items-center">
|
||||
<div class="relative inline-flex whitespace-nowrap rounded-full border-gray-700 font-semibold leading-5 ring-gray-700">
|
||||
<div class="rounded-full bg-opacity-80 shadow-md w-5 border p-0 bg-green-500 border-green-400 ring-green-400 text-green-100">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
class="relative inline-flex whitespace-nowrap rounded-full border-gray-700 font-semibold leading-5 ring-gray-700"
|
||||
>
|
||||
<div
|
||||
class="rounded-full bg-opacity-80 w-5 border p-0 bg-green-500 border-green-400 ring-green-400 text-green-100"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||
|
||||
18
src/@core/components/PageContentTitle.vue
Normal file
18
src/@core/components/PageContentTitle.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
// 标题
|
||||
title: String,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="title" class="my-3 md:flex md:items-center md:justify-between">
|
||||
<div class="min-w-0 flex-1 mx-0">
|
||||
<h2
|
||||
class="ms-1 truncate text-2xl font-bold leading-7 text-gray-100 sm:overflow-visible sm:text-3xl sm:leading-9 md:mb-0"
|
||||
data-testid="page-header"
|
||||
>
|
||||
<span class="text-moviepilot">{{ title }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
85
src/@core/components/ScrollToTopBtn.vue
Normal file
85
src/@core/components/ScrollToTopBtn.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script lang="ts" setup>
|
||||
// 控制回到顶部按钮的可见性
|
||||
const showScrollToTop = ref(false)
|
||||
const scrollThreshold = 200 // 滚动多少像素后显示按钮
|
||||
|
||||
// 滚动事件处理函数
|
||||
const handleScroll = () => {
|
||||
showScrollToTop.value = window.scrollY > scrollThreshold
|
||||
}
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Add scroll event listener
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
// Initial check for scroll-to-top
|
||||
handleScroll()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// Remove scroll event listener
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="global-action-buttons d-none d-sm-block">
|
||||
<Transition name="scroll-fade">
|
||||
<button v-show="showScrollToTop" class="global-action-button" @click="scrollToTop">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7 14L12 9L17 14"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* Global Action Button Styles (FAB) */
|
||||
.global-action-buttons {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
inset-block-end: 30px;
|
||||
inset-inline-end: 30px;
|
||||
}
|
||||
|
||||
.global-action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.05);
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(var(--v-theme-background), 0.8);
|
||||
block-size: 44px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 8%);
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
cursor: pointer;
|
||||
inline-size: 44px;
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--v-theme-background), 0.95);
|
||||
color: rgb(var(--v-theme-primary));
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
svg {
|
||||
block-size: 20px;
|
||||
inline-size: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,9 +8,9 @@ const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute top-2 right-2 flex items-center justify-between p-2 shadow">
|
||||
<div class="absolute top-2 right-2 flex items-center justify-between p-2">
|
||||
<VBadge :color="props.color" bordered>
|
||||
<template #badge>
|
||||
<template #badge>
|
||||
<VIcon icon="mdi-pulse"></VIcon>
|
||||
</template>
|
||||
</VBadge>
|
||||
|
||||
@@ -18,10 +18,12 @@ const { name: themeName, global: globalTheme } = useTheme()
|
||||
|
||||
const savedTheme = ref(localStorage.getItem('theme') ?? themeName)
|
||||
|
||||
const { state: currentThemeName, next: getNextThemeName } = useCycleList(
|
||||
props.themes.map(t => t.name),
|
||||
{ initialValue: savedTheme.value },
|
||||
)
|
||||
const currentThemeName = ref(savedTheme.value)
|
||||
const getNextThemeName = () => {
|
||||
const currentIndex = props.themes.findIndex(t => t.name === currentThemeName.value)
|
||||
const nextIndex = (currentIndex + 1) % props.themes.length
|
||||
return props.themes[nextIndex].name
|
||||
}
|
||||
|
||||
const $toast = useToast()
|
||||
|
||||
@@ -34,76 +36,17 @@ const customCSS = ref('')
|
||||
// 编辑器主题
|
||||
const editorTheme = computed(() => (currentThemeName.value === 'light' ? 'github' : 'monokai'))
|
||||
|
||||
// 主题切换动画
|
||||
function themeTransition() {
|
||||
const x = performance.now()
|
||||
for (let i = 0; i++ < 1e7; (i << 9) & ((9 % 9) * 9 + 9));
|
||||
const cost = performance.now() - x
|
||||
if (cost > 10) return
|
||||
|
||||
const el: HTMLElement = document.querySelector('[data-v-app]')!
|
||||
const children = el.querySelectorAll('*') as NodeListOf<HTMLElement>
|
||||
|
||||
children.forEach(el => {
|
||||
if (hasScrollbar(el)) {
|
||||
el.dataset.scrollX = String(el.scrollLeft)
|
||||
el.dataset.scrollY = String(el.scrollTop)
|
||||
}
|
||||
})
|
||||
|
||||
const copy = el.cloneNode(true) as HTMLElement
|
||||
copy.classList.add('app-copy')
|
||||
const rect = el.getBoundingClientRect()
|
||||
copy.style.top = `${rect.top}px`
|
||||
copy.style.left = `${rect.left}px`
|
||||
copy.style.width = `${rect.width}px`
|
||||
copy.style.height = `${rect.height}px`
|
||||
|
||||
const targetEl = document.activeElement as HTMLElement
|
||||
const targetRect = targetEl.getBoundingClientRect()
|
||||
const left = targetRect.left + targetRect.width / 2 + window.scrollX
|
||||
const top = targetRect.top + targetRect.height / 2 + window.scrollY
|
||||
el.style.setProperty('--clip-pos', `${left}px ${top}px`)
|
||||
el.style.removeProperty('--clip-size')
|
||||
|
||||
nextTick(() => {
|
||||
el.classList.add('app-transition')
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
el.style.setProperty('--clip-size', `${Math.hypot(window.innerWidth, window.innerHeight)}px`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
document.body.append(copy)
|
||||
;(copy.querySelectorAll('[data-scroll-x], [data-scroll-y]') as NodeListOf<HTMLElement>).forEach(el => {
|
||||
el.scrollLeft = +el.dataset.scrollX!
|
||||
el.scrollTop = +el.dataset.scrollY!
|
||||
})
|
||||
|
||||
function onTransitionend(e: TransitionEvent) {
|
||||
if (e.target === e.currentTarget) {
|
||||
copy.remove()
|
||||
el.removeEventListener('transitionend', onTransitionend)
|
||||
el.removeEventListener('transitioncancel', onTransitionend)
|
||||
el.classList.remove('app-transition')
|
||||
el.style.removeProperty('--clip-size')
|
||||
el.style.removeProperty('--clip-pos')
|
||||
}
|
||||
}
|
||||
el.addEventListener('transitionend', onTransitionend)
|
||||
el.addEventListener('transitioncancel', onTransitionend)
|
||||
}
|
||||
|
||||
// 更新主题
|
||||
function updateTheme() {
|
||||
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
|
||||
const theme = currentThemeName.value === 'auto' ? autoTheme : currentThemeName.value
|
||||
globalTheme.name.value = theme
|
||||
savedTheme.value = theme
|
||||
themeTransition()
|
||||
// 保存原始主题设置,而不是计算后的值
|
||||
savedTheme.value = currentThemeName.value
|
||||
// 保存主题到本地
|
||||
saveLocalTheme(theme, globalTheme)
|
||||
saveLocalTheme(currentThemeName.value, globalTheme)
|
||||
// 刷新页面
|
||||
location.reload()
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
@@ -117,7 +60,7 @@ function changeTheme(theme: string) {
|
||||
theme: nextTheme,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('保存主题到服务端失败')
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,38 +130,51 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VMenu v-if="props.themes">
|
||||
<VMenu v-if="props.themes" class="theme-menu" scrim>
|
||||
<template v-slot:activator="{ props }">
|
||||
<IconBtn v-bind="props">
|
||||
<VIcon :icon="getThemeIcon" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VList>
|
||||
<VListItem v-for="theme in props.themes" :key="theme.name" @click="changeTheme(theme.name)">
|
||||
<template #prepend>
|
||||
<VIcon :icon="theme.icon" />
|
||||
</template>
|
||||
<VListItemTitle>{{ theme.title }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem @click="cssDialog = true">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-palette" />
|
||||
</template>
|
||||
<VListItemTitle>自定义</VListItemTitle>
|
||||
</VListItem>
|
||||
<div class="px-2">
|
||||
<VListItem
|
||||
v-for="theme in props.themes"
|
||||
:key="theme.name"
|
||||
@click="changeTheme(theme.name)"
|
||||
:active="currentThemeName === theme.name"
|
||||
class="mb-1"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="theme.icon" />
|
||||
</template>
|
||||
<VListItemTitle>{{ theme.title }}</VListItemTitle>
|
||||
<template #append v-if="currentThemeName === theme.name">
|
||||
<VIcon icon="mdi-check" color="primary" size="small" />
|
||||
</template>
|
||||
</VListItem>
|
||||
<VDivider class="my-2" />
|
||||
<VListItem @click="cssDialog = true">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-palette" />
|
||||
</template>
|
||||
<VListItemTitle>自定义主题</VListItemTitle>
|
||||
</VListItem>
|
||||
</div>
|
||||
</VList>
|
||||
</VMenu>
|
||||
<!-- 自定义 CSS -- -->
|
||||
<VDialog v-if="cssDialog" v-model="cssDialog" persistent max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard title="自定义主题风格">
|
||||
<DialogCloseBtn @click="cssDialog = false" />
|
||||
<VDialog v-if="cssDialog" v-model="cssDialog" max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-palette" class="me-2" />
|
||||
自定义主题风格
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="cssDialog = false" />
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VAceEditor
|
||||
v-model:value="customCSS"
|
||||
lang="css"
|
||||
:theme="editorTheme"
|
||||
style="block-size: 100%; min-block-size: 30rem"
|
||||
/>
|
||||
<VAceEditor v-model:value="customCSS" lang="css" :theme="editorTheme" class="w-full min-h-[30rem]" />
|
||||
<VDivider />
|
||||
<VCardText class="text-center">
|
||||
<VBtn @click="saveCustomCSS" class="w-1/2">
|
||||
@@ -231,19 +187,3 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
// Theme transition
|
||||
.app-copy
|
||||
position: fixed !important
|
||||
z-index: -1 !important
|
||||
pointer-events: none !important
|
||||
contain: size style !important
|
||||
overflow: clip !important
|
||||
|
||||
.app-transition
|
||||
--clip-size: 0
|
||||
--clip-pos: 0 0
|
||||
clip-path: circle(var(--clip-size) at var(--clip-pos))
|
||||
transition: clip-path .35s ease-out
|
||||
</style>
|
||||
|
||||
58
src/@core/scss/README.md
Normal file
58
src/@core/scss/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# SCSS结构说明
|
||||
|
||||
## 目录整合
|
||||
|
||||
本项目SCSS文件已完成整合:
|
||||
- 主入口文件:`src/@core/scss/index.scss`
|
||||
- 实际功能文件位于:`src/@core/scss/template/index.scss`
|
||||
|
||||
## 整合内容
|
||||
|
||||
- 整合了原`src/@core/scss/base`和`src/@core/scss/template`目录的功能
|
||||
- 统一使用`template`目录作为SCSS样式的主要引用点
|
||||
- 保留原有引用结构以保证向后兼容性
|
||||
|
||||
## 整合进度
|
||||
|
||||
已完成:
|
||||
- ✅ 主入口文件引用更新
|
||||
- ✅ mixins文件合并
|
||||
- ✅ placeholders目录下文件转移
|
||||
- ✅ perfect-scrollbar文件整合
|
||||
- ✅ vuetify相关文件整合
|
||||
- ✅ default-layout-w-vertical-nav文件整合
|
||||
- ✅ 移除了template/index.scss中对base目录组件的依赖
|
||||
- ✅ 修复了components.scss中对base/mixins的引用
|
||||
- ✅ 修复了variables.scss中对base/variables的引用
|
||||
- ✅ 修复了apex-chart.scss和full-calendar.scss的linter错误
|
||||
- ✅ 整合并移除了对vuetify/variables的依赖
|
||||
- ✅ 修复了SCSS变量名冲突问题
|
||||
- ✅ 修复了SASS模块重复加载配置问题
|
||||
- ✅ 修复了导入路径问题(misc、utils等模块的引用路径)
|
||||
|
||||
待完成:
|
||||
- ⬜ 最终测试确保无样式问题
|
||||
- ⬜ 清理冗余文件
|
||||
|
||||
## 使用方式
|
||||
|
||||
在项目中引用SCSS时,应使用:
|
||||
```scss
|
||||
@use "@core/scss";
|
||||
```
|
||||
|
||||
这将自动加载所有必要的样式文件。
|
||||
|
||||
## 注意事项
|
||||
|
||||
此次整合已将所有功能文件整合到template目录,不再依赖base目录的代码。现在可以安全地从外部引用template目录下的文件,但需要进行最终测试以确保样式正常工作。
|
||||
|
||||
测试无误后,可以考虑完全删除base目录,以简化项目结构。
|
||||
|
||||
## 最近修复
|
||||
|
||||
在最近的更新中,我们修复了以下问题:
|
||||
1. 解决了变量名冲突问题,通过使用命名空间(如`layouts-vars`)来引用外部模块变量
|
||||
2. 修复了SASS模块重复配置问题,将多处的`@forward...with`配置合并到了template/_variables.scss文件中
|
||||
3. 统一使用命名空间引用模块,避免后续出现冲突
|
||||
4. 修复了`_default-layout-w-vertical-nav.scss`中导入路径错误,将`@use "misc"`修改为`@use "../misc"`
|
||||
@@ -1,8 +1,8 @@
|
||||
@use "@core/scss/placeholders";
|
||||
@use "@core/scss/variables";
|
||||
@use "@core/scss/variables" as core-vars;
|
||||
|
||||
.layout-navbar {
|
||||
@if variables.$navbar-high-emphasis-text {
|
||||
@if core-vars.$navbar-high-emphasis-text {
|
||||
@extend %layout-navbar;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,17 +10,19 @@
|
||||
*/
|
||||
@use "sass:map";
|
||||
|
||||
// @forward "@layouts/styles/variables";
|
||||
// 使用模板中的变量,不再进行配置
|
||||
@use "@layouts/styles/variables" as layouts-vars;
|
||||
@use "utils";
|
||||
|
||||
// 👉 Default layout
|
||||
|
||||
$navbar-high-emphasis-text: true !default;
|
||||
|
||||
@forward "@layouts/styles/variables" with (
|
||||
$layout-vertical-nav-collapsed-width: 68px !default,
|
||||
);
|
||||
@use "@layouts/styles/variables" as *;
|
||||
// 移除@forward配置,已合并到template/_variables.scss
|
||||
// @forward "@layouts/styles/variables" with (
|
||||
// $layout-vertical-nav-collapsed-width: 68px !default,
|
||||
// );
|
||||
// @use "@layouts/styles/variables" as *;
|
||||
|
||||
$theme-colors-name: (
|
||||
"primary",
|
||||
@@ -55,7 +57,7 @@ $vertical-nav-horizontal-padding: 1.375rem 1rem !default;
|
||||
$vertical-nav-horizontal-padding-start: utils.get-first-value($vertical-nav-horizontal-padding);
|
||||
|
||||
// Vertical nav header height. Mostly we will align it with navbar height;
|
||||
$vertical-nav-header-height: $layout-vertical-nav-navbar-height !default;
|
||||
$vertical-nav-header-height: layouts-vars.$layout-vertical-nav-navbar-height !default;
|
||||
$vertical-nav-navbar-shadow: 0 4px 8px -4px rgb(94 86 105 / 42%);
|
||||
|
||||
// Vertical nav header padding
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
@use "mixins";
|
||||
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
|
||||
@use "@layouts/styles/placeholders";
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
// 👉 Avatar group
|
||||
.v-avatar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
&:not(:first-child) {
|
||||
margin-inline-start: -0.8rem;
|
||||
}
|
||||
|
||||
transition: transform 0.25s ease, box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
z-index: 2;
|
||||
transform: translateY(-5px) scale(1.05);
|
||||
|
||||
@include mixins.elevation(3);
|
||||
}
|
||||
}
|
||||
|
||||
> .v-avatar {
|
||||
border: 2px solid rgb(var(--v-theme-surface));
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Button outline with default color border color
|
||||
.v-alert--variant-outlined,
|
||||
.v-avatar--variant-outlined,
|
||||
.v-btn.v-btn--variant-outlined,
|
||||
.v-card--variant-outlined,
|
||||
.v-chip--variant-outlined,
|
||||
.v-list-item--variant-outlined {
|
||||
&:not([class*="text-"]) {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
&.text-default {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Custom Input
|
||||
.v-label.custom-input {
|
||||
padding: 1rem;
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
opacity: 1;
|
||||
white-space: normal;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(var(--v-border-color), 0.25);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
|
||||
.v-icon {
|
||||
color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dialog responsive width
|
||||
.v-dialog {
|
||||
// dialog custom close btn
|
||||
.v-dialog-close-btn {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)) !important;
|
||||
inset-block-start: 0.9375rem;
|
||||
inset-inline-end: 0.9375rem;
|
||||
|
||||
.v-btn__overlay {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.v-card {
|
||||
@extend %style-scroll-bar;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.v-dialog {
|
||||
&.v-dialog-sm,
|
||||
&.v-dialog-lg,
|
||||
&.v-dialog-xl {
|
||||
.v-overlay__content {
|
||||
inline-size: 565px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.v-dialog {
|
||||
&.v-dialog-lg,
|
||||
&.v-dialog-xl {
|
||||
.v-overlay__content {
|
||||
inline-size: 865px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1264px) {
|
||||
.v-dialog.v-dialog-xl {
|
||||
.v-overlay__content {
|
||||
inline-size: 1165px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// v-tab with pill support
|
||||
|
||||
.v-tabs.v-tabs-pill {
|
||||
.v-tab.v-btn {
|
||||
border-radius: 0.25rem !important;
|
||||
transition: none;
|
||||
|
||||
.v-tab__slider {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loop for all colors bg
|
||||
@each $color-name in variables.$theme-colors-name {
|
||||
.v-tabs.v-tabs-pill {
|
||||
.v-slide-group-item--active.v-tab--selected.text-#{$color-name} {
|
||||
background-color: rgb(var(--v-theme-#{$color-name}));
|
||||
color: rgb(var(--v-theme-on-#{$color-name})) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ℹ️ We are make even width of all v-timeline body
|
||||
.v-timeline--vertical.v-timeline {
|
||||
.v-timeline-item {
|
||||
.v-timeline-item__body {
|
||||
justify-self: stretch !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Textarea
|
||||
|
||||
.v-textarea .v-field__input {
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-mask-image: none !important;
|
||||
mask-image: none !important;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
// ————————————————————————————————————
|
||||
// * ——— Perfect Scrollbar
|
||||
// ————————————————————————————————————
|
||||
|
||||
.v-application.v-theme--dark {
|
||||
.ps__rail-y,
|
||||
.ps__rail-x {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.ps__thumb-y {
|
||||
background-color: variables.$plugin-ps-thumb-y-dark;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
@use "@core/scss/base/placeholders";
|
||||
@use "@core/scss/base/variables";
|
||||
|
||||
.layout-vertical-nav,
|
||||
.layout-horizontal-nav {
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-navbar {
|
||||
@if variables.$navbar-high-emphasis-text {
|
||||
@extend %layout-navbar;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
@use "sass:map";
|
||||
|
||||
// Layout
|
||||
@use "vertical-nav";
|
||||
@use "default-layout";
|
||||
@use "default-layout-w-vertical-nav";
|
||||
|
||||
// Layouts package
|
||||
@use "layouts";
|
||||
|
||||
// Components
|
||||
@use "components";
|
||||
|
||||
// Utilities
|
||||
@use "utilities";
|
||||
|
||||
// Misc
|
||||
@use "misc";
|
||||
|
||||
// Dark
|
||||
@use "dark";
|
||||
|
||||
// libs
|
||||
@use "libs/perfect-scrollbar";
|
||||
|
||||
a {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// Vuetify 3 don't provide margin bottom style like vuetify 2
|
||||
p {
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
// Iconify icon size
|
||||
svg.iconify {
|
||||
block-size: 1em;
|
||||
inline-size: 1em;
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
/* ℹ️ This styles extends the existing layout package's styles for handling cases that aren't related to layouts package */
|
||||
|
||||
/*
|
||||
ℹ️ When we use v-layout as immediate first child of `.page-content-container`, it adds display:flex and page doesn't get contained height
|
||||
*/
|
||||
// .layout-wrapper.layout-nav-type-vertical {
|
||||
// &.layout-content-height-fixed {
|
||||
// .page-content-container {
|
||||
// > .v-layout:first-child > :not(.v-navigation-drawer):first-child {
|
||||
// flex-grow: 1;
|
||||
// block-size: 100%;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
&.layout-content-height-fixed {
|
||||
.page-content-container {
|
||||
> .v-layout:first-child {
|
||||
overflow: hidden;
|
||||
min-block-size: 100%;
|
||||
|
||||
> .v-main {
|
||||
// overflow-y: auto;
|
||||
|
||||
.v-main__wrap > :first-child {
|
||||
block-size: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ℹ️ Let div/v-layout take full height. E.g. Email App
|
||||
.layout-wrapper.layout-nav-type-horizontal {
|
||||
&.layout-content-height-fixed {
|
||||
> .layout-page-content {
|
||||
// display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Floating navbar styles
|
||||
@if variables.$vertical-nav-navbar-style == "floating" {
|
||||
// ℹ️ Add spacing above navbar if navbar is floating (was in %layout-navbar-fixed placeholder)
|
||||
.layout-wrapper.layout-nav-type-vertical.layout-navbar-fixed {
|
||||
.layout-navbar {
|
||||
inset-block-start: variables.$vertical-nav-floating-navbar-top;
|
||||
}
|
||||
|
||||
/*
|
||||
ℹ️ If it's floating navbar
|
||||
Add `vertical-nav-floating-navbar-top` as margin top to .layout-page-content
|
||||
*/
|
||||
.layout-page-content {
|
||||
margin-block-start: variables.$vertical-nav-floating-navbar-top;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// ℹ️ scrollable-content allows creating fixed header and scrollable content for VNavigationDrawer (Used when perfect scrollbar is used)
|
||||
.scrollable-content {
|
||||
&.v-navigation-drawer {
|
||||
.v-navigation-drawer__content {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ℹ️ adding styling for code tag
|
||||
code {
|
||||
border-radius: 3px;
|
||||
color: rgb(var(--v-code-color));
|
||||
font-size: 90%;
|
||||
font-weight: 400;
|
||||
padding-block: 0.2em;
|
||||
padding-inline: 0.4em;
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
@use "sass:map";
|
||||
@use "@styles/variables/_vuetify.scss";
|
||||
|
||||
@mixin elevation($z, $important: false) {
|
||||
box-shadow: map.get(vuetify.$shadow-key-umbra, $z), map.get(vuetify.$shadow-key-penumbra, $z), map.get(vuetify.$shadow-key-ambient, $z) if($important, !important, null);
|
||||
}
|
||||
|
||||
// ℹ️ This mixin is inspired from vuetify for adding hover styles via before pseudo element
|
||||
@mixin before-pseudo() {
|
||||
position: relative;
|
||||
&::before {
|
||||
position: absolute;
|
||||
border-radius: inherit;
|
||||
background: currentcolor;
|
||||
block-size: 100%;
|
||||
content: "";
|
||||
inline-size: 100%;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin bordered-skin($component, $border-property: "border", $important: false) {
|
||||
#{$component} {
|
||||
// background-color: rgb(var(--v-theme-background));
|
||||
box-shadow: none !important;
|
||||
#{$border-property}: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) if($important, !important, null);
|
||||
}
|
||||
}
|
||||
|
||||
// ℹ️ Inspired from vuetify's active-states mixin
|
||||
// focus => 0.12 & selected => 0.08
|
||||
@mixin selected-states($selector) {
|
||||
// #{$selector} {
|
||||
// opacity: calc(#{map.get(vuetify.$states, "selected")} * var(--v-theme-overlay-multiplier));
|
||||
// }
|
||||
|
||||
// &:hover
|
||||
// #{$selector} {
|
||||
// opacity: calc(#{map.get(vuetify.$states, "selected") + map.get(vuetify.$states, "hover")} * var(--v-theme-overlay-multiplier));
|
||||
// }
|
||||
|
||||
// &:focus-visible
|
||||
// #{$selector} {
|
||||
// opacity: calc(#{map.get(vuetify.$states, "selected") + map.get(vuetify.$states, "focus")} * var(--v-theme-overlay-multiplier));
|
||||
// }
|
||||
|
||||
// @supports not selector(:focus-visible) {
|
||||
// &:focus {
|
||||
// #{$selector} {
|
||||
// opacity: calc(#{map.get(vuetify.$states, "selected") + map.get(vuetify.$states, "focus")} * var(--v-theme-overlay-multiplier));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
|
||||
&:hover
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) + var(--v-hover-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
|
||||
&:focus-visible
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
&:focus {
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
@use "@configured-variables" as variables;
|
||||
@use "@layouts/styles/mixins" as layoutsMixins;
|
||||
|
||||
// 👉 Demo spacers
|
||||
// TODO: Use vuetify SCSS variable here
|
||||
$card-spacer-content: 16px;
|
||||
|
||||
.demo-space-x {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-block-start: -$card-spacer-content;
|
||||
|
||||
& > * {
|
||||
margin-block-start: $card-spacer-content;
|
||||
margin-inline-end: $card-spacer-content;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-space-y {
|
||||
& > * {
|
||||
margin-block-end: $card-spacer-content;
|
||||
|
||||
&:last-child {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Card match height
|
||||
.match-height.v-row {
|
||||
.v-card {
|
||||
block-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Whitespace
|
||||
.whitespace-no-wrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 👉 Colors
|
||||
|
||||
/*
|
||||
ℹ️ Vuetify is applying `.text-white` class to badge icon but don't provide its styles
|
||||
Moreover, we also use this class in some places
|
||||
|
||||
ℹ️ In vuetify 2 with `$color-pack: false` SCSS var config this class was getting generated but this is not the case in v3
|
||||
|
||||
ℹ️ We also need !important to get correct color in badge icon
|
||||
*/
|
||||
.text-white {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.bg-var-theme-background {
|
||||
background-color: rgba(var(--v-theme-background), var(--v-hover-opacity)) !important;
|
||||
}
|
||||
|
||||
// [/^bg-light-(\w+)$/, ([, w]) => ({ backgroundColor: `rgba(var(--v-theme-${w}), var(--v-activated-opacity))` })],
|
||||
@each $color-name in variables.$theme-colors-name {
|
||||
.bg-light-#{$color-name} {
|
||||
background-color: rgba(var(--v-theme-#{$color-name}), var(--v-activated-opacity)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 clamp text
|
||||
.clamp-text {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.leading-normal {
|
||||
line-height: normal !important;
|
||||
}
|
||||
|
||||
// 👉 for rtl only
|
||||
.flip-in-rtl {
|
||||
@include layoutsMixins.rtl {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Carousel
|
||||
.carousel-delimiter-top-end {
|
||||
.v-carousel__controls {
|
||||
justify-content: end;
|
||||
block-size: 40px;
|
||||
inset-block-start: 0;
|
||||
padding-inline: 1rem;
|
||||
|
||||
.v-btn--icon.v-btn--density-default {
|
||||
block-size: calc(var(--v-btn-height) + -10px);
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
inline-size: calc(var(--v-btn-height) + -10px);
|
||||
|
||||
&.v-btn--active {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.v-btn__overlay {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@each $color-name in variables.$theme-colors-name {
|
||||
|
||||
&.dots-active-#{$color-name} {
|
||||
.v-carousel__controls {
|
||||
.v-btn--active {
|
||||
color: rgb(var(--v-theme-#{$color-name})) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-timeline-item {
|
||||
.app-timeline-title {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 1.3125rem;
|
||||
}
|
||||
|
||||
.app-timeline-meta {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
|
||||
font-size: 12px;
|
||||
line-height: 0.875rem;
|
||||
}
|
||||
|
||||
.app-timeline-text {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
font-size: 14px;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
@use "sass:map";
|
||||
@use "sass:list";
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
// Thanks: https://css-tricks.com/snippets/sass/deep-getset-maps/
|
||||
@function map-deep-get($map, $keys...) {
|
||||
@each $key in $keys {
|
||||
$map: map.get($map, $key);
|
||||
}
|
||||
|
||||
@return $map;
|
||||
}
|
||||
|
||||
@function map-deep-set($map, $keys, $value) {
|
||||
$maps: ($map,);
|
||||
$result: null;
|
||||
|
||||
// If the last key is a map already
|
||||
// Warn the user we will be overriding it with $value
|
||||
@if type-of(nth($keys, -1)) == "map" {
|
||||
@warn "The last key you specified is a map; it will be overrided with `#{$value}`.";
|
||||
}
|
||||
|
||||
// If $keys is a single key
|
||||
// Just merge and return
|
||||
@if length($keys) == 1 {
|
||||
@return map-merge($map, ($keys: $value));
|
||||
}
|
||||
|
||||
// Loop from the first to the second to last key from $keys
|
||||
// Store the associated map to this key in the $maps list
|
||||
// If the key doesn't exist, throw an error
|
||||
@for $i from 1 through length($keys) - 1 {
|
||||
$current-key: list.nth($keys, $i);
|
||||
$current-map: list.nth($maps, -1);
|
||||
$current-get: map.get($current-map, $current-key);
|
||||
|
||||
@if not $current-get {
|
||||
@error "Key `#{$key}` doesn't exist at current level in map.";
|
||||
}
|
||||
|
||||
$maps: list.append($maps, $current-get);
|
||||
}
|
||||
|
||||
// Loop from the last map to the first one
|
||||
// Merge it with the previous one
|
||||
@for $i from length($maps) through 1 {
|
||||
$current-map: list.nth($maps, $i);
|
||||
$current-key: list.nth($keys, $i);
|
||||
$current-val: if($i == list.length($maps), $value, $result);
|
||||
$result: map.map-merge($current-map, ($current-key: $current-val));
|
||||
}
|
||||
|
||||
// Return result
|
||||
@return $result;
|
||||
}
|
||||
|
||||
// font size utility classes
|
||||
@each $name, $size in variables.$font-sizes {
|
||||
.text-#{$name} {
|
||||
font-size: $size;
|
||||
line-height: map.get(variables.$font-line-height, $name);
|
||||
}
|
||||
}
|
||||
|
||||
// truncate utility class
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// gap utility class
|
||||
@each $name, $size in variables.$gap {
|
||||
.gap-#{$name} {
|
||||
gap: $size;
|
||||
}
|
||||
|
||||
.gap-x-#{$name} {
|
||||
column-gap: $size;
|
||||
}
|
||||
|
||||
.gap-y-#{$name} {
|
||||
row-gap: $size;
|
||||
}
|
||||
}
|
||||
|
||||
.list-none {
|
||||
list-style-type: none;
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
@use "vuetify/lib/styles/tools/functions" as *;
|
||||
|
||||
/*
|
||||
TODO: Add docs on when to use placeholder vs when to use SASS variable
|
||||
|
||||
Placeholder
|
||||
- When we want to keep customization to our self between templates use it
|
||||
|
||||
Variables
|
||||
- When we want to allow customization from both user and our side
|
||||
- You can also use variable for consistency (e.g. mx 1 rem should be applied to both vertical nav items and vertical nav header)
|
||||
*/
|
||||
|
||||
@forward "@layouts/styles/variables" with (
|
||||
// Adjust z-index so vertical nav & overlay stays on top of v-layout in v-main. E.g. Email app
|
||||
$layout-vertical-nav-z-index: 1004,
|
||||
$layout-overlay-z-index: 1003,
|
||||
);
|
||||
@use "@layouts/styles/variables" as *;
|
||||
|
||||
// 👉 Default layout
|
||||
|
||||
$navbar-high-emphasis-text: true !default;
|
||||
|
||||
// @forward "@layouts/styles/variables" with (
|
||||
// $layout-vertical-nav-width: 350px !default,
|
||||
// );
|
||||
|
||||
$theme-colors-name: (
|
||||
"primary",
|
||||
"secondary",
|
||||
"error",
|
||||
"info",
|
||||
"success",
|
||||
"warning"
|
||||
) !default;
|
||||
|
||||
// 👉 Default layout with vertical nav
|
||||
|
||||
$default-layout-with-vertical-nav-navbar-footer-roundness: 10px !default;
|
||||
|
||||
// 👉 Vertical nav
|
||||
$vertical-nav-background-color-rgb: var(--v-theme-background) !default;
|
||||
$vertical-nav-background-color: rgb(#{$vertical-nav-background-color-rgb}) !default;
|
||||
|
||||
// ℹ️ This is used to keep consistency between nav items and nav header left & right margin
|
||||
// This is used by nav items & nav header
|
||||
$vertical-nav-horizontal-spacing: 1rem !default;
|
||||
$vertical-nav-horizontal-padding: 0.75rem !default;
|
||||
|
||||
// Vertical nav header height. Mostly we will align it with navbar height;
|
||||
$vertical-nav-header-height: $layout-vertical-nav-navbar-height !default;
|
||||
$vertical-nav-navbar-elevation: 3 !default;
|
||||
$vertical-nav-navbar-style: "elevated" !default; // options: elevated, floating
|
||||
$vertical-nav-floating-navbar-top: 1rem !default;
|
||||
|
||||
// Vertical nav header padding
|
||||
$vertical-nav-header-padding: 1rem $vertical-nav-horizontal-padding !default;
|
||||
$vertical-nav-header-inline-spacing: $vertical-nav-horizontal-spacing !default;
|
||||
|
||||
// Move logo when vertical nav is mini (collapsed but not hovered)
|
||||
$vertical-nav-header-logo-translate-x-when-vertical-nav-mini: -4px !default;
|
||||
|
||||
// Space between logo and title
|
||||
$vertical-nav-header-logo-title-spacing: 0.9rem !default;
|
||||
|
||||
// Section title margin top (when its not first child)
|
||||
$vertical-nav-section-title-mt: 1.5rem !default;
|
||||
|
||||
// Section title margin bottom
|
||||
$vertical-nav-section-title-mb: 0.5rem !default;
|
||||
|
||||
// Vertical nav icons
|
||||
$vertical-nav-items-icon-size: 1.5rem !default;
|
||||
$vertical-nav-items-nested-icon-size: 0.9rem !default;
|
||||
$vertical-nav-items-icon-margin-inline-end: 0.5rem !default;
|
||||
|
||||
// Transition duration for nav group arrow
|
||||
$vertical-nav-nav-group-arrow-transition-duration: 0.15s !default;
|
||||
|
||||
// Timing function for nav group arrow
|
||||
$vertical-nav-nav-group-arrow-transition-timing-function: ease-in-out !default;
|
||||
|
||||
// 👉 Horizontal nav
|
||||
|
||||
/*
|
||||
❗ Heads up
|
||||
==================
|
||||
Here we assume we will always use shorthand property which will apply same padding on four side
|
||||
This is because this have been used as value of top property by `.popper-content`
|
||||
*/
|
||||
$horizontal-nav-padding: 0.6875rem !default;
|
||||
|
||||
// Gap between top level horizontal nav items
|
||||
$horizontal-nav-top-level-items-gap: 4px !default;
|
||||
|
||||
// Horizontal nav icons
|
||||
$horizontal-nav-items-icon-size: 1.5rem !default;
|
||||
$horizontal-nav-third-level-icon-size: 0.9rem !default;
|
||||
$horizontal-nav-items-icon-margin-inline-end: 0.625rem !default;
|
||||
|
||||
// ℹ️ We used SCSS variable because we want to allow users to update max height of popper content
|
||||
// 120px is combined height of navbar & horizontal nav
|
||||
$horizontal-nav-popper-content-max-height: calc((var(--vh, 1vh) * 100) - 120px - 4rem) !default;
|
||||
|
||||
// ℹ️ This variable is used for horizontal nav popper content's `margin-top` and "The bridge"'s height. We need to sync both values.
|
||||
$horizontal-nav-popper-content-top: calc($horizontal-nav-padding + 0.375rem) !default;
|
||||
|
||||
// 👉 Plugins
|
||||
|
||||
$plugin-ps-thumb-y-dark: rgba(var(--v-theme-surface-variant), 0.35) !default;
|
||||
|
||||
// 👉 Vuetify
|
||||
|
||||
// Used in src/@core/scss/base/libs/vuetify/_overrides.scss
|
||||
$vuetify-reduce-default-compact-button-icon-size: true !default;
|
||||
|
||||
// 👉 Custom variables
|
||||
// for utility classes
|
||||
$font-sizes: () !default;
|
||||
$font-sizes: map-deep-merge(
|
||||
(
|
||||
"xs": 0.75rem,
|
||||
"sm": 0.875rem,
|
||||
"base": 1rem,
|
||||
"lg": 1.125rem,
|
||||
"xl": 1.25rem,
|
||||
"2xl": 1.5rem,
|
||||
"3xl": 1.875rem,
|
||||
"4xl": 2.25rem,
|
||||
"5xl": 3rem,
|
||||
"6xl": 3.75rem,
|
||||
"7xl": 4.5rem,
|
||||
"8xl": 6rem,
|
||||
"9xl": 8rem
|
||||
),
|
||||
$font-sizes
|
||||
);
|
||||
|
||||
// line height
|
||||
$font-line-height: () !default;
|
||||
$font-line-height: map-deep-merge(
|
||||
(
|
||||
"xs": 1rem,
|
||||
"sm": 1.25rem,
|
||||
"base": 1.5rem,
|
||||
"lg": 1.75rem,
|
||||
"xl": 1.75rem,
|
||||
"2xl": 2rem,
|
||||
"3xl": 2.25rem,
|
||||
"4xl": 2.5rem,
|
||||
"5xl": 1,
|
||||
"6xl": 1,
|
||||
"7xl": 1,
|
||||
"8xl": 1,
|
||||
"9xl": 1
|
||||
),
|
||||
$font-line-height
|
||||
);
|
||||
|
||||
// gap utility class
|
||||
$gap: () !default;
|
||||
$gap: map-deep-merge(
|
||||
(
|
||||
"0": 0,
|
||||
"1": 0.25rem,
|
||||
"2": 0.5rem,
|
||||
"3": 0.75rem,
|
||||
"4": 1rem,
|
||||
"5": 1.25rem,
|
||||
"6":1.5rem,
|
||||
"7": 1.75rem,
|
||||
"8": 2rem,
|
||||
"9": 2.25rem,
|
||||
"10": 2.5rem,
|
||||
"11": 2.75rem,
|
||||
"12": 3rem,
|
||||
"14": 3.5rem,
|
||||
"16": 4rem,
|
||||
"20": 5rem,
|
||||
"24": 6rem,
|
||||
"28": 7rem,
|
||||
"32": 8rem,
|
||||
"36": 9rem,
|
||||
"40": 10rem,
|
||||
"44": 11rem,
|
||||
"48": 12rem,
|
||||
"52": 13rem,
|
||||
"56": 14rem,
|
||||
"60": 15rem,
|
||||
"64": 16rem,
|
||||
"72": 18rem,
|
||||
"80": 20rem,
|
||||
"96": 24rem
|
||||
),
|
||||
$gap
|
||||
);
|
||||
@@ -1,250 +0,0 @@
|
||||
@use "@core/scss/base/placeholders" as *;
|
||||
@use "@core/scss/template/placeholders" as *;
|
||||
@use "@layouts/styles/mixins" as layoutsMixins;
|
||||
@use "@configured-variables" as variables;
|
||||
@use "@core/scss/base/mixins" as mixins;
|
||||
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
|
||||
|
||||
.layout-nav-type-vertical {
|
||||
// 👉 Layout Vertical nav
|
||||
.layout-vertical-nav {
|
||||
$sl-layout-nav-type-vertical: &;
|
||||
|
||||
@extend %nav;
|
||||
|
||||
@at-root {
|
||||
// ℹ️ Add styles for collapsed vertical nav
|
||||
.layout-vertical-nav-collapsed#{$sl-layout-nav-type-vertical}.hovered {
|
||||
@include mixins.elevation(6);
|
||||
}
|
||||
}
|
||||
|
||||
background-color: variables.$vertical-nav-background-color;
|
||||
|
||||
// 👉 Nav header
|
||||
.nav-header {
|
||||
overflow: hidden;
|
||||
padding: variables.$vertical-nav-header-padding;
|
||||
margin-inline: variables.$vertical-nav-header-inline-spacing;
|
||||
min-block-size: variables.$vertical-nav-header-height;
|
||||
|
||||
// TEMPLATE: Check if we need to move this to master
|
||||
.app-logo {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.25s ease-in-out;
|
||||
|
||||
@at-root {
|
||||
// Move logo a bit to align center with the icons in vertical nav mini variant
|
||||
.layout-vertical-nav-collapsed#{$sl-layout-nav-type-vertical}:not(.hovered) .nav-header .app-logo {
|
||||
transform: translateX(variables.$vertical-nav-header-logo-translate-x-when-vertical-nav-mini);
|
||||
|
||||
@include layoutsMixins.rtl {
|
||||
transform: translateX(-(variables.$vertical-nav-header-logo-translate-x-when-vertical-nav-mini));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-title {
|
||||
margin-inline-start: variables.$vertical-nav-header-logo-title-spacing;
|
||||
}
|
||||
|
||||
.header-action {
|
||||
@extend %nav-header-action;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Nav items shadow
|
||||
.vertical-nav-items-shadow {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background:
|
||||
linear-gradient(
|
||||
rgb(#{variables.$vertical-nav-background-color-rgb}) 5%,
|
||||
rgba(#{variables.$vertical-nav-background-color-rgb}, 75%) 45%,
|
||||
rgba(#{variables.$vertical-nav-background-color-rgb}, 20%) 80%,
|
||||
transparent
|
||||
);
|
||||
block-size: 55px;
|
||||
inline-size: 100%;
|
||||
inset-block-start: calc(#{variables.$vertical-nav-header-height} - 2px);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
will-change: opacity;
|
||||
|
||||
@include layoutsMixins.rtl {
|
||||
transform: translateX(8px);
|
||||
}
|
||||
}
|
||||
|
||||
&.scrolled {
|
||||
.vertical-nav-items-shadow {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ps__rail-y {
|
||||
// ℹ️ Setting z-index: 1 will make perfect scrollbar thumb appear on top of vertical nav items shadow
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
// 👉 Nav section title
|
||||
.nav-section-title {
|
||||
@extend %vertical-nav-item;
|
||||
@extend %vertical-nav-section-title;
|
||||
|
||||
margin-block-end: variables.$vertical-nav-section-title-mb;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-block-start: variables.$vertical-nav-section-title-mt;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
margin-inline: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Nav item badge
|
||||
.nav-item-badge {
|
||||
@extend %vertical-nav-item-badge;
|
||||
}
|
||||
|
||||
// 👉 Nav group & Link
|
||||
.nav-link,
|
||||
.nav-group {
|
||||
overflow: hidden;
|
||||
|
||||
> :first-child {
|
||||
@extend %vertical-nav-item;
|
||||
@extend %vertical-nav-item-interactive;
|
||||
}
|
||||
|
||||
.nav-item-icon {
|
||||
@extend %vertical-nav-items-icon;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: var(--v-disabled-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Vertical nav link
|
||||
.nav-link {
|
||||
@extend %nav-link;
|
||||
|
||||
> .router-link-exact-active {
|
||||
@extend %nav-link-active;
|
||||
}
|
||||
|
||||
> a {
|
||||
// Adds before psudo element to style hover state
|
||||
@include mixins.before-pseudo;
|
||||
|
||||
// Adds vuetify states
|
||||
@include vuetifyStates.states($active: false);
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Vertical nav group
|
||||
.nav-group {
|
||||
// Reduce the size of icon if link/group is inside group
|
||||
.nav-group,
|
||||
.nav-link {
|
||||
.nav-item-icon {
|
||||
@extend %vertical-nav-items-nested-icon;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide icons after 2nd level
|
||||
& .nav-group {
|
||||
.nav-link,
|
||||
.nav-group {
|
||||
.nav-item-icon {
|
||||
@extend %vertical-nav-items-icon-after-2nd-level;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-group-arrow {
|
||||
flex-shrink: 0;
|
||||
transform-origin: center;
|
||||
transition: transform variables.$vertical-nav-nav-group-arrow-transition-duration variables.$vertical-nav-nav-group-arrow-transition-timing-function;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
// Rotate arrow icon if group is opened
|
||||
&.open {
|
||||
> .nav-group-label .nav-group-arrow {
|
||||
transform: rotateZ(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Nav group label
|
||||
> :first-child {
|
||||
// Adds before psudo element to style hover state
|
||||
@include mixins.before-pseudo;
|
||||
|
||||
// Adds vuetify states
|
||||
@include vuetifyStates.states($active: false);
|
||||
}
|
||||
|
||||
// Active & open states for nav group label
|
||||
&.active,
|
||||
&.open {
|
||||
> :first-child {
|
||||
@extend %vertical-nav-group-open-active;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SECTION: Transitions
|
||||
.vertical-nav-section-title-enter-active,
|
||||
.vertical-nav-section-title-leave-active {
|
||||
transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.vertical-nav-section-title-enter-from,
|
||||
.vertical-nav-section-title-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(15px);
|
||||
|
||||
@include layoutsMixins.rtl {
|
||||
transform: translateX(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
.transition-slide-x-enter-active,
|
||||
.transition-slide-x-leave-active {
|
||||
transition: opacity 0.1s ease-in-out, transform 0.12s ease-in-out;
|
||||
}
|
||||
|
||||
.transition-slide-x-enter-from,
|
||||
.transition-slide-x-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-15px);
|
||||
|
||||
@include layoutsMixins.rtl {
|
||||
transform: translateX(15px);
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-nav-app-title-enter-active,
|
||||
.vertical-nav-app-title-leave-active {
|
||||
transition: opacity 0.1s ease-in-out, transform 0.12s ease-in-out;
|
||||
}
|
||||
|
||||
.vertical-nav-app-title-enter-from,
|
||||
.vertical-nav-app-title-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-15px);
|
||||
|
||||
@include layoutsMixins.rtl {
|
||||
transform: translateX(15px);
|
||||
}
|
||||
}
|
||||
|
||||
// !SECTION
|
||||
@@ -1 +0,0 @@
|
||||
@use "overrides";
|
||||
@@ -1,49 +0,0 @@
|
||||
// 👉 Shadow opacities
|
||||
$shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity);
|
||||
$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity);
|
||||
$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity);
|
||||
|
||||
// 👉 Card transition properties
|
||||
$card-transition-property-custom: box-shadow, opacity;
|
||||
|
||||
@forward "vuetify/settings" with (
|
||||
// 👉 General settings
|
||||
$color-pack: false !default,
|
||||
|
||||
// 👉 Shadow opacity
|
||||
$shadow-key-umbra-opacity: $shadow-key-umbra-opacity-custom !default,
|
||||
$shadow-key-penumbra-opacity: $shadow-key-penumbra-opacity-custom !default,
|
||||
$shadow-key-ambient-opacity: $shadow-key-ambient-opacity-custom !default,
|
||||
|
||||
// 👉 Card
|
||||
$card-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default,
|
||||
$card-elevation: 6 !default,
|
||||
$card-title-line-height: 1.6 !default,
|
||||
$card-actions-min-height: unset !default,
|
||||
$card-text-padding: 1.25rem !default,
|
||||
$card-item-padding: 1.25rem !default,
|
||||
$card-actions-padding: 0 12px 12px !default,
|
||||
$card-transition-property: $card-transition-property-custom !default,
|
||||
$card-subtitle-opacity: 1 !default,
|
||||
|
||||
// 👉 Expansion Panel
|
||||
$expansion-panel-active-title-min-height: 48px !default,
|
||||
|
||||
// 👉 List
|
||||
$list-item-icon-margin-end: 16px !default,
|
||||
$list-item-icon-margin-start: 16px !default,
|
||||
$list-item-subtitle-opacity: 1 !default,
|
||||
|
||||
// 👉 Tooltip
|
||||
$tooltip-background-color: rgba(59, 55, 68, 0.9) !default,
|
||||
$tooltip-text-color: rgb(var(--v-theme-on-primary)) !default,
|
||||
$tooltip-font-size: 0.75rem !default,
|
||||
|
||||
$button-icon-density: ("default": 2, "comfortable": 0, "compact": -1 ) !default,
|
||||
|
||||
// 👉 VTimeline
|
||||
$timeline-dot-size: 34px !default,
|
||||
|
||||
// 👉 VOverlay
|
||||
$overlay-opacity: 1 !default,
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
@forward "vertical-nav";
|
||||
@forward "nav";
|
||||
@forward "default-layout";
|
||||
@forward "default-layout-vertical-nav";
|
||||
@forward "misc";
|
||||
@@ -1,7 +0,0 @@
|
||||
%blurry-bg {
|
||||
/* stylelint-disable property-no-vendor-prefix */
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
backdrop-filter: blur(6px);
|
||||
/* stylelint-enable */
|
||||
background-color: rgb(var(--v-theme-surface), 0.8);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
|
||||
@use "@core/scss/base/mixins";
|
||||
|
||||
// ℹ️ This is common style that needs to be applied to both navs
|
||||
%nav {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
|
||||
.nav-item-title {
|
||||
letter-spacing: 0.15px;
|
||||
}
|
||||
|
||||
.nav-section-title {
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Active nav link styles for horizontal & vertical nav
|
||||
|
||||
For horizontal nav it will be only applied to top level nav items
|
||||
For vertical nav it will be only applied to nav links (not nav groups)
|
||||
*/
|
||||
%nav-link-active {
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
color: rgb(var(--v-theme-on-primary));
|
||||
|
||||
@include mixins.elevation(3);
|
||||
}
|
||||
|
||||
%nav-link {
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
@use "@core/scss/base/mixins";
|
||||
@use "@configured-variables" as variables;
|
||||
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
|
||||
|
||||
%nav-header-action {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
// Nav items styles (including section title)
|
||||
%vertical-nav-item {
|
||||
margin-block: 0;
|
||||
margin-inline: variables.$vertical-nav-horizontal-spacing;
|
||||
padding-block: 0;
|
||||
padding-inline: variables.$vertical-nav-horizontal-padding;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// This is same as `%vertical-nav-item` except section title is excluded
|
||||
%vertical-nav-item-interactive {
|
||||
border-radius: 0.4rem;
|
||||
block-size: 2.75rem;
|
||||
|
||||
/*
|
||||
ℹ️ We will use `margin-block-end` instead of `margin-block` to give more space for shadow to appear.
|
||||
With `margin-block`, due to small space (space gets divided between top & bottom) shadow cuts
|
||||
*/
|
||||
margin-block-end: 0.375rem;
|
||||
}
|
||||
|
||||
// Common styles for nav item icon styles
|
||||
// ℹ️ Nav group's children icon styles are not here (Adjusts height, width & margin)
|
||||
%vertical-nav-items-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: variables.$vertical-nav-items-icon-size;
|
||||
margin-inline-end: variables.$vertical-nav-items-icon-margin-inline-end;
|
||||
}
|
||||
|
||||
// ℹ️ Icon styling for icon nested inside another nav item (2nd level)
|
||||
%vertical-nav-items-nested-icon {
|
||||
/*
|
||||
ℹ️ `margin-inline` will be (normal icon font-size - small icon font-size) / 2
|
||||
(1.5rem - 0.9rem) / 2 => 0.6rem / 2 => 0.3rem
|
||||
*/
|
||||
$vertical-nav-items-nested-icon-margin-inline: calc((variables.$vertical-nav-items-icon-size - variables.$vertical-nav-items-nested-icon-size) / 2);
|
||||
|
||||
font-size: variables.$vertical-nav-items-nested-icon-size;
|
||||
margin-inline-end: $vertical-nav-items-nested-icon-margin-inline + variables.$vertical-nav-items-icon-margin-inline-end;
|
||||
margin-inline-start: $vertical-nav-items-nested-icon-margin-inline;
|
||||
}
|
||||
|
||||
%vertical-nav-items-icon-after-2nd-level {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
// Open & Active nav group styles
|
||||
%vertical-nav-group-open-active {
|
||||
@include mixins.selected-states("&::before");
|
||||
}
|
||||
|
||||
// Section title
|
||||
%vertical-nav-section-title {
|
||||
// ℹ️ Setting height will prevent jerking when text & icon is toggled
|
||||
block-size: 1.5rem;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
// Vertical nav item badge styles
|
||||
%vertical-nav-item-badge {
|
||||
display: inline-block;
|
||||
border-radius: 1.5rem;
|
||||
font-size: 0.8em;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
padding-block: 0.25em;
|
||||
padding-inline: 0.55em;
|
||||
text-align: center;
|
||||
vertical-align: baseline;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1,33 +1,5 @@
|
||||
@use "sass:map";
|
||||
@use "template/index";
|
||||
|
||||
// Layout
|
||||
@use "vertical-nav";
|
||||
@use "default-layout";
|
||||
|
||||
// Components
|
||||
@use "components";
|
||||
|
||||
// Utilities
|
||||
@use "utilities";
|
||||
|
||||
// Misc
|
||||
@use "misc";
|
||||
|
||||
// Dark
|
||||
@use "dark";
|
||||
|
||||
a {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// Vuetify 3 don't provide margin bottom style like vuetify 2
|
||||
p {
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
// Iconify icon size
|
||||
svg.iconify {
|
||||
block-size: 1em;
|
||||
inline-size: 1em;
|
||||
}
|
||||
// 保留这个引用以向后兼容,但实际功能已经移至template/index.scss
|
||||
@use "variables";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
$shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity);
|
||||
$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity);
|
||||
$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity);
|
||||
$font-family-custom: inter, sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
$font-family-custom: 'Inter', 'Noto Sans SC', sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
|
||||
// 👉 Card transition properties
|
||||
$card-transition-property-custom: box-shadow, opacity;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
|
||||
@use "@configured-variables" as variables;
|
||||
@use "mixins";
|
||||
@use "@core/scss/base/mixins" as mixins_base;
|
||||
|
||||
// 👉 Alert
|
||||
.v-alert {
|
||||
@@ -190,5 +189,5 @@
|
||||
|
||||
// 👉 SnackBar
|
||||
.v-snackbar--variant-elevated {
|
||||
@include mixins_base.elevation(6);
|
||||
@include mixins.elevation(6);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
@use "@configured-variables" as variables;
|
||||
@use "@core/scss/base/placeholders" as *;
|
||||
@use "@core/scss/template/placeholders" as *;
|
||||
@use "placeholders" as *;
|
||||
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
|
||||
@use "misc";
|
||||
@use "@core/scss/base/mixins";
|
||||
@use "../misc";
|
||||
@use "mixins";
|
||||
|
||||
$header: ".layout-navbar";
|
||||
|
||||
@@ -23,7 +22,7 @@ $header: ".layout-navbar";
|
||||
// If navbar is contained => Add border radius to header
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
#{$header} {
|
||||
border-radius: 0 0 variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
|
||||
// border-radius: 0 0 variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +63,7 @@ $header: ".layout-navbar";
|
||||
|
||||
#{$header} {
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
|
||||
// border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
|
||||
}
|
||||
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
@@ -101,4 +100,4 @@ $header: ".layout-navbar";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@use "sass:map";
|
||||
@use "vuetify/lib/styles/settings" as vuetify_settings;
|
||||
@use "@styles/variables/_vuetify.scss" as vuetify;
|
||||
|
||||
@mixin avatar-font-sizes($map: $avatar-sizes) {
|
||||
@each $sizeName, $multiplier in vuetify_settings.$size-scales {
|
||||
@@ -11,3 +12,90 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin elevation($z, $important: false) {
|
||||
box-shadow: map.get(vuetify.$shadow-key-umbra, $z), map.get(vuetify.$shadow-key-penumbra, $z), map.get(vuetify.$shadow-key-ambient, $z) if($important, !important, null);
|
||||
}
|
||||
|
||||
@mixin before-pseudo() {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
border-radius: inherit;
|
||||
background: currentcolor;
|
||||
block-size: 100%;
|
||||
content: "";
|
||||
inline-size: 100%;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin bordered-skin($component, $border-property: "border", $important: false) {
|
||||
#{$component} {
|
||||
box-shadow: none !important;
|
||||
#{$border-property}: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) if($important, !important, null);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin selected-states($selector) {
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
|
||||
&:hover
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) + var(--v-hover-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
|
||||
&:focus-visible
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
&:focus {
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin push-anchors() {
|
||||
:target {
|
||||
scroll-margin-block-start: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin xs {
|
||||
@media (width >= 0) and (width <= 599.98px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin sm {
|
||||
@media (width >= 600px) and (width <= 959.98px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin md {
|
||||
@media (width >= 960px) and (width <= 1279.98px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin lg {
|
||||
@media (width >= 1280px) and (width <= 1919.98px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin xl {
|
||||
@media (width >= 1920px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@use "sass:map";
|
||||
@use "utils";
|
||||
@use "vuetify/lib/styles/tools/functions" as *;
|
||||
|
||||
$vertical-nav-horizontal-padding-custom: 1.375rem 1rem;
|
||||
|
||||
@@ -13,15 +14,16 @@ $vertical-nav-horizontal-padding-custom: 1.375rem 1rem;
|
||||
$vertical-nav-horizontal-padding-start: utils.get-first-value($vertical-nav-horizontal-padding-custom) !default;
|
||||
$vertical-nav-items-icon-margin-inline-end: 0.625rem !default;
|
||||
|
||||
@forward "@core/scss/base/variables" with (
|
||||
$layout-vertical-nav-collapsed-width: 68px !default,
|
||||
// ℹ️ This is used to keep consistency between nav items and nav header left & right margin
|
||||
// This is used by nav items & nav header
|
||||
$vertical-nav-horizontal-spacing: 0 1.125rem !default,
|
||||
$vertical-nav-horizontal-padding: $vertical-nav-horizontal-padding-custom !default,
|
||||
// Vertical nav header padding
|
||||
$vertical-nav-header-padding: 1rem 0.25rem 1rem $vertical-nav-horizontal-padding-start !default,
|
||||
);
|
||||
// Vertical Nav Configuration
|
||||
$vertical-nav-collapsed-width: 68px !default;
|
||||
|
||||
// ℹ️ This is used to keep consistency between nav items and nav header left & right margin
|
||||
// This is used by nav items & nav header
|
||||
$vertical-nav-horizontal-spacing: 0 1.125rem !default;
|
||||
$vertical-nav-horizontal-padding: $vertical-nav-horizontal-padding-custom !default;
|
||||
|
||||
// Vertical nav header padding
|
||||
$vertical-nav-header-padding: 1rem 0.25rem 1rem $vertical-nav-horizontal-padding-start !default;
|
||||
|
||||
// 👉 Custom Variables
|
||||
$avatar-font-sizes: (
|
||||
@@ -31,3 +33,195 @@ $avatar-font-sizes: (
|
||||
"large":20,
|
||||
"x-large":24
|
||||
) !default;
|
||||
|
||||
// 合并两个文件中的@forward配置
|
||||
@forward "@layouts/styles/variables" with (
|
||||
// 来自_variables.scss的配置
|
||||
$layout-vertical-nav-collapsed-width: 68px !default,
|
||||
|
||||
// 来自template/_variables.scss的配置
|
||||
$layout-vertical-nav-z-index: 1004,
|
||||
$layout-overlay-z-index: 1003
|
||||
);
|
||||
|
||||
// 使用命名空间来避免变量冲突
|
||||
@use "@layouts/styles/variables" as layouts-vars;
|
||||
|
||||
$theme-colors-name: (
|
||||
"primary",
|
||||
"secondary",
|
||||
"error",
|
||||
"info",
|
||||
"success",
|
||||
"warning"
|
||||
) !default;
|
||||
|
||||
// 👉 Default layout with vertical nav
|
||||
|
||||
$default-layout-with-vertical-nav-navbar-footer-roundness: 10px !default;
|
||||
|
||||
// 👉 Vertical nav
|
||||
$vertical-nav-background-color-rgb: var(--v-theme-background) !default;
|
||||
$vertical-nav-background-color: rgb(#{$vertical-nav-background-color-rgb}) !default;
|
||||
|
||||
// ℹ️ This is used to keep consistency between nav items and nav header left & right margin
|
||||
// This is used by nav items & nav header
|
||||
$vertical-nav-horizontal-spacing: 1rem !default;
|
||||
$vertical-nav-horizontal-padding: 0.75rem !default;
|
||||
|
||||
// Vertical nav header height. Mostly we will align it with navbar height;
|
||||
$vertical-nav-header-height: layouts-vars.$layout-vertical-nav-navbar-height !default;
|
||||
$vertical-nav-navbar-elevation: 3 !default;
|
||||
$vertical-nav-navbar-style: "elevated" !default; // options: elevated, floating
|
||||
$vertical-nav-floating-navbar-top: 1rem !default;
|
||||
|
||||
// Vertical nav header padding
|
||||
$vertical-nav-header-padding: 1rem $vertical-nav-horizontal-padding !default;
|
||||
$vertical-nav-header-inline-spacing: $vertical-nav-horizontal-spacing !default;
|
||||
|
||||
// Move logo when vertical nav is mini (collapsed but not hovered)
|
||||
$vertical-nav-header-logo-translate-x-when-vertical-nav-mini: -4px !default;
|
||||
|
||||
// Space between logo and title
|
||||
$vertical-nav-header-logo-title-spacing: 0.9rem !default;
|
||||
|
||||
// Section title margin top (when its not first child)
|
||||
$vertical-nav-section-title-mt: 1.5rem !default;
|
||||
|
||||
// Section title margin bottom
|
||||
$vertical-nav-section-title-mb: 0.5rem !default;
|
||||
|
||||
// Vertical nav icons
|
||||
$vertical-nav-items-icon-size: 1.5rem !default;
|
||||
$vertical-nav-items-nested-icon-size: 0.9rem !default;
|
||||
$vertical-nav-items-icon-margin-inline-end: 0.5rem !default;
|
||||
|
||||
// Transition duration for nav group arrow
|
||||
$vertical-nav-nav-group-arrow-transition-duration: 0.15s !default;
|
||||
|
||||
// Timing function for nav group arrow
|
||||
$vertical-nav-nav-group-arrow-transition-timing-function: ease-in-out !default;
|
||||
|
||||
// 👉 Horizontal nav
|
||||
|
||||
/*
|
||||
❗ Heads up
|
||||
==================
|
||||
Here we assume we will always use shorthand property which will apply same padding on four side
|
||||
This is because this have been used as value of top property by `.popper-content`
|
||||
*/
|
||||
$horizontal-nav-padding: 0.6875rem !default;
|
||||
|
||||
// Gap between top level horizontal nav items
|
||||
$horizontal-nav-top-level-items-gap: 4px !default;
|
||||
|
||||
// Horizontal nav icons
|
||||
$horizontal-nav-items-icon-size: 1.5rem !default;
|
||||
$horizontal-nav-third-level-icon-size: 0.9rem !default;
|
||||
$horizontal-nav-items-icon-margin-inline-end: 0.625rem !default;
|
||||
|
||||
// ℹ️ We used SCSS variable because we want to allow users to update max height of popper content
|
||||
// 120px is combined height of navbar & horizontal nav
|
||||
$horizontal-nav-popper-content-max-height: calc((var(--vh, 1vh) * 100) - 120px - 4rem) !default;
|
||||
|
||||
// ℹ️ This variable is used for horizontal nav popper content's `margin-top` and "The bridge"'s height. We need to sync both values.
|
||||
$horizontal-nav-popper-content-top: calc($horizontal-nav-padding + 0.375rem) !default;
|
||||
|
||||
// 👉 Plugins
|
||||
|
||||
$plugin-ps-thumb-y-dark: rgba(var(--v-theme-surface-variant), 0.35) !default;
|
||||
|
||||
// 👉 Vuetify
|
||||
|
||||
// Used in src/@core/scss/base/libs/vuetify/_overrides.scss
|
||||
$vuetify-reduce-default-compact-button-icon-size: true !default;
|
||||
|
||||
// 👉 Custom variables
|
||||
// for utility classes
|
||||
$font-sizes: () !default;
|
||||
$font-sizes: map-deep-merge(
|
||||
(
|
||||
"xs": 0.75rem,
|
||||
"sm": 0.875rem,
|
||||
"base": 1rem,
|
||||
"lg": 1.125rem,
|
||||
"xl": 1.25rem,
|
||||
"2xl": 1.5rem,
|
||||
"3xl": 1.875rem,
|
||||
"4xl": 2.25rem,
|
||||
"5xl": 3rem,
|
||||
"6xl": 3.75rem,
|
||||
"7xl": 4.5rem,
|
||||
"8xl": 6rem,
|
||||
"9xl": 8rem
|
||||
),
|
||||
$font-sizes
|
||||
);
|
||||
|
||||
// line height
|
||||
$font-line-height: () !default;
|
||||
$font-line-height: map-deep-merge(
|
||||
(
|
||||
"xs": 1rem,
|
||||
"sm": 1.25rem,
|
||||
"base": 1.5rem,
|
||||
"lg": 1.75rem,
|
||||
"xl": 1.75rem,
|
||||
"2xl": 2rem,
|
||||
"3xl": 2.25rem,
|
||||
"4xl": 2.5rem,
|
||||
"5xl": 1,
|
||||
"6xl": 1,
|
||||
"7xl": 1,
|
||||
"8xl": 1,
|
||||
"9xl": 1
|
||||
),
|
||||
$font-line-height
|
||||
);
|
||||
|
||||
// gap utility class
|
||||
$gap: () !default;
|
||||
$gap: map-deep-merge(
|
||||
(
|
||||
"0": 0,
|
||||
"1": 0.25rem,
|
||||
"2": 0.5rem,
|
||||
"3": 0.75rem,
|
||||
"4": 1rem,
|
||||
"5": 1.25rem,
|
||||
"6":1.5rem,
|
||||
"7": 1.75rem,
|
||||
"8": 2rem,
|
||||
"9": 2.25rem,
|
||||
"10": 2.5rem,
|
||||
"11": 2.75rem,
|
||||
"12": 3rem,
|
||||
"14": 3.5rem,
|
||||
"16": 4rem,
|
||||
"20": 5rem,
|
||||
"24": 6rem,
|
||||
"28": 7rem,
|
||||
"32": 8rem,
|
||||
"36": 9rem,
|
||||
"40": 10rem,
|
||||
"44": 11rem,
|
||||
"48": 12rem,
|
||||
"52": 13rem,
|
||||
"56": 14rem,
|
||||
"60": 15rem,
|
||||
"64": 16rem,
|
||||
"72": 18rem,
|
||||
"80": 20rem,
|
||||
"96": 24rem
|
||||
),
|
||||
$gap
|
||||
);
|
||||
|
||||
// Avatar sizes map
|
||||
$avatar-font-sizes: (
|
||||
"x-small": 0.625rem,
|
||||
"small": 0.75rem,
|
||||
"default": 0.875rem,
|
||||
"large": 1rem,
|
||||
"x-large": 1.125rem,
|
||||
) !default;
|
||||
|
||||
@@ -1,8 +1,42 @@
|
||||
@use "sass:map";
|
||||
@use "@core/scss/base";
|
||||
|
||||
// Layout
|
||||
@use "../vertical-nav";
|
||||
@use "../default-layout";
|
||||
@use "default-layout-w-vertical-nav";
|
||||
|
||||
// Components
|
||||
@use "components";
|
||||
|
||||
// Utilities
|
||||
@use "utilities";
|
||||
@use "../utils";
|
||||
|
||||
// Misc
|
||||
@use "../misc";
|
||||
|
||||
// Dark
|
||||
@use "../dark";
|
||||
|
||||
// Variables
|
||||
@use "variables";
|
||||
|
||||
// libs
|
||||
@use "libs/perfect-scrollbar";
|
||||
@use "libs/vuetify";
|
||||
|
||||
a {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// Vuetify 3 don't provide margin bottom style like vuetify 2
|
||||
p {
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
// Iconify icon size
|
||||
svg.iconify {
|
||||
block-size: 1em;
|
||||
inline-size: 1em;
|
||||
}
|
||||
|
||||
@@ -1,76 +1,89 @@
|
||||
@use "@styles/variables/_vuetify.scss" as vuetify;
|
||||
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
|
||||
@use "@layouts/styles/mixins" as layoutsMixins;
|
||||
@use "@core/scss/base/mixins";
|
||||
@use "@configureTheme" as theme;
|
||||
@use "@configured-variables" as variables;
|
||||
@use "../mixins";
|
||||
|
||||
.v-application .apexcharts-canvas {
|
||||
&line[stroke="transparent"] {
|
||||
display: "none";
|
||||
// 👉 Apex chart
|
||||
.apexcharts-canvas {
|
||||
// For RTL alignment
|
||||
.apexcharts-yaxis-texts-g {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
.apexcharts-tooltip {
|
||||
@include mixins.elevation(3);
|
||||
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
background: rgb(var(--v-theme-surface));
|
||||
line-height: 1.5;
|
||||
|
||||
.apexcharts-tooltip-title {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
background: rgb(var(--v-theme-surface));
|
||||
font-weight: 500;
|
||||
margin-block-end: 0.25rem;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
font-size: inherit;
|
||||
gap: 0.5rem;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-text-label,
|
||||
.apexcharts-tooltip-text-value {
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-series-group {
|
||||
padding-block: 0 0.5rem;
|
||||
padding-inline: 1rem;
|
||||
|
||||
&:last-child {
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
&.active {
|
||||
padding-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.apexcharts-theme-light {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
&.apexcharts-theme-dark {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-series-group:first-of-type {
|
||||
padding-block-end: 0;
|
||||
border-color: rgb(var(--v-border-color));
|
||||
background: rgb(var(--v-theme-surface));
|
||||
box-shadow: none;
|
||||
|
||||
.apexcharts-tooltip-text-label,
|
||||
.apexcharts-tooltip-text-value {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
background: rgb(var(--v-theme-grey-50));
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
|
||||
&::after {
|
||||
border-block-end-color: rgb(var(--v-theme-grey-50));
|
||||
}
|
||||
|
||||
&::before {
|
||||
border-block-end-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
.apexcharts-marker {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
// 👉 stroke-dasharray
|
||||
.apexcharts-radialbar,
|
||||
.apexcharts-radialbar-slice-current {
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip,
|
||||
.apexcharts-yaxistooltip {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
background: rgb(var(--v-theme-grey-50));
|
||||
|
||||
&::after {
|
||||
border-inline-start-color: rgb(var(--v-theme-grey-50));
|
||||
}
|
||||
border-color: rgb(var(--v-border-color));
|
||||
background: rgb(var(--v-theme-surface));
|
||||
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
border-inline-start-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip-text,
|
||||
.apexcharts-yaxistooltip-text {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
.apexcharts-yaxis .apexcharts-yaxis-texts-g .apexcharts-yaxis-label {
|
||||
@include layoutsMixins.rtl {
|
||||
text-anchor: start;
|
||||
border-block-end-color: rgb(var(--v-border-color));
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Text color
|
||||
.apexcharts-text,
|
||||
.apexcharts-tooltip-text,
|
||||
.apexcharts-datalabel-label,
|
||||
@@ -78,23 +91,16 @@
|
||||
.apexcharts-xaxistooltip-text,
|
||||
.apexcharts-yaxistooltip-text,
|
||||
.apexcharts-legend-text {
|
||||
font-family: vuetify.$body-font-family !important;
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity)) !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
.apexcharts-pie-label {
|
||||
fill: white;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.apexcharts-marker {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.apexcharts-legend-marker {
|
||||
margin-inline-end: 0.3875rem !important;
|
||||
|
||||
@include layoutsMixins.rtl {
|
||||
margin-inline-end: 0.75rem !important;
|
||||
// 👉 Annotation Label
|
||||
.apexcharts-annotation-rect {
|
||||
&.apexcharts-xaxis-annotation-rect,
|
||||
&.apexcharts-yaxis-annotation-rect {
|
||||
fill-opacity: 0.05;
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@use "@core/scss/base/mixins";
|
||||
@use "../mixins";
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
.v-application .fc {
|
||||
--fc-today-bg-color: rgba(var(--v-theme-on-surface), 0.04);
|
||||
@@ -16,16 +17,20 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.fc-toolbar-title {
|
||||
display: inline-block;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
|
||||
.fc-col-header-cell-cushion {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fc-toolbar .fc-toolbar-title {
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
|
||||
.fc-event-time {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
@@ -95,6 +100,7 @@
|
||||
gap: 1rem 0.5rem;
|
||||
}
|
||||
|
||||
// 👉 Toolbar Chunk and Button Group
|
||||
.fc-toolbar-chunk {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -102,19 +108,38 @@
|
||||
.fc-button-group {
|
||||
.fc-button-primary {
|
||||
&,
|
||||
&:focus,
|
||||
&:hover,
|
||||
&:not(.disabled):active {
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none !important;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 sidebar toggler
|
||||
.fc-drawerToggler-button {
|
||||
display: none;
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(94,86,105,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
|
||||
background-position: 50%;
|
||||
background-repeat: no-repeat;
|
||||
block-size: 1.5625rem;
|
||||
font-size: 0;
|
||||
inline-size: 1.5625rem;
|
||||
margin-inline-end: 0.25rem;
|
||||
|
||||
@media (width <= 1264px) {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.v-theme--dark & {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(232,232,241,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
|
||||
// Special styling for the last toolbar chunk
|
||||
&:last-child {
|
||||
.fc-button-group {
|
||||
border: 0.0625rem solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
@@ -139,13 +164,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.fc-toolbar-title {
|
||||
display: inline-block;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fc-scrollgrid-section {
|
||||
th {
|
||||
border-inline: 0;
|
||||
@@ -217,37 +235,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 sidebar toggler
|
||||
.fc-toolbar-chunk {
|
||||
.fc-button-group {
|
||||
align-items: center;
|
||||
|
||||
.fc-button .fc-icon {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
// ℹ️ Below two `background-image` styles contains static color due to browser limitation of not parsing the css var inside CSS url()
|
||||
.fc-drawerToggler-button {
|
||||
display: none;
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(94,86,105,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
|
||||
background-position: 50%;
|
||||
background-repeat: no-repeat;
|
||||
block-size: 1.5625rem;
|
||||
font-size: 0;
|
||||
inline-size: 1.5625rem;
|
||||
margin-inline-end: 0.25rem;
|
||||
|
||||
@media (width <= 1264px) {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.v-theme--dark & {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(232,232,241,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ℹ️ Workaround of https://github.com/fullcalendar/fullcalendar/issues/6407
|
||||
.fc-col-header,
|
||||
.fc-daygrid-body,
|
||||
|
||||
@@ -2,6 +2,11 @@ $ps-size: 0.25rem;
|
||||
$ps-hover-size: 0.375rem;
|
||||
$ps-track-size: 0.5rem;
|
||||
|
||||
.ps__thumb-x,
|
||||
.ps__thumb-y {
|
||||
background-color: rgb(var(--v-theme-perfect-scrollbar-thumb)) !important;
|
||||
}
|
||||
|
||||
.ps__thumb-y {
|
||||
inline-size: $ps-size;
|
||||
inset-inline-end: 0.0625rem;
|
||||
@@ -29,15 +34,10 @@ $ps-track-size: 0.5rem;
|
||||
inline-size: $ps-hover-size;
|
||||
}
|
||||
|
||||
.ps__thumb-x,
|
||||
.ps__thumb-y {
|
||||
background-color: rgb(var(--v-theme-perfect-scrollbar-thumb)) !important;
|
||||
}
|
||||
|
||||
// fix bug
|
||||
@media(hover: none) {
|
||||
.ps > .ps__rail-x,
|
||||
.ps > .ps__rail-y {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
@use "@core/scss/base/utils";
|
||||
@use "@configured-variables" as variables;
|
||||
@use "../../../utils";
|
||||
|
||||
// 👉 Application
|
||||
// ℹ️ We need accurate vh in mobile devices as well
|
||||
@@ -195,7 +195,6 @@ h6,
|
||||
color: rgb(var(--v-border-color));
|
||||
}
|
||||
|
||||
// 👉 DataTable
|
||||
// 👉 DataTable
|
||||
.v-data-table {
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
@@ -250,34 +249,53 @@ h6,
|
||||
.v-badge__badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// 👉 Btn focus outline style removed
|
||||
.v-btn:focus-visible::after {
|
||||
opacity: 0 !important;
|
||||
// 👉 Dialog
|
||||
.v-dialog--fullscreen {
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
}
|
||||
|
||||
// .v-select chip spacing for slot
|
||||
.v-input:not(.v-select--chips) .v-select__selection {
|
||||
.v-chip {
|
||||
margin-block: 2px var(--select-chips-margin-bottom);
|
||||
}
|
||||
// For dialog card title
|
||||
.v-card-item + .v-card-text {
|
||||
padding-block-start: 0 !important;
|
||||
}
|
||||
|
||||
// 👉 VCard and VList subtitle color
|
||||
.v-card-subtitle,
|
||||
.v-list-item-subtitle {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
|
||||
}
|
||||
// 👉 v-slide-group (List of chips)
|
||||
.v-slide-group {
|
||||
.v-slide-group__container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
// 👉 placeholders
|
||||
.v-field__input {
|
||||
@at-root {
|
||||
& input::placeholder,
|
||||
input#{&}::placeholder,
|
||||
textarea#{&}::placeholder {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)) !important;
|
||||
opacity: 1 !important;
|
||||
// Spacing between buttons in v-slide-group
|
||||
.v-slide-group-item:not(:last-child) {
|
||||
margin-inline-end: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Expansion Panel
|
||||
.v-expansion-panels {
|
||||
.v-expansion-panel-title {
|
||||
min-block-size: unset !important;
|
||||
padding-block: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 v-textarea
|
||||
.v-textarea {
|
||||
textarea {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Cursor
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -2,9 +2,20 @@ $shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity);
|
||||
$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity);
|
||||
$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity);
|
||||
/* stylelint-disable-next-line max-line-length */
|
||||
$font-family-custom: inter, sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
$font-family-custom: 'Inter', 'Noto Sans SC', sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
|
||||
// 👉 Card transition properties
|
||||
$card-transition-property-custom: box-shadow, opacity;
|
||||
|
||||
@forward "vuetify/settings" with (
|
||||
// 👉 General settings
|
||||
$color-pack: false !default,
|
||||
|
||||
// 👉 Shadow opacity
|
||||
$shadow-key-umbra-opacity: $shadow-key-umbra-opacity-custom !default,
|
||||
$shadow-key-penumbra-opacity: $shadow-key-penumbra-opacity-custom !default,
|
||||
$shadow-key-ambient-opacity: $shadow-key-ambient-opacity-custom !default,
|
||||
|
||||
@forward "../../../base/libs/vuetify/variables" with (
|
||||
$body-font-family: $font-family-custom !default,
|
||||
$border-radius-root: 6px !default,
|
||||
|
||||
@@ -110,6 +121,18 @@ $font-family-custom: inter, sans-serif, -apple-system, blinkmacsystemfont, "Sego
|
||||
24: (0 9px 46px 8px $shadow-key-ambient-opacity-custom)
|
||||
) !default,
|
||||
|
||||
// 👉 Card
|
||||
$card-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default,
|
||||
$card-elevation: 6 !default,
|
||||
$card-title-line-height: 2rem !default,
|
||||
$card-actions-min-height: unset !default,
|
||||
$card-text-padding: 1.25rem !default,
|
||||
$card-item-padding: 1.25rem !default,
|
||||
$card-actions-padding: 0 12px 12px !default,
|
||||
$card-transition-property: $card-transition-property-custom !default,
|
||||
$card-subtitle-opacity: 1 !default,
|
||||
$card-title-letter-spacing: 0.0094rem !default,
|
||||
|
||||
// 👉 Typography
|
||||
$typography: (
|
||||
"h1": (
|
||||
@@ -161,13 +184,16 @@ $font-family-custom: inter, sans-serif, -apple-system, blinkmacsystemfont, "Sego
|
||||
)
|
||||
) !default,
|
||||
|
||||
// 👉 Card
|
||||
$card-title-letter-spacing: 0.0094rem !default,
|
||||
$card-title-line-height: 2rem !default,
|
||||
$card-subtitle-opacity: 1 !default,
|
||||
// 👉 List
|
||||
$list-item-icon-margin-end: 16px !default,
|
||||
$list-item-icon-margin-start: 16px !default,
|
||||
$list-item-subtitle-opacity: 1 !default,
|
||||
$list-subheader-text-opacity: 1 !default,
|
||||
|
||||
// 👉 Tooltip
|
||||
$tooltip-background-color:#212121 !default,
|
||||
$tooltip-background-color: #212121 !default,
|
||||
$tooltip-text-color: rgb(var(--v-theme-on-primary)) !default,
|
||||
$tooltip-font-size: 0.75rem !default,
|
||||
$tooltip-border-radius: 4px !default,
|
||||
$tooltip-padding: 4px 8px !default,
|
||||
|
||||
@@ -209,9 +235,6 @@ $font-family-custom: inter, sans-serif, -apple-system, blinkmacsystemfont, "Sego
|
||||
// 👉 Menu
|
||||
$menu-content-border-radius: 5px !default,
|
||||
|
||||
// 👉 List
|
||||
$list-subheader-text-opacity: 1 !default,
|
||||
|
||||
// 👉 Snackbar
|
||||
$snackbar-background:#212121 !default,
|
||||
$snackbar-border-radius: 4px !default,
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
@use "@core/scss/base/libs/vuetify";
|
||||
@use "variables";
|
||||
@use "overrides";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
@use "@configured-variables" as variables;
|
||||
@use "misc";
|
||||
@use "@core/scss/base/mixins";
|
||||
@use "../mixins";
|
||||
|
||||
%default-layout-vertical-nav-scrolled-sticky-elevated-nav {
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
}
|
||||
|
||||
%default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled {
|
||||
@include mixins.elevation(variables.$vertical-nav-navbar-elevation);
|
||||
// @include mixins.elevation(variables.$vertical-nav-navbar-elevation);
|
||||
|
||||
// If navbar is contained => Squeeze navbar content on scroll
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
@@ -36,11 +36,10 @@
|
||||
block-size: calc(variables.$layout-vertical-nav-navbar-height + variables.$vertical-nav-floating-navbar-top + 0.5rem);
|
||||
content: "";
|
||||
inset-block-start: -(variables.$vertical-nav-floating-navbar-top);
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline: 0;
|
||||
/* stylelint-disable property-no-vendor-prefix */
|
||||
-webkit-mask: linear-gradient(black, black 18%, transparent 100%);
|
||||
mask: linear-gradient(black, black 18%, transparent 100%);
|
||||
/* stylelint-enable */
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
%layout-navbar {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,5 @@
|
||||
@forward "nav";
|
||||
@forward "vertical-nav";
|
||||
@forward "default-layout";
|
||||
@forward "default-layout-vertical-nav";
|
||||
@forward "misc";
|
||||
|
||||
120
src/@core/scss/template/placeholders/_misc.scss
Normal file
120
src/@core/scss/template/placeholders/_misc.scss
Normal file
@@ -0,0 +1,120 @@
|
||||
%blurry-bg {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
block-size: calc(env(safe-area-inset-top, 0px) + 5rem);
|
||||
content: "";
|
||||
inset-block-start: 0;
|
||||
inset-inline: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease-in-out, background 0.2s ease-in-out;
|
||||
|
||||
// PC端样式 (默认)
|
||||
.v-theme--light & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-surface), 0.9) 0%,
|
||||
rgba(var(--v-theme-surface), 0.7) 20%,
|
||||
rgba(var(--v-theme-surface), 0.5) 40%,
|
||||
rgba(var(--v-theme-surface), 0.3) 60%,
|
||||
rgba(var(--v-theme-surface), 0.1) 80%,
|
||||
rgba(var(--v-theme-surface), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--dark & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 0.8) 0%,
|
||||
rgba(var(--v-theme-background), 0.6) 20%,
|
||||
rgba(var(--v-theme-background), 0.4) 40%,
|
||||
rgba(var(--v-theme-background), 0.25) 60%,
|
||||
rgba(var(--v-theme-background), 0.1) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--purple & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 0.8) 0%,
|
||||
rgba(var(--v-theme-background), 0.6) 20%,
|
||||
rgba(var(--v-theme-background), 0.4) 40%,
|
||||
rgba(var(--v-theme-background), 0.25) 60%,
|
||||
rgba(var(--v-theme-background), 0.1) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--transparent & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(11, 11, 11, 60%) 0%,
|
||||
rgba(11, 11, 11, 50%) 20%,
|
||||
rgba(11, 11, 11, 40%) 40%,
|
||||
rgba(11, 11, 11, 25%) 60%,
|
||||
rgba(11, 11, 11, 10%) 80%,
|
||||
rgba(11, 11, 11, 0%) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端样式
|
||||
@media (pointer: coarse) {
|
||||
%blurry-bg {
|
||||
&::before {
|
||||
.v-theme--light & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-surface), 1) 0%,
|
||||
rgba(var(--v-theme-surface), 0.9) 20%,
|
||||
rgba(var(--v-theme-surface), 0.7) 40%,
|
||||
rgba(var(--v-theme-surface), 0.5) 60%,
|
||||
rgba(var(--v-theme-surface), 0.2) 80%,
|
||||
rgba(var(--v-theme-surface), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--dark & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 1) 0%,
|
||||
rgba(var(--v-theme-background), 0.85) 20%,
|
||||
rgba(var(--v-theme-background), 0.7) 40%,
|
||||
rgba(var(--v-theme-background), 0.5) 60%,
|
||||
rgba(var(--v-theme-background), 0.3) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--purple & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--v-theme-background), 1) 0%,
|
||||
rgba(var(--v-theme-background), 0.85) 20%,
|
||||
rgba(var(--v-theme-background), 0.7) 40%,
|
||||
rgba(var(--v-theme-background), 0.5) 60%,
|
||||
rgba(var(--v-theme-background), 0.3) 80%,
|
||||
rgba(var(--v-theme-background), 0.0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.v-theme--transparent & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(11, 11, 11, 90%) 0%,
|
||||
rgba(11, 11, 11, 80%) 20%,
|
||||
rgba(11, 11, 11, 60%) 40%,
|
||||
rgba(11, 11, 11, 40%) 60%,
|
||||
rgba(11, 11, 11, 15%) 80%,
|
||||
rgba(11, 11, 11, 0%) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,92 +1,209 @@
|
||||
<!-- Thanks: https://markus.oberlehner.net/blog/transition-to-height-auto-with-vue/ -->
|
||||
|
||||
<script lang="ts">
|
||||
import { Transition } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import VerticalNav from '@layouts/components/VerticalNav.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TransitionExpand',
|
||||
setup(_, { slots }) {
|
||||
const onEnter = (element: HTMLElement) => {
|
||||
const width = getComputedStyle(element).width
|
||||
setup(props, { slots }) {
|
||||
const isOverlayNavActive = ref(false)
|
||||
const isLayoutOverlayVisible = ref(false)
|
||||
const toggleIsOverlayNavActive = useToggle(isOverlayNavActive)
|
||||
|
||||
element.style.width = width
|
||||
element.style.position = 'absolute'
|
||||
element.style.visibility = 'hidden'
|
||||
element.style.height = 'auto'
|
||||
const route = useRoute()
|
||||
const { mdAndDown } = useDisplay()
|
||||
|
||||
const height = getComputedStyle(element).height
|
||||
// ℹ️ This is alternative to below two commented watcher
|
||||
// We want to show overlay if overlay nav is visible and want to hide overlay if overlay is hidden and vice versa.
|
||||
syncRef(isOverlayNavActive, isLayoutOverlayVisible)
|
||||
|
||||
element.style.width = ''
|
||||
element.style.position = ''
|
||||
element.style.visibility = ''
|
||||
element.style.height = '0px'
|
||||
const scrollDistance = ref(window.scrollY)
|
||||
|
||||
// Force repaint to make sure the
|
||||
// animation is triggered correctly.
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
getComputedStyle(element).height
|
||||
|
||||
// Trigger the animation.
|
||||
// We use `requestAnimationFrame` because we need
|
||||
// to make sure the browser has finished
|
||||
// painting after setting the `height`
|
||||
// to `0` in the line above.
|
||||
requestAnimationFrame(() => {
|
||||
element.style.height = height
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', () => {
|
||||
scrollDistance.value = window.scrollY
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const onAfterEnter = (element: HTMLElement) => {
|
||||
element.style.height = 'auto'
|
||||
}
|
||||
return () => {
|
||||
// 👉 Vertical nav
|
||||
const verticalNav = h(
|
||||
VerticalNav,
|
||||
{ isOverlayNavActive: isOverlayNavActive.value, toggleIsOverlayNavActive },
|
||||
{
|
||||
'nav-header': () => slots['vertical-nav-header']?.(),
|
||||
'before-nav-items': () => slots['before-vertical-nav-items']?.(),
|
||||
'default': () => slots['vertical-nav-content']?.(),
|
||||
'after-nav-items': () => slots['after-vertical-nav-items']?.(),
|
||||
},
|
||||
)
|
||||
|
||||
const onLeave = (element: HTMLElement) => {
|
||||
const height = getComputedStyle(element).height
|
||||
// 👉 Navbar
|
||||
const navbar = h('header', { class: ['layout-navbar navbar-blur'] }, [
|
||||
h(
|
||||
'div',
|
||||
{ class: 'navbar-content-container' },
|
||||
slots.navbar?.({
|
||||
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
|
||||
}),
|
||||
),
|
||||
])
|
||||
|
||||
element.style.height = height
|
||||
const main = h(
|
||||
'main',
|
||||
{ class: 'layout-page-content' },
|
||||
h(Transition, { name: 'fade-slide', mode: 'out-in', appear: true }, () =>
|
||||
h('section', { class: 'page-content-container' }, slots.default?.()),
|
||||
),
|
||||
)
|
||||
|
||||
// Force repaint to make sure the
|
||||
// animation is triggered correctly.
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
getComputedStyle(element).height
|
||||
// 👉 根据路由 meta 决定 footer 高度
|
||||
const shouldShowFooter = !route.meta.hideFooter
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
element.style.height = '0px'
|
||||
// 👉 Footer
|
||||
const footer = h('footer', { class: 'layout-footer' }, [
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: ['footer-content-container', !shouldShowFooter && 'footer-content-container-noheight'],
|
||||
},
|
||||
slots.footer?.(),
|
||||
),
|
||||
])
|
||||
|
||||
// 👉 Overlay
|
||||
const layoutOverlay = h('div', {
|
||||
class: ['layout-overlay', 'touch-none', { visible: isLayoutOverlayVisible.value }],
|
||||
onClick: () => {
|
||||
isLayoutOverlayVisible.value = !isLayoutOverlayVisible.value
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return () => h(
|
||||
h(Transition),
|
||||
{
|
||||
name: 'expand',
|
||||
onEnter,
|
||||
onAfterEnter,
|
||||
onLeave,
|
||||
},
|
||||
() => slots.default?.(),
|
||||
)
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: [
|
||||
'layout-wrapper layout-nav-type-vertical layout-navbar-static layout-footer-static layout-content-width-fluid',
|
||||
'layout-navbar-fixed',
|
||||
mdAndDown.value && 'layout-overlay-nav',
|
||||
route.meta.layoutWrapperClasses,
|
||||
scrollDistance.value && 'window-scrolled',
|
||||
],
|
||||
},
|
||||
[verticalNav, h('div', { class: 'layout-content-wrapper' }, [navbar, main, footer]), layoutOverlay],
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.expand-enter-active,
|
||||
.expand-leave-active {
|
||||
overflow: hidden;
|
||||
transition: block-size var(--expand-transition-duration, 0.25s) ease;
|
||||
<style lang="scss">
|
||||
@use '@configured-variables' as variables;
|
||||
@use '@layouts/styles/placeholders';
|
||||
@use '@layouts/styles/mixins';
|
||||
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
// TODO(v2): Check why we need height in vertical nav & min-height in horizontal nav
|
||||
block-size: 100%;
|
||||
|
||||
.layout-content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
min-block-size: calc(var(--vh, 1vh) * 100);
|
||||
transition: padding-inline-start 0.2s ease-in-out;
|
||||
will-change: padding-inline-start;
|
||||
}
|
||||
|
||||
.layout-navbar {
|
||||
position: fixed;
|
||||
width: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
|
||||
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
|
||||
inset-block-start: 0;
|
||||
|
||||
.navbar-content-container {
|
||||
block-size: calc(env(safe-area-inset-top) + variables.$layout-vertical-nav-navbar-height);
|
||||
}
|
||||
|
||||
@at-root {
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
.layout-navbar {
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
@include mixins.boxed-content;
|
||||
} @else {
|
||||
.navbar-content-container {
|
||||
// @include mixins.boxed-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.layout-navbar-fixed .layout-navbar {
|
||||
@extend %layout-navbar-fixed;
|
||||
}
|
||||
|
||||
&.layout-navbar-hidden .layout-navbar {
|
||||
@extend %layout-navbar-hidden;
|
||||
}
|
||||
|
||||
// 👉 Footer
|
||||
.layout-footer {
|
||||
@include mixins.boxed-content;
|
||||
}
|
||||
|
||||
// 👉 Layout overlay
|
||||
.layout-overlay {
|
||||
position: fixed;
|
||||
z-index: variables.$layout-overlay-z-index;
|
||||
background-color: rgb(0 0 0 / 60%);
|
||||
cursor: pointer;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s ease-in-out;
|
||||
will-change: transform;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.layout-overlay-nav) .layout-content-wrapper {
|
||||
padding-inline-start: variables.$layout-vertical-nav-width;
|
||||
}
|
||||
|
||||
// Adjust right column pl when vertical nav is collapsed
|
||||
&.layout-vertical-nav-collapsed .layout-content-wrapper {
|
||||
padding-inline-start: variables.$layout-vertical-nav-collapsed-width;
|
||||
}
|
||||
|
||||
// 👉 Content height fixed
|
||||
&.layout-content-height-fixed {
|
||||
.layout-content-wrapper {
|
||||
max-block-size: calc(var(--vh) * 100);
|
||||
}
|
||||
|
||||
.layout-page-content {
|
||||
// display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
.page-content-container {
|
||||
inline-size: 100%;
|
||||
|
||||
> :first-child {
|
||||
max-block-size: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.expand-enter-from,
|
||||
.expand-leave-to {
|
||||
block-size: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
backface-visibility: hidden;
|
||||
perspective: 62.5rem;
|
||||
transform: translateZ(0);
|
||||
will-change: block-size;
|
||||
.layout-wrapper.layout-nav-type-vertical.layout-overlay-nav {
|
||||
.layout-navbar {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -59,9 +59,6 @@ function handleNavScroll(evt: Event) {
|
||||
</RouterLink>
|
||||
</slot>
|
||||
</div>
|
||||
<slot name="before-nav-items">
|
||||
<div class="vertical-nav-items-shadow" />
|
||||
</slot>
|
||||
<slot name="nav-items" :update-is-vertical-nav-scrolled="updateIsVerticalNavScrolled">
|
||||
<PerfectScrollbar
|
||||
tag="ul"
|
||||
|
||||
@@ -101,6 +101,12 @@ export default defineComponent({
|
||||
@use '@layouts/styles/placeholders';
|
||||
@use '@layouts/styles/mixins';
|
||||
|
||||
.layout-page-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
// TODO(v2): Check why we need height in vertical nav & min-height in horizontal nav
|
||||
block-size: 100%;
|
||||
@@ -116,8 +122,8 @@ export default defineComponent({
|
||||
|
||||
.layout-navbar {
|
||||
position: fixed;
|
||||
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
|
||||
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
|
||||
inline-size: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
|
||||
inset-block-start: 0;
|
||||
|
||||
.navbar-content-container {
|
||||
@@ -131,7 +137,7 @@ export default defineComponent({
|
||||
@include mixins.boxed-content;
|
||||
} @else {
|
||||
.navbar-content-container {
|
||||
@include mixins.boxed-content;
|
||||
// @include mixins.boxed-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,7 +177,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
&:not(.layout-overlay-nav) .layout-content-wrapper {
|
||||
padding-inline-start: variables.$layout-vertical-nav-width;
|
||||
padding-inline-start: calc(variables.$layout-vertical-nav-width);
|
||||
}
|
||||
|
||||
// Adjust right column pl when vertical nav is collapsed
|
||||
@@ -203,7 +209,8 @@ export default defineComponent({
|
||||
|
||||
.layout-wrapper.layout-nav-type-vertical.layout-overlay-nav {
|
||||
.layout-navbar {
|
||||
width: 100%;
|
||||
inline-size: 100%;
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,19 +7,9 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
class="nav-link"
|
||||
:class="{ disabled: item.disable }"
|
||||
>
|
||||
<Component
|
||||
:is="item.to ? 'RouterLink' : 'a'"
|
||||
:to="item.to"
|
||||
:href="item.href"
|
||||
>
|
||||
<VIcon
|
||||
:icon="item.icon"
|
||||
class="nav-item-icon"
|
||||
/>
|
||||
<li class="nav-link" :class="{ disabled: item.disable }">
|
||||
<Component :is="item.to ? 'RouterLink' : 'a'" :to="item.to" :href="item.href">
|
||||
<VIcon :icon="item.icon as string" class="nav-item-icon" />
|
||||
<!-- 👉 Title -->
|
||||
<span class="nav-item-title">
|
||||
{{ item.title }}
|
||||
|
||||
@@ -10,10 +10,7 @@ defineProps<{
|
||||
<li class="nav-section-title">
|
||||
<div class="title-wrapper">
|
||||
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||
<span
|
||||
class="title-text"
|
||||
v-text="item.heading"
|
||||
/>
|
||||
<span class="title-text" v-text="item.heading" />
|
||||
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -5,58 +5,62 @@
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
html {
|
||||
min-height: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
||||
background: rgb(var(--v-theme-background));
|
||||
min-block-size: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
||||
overflow-y: overlay;
|
||||
}
|
||||
|
||||
body {
|
||||
overscroll-behavior-y: contain;
|
||||
--webkit-overflow-scrolling: touch;
|
||||
background: rgb(var(--v-theme-background));
|
||||
overscroll-behavior-y: contain;
|
||||
|
||||
--webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body,
|
||||
#app,
|
||||
.v-application {
|
||||
min-height: 100%;
|
||||
min-block-size: 100%;
|
||||
}
|
||||
|
||||
.layout-vertical-nav {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-block: env(safe-area-inset-top) env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.navbar-content-container {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-block-start: env(safe-area-inset-top);
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
|
||||
.layout-page-content {
|
||||
@include mixins.boxed-content(true);
|
||||
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
// TODO: Use grid gutter variable here
|
||||
flex-grow: 1;
|
||||
|
||||
// TODO: Use grid gutter variable here;
|
||||
padding-block: 1.5rem;
|
||||
padding-top: calc(env(safe-area-inset-top) + 4.25rem);
|
||||
// display: flex;
|
||||
padding-inline: 0.5rem;
|
||||
padding-block-start: calc(env(safe-area-inset-top) + 4.5rem);
|
||||
|
||||
// display: flex;display
|
||||
|
||||
|
||||
.page-content-container {
|
||||
// flex: 1;
|
||||
// flex: 1;flex
|
||||
display: flex;
|
||||
|
||||
& > div:first-child {
|
||||
flex: auto;
|
||||
position: relative;
|
||||
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
|
||||
flex: auto;
|
||||
inline-size: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1280px){
|
||||
@media screen and (width <= 1280px){
|
||||
.page-content-container > div:first-child {
|
||||
width: calc(100vw - 1rem) !important;
|
||||
inline-size: calc(100vw - 1rem) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +70,7 @@ body,
|
||||
}
|
||||
|
||||
.footer-content-container-noheight {
|
||||
block-size: 0px !important;
|
||||
block-size: 0 !important;
|
||||
}
|
||||
|
||||
.layout-footer-sticky & {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
%boxed-content {
|
||||
@at-root #{&}-spacing {
|
||||
// TODO: Use grid gutter variable here
|
||||
padding-inline: 0.5rem;
|
||||
// padding-inline: 0.5rem;
|
||||
}
|
||||
|
||||
inline-size: 100%;
|
||||
|
||||
@@ -19,7 +19,7 @@ $layout-horizontal-nav-layout-navbar-z-index: 11 !default;
|
||||
$layout-boxed-content-width: 90rem !default;
|
||||
|
||||
// 👉Footer
|
||||
$layout-vertical-nav-footer-height: 3.5rem !default;
|
||||
$layout-vertical-nav-footer-height: 8rem !default;
|
||||
|
||||
// 👉 Layout overlay
|
||||
$layout-overlay-z-index: 11 !default;
|
||||
|
||||
1
src/@layouts/types.d.ts
vendored
1
src/@layouts/types.d.ts
vendored
@@ -114,6 +114,7 @@ export interface NavLinkProps {
|
||||
|
||||
export interface NavLink extends NavLinkProps, Partial<AclProperties> {
|
||||
title: string
|
||||
full_title?: string
|
||||
icon?: unknown
|
||||
badgeContent?: string
|
||||
badgeClass?: string
|
||||
|
||||
178
src/App.vue
178
src/App.vue
@@ -2,6 +2,7 @@
|
||||
import { useTheme } from 'vuetify'
|
||||
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||
import { ensureRenderComplete, removeEl } from './@core/utils/dom'
|
||||
import api from '@/api'
|
||||
|
||||
// 生效主题
|
||||
const { global: globalTheme } = useTheme()
|
||||
@@ -9,9 +10,88 @@ let themeValue = localStorage.getItem('theme') || 'light'
|
||||
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
|
||||
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
|
||||
|
||||
// 从 provide 中获取全局设置
|
||||
const globalSettings: any = inject('globalSettings')
|
||||
|
||||
// 更新data-theme属性以便CSS选择器能正确匹配
|
||||
function updateHtmlThemeAttribute(themeName: string) {
|
||||
document.documentElement.setAttribute('data-theme', themeName)
|
||||
// 确保body元素也有相同的主题属性,以便更好地选择弹出窗口
|
||||
document.body.setAttribute('data-theme', themeName)
|
||||
}
|
||||
|
||||
// 显示状态
|
||||
const show = ref(false)
|
||||
|
||||
// 背景图片
|
||||
const backgroundImages = ref<string[]>([])
|
||||
const activeImageIndex = ref(0)
|
||||
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
|
||||
let backgroundRotationTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 获取背景图片
|
||||
async function fetchBackgroundImages() {
|
||||
try {
|
||||
backgroundImages.value = await api.get('/login/wallpapers')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始背景图片轮换
|
||||
function startBackgroundRotation() {
|
||||
if (backgroundRotationTimer) clearInterval(backgroundRotationTimer)
|
||||
|
||||
if (backgroundImages.value.length > 1) {
|
||||
backgroundRotationTimer = setInterval(() => {
|
||||
activeImageIndex.value = (activeImageIndex.value + 1) % backgroundImages.value.length
|
||||
}, 10000) // 每10秒切换一次
|
||||
}
|
||||
}
|
||||
|
||||
// 计算图片地址
|
||||
function getImgUrl(url: string) {
|
||||
// 使用图片缓存
|
||||
if (globalSettings.GLOBAL_IMAGE_CACHE)
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`
|
||||
// 如果地址中包含douban则使用中转代理
|
||||
if (url.includes('doubanio.com'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
|
||||
return url
|
||||
}
|
||||
|
||||
// 处理页面可见性变化
|
||||
function handleVisibilityChange() {
|
||||
if (document.visibilityState === 'visible' && isTransparentTheme.value) {
|
||||
// 如果已有背景图片数据,直接重启轮换
|
||||
if (backgroundImages.value.length > 0) {
|
||||
startBackgroundRotation()
|
||||
}
|
||||
// 如果没有背景图片数据,重新获取
|
||||
else {
|
||||
fetchBackgroundImages().then(() => startBackgroundRotation())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听主题变化
|
||||
watch(
|
||||
() => globalTheme.name.value,
|
||||
async newTheme => {
|
||||
// 更新HTML属性
|
||||
updateHtmlThemeAttribute(newTheme)
|
||||
|
||||
if (newTheme === 'transparent' && backgroundImages.value.length === 0) {
|
||||
await fetchBackgroundImages()
|
||||
startBackgroundRotation()
|
||||
} else if (newTheme !== 'transparent' && backgroundRotationTimer) {
|
||||
clearInterval(backgroundRotationTimer)
|
||||
backgroundRotationTimer = null
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// ApexCharts 全局配置
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -43,6 +123,12 @@ if (window.Apex) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化data-theme属性
|
||||
updateHtmlThemeAttribute(globalTheme.name.value)
|
||||
|
||||
// 添加页面可见性变化监听
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
ensureRenderComplete(() => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
@@ -56,10 +142,96 @@ onMounted(() => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除页面可见性监听
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
// 清除轮换定时器
|
||||
if (backgroundRotationTimer) {
|
||||
clearInterval(backgroundRotationTimer)
|
||||
backgroundRotationTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VApp v-show="show">
|
||||
<RouterView />
|
||||
</VApp>
|
||||
<div class="app-wrapper">
|
||||
<!-- 透明主题背景 -->
|
||||
<template v-if="isTransparentTheme && backgroundImages.length > 0">
|
||||
<div class="background-container">
|
||||
<div
|
||||
v-for="(imageUrl, index) in backgroundImages"
|
||||
:key="index"
|
||||
class="background-image"
|
||||
:class="{ 'active': index === activeImageIndex }"
|
||||
:style="{ backgroundImage: `url(${getImgUrl(imageUrl)})` }"
|
||||
></div>
|
||||
<!-- 全局磨砂层 -->
|
||||
<div class="global-blur-layer"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<VApp v-show="show" :class="{ 'transparent-app': isTransparentTheme }">
|
||||
<RouterView />
|
||||
</VApp>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
/* 全局样式 */
|
||||
.app-wrapper {
|
||||
position: relative;
|
||||
inline-size: 100%;
|
||||
min-block-size: 100vh;
|
||||
}
|
||||
|
||||
.background-container {
|
||||
position: fixed;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
|
||||
.background-image {
|
||||
position: absolute;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 1.5s ease;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
background: linear-gradient(rgba(0, 0, 0, 30%) 0%, rgba(0, 0, 0, 60%) 100%);
|
||||
block-size: 100%;
|
||||
content: '';
|
||||
inline-size: 100%;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全局磨砂层 */
|
||||
.global-blur-layer {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
backdrop-filter: blur(16px);
|
||||
background-color: rgba(128, 128, 128, 30%);
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,26 +3,31 @@ export const storageOptions = [
|
||||
title: '本地',
|
||||
value: 'local',
|
||||
icon: 'mdi-folder-multiple-outline',
|
||||
remote: false,
|
||||
},
|
||||
{
|
||||
title: '阿里云盘',
|
||||
value: 'alipan',
|
||||
icon: 'mdi-cloud-outline',
|
||||
remote: true,
|
||||
},
|
||||
{
|
||||
title: '115网盘',
|
||||
value: 'u115',
|
||||
icon: 'mdi-cloud-outline',
|
||||
remote: true,
|
||||
},
|
||||
{
|
||||
title: 'RClone',
|
||||
value: 'rclone',
|
||||
icon: 'mdi-cloud-outline',
|
||||
icon: 'mdi-server-network-outline',
|
||||
remote: true,
|
||||
},
|
||||
{
|
||||
title: 'AList',
|
||||
value: 'alist',
|
||||
icon: 'mdi-cloud-outline',
|
||||
icon: 'mdi-server-network-outline',
|
||||
remote: true,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -77,7 +77,9 @@ export interface Subscribe {
|
||||
// 过滤规则组
|
||||
filter_groups?: string[]
|
||||
// 下载器
|
||||
downloader: string
|
||||
downloader?: string
|
||||
// 自定义剧集组
|
||||
episode_group?: string
|
||||
}
|
||||
|
||||
// 订阅分享
|
||||
@@ -138,6 +140,8 @@ export interface SubscribeShare {
|
||||
media_category?: string
|
||||
// 复用次数
|
||||
count?: number
|
||||
// 自定义剧集组
|
||||
episode_group?: string
|
||||
}
|
||||
|
||||
// 历史记录
|
||||
@@ -184,6 +188,8 @@ export interface TransferHistory {
|
||||
errmsg?: string
|
||||
// 日期
|
||||
date?: string
|
||||
// 源文件项
|
||||
src_fileitem?: FileItem
|
||||
}
|
||||
|
||||
// 媒体信息
|
||||
@@ -286,6 +292,8 @@ export interface MediaInfo {
|
||||
next_episode_to_air?: object
|
||||
// 别名
|
||||
names?: string[]
|
||||
// 剧集组
|
||||
episode_group?: string
|
||||
}
|
||||
|
||||
// 季信息
|
||||
@@ -795,6 +803,8 @@ export interface User {
|
||||
permissions: { [key: string]: any }
|
||||
// 用户个性化设置 json
|
||||
settings: { [key: string]: string | null }
|
||||
// 昵称
|
||||
nickname?: string
|
||||
}
|
||||
|
||||
// 存储空间
|
||||
@@ -1212,6 +1222,8 @@ export interface TransferForm {
|
||||
library_type_folder?: boolean
|
||||
// 媒体库类别子目录
|
||||
library_category_folder?: boolean
|
||||
// 剧集组编号
|
||||
episode_group?: string
|
||||
}
|
||||
|
||||
// 整理队列
|
||||
@@ -1253,6 +1265,8 @@ export interface RecommendSource {
|
||||
name: string
|
||||
// 媒体数据源API地址
|
||||
api_path: string
|
||||
// 类型
|
||||
type: string
|
||||
}
|
||||
|
||||
// 站点资源分类
|
||||
|
||||
BIN
src/assets/images/logos/site.webp
Normal file
BIN
src/assets/images/logos/site.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
BIN
src/assets/images/logos/trimemedia.png
Normal file
BIN
src/assets/images/logos/trimemedia.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -1,9 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Axios } from 'axios'
|
||||
import FileList from './filebrowser/FileList.vue'
|
||||
import FileToolbar from './filebrowser/FileToolbar.vue'
|
||||
import FileNavigator from './filebrowser/FileNavigator.vue'
|
||||
import type { EndPoints, FileItem, StorageConf } from '@/api/types'
|
||||
import { storageOptions } from '@/api/constants'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -11,7 +12,7 @@ const props = defineProps({
|
||||
tree: Boolean,
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
axios: {
|
||||
type: Object as PropType<Axios>,
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
axiosconfig: Object,
|
||||
@@ -28,6 +29,12 @@ const props = defineProps({
|
||||
// 对外事件
|
||||
const emit = defineEmits(['pathchanged'])
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// APP
|
||||
const appMode = inject('pwaMode') && display.mdAndDown.value
|
||||
|
||||
const fileIcons = {
|
||||
// 压缩包
|
||||
zip: 'mdi-folder-zip-outline',
|
||||
@@ -126,6 +133,9 @@ const refreshPending = ref(false)
|
||||
// 排序
|
||||
const sort = ref('name')
|
||||
|
||||
// 是否显示目录树
|
||||
const showDirTree = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const storagesArray = computed(() => {
|
||||
const storageCodes = props.storages?.map(item => item.type)
|
||||
@@ -154,10 +164,37 @@ function sortChanged(s: string) {
|
||||
sort.value = s
|
||||
refreshPending.value = true
|
||||
}
|
||||
|
||||
// 切换目录树
|
||||
function switchDirTree(state: boolean) {
|
||||
showDirTree.value = state
|
||||
}
|
||||
|
||||
// 文件列表
|
||||
const fileListItems = ref<FileItem[]>([])
|
||||
|
||||
// 文件列表数据更新
|
||||
function fileListUpdated(items: FileItem[]) {
|
||||
fileListItems.value = items
|
||||
}
|
||||
|
||||
// 外层DIV大小控制
|
||||
const scrollStyle = computed(() => {
|
||||
return appMode
|
||||
? 'height: calc(100vh - 10.5rem - env(safe-area-inset-bottom) - 6.5rem)'
|
||||
: 'height: calc(100vh - 10.5rem - env(safe-area-inset-bottom)'
|
||||
})
|
||||
|
||||
// 文件列表大小限制
|
||||
const fileListStyle = computed(() => {
|
||||
return appMode
|
||||
? 'height: calc(100vh - 14rem - env(safe-area-inset-bottom) - 7rem)'
|
||||
: 'height: calc(100vh - 14rem - env(safe-area-inset-bottom)'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="mx-auto" :loading="loading > 0">
|
||||
<div class="mx-auto" :loading="loading > 0">
|
||||
<div v-if="activeStorage && item">
|
||||
<FileToolbar
|
||||
:item="item"
|
||||
@@ -171,20 +208,35 @@ function sortChanged(s: string) {
|
||||
@foldercreated="refreshPending = true"
|
||||
@sortchanged="sortChanged"
|
||||
/>
|
||||
<FileList
|
||||
:item="item"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axios"
|
||||
:refreshpending="refreshPending"
|
||||
:sort="sort"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
@filedeleted="refreshPending = true"
|
||||
@renamed="refreshPending = true"
|
||||
/>
|
||||
<div class="flex" :style="scrollStyle">
|
||||
<FileNavigator
|
||||
v-if="showDirTree"
|
||||
:storage="activeStorage"
|
||||
:currentPath="item.path"
|
||||
:items="fileListItems"
|
||||
:endpoints="endpoints"
|
||||
:axios="axios"
|
||||
@navigate="pathChanged"
|
||||
/>
|
||||
<FileList
|
||||
:item="item"
|
||||
:storage="activeStorage"
|
||||
:icons="fileIcons"
|
||||
:endpoints="endpoints"
|
||||
:axios="axios"
|
||||
:refreshpending="refreshPending"
|
||||
:sort="sort"
|
||||
:listStyle="fileListStyle"
|
||||
:showTree="showDirTree"
|
||||
@pathchanged="pathChanged"
|
||||
@loading="loadingChanged"
|
||||
@refreshed="refreshPending = false"
|
||||
@filedeleted="refreshPending = true"
|
||||
@renamed="refreshPending = true"
|
||||
@items-updated="fileListUpdated"
|
||||
@switch-tree="switchDirTree"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,31 +1,244 @@
|
||||
<script setup lang="ts">
|
||||
import image from '@images/no-data.svg'
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
interface Props {
|
||||
errorCode?: string
|
||||
errorTitle?: string
|
||||
errorDescription?: string
|
||||
icon?: string
|
||||
iconColor?: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VEmptyState :image="image" size="250">
|
||||
<template #title>
|
||||
<div class="mt-8 text-2xl">
|
||||
{{ props.errorTitle }}
|
||||
<div class="no-data-container">
|
||||
<!-- 图标容器 -->
|
||||
<div class="icon-wrapper">
|
||||
<div class="icon-glow"></div>
|
||||
<div class="icon-container">
|
||||
<VIcon
|
||||
:icon="props.icon || 'mdi-file-search-outline'"
|
||||
:color="props.iconColor || 'white'"
|
||||
size="48"
|
||||
class="main-icon"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="pulse-ring"></div>
|
||||
</div>
|
||||
|
||||
<template #text>
|
||||
<div class="text-subtitle mt-3">
|
||||
{{ props.errorDescription }}
|
||||
</div>
|
||||
</template>
|
||||
<!-- 标题 -->
|
||||
<div class="error-title">
|
||||
{{ props.errorTitle || '暂无数据' }}
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<!-- 描述 -->
|
||||
<div class="error-description">
|
||||
{{ props.errorDescription || '没有找到相关内容' }}
|
||||
</div>
|
||||
|
||||
<!-- 按钮插槽 -->
|
||||
<div class="actions-container">
|
||||
<slot name="button" />
|
||||
</template>
|
||||
</VEmptyState>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.no-data-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inline-size: 100%;
|
||||
min-block-size: 300px;
|
||||
padding-block: 3rem;
|
||||
padding-inline: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 图标样式 */
|
||||
.icon-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
block-size: 100px;
|
||||
inline-size: 100px;
|
||||
margin-block: 0 2rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.icon-glow {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
animation: pulse 3s infinite ease-in-out;
|
||||
background: radial-gradient(circle, rgba(var(--v-theme-primary), 0.8) 0%, rgba(var(--v-theme-primary), 0) 70%);
|
||||
block-size: 80px;
|
||||
filter: blur(15px);
|
||||
inline-size: 80px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.9), rgba(var(--v-theme-secondary), 0.8));
|
||||
block-size: 80px;
|
||||
inline-size: 80px;
|
||||
}
|
||||
|
||||
.main-icon {
|
||||
animation: slight-bounce 3s infinite ease-in-out;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 30%));
|
||||
}
|
||||
|
||||
.pulse-ring {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
border: 2px solid rgba(var(--v-theme-primary), 0.5);
|
||||
border-radius: 50%;
|
||||
animation: ripple 2s infinite ease-out;
|
||||
block-size: 100px;
|
||||
inline-size: 100px;
|
||||
inset-block-start: 50%;
|
||||
inset-inline-start: 50%;
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.pulse-ring::before {
|
||||
position: absolute;
|
||||
border: 2px solid rgba(var(--v-theme-primary), 0.3);
|
||||
border-radius: 50%;
|
||||
animation: ripple 2s infinite 0.5s ease-out;
|
||||
block-size: 85px;
|
||||
content: '';
|
||||
inline-size: 85px;
|
||||
inset-block-start: 50%;
|
||||
inset-inline-start: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slight-bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 文字样式 */
|
||||
.error-title {
|
||||
position: relative;
|
||||
color: rgba(var(--v-theme-on-surface), 0.95);
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
margin-block-end: 0.75rem;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 5%);
|
||||
}
|
||||
|
||||
.error-title::after {
|
||||
display: block;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(90deg, rgba(var(--v-theme-primary), 0.8), rgba(var(--v-theme-primary), 0.2));
|
||||
block-size: 3px;
|
||||
content: '';
|
||||
inline-size: 40px;
|
||||
margin-block: 0.5rem 0;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
color: rgba(var(--v-theme-on-surface), 0.75);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
margin-block-end: 1.5rem;
|
||||
margin-inline: auto;
|
||||
max-inline-size: 80%;
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
margin-block-start: 1.5rem;
|
||||
}
|
||||
|
||||
.actions-container :deep(.v-btn) {
|
||||
transform: translateY(0);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.actions-container :deep(.v-btn:hover) {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (width <= 600px) {
|
||||
.no-data-container {
|
||||
padding-block: 2rem;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
block-size: 80px;
|
||||
inline-size: 80px;
|
||||
margin-block-end: 1.5rem;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
block-size: 70px;
|
||||
inline-size: 70px;
|
||||
}
|
||||
|
||||
.icon-glow {
|
||||
block-size: 70px;
|
||||
inline-size: 70px;
|
||||
}
|
||||
|
||||
.pulse-ring,
|
||||
.pulse-ring::before {
|
||||
block-size: 80px;
|
||||
inline-size: 80px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 0.95rem;
|
||||
max-inline-size: 90%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -37,7 +37,7 @@ const getImgUrl = computed(() => {
|
||||
:width="props.width"
|
||||
class="ring-gray-500"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
'ring-1': imageLoaded,
|
||||
}"
|
||||
@click="goPlay"
|
||||
|
||||
@@ -95,7 +95,7 @@ function onClose() {
|
||||
<VIcon class="cursor-move" icon="mdi-drag" />
|
||||
</IconBtn>
|
||||
</span>
|
||||
<DialogCloseBtn @click="onClose" />
|
||||
<VDialogCloseBtn @click="onClose" />
|
||||
<VCardText class="flex justify-space-between align-center gap-3">
|
||||
<div class="align-self-start">
|
||||
<h5 class="text-h6 mb-1">{{ props.rule.name }}</h5>
|
||||
@@ -106,7 +106,7 @@ function onClose() {
|
||||
</VCard>
|
||||
<VDialog v-if="ruleInfoDialog" v-model="ruleInfoDialog" scrollable max-width="40rem" persistent>
|
||||
<VCard :title="`${props.rule.id} - 配置`" class="rounded-t">
|
||||
<DialogCloseBtn v-model="ruleInfoDialog" />
|
||||
<VDialogCloseBtn v-model="ruleInfoDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VForm>
|
||||
|
||||
@@ -29,6 +29,11 @@ const typeItems = [
|
||||
{ title: '电视剧', value: '电视剧' },
|
||||
]
|
||||
|
||||
// 计算资源存储字典(整理方式为下载器时不能为远程存储)
|
||||
const resourceStorageOptions = computed(() => {
|
||||
return storageOptions.filter(item => !item.remote || props.directory.monitor_type !== 'downloader')
|
||||
})
|
||||
|
||||
// 自动整理方式下拉字典
|
||||
const transferSourceItems = [
|
||||
{ title: '不整理', value: '' },
|
||||
@@ -131,7 +136,7 @@ const getCategories = computed(() => {
|
||||
return default_value.concat(props.categories[props.directory.media_type ?? ''])
|
||||
})
|
||||
|
||||
// 监听 下载储存与媒体库储存 变化,重新加载整理方式下拉字典
|
||||
// 监听 资源存储与媒体库储存 变化,重新加载整理方式下拉字典
|
||||
watch(
|
||||
[() => props.directory.library_storage, () => props.directory.storage],
|
||||
([newLibraryStorage, newStorage], [oldLibraryStorage, oldStorage]) => {
|
||||
@@ -156,11 +161,21 @@ watch(
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 监听monitor_type变化,如果为downloader则设置为本地
|
||||
watch(
|
||||
() => props.directory.monitor_type,
|
||||
newMonitorType => {
|
||||
if (newMonitorType === 'downloader') {
|
||||
props.directory.storage = 'local'
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard variant="tonal" :width="props.width" :height="props.height">
|
||||
<DialogCloseBtn @click="onClose" />
|
||||
<VDialogCloseBtn @click="onClose" />
|
||||
<VCardItem>
|
||||
<VTextField
|
||||
v-model="props.directory.name"
|
||||
@@ -198,8 +213,8 @@ watch(
|
||||
<VSelect
|
||||
v-model="props.directory.storage"
|
||||
variant="underlined"
|
||||
:items="storageOptions"
|
||||
label="下载存储/源存储"
|
||||
:items="resourceStorageOptions"
|
||||
label="资源存储"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="8">
|
||||
@@ -207,7 +222,7 @@ watch(
|
||||
v-model="props.directory.download_path"
|
||||
:storage="props.directory.storage"
|
||||
variant="underlined"
|
||||
label="下载目录/源目录"
|
||||
label="资源目录"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" v-if="!props.directory.media_type || props.directory.media_type === ''">
|
||||
|
||||
@@ -143,38 +143,45 @@ onUnmounted(() => {
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<VCard variant="tonal" @click="openDownloaderInfoDialog">
|
||||
<DialogCloseBtn @click="onClose" />
|
||||
<span class="absolute top-3 right-12">
|
||||
<IconBtn>
|
||||
<VIcon class="cursor-move" icon="mdi-drag" />
|
||||
</IconBtn>
|
||||
</span>
|
||||
<VCardText class="flex justify-space-between align-center gap-4">
|
||||
<div class="align-self-start flex-1">
|
||||
<div class="flex items-center">
|
||||
<VBadge
|
||||
v-if="props.downloader.default && props.downloader.enabled"
|
||||
dot
|
||||
inline
|
||||
color="success"
|
||||
class="me-1"
|
||||
/>
|
||||
<span class="text-h6">{{ downloader.name }}</span>
|
||||
<VHover v-slot="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
variant="tonal"
|
||||
@click="openDownloaderInfoDialog"
|
||||
:class="{ 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering }"
|
||||
>
|
||||
<VDialogCloseBtn @click="onClose" />
|
||||
<span class="absolute top-3 right-12">
|
||||
<IconBtn>
|
||||
<VIcon class="cursor-move" icon="mdi-drag" />
|
||||
</IconBtn>
|
||||
</span>
|
||||
<VCardText class="flex justify-space-between align-center gap-4">
|
||||
<div class="align-self-start flex-1">
|
||||
<div class="flex items-center">
|
||||
<VBadge
|
||||
v-if="props.downloader.default && props.downloader.enabled"
|
||||
dot
|
||||
inline
|
||||
color="success"
|
||||
class="me-1"
|
||||
/>
|
||||
<span class="text-h6">{{ downloader.name }}</span>
|
||||
</div>
|
||||
<div class="mt-1 flex flex-wrap text-sm" v-if="props.downloader.enabled">
|
||||
<span class="me-2">{{ `↑ ${formatFileSize(upload_rate, 1)}/s ` }}</span>
|
||||
<span>{{ `↓ ${formatFileSize(download_rate, 1)}/s` }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 flex flex-wrap text-sm" v-if="props.downloader.enabled">
|
||||
<span class="me-2">{{ `↑ ${formatFileSize(upload_rate, 1)}/s ` }}</span>
|
||||
<span>{{ `↓ ${formatFileSize(download_rate, 1)}/s` }}</span>
|
||||
<div class="h-20">
|
||||
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" min-width="3rem" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-20">
|
||||
<VImg :src="getIcon" cover class="mt-7 me-3" max-width="3rem" min-width="3rem" />
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VHover>
|
||||
<VDialog v-if="downloaderInfoDialog" v-model="downloaderInfoDialog" scrollable max-width="40rem" persistent>
|
||||
<VCard :title="`${props.downloader.name} - 配置`" class="rounded-t">
|
||||
<DialogCloseBtn v-model="downloaderInfoDialog" />
|
||||
<VDialogCloseBtn v-model="downloaderInfoDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VForm>
|
||||
|
||||
@@ -47,7 +47,7 @@ onMounted(() => {
|
||||
<VIcon class="cursor-move" icon="mdi-drag" />
|
||||
</IconBtn>
|
||||
</span>
|
||||
<DialogCloseBtn @click="onClose" />
|
||||
<VDialogCloseBtn @click="onClose" />
|
||||
<VCardItem>
|
||||
<VCardTitle>优先级 {{ props.pri }}</VCardTitle>
|
||||
<VRow>
|
||||
|
||||
@@ -208,7 +208,7 @@ function onClose() {
|
||||
<VIcon class="cursor-move" icon="mdi-drag" />
|
||||
</IconBtn>
|
||||
</span>
|
||||
<DialogCloseBtn @click="onClose" />
|
||||
<VDialogCloseBtn @click="onClose" />
|
||||
<VCardText class="flex justify-space-between align-center gap-3">
|
||||
<div class="align-self-start">
|
||||
<h5 class="text-h6 mb-1">{{ props.group.name }}</h5>
|
||||
@@ -222,7 +222,7 @@ function onClose() {
|
||||
</VCard>
|
||||
<VDialog v-if="groupInfoDialog" v-model="groupInfoDialog" scrollable max-width="80rem" persistent>
|
||||
<VCard :title="`${props.group.name} - 配置`" class="rounded-t">
|
||||
<DialogCloseBtn v-model="groupInfoDialog" />
|
||||
<VDialogCloseBtn v-model="groupInfoDialog" />
|
||||
<VDivider />
|
||||
<VCardItem class="pt-1">
|
||||
<VRow class="mt-1">
|
||||
|
||||
@@ -3,6 +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'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -38,6 +39,7 @@ function getDefaultImage() {
|
||||
if (props.media?.server === 'plex') return plex
|
||||
else if (props.media?.server === 'emby') return emby
|
||||
else if (props.media?.server === 'jellyfin') return jellyfin
|
||||
else if (props.media?.server === 'trimemedia') return trimemedia
|
||||
else return plex
|
||||
}
|
||||
|
||||
@@ -156,7 +158,7 @@ onMounted(async () => {
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
}"
|
||||
@click="goPlay"
|
||||
>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType, Ref } from 'vue'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
||||
import { formatSeason, formatRating } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import type { MediaInfo, NotExistMediaInfo, Subscribe, MediaSeason, Site } from '@/api/types'
|
||||
import router, { registerAbortController } from '@/router'
|
||||
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 api from '@/api'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { formatSeason, formatRating } from '@/@core/utils/formatters'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import type { MediaInfo, Subscribe, MediaSeason, Site } from '@/api/types'
|
||||
import router, { registerAbortController } from '@/router'
|
||||
import { useUserStore } from '@/stores'
|
||||
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
||||
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
||||
import SubscribeSeasonDialog from '../dialog/SubscribeSeasonDialog.vue'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -35,18 +36,12 @@ const isImageLoaded = ref(false)
|
||||
// 图片加载失败
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
// TMDB识别标志
|
||||
const tmdbFlag = ref(true)
|
||||
|
||||
// 当前订阅状态
|
||||
const isSubscribed = ref(false)
|
||||
|
||||
// 本地存在状态
|
||||
const isExists = ref(false)
|
||||
|
||||
// 各季缺失状态:0-已入库 1-部分缺失 2-全部缺失,没有数据也是已入库
|
||||
const seasonsNotExisted = ref<{ [key: number]: number }>({})
|
||||
|
||||
// 订阅季弹窗
|
||||
const subscribeSeasonDialog = ref(false)
|
||||
|
||||
@@ -56,9 +51,6 @@ const subscribeEditDialog = ref(false)
|
||||
// 订阅ID
|
||||
const subscribeId = ref<number>()
|
||||
|
||||
// 季详情
|
||||
const seasonInfos = ref<MediaSeason[]>([])
|
||||
|
||||
// 选中的订阅季
|
||||
const seasonsSelected = ref<MediaSeason[]>([])
|
||||
|
||||
@@ -84,6 +76,12 @@ const selectedSites = ref<number[]>([])
|
||||
// 搜索菜单显示状态
|
||||
const searchMenuShow = ref(false)
|
||||
|
||||
// 选择站点对话框
|
||||
const chooseSiteDialog = ref(false)
|
||||
|
||||
// 选择的剧集组
|
||||
const episodeGroup = ref('')
|
||||
|
||||
// 查询所有站点
|
||||
async function querySites() {
|
||||
try {
|
||||
@@ -100,7 +98,6 @@ async function querySites() {
|
||||
async function querySelectedSites() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('system/setting/IndexerSites')
|
||||
|
||||
selectedSites.value = result.data?.value ?? []
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
@@ -115,14 +112,6 @@ function getMediaId() {
|
||||
else return `${props.media?.mediaid_prefix}:${props.media?.media_id}`
|
||||
}
|
||||
|
||||
// 订阅弹窗选择的多季
|
||||
function subscribeSeasons() {
|
||||
subscribeSeasonDialog.value = false
|
||||
seasonsSelected.value.forEach(season => {
|
||||
addSubscribe(season.season_number)
|
||||
})
|
||||
}
|
||||
|
||||
// 角标颜色
|
||||
function getChipColor(type: string) {
|
||||
if (type === '电影') return 'border-blue-500 bg-blue-600'
|
||||
@@ -133,24 +122,9 @@ function getChipColor(type: string) {
|
||||
// 添加订阅处理
|
||||
async function handleAddSubscribe() {
|
||||
if (props.media?.type === '电视剧') {
|
||||
// 查询所有季信息
|
||||
await getMediaSeasons()
|
||||
if (!seasonInfos.value || seasonInfos.value.length === 0) {
|
||||
$toast.error(`${props.media?.title} 查询剧集信息失败!`)
|
||||
return
|
||||
}
|
||||
// 检查各季的缺失状态
|
||||
await checkSeasonsNotExists()
|
||||
if (!tmdbFlag.value) return
|
||||
|
||||
if (seasonInfos.value.length === 1) {
|
||||
// 添加订阅
|
||||
addSubscribe(1)
|
||||
} else {
|
||||
// 弹出季选择列表,支持多选
|
||||
seasonsSelected.value = []
|
||||
subscribeSeasonDialog.value = true
|
||||
}
|
||||
// 弹出季选择列表,支持多选
|
||||
seasonsSelected.value = []
|
||||
subscribeSeasonDialog.value = true
|
||||
} else {
|
||||
// 电影
|
||||
addSubscribe()
|
||||
@@ -158,15 +132,12 @@ async function handleAddSubscribe() {
|
||||
}
|
||||
|
||||
// 调用API添加订阅,电视剧的话需要指定季
|
||||
async function addSubscribe(season = 0) {
|
||||
async function addSubscribe(season: number = 0, best_version: number = 0) {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
// 是否洗版
|
||||
let best_version = isExists.value ? 1 : 0
|
||||
if (season && props.media?.tmdb_id)
|
||||
// 全部存在时洗版
|
||||
best_version = !seasonsNotExisted.value[season] ? 1 : 0
|
||||
if (!best_version && props.media?.type == '电影') best_version = isExists.value ? 1 : 0
|
||||
// 请求API
|
||||
const result: { [key: string]: any } = await api.post('subscribe/', {
|
||||
name: props.media?.title,
|
||||
@@ -178,6 +149,7 @@ async function addSubscribe(season = 0) {
|
||||
mediaid: props.media?.media_id ? `${props.media?.mediaid_prefix}:${props.media?.media_id}` : '',
|
||||
season,
|
||||
best_version,
|
||||
episode_group: episodeGroup.value,
|
||||
})
|
||||
|
||||
// 订阅状态
|
||||
@@ -297,48 +269,6 @@ async function checkSubscribe(season = 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查所有季的缺失状态(数据库)
|
||||
async function checkSeasonsNotExists() {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', props.media)
|
||||
if (result) {
|
||||
result.forEach(item => {
|
||||
// 0-已入库 1-部分缺失 2-全部缺失
|
||||
let state = 0
|
||||
if (item.episodes.length === 0) state = 2
|
||||
else if (item.episodes.length < item.total_episode) state = 1
|
||||
seasonsNotExisted.value[item.season] = state
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
$toast.error(`${props.media?.title}无法识别TMDB媒体信息!`)
|
||||
tmdbFlag.value = false
|
||||
} finally {
|
||||
// 处理完成
|
||||
doneNProgress()
|
||||
}
|
||||
}
|
||||
|
||||
// 查询TMDB的所有季信息
|
||||
async function getMediaSeasons() {
|
||||
startNProgress()
|
||||
try {
|
||||
seasonInfos.value = await api.get('media/seasons', {
|
||||
params: {
|
||||
mediaid: getMediaId(),
|
||||
title: props.media?.title,
|
||||
year: props.media?.year,
|
||||
season: props.media?.season,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 查询订阅弹窗规则
|
||||
async function queryDefaultSubscribeConfig() {
|
||||
// 非管理员不显示
|
||||
@@ -347,9 +277,7 @@ async function queryDefaultSubscribeConfig() {
|
||||
let subscribe_config_url = ''
|
||||
if (props.media?.type === '电影') subscribe_config_url = 'system/setting/DefaultMovieSubscribeConfig'
|
||||
else subscribe_config_url = 'system/setting/DefaultTvSubscribeConfig'
|
||||
|
||||
const result: { [key: string]: any } = await api.get(subscribe_config_url)
|
||||
|
||||
if (result.data?.value) return result.data.value.show_edit_dialog
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
@@ -363,24 +291,18 @@ function handleSubscribe() {
|
||||
else handleAddSubscribe()
|
||||
}
|
||||
|
||||
// 计算存在状态的颜色
|
||||
function getExistColor(season: number) {
|
||||
const state = seasonsNotExisted.value[season]
|
||||
if (!state) return 'success'
|
||||
|
||||
if (state === 1) return 'warning'
|
||||
else if (state === 2) return 'error'
|
||||
else return 'success'
|
||||
}
|
||||
|
||||
// 计算存在状态的文本
|
||||
function getExistText(season: number) {
|
||||
const state = seasonsNotExisted.value[season]
|
||||
if (!state) return '已入库'
|
||||
|
||||
if (state === 1) return '部分缺失'
|
||||
else if (state === 2) return '缺失'
|
||||
else return '已入库'
|
||||
// 订阅多季
|
||||
function subscribeSeasons(seasons: MediaSeason[], seasonNoExists: { [key: number]: number }, groudId: string) {
|
||||
subscribeSeasonDialog.value = false
|
||||
episodeGroup.value = groudId
|
||||
seasonsSelected.value = seasons || []
|
||||
seasonsSelected.value.forEach(season => {
|
||||
let best_version = 0
|
||||
if (season && props.media?.tmdb_id)
|
||||
// 全部存在时洗版
|
||||
best_version = !seasonNoExists[season.season_number || 0] ? 1 : 0
|
||||
addSubscribe(season.season_number, best_version)
|
||||
})
|
||||
}
|
||||
|
||||
// 打开详情页
|
||||
@@ -411,9 +333,11 @@ function goMediaDetail(isHovering = false) {
|
||||
|
||||
// 点击搜索
|
||||
async function clickSearch() {
|
||||
if (allSites.value?.length > 0) return
|
||||
querySites()
|
||||
querySelectedSites()
|
||||
if (allSites.value?.length == 0) {
|
||||
querySites()
|
||||
querySelectedSites()
|
||||
}
|
||||
chooseSiteDialog.value = true
|
||||
}
|
||||
|
||||
// 开始搜索
|
||||
@@ -432,6 +356,13 @@ function handleSearch() {
|
||||
})
|
||||
}
|
||||
|
||||
// 搜索多站点
|
||||
function searchSites(sites: number[]) {
|
||||
chooseSiteDialog.value = false
|
||||
selectedSites.value = sites
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// 懒加载检查
|
||||
function handleCheckLazy() {
|
||||
if (props.media?.collection_id) {
|
||||
@@ -484,26 +415,6 @@ const getImgUrl: Ref<string> = computed(() => {
|
||||
return url
|
||||
})
|
||||
|
||||
// 拼装季图片地址
|
||||
function getSeasonPoster(posterPath: string) {
|
||||
if (!posterPath) return ''
|
||||
return `https://${globalSettings.TMDB_IMAGE_DOMAIN}/t/p/w500${posterPath}`
|
||||
}
|
||||
|
||||
// 将yyyy-mm-dd转换为yyyy年mm月dd日
|
||||
function formatAirDate(airDate: string) {
|
||||
if (!airDate) return ''
|
||||
const date = new Date(airDate.replaceAll(/-/g, '/'))
|
||||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
|
||||
}
|
||||
|
||||
// 从yyyy-mm-dd中提取年份
|
||||
function getYear(airDate: string) {
|
||||
if (!airDate) return ''
|
||||
const date = new Date(airDate.replaceAll(/-/g, '/'))
|
||||
return date.getFullYear()
|
||||
}
|
||||
|
||||
// 移除订阅
|
||||
function onRemoveSubscribe() {
|
||||
subscribeEditDialog.value = false
|
||||
@@ -518,9 +429,9 @@ function onRemoveSubscribe() {
|
||||
v-bind="hover.props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
class="outline-none shadow ring-gray-500 rounded-lg"
|
||||
class="outline-none ring-gray-500 media-card"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
'ring-1': isImageLoaded,
|
||||
}"
|
||||
@click.stop="goMediaDetail(hover.isHovering ?? false)"
|
||||
@@ -539,6 +450,7 @@ function onRemoveSubscribe() {
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
|
||||
<!-- 详情 -->
|
||||
<VCardText
|
||||
v-show="hover.isHovering || imageLoadError || searchMenuShow"
|
||||
@@ -554,31 +466,7 @@ function onRemoveSubscribe() {
|
||||
</p>
|
||||
<div v-if="props.media?.collection_id" class="mb-3" @click.stop=""></div>
|
||||
<div v-else class="flex align-center justify-between">
|
||||
<VMenu close-on-content-click v-model="searchMenuShow" max-width="450">
|
||||
<template v-slot:activator="{ props }">
|
||||
<IconBtn v-bind="props" icon="mdi-magnify" color="white" @click.stop="clickSearch" />
|
||||
</template>
|
||||
<VList>
|
||||
<VListItem>
|
||||
<VChipGroup v-model="selectedSites" column multiple @click.stop>
|
||||
<VChip
|
||||
v-for="site in allSites"
|
||||
:key="site.id"
|
||||
:color="selectedSites.includes(site.id) ? 'primary' : ''"
|
||||
filter
|
||||
variant="outlined"
|
||||
:value="site.id"
|
||||
size="small"
|
||||
>
|
||||
{{ site.name }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<VBtn @click="handleSearch" block>搜索</VBtn>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
<IconBtn icon="mdi-magnify" color="white" @click.stop="clickSearch" />
|
||||
<IconBtn icon="mdi-heart" :color="isSubscribed ? 'error' : 'white'" @click.stop="handleSubscribe" />
|
||||
</div>
|
||||
</VCardText>
|
||||
@@ -588,7 +476,7 @@ function onRemoveSubscribe() {
|
||||
variant="elevated"
|
||||
size="small"
|
||||
:class="getChipColor(props.media?.type || '')"
|
||||
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||
class="absolute left-2 top-2 bg-opacity-80 text-white font-bold"
|
||||
>
|
||||
{{ props.media?.type }}
|
||||
</VChip>
|
||||
@@ -600,7 +488,7 @@ function onRemoveSubscribe() {
|
||||
variant="elevated"
|
||||
size="small"
|
||||
:class="getChipColor('rating')"
|
||||
class="absolute right-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||
class="absolute right-2 top-2 bg-opacity-80 text-white font-bold"
|
||||
>
|
||||
{{ formatRating(props.media?.vote_average) }}
|
||||
</VChip>
|
||||
@@ -619,62 +507,13 @@ function onRemoveSubscribe() {
|
||||
</template>
|
||||
</VHover>
|
||||
<!-- 订阅季弹窗 -->
|
||||
<VBottomSheet v-if="subscribeSeasonDialog" v-model="subscribeSeasonDialog" inset scrollable>
|
||||
<VCard class="rounded-t">
|
||||
<DialogCloseBtn @click="subscribeSeasonDialog = false" />
|
||||
<VCardItem>
|
||||
<VCardTitle class="pe-10"> 订阅 - {{ props.media?.title }} </VCardTitle>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VList v-model:selected="seasonsSelected" lines="three" select-strategy="classic">
|
||||
<VListItem v-for="(item, i) in seasonInfos" :key="i" :value="item">
|
||||
<template #prepend>
|
||||
<VImg
|
||||
height="90"
|
||||
width="60"
|
||||
:src="getSeasonPoster(item.poster_path || '')"
|
||||
aspect-ratio="2/3"
|
||||
class="object-cover rounded shadow ring-gray-500 me-3"
|
||||
cover
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</template>
|
||||
<VListItemTitle> 第 {{ item.season_number }} 季 </VListItemTitle>
|
||||
<VListItemSubtitle class="mt-1 me-2">
|
||||
<VChip v-if="item.vote_average" color="primary" size="small" class="mb-1">
|
||||
<VIcon icon="mdi-star" /> {{ item.vote_average }}
|
||||
</VChip>
|
||||
{{ getYear(item.air_date || '') }} • {{ item.episode_count }} 集
|
||||
</VListItemSubtitle>
|
||||
<VListItemSubtitle>
|
||||
《{{ media?.title }}》第 {{ item.season_number }} 季于 {{ formatAirDate(item.air_date || '') }} 首播。
|
||||
</VListItemSubtitle>
|
||||
<VListItemSubtitle>
|
||||
<VChip v-if="seasonsNotExisted" class="mt-2" size="small" :color="getExistColor(item.season_number || 0)">
|
||||
{{ getExistText(item.season_number || 0) }}
|
||||
</VChip>
|
||||
</VListItemSubtitle>
|
||||
<template #append="{ isSelected }">
|
||||
<VListItemAction start>
|
||||
<VSwitch :model-value="isSelected" />
|
||||
</VListItemAction>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
<div class="my-2 text-center">
|
||||
<VBtn size="large" :disabled="seasonsSelected.length === 0" width="30%" @click="subscribeSeasons">
|
||||
{{ seasonsSelected.length === 0 ? '请选择订阅季' : '提交订阅' }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCard>
|
||||
</VBottomSheet>
|
||||
<subscribeSeasonDialog
|
||||
v-if="subscribeSeasonDialog"
|
||||
v-model="subscribeSeasonDialog"
|
||||
:media="media"
|
||||
@subscribe="subscribeSeasons"
|
||||
@close="subscribeSeasonDialog = false"
|
||||
/>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
<SubscribeEditDialog
|
||||
v-if="subscribeEditDialog"
|
||||
@@ -684,4 +523,13 @@ function onRemoveSubscribe() {
|
||||
@save="subscribeEditDialog = false"
|
||||
@remove="onRemoveSubscribe"
|
||||
/>
|
||||
<!-- 站点选择对话框 -->
|
||||
<SearchSiteDialog
|
||||
v-if="chooseSiteDialog"
|
||||
v-model="chooseSiteDialog"
|
||||
:sites="allSites"
|
||||
:selected="selectedSites"
|
||||
@search="searchSites"
|
||||
@close="chooseSiteDialog = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useToast } from 'vue-toast-notification'
|
||||
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 api from '@/api'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
@@ -101,6 +102,8 @@ const getIcon = computed(() => {
|
||||
return emby_image
|
||||
case 'jellyfin':
|
||||
return jellyfin_image
|
||||
case 'trimemedia':
|
||||
return trimemedia_image
|
||||
default:
|
||||
return plex_image
|
||||
}
|
||||
@@ -172,7 +175,7 @@ onMounted(() => {
|
||||
<template>
|
||||
<div>
|
||||
<VCard variant="tonal" @click="openMediaServerInfoDialog">
|
||||
<DialogCloseBtn @click="onClose" />
|
||||
<VDialogCloseBtn @click="onClose" />
|
||||
<VCardText class="flex justify-space-between align-center gap-3">
|
||||
<div class="align-self-start flex-1">
|
||||
<div class="text-h6 mb-1">{{ mediaserver.name }}</div>
|
||||
@@ -187,7 +190,7 @@ onMounted(() => {
|
||||
</VCard>
|
||||
<VDialog v-if="mediaServerInfoDialog" v-model="mediaServerInfoDialog" scrollable max-width="40rem" persistent>
|
||||
<VCard :title="`${props.mediaserver.name} - 配置`" class="rounded-t">
|
||||
<DialogCloseBtn v-model="mediaServerInfoDialog" />
|
||||
<VDialogCloseBtn v-model="mediaServerInfoDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VForm>
|
||||
@@ -278,6 +281,44 @@ onMounted(() => {
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-if="mediaServerInfo.type == 'trimemedia'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaServerInfo.name"
|
||||
label="名称"
|
||||
placeholder="必填;不可与其他名称重名"
|
||||
hint="媒体服务器的别名"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="mediaServerInfo.config.host"
|
||||
label="地址"
|
||||
placeholder="http(s)://ip:port"
|
||||
hint="服务端地址,格式:http(s)://ip:port"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="mediaServerInfo.config.play_host"
|
||||
label="外网播放地址"
|
||||
placeholder="http(s)://domain:port"
|
||||
hint="跳转播放页面使用的地址,格式:http(s)://domain:port"
|
||||
persistent-hint
|
||||
active
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField v-model="mediaServerInfo.config.username" label="用户名" active />
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField type="password" v-model="mediaServerInfo.config.password" label="密码" active />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-if="mediaServerInfo.type == 'plex'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
|
||||
@@ -53,7 +53,6 @@ function replaceNewLine(value: string) {
|
||||
aspect-ratio="3/2"
|
||||
cover
|
||||
position="top"
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
|
||||
@@ -66,7 +66,6 @@ const notificationTypes = [
|
||||
function openNotificationInfoDialog() {
|
||||
// 替换成深复制,避免修改时影响原数据
|
||||
notificationInfo.value = cloneDeep(props.notification)
|
||||
console.log(`当前卡片的通知信息:${JSON.stringify(notificationInfo.value)}`)
|
||||
notificationInfoDialog.value = true
|
||||
}
|
||||
|
||||
@@ -120,7 +119,7 @@ function onClose() {
|
||||
<VIcon class="cursor-move" icon="mdi-drag" />
|
||||
</IconBtn>
|
||||
</span>
|
||||
<DialogCloseBtn @click="onClose" />
|
||||
<VDialogCloseBtn @click="onClose" />
|
||||
<VCardText class="flex justify-space-between align-center gap-3">
|
||||
<div class="align-self-start">
|
||||
<div class="flex items-center">
|
||||
@@ -134,7 +133,7 @@ function onClose() {
|
||||
</VCard>
|
||||
<VDialog v-if="notificationInfoDialog" v-model="notificationInfoDialog" scrollable max-width="40rem" persistent>
|
||||
<VCard :title="`${props.notification.name} - 配置`" class="rounded-t">
|
||||
<DialogCloseBtn v-model="notificationInfoDialog" />
|
||||
<VDialogCloseBtn v-model="notificationInfoDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VForm>
|
||||
|
||||
@@ -77,14 +77,13 @@ function goPersonDetail() {
|
||||
v-bind="hover.props"
|
||||
:height="personProps.height"
|
||||
:width="personProps.width"
|
||||
class="rounded-lg"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105': hover.isHovering,
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
}"
|
||||
@click.stop="goPersonDetail"
|
||||
>
|
||||
<div
|
||||
class="person-card relative transform-gpu cursor-pointer rounded shadow transition duration-150 ease-in-out scale-100 ring-gray-700"
|
||||
class="person-card relative transform-gpu cursor-pointer rounded transition duration-150 ease-in-out scale-100 ring-gray-700"
|
||||
>
|
||||
<div style="padding-block-end: 150%">
|
||||
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2">
|
||||
@@ -116,12 +115,20 @@ function goPersonDetail() {
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.person-card {
|
||||
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-theme-surface)) 60%);
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
rgb工(var(--v-theme-background), 0.3),
|
||||
rgba(var(--v-theme-surface), 0.3) 60%
|
||||
);
|
||||
}
|
||||
|
||||
.person-card:hover {
|
||||
background-image: linear-gradient(45deg, rgb(var(--v-theme-background)), rgb(var(--v-custom-background)) 60%);
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
rgba(var(--v-theme-background), 0.3),
|
||||
rgba(var(--v-custom-background), 0.3) 60%
|
||||
);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -147,78 +147,86 @@ const dropdownItems = ref([
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VCard :width="props.width" :height="props.height" @click="detailDialog = true" class="flex flex-col h-full">
|
||||
<div
|
||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center"
|
||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
||||
></div>
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<VCardTitle class="text-white text-lg px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis ...">
|
||||
{{ props.plugin?.plugin_name }}
|
||||
<span class="text-sm text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
||||
</VCardTitle>
|
||||
<VCardText class="text-white text-sm px-2 py-0 text-shadow overflow-hidden line-clamp-3 ...">
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:width="props.width"
|
||||
:height="props.height"
|
||||
@click="detailDialog = true"
|
||||
class="flex flex-col h-full"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center"
|
||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
||||
></div>
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<VCardTitle
|
||||
class="text-white text-lg px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis ..."
|
||||
>
|
||||
{{ props.plugin?.plugin_name }}
|
||||
<span class="text-sm text-gray-200">v{{ props.plugin?.plugin_version }}</span>
|
||||
</VCardTitle>
|
||||
<VCardText class="text-white text-sm px-2 py-0 text-shadow overflow-hidden line-clamp-3 ...">
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</VCardText>
|
||||
</div>
|
||||
<div class="relative flex-shrink-0 self-center">
|
||||
<VAvatar size="64">
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</div>
|
||||
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||
<span>
|
||||
<VIcon icon="mdi-github" class="me-1" />
|
||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a>
|
||||
</span>
|
||||
<span v-if="props.count" class="ms-3">
|
||||
<VIcon icon="mdi-download" />
|
||||
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||
</span>
|
||||
<div class="me-n3 absolute bottom-1 right-3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem v-for="(item, i) in dropdownItems" v-show="item.show" :key="i" @click="item.props.click">
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
</div>
|
||||
<div class="relative flex-shrink-0 self-center">
|
||||
<VAvatar size="64">
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</div>
|
||||
<VCardText class="flex flex-none align-self-baseline py-3 w-full align-end">
|
||||
<span>
|
||||
<VIcon icon="mdi-github" class="me-1" />
|
||||
<a :href="props.plugin?.author_url" target="_blank" @click.stop>
|
||||
{{ props.plugin?.plugin_author }}
|
||||
</a>
|
||||
</span>
|
||||
<span v-if="props.count" class="ms-3">
|
||||
<VIcon icon="mdi-download" />
|
||||
<span class="text-sm ms-1 mt-1">{{ props.count?.toLocaleString() }}</span>
|
||||
</span>
|
||||
<div class="me-n3 absolute bottom-1 right-3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(item, i) in dropdownItems"
|
||||
v-show="item.show"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCard>
|
||||
</template>
|
||||
</VHover>
|
||||
<!-- 安装插件进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
<!-- 更新日志 -->
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
||||
<DialogCloseBtn @click="releaseDialog = false" />
|
||||
<VDialogCloseBtn @click="releaseDialog = false" />
|
||||
<VDivider />
|
||||
<VersionHistory :history="props.plugin?.history" />
|
||||
</VCard>
|
||||
@@ -226,7 +234,7 @@ const dropdownItems = ref([
|
||||
<!-- 插件详情-->
|
||||
<VDialog v-if="detailDialog" v-model="detailDialog" max-width="30rem">
|
||||
<VCard>
|
||||
<DialogCloseBtn @click="detailDialog = false" />
|
||||
<VDialogCloseBtn @click="detailDialog = false" />
|
||||
<VCardText>
|
||||
<VCol>
|
||||
<div class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row">
|
||||
@@ -237,7 +245,6 @@ const dropdownItems = ref([
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
|
||||
@@ -335,6 +335,9 @@ watch(
|
||||
:height="props.height"
|
||||
@click="openPluginDetail"
|
||||
class="flex flex-col h-full"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||
@@ -354,14 +357,13 @@ watch(
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</VCardText>
|
||||
</div>
|
||||
<div class="relative flex-shrink-0 self-center">
|
||||
<div class="relative flex-shrink-0 self-center cursor-move">
|
||||
<VAvatar size="64">
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
@@ -390,7 +392,6 @@ watch(
|
||||
v-for="(item, i) in dropdownItems"
|
||||
v-show="item.show"
|
||||
:key="i"
|
||||
variant="plain"
|
||||
:base-color="item.props.color"
|
||||
@click="item.props.click"
|
||||
>
|
||||
@@ -404,10 +405,7 @@ watch(
|
||||
</IconBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
<div v-if="hover.isHovering" class="me-n3 absolute top-0 right-5">
|
||||
<VIcon class="cursor-move text-white">mdi-drag</VIcon>
|
||||
</div>
|
||||
<div v-else-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
|
||||
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
|
||||
<VIcon icon="mdi-new-box" class="text-white" />
|
||||
</div>
|
||||
</VCard>
|
||||
@@ -437,9 +435,9 @@ watch(
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
|
||||
<!-- 更新日志 -->
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" max-height="80vh" scrollable>
|
||||
<VCard :title="`${props.plugin?.plugin_name} 更新说明`">
|
||||
<DialogCloseBtn @click="releaseDialog = false" />
|
||||
<VDialogCloseBtn @click="releaseDialog = false" />
|
||||
<VDivider />
|
||||
<VersionHistory :history="props.plugin?.history" />
|
||||
<VDivider />
|
||||
|
||||
@@ -43,9 +43,9 @@ function goPlay(isHovering: boolean | null = false) {
|
||||
v-bind="hover.props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
class="outline-none shadow ring-gray-500 rounded-lg"
|
||||
class="outline-none ring-gray-500"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
'ring-1': isImageLoaded,
|
||||
}"
|
||||
>
|
||||
@@ -69,7 +69,7 @@ function goPlay(isHovering: boolean | null = false) {
|
||||
variant="elevated"
|
||||
size="small"
|
||||
:class="getChipColor(props.media?.type || '')"
|
||||
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||
class="absolute left-2 top-2 bg-opacity-80 text-white font-bold"
|
||||
>
|
||||
{{ props.media?.type }}
|
||||
</VChip>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
import noImage from '@images/logos/site.webp'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import SiteAddEditDialog from '../dialog/SiteAddEditDialog.vue'
|
||||
import SiteUserDataDialog from '../dialog/SiteUserDataDialog.vue'
|
||||
@@ -9,6 +10,7 @@ import api from '@/api'
|
||||
import type { Site, SiteStatistic, SiteUserData } from '@/api/types'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
|
||||
// 输入参数
|
||||
const cardProps = defineProps({
|
||||
@@ -19,6 +21,9 @@ const cardProps = defineProps({
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['update', 'remove'])
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 图标
|
||||
const siteIcon = ref<string>('')
|
||||
|
||||
@@ -50,6 +55,9 @@ const siteStats = ref<SiteStatistic>({})
|
||||
async function getSiteIcon() {
|
||||
try {
|
||||
siteIcon.value = (await api.get(`site/icon/${cardProps.site?.id}`)).data.icon
|
||||
if (!siteIcon.value) {
|
||||
siteIcon.value = noImage
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -103,6 +111,25 @@ function openSitePage() {
|
||||
window.open(cardProps.site?.url, '_blank')
|
||||
}
|
||||
|
||||
// 调用API删除站点信息
|
||||
async function deleteSiteInfo() {
|
||||
const isConfirmed = await createConfirm({
|
||||
title: '确认',
|
||||
content: `是否确认删除站点?`,
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(`site/${cardProps.site?.id}`)
|
||||
if (result.success) emit('remove')
|
||||
else $toast.error(`${cardProps.site?.name} 删除失败:${result.message}`)
|
||||
} catch (error) {
|
||||
$toast.error(`${cardProps.site?.name} 删除失败!`)
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据站点状态显示不同的状态图标
|
||||
const statColor = computed(() => {
|
||||
if (isNullOrEmptyObject(siteStats.value)) {
|
||||
@@ -117,10 +144,28 @@ const statColor = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 计算上传量和下载量的百分比
|
||||
const getPercentage = computed(() => {
|
||||
if (cardProps.data?.upload === 0) return 100
|
||||
return ((cardProps.data?.download ?? 0) / ((cardProps.data?.download ?? 0) + (cardProps.data?.upload ?? 0))) * 100
|
||||
// 数据百分比计算
|
||||
const getMaxDataValue = computed(() => {
|
||||
// 获取站点数据中的最大值作为基准
|
||||
const upload = cardProps.data?.upload || 0
|
||||
const download = cardProps.data?.download || 0
|
||||
|
||||
// 避免两者都为0的情况
|
||||
if (upload === 0 && download === 0) return 1
|
||||
|
||||
return Math.max(upload, download)
|
||||
})
|
||||
|
||||
// 上传百分比
|
||||
const getUploadPercent = computed(() => {
|
||||
const upload = cardProps.data?.upload || 0
|
||||
return Math.min(100, Math.max(3, (upload / getMaxDataValue.value) * 100))
|
||||
})
|
||||
|
||||
// 下载百分比
|
||||
const getDownloadPercent = computed(() => {
|
||||
const download = cardProps.data?.download || 0
|
||||
return Math.min(100, Math.max(3, (download / getMaxDataValue.value) * 100))
|
||||
})
|
||||
|
||||
// 保存站点
|
||||
@@ -151,98 +196,161 @@ onMounted(() => {
|
||||
<template>
|
||||
<div>
|
||||
<VCard
|
||||
:variant="cardProps.site?.is_active ? 'elevated' : 'outlined'"
|
||||
class="overflow-hidden h-full flex flex-col"
|
||||
@click="handleResourceBrowse"
|
||||
class="site-card relative h-full flex flex-col overflow-hidden group transition-all duration-300 cursor-pointer hover:-translate-y-1"
|
||||
:class="[
|
||||
cardProps.site?.is_active ? '' : 'opacity-70',
|
||||
{
|
||||
'border-error': statColor === 'error',
|
||||
'border-warning': statColor === 'warning',
|
||||
'border-success': statColor === 'success',
|
||||
},
|
||||
]"
|
||||
:ripple="false"
|
||||
variant="flat"
|
||||
elevation="0"
|
||||
rounded="lg"
|
||||
hover
|
||||
@click="siteEditDialog = true"
|
||||
>
|
||||
<template #image>
|
||||
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
|
||||
<VImg :src="siteIcon" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardItem style="padding-block-end: 0">
|
||||
<VCardTitle class="font-bold">
|
||||
{{ cardProps.site?.name }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
{{ cardProps.site?.url }}
|
||||
</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VCardText class="py-1">
|
||||
<VTooltip v-if="cardProps.site?.limit_interval" text="流控">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-speedometer" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip v-if="cardProps.site?.proxy === 1" text="代理">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-network-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip v-if="cardProps.site?.render === 1" text="浏览器仿真">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-apple-safari" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
<VTooltip v-if="cardProps.site?.filter" text="过滤">
|
||||
<template #activator="{ props }">
|
||||
<VIcon color="primary" class="me-2" v-bind="props" icon="mdi-filter-cog-outline" />
|
||||
</template>
|
||||
</VTooltip>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<div class="text-sm">
|
||||
↑ {{ formatFileSize(cardProps.data?.upload || 0) }} / ↓ {{ formatFileSize(cardProps.data?.download || 0) }}
|
||||
<!-- 装饰性状态指示器 -->
|
||||
<div v-if="cardProps.site?.is_active" class="site-status-indicator" :class="statColor"></div>
|
||||
|
||||
<!-- 主体部分 -->
|
||||
<div class="relative flex-1 flex flex-col p-3 z-1">
|
||||
<!-- 顶部:图标和站点名称 -->
|
||||
<div class="flex items-center mb-1">
|
||||
<!-- 站点图标 -->
|
||||
<VAvatar tile rounded="lg" size="32" class="me-2 cursor-move">
|
||||
<VImg :src="siteIcon" class="w-full h-full" :alt="cardProps.site?.name" cover>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-square" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</VAvatar>
|
||||
|
||||
<!-- 站点名称和特性图标 -->
|
||||
<div class="flex-1 min-w-0 flex items-center">
|
||||
<h3 class="text-lg font-semibold leading-tight truncate">{{ cardProps.site?.name }}</h3>
|
||||
|
||||
<!-- 站点特性图标 -->
|
||||
<div class="flex items-center gap-2 ml-auto mr-10">
|
||||
<div v-if="cardProps.site?.limit_interval" class="hover:bg-primary/8 transition-colors">
|
||||
<VIcon icon="mdi-speedometer" size="16" color="primary" class="opacity-85 hover:opacity-100" />
|
||||
</div>
|
||||
<div v-if="cardProps.site?.proxy" class="hover:bg-primary/8 transition-colors">
|
||||
<VIcon icon="mdi-network-outline" size="16" color="primary" class="opacity-85 hover:opacity-100" />
|
||||
</div>
|
||||
<div v-if="cardProps.site?.render" class="hover:bg-primary/8 transition-colors">
|
||||
<VIcon icon="mdi-apple-safari" size="16" color="primary" class="opacity-85 hover:opacity-100" />
|
||||
</div>
|
||||
<div v-if="cardProps.site?.filter" class="hover:bg-primary/8 transition-colors">
|
||||
<VIcon icon="mdi-filter-cog-outline" size="16" color="primary" class="opacity-85 hover:opacity-100" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-chevron-down" color="primary" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
|
||||
<!-- 中间部分:网址 -->
|
||||
<div class="my-3">
|
||||
<div class="text-sm text-medium-emphasis truncate" @click.stop="openSitePage">
|
||||
{{ cardProps.site?.url }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部:数据统计 -->
|
||||
<div class="flex-1 flex flex-col justify-end">
|
||||
<!-- 更直观的上传下载数据条 -->
|
||||
<div class="border-t mt-1.5 pt-1.5">
|
||||
<!-- 上传数据 -->
|
||||
<div class="flex items-center justify-between gap-3 mb-1.5">
|
||||
<div class="text-sm text-medium-emphasis min-w-[70px]">
|
||||
<VIcon icon="mdi-arrow-up" size="14" color="info" class="mr-1" />
|
||||
<span>{{ formatFileSize(cardProps.data?.upload || 0) }}</span>
|
||||
</div>
|
||||
<div class="flex-grow h-1 rounded bg-on-surface/8 relative overflow-hidden">
|
||||
<VProgressLinear :model-value="getUploadPercent" color="info" height="4" rounded="lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下载数据 -->
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center text-[0.8rem] text-medium-emphasis min-w-[70px]">
|
||||
<VIcon icon="mdi-arrow-down" size="14" color="success" class="mr-1" />
|
||||
<span>{{ formatFileSize(cardProps.data?.download || 0) }}</span>
|
||||
</div>
|
||||
<div class="flex-grow h-1 rounded bg-on-surface/8 relative overflow-hidden">
|
||||
<VProgressLinear :model-value="getDownloadPercent" color="warning" height="4" rounded="lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作按钮区 -->
|
||||
<VSheet
|
||||
class="site-card-actions absolute inset-y-0 right-0 z-20 flex flex-col py-2 px-1 transform translate-x-full transition-transform duration-200"
|
||||
>
|
||||
<!-- 测试按钮 -->
|
||||
<VBtn
|
||||
icon
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
class="mb-1 relative w-10 h-10 min-w-10 flex items-center justify-center rounded-full"
|
||||
:disabled="testButtonDisable"
|
||||
@click.stop="testSite"
|
||||
>
|
||||
<div class="relative flex items-center justify-center w-full h-full">
|
||||
<div
|
||||
class="w-[22px] h-[22px] rounded-full shadow-[inset_0_0_0_2px_rgba(var(--v-theme-on-surface),0.1)] pulse-dot"
|
||||
:class="statColor"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="testButtonDisable"
|
||||
class="absolute inset-0 flex flex-col items-center justify-center bg-surface/95 rounded-full shadow-md animate-fade-in"
|
||||
>
|
||||
<div class="relative w-6 h-6">
|
||||
<div class="spinner-circle"></div>
|
||||
</div>
|
||||
</div>
|
||||
</VBtn>
|
||||
|
||||
<!-- 用户数据按钮 -->
|
||||
<VBtn icon variant="text" @click.stop="handleSiteUserData">
|
||||
<VIcon icon="mdi-chart-bell-curve" size="small" />
|
||||
</VBtn>
|
||||
|
||||
<!-- 更新按钮 -->
|
||||
<VBtn icon variant="text" @click.stop="handleSiteUpdate">
|
||||
<VIcon icon="mdi-refresh" size="small" />
|
||||
</VBtn>
|
||||
|
||||
<!-- 更多选项按钮 -->
|
||||
<VBtn icon variant="text" class="mt-auto">
|
||||
<VIcon icon="mdi-dots-vertical" size="small" />
|
||||
<VMenu :activator="'parent'" :close-on-content-click="true" :location="'left'">
|
||||
<VList>
|
||||
<VListItem variant="plain" @click="siteEditDialog = true" base-color="info">
|
||||
<VListItem @click="handleResourceBrowse" base-color="info">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-file-edit-outline" />
|
||||
<VIcon icon="mdi-web" size="small" />
|
||||
</template>
|
||||
<VListItemTitle>编辑站点</VListItemTitle>
|
||||
<VListItemTitle>浏览资源</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" @click="handleSiteUserData">
|
||||
<VListItem @click="deleteSiteInfo">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-chart-bell-curve" />
|
||||
<VIcon icon="mdi-delete-outline" size="small" color="error" />
|
||||
</template>
|
||||
<VListItemTitle>查看站点数据</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" :disabled="testButtonDisable" @click.stop="testSite">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-link" />
|
||||
</template>
|
||||
<VListItemTitle>{{ testButtonText }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" v-if="!cardProps.site?.public" @click="handleSiteUpdate">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-refresh" />
|
||||
</template>
|
||||
<VListItemTitle>更新 Cookie & UA</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" @click="openSitePage">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-open-in-new" />
|
||||
</template>
|
||||
<VListItemTitle>访问站点</VListItemTitle>
|
||||
<VListItemTitle class="text-error">删除站点</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<VSpacer />
|
||||
</VCardActions>
|
||||
<StatIcon v-if="cardProps.site?.is_active" :color="statColor" />
|
||||
<span class="absolute top-1 right-8">
|
||||
<VIcon class="cursor-move">mdi-drag</VIcon>
|
||||
</span>
|
||||
<div class="w-full absolute bottom-0" v-if="(cardProps.data?.upload || cardProps.data?.download || 0) > 0">
|
||||
<VProgressLinear :model-value="getPercentage" bg-color="success" color="warning" bg-opacity="0.5" height="3" />
|
||||
</div>
|
||||
</VBtn>
|
||||
</VSheet>
|
||||
</VCard>
|
||||
<!-- 更新站点Cookie & UA弹窗 -->
|
||||
|
||||
<!-- 对话框组件 -->
|
||||
<SiteCookieUpdateDialog
|
||||
v-if="siteCookieDialog"
|
||||
v-model="siteCookieDialog"
|
||||
@@ -250,7 +358,6 @@ onMounted(() => {
|
||||
@close="siteCookieDialog = false"
|
||||
@done="onSiteCookieUpdated"
|
||||
/>
|
||||
<!-- 站点编辑弹窗 -->
|
||||
<SiteAddEditDialog
|
||||
v-if="siteEditDialog"
|
||||
v-model="siteEditDialog"
|
||||
@@ -259,14 +366,12 @@ onMounted(() => {
|
||||
@remove="emit('remove')"
|
||||
@close="siteEditDialog = false"
|
||||
/>
|
||||
<!-- 站点数据弹窗 -->
|
||||
<SiteUserDataDialog
|
||||
v-if="siteUserDataDialog"
|
||||
v-model="siteUserDataDialog"
|
||||
:site="cardProps.site"
|
||||
@close="siteUserDataDialog = false"
|
||||
/>
|
||||
<!-- 站点资源弹窗 -->
|
||||
<SiteResourceDialog
|
||||
v-if="resourceDialog"
|
||||
v-model="resourceDialog"
|
||||
@@ -275,3 +380,213 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.site-card:hover {
|
||||
.site-card-actions {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.site-status-indicator {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
block-size: 2px;
|
||||
inset-block-start: 0;
|
||||
inset-inline: 0;
|
||||
opacity: 0.5;
|
||||
transition: block-size 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.site-status-indicator.error {
|
||||
background: linear-gradient(90deg, transparent, rgba(var(--v-theme-error), 0.7), transparent);
|
||||
box-shadow: 0 0 8px rgba(var(--v-theme-error), 0.3);
|
||||
}
|
||||
|
||||
.site-status-indicator.warning {
|
||||
background: linear-gradient(90deg, transparent, rgba(var(--v-theme-warning), 0.7), transparent);
|
||||
box-shadow: 0 0 8px rgba(var(--v-theme-warning), 0.3);
|
||||
}
|
||||
|
||||
.site-status-indicator.success {
|
||||
background: linear-gradient(90deg, transparent, rgba(var(--v-theme-success), 0.7), transparent);
|
||||
box-shadow: 0 0 8px rgba(var(--v-theme-success), 0.3);
|
||||
}
|
||||
|
||||
.site-status-indicator.secondary {
|
||||
background: linear-gradient(90deg, transparent, rgba(var(--v-theme-secondary), 0.7), transparent);
|
||||
box-shadow: 0 0 8px rgba(var(--v-theme-secondary), 0.3);
|
||||
}
|
||||
|
||||
/* 站点卡片悬停时状态指示器变化 */
|
||||
.site-card:hover .site-status-indicator {
|
||||
block-size: 2px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 上传下载条样式 */
|
||||
.upload-bar {
|
||||
background: linear-gradient(90deg, #4d79ff, #07f);
|
||||
box-shadow: 0 0 4px rgba(0, 119, 255, 50%);
|
||||
animation: pulse-width 2s infinite;
|
||||
}
|
||||
|
||||
.download-bar {
|
||||
background: linear-gradient(90deg, #42d392, #00b77e);
|
||||
box-shadow: 0 0 4px rgba(0, 183, 126, 50%);
|
||||
animation: pulse-width 2s infinite;
|
||||
}
|
||||
|
||||
/* 测试状态点样式 */
|
||||
.pulse-dot::before {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
height: 70%;
|
||||
width: 70%;
|
||||
top: 15%;
|
||||
left: 15%;
|
||||
}
|
||||
|
||||
.pulse-dot::after {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.pulse-dot.error::before {
|
||||
background-color: rgba(var(--v-theme-error), 1);
|
||||
box-shadow: 0 0 10px rgba(var(--v-theme-error), 0.8);
|
||||
}
|
||||
|
||||
.pulse-dot.error::after {
|
||||
animation: pulse-animation-error 2s infinite;
|
||||
box-shadow: 0 0 0 2px rgba(var(--v-theme-error), 0.3);
|
||||
}
|
||||
|
||||
.pulse-dot.warning::before {
|
||||
background-color: rgba(var(--v-theme-warning), 1);
|
||||
box-shadow: 0 0 10px rgba(var(--v-theme-warning), 0.8);
|
||||
}
|
||||
|
||||
.pulse-dot.warning::after {
|
||||
animation: pulse-animation-warning 2s infinite;
|
||||
box-shadow: 0 0 0 2px rgba(var(--v-theme-warning), 0.3);
|
||||
}
|
||||
|
||||
.pulse-dot.success::before {
|
||||
background-color: rgba(var(--v-theme-success), 1);
|
||||
box-shadow: 0 0 10px rgba(var(--v-theme-success), 0.8);
|
||||
}
|
||||
|
||||
.pulse-dot.success::after {
|
||||
animation: pulse-animation-success 2s infinite;
|
||||
box-shadow: 0 0 0 2px rgba(var(--v-theme-success), 0.3);
|
||||
}
|
||||
|
||||
.pulse-dot.secondary::before {
|
||||
background-color: rgba(var(--v-theme-secondary), 1);
|
||||
box-shadow: 0 0 10px rgba(var(--v-theme-secondary), 0.8);
|
||||
}
|
||||
|
||||
.pulse-dot.secondary::after {
|
||||
animation: pulse-animation-secondary 2s infinite;
|
||||
box-shadow: 0 0 0 2px rgba(var(--v-theme-secondary), 0.3);
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.spinner-circle {
|
||||
position: absolute;
|
||||
border: 1px solid rgba(var(--v-theme-primary), 0.2);
|
||||
border-top-color: rgba(var(--v-theme-primary), 1);
|
||||
border-radius: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
/* 动画关键帧 */
|
||||
@keyframes pulse-width {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.85;
|
||||
transform: scaleX(0.95);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scaleX(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-animation-error {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--v-theme-error), 0.6);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(var(--v-theme-error), 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--v-theme-error), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-animation-warning {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--v-theme-warning), 0.6);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(var(--v-theme-warning), 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--v-theme-warning), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-animation-success {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--v-theme-success), 0.6);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(var(--v-theme-success), 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--v-theme-success), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-animation-secondary {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--v-theme-secondary), 0.6);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(var(--v-theme-secondary), 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--v-theme-secondary), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -128,45 +128,47 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VCard variant="tonal" @click="openStorageDialog">
|
||||
<VCardText class="flex justify-space-between align-center gap-3">
|
||||
<div class="align-self-start flex-1">
|
||||
<h5 class="text-h6 mb-1">{{ storage.name }}</h5>
|
||||
<div class="mb-3 text-sm" v-if="total">{{ formatBytes(used, 1) }} / {{ formatBytes(total, 1) }}</div>
|
||||
<div v-else-if="isNullOrEmptyObject(storage.config)">未配置</div>
|
||||
<div>
|
||||
<VCard variant="tonal" @click="openStorageDialog">
|
||||
<VCardText class="flex justify-space-between align-center gap-3">
|
||||
<div class="align-self-start flex-1">
|
||||
<h5 class="text-h6 mb-1">{{ storage.name }}</h5>
|
||||
<div class="mb-3 text-sm" v-if="total">{{ formatBytes(used, 1) }} / {{ formatBytes(total, 1) }}</div>
|
||||
<div v-else-if="isNullOrEmptyObject(storage.config)">未配置</div>
|
||||
</div>
|
||||
<VImg :src="getIcon" cover class="mt-5" max-width="3rem" min-width="3rem" />
|
||||
</VCardText>
|
||||
<div class="w-full absolute bottom-0">
|
||||
<VProgressLinear v-if="usage > 0" :model-value="usage" :bg-color="progressColor" :color="progressColor" />
|
||||
</div>
|
||||
<VImg :src="getIcon" cover class="mt-5" max-width="3rem" min-width="3rem" />
|
||||
</VCardText>
|
||||
<div class="w-full absolute bottom-0">
|
||||
<VProgressLinear v-if="usage > 0" :model-value="usage" :bg-color="progressColor" :color="progressColor" />
|
||||
</div>
|
||||
</VCard>
|
||||
<AliyunAuthDialog
|
||||
v-if="aliyunAuthDialog"
|
||||
v-model="aliyunAuthDialog"
|
||||
:conf="props.storage.config || {}"
|
||||
@close="aliyunAuthDialog = false"
|
||||
@done="handleDone"
|
||||
/>
|
||||
<U115AuthDialog
|
||||
v-if="u115AuthDialog"
|
||||
v-model="u115AuthDialog"
|
||||
:conf="props.storage.config || {}"
|
||||
@close="u115AuthDialog = false"
|
||||
@done="handleDone"
|
||||
/>
|
||||
<RcloneConfigDialog
|
||||
v-if="rcloneConfigDialog"
|
||||
v-model="rcloneConfigDialog"
|
||||
:conf="props.storage.config || {}"
|
||||
@close="rcloneConfigDialog = false"
|
||||
@done="handleDone"
|
||||
/>
|
||||
<AlistConfigDialog
|
||||
v-if="aListConfigDialog"
|
||||
v-model="aListConfigDialog"
|
||||
:conf="props.storage.config || {}"
|
||||
@close="aListConfigDialog = false"
|
||||
@done="handleDone"
|
||||
/>
|
||||
</VCard>
|
||||
<AliyunAuthDialog
|
||||
v-if="aliyunAuthDialog"
|
||||
v-model="aliyunAuthDialog"
|
||||
:conf="props.storage.config || {}"
|
||||
@close="aliyunAuthDialog = false"
|
||||
@done="handleDone"
|
||||
/>
|
||||
<U115AuthDialog
|
||||
v-if="u115AuthDialog"
|
||||
v-model="u115AuthDialog"
|
||||
:conf="props.storage.config || {}"
|
||||
@close="u115AuthDialog = false"
|
||||
@done="handleDone"
|
||||
/>
|
||||
<RcloneConfigDialog
|
||||
v-if="rcloneConfigDialog"
|
||||
v-model="rcloneConfigDialog"
|
||||
:conf="props.storage.config || {}"
|
||||
@close="rcloneConfigDialog = false"
|
||||
@done="handleDone"
|
||||
/>
|
||||
<AlistConfigDialog
|
||||
v-if="aListConfigDialog"
|
||||
v-model="aListConfigDialog"
|
||||
:conf="props.storage.config || {}"
|
||||
@close="aListConfigDialog = false"
|
||||
@done="handleDone"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -295,10 +295,10 @@ function onSubscribeEditRemove() {
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:key="props.media?.id"
|
||||
class="flex flex-col rounded-lg h-full"
|
||||
class="flex flex-col h-full"
|
||||
:class="{
|
||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
'opacity-70': subscribeState === 'S',
|
||||
}"
|
||||
min-height="170"
|
||||
@@ -311,12 +311,7 @@ function onSubscribeEditRemove() {
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<template v-for="(item, i) in dropdownItems" :key="i">
|
||||
<VListItem
|
||||
v-if="item.show !== false"
|
||||
variant="plain"
|
||||
:base-color="item.props.color"
|
||||
@click="item.props.click"
|
||||
>
|
||||
<VListItem v-if="item.show !== false" :base-color="item.props.color" @click="item.props.click">
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
@@ -339,9 +334,9 @@ function onSubscribeEditRemove() {
|
||||
<div v-if="subscribeState === 'P'" class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none" />
|
||||
</template>
|
||||
<div>
|
||||
<VCardText class="flex items-center">
|
||||
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||
<VCardText class="flex items-center py-3">
|
||||
<div class="h-auto w-16 flex-shrink-0 overflow-hidden rounded-md cursor-move" v-if="imageLoaded">
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
@@ -357,7 +352,7 @@ function onSubscribeEditRemove() {
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap">
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap py-3">
|
||||
<div class="flex align-center">
|
||||
<IconBtn
|
||||
v-if="props.media?.total_episode"
|
||||
@@ -388,9 +383,6 @@ function onSubscribeEditRemove() {
|
||||
color="success"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hover.isHovering" class="me-n3 absolute top-1 right-10">
|
||||
<IconBtn><VIcon class="cursor-move text-white">mdi-drag</VIcon></IconBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -421,7 +413,7 @@ function onSubscribeEditRemove() {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.subscribe-card-background {
|
||||
background-image: linear-gradient(90deg, rgba(31, 41, 55, 47%) 0%, rgb(31, 41, 55) 100%);
|
||||
}
|
||||
|
||||
@@ -100,9 +100,9 @@ function doDelete() {
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:key="props.media?.id"
|
||||
class="flex flex-col rounded-lg h-full"
|
||||
class="flex flex-col h-full"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
}"
|
||||
min-height="170"
|
||||
@click="showForkSubscribe"
|
||||
@@ -119,7 +119,7 @@ function doDelete() {
|
||||
</template>
|
||||
<div class="h-full flex flex-col">
|
||||
<VCardText class="flex items-center pb-1 grow">
|
||||
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
|
||||
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md" v-if="imageLoaded">
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
@@ -177,7 +177,7 @@ function doDelete() {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.subscribe-card-background {
|
||||
background-image: linear-gradient(90deg, rgba(31, 41, 55, 47%) 0%, rgb(31, 41, 55) 100%);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ const meta = ref(props.torrent?.meta_info)
|
||||
const downloadItem = ref(props.torrent)
|
||||
|
||||
// 站点图标
|
||||
const siteIcon = ref('')
|
||||
const siteIcons = ref<Record<number, string>>({})
|
||||
|
||||
// 存储是否已经下载过的记录
|
||||
const downloaded = ref<string[]>([])
|
||||
@@ -51,9 +51,10 @@ function addDownloadError(error: string) {
|
||||
}
|
||||
|
||||
// 查询站点图标
|
||||
async function getSiteIcon() {
|
||||
async function getSiteIcon(site: number | undefined) {
|
||||
if (!site) return
|
||||
try {
|
||||
siteIcon.value = (await api.get(`site/icon/${torrent?.value?.site}`)).data.icon
|
||||
siteIcons.value[site] = (await api.get(`site/icon/${site}`)).data.icon
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -78,141 +79,275 @@ async function downloadTorrentFile() {
|
||||
window.open(torrent.value?.enclosure, '_blank')
|
||||
}
|
||||
|
||||
// 促销Chip类
|
||||
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||
if (downloadVolume === 0) return 'text-white bg-lime-500'
|
||||
else if (downloadVolume < 1) return 'text-white bg-green-500'
|
||||
else if (uploadVolume !== 1) return 'text-white bg-sky-500'
|
||||
else return 'text-white bg-gray-500'
|
||||
// 获取优惠类型样式
|
||||
function getPromotionClass(downloadVolumeFactor: number | undefined, uploadVolumeFactor: number | undefined) {
|
||||
if (!downloadVolumeFactor) return 'bg-success'
|
||||
if (downloadVolumeFactor === 0) return 'bg-success'
|
||||
else if (downloadVolumeFactor < 1) return 'bg-orange'
|
||||
else if (uploadVolumeFactor !== undefined && uploadVolumeFactor > 1) return 'bg-purple'
|
||||
else return ''
|
||||
}
|
||||
|
||||
// 获取优惠标签类
|
||||
function getPromotionChipClass(downloadVolumeFactor: number | undefined, uploadVolumeFactor: number | undefined) {
|
||||
if (!downloadVolumeFactor) return 'chip-free'
|
||||
if (downloadVolumeFactor === 0) return 'chip-free'
|
||||
else if (downloadVolumeFactor < 1) return 'chip-discount'
|
||||
else if (uploadVolumeFactor !== undefined && uploadVolumeFactor > 1) return 'chip-bonus'
|
||||
else return ''
|
||||
}
|
||||
|
||||
// 打开更多来源对话框
|
||||
async function openMoreTorrentsDialog() {
|
||||
props.more?.forEach(t => {
|
||||
return getSiteIcon(t.torrent_info?.site)
|
||||
})
|
||||
showMoreTorrents.value = true
|
||||
}
|
||||
|
||||
// 装载时查询站点图标
|
||||
onMounted(() => {
|
||||
getSiteIcon()
|
||||
getSiteIcon(props.torrent?.torrent_info?.site)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="h-full">
|
||||
<VCard
|
||||
:width="props.width"
|
||||
:height="props.height"
|
||||
:variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'elevated'"
|
||||
:width="props.width || '100%'"
|
||||
:variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'flat'"
|
||||
@click="handleAddDownload(props.torrent)"
|
||||
class="h-full cursor-pointer transition-transform hover:-translate-y-1 duration-300 d-flex flex-column overflow-hidden torrent-card"
|
||||
:class="{ 'border-success border-2 opacity-85': downloaded.includes(torrent?.enclosure || '') }"
|
||||
hover
|
||||
>
|
||||
<template v-if="!showMoreTorrents" #image>
|
||||
<VAvatar class="absolute right-2 bottom-2 rounded" variant="flat" rounded="0">
|
||||
<VImg :src="siteIcon" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardItem class="py-1">
|
||||
<VCardTitle class="break-words overflow-visible whitespace-break-spaces">
|
||||
{{ media?.title ?? meta?.name }} {{ meta?.season_episode }}
|
||||
<span class="text-green-700 ms-2 text-sm">↑{{ torrent?.seeders }}</span>
|
||||
<span class="text-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
||||
</VCardTitle>
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem variant="plain" @click="openTorrentDetail()">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-information" />
|
||||
</template>
|
||||
<VListItemTitle>查看详情</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
v-if="props.torrent?.torrent_info?.enclosure?.startsWith('http')"
|
||||
variant="plain"
|
||||
@click="downloadTorrentFile()"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-download" />
|
||||
</template>
|
||||
<VListItemTitle>下载种子文件</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
<VCardText class="text-subtitle-2">
|
||||
{{ torrent?.title }}
|
||||
</VCardText>
|
||||
<VCardText>【{{ torrent?.site_name }}】{{ torrent?.description }}</VCardText>
|
||||
<VCardItem v-if="torrent?.labels" class="pb-3 pt-0 pe-12">
|
||||
<VChip v-if="torrent?.hit_and_run" variant="elevated" size="small" class="me-1 mb-1 text-white bg-black">
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip v-if="torrent?.freedate_diff" variant="elevated" color="secondary" size="small" class="me-1 mb-1">
|
||||
{{ torrent?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(label, index) in torrent?.labels"
|
||||
:key="index"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
color="primary"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ label }}
|
||||
</VChip>
|
||||
<VChip v-if="meta?.edition" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
||||
{{ meta?.edition }}
|
||||
</VChip>
|
||||
<VChip v-if="meta?.resource_pix" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
||||
{{ meta?.resource_pix }}
|
||||
</VChip>
|
||||
<VChip v-if="meta?.video_encode" variant="elevated" size="small" class="me-1 mb-1 text-white bg-orange-500">
|
||||
{{ meta?.video_encode }}
|
||||
</VChip>
|
||||
<VChip v-if="torrent?.size" variant="elevated" size="small" class="me-1 mb-1 text-white bg-yellow-500">
|
||||
{{ formatFileSize(torrent?.size) }}
|
||||
</VChip>
|
||||
<VChip v-if="meta?.resource_team" variant="elevated" size="small" class="me-1 mb-1 text-white bg-cyan-500">
|
||||
{{ meta?.resource_team }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
|
||||
:class="getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ torrent?.volume_factor }}
|
||||
</VChip>
|
||||
</VCardItem>
|
||||
<VCardActions>
|
||||
<VBtn v-if="props.more && props.more.length > 0" @click.stop="showMoreTorrents = !showMoreTorrents">
|
||||
<template #append>
|
||||
<VIcon :icon="showMoreTorrents ? 'mdi-chevron-up' : 'mdi-chevron-down'" />
|
||||
</template>
|
||||
更多来源
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
<VExpandTransition>
|
||||
<div v-show="showMoreTorrents">
|
||||
<VDivider />
|
||||
<VChipGroup class="p-3" column>
|
||||
<VChip v-for="(item, index) in props.more" :key="index" @click.stop="handleAddDownload(item)">
|
||||
<template #append>
|
||||
<VBadge color="primary" :content="`↑${item.torrent_info?.seeders}`" inline size="small" />
|
||||
<VBadge
|
||||
v-if="item.torrent_info?.downloadvolumefactor !== 1 || item.torrent_info?.uploadvolumefactor !== 1"
|
||||
:content="item.torrent_info?.volume_factor"
|
||||
inline
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
{{ item.torrent_info.site_name }}
|
||||
</VChip>
|
||||
</VChipGroup>
|
||||
<!-- 优惠标签 -->
|
||||
<div
|
||||
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
|
||||
class="discount-banner text-white px-2 py-1 text-sm font-weight-bold rounded-bl-lg"
|
||||
:class="getPromotionClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)"
|
||||
>
|
||||
{{ torrent?.volume_factor }}
|
||||
</div>
|
||||
|
||||
<!-- 媒体标题 -->
|
||||
<VCardItem class="pt-3 pb-0">
|
||||
<div class="d-flex flex-row flex-wrap justify-start mb-2 pr-8">
|
||||
<span class="text-h6 font-weight-bold text-truncate me-2">
|
||||
{{ media?.title ?? meta?.name }}
|
||||
</span>
|
||||
<VChip
|
||||
v-if="meta?.season_episode"
|
||||
class="chip-season rounded-sm font-weight-bold"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
>
|
||||
{{ meta?.season_episode }}
|
||||
</VChip>
|
||||
</div>
|
||||
</VExpandTransition>
|
||||
|
||||
<!-- 站点信息条 -->
|
||||
<div class="d-flex justify-space-between align-center flex-wrap">
|
||||
<div class="d-flex align-center">
|
||||
<VImg
|
||||
v-if="siteIcons[torrent?.site || 0]"
|
||||
:src="siteIcons[torrent?.site || 0]"
|
||||
:alt="torrent?.site_name"
|
||||
class="mr-2 rounded"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
<VAvatar v-else size="20" class="mr-2 text-caption bg-surface-variant" color="surface-variant">
|
||||
{{ torrent?.site_name?.substring(0, 1) }}
|
||||
</VAvatar>
|
||||
<span class="font-weight-bold text-body-2">{{ torrent?.site_name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center gap-3">
|
||||
<span v-if="torrent?.seeders" class="d-flex align-center font-weight-bold">
|
||||
<VIcon size="small" color="success" icon="mdi-arrow-up" class="mr-1"></VIcon>
|
||||
{{ torrent?.seeders }}
|
||||
</span>
|
||||
<span v-if="torrent?.peers" class="d-flex align-center font-weight-bold">
|
||||
<VIcon size="small" color="warning" icon="mdi-arrow-down" class="mr-1"></VIcon>
|
||||
{{ torrent?.peers }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</VCardItem>
|
||||
|
||||
<!-- 种子内容 -->
|
||||
<VCardText class="d-flex flex-column flex-grow-1 pa-3 overflow-hidden">
|
||||
<!-- 种子标题 -->
|
||||
<div class="text-subtitle-2 text-high-emphasis font-weight-medium mb-1" :title="torrent?.title">
|
||||
{{ torrent?.title }}
|
||||
</div>
|
||||
|
||||
<!-- 种子描述 -->
|
||||
<div
|
||||
v-if="meta?.subtitle || torrent?.description"
|
||||
class="text-body-2 text-medium-emphasis mb-2"
|
||||
:title="meta?.subtitle || torrent?.description"
|
||||
>
|
||||
{{ meta?.subtitle || torrent?.description }}
|
||||
</div>
|
||||
|
||||
<!-- 资源标签区 -->
|
||||
<div class="d-flex flex-wrap gap-1 mb-2">
|
||||
<!-- 版本标签 -->
|
||||
<VChip v-if="meta?.edition" class="chip-edition rounded-sm" size="x-small" variant="elevated">
|
||||
{{ meta?.edition }}
|
||||
</VChip>
|
||||
|
||||
<!-- 分辨率标签 -->
|
||||
<VChip v-if="meta?.resource_pix" class="chip-resolution rounded-sm" size="x-small" variant="elevated">
|
||||
{{ meta?.resource_pix }}
|
||||
</VChip>
|
||||
|
||||
<!-- 编码标签 -->
|
||||
<VChip v-if="meta?.video_encode" class="chip-codec rounded-sm" size="x-small" variant="elevated">
|
||||
{{ meta?.video_encode }}
|
||||
</VChip>
|
||||
|
||||
<!-- 制作组标签 -->
|
||||
<VChip v-if="meta?.resource_team" class="chip-team rounded-sm" size="x-small" variant="elevated">
|
||||
{{ meta?.resource_team }}
|
||||
</VChip>
|
||||
|
||||
<!-- 其他标签 -->
|
||||
<VChip
|
||||
v-for="(label, index) in torrent?.labels"
|
||||
:key="index"
|
||||
class="chip-label rounded-sm"
|
||||
size="x-small"
|
||||
variant="elevated"
|
||||
>
|
||||
{{ label }}
|
||||
</VChip>
|
||||
|
||||
<!-- 特殊标签 -->
|
||||
<VChip v-if="torrent?.hit_and_run" class="chip-hr rounded-sm" size="x-small" variant="elevated">H&R</VChip>
|
||||
<VChip v-if="torrent?.freedate_diff" class="chip-expire rounded-sm" size="x-small" variant="elevated">
|
||||
{{ torrent?.freedate_diff }}
|
||||
</VChip>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<!-- 卡片底部信息 -->
|
||||
<VCardActions class="border-t border-opacity-10 mt-auto pa-2">
|
||||
<div v-if="props.more && props.more.length > 0">
|
||||
<VBtn
|
||||
variant="text"
|
||||
color="primary"
|
||||
size="small"
|
||||
class="pa-1 d-flex align-center"
|
||||
@click.stop="openMoreTorrentsDialog"
|
||||
>
|
||||
<VIcon :icon="showMoreTorrents ? 'mdi-chevron-up' : 'mdi-chevron-down'" size="small" class="mr-1"></VIcon>
|
||||
更多来源 ({{ props.more.length }})
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<!-- 体积和详情按钮并排 -->
|
||||
<div class="d-flex align-center">
|
||||
<VChip v-if="torrent?.size" color="primary" size="x-small" variant="elevated" class="rounded-sm mr-2">
|
||||
{{ formatFileSize(torrent.size) }}
|
||||
</VChip>
|
||||
<VBtn icon size="small" variant="text" color="primary" @click.stop="openTorrentDetail">
|
||||
<VIcon icon="mdi-information-outline"></VIcon>
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
|
||||
<!-- 更多来源对话框 -->
|
||||
<VDialog v-model="showMoreTorrents" max-width="25rem" location="center">
|
||||
<VCard>
|
||||
<VCardTitle class="py-3 d-flex align-center">
|
||||
<span>其他来源</span>
|
||||
<VSpacer />
|
||||
<VBtn variant="text" size="small" icon="mdi-close" @click.stop="showMoreTorrents = false"></VBtn>
|
||||
</VCardTitle>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VCardText class="more-sources-content pa-0">
|
||||
<VList lines="one" density="compact">
|
||||
<VListItem
|
||||
v-for="(item, index) in props.more"
|
||||
:key="index"
|
||||
@click.stop="handleAddDownload(item)"
|
||||
class="border-b border-opacity-5 hover:bg-primary-lighten-5"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<div class="d-flex align-center gap-1">
|
||||
<VImg
|
||||
v-if="siteIcons[item.torrent_info?.site || 0]"
|
||||
:src="siteIcons[item.torrent_info?.site || 0]"
|
||||
:alt="item.torrent_info?.site_name"
|
||||
width="16"
|
||||
height="16"
|
||||
class="rounded"
|
||||
/>
|
||||
<VAvatar v-else size="16" class="text-caption bg-surface-variant">
|
||||
{{ item.torrent_info?.site_name?.substring(0, 1) }}
|
||||
</VAvatar>
|
||||
<span class="text-body-2 font-weight-bold">{{ item.torrent_info.site_name }}</span>
|
||||
|
||||
<VChip
|
||||
v-if="item.meta_info?.season_episode"
|
||||
class="chip-season rounded-sm ml-1"
|
||||
size="x-small"
|
||||
variant="elevated"
|
||||
>
|
||||
{{ item.meta_info.season_episode }}
|
||||
</VChip>
|
||||
|
||||
<VChip
|
||||
v-if="item.torrent_info?.downloadvolumefactor !== 1 || item.torrent_info?.uploadvolumefactor !== 1"
|
||||
:class="
|
||||
getPromotionChipClass(
|
||||
item.torrent_info?.downloadvolumefactor,
|
||||
item.torrent_info?.uploadvolumefactor,
|
||||
)
|
||||
"
|
||||
size="x-small"
|
||||
variant="elevated"
|
||||
class="rounded-sm ml-1"
|
||||
>
|
||||
{{ item.torrent_info?.volume_factor }}
|
||||
</VChip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:append>
|
||||
<div class="d-flex align-center gap-2">
|
||||
<span class="text-caption font-weight-bold text-primary">
|
||||
{{ formatFileSize(item.torrent_info?.size) }}
|
||||
</span>
|
||||
<span class="d-flex align-center text-caption font-weight-bold">
|
||||
<VIcon size="small" color="success" icon="mdi-arrow-up" class="mr-1"></VIcon>
|
||||
{{ item.torrent_info?.seeders }}
|
||||
</span>
|
||||
<span>
|
||||
<VIcon
|
||||
@click.stop="openTorrentDetail"
|
||||
size="small"
|
||||
color="secondary"
|
||||
icon="mdi-arrow-top-right"
|
||||
class="mr-1"
|
||||
></VIcon>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<AddDownloadDialog
|
||||
v-if="addDownloadDialog"
|
||||
v-model="addDownloadDialog"
|
||||
@@ -227,3 +362,93 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.discount-banner {
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
.more-sources-content {
|
||||
max-block-size: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 卡片悬停效果 */
|
||||
.torrent-card {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.torrent-card:hover {
|
||||
border-color: rgba(var(--v-theme-primary), 0.3);
|
||||
}
|
||||
|
||||
/* 优惠标签样式 */
|
||||
.bg-success {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.bg-orange {
|
||||
background-color: #ff5722;
|
||||
}
|
||||
|
||||
.bg-purple {
|
||||
background-color: #9c27b0;
|
||||
}
|
||||
|
||||
.chip-season {
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-edition {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-resolution {
|
||||
background-color: #7b1fa2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-codec {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-team {
|
||||
background-color: #00897b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-label {
|
||||
background-color: #5c6bc0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-hr {
|
||||
background-color: #212121;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-expire {
|
||||
background-color: #7e57c2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-free {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-discount {
|
||||
background-color: #ff5722;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-bonus {
|
||||
background-color: #9c27b0;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,9 +10,6 @@ const props = defineProps({
|
||||
torrent: Object as PropType<Context>,
|
||||
})
|
||||
|
||||
// 更多来源界面
|
||||
const showMoreTorrents = ref(false)
|
||||
|
||||
// 种子信息
|
||||
const torrent = ref(props.torrent?.torrent_info)
|
||||
|
||||
@@ -25,6 +22,10 @@ const meta = ref(props.torrent?.meta_info)
|
||||
// 站点图标
|
||||
const siteIcon = ref('')
|
||||
|
||||
// 站点图标加载状态
|
||||
const iconLoading = ref(false)
|
||||
const iconError = ref(false)
|
||||
|
||||
// 存储是否已经下载过的记录
|
||||
const downloaded = ref<string[]>([])
|
||||
|
||||
@@ -33,11 +34,44 @@ const addDownloadDialog = ref(false)
|
||||
|
||||
// 查询站点图标
|
||||
async function getSiteIcon() {
|
||||
try {
|
||||
siteIcon.value = (await api.get(`site/icon/${torrent?.value?.site}`)).data.icon
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
if (!torrent?.value?.site || iconLoading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
iconLoading.value = true
|
||||
iconError.value = false
|
||||
|
||||
try {
|
||||
const response = await api.get(`site/icon/${torrent.value.site}`)
|
||||
if (response && response.data && response.data.icon) {
|
||||
siteIcon.value = response.data.icon
|
||||
} else {
|
||||
iconError.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load site icon:', error)
|
||||
iconError.value = true
|
||||
} finally {
|
||||
iconLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取优惠类型样式
|
||||
function getPromotionClass(downloadVolumeFactor: number | undefined, uploadVolumeFactor: number | undefined) {
|
||||
if (!downloadVolumeFactor) return 'bg-success'
|
||||
if (downloadVolumeFactor === 0) return 'bg-success'
|
||||
else if (downloadVolumeFactor < 1) return 'bg-orange'
|
||||
else if (uploadVolumeFactor !== undefined && uploadVolumeFactor > 1) return 'bg-purple'
|
||||
else return ''
|
||||
}
|
||||
|
||||
// 获取优惠标签类
|
||||
function getPromotionChipClass(downloadVolumeFactor: number | undefined, uploadVolumeFactor: number | undefined) {
|
||||
if (!downloadVolumeFactor) return 'chip-free'
|
||||
if (downloadVolumeFactor === 0) return 'chip-free'
|
||||
else if (downloadVolumeFactor < 1) return 'chip-discount'
|
||||
else if (uploadVolumeFactor !== undefined && uploadVolumeFactor > 1) return 'chip-bonus'
|
||||
else return ''
|
||||
}
|
||||
|
||||
// 询问并添加下载
|
||||
@@ -63,19 +97,6 @@ function openTorrentDetail() {
|
||||
window.open(torrent.value?.page_url, '_blank')
|
||||
}
|
||||
|
||||
// 下载种子文件
|
||||
async function downloadTorrentFile() {
|
||||
window.open(torrent.value?.enclosure, '_blank')
|
||||
}
|
||||
|
||||
// 促销Chip类
|
||||
function getVolumeFactorClass(downloadVolume: number, uploadVolume: number) {
|
||||
if (downloadVolume === 0) return 'text-white bg-lime-500'
|
||||
else if (downloadVolume < 1) return 'text-white bg-green-500'
|
||||
else if (uploadVolume !== 1) return 'text-white bg-sky-500'
|
||||
else return 'text-white bg-gray-500'
|
||||
}
|
||||
|
||||
// 装载时查询站点图标
|
||||
onMounted(() => {
|
||||
getSiteIcon()
|
||||
@@ -83,96 +104,121 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="w-100">
|
||||
<VListItem
|
||||
:value="props.torrent?.torrent_info?.enclosure"
|
||||
class="pa-3 mb-2 rounded torrent-item transition-all duration-300 hover:-translate-y-1 overflow-hidden"
|
||||
:class="{ 'border-start border-success border-3 opacity-85': downloaded.includes(torrent?.enclosure || '') }"
|
||||
@click="handleAddDownload"
|
||||
:variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'flat'"
|
||||
>
|
||||
<template v-if="!showMoreTorrents" #prepend>
|
||||
<VAvatar class="rounded" variant="flat" @click.stop="openTorrentDetail">
|
||||
<VImg :src="siteIcon" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VListItemTitle class="break-words overflow-visible whitespace-break-spaces">
|
||||
{{ torrent?.title }}
|
||||
<span class="text-green-700 ms-2 text-sm">↑{{ torrent?.seeders }}</span>
|
||||
<span class="text-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle> 【{{ torrent?.site_name }}】{{ torrent?.description }} </VListItemSubtitle>
|
||||
<div v-if="torrent?.labels" class="pt-2">
|
||||
<VChip v-if="torrent?.hit_and_run" variant="elevated" size="small" class="me-1 mb-1 text-white bg-black">
|
||||
H&R
|
||||
</VChip>
|
||||
<VChip v-if="torrent?.freedate_diff" variant="elevated" color="secondary" size="small" class="me-1 mb-1">
|
||||
{{ torrent?.freedate_diff }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(label, index) in torrent?.labels"
|
||||
:key="index"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
color="primary"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ label }}
|
||||
</VChip>
|
||||
<VChip v-if="meta?.edition" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
||||
{{ meta?.edition }}
|
||||
</VChip>
|
||||
<VChip v-if="meta?.resource_pix" variant="elevated" size="small" class="me-1 mb-1 text-white bg-red-500">
|
||||
{{ meta?.resource_pix }}
|
||||
</VChip>
|
||||
<VChip v-if="meta?.video_encode" variant="elevated" size="small" class="me-1 mb-1 text-white bg-orange-500">
|
||||
{{ meta?.video_encode }}
|
||||
</VChip>
|
||||
<VChip v-if="torrent?.size" variant="elevated" size="small" class="me-1 mb-1 text-white bg-yellow-500">
|
||||
{{ formatFileSize(torrent?.size) }}
|
||||
</VChip>
|
||||
<VChip v-if="meta?.resource_team" variant="elevated" size="small" class="me-1 mb-1 text-white bg-cyan-500">
|
||||
{{ meta?.resource_team }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
|
||||
:class="getVolumeFactorClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
class="me-1 mb-1"
|
||||
>
|
||||
{{ torrent?.volume_factor }}
|
||||
</VChip>
|
||||
<!-- 优惠标签 -->
|
||||
<div
|
||||
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
|
||||
class="discount-banner text-white px-2 py-1 text-sm font-weight-bold rounded-bl-lg"
|
||||
:class="getPromotionClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)"
|
||||
>
|
||||
{{ torrent?.volume_factor }}
|
||||
</div>
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem variant="plain" @click="openTorrentDetail()">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-information" />
|
||||
</template>
|
||||
<VListItemTitle>查看详情</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
v-if="props.torrent?.torrent_info?.enclosure?.startsWith('http')"
|
||||
variant="plain"
|
||||
@click="downloadTorrentFile()"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-download" />
|
||||
</template>
|
||||
<VListItemTitle>下载种子文件</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
|
||||
<template v-slot:prepend>
|
||||
<div class="d-flex align-center">
|
||||
<img v-if="siteIcon" :src="siteIcon" :alt="torrent?.site_name" class="rounded mr-2" width="32" height="32" />
|
||||
<VAvatar v-else size="24" class="mr-2 text-caption bg-primary-lighten-4 text-primary font-weight-bold">
|
||||
{{ torrent?.site_name?.substring(0, 1) }}
|
||||
</VAvatar>
|
||||
<div class="font-weight-bold text-body-2 d-none d-sm-block">{{ torrent?.site_name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<VListItemTitle>
|
||||
<div class="d-flex flex-row flex-wrap align-center mb-2">
|
||||
<span class="text-h6 font-weight-bold me-2">{{ media?.title ?? meta?.name }}</span>
|
||||
<VChip v-if="meta?.season_episode" class="chip-season rounded-sm font-weight-bold" variant="elevated">
|
||||
{{ meta?.season_episode }}
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<div class="text-subtitle-2 font-weight-medium mb-2" :title="torrent?.title">
|
||||
{{ torrent?.title }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-body-2 text-medium-emphasis mb-2"
|
||||
:title="meta?.subtitle || torrent?.description || '暂无描述'"
|
||||
>
|
||||
{{ meta?.subtitle || torrent?.description || '暂无描述' }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-1 mb-2">
|
||||
<!-- 版本标签 -->
|
||||
<VChip v-if="meta?.edition" class="chip-edition rounded-sm" size="x-small" variant="elevated">
|
||||
{{ meta?.edition }}
|
||||
</VChip>
|
||||
|
||||
<!-- 分辨率标签 -->
|
||||
<VChip v-if="meta?.resource_pix" class="chip-resolution rounded-sm" size="x-small" variant="elevated">
|
||||
{{ meta?.resource_pix }}
|
||||
</VChip>
|
||||
|
||||
<!-- 编码标签 -->
|
||||
<VChip v-if="meta?.video_encode" class="chip-codec rounded-sm" size="x-small" variant="elevated">
|
||||
{{ meta?.video_encode }}
|
||||
</VChip>
|
||||
|
||||
<!-- 制作组标签 -->
|
||||
<VChip v-if="meta?.resource_team" class="chip-team rounded-sm" size="x-small" variant="elevated">
|
||||
{{ meta?.resource_team }}
|
||||
</VChip>
|
||||
|
||||
<!-- 其他标签 -->
|
||||
<VChip
|
||||
v-for="(label, index) in torrent?.labels"
|
||||
:key="index"
|
||||
class="chip-label rounded-sm"
|
||||
size="x-small"
|
||||
variant="elevated"
|
||||
>
|
||||
{{ label }}
|
||||
</VChip>
|
||||
|
||||
<!-- 特殊标签 -->
|
||||
<VChip v-if="torrent?.hit_and_run" class="chip-hr rounded-sm" size="x-small" variant="elevated"> H&R </VChip>
|
||||
<VChip v-if="torrent?.freedate_diff" class="chip-expire rounded-sm" size="x-small" variant="elevated">
|
||||
{{ torrent?.freedate_diff }}
|
||||
</VChip>
|
||||
</div>
|
||||
</VListItemTitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<div class="d-flex flex-column align-end gap-2">
|
||||
<div class="d-flex align-center gap-3">
|
||||
<span v-if="torrent?.seeders" class="d-flex align-center font-weight-bold">
|
||||
<VIcon size="small" color="success" icon="mdi-arrow-up" class="mr-1"></VIcon>
|
||||
{{ torrent?.seeders }}
|
||||
</span>
|
||||
<span v-if="torrent?.peers" class="d-flex align-center font-weight-bold">
|
||||
<VIcon size="small" color="warning" icon="mdi-arrow-down" class="mr-1"></VIcon>
|
||||
{{ torrent?.peers }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center">
|
||||
<VChip v-if="torrent?.size" color="primary" size="x-small" variant="elevated" class="rounded-sm mr-2">
|
||||
{{ formatFileSize(torrent.size) }}
|
||||
</VChip>
|
||||
|
||||
<VBtn icon size="small" variant="text" color="primary" @click.stop="openTorrentDetail">
|
||||
<VIcon icon="mdi-information-outline"></VIcon>
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VListItem>
|
||||
|
||||
<AddDownloadDialog
|
||||
v-if="addDownloadDialog"
|
||||
v-model="addDownloadDialog"
|
||||
:title="`${media?.title_year || meta?.name} ${meta?.season_episode}`"
|
||||
:title="`${media?.title_year || meta?.name} ${meta?.season_episode || ''}`"
|
||||
:media="media"
|
||||
:torrent="torrent"
|
||||
@done="addDownloadSuccess"
|
||||
@@ -181,3 +227,88 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.discount-banner {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
inset-block-start: 0;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
.torrent-item {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.torrent-item:hover {
|
||||
border-color: rgba(var(--v-theme-primary), 0.3);
|
||||
}
|
||||
|
||||
.chip-season {
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-edition {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-resolution {
|
||||
background-color: #7b1fa2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-codec {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-team {
|
||||
background-color: #00897b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-label {
|
||||
background-color: #5c6bc0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-hr {
|
||||
background-color: #212121;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-expire {
|
||||
background-color: #7e57c2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 优惠标签样式 */
|
||||
.bg-success {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.bg-orange {
|
||||
background-color: #ff5722;
|
||||
}
|
||||
|
||||
.bg-purple {
|
||||
background-color: #9c27b0;
|
||||
}
|
||||
|
||||
.chip-free {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-discount {
|
||||
background-color: #ff5722;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-bonus {
|
||||
background-color: #9c27b0;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,12 +6,18 @@ import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { useConfirm } from 'vuetify-use-dialog'
|
||||
import UserAddEditDialog from '@/components/dialog/UserAddEditDialog.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 扩展User类型以包含昵称字段
|
||||
interface ExtendedUser extends User {
|
||||
nickname?: string
|
||||
}
|
||||
|
||||
// 定义输入变量
|
||||
const props = defineProps({
|
||||
// 用户信息
|
||||
user: {
|
||||
type: Object as PropType<User>,
|
||||
type: Object as PropType<ExtendedUser>,
|
||||
required: true,
|
||||
},
|
||||
// 所有用户
|
||||
@@ -21,6 +27,9 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const display = useDisplay()
|
||||
const isMobile = computed(() => display.mdAndDown.value)
|
||||
|
||||
// 当前用户的ID
|
||||
const currentLoginUserId = computed(() => useUserStore().userID)
|
||||
|
||||
@@ -45,6 +54,13 @@ const movieSubscriptions = ref(0)
|
||||
// 用户电视剧订阅数量
|
||||
const tvShowSubscriptions = ref(0)
|
||||
|
||||
// 显示名称 - 如果有昵称则优先显示昵称
|
||||
const displayName = computed(() => {
|
||||
const settingsNickname = props.user.settings?.nickname as string | undefined
|
||||
const nickname = props.user.nickname || settingsNickname
|
||||
return nickname || props.user.name
|
||||
})
|
||||
|
||||
// 按用户查询订阅数量
|
||||
async function fetchSubscriptions() {
|
||||
try {
|
||||
@@ -87,7 +103,7 @@ function editUser() {
|
||||
userEditDialog.value = true
|
||||
}
|
||||
|
||||
// 用户重新完成时
|
||||
// 用户更新完成时
|
||||
function onUserUpdate() {
|
||||
userEditDialog.value = false
|
||||
emit('save')
|
||||
@@ -98,80 +114,162 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardText class="text-center pt-10 pb-3">
|
||||
<VAvatar variant="flat" size="100" rounded>
|
||||
<VImg :src="user.avatar || avatar1" alt="avatar" />
|
||||
</VAvatar>
|
||||
<h5 class="text-h5 mt-3">{{ user.name }}</h5>
|
||||
<VChip size="small" class="mt-3" :class="{ 'text-error': user.is_superuser }">
|
||||
{{ user.is_superuser ? '管理员' : '普通用户' }}
|
||||
</VChip>
|
||||
<VCard
|
||||
:class="[
|
||||
'transition-transform duration-300 hover:-translate-y-1',
|
||||
!props.user.is_active ? 'opacity-85 bg-surface-lighten-1' : '',
|
||||
]"
|
||||
@click="userEditDialog = true"
|
||||
>
|
||||
<!-- 用户头像和基本信息 -->
|
||||
<VCardItem :class="[user.is_superuser ? 'admin-header' : '']">
|
||||
<template v-slot:prepend>
|
||||
<div class="position-relative mr-4">
|
||||
<VAvatar
|
||||
size="72"
|
||||
rounded="lg"
|
||||
:class="[
|
||||
user.is_superuser ? 'admin-avatar' : 'border-4 bg-surface',
|
||||
!user.is_active ? 'grayscale-50 opacity-90' : '',
|
||||
]"
|
||||
:style="user.is_superuser ? 'border: 4px solid rgba(var(--v-theme-warning), 0.3);' : ''"
|
||||
>
|
||||
<VImg :src="user.avatar || avatar1" :alt="user.name" />
|
||||
<div
|
||||
v-if="!user.is_active"
|
||||
class="position-absolute d-flex align-center justify-center rounded-lg bg-surface-variant opacity-20"
|
||||
style="inset: 0"
|
||||
>
|
||||
<VIcon icon="mdi-account-lock" color="white" />
|
||||
</div>
|
||||
</VAvatar>
|
||||
<div v-if="user.is_superuser" class="admin-crown">
|
||||
<VIcon icon="mdi-crown" color="warning" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<VCardTitle class="pa-0 d-flex flex-column">
|
||||
<div class="d-flex flex-column mb-1">
|
||||
<div class="d-flex align-center">
|
||||
<span
|
||||
:class="[
|
||||
'text-h6 font-weight-bold truncate',
|
||||
user.is_superuser ? 'text-warning' : '',
|
||||
!user.is_active ? 'text-medium-emphasis' : '',
|
||||
]"
|
||||
>
|
||||
{{ displayName }}
|
||||
<VIcon
|
||||
v-if="user.nickname || user.settings?.nickname"
|
||||
icon="mdi-format-quote-close"
|
||||
size="x-small"
|
||||
color="info"
|
||||
class="animate-pulse"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1 overflow-auto">
|
||||
<VChip v-if="user.is_superuser" size="x-small" color="error" variant="outlined" label>管理员</VChip>
|
||||
<VChip v-else size="x-small" label>普通用户</VChip>
|
||||
<VChip size="x-small" :color="user.is_active ? 'success' : 'grey'" variant="tonal" label>
|
||||
{{ user.is_active ? '激活' : '已停用' }}
|
||||
</VChip>
|
||||
<VChip v-if="user.is_otp" size="x-small" color="info" variant="tonal" label>2FA</VChip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端订阅数据信息 -->
|
||||
<div v-if="isMobile" class="d-flex gap-5 mt-2">
|
||||
<div class="d-flex align-center">
|
||||
<VIcon size="x-small" icon="mdi-movie-outline" color="primary" class="mr-1" />
|
||||
<span class="text-body-2">{{ movieSubscriptions }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<VIcon size="x-small" icon="mdi-television-classic" color="primary" class="mr-1" />
|
||||
<span class="text-body-2">{{ tvShowSubscriptions }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</VCardTitle>
|
||||
|
||||
<!-- 头部操作按钮 -->
|
||||
<template v-slot:append>
|
||||
<div :class="['d-flex', isMobile ? 'position-absolute top-2 right-2' : '']">
|
||||
<VBtn
|
||||
icon
|
||||
size="small"
|
||||
:color="user.is_superuser ? 'warning' : 'primary'"
|
||||
variant="text"
|
||||
class="opacity-70 hover:opacity-100 transition-opacity"
|
||||
@click.stop="editUser"
|
||||
>
|
||||
<VIcon icon="mdi-pencil" />
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
v-if="props.user.id != currentLoginUserId && currentUserIsSuperuser"
|
||||
icon
|
||||
size="small"
|
||||
color="error"
|
||||
variant="text"
|
||||
class="opacity-70 hover:opacity-100 transition-opacity"
|
||||
@click.stop="removeUser"
|
||||
>
|
||||
<VIcon icon="mdi-delete" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<!-- 独立的邮箱显示 -->
|
||||
<VDivider class="mx-4" />
|
||||
|
||||
<VCardText class="d-flex align-center py-2 px-4 text-medium-emphasis">
|
||||
<VIcon icon="mdi-email-outline" size="small" color="primary" class="mr-2 opacity-70" />
|
||||
<span class="text-body-2 truncate">{{ user.email || '未设置邮箱' }}</span>
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-center gap-6 pb-5">
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar size="40" color="primary" rounded variant="tonal" class="me-4">
|
||||
<VIcon size="24" icon="mdi-movie-open-outline"></VIcon>
|
||||
</VAvatar>
|
||||
<div>
|
||||
<div class="text-h6">{{ movieSubscriptions }}</div>
|
||||
<div class="text-sm text-no-wrap">电影订阅</div>
|
||||
|
||||
<!-- PC端显示订阅统计信息 -->
|
||||
<VCardText v-if="!isMobile" class="px-4 pt-0 pb-4">
|
||||
<div rounded="lg" class="d-flex justify-space-around pa-3">
|
||||
<div class="d-flex align-center gap-3">
|
||||
<VAvatar
|
||||
tile
|
||||
rounded="lg"
|
||||
size="large"
|
||||
class="mr-1"
|
||||
:class="user.is_superuser ? 'admin-stats-container' : 'user-stats-container'"
|
||||
>
|
||||
<div :class="['d-flex align-center justify-center rounded-lg w-10 h-10']">
|
||||
<VIcon :color="user.is_superuser ? 'warning' : 'primary'" icon="mdi-movie-outline" size="20" />
|
||||
</div>
|
||||
</VAvatar>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-lg text-medium-emphasis font-weight-bold">{{ movieSubscriptions }}</span>
|
||||
<span class="text-caption text-medium-emphasis">电影订阅</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-center gap-3">
|
||||
<VAvatar
|
||||
tile
|
||||
rounded="lg"
|
||||
size="large"
|
||||
class="mr-1"
|
||||
:class="user.is_superuser ? 'admin-stats-container' : 'user-stats-container'"
|
||||
>
|
||||
<div :class="['d-flex align-center justify-center rounded-lg w-10 h-10']">
|
||||
<VIcon :color="user.is_superuser ? 'warning' : 'primary'" icon="mdi-television-classic" />
|
||||
</div>
|
||||
</VAvatar>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-lg text-medium-emphasis">{{ tvShowSubscriptions }}</span>
|
||||
<span class="text-caption text-medium-emphasis">剧集订阅</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar size="40" color="primary" rounded variant="tonal" class="me-4">
|
||||
<VIcon size="24" icon="mdi-television"></VIcon>
|
||||
</VAvatar>
|
||||
<div>
|
||||
<div class="text-h6">{{ tvShowSubscriptions }}</div>
|
||||
<div class="text-sm text-no-wrap">电视剧订阅</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="pb-6">
|
||||
<VDivider class="my-2">
|
||||
<h5 class="text-h6">详情</h5>
|
||||
</VDivider>
|
||||
<VList lines="one">
|
||||
<VListItem>
|
||||
<VListItemTitle class="text-sm">
|
||||
<span class="font-weight-medium">邮箱:</span><span class="text-body-1"> {{ user.email }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<VListItemTitle class="text-sm">
|
||||
<span class="font-weight-medium">状态:</span
|
||||
><span class="text-body-1">
|
||||
<VChip size="small" :class="{ 'text-success': user.is_active }" variant="tonal">
|
||||
{{ user.is_active ? '激活' : '已停用' }}
|
||||
</VChip>
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<VListItemTitle class="text-sm">
|
||||
<span class="font-weight-medium">双重认证:</span
|
||||
><span class="text-body-1">
|
||||
<VChip size="small" :class="{ 'text-success': user.is_otp }" variant="tonal">
|
||||
{{ user.is_otp ? '已启用' : '未启用' }}
|
||||
</VChip>
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
<VCardText class="flex flex-row justify-center">
|
||||
<VBtn v-if="currentUserIsSuperuser" color="primary" class="me-4" @click="editUser"> 编辑 </VBtn>
|
||||
<VBtn
|
||||
v-if="currentUserIsSuperuser && props.user.id != currentLoginUserId"
|
||||
color="error"
|
||||
variant="outlined"
|
||||
@click="removeUser"
|
||||
>
|
||||
删除
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 用户编辑弹窗 -->
|
||||
<UserAddEditDialog
|
||||
v-if="userEditDialog"
|
||||
@@ -183,3 +281,94 @@ onMounted(() => {
|
||||
@close="userEditDialog = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-decoration {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
background: linear-gradient(to bottom, rgba(var(--v-theme-warning), 0.05), transparent);
|
||||
}
|
||||
|
||||
.admin-avatar::after {
|
||||
position: absolute;
|
||||
border: 1px solid rgba(var(--v-theme-warning), 0.3);
|
||||
border-radius: 12px;
|
||||
animation: pulse 2.5s infinite;
|
||||
content: '';
|
||||
inset: -5px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.admin-stats-container {
|
||||
background-color: rgba(var(--v-theme-warning), 0.1);
|
||||
}
|
||||
|
||||
.user-stats-container {
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 0.6;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
70% {
|
||||
opacity: 0.2;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.admin-crown {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
top: -10px;
|
||||
left: -6px;
|
||||
transform: rotate(-25deg);
|
||||
filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 40%));
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% {
|
||||
transform: rotate(-25deg) translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(-25deg) translateY(-3px);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(-25deg) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse-nickname 2s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-nickname {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.9;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.grayscale-50 {
|
||||
filter: grayscale(50%);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -172,132 +172,131 @@ const resolveProgress = (item: Workflow) => {
|
||||
</script>
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<VCard class="mx-auto h-full" @click="handleFlow(workflow)" :ripple="false" :loading="loading">
|
||||
<VCardItem class="py-3" :class="`bg-${resolveStatusVariant(workflow?.state).color}`">
|
||||
<template #prepend>
|
||||
<VAvatar variant="text" class="me-2">
|
||||
<VIcon
|
||||
v-if="workflow?.state === 'P'"
|
||||
color="success"
|
||||
size="x-large"
|
||||
icon="mdi-play"
|
||||
@click.stop="handleEnable(workflow)"
|
||||
/>
|
||||
<VIcon v-else color="warning" icon="mdi-pause" size="x-large" @click.stop="handlePause(workflow)" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardTitle class="text-white">
|
||||
{{ workflow?.name }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle class="text-white">{{ workflow?.description }}</VCardSubtitle>
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-vector-polyline-edit" @click.stop="handleFlow(workflow)" />
|
||||
</IconBtn>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem variant="plain" base-color="primary" @click="handleEdit(workflow)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-note-edit" />
|
||||
</template>
|
||||
<VListItemTitle>编辑任务</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
v-if="workflow.current_action"
|
||||
variant="plain"
|
||||
base-color="info"
|
||||
@click="handleRun(workflow, false)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-play-speed" />
|
||||
</template>
|
||||
<VListItemTitle>继续执行</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
v-if="workflow.current_action"
|
||||
variant="plain"
|
||||
base-color="info"
|
||||
@click="handleRun(workflow, true)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-replay" />
|
||||
</template>
|
||||
<VListItemTitle>重新执行</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem v-else variant="plain" base-color="info" @click="handleRun(workflow, true)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-run" />
|
||||
</template>
|
||||
<VListItemTitle>立即执行</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" base-color="warning" @click="handleReset(workflow)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-restore-alert" />
|
||||
</template>
|
||||
<VListItemTitle>重置任务</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem variant="plain" base-color="error" @click="handleDelete(workflow)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-delete" />
|
||||
</template>
|
||||
<VListItemTitle>删除任务</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<div class="d-flex flex-column gap-y-4">
|
||||
<div class="d-flex flex-wrap gap-x-6">
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">定时</div>
|
||||
<h5 class="text-h6">{{ workflow?.timer }}</h5>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">状态</div>
|
||||
<h5 class="text-h6" :class="`text-${resolveStatusVariant(workflow?.state).color}`">
|
||||
{{ resolveStatusVariant(workflow?.state).text }}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-x-6">
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">动作数</div>
|
||||
<div>
|
||||
<VAvatar size="32" color="primary" variant="tonal">
|
||||
<span class="text-sm">{{ workflow?.actions?.length }}</span>
|
||||
</VAvatar>
|
||||
<VHover v-slot="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
class="mx-auto h-full"
|
||||
@click="handleFlow(workflow)"
|
||||
:ripple="false"
|
||||
:loading="loading"
|
||||
:class="{ 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering }"
|
||||
>
|
||||
<VCardItem class="py-3" :class="`bg-${resolveStatusVariant(workflow?.state).color}`">
|
||||
<template #prepend>
|
||||
<VAvatar variant="text" class="me-2">
|
||||
<VIcon
|
||||
v-if="workflow?.state === 'P'"
|
||||
color="success"
|
||||
size="x-large"
|
||||
icon="mdi-play"
|
||||
@click.stop="handleEnable(workflow)"
|
||||
/>
|
||||
<VIcon v-else color="warning" icon="mdi-pause" size="x-large" @click.stop="handlePause(workflow)" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardTitle class="text-white">
|
||||
{{ workflow?.name }}
|
||||
</VCardTitle>
|
||||
<VCardSubtitle class="text-white">{{ workflow?.description }}</VCardSubtitle>
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-vector-polyline-edit" @click.stop="handleFlow(workflow)" />
|
||||
</IconBtn>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem base-color="primary" @click="handleEdit(workflow)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-note-edit" />
|
||||
</template>
|
||||
<VListItemTitle>编辑任务</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem v-if="workflow.current_action" base-color="info" @click="handleRun(workflow, false)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-play-speed" />
|
||||
</template>
|
||||
<VListItemTitle>继续执行</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem v-if="workflow.current_action" base-color="info" @click="handleRun(workflow, true)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-replay" />
|
||||
</template>
|
||||
<VListItemTitle>重新执行</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem v-else base-color="info" @click="handleRun(workflow, true)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-run" />
|
||||
</template>
|
||||
<VListItemTitle>立即执行</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem base-color="warning" @click="handleReset(workflow)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-restore-alert" />
|
||||
</template>
|
||||
<VListItemTitle>重置任务</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem base-color="error" @click="handleDelete(workflow)">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-delete" />
|
||||
</template>
|
||||
<VListItemTitle>删除任务</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<div class="d-flex flex-column gap-y-4">
|
||||
<div class="d-flex flex-wrap gap-x-6">
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">定时</div>
|
||||
<h5 class="text-h6">{{ workflow?.timer }}</h5>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">状态</div>
|
||||
<h5 class="text-h6" :class="`text-${resolveStatusVariant(workflow?.state).color}`">
|
||||
{{ resolveStatusVariant(workflow?.state).text }}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">已执行次数</div>
|
||||
<h5 class="text-h6">{{ workflow?.run_count }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-x-6">
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">进度</div>
|
||||
<div class="d-flex align-center gap-5">
|
||||
<div class="flex-grow-1">
|
||||
<VProgressLinear color="info" rounded :model-value="resolveProgress(workflow)" />
|
||||
<div class="d-flex flex-wrap gap-x-6">
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">动作数</div>
|
||||
<div>
|
||||
<VAvatar size="32" color="primary" variant="tonal">
|
||||
<span class="text-sm">{{ workflow?.actions?.length }}</span>
|
||||
</VAvatar>
|
||||
</div>
|
||||
<span> {{ resolveProgress(workflow) }}% </span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">已执行次数</div>
|
||||
<h5 class="text-h6">{{ workflow?.run_count }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-x-6">
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">进度</div>
|
||||
<div class="d-flex align-center gap-5">
|
||||
<div class="flex-grow-1">
|
||||
<VProgressLinear color="info" rounded :model-value="resolveProgress(workflow)" />
|
||||
</div>
|
||||
<span> {{ resolveProgress(workflow) }}% </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-x-6" v-if="workflow?.result">
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">错误信息</div>
|
||||
<div class="text-error">{{ workflow?.result }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-x-6" v-if="workflow?.result">
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">错误信息</div>
|
||||
<div class="text-error">{{ workflow?.result }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VHover>
|
||||
<!-- 流程对话框 -->
|
||||
<WorkflowActionsDialog
|
||||
v-if="flowDialog"
|
||||
|
||||
@@ -118,77 +118,73 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VDialog max-width="45rem" scrollable>
|
||||
<VDialog max-width="35rem" scrollable>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle v-if="title">{{ torrent?.site_name }} - {{ title }}</VCardTitle>
|
||||
<VCardTitle v-else>确认下载</VCardTitle>
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
</VCardItem>
|
||||
<VCardTitle class="py-4 me-12">
|
||||
<VIcon icon="mdi-download" class="me-2" />
|
||||
<span v-if="title">{{ torrent?.site_name }} - {{ title }}</span>
|
||||
<span v-else>确认下载</span>
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VList lines="one">
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-web"></VIcon>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<span class="whitespace-break-spaces me-2">{{ torrent?.title }}</span>
|
||||
<span class="text-green-700 ms-2 text-sm">↑{{ torrent?.seeders }}</span>
|
||||
<span class="text-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-subtitles-outline"></VIcon>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<span class="text-body-1 whitespace-break-spaces">{{ torrent?.description }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-database"></VIcon>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<span class="text-body-1">
|
||||
<VChip variant="tonal" label>
|
||||
{{ formatFileSize(torrent?.size || 0) }}
|
||||
</VChip>
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
v-model="selectedDownloader"
|
||||
:items="downloaderOptions"
|
||||
label="指定下载器"
|
||||
variant="underlined"
|
||||
placeholder="留空默认"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="8">
|
||||
<VCombobox
|
||||
v-model="selectedDirectory"
|
||||
:items="targetDirectories"
|
||||
label="指定保存目录"
|
||||
placeholder="留空自动匹配"
|
||||
variant="underlined"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VList lines="one">
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-web"></VIcon>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<span class="whitespace-break-spaces me-2">{{ torrent?.title }}</span>
|
||||
<span class="text-green-700 ms-2 text-sm">↑{{ torrent?.seeders }}</span>
|
||||
<span class="text-orange-700 ms-2 text-sm">↓{{ torrent?.peers }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem v-if="torrent?.description">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-subtitles-outline"></VIcon>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<span class="text-body-2 whitespace-break-spaces">{{ torrent?.description }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem v-if="torrent?.size">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-database"></VIcon>
|
||||
</template>
|
||||
<VListItemTitle>
|
||||
<span class="text-body-2">
|
||||
<VChip variant="tonal" label>
|
||||
{{ formatFileSize(torrent?.size || 0) }}
|
||||
</VChip>
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<VRow class="px-7">
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
v-model="selectedDownloader"
|
||||
:items="downloaderOptions"
|
||||
size="small"
|
||||
label="下载器(默认)"
|
||||
variant="underlined"
|
||||
placeholder="留空默认"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="8">
|
||||
<VCombobox
|
||||
v-model="selectedDirectory"
|
||||
:items="targetDirectories"
|
||||
label="保存目录(自动)"
|
||||
size="small"
|
||||
placeholder="留空自动匹配"
|
||||
variant="underlined"
|
||||
density="compact"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VCardText class="text-center">
|
||||
<VBtn
|
||||
variant="elevated"
|
||||
:disabled="loading"
|
||||
@click="addDownload"
|
||||
:prepend-icon="icon"
|
||||
class="px-5"
|
||||
size="large"
|
||||
>
|
||||
<VBtn variant="elevated" :disabled="loading" @click="addDownload" :prepend-icon="icon" class="px-5">
|
||||
{{ buttonText }}
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
|
||||
@@ -31,7 +31,7 @@ async function savaAlistConfig() {
|
||||
<template>
|
||||
<VDialog width="50rem" scrollable max-height="85vh">
|
||||
<VCard title="AList配置" class="rounded-t">
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import api from '@/api'
|
||||
|
||||
// 定义输入
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
conf: {
|
||||
type: Object as PropType<{ [key: string]: any }>,
|
||||
required: true,
|
||||
@@ -14,13 +13,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(['done', 'close'])
|
||||
|
||||
// 二维码内容
|
||||
const qrCodeContent = ref('')
|
||||
|
||||
// ck参数
|
||||
const ck = ref('')
|
||||
|
||||
// t参数
|
||||
const t = ref('')
|
||||
const qrCodeUrl = ref('')
|
||||
|
||||
// 下方的提示信息
|
||||
const text = ref('请用阿里云盘 App 扫码')
|
||||
@@ -34,9 +27,6 @@ let timeoutTimer: NodeJS.Timeout | undefined = undefined
|
||||
// 完成
|
||||
async function handleDone() {
|
||||
clearTimeout(timeoutTimer)
|
||||
if (props.conf?.refreshToken) {
|
||||
await savaAliPanConfig()
|
||||
}
|
||||
emit('done')
|
||||
}
|
||||
|
||||
@@ -45,9 +35,8 @@ async function getQrcode() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/storage/qrcode/alipan')
|
||||
if (result.success && result.data) {
|
||||
qrCodeContent.value = result.data.codeContent
|
||||
ck.value = result.data.ck
|
||||
t.value = result.data.t
|
||||
qrCodeUrl.value = result.data.codeUrl
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
} else {
|
||||
text.value = result.message
|
||||
}
|
||||
@@ -59,23 +48,21 @@ async function getQrcode() {
|
||||
// 调用/aliyun/check api验证二维码
|
||||
async function checkQrcode() {
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('/storage/check/alipan', {
|
||||
params: { ck: ck.value, t: t.value },
|
||||
})
|
||||
const result: { [key: string]: any } = await api.get('/storage/check/alipan')
|
||||
if (result.success && result.data) {
|
||||
const qrCodeStatus = result.data.qrCodeStatus
|
||||
const qrCodeStatus = result.data.status
|
||||
text.value = result.data.tip
|
||||
if (qrCodeStatus == 'CONFIRMED') {
|
||||
// 已确认完成
|
||||
if (qrCodeStatus == 'LoginSuccess') {
|
||||
// 登录成功
|
||||
alertType.value = 'success'
|
||||
handleDone()
|
||||
} else if (qrCodeStatus == 'NEW' || qrCodeStatus == 'SCANED') {
|
||||
} else if (qrCodeStatus == 'WaitLogin' || qrCodeStatus == 'ScanSuccess') {
|
||||
// 等待登录扫码成功
|
||||
alertType.value = 'info'
|
||||
// 新建、待扫码
|
||||
clearTimeout(timeoutTimer)
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
} else {
|
||||
// 过期或者已取消
|
||||
// 二维码过期
|
||||
alertType.value = 'error'
|
||||
}
|
||||
} else {
|
||||
@@ -87,18 +74,8 @@ async function checkQrcode() {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存cookie设置
|
||||
async function savaAliPanConfig() {
|
||||
try {
|
||||
await api.post(`storage/save/alipan`, props.conf)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getQrcode()
|
||||
timeoutTimer = setTimeout(checkQrcode, 3000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -109,22 +86,21 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<VDialog width="40rem" scrollable max-height="85vh">
|
||||
<VCard title="阿里云盘登录" class="rounded-t">
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardText class="pt-2 flex flex-col items-center">
|
||||
<div class="my-6 shadow-lg rounded text-center p-3 border">
|
||||
<QrcodeVue class="mx-auto" :value="qrCodeContent" :size="200" />
|
||||
<div class="my-6 rounded text-center p-3 border">
|
||||
<VImg class="mx-auto" :src="qrCodeUrl" width="200" height="200">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-1 aspect-h-1" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
|
||||
<template #prepend />
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol class="mt-2">
|
||||
<VTextField label="自定义refreshToken" v-model="props.conf.refreshToken" outlined dense />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3"> 完成 </VBtn>
|
||||
|
||||
@@ -165,7 +165,7 @@ onMounted(() => {
|
||||
<template>
|
||||
<VDialog max-width="40rem" scrollable>
|
||||
<VCard>
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardText>
|
||||
<VCol>
|
||||
<div class="d-flex justify-space-between flex-wrap flex-md-nowrap flex-column flex-md-row">
|
||||
@@ -230,26 +230,16 @@ onMounted(() => {
|
||||
@click="doFork"
|
||||
prepend-icon="mdi-heart"
|
||||
:loading="processing"
|
||||
class="mb-2 me-2"
|
||||
>
|
||||
订阅
|
||||
</VBtn>
|
||||
<VBtn
|
||||
v-if="props.media?.share_uid && props.media?.share_uid === globalSettings.USER_UNIQUE_ID"
|
||||
color="error"
|
||||
:disabled="deleting"
|
||||
@click="doDelete"
|
||||
prepend-icon="mdi-delete"
|
||||
:loading="deleting"
|
||||
class="ms-2"
|
||||
>
|
||||
取消分享
|
||||
</VBtn>
|
||||
<VBtn
|
||||
v-else-if="isFollowed && props.media?.share_uid"
|
||||
v-if="isFollowed && props.media?.share_uid"
|
||||
color="warning"
|
||||
@click="unfollowUser"
|
||||
prepend-icon="mdi-account-remove"
|
||||
class="ms-2"
|
||||
class="mb-2 me-2"
|
||||
>
|
||||
取消关注
|
||||
</VBtn>
|
||||
@@ -258,10 +248,24 @@ onMounted(() => {
|
||||
@click="followUser"
|
||||
color="info"
|
||||
prepend-icon="mdi-account-plus"
|
||||
class="ms-2"
|
||||
class="mb-2 me-2"
|
||||
>
|
||||
关注
|
||||
</VBtn>
|
||||
<VBtn
|
||||
v-if="
|
||||
(props.media?.share_uid && props.media?.share_uid === globalSettings.USER_UNIQUE_ID) ||
|
||||
globalSettings.SUBSCRIBE_SHARE_MANAGE
|
||||
"
|
||||
color="error"
|
||||
:disabled="deleting"
|
||||
@click="doDelete"
|
||||
prepend-icon="mdi-delete"
|
||||
:loading="deleting"
|
||||
class="mb-2 me-2"
|
||||
>
|
||||
取消分享
|
||||
</VBtn>
|
||||
</div>
|
||||
<div class="text-xs mt-2" v-if="props.media?.count">
|
||||
<VIcon icon="mdi-fire" />共 {{ props.media?.count?.toLocaleString() }} 次复用
|
||||
|
||||
@@ -21,7 +21,7 @@ function handleImport() {
|
||||
<template>
|
||||
<VDialog width="40rem" scrollable max-height="85vh" persistent>
|
||||
<VCard :title="props.title" class="rounded-t">
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardText class="pt-2">
|
||||
<VTextarea v-model="codeString" />
|
||||
</VCardText>
|
||||
|
||||
@@ -13,7 +13,7 @@ const emit = defineEmits(['close'])
|
||||
<template>
|
||||
<VDialog max-width="50rem">
|
||||
<VCard>
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardItem>
|
||||
<MediaInfoCard :context="context" />
|
||||
</VCardItem>
|
||||
|
||||
@@ -92,7 +92,7 @@ onBeforeMount(async () => {
|
||||
<template>
|
||||
<VDialog scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard :title="`${props.plugin?.plugin_name} - 配置`" class="rounded-t">
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VDivider />
|
||||
<VCardText v-if="isRefreshed">
|
||||
<FormRender v-for="(item, index) in pluginFormItems" :key="index" :config="item" :model="pluginConfigForm" />
|
||||
|
||||
@@ -43,7 +43,7 @@ onMounted(() => {
|
||||
<template>
|
||||
<VDialog scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard :title="`${props.plugin?.plugin_name}`" class="rounded-t">
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-5" />
|
||||
<VCardText v-else class="min-h-40">
|
||||
<PageRender @action="loadPluginPage" v-for="(item, index) in pluginPageItems" :key="index" :config="item" />
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
import api from '@/api'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
})
|
||||
|
||||
const $toast = useToast()
|
||||
|
||||
// 插件仓库设置字符串
|
||||
@@ -47,8 +42,14 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<VDialog width="50rem" scrollable max-height="85vh">
|
||||
<VCard title="插件仓库设置" class="rounded-t">
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VCard class="rounded-t">
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-store-cog" class="me-2" />
|
||||
插件仓库设置
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
</VCardItem>
|
||||
<VCardText class="pt-2">
|
||||
<VTextarea
|
||||
v-model="repoString"
|
||||
|
||||
@@ -7,7 +7,7 @@ const props = defineProps({
|
||||
<template>
|
||||
<!-- 手动整理进度框 -->
|
||||
<VDialog :scrim="false" width="25rem">
|
||||
<VCard color="primary">
|
||||
<VCard elevation="3" color="primary">
|
||||
<VCardText class="text-center">
|
||||
{{ props.text }}
|
||||
<VProgressLinear color="white" class="mb-0 mt-1" :model-value="props.value" indeterminate />
|
||||
|
||||
@@ -39,7 +39,7 @@ async function savaRcloneConfig() {
|
||||
<template>
|
||||
<VDialog width="50rem" scrollable max-height="85vh">
|
||||
<VCard title="RClone配置" class="rounded-t">
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
|
||||
@@ -87,6 +87,7 @@ const transferForm = reactive<TransferForm>({
|
||||
|
||||
// 所有媒体库目录
|
||||
const directories = ref<TransferDirectoryConf[]>([])
|
||||
|
||||
// 查询目录
|
||||
async function loadDirectories() {
|
||||
try {
|
||||
@@ -221,9 +222,9 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard :title="dialogTitle" class="rounded-t">
|
||||
<DialogCloseBtn @click="emit('close')" />
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
@@ -263,7 +264,7 @@ onUnmounted(() => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="transferForm.type_name"
|
||||
label="类型"
|
||||
@@ -276,7 +277,7 @@ onUnmounted(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-if="mediaSource === 'themoviedb'"
|
||||
v-model="transferForm.tmdbid"
|
||||
@@ -302,19 +303,37 @@ onUnmounted(() => {
|
||||
@click:append-inner="mediaSelectorDialog = true"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
v-show="transferForm.type_name === '电视剧'"
|
||||
v-model.number="transferForm.season"
|
||||
label="季"
|
||||
:items="seasonItems"
|
||||
hint="指定季数"
|
||||
</VRow>
|
||||
<VRow v-show="transferForm.type_name === '电视剧'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_group"
|
||||
label="剧集组编号"
|
||||
placeholder="手动查询剧集组"
|
||||
hint="指定剧集组"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="8">
|
||||
<VCol cols="12" md="3">
|
||||
<VSelect
|
||||
v-model.number="transferForm.season"
|
||||
label="季"
|
||||
:items="seasonItems"
|
||||
hint="第几季"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_detail"
|
||||
:disabled="disableEpisodeDetail"
|
||||
label="集"
|
||||
placeholder="起始集,终止集"
|
||||
hint="集数或范围,如1或1,2"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_format"
|
||||
label="集数定位"
|
||||
@@ -323,26 +342,7 @@ onUnmounted(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_detail"
|
||||
:disabled="disableEpisodeDetail"
|
||||
label="指定集数"
|
||||
placeholder="起始集,终止集,如1或1,2"
|
||||
hint="指定集数或范围,如1或1,2"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_part"
|
||||
label="指定Part"
|
||||
placeholder="如part1"
|
||||
hint="指定Part,如part1"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_offset"
|
||||
label="集数偏移"
|
||||
@@ -351,7 +351,18 @@ onUnmounted(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="transferForm.episode_part"
|
||||
label="指定Part"
|
||||
placeholder="如part1"
|
||||
hint="指定Part,如part1"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model.number="transferForm.min_filesize"
|
||||
label="最小文件大小(MB)"
|
||||
|
||||
855
src/components/dialog/SearchBarDialog.vue
Normal file
855
src/components/dialog/SearchBarDialog.vue
Normal file
@@ -0,0 +1,855 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import type { Site, Plugin, Subscribe } from '@/api/types'
|
||||
import { SystemNavMenus, SettingTabs } from '@/router/menu'
|
||||
import { NavMenu } from '@/@layouts/types'
|
||||
import { useUserStore } from '@/stores'
|
||||
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
||||
|
||||
// 定义props,接收modelValue
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
}>()
|
||||
|
||||
// 路由
|
||||
const router = useRouter()
|
||||
|
||||
// 用户 Store
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 超级用户
|
||||
const superUser = userStore.superUser
|
||||
|
||||
// 当前用户名
|
||||
const userName = userStore.userName
|
||||
|
||||
// 所有订阅数据
|
||||
const SubscribeItems = ref<Subscribe[]>([])
|
||||
|
||||
// 站点选择对话框
|
||||
const chooseSiteDialog = ref(false)
|
||||
const selectedSites = ref<number[]>([])
|
||||
const allSites = ref<Site[]>([])
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['close', 'update:modelValue'])
|
||||
|
||||
// 对话框状态的本地计算属性
|
||||
const dialog = computed({
|
||||
get: () => props.modelValue,
|
||||
set: val => emit('update:modelValue', val),
|
||||
})
|
||||
|
||||
// 搜索词
|
||||
const searchWord = ref<string | null>(null)
|
||||
|
||||
// ref
|
||||
const searchWordInput = ref<HTMLElement | null>(null)
|
||||
|
||||
// 近期搜索词条
|
||||
const recentSearches = ref<string[]>([])
|
||||
|
||||
// 保存近期搜索到本地
|
||||
function saveRecentSearches(keyword: string) {
|
||||
if (!keyword) return
|
||||
if (recentSearches.value.includes(keyword)) return
|
||||
recentSearches.value.unshift(keyword)
|
||||
localStorage.setItem('MP_RecentSearches', JSON.stringify(recentSearches.value))
|
||||
}
|
||||
|
||||
// 从本地加载近期搜索
|
||||
function loadRecentSearches() {
|
||||
const recentSearchesStr = localStorage.getItem('MP_RecentSearches')
|
||||
if (recentSearchesStr) {
|
||||
recentSearches.value = JSON.parse(recentSearchesStr)
|
||||
// 只保留最近的 5 条
|
||||
if (recentSearches.value.length > 5) {
|
||||
recentSearches.value = recentSearches.value.slice(0, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有菜单功能
|
||||
function getMenus(): NavMenu[] {
|
||||
let menus: NavMenu[] = []
|
||||
// 导航菜单
|
||||
SystemNavMenus.forEach(
|
||||
item =>
|
||||
item &&
|
||||
menus.push({
|
||||
title: item.full_title ?? item.title,
|
||||
icon: item.icon,
|
||||
to: item.to,
|
||||
header: item.header,
|
||||
admin: item.admin,
|
||||
}),
|
||||
)
|
||||
// 设置标签页
|
||||
SettingTabs.forEach(
|
||||
item =>
|
||||
item &&
|
||||
menus.push({
|
||||
title: '设定 -> ' + item.title,
|
||||
icon: item.icon,
|
||||
to: `/setting?tab=${item.tab}`,
|
||||
header: '',
|
||||
admin: true,
|
||||
description: item.description,
|
||||
}),
|
||||
)
|
||||
|
||||
return menus
|
||||
}
|
||||
|
||||
// 匹配的菜单列表
|
||||
const matchedMenuItems = computed(() => {
|
||||
if (!searchWord.value) return []
|
||||
if (!superUser) return []
|
||||
const lowerWord = (searchWord.value as string).toLowerCase()
|
||||
const menuItems = getMenus()
|
||||
if (menuItems)
|
||||
return menuItems.filter(
|
||||
item =>
|
||||
item.title.toLowerCase().includes(lowerWord) ||
|
||||
(item.description && item.description.toLowerCase().includes(lowerWord)),
|
||||
)
|
||||
return []
|
||||
})
|
||||
|
||||
// 所有插件(已安装)
|
||||
const pluginItems = ref<Plugin[]>([])
|
||||
|
||||
// 获取插件列表数据
|
||||
async function fetchInstalledPlugins() {
|
||||
try {
|
||||
pluginItems.value = await api.get('plugin/', {
|
||||
params: {
|
||||
state: 'installed',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 区配的插件列表
|
||||
const matchedPluginItems = computed(() => {
|
||||
if (!searchWord.value) return []
|
||||
if (!superUser) return []
|
||||
const lowerWord = (searchWord.value as string).toLowerCase()
|
||||
return pluginItems.value.filter((item: Plugin) => {
|
||||
if (!item.plugin_name && !item.plugin_desc) return false
|
||||
return item.plugin_name?.toLowerCase().includes(lowerWord) || item.plugin_desc?.toLowerCase().includes(lowerWord)
|
||||
})
|
||||
})
|
||||
|
||||
// 获取订阅列表数据
|
||||
async function fetchSubscribes() {
|
||||
try {
|
||||
SubscribeItems.value = await api.get('subscribe/')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 从接口加载用户站点偏好设置
|
||||
const loadUserSitePreferences = async () => {
|
||||
try {
|
||||
const result = await api.get('system/setting/IndexerSites')
|
||||
if (result && result.data && result.data.value) {
|
||||
selectedSites.value = result.data.value
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询所有站点
|
||||
async function queryAllSites() {
|
||||
try {
|
||||
const data: Site[] = await api.get('site/')
|
||||
// 过滤站点,只有启用的站点才显示
|
||||
allSites.value = data.filter(item => item.is_active)
|
||||
// 如果没有选择任何站点并且有可用站点,才默认选择全部
|
||||
if (selectedSites.value.length === 0 && allSites.value.length > 0) {
|
||||
selectedSites.value = allSites.value.map((site: Site) => site.id)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开站点选择对话框
|
||||
const openSiteDialog = () => {
|
||||
chooseSiteDialog.value = true
|
||||
}
|
||||
|
||||
// 匹配的订阅列表
|
||||
const matchedSubscribeItems = computed(() => {
|
||||
if (!searchWord.value) return []
|
||||
const lowerWord = (searchWord.value as string).toLowerCase()
|
||||
return SubscribeItems.value.filter((item: Subscribe) => {
|
||||
return (item.name.toLowerCase().includes(lowerWord) && (superUser || userName === item.username)) || false
|
||||
})
|
||||
})
|
||||
|
||||
// 搜索多站点
|
||||
function searchSites(sites: number[]) {
|
||||
chooseSiteDialog.value = false
|
||||
selectedSites.value = sites
|
||||
searchTorrent()
|
||||
}
|
||||
|
||||
// 搜索资源
|
||||
function searchTorrent() {
|
||||
if (!searchWord.value) return
|
||||
// 记录搜索词
|
||||
saveRecentSearches(searchWord.value)
|
||||
// 跳转到搜索页面
|
||||
router.push({
|
||||
path: '/resource',
|
||||
query: {
|
||||
keyword: searchWord.value,
|
||||
area: 'title',
|
||||
sites: selectedSites.value.join(','),
|
||||
},
|
||||
})
|
||||
// 关闭搜索对话框
|
||||
dialog.value = false
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 跳转媒体搜索页面
|
||||
function searchMedia(searchType: string) {
|
||||
// 搜索类型 media/person
|
||||
if (!searchWord.value) return
|
||||
saveRecentSearches(searchWord.value)
|
||||
router.push({
|
||||
path: '/browse/media/search',
|
||||
query: {
|
||||
title: searchWord.value,
|
||||
type: searchType,
|
||||
},
|
||||
})
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 跳转到历史记录页面
|
||||
function searchHistory() {
|
||||
if (!searchWord.value) return
|
||||
saveRecentSearches(searchWord.value)
|
||||
router.push({
|
||||
path: '/history',
|
||||
query: {
|
||||
search: searchWord.value,
|
||||
},
|
||||
})
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 跳转插件页面
|
||||
function showPlugin(pluginId: string) {
|
||||
router.push({
|
||||
path: `/plugins/`,
|
||||
query: {
|
||||
tab: 'installed',
|
||||
id: pluginId,
|
||||
},
|
||||
})
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 跳转菜单页面
|
||||
function goPage(to: string) {
|
||||
router.push(to)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 跳转订阅页面
|
||||
function goSubscribe(subscribe: Subscribe) {
|
||||
if (subscribe.type === '电影') {
|
||||
router.push({
|
||||
path: '/subscribe/movie',
|
||||
query: {
|
||||
id: subscribe.id,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
router.push({
|
||||
path: '/subscribe/tv',
|
||||
query: {
|
||||
id: subscribe.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
emit('close')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
searchWordInput.value?.focus()
|
||||
}, 500)
|
||||
fetchInstalledPlugins()
|
||||
fetchSubscribes()
|
||||
loadRecentSearches()
|
||||
loadUserSitePreferences()
|
||||
if (superUser) queryAllSites()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VDialog v-model="dialog" max-width="42rem" scrollable>
|
||||
<VCard class="search-dialog">
|
||||
<!-- 搜索输入框 -->
|
||||
<VCardItem class="pa-4 pa-sm-5 search-box-container">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-magnify" color="primary" size="x-large" />
|
||||
</template>
|
||||
<VCombobox
|
||||
ref="searchWordInput"
|
||||
v-model="searchWord"
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
class="search-input"
|
||||
placeholder="输入关键词搜索..."
|
||||
@keydown.enter="searchMedia('media')"
|
||||
hide-details
|
||||
clearable
|
||||
/>
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-close" color="primary" @click="emit('close')" size="x-large" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<!-- 主搜索结果区域 -->
|
||||
<VCardText class="search-results-container pa-0">
|
||||
<!-- 有搜索词时显示结果 -->
|
||||
<VList lines="two" v-if="searchWord" class="search-list py-2">
|
||||
<!-- 搜索结果分组标题 -->
|
||||
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6"> 媒体 </VListSubheader>
|
||||
|
||||
<!-- 媒体搜索选项 -->
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
density="comfortable"
|
||||
link
|
||||
rounded="xl"
|
||||
v-bind="hover.props"
|
||||
@click="searchMedia('media')"
|
||||
class="search-option mx-2 mx-sm-4 my-1"
|
||||
>
|
||||
<template #prepend>
|
||||
<div class="option-icon-wrapper d-flex align-center justify-center">
|
||||
<VIcon
|
||||
icon="mdi-movie-search"
|
||||
:color="hover.isHovering ? 'primary' : 'medium-emphasis'"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<VListItemTitle class="font-weight-medium"> 电影、电视剧 </VListItemTitle>
|
||||
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
|
||||
搜索 <span class="primary-text font-weight-medium">{{ searchWord }}</span> 相关的影视作品
|
||||
</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="mdi-chevron-right" color="primary" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
density="comfortable"
|
||||
link
|
||||
rounded="xl"
|
||||
v-bind="hover.props"
|
||||
@click="searchMedia('collection')"
|
||||
class="search-option mx-2 mx-sm-4 my-1"
|
||||
>
|
||||
<template #prepend>
|
||||
<div class="option-icon-wrapper d-flex align-center justify-center">
|
||||
<VIcon
|
||||
icon="mdi-movie-filter"
|
||||
:color="hover.isHovering ? 'primary' : 'medium-emphasis'"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<VListItemTitle class="font-weight-medium"> 系列合集 </VListItemTitle>
|
||||
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
|
||||
搜索 <span class="primary-text font-weight-medium">{{ searchWord }}</span> 相关的系列作品
|
||||
</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="mdi-chevron-right" color="primary" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
density="comfortable"
|
||||
link
|
||||
rounded="xl"
|
||||
v-bind="hover.props"
|
||||
@click="searchMedia('person')"
|
||||
class="search-option mx-2 mx-sm-4 my-1"
|
||||
>
|
||||
<template #prepend>
|
||||
<div class="option-icon-wrapper d-flex align-center justify-center">
|
||||
<VIcon
|
||||
icon="mdi-account-search"
|
||||
:color="hover.isHovering ? 'primary' : 'medium-emphasis'"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<VListItemTitle class="font-weight-medium"> 演职人员 </VListItemTitle>
|
||||
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
|
||||
搜索 <span class="primary-text font-weight-medium">{{ searchWord }}</span> 相关的演员、导演等
|
||||
</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="mdi-chevron-right" color="primary" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
|
||||
<VHover v-if="superUser">
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
density="comfortable"
|
||||
link
|
||||
rounded="xl"
|
||||
v-bind="hover.props"
|
||||
@click="searchHistory"
|
||||
class="search-option mx-2 mx-sm-4 my-1"
|
||||
>
|
||||
<template #prepend>
|
||||
<div class="option-icon-wrapper d-flex align-center justify-center">
|
||||
<VIcon icon="mdi-history" :color="hover.isHovering ? 'primary' : 'medium-emphasis'" size="small" />
|
||||
</div>
|
||||
</template>
|
||||
<VListItemTitle class="font-weight-medium"> 整理记录 </VListItemTitle>
|
||||
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
|
||||
搜索 <span class="primary-text font-weight-medium">{{ searchWord }}</span> 相关的历史记录
|
||||
</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="mdi-chevron-right" color="primary" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
|
||||
<!-- 其他搜索结果 -->
|
||||
<template v-if="matchedSubscribeItems.length > 0">
|
||||
<VDivider class="mx-4 mx-sm-6 my-2 search-divider" />
|
||||
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6"> 订阅 </VListSubheader>
|
||||
|
||||
<VHover v-for="subscribe in matchedSubscribeItems" :key="subscribe.id">
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
density="comfortable"
|
||||
link
|
||||
rounded="xl"
|
||||
v-bind="hover.props"
|
||||
@click="goSubscribe(subscribe)"
|
||||
class="search-option mx-2 mx-sm-4 my-1"
|
||||
>
|
||||
<template #prepend>
|
||||
<div class="option-icon-wrapper d-flex align-center justify-center">
|
||||
<VIcon
|
||||
:icon="subscribe.type === '电影' ? 'mdi-movie-roll' : 'mdi-television-classic'"
|
||||
:color="hover.isHovering ? 'primary' : 'medium-emphasis'"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<VListItemTitle class="font-weight-medium">
|
||||
{{ subscribe.name
|
||||
}}<span v-if="subscribe.season" class="text-body-2"> 第 {{ subscribe.season }} 季</span>
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
|
||||
{{ subscribe.type }}
|
||||
</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="mdi-chevron-right" color="primary" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<template v-if="matchedMenuItems.length > 0">
|
||||
<VDivider class="mx-4 mx-sm-6 my-2 search-divider" />
|
||||
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6"> 功能 </VListSubheader>
|
||||
|
||||
<VHover v-for="menu in matchedMenuItems" :key="menu.title">
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
density="comfortable"
|
||||
link
|
||||
rounded="xl"
|
||||
v-bind="hover.props"
|
||||
@click="goPage(menu.to as string)"
|
||||
class="search-option mx-2 mx-sm-4 my-1"
|
||||
>
|
||||
<template #prepend>
|
||||
<div class="option-icon-wrapper d-flex align-center justify-center">
|
||||
<VIcon
|
||||
:icon="menu.icon as string"
|
||||
:color="hover.isHovering ? 'primary' : 'medium-emphasis'"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<VListItemTitle class="font-weight-medium">
|
||||
{{ menu.title }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle v-if="menu.description" class="text-body-2 text-medium-emphasis mt-1">
|
||||
{{ menu.description }}
|
||||
</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="mdi-chevron-right" color="primary" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<template v-if="matchedPluginItems.length > 0">
|
||||
<VDivider class="mx-4 mx-sm-6 my-2 search-divider" />
|
||||
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6"> 插件 </VListSubheader>
|
||||
|
||||
<VHover v-for="plugin in matchedPluginItems" :key="plugin.id">
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
density="comfortable"
|
||||
link
|
||||
rounded="xl"
|
||||
v-bind="hover.props"
|
||||
@click="showPlugin(plugin.id ?? '')"
|
||||
class="search-option mx-2 mx-sm-4 my-1"
|
||||
>
|
||||
<template #prepend>
|
||||
<div class="option-icon-wrapper d-flex align-center justify-center">
|
||||
<VIcon icon="mdi-puzzle" :color="hover.isHovering ? 'primary' : 'medium-emphasis'" size="small" />
|
||||
</div>
|
||||
</template>
|
||||
<VListItemTitle class="font-weight-medium">
|
||||
{{ plugin.plugin_name }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle class="text-body-2 text-medium-emphasis mt-1">
|
||||
{{ plugin.plugin_desc }}
|
||||
</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VIcon v-if="hover.isHovering" icon="mdi-chevron-right" color="primary" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<!-- 将站点资源搜索移到最底部 -->
|
||||
<template v-if="searchWord">
|
||||
<VDivider class="mx-4 mx-sm-6 my-2 search-divider" />
|
||||
<VListSubheader class="font-weight-medium text-uppercase py-2 px-4 px-sm-6"> 站点资源 </VListSubheader>
|
||||
|
||||
<VCard class="mx-3 mx-sm-6 mb-4 mt-2 site-search-card">
|
||||
<VCardText class="pa-3 pa-sm-4">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="d-flex align-center">
|
||||
<div class="search-icon-wrapper mr-3">
|
||||
<VIcon icon="mdi-file-search" color="primary" size="small" />
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="font-weight-medium text-body-1">在站点中搜索种子资源</div>
|
||||
<div class="text-caption text-medium-emphasis mt-1">
|
||||
搜索 <span class="primary-text font-weight-medium">{{ searchWord }}</span> 相关资源
|
||||
</div>
|
||||
</div>
|
||||
<VBtn
|
||||
color="primary"
|
||||
@click="searchTorrent"
|
||||
prepend-icon="mdi-magnify"
|
||||
size="small"
|
||||
variant="flat"
|
||||
class="search-btn"
|
||||
>
|
||||
搜索
|
||||
</VBtn>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="superUser"
|
||||
class="d-flex align-center flex-wrap site-chips-container mt-4 py-2 px-2 px-sm-3"
|
||||
>
|
||||
<div class="d-flex align-center flex-wrap flex-grow-1">
|
||||
<VChip
|
||||
v-if="selectedSites.length > 0"
|
||||
color="primary"
|
||||
size="small"
|
||||
variant="flat"
|
||||
class="mr-2 mb-1 font-weight-medium"
|
||||
>
|
||||
{{ selectedSites.length }}/{{ allSites.length }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-for="(site, index) in allSites.filter(s => selectedSites.includes(s.id)).slice(0, 5)"
|
||||
:key="site.id"
|
||||
size="x-small"
|
||||
variant="outlined"
|
||||
class="mr-1 mb-1 site-chip"
|
||||
>
|
||||
{{ site.name }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="selectedSites.length > 5"
|
||||
size="x-small"
|
||||
variant="outlined"
|
||||
class="mr-1 mb-1 site-chip text-medium-emphasis"
|
||||
>
|
||||
+{{ selectedSites.length - 5 }}
|
||||
</VChip>
|
||||
</div>
|
||||
<VBtn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
@click="openSiteDialog"
|
||||
class="ml-auto site-select-btn"
|
||||
rounded="pill"
|
||||
>
|
||||
选择站点
|
||||
<VIcon size="small" class="ml-1">mdi-cog-outline</VIcon>
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
</VList>
|
||||
|
||||
<!-- 无搜索词时显示最近搜索和提示 -->
|
||||
<div v-else class="recent-searches py-6 px-4 px-sm-6">
|
||||
<div v-if="recentSearches.length > 0" class="mb-6">
|
||||
<div class="text-h6 font-weight-medium mb-3">最近搜索</div>
|
||||
<div class="d-flex flex-wrap">
|
||||
<VChip
|
||||
v-for="(word, index) in recentSearches"
|
||||
:key="index"
|
||||
class="me-2 mb-2"
|
||||
variant="flat"
|
||||
color="primary"
|
||||
size="small"
|
||||
@click="searchWord = word"
|
||||
>
|
||||
<VIcon start size="x-small">mdi-history</VIcon>
|
||||
{{ word }}
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-6 py-6 empty-search-state">
|
||||
<div class="search-icon-wrapper mx-auto mb-4">
|
||||
<VIcon icon="mdi-magnify" size="large" color="primary" />
|
||||
</div>
|
||||
<div class="text-h6 font-weight-medium mb-2">输入关键词开始搜索</div>
|
||||
<div class="text-body-2 text-medium-emphasis">可搜索电影、电视剧、演员、资源等</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- 站点选择对话框 -->
|
||||
<SearchSiteDialog
|
||||
v-if="chooseSiteDialog"
|
||||
v-model="chooseSiteDialog"
|
||||
:sites="allSites"
|
||||
:selected="selectedSites"
|
||||
@search="searchSites"
|
||||
@close="chooseSiteDialog = false"
|
||||
@reload="queryAllSites"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-dialog {
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 8%);
|
||||
}
|
||||
|
||||
.site-dialog {
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 8%);
|
||||
}
|
||||
|
||||
.search-divider {
|
||||
opacity: 0.08;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.04);
|
||||
block-size: 36px;
|
||||
inline-size: 36px;
|
||||
inset-block-start: 1.4rem;
|
||||
inset-inline-end: 1.2rem;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background-color: rgba(var(--v-theme-error), 0.1);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.option-icon-wrapper {
|
||||
border-radius: 8px;
|
||||
background-color: rgba(var(--v-theme-surface-variant), 0.12);
|
||||
block-size: 32px;
|
||||
inline-size: 32px;
|
||||
margin-inline-end: 12px;
|
||||
}
|
||||
|
||||
.search-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||
block-size: 36px;
|
||||
inline-size: 36px;
|
||||
}
|
||||
|
||||
.search-icon-wrapper.warning {
|
||||
background-color: rgba(var(--v-theme-warning), 0.08);
|
||||
}
|
||||
|
||||
.primary-text {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.search-option {
|
||||
border: 1px solid transparent;
|
||||
margin-block-end: 2px;
|
||||
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.search-option:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.04);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.recent-searches {
|
||||
min-block-size: 200px;
|
||||
}
|
||||
|
||||
.site-search-card {
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.site-chip {
|
||||
font-weight: normal;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.site-chip:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
min-inline-size: 70px;
|
||||
}
|
||||
|
||||
.empty-search-state,
|
||||
.empty-site-state {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.clear-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.site-select-btn {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
min-block-size: 32px;
|
||||
padding-block: 0;
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.site-chips-container {
|
||||
border-radius: 10px;
|
||||
background-color: rgba(var(--v-theme-surface-variant), 0.06);
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.search-box-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
block-size: 32px;
|
||||
inline-size: 32px;
|
||||
inset-block-start: 1rem;
|
||||
inset-inline-end: 0.8rem;
|
||||
}
|
||||
|
||||
.site-chips-container {
|
||||
padding-block: 6px;
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
.site-select-btn {
|
||||
font-size: 11px;
|
||||
min-block-size: 28px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
213
src/components/dialog/SearchSiteDialog.vue
Normal file
213
src/components/dialog/SearchSiteDialog.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<script setup lang="ts">
|
||||
import type { Site, Plugin, Subscribe } from '@/api/types'
|
||||
import { popScopeId, PropType } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
sites: {
|
||||
type: Array as PropType<Site[]>,
|
||||
required: true,
|
||||
},
|
||||
selected: Array as PropType<Number[]>,
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['close', 'search', 'reload'])
|
||||
|
||||
// 过滤词
|
||||
const siteFilter = ref('')
|
||||
|
||||
// 已选择站点
|
||||
const selectedSites = ref<any[]>(props.selected || [])
|
||||
|
||||
watch(
|
||||
() => props.selected,
|
||||
value => {
|
||||
if (selectedSites.value.length == 0 && value) {
|
||||
selectedSites.value = value
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 全选/全不选按钮文字
|
||||
const checkAllText = computed(() => {
|
||||
return selectedSites.value.length < props.sites?.length ? '选择全部' : '取消全选'
|
||||
})
|
||||
|
||||
// 全选/全不选
|
||||
const checkAllSitesorNot = () => {
|
||||
if (selectedSites.value.length < props.sites?.length) {
|
||||
selectedSites.value = props.sites?.map((item: Site) => item.id)
|
||||
} else {
|
||||
selectedSites.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 根据筛选条件过滤站点
|
||||
const filteredSites = computed(() => {
|
||||
if (!siteFilter.value) return props.sites
|
||||
const filter = siteFilter.value.toLowerCase()
|
||||
return props.sites?.filter((site: Site) => site.name.toLowerCase().includes(filter))
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<!-- 手动整理进度框 -->
|
||||
<VDialog max-width="40rem" fullscreen-mobile>
|
||||
<VCard class="site-dialog">
|
||||
<VCardTitle class="d-flex align-center pa-4">
|
||||
<span class="text-h6 font-weight-medium">选择搜索站点</span>
|
||||
<VSpacer />
|
||||
<VTextField
|
||||
v-model="siteFilter"
|
||||
placeholder="过滤站点..."
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="ml-4"
|
||||
style="max-width: 200px"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
clearable
|
||||
/>
|
||||
</VCardTitle>
|
||||
<VDivider class="search-divider" />
|
||||
|
||||
<VCardText style="max-height: 420px" class="overflow-y-auto px-4 py-4">
|
||||
<!-- 站点列表 -->
|
||||
<div v-if="filteredSites.length > 0">
|
||||
<!-- 选择操作 -->
|
||||
<div class="d-flex align-center mb-4">
|
||||
<VBtn
|
||||
size="small"
|
||||
:color="selectedSites.length < sites.length ? 'primary' : 'error'"
|
||||
@click="checkAllSitesorNot"
|
||||
class="me-2"
|
||||
rounded="pill"
|
||||
variant="flat"
|
||||
>
|
||||
<VIcon start size="small">
|
||||
{{ selectedSites.length < sites.length ? 'mdi-check-all' : 'mdi-close-circle-outline' }}
|
||||
</VIcon>
|
||||
{{ checkAllText }}
|
||||
</VBtn>
|
||||
<div
|
||||
class="text-body-2 font-weight-medium"
|
||||
:class="selectedSites.length > 0 ? 'text-primary' : 'text-medium-emphasis'"
|
||||
>
|
||||
已选择 {{ selectedSites.length }}/{{ sites.length }} 个站点
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 站点选择器 -->
|
||||
<VRow dense>
|
||||
<VCol v-for="site in filteredSites" :key="site.id" cols="6" sm="6" md="4">
|
||||
<VHover v-slot="{ isHovering, props }">
|
||||
<div
|
||||
v-bind="props"
|
||||
:class="[
|
||||
'site-checkbox-wrapper pa-2 pa-sm-3 rounded-lg d-flex align-center',
|
||||
{
|
||||
'site-selected': selectedSites.includes(site.id),
|
||||
'site-hover': isHovering && !selectedSites.includes(site.id),
|
||||
},
|
||||
]"
|
||||
@click="
|
||||
() => {
|
||||
const index = selectedSites.indexOf(site.id)
|
||||
if (index === -1) {
|
||||
selectedSites.push(site.id)
|
||||
} else {
|
||||
selectedSites.splice(index, 1)
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<VIcon
|
||||
:icon="selectedSites.includes(site.id) ? 'mdi-check-circle' : 'mdi-checkbox-blank-circle-outline'"
|
||||
:color="selectedSites.includes(site.id) ? 'primary' : 'medium-emphasis'"
|
||||
class="me-2"
|
||||
size="small"
|
||||
/>
|
||||
<span :class="['text-body-2 site-name', { 'font-weight-medium': selectedSites.includes(site.id) }]">
|
||||
{{ site.name }}
|
||||
</span>
|
||||
</div>
|
||||
</VHover>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
<div v-else class="text-center py-8 empty-site-state">
|
||||
<div class="search-icon-wrapper mb-4 mx-auto warning">
|
||||
<VIcon icon="mdi-alert-circle-outline" size="large" color="warning" />
|
||||
</div>
|
||||
<div class="text-h6 font-weight-medium mb-2">没有找到匹配的站点</div>
|
||||
<div class="text-subtitle-1 text-medium-emphasis mb-4">
|
||||
{{ siteFilter ? '请尝试修改过滤条件' : '站点数据加载失败,请刷新页面重试' }}
|
||||
</div>
|
||||
<VBtn
|
||||
v-if="siteFilter"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
class="mt-3"
|
||||
prepend-icon="mdi-refresh"
|
||||
@click="siteFilter = ''"
|
||||
>
|
||||
重置
|
||||
</VBtn>
|
||||
<VBtn v-else color="primary" variant="flat" class="mt-3" prepend-icon="mdi-refresh" @click="emit('reload')">
|
||||
重新加载站点
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VDivider class="search-divider" />
|
||||
|
||||
<VCardActions class="pa-4">
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
color="grey-darken-1"
|
||||
variant="text"
|
||||
@click="emit('close')"
|
||||
class="mr-2 d-flex align-center justify-center"
|
||||
>
|
||||
取消
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
:disabled="selectedSites.length === 0"
|
||||
@click="emit('search', selectedSites)"
|
||||
prepend-icon="mdi-magnify"
|
||||
class="d-flex align-center justify-center px-5"
|
||||
>
|
||||
搜索
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
<style scoped>
|
||||
.site-checkbox-wrapper {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
}
|
||||
|
||||
.site-checkbox-wrapper:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.site-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.site-selected {
|
||||
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||
color: rgb(var(--v-theme-primary));
|
||||
border-color: rgba(var(--v-theme-primary), 0.2);
|
||||
}
|
||||
|
||||
.site-hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.04);
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user