mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-09 05:42:39 +08:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb36033939 | ||
|
|
584e7672df | ||
|
|
d4f7a5a1c0 | ||
|
|
2a9ea81ad4 | ||
|
|
276948dd68 | ||
|
|
990c5583f2 | ||
|
|
644f1b5640 | ||
|
|
5261fbe870 | ||
|
|
e4f2d85e2b | ||
|
|
8e3ccdc24a | ||
|
|
cd6d93affd | ||
|
|
6096ab0c9b | ||
|
|
0a87bb1db1 | ||
|
|
a19042c655 | ||
|
|
a889687a6a | ||
|
|
e1cdc715aa | ||
|
|
a82b3a0a29 | ||
|
|
d93a71f0be | ||
|
|
899dc765bc | ||
|
|
449490e52d | ||
|
|
5541d7974e | ||
|
|
ae3eb36183 | ||
|
|
d57e9a397c | ||
|
|
9d4fd16d81 | ||
|
|
3b16e7a123 | ||
|
|
1c4a2176e9 | ||
|
|
62f9243714 | ||
|
|
03bd23d314 | ||
|
|
27497d1812 | ||
|
|
f36c1bd2b5 | ||
|
|
cf72b2cdb9 | ||
|
|
44f6950fea | ||
|
|
308ddfedea | ||
|
|
ac7c330e2f | ||
|
|
1bde3492da | ||
|
|
f884518df3 | ||
|
|
1f7f9ce9db |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
name: ${{ env.frontend_version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
make_latest: false
|
||||
make_latest: true
|
||||
files: |
|
||||
dist.zip
|
||||
env:
|
||||
|
||||
688
index.html
688
index.html
@@ -1,344 +1,430 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" style="
|
||||
<html
|
||||
lang="zh-CN"
|
||||
style="
|
||||
overflow: hidden auto;
|
||||
min-block-size: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
||||
--safe-area-inset-bottom: env(safe-area-inset-bottom);
|
||||
--safe-area-inset-top: env(safe-area-inset-top);
|
||||
background: var(--initial-loader-bg, #fff);
|
||||
">
|
||||
"
|
||||
>
|
||||
<head>
|
||||
<title>MoviePilot</title>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- 核心viewport设置 - 针对PWA优化 -->
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=no"
|
||||
/>
|
||||
|
||||
<head>
|
||||
<title>MoviePilot</title>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- 核心viewport设置 - 针对PWA优化 -->
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=no" />
|
||||
<!-- 防止缩放和选择,提供原生应用体验 -->
|
||||
<meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
|
||||
|
||||
<!-- 防止缩放和选择,提供原生应用体验 -->
|
||||
<meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
|
||||
<!-- 基础信息 -->
|
||||
<meta name="description" content="MoviePilot - 智能影视媒体库管理工具" />
|
||||
<meta name="author" content="MoviePilot" />
|
||||
<meta name="keywords" content="MoviePilot,影视,媒体库,管理" />
|
||||
|
||||
<!-- 基础信息 -->
|
||||
<meta name="description" content="MoviePilot - 智能影视媒体库管理工具" />
|
||||
<meta name="author" content="MoviePilot" />
|
||||
<meta name="keywords" content="MoviePilot,影视,媒体库,管理" />
|
||||
<!-- 安全和隐私 -->
|
||||
<meta name="Robots" content="noindex,nofollow,noarchive" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
|
||||
<!-- 安全和隐私 -->
|
||||
<meta name="Robots" content="noindex,nofollow,noarchive" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<!-- PWA - 基础图标 -->
|
||||
<link rel="icon" type="image/png" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" sizes="any" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
|
||||
<!-- PWA - 基础图标 -->
|
||||
<link rel="icon" type="image/png" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" sizes="any" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<!-- iOS Safari PWA 优化 -->
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="apple-touch-icon-precomposed" href="/apple-touch-icon-precomposed.png" />
|
||||
<link rel="apple-touch-startup-image" href="/splash/apple-splash.png" />
|
||||
|
||||
<!-- iOS Safari PWA 优化 -->
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="apple-touch-icon-precomposed" href="/apple-touch-icon-precomposed.png" />
|
||||
<link rel="apple-touch-startup-image" href="/splash/apple-splash.png" />
|
||||
<!-- iOS Safari 全屏模式 -->
|
||||
<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" />
|
||||
|
||||
<!-- iOS Safari 全屏模式 -->
|
||||
<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" />
|
||||
<!-- iOS Safari 防止自动识别 -->
|
||||
<meta name="apple-mobile-web-app-orientations" content="portrait" />
|
||||
|
||||
<!-- iOS Safari 防止自动识别 -->
|
||||
<meta name="apple-mobile-web-app-orientations" content="portrait" />
|
||||
<!-- Android Chrome PWA 优化 -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="mobile-web-app-title" content="MoviePilot" />
|
||||
|
||||
<!-- Android Chrome PWA 优化 -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="mobile-web-app-title" content="MoviePilot" />
|
||||
<!-- Microsoft Windows PWA -->
|
||||
<meta name="msapplication-TileColor" content="#0E1116" />
|
||||
<meta name="msapplication-TileImage" content="/android-chrome-192x192.png" />
|
||||
<meta name="msapplication-config" content="none" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
<meta name="msapplication-navbutton-color" content="#0E1116" />
|
||||
|
||||
<!-- Microsoft Windows PWA -->
|
||||
<meta name="msapplication-TileColor" content="#0E1116" />
|
||||
<meta name="msapplication-TileImage" content="/android-chrome-192x192.png" />
|
||||
<meta name="msapplication-config" content="none" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
<meta name="msapplication-navbutton-color" content="#0E1116" />
|
||||
<!-- 主题色彩 - 适配深色和浅色模式 -->
|
||||
<meta name="theme-color" content="#0E1116" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)" />
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
|
||||
<!-- 主题色彩 - 适配深色和浅色模式 -->
|
||||
<meta name="theme-color" content="#0E1116" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#F4F5FA" media="(prefers-color-scheme: light)" />
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<!-- 屏幕方向锁定 -->
|
||||
<meta name="screen-orientation" content="portrait" />
|
||||
<meta name="x5-orientation" content="portrait" />
|
||||
<meta name="x5-fullscreen" content="true" />
|
||||
<meta name="x5-page-mode" content="app" />
|
||||
|
||||
<!-- 屏幕方向锁定 -->
|
||||
<meta name="screen-orientation" content="portrait" />
|
||||
<meta name="x5-orientation" content="portrait" />
|
||||
<meta name="x5-fullscreen" content="true" />
|
||||
<meta name="x5-page-mode" content="app" />
|
||||
<!-- UC浏览器优化 -->
|
||||
<meta name="browsermode" content="application" />
|
||||
<meta name="wap-font-scale" content="no" />
|
||||
|
||||
<!-- UC浏览器优化 -->
|
||||
<meta name="browsermode" content="application" />
|
||||
<meta name="wap-font-scale" content="no" />
|
||||
<!-- 360浏览器优化 -->
|
||||
<meta name="renderer" content="webkit" />
|
||||
|
||||
<!-- 360浏览器优化 -->
|
||||
<meta name="renderer" content="webkit" />
|
||||
<!-- 触摸优化 -->
|
||||
<meta name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
|
||||
<!-- 触摸优化 -->
|
||||
<meta name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<!-- 缓存控制 -->
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
|
||||
<!-- 缓存控制 -->
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<!-- DNS预解析和预连接 -->
|
||||
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
|
||||
<link rel="dns-prefetch" href="//cdn.jsdelivr.net" />
|
||||
<link rel="dns-prefetch" href="//image.tmdb.org" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
|
||||
|
||||
<!-- DNS预解析和预连接 -->
|
||||
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
|
||||
<link rel="dns-prefetch" href="//cdn.jsdelivr.net" />
|
||||
<link rel="dns-prefetch" href="//image.tmdb.org" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
|
||||
<!-- 预加载关键资源 -->
|
||||
<link rel="preload" href="/logo.png" as="image" />
|
||||
<link rel="modulepreload" href="/src/main.ts" />
|
||||
|
||||
<!-- 预加载关键资源 -->
|
||||
<link rel="preload" href="/logo.png" as="image" />
|
||||
<link rel="modulepreload" href="/src/main.ts" />
|
||||
|
||||
<!-- 内联关键CSS -->
|
||||
<style>
|
||||
/* 关键路径CSS - 从loader.css内联 */
|
||||
#loading-bg {
|
||||
position: fixed;
|
||||
z-index: 99999;
|
||||
display: block;
|
||||
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 {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 50%;
|
||||
block-size: 55px;
|
||||
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,
|
||||
.loading .effect-2,
|
||||
.loading .effect-3 {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 50%;
|
||||
block-size: 100%;
|
||||
border-inline-start: 3px solid var(--initial-loader-color, #eee);
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.loading .effect-1 {
|
||||
animation: rotate 1s ease infinite;
|
||||
}
|
||||
|
||||
.loading .effect-2 {
|
||||
animation: rotate-opacity 1s ease infinite 0.1s;
|
||||
}
|
||||
|
||||
.loading .effect-3 {
|
||||
animation: rotate-opacity 1s ease infinite 0.2s;
|
||||
}
|
||||
|
||||
.loading .effects {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
<!-- 内联关键CSS -->
|
||||
<style>
|
||||
/* 关键路径CSS - 从loader.css内联 */
|
||||
#loading-bg {
|
||||
position: fixed;
|
||||
z-index: 99999;
|
||||
display: block;
|
||||
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;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate-opacity {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
transform: rotate(0deg);
|
||||
.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;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: rotate(1turn);
|
||||
/* 添加logo完成动画 - 放大虚化效果 */
|
||||
.loading-complete .loading-logo {
|
||||
filter: blur(10px);
|
||||
opacity: 0;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 初始化脚本 -->
|
||||
<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)
|
||||
/* 添加加载背景消失动画 - 放大虚化效果 */
|
||||
.loading-complete {
|
||||
filter: blur(15px);
|
||||
opacity: 0;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
// 状态栏适配
|
||||
if (window.navigator.standalone) {
|
||||
document.documentElement.style.setProperty('--status-bar-height', '20px')
|
||||
}
|
||||
.loading {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 50%;
|
||||
block-size: 55px;
|
||||
inline-size: 55px;
|
||||
inset-block-start: 80%;
|
||||
inset-inline-start: calc(50% - 27.5px);
|
||||
transition: opacity 0.6s ease;
|
||||
}
|
||||
|
||||
// 安全区域适配
|
||||
function updateSafeArea() {
|
||||
const safeAreaTop = getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-top)')
|
||||
const safeAreaBottom = getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-bottom)')
|
||||
/* 完成时隐藏加载动画 */
|
||||
.loading-complete .loading {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
if (safeAreaTop) document.documentElement.style.setProperty('--safe-area-top', safeAreaTop)
|
||||
if (safeAreaBottom) document.documentElement.style.setProperty('--safe-area-bottom', safeAreaBottom)
|
||||
}
|
||||
.loading .effect-1,
|
||||
.loading .effect-2,
|
||||
.loading .effect-3 {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 50%;
|
||||
block-size: 100%;
|
||||
border-inline-start: 3px solid var(--initial-loader-color, #eee);
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
updateSafeArea()
|
||||
window.addEventListener('resize', updateSafeArea)
|
||||
window.addEventListener('orientationchange', updateSafeArea)
|
||||
</script>
|
||||
</head>
|
||||
.loading .effect-1 {
|
||||
animation: rotate 1s ease infinite;
|
||||
}
|
||||
|
||||
<body style="margin: 0; overflow: hidden; overscroll-behavior: none; -webkit-overflow-scrolling: touch;">
|
||||
<div id="loading-bg">
|
||||
<div class="loading-logo">
|
||||
<!-- Logo -->
|
||||
<svg width="160px" height="160px" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2">
|
||||
<g transform="matrix(1,0,0,1,-2606,-236)">
|
||||
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">
|
||||
<rect x="0" y="0" width="192" height="192" style="fill: none" />
|
||||
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)">
|
||||
<path
|
||||
d="M2241.27,-28.175C2238.86,-28.931 2236.64,-29.181 2234.48,-29.254L2159.78,-29.286L2165.01,-11.207C2167.16,-13.121 2169.64,-13.722 2172.26,-13.808L2222.12,-13.822C2223.52,-13.824 2225,-13.701 2226.78,-13.108L2241.27,-28.175Z"
|
||||
style="fill: url(#_Linear1)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2205.67,331.428L2205.67,332.25L2205.67,352.835C2205.67,354.263 2204.91,355.583 2203.67,356.298C2202.43,357.012 2200.91,357.013 2199.67,356.3L2190.78,351.174C2189.73,350.595 2188.83,350.083 2188.03,349.59L2187.45,349.257C2186.66,348.725 2185.91,348.142 2185.21,347.461C2185.08,347.331 2184.95,347.198 2184.82,347.061C2184.26,346.457 2183.75,345.778 2183.3,344.995C2182.16,343.05 2181.69,341.024 2181.68,338.948L2181.67,268.923L2209.77,274.425C2207.5,275.639 2205.68,278.3 2205.67,281.429L2205.67,331.428Z"
|
||||
style="fill: url(#_Linear2)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2295.93,363.064C2295.73,363.184 2295.53,363.301 2295.32,363.414L2295.93,363.064Z"
|
||||
style="fill: rgb(141, 81, 249)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2299.79,360.238C2299.79,360.238 2320.03,348.464 2320.04,348.461C2323.1,346.372 2324.69,343.444 2325.17,339.877C2325.17,339.877 2325.17,269.846 2325.17,269.839C2325.06,267.482 2324.56,265.739 2323.61,264.133C2322.56,262.445 2321.26,261.005 2319.55,259.97L2304.42,251.217C2303.96,250.949 2303.39,250.948 2302.92,251.216C2302.46,251.484 2302.17,251.979 2302.17,252.515L2302.17,276.775L2302.17,277.879L2302.17,352.926C2302.17,352.933 2302.17,352.941 2302.17,352.948C2302.04,355.861 2301.23,358.279 2299.79,360.238Z"
|
||||
style="fill: url(#_Linear3)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256Z"
|
||||
style="fill: rgb(165, 118, 255)" />
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256ZM2253.68,223.756C2251.6,223.789 2249.87,224.269 2248.47,224.996L2188.17,259.754C2184.35,261.992 2182.35,265.367 2182.18,269.874C2182.18,269.874 2182.17,292.759 2182.17,292.757C2183.25,290.047 2185.13,288.051 2187.62,286.607L2249.57,250.919C2249.58,250.917 2249.58,250.915 2249.59,250.913C2250.83,250.243 2252.17,249.839 2253.67,249.847C2255.21,249.841 2256.54,250.253 2257.76,250.914C2257.76,250.916 2257.76,250.917 2257.76,250.919L2274.92,260.807C2275.38,261.075 2275.95,261.074 2276.42,260.806C2276.88,260.538 2277.17,260.043 2277.17,259.508L2277.17,237.568C2277.17,236.317 2276.5,235.16 2275.42,234.535C2275.42,234.535 2258.88,225 2258.87,224.996C2256.87,224.049 2255.2,223.746 2253.68,223.756Z"
|
||||
style="fill: url(#_Linear4)" />
|
||||
</g>
|
||||
<g transform="matrix(0.800798,0.462341,0.769972,-1.33363,-1677.22,-896.858)">
|
||||
<path
|
||||
d="M2241.55,-28.184C2239.1,-28.989 2236.83,-29.204 2234.68,-29.295C2234.68,-29.295 2220.82,-29.3 2215.03,-29.303C2213.48,-29.303 2212.05,-28.808 2211.28,-28.004C2208.65,-25.275 2202.56,-18.936 2199.45,-15.709C2199.07,-15.306 2199.07,-14.809 2199.46,-14.406C2199.85,-14.004 2200.57,-13.758 2201.34,-13.761C2208.36,-13.788 2222.72,-13.845 2222.72,-13.845C2223.98,-13.851 2225.44,-13.657 2227.06,-13.117L2241.55,-28.184Z"
|
||||
style="fill: rgb(141, 81, 249)" />
|
||||
</g>
|
||||
<g transform="matrix(-4.32309,0,0,12.4454,9610.35,-1450.35)">
|
||||
<path
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
|
||||
style="fill: rgb(104, 0, 197)" />
|
||||
<clipPath id="_clip5">
|
||||
.loading .effect-2 {
|
||||
animation: rotate-opacity 1s ease infinite 0.1s;
|
||||
}
|
||||
|
||||
.loading .effect-3 {
|
||||
animation: rotate-opacity 1s ease infinite 0.2s;
|
||||
}
|
||||
|
||||
.loading .effects {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate-opacity {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 初始化脚本 -->
|
||||
<script>
|
||||
// 检测系统主题是否为深色模式
|
||||
function checkPrefersColorSchemeIsDark() {
|
||||
try {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 主题色彩初始化
|
||||
let loaderColor = localStorage.getItem('materio-initial-loader-bg')
|
||||
let primaryColor = localStorage.getItem('materio-initial-loader-color')
|
||||
|
||||
// 检查主题设置
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
const isAutoTheme = savedTheme === 'auto'
|
||||
|
||||
// 如果是自动主题或者没有保存的背景色,根据系统主题设置背景色
|
||||
if (isAutoTheme || !loaderColor) {
|
||||
loaderColor = checkPrefersColorSchemeIsDark() ? '#0E1116' : '#FFFFFF'
|
||||
}
|
||||
if (!primaryColor) {
|
||||
primaryColor = '#9155FD'
|
||||
}
|
||||
|
||||
// 应用主题色彩
|
||||
document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
|
||||
document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
||||
|
||||
// 状态栏适配
|
||||
if (window.navigator.standalone) {
|
||||
document.documentElement.style.setProperty('--status-bar-height', '20px')
|
||||
}
|
||||
|
||||
// 安全区域适配
|
||||
function updateSafeArea() {
|
||||
const safeAreaTop = getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-top)')
|
||||
const safeAreaBottom = getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-bottom)',
|
||||
)
|
||||
|
||||
if (safeAreaTop) document.documentElement.style.setProperty('--safe-area-top', safeAreaTop)
|
||||
if (safeAreaBottom) document.documentElement.style.setProperty('--safe-area-bottom', safeAreaBottom)
|
||||
}
|
||||
|
||||
updateSafeArea()
|
||||
window.addEventListener('resize', updateSafeArea)
|
||||
window.addEventListener('orientationchange', updateSafeArea)
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body style="margin: 0; overflow: hidden; overscroll-behavior: none; -webkit-overflow-scrolling: touch">
|
||||
<div id="loading-bg">
|
||||
<div class="loading-logo">
|
||||
<!-- Logo -->
|
||||
<svg
|
||||
width="160px"
|
||||
height="160px"
|
||||
viewBox="0 0 192 192"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2"
|
||||
>
|
||||
<g transform="matrix(1,0,0,1,-2606,-236)">
|
||||
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">
|
||||
<rect x="0" y="0" width="192" height="192" style="fill: none" />
|
||||
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)">
|
||||
<path
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip5)">
|
||||
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
|
||||
d="M2241.27,-28.175C2238.86,-28.931 2236.64,-29.181 2234.48,-29.254L2159.78,-29.286L2165.01,-11.207C2167.16,-13.121 2169.64,-13.722 2172.26,-13.808L2222.12,-13.822C2223.52,-13.824 2225,-13.701 2226.78,-13.108L2241.27,-28.175Z"
|
||||
style="fill: url(#_Linear1)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2205.67,331.428L2205.67,332.25L2205.67,352.835C2205.67,354.263 2204.91,355.583 2203.67,356.298C2202.43,357.012 2200.91,357.013 2199.67,356.3L2190.78,351.174C2189.73,350.595 2188.83,350.083 2188.03,349.59L2187.45,349.257C2186.66,348.725 2185.91,348.142 2185.21,347.461C2185.08,347.331 2184.95,347.198 2184.82,347.061C2184.26,346.457 2183.75,345.778 2183.3,344.995C2182.16,343.05 2181.69,341.024 2181.68,338.948L2181.67,268.923L2209.77,274.425C2207.5,275.639 2205.68,278.3 2205.67,281.429L2205.67,331.428Z"
|
||||
style="fill: url(#_Linear2)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2295.93,363.064C2295.73,363.184 2295.53,363.301 2295.32,363.414L2295.93,363.064Z"
|
||||
style="fill: rgb(141, 81, 249)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2299.79,360.238C2299.79,360.238 2320.03,348.464 2320.04,348.461C2323.1,346.372 2324.69,343.444 2325.17,339.877C2325.17,339.877 2325.17,269.846 2325.17,269.839C2325.06,267.482 2324.56,265.739 2323.61,264.133C2322.56,262.445 2321.26,261.005 2319.55,259.97L2304.42,251.217C2303.96,250.949 2303.39,250.948 2302.92,251.216C2302.46,251.484 2302.17,251.979 2302.17,252.515L2302.17,276.775L2302.17,277.879L2302.17,352.926C2302.17,352.933 2302.17,352.941 2302.17,352.948C2302.04,355.861 2301.23,358.279 2299.79,360.238Z"
|
||||
style="fill: url(#_Linear3)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256Z"
|
||||
style="fill: rgb(165, 118, 255)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256ZM2253.68,223.756C2251.6,223.789 2249.87,224.269 2248.47,224.996L2188.17,259.754C2184.35,261.992 2182.35,265.367 2182.18,269.874C2182.18,269.874 2182.17,292.759 2182.17,292.757C2183.25,290.047 2185.13,288.051 2187.62,286.607L2249.57,250.919C2249.58,250.917 2249.58,250.915 2249.59,250.913C2250.83,250.243 2252.17,249.839 2253.67,249.847C2255.21,249.841 2256.54,250.253 2257.76,250.914C2257.76,250.916 2257.76,250.917 2257.76,250.919L2274.92,260.807C2275.38,261.075 2275.95,261.074 2276.42,260.806C2276.88,260.538 2277.17,260.043 2277.17,259.508L2277.17,237.568C2277.17,236.317 2276.5,235.16 2275.42,234.535C2275.42,234.535 2258.88,225 2258.87,224.996C2256.87,224.049 2255.2,223.746 2253.68,223.756Z"
|
||||
style="fill: url(#_Linear4)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(0.800798,0.462341,0.769972,-1.33363,-1677.22,-896.858)">
|
||||
<path
|
||||
d="M2241.55,-28.184C2239.1,-28.989 2236.83,-29.204 2234.68,-29.295C2234.68,-29.295 2220.82,-29.3 2215.03,-29.303C2213.48,-29.303 2212.05,-28.808 2211.28,-28.004C2208.65,-25.275 2202.56,-18.936 2199.45,-15.709C2199.07,-15.306 2199.07,-14.809 2199.46,-14.406C2199.85,-14.004 2200.57,-13.758 2201.34,-13.761C2208.36,-13.788 2222.72,-13.845 2222.72,-13.845C2223.98,-13.851 2225.44,-13.657 2227.06,-13.117L2241.55,-28.184Z"
|
||||
style="fill: rgb(141, 81, 249)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-4.32309,0,0,12.4454,9610.35,-1450.35)">
|
||||
<path
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
|
||||
style="fill: rgb(104, 0, 197)"
|
||||
/>
|
||||
<clipPath id="_clip5">
|
||||
<path
|
||||
d="M1726.17,-64.249L1708.16,-72.303L1708.05,-23.514L1721.88,-32.386C1722.96,-33.241 1723.09,-33.944 1723.15,-34.636L1723.15,-54.373C1723.19,-56.238 1724.96,-57.594 1726.87,-56.686L1726.17,-64.249Z"
|
||||
style="fill: url(#_Linear6)" />
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1704.47,-40.254C1706.28,-40.527 1708.14,-40.212 1708.16,-39.416L1708.16,-18.976L1726.17,-18.976L1726.17,-45.661Z"
|
||||
style="fill: rgb(141, 81, 249)" />
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1726.17,-18.976L1708.16,-18.976L1708.16,-39.416C1707.79,-40.732 1704.5,-40.298 1702.68,-40.025L1726.17,-45.661ZM1705.49,-40.491C1706.2,-40.507 1706.87,-40.464 1707.4,-40.327C1708.01,-40.173 1708.48,-39.899 1708.62,-39.436C1708.62,-39.429 1708.62,-39.423 1708.62,-39.416L1708.62,-19.152C1708.62,-19.152 1725.72,-19.152 1725.72,-19.152L1725.72,-45.345L1705.49,-40.491Z"
|
||||
style="fill: url(#_Radial7)" />
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
|
||||
/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip5)">
|
||||
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
|
||||
<path
|
||||
d="M1726.17,-64.249L1708.16,-72.303L1708.05,-23.514L1721.88,-32.386C1722.96,-33.241 1723.09,-33.944 1723.15,-34.636L1723.15,-54.373C1723.19,-56.238 1724.96,-57.594 1726.87,-56.686L1726.17,-64.249Z"
|
||||
style="fill: url(#_Linear6)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1704.47,-40.254C1706.28,-40.527 1708.14,-40.212 1708.16,-39.416L1708.16,-18.976L1726.17,-18.976L1726.17,-45.661Z"
|
||||
style="fill: rgb(141, 81, 249)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1726.17,-18.976L1708.16,-18.976L1708.16,-39.416C1707.79,-40.732 1704.5,-40.298 1702.68,-40.025L1726.17,-45.661ZM1705.49,-40.491C1706.2,-40.507 1706.87,-40.464 1707.4,-40.327C1708.01,-40.173 1708.48,-39.899 1708.62,-39.436C1708.62,-39.429 1708.62,-39.423 1708.62,-39.416L1708.62,-19.152C1708.62,-19.152 1725.72,-19.152 1725.72,-19.152L1725.72,-45.345L1705.49,-40.491Z"
|
||||
style="fill: url(#_Radial7)"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)">
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)">
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)">
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)">
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)">
|
||||
<stop offset="0" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
<stop offset="0.51" style="stop-color: rgb(110, 38, 217); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(91, 0, 197); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<radialGradient id="_Radial7" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)">
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="_Linear1"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear2"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear3"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear4"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear6"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
<stop offset="0.51" style="stop-color: rgb(110, 38, 217); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(91, 0, 197); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="_Radial7"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="loading">
|
||||
<div class="effect-1 effects"></div>
|
||||
<div class="effect-2 effects"></div>
|
||||
<div class="effect-3 effects"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading">
|
||||
<div class="effect-1 effects"></div>
|
||||
<div class="effect-2 effects"></div>
|
||||
<div class="effect-3 effects"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.6.7",
|
||||
"version": "2.7.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": "dist/service.js",
|
||||
|
||||
@@ -28,8 +28,8 @@ code {
|
||||
background: rgba(var(--v-theme-background), 1);
|
||||
|
||||
.v-theme--transparent & {
|
||||
backdrop-filter: blur(5px);
|
||||
background: rgba(var(--v-theme-background), 0.1) !important;
|
||||
backdrop-filter: blur(var(--transparent-blur-light, 5px));
|
||||
background: rgba(var(--v-theme-background), var(--transparent-opacity-light, 0.1)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,8 @@ code {
|
||||
}
|
||||
|
||||
.v-theme--transparent & {
|
||||
background: rgba(var(--v-theme-background), 0.3);
|
||||
backdrop-filter: blur(var(--transparent-blur-heavy, 16px));
|
||||
background: rgba(var(--v-theme-background), var(--transparent-opacity-heavy, 0.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { preloadImage } from './@core/utils/image'
|
||||
import { globalLoadingStateManager } from '@/utils/loadingStateManager'
|
||||
import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundManager'
|
||||
import PWAInstallPrompt from '@/components/PWAInstallPrompt.vue'
|
||||
import { themeManager } from '@/utils/themeManager'
|
||||
|
||||
// 生效主题
|
||||
const { global: globalTheme } = useTheme()
|
||||
@@ -212,6 +213,9 @@ onMounted(async () => {
|
||||
// 初始化data-theme属性
|
||||
updateHtmlThemeAttribute(globalTheme.name.value)
|
||||
|
||||
// 初始化主题管理器 - 统一处理主题初始化
|
||||
await themeManager.setTheme(themeValue)
|
||||
|
||||
// 监听主题变化
|
||||
watch(
|
||||
() => globalTheme.name.value,
|
||||
|
||||
@@ -164,6 +164,10 @@ export interface WorkflowShare {
|
||||
description?: string
|
||||
// 定时器
|
||||
timer?: string
|
||||
// 触发类型:timer-定时触发 event-事件触发 manual-手动触发
|
||||
trigger_type?: string
|
||||
// 事件类型(当trigger_type为event时使用)
|
||||
event_type?: string
|
||||
// 动作列表
|
||||
actions?: any[]
|
||||
// 动作流
|
||||
@@ -1328,6 +1332,10 @@ export interface Workflow {
|
||||
description?: string
|
||||
// 定时器
|
||||
timer?: string
|
||||
// 触发类型:timer-定时触发 event-事件触发 manual-手动触发
|
||||
trigger_type?: string
|
||||
// 事件类型(当trigger_type为event时使用)
|
||||
event_type?: string
|
||||
// 状态
|
||||
state?: string
|
||||
// 当前执行动作
|
||||
|
||||
@@ -72,20 +72,18 @@ async function drawImages(imageList: string[]) {
|
||||
if (!canvas) return getDefaultImage()
|
||||
|
||||
// 画布参数
|
||||
const POSTER_WIDTH = (canvas.width - 32) / 4
|
||||
const POSTER_HEIGHT = canvas.height * 0.75 - 8
|
||||
const MARGIN_WIDTH = 4
|
||||
const MARGIN_HEIGHT = 4
|
||||
const REFLECTION_HEIGHT = POSTER_HEIGHT / 2
|
||||
const REFLECTION_SHOW_HEIGHT = canvas.height / 4
|
||||
const POSTER_WIDTH = (canvas.width - 40) / 4 // 左右边框8px + 3个间隔24px = 40px
|
||||
const POSTER_HEIGHT = 256 // 上方海报高256
|
||||
const MARGIN_WIDTH = 8 // 左右间隔为8
|
||||
const MARGIN_HEIGHT = 4 // 海报和倒影之间的间隔为4
|
||||
const REFLECTION_HEIGHT = canvas.height - POSTER_HEIGHT - MARGIN_HEIGHT // 下方倒影使用剩余全部高度
|
||||
|
||||
// 获取画布上下文
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return getDefaultImage()
|
||||
|
||||
// 设置背景色为黑色
|
||||
ctx.fillStyle = '#000000'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
// 设置背景色为透明
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 绘制图片
|
||||
async function drawImageWithReflection(imgSrc: string, index: number) {
|
||||
@@ -104,12 +102,12 @@ async function drawImages(imageList: string[]) {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ctx.fillStyle = '#e5e7eb'
|
||||
ctx.fillRect(MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1), MARGIN_HEIGHT, POSTER_WIDTH, POSTER_HEIGHT)
|
||||
ctx.fillRect(MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1), 0, POSTER_WIDTH, POSTER_HEIGHT)
|
||||
return
|
||||
}
|
||||
|
||||
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
|
||||
const y = MARGIN_HEIGHT
|
||||
const y = 0 // 海报紧贴顶部
|
||||
|
||||
ctx.drawImage(img, x, y, POSTER_WIDTH, POSTER_HEIGHT)
|
||||
|
||||
@@ -123,17 +121,18 @@ async function drawImages(imageList: string[]) {
|
||||
img.width,
|
||||
img.height,
|
||||
x,
|
||||
REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT,
|
||||
0,
|
||||
POSTER_WIDTH,
|
||||
REFLECTION_HEIGHT,
|
||||
)
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT, 0, REFLECTION_HEIGHT)
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height - (POSTER_HEIGHT + MARGIN_HEIGHT))
|
||||
|
||||
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)')
|
||||
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.3)')
|
||||
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.7)')
|
||||
ctx.globalCompositeOperation = 'destination-out';
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(x, 0, POSTER_WIDTH, REFLECTION_SHOW_HEIGHT)
|
||||
ctx.fillRect(x, 0, POSTER_WIDTH, REFLECTION_HEIGHT)
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
@@ -166,7 +165,7 @@ onMounted(async () => {
|
||||
@click="goPlay"
|
||||
>
|
||||
<template #image>
|
||||
<canvas ref="canvasRef" class="w-full h-full hidden" />
|
||||
<canvas ref="canvasRef" width="640" height="360" class="w-full h-full hidden" />
|
||||
<VImg :src="imgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler" @error="imageErrorHandler">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
|
||||
@@ -6,6 +6,10 @@ import ForkWorkflowDialog from '../dialog/ForkWorkflowDialog.vue'
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
workflow: Object as PropType<WorkflowShare>,
|
||||
eventTypes: {
|
||||
type: Array as PropType<Array<{ title: string; value: string }>>,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
// 定义删除事件
|
||||
@@ -135,6 +139,7 @@ function doDelete() {
|
||||
v-if="forkWorkflowDialog"
|
||||
v-model="forkWorkflowDialog"
|
||||
:workflow="props.workflow"
|
||||
:event-types="props.eventTypes"
|
||||
@close="forkWorkflowDialog = false"
|
||||
@fork="finishForkWorkflow"
|
||||
@delete="doDelete"
|
||||
|
||||
@@ -16,6 +16,10 @@ const props = defineProps({
|
||||
required: true,
|
||||
type: Object as PropType<Workflow>,
|
||||
},
|
||||
eventTypes: {
|
||||
type: Array as PropType<Array<{ title: string; value: string }>>,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
@@ -39,6 +43,12 @@ const shareDialog = ref(false)
|
||||
// 加载中
|
||||
const loading = ref(false)
|
||||
|
||||
// 根据事件类型值获取显示文本
|
||||
const getEventTypeText = (eventTypeValue: string) => {
|
||||
const eventType = props.eventTypes.find(item => item.value === eventTypeValue)
|
||||
return eventType ? eventType.title : eventTypeValue
|
||||
}
|
||||
|
||||
// 编辑任务
|
||||
function handleEdit(item: Workflow) {
|
||||
editDialog.value = true
|
||||
@@ -165,11 +175,36 @@ async function handleReset(item: Workflow) {
|
||||
|
||||
// 计算状态颜色
|
||||
const resolveStatusVariant = (status: string | undefined) => {
|
||||
if (status === 'S') return { color: 'success', text: t('workflow.task.status.success') }
|
||||
else if (status === 'R') return { color: 'primary', text: t('workflow.task.status.running') }
|
||||
else if (status === 'F') return { color: 'error', text: t('workflow.task.status.failed') }
|
||||
else if (status === 'P') return { color: 'secondary', text: t('workflow.task.status.paused') }
|
||||
else return { color: 'info', text: t('workflow.task.status.waiting') }
|
||||
if (status === 'S')
|
||||
return {
|
||||
color: 'success',
|
||||
bgColor: 'linear-gradient(to bottom right, rgba(76, 175, 80, 0.9), rgba(76, 175, 80, 0.7))',
|
||||
text: t('workflow.task.status.success'),
|
||||
}
|
||||
else if (status === 'R')
|
||||
return {
|
||||
color: 'primary',
|
||||
bgColor: 'linear-gradient(to bottom right, rgba(33, 150, 243, 0.9), rgba(33, 150, 243, 0.7))',
|
||||
text: t('workflow.task.status.running'),
|
||||
}
|
||||
else if (status === 'F')
|
||||
return {
|
||||
color: 'error',
|
||||
bgColor: 'linear-gradient(to bottom right, rgba(244, 67, 54, 0.9), rgba(244, 67, 54, 0.7))',
|
||||
text: t('workflow.task.status.failed'),
|
||||
}
|
||||
else if (status === 'P')
|
||||
return {
|
||||
color: 'warning',
|
||||
bgColor: 'linear-gradient(to bottom right, rgba(255, 152, 0, 0.9), rgba(255, 152, 0, 0.7))',
|
||||
text: t('workflow.task.status.paused'),
|
||||
}
|
||||
else
|
||||
return {
|
||||
color: 'info',
|
||||
bgColor: 'linear-gradient(to bottom right, rgba(33, 150, 243, 0.9), rgba(33, 150, 243, 0.7))',
|
||||
text: t('workflow.task.status.waiting'),
|
||||
}
|
||||
}
|
||||
|
||||
// 计算当前动作占比
|
||||
@@ -190,11 +225,9 @@ const resolveProgress = (item: Workflow) => {
|
||||
:class="{ 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering }"
|
||||
>
|
||||
<VCardItem
|
||||
class="px-2"
|
||||
:class="{
|
||||
'py-0': workflow?.description,
|
||||
'py-2': !workflow?.description,
|
||||
[`bg-${resolveStatusVariant(workflow?.state).color}`]: true,
|
||||
class="px-2 py-2"
|
||||
:style="{
|
||||
background: resolveStatusVariant(workflow?.state).bgColor,
|
||||
}"
|
||||
>
|
||||
<template #prepend>
|
||||
@@ -209,9 +242,8 @@ const resolveProgress = (item: Workflow) => {
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VCardTitle class="text-white text-lg">
|
||||
{{ workflow?.name }}
|
||||
<span :title="workflow?.description">{{ workflow?.name }}</span>
|
||||
</VCardTitle>
|
||||
<VCardSubtitle class="text-white">{{ workflow?.description }}</VCardSubtitle>
|
||||
<template #append>
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
@@ -272,15 +304,28 @@ const resolveProgress = (item: Workflow) => {
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText class="pa-3">
|
||||
<div class="d-flex flex-column gap-y-2">
|
||||
<div class="d-flex flex-column gap-y-3">
|
||||
<div class="d-flex flex-wrap gap-x-3">
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">{{ t('workflow.task.info.timer') }}</div>
|
||||
<h5 class="text-lg">{{ workflow?.timer }}</h5>
|
||||
<div class="mb-1">{{ t('workflow.task.info.trigger') }}</div>
|
||||
<h5>
|
||||
<span v-if="workflow?.trigger_type === 'timer' || !workflow?.trigger_type">
|
||||
<VIcon icon="mdi-clock-outline" size="small" class="me-1" />
|
||||
{{ workflow?.timer }}
|
||||
</span>
|
||||
<span v-else-if="workflow?.trigger_type === 'event'">
|
||||
<VIcon icon="mdi-calendar-check" size="small" class="me-1" />
|
||||
{{ getEventTypeText(workflow?.event_type || '') }}
|
||||
</span>
|
||||
<span v-else-if="workflow?.trigger_type === 'manual'">
|
||||
<VIcon icon="mdi-hand-pointing-up" size="small" class="me-1" />
|
||||
{{ t('workflow.task.info.manualTrigger') }}
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">{{ t('workflow.task.info.status') }}</div>
|
||||
<h5 class="text-lg" :class="`text-${resolveStatusVariant(workflow?.state).color}`">
|
||||
<h5 :class="`text-${resolveStatusVariant(workflow?.state).color}`">
|
||||
{{ resolveStatusVariant(workflow?.state).text }}
|
||||
</h5>
|
||||
</div>
|
||||
@@ -289,14 +334,14 @@ const resolveProgress = (item: Workflow) => {
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">{{ t('workflow.task.info.actionCount') }}</div>
|
||||
<div>
|
||||
<VAvatar size="28" color="primary" variant="tonal">
|
||||
<span class="text-sm">{{ workflow?.actions?.length }}</span>
|
||||
<VAvatar size="24" color="primary" variant="tonal">
|
||||
<span class="text-xs">{{ workflow?.actions?.length }}</span>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="mb-1">{{ t('workflow.task.info.runCount') }}</div>
|
||||
<h5 class="text-lg">{{ workflow?.run_count }}</h5>
|
||||
<h5>{{ workflow?.run_count }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-x-3">
|
||||
|
||||
@@ -13,6 +13,10 @@ const { t } = useI18n()
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
workflow: Object as PropType<WorkflowShare>,
|
||||
eventTypes: {
|
||||
type: Array as PropType<Array<{ title: string; value: string }>>,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
@@ -32,6 +36,12 @@ const processing = ref(false)
|
||||
// 删除中
|
||||
const deleting = ref(false)
|
||||
|
||||
// 根据事件类型值获取显示文本
|
||||
const getEventTypeText = (eventTypeValue: string) => {
|
||||
const eventType = props.eventTypes.find(item => item.value === eventTypeValue)
|
||||
return eventType ? eventType.title : eventTypeValue
|
||||
}
|
||||
|
||||
// 流程图相关
|
||||
const { nodes, edges } = useVueFlow()
|
||||
|
||||
@@ -190,10 +200,23 @@ async function doDelete() {
|
||||
<span class="text-body-1"> {{ props.workflow?.share_user }}</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem class="ps-0" v-if="props.workflow?.timer">
|
||||
<VListItem class="ps-0" v-if="props.workflow?.trigger_type || props.workflow?.timer">
|
||||
<VListItemTitle class="text-center text-md-left">
|
||||
<span class="font-weight-medium">{{ t('workflow.timer') }}:</span>
|
||||
<span class="text-body-1"> {{ props.workflow?.timer }}</span>
|
||||
<span class="font-weight-medium">{{ t('workflow.trigger') }}:</span>
|
||||
<span class="text-body-1">
|
||||
<span v-if="props.workflow?.trigger_type === 'timer' || !props.workflow?.trigger_type">
|
||||
<VIcon icon="mdi-clock-outline" size="small" class="me-1" />
|
||||
{{ props.workflow?.timer }}
|
||||
</span>
|
||||
<span v-else-if="props.workflow?.trigger_type === 'event'">
|
||||
<VIcon icon="mdi-calendar-check" size="small" class="me-1" />
|
||||
{{ getEventTypeText(props.workflow?.event_type || '') }}
|
||||
</span>
|
||||
<span v-else-if="props.workflow?.trigger_type === 'manual'">
|
||||
<VIcon icon="mdi-hand-pointing-up" size="small" class="me-1" />
|
||||
{{ t('workflow.manualTrigger') }}
|
||||
</span>
|
||||
</span>
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem class="ps-0" v-if="parsedWorkflow?.actions">
|
||||
|
||||
@@ -203,7 +203,7 @@ onMounted(async () => {
|
||||
prepend-inner-icon="mdi-rss"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="3">
|
||||
<VCol cols="6" md="3">
|
||||
<VTextField
|
||||
v-model="siteForm.timeout"
|
||||
:label="t('site.fields.timeout')"
|
||||
|
||||
478
src/components/dialog/SiteStatisticsDialog.vue
Normal file
478
src/components/dialog/SiteStatisticsDialog.vue
Normal file
@@ -0,0 +1,478 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
import api from '@/api'
|
||||
import type { Site, SiteStatistic } from '@/api/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
sites: {
|
||||
type: Array as PropType<Site[]>,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// 站点统计数据
|
||||
const siteStats = ref<SiteStatistic[]>([])
|
||||
|
||||
// 是否加载中
|
||||
const loading = ref(false)
|
||||
|
||||
// 当前选中的站点
|
||||
const selectedSite = ref<Site | null>(null)
|
||||
|
||||
// 耗时记录详情弹窗
|
||||
const detailDialog = ref(false)
|
||||
|
||||
// 获取站点统计数据
|
||||
async function fetchSiteStats() {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await api.get('site/statistic')
|
||||
siteStats.value = Array.isArray(response) ? response : response.data || []
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch site statistics:', error)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 根据站点域名获取统计数据
|
||||
function getSiteStats(domain: string): SiteStatistic | undefined {
|
||||
return siteStats.value.find(stat => stat.domain === domain)
|
||||
}
|
||||
|
||||
// 获取站点连接状态
|
||||
function getConnectionStatus(stats: SiteStatistic | undefined): string {
|
||||
if (!stats || Object.keys(stats).length === 0) {
|
||||
return 'unknown'
|
||||
}
|
||||
if (stats.lst_state === 1) {
|
||||
return 'failed'
|
||||
} else if (stats.lst_state === 0) {
|
||||
if (!stats.seconds) return 'unknown'
|
||||
if (stats.seconds >= 5) return 'slow'
|
||||
return 'connected'
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
function getStatusColor(status: string): string {
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
return 'success'
|
||||
case 'slow':
|
||||
return 'warning'
|
||||
case 'failed':
|
||||
return 'error'
|
||||
default:
|
||||
return 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态图标
|
||||
function getStatusIcon(status: string): string {
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
return 'mdi-wifi'
|
||||
case 'slow':
|
||||
return 'mdi-wifi-strength-2'
|
||||
case 'failed':
|
||||
return 'mdi-wifi-off'
|
||||
default:
|
||||
return 'mdi-help-circle'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status: string): string {
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
return t('site.connectionNormal')
|
||||
case 'slow':
|
||||
return t('site.connectionSlow')
|
||||
case 'failed':
|
||||
return t('site.connectionFailed')
|
||||
default:
|
||||
return t('site.connectionUnknown')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取耗时颜色
|
||||
function getTimeColor(seconds: number | undefined): string {
|
||||
if (!seconds) return 'secondary'
|
||||
if (seconds < 2) return 'success'
|
||||
if (seconds < 5) return 'warning'
|
||||
return 'error'
|
||||
}
|
||||
|
||||
// 获取成功率(与列表/概览口径一致)
|
||||
function getSuccessRate(stats: SiteStatistic | undefined): string {
|
||||
if (!stats) return '-'
|
||||
const success = Number(stats.success ?? 0)
|
||||
const fail = Number(stats.fail ?? 0)
|
||||
const total = success + fail
|
||||
if (total <= 0) return '-'
|
||||
return String(Math.round((success / total) * 100))
|
||||
}
|
||||
|
||||
// 解析耗时记录
|
||||
function parseTimeRecords(note: any): Array<{ time: string; duration: number }> {
|
||||
if (!note) return []
|
||||
|
||||
try {
|
||||
// note可能是字符串或对象,如果是字符串则解析
|
||||
const records = typeof note === 'string' ? JSON.parse(note) : note
|
||||
|
||||
if (typeof records === 'object' && records !== null) {
|
||||
const result = Object.entries(records)
|
||||
.map(([time, duration]) => ({
|
||||
time,
|
||||
duration: Number(duration) || 0,
|
||||
}))
|
||||
.sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime())
|
||||
.slice(0, 10) // 只显示最近10条记录
|
||||
|
||||
return result
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse time records:', error)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
function viewDetail(site: Site) {
|
||||
selectedSite.value = site
|
||||
detailDialog.value = true
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function closeDialog() {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
// 计算属性:按平均耗时排序的站点列表
|
||||
const sortedSites = computed(() => {
|
||||
return props.sites
|
||||
.map(site => {
|
||||
const stats = getSiteStats(site.domain)
|
||||
return {
|
||||
site,
|
||||
stats,
|
||||
status: getConnectionStatus(stats),
|
||||
avgTime: stats?.seconds || 0,
|
||||
}
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// 先按状态排序:connected > slow > failed > unknown
|
||||
const statusOrder = { connected: 0, slow: 1, failed: 2, unknown: 3 }
|
||||
const statusDiff =
|
||||
statusOrder[a.status as keyof typeof statusOrder] - statusOrder[b.status as keyof typeof statusOrder]
|
||||
if (statusDiff !== 0) return statusDiff
|
||||
|
||||
// 再按平均耗时排序
|
||||
return a.avgTime - b.avgTime
|
||||
})
|
||||
})
|
||||
|
||||
// 统计总览(与列表口径一致)
|
||||
const overviewCounts = computed(() => {
|
||||
const items = sortedSites.value
|
||||
const total = items.length
|
||||
const connected = items.filter(i => i.status === 'connected').length
|
||||
const slow = items.filter(i => i.status === 'slow').length
|
||||
const failed = items.filter(i => i.status === 'failed').length
|
||||
const unknown = total - connected - slow - failed
|
||||
return { total, connected, slow, failed, unknown }
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchSiteStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper max-width="50rem" :fullscreen="display.smAndDown.value" scrollable>
|
||||
<VCard>
|
||||
<!-- 标题栏 -->
|
||||
<VCardItem>
|
||||
<VDialogCloseBtn @click="closeDialog" />
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-chart-line" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>
|
||||
{{ t('site.statistics') }}
|
||||
</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<!-- 内容区域 -->
|
||||
<VCardText class="pa-0">
|
||||
<LoadingBanner v-if="loading" class="my-8" />
|
||||
|
||||
<div v-else class="site-statistics-content">
|
||||
<!-- 统计概览 -->
|
||||
<div class="statistics-overview pa-4">
|
||||
<div class="d-flex flex-wrap gap-4">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ overviewCounts.total }}</div>
|
||||
<div class="stat-label">{{ t('site.totalSites') }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number success--text">{{ overviewCounts.connected }}</div>
|
||||
<div class="stat-label">{{ t('site.normalSites') }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number warning--text">{{ overviewCounts.slow }}</div>
|
||||
<div class="stat-label">{{ t('site.slowSites') }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number error--text">{{ overviewCounts.failed }}</div>
|
||||
<div class="stat-label">{{ t('site.failedSites') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 站点列表 -->
|
||||
<div class="sites-list">
|
||||
<div
|
||||
v-for="item in sortedSites"
|
||||
:key="item.site.id"
|
||||
class="site-item pa-4 border-b"
|
||||
:class="`border-${getStatusColor(item.status)}`"
|
||||
>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<!-- 左侧:站点信息 -->
|
||||
<div class="d-flex align-center flex-1 min-w-0">
|
||||
<!-- 状态指示器 -->
|
||||
<div class="status-indicator me-3" :class="getStatusColor(item.status)">
|
||||
<VIcon :icon="getStatusIcon(item.status)" size="20" />
|
||||
</div>
|
||||
|
||||
<!-- 站点名称和状态 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="d-flex align-center">
|
||||
<h4 class="text-h6 mb-1 truncate">{{ item.site.name }}</h4>
|
||||
<VChip :color="getStatusColor(item.status)" size="small" class="ml-2" variant="tonal">
|
||||
{{ getStatusText(item.status) }}
|
||||
</VChip>
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ item.site.domain }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:统计信息 -->
|
||||
<div class="d-flex align-center gap-4">
|
||||
<!-- 平均耗时 -->
|
||||
<div class="text-center">
|
||||
<div class="text-h6 font-weight-bold" :class="`text-${getTimeColor(item.stats?.seconds)}`">
|
||||
{{ item.stats?.seconds || '-' }}s
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ t('site.averageTime') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 成功率 -->
|
||||
<div class="text-center">
|
||||
<div class="text-h6 font-weight-bold">{{ getSuccessRate(item.stats) }}%</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ t('site.successRate') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情按钮 -->
|
||||
<VBtn icon variant="text" size="small" @click="viewDetail(item.site)">
|
||||
<VIcon icon="mdi-information-outline" />
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<DialogWrapper v-model="detailDialog" :max-width="display.mdAndUp.value ? 600 : '95%'" scrollable>
|
||||
<VCard v-if="selectedSite">
|
||||
<VCardItem class="py-3">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-information-outline" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle> {{ selectedSite.name }} - {{ t('site.timeRecords') }} </VCardTitle>
|
||||
<VDialogCloseBtn @click="detailDialog = false" />
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<div v-if="getSiteStats(selectedSite.domain)">
|
||||
<div class="mb-4">
|
||||
<h5 class="text-h6 mb-2">{{ t('site.statistics') }}</h5>
|
||||
<div class="d-flex flex-wrap gap-4">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">{{ t('site.successCount') }}:</span>
|
||||
<span class="stat-value success--text">
|
||||
{{ getSiteStats(selectedSite.domain)?.success || 0 }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">{{ t('site.failCount') }}:</span>
|
||||
<span class="stat-value error--text">
|
||||
{{ getSiteStats(selectedSite.domain)?.fail || 0 }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">{{ t('site.averageTime') }}:</span>
|
||||
<span class="stat-value" :class="`text-${getTimeColor(getSiteStats(selectedSite.domain)?.seconds)}`">
|
||||
{{ getSiteStats(selectedSite.domain)?.seconds || '-' }}s
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">{{ t('site.lastAccess') }}:</span>
|
||||
<span class="stat-value">
|
||||
{{ getSiteStats(selectedSite.domain)?.lst_mod_date || '-' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 class="text-h6 mb-2">{{ t('site.recentTimeRecords') }}</h5>
|
||||
<div class="time-records">
|
||||
<div
|
||||
v-for="(record, index) in parseTimeRecords(getSiteStats(selectedSite.domain)?.note)"
|
||||
:key="index"
|
||||
class="time-record-item pa-3 border rounded mb-2"
|
||||
:class="`border-${getTimeColor(record.duration)}`"
|
||||
>
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<div>
|
||||
<div class="text-body-2 font-weight-medium">{{ record.time }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ t('site.accessTime') }}</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="text-h6 font-weight-bold" :class="`text-${getTimeColor(record.duration)}`">
|
||||
{{ record.duration }}s
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ t('site.responseTime') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="parseTimeRecords(getSiteStats(selectedSite.domain)?.note).length === 0"
|
||||
class="text-center pa-4"
|
||||
>
|
||||
<VIcon icon="mdi-information-outline" size="48" color="secondary" class="mb-2" />
|
||||
<div class="text-body-1 text-medium-emphasis">{{ t('site.noTimeRecords') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</DialogWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.statistics-overview {
|
||||
background: linear-gradient(135deg, var(--v-theme-surface) 0%, var(--v-theme-surface-variant) 100%);
|
||||
border-block-end: 1px solid var(--v-border-color);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--v-border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--v-theme-surface);
|
||||
min-inline-size: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
margin-block-end: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--v-theme-on-surface-variant);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sites-list {
|
||||
background: var(--v-theme-surface);
|
||||
}
|
||||
|
||||
.site-item {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.site-item:hover {
|
||||
background: var(--v-theme-surface-variant);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--v-theme-surface-variant);
|
||||
block-size: 40px;
|
||||
inline-size: 40px;
|
||||
}
|
||||
|
||||
.status-indicator.success {
|
||||
background: rgba(var(--v-theme-success), 0.1);
|
||||
color: rgb(var(--v-theme-success));
|
||||
}
|
||||
|
||||
.status-indicator.warning {
|
||||
background: rgba(var(--v-theme-warning), 0.1);
|
||||
color: rgb(var(--v-theme-warning));
|
||||
}
|
||||
|
||||
.status-indicator.error {
|
||||
background: rgba(var(--v-theme-error), 0.1);
|
||||
color: rgb(var(--v-theme-error));
|
||||
}
|
||||
|
||||
.status-indicator.secondary {
|
||||
background: rgba(var(--v-theme-secondary), 0.1);
|
||||
color: rgb(var(--v-theme-secondary));
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-item .stat-label {
|
||||
color: var(--v-theme-on-surface-variant);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.time-records {
|
||||
max-block-size: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.time-record-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
@@ -290,8 +290,8 @@ onBeforeMount(() => {
|
||||
<DialogWrapper scrollable eager max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle
|
||||
>{{ t('dialog.siteUserData.title') }} - {{ props.site?.name }}
|
||||
<VCardTitle>
|
||||
{{ t('dialog.siteUserData.title') }} - {{ props.site?.name }}
|
||||
<IconBtn @click.stop="refreshSiteData" color="info"><VIcon icon="mdi-refresh" /></IconBtn>
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
|
||||
@@ -127,7 +127,7 @@ const progressSSE = useProgressSSE(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`,
|
||||
handleProgressMessage,
|
||||
'transfer-queue-progress',
|
||||
progressActive
|
||||
progressActive,
|
||||
)
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
@@ -166,6 +166,7 @@ onUnmounted(() => {
|
||||
:value="progressValue"
|
||||
color="primary"
|
||||
indeterminate
|
||||
:height="2"
|
||||
/>
|
||||
<VCardItem v-if="dataList.length > 0 && progressValue > 0" class="text-center pt-2">
|
||||
<span class="text-sm">{{ progressText }}</span>
|
||||
|
||||
@@ -33,20 +33,95 @@ const workflowForm = ref<Workflow>(
|
||||
name: undefined,
|
||||
timer: undefined,
|
||||
description: undefined,
|
||||
trigger_type: 'timer',
|
||||
event_type: undefined,
|
||||
state: 'P',
|
||||
run_count: 0,
|
||||
},
|
||||
)
|
||||
|
||||
// 监听props变化,处理存量数据
|
||||
watch(
|
||||
() => props.workflow,
|
||||
newWorkflow => {
|
||||
if (newWorkflow) {
|
||||
// 如果trigger_type为空,默认为timer
|
||||
if (!newWorkflow.trigger_type) {
|
||||
newWorkflow.trigger_type = 'timer'
|
||||
}
|
||||
workflowForm.value = { ...newWorkflow }
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 事件类型列表
|
||||
const eventTypes = ref<Array<{ title: string; value: string }>>([])
|
||||
|
||||
// 触发类型选项
|
||||
const triggerTypeOptions = computed(() => [
|
||||
{
|
||||
title: t('dialog.workflowAddEdit.triggerTypeTimer'),
|
||||
value: 'timer',
|
||||
prependIcon: 'mdi-clock-outline',
|
||||
},
|
||||
{
|
||||
title: t('dialog.workflowAddEdit.triggerTypeEvent'),
|
||||
value: 'event',
|
||||
prependIcon: 'mdi-calendar-check',
|
||||
},
|
||||
{
|
||||
title: t('dialog.workflowAddEdit.triggerTypeManual'),
|
||||
value: 'manual',
|
||||
prependIcon: 'mdi-hand-pointing-up',
|
||||
},
|
||||
])
|
||||
|
||||
// 加载事件类型列表
|
||||
async function loadEventTypes() {
|
||||
try {
|
||||
eventTypes.value = await api.get('workflow/event_types')
|
||||
} catch (error) {
|
||||
console.error('Failed to load event types:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听触发类型变化
|
||||
watch(
|
||||
() => workflowForm.value.trigger_type,
|
||||
newType => {
|
||||
if (newType !== 'event') {
|
||||
workflowForm.value.event_type = undefined
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 调用API 新增任务
|
||||
async function addWorkflow() {
|
||||
if (!workflowForm.value.name || !workflowForm.value.timer) {
|
||||
if (!workflowForm.value.name) {
|
||||
$toast.error(t('dialog.workflowAddEdit.nameRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!workflowForm.value.trigger_type) {
|
||||
$toast.error(t('dialog.workflowAddEdit.triggerRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
// 根据触发类型验证必填字段
|
||||
if (workflowForm.value.trigger_type === 'timer' && !workflowForm.value.timer) {
|
||||
$toast.error(t('dialog.workflowAddEdit.timerRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
if (workflowForm.value.trigger_type === 'event' && !workflowForm.value.event_type) {
|
||||
$toast.error(t('dialog.workflowAddEdit.eventTypeRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.post('workflow/', workflowForm.value)
|
||||
@@ -64,10 +139,27 @@ async function addWorkflow() {
|
||||
|
||||
// 调用API 编辑任务
|
||||
async function editWorkflow() {
|
||||
if (!workflowForm.value.name || !workflowForm.value.timer) {
|
||||
if (!workflowForm.value.name) {
|
||||
$toast.error(t('dialog.workflowAddEdit.nameRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!workflowForm.value.trigger_type) {
|
||||
$toast.error(t('dialog.workflowAddEdit.triggerRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
// 根据触发类型验证必填字段
|
||||
if (workflowForm.value.trigger_type === 'timer' && !workflowForm.value.timer) {
|
||||
$toast.error(t('dialog.workflowAddEdit.timerRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
if (workflowForm.value.trigger_type === 'event' && !workflowForm.value.event_type) {
|
||||
$toast.error(t('dialog.workflowAddEdit.eventTypeRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: string } = await api.put(`workflow/${workflowForm.value.id}`, workflowForm.value)
|
||||
@@ -82,6 +174,11 @@ async function editWorkflow() {
|
||||
}
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 组件挂载时加载事件类型
|
||||
onMounted(() => {
|
||||
loadEventTypes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -109,6 +206,25 @@ async function editWorkflow() {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="workflowForm.trigger_type"
|
||||
:label="t('dialog.workflowAddEdit.triggerType')"
|
||||
:items="triggerTypeOptions"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
:rules="[requiredValidator]"
|
||||
prepend-inner-icon="mdi-run"
|
||||
>
|
||||
<template #item="{ item, props: itemProps }">
|
||||
<VListItem v-bind="itemProps">
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.raw.prependIcon" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VSelect>
|
||||
</VCol>
|
||||
<VCol v-if="workflowForm.trigger_type === 'timer'" cols="12">
|
||||
<VCronField
|
||||
v-model="workflowForm.timer"
|
||||
:label="t('dialog.workflowAddEdit.schedule')"
|
||||
@@ -119,6 +235,19 @@ async function editWorkflow() {
|
||||
prepend-inner-icon="mdi-clock-outline"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="workflowForm.trigger_type === 'event'" cols="12">
|
||||
<VSelect
|
||||
v-model="workflowForm.event_type"
|
||||
:label="t('dialog.workflowAddEdit.eventType')"
|
||||
:items="eventTypes"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
:rules="[requiredValidator]"
|
||||
persistent-hint
|
||||
:hint="t('dialog.workflowAddEdit.eventTypePlaceholder')"
|
||||
prepend-inner-icon="mdi-calendar-check"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="workflowForm.description"
|
||||
|
||||
@@ -22,22 +22,38 @@ export function useBackgroundOptimization() {
|
||||
backgroundCloseDelay?: number
|
||||
reconnectDelay?: number
|
||||
maxReconnectAttempts?: number
|
||||
connectDelay?: number // 新增:连接延迟
|
||||
},
|
||||
) => {
|
||||
const manager = sseManagerSingleton.getManager(url, options)
|
||||
const isConnected = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
manager.addMessageListener(listenerId, messageHandler)
|
||||
// 延迟建立连接,确保组件完全挂载
|
||||
const connectDelay = options?.connectDelay || 100
|
||||
setTimeout(() => {
|
||||
try {
|
||||
manager.addMessageListener(listenerId, event => {
|
||||
messageHandler(event)
|
||||
isConnected.value = true
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('SSE连接建立失败:', error)
|
||||
}
|
||||
}, connectDelay)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
manager.removeMessageListener(listenerId)
|
||||
isConnected.value = false
|
||||
})
|
||||
|
||||
return {
|
||||
manager,
|
||||
readyState: () => manager.readyState,
|
||||
close: () => manager.removeMessageListener(listenerId),
|
||||
isConnected,
|
||||
forceReconnect: () => manager.forceReconnect(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -252,6 +252,12 @@ const showDynamicButton = computed(() => {
|
||||
pointer-events: auto;
|
||||
transition: all 0.5s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
|
||||
// 透明主题下的特殊样式
|
||||
.v-theme--transparent & {
|
||||
backdrop-filter: blur(var(--transparent-blur-heavy, 16px));
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy, 0.5));
|
||||
}
|
||||
|
||||
&.shift-left {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getCurrentLocale, setI18nLanguage } from '@/plugins/i18n'
|
||||
import { saveLocalTheme } from '@/@core/utils/theme'
|
||||
import type { ThemeSwitcherTheme } from '@layouts/types'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { themeManager } from '@/utils/themeManager'
|
||||
|
||||
// 认证 Store
|
||||
const authStore = useAuthStore()
|
||||
@@ -45,6 +46,33 @@ const showLanguageMenu = ref(false)
|
||||
// 自定义CSS
|
||||
const customCSS = ref('')
|
||||
|
||||
// 透明度相关
|
||||
const transparencyOpacity = ref(parseFloat(localStorage.getItem('transparency-opacity') || '0.3'))
|
||||
const transparencyBlur = ref(parseFloat(localStorage.getItem('transparency-blur') || '10'))
|
||||
const transparencyLevel = ref(localStorage.getItem('transparency-level') || 'medium')
|
||||
const isTransparentTheme = computed(() => currentThemeName.value === 'transparent')
|
||||
const showTransparencyDialog = ref(false)
|
||||
|
||||
// 预设值配置
|
||||
const transparencyPresets = {
|
||||
low: { opacity: 0.1, blur: 5 },
|
||||
medium: { opacity: 0.3, blur: 10 },
|
||||
high: { opacity: 0.6, blur: 15 },
|
||||
}
|
||||
|
||||
// 判断当前值是否匹配预设值
|
||||
const currentPresetLevel = computed(() => {
|
||||
for (const [level, preset] of Object.entries(transparencyPresets)) {
|
||||
if (
|
||||
Math.abs(transparencyOpacity.value - preset.opacity) < 0.01 &&
|
||||
Math.abs(transparencyBlur.value - preset.blur) < 0.1
|
||||
) {
|
||||
return level
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// 重启轮询控制标识
|
||||
const restartPollingId = ref<number | null>(null)
|
||||
const isRestarting = ref(false)
|
||||
@@ -226,22 +254,35 @@ const themes: ThemeSwitcherTheme[] = [
|
||||
const editorTheme = computed(() => (currentThemeName.value === 'light' ? 'github' : 'monokai'))
|
||||
|
||||
// 更新主题
|
||||
function updateTheme() {
|
||||
async function updateTheme() {
|
||||
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
|
||||
const theme = currentThemeName.value === 'auto' ? autoTheme : currentThemeName.value
|
||||
|
||||
// 设置Vuetify主题
|
||||
globalTheme.name.value = theme
|
||||
|
||||
// 统一处理主题切换 - 主题管理器会自动处理CSS加载和错误
|
||||
await themeManager.setTheme(currentThemeName.value)
|
||||
|
||||
// 保存原始主题设置,而不是计算后的值
|
||||
savedTheme.value = currentThemeName.value
|
||||
// 保存主题到本地
|
||||
saveLocalTheme(currentThemeName.value, globalTheme)
|
||||
// 刷新页面
|
||||
location.reload()
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
function changeTheme(theme: string) {
|
||||
async function changeTheme(theme: string) {
|
||||
currentThemeName.value = theme
|
||||
showThemeMenu.value = false
|
||||
|
||||
// 立即更新主题(不再刷新页面)
|
||||
await updateTheme()
|
||||
|
||||
// 如果是透明主题,应用透明度设置
|
||||
if (theme === 'transparent') {
|
||||
applyTransparencySettings()
|
||||
}
|
||||
|
||||
// 保存主题到服务端
|
||||
try {
|
||||
api.post('/user/config/Layout', {
|
||||
@@ -285,15 +326,87 @@ async function saveCustomCSS() {
|
||||
}
|
||||
}
|
||||
|
||||
// 应用透明度设置
|
||||
function applyTransparencySettings() {
|
||||
const root = document.documentElement
|
||||
|
||||
// 设置CSS变量
|
||||
root.style.setProperty('--transparent-opacity', transparencyOpacity.value.toString())
|
||||
root.style.setProperty('--transparent-opacity-light', (transparencyOpacity.value * 0.67).toString())
|
||||
root.style.setProperty('--transparent-opacity-heavy', (transparencyOpacity.value * 1.67).toString())
|
||||
root.style.setProperty('--transparent-blur', `${transparencyBlur.value}px`)
|
||||
root.style.setProperty('--transparent-blur-light', `${transparencyBlur.value * 0.6}px`)
|
||||
root.style.setProperty('--transparent-blur-heavy', `${transparencyBlur.value * 1.6}px`)
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('transparency-opacity', transparencyOpacity.value.toString())
|
||||
localStorage.setItem('transparency-blur', transparencyBlur.value.toString())
|
||||
}
|
||||
|
||||
// 调整透明度预设
|
||||
function adjustTransparency(level: string) {
|
||||
transparencyLevel.value = level
|
||||
localStorage.setItem('transparency-level', level)
|
||||
|
||||
// 设置预设值
|
||||
switch (level) {
|
||||
case 'low':
|
||||
transparencyOpacity.value = 0.1
|
||||
transparencyBlur.value = 5
|
||||
break
|
||||
case 'medium':
|
||||
transparencyOpacity.value = 0.3
|
||||
transparencyBlur.value = 10
|
||||
break
|
||||
case 'high':
|
||||
transparencyOpacity.value = 0.6
|
||||
transparencyBlur.value = 15
|
||||
break
|
||||
}
|
||||
|
||||
applyTransparencySettings()
|
||||
}
|
||||
|
||||
// 透明度变化处理
|
||||
function onOpacityChange() {
|
||||
applyTransparencySettings()
|
||||
// 清除预设级别,因为用户手动调整了
|
||||
transparencyLevel.value = ''
|
||||
}
|
||||
|
||||
// 模糊度变化处理
|
||||
function onBlurChange() {
|
||||
applyTransparencySettings()
|
||||
// 清除预设级别,因为用户手动调整了
|
||||
transparencyLevel.value = ''
|
||||
}
|
||||
|
||||
// 重置透明度设置
|
||||
function resetTransparencySettings() {
|
||||
transparencyOpacity.value = 0.3
|
||||
transparencyBlur.value = 10
|
||||
transparencyLevel.value = 'medium'
|
||||
applyTransparencySettings()
|
||||
}
|
||||
|
||||
// 监听主题变化
|
||||
watch(
|
||||
() => currentThemeName.value,
|
||||
() => updateTheme(),
|
||||
async () => {
|
||||
await updateTheme()
|
||||
|
||||
// 如果切换到透明主题,应用透明度设置
|
||||
if (currentThemeName.value === 'transparent') {
|
||||
applyTransparencySettings()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 监听系统主题变化
|
||||
try {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', async () => {
|
||||
await updateTheme()
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(t('theme.deviceNotSupport'))
|
||||
}
|
||||
@@ -338,6 +451,11 @@ const getThemeIcon = computed(() => {
|
||||
|
||||
onMounted(() => {
|
||||
getCustomCSS()
|
||||
|
||||
// 初始化透明度设置
|
||||
if (isTransparentTheme.value) {
|
||||
applyTransparencySettings()
|
||||
}
|
||||
})
|
||||
|
||||
// 组件卸载时清理轮询
|
||||
@@ -443,6 +561,20 @@ onUnmounted(() => {
|
||||
</template>
|
||||
<VListItemTitle>{{ t('theme.custom') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
|
||||
<!-- 透明度调整 - 仅在透明主题下显示 -->
|
||||
<template v-if="isTransparentTheme">
|
||||
<VDivider class="my-2" />
|
||||
<VListItem @click="showTransparencyDialog = true">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-opacity" />
|
||||
</template>
|
||||
<VListItemTitle>{{ t('theme.transparencyAdjust') }}</VListItemTitle>
|
||||
<template #append>
|
||||
<VIcon icon="mdi-chevron-right" size="small" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VList>
|
||||
</VMenu>
|
||||
|
||||
@@ -540,6 +672,98 @@ onUnmounted(() => {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
|
||||
<!-- 透明度调整对话框 -->
|
||||
<DialogWrapper v-if="showTransparencyDialog" v-model="showTransparencyDialog" max-width="30rem">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-opacity" class="me-2" />
|
||||
{{ t('theme.transparencyAdjust') }}
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="showTransparencyDialog = false" />
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<div class="space-y-6">
|
||||
<!-- 透明度滑动条 -->
|
||||
<div>
|
||||
<div class="d-flex align-center justify-space-between mb-2">
|
||||
<span class="text-body-2">{{ t('theme.transparencyOpacity') }}</span>
|
||||
<span class="text-caption">{{ Math.round(transparencyOpacity * 100) }}%</span>
|
||||
</div>
|
||||
<VSlider
|
||||
v-model="transparencyOpacity"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
color="primary"
|
||||
@update:model-value="onOpacityChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 模糊度滑动条 -->
|
||||
<div>
|
||||
<div class="d-flex align-center justify-space-between mb-2">
|
||||
<span class="text-body-2">{{ t('theme.transparencyBlur') }}</span>
|
||||
<span class="text-caption">{{ transparencyBlur }}px</span>
|
||||
</div>
|
||||
<VSlider
|
||||
v-model="transparencyBlur"
|
||||
:min="0"
|
||||
:max="30"
|
||||
:step="1"
|
||||
color="primary"
|
||||
@update:model-value="onBlurChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 预设按钮 -->
|
||||
<div>
|
||||
<span class="text-body-2 d-block mb-2">{{ t('common.preset') }}</span>
|
||||
<VBtnGroup density="compact" variant="outlined" class="w-full">
|
||||
<VBtn
|
||||
size="small"
|
||||
:color="currentPresetLevel === 'low' ? 'primary' : undefined"
|
||||
@click="adjustTransparency('low')"
|
||||
class="flex-1"
|
||||
>
|
||||
{{ t('theme.transparencyLow') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
size="small"
|
||||
:color="currentPresetLevel === 'medium' ? 'primary' : undefined"
|
||||
@click="adjustTransparency('medium')"
|
||||
class="flex-1"
|
||||
>
|
||||
{{ t('theme.transparencyMedium') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
size="small"
|
||||
:color="currentPresetLevel === 'high' ? 'primary' : undefined"
|
||||
@click="adjustTransparency('high')"
|
||||
class="flex-1"
|
||||
>
|
||||
{{ t('theme.transparencyHigh') }}
|
||||
</VBtn>
|
||||
</VBtnGroup>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VDivider />
|
||||
<VCardText class="text-center">
|
||||
<VBtn @click="resetTransparencySettings" variant="outlined" class="me-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-refresh" />
|
||||
</template>
|
||||
{{ t('theme.transparencyReset') }}
|
||||
</VBtn>
|
||||
<VBtn @click="showTransparencyDialog = false" color="primary">
|
||||
{{ t('common.confirm') }}
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -62,6 +62,7 @@ export default {
|
||||
serviceAvailable: 'Service Available',
|
||||
serviceUnavailable: 'Service Unavailable',
|
||||
status: 'Status',
|
||||
preset: 'Preset',
|
||||
},
|
||||
mediaType: {
|
||||
movie: 'Movie',
|
||||
@@ -127,7 +128,15 @@ export default {
|
||||
auto: 'Follow System',
|
||||
transparent: 'Transparent',
|
||||
purple: 'Purple',
|
||||
custom: 'Custom Theme',
|
||||
custom: 'Custom Style',
|
||||
transparency: 'Transparency',
|
||||
transparencyAdjust: 'Transparency Adjustment',
|
||||
transparencyOpacity: 'Opacity',
|
||||
transparencyBlur: 'Blur',
|
||||
transparencyReset: 'Reset',
|
||||
transparencyLow: 'Low Transparency',
|
||||
transparencyMedium: 'Medium Transparency',
|
||||
transparencyHigh: 'High Transparency',
|
||||
customCssSaveSuccess: 'Custom CSS saved successfully, please refresh the page to take effect!',
|
||||
customCssSaveFailed: 'Failed to save custom CSS to server',
|
||||
deviceNotSupport: 'Current device does not support monitoring system theme changes',
|
||||
@@ -523,12 +532,14 @@ export default {
|
||||
waiting: 'Waiting',
|
||||
},
|
||||
info: {
|
||||
trigger: 'Trigger',
|
||||
timer: 'Timer',
|
||||
status: 'Status',
|
||||
actionCount: 'Action Count',
|
||||
runCount: 'Run Count',
|
||||
progress: 'Progress',
|
||||
error: 'Error Message',
|
||||
manualTrigger: 'Manual',
|
||||
},
|
||||
},
|
||||
scanFile: {
|
||||
@@ -675,7 +686,9 @@ export default {
|
||||
searchShares: 'Search Workflow Shares',
|
||||
noShareData: 'No shared workflows',
|
||||
sharer: 'Sharer',
|
||||
trigger: 'Trigger',
|
||||
timer: 'Timer',
|
||||
manualTrigger: 'Manual Trigger',
|
||||
actionCount: 'Action Count',
|
||||
normalFork: 'Fork Workflow',
|
||||
cancelShare: 'Cancel Share',
|
||||
@@ -1040,6 +1053,21 @@ export default {
|
||||
deleteSite: 'Delete Site',
|
||||
updateCookie: 'Update Cookie',
|
||||
viewUserData: 'View User Data',
|
||||
statistics: 'Statistics',
|
||||
totalSites: 'Total Sites',
|
||||
normalSites: 'Normal Sites',
|
||||
slowSites: 'Slow Sites',
|
||||
failedSites: 'Failed Sites',
|
||||
averageTime: 'Average Time',
|
||||
successRate: 'Success Rate',
|
||||
successCount: 'Success Count',
|
||||
failCount: 'Fail Count',
|
||||
lastAccess: 'Last Access',
|
||||
timeRecords: 'Time Records',
|
||||
recentTimeRecords: 'Recent Time Records',
|
||||
accessTime: 'Access Time',
|
||||
responseTime: 'Response Time',
|
||||
noTimeRecords: 'No Time Records',
|
||||
},
|
||||
message: {
|
||||
loadMore: 'Load More',
|
||||
@@ -1051,6 +1079,7 @@ export default {
|
||||
program: 'Program',
|
||||
content: 'Content',
|
||||
refreshing: 'Refreshing',
|
||||
initializing: 'Initializing',
|
||||
},
|
||||
moduleTest: {
|
||||
normal: 'Normal',
|
||||
@@ -1179,7 +1208,7 @@ export default {
|
||||
workflowStatisticShareHint: 'Share workflow statistics to popular workflows for other MP users to reference',
|
||||
bigMemoryMode: 'Large Memory Mode',
|
||||
bigMemoryModeHint: 'Use more memory to cache data and improve system performance',
|
||||
dbWalEnable: 'WAL Mode',
|
||||
dbWalEnable: 'Sqlite WAL Mode',
|
||||
dbWalEnableHint:
|
||||
'Can improve read/write concurrency performance, but may increase the risk of data loss in exceptional cases, requires restart to take effect',
|
||||
tmdbApiDomain: 'TMDB API Service Address',
|
||||
@@ -1340,7 +1369,12 @@ export default {
|
||||
syncBlacklistHint: 'CookieCloud sync domain blacklist, multiple domains separated by commas',
|
||||
userAgent: 'Browser User-Agent',
|
||||
userAgentHint: 'User-Agent of the browser with CookieCloud plugin',
|
||||
browserEmulation: 'Browser Emulation',
|
||||
browserEmulationHint: 'Choose how to emulate browser when accessing sites (Playwright or FlareSolverr)',
|
||||
flaresolverrUrl: 'FlareSolverr URL',
|
||||
flaresolverrUrlHint: 'Required when using FlareSolverr, e.g. http://127.0.0.1:8191',
|
||||
siteDataRefresh: 'Site Data Refresh',
|
||||
siteOptions: 'Site Options',
|
||||
siteDataRefreshInterval: 'Site Data Refresh Interval',
|
||||
siteDataRefreshIntervalHint: 'Time interval for refreshing site user upload/download data',
|
||||
readSiteMessage: 'Read Site Messages',
|
||||
@@ -1582,8 +1616,9 @@ export default {
|
||||
bestVersionRuleGroupHint: 'Filter version upgrade subscriptions based on selected filter rule groups',
|
||||
timedSearch: 'Subscription Scheduled Search',
|
||||
timedSearchHint: 'Search all sites every 24 hours to supplement resources that may be missed by subscription',
|
||||
checkLocalMedia: 'Check Local Media Library',
|
||||
checkLocalMediaHint: 'Check if resources exist on storage disk to avoid duplicate downloads',
|
||||
checkLocalMedia: 'Check File System Resources',
|
||||
checkLocalMediaHint:
|
||||
'Scan the storage directory for existing resource files to avoid duplicate downloads; regardless of whether it is enabled, the media server will be checked',
|
||||
modes: {
|
||||
auto: 'Auto',
|
||||
rss: 'Site RSS',
|
||||
@@ -1867,10 +1902,19 @@ export default {
|
||||
desc: 'Description',
|
||||
descPlaceholder: 'Workflow description',
|
||||
enabled: 'Enabled',
|
||||
triggerType: 'Trigger Type',
|
||||
triggerTypeTimer: 'Timer Trigger',
|
||||
triggerTypeEvent: 'Event Trigger',
|
||||
triggerTypeManual: 'Manual Trigger',
|
||||
schedule: 'Schedule',
|
||||
cronExpr: 'Cron Expression',
|
||||
cronExprDesc: 'Cron expression for workflow scheduling',
|
||||
eventType: 'Event Type',
|
||||
eventTypePlaceholder: 'Please select event type',
|
||||
nameRequired: 'Please fill in complete information!',
|
||||
triggerRequired: 'Please select trigger type!',
|
||||
timerRequired: 'Please fill in timer expression!',
|
||||
eventTypeRequired: 'Please select event type!',
|
||||
addSuccess: 'Task created successfully, please edit the workflow!',
|
||||
addFailed: 'Failed to create task: {message}',
|
||||
editSuccess: 'Task modified successfully!',
|
||||
|
||||
@@ -62,6 +62,7 @@ export default {
|
||||
serviceAvailable: '服务可用',
|
||||
serviceUnavailable: '服务不可用',
|
||||
status: '状态',
|
||||
preset: '预设',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '电影',
|
||||
@@ -127,7 +128,15 @@ export default {
|
||||
auto: '跟随系统',
|
||||
transparent: '透明',
|
||||
purple: '幻紫',
|
||||
custom: '自定义主题',
|
||||
custom: '附加样式',
|
||||
transparency: '透明度',
|
||||
transparencyAdjust: '透明度调整',
|
||||
transparencyOpacity: '透明度',
|
||||
transparencyBlur: '模糊度',
|
||||
transparencyReset: '重置',
|
||||
transparencyLow: '低透明度',
|
||||
transparencyMedium: '中等透明度',
|
||||
transparencyHigh: '高透明度',
|
||||
customCssSaveSuccess: '自定义CSS保存成功,请刷新页面生效!',
|
||||
customCssSaveFailed: '保存自定义CSS到服务端失败',
|
||||
deviceNotSupport: '当前设备不支持监听系统主题变化',
|
||||
@@ -520,12 +529,14 @@ export default {
|
||||
waiting: '等待',
|
||||
},
|
||||
info: {
|
||||
trigger: '触发方式',
|
||||
timer: '定时',
|
||||
status: '状态',
|
||||
actionCount: '动作数',
|
||||
runCount: '已执行次数',
|
||||
progress: '进度',
|
||||
error: '错误信息',
|
||||
manualTrigger: '手动',
|
||||
},
|
||||
},
|
||||
scanFile: {
|
||||
@@ -672,7 +683,9 @@ export default {
|
||||
searchShares: '搜索工作流分享',
|
||||
noShareData: '暂无分享的工作流',
|
||||
sharer: '分享人',
|
||||
trigger: '触发方式',
|
||||
timer: '定时器',
|
||||
manualTrigger: '手动触发',
|
||||
actionCount: '动作数量',
|
||||
normalFork: '复用工作流',
|
||||
cancelShare: '取消分享',
|
||||
@@ -1036,6 +1049,21 @@ export default {
|
||||
deleteSite: '删除站点',
|
||||
updateCookie: '更新Cookie',
|
||||
viewUserData: '查看用户数据',
|
||||
statistics: '统计信息',
|
||||
totalSites: '总站点数',
|
||||
normalSites: '正常站点',
|
||||
slowSites: '缓慢站点',
|
||||
failedSites: '失败站点',
|
||||
averageTime: '平均耗时',
|
||||
successRate: '成功率',
|
||||
successCount: '成功次数',
|
||||
failCount: '失败次数',
|
||||
lastAccess: '最后访问',
|
||||
timeRecords: '耗时记录',
|
||||
recentTimeRecords: '最近耗时记录',
|
||||
accessTime: '访问时间',
|
||||
responseTime: '响应时间',
|
||||
noTimeRecords: '暂无耗时记录',
|
||||
},
|
||||
message: {
|
||||
loadMore: '加载更多',
|
||||
@@ -1047,6 +1075,7 @@ export default {
|
||||
program: '程序',
|
||||
content: '内容',
|
||||
refreshing: '正在刷新',
|
||||
initializing: '正在初始化',
|
||||
},
|
||||
moduleTest: {
|
||||
normal: '正常',
|
||||
@@ -1174,7 +1203,7 @@ export default {
|
||||
workflowStatisticShareHint: '分享工作流统计数据到热门工作流,供其他MPer参考',
|
||||
bigMemoryMode: '大内存模式',
|
||||
bigMemoryModeHint: '使用更大的内存缓存数据,提升系统性能',
|
||||
dbWalEnable: 'WAL模式',
|
||||
dbWalEnable: '数据库WAL模式',
|
||||
dbWalEnableHint: '可提升读写并发性能,但可能在异常情况下增加数据丢失风险,更改后需重启生效',
|
||||
tmdbApiDomain: 'TMDB API服务地址',
|
||||
tmdbApiDomainPlaceholder: 'api.themoviedb.org',
|
||||
@@ -1328,6 +1357,11 @@ export default {
|
||||
userAgent: '浏览器User-Agent',
|
||||
userAgentHint: 'CookieCloud插件所在的浏览器的User-Agent',
|
||||
siteDataRefresh: '站点数据刷新',
|
||||
siteOptions: '站点选项',
|
||||
browserEmulation: '浏览器仿真',
|
||||
browserEmulationHint: '站点访问仿真方式,支持 Playwright 或 FlareSolverr',
|
||||
flaresolverrUrl: 'FlareSolverr 服务地址',
|
||||
flaresolverrUrlHint: '当仿真方式为 FlareSolverr 时生效,例如:http://127.0.0.1:8191',
|
||||
siteDataRefreshInterval: '站点数据刷新间隔',
|
||||
siteDataRefreshIntervalHint: '刷新站点用户上传下载等数据的时间间隔',
|
||||
readSiteMessage: '阅读站点消息',
|
||||
@@ -1561,8 +1595,8 @@ export default {
|
||||
bestVersionRuleGroupHint: '按选定的过滤规则组对洗版订阅进行过滤',
|
||||
timedSearch: '订阅定时搜索',
|
||||
timedSearchHint: '每隔24小时全站搜索,以补全订阅可能漏掉的资源',
|
||||
checkLocalMedia: '检查本地媒体库资源',
|
||||
checkLocalMediaHint: '检查存储盘是否存在资源,以避免重复下载',
|
||||
checkLocalMedia: '检查文件系统资源',
|
||||
checkLocalMediaHint: '扫描存储目录中是否已存在相应资源文件,以避免重复下载;不管是否开启都会检查媒体服务器',
|
||||
modes: {
|
||||
auto: '自动',
|
||||
rss: '站点RSS',
|
||||
@@ -1842,10 +1876,19 @@ export default {
|
||||
desc: '描述',
|
||||
descPlaceholder: '工作流描述',
|
||||
enabled: '启用',
|
||||
triggerType: '触发类型',
|
||||
triggerTypeTimer: '定时触发',
|
||||
triggerTypeEvent: '事件触发',
|
||||
triggerTypeManual: '手动触发',
|
||||
schedule: '定时执行',
|
||||
cronExpr: 'Cron表达式',
|
||||
cronExprDesc: '工作流定时执行的cron表达式',
|
||||
eventType: '事件类型',
|
||||
eventTypePlaceholder: '请选择事件类型',
|
||||
nameRequired: '请填写完整信息!',
|
||||
triggerRequired: '请选择触发类型!',
|
||||
timerRequired: '请填写定时表达式!',
|
||||
eventTypeRequired: '请选择事件类型!',
|
||||
addSuccess: '创建任务成功,请编辑流程!',
|
||||
addFailed: '创建任务失败:{message}',
|
||||
editSuccess: '修改任务成功!',
|
||||
|
||||
@@ -62,6 +62,7 @@ export default {
|
||||
serviceAvailable: '服務可用',
|
||||
serviceUnavailable: '服務不可用',
|
||||
status: '狀態',
|
||||
preset: '預設',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '電影',
|
||||
@@ -127,7 +128,15 @@ export default {
|
||||
auto: '跟隨系統',
|
||||
transparent: '透明',
|
||||
purple: '幻紫',
|
||||
custom: '自定義主題',
|
||||
custom: '附加樣式',
|
||||
transparency: '透明度',
|
||||
transparencyAdjust: '透明度調整',
|
||||
transparencyOpacity: '透明度',
|
||||
transparencyBlur: '模糊度',
|
||||
transparencyReset: '重置',
|
||||
transparencyLow: '低透明度',
|
||||
transparencyMedium: '中等透明度',
|
||||
transparencyHigh: '高透明度',
|
||||
customCssSaveSuccess: '自定義CSS保存成功,請刷新頁面生效!',
|
||||
customCssSaveFailed: '保存自定義CSS到服務端失敗',
|
||||
deviceNotSupport: '當前設備不支持監聽系統主題變化',
|
||||
@@ -518,12 +527,14 @@ export default {
|
||||
waiting: '等待中',
|
||||
},
|
||||
info: {
|
||||
trigger: '觸發方式',
|
||||
timer: '定時器',
|
||||
status: '狀態',
|
||||
actionCount: '動作數量',
|
||||
runCount: '執行次數',
|
||||
progress: '進度',
|
||||
error: '錯誤訊息',
|
||||
manualTrigger: '手動',
|
||||
},
|
||||
},
|
||||
scanFile: {
|
||||
@@ -670,7 +681,9 @@ export default {
|
||||
searchShares: '搜索工作流分享',
|
||||
noShareData: '暫無分享的工作流',
|
||||
sharer: '分享人',
|
||||
trigger: '觸發方式',
|
||||
timer: '定時器',
|
||||
manualTrigger: '手動觸發',
|
||||
actionCount: '動作數量',
|
||||
normalFork: '復用工作流',
|
||||
cancelShare: '取消分享',
|
||||
@@ -1035,6 +1048,21 @@ export default {
|
||||
deleteSite: '刪除站點',
|
||||
updateCookie: '更新Cookie',
|
||||
viewUserData: '查看用戶數據',
|
||||
statistics: '統計信息',
|
||||
totalSites: '總站點數',
|
||||
normalSites: '正常站點',
|
||||
slowSites: '緩慢站點',
|
||||
failedSites: '失敗站點',
|
||||
averageTime: '平均耗時',
|
||||
successRate: '成功率',
|
||||
successCount: '成功次數',
|
||||
failCount: '失敗次數',
|
||||
lastAccess: '最後訪問',
|
||||
timeRecords: '耗時記錄',
|
||||
recentTimeRecords: '最近耗時記錄',
|
||||
accessTime: '訪問時間',
|
||||
responseTime: '響應時間',
|
||||
noTimeRecords: '暫無耗時記錄',
|
||||
},
|
||||
message: {
|
||||
loadMore: '加載更多',
|
||||
@@ -1046,6 +1074,7 @@ export default {
|
||||
program: '程序',
|
||||
content: '內容',
|
||||
refreshing: '正在刷新',
|
||||
initializing: '正在初始化',
|
||||
},
|
||||
moduleTest: {
|
||||
normal: '正常',
|
||||
@@ -1173,7 +1202,7 @@ export default {
|
||||
workflowStatisticShareHint: '分享工作流統計數據到熱門工作流,供其他MPer參考',
|
||||
bigMemoryMode: '大內存模式',
|
||||
bigMemoryModeHint: '使用更大的內存緩存數據,提升系統性能',
|
||||
dbWalEnable: 'WAL模式',
|
||||
dbWalEnable: '數據庫WAL模式',
|
||||
dbWalEnableHint: '可提升讀寫併發性能,但可能在異常情況下增加數據丟失風險,更改後需重啟生效',
|
||||
tmdbApiDomain: 'TMDB API服務地址',
|
||||
tmdbApiDomainPlaceholder: 'api.themoviedb.org',
|
||||
@@ -1327,6 +1356,11 @@ export default {
|
||||
userAgent: '瀏覽器User-Agent',
|
||||
userAgentHint: 'CookieCloud插件所在的瀏覽器的User-Agent',
|
||||
siteDataRefresh: '站點數據刷新',
|
||||
siteOptions: '站點選項',
|
||||
browserEmulation: '瀏覽器仿真',
|
||||
browserEmulationHint: '站點訪問仿真方式,支援 Playwright 或 FlareSolverr',
|
||||
flaresolverrUrl: 'FlareSolverr 服務地址',
|
||||
flaresolverrUrlHint: '當仿真方式為 FlareSolverr 時生效,例如:http://127.0.0.1:8191',
|
||||
siteDataRefreshInterval: '站點數據刷新間隔',
|
||||
siteDataRefreshIntervalHint: '刷新站點用戶上傳下載等數據的時間間隔',
|
||||
readSiteMessage: '閱讀站點消息',
|
||||
@@ -1559,8 +1593,8 @@ export default {
|
||||
bestVersionRuleGroupHint: '按選定的過濾規則組對洗版訂閱進行過濾',
|
||||
timedSearch: '訂閱定時搜索',
|
||||
timedSearchHint: '每隔24小時全站搜索,以補全訂閱可能漏掉的資源',
|
||||
checkLocalMedia: '檢查本地媒體庫資源',
|
||||
checkLocalMediaHint: '檢查存儲盤是否存在資源,以避免重複下載',
|
||||
checkLocalMedia: '檢查文件系統資源',
|
||||
checkLocalMediaHint: '掃描存儲目錄中是否已存在相應資源文件,以避免重複下載;不管是否開啟都會檢查媒體伺服器',
|
||||
modes: {
|
||||
auto: '自動',
|
||||
rss: '站點RSS',
|
||||
@@ -1841,10 +1875,19 @@ export default {
|
||||
desc: '描述',
|
||||
descPlaceholder: '工作流描述',
|
||||
enabled: '啟用',
|
||||
triggerType: '觸發類型',
|
||||
triggerTypeTimer: '定時觸發',
|
||||
triggerTypeEvent: '事件觸發',
|
||||
triggerTypeManual: '手動觸發',
|
||||
schedule: '定時執行',
|
||||
cronExpr: 'Cron表達式',
|
||||
cronExprDesc: '工作流定時執行的cron表達式',
|
||||
eventType: '事件類型',
|
||||
eventTypePlaceholder: '請選擇事件類型',
|
||||
nameRequired: '請填寫完整資訊!',
|
||||
triggerRequired: '請選擇觸發類型!',
|
||||
timerRequired: '請填寫定時表達式!',
|
||||
eventTypeRequired: '請選擇事件類型!',
|
||||
addSuccess: '建立任務成功,請編輯流程!',
|
||||
addFailed: '建立任務失敗:{message}',
|
||||
editSuccess: '修改任務成功!',
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { VCardActions } from 'vuetify/components'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { getItemColor, initializeItemColors } from '@/utils/colorUtils'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -161,6 +162,18 @@ const pluginDashboardRefreshStatus = ref<{ [key: string]: boolean }>({})
|
||||
// 弹窗
|
||||
const dialog = ref(false)
|
||||
|
||||
// 为每个项目生成随机颜色
|
||||
const itemColors = ref<{ [key: string]: string }>({})
|
||||
|
||||
// 初始化颜色
|
||||
function initializeColors() {
|
||||
initializeItemColors(dashboardConfigs.value, item => buildPluginDashboardId(item.id, item.key))
|
||||
dashboardConfigs.value.forEach(item => {
|
||||
const itemId = buildPluginDashboardId(item.id, item.key)
|
||||
itemColors.value[itemId] = getItemColor(itemId)
|
||||
})
|
||||
}
|
||||
|
||||
// 使用动态按钮钩子
|
||||
useDynamicButton({
|
||||
icon: 'mdi-view-dashboard-edit',
|
||||
@@ -286,6 +299,11 @@ async function getPluginDashboard(id: string, key: string) {
|
||||
dashboardConfigs.value[index] = res
|
||||
} else {
|
||||
dashboardConfigs.value.push(res)
|
||||
// 为新增的插件仪表板生成颜色
|
||||
const pluginDashboardId = buildPluginDashboardId(id, key)
|
||||
if (!itemColors.value[pluginDashboardId]) {
|
||||
itemColors.value[pluginDashboardId] = getItemColor(pluginDashboardId)
|
||||
}
|
||||
// 排序
|
||||
sortDashboardConfigs()
|
||||
}
|
||||
@@ -322,6 +340,7 @@ function dragOrderEnd() {
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await loadDashboardConfig()
|
||||
initializeColors()
|
||||
getPluginDashboardMeta()
|
||||
})
|
||||
|
||||
@@ -390,6 +409,7 @@ onDeactivated(() => {
|
||||
:class="{
|
||||
'enabled': enableConfig[buildPluginDashboardId(item.id, item.key)],
|
||||
}"
|
||||
:style="{ '--item-color': itemColors[buildPluginDashboardId(item.id, item.key)] }"
|
||||
@click="
|
||||
enableConfig[buildPluginDashboardId(item.id, item.key)] =
|
||||
!enableConfig[buildPluginDashboardId(item.id, item.key)]
|
||||
@@ -444,8 +464,11 @@ onDeactivated(() => {
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
flex: 1;
|
||||
color: rgba(var(--v-theme-on-surface), 0.8);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -462,7 +485,7 @@ onDeactivated(() => {
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
background-color: var(--item-color, #4caf50);
|
||||
block-size: 100%;
|
||||
content: '';
|
||||
inline-size: 4px;
|
||||
@@ -472,16 +495,15 @@ onDeactivated(() => {
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(var(--v-theme-on-surface), 0.15);
|
||||
background-color: rgba(var(--v-theme-surface-variant), 0.6);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.enabled {
|
||||
border-color: rgba(var(--v-theme-primary), 0.5);
|
||||
background-color: rgba(var(--v-theme-primary), 0.05);
|
||||
border-color: rgba(var(--v-theme-primary), 0.3);
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
|
||||
.setting-label {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
color: rgba(var(--v-theme-primary), 0.9);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -490,9 +512,16 @@ onDeactivated(() => {
|
||||
.setting-item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.setting-check {
|
||||
margin-inline-end: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.settings-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,6 +10,7 @@ import api from '@/api'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
||||
import { getItemColor, initializeItemColors } from '@/utils/colorUtils'
|
||||
|
||||
const display = useDisplay()
|
||||
|
||||
@@ -44,6 +45,17 @@ const extraDiscoverSources = ref<DiscoverSource[]>([])
|
||||
// 排序对话框
|
||||
const orderConfigDialog = ref(false)
|
||||
|
||||
// 为每个项目生成随机颜色
|
||||
const itemColors = ref<{ [key: string]: string }>({})
|
||||
|
||||
// 初始化颜色
|
||||
function initializeColors() {
|
||||
initializeItemColors(discoverTabs.value, item => item.mediaid_prefix)
|
||||
discoverTabs.value.forEach(item => {
|
||||
itemColors.value[item.mediaid_prefix] = getItemColor(item.mediaid_prefix)
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化发现标签
|
||||
function initDiscoverTabs() {
|
||||
const tabs = getDiscoverTabs()
|
||||
@@ -70,6 +82,10 @@ async function loadExtraDiscoverSources() {
|
||||
continue
|
||||
}
|
||||
discoverTabs.value.push(source)
|
||||
// 为新增的数据源生成颜色
|
||||
if (!itemColors.value[source.mediaid_prefix]) {
|
||||
itemColors.value[source.mediaid_prefix] = getItemColor(source.mediaid_prefix)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
@@ -145,6 +161,7 @@ registerHeaderTab({
|
||||
|
||||
onBeforeMount(async () => {
|
||||
initDiscoverTabs()
|
||||
initializeColors()
|
||||
await loadOrderConfig()
|
||||
await loadExtraDiscoverSources()
|
||||
sortSubscribeOrder()
|
||||
@@ -225,9 +242,14 @@ onActivated(async () => {
|
||||
:component-data="{ 'class': 'settings-grid' }"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<VCard variant="text" class="setting-item enabled">
|
||||
<div class="setting-item-inner cursor-move text-center">
|
||||
<VCard
|
||||
variant="text"
|
||||
class="setting-item enabled"
|
||||
:style="{ '--item-color': itemColors[element.mediaid_prefix] }"
|
||||
>
|
||||
<div class="setting-item-inner">
|
||||
<span class="setting-label">{{ element.name }}</span>
|
||||
<VIcon icon="mdi-drag" class="drag-icon cursor-move" />
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -269,8 +291,11 @@ onActivated(async () => {
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
flex: 1;
|
||||
color: rgba(var(--v-theme-on-surface), 0.8);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -287,8 +312,7 @@ onActivated(async () => {
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
background-color: var(--item-color, #4caf50);
|
||||
block-size: 100%;
|
||||
content: '';
|
||||
inline-size: 4px;
|
||||
@@ -298,16 +322,15 @@ onActivated(async () => {
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(var(--v-theme-on-surface), 0.15);
|
||||
background-color: rgba(var(--v-theme-surface-variant), 0.6);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.enabled {
|
||||
border-color: rgba(var(--v-theme-primary), 0.5);
|
||||
background-color: rgba(var(--v-theme-primary), 0.05);
|
||||
border-color: rgba(var(--v-theme-primary), 0.3);
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
|
||||
.setting-label {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
color: rgba(var(--v-theme-primary), 0.9);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -316,9 +339,22 @@ onActivated(async () => {
|
||||
.setting-item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.setting-check {
|
||||
margin-inline-end: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.drag-icon {
|
||||
flex-shrink: 0;
|
||||
color: rgba(var(--v-theme-on-surface), 0.5);
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.settings-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,7 @@ import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
|
||||
import { getItemColor, initializeItemColors } from '@/utils/colorUtils'
|
||||
|
||||
const display = useDisplay()
|
||||
|
||||
@@ -114,6 +115,17 @@ const enableConfig = ref<{ [key: string]: boolean }>({
|
||||
...Object.fromEntries(viewList.map(item => [item.title, true])),
|
||||
})
|
||||
|
||||
// 为每个项目生成随机颜色
|
||||
const itemColors = ref<{ [key: string]: string }>({})
|
||||
|
||||
// 初始化颜色
|
||||
function initializeColors() {
|
||||
initializeItemColors(viewList, item => item.title)
|
||||
viewList.forEach(item => {
|
||||
itemColors.value[item.title] = getItemColor(item.title)
|
||||
})
|
||||
}
|
||||
|
||||
// 弹窗
|
||||
const dialog = ref(false)
|
||||
|
||||
@@ -127,9 +139,11 @@ async function loadExtraRecommendSources() {
|
||||
if (extraRecommendSources.value.length > 0) {
|
||||
extraRecommendSources.value.map(source => {
|
||||
if (!viewList.some(item => item.apipath === source.api_path)) {
|
||||
const querySeparator = source.api_path.includes('?') ? '&' : '?'
|
||||
const linkUrl = `/browse/${source.api_path}${querySeparator}title=${encodeURIComponent(source.name)}`
|
||||
viewList.push({
|
||||
apipath: source.api_path,
|
||||
linkurl: `/browse/${source.api_path}&title=${source.name}`,
|
||||
linkurl: linkUrl,
|
||||
title: source.name,
|
||||
type: source.type,
|
||||
})
|
||||
@@ -219,10 +233,17 @@ registerHeaderTab({
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await loadConfig()
|
||||
initializeColors()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadExtraRecommendSources()
|
||||
// 为新增的数据源也生成颜色
|
||||
extraRecommendSources.value.forEach(source => {
|
||||
if (!itemColors.value[source.name]) {
|
||||
itemColors.value[source.name] = getItemColor(source.name)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onActivated(async () => {
|
||||
@@ -273,8 +294,8 @@ onActivated(async () => {
|
||||
class="setting-item"
|
||||
:class="{
|
||||
'enabled': enableConfig[item.title],
|
||||
[item.type]: true,
|
||||
}"
|
||||
:style="{ '--item-color': itemColors[item.title] }"
|
||||
@click="enableConfig[item.title] = !enableConfig[item.title]"
|
||||
>
|
||||
<div class="setting-item-inner">
|
||||
@@ -392,7 +413,7 @@ onActivated(async () => {
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
background-color: var(--item-color, #4caf50);
|
||||
block-size: 100%;
|
||||
content: '';
|
||||
inline-size: 4px;
|
||||
@@ -401,19 +422,6 @@ onActivated(async () => {
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
&.电影::before {
|
||||
background-color: #4caf50;
|
||||
} // Green
|
||||
&.电视剧::before {
|
||||
background-color: #2196f3;
|
||||
} // Blue
|
||||
&.动漫::before {
|
||||
background-color: #ff9800;
|
||||
} // Orange
|
||||
&.排行榜::before {
|
||||
background-color: #9c27b0;
|
||||
} // Purple
|
||||
|
||||
&.enabled {
|
||||
border-color: rgba(var(--v-theme-primary), 0.3);
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
@@ -450,7 +458,7 @@ onActivated(async () => {
|
||||
|
||||
@media (width <= 600px) {
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,7 @@ declare let self: ServiceWorkerGlobalScope & {
|
||||
}
|
||||
|
||||
// 缓存版本控制
|
||||
const CACHE_VERSION = 'v1.0.4'
|
||||
const CACHE_VERSION = 'v1.0.5'
|
||||
const CACHE_NAMES = {
|
||||
appShell: `app-shell-${CACHE_VERSION}`,
|
||||
static: `static-resources-${CACHE_VERSION}`,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Write your overrides
|
||||
// 公共样式 - 所有主题都需要
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
// 基础样式
|
||||
html.v-overlay-scroll-blocked {
|
||||
position: fixed;
|
||||
position: relative;
|
||||
@@ -30,6 +30,7 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
// 进度条样式
|
||||
#nprogress .bar {
|
||||
background: rgb(var(--v-theme-primary)) !important;
|
||||
inset-block-start: env(safe-area-inset-top) !important;
|
||||
@@ -38,15 +39,17 @@ body {
|
||||
#nprogress .peg {
|
||||
box-shadow: 0 0 10px rgb(var(--v-theme-primary)), 0 0 5px rgb(var(--v-theme-primary)) !important;
|
||||
inline-size: 5px;
|
||||
transform: rotate(0deg) translate(0, 0);
|
||||
transform: rotate(0deg) translate(0, 0);
|
||||
}
|
||||
|
||||
// 卡片高度匹配
|
||||
.match-height.v-row {
|
||||
.v-card {
|
||||
block-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Toast通知样式
|
||||
.Vue-Toastification__container {
|
||||
z-index: 2500;
|
||||
margin-block: env(safe-area-inset-top) env(safe-area-inset-bottom);
|
||||
@@ -64,11 +67,12 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
// 对话框样式
|
||||
.v-dialog > .v-overlay__content > .v-card > .v-card-item {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* router view transition fade-slide */
|
||||
// 路由过渡动画
|
||||
.fade-slide-leave-active,
|
||||
.fade-slide-enter-active {
|
||||
transition: all 0.6s;
|
||||
@@ -84,99 +88,13 @@ body {
|
||||
transform: translateY(45px);
|
||||
}
|
||||
|
||||
// 网格布局样式
|
||||
.grid-info-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
|
||||
.text-moviepilot {
|
||||
background-clip: text;
|
||||
background-image: linear-gradient(to bottom right,var(--tw-gradient-stops));
|
||||
color: transparent;
|
||||
|
||||
--tw-gradient-from: #818cf8;
|
||||
--tw-gradient-stops: var(--tw-gradient-from),var(--tw-gradient-to);
|
||||
--tw-gradient-to: #c084fc;
|
||||
}
|
||||
|
||||
.slider-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.slider-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
@media (width >= 640px){
|
||||
.slider-title {
|
||||
overflow: hidden;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2.25rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// 美化滚动条
|
||||
::-webkit-scrollbar {
|
||||
block-size: 4px;
|
||||
inline-size: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 2px;
|
||||
background: rgb(var(--v-theme-perfect-scrollbar-thumb));
|
||||
box-shadow: inset 0 0 10px rgba(0,0,0,20%);
|
||||
|
||||
@media(hover){
|
||||
&:hover{
|
||||
background: #a1a1a1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 当鼠标悬停在可滚动元素上时显示滚动条
|
||||
*:hover::-webkit-scrollbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 当元素正在滚动时显示滚动条
|
||||
*:active::-webkit-scrollbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.v-alert--variant-elevated, .v-alert--variant-flat {
|
||||
background: rgb(var(--v-table-header-background));
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
.backdrop-blur {
|
||||
--tw-backdrop-blur: blur(8px)!important;
|
||||
|
||||
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important;
|
||||
}
|
||||
|
||||
.v-toolbar{
|
||||
background: rgb(var(--v-table-header-background));
|
||||
}
|
||||
|
||||
.v-divider {
|
||||
border-color: rgba(var(--v-theme-on-background), var(--v-selected-opacity));
|
||||
opacity:0.75;
|
||||
}
|
||||
|
||||
.apexcharts-title-text {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
|
||||
}
|
||||
|
||||
.grid-site-card {
|
||||
.grid-site-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
||||
padding-block-end: 1rem;
|
||||
}
|
||||
@@ -233,6 +151,98 @@ body {
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
}
|
||||
|
||||
// 文本样式
|
||||
.text-moviepilot {
|
||||
background-clip: text;
|
||||
background-image: linear-gradient(to bottom right,var(--tw-gradient-stops));
|
||||
color: transparent;
|
||||
|
||||
--tw-gradient-from: #818cf8;
|
||||
--tw-gradient-stops: var(--tw-gradient-from),var(--tw-gradient-to);
|
||||
--tw-gradient-to: #c084fc;
|
||||
}
|
||||
|
||||
.text-shadow {
|
||||
text-shadow: 1px 1px #777;
|
||||
}
|
||||
|
||||
// 滑块标题样式
|
||||
.slider-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.slider-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
@media (width >= 640px){
|
||||
.slider-title {
|
||||
overflow: hidden;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2.25rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
::-webkit-scrollbar {
|
||||
block-size: 4px;
|
||||
inline-size: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 2px;
|
||||
background: rgb(var(--v-theme-perfect-scrollbar-thumb));
|
||||
box-shadow: inset 0 0 10px rgba(0,0,0,20%);
|
||||
|
||||
@media(hover){
|
||||
&:hover{
|
||||
background: #a1a1a1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*:hover::-webkit-scrollbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
*:active::-webkit-scrollbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 组件样式
|
||||
.v-alert--variant-elevated, .v-alert--variant-flat {
|
||||
background: rgb(var(--v-table-header-background));
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
.backdrop-blur {
|
||||
--tw-backdrop-blur: blur(8px)!important;
|
||||
|
||||
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)!important;
|
||||
}
|
||||
|
||||
.v-toolbar{
|
||||
background: rgb(var(--v-table-header-background));
|
||||
}
|
||||
|
||||
.v-divider {
|
||||
border-color: rgba(var(--v-theme-on-background), var(--v-selected-opacity));
|
||||
opacity:0.75;
|
||||
}
|
||||
|
||||
.apexcharts-title-text {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
|
||||
}
|
||||
|
||||
.v-tabs:not(.v-tabs-pill).v-tabs--horizontal {
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
@@ -241,10 +251,6 @@ body {
|
||||
padding-block-end: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.text-shadow {
|
||||
text-shadow: 1px 1px #777;
|
||||
}
|
||||
|
||||
.card-cover-blurred::before {
|
||||
position: absolute;
|
||||
backdrop-filter: blur(2px);
|
||||
@@ -253,6 +259,7 @@ body {
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
// 弹出层样式
|
||||
.v-overlay__content .v-list{
|
||||
backdrop-filter: blur(6px);
|
||||
background-color: rgb(var(--v-theme-surface), 0.9) !important;
|
||||
@@ -308,121 +315,10 @@ body {
|
||||
min-inline-size: auto;
|
||||
}
|
||||
|
||||
|
||||
.v-infinite-scroll__side {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.v-menu .v-overlay__content {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
// 透明主题下的弹出窗口样式
|
||||
html[data-theme="transparent"] {
|
||||
// 先将所有全局组件定义放在前面,避免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);
|
||||
|
||||
.v-table__wrapper > table > thead {
|
||||
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);
|
||||
}
|
||||
|
||||
// 输入框和搜索框
|
||||
.v-field {
|
||||
background-color: rgba(var(--v-theme-surface), 0);
|
||||
}
|
||||
|
||||
.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__wrapper table thead {
|
||||
background-color: rgba(var(--v-theme-surface), 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
@use '@core/scss/index' as template;
|
||||
@use '@layouts/styles/index' as layouts;
|
||||
@use 'vuetify/styles' as vuetify;
|
||||
@use '@styles/custom' as custom;
|
||||
@use '@styles/common' as common;
|
||||
|
||||
/* 第三方库纯CSS样式 */
|
||||
@import 'vue-toastification/dist/index.css';
|
||||
|
||||
118
src/styles/themes/transparent.scss
Normal file
118
src/styles/themes/transparent.scss
Normal file
@@ -0,0 +1,118 @@
|
||||
// 透明主题专用样式
|
||||
html[data-theme="transparent"] {
|
||||
// 定义透明度变量
|
||||
--transparent-opacity: 0.3;
|
||||
--transparent-opacity-light: 0.2;
|
||||
--transparent-opacity-heavy: 0.5;
|
||||
--transparent-blur: 10px;
|
||||
--transparent-blur-light: 6px;
|
||||
--transparent-blur-heavy: 16px;
|
||||
|
||||
// 应用、布局、主内容区域
|
||||
.v-application, .v-layout, .v-main, .layout-page-content {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// 侧边导航栏
|
||||
.layout-vertical-nav {
|
||||
backdrop-filter: blur(var(--transparent-blur-heavy));
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-light));
|
||||
border-inline-end: 1px solid rgba(var(--v-theme-on-surface), 0.05);
|
||||
}
|
||||
|
||||
// 列表
|
||||
.v-list {
|
||||
backdrop-filter: blur(var(--transparent-blur));
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity));
|
||||
}
|
||||
|
||||
// 卡片
|
||||
.v-card:not(.no-blur) {
|
||||
backdrop-filter: blur(var(--transparent-blur));
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity));
|
||||
|
||||
.v-list {
|
||||
backdrop-filter: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// 工具栏
|
||||
.v-toolbar {
|
||||
backdrop-filter: blur(var(--transparent-blur));
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity));
|
||||
}
|
||||
|
||||
// 表格
|
||||
.v-table {
|
||||
border-radius: 0;
|
||||
background-color: rgba(var(--v-theme-surface), 0);
|
||||
|
||||
.v-table__wrapper > table > thead {
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity));
|
||||
}
|
||||
}
|
||||
|
||||
// 页脚
|
||||
.v-footer {
|
||||
backdrop-filter: blur(var(--transparent-blur));
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity));
|
||||
}
|
||||
|
||||
// Sheet
|
||||
.v-sheet {
|
||||
backdrop-filter: blur(var(--transparent-blur));
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity));
|
||||
}
|
||||
|
||||
// 页面容器
|
||||
.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(var(--transparent-blur));
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity));
|
||||
}
|
||||
|
||||
// 加载占位
|
||||
.v-skeleton-loader {
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity));
|
||||
}
|
||||
|
||||
// 输入框和搜索框
|
||||
.v-field {
|
||||
background-color: rgba(var(--v-theme-surface), 0);
|
||||
}
|
||||
|
||||
// 弹出层内容
|
||||
.v-overlay__content {
|
||||
border-radius: 12px !important;
|
||||
backdrop-filter: blur(var(--transparent-blur)) !important;
|
||||
|
||||
.v-card:not(.bg-primary) {
|
||||
backdrop-filter: blur(var(--transparent-blur));
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy)) !important;
|
||||
}
|
||||
|
||||
.v-list {
|
||||
backdrop-filter: blur(var(--transparent-blur));
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy)) !important;
|
||||
}
|
||||
|
||||
.v-table__wrapper table thead {
|
||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
137
src/utils/colorUtils.ts
Normal file
137
src/utils/colorUtils.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
// 预定义的颜色数组,包含更多丰富的颜色选项
|
||||
const COLORS = [
|
||||
// 基础颜色
|
||||
'#4caf50', // 绿色
|
||||
'#2196f3', // 蓝色
|
||||
'#ff9800', // 橙色
|
||||
'#9c27b0', // 紫色
|
||||
'#f44336', // 红色
|
||||
'#00bcd4', // 青色
|
||||
'#8bc34a', // 浅绿色
|
||||
'#ff5722', // 深橙色
|
||||
'#3f51b5', // 靛蓝色
|
||||
'#009688', // 青绿色
|
||||
'#e91e63', // 粉红色
|
||||
'#673ab7', // 深紫色
|
||||
'#ffc107', // 琥珀色
|
||||
'#795548', // 棕色
|
||||
'#607d8b', // 蓝灰色
|
||||
|
||||
// 扩展颜色
|
||||
'#ff4081', // 深粉红色
|
||||
'#00e676', // 浅绿色
|
||||
'#ff6f00', // 深橙色
|
||||
'#4fc3f7', // 浅蓝色
|
||||
'#ba68c8', // 浅紫色
|
||||
'#81c784', // 浅绿色
|
||||
'#ffb74d', // 浅橙色
|
||||
'#64b5f6', // 浅蓝色
|
||||
'#f06292', // 浅粉红色
|
||||
'#4db6ac', // 浅青绿色
|
||||
'#aed581', // 浅绿色
|
||||
'#ffd54f', // 浅黄色
|
||||
'#7986cb', // 浅靛蓝色
|
||||
'#4dd0e1', // 浅青色
|
||||
'#ff8a65', // 浅红色
|
||||
'#9575cd', // 浅紫色
|
||||
'#4fc3f7', // 天蓝色
|
||||
'#ffcc02', // 金黄色
|
||||
'#7cb342', // 浅绿色
|
||||
'#42a5f5', // 蓝色
|
||||
'#ab47bc', // 紫色
|
||||
'#26a69a', // 青绿色
|
||||
'#66bb6a', // 绿色
|
||||
'#ff7043', // 深橙色
|
||||
'#29b6f6', // 浅蓝色
|
||||
'#7e57c2', // 紫色
|
||||
'#26c6da', // 青色
|
||||
'#9ccc65', // 浅绿色
|
||||
'#ffb300', // 琥珀色
|
||||
'#8d6e63', // 棕色
|
||||
'#78909c', // 蓝灰色
|
||||
'#ef5350', // 红色
|
||||
'#ec407a', // 粉红色
|
||||
'#ab47bc', // 紫色
|
||||
'#42a5f5', // 蓝色
|
||||
'#7cb342', // 绿色
|
||||
'#ffa726', // 橙色
|
||||
'#26c6da', // 青色
|
||||
'#d4e157', // 浅绿色
|
||||
'#ffca28', // 黄色
|
||||
'#9fa8da', // 浅靛蓝色
|
||||
'#80cbc4', // 浅青绿色
|
||||
'#c5e1a5', // 浅绿色
|
||||
'#ffe082', // 浅黄色
|
||||
'#b39ddb', // 浅紫色
|
||||
'#90caf9', // 浅蓝色
|
||||
'#a5d6a7', // 浅绿色
|
||||
'#ffcc80', // 浅橙色
|
||||
'#b2dfdb', // 浅青绿色
|
||||
'#f8bbd9', // 浅粉红色
|
||||
'#c8e6c9', // 浅绿色
|
||||
'#fff9c4', // 浅黄色
|
||||
'#d1c4e9', // 浅紫色
|
||||
'#bbdefb', // 浅蓝色
|
||||
'#c8e6c9', // 浅绿色
|
||||
'#ffecb3', // 浅琥珀色
|
||||
'#d7ccc8', // 浅棕色
|
||||
'#cfd8dc', // 浅蓝灰色
|
||||
]
|
||||
|
||||
// 颜色缓存,确保同一项目总是获得相同颜色
|
||||
const colorCache = new Map<string, string>()
|
||||
|
||||
/**
|
||||
* 生成随机颜色
|
||||
* @returns 随机颜色值
|
||||
*/
|
||||
export function generateRandomColor(): string {
|
||||
return COLORS[Math.floor(Math.random() * COLORS.length)]
|
||||
}
|
||||
|
||||
/**
|
||||
* 为指定项目获取或生成颜色
|
||||
* @param itemKey 项目的唯一标识
|
||||
* @returns 颜色值
|
||||
*/
|
||||
export function getItemColor(itemKey: string): string {
|
||||
if (!colorCache.has(itemKey)) {
|
||||
colorCache.set(itemKey, generateRandomColor())
|
||||
}
|
||||
return colorCache.get(itemKey)!
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化项目颜色
|
||||
* @param items 项目数组
|
||||
* @param keyExtractor 从项目中提取唯一键的函数
|
||||
*/
|
||||
export function initializeItemColors<T>(items: T[], keyExtractor: (item: T) => string): void {
|
||||
items.forEach(item => {
|
||||
const key = keyExtractor(item)
|
||||
getItemColor(key) // 这会自动缓存颜色
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除颜色缓存
|
||||
*/
|
||||
export function clearColorCache(): void {
|
||||
colorCache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有预定义颜色
|
||||
* @returns 颜色数组
|
||||
*/
|
||||
export function getAllColors(): string[] {
|
||||
return [...COLORS]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取颜色总数
|
||||
* @returns 颜色数量
|
||||
*/
|
||||
export function getColorCount(): number {
|
||||
return COLORS.length
|
||||
}
|
||||
@@ -14,6 +14,8 @@ export class SSEManager {
|
||||
reconnectDelay: number
|
||||
maxReconnectAttempts: number
|
||||
}
|
||||
private reconnectAttempts = 0
|
||||
private isConnecting = false
|
||||
|
||||
constructor(url: string, options: Partial<typeof SSEManager.prototype.options> = {}) {
|
||||
this.url = url
|
||||
@@ -21,7 +23,7 @@ export class SSEManager {
|
||||
backgroundCloseDelay: 5000, // 5秒后关闭后台连接
|
||||
reconnectDelay: 3000, // 3秒后重连
|
||||
maxReconnectAttempts: 3,
|
||||
...options
|
||||
...options,
|
||||
}
|
||||
|
||||
this.setupVisibilityListener()
|
||||
@@ -44,15 +46,14 @@ export class SSEManager {
|
||||
|
||||
private handleBackground() {
|
||||
this.isBackground = true
|
||||
|
||||
|
||||
// 延迟关闭SSE连接,避免频繁切换
|
||||
if (this.backgroundCloseTimer) {
|
||||
clearTimeout(this.backgroundCloseTimer)
|
||||
}
|
||||
|
||||
|
||||
this.backgroundCloseTimer = window.setTimeout(() => {
|
||||
if (this.isBackground && this.eventSource) {
|
||||
console.log('SSE: 后台关闭连接')
|
||||
this.eventSource.close()
|
||||
this.eventSource = null
|
||||
}
|
||||
@@ -61,51 +62,57 @@ export class SSEManager {
|
||||
|
||||
private handleForeground() {
|
||||
this.isBackground = false
|
||||
|
||||
|
||||
// 清除后台关闭定时器
|
||||
if (this.backgroundCloseTimer) {
|
||||
clearTimeout(this.backgroundCloseTimer)
|
||||
this.backgroundCloseTimer = null
|
||||
}
|
||||
|
||||
|
||||
// 立即重新建立连接
|
||||
if (!this.eventSource || this.eventSource.readyState === EventSource.CLOSED) {
|
||||
console.log('SSE: 前台恢复连接')
|
||||
this.reconnectSSE()
|
||||
}
|
||||
}
|
||||
|
||||
private reconnectSSE(attemptCount = 0) {
|
||||
if (attemptCount >= this.options.maxReconnectAttempts) {
|
||||
console.warn('SSE: 达到最大重连次数')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isConnecting) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isConnecting = true
|
||||
this.reconnectAttempts = attemptCount
|
||||
|
||||
try {
|
||||
this.eventSource = new EventSource(this.url)
|
||||
|
||||
|
||||
this.eventSource.onopen = () => {
|
||||
console.log('SSE: 连接已建立')
|
||||
this.isConnecting = false
|
||||
this.reconnectAttempts = 0
|
||||
}
|
||||
|
||||
this.eventSource.onerror = (error) => {
|
||||
console.error('SSE: 连接错误', error)
|
||||
|
||||
|
||||
this.eventSource.onerror = error => {
|
||||
this.isConnecting = false
|
||||
|
||||
if (this.eventSource?.readyState === EventSource.CLOSED) {
|
||||
// 连接已关闭,尝试重连
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
}
|
||||
|
||||
|
||||
this.reconnectTimer = window.setTimeout(() => {
|
||||
if (!this.isBackground) {
|
||||
this.reconnectSSE(attemptCount + 1)
|
||||
this.reconnectSSE(this.reconnectAttempts + 1)
|
||||
}
|
||||
}, this.options.reconnectDelay)
|
||||
}
|
||||
}
|
||||
|
||||
this.eventSource.onmessage = (event) => {
|
||||
|
||||
this.eventSource.onmessage = event => {
|
||||
// 分发消息给所有监听器
|
||||
this.listeners.forEach(listener => {
|
||||
try {
|
||||
@@ -115,9 +122,19 @@ export class SSEManager {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('SSE: 创建连接失败', error)
|
||||
this.isConnecting = false
|
||||
|
||||
// 连接创建失败,尝试重连
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
}
|
||||
|
||||
this.reconnectTimer = window.setTimeout(() => {
|
||||
if (!this.isBackground) {
|
||||
this.reconnectSSE(this.reconnectAttempts + 1)
|
||||
}
|
||||
}, this.options.reconnectDelay)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,9 +143,9 @@ export class SSEManager {
|
||||
*/
|
||||
addMessageListener(id: string, listener: (event: MessageEvent) => void) {
|
||||
this.listeners.set(id, listener)
|
||||
|
||||
// 如果还没有连接,现在建立连接
|
||||
if (!this.eventSource && !this.isBackground) {
|
||||
|
||||
// 如果还没有连接且不在后台,现在建立连接
|
||||
if (!this.eventSource && !this.isBackground && !this.isConnecting) {
|
||||
this.reconnectSSE()
|
||||
}
|
||||
}
|
||||
@@ -138,7 +155,7 @@ export class SSEManager {
|
||||
*/
|
||||
removeMessageListener(id: string) {
|
||||
this.listeners.delete(id)
|
||||
|
||||
|
||||
// 如果没有监听器了,关闭连接
|
||||
if (this.listeners.size === 0) {
|
||||
this.close()
|
||||
@@ -153,18 +170,20 @@ export class SSEManager {
|
||||
this.eventSource.close()
|
||||
this.eventSource = null
|
||||
}
|
||||
|
||||
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
this.reconnectTimer = null
|
||||
}
|
||||
|
||||
|
||||
if (this.backgroundCloseTimer) {
|
||||
clearTimeout(this.backgroundCloseTimer)
|
||||
this.backgroundCloseTimer = null
|
||||
}
|
||||
|
||||
|
||||
this.listeners.clear()
|
||||
this.isConnecting = false
|
||||
this.reconnectAttempts = 0
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,6 +199,37 @@ export class SSEManager {
|
||||
get connectionUrl(): string {
|
||||
return this.url
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制重新连接
|
||||
*/
|
||||
forceReconnect() {
|
||||
this.close()
|
||||
if (!this.isBackground) {
|
||||
this.reconnectSSE()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有活跃的监听器
|
||||
*/
|
||||
get hasActiveListeners(): boolean {
|
||||
return this.listeners.size > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前重连次数
|
||||
*/
|
||||
get currentReconnectAttempts(): number {
|
||||
return this.reconnectAttempts
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否达到最大重连次数
|
||||
*/
|
||||
get hasReachedMaxAttempts(): boolean {
|
||||
return this.reconnectAttempts >= this.options.maxReconnectAttempts
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,4 +268,4 @@ class SSEManagerSingleton {
|
||||
}
|
||||
}
|
||||
|
||||
export const sseManagerSingleton = new SSEManagerSingleton()
|
||||
export const sseManagerSingleton = new SSEManagerSingleton()
|
||||
|
||||
212
src/utils/themeManager.ts
Normal file
212
src/utils/themeManager.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
// 主题管理器 - 动态加载主题CSS
|
||||
export interface ThemeConfig {
|
||||
name: string
|
||||
cssPath: string
|
||||
isLoaded: boolean
|
||||
}
|
||||
|
||||
class ThemeManager {
|
||||
private themes: Map<string, ThemeConfig> = new Map()
|
||||
private currentTheme: string = 'default'
|
||||
private loadedLinks: Map<string, HTMLLinkElement> = new Map()
|
||||
|
||||
constructor() {
|
||||
// 注册所有可用主题
|
||||
this.registerTheme('default', '')
|
||||
this.registerTheme('light', '')
|
||||
this.registerTheme('dark', '')
|
||||
this.registerTheme('purple', '')
|
||||
this.registerTheme('auto', '')
|
||||
// 只有透明主题有特定的CSS文件
|
||||
this.registerTheme('transparent', './src/styles/themes/transparent.css')
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册主题
|
||||
*/
|
||||
registerTheme(name: string, cssPath: string): void {
|
||||
this.themes.set(name, {
|
||||
name,
|
||||
cssPath,
|
||||
isLoaded: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前主题
|
||||
*/
|
||||
getCurrentTheme(): string {
|
||||
return this.currentTheme
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置主题
|
||||
*/
|
||||
async setTheme(themeName: string): Promise<void> {
|
||||
if (!this.themes.has(themeName)) {
|
||||
console.warn(`Theme "${themeName}" not found`)
|
||||
return
|
||||
}
|
||||
|
||||
const theme = this.themes.get(themeName)!
|
||||
|
||||
// 清理其他主题的CSS(除了当前要设置的主题)
|
||||
this.unloadOtherThemes()
|
||||
|
||||
// 如果主题有CSS文件,则加载CSS
|
||||
if (theme.cssPath) {
|
||||
try {
|
||||
await this.loadThemeCSS(themeName, theme.cssPath)
|
||||
} catch (error) {
|
||||
console.error(`Failed to load CSS for theme "${themeName}":`, error)
|
||||
// 即使CSS加载失败,也继续应用主题(使用默认样式)
|
||||
}
|
||||
}
|
||||
|
||||
// 应用主题(无论是否有CSS文件)
|
||||
this.applyTheme(themeName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载主题CSS文件
|
||||
*/
|
||||
private async loadThemeCSS(themeName: string, cssPath: string): Promise<void> {
|
||||
// 如果已经加载过,直接返回
|
||||
if (this.loadedLinks.has(themeName)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 动态导入CSS模块
|
||||
if (themeName === 'transparent') {
|
||||
await import('@/styles/themes/transparent.scss')
|
||||
this.themes.get(themeName)!.isLoaded = true
|
||||
return
|
||||
}
|
||||
|
||||
// 对于其他主题,使用传统的link方式
|
||||
const link = document.createElement('link')
|
||||
link.rel = 'stylesheet'
|
||||
link.type = 'text/css'
|
||||
link.href = cssPath
|
||||
link.id = `theme-${themeName}`
|
||||
|
||||
// 等待CSS加载完成
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
link.onload = () => {
|
||||
this.loadedLinks.set(themeName, link)
|
||||
this.themes.get(themeName)!.isLoaded = true
|
||||
resolve()
|
||||
}
|
||||
link.onerror = () => {
|
||||
reject(new Error(`Failed to load theme CSS: ${cssPath}`))
|
||||
}
|
||||
})
|
||||
|
||||
// 添加到head
|
||||
document.head.appendChild(link)
|
||||
} catch (error) {
|
||||
console.error(`Error loading theme "${themeName}":`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用主题到DOM
|
||||
*/
|
||||
private applyTheme(themeName: string): void {
|
||||
// 移除之前的主题属性
|
||||
document.documentElement.removeAttribute('data-theme')
|
||||
|
||||
// 设置新主题(除了default主题)
|
||||
if (themeName !== 'default') {
|
||||
document.documentElement.setAttribute('data-theme', themeName)
|
||||
}
|
||||
|
||||
this.currentTheme = themeName
|
||||
|
||||
// 触发主题变更事件
|
||||
this.dispatchThemeChangeEvent(themeName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载主题CSS
|
||||
*/
|
||||
unloadTheme(themeName: string): void {
|
||||
const theme = this.themes.get(themeName)
|
||||
if (!theme) return
|
||||
|
||||
// 对于动态导入的CSS,我们无法直接卸载,但可以标记为未加载
|
||||
if (themeName === 'transparent') {
|
||||
theme.isLoaded = false
|
||||
return
|
||||
}
|
||||
|
||||
// 对于传统link方式加载的CSS
|
||||
const link = this.loadedLinks.get(themeName)
|
||||
if (link) {
|
||||
link.remove()
|
||||
this.loadedLinks.delete(themeName)
|
||||
theme.isLoaded = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载所有主题CSS(除了当前主题)
|
||||
*/
|
||||
unloadOtherThemes(): void {
|
||||
for (const [themeName] of this.themes) {
|
||||
if (themeName !== this.currentTheme && this.themes.get(themeName)?.isLoaded) {
|
||||
this.unloadTheme(themeName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已注册的主题列表
|
||||
*/
|
||||
getAvailableThemes(): string[] {
|
||||
return Array.from(this.themes.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查主题是否已加载
|
||||
*/
|
||||
isThemeLoaded(themeName: string): boolean {
|
||||
return this.themes.get(themeName)?.isLoaded || false
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发主题变更事件
|
||||
*/
|
||||
private dispatchThemeChangeEvent(themeName: string): void {
|
||||
const event = new CustomEvent('themechange', {
|
||||
detail: { theme: themeName },
|
||||
})
|
||||
document.dispatchEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听主题变更事件
|
||||
*/
|
||||
onThemeChange(callback: (theme: string) => void): void {
|
||||
document.addEventListener('themechange', (event: any) => {
|
||||
callback(event.detail.theme)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除主题变更监听器
|
||||
*/
|
||||
offThemeChange(callback: (theme: string) => void): void {
|
||||
document.removeEventListener('themechange', (event: any) => {
|
||||
callback(event.detail.theme)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
export const themeManager = new ThemeManager()
|
||||
|
||||
// 导出类型
|
||||
export type { ThemeManager }
|
||||
@@ -619,7 +619,7 @@ onBeforeMount(() => {
|
||||
<VListItem @click="clickSearch('title')">
|
||||
<VListItemTitle>{{ t('media.search.byTitle') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem @click="clickSearch('imdb')">
|
||||
<VListItem @click="clickSearch('imdbid')">
|
||||
<VListItemTitle>{{ t('media.search.byImdb') }}</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
|
||||
@@ -37,6 +37,8 @@ const siteSetting = ref<any>({
|
||||
Site: {
|
||||
SITEDATA_REFRESH_INTERVAL: 0,
|
||||
SITE_MESSAGE: false,
|
||||
BROWSER_EMULATION: 'playwright',
|
||||
FLARESOLVERR_URL: '',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -61,6 +63,12 @@ const SiteDataRefreshIntervalItems = [
|
||||
{ title: t('setting.site.syncInterval.never'), value: 0 },
|
||||
]
|
||||
|
||||
// 站点访问仿真方式
|
||||
const BrowserEmulationItems = [
|
||||
{ title: 'Playwright', value: 'playwright' },
|
||||
{ title: 'FlareSolverr', value: 'flaresolverr' },
|
||||
]
|
||||
|
||||
// 重置站点
|
||||
async function resetSites() {
|
||||
try {
|
||||
@@ -206,7 +214,7 @@ onMounted(() => {
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard :title="t('setting.site.siteDataRefresh')">
|
||||
<VCard :title="t('setting.site.siteOptions')">
|
||||
<VCardText>
|
||||
<VForm>
|
||||
<VRow>
|
||||
@@ -220,6 +228,27 @@ onMounted(() => {
|
||||
prepend-inner-icon="mdi-refresh"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="siteSetting.Site.BROWSER_EMULATION"
|
||||
:items="BrowserEmulationItems"
|
||||
:label="t('setting.site.browserEmulation')"
|
||||
:hint="t('setting.site.browserEmulationHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-web"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6" v-if="siteSetting.Site.BROWSER_EMULATION == 'flaresolverr'">
|
||||
<VTextField
|
||||
v-model="siteSetting.Site.FLARESOLVERR_URL"
|
||||
:label="t('setting.site.flaresolverrUrl')"
|
||||
:placeholder="'http://127.0.0.1:8191'"
|
||||
:hint="t('setting.site.flaresolverrUrlHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-server"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
|
||||
@@ -22,6 +22,7 @@ const { t } = useI18n()
|
||||
const SystemSettings = ref<any>({
|
||||
// 基础设置
|
||||
Basic: {
|
||||
DB_TYPE: 'sqlite',
|
||||
APP_DOMAIN: null,
|
||||
API_TOKEN: null,
|
||||
WALLPAPER: 'tmdb',
|
||||
@@ -818,7 +819,7 @@ onDeactivated(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol v-if="SystemSettings.Basic.DB_TYPE === 'sqlite'" cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.DB_WAL_ENABLE"
|
||||
:label="t('setting.system.dbWalEnable')"
|
||||
@@ -867,7 +868,7 @@ onDeactivated(() => {
|
||||
:hint="t('setting.system.tmdbImageDomainHint')"
|
||||
persistent-hint
|
||||
:placeholder="t('setting.system.tmdbImageDomainPlaceholder')"
|
||||
:items="['image.tmdb.org', 'static-mdb.v.geilijiasu.com']"
|
||||
:items="['image.tmdb.org']"
|
||||
:rules="[(v: string) => !!v || t('setting.system.tmdbImageDomainRequired')]"
|
||||
prepend-inner-icon="mdi-image"
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Site, SiteUserData } from '@/api/types'
|
||||
import SiteCard from '@/components/cards/SiteCard.vue'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue'
|
||||
import SiteStatisticsDialog from '@/components/dialog/SiteStatisticsDialog.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -39,6 +40,9 @@ const loading = ref(false)
|
||||
// 新增站点对话框
|
||||
const siteAddDialog = ref(false)
|
||||
|
||||
// 统计信息对话框
|
||||
const siteStatsDialog = ref(false)
|
||||
|
||||
// 筛选相关
|
||||
const filterMenu = ref(false)
|
||||
const filterOption = ref('all') // all, active, inactive, connected, slow, failed, unknown
|
||||
@@ -235,44 +239,54 @@ useDynamicButton({
|
||||
<!-- 页面标题和筛选按钮 -->
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
<VPageContentTitle :title="t('navItems.siteManager')" class="mb-0" />
|
||||
<!-- 筛选按钮 -->
|
||||
<VMenu v-model="filterMenu" offset-y :close-on-content-click="false" location="bottom end">
|
||||
<template #activator="{ props }">
|
||||
<VBtn
|
||||
v-bind="props"
|
||||
:icon="display.smAndDown.value"
|
||||
:variant="filterOption === 'all' ? 'text' : 'tonal'"
|
||||
:color="currentFilter?.color"
|
||||
>
|
||||
<VIcon :icon="currentFilter?.icon || 'mdi-filter'" />
|
||||
<span v-if="!display.smAndDown.value" class="ml-2">
|
||||
{{ currentFilter?.label }}
|
||||
</span>
|
||||
<VIcon v-if="!display.smAndDown.value" icon="mdi-chevron-down" class="ml-1" />
|
||||
</VBtn>
|
||||
</template>
|
||||
|
||||
<!-- 筛选菜单 -->
|
||||
<VCard min-width="200">
|
||||
<VList class="px-2">
|
||||
<VListSubheader>{{ t('common.filter') }}</VListSubheader>
|
||||
<VListItem
|
||||
v-for="option in filterOptions"
|
||||
:key="option.value"
|
||||
:active="filterOption === option.value"
|
||||
@click="selectFilter(option.value)"
|
||||
<!-- 右侧按钮组 -->
|
||||
<div class="d-flex align-center gap-2">
|
||||
<!-- 统计信息按钮 -->
|
||||
<VBtn :icon="display.smAndDown.value" variant="text" color="info" @click="siteStatsDialog = true">
|
||||
<VIcon icon="mdi-chart-line" />
|
||||
<span v-if="!display.smAndDown.value" class="ml-2">
|
||||
{{ t('site.statistics') }}
|
||||
</span>
|
||||
</VBtn>
|
||||
<!-- 筛选按钮 -->
|
||||
<VMenu v-model="filterMenu" offset-y :close-on-content-click="false" location="bottom end">
|
||||
<template #activator="{ props }">
|
||||
<VBtn
|
||||
v-bind="props"
|
||||
:icon="display.smAndDown.value"
|
||||
:variant="filterOption === 'all' ? 'text' : 'tonal'"
|
||||
:color="currentFilter?.color"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="option.icon" :color="option.color" />
|
||||
</template>
|
||||
<VListItemTitle>{{ option.label }}</VListItemTitle>
|
||||
<template #append>
|
||||
<VIcon v-if="filterOption === option.value" icon="mdi-check" color="primary" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCard>
|
||||
</VMenu>
|
||||
<VIcon :icon="currentFilter?.icon || 'mdi-filter'" />
|
||||
<span v-if="!display.smAndDown.value" class="ml-2">
|
||||
{{ currentFilter?.label }}
|
||||
</span>
|
||||
<VIcon v-if="!display.smAndDown.value" icon="mdi-chevron-down" class="ml-1" />
|
||||
</VBtn>
|
||||
</template>
|
||||
|
||||
<!-- 筛选菜单 -->
|
||||
<VCard min-width="200">
|
||||
<VList class="px-2">
|
||||
<VListSubheader>{{ t('common.filter') }}</VListSubheader>
|
||||
<VListItem
|
||||
v-for="option in filterOptions"
|
||||
:key="option.value"
|
||||
:active="filterOption === option.value"
|
||||
@click="selectFilter(option.value)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="option.icon" :color="option.color" />
|
||||
</template>
|
||||
<VListItemTitle>{{ option.label }}</VListItemTitle>
|
||||
<template #append>
|
||||
<VIcon v-if="filterOption === option.value" icon="mdi-check" color="primary" />
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCard>
|
||||
</VMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
@@ -326,4 +340,7 @@ useDynamicButton({
|
||||
@save="onSiteSave"
|
||||
@close="siteAddDialog = false"
|
||||
/>
|
||||
|
||||
<!-- 统计信息弹窗 -->
|
||||
<SiteStatisticsDialog v-if="siteStatsDialog" v-model="siteStatsDialog" :sites="siteList" />
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { isToday } from '@/@core/utils/index'
|
||||
import dayjs from 'dayjs';
|
||||
import dayjs from 'dayjs'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
|
||||
// 定义输入变量
|
||||
@@ -16,6 +16,9 @@ const { useSSE } = useBackgroundOptimization()
|
||||
// 已解析的日志列表
|
||||
const parsedLogs = ref<{ level: string; date: string; time: string; program: string; content: string }[]>([])
|
||||
|
||||
// 组件是否已挂载
|
||||
const isMounted = ref(false)
|
||||
|
||||
// 表头
|
||||
const headers = [
|
||||
{ title: t('logging.level'), value: 'level' },
|
||||
@@ -72,23 +75,40 @@ function handleSSEMessage(event: MessageEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化的SSE连接
|
||||
useSSE(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/logging?logfile=${
|
||||
encodeURIComponent(props.logfile) ?? 'moviepilot.log'
|
||||
}`,
|
||||
// 使用优化的SSE连接,添加延迟确保弹窗完全打开
|
||||
const { manager, isConnected } = useSSE(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/logging?logfile=${encodeURIComponent(props.logfile) ?? 'moviepilot.log'}`,
|
||||
handleSSEMessage,
|
||||
`logging-${props.logfile}`,
|
||||
{
|
||||
backgroundCloseDelay: 5000,
|
||||
reconnectDelay: 3000,
|
||||
maxReconnectAttempts: 3
|
||||
}
|
||||
maxReconnectAttempts: 3,
|
||||
connectDelay: 300, // 延迟300ms建立连接,确保弹窗完全打开
|
||||
},
|
||||
)
|
||||
|
||||
// 监听弹窗状态变化,确保弹窗完全打开后再建立连接
|
||||
onMounted(() => {
|
||||
// 延迟标记组件已挂载,确保弹窗完全渲染
|
||||
setTimeout(() => {
|
||||
isMounted.value = true
|
||||
}, 200)
|
||||
})
|
||||
|
||||
// 监听连接状态变化
|
||||
watch(isConnected, connected => {})
|
||||
|
||||
// 监听日志数据变化
|
||||
watch(parsedLogs, logs => {}, { deep: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadingBanner v-if="parsedLogs.length === 0" class="mt-12" :text="t('logging.refreshing') + ' ...'" />
|
||||
<LoadingBanner
|
||||
v-if="!isMounted || !isConnected || parsedLogs.length === 0"
|
||||
class="mt-12"
|
||||
:text="!isMounted ? t('logging.initializing') + ' ...' : t('logging.refreshing') + ' ...'"
|
||||
/>
|
||||
<div v-else>
|
||||
<VTable class="table-rounded" hide-default-footer disable-sort>
|
||||
<tbody>
|
||||
@@ -104,8 +124,14 @@ useSSE(
|
||||
<VChip size="small" :color="getLogColor(item.level)" variant="elevated" v-text="item.level" />
|
||||
</template>
|
||||
<template #item.time="{ item }">
|
||||
<span class="text-sm">{{ isToday(dayjs(item.date).toDate()) ? item.time : `${item.date}
|
||||
${item.time}` }}</span>
|
||||
<span class="text-sm">
|
||||
{{
|
||||
isToday(dayjs(item.date).toDate())
|
||||
? item.time
|
||||
: `${item.date}
|
||||
${item.time}`
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
<template #item.program="{ item }">
|
||||
<h6 class="text-sm font-weight-medium">{{ item.program }}</h6>
|
||||
|
||||
@@ -34,7 +34,9 @@ function handleSSEMessage(event: MessageEvent) {
|
||||
const message = event.data
|
||||
if (message) {
|
||||
const object = JSON.parse(message)
|
||||
if (compareTime(object.date, lastTime.value) <= 0) return
|
||||
// 使用reg_time或date字段进行比较
|
||||
const messageTime = object.reg_time || object.date
|
||||
if (compareTime(messageTime, lastTime.value) <= 0) return
|
||||
messages.value.push(object)
|
||||
nextTick(() => {
|
||||
emit('scroll') // 新消息到达时触发智能滚动
|
||||
@@ -76,14 +78,23 @@ async function loadMessages({ done }: { done: any }) {
|
||||
})
|
||||
|
||||
// 取最后一条时间为存量消息最新时间
|
||||
lastTime.value =
|
||||
currData.value[currData.value.length - 1].reg_time ?? currData.value[currData.value.length - 1].date ?? ''
|
||||
const lastMessage = currData.value[currData.value.length - 1]
|
||||
lastTime.value = lastMessage.reg_time || lastMessage.date || ''
|
||||
|
||||
// 合并数据并重新排序
|
||||
const allMessages = [...currData.value, ...messages.value]
|
||||
allMessages.sort((a, b) => {
|
||||
const timeA = a.reg_time || a.date || ''
|
||||
const timeB = b.reg_time || b.date || ''
|
||||
return compareTime(timeA, timeB)
|
||||
})
|
||||
messages.value = allMessages
|
||||
|
||||
// 合并数据
|
||||
messages.value = [...currData.value, ...messages.value]
|
||||
// 首次加载时滚动到底部
|
||||
if (page.value === 1) {
|
||||
emit('scroll')
|
||||
nextTick(() => {
|
||||
emit('scroll')
|
||||
})
|
||||
}
|
||||
// 页码+1
|
||||
page.value++
|
||||
@@ -96,15 +107,37 @@ async function loadMessages({ done }: { done: any }) {
|
||||
// 取消加载中
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.error('加载消息失败:', error)
|
||||
loading.value = false
|
||||
done('error')
|
||||
}
|
||||
}
|
||||
|
||||
// 比较yyyy-MM-dd HH:mm:ss时间大小
|
||||
function compareTime(time1: string, time2: string) {
|
||||
if (!time1 && !time2) return 0
|
||||
if (!time1) return -1
|
||||
if (!time2) return 1
|
||||
return new Date(time1.replaceAll(/-/g, '/')).getTime() - new Date(time2.replaceAll(/-/g, '/')).getTime()
|
||||
|
||||
try {
|
||||
// 统一时间格式处理,支持多种格式
|
||||
const normalizeTime = (time: string) => {
|
||||
// 如果是ISO格式,直接使用
|
||||
if (time.includes('T')) {
|
||||
return new Date(time).getTime()
|
||||
}
|
||||
// 如果是yyyy-MM-dd HH:mm:ss格式,替换-为/
|
||||
return new Date(time.replaceAll(/-/g, '/')).getTime()
|
||||
}
|
||||
|
||||
const timestamp1 = normalizeTime(time1)
|
||||
const timestamp2 = normalizeTime(time2)
|
||||
|
||||
return timestamp1 - timestamp2
|
||||
} catch (error) {
|
||||
console.error('时间比较错误:', error, 'time1:', time1, 'time2:', time2)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 图片加载完成时触发智能滚动
|
||||
|
||||
@@ -693,7 +693,7 @@ const handleSortIconClick = () => {
|
||||
</DialogWrapper>
|
||||
|
||||
<!-- 筛选弹窗 -->
|
||||
<DialogWrapper v-model="filterMenuOpen" max-width="25rem" location="center" max-height="85vh">
|
||||
<DialogWrapper v-model="filterMenuOpen" max-width="25rem" location="center" max-height="85vh" scrollable>
|
||||
<VCard>
|
||||
<VCardTitle class="py-3 d-flex align-center">
|
||||
<VIcon :icon="getFilterIcon(currentFilter)" class="me-2"></VIcon>
|
||||
|
||||
@@ -673,7 +673,7 @@ onMounted(() => {
|
||||
</DialogWrapper>
|
||||
|
||||
<!-- 筛选弹窗 -->
|
||||
<DialogWrapper v-model="filterMenuOpen" max-width="25rem" max-height="85vh" location="center">
|
||||
<DialogWrapper v-model="filterMenuOpen" max-width="25rem" max-height="85vh" location="center" scrollable>
|
||||
<VCard>
|
||||
<VCardTitle class="py-3 d-flex align-center">
|
||||
<VIcon :icon="getFilterIcon(currentFilter)" class="me-2"></VIcon>
|
||||
|
||||
@@ -26,6 +26,18 @@ const addDialog = ref(false)
|
||||
// 所有任务
|
||||
const workflowList = ref<Workflow[]>([])
|
||||
|
||||
// 事件类型列表
|
||||
const eventTypes = ref<Array<{ title: string; value: string }>>([])
|
||||
|
||||
// 加载事件类型列表
|
||||
async function loadEventTypes() {
|
||||
try {
|
||||
eventTypes.value = await api.get('workflow/event_types')
|
||||
} catch (error) {
|
||||
console.error('Failed to load event types:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
async function fetchData() {
|
||||
try {
|
||||
@@ -51,6 +63,7 @@ useDynamicButton({
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadEventTypes()
|
||||
fetchData()
|
||||
})
|
||||
|
||||
@@ -62,7 +75,7 @@ onActivated(() => {
|
||||
<div>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
<div v-if="workflowList.length > 0 && isRefreshed" class="grid gap-4 grid-workflow-card px-2">
|
||||
<WorkflowTaskCard v-for="item in workflowList" :key="item.id" :workflow="item" @refresh="fetchData" />
|
||||
<WorkflowTaskCard v-for="item in workflowList" :key="item.id" :workflow="item" :event-types="eventTypes" @refresh="fetchData" />
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="workflowList.length === 0 && isRefreshed"
|
||||
|
||||
@@ -41,6 +41,18 @@ const isRefreshed = ref(false)
|
||||
const dataList = ref<WorkflowShare[]>([])
|
||||
const currData = ref<WorkflowShare[]>([])
|
||||
|
||||
// 事件类型列表
|
||||
const eventTypes = ref<Array<{ title: string; value: string }>>([])
|
||||
|
||||
// 加载事件类型列表
|
||||
async function loadEventTypes() {
|
||||
try {
|
||||
eventTypes.value = await api.get('workflow/event_types')
|
||||
} catch (error) {
|
||||
console.error('Failed to load event types:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 拼装参数
|
||||
function getParams() {
|
||||
let params = {
|
||||
@@ -121,6 +133,7 @@ function removeData(id: string) {
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
loadEventTypes()
|
||||
fetchData({ done: () => {} })
|
||||
})
|
||||
</script>
|
||||
@@ -133,7 +146,12 @@ onActivated(() => {
|
||||
<template #empty />
|
||||
<div v-if="dataList.length > 0" class="grid gap-4 grid-workflow-share-card" tabindex="0">
|
||||
<div v-for="data in dataList" :key="data.id">
|
||||
<WorkflowShareCard :workflow="data" @delete="removeData(data.id || '')" @update="emit('update')" />
|
||||
<WorkflowShareCard
|
||||
:workflow="data"
|
||||
:event-types="eventTypes"
|
||||
@delete="removeData(data.id || '')"
|
||||
@update="emit('update')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NoDataFound
|
||||
|
||||
Reference in New Issue
Block a user