mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-09 05:12:40 +08:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dee6503e33 | ||
|
|
3b0123f2be | ||
|
|
74d7b2b280 | ||
|
|
712623806a | ||
|
|
ecb4fda5fc | ||
|
|
1ee577677a | ||
|
|
f091cfd7be | ||
|
|
45eee811c1 | ||
|
|
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 |
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
|
||||
}
|
||||
}
|
||||
|
||||
411
index.html
411
index.html
@@ -1,221 +1,214 @@
|
||||
<!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="#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="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">
|
||||
<style>
|
||||
/* 添加SVG内部的动画样式 */
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
filter: drop-shadow(0 0 3px rgba(141, 81, 249, 0.3));
|
||||
}
|
||||
|
||||
50% {
|
||||
filter: drop-shadow(0 0 6px rgba(141, 81, 249, 0.6));
|
||||
}
|
||||
}
|
||||
|
||||
/* 为各个元素添加动画 */
|
||||
#a2-c {
|
||||
filter: drop-shadow(0 0 5px rgba(141, 81, 249, 0.3));
|
||||
animation: glow 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
path {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 错开不同元素的动画开始时间 */
|
||||
g:nth-child(2) path {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
g:nth-child(3) path {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
g:nth-child(4) path {
|
||||
animation-delay: 0.9s;
|
||||
}
|
||||
|
||||
g:nth-child(5) path {
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
</style>
|
||||
<g transform="matrix(1,0,0,1,-2606,-236)">
|
||||
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">
|
||||
<rect x="0" y="0" width="192" height="192" style="fill: none" />
|
||||
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)">
|
||||
<path
|
||||
d="M2241.27,-28.175C2238.86,-28.931 2236.64,-29.181 2234.48,-29.254L2159.78,-29.286L2165.01,-11.207C2167.16,-13.121 2169.64,-13.722 2172.26,-13.808L2222.12,-13.822C2223.52,-13.824 2225,-13.701 2226.78,-13.108L2241.27,-28.175Z"
|
||||
style="fill: url(#_Linear1)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2205.67,331.428L2205.67,332.25L2205.67,352.835C2205.67,354.263 2204.91,355.583 2203.67,356.298C2202.43,357.012 2200.91,357.013 2199.67,356.3L2190.78,351.174C2189.73,350.595 2188.83,350.083 2188.03,349.59L2187.45,349.257C2186.66,348.725 2185.91,348.142 2185.21,347.461C2185.08,347.331 2184.95,347.198 2184.82,347.061C2184.26,346.457 2183.75,345.778 2183.3,344.995C2182.16,343.05 2181.69,341.024 2181.68,338.948L2181.67,268.923L2209.77,274.425C2207.5,275.639 2205.68,278.3 2205.67,281.429L2205.67,331.428Z"
|
||||
style="fill: url(#_Linear2)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2295.93,363.064C2295.73,363.184 2295.53,363.301 2295.32,363.414L2295.93,363.064Z"
|
||||
style="fill: rgb(141, 81, 249)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2299.79,360.238C2299.79,360.238 2320.03,348.464 2320.04,348.461C2323.1,346.372 2324.69,343.444 2325.17,339.877C2325.17,339.877 2325.17,269.846 2325.17,269.839C2325.06,267.482 2324.56,265.739 2323.61,264.133C2322.56,262.445 2321.26,261.005 2319.55,259.97L2304.42,251.217C2303.96,250.949 2303.39,250.948 2302.92,251.216C2302.46,251.484 2302.17,251.979 2302.17,252.515L2302.17,276.775L2302.17,277.879L2302.17,352.926C2302.17,352.933 2302.17,352.941 2302.17,352.948C2302.04,355.861 2301.23,358.279 2299.79,360.238Z"
|
||||
style="fill: url(#_Linear3)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256Z"
|
||||
style="fill: rgb(165, 118, 255)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256ZM2253.68,223.756C2251.6,223.789 2249.87,224.269 2248.47,224.996L2188.17,259.754C2184.35,261.992 2182.35,265.367 2182.18,269.874C2182.18,269.874 2182.17,292.759 2182.17,292.757C2183.25,290.047 2185.13,288.051 2187.62,286.607L2249.57,250.919C2249.58,250.917 2249.58,250.915 2249.59,250.913C2250.83,250.243 2252.17,249.839 2253.67,249.847C2255.21,249.841 2256.54,250.253 2257.76,250.914C2257.76,250.916 2257.76,250.917 2257.76,250.919L2274.92,260.807C2275.38,261.075 2275.95,261.074 2276.42,260.806C2276.88,260.538 2277.17,260.043 2277.17,259.508L2277.17,237.568C2277.17,236.317 2276.5,235.16 2275.42,234.535C2275.42,234.535 2258.88,225 2258.87,224.996C2256.87,224.049 2255.2,223.746 2253.68,223.756Z"
|
||||
style="fill: url(#_Linear4)" />
|
||||
</g>
|
||||
<g transform="matrix(0.800798,0.462341,0.769972,-1.33363,-1677.22,-896.858)">
|
||||
<path
|
||||
d="M2241.55,-28.184C2239.1,-28.989 2236.83,-29.204 2234.68,-29.295C2234.68,-29.295 2220.82,-29.3 2215.03,-29.303C2213.48,-29.303 2212.05,-28.808 2211.28,-28.004C2208.65,-25.275 2202.56,-18.936 2199.45,-15.709C2199.07,-15.306 2199.07,-14.809 2199.46,-14.406C2199.85,-14.004 2200.57,-13.758 2201.34,-13.761C2208.36,-13.788 2222.72,-13.845 2222.72,-13.845C2223.98,-13.851 2225.44,-13.657 2227.06,-13.117L2241.55,-28.184Z"
|
||||
style="fill: rgb(141, 81, 249)" />
|
||||
</g>
|
||||
<g transform="matrix(-4.32309,0,0,12.4454,9610.35,-1450.35)">
|
||||
<path
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
|
||||
style="fill: rgb(104, 0, 197)" />
|
||||
<clipPath id="_clip5">
|
||||
<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>
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.2",
|
||||
"private": true,
|
||||
"bin": "dist/service.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -5,12 +5,28 @@
|
||||
background: var(--initial-loader-bg, #fff);
|
||||
block-size: 100vh;
|
||||
inline-size: 100vw;
|
||||
transition: opacity 0.8s ease, transform 0.8s ease, filter 0.8s ease;
|
||||
}
|
||||
|
||||
.loading-logo {
|
||||
position: absolute;
|
||||
inset-block-start: 35%;
|
||||
inset-inline-start: calc(50% - 5rem);
|
||||
transition: opacity 0.8s ease, transform 0.8s ease, filter 0.8s ease;
|
||||
}
|
||||
|
||||
/* 添加logo完成动画 - 放大虚化效果 */
|
||||
.loading-complete .loading-logo {
|
||||
filter: blur(10px);
|
||||
opacity: 0;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
/* 添加加载背景消失动画 - 放大虚化效果 */
|
||||
.loading-complete {
|
||||
filter: blur(15px);
|
||||
opacity: 0;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.loading {
|
||||
@@ -22,6 +38,12 @@
|
||||
inline-size: 55px;
|
||||
inset-block-start: 80%;
|
||||
inset-inline-start: calc(50% - 27.5px);
|
||||
transition: opacity 0.6s ease;
|
||||
}
|
||||
|
||||
/* 完成时隐藏加载动画 */
|
||||
.loading-complete .loading {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.loading .effect-1,
|
||||
@@ -72,4 +94,4 @@
|
||||
opacity: 1;
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -36,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()
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
@@ -119,7 +60,7 @@ function changeTheme(theme: string) {
|
||||
theme: nextTheme,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('保存主题到服务端失败')
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,37 +136,27 @@ onMounted(() => {
|
||||
<VIcon :icon="getThemeIcon" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
<VList class="theme-switcher-list pt-0">
|
||||
<VCardItem class="theme-switcher-header">
|
||||
<VCardTitle class="font-weight-medium text-primary">主题选择</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<div class="theme-switcher-options px-2">
|
||||
<VList>
|
||||
<div class="px-2">
|
||||
<VListItem
|
||||
v-for="theme in props.themes"
|
||||
:key="theme.name"
|
||||
@click="changeTheme(theme.name)"
|
||||
class="theme-option"
|
||||
:class="{ 'theme-option-active': currentThemeName === theme.name }"
|
||||
:active="currentThemeName === theme.name"
|
||||
class="mb-1"
|
||||
>
|
||||
<template #prepend>
|
||||
<div class="theme-icon-wrapper">
|
||||
<VIcon :icon="theme.icon" />
|
||||
</div>
|
||||
<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" class="theme-option custom-theme-option">
|
||||
<VListItem @click="cssDialog = true">
|
||||
<template #prepend>
|
||||
<div class="theme-icon-wrapper custom-theme-icon">
|
||||
<VIcon icon="mdi-palette" />
|
||||
</div>
|
||||
<VIcon icon="mdi-palette" />
|
||||
</template>
|
||||
<VListItemTitle>自定义主题</VListItemTitle>
|
||||
</VListItem>
|
||||
@@ -243,12 +174,7 @@ onMounted(() => {
|
||||
<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">
|
||||
@@ -261,74 +187,3 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.theme-switcher-header {
|
||||
background: linear-gradient(to right, rgba(var(--v-theme-primary), 0.04), rgba(var(--v-theme-primary), 0.01));
|
||||
border-block-end: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
padding-block: 12px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
.theme-switcher-options {
|
||||
max-block-size: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.theme-option {
|
||||
border-radius: 8px;
|
||||
margin-block: 4px;
|
||||
margin-inline: 0;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.04);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
&.theme-option-active {
|
||||
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||
block-size: 36px;
|
||||
inline-size: 36px;
|
||||
margin-inline-end: 12px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.v-icon {
|
||||
color: rgba(var(--v-theme-primary), 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.custom-theme-icon {
|
||||
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.15), rgba(var(--v-theme-info), 0.15));
|
||||
|
||||
.v-icon {
|
||||
color: rgba(var(--v-theme-primary), 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
// Theme transition
|
||||
.app-copy {
|
||||
position: fixed !important;
|
||||
z-index: -1 !important;
|
||||
overflow: clip !important;
|
||||
contain: size style !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.app-transition {
|
||||
--clip-size: 0;
|
||||
--clip-pos: 0 0;
|
||||
|
||||
clip-path: circle(var(--clip-size) at var(--clip-pos));
|
||||
transition: clip-path 0.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,251 +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(var(--v-theme-surface)) 5%,
|
||||
rgba(var(--v-theme-surface), 75%) 45%,
|
||||
rgba(var(--v-theme-surface), 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;Settingz-indexSettingz-indexSettingz-indexSettingz-index
|
||||
z-index: 1z-indexz-indexz-index
|
||||
}
|
||||
|
||||
// 👉 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%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ export default defineComponent({
|
||||
@include mixins.boxed-content;
|
||||
} @else {
|
||||
.navbar-content-container {
|
||||
@include mixins.boxed-content;
|
||||
// @include mixins.boxed-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,6 @@ export default defineComponent({
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-block-start: 0;
|
||||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
@@ -124,7 +123,7 @@ export default defineComponent({
|
||||
.layout-navbar {
|
||||
position: fixed;
|
||||
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
|
||||
inline-size: calc(100vw - variables.$layout-vertical-nav-width - 1rem);
|
||||
inline-size: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
|
||||
inset-block-start: 0;
|
||||
|
||||
.navbar-content-container {
|
||||
@@ -138,7 +137,7 @@ export default defineComponent({
|
||||
@include mixins.boxed-content;
|
||||
} @else {
|
||||
.navbar-content-container {
|
||||
@include mixins.boxed-content;
|
||||
// @include mixins.boxed-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,7 +177,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
&:not(.layout-overlay-nav) .layout-content-wrapper {
|
||||
padding-inline-start: calc(variables.$layout-vertical-nav-width + 0.5rem);
|
||||
padding-inline-start: calc(variables.$layout-vertical-nav-width);
|
||||
}
|
||||
|
||||
// Adjust right column pl when vertical nav is collapsed
|
||||
@@ -210,9 +209,8 @@ export default defineComponent({
|
||||
|
||||
.layout-wrapper.layout-nav-type-vertical.layout-overlay-nav {
|
||||
.layout-navbar {
|
||||
inline-size: calc(100% - 0.5rem);
|
||||
margin-inline-start: 0.5rem;
|
||||
padding-inline-start: 0;
|
||||
inline-size: 100%;
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -29,6 +29,7 @@ body,
|
||||
|
||||
.navbar-content-container {
|
||||
padding-block-start: env(safe-area-inset-top);
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
|
||||
.layout-page-content {
|
||||
@@ -39,7 +40,8 @@ body,
|
||||
|
||||
// TODO: Use grid gutter variable here;
|
||||
padding-block: 1.5rem;
|
||||
padding-block-start: calc(env(safe-area-inset-top) + 4.25rem);
|
||||
padding-inline: 0.5rem;
|
||||
padding-block-start: calc(env(safe-area-inset-top) + 4.5rem);
|
||||
|
||||
// display: flex;display
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
192
src/App.vue
192
src/App.vue
@@ -2,6 +2,8 @@
|
||||
import { useTheme } from 'vuetify'
|
||||
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||
import { ensureRenderComplete, removeEl } from './@core/utils/dom'
|
||||
import api from '@/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
// 生效主题
|
||||
const { global: globalTheme } = useTheme()
|
||||
@@ -9,9 +11,22 @@ let themeValue = localStorage.getItem('theme') || 'light'
|
||||
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
|
||||
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
|
||||
|
||||
// 从 provide 中获取全局设置
|
||||
const globalSettings: any = inject('globalSettings')
|
||||
|
||||
// 显示状态
|
||||
const show = ref(false)
|
||||
|
||||
// 检查是否登录
|
||||
const authStore = useAuthStore()
|
||||
const isLogin = computed(() => authStore.token)
|
||||
|
||||
// 背景图片
|
||||
const backgroundImages = ref<string[]>([])
|
||||
const activeImageIndex = ref(0)
|
||||
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
|
||||
let backgroundRotationTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// ApexCharts 全局配置
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -42,24 +57,185 @@ if (window.Apex) {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新data-theme属性以便CSS选择器能正确匹配
|
||||
function updateHtmlThemeAttribute(themeName: string) {
|
||||
document.documentElement.setAttribute('data-theme', themeName)
|
||||
// 确保body元素也有相同的主题属性,以便更好地选择弹出窗口
|
||||
document.body.setAttribute('data-theme', themeName)
|
||||
}
|
||||
|
||||
// 获取背景图片
|
||||
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') {
|
||||
// 如果已有背景图片数据,直接重启轮换
|
||||
if (backgroundImages.value.length > 0) {
|
||||
startBackgroundRotation()
|
||||
}
|
||||
// 如果没有背景图片数据,重新获取
|
||||
else {
|
||||
fetchBackgroundImages().then(() => startBackgroundRotation())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加logo动画效果并延迟移除加载界面
|
||||
function animateAndRemoveLoader() {
|
||||
const loadingBg = document.querySelector('#loading-bg') as HTMLElement
|
||||
if (loadingBg) {
|
||||
// 先添加完成动画类
|
||||
loadingBg.classList.add('loading-complete')
|
||||
|
||||
// 等待动画完成后再移除元素
|
||||
setTimeout(() => {
|
||||
removeEl('#loading-bg')
|
||||
// 将background属性从html的style中移除
|
||||
document.documentElement.style.removeProperty('background')
|
||||
// 显示页面
|
||||
show.value = true
|
||||
}, 500) // 与CSS动画持续时间匹配
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化data-theme属性
|
||||
updateHtmlThemeAttribute(globalTheme.name.value)
|
||||
|
||||
// 加载背景图片并开始轮换
|
||||
fetchBackgroundImages().then(() => startBackgroundRotation())
|
||||
|
||||
// 添加页面可见性变化监听
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
ensureRenderComplete(() => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
// 移除加载动画
|
||||
removeEl('#loading-bg')
|
||||
// 将background属性从html的style中移除
|
||||
document.documentElement.style.removeProperty('background')
|
||||
// 显示页面
|
||||
show.value = true
|
||||
animateAndRemoveLoader()
|
||||
}, 1500)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)">
|
||||
<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>
|
||||
|
||||
@@ -181,14 +181,14 @@ function fileListUpdated(items: FileItem[]) {
|
||||
// 外层DIV大小控制
|
||||
const scrollStyle = computed(() => {
|
||||
return appMode
|
||||
? 'height: calc(100vh - 10rem - env(safe-area-inset-bottom) - 6rem)'
|
||||
: 'height: calc(100vh - 10rem - env(safe-area-inset-bottom)'
|
||||
? '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) - 6rem)'
|
||||
? 'height: calc(100vh - 14rem - env(safe-area-inset-bottom) - 7rem)'
|
||||
: 'height: calc(100vh - 14rem - env(safe-area-inset-bottom)'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -37,7 +37,7 @@ const getImgUrl = computed(() => {
|
||||
:width="props.width"
|
||||
class="ring-gray-500"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 -translate-y-1 shadow-lg': hover.isHovering,
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
'ring-1': imageLoaded,
|
||||
}"
|
||||
@click="goPlay"
|
||||
|
||||
@@ -158,7 +158,7 @@ onMounted(async () => {
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 -translate-y-1 shadow-lg': hover.isHovering,
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
}"
|
||||
@click="goPlay"
|
||||
>
|
||||
|
||||
@@ -334,10 +334,14 @@ function goMediaDetail(isHovering = false) {
|
||||
// 点击搜索
|
||||
async function clickSearch() {
|
||||
if (allSites.value?.length == 0) {
|
||||
querySites()
|
||||
querySelectedSites()
|
||||
await querySites()
|
||||
await querySelectedSites()
|
||||
}
|
||||
if (allSites.value?.length > 0) {
|
||||
chooseSiteDialog.value = true
|
||||
} else {
|
||||
handleSearch()
|
||||
}
|
||||
chooseSiteDialog.value = true
|
||||
}
|
||||
|
||||
// 开始搜索
|
||||
@@ -429,7 +433,7 @@ function onRemoveSubscribe() {
|
||||
v-bind="hover.props"
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
class="outline-none shadow ring-gray-500 media-card"
|
||||
class="outline-none ring-gray-500 media-card"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
'ring-1': isImageLoaded,
|
||||
@@ -476,7 +480,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>
|
||||
@@ -488,7 +492,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>
|
||||
|
||||
@@ -53,7 +53,6 @@ function replaceNewLine(value: string) {
|
||||
aspect-ratio="3/2"
|
||||
cover
|
||||
position="top"
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
|
||||
@@ -83,7 +83,7 @@ function goPersonDetail() {
|
||||
@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">
|
||||
@@ -117,10 +117,18 @@ function goPersonDetail() {
|
||||
|
||||
<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>
|
||||
|
||||
@@ -156,7 +156,7 @@ const dropdownItems = ref([
|
||||
@click="detailDialog = true"
|
||||
class="flex flex-col h-full"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 -translate-y-1 shadow-lg': hover.isHovering,
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
@@ -185,7 +185,6 @@ const dropdownItems = ref([
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
@@ -246,7 +245,6 @@ const dropdownItems = ref([
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
|
||||
@@ -336,7 +336,7 @@ watch(
|
||||
@click="openPluginDetail"
|
||||
class="flex flex-col h-full"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 -translate-y-1 shadow-lg': hover.isHovering,
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
@@ -364,7 +364,6 @@ watch(
|
||||
:src="iconPath"
|
||||
aspect-ratio="4/3"
|
||||
cover
|
||||
:class="{ shadow: isImageLoaded }"
|
||||
@load="imageLoaded"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
@@ -436,7 +435,7 @@ 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} 更新说明`">
|
||||
<VDialogCloseBtn @click="releaseDialog = false" />
|
||||
<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"
|
||||
class="outline-none ring-gray-500"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 -translate-y-1 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>
|
||||
|
||||
@@ -196,95 +196,92 @@ onMounted(() => {
|
||||
<template>
|
||||
<div>
|
||||
<VCard
|
||||
class="site-card relative h-full flex flex-col overflow-hidden group"
|
||||
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 ? '' : 'inactive',
|
||||
cardProps.site?.is_active ? '' : 'opacity-70',
|
||||
{
|
||||
'status-error': statColor === 'error',
|
||||
'status-warning': statColor === 'warning',
|
||||
'status-success': statColor === 'success',
|
||||
'border-error': statColor === 'error',
|
||||
'border-warning': statColor === 'warning',
|
||||
'border-success': statColor === 'success',
|
||||
},
|
||||
]"
|
||||
:ripple="false"
|
||||
@click="handleResourceBrowse"
|
||||
variant="flat"
|
||||
elevation="0"
|
||||
rounded="lg"
|
||||
hover
|
||||
@click="siteEditDialog = true"
|
||||
>
|
||||
<!-- 装饰性状态指示器 -->
|
||||
<div v-if="cardProps.site?.is_active" class="site-status-indicator" :class="statColor"></div>
|
||||
|
||||
<!-- 主体部分 -->
|
||||
<div class="site-card-content relative flex-1 flex flex-col">
|
||||
<div class="relative flex-1 flex flex-col p-3 z-1">
|
||||
<!-- 顶部:图标和站点名称 -->
|
||||
<div class="flex items-center mb-1">
|
||||
<!-- 站点图标 -->
|
||||
<div class="site-icon-container mr-2.5">
|
||||
<VImg :src="siteIcon" class="site-icon" :alt="cardProps.site?.name">
|
||||
<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-w-1 aspect-h-1" />
|
||||
<VSkeletonLoader class="object-cover aspect-square" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
<div class="site-icon-edit-overlay">
|
||||
<VIcon icon="mdi-drag" color="white" size="24" class="cursor-move" />
|
||||
</div>
|
||||
</div>
|
||||
</VAvatar>
|
||||
|
||||
<!-- 站点名称和特性图标 -->
|
||||
<div class="flex-1 min-w-0 flex items-center">
|
||||
<h3 class="site-title truncate">{{ cardProps.site?.name }}</h3>
|
||||
<h3 class="text-lg font-semibold leading-tight truncate">{{ cardProps.site?.name }}</h3>
|
||||
|
||||
<!-- 站点特性图标 -->
|
||||
<div class="site-features flex items-center gap-1 ml-auto">
|
||||
<div v-if="cardProps.site?.limit_interval" class="feature-icon-wrapper">
|
||||
<VIcon icon="mdi-speedometer" size="16" class="site-feature-icon" />
|
||||
<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 === 1" class="feature-icon-wrapper">
|
||||
<VIcon icon="mdi-network-outline" size="16" class="site-feature-icon" />
|
||||
<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 === 1" class="feature-icon-wrapper">
|
||||
<VIcon icon="mdi-apple-safari" size="16" class="site-feature-icon" />
|
||||
<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="feature-icon-wrapper">
|
||||
<VIcon icon="mdi-filter-cog-outline" size="16" class="site-feature-icon" />
|
||||
<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>
|
||||
|
||||
<!-- 中间部分:网址 -->
|
||||
<div class="site-meta my-3">
|
||||
<div class="site-url truncate" @click.stop="openSitePage">
|
||||
<div class="my-3">
|
||||
<div class="text-sm text-medium-emphasis truncate" @click.stop="openSitePage">
|
||||
{{ cardProps.site?.url }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部:数据统计 -->
|
||||
<div class="site-stats flex-1 flex flex-col justify-end">
|
||||
<div class="flex-1 flex flex-col justify-end">
|
||||
<!-- 更直观的上传下载数据条 -->
|
||||
<div class="data-transfer-stats">
|
||||
<div class="border-t mt-1.5 pt-1.5">
|
||||
<!-- 上传数据 -->
|
||||
<div class="data-row upload-row">
|
||||
<div class="data-label">
|
||||
<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="data-progress-bar">
|
||||
<div class="progress-filled upload-filled" :style="`width: ${getUploadPercent}%`">
|
||||
<div class="progress-glow"></div>
|
||||
</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="data-row download-row">
|
||||
<div class="data-label">
|
||||
<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="data-progress-bar">
|
||||
<div class="progress-filled download-filled" :style="`width: ${getDownloadPercent}%`">
|
||||
<div class="progress-glow"></div>
|
||||
</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>
|
||||
@@ -292,39 +289,54 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作按钮区 -->
|
||||
<div class="site-card-actions">
|
||||
<IconBtn
|
||||
elevation="0"
|
||||
class="site-action-btn test-btn"
|
||||
<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"
|
||||
:class="{ 'testing': testButtonDisable }"
|
||||
>
|
||||
<div class="test-btn-content">
|
||||
<div class="pulse-dot" :class="statColor"></div>
|
||||
<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="loading-overlay">
|
||||
<div class="loading-spinner">
|
||||
<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 class="spinner-circle-dot"></div>
|
||||
</div>
|
||||
<span class="loading-text">测试中</span>
|
||||
</div>
|
||||
</IconBtn>
|
||||
<IconBtn elevation="0" class="site-action-btn" @click.stop="handleSiteUserData">
|
||||
<VIcon icon="mdi-chart-bell-curve" size="18" />
|
||||
</IconBtn>
|
||||
<IconBtn elevation="0" class="site-action-btn" @click.stop="handleSiteUpdate">
|
||||
<VIcon icon="mdi-refresh" size="18" />
|
||||
</IconBtn>
|
||||
<IconBtn elevation="0" class="site-action-btn more-btn">
|
||||
<VIcon icon="mdi-dots-vertical" size="18" />
|
||||
<VMenu activator="parent" close-on-content-click location="left">
|
||||
<VList density="compact" nav class="dropdown-menu">
|
||||
<VListItem @click="siteEditDialog = true" base-color="info">
|
||||
</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 @click="handleResourceBrowse" base-color="info">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-file-edit-outline" size="small" />
|
||||
<VIcon icon="mdi-web" size="small" />
|
||||
</template>
|
||||
<VListItemTitle>编辑站点</VListItemTitle>
|
||||
<VListItemTitle>浏览资源</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem @click="deleteSiteInfo">
|
||||
<template #prepend>
|
||||
@@ -334,8 +346,8 @@ onMounted(() => {
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</VBtn>
|
||||
</VSheet>
|
||||
</VCard>
|
||||
|
||||
<!-- 对话框组件 -->
|
||||
@@ -370,36 +382,16 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.site-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
background: rgba(var(--v-theme-surface), 0.95);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.site-card:hover {
|
||||
border: 1px solid rgba(var(--v-theme-primary), 0.2);
|
||||
box-shadow: 0 3px 12px -6px rgba(0, 0, 0, 10%);
|
||||
transform: translateY(-4px);
|
||||
.site-card-actions {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.inactive {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.site-card-content {
|
||||
z-index: 1;
|
||||
padding-block: 10px;
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
/* 站点状态指示器 - 更精致的渐变指示 */
|
||||
.site-status-indicator {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
block-size: 4px;
|
||||
block-size: 2px;
|
||||
inset-block-start: 0;
|
||||
inset-inline: 0;
|
||||
opacity: 0.5;
|
||||
@@ -432,403 +424,40 @@ onMounted(() => {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 拖动手柄 */
|
||||
.drag-handle {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 数据显示相关样式 */
|
||||
.data-transfer-stats {
|
||||
border-block-start: 1px solid rgba(var(--v-theme-on-surface), 0.05);
|
||||
margin-block-start: 6px;
|
||||
padding-block-start: 6px;
|
||||
}
|
||||
|
||||
.data-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-block-end: 6px;
|
||||
}
|
||||
|
||||
.data-row:last-child {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.data-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(var(--v-theme-on-surface), 0.8);
|
||||
font-size: 0.8rem;
|
||||
min-inline-size: 70px;
|
||||
}
|
||||
|
||||
.data-progress-bar {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
border-radius: 4px;
|
||||
background: rgba(var(--v-theme-on-surface), 0.08);
|
||||
block-size: 4px;
|
||||
}
|
||||
|
||||
.progress-filled {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
block-size: 100%;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
min-inline-size: 3px;
|
||||
transition: inline-size 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.upload-filled {
|
||||
animation: pulse-width 2s infinite;
|
||||
/* 上传下载条样式 */
|
||||
.upload-bar {
|
||||
background: linear-gradient(90deg, #4d79ff, #07f);
|
||||
box-shadow: 0 0 4px rgba(0, 119, 255, 50%);
|
||||
animation: pulse-width 2s infinite;
|
||||
}
|
||||
|
||||
.download-filled {
|
||||
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;
|
||||
}
|
||||
|
||||
.progress-glow {
|
||||
position: absolute;
|
||||
animation: shimmer 1.5s linear infinite;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 50%), transparent);
|
||||
background-size: 200% 100%;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
@keyframes pulse-width {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -100% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 速度等级样式 */
|
||||
.speed-idle {
|
||||
animation: none !important;
|
||||
inline-size: 5% !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.speed-low {
|
||||
animation-duration: 6s !important;
|
||||
inline-size: 30% !important;
|
||||
}
|
||||
|
||||
.speed-medium {
|
||||
animation-duration: 4s !important;
|
||||
inline-size: 50% !important;
|
||||
}
|
||||
|
||||
.speed-high {
|
||||
animation-duration: 2s !important;
|
||||
inline-size: 70% !important;
|
||||
}
|
||||
|
||||
@keyframes pulse-width {
|
||||
0%,
|
||||
100% {
|
||||
transform: scaleX(0.95);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scaleX(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 站点图标 */
|
||||
.site-icon-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
block-size: 38px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 6%);
|
||||
cursor: pointer;
|
||||
inline-size: 38px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.site-icon-container:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.site-icon {
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.site-icon-edit-overlay {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 50%);
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.site-icon-container:hover .site-icon-edit-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 站点标题 */
|
||||
.site-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 站点网址 */
|
||||
.site-url {
|
||||
color: rgba(var(--v-theme-on-surface), 0.6);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.site-url:hover {
|
||||
color: rgba(var(--v-theme-primary), 0.9);
|
||||
}
|
||||
|
||||
/* 站点特性图标 */
|
||||
.site-feature-icon {
|
||||
color: rgba(var(--v-theme-primary), 0.95);
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 5%));
|
||||
margin-block: 0;
|
||||
margin-inline: 1px;
|
||||
opacity: 0.85;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.site-feature-icon:hover {
|
||||
filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 10%));
|
||||
opacity: 1;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 特性标签 */
|
||||
.site-features {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
/* 数据统计 */
|
||||
.site-stats {
|
||||
margin-block-start: auto;
|
||||
}
|
||||
|
||||
.site-data-values {
|
||||
color: rgba(var(--v-theme-on-surface), 0.8);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.site-data-bar {
|
||||
overflow: hidden;
|
||||
border-radius: 1.5px;
|
||||
block-size: 3px;
|
||||
}
|
||||
|
||||
.site-data-bar-bg {
|
||||
position: absolute;
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.05);
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.site-data-bar-upload {
|
||||
background-color: rgba(var(--v-theme-info), 0.4);
|
||||
}
|
||||
|
||||
.site-data-bar-download {
|
||||
background-color: rgba(var(--v-theme-success), 0.4);
|
||||
}
|
||||
|
||||
/* 状态样式 */
|
||||
.status-error {
|
||||
border-color: rgba(var(--v-theme-error), 0.2);
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
border-color: rgba(var(--v-theme-warning), 0.2);
|
||||
}
|
||||
|
||||
.status-success {
|
||||
border-color: rgba(var(--v-theme-success), 0.2);
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.site-card-actions {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgba(var(--v-theme-surface), 0.97);
|
||||
border-inline-start: 1px solid rgba(var(--v-theme-on-surface), 0.06);
|
||||
inset-block: 0;
|
||||
inset-inline-end: 0;
|
||||
padding-block: 8px;
|
||||
padding-inline: 4px;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* 测试按钮特殊样式 */
|
||||
.test-btn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border-radius: 50% !important;
|
||||
block-size: 40px !important;
|
||||
inline-size: 40px !important;
|
||||
margin-block-end: 12px;
|
||||
min-inline-size: 40px;
|
||||
}
|
||||
|
||||
.test-btn-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
animation: fade-in 0.2s ease;
|
||||
background: rgba(var(--v-theme-surface), 0.95);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 10%);
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
position: relative;
|
||||
block-size: 24px;
|
||||
inline-size: 24px;
|
||||
}
|
||||
|
||||
.spinner-circle {
|
||||
position: absolute;
|
||||
border: 2px solid rgba(var(--v-theme-primary), 0.2);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
block-size: 100%;
|
||||
border-block-start-color: rgba(var(--v-theme-primary), 1);
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.spinner-circle-dot {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite reverse;
|
||||
background-color: rgba(var(--v-theme-primary), 1);
|
||||
block-size: 4px;
|
||||
inline-size: 4px;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 50%;
|
||||
margin-block-start: -2px;
|
||||
margin-inline-start: -2px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
position: absolute;
|
||||
color: rgba(var(--v-theme-primary), 1);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
inset-block-end: -20px;
|
||||
margin-block-start: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
block-size: 22px;
|
||||
box-shadow: inset 0 0 0 2px rgba(var(--v-theme-on-surface), 0.1);
|
||||
inline-size: 22px;
|
||||
}
|
||||
|
||||
/* 测试状态点样式 */
|
||||
.pulse-dot::before {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
border-radius: 50%;
|
||||
block-size: 70%;
|
||||
content: '';
|
||||
inline-size: 70%;
|
||||
inset-block-start: 15%;
|
||||
inset-inline-start: 15%;
|
||||
height: 70%;
|
||||
width: 70%;
|
||||
top: 15%;
|
||||
left: 15%;
|
||||
}
|
||||
|
||||
.pulse-dot::after {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
border-radius: 50%;
|
||||
block-size: 100%;
|
||||
content: '';
|
||||
inline-size: 100%;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.pulse-dot.error::before {
|
||||
@@ -871,15 +500,37 @@ onMounted(() => {
|
||||
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);
|
||||
}
|
||||
@@ -889,11 +540,9 @@ onMounted(() => {
|
||||
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);
|
||||
}
|
||||
@@ -903,11 +552,9 @@ onMounted(() => {
|
||||
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);
|
||||
}
|
||||
@@ -917,96 +564,29 @@ onMounted(() => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.site-card:hover .site-card-actions {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.site-action-btn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(var(--v-theme-surface), 1);
|
||||
block-size: 32px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 5%);
|
||||
color: rgba(var(--v-theme-on-surface), 0.8);
|
||||
cursor: pointer;
|
||||
inline-size: 36px;
|
||||
margin-block-end: 4px;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.site-action-btn::before {
|
||||
position: absolute;
|
||||
background: radial-gradient(circle at center, rgba(var(--v-theme-primary), 0.1), transparent 70%);
|
||||
content: '';
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.site-action-btn:hover {
|
||||
background-color: white;
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 10%);
|
||||
color: rgba(var(--v-theme-primary), 1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.site-action-btn:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.site-action-btn.animate-pulse {
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
@keyframes spin {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--v-theme-primary), 0.4);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
70% {
|
||||
box-shadow: 0 0 0 6px rgba(var(--v-theme-primary), 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--v-theme-primary), 0);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.site-action-btn.more-btn {
|
||||
margin-block: auto 0;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.feature-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
block-size: 24px;
|
||||
inline-size: 24px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.feature-icon-wrapper:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -298,7 +298,7 @@ function onSubscribeEditRemove() {
|
||||
class="flex flex-col h-full"
|
||||
:class="{
|
||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||
'transition transform-cpu duration-300 -translate-y-1 shadow-lg': hover.isHovering,
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
'opacity-70': subscribeState === 'S',
|
||||
}"
|
||||
min-height="170"
|
||||
@@ -335,10 +335,7 @@ function onSubscribeEditRemove() {
|
||||
</template>
|
||||
<div>
|
||||
<VCardText class="flex items-center py-3">
|
||||
<div
|
||||
class="h-auto w-16 flex-shrink-0 overflow-hidden rounded-md shadow-lg cursor-move"
|
||||
v-if="imageLoaded"
|
||||
>
|
||||
<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">
|
||||
|
||||
@@ -102,7 +102,7 @@ function doDelete() {
|
||||
:key="props.media?.id"
|
||||
class="flex flex-col h-full"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 -translate-y-1 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">
|
||||
|
||||
@@ -81,10 +81,19 @@ async function downloadTorrentFile() {
|
||||
|
||||
// 获取优惠类型样式
|
||||
function getPromotionClass(downloadVolumeFactor: number | undefined, uploadVolumeFactor: number | undefined) {
|
||||
if (!downloadVolumeFactor) return 'free-discount'
|
||||
if (downloadVolumeFactor === 0) return 'free-discount'
|
||||
else if (downloadVolumeFactor < 1) return 'percent-discount'
|
||||
else if (uploadVolumeFactor !== undefined && uploadVolumeFactor > 1) return 'upload-bonus'
|
||||
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 ''
|
||||
}
|
||||
|
||||
@@ -108,112 +117,155 @@ onMounted(() => {
|
||||
:width="props.width || '100%'"
|
||||
:variant="downloaded.includes(torrent?.enclosure || '') ? 'outlined' : 'flat'"
|
||||
@click="handleAddDownload(props.torrent)"
|
||||
class="torrent-card h-full"
|
||||
:class="{ 'downloaded-card': downloaded.includes(torrent?.enclosure || '') }"
|
||||
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
|
||||
>
|
||||
<!-- 优惠标签 -->
|
||||
<div
|
||||
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
|
||||
class="discount-banner"
|
||||
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>
|
||||
|
||||
<!-- 媒体标题 -->
|
||||
<div class="card-header">
|
||||
<div class="media-title-wrapper flex flex-row flex-wrap justify-start">
|
||||
<span class="media-title me-2">
|
||||
<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>
|
||||
<span v-if="meta?.season_episode" class="season-tag">{{ meta?.season_episode }}</span>
|
||||
<VChip
|
||||
v-if="meta?.season_episode"
|
||||
class="chip-season rounded-sm font-weight-bold"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
>
|
||||
{{ meta?.season_episode }}
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<!-- 站点信息条 -->
|
||||
<div class="site-info">
|
||||
<div class="d-flex justify-space-between align-center flex-wrap">
|
||||
<div class="d-flex align-center">
|
||||
<img
|
||||
:alt="torrent?.site_name"
|
||||
<VImg
|
||||
v-if="siteIcons[torrent?.site || 0]"
|
||||
:src="siteIcons[torrent?.site || 0]"
|
||||
class="site-icon"
|
||||
:alt="torrent?.site_name"
|
||||
class="mr-2 rounded"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
<span v-else class="site-fallback">{{ torrent?.site_name?.substring(0, 1) }}</span>
|
||||
<span class="site-name">{{ torrent?.site_name }}</span>
|
||||
<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="seeder-peers">
|
||||
<span v-if="torrent?.seeders" class="seed-info">
|
||||
<VIcon size="small" color="success" icon="mdi-arrow-up"></VIcon>{{ torrent?.seeders }}
|
||||
<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="peer-info">
|
||||
<VIcon size="small" color="warning" icon="mdi-arrow-down"></VIcon>{{ torrent?.peers }}
|
||||
<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>
|
||||
</div>
|
||||
</VCardItem>
|
||||
|
||||
<!-- 种子内容 -->
|
||||
<div class="card-content">
|
||||
<VCardText class="d-flex flex-column flex-grow-1 pa-3 overflow-hidden">
|
||||
<!-- 种子标题 -->
|
||||
<div class="torrent-title" :title="torrent?.title">
|
||||
<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="torrent-desc grow"
|
||||
class="text-body-2 text-medium-emphasis mb-2"
|
||||
:title="meta?.subtitle || torrent?.description"
|
||||
>
|
||||
{{ meta?.subtitle || torrent?.description }}
|
||||
</div>
|
||||
|
||||
<!-- 资源标签区 -->
|
||||
<div class="tags-container">
|
||||
<div v-if="meta?.edition" class="resource-tag edition">{{ meta?.edition }}</div>
|
||||
<div v-if="meta?.resource_pix" class="resource-tag resolution">{{ meta?.resource_pix }}</div>
|
||||
<div v-if="meta?.video_encode" class="resource-tag codec">{{ meta?.video_encode }}</div>
|
||||
<div v-if="meta?.resource_team" class="resource-tag team">{{ meta?.resource_team }}</div>
|
||||
<div v-for="(label, index) in torrent?.labels" :key="index" class="resource-tag label">{{ label }}</div>
|
||||
<div v-if="torrent?.hit_and_run" class="resource-tag hr">H&R</div>
|
||||
<div v-if="torrent?.freedate_diff" class="resource-tag expire">{{ torrent?.freedate_diff }}</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>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<!-- 卡片底部信息 -->
|
||||
<div class="card-footer">
|
||||
<div class="more-sources-wrapper" v-if="props.more && props.more.length > 0">
|
||||
<div class="more-sources-toggle" @click.stop="openMoreTorrentsDialog">
|
||||
<VIcon :icon="showMoreTorrents ? 'mdi-chevron-up' : 'mdi-chevron-down'" size="small" class="me-1"></VIcon>
|
||||
<span>更多来源 ({{ props.more.length }})</span>
|
||||
</div>
|
||||
<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="card-actions">
|
||||
<div v-if="torrent?.size" class="size-badge">
|
||||
<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) }}
|
||||
</div>
|
||||
|
||||
<VBtn
|
||||
size="small"
|
||||
icon="mdi-information-outline"
|
||||
variant="text"
|
||||
color="primary"
|
||||
class="detail-btn"
|
||||
@click.stop="openTorrentDetail"
|
||||
></VBtn>
|
||||
</VChip>
|
||||
<VBtn icon size="small" variant="text" color="primary" @click.stop="openTorrentDetail">
|
||||
<VIcon icon="mdi-information-outline"></VIcon>
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
|
||||
<!-- 更多来源对话框 - 改为独立对话框 -->
|
||||
<VDialog v-model="showMoreTorrents" max-width="380px" location="center">
|
||||
<!-- 更多来源对话框 -->
|
||||
<VDialog v-model="showMoreTorrents" max-width="25rem" location="center">
|
||||
<VCard>
|
||||
<VCardTitle class="py-2 d-flex align-center">
|
||||
<VCardTitle class="py-3 d-flex align-center">
|
||||
<span>其他来源</span>
|
||||
<VSpacer />
|
||||
<VBtn variant="text" size="small" icon="mdi-close" @click.stop="showMoreTorrents = false"></VBtn>
|
||||
@@ -221,45 +273,77 @@ onMounted(() => {
|
||||
|
||||
<VDivider />
|
||||
|
||||
<VCardText class="more-sources-content">
|
||||
<div
|
||||
v-for="(item, index) in props.more"
|
||||
:key="index"
|
||||
@click.stop="handleAddDownload(item)"
|
||||
class="more-source-item cursor-pointer"
|
||||
>
|
||||
<div class="source-site-info">
|
||||
<img
|
||||
:alt="item.torrent_info?.site_name"
|
||||
v-if="siteIcons[item.torrent_info?.site || 0]"
|
||||
:src="siteIcons[item.torrent_info?.site || 0]"
|
||||
class="source-site-icon"
|
||||
/>
|
||||
<span v-else class="source-site-fallback">{{ item.torrent_info?.site_name?.substring(0, 1) }}</span>
|
||||
<span class="source-site-name">{{ item.torrent_info.site_name }}</span>
|
||||
<span v-if="item.meta_info?.season_episode" class="season-tag source-season-tag">
|
||||
{{ item.meta_info.season_episode }}
|
||||
</span>
|
||||
<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>
|
||||
|
||||
<span
|
||||
v-if="item.torrent_info?.downloadvolumefactor !== 1 || item.torrent_info?.uploadvolumefactor !== 1"
|
||||
class="source-discount"
|
||||
:class="
|
||||
getPromotionClass(item.torrent_info?.downloadvolumefactor, item.torrent_info?.uploadvolumefactor)
|
||||
"
|
||||
>
|
||||
{{ item.torrent_info?.volume_factor }}
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div class="source-stats">
|
||||
<span class="source-size">{{ formatFileSize(item.torrent_info?.size) }}</span>
|
||||
<span class="source-seeders">
|
||||
<VIcon size="x-small" color="success" icon="mdi-arrow-up"></VIcon>
|
||||
{{ item.torrent_info?.seeders }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
@@ -280,381 +364,91 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.torrent-card {
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
box-shadow: none;
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.torrent-card:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: rgba(var(--v-theme-primary), 0.3);
|
||||
}
|
||||
|
||||
.discount-banner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: white;
|
||||
padding: 4px 10px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
border-radius: 0 0 0 12px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.free-discount {
|
||||
background-color: #4caf50;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.percent-discount {
|
||||
background-color: #ff5722;
|
||||
}
|
||||
|
||||
.upload-bonus {
|
||||
background-color: #9c27b0;
|
||||
}
|
||||
|
||||
.size-badge {
|
||||
background-color: rgba(var(--v-theme-primary), 0.9);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
border-radius: 4px;
|
||||
margin-right: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 12px 16px 0;
|
||||
}
|
||||
|
||||
.media-title-wrapper {
|
||||
margin-bottom: 8px;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.media-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
color: rgba(var(--v-theme-on-surface), 0.87);
|
||||
}
|
||||
|
||||
.season-tag {
|
||||
font-size: 0.875rem;
|
||||
background-color: #5c6bc0;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.site-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.site-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.site-fallback {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
font-weight: 700;
|
||||
color: rgba(var(--v-theme-on-surface), 0.8);
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.site-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: rgba(var(--v-theme-on-surface), 0.85);
|
||||
}
|
||||
|
||||
.seeder-peers {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.seed-info {
|
||||
font-size: 0.95rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.peer-info {
|
||||
font-size: 0.95rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.torrent-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: rgba(var(--v-theme-on-surface), 0.87);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.torrent-desc {
|
||||
font-size: 0.85rem;
|
||||
color: rgba(var(--v-theme-on-surface), 0.6);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.resource-tag {
|
||||
font-size: 0.8rem;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.edition {
|
||||
background-color: #f44336;
|
||||
}
|
||||
|
||||
.resolution {
|
||||
background-color: #e91e63;
|
||||
}
|
||||
|
||||
.codec {
|
||||
background-color: #ff9800;
|
||||
}
|
||||
|
||||
.team {
|
||||
background-color: #03a9f4;
|
||||
}
|
||||
|
||||
.expire {
|
||||
background-color: #9c27b0;
|
||||
}
|
||||
|
||||
.label {
|
||||
background-color: #3f51b5;
|
||||
}
|
||||
|
||||
.hr {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.more-sources-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.more-sources-toggle {
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--v-theme-primary));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.more-sources-toggle:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||
inset-block-start: 0;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
.more-sources-content {
|
||||
max-height: 60vh;
|
||||
max-block-size: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.more-source-item {
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s;
|
||||
border-bottom: 1px solid rgba(var(--v-theme-on-surface), 0.05);
|
||||
/* 卡片悬停效果 */
|
||||
.torrent-card {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.more-source-item:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.05);
|
||||
.torrent-card:hover {
|
||||
border-color: rgba(var(--v-theme-primary), 0.3);
|
||||
}
|
||||
|
||||
.source-site-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
/* 优惠标签样式 */
|
||||
.bg-success {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.source-site-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
.bg-orange {
|
||||
background-color: #ff5722;
|
||||
}
|
||||
|
||||
.source-site-fallback {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 0.7rem;
|
||||
color: rgba(var(--v-theme-on-surface), 0.8);
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.1);
|
||||
border-radius: 2px;
|
||||
.bg-purple {
|
||||
background-color: #9c27b0;
|
||||
}
|
||||
|
||||
.source-site-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.source-season-tag {
|
||||
font-size: 0.75rem;
|
||||
padding: 1px 4px;
|
||||
margin-left: 4px;
|
||||
background-color: #5c6bc0;
|
||||
}
|
||||
|
||||
.source-discount {
|
||||
font-weight: 700;
|
||||
font-size: 0.8rem;
|
||||
margin-left: 6px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
.chip-season {
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.source-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
.chip-edition {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.source-size {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--v-theme-primary));
|
||||
.chip-resolution {
|
||||
background-color: #7b1fa2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.source-seeders {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
.chip-codec {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.chip-team {
|
||||
background-color: #00897b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
border-radius: 50%;
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
.chip-label {
|
||||
background-color: #5c6bc0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.downloaded-card {
|
||||
border: 2px solid #4caf50 !important;
|
||||
opacity: 0.85;
|
||||
.chip-hr {
|
||||
background-color: #212121;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.resource-tag {
|
||||
font-size: 0.75rem;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
.chip-expire {
|
||||
background-color: #7e57c2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.full-text {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
.chip-free {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menu-activator {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
.chip-discount {
|
||||
background-color: #ff5722;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.break-words {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.overflow-visible {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.whitespace-break-spaces {
|
||||
white-space: normal !important;
|
||||
.chip-bonus {
|
||||
background-color: #9c27b0;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -58,10 +58,19 @@ async function getSiteIcon() {
|
||||
|
||||
// 获取优惠类型样式
|
||||
function getPromotionClass(downloadVolumeFactor: number | undefined, uploadVolumeFactor: number | undefined) {
|
||||
if (!downloadVolumeFactor) return 'free-discount'
|
||||
if (downloadVolumeFactor === 0) return 'free-discount'
|
||||
else if (downloadVolumeFactor < 1) return 'percent-discount'
|
||||
else if (uploadVolumeFactor !== undefined && uploadVolumeFactor > 1) return 'upload-bonus'
|
||||
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 ''
|
||||
}
|
||||
|
||||
@@ -95,80 +104,112 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="list-item-wrapper">
|
||||
<div class="w-100">
|
||||
<VListItem
|
||||
:value="props.torrent?.torrent_info?.enclosure"
|
||||
class="torrent-item rounded"
|
||||
:class="{ 'downloaded-item': downloaded.includes(torrent?.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"
|
||||
>
|
||||
<!-- 优惠标签 -->
|
||||
<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 v-slot:prepend>
|
||||
<div class="site-wrapper">
|
||||
<img :alt="torrent?.site_name" v-if="siteIcon" :src="siteIcon" class="site-icon" />
|
||||
<span v-else class="site-fallback">{{ torrent?.site_name?.substring(0, 1) }}</span>
|
||||
<div class="site-name d-none d-sm-block">{{ torrent?.site_name }}</div>
|
||||
<span
|
||||
v-if="torrent?.downloadvolumefactor !== 1 || torrent?.uploadvolumefactor !== 1"
|
||||
class="free-tag"
|
||||
:class="getPromotionClass(torrent?.downloadvolumefactor, torrent?.uploadvolumefactor)"
|
||||
>
|
||||
{{ torrent?.volume_factor }}
|
||||
</span>
|
||||
<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 class="item-content">
|
||||
<div class="item-header">
|
||||
<div class="media-info flex flex-row flex-wrap justify-start">
|
||||
<span class="media-title me-2">{{ media?.title ?? meta?.name }}</span>
|
||||
<span v-if="meta?.season_episode" class="season-tag">{{ meta?.season_episode }}</span>
|
||||
</div>
|
||||
<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="torrent-title" :title="torrent?.title">
|
||||
<div class="text-subtitle-2 font-weight-medium mb-2" :title="torrent?.title">
|
||||
{{ torrent?.title }}
|
||||
</div>
|
||||
|
||||
<div class="torrent-description" :title="meta?.subtitle || torrent?.description || '暂无描述'">
|
||||
<div
|
||||
class="text-body-2 text-medium-emphasis mb-2"
|
||||
:title="meta?.subtitle || torrent?.description || '暂无描述'"
|
||||
>
|
||||
{{ meta?.subtitle || torrent?.description || '暂无描述' }}
|
||||
</div>
|
||||
|
||||
<div class="tags-container">
|
||||
<div v-if="meta?.edition" class="resource-tag edition">{{ meta?.edition }}</div>
|
||||
<div v-if="meta?.resource_pix" class="resource-tag resolution">{{ meta?.resource_pix }}</div>
|
||||
<div v-if="meta?.video_encode" class="resource-tag codec">{{ meta?.video_encode }}</div>
|
||||
<div v-if="meta?.resource_team" class="resource-tag team">{{ meta?.resource_team }}</div>
|
||||
<div v-for="(label, index) in torrent?.labels" :key="index" class="resource-tag label">{{ label }}</div>
|
||||
<div v-if="torrent?.hit_and_run" class="resource-tag hr">H&R</div>
|
||||
<div v-if="torrent?.freedate_diff" class="resource-tag expire">{{ torrent?.freedate_diff }}</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="item-actions">
|
||||
<div class="torrent-stats">
|
||||
<span v-if="torrent?.seeders" class="seed-info">
|
||||
<VIcon size="small" color="success" icon="mdi-arrow-up"></VIcon>{{ torrent?.seeders }}
|
||||
<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="peer-info">
|
||||
<VIcon size="small" color="warning" icon="mdi-arrow-down"></VIcon>{{ torrent?.peers }}
|
||||
<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="action-buttons">
|
||||
<div v-if="torrent?.size" class="size-badge">
|
||||
<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) }}
|
||||
</div>
|
||||
</VChip>
|
||||
|
||||
<VBtn
|
||||
density="comfortable"
|
||||
variant="text"
|
||||
color="primary"
|
||||
icon="mdi-information-outline"
|
||||
size="small"
|
||||
class="detail-btn"
|
||||
@click.stop="openTorrentDetail"
|
||||
></VBtn>
|
||||
<VBtn icon size="small" variant="text" color="primary" @click.stop="openTorrentDetail">
|
||||
<VIcon icon="mdi-information-outline"></VIcon>
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -188,293 +229,86 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.list-item-wrapper {
|
||||
inline-size: 100%;
|
||||
.discount-banner {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
inset-block-start: 0;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
.torrent-item {
|
||||
padding: 12px;
|
||||
box-shadow: none;
|
||||
margin-block-end: 8px;
|
||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.torrent-item:hover {
|
||||
border-color: rgba(var(--v-theme-primary), 0.3);
|
||||
background-color: rgba(var(--v-theme-primary), 0.04);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.site-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
min-inline-size: 100px;
|
||||
.chip-season {
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.site-icon {
|
||||
border-radius: 4px;
|
||||
block-size: 32px;
|
||||
inline-size: 32px;
|
||||
margin-inline-end: 8px;
|
||||
.chip-edition {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.site-fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
block-size: 24px;
|
||||
color: rgb(var(--v-theme-primary));
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
inline-size: 24px;
|
||||
margin-inline-end: 8px;
|
||||
.chip-resolution {
|
||||
background-color: #7b1fa2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.site-name {
|
||||
color: rgba(var(--v-theme-on-surface), 0.8);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
margin-inline-end: 8px;
|
||||
.chip-codec {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.season-tag {
|
||||
z-index: 2;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
.chip-team {
|
||||
background-color: #00897b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-label {
|
||||
background-color: #5c6bc0;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-inline-end: 8px;
|
||||
padding-block: 2px;
|
||||
padding-inline: 6px;
|
||||
}
|
||||
|
||||
.free-tag {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
border-radius: 4px;
|
||||
.chip-hr {
|
||||
background-color: #212121;
|
||||
color: white;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
inset-block-start: 0;
|
||||
inset-inline-end: 0;
|
||||
padding-block: 2px;
|
||||
padding-inline: 6px;
|
||||
}
|
||||
|
||||
.free-discount {
|
||||
.chip-expire {
|
||||
background-color: #7e57c2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 优惠标签样式 */
|
||||
.bg-success {
|
||||
background-color: #4caf50;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.percent-discount {
|
||||
.bg-orange {
|
||||
background-color: #ff5722;
|
||||
}
|
||||
|
||||
.upload-bonus {
|
||||
.bg-purple {
|
||||
background-color: #9c27b0;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.media-info {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.media-title {
|
||||
color: rgba(var(--v-theme-on-surface), 0.87);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.torrent-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.seed-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.peer-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.size-badge {
|
||||
border-radius: 4px;
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
color: rgb(var(--v-theme-primary));
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
margin-inline-end: 6px;
|
||||
padding-block: 2px;
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
.torrent-title {
|
||||
overflow: hidden;
|
||||
color: rgba(var(--v-theme-on-surface), 0.87);
|
||||
font-size: 0.9rem;
|
||||
margin-block-end: 6px;
|
||||
max-inline-size: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.torrent-description {
|
||||
overflow: hidden;
|
||||
color: rgba(var(--v-theme-on-surface), 0.65);
|
||||
font-size: 0.8rem;
|
||||
inline-size: 100%;
|
||||
margin-block-end: 8px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.resource-tag {
|
||||
border-radius: 4px;
|
||||
.chip-free {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
padding-block: 3px;
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
.edition {
|
||||
background-color: #f44336;
|
||||
.chip-discount {
|
||||
background-color: #ff5722;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.resolution {
|
||||
background-color: #e91e63;
|
||||
}
|
||||
|
||||
.codec {
|
||||
background-color: #ff9800;
|
||||
}
|
||||
|
||||
.team {
|
||||
background-color: #03a9f4;
|
||||
}
|
||||
|
||||
.expire {
|
||||
.chip-bonus {
|
||||
background-color: #9c27b0;
|
||||
}
|
||||
|
||||
.label {
|
||||
background-color: #3f51b5;
|
||||
}
|
||||
|
||||
.hr {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.downloaded-item {
|
||||
border-inline-start: 4px solid #4caf50;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.break-words {
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.overflow-visible {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.whitespace-break-spaces {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.torrent-item {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.site-icon,
|
||||
.site-fallback {
|
||||
block-size: 24px;
|
||||
inline-size: 24px;
|
||||
}
|
||||
|
||||
.site-wrapper {
|
||||
flex-wrap: wrap;
|
||||
margin-inline-end: 10px;
|
||||
min-inline-size: 24px;
|
||||
}
|
||||
|
||||
.site-name {
|
||||
font-size: 0.8rem;
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
|
||||
.size-badge {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.resource-tag {
|
||||
font-size: 0.75rem;
|
||||
padding-block: 2px;
|
||||
padding-inline: 6px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.torrent-description {
|
||||
max-inline-size: calc(100vw - 150px);
|
||||
}
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 {
|
||||
@@ -26,6 +27,9 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const display = useDisplay()
|
||||
const isMobile = computed(() => display.mdAndDown.value)
|
||||
|
||||
// 当前用户的ID
|
||||
const currentLoginUserId = computed(() => useUserStore().userID)
|
||||
|
||||
@@ -50,15 +54,6 @@ const movieSubscriptions = ref(0)
|
||||
// 用户电视剧订阅数量
|
||||
const tvShowSubscriptions = ref(0)
|
||||
|
||||
// 是否显示更多操作菜单
|
||||
const showMenu = ref(false)
|
||||
|
||||
// 鼠标悬停状态
|
||||
const isHovered = ref(false)
|
||||
|
||||
// 是否为移动设备
|
||||
const isMobile = ref(window.innerWidth < 600)
|
||||
|
||||
// 显示名称 - 如果有昵称则优先显示昵称
|
||||
const displayName = computed(() => {
|
||||
const settingsNickname = props.user.settings?.nickname as string | undefined
|
||||
@@ -66,13 +61,6 @@ const displayName = computed(() => {
|
||||
return nickname || props.user.name
|
||||
})
|
||||
|
||||
// 计算用户卡片状态类
|
||||
const cardStatusClass = computed(() => {
|
||||
if (!props.user.is_active) return 'user-card-inactive'
|
||||
if (props.user.is_superuser) return 'user-card-admin'
|
||||
return ''
|
||||
})
|
||||
|
||||
// 按用户查询订阅数量
|
||||
async function fetchSubscriptions() {
|
||||
try {
|
||||
@@ -121,147 +109,165 @@ function onUserUpdate() {
|
||||
emit('save')
|
||||
}
|
||||
|
||||
// 更新窗口大小监听
|
||||
function handleResize() {
|
||||
isMobile.value = window.innerWidth < 600
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSubscriptions()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VCard
|
||||
class="user-card"
|
||||
:class="[{ 'user-card-hover': isHovered }, cardStatusClass, { 'mobile-card': isMobile }]"
|
||||
@mouseenter="isHovered = true"
|
||||
@mouseleave="isHovered = false"
|
||||
:class="[
|
||||
'transition-transform duration-300 hover:-translate-y-1',
|
||||
!props.user.is_active ? 'opacity-85 bg-surface-lighten-1' : '',
|
||||
]"
|
||||
@click="userEditDialog = true"
|
||||
>
|
||||
<!-- 管理员卡片装饰 -->
|
||||
<div v-if="user.is_superuser" class="admin-decoration">
|
||||
<div class="decoration-line"></div>
|
||||
<div class="decoration-circle"><VIcon icon="mdi-shield-star" size="x-small" color="warning" /></div>
|
||||
<div class="decoration-line"></div>
|
||||
</div>
|
||||
|
||||
<!-- 用户头像和基本信息 -->
|
||||
<div class="user-card-header" :class="{ 'admin-header': user.is_superuser }">
|
||||
<div class="user-avatar-container">
|
||||
<VAvatar
|
||||
:size="isMobile ? 50 : 74"
|
||||
rounded="lg"
|
||||
class="user-avatar"
|
||||
:class="{ 'admin-avatar': user.is_superuser, 'inactive-avatar': !user.is_active }"
|
||||
>
|
||||
<VImg :src="user.avatar || avatar1" :alt="user.name" />
|
||||
<div v-if="!user.is_active" class="avatar-overlay">
|
||||
<VIcon icon="mdi-account-lock" color="white" size="small" />
|
||||
<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>
|
||||
</VAvatar>
|
||||
<div v-if="user.is_superuser" class="admin-crown">
|
||||
<VIcon icon="mdi-crown" color="warning" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="user-info">
|
||||
<div class="user-name-section">
|
||||
<div class="name-and-badges">
|
||||
<h3 class="user-name" :class="{ 'admin-name': user.is_superuser, 'inactive-name': !user.is_active }">
|
||||
<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="nickname-icon"
|
||||
class="animate-pulse"
|
||||
/>
|
||||
</h3>
|
||||
<div class="user-badges">
|
||||
<VChip v-if="user.is_superuser" size="x-small" color="error" class="user-badge admin-badge">管理员</VChip>
|
||||
<VChip v-else size="x-small" color="default" class="user-badge">普通用户</VChip>
|
||||
<VChip size="x-small" :color="user.is_active ? 'success' : 'grey'" variant="tonal" class="user-badge">
|
||||
{{ user.is_active ? '激活' : '已停用' }}
|
||||
</VChip>
|
||||
<VChip v-if="user.is_otp" size="x-small" color="info" variant="tonal" class="user-badge"> 2FA </VChip>
|
||||
</div>
|
||||
</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="mobile-stats">
|
||||
<div class="mobile-stat-item">
|
||||
<VIcon size="x-small" icon="mdi-movie-outline" color="primary" />
|
||||
<span>{{ movieSubscriptions }}</span>
|
||||
<div v-if="isMobile" class="d-flex gap-5 mt-2">
|
||||
<div class="d-flex align-center">
|
||||
<VIcon size="x-small" icon="mdi-movie-outline" color="primary" class="mr-1" />
|
||||
<span class="text-body-2">{{ movieSubscriptions }}</span>
|
||||
</div>
|
||||
<div class="mobile-stat-item">
|
||||
<VIcon size="x-small" icon="mdi-television-classic" color="primary" />
|
||||
<span>{{ tvShowSubscriptions }}</span>
|
||||
<div class="d-flex align-center">
|
||||
<VIcon size="x-small" icon="mdi-television-classic" color="primary" class="mr-1" />
|
||||
<span class="text-body-2">{{ tvShowSubscriptions }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardTitle>
|
||||
|
||||
<!-- 头部操作按钮 -->
|
||||
<div class="user-actions" :class="{ 'mobile-actions': isMobile }">
|
||||
<VBtn
|
||||
icon
|
||||
size="small"
|
||||
:color="user.is_superuser ? 'warning' : 'primary'"
|
||||
variant="text"
|
||||
@click="editUser"
|
||||
class="action-btn"
|
||||
>
|
||||
<VIcon icon="mdi-pencil" />
|
||||
</VBtn>
|
||||
<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"
|
||||
@click="removeUser"
|
||||
class="action-btn"
|
||||
>
|
||||
<VIcon icon="mdi-delete" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- 独立的邮箱显示 -->
|
||||
<div class="email-container" :class="{ 'admin-email': user.is_superuser, 'inactive-email': !user.is_active }">
|
||||
<VIcon icon="mdi-email-outline" size="small" color="primary" class="email-icon" />
|
||||
<span class="email-text">{{ user.email || '未设置邮箱' }}</span>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- PC端显示订阅统计信息 -->
|
||||
<div v-if="!isMobile" class="user-card-body">
|
||||
<div class="user-stats-container">
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon-container" :class="{ 'admin-stat': user.is_superuser }">
|
||||
<VIcon :color="user.is_superuser ? 'warning' : 'primary'" icon="mdi-movie-outline" size="20" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ movieSubscriptions }}</div>
|
||||
<div class="stat-label">电影订阅</div>
|
||||
<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="stat-item">
|
||||
<div class="stat-icon-container" :class="{ 'admin-stat': user.is_superuser }">
|
||||
<VIcon :color="user.is_superuser ? 'warning' : 'primary'" icon="mdi-television-classic" size="20" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ tvShowSubscriptions }}</div>
|
||||
<div class="stat-label">剧集订阅</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>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 用户编辑弹窗 -->
|
||||
@@ -277,101 +283,20 @@ onUnmounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.user-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.user-card-hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.user-card-admin {
|
||||
border: 1px solid transparent;
|
||||
background-clip: content-box, border-box;
|
||||
background-image: linear-gradient(rgb(var(--v-theme-surface)), rgb(var(--v-theme-surface))),
|
||||
linear-gradient(120deg, rgba(var(--v-theme-warning), 0.5), rgba(var(--v-theme-error), 0.5));
|
||||
background-origin: border-box;
|
||||
}
|
||||
|
||||
.user-card-inactive {
|
||||
position: relative;
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
|
||||
background-color: rgba(var(--v-theme-surface), 0.95);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.user-card-inactive::before {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
backdrop-filter: grayscale(30%);
|
||||
content: '';
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.admin-decoration {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
inset-block-start: 0;
|
||||
inset-inline: 0;
|
||||
padding-block: 8px;
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.decoration-line {
|
||||
flex: 1;
|
||||
background: linear-gradient(90deg, rgba(var(--v-theme-warning), 0.1), rgba(var(--v-theme-warning), 0.7));
|
||||
block-size: 1px;
|
||||
}
|
||||
|
||||
.decoration-line:last-child {
|
||||
background: linear-gradient(90deg, rgba(var(--v-theme-warning), 0.7), rgba(var(--v-theme-warning), 0.1));
|
||||
}
|
||||
|
||||
.decoration-circle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(var(--v-theme-warning), 0.5);
|
||||
border-radius: 50%;
|
||||
block-size: 18px;
|
||||
inline-size: 18px;
|
||||
margin-block: 0;
|
||||
margin-inline: 8px;
|
||||
}
|
||||
|
||||
.user-card-header {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
padding-block: 20px 12px;
|
||||
padding-inline: 16px;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
background: linear-gradient(to bottom, rgba(var(--v-theme-warning), 0.05), transparent);
|
||||
}
|
||||
|
||||
.user-avatar-container {
|
||||
position: relative;
|
||||
margin-inline-end: 16px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
border: 4px solid rgb(var(--v-theme-surface));
|
||||
box-shadow: 0 4px 8px rgba(var(--v-theme-on-surface), 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.admin-avatar {
|
||||
border: 4px solid rgba(var(--v-theme-warning), 0.1);
|
||||
box-shadow: 0 5px 15px rgba(var(--v-theme-warning), 0.2);
|
||||
}
|
||||
|
||||
.admin-avatar::after {
|
||||
position: absolute;
|
||||
border: 1px solid rgba(var(--v-theme-warning), 0.3);
|
||||
@@ -382,114 +307,53 @@ onUnmounted(() => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.inactive-avatar {
|
||||
border-color: rgba(var(--v-theme-on-surface), 0.1);
|
||||
filter: grayscale(50%);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.avatar-overlay {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
backdrop-filter: blur(1px);
|
||||
background: rgba(var(--v-theme-on-surface), 0.2);
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.otp-badge {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: glow 2s infinite alternate;
|
||||
inset-block-end: 0;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
.otp-badge .v-icon {
|
||||
color: #4caf50 !important;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 40%));
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
from {
|
||||
opacity: 0.9;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1.15);
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-otp {
|
||||
inset-block-end: 0 !important;
|
||||
inset-inline-end: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-otp .v-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.admin-crown {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
background: transparent;
|
||||
filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 40%));
|
||||
inset-block-start: -10px;
|
||||
inset-inline-start: -6px;
|
||||
top: -10px;
|
||||
left: -6px;
|
||||
transform: rotate(-25deg);
|
||||
}
|
||||
|
||||
.admin-crown .v-icon {
|
||||
color: #ffc107 !important;
|
||||
font-size: 24px;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.nickname-icon {
|
||||
.animate-pulse {
|
||||
animation: pulse-nickname 2s ease infinite;
|
||||
filter: brightness(1.1);
|
||||
margin-inline-start: 4px;
|
||||
opacity: 0.9;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@keyframes pulse-nickname {
|
||||
@@ -498,287 +362,13 @@ onUnmounted(() => {
|
||||
opacity: 0.9;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: move;
|
||||
margin-inline-end: 6px;
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.user-card:hover .drag-handle {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.user-name-section {
|
||||
margin-block-end: 8px;
|
||||
}
|
||||
|
||||
.name-and-badges {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-block-end: 4px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin-block: 0 4px;
|
||||
margin-inline: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-name {
|
||||
color: rgb(var(--v-theme-warning));
|
||||
font-weight: 700;
|
||||
text-shadow: 0 1px 2px rgba(var(--v-theme-warning), 0.1);
|
||||
}
|
||||
|
||||
.inactive-name {
|
||||
color: rgba(var(--v-theme-on-surface), 0.6);
|
||||
}
|
||||
|
||||
.user-badges {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 4px;
|
||||
margin-block-end: 4px;
|
||||
-ms-overflow-style: none;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.user-badges::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-badge {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.7rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-badge {
|
||||
border: 1px solid rgba(var(--v-theme-error), 0.3);
|
||||
}
|
||||
|
||||
.user-account,
|
||||
.user-email {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
color: rgba(var(--v-theme-on-surface), 0.7);
|
||||
font-size: 0.8rem;
|
||||
inline-size: 100%;
|
||||
inset-block-start: 100%;
|
||||
inset-inline-start: 0;
|
||||
margin-block-start: 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.account-label {
|
||||
color: rgba(var(--v-theme-on-surface), 0.5);
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
|
||||
.account-value {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
margin-inline-end: 4px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.email-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.mobile-actions {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
inset-block-start: 10px;
|
||||
inset-inline-end: 10px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
opacity: 0.7;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.mobile-card {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.mobile-stats {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 20px;
|
||||
margin-block-start: 8px;
|
||||
padding-block: 4px;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.mobile-stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.95rem;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mobile-stat-item .v-icon {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
.mobile-stat-item span {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-card-body {
|
||||
padding-block: 0 16px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
.user-stats-container {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.02);
|
||||
margin-block-start: 8px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stat-icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
block-size: 40px;
|
||||
box-shadow: 0 2px 6px rgba(var(--v-theme-on-surface), 0.05);
|
||||
inline-size: 40px;
|
||||
}
|
||||
|
||||
.admin-stat {
|
||||
background-color: rgba(var(--v-theme-warning), 0.1);
|
||||
box-shadow: 0 2px 6px rgba(var(--v-theme-warning), 0.2);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: rgba(var(--v-theme-on-surface), 0.6);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.text-error {
|
||||
color: rgb(var(--v-theme-error));
|
||||
}
|
||||
|
||||
.email-container {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
border-block-start: 1px solid rgba(var(--v-theme-on-surface), 0.05);
|
||||
padding-block: 8px;
|
||||
padding-inline: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-email {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.inactive-email {
|
||||
background-color: transparent;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.email-container .email-icon {
|
||||
flex-shrink: 0;
|
||||
margin-inline-end: 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.email-container .email-text {
|
||||
overflow: hidden;
|
||||
color: rgba(var(--v-theme-on-surface), 0.8);
|
||||
font-size: 0.9rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-card .email-container {
|
||||
padding-block: 6px;
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.mobile-card .email-container .email-text {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.mobile-card .user-avatar-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mobile-card .otp-badge {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
inset-block-end: 0 !important;
|
||||
inset-inline-end: 0 !important;
|
||||
.grayscale-50 {
|
||||
filter: grayscale(50%);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -120,7 +120,7 @@ onMounted(() => {
|
||||
<template>
|
||||
<VDialog max-width="35rem" scrollable>
|
||||
<VCard>
|
||||
<VCardTitle class="py-3 me-12">
|
||||
<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>
|
||||
|
||||
@@ -88,7 +88,7 @@ onUnmounted(() => {
|
||||
<VCard title="阿里云盘登录" class="rounded-t">
|
||||
<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">
|
||||
<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">
|
||||
|
||||
@@ -7,7 +7,7 @@ const props = defineProps({
|
||||
<template>
|
||||
<!-- 手动整理进度框 -->
|
||||
<VDialog :scrim="false" width="25rem">
|
||||
<VCard color="primary" rounded="md">
|
||||
<VCard elevation="3" color="primary">
|
||||
<VCardText class="text-center">
|
||||
{{ props.text }}
|
||||
<VProgressLinear color="white" class="mb-0 mt-1" :model-value="props.value" indeterminate />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { Site, Plugin, Subscribe } from '@/api/types'
|
||||
import { popScopeId, PropType } from 'vue'
|
||||
import type { Site } from '@/api/types'
|
||||
|
||||
const props = defineProps({
|
||||
sites: {
|
||||
@@ -63,14 +62,14 @@ const filteredSites = computed(() => {
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="ml-4"
|
||||
style="max-width: 200px"
|
||||
style="max-inline-size: 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">
|
||||
<VCardText style="max-block-size: 420px" class="overflow-y-auto px-4 py-4">
|
||||
<!-- 站点列表 -->
|
||||
<div v-if="filteredSites.length > 0">
|
||||
<!-- 选择操作 -->
|
||||
@@ -186,9 +185,9 @@ const filteredSites = computed(() => {
|
||||
</template>
|
||||
<style scoped>
|
||||
.site-checkbox-wrapper {
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
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 {
|
||||
@@ -196,15 +195,15 @@ const filteredSites = computed(() => {
|
||||
}
|
||||
|
||||
.site-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.site-selected {
|
||||
border-color: rgba(var(--v-theme-primary), 0.2);
|
||||
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 {
|
||||
|
||||
@@ -140,7 +140,7 @@ const dropdownItems = ref([
|
||||
<VDivider />
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VList lines="two">
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="historyList" class="overflow-hidden" @load="loadHistory">
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="historyList" class="overflow-visible" @load="loadHistory">
|
||||
<template #loading>
|
||||
<LoadingBanner />
|
||||
</template>
|
||||
@@ -154,7 +154,7 @@ const dropdownItems = ref([
|
||||
width="50"
|
||||
:src="item.poster"
|
||||
aspect-ratio="2/3"
|
||||
class="object-cover rounded shadow ring-gray-500 me-3"
|
||||
class="object-cover rounded ring-gray-500 me-3"
|
||||
cover
|
||||
>
|
||||
<template #placeholder>
|
||||
|
||||
@@ -188,7 +188,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<VBottomSheet inset scrollable>
|
||||
<VCard class="rounded-t">
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardItem>
|
||||
<VCardTitle class="pe-10"> 订阅 - {{ props.media?.title }} </VCardTitle>
|
||||
@@ -212,7 +212,7 @@ onMounted(async () => {
|
||||
width="60"
|
||||
:src="getSeasonPoster(item.poster_path || '')"
|
||||
aspect-ratio="2/3"
|
||||
class="object-cover rounded shadow ring-gray-500 me-3"
|
||||
class="object-cover rounded ring-gray-500 me-3"
|
||||
cover
|
||||
>
|
||||
<template #placeholder>
|
||||
|
||||
@@ -95,7 +95,7 @@ onUnmounted(() => {
|
||||
<VCard title="115网盘登录" class="rounded-t">
|
||||
<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">
|
||||
<div class="my-6 rounded text-center p-3 border">
|
||||
<QrcodeVue class="mx-auto" :value="qrCodeContent" :size="200" />
|
||||
</div>
|
||||
<VAlert variant="tonal" :type="alertType" class="my-4 text-center" :text="text">
|
||||
|
||||
@@ -20,7 +20,7 @@ const inProps = defineProps({
|
||||
storage: String,
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
axios: {
|
||||
type: Function,
|
||||
type: Object as PropType<any>,
|
||||
required: true,
|
||||
},
|
||||
refreshpending: Boolean,
|
||||
@@ -548,7 +548,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<VCard class="d-flex flex-column w-full h-full rounded-t-0" :class="{ 'rounded-s-0': showTree }">
|
||||
<VToolbar v-if="!loading" density="compact" flat color="gray">
|
||||
<div v-if="!loading" class="flex">
|
||||
<IconBtn v-if="display.mdAndUp.value">
|
||||
<VIcon v-if="showTree" icon="mdi-file-tree" @click="switchFileTree(false)" />
|
||||
<VIcon v-else icon="mdi-file-tree-outline" @click="switchFileTree(true)" />
|
||||
@@ -559,10 +559,10 @@ onMounted(() => {
|
||||
hide-details
|
||||
flat
|
||||
density="compact"
|
||||
variant="solo-filled"
|
||||
variant="plain"
|
||||
placeholder="搜索 ..."
|
||||
prepend-inner-icon="mdi-filter-outline"
|
||||
class="me-2"
|
||||
class="mx-2"
|
||||
rounded
|
||||
/>
|
||||
<VSpacer v-if="isFile" />
|
||||
@@ -591,14 +591,14 @@ onMounted(() => {
|
||||
<VIcon icon="mdi-delete-outline" color="error" />
|
||||
</IconBtn>
|
||||
</span>
|
||||
</VToolbar>
|
||||
</div>
|
||||
<VCardText v-if="loading" class="text-center flex flex-col items-center">
|
||||
<VProgressCircular size="48" indeterminate color="primary" />
|
||||
</VCardText>
|
||||
<!-- 文件详情 -->
|
||||
<VCardText v-else-if="isFile && !isImage && items.length > 0" class="text-center break-all">
|
||||
<div v-if="items[0]?.thumbnail" class="flex justify-center">
|
||||
<VImg max-width="15rem" cover :src="items[0]?.thumbnail" class="rounded border shadow-lg">
|
||||
<VImg max-width="15rem" cover :src="items[0]?.thumbnail" class="rounded border">
|
||||
<template #placeholder>
|
||||
<VSkeletonLoader class="object-cover w-full h-full" />
|
||||
</template>
|
||||
@@ -616,7 +616,7 @@ onMounted(() => {
|
||||
</VCardText>
|
||||
<!-- 目录和文件列表 -->
|
||||
<VCardText v-else-if="dirs.length || files.length" class="p-0">
|
||||
<VList subheader>
|
||||
<VList class="text-high-emphasis">
|
||||
<VVirtualScroll :items="[...dirs, ...files]" :style="listStyle">
|
||||
<template #default="{ item }">
|
||||
<VHover>
|
||||
@@ -729,13 +729,3 @@ onMounted(() => {
|
||||
@close="nameTestDialog = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-card {
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
.v-toolbar {
|
||||
background: rgb(var(--v-table-header-background));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,7 +23,7 @@ const props = defineProps({
|
||||
},
|
||||
endpoints: Object,
|
||||
axios: {
|
||||
type: Function,
|
||||
type: Object as PropType<any>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ const inProps = defineProps({
|
||||
},
|
||||
endpoints: Object as PropType<EndPoints>,
|
||||
axios: {
|
||||
type: Function,
|
||||
type: Object as PropType<any>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -120,7 +120,7 @@ onMounted(() => {
|
||||
width="50"
|
||||
:src="item.poster"
|
||||
aspect-ratio="2/3"
|
||||
class="object-cover rounded shadow ring-gray-500 me-3"
|
||||
class="object-cover rounded ring-gray-500 me-3"
|
||||
cover
|
||||
>
|
||||
<template #placeholder>
|
||||
|
||||
124
src/composables/useDynamicButton.ts
Normal file
124
src/composables/useDynamicButton.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { ref, inject, nextTick, onMounted, onActivated, onDeactivated, onUnmounted } from 'vue'
|
||||
|
||||
// 声明全局变量类型
|
||||
declare global {
|
||||
interface Window {
|
||||
__VUE_INJECT_DYNAMIC_BUTTON__?: (button: any) => void
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态按钮钩子函数
|
||||
*
|
||||
* @param options 配置选项
|
||||
* @returns 控制函数和状态
|
||||
*
|
||||
* @example
|
||||
* // 在页面中使用
|
||||
* const { openDialog } = useDynamicButton({
|
||||
* icon: 'mdi-cog',
|
||||
* onClick: () => {
|
||||
* dialog.value = true
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
export function useDynamicButton(options: {
|
||||
icon: string
|
||||
onClick: () => void
|
||||
autoRegister?: boolean // 是否自动注册,默认为true
|
||||
}) {
|
||||
// 提取配置
|
||||
const { icon, onClick, autoRegister = true } = options
|
||||
|
||||
// 动态按钮相关
|
||||
const registerDynamicButton = inject<((button: any) => void) | null>('registerDynamicButton', null)
|
||||
const unregisterDynamicButton = inject<(() => void) | null>('unregisterDynamicButton', null)
|
||||
|
||||
// 按钮注册状态
|
||||
const dynamicButtonRegistered = ref(false)
|
||||
|
||||
// 注册动态按钮
|
||||
function setupDynamicButton() {
|
||||
// 避免重复注册
|
||||
if (dynamicButtonRegistered.value) return
|
||||
|
||||
// 确保注册方法存在
|
||||
if (!registerDynamicButton) {
|
||||
// 尝试获取全局注册方法
|
||||
const tryUseGlobalMethod = () => {
|
||||
if (typeof window !== 'undefined' && window.__VUE_INJECT_DYNAMIC_BUTTON__) {
|
||||
window.__VUE_INJECT_DYNAMIC_BUTTON__({
|
||||
icon,
|
||||
action: onClick,
|
||||
show: true,
|
||||
})
|
||||
dynamicButtonRegistered.value = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 立即尝试一次
|
||||
if (!tryUseGlobalMethod()) {
|
||||
// 如果失败,延迟再试一次
|
||||
setTimeout(tryUseGlobalMethod, 1000)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 如果注册方法存在,直接注册
|
||||
nextTick(() => {
|
||||
registerDynamicButton({
|
||||
icon,
|
||||
action: onClick,
|
||||
show: true,
|
||||
})
|
||||
dynamicButtonRegistered.value = true
|
||||
})
|
||||
}
|
||||
|
||||
// 取消注册动态按钮
|
||||
function cleanupDynamicButton() {
|
||||
if (unregisterDynamicButton && dynamicButtonRegistered.value) {
|
||||
unregisterDynamicButton()
|
||||
dynamicButtonRegistered.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露方法:手动打开对话框
|
||||
function openDialog() {
|
||||
onClick()
|
||||
}
|
||||
|
||||
// 生命周期钩子
|
||||
if (autoRegister) {
|
||||
onMounted(() => {
|
||||
// 延迟执行,确保Footer组件已加载
|
||||
setTimeout(() => {
|
||||
setupDynamicButton()
|
||||
}, 500)
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
// 重置注册状态,确保每次激活时都重新注册
|
||||
dynamicButtonRegistered.value = false
|
||||
setupDynamicButton()
|
||||
})
|
||||
|
||||
onDeactivated(() => {
|
||||
cleanupDynamicButton()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupDynamicButton()
|
||||
})
|
||||
}
|
||||
|
||||
// 返回控制函数和状态
|
||||
return {
|
||||
setupDynamicButton, // 手动注册按钮
|
||||
cleanupDynamicButton, // 手动取消注册
|
||||
openDialog, // 手动触发点击事件
|
||||
isRegistered: dynamicButtonRegistered, // 注册状态
|
||||
}
|
||||
}
|
||||
@@ -1,98 +1,278 @@
|
||||
<script setup lang="ts">
|
||||
import { SystemNavMenus } from '@/router/menu'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { VMenu } from 'vuetify/lib/components/index.mjs'
|
||||
|
||||
const display = useDisplay()
|
||||
const appMode = inject('pwaMode') && display.mdAndDown.value
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const moreMenuDialog = ref(false)
|
||||
// 根据当前路径获取匹配的菜单路径
|
||||
function getMenuPathFromRoute(path: string): string {
|
||||
const matchedMenu = SystemNavMenus.find(menu => menu.footer === true && path.startsWith(menu.to))
|
||||
return matchedMenu ? matchedMenu.to : '/apps'
|
||||
}
|
||||
|
||||
const moreMemus = computed(() => SystemNavMenus.filter(menu => !menu.footer))
|
||||
// 当前选中的菜单,初始值基于当前路由
|
||||
const currentMenu = ref<string>(getMenuPathFromRoute(route.path))
|
||||
|
||||
const activeState = computed(() => {
|
||||
return {
|
||||
home: route.path === '/dashboard',
|
||||
recommend: route.path === '/recommend',
|
||||
movie: route.path === '/subscribe/movie',
|
||||
tv: route.path === '/subscribe/tv',
|
||||
// 过滤出底部菜单项
|
||||
const footerMenus = computed(() => {
|
||||
return SystemNavMenus.filter(menu => menu.footer === true)
|
||||
})
|
||||
|
||||
// 监听路由变化来更新currentMenu
|
||||
watch(
|
||||
() => route.path,
|
||||
newPath => {
|
||||
currentMenu.value = getMenuPathFromRoute(newPath)
|
||||
// 当路由变化时,清除动态按钮
|
||||
dynamicButton.value = null
|
||||
},
|
||||
{ immediate: false },
|
||||
)
|
||||
|
||||
// 动态按钮相关
|
||||
// 定义动态按钮类型
|
||||
interface DynamicButton {
|
||||
icon: string
|
||||
action: () => void
|
||||
show: boolean
|
||||
routePath?: string // 添加路径属性,用于标识哪个路由注册的
|
||||
}
|
||||
|
||||
// 提供动态按钮注册和获取的方法
|
||||
const dynamicButton = ref<DynamicButton | null>(null)
|
||||
|
||||
// 提供一个方法让其他组件注册动态按钮
|
||||
const registerDynamicButton = (button: DynamicButton) => {
|
||||
// 保存注册按钮的路由路径
|
||||
button.routePath = route.path
|
||||
dynamicButton.value = button
|
||||
}
|
||||
|
||||
// 提供一个方法让其他组件取消注册动态按钮
|
||||
const unregisterDynamicButton = () => {
|
||||
dynamicButton.value = null
|
||||
}
|
||||
|
||||
// 添加全局注册方法,解决注入不可用的问题
|
||||
if (typeof window !== 'undefined') {
|
||||
// 确保在浏览器环境中
|
||||
;(window as any).__VUE_INJECT_DYNAMIC_BUTTON__ = registerDynamicButton
|
||||
}
|
||||
|
||||
// 提供给其他组件使用
|
||||
provide('registerDynamicButton', registerDynamicButton)
|
||||
provide('unregisterDynamicButton', unregisterDynamicButton)
|
||||
|
||||
// 在组件销毁时清理
|
||||
onUnmounted(() => {
|
||||
dynamicButton.value = null
|
||||
// 清理全局方法
|
||||
if (typeof window !== 'undefined') {
|
||||
delete (window as any).__VUE_INJECT_DYNAMIC_BUTTON__
|
||||
}
|
||||
})
|
||||
|
||||
const moreActiveState = computed(() => {
|
||||
return !Object.values(activeState.value).some(v => v)
|
||||
// 显示动态按钮
|
||||
const showDynamicButton = computed(() => {
|
||||
return (
|
||||
dynamicButton.value &&
|
||||
dynamicButton.value.show &&
|
||||
// 确保只在注册的路由路径下显示按钮
|
||||
(!dynamicButton.value.routePath || dynamicButton.value.routePath === route.path)
|
||||
)
|
||||
})
|
||||
|
||||
const currentPath = computed(() => route.path)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="appMode" class="w-100">
|
||||
<VBottomNavigation
|
||||
grow
|
||||
horizontal
|
||||
color="primary"
|
||||
class="footer-nav border-t"
|
||||
style="block-size: calc(3.5rem + env(safe-area-inset-bottom))"
|
||||
:z-index="9998"
|
||||
>
|
||||
<VBtn to="/dashboard" :ripple="false">
|
||||
<VIcon v-if="activeState.home" size="28">mdi-home</VIcon>
|
||||
<VIcon v-else size="28">mdi-home-outline</VIcon>
|
||||
</VBtn>
|
||||
<VBtn to="/recommend" :ripple="false">
|
||||
<VIcon v-if="activeState.recommend" size="28">mdi-star</VIcon>
|
||||
<VIcon v-else size="28">mdi-star-outline</VIcon>
|
||||
</VBtn>
|
||||
<VBtn to="/subscribe/movie" :ripple="false">
|
||||
<VIcon v-if="activeState.movie" size="28">mdi-movie-open</VIcon>
|
||||
<VIcon v-else size="28">mdi-movie-open-outline</VIcon>
|
||||
</VBtn>
|
||||
<VBtn to="/subscribe/tv" :ripple="false">
|
||||
<VIcon v-if="activeState.tv" size="28">mdi-television-play</VIcon>
|
||||
<VIcon v-else size="28">mdi-television</VIcon>
|
||||
</VBtn>
|
||||
<VBtn :ripple="false">
|
||||
<VIcon
|
||||
size="28"
|
||||
:icon="moreMenuDialog ? 'mdi-close' : 'mdi-dots-horizontal'"
|
||||
:color="moreActiveState ? 'primary' : ''"
|
||||
/>
|
||||
<VMenu v-model="moreMenuDialog" close-on-content-click activator="parent" scrim>
|
||||
<VList class="font-bold" lines="one">
|
||||
<VListSubheader class="bg-transparent"> 更多 </VListSubheader>
|
||||
<VListItem
|
||||
class="pe-20 ps-5"
|
||||
v-for="(menu, index) in moreMemus"
|
||||
:key="index"
|
||||
:prepend-icon="menu.icon"
|
||||
nav
|
||||
<Teleport v-if="appMode" to="body">
|
||||
<div class="footer-nav-container">
|
||||
<VCard elevation="3" class="footer-nav-card border" rounded="pill" :class="{ 'shift-left': showDynamicButton }">
|
||||
<VCardText class="footer-card-content">
|
||||
<!-- 添加指示器 -->
|
||||
<div ref="indicator" class="nav-indicator"></div>
|
||||
<VBtnToggle class="footer-btn-group" :mandatory="true" v-model="currentMenu">
|
||||
<!-- 遍历底部菜单项 -->
|
||||
<VBtn
|
||||
v-for="menu in footerMenus"
|
||||
:key="menu.to"
|
||||
:to="menu.to"
|
||||
:base-color="currentPath === menu.to ? 'primary' : undefined"
|
||||
:variant="currentMenu === menu.to ? 'text' : 'plain'"
|
||||
color="primary"
|
||||
:ripple="false"
|
||||
class="footer-nav-btn"
|
||||
rounded="pill"
|
||||
:class="{ 'footer-nav-btn-active': currentMenu === menu.to }"
|
||||
:value="menu.to"
|
||||
>
|
||||
<VListItemTitle>
|
||||
<span class="text-lg">{{ menu.title }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</VBtn>
|
||||
</VBottomNavigation>
|
||||
</div>
|
||||
<div class="btn-content">
|
||||
<VIcon :icon="menu.icon" size="24"></VIcon>
|
||||
<span class="text-xs">{{ menu.title }}</span>
|
||||
</div>
|
||||
</VBtn>
|
||||
|
||||
<!-- 更多按钮 -->
|
||||
<VBtn
|
||||
:variant="currentMenu === '/apps' ? 'text' : 'plain'"
|
||||
color="primary"
|
||||
:ripple="false"
|
||||
to="/apps"
|
||||
rounded="pill"
|
||||
class="footer-nav-btn"
|
||||
:class="{ 'footer-nav-btn-active': currentMenu === '/apps' }"
|
||||
value="/apps"
|
||||
>
|
||||
<div class="btn-content">
|
||||
<VIcon icon="mdi-dots-horizontal" size="24"></VIcon>
|
||||
<span class="btn-text">更多</span>
|
||||
</div>
|
||||
</VBtn>
|
||||
</VBtnToggle>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<Transition name="fade-slide">
|
||||
<VCard v-if="showDynamicButton" elevation="3" class="footer-nav-card dynamic-btn-card border" rounded="pill">
|
||||
<VCardText class="footer-card-content">
|
||||
<!-- 各页面的动态按钮 -->
|
||||
<VBtn
|
||||
icon
|
||||
variant="text"
|
||||
:ripple="false"
|
||||
@click="dynamicButton?.action()"
|
||||
rounded="pill"
|
||||
class="footer-nav-btn"
|
||||
>
|
||||
<VIcon color="secondary" :icon="dynamicButton?.icon || 'mdi-plus'" size="24"></VIcon>
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</Transition>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.footer-nav {
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
backdrop-filter: blur(6px);
|
||||
background-color: rgb(var(--v-theme-surface), 0.8);
|
||||
padding-block-end: env(safe-area-inset-bottom);
|
||||
.footer-nav-container {
|
||||
position: fixed;
|
||||
z-index: 1999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inset-block-end: 0;
|
||||
inset-inline: 0;
|
||||
padding-block-end: calc(6px + env(safe-area-inset-bottom, 0px));
|
||||
pointer-events: none;
|
||||
|
||||
// 按钮卡片之间的间距
|
||||
> .v-card + .v-card {
|
||||
margin-inline-start: 2px; // 减少间距
|
||||
}
|
||||
}
|
||||
|
||||
.footer-nav .v-btn--variant-text .v-btn__overlay {
|
||||
background-color: transparent !important;
|
||||
.footer-nav-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(12px);
|
||||
background-color: rgba(var(--v-theme-surface), 0.8);
|
||||
pointer-events: auto;
|
||||
transition: all 0.5s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
|
||||
&.shift-left {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card-content {
|
||||
position: relative;
|
||||
padding-block: 6px;
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
.footer-btn-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.footer-nav-btn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 0;
|
||||
background-color: transparent;
|
||||
|
||||
&.v-btn--active {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
// 动态按钮卡片样式
|
||||
.dynamic-btn-card {
|
||||
block-size: auto;
|
||||
inline-size: auto;
|
||||
min-block-size: 0;
|
||||
|
||||
.footer-card-content {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.footer-nav-btn {
|
||||
padding: 0;
|
||||
block-size: 36px;
|
||||
inline-size: 36px;
|
||||
min-inline-size: 36px;
|
||||
|
||||
.btn-content {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.v-icon {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 淡入滑动动画
|
||||
.fade-slide-enter-active {
|
||||
transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
}
|
||||
|
||||
.fade-slide-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
}
|
||||
|
||||
.fade-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -70,7 +70,7 @@ onUnmounted(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="tab-header">
|
||||
<div class="tab-header rounded-t-lg">
|
||||
<div ref="tabsContainerRef" class="header-tabs" :class="{ 'show-indicator': showTabsScrollIndicator }">
|
||||
<div
|
||||
v-for="(item, index) in items"
|
||||
@@ -94,7 +94,6 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(var(--v-theme-background), 0.8);
|
||||
border-block-end: 1px solid rgba(var(--v-theme-on-surface), 0.05);
|
||||
inset-block-start: 0;
|
||||
margin-block-end: 16px;
|
||||
@@ -127,7 +126,7 @@ onUnmounted(() => {
|
||||
&::after {
|
||||
position: absolute;
|
||||
z-index: 1; // Ensure it's above the tabs but below other header elements if needed
|
||||
background: linear-gradient(to left, rgba(var(--v-theme-background), 1) 30%, transparent);
|
||||
background: linear-gradient(to left, rgba(var(--v-theme-background), 10.3) 30%, transparent);
|
||||
content: '';
|
||||
inline-size: 40px; // Width of the fade effect
|
||||
inset-block: 0;
|
||||
@@ -136,11 +135,6 @@ onUnmounted(() => {
|
||||
pointer-events: none; // Allow interaction with content behind it
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
// Show the indicator when the class is present
|
||||
&.show-indicator::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.header-tab-icon {
|
||||
|
||||
@@ -19,9 +19,14 @@ const themes: ThemeSwitcherTheme[] = [
|
||||
},
|
||||
{
|
||||
name: 'purple',
|
||||
title: '紫韵幽兰',
|
||||
title: '幻紫',
|
||||
icon: 'mdi-brightness-4',
|
||||
},
|
||||
{
|
||||
name: 'transparent',
|
||||
title: '透明',
|
||||
icon: 'mdi-gradient-horizontal',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const display = useDisplay()
|
||||
const appsMenu = ref(false)
|
||||
|
||||
// 菜单最大宽度
|
||||
const menuMaxWidth = ref(480)
|
||||
const menuMaxWidth = ref(420)
|
||||
|
||||
// 名称测试弹窗
|
||||
const nameTestDialog = ref(false)
|
||||
@@ -45,6 +45,57 @@ const sendButtonDisabled = ref(false)
|
||||
// 聊天容器
|
||||
const chatContainer = ref<HTMLElement>()
|
||||
|
||||
// 定义捷径列表
|
||||
const shortcuts = [
|
||||
{
|
||||
title: '识别',
|
||||
subtitle: '名称识别测试',
|
||||
icon: 'mdi-text-recognition',
|
||||
dialog: 'nameTest',
|
||||
dialogRef: nameTestDialog,
|
||||
},
|
||||
{
|
||||
title: '规则',
|
||||
subtitle: '规则测试',
|
||||
icon: 'mdi-filter-cog',
|
||||
dialog: 'ruleTest',
|
||||
dialogRef: ruleTestDialog,
|
||||
},
|
||||
{
|
||||
title: '日志',
|
||||
subtitle: '实时日志',
|
||||
icon: 'mdi-file-document',
|
||||
dialog: 'logging',
|
||||
dialogRef: loggingDialog,
|
||||
},
|
||||
{
|
||||
title: '网络',
|
||||
subtitle: '网速连通性测试',
|
||||
icon: 'mdi-network',
|
||||
dialog: 'netTest',
|
||||
dialogRef: netTestDialog,
|
||||
},
|
||||
{
|
||||
title: '系统',
|
||||
subtitle: '健康检查',
|
||||
icon: 'mdi-cog',
|
||||
dialog: 'systemTest',
|
||||
dialogRef: systemTestDialog,
|
||||
},
|
||||
{
|
||||
title: '消息',
|
||||
subtitle: '消息中心',
|
||||
icon: 'mdi-message',
|
||||
dialog: 'message',
|
||||
dialogRef: messageDialog,
|
||||
},
|
||||
]
|
||||
|
||||
// 打开对话框
|
||||
function openDialog(dialogRef: any) {
|
||||
dialogRef.value = true
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
function scrollMessageToEnd() {
|
||||
nextTick(() => {
|
||||
@@ -78,25 +129,9 @@ onMounted(() => {
|
||||
scrollMessageToEnd()
|
||||
const shortcut = getQueryValue('shortcut')
|
||||
if (shortcut) {
|
||||
switch (shortcut) {
|
||||
case 'nameTest':
|
||||
nameTestDialog.value = true
|
||||
break
|
||||
case 'netTest':
|
||||
netTestDialog.value = true
|
||||
break
|
||||
case 'logging':
|
||||
loggingDialog.value = true
|
||||
break
|
||||
case 'ruleTest':
|
||||
ruleTestDialog.value = true
|
||||
break
|
||||
case 'systemTest':
|
||||
systemTestDialog.value = true
|
||||
break
|
||||
case 'message':
|
||||
messageDialog.value = true
|
||||
break
|
||||
const found = shortcuts.find(item => item.dialog === shortcut)
|
||||
if (found) {
|
||||
found.dialogRef.value = true
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -110,7 +145,6 @@ onMounted(() => {
|
||||
max-height="560"
|
||||
location="top end"
|
||||
origin="top end"
|
||||
transition="scale-transition"
|
||||
close-on-content-click
|
||||
close-on-back
|
||||
scrim
|
||||
@@ -122,81 +156,35 @@ onMounted(() => {
|
||||
</IconBtn>
|
||||
</template>
|
||||
<!-- Menu Content -->
|
||||
<VCard class="shortcut-menu-card">
|
||||
<VCardItem class="shortcut-header border-b">
|
||||
<VCardTitle class="font-weight-medium text-primary">捷径</VCardTitle>
|
||||
<VCard class="overflow-hidden">
|
||||
<VCardItem class="py-3">
|
||||
<VCardTitle>捷径</VCardTitle>
|
||||
<template #append>
|
||||
<IconBtn @click="appsMenu = false" class="shortcut-close-btn">
|
||||
<IconBtn @click="appsMenu = false">
|
||||
<VIcon icon="mdi-close" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VCardItem>
|
||||
<div class="ps ps--active-y shortcut-menu-container">
|
||||
<div class="shortcut-grid">
|
||||
<!-- 识别 -->
|
||||
<div class="shortcut-item" @click="nameTestDialog = true">
|
||||
<div class="shortcut-icon-wrapper">
|
||||
<VIcon icon="mdi-text-recognition" size="24" />
|
||||
</div>
|
||||
<div class="shortcut-text">
|
||||
<div class="shortcut-title">识别</div>
|
||||
<div class="shortcut-subtitle">名称识别测试</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 规则 -->
|
||||
<div class="shortcut-item" @click="ruleTestDialog = true">
|
||||
<div class="shortcut-icon-wrapper">
|
||||
<VIcon icon="mdi-filter-cog" size="24" />
|
||||
</div>
|
||||
<div class="shortcut-text">
|
||||
<div class="shortcut-title">规则</div>
|
||||
<div class="shortcut-subtitle">规则测试</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志 -->
|
||||
<div class="shortcut-item" @click="loggingDialog = true">
|
||||
<div class="shortcut-icon-wrapper">
|
||||
<VIcon icon="mdi-file-document" size="24" />
|
||||
</div>
|
||||
<div class="shortcut-text">
|
||||
<div class="shortcut-title">日志</div>
|
||||
<div class="shortcut-subtitle">实时日志</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 网络 -->
|
||||
<div class="shortcut-item" @click="netTestDialog = true">
|
||||
<div class="shortcut-icon-wrapper">
|
||||
<VIcon icon="mdi-network" size="24" />
|
||||
</div>
|
||||
<div class="shortcut-text">
|
||||
<div class="shortcut-title">网络</div>
|
||||
<div class="shortcut-subtitle">网速连通性测试</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统 -->
|
||||
<div class="shortcut-item" @click="systemTestDialog = true">
|
||||
<div class="shortcut-icon-wrapper">
|
||||
<VIcon icon="mdi-cog" size="24" />
|
||||
</div>
|
||||
<div class="shortcut-text">
|
||||
<div class="shortcut-title">系统</div>
|
||||
<div class="shortcut-subtitle">健康检查</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息 -->
|
||||
<div class="shortcut-item" @click="messageDialog = true">
|
||||
<div class="shortcut-icon-wrapper">
|
||||
<VIcon icon="mdi-message" size="24" />
|
||||
</div>
|
||||
<div class="shortcut-text">
|
||||
<div class="shortcut-title">消息</div>
|
||||
<div class="shortcut-subtitle">消息中心</div>
|
||||
</div>
|
||||
<VDivider />
|
||||
<div class="pa-3">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<!-- 循环渲染快捷方式 -->
|
||||
<div v-for="(item, index) in shortcuts" :key="index">
|
||||
<VCard
|
||||
flat
|
||||
variant="tonal"
|
||||
class="pa-2 d-flex align-center rounded-lg cursor-pointer transition-transform duration-300 hover:-translate-y-1 border"
|
||||
hover
|
||||
@click="openDialog(item.dialogRef)"
|
||||
>
|
||||
<VAvatar variant="text" size="48" rounded="lg">
|
||||
<VIcon color="primary" :icon="item.icon" size="24" />
|
||||
</VAvatar>
|
||||
<div>
|
||||
<div class="text-body-1 text-high-emphasis font-weight-medium">{{ item.title }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ item.subtitle }}</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -245,16 +233,14 @@ onMounted(() => {
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="loggingDialog = false" />
|
||||
<VCardItem>
|
||||
<VCardTitle class="inline-flex">
|
||||
<VCardTitle class="d-inline-flex">
|
||||
<VIcon icon="mdi-file-document" class="me-2" />
|
||||
实时日志
|
||||
<a class="mx-2 inline-flex items-center justify-center" :href="allLoggingUrl()" target="_blank">
|
||||
<div
|
||||
class="inline-flex cursor-pointer items-center rounded-full bg-gray-600 px-2 text-sm text-gray-200 ring-1 ring-gray-500 transition hover:bg-gray-700"
|
||||
>
|
||||
<VIcon icon="mdi-open-in-new" />
|
||||
<span class="ms-1">在新窗口中打开</span>
|
||||
</div>
|
||||
<a class="mx-2 d-inline-flex align-center" :href="allLoggingUrl()" target="_blank">
|
||||
<VChip color="grey-darken-1" size="small" class="ml-2">
|
||||
<VIcon icon="mdi-open-in-new" size="small" start />
|
||||
在新窗口中打开
|
||||
</VChip>
|
||||
</a>
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
@@ -333,113 +319,3 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shortcut-menu-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shortcut-header {
|
||||
background: linear-gradient(to right, rgba(var(--v-theme-primary), 0.04), rgba(var(--v-theme-primary), 0.01));
|
||||
border-block-end: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
padding-block: 12px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
.shortcut-close-btn {
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-menu-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.shortcut-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.shortcut-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||
block-size: 48px;
|
||||
inline-size: 48px;
|
||||
margin-inline-end: 16px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.v-icon {
|
||||
color: rgba(var(--v-theme-primary), 1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-item {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.05);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.08) 0%, rgba(var(--v-theme-primary), 0) 60%);
|
||||
content: '';
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(var(--v-theme-primary), 0.15);
|
||||
transform: translateY(-4px);
|
||||
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.shortcut-icon-wrapper {
|
||||
background-color: rgba(var(--v-theme-primary), 0.12);
|
||||
transform: scale(1.1);
|
||||
|
||||
.v-icon {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 3px 10px rgba(var(--v-theme-on-surface), 0.08);
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.shortcut-title {
|
||||
color: rgba(var(--v-theme-on-surface), 0.95);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-block-end: 4px;
|
||||
}
|
||||
|
||||
.shortcut-subtitle {
|
||||
color: rgba(var(--v-theme-on-surface), 0.7);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -41,7 +41,14 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VMenu v-model="appsMenu" width="400" transition="scale-transition" close-on-content-click class="notification-menu" scrim>
|
||||
<VMenu
|
||||
v-model="appsMenu"
|
||||
width="400"
|
||||
transition="scale-transition"
|
||||
close-on-content-click
|
||||
class="notification-menu"
|
||||
scrim
|
||||
>
|
||||
<!-- Menu Activator -->
|
||||
<template #activator="{ props }">
|
||||
<VBadge v-if="hasNewMessage" dot color="error" :offset-x="5" :offset-y="5" v-bind="props">
|
||||
@@ -55,13 +62,12 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
<!-- Menu Content -->
|
||||
<VCard>
|
||||
<VCardItem class="notification-header">
|
||||
<VCardTitle class="font-weight-medium text-primary">通知中心</VCardTitle>
|
||||
<VCardItem class="py-3">
|
||||
<VCardTitle>通知中心</VCardTitle>
|
||||
<template #append>
|
||||
<VTooltip text="设为已读">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn
|
||||
class="mark-read-btn"
|
||||
v-bind="props"
|
||||
@click="
|
||||
() => {
|
||||
@@ -76,89 +82,33 @@ onBeforeUnmount(() => {
|
||||
</VTooltip>
|
||||
</template>
|
||||
</VCardItem>
|
||||
<div v-if="notificationList.length > 0" class="notification-list">
|
||||
<VListItem v-for="(item, i) in notificationList" :key="i" lines="two" class="notification-item">
|
||||
<VDivider />
|
||||
<div v-if="notificationList.length > 0">
|
||||
<VListItem v-for="(item, i) in notificationList" :key="i" lines="two" class="mb-1">
|
||||
<template #prepend>
|
||||
<VAvatar rounded class="notification-avatar">
|
||||
<VAvatar rounded>
|
||||
<VIcon v-if="item.type === 'user'" icon="mdi-account-alert" size="large"></VIcon>
|
||||
<VIcon v-else-if="item.type === 'plugin'" icon="mdi-robot" size="large"></VIcon>
|
||||
<VIcon v-else icon="mdi-laptop" size="large"></VIcon>
|
||||
</VAvatar>
|
||||
</template>
|
||||
<div class="notification-content">
|
||||
<div class="notification-title overflow-visiable break-words whitespace-break-spaces">
|
||||
<div>
|
||||
<div class="text-body-1 text-high-emphasis break-words whitespace-break-spaces">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="notification-text">{{ item.text }}</div>
|
||||
<div class="notification-time">{{ formatDateDifference(item.date) }}</div>
|
||||
<div class="text-caption mt-1.5">
|
||||
{{ item.text }}
|
||||
</div>
|
||||
<div class="text-sm text-primary mt-1.5">
|
||||
{{ formatDateDifference(item.date) }}
|
||||
</div>
|
||||
</div>
|
||||
</VListItem>
|
||||
</div>
|
||||
<div v-else class="no-notification">
|
||||
<div class="text-center">
|
||||
<VIcon icon="mdi-bell-sleep-outline" size="40" class="mb-3 text-primary" />
|
||||
<div>暂无通知</div>
|
||||
</div>
|
||||
<div v-else class="py-8 text-center">
|
||||
<VIcon icon="mdi-bell-sleep-outline" size="40" class="mb-3" />
|
||||
<div>暂无通知</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</VMenu>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notification-header {
|
||||
background: linear-gradient(to right, rgba(var(--v-theme-primary), 0.04), rgba(var(--v-theme-primary), 0.01));
|
||||
border-block-end: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
padding-block: 12px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
padding: 8px;
|
||||
max-block-size: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
.notification-avatar {
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.notification-text {
|
||||
color: rgba(var(--v-theme-on-surface), 0.75);
|
||||
font-size: 0.85rem;
|
||||
margin-block-start: 6px;
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
color: rgba(var(--v-theme-primary), 0.8);
|
||||
font-size: 0.8rem;
|
||||
margin-block-start: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-notification {
|
||||
color: rgba(var(--v-theme-on-surface), 0.6);
|
||||
font-size: 0.95rem;
|
||||
padding-block: 30px;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.mark-read-btn {
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.notification-menu .v-overlay__content {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -86,59 +86,52 @@ const userLevel = computed(() => userStore.level)
|
||||
<VImg :src="avatar" />
|
||||
|
||||
<VMenu activator="parent" width="230" location="bottom end" offset="14px" class="user-menu" scrim>
|
||||
<VList class="overflow-hidden pt-0">
|
||||
<VList class="pt-0">
|
||||
<!-- 👉 User Avatar & Name -->
|
||||
<div class="user-profile-header px-2 py-4 mb-2">
|
||||
<div class="d-flex align-center">
|
||||
<VAvatar size="60" class="user-avatar" color="primary" rounded="sm">
|
||||
<VListItem class="py-4" bg-color="primary" bg-opacity="0.05">
|
||||
<template #prepend>
|
||||
<VAvatar size="60" color="primary" rounded="sm" class="border-2 border-opacity-10">
|
||||
<VImg :src="avatar" />
|
||||
</VAvatar>
|
||||
<div class="ms-4">
|
||||
<div class="user-role">
|
||||
{{ superUser ? '管理员' : '普通用户' }}
|
||||
</div>
|
||||
<div class="user-name">
|
||||
{{ userName }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<span class="text-primary text-sm font-medium d-block">
|
||||
{{ superUser ? '管理员' : '普通用户' }}
|
||||
</span>
|
||||
<span class="text-high-emphasis text-lg font-weight-bold">
|
||||
{{ userName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</VListItem>
|
||||
<VDivider class="mb-2" />
|
||||
<div class="px-2">
|
||||
<!-- 👉 Profile -->
|
||||
<VListItem link @click="router.push('/profile')" class="user-menu-item mb-1">
|
||||
<VListItem link @click="router.push('/profile')" class="mb-1 rounded-lg" hover>
|
||||
<template #prepend>
|
||||
<div class="user-menu-icon">
|
||||
<VIcon icon="mdi-account-outline" />
|
||||
</div>
|
||||
<VIcon icon="mdi-account-outline" />
|
||||
</template>
|
||||
<VListItemTitle>个人信息</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<VListItem link @click="router.push('/apps')" class="user-menu-item mb-1">
|
||||
<VListItem link @click="router.push('/setting')" class="mb-1 rounded-lg" hover>
|
||||
<template #prepend>
|
||||
<div class="user-menu-icon">
|
||||
<VIcon icon="mdi-view-grid-outline" />
|
||||
</div>
|
||||
<VIcon icon="mdi-cog-outline" />
|
||||
</template>
|
||||
<VListItemTitle>功能视图</VListItemTitle>
|
||||
<VListItemTitle>系统设定</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<!-- 👉 Site Auth -->
|
||||
<VListItem v-if="userLevel < 2 && superUser" link @click="showSiteAuthDialog" class="user-menu-item mb-1">
|
||||
<VListItem v-if="userLevel < 2 && superUser" link @click="showSiteAuthDialog" class="mb-1 rounded-lg" hover>
|
||||
<template #prepend>
|
||||
<div class="user-menu-icon">
|
||||
<VIcon icon="mdi-lock-check-outline" />
|
||||
</div>
|
||||
<VIcon icon="mdi-lock-check-outline" />
|
||||
</template>
|
||||
<VListItemTitle>用户认证</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<!-- 👉 FAQ -->
|
||||
<VListItem href="https://wiki.movie-pilot.org" target="_blank" class="user-menu-item mb-1">
|
||||
<VListItem href="https://wiki.movie-pilot.org" target="_blank" class="mb-1 rounded-lg" hover>
|
||||
<template #prepend>
|
||||
<div class="user-menu-icon">
|
||||
<VIcon icon="mdi-help-circle-outline" />
|
||||
</div>
|
||||
<VIcon icon="mdi-help-circle-outline" />
|
||||
</template>
|
||||
<VListItemTitle>帮助文档</VListItemTitle>
|
||||
</VListItem>
|
||||
@@ -147,19 +140,19 @@ const userLevel = computed(() => userStore.level)
|
||||
<VDivider v-if="superUser" class="my-3" />
|
||||
|
||||
<!-- 👉 restart -->
|
||||
<VListItem v-if="superUser" @click="showRestartDialog" class="user-menu-item mb-1">
|
||||
<VListItem v-if="superUser" @click="showRestartDialog" class="mb-1 rounded-lg" hover>
|
||||
<template #prepend>
|
||||
<div class="user-menu-icon restart-icon">
|
||||
<VIcon icon="mdi-restart" />
|
||||
</div>
|
||||
<VIcon icon="mdi-restart" />
|
||||
</template>
|
||||
<VListItemTitle>重启</VListItemTitle>
|
||||
</VListItem>
|
||||
</div>
|
||||
<!-- 👉 Logout -->
|
||||
<div class="px-2 mt-3 mb-2">
|
||||
<VBtn color="error" block class="logout-btn" @click="logout">
|
||||
<template #prepend> <VIcon icon="mdi-logout" /> </template>
|
||||
<VBtn color="error" block class="py-3" elevation="2" @click="logout">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-logout" />
|
||||
</template>
|
||||
退出登录
|
||||
</VBtn>
|
||||
</div>
|
||||
@@ -175,94 +168,21 @@ const userLevel = computed(() => userStore.level)
|
||||
<VDialog v-if="restartDialog" v-model="restartDialog" max-width="25rem">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<div class="flex items-center justify-center mt-3">
|
||||
<div class="d-flex align-center justify-center mt-3">
|
||||
<VAvatar color="warning" variant="text" size="x-large">
|
||||
<VIcon size="x-large" icon="mdi-alert" />
|
||||
</VAvatar>
|
||||
<div class="ms-3">
|
||||
<p class="font-bold text-xl text-high-emphasis">确认重启系统吗?</p>
|
||||
<p class="font-weight-bold text-xl text-high-emphasis">确认重启系统吗?</p>
|
||||
<p>重启后,您将被注销并需要重新登录。</p>
|
||||
</div>
|
||||
</div>
|
||||
</VCardItem>
|
||||
<VCardActions class="mx-auto">
|
||||
<VBtn variant="elevated" color="error" @click="restart" prepend-icon="mdi-restart" class="px-5"> 确定 </VBtn>
|
||||
<VBtn variant="tonal" color="secondary" class="px-5" @click="restartDialog = false">取消</VBtn>
|
||||
<VBtn variant="elevated" color="error" @click="restart" prepend-icon="mdi-restart" class="px-5"> 确定 </VBtn>
|
||||
</VCardActions>
|
||||
<VDialogCloseBtn @click="restartDialog = false" />
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-profile-header {
|
||||
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.05), rgba(var(--v-theme-primary), 0.02));
|
||||
}
|
||||
|
||||
.user-role {
|
||||
color: rgba(var(--v-theme-primary), 0.9);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-block-end: 4px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
color: rgba(var(--v-theme-on-surface), 0.9);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
border: 2px solid rgba(var(--v-theme-on-surface), 0.1);
|
||||
box-shadow: 0 4px 12px rgba(var(--v-theme-primary), 0.2);
|
||||
}
|
||||
|
||||
.user-menu-item {
|
||||
border-radius: 8px;
|
||||
margin-block: 4px;
|
||||
margin-inline: 0;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.06);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
.user-menu-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||
block-size: 36px;
|
||||
inline-size: 36px;
|
||||
margin-inline-end: 12px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.v-icon {
|
||||
color: rgba(var(--v-theme-primary), 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.restart-icon {
|
||||
background-color: rgba(var(--v-theme-error), 0.1);
|
||||
|
||||
.v-icon {
|
||||
color: rgba(var(--v-theme-error), 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(var(--v-theme-error), 0.2);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.user-menu .v-overlay__content {
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 12px rgba(var(--v-theme-on-surface), 0.08) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -136,7 +136,7 @@ onMounted(() => {
|
||||
@dragstart="!display.smAndDown.value && onDragStart($event, action)"
|
||||
@click="display.smAndDown.value && handleComponentClick(action)"
|
||||
>
|
||||
<div class="component-card">
|
||||
<VCard class="component-card">
|
||||
<VAvatar size="36" class="component-avatar">
|
||||
<VIcon :icon="getActionIcon(action.type)" size="18" />
|
||||
</VAvatar>
|
||||
@@ -144,7 +144,7 @@ onMounted(() => {
|
||||
<div class="component-name">{{ action.name }}</div>
|
||||
<div class="component-desc">{{ display.smAndDown.value ? '点击添加' : '拖动到画布' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -171,7 +171,7 @@ onMounted(() => {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f7;
|
||||
background-color: rgb(var(--v-theme-background));
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 8%);
|
||||
inline-size: 280px;
|
||||
inset-block: 0;
|
||||
@@ -201,8 +201,8 @@ onMounted(() => {
|
||||
.sidebar-header {
|
||||
flex-shrink: 0;
|
||||
padding: 16px;
|
||||
background-color: #fff;
|
||||
border-block-end: 1px solid rgba(0, 0, 0, 6%);
|
||||
background-color: rgb(var(--v-theme-background));
|
||||
border-block-end: 1px solid rgba(var(--v-theme-on-background), 0.06);
|
||||
|
||||
.header-content {
|
||||
position: relative;
|
||||
@@ -211,20 +211,20 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.workflow-logo {
|
||||
background-color: #8c58f5;
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
color: white;
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
color: #1a1a1a;
|
||||
color: rgb(var(--v-theme-on-background));
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
position: absolute;
|
||||
color: #8c58f5;
|
||||
color: rgb(var(--v-theme-primary));
|
||||
inset-block-start: 0;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
@@ -245,7 +245,7 @@ onMounted(() => {
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
background-color: rgba(140, 88, 245, 30%);
|
||||
background-color: rgba(var(--v-theme-primary), 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,18 +263,18 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
background-color: #e4e4e7;
|
||||
background-color: rgb(var(--v-theme-surface-variant));
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #d4d4d8;
|
||||
background-color: rgb(var(--v-theme-surface-variant));
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.component-avatar {
|
||||
flex-shrink: 0;
|
||||
background-color: #8c58f5;
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
color: white;
|
||||
margin-inline-end: 12px;
|
||||
|
||||
@@ -291,7 +291,7 @@ onMounted(() => {
|
||||
|
||||
.component-name {
|
||||
overflow: hidden;
|
||||
color: #1a1a1a;
|
||||
color: rgb(var(--v-theme-on-background));
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
@@ -309,21 +309,17 @@ onMounted(() => {
|
||||
.sidebar-footer {
|
||||
flex-shrink: 0;
|
||||
padding: 12px;
|
||||
background-color: #fff;
|
||||
background-color: rgb(var(--v-theme-background));
|
||||
border-block-start: 1px solid rgba(0, 0, 0, 6%);
|
||||
|
||||
.drag-btn {
|
||||
background-color: #8c58f5;
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
block-size: 44px;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
|
||||
&:hover {
|
||||
background-color: color.adjust(#8c58f5, $lightness: -5%);
|
||||
}
|
||||
|
||||
.btn-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -344,8 +340,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.workflow-sidebar-fab {
|
||||
background-color: #8c58f5;
|
||||
box-shadow: 0 4px 10px rgba(140, 88, 245, 40%);
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
box-shadow: 0 4px 10px rgba(var(--v-theme-primary), 40%);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
|
||||
11
src/main.ts
11
src/main.ts
@@ -40,15 +40,8 @@ import CronField from './components/field/CronField.vue'
|
||||
import PathField from './components/field/PathField.vue'
|
||||
import HeaderTab from './layouts/components/HeaderTab.vue'
|
||||
|
||||
// 7. 样式文件
|
||||
import '@core/scss/template/libs/vuetify/index.scss'
|
||||
import 'vuetify/styles'
|
||||
import '@core/scss/template/index.scss'
|
||||
import '@layouts/styles/index.scss'
|
||||
import 'vue-toast-notification/dist/theme-bootstrap.css'
|
||||
import 'vue3-perfect-scrollbar/style.css'
|
||||
import '@vue-js-cron/vuetify/dist/vuetify.css'
|
||||
import '@styles/styles.scss'
|
||||
// 7. 样式文件 - 合并为单一导入
|
||||
import '@/styles/main.scss'
|
||||
|
||||
// 创建Vue实例
|
||||
const app = createApp(App)
|
||||
|
||||
@@ -2,70 +2,110 @@
|
||||
import { NavMenu } from '@/@layouts/types'
|
||||
import { SystemNavMenus } from '@/router/menu'
|
||||
import { useUserStore } from '@/stores'
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
// 从 Store 中获取superuser信息
|
||||
const superUser = useUserStore().superUser
|
||||
|
||||
// APP图标顺序
|
||||
const appOrder = ref<string[]>([])
|
||||
// 应用分组(以header分组)
|
||||
const appGroups = ref<Record<string, NavMenu[]>>({})
|
||||
|
||||
// 根据分类获取菜单列表
|
||||
const getMenuList = () => {
|
||||
return SystemNavMenus.filter((item: NavMenu) => !item.admin || superUser)
|
||||
}
|
||||
|
||||
// APP列表
|
||||
const appList = ref<NavMenu[]>(getMenuList())
|
||||
|
||||
// 保存APP图标顺序到localStorage
|
||||
function saveAppsOrder() {
|
||||
appOrder.value = appList.value.map(app => app.title)
|
||||
localStorage.setItem('MP_APPS_ORDER', JSON.stringify(appOrder.value))
|
||||
// 根据header属性对应用进行分类
|
||||
function categorizeApps() {
|
||||
// 获取可见的菜单项
|
||||
const menus = SystemNavMenus.filter((item: NavMenu) => (!item.admin || superUser) && !item.footer)
|
||||
|
||||
// 按header属性分组
|
||||
const groupedMenus: Record<string, NavMenu[]> = {}
|
||||
|
||||
menus.forEach(menu => {
|
||||
const header = menu.header || '其他'
|
||||
if (!groupedMenus[header]) {
|
||||
groupedMenus[header] = []
|
||||
}
|
||||
groupedMenus[header].push(menu)
|
||||
})
|
||||
|
||||
// 将分组结果赋值给响应式变量
|
||||
appGroups.value = groupedMenus
|
||||
}
|
||||
|
||||
// 页面加载时对应用进行分类
|
||||
onMounted(() => {
|
||||
const localOrder = localStorage.getItem('MP_APPS_ORDER')
|
||||
if (localOrder) {
|
||||
appOrder.value = JSON.parse(localOrder)
|
||||
// 对appList进行排序
|
||||
appList.value.sort((a, b) => {
|
||||
const aIndex = appOrder.value.findIndex(item => item === a.title)
|
||||
const bIndex = appOrder.value.findIndex(item => item === b.title)
|
||||
return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex)
|
||||
})
|
||||
}
|
||||
categorizeApps()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="ps ps--active-y mx-3 appcenter-grid" tabindex="0">
|
||||
<draggable
|
||||
v-model="appList"
|
||||
item-key="title"
|
||||
tag="VRow"
|
||||
delay="300"
|
||||
@end="saveAppsOrder"
|
||||
:component-data="{ 'class': 'ma-0 mt-n1' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<VCol cols="6" md="3" lg="2" class="text-center cursor-pointer shortcut-icon select-none">
|
||||
<VCard class="pa-4" :to="element.to" variant="flat">
|
||||
<VAvatar size="64" variant="text">
|
||||
<VIcon size="48" :icon="element.icon" color="primary" />
|
||||
</VAvatar>
|
||||
<h6 class="text-base font-weight-medium mt-2 mb-0">{{ element.full_title || element.title }}</h6>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</template>
|
||||
</draggable>
|
||||
<div class="app-settings-container">
|
||||
<VContainer>
|
||||
<!-- 遍历所有分组 -->
|
||||
<div v-for="(apps, header) in appGroups" :key="header" class="mb-3">
|
||||
<VListSubheader class="ps-1">
|
||||
{{ header }}
|
||||
</VListSubheader>
|
||||
<!-- 分组内容 - 使用卡片包装 -->
|
||||
<VCard variant="flat" class="settings-section-card">
|
||||
<VList lines="one" class="settings-list">
|
||||
<VListItem
|
||||
v-for="(app, appIndex) in apps"
|
||||
:key="appIndex"
|
||||
:to="app.to || ''"
|
||||
color="primary"
|
||||
class="settings-list-item"
|
||||
rounded="0"
|
||||
>
|
||||
<template #prepend>
|
||||
<VAvatar size="42" color="primary" variant="text" class="me-3">
|
||||
<VIcon :icon="app.icon as string" size="24"></VIcon>
|
||||
</VAvatar>
|
||||
</template>
|
||||
|
||||
<VListItemTitle class="font-weight-medium">
|
||||
{{ app.full_title || app.title }}
|
||||
</VListItemTitle>
|
||||
|
||||
<VListItemSubtitle v-if="app.description">
|
||||
{{ app.description }}
|
||||
</VListItemSubtitle>
|
||||
|
||||
<template #append>
|
||||
<VIcon icon="mdi-chevron-right"></VIcon>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCard>
|
||||
</div>
|
||||
</VContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style type="scss" scoped>
|
||||
.appcenter-grid .v-card {
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
backdrop-filter: blur(6px);
|
||||
background-color: rgb(var(--v-theme-surface), 0.8);
|
||||
<style lang="scss" scoped>
|
||||
.app-settings-container {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings-section-card {
|
||||
overflow: hidden;
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.settings-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.settings-list-item {
|
||||
padding: 8px 12px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid rgba(var(--v-border-color), 0.12);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.05);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { DashboardItem } from '@/api/types'
|
||||
import { useUserStore } from '@/stores'
|
||||
import DashboardElement from '@/components/misc/DashboardElement.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
|
||||
// APP
|
||||
const display = useDisplay()
|
||||
@@ -141,6 +142,14 @@ const pluginDashboardRefreshStatus = ref<{ [key: string]: boolean }>({})
|
||||
// 弹窗
|
||||
const dialog = ref(false)
|
||||
|
||||
// 使用动态按钮钩子
|
||||
useDynamicButton({
|
||||
icon: 'mdi-view-dashboard-edit',
|
||||
onClick: () => {
|
||||
dialog.value = true
|
||||
},
|
||||
})
|
||||
|
||||
// 加载用户监控面板配置(本地无配置时才加载)
|
||||
async function loadDashboardConfig() {
|
||||
// 显示配置
|
||||
@@ -297,7 +306,7 @@ onBeforeMount(async () => {
|
||||
getPluginDashboardMeta()
|
||||
})
|
||||
|
||||
onActivated(async () => {
|
||||
onActivated(() => {
|
||||
isRequest.value = true
|
||||
})
|
||||
|
||||
@@ -327,8 +336,9 @@ onDeactivated(() => {
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<!-- 底部操作按钮(只在非移动设备上显示) -->
|
||||
<VFab
|
||||
v-if="!appMode"
|
||||
icon="mdi-view-dashboard-edit"
|
||||
location="bottom"
|
||||
size="x-large"
|
||||
@@ -336,7 +346,6 @@ onDeactivated(() => {
|
||||
app
|
||||
appear
|
||||
@click="dialog = true"
|
||||
:class="{ 'mb-12': appMode }"
|
||||
/>
|
||||
|
||||
<!-- 弹窗,根据配置生成选项 -->
|
||||
|
||||
@@ -35,28 +35,12 @@ const isPasswordVisible = ref(false)
|
||||
// 错误信息
|
||||
const errorMessage = ref('')
|
||||
|
||||
// 背景图片 URL 和预加载 URL
|
||||
const backgroundImages = ref<string[]>([])
|
||||
const activeImageIndex = ref(0)
|
||||
|
||||
// 是否开启双重验证
|
||||
const isOTP = ref(false)
|
||||
|
||||
// 用户名称输入框
|
||||
const usernameInput = ref()
|
||||
|
||||
// Interval定时器
|
||||
let intervalTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 获取背景图片
|
||||
async function fetchBackgroundImage() {
|
||||
try {
|
||||
backgroundImages.value = await api.get('/login/wallpapers')
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询是否开启双重验证
|
||||
const fetchOTP = debounce(async () => {
|
||||
const userid = usernameInput.value?.value
|
||||
@@ -180,13 +164,6 @@ function login() {
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化背景图片轮循
|
||||
function startBackgroundRotation() {
|
||||
intervalTimer = setInterval(() => {
|
||||
activeImageIndex.value = (activeImageIndex.value + 1) % backgroundImages.value.length
|
||||
}, 5000) // 每5秒切换一次图片
|
||||
}
|
||||
|
||||
// 自动登录
|
||||
onMounted(async () => {
|
||||
// 获取token和remember状态
|
||||
@@ -196,39 +173,20 @@ onMounted(async () => {
|
||||
// 如果token存在,且保持登录状态为true,则跳转到首页
|
||||
if (token && remember) {
|
||||
router.push('/')
|
||||
} else {
|
||||
// 获取背景图片
|
||||
await fetchBackgroundImage()
|
||||
if (backgroundImages.value.length > 1) {
|
||||
startBackgroundRotation()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (intervalTimer) clearInterval(intervalTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 当前背景图片 -->
|
||||
<div class="relative flex min-h-screen flex-col bg-gray-900 items-center justify-center">
|
||||
<div>
|
||||
<div
|
||||
v-for="(imageUrl, index) in backgroundImages"
|
||||
class="absolute-top-shift absolute inset-0 bg-cover bg-center transition-opacity duration-300 ease-in"
|
||||
:class="{ 'opacity-100': index === activeImageIndex, 'opacity-0': index !== activeImageIndex }"
|
||||
>
|
||||
<VImg :src="imageUrl" class="absolute inset-0 transition-opacity duration-1000" cover position="center top" />
|
||||
<div
|
||||
class="absolute inset-0"
|
||||
style="background-image: linear-gradient(rgba(45, 55, 72, 47%) 0%, rgb(26, 32, 46) 100%)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 登录页面容器 -->
|
||||
<div class="relative flex min-h-screen flex-col items-center justify-center">
|
||||
<!-- 登录表单 -->
|
||||
<div class="auth-wrapper d-flex align-center justify-center">
|
||||
<VCard class="auth-card px-7 py-3 w-full h-full opacity-85" max-width="24rem">
|
||||
<VCard
|
||||
class="auth-card px-7 py-3 w-full h-full"
|
||||
:class="{ 'opacity-75': globalTheme.name.value !== 'transparent' }"
|
||||
max-width="24rem"
|
||||
>
|
||||
<VCardItem class="justify-center">
|
||||
<template #prepend>
|
||||
<div class="d-flex pe-0">
|
||||
@@ -295,10 +253,6 @@ onUnmounted(() => {
|
||||
padding-inline-end: 0 !important;
|
||||
}
|
||||
|
||||
.absolute-top-shift {
|
||||
inset-block-start: calc(-4rem - env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.auth-wrapper {
|
||||
overflow: hidden;
|
||||
block-size: auto;
|
||||
|
||||
@@ -183,7 +183,7 @@ onUnmounted(() => {
|
||||
<!-- 加载进度条 -->
|
||||
<VFadeTransition>
|
||||
<div v-if="progressValue > 0" class="search-progress-container">
|
||||
<VCard class="search-progress-card">
|
||||
<VCard elevation="3" class="search-progress-card">
|
||||
<div class="progress-header">
|
||||
<VIcon icon="mdi-movie-search" color="primary" size="small" class="me-2" />
|
||||
<span class="progress-title">{{ progressText }}</span>
|
||||
@@ -197,7 +197,7 @@ onUnmounted(() => {
|
||||
</VFadeTransition>
|
||||
|
||||
<!-- 精简标题栏 -->
|
||||
<VCard v-if="isRefreshed" class="search-header d-flex align-center mb-4">
|
||||
<VCard v-if="isRefreshed" class="search-header d-flex align-center mb-3">
|
||||
<div class="search-info-container d-flex align-center flex-wrap">
|
||||
<div class="search-title text-primary">资源搜索结果</div>
|
||||
<div class="search-tags d-flex flex-wrap">
|
||||
@@ -225,7 +225,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 视图切换加载状态 -->
|
||||
<VFadeTransition>
|
||||
<div v-if="isRefreshed && isViewChanging" class="view-changing-container">
|
||||
<div v-if="isRefreshed && isViewChanging" class="view-changing-container rounded-lg">
|
||||
<div class="view-changing-content">
|
||||
<div class="pulse-loader">
|
||||
<div class="pulse-circle"></div>
|
||||
@@ -327,7 +327,6 @@ onUnmounted(() => {
|
||||
/* 精简标题栏样式 */
|
||||
.search-header {
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 5%);
|
||||
padding-block: 12px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
@@ -390,7 +389,6 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(8px);
|
||||
background-color: rgba(var(--v-theme-background), 0.7);
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,24 +24,10 @@ function jumpTab(tab: string) {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VTabs v-model="activeTab" show-arrows class="v-tabs-pill">
|
||||
<VTab
|
||||
v-for="item in SettingTabs"
|
||||
:key="item.icon"
|
||||
:value="item.tab"
|
||||
@click="jumpTab(item.tab)"
|
||||
selected-class="v-slide-group-item--active v-tab--selected"
|
||||
>
|
||||
<div class="flex align-center">
|
||||
<VIcon size="20" start :icon="item.icon" />
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</VTab>
|
||||
</VTabs>
|
||||
|
||||
<VHeaderTab :items="SettingTabs" v-model="activeTab" />
|
||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<!-- 系统 -->
|
||||
<VWindowItem value="system">
|
||||
<VWindowItem value="系统">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingSystem />
|
||||
@@ -50,7 +36,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 目录 -->
|
||||
<VWindowItem value="directory">
|
||||
<VWindowItem value="存储 & 目录">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingDirectory />
|
||||
@@ -59,7 +45,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 站点 -->
|
||||
<VWindowItem value="site">
|
||||
<VWindowItem value="站点">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingSite />
|
||||
@@ -68,7 +54,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 规则 -->
|
||||
<VWindowItem value="rule">
|
||||
<VWindowItem value="规则">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingRule />
|
||||
@@ -77,7 +63,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 搜索 -->
|
||||
<VWindowItem value="search">
|
||||
<VWindowItem value="搜索 & 下载">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingSearch />
|
||||
@@ -86,7 +72,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 订阅 -->
|
||||
<VWindowItem value="subscribe">
|
||||
<VWindowItem value="订阅">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingSubscribe />
|
||||
@@ -95,7 +81,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 服务 -->
|
||||
<VWindowItem value="scheduler">
|
||||
<VWindowItem value="服务">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingScheduler />
|
||||
@@ -104,7 +90,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 通知 -->
|
||||
<VWindowItem value="notification">
|
||||
<VWindowItem value="通知">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingNotification />
|
||||
@@ -113,7 +99,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 词表 -->
|
||||
<VWindowItem value="words">
|
||||
<VWindowItem value="词表">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingWords />
|
||||
@@ -122,7 +108,7 @@ function jumpTab(tab: string) {
|
||||
</VWindowItem>
|
||||
|
||||
<!-- 关于 -->
|
||||
<VWindowItem value="about">
|
||||
<VWindowItem value="关于">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<AccountSettingAbout />
|
||||
|
||||
@@ -26,6 +26,7 @@ export default {
|
||||
VBtn: {
|
||||
// set v-btn default color to primary
|
||||
color: 'primary',
|
||||
elevation: 0,
|
||||
},
|
||||
VCard: {
|
||||
elevation: 0,
|
||||
@@ -40,6 +41,10 @@ export default {
|
||||
VBottomSheet: {
|
||||
elevation: 0,
|
||||
},
|
||||
VDialog: {
|
||||
elevation: 0,
|
||||
rounded: 'lg',
|
||||
},
|
||||
VExpansionPanels: {
|
||||
elevation: 0,
|
||||
},
|
||||
@@ -85,6 +90,7 @@ export default {
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
menuProps: { elevation: 0 },
|
||||
},
|
||||
VRangeSlider: {
|
||||
// set v-range-slider default color to primary
|
||||
@@ -122,6 +128,7 @@ export default {
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
hideDetails: 'auto',
|
||||
menuProps: { elevation: 0 },
|
||||
},
|
||||
VFileInput: {
|
||||
variant: 'outlined',
|
||||
|
||||
@@ -8,7 +8,7 @@ const theme: VuetifyOptions['theme'] = {
|
||||
colors: {
|
||||
'primary': '#9155FD',
|
||||
'secondary': '#8A8D93',
|
||||
'on-secondary': '#fff',
|
||||
'on-secondary': '#FFFFFF',
|
||||
'success': '#56CA00',
|
||||
'info': '#16B1FF',
|
||||
'warning': '#FFB400',
|
||||
@@ -30,12 +30,12 @@ const theme: VuetifyOptions['theme'] = {
|
||||
'grey-800': '#424242',
|
||||
'grey-900': '#212121',
|
||||
'perfect-scrollbar-thumb': '#DBDADE',
|
||||
'skin-bordered-background': '#fff',
|
||||
'skin-bordered-surface': '#fff',
|
||||
'skin-bordered-background': '#FFFFFF',
|
||||
'skin-bordered-surface': '#FFFFFF',
|
||||
},
|
||||
|
||||
variables: {
|
||||
'code-color': '#d400ff',
|
||||
'code-color': '#D400FF',
|
||||
'overlay-scrim-background': '#3A3541',
|
||||
'overlay-scrim-opacity': 0.5,
|
||||
'hover-opacity': 0.04,
|
||||
@@ -59,7 +59,7 @@ const theme: VuetifyOptions['theme'] = {
|
||||
colors: {
|
||||
'primary': '#6E66ED',
|
||||
'secondary': '#8A8D93',
|
||||
'on-secondary': '#fff',
|
||||
'on-secondary': '#FFFFFF',
|
||||
'success': '#56CA00',
|
||||
'info': '#16B1FF',
|
||||
'warning': '#FFB400',
|
||||
@@ -109,7 +109,7 @@ const theme: VuetifyOptions['theme'] = {
|
||||
colors: {
|
||||
'primary': '#9155FD',
|
||||
'secondary': '#8A8D93',
|
||||
'on-secondary': '#fff',
|
||||
'on-secondary': '#FFFFFF',
|
||||
'success': '#56CA00',
|
||||
'info': '#16B1FF',
|
||||
'warning': '#FFB400',
|
||||
@@ -155,6 +155,61 @@ const theme: VuetifyOptions['theme'] = {
|
||||
'shadow-key-ambient-opacity': 'rgba(20, 18, 33, 0.04)',
|
||||
},
|
||||
},
|
||||
transparent: {
|
||||
dark: true,
|
||||
colors: {
|
||||
'primary': '#A370F7',
|
||||
'secondary': '#8A8D93',
|
||||
'on-secondary': '#FFFFFF',
|
||||
'success': '#66BB6A',
|
||||
'info': '#42A5F5',
|
||||
'warning': '#FFA726',
|
||||
'error': '#EF5350',
|
||||
'on-primary': '#FFFFFF',
|
||||
'on-success': '#FFFFFF',
|
||||
'on-warning': '#FFFFFF',
|
||||
'background': '#000000',
|
||||
'on-background': '#E7E3FC',
|
||||
'surface': 'rgba(30, 30, 30, 0.3)',
|
||||
'on-surface': '#E7E3FC',
|
||||
'surface-variant': 'rgba(30, 30, 30, 0.2)',
|
||||
'on-surface-variant': 'rgba(255, 255, 255, 0.65)',
|
||||
'grey-50': 'rgba(42, 46, 66, 0.15)',
|
||||
'grey-100': 'rgba(71, 67, 96, 0.15)',
|
||||
'grey-200': 'rgba(74, 80, 114, 0.15)',
|
||||
'grey-300': 'rgba(94, 102, 146, 0.15)',
|
||||
'grey-400': 'rgba(121, 131, 187, 0.15)',
|
||||
'grey-500': 'rgba(134, 146, 208, 0.15)',
|
||||
'grey-600': 'rgba(170, 179, 222, 0.15)',
|
||||
'grey-700': 'rgba(182, 190, 227, 0.15)',
|
||||
'grey-800': 'rgba(207, 211, 236, 0.15)',
|
||||
'grey-900': 'rgba(231, 233, 246, 0.15)',
|
||||
'perfect-scrollbar-thumb': 'rgba(158, 158, 190, 0.4)',
|
||||
'skin-bordered-background': 'rgba(30, 30, 30, 0.3)',
|
||||
'skin-bordered-surface': 'rgba(30, 30, 30, 0.3)',
|
||||
'card-background': 'rgba(30, 30, 30, 0.3)',
|
||||
},
|
||||
variables: {
|
||||
'code-color': '#6D9EEB',
|
||||
'overlay-scrim-background': '0, 0, 0',
|
||||
'overlay-scrim-opacity': 0.7,
|
||||
'hover-opacity': 0.1,
|
||||
'focus-opacity': 0.15,
|
||||
'selected-opacity': 0.2,
|
||||
'activated-opacity': 0.15,
|
||||
'pressed-opacity': 0.2,
|
||||
'dragged-opacity': 0.15,
|
||||
'border-color': '#E7E3FC',
|
||||
'table-header-background': 'rgba(30, 30, 30, 0.3)',
|
||||
'custom-background': 'rgba(30, 30, 30, 0.3)',
|
||||
'card-background': 'rgba(30, 30, 30, 0.3)',
|
||||
|
||||
// Shadows
|
||||
'shadow-key-umbra-opacity': 'rgba(0, 0, 0, 0.07)',
|
||||
'shadow-key-penumbra-opacity': 'rgba(0, 0, 0, 0.1)',
|
||||
'shadow-key-ambient-opacity': 'rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
webFontLoader.load({
|
||||
google: {
|
||||
families: ['Inter:100,200,300,400,500,600,700&display=swap'],
|
||||
families: ['Inter:100,200,300,400,500,600,700&display=swap', 'Noto+Sans+SC:400,500,700&display=swap'],
|
||||
},
|
||||
})
|
||||
})()
|
||||
|
||||
@@ -29,7 +29,7 @@ export const SystemNavMenus = [
|
||||
to: '/discover',
|
||||
header: '发现',
|
||||
admin: false,
|
||||
footer: false,
|
||||
footer: true,
|
||||
},
|
||||
{
|
||||
title: '电影',
|
||||
@@ -38,7 +38,7 @@ export const SystemNavMenus = [
|
||||
to: '/subscribe/movie',
|
||||
header: '订阅',
|
||||
admin: false,
|
||||
footer: true,
|
||||
footer: false,
|
||||
},
|
||||
{
|
||||
title: '电视剧',
|
||||
@@ -47,7 +47,7 @@ export const SystemNavMenus = [
|
||||
to: '/subscribe/tv',
|
||||
header: '订阅',
|
||||
admin: false,
|
||||
footer: true,
|
||||
footer: false,
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ html.v-overlay-scroll-blocked {
|
||||
@media (width <= 768px){
|
||||
html.v-overlay-scroll-blocked {
|
||||
position: relative;
|
||||
|
||||
--v-body-scroll-y: 0px !important;
|
||||
}
|
||||
}
|
||||
@@ -251,8 +252,6 @@ html.v-overlay-scroll-blocked {
|
||||
|
||||
.card-cover-blurred::before {
|
||||
position: absolute;
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
backdrop-filter: blur(2px);
|
||||
background: rgba(29, 39, 59, 48%);
|
||||
content: '';
|
||||
@@ -260,28 +259,24 @@ html.v-overlay-scroll-blocked {
|
||||
}
|
||||
|
||||
.v-overlay__content .v-list{
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
backdrop-filter: blur(6px);
|
||||
background-color: rgb(var(--v-theme-surface), 0.9) !important;
|
||||
}
|
||||
|
||||
.v-overlay__content .v-card:not(.bg-primary){
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
background-color: rgb(var(--v-theme-surface), 0.95) !important;
|
||||
|
||||
.v-list, .v-table {
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-backdrop-filter: none;
|
||||
backdrop-filter: none;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-item:hover {
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.04) !important;
|
||||
.v-menu {
|
||||
.v-list-item:hover {
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.04) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-btn.v-btn--icon {
|
||||
@@ -322,3 +317,112 @@ html.v-overlay-scroll-blocked {
|
||||
.v-infinite-scroll__side {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.v-menu .v-overlay__content {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
|
||||
// 透明主题特殊样式
|
||||
.transparent-app {
|
||||
// 先将所有全局组件定义放在前面,避免CSS优先级问题
|
||||
.v-application, .v-layout, .v-main, .layout-page-content {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// 侧边导航栏
|
||||
.layout-vertical-nav {
|
||||
backdrop-filter: blur(16px);
|
||||
background-color: rgba(var(--v-theme-surface), 0.2);
|
||||
border-inline-end: 1px solid rgba(var(--v-theme-on-surface), 0.05);
|
||||
}
|
||||
|
||||
// 列表
|
||||
.v-list {
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(var(--v-theme-surface), 0.3);
|
||||
}
|
||||
|
||||
// 卡片
|
||||
.v-card:not(.no-blur) {
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(var(--v-theme-surface), 0.3);
|
||||
|
||||
.v-list {
|
||||
backdrop-filter: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// 工具栏
|
||||
.v-toolbar {
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(var(--v-theme-surface), 0.3);
|
||||
}
|
||||
|
||||
// 表格
|
||||
.v-table {
|
||||
border-radius: 0;
|
||||
background-color: rgba(var(--v-theme-surface), 0.3);
|
||||
}
|
||||
|
||||
// 页脚
|
||||
.v-footer {
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(var(--v-theme-surface), 0.3);
|
||||
}
|
||||
|
||||
// Sheet
|
||||
.v-sheet {
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(var(--v-theme-surface), 0.3);
|
||||
}
|
||||
|
||||
// 页面容器
|
||||
.layout-content-wrapper {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// 无内容区域的背景设为透明
|
||||
.page-content-container {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// 对话框和菜单蒙层样式
|
||||
.v-overlay__scrim {
|
||||
background: rgba(var(--v-overlay-scrim-background), var(--v-overlay-scrim-opacity));
|
||||
}
|
||||
|
||||
// 折叠面板
|
||||
.v-expansion-panel {
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(var(--v-theme-surface), 0.3);
|
||||
}
|
||||
|
||||
// 加载占位
|
||||
.v-skeleton-loader {
|
||||
background-color: rgba(var(--v-theme-surface), 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// 透明主题下的弹出窗口样式
|
||||
html[data-theme="transparent"] {
|
||||
.v-overlay__content {
|
||||
border-radius: 12px !important;
|
||||
backdrop-filter: blur(10px) !important;
|
||||
|
||||
.v-list {
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgb(var(--v-theme-surface), 0.5) !important;
|
||||
}
|
||||
|
||||
.v-card:not(.bg-primary) {
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgb(var(--v-theme-surface), 0.5) !important;
|
||||
}
|
||||
|
||||
.v-table thead {
|
||||
background-color: rgb(var(--v-theme-surface), 0.5) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/styles/main.scss
Normal file
13
src/styles/main.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
/* 主样式文件 - 合并所有CSS/SCSS引用 */
|
||||
|
||||
/* Vuetify和模板核心样式 */
|
||||
@use '@core/scss/template/libs/vuetify/index' as vuetify-lib;
|
||||
@use '@core/scss/template/index' as template;
|
||||
@use '@layouts/styles/index' as layouts;
|
||||
@use 'vuetify/styles' as vuetify;
|
||||
@use '@styles/custom' as custom;
|
||||
|
||||
/* 第三方库纯CSS样式 */
|
||||
@import 'vue-toast-notification/dist/theme-bootstrap.css';
|
||||
@import 'vue3-perfect-scrollbar/style.css';
|
||||
@import '@vue-js-cron/vuetify/dist/vuetify.css';
|
||||
@@ -109,7 +109,7 @@ async function fetchData({ done }: { done: any }) {
|
||||
|
||||
<template>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-hidden" @load="fetchData">
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible pt-3" @load="fetchData">
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-media-card" tabindex="0">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { useUserStore } from '@/stores'
|
||||
import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
|
||||
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
|
||||
// 输入参数
|
||||
const mediaProps = defineProps({
|
||||
@@ -30,6 +31,9 @@ const userStore = useUserStore()
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 获取主题信息
|
||||
const theme = useTheme()
|
||||
|
||||
// 媒体详情
|
||||
const mediaDetail = ref<MediaInfo>({} as MediaInfo)
|
||||
|
||||
@@ -72,6 +76,11 @@ const searchType = ref('title')
|
||||
// 选择站点对话框
|
||||
const chooseSiteDialog = ref(false)
|
||||
|
||||
// 计算主题是否为透明
|
||||
const isNonTransparentTheme = computed(() => {
|
||||
return theme.name.value !== 'transparent'
|
||||
})
|
||||
|
||||
// 查询所有站点
|
||||
async function querySites() {
|
||||
try {
|
||||
@@ -498,10 +507,14 @@ function onSubscribeEditRemove() {
|
||||
async function clickSearch(type: string) {
|
||||
searchType.value = type
|
||||
if (allSites.value?.length == 0) {
|
||||
querySites()
|
||||
querySelectedSites()
|
||||
await querySites()
|
||||
await querySelectedSites()
|
||||
}
|
||||
if (allSites.value?.length > 0) {
|
||||
chooseSiteDialog.value = true
|
||||
} else {
|
||||
handleSearch()
|
||||
}
|
||||
chooseSiteDialog.value = true
|
||||
}
|
||||
|
||||
// 搜索多站点
|
||||
@@ -519,7 +532,7 @@ onBeforeMount(() => {
|
||||
<template>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" class="max-w-8xl mx-auto px-4">
|
||||
<template v-if="getBackdropUrl || getPosterUrl">
|
||||
<template v-if="(getBackdropUrl || getPosterUrl) && isNonTransparentTheme">
|
||||
<div class="vue-media-back absolute left-0 top-0 w-full h-96">
|
||||
<VImg class="h-96" position="top" :src="getBackdropUrl || getPosterUrl" cover />
|
||||
</div>
|
||||
@@ -972,7 +985,6 @@ onBeforeMount(() => {
|
||||
),
|
||||
linear-gradient(90deg, rgba(var(--v-theme-background), 0) 50%, rgba(var(--v-theme-background), 1) 100%),
|
||||
linear-gradient(270deg, rgba(var(--v-theme-background), 0) 50%, rgba(var(--v-theme-background), 1) 100%);
|
||||
box-shadow: 0 0 0 2px rgb(var(--v-theme-background));
|
||||
margin-block-start: calc(-70px - env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ async function fetchData({ done }: { done: any }) {
|
||||
|
||||
<template>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-hidden" @load="fetchData">
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible" @load="fetchData">
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-media-card" tabindex="0">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user