mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-09 22:22:58 +08:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cbdf24315 | ||
|
|
164ea79bd1 | ||
|
|
97f3435bb3 | ||
|
|
63b108ff6b | ||
|
|
b0880cb369 | ||
|
|
5f70ee8e18 | ||
|
|
4c64f7a2c3 | ||
|
|
262927e459 | ||
|
|
b16c566004 | ||
|
|
1af82dbee6 | ||
|
|
2e9a5a4e13 | ||
|
|
b455f603dc | ||
|
|
37c0c3e339 | ||
|
|
b6cb341082 | ||
|
|
1af1a06700 | ||
|
|
79e4ecfdbe | ||
|
|
1585271e37 | ||
|
|
c240b171e4 | ||
|
|
9c405e90ac | ||
|
|
3ec3212ca5 | ||
|
|
b1289f6177 | ||
|
|
64b7ba48c8 | ||
|
|
f093053ea4 | ||
|
|
9faa0ded59 | ||
|
|
0f7dafeb23 | ||
|
|
472d1960d9 | ||
|
|
6e50acf106 | ||
|
|
a3fb4b1534 | ||
|
|
382cae32a2 | ||
|
|
0aa4851f8e | ||
|
|
65271e6d13 | ||
|
|
671cf8d588 | ||
|
|
afc7c81028 | ||
|
|
c330aee560 | ||
|
|
eafe63c886 | ||
|
|
53206d05b8 | ||
|
|
af085d457e | ||
|
|
fb36033939 | ||
|
|
584e7672df | ||
|
|
d4f7a5a1c0 | ||
|
|
2a9ea81ad4 | ||
|
|
276948dd68 | ||
|
|
990c5583f2 | ||
|
|
644f1b5640 | ||
|
|
5261fbe870 | ||
|
|
e4f2d85e2b | ||
|
|
8e3ccdc24a | ||
|
|
cd6d93affd | ||
|
|
6096ab0c9b | ||
|
|
0a87bb1db1 | ||
|
|
a19042c655 | ||
|
|
a889687a6a | ||
|
|
e1cdc715aa | ||
|
|
a82b3a0a29 | ||
|
|
d93a71f0be | ||
|
|
899dc765bc | ||
|
|
449490e52d | ||
|
|
5541d7974e | ||
|
|
ae3eb36183 | ||
|
|
d57e9a397c |
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:
|
||||
|
||||
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -10,7 +10,6 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ConfirmDialog: typeof import('./src/@core/components/ConfirmDialog.vue')['default']
|
||||
DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default']
|
||||
DialogWrapper: typeof import('./src/@core/components/DialogWrapper.vue')['default']
|
||||
ErrorHeader: typeof import('./src/@core/components/ErrorHeader.vue')['default']
|
||||
ExistIcon: typeof import('./src/@core/components/ExistIcon.vue')['default']
|
||||
LoadingBanner: typeof import('./src/@core/components/LoadingBanner.vue')['default']
|
||||
|
||||
705
index.html
705
index.html
@@ -1,430 +1,369 @@
|
||||
<!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));
|
||||
min-block-size: 100vh;
|
||||
min-block-size: 100dvh;
|
||||
--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"
|
||||
/>
|
||||
">
|
||||
|
||||
<!-- 防止缩放和选择,提供原生应用体验 -->
|
||||
<meta name="format-detection" content="telephone=no, date=no, email=no, address=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="description" content="MoviePilot - 智能影视媒体库管理工具" />
|
||||
<meta name="author" content="MoviePilot" />
|
||||
<meta name="keywords" content="MoviePilot,影视,媒体库,管理" />
|
||||
<!-- 防止缩放和选择,提供原生应用体验 -->
|
||||
<meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
|
||||
|
||||
<!-- 安全和隐私 -->
|
||||
<meta name="Robots" content="noindex,nofollow,noarchive" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<!-- 基础信息 -->
|
||||
<meta name="description" content="MoviePilot - 智能影视媒体库管理工具" />
|
||||
<meta name="author" content="MoviePilot" />
|
||||
<meta name="keywords" content="MoviePilot,影视,媒体库,管理" />
|
||||
|
||||
<!-- 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" />
|
||||
<!-- 安全和隐私 -->
|
||||
<meta name="Robots" content="noindex,nofollow,noarchive" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
|
||||
<!-- 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" />
|
||||
<!-- 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 全屏模式 -->
|
||||
<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 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-orientations" content="portrait" />
|
||||
<!-- 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" />
|
||||
|
||||
<!-- 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" />
|
||||
<!-- iOS Safari 防止自动识别 -->
|
||||
<meta name="apple-mobile-web-app-orientations" content="portrait" />
|
||||
|
||||
<!-- 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" />
|
||||
<!-- 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" />
|
||||
|
||||
<!-- 主题色彩 - 适配深色和浅色模式 -->
|
||||
<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" />
|
||||
<!-- 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="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="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" />
|
||||
|
||||
<!-- UC浏览器优化 -->
|
||||
<meta name="browsermode" content="application" />
|
||||
<meta name="wap-font-scale" content="no" />
|
||||
<!-- 屏幕方向锁定 -->
|
||||
<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" />
|
||||
|
||||
<!-- 360浏览器优化 -->
|
||||
<meta name="renderer" content="webkit" />
|
||||
<!-- UC浏览器优化 -->
|
||||
<meta name="browsermode" content="application" />
|
||||
<meta name="wap-font-scale" content="no" />
|
||||
|
||||
<!-- 触摸优化 -->
|
||||
<meta name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<!-- 360浏览器优化 -->
|
||||
<meta name="renderer" content="webkit" />
|
||||
|
||||
<!-- 缓存控制 -->
|
||||
<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 name="HandheldFriendly" content="True" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
|
||||
<!-- 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 />
|
||||
<!-- 缓存控制 -->
|
||||
<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" />
|
||||
|
||||
<!-- 预加载关键资源 -->
|
||||
<link rel="preload" href="/logo.png" as="image" />
|
||||
<link rel="modulepreload" href="/src/main.ts" />
|
||||
<!-- 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 />
|
||||
|
||||
<!-- 内联关键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;
|
||||
<!-- 预加载关键资源 -->
|
||||
<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);
|
||||
}
|
||||
|
||||
.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% {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate-opacity {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
/* 添加logo完成动画 - 放大虚化效果 */
|
||||
.loading-complete .loading-logo {
|
||||
filter: blur(10px);
|
||||
opacity: 0;
|
||||
transform: scale(1.5);
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
/* 添加加载背景消失动画 - 放大虚化效果 */
|
||||
.loading-complete {
|
||||
filter: blur(15px);
|
||||
opacity: 0;
|
||||
transform: scale(1.2);
|
||||
<!-- 初始化脚本 -->
|
||||
<script>
|
||||
// 检测系统主题是否为深色模式
|
||||
function checkPrefersColorSchemeIsDark() {
|
||||
try {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
// 主题色彩初始化
|
||||
let loaderColor = localStorage.getItem('materio-initial-loader-bg')
|
||||
let primaryColor = localStorage.getItem('materio-initial-loader-color')
|
||||
|
||||
/* 完成时隐藏加载动画 */
|
||||
.loading-complete .loading {
|
||||
opacity: 0;
|
||||
}
|
||||
// 检查主题设置
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
const isAutoTheme = savedTheme === 'auto'
|
||||
|
||||
.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%;
|
||||
}
|
||||
// 如果是自动主题或者没有保存的背景色,根据系统主题设置背景色
|
||||
if (isAutoTheme || !loaderColor) {
|
||||
loaderColor = checkPrefersColorSchemeIsDark() ? '#0E1116' : '#FFFFFF'
|
||||
}
|
||||
if (!primaryColor) {
|
||||
primaryColor = '#9155FD'
|
||||
}
|
||||
|
||||
.loading .effect-1 {
|
||||
animation: rotate 1s ease infinite;
|
||||
}
|
||||
// 应用主题色彩
|
||||
document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
|
||||
document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
|
||||
|
||||
.loading .effect-2 {
|
||||
animation: rotate-opacity 1s ease infinite 0.1s;
|
||||
}
|
||||
// 状态栏适配
|
||||
if (window.navigator.standalone) {
|
||||
document.documentElement.style.setProperty('--status-bar-height', '20px')
|
||||
}
|
||||
|
||||
.loading .effect-3 {
|
||||
animation: rotate-opacity 1s ease infinite 0.2s;
|
||||
}
|
||||
// 安全区域适配
|
||||
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 .effects {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
if (safeAreaTop) document.documentElement.style.setProperty('--safe-area-top', safeAreaTop)
|
||||
if (safeAreaBottom) document.documentElement.style.setProperty('--safe-area-bottom', safeAreaBottom)
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
updateSafeArea()
|
||||
window.addEventListener('resize', updateSafeArea)
|
||||
window.addEventListener('orientationchange', updateSafeArea)
|
||||
</script>
|
||||
</head>
|
||||
|
||||
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)">
|
||||
<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">
|
||||
<path
|
||||
d="M2241.27,-28.175C2238.86,-28.931 2236.64,-29.181 2234.48,-29.254L2159.78,-29.286L2165.01,-11.207C2167.16,-13.121 2169.64,-13.722 2172.26,-13.808L2222.12,-13.822C2223.52,-13.824 2225,-13.701 2226.78,-13.108L2241.27,-28.175Z"
|
||||
style="fill: url(#_Linear1)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2205.67,331.428L2205.67,332.25L2205.67,352.835C2205.67,354.263 2204.91,355.583 2203.67,356.298C2202.43,357.012 2200.91,357.013 2199.67,356.3L2190.78,351.174C2189.73,350.595 2188.83,350.083 2188.03,349.59L2187.45,349.257C2186.66,348.725 2185.91,348.142 2185.21,347.461C2185.08,347.331 2184.95,347.198 2184.82,347.061C2184.26,346.457 2183.75,345.778 2183.3,344.995C2182.16,343.05 2181.69,341.024 2181.68,338.948L2181.67,268.923L2209.77,274.425C2207.5,275.639 2205.68,278.3 2205.67,281.429L2205.67,331.428Z"
|
||||
style="fill: url(#_Linear2)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2295.93,363.064C2295.73,363.184 2295.53,363.301 2295.32,363.414L2295.93,363.064Z"
|
||||
style="fill: rgb(141, 81, 249)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2299.79,360.238C2299.79,360.238 2320.03,348.464 2320.04,348.461C2323.1,346.372 2324.69,343.444 2325.17,339.877C2325.17,339.877 2325.17,269.846 2325.17,269.839C2325.06,267.482 2324.56,265.739 2323.61,264.133C2322.56,262.445 2321.26,261.005 2319.55,259.97L2304.42,251.217C2303.96,250.949 2303.39,250.948 2302.92,251.216C2302.46,251.484 2302.17,251.979 2302.17,252.515L2302.17,276.775L2302.17,277.879L2302.17,352.926C2302.17,352.933 2302.17,352.941 2302.17,352.948C2302.04,355.861 2301.23,358.279 2299.79,360.238Z"
|
||||
style="fill: url(#_Linear3)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256Z"
|
||||
style="fill: rgb(165, 118, 255)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path
|
||||
d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256ZM2253.68,223.756C2251.6,223.789 2249.87,224.269 2248.47,224.996L2188.17,259.754C2184.35,261.992 2182.35,265.367 2182.18,269.874C2182.18,269.874 2182.17,292.759 2182.17,292.757C2183.25,290.047 2185.13,288.051 2187.62,286.607L2249.57,250.919C2249.58,250.917 2249.58,250.915 2249.59,250.913C2250.83,250.243 2252.17,249.839 2253.67,249.847C2255.21,249.841 2256.54,250.253 2257.76,250.914C2257.76,250.916 2257.76,250.917 2257.76,250.919L2274.92,260.807C2275.38,261.075 2275.95,261.074 2276.42,260.806C2276.88,260.538 2277.17,260.043 2277.17,259.508L2277.17,237.568C2277.17,236.317 2276.5,235.16 2275.42,234.535C2275.42,234.535 2258.88,225 2258.87,224.996C2256.87,224.049 2255.2,223.746 2253.68,223.756Z"
|
||||
style="fill: url(#_Linear4)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(0.800798,0.462341,0.769972,-1.33363,-1677.22,-896.858)">
|
||||
<path
|
||||
d="M2241.55,-28.184C2239.1,-28.989 2236.83,-29.204 2234.68,-29.295C2234.68,-29.295 2220.82,-29.3 2215.03,-29.303C2213.48,-29.303 2212.05,-28.808 2211.28,-28.004C2208.65,-25.275 2202.56,-18.936 2199.45,-15.709C2199.07,-15.306 2199.07,-14.809 2199.46,-14.406C2199.85,-14.004 2200.57,-13.758 2201.34,-13.761C2208.36,-13.788 2222.72,-13.845 2222.72,-13.845C2223.98,-13.851 2225.44,-13.657 2227.06,-13.117L2241.55,-28.184Z"
|
||||
style="fill: rgb(141, 81, 249)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-4.32309,0,0,12.4454,9610.35,-1450.35)">
|
||||
<path
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
|
||||
style="fill: rgb(104, 0, 197)"
|
||||
/>
|
||||
<clipPath id="_clip5">
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip5)">
|
||||
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
|
||||
<path
|
||||
d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"
|
||||
/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip5)">
|
||||
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
|
||||
<path
|
||||
d="M1726.17,-64.249L1708.16,-72.303L1708.05,-23.514L1721.88,-32.386C1722.96,-33.241 1723.09,-33.944 1723.15,-34.636L1723.15,-54.373C1723.19,-56.238 1724.96,-57.594 1726.87,-56.686L1726.17,-64.249Z"
|
||||
style="fill: url(#_Linear6)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1704.47,-40.254C1706.28,-40.527 1708.14,-40.212 1708.16,-39.416L1708.16,-18.976L1726.17,-18.976L1726.17,-45.661Z"
|
||||
style="fill: rgb(141, 81, 249)"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1726.17,-18.976L1708.16,-18.976L1708.16,-39.416C1707.79,-40.732 1704.5,-40.298 1702.68,-40.025L1726.17,-45.661ZM1705.49,-40.491C1706.2,-40.507 1706.87,-40.464 1707.4,-40.327C1708.01,-40.173 1708.48,-39.899 1708.62,-39.436C1708.62,-39.429 1708.62,-39.423 1708.62,-39.416L1708.62,-19.152C1708.62,-19.152 1725.72,-19.152 1725.72,-19.152L1725.72,-45.345L1705.49,-40.491Z"
|
||||
style="fill: url(#_Radial7)"
|
||||
/>
|
||||
</g>
|
||||
d="M1726.17,-64.249L1708.16,-72.303L1708.05,-23.514L1721.88,-32.386C1722.96,-33.241 1723.09,-33.944 1723.15,-34.636L1723.15,-54.373C1723.19,-56.238 1724.96,-57.594 1726.87,-56.686L1726.17,-64.249Z"
|
||||
style="fill: url(#_Linear6)" />
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1704.47,-40.254C1706.28,-40.527 1708.14,-40.212 1708.16,-39.416L1708.16,-18.976L1726.17,-18.976L1726.17,-45.661Z"
|
||||
style="fill: rgb(141, 81, 249)" />
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path
|
||||
d="M1726.17,-45.661L1726.17,-18.976L1708.16,-18.976L1708.16,-39.416C1707.79,-40.732 1704.5,-40.298 1702.68,-40.025L1726.17,-45.661ZM1705.49,-40.491C1706.2,-40.507 1706.87,-40.464 1707.4,-40.327C1708.01,-40.173 1708.48,-39.899 1708.62,-39.436C1708.62,-39.429 1708.62,-39.423 1708.62,-39.416L1708.62,-19.152C1708.62,-19.152 1725.72,-19.152 1725.72,-19.152L1725.72,-45.345L1705.49,-40.491Z"
|
||||
style="fill: url(#_Radial7)" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="_Linear1"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear2"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear3"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear4"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="_Linear6"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
<stop offset="0.51" style="stop-color: rgb(110, 38, 217); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(91, 0, 197); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="_Radial7"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)"
|
||||
>
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="loading">
|
||||
<div class="effect-1 effects"></div>
|
||||
<div class="effect-2 effects"></div>
|
||||
<div class="effect-3 effects"></div>
|
||||
</div>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)">
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)">
|
||||
<stop offset="0" style="stop-color: rgb(141, 81, 249); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)">
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)">
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)">
|
||||
<stop offset="0" style="stop-color: rgb(116, 50, 223); stop-opacity: 1" />
|
||||
<stop offset="0.51" style="stop-color: rgb(110, 38, 217); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(91, 0, 197); stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
<radialGradient id="_Radial7" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)">
|
||||
<stop offset="0" style="stop-color: rgb(211, 187, 255); stop-opacity: 1" />
|
||||
<stop offset="1" style="stop-color: rgb(211, 187, 255); stop-opacity: 0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
<div class="loading">
|
||||
<div class="effect-1 effects"></div>
|
||||
<div class="effect-2 effects"></div>
|
||||
<div class="effect-3 effects"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.6.8",
|
||||
"version": "2.7.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": "dist/service.js",
|
||||
@@ -27,6 +27,7 @@
|
||||
"@fullcalendar/timegrid": "^6.1.15",
|
||||
"@fullcalendar/vue3": "^6.1.15",
|
||||
"@iconify/utils": "^2.2.1",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@vue-flow/background": "^1.3.2",
|
||||
"@vue-flow/controls": "^1.1.2",
|
||||
@@ -40,8 +41,10 @@
|
||||
"ace-builds": "^1.37.4",
|
||||
"apexcharts": "^4.0.0",
|
||||
"axios": "^1.7.9",
|
||||
"body-scroll-lock": "^3.1.5",
|
||||
"colorthief": "^2.6.0",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"express": "^4.21.2",
|
||||
"express-http-proxy": "^2.1.1",
|
||||
@@ -72,6 +75,7 @@
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
||||
"@originjs/vite-plugin-federation": "^1.4.1",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@types/body-scroll-lock": "^3.1.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^20.1.4",
|
||||
|
||||
53
public/logo.svg
Normal file
53
public/logo.svg
Normal file
@@ -0,0 +1,53 @@
|
||||
<svg width="3em" height="3em" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-2606,-236)">
|
||||
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">
|
||||
<rect x="0" y="0" width="192" height="192" style="fill:none;"/>
|
||||
<g transform="matrix(-0.800798,0.462341,-0.769972,-1.33363,1869.11,-896.718)">
|
||||
<path d="M2241.27,-28.175C2238.86,-28.931 2236.64,-29.181 2234.48,-29.254L2159.78,-29.286L2165.01,-11.207C2167.16,-13.121 2169.64,-13.722 2172.26,-13.808L2222.12,-13.822C2223.52,-13.824 2225,-13.701 2226.78,-13.108L2241.27,-28.175Z" style="fill:url(#_Linear1);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path d="M2205.67,331.428L2205.67,332.25L2205.67,352.835C2205.67,354.263 2204.91,355.583 2203.67,356.298C2202.43,357.012 2200.91,357.013 2199.67,356.3L2190.78,351.174C2189.73,350.595 2188.83,350.083 2188.03,349.59L2187.45,349.257C2186.66,348.725 2185.91,348.142 2185.21,347.461C2185.08,347.331 2184.95,347.198 2184.82,347.061C2184.26,346.457 2183.75,345.778 2183.3,344.995C2182.16,343.05 2181.69,341.024 2181.68,338.948L2181.67,268.923L2209.77,274.425C2207.5,275.639 2205.68,278.3 2205.67,281.429L2205.67,331.428Z" style="fill:url(#_Linear2);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2295.93,363.064C2295.73,363.184 2295.53,363.301 2295.32,363.414L2295.93,363.064Z" style="fill:rgb(141,81,249);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path d="M2295.93,363.064C2299.48,360.882 2301.46,357.55 2301.67,352.926L2301.67,277.879L2301.67,276.775L2301.67,252.515C2301.67,251.801 2302.05,251.14 2302.67,250.783C2303.29,250.426 2304.05,250.426 2304.67,250.784L2319.81,259.54C2321.59,260.617 2322.95,262.115 2324.04,263.875C2325.03,265.551 2325.56,267.37 2325.67,269.835L2325.67,339.91C2325.18,343.645 2323.51,346.705 2320.3,348.887L2295.93,363.064ZM2299.79,360.238C2299.79,360.238 2320.03,348.464 2320.04,348.461C2323.1,346.372 2324.69,343.444 2325.17,339.877C2325.17,339.877 2325.17,269.846 2325.17,269.839C2325.06,267.482 2324.56,265.739 2323.61,264.133C2322.56,262.445 2321.26,261.005 2319.55,259.97L2304.42,251.217C2303.96,250.949 2303.39,250.948 2302.92,251.216C2302.46,251.484 2302.17,251.979 2302.17,252.515L2302.17,276.775L2302.17,277.879L2302.17,352.926C2302.17,352.933 2302.17,352.941 2302.17,352.948C2302.04,355.861 2301.23,358.279 2299.79,360.238Z" style="fill:url(#_Linear3);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256Z" style="fill:rgb(165,118,255);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2157.67,-208.423)">
|
||||
<path d="M2253.67,223.256C2255.26,223.245 2257.02,223.56 2259.11,224.557L2275.67,234.102C2276.91,234.816 2277.67,236.138 2277.67,237.568L2277.67,259.508C2277.67,260.222 2277.29,260.882 2276.67,261.239C2276.05,261.597 2275.29,261.597 2274.67,261.24L2257.52,251.353C2256.38,250.731 2255.12,250.341 2253.67,250.347C2252.26,250.339 2250.99,250.721 2249.82,251.353L2187.87,287.04C2184.23,289.147 2181.96,292.478 2181.67,297.57L2181.68,269.865C2181.85,265.167 2183.93,261.653 2187.92,259.322L2248.23,224.557C2249.69,223.796 2251.5,223.29 2253.67,223.256ZM2253.68,223.756C2251.6,223.789 2249.87,224.269 2248.47,224.996L2188.17,259.754C2184.35,261.992 2182.35,265.367 2182.18,269.874C2182.18,269.874 2182.17,292.759 2182.17,292.757C2183.25,290.047 2185.13,288.051 2187.62,286.607L2249.57,250.919C2249.58,250.917 2249.58,250.915 2249.59,250.913C2250.83,250.243 2252.17,249.839 2253.67,249.847C2255.21,249.841 2256.54,250.253 2257.76,250.914C2257.76,250.916 2257.76,250.917 2257.76,250.919L2274.92,260.807C2275.38,261.075 2275.95,261.074 2276.42,260.806C2276.88,260.538 2277.17,260.043 2277.17,259.508L2277.17,237.568C2277.17,236.317 2276.5,235.16 2275.42,234.535C2275.42,234.535 2258.88,225 2258.87,224.996C2256.87,224.049 2255.2,223.746 2253.68,223.756Z" style="fill:url(#_Linear4);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.800798,0.462341,0.769972,-1.33363,-1677.22,-896.858)">
|
||||
<path d="M2241.55,-28.184C2239.1,-28.989 2236.83,-29.204 2234.68,-29.295C2234.68,-29.295 2220.82,-29.3 2215.03,-29.303C2213.48,-29.303 2212.05,-28.808 2211.28,-28.004C2208.65,-25.275 2202.56,-18.936 2199.45,-15.709C2199.07,-15.306 2199.07,-14.809 2199.46,-14.406C2199.85,-14.004 2200.57,-13.758 2201.34,-13.761C2208.36,-13.788 2222.72,-13.845 2222.72,-13.845C2223.98,-13.851 2225.44,-13.657 2227.06,-13.117L2241.55,-28.184Z" style="fill:rgb(141,81,249);"/>
|
||||
</g>
|
||||
<g transform="matrix(-4.32309,0,0,12.4454,9610.35,-1450.35)">
|
||||
<path d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z" style="fill:rgb(104,0,197);"/>
|
||||
<clipPath id="_clip5">
|
||||
<path d="M2205.31,121.966C2205.31,121.88 2205.18,121.8 2204.96,121.757C2204.74,121.714 2204.48,121.714 2204.27,121.757C2201.75,122.263 2195.36,123.547 2192.85,124.052C2192.63,124.095 2192.5,124.174 2192.5,124.261C2192.5,124.347 2192.63,124.426 2192.85,124.469C2195.36,124.974 2201.75,126.255 2204.27,126.759C2204.48,126.802 2204.74,126.802 2204.96,126.759C2205.18,126.716 2205.31,126.636 2205.31,126.55C2205.31,125.541 2205.31,122.976 2205.31,121.966Z"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip5)">
|
||||
<g transform="matrix(0.124502,0.074907,0.206623,-0.0414384,1997.62,-7.40235)">
|
||||
<path d="M1726.17,-64.249L1708.16,-72.303L1708.05,-23.514L1721.88,-32.386C1722.96,-33.241 1723.09,-33.944 1723.15,-34.636L1723.15,-54.373C1723.19,-56.238 1724.96,-57.594 1726.87,-56.686L1726.17,-64.249Z" style="fill:url(#_Linear6);"/>
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path d="M1726.17,-45.661L1704.47,-40.254C1706.28,-40.527 1708.14,-40.212 1708.16,-39.416L1708.16,-18.976L1726.17,-18.976L1726.17,-45.661Z" style="fill:rgb(141,81,249);"/>
|
||||
</g>
|
||||
<g transform="matrix(-0.126036,0.0767377,0.569859,0.112933,2435.01,-3.09225)">
|
||||
<path d="M1726.17,-45.661L1726.17,-18.976L1708.16,-18.976L1708.16,-39.416C1707.79,-40.732 1704.5,-40.298 1702.68,-40.025L1726.17,-45.661ZM1705.49,-40.491C1706.2,-40.507 1706.87,-40.464 1707.4,-40.327C1708.01,-40.173 1708.48,-39.899 1708.62,-39.436C1708.62,-39.429 1708.62,-39.423 1708.62,-39.416L1708.62,-19.152C1708.62,-19.152 1725.72,-19.152 1725.72,-19.152L1725.72,-45.345L1705.49,-40.491Z" style="fill:url(#_Radial7);"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-70.0711,-0.927611,1.54482,-42.0752,2233.59,-20.1891)"><stop offset="0" style="stop-color:rgb(141,81,249);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(116,50,223);stop-opacity:1"/></linearGradient>
|
||||
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.78193e-15,-78.0949,78.0949,4.78193e-15,2195.72,354.021)"><stop offset="0" style="stop-color:rgb(141,81,249);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(116,50,223);stop-opacity:1"/></linearGradient>
|
||||
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(41.6089,41.5866,-41.5866,41.6089,2282.31,262.837)"><stop offset="0" style="stop-color:rgb(211,187,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(211,187,255);stop-opacity:0"/></linearGradient>
|
||||
<linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(9.25616,16.7005,-16.7005,9.25616,2215,243.712)"><stop offset="0" style="stop-color:rgb(211,187,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(211,187,255);stop-opacity:0"/></linearGradient>
|
||||
<linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.130164,-61.9937,59.4003,-0.135847,1711.63,-25.7957)"><stop offset="0" style="stop-color:rgb(116,50,223);stop-opacity:1"/><stop offset="0.51" style="stop-color:rgb(110,38,217);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(91,0,197);stop-opacity:1"/></linearGradient>
|
||||
<radialGradient id="_Radial7" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(13.8659,4.71436,-12.1609,5.37534,1708.16,-32.287)"><stop offset="0" style="stop-color:rgb(211,187,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(211,187,255);stop-opacity:0"/></radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
@@ -59,7 +59,7 @@ function handleCancel() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper :model-value="modelValue" @update:model-value="emit('update:modelValue', $event)" :max-width="width">
|
||||
<VDialog :model-value="modelValue" @update:model-value="emit('update:modelValue', $event)" :max-width="width">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<div class="d-flex align-center justify-start mt-3">
|
||||
@@ -82,5 +82,5 @@ function handleCancel() {
|
||||
</VCardActions>
|
||||
<VDialogCloseBtn @click="handleCancel" />
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
<template>
|
||||
<VDialog v-model="dialogModel" v-bind="$attrs" @update:model-value="handleDialogChange">
|
||||
<slot />
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, onBeforeUnmount } from 'vue'
|
||||
import { useScrollLockWithWatch } from '@/composables/useScrollLock'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
modelValue?: boolean
|
||||
// 滚动锁定配置
|
||||
scrollLock?: boolean
|
||||
preserveScrollPosition?: boolean
|
||||
preventTouchScroll?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
scrollLock: true,
|
||||
preserveScrollPosition: true,
|
||||
preventTouchScroll: true,
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
}>()
|
||||
|
||||
// 计算属性
|
||||
const dialogModel = computed({
|
||||
get: () => props.modelValue || false,
|
||||
set: (value: boolean) => emit('update:modelValue', value),
|
||||
})
|
||||
|
||||
// 使用滚动锁定
|
||||
const { isLocked, lockScroll, restoreScroll } = useScrollLockWithWatch(dialogModel, {
|
||||
autoRestore: true,
|
||||
preserveScrollPosition: props.preserveScrollPosition,
|
||||
preventTouchScroll: props.preventTouchScroll,
|
||||
})
|
||||
|
||||
// 处理弹窗状态变化
|
||||
const handleDialogChange = (value: boolean) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
// 监听弹窗状态变化
|
||||
watch(
|
||||
dialogModel,
|
||||
newValue => {
|
||||
if (props.scrollLock) {
|
||||
if (newValue) {
|
||||
lockScroll()
|
||||
} else {
|
||||
restoreScroll()
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 组件卸载时确保恢复滚动
|
||||
onBeforeUnmount(() => {
|
||||
if (isLocked.value) {
|
||||
restoreScroll()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -46,10 +46,9 @@ $header: ".layout-navbar";
|
||||
}
|
||||
|
||||
/* ℹ️ Ensure header styles are preserved when dialog is opened,
|
||||
regardless of scroll state
|
||||
but only if window was scrolled before dialog opened
|
||||
*/
|
||||
html.v-overlay-scroll-blocked &.window-scrolled.layout-navbar-fixed,
|
||||
html.dialog-scroll-locked &.layout-navbar-fixed {
|
||||
html.v-overlay-scroll-blocked &.window-scrolled.layout-navbar-fixed {
|
||||
|
||||
#{$header} {
|
||||
padding-inline: 1rem;
|
||||
|
||||
@@ -45,7 +45,7 @@ code {
|
||||
inset-block-start: 0;
|
||||
inset-inline: 0;
|
||||
pointer-events: none;
|
||||
transition: all 0.3s ease-in-out;
|
||||
transition: padding 0.3s ease-in-out;
|
||||
|
||||
.v-theme--light & {
|
||||
background: rgba(var(--v-theme-surface), 0.6);
|
||||
|
||||
@@ -17,11 +17,34 @@ export default defineComponent({
|
||||
syncRef(isOverlayNavActive, isLayoutOverlayVisible)
|
||||
|
||||
const scrollDistance = ref(window.scrollY)
|
||||
const isDialogOpen = ref(false)
|
||||
const wasScrolledBeforeDialog = ref(false)
|
||||
|
||||
// 监听弹窗状态变化
|
||||
const checkDialogState = () => {
|
||||
const wasDialogOpen = isDialogOpen.value
|
||||
isDialogOpen.value = document.documentElement.classList.contains('v-overlay-scroll-blocked')
|
||||
|
||||
// 当弹窗刚打开时,记录当前的滚动状态
|
||||
if (!wasDialogOpen && isDialogOpen.value) {
|
||||
wasScrolledBeforeDialog.value = scrollDistance.value > 0
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', () => {
|
||||
scrollDistance.value = window.scrollY
|
||||
})
|
||||
|
||||
// 初始检查弹窗状态
|
||||
checkDialogState()
|
||||
|
||||
// 监听 DOM 变化以检测弹窗状态
|
||||
const observer = new MutationObserver(checkDialogState)
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
@@ -88,9 +111,6 @@ export default defineComponent({
|
||||
},
|
||||
})
|
||||
|
||||
// 检查是否有弹窗打开(通过CSS类名判断)
|
||||
const isDialogOpen = document.documentElement.classList.contains('dialog-scroll-locked')
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
@@ -99,7 +119,7 @@ export default defineComponent({
|
||||
'layout-navbar-fixed',
|
||||
mdAndDown.value && 'layout-overlay-nav',
|
||||
route.meta.layoutWrapperClasses,
|
||||
(scrollDistance.value || isDialogOpen) && 'window-scrolled',
|
||||
(scrollDistance.value > 5 || (isDialogOpen.value && wasScrolledBeforeDialog.value)) && 'window-scrolled',
|
||||
],
|
||||
},
|
||||
[verticalNav, h('div', { class: 'layout-content-wrapper' }, [navbar, main, footer]), layoutOverlay],
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
|
||||
html {
|
||||
background: rgb(var(--v-theme-background));
|
||||
min-block-size: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
||||
overflow-y: overlay;
|
||||
min-block-size: 100vh;
|
||||
min-block-size: 100dvh;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -40,8 +41,8 @@ body,
|
||||
|
||||
// TODO: Use grid gutter variable here;
|
||||
padding-block: 1.5rem;
|
||||
padding-inline: 0.5rem;
|
||||
padding-block-start: calc(env(safe-area-inset-top) + 4.5rem);
|
||||
padding-inline: 0.5rem;
|
||||
|
||||
// display: flex;display
|
||||
|
||||
|
||||
@@ -520,7 +520,7 @@ export interface SiteUserData {
|
||||
// 用户名
|
||||
username?: string
|
||||
// 用户ID
|
||||
userid?: number
|
||||
userid?: string
|
||||
// 用户等级
|
||||
user_level?: string
|
||||
// 加入时间
|
||||
|
||||
@@ -133,7 +133,7 @@ const instructions = computed(() => {
|
||||
</Teleport>
|
||||
|
||||
<!-- 手动安装说明对话框 -->
|
||||
<DialogWrapper v-model="showInstructions" max-width="500">
|
||||
<VDialog v-model="showInstructions" max-width="500">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle class="d-flex align-center">
|
||||
@@ -170,7 +170,7 @@ const instructions = computed(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -116,7 +116,7 @@ function onClose() {
|
||||
<VImg :src="filter_svg" cover class="mt-7" max-width="3rem" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="ruleInfoDialog"
|
||||
v-model="ruleInfoDialog"
|
||||
scrollable
|
||||
@@ -222,6 +222,6 @@ function onClose() {
|
||||
}}</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -147,7 +147,7 @@ const { stop: stopRefresh } = useConditionalDataRefresh(
|
||||
loadDownloaderInfo,
|
||||
shouldRefresh, // 响应式条件:只有当allowRefresh为true且downloader启用时才运行
|
||||
3000, // 3秒间隔
|
||||
true // 立即执行一次
|
||||
true, // 立即执行一次
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -196,7 +196,7 @@ onUnmounted(() => {
|
||||
</VCard>
|
||||
</VHover>
|
||||
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="downloaderInfoDialog"
|
||||
v-model="downloaderInfoDialog"
|
||||
scrollable
|
||||
@@ -383,6 +383,6 @@ onUnmounted(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -223,7 +223,7 @@ function onClose() {
|
||||
<VImg :src="filter_group_svg" cover class="mt-10" max-width="3rem" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="groupInfoDialog"
|
||||
v-model="groupInfoDialog"
|
||||
scrollable
|
||||
@@ -308,7 +308,7 @@ function onClose() {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
<ImportCodeDialog
|
||||
v-if="importCodeDialog"
|
||||
v-model="importCodeDialog"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -204,7 +204,7 @@ onMounted(() => {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="mediaServerInfoDialog"
|
||||
v-model="mediaServerInfoDialog"
|
||||
scrollable
|
||||
@@ -506,6 +506,6 @@ onMounted(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -141,7 +141,7 @@ function onClose() {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="notificationInfoDialog"
|
||||
v-model="notificationInfoDialog"
|
||||
scrollable
|
||||
@@ -476,6 +476,6 @@ function onClose() {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -106,7 +106,9 @@ const iconPath: Ref<string> = computed(() => {
|
||||
if (imageLoadError.value) return noImage
|
||||
// 如果是网络图片则使用代理后返回
|
||||
if (props.plugin?.plugin_icon?.startsWith('http'))
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(props.plugin?.plugin_icon)}&cache=true`
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(
|
||||
props.plugin?.plugin_icon,
|
||||
)}&cache=true`
|
||||
|
||||
return `./plugin_icon/${props.plugin?.plugin_icon}`
|
||||
})
|
||||
@@ -267,15 +269,15 @@ const dropdownItems = ref([
|
||||
<!-- 安装插件进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
<!-- 更新日志 -->
|
||||
<DialogWrapper v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||
<VCard :title="t('plugin.updateHistoryTitle', { name: props.plugin?.plugin_name })">
|
||||
<VDialogCloseBtn @click="releaseDialog = false" />
|
||||
<VDivider />
|
||||
<VersionHistory :history="props.plugin?.history" />
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
<!-- 插件详情-->
|
||||
<DialogWrapper v-if="detailDialog" v-model="detailDialog" max-width="30rem">
|
||||
<VDialog v-if="detailDialog" v-model="detailDialog" max-width="30rem">
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="detailDialog = false" />
|
||||
<VCardText>
|
||||
@@ -335,6 +337,6 @@ const dropdownItems = ref([
|
||||
</VCol>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -547,7 +547,7 @@ watch(
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
|
||||
<!-- 更新日志 -->
|
||||
<DialogWrapper v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard :title="t('plugin.updateHistoryTitle', { name: props.plugin?.plugin_name })">
|
||||
<VDialogCloseBtn @click="releaseDialog = false" />
|
||||
<VDivider />
|
||||
@@ -562,10 +562,10 @@ watch(
|
||||
</VBtn>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
|
||||
<!-- 实时日志弹窗 -->
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="loggingDialog"
|
||||
v-model="loggingDialog"
|
||||
scrollable
|
||||
@@ -591,10 +591,10 @@ watch(
|
||||
<LoggingView :logfile="`plugins/${props.plugin?.id?.toLowerCase()}.log`" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
|
||||
<!-- 插件分身对话框 -->
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="pluginCloneDialog"
|
||||
v-model="pluginCloneDialog"
|
||||
width="600"
|
||||
@@ -700,7 +700,7 @@ watch(
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -350,7 +350,7 @@ const dropdownItems = ref([
|
||||
</VHover>
|
||||
|
||||
<!-- 重命名对话框 -->
|
||||
<DialogWrapper v-if="renameDialog" v-model="renameDialog" max-width="400">
|
||||
<VDialog v-if="renameDialog" v-model="renameDialog" max-width="400">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
@@ -374,10 +374,10 @@ const dropdownItems = ref([
|
||||
<VBtn color="primary" prepend-icon="mdi-check" class="px-5" @click="confirmRename">确认</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
|
||||
<!-- 设置对话框 -->
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="settingDialog"
|
||||
v-model="settingDialog"
|
||||
max-width="600"
|
||||
@@ -480,7 +480,7 @@ const dropdownItems = ref([
|
||||
<VBtn color="primary" prepend-icon="mdi-content-save" class="px-5" @click="saveSettings">保存</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ function onClose() {
|
||||
@close="smbConfigDialog = false"
|
||||
@done="handleDone"
|
||||
/>
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="customConfigDialog"
|
||||
v-model="customConfigDialog"
|
||||
scrollable
|
||||
@@ -263,6 +263,6 @@ function onClose() {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -21,6 +21,14 @@ const { t } = useI18n()
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
media: Object as PropType<Subscribe>,
|
||||
batchMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
// 从 provide 中获取全局设置
|
||||
@@ -29,7 +37,7 @@ const globalSettingsStore = useGlobalSettingsStore()
|
||||
const globalSettings = globalSettingsStore.globalSettings
|
||||
|
||||
// 定义触发的自定义事件
|
||||
const emit = defineEmits(['remove', 'save'])
|
||||
const emit = defineEmits(['remove', 'save', 'select'])
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
@@ -297,6 +305,17 @@ function onSubscribeEditRemove() {
|
||||
subscribeEditDialog.value = false
|
||||
emit('remove')
|
||||
}
|
||||
|
||||
// 处理卡片点击事件
|
||||
function handleCardClick() {
|
||||
if (props.batchMode) {
|
||||
// 批量模式下触发选择事件
|
||||
emit('select')
|
||||
} else {
|
||||
// 非批量模式下打开编辑弹窗
|
||||
editSubscribeDialog()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -308,6 +327,7 @@ function onSubscribeEditRemove() {
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||
'outline-dotted outline-pink-500 outline-2': props.batchMode && props.selected,
|
||||
}"
|
||||
>
|
||||
<VCard
|
||||
@@ -319,8 +339,8 @@ function onSubscribeEditRemove() {
|
||||
}"
|
||||
rounded="0"
|
||||
min-height="150"
|
||||
@click="editSubscribeDialog"
|
||||
:ripple="false"
|
||||
@click="handleCardClick"
|
||||
:ripple="!props.batchMode"
|
||||
>
|
||||
<div class="me-n3 absolute top-1 right-4">
|
||||
<IconBtn>
|
||||
|
||||
@@ -278,7 +278,7 @@ onMounted(() => {
|
||||
</VCard>
|
||||
|
||||
<!-- 更多来源对话框 -->
|
||||
<DialogWrapper v-model="showMoreTorrents" max-width="25rem" location="center">
|
||||
<VDialog v-model="showMoreTorrents" max-width="25rem" location="center">
|
||||
<VCard>
|
||||
<VCardTitle class="py-3 d-flex align-center">
|
||||
<span>其他来源</span>
|
||||
@@ -361,7 +361,7 @@ onMounted(() => {
|
||||
</VList>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
|
||||
<AddDownloadDialog
|
||||
v-if="addDownloadDialog"
|
||||
@@ -418,7 +418,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.chip-web-source {
|
||||
background-color: #8000FF;
|
||||
background-color: #8000ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<DialogWrapper max-width="35rem" scrollable>
|
||||
<VDialog max-width="35rem" scrollable>
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
@@ -209,5 +209,5 @@ onMounted(() => {
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -70,7 +70,7 @@ async function savaAlistConfig() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardItem>
|
||||
@@ -143,5 +143,5 @@ async function savaAlistConfig() {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -110,7 +110,7 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardItem>
|
||||
@@ -148,5 +148,5 @@ onUnmounted(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -170,7 +170,7 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<DialogWrapper max-width="40rem" scrollable>
|
||||
<VDialog max-width="40rem" scrollable>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<VCol>
|
||||
@@ -286,5 +286,5 @@ onMounted(() => {
|
||||
</VCardText>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -156,7 +156,7 @@ async function doDelete() {
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<DialogWrapper max-width="40rem" scrollable>
|
||||
<VDialog max-width="40rem" scrollable>
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<VCol>
|
||||
@@ -266,7 +266,7 @@ async function doDelete() {
|
||||
</VCardText>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -24,7 +24,7 @@ function handleImport() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper width="40rem" scrollable max-height="85vh">
|
||||
<VDialog width="40rem" scrollable max-height="85vh">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
@@ -43,5 +43,5 @@ function handleImport() {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -15,12 +15,12 @@ defineProps({
|
||||
const emit = defineEmits(['close'])
|
||||
</script>
|
||||
<template>
|
||||
<DialogWrapper max-width="50rem">
|
||||
<VDialog max-width="50rem">
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardItem>
|
||||
<MediaInfoCard :context="context" />
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -148,7 +148,7 @@ onBeforeMount(async () => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<DialogWrapper scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
||||
<!-- Vuetify 渲染模式 -->
|
||||
<VCard v-if="renderMode === 'vuetify'" :title="`${props.plugin?.plugin_name} - ${t('dialog.pluginConfig.title')}`">
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
@@ -187,5 +187,5 @@ onBeforeMount(async () => {
|
||||
|
||||
<!-- 进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -124,7 +124,7 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<DialogWrapper scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||
<!-- Vuetify 渲染模式 -->
|
||||
<VCard v-if="renderMode === 'vuetify'" :title="`${props.plugin?.plugin_name}`">
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
@@ -160,5 +160,5 @@ onMounted(() => {
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -63,7 +63,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
@@ -89,5 +89,5 @@ onMounted(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -10,12 +10,12 @@ const props = defineProps({
|
||||
</script>
|
||||
<template>
|
||||
<!-- Progress Dialog -->
|
||||
<DialogWrapper :scrim="false" width="25rem">
|
||||
<VDialog :scrim="false" width="25rem">
|
||||
<VCard elevation="3" color="primary">
|
||||
<VCardText class="text-center">
|
||||
{{ props.text || t('dialog.progress.processing') }}
|
||||
<VProgressLinear color="white" class="mb-0 mt-1" :model-value="props.value" indeterminate />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -57,7 +57,7 @@ async function handleReset() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardItem>
|
||||
@@ -99,5 +99,5 @@ async function handleReset() {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -205,7 +205,7 @@ const progressSSE = useProgressSSE(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`,
|
||||
handleProgressMessage,
|
||||
'reorganize-progress',
|
||||
progressActive
|
||||
progressActive,
|
||||
)
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
@@ -269,7 +269,7 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend> <VIcon icon="mdi-folder-move" class="me-2" /> </template>
|
||||
@@ -487,7 +487,7 @@ onUnmounted(() => {
|
||||
<!-- 手动整理进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" :value="progressValue" />
|
||||
<!-- TMDB ID搜索框 -->
|
||||
<DialogWrapper v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh">
|
||||
<VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh">
|
||||
<MediaIdSelector
|
||||
v-if="mediaSource === 'themoviedb'"
|
||||
v-model="transferForm.tmdbid"
|
||||
@@ -500,6 +500,6 @@ onUnmounted(() => {
|
||||
@close="mediaSelectorDialog = false"
|
||||
:type="mediaSource"
|
||||
/>
|
||||
</DialogWrapper>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,7 @@ import api from '@/api'
|
||||
import type { Site, Plugin, Subscribe } from '@/api/types'
|
||||
import { getNavMenus, getSettingTabs } from '@/router/i18n-menu'
|
||||
import { NavMenu } from '@/@layouts/types'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { useUserStore, useGlobalSettingsStore } from '@/stores'
|
||||
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
@@ -26,6 +26,10 @@ const router = useRouter()
|
||||
// 用户 Store
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 全局设置 Store
|
||||
const globalSettingsStore = useGlobalSettingsStore()
|
||||
const globalSettings = globalSettingsStore.globalSettings
|
||||
|
||||
// 超级用户
|
||||
const superUser = userStore.superUser
|
||||
|
||||
@@ -63,6 +67,11 @@ const hasManagePermission = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
// 是否显示合集搜索项(当SEARCH_SOURCE包含themoviedb时显示)
|
||||
const showCollectionSearch = computed(() => {
|
||||
return globalSettings.SEARCH_SOURCE?.includes('themoviedb') || false
|
||||
})
|
||||
|
||||
// 所有订阅数据
|
||||
const SubscribeItems = ref<Subscribe[]>([])
|
||||
|
||||
@@ -370,7 +379,7 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<DialogWrapper v-model="dialog" max-width="42rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog v-model="dialog" max-width="42rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard class="search-dialog">
|
||||
<!-- 搜索输入框 -->
|
||||
<VCardItem class="pa-4 pa-sm-5 search-box-container">
|
||||
@@ -435,7 +444,7 @@ onMounted(() => {
|
||||
</template>
|
||||
</VHover>
|
||||
|
||||
<VHover>
|
||||
<VHover v-if="showCollectionSearch">
|
||||
<template #default="hover">
|
||||
<VListItem
|
||||
density="comfortable"
|
||||
@@ -785,7 +794,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
|
||||
<!-- 站点选择对话框 -->
|
||||
<SearchSiteDialog
|
||||
|
||||
@@ -56,7 +56,7 @@ const filteredSites = computed(() => {
|
||||
</script>
|
||||
<template>
|
||||
<!-- Site Selection Dialog -->
|
||||
<DialogWrapper max-width="40rem" fullscreen-mobile>
|
||||
<VDialog max-width="40rem" fullscreen-mobile>
|
||||
<VCard class="site-dialog">
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
@@ -169,7 +169,7 @@ const filteredSites = computed(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
<style scoped>
|
||||
.site-checkbox-wrapper {
|
||||
|
||||
@@ -147,7 +147,7 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper scrollable :close-on-back="false" eager max-width="45rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable :close-on-back="false" eager max-width="45rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem :class="props.oper === 'add' ? 'py-3' : 'py-2'">
|
||||
<template #prepend>
|
||||
@@ -350,5 +350,5 @@ onMounted(async () => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -71,7 +71,7 @@ async function updateSiteCookie() {
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<DialogWrapper max-width="30rem" scrollable>
|
||||
<VDialog max-width="30rem" scrollable>
|
||||
<!-- Dialog Content -->
|
||||
<VCard :title="t('dialog.siteCookieUpdate.title')">
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
@@ -114,5 +114,5 @@ async function updateSiteCookie() {
|
||||
</VCard>
|
||||
<!-- 进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
423
src/components/dialog/SiteImportDialog.vue
Normal file
423
src/components/dialog/SiteImportDialog.vue
Normal file
@@ -0,0 +1,423 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toastification'
|
||||
import type { Site } from '@/api/types'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import api from '@/api'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 显示器宽度
|
||||
const display = useDisplay()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 注册事件
|
||||
const emit = defineEmits(['update:modelValue', 'import-success'])
|
||||
|
||||
// 界面阶段枚举
|
||||
enum ImportStage {
|
||||
SELECT_FILE = 'select_file', // 选择文件阶段
|
||||
PREVIEW_FILE = 'preview_file', // 文件预览阶段
|
||||
IMPORTING = 'importing', // 正在导入阶段
|
||||
IMPORT_COMPLETE = 'import_complete', // 导入完成阶段
|
||||
}
|
||||
|
||||
// 当前阶段
|
||||
const currentStage = ref<ImportStage>(ImportStage.SELECT_FILE)
|
||||
|
||||
// 是否拖拽中
|
||||
const isDragging = ref(false)
|
||||
|
||||
// 导入的文件数据
|
||||
const importData = ref<Site[]>([])
|
||||
|
||||
// 导入进度
|
||||
const importProgress = ref(0)
|
||||
|
||||
// 预览数据
|
||||
const previewData = ref<Site[]>([])
|
||||
|
||||
// 选中的文件
|
||||
const selectedFile = ref<File | null>(null)
|
||||
|
||||
// 导入错误信息
|
||||
const importErrors = ref<Array<{ site: Site; error: string }>>([])
|
||||
|
||||
// 导入成功的站点
|
||||
const importSuccesses = ref<Site[]>([])
|
||||
|
||||
// 是否显示错误详情
|
||||
const showErrorDetails = ref(false)
|
||||
|
||||
// 处理拖拽事件
|
||||
function handleDragOver(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
isDragging.value = true
|
||||
}
|
||||
|
||||
function handleDragLeave(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
async function handleDrop(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
isDragging.value = false
|
||||
|
||||
const files = event.dataTransfer?.files
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0]
|
||||
if (file.type === 'application/json' || file.name.endsWith('.json')) {
|
||||
selectedFile.value = file
|
||||
await processFile(file)
|
||||
} else {
|
||||
$toast.error(t('site.messages.invalidFileType'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件
|
||||
async function processFile(file: File) {
|
||||
try {
|
||||
const text = await file.text()
|
||||
const data = JSON.parse(text)
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
importData.value = data
|
||||
previewData.value = data.slice(0, 5) // 只显示前5个站点作为预览
|
||||
currentStage.value = ImportStage.PREVIEW_FILE
|
||||
} else {
|
||||
$toast.error(t('site.messages.invalidFileFormat'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Parse file error:', error)
|
||||
$toast.error(t('site.messages.parseFileError'))
|
||||
}
|
||||
}
|
||||
|
||||
// 验证站点数据
|
||||
function validateSiteData(site: any): boolean {
|
||||
const requiredFields = ['name', 'domain', 'url']
|
||||
return requiredFields.every(field => site[field])
|
||||
}
|
||||
|
||||
// 批量导入站点
|
||||
async function importSites() {
|
||||
if (importData.value.length === 0) {
|
||||
$toast.error(t('site.messages.noDataToImport'))
|
||||
return
|
||||
}
|
||||
|
||||
// 验证数据
|
||||
const validSites = importData.value.filter(validateSiteData)
|
||||
if (validSites.length === 0) {
|
||||
$toast.error(t('site.messages.noValidData'))
|
||||
return
|
||||
}
|
||||
|
||||
if (validSites.length !== importData.value.length) {
|
||||
$toast.warning(t('site.messages.someInvalidData', { valid: validSites.length, total: importData.value.length }))
|
||||
}
|
||||
|
||||
// 进入导入阶段
|
||||
currentStage.value = ImportStage.IMPORTING
|
||||
startNProgress()
|
||||
importProgress.value = 0
|
||||
|
||||
try {
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
importErrors.value = [] // 清空之前的错误信息
|
||||
importSuccesses.value = [] // 清空之前的成功信息
|
||||
|
||||
for (let i = 0; i < validSites.length; i++) {
|
||||
const site = validSites[i]
|
||||
try {
|
||||
// 移除id字段,避免冲突
|
||||
const { id, ...siteData } = site
|
||||
const result: { success: boolean; message?: string } = await api.post('site/', siteData)
|
||||
if (result.success) {
|
||||
// 记录成功的站点
|
||||
successCount++
|
||||
importSuccesses.value.push(site)
|
||||
} else {
|
||||
failCount++
|
||||
// 记录失败信息
|
||||
importErrors.value.push({
|
||||
site,
|
||||
error: result.message || t('site.messages.importFailed'),
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Import site ${site.name} failed:`, error)
|
||||
failCount++
|
||||
// 记录错误信息
|
||||
importErrors.value.push({
|
||||
site,
|
||||
error: error instanceof Error ? error.message : t('site.messages.importFailed'),
|
||||
})
|
||||
}
|
||||
// 更新进度
|
||||
importProgress.value = Math.round(((i + 1) / validSites.length) * 100)
|
||||
}
|
||||
|
||||
// 进入完成阶段
|
||||
currentStage.value = ImportStage.IMPORT_COMPLETE
|
||||
|
||||
// 显示导入结果
|
||||
if (failCount === 0 && successCount > 0) {
|
||||
// 全部成功,直接关闭对话框
|
||||
$toast.success(t('site.messages.importSuccess', { count: successCount }))
|
||||
closeDialog(true)
|
||||
} else if (successCount === 0 && failCount > 0) {
|
||||
// 全部失败的情况
|
||||
$toast.error(t('site.messages.importAllFailed', { count: failCount }))
|
||||
showErrorDetails.value = true
|
||||
} else {
|
||||
// 部分成功部分失败的情况
|
||||
$toast.error(t('site.messages.importPartialFailed', { success: successCount, failed: failCount }))
|
||||
showErrorDetails.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Import sites failed:', error)
|
||||
$toast.error(t('site.messages.importFailed'))
|
||||
// 出错时回到预览阶段
|
||||
currentStage.value = ImportStage.PREVIEW_FILE
|
||||
} finally {
|
||||
doneNProgress()
|
||||
}
|
||||
}
|
||||
|
||||
// 重置到文件选择阶段
|
||||
function resetToFileSelection() {
|
||||
currentStage.value = ImportStage.SELECT_FILE
|
||||
importData.value = []
|
||||
previewData.value = []
|
||||
importProgress.value = 0
|
||||
isDragging.value = false
|
||||
selectedFile.value = null
|
||||
importErrors.value = []
|
||||
importSuccesses.value = []
|
||||
showErrorDetails.value = false
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
function closeDialog(success: boolean = false) {
|
||||
if (success) {
|
||||
emit('import-success')
|
||||
}
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
// 监听文件选择
|
||||
watch(selectedFile, async newFile => {
|
||||
if (newFile) {
|
||||
await processFile(newFile)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-upload" class="me-2" />
|
||||
</template>
|
||||
<VCardTitle>{{ t('site.actions.import') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('site.hints.import') }}</VCardSubtitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="closeDialog" />
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<!-- 阶段1:选择文件阶段 -->
|
||||
<div v-if="currentStage === ImportStage.SELECT_FILE" class="upload-area">
|
||||
<div
|
||||
class="upload-zone"
|
||||
:class="{ 'dragging': isDragging }"
|
||||
@dragover="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<VFileInput
|
||||
v-model="selectedFile"
|
||||
accept=".json"
|
||||
:label="t('site.fields.selectFile')"
|
||||
:hint="t('site.hints.selectFile')"
|
||||
persistent-hint
|
||||
prepend-icon="mdi-file-upload"
|
||||
/>
|
||||
<div class="text-center mt-4">
|
||||
<VIcon icon="mdi-cloud-upload" size="48" color="primary" />
|
||||
<p class="text-body-1 mt-2">{{ t('site.hints.dragDropFile') }}</p>
|
||||
<p class="text-caption text-medium-emphasis">{{ t('site.hints.supportedFormat') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阶段2:文件预览阶段 -->
|
||||
<div v-if="currentStage === ImportStage.PREVIEW_FILE" class="preview-area">
|
||||
<VAlert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
:text="t('site.messages.previewData', { count: importData.length })"
|
||||
/>
|
||||
|
||||
<!-- 预览列表 -->
|
||||
<VCard variant="outlined" class="mb-4">
|
||||
<VCardTitle class="text-subtitle-1">
|
||||
{{ t('site.preview.title') }} ({{
|
||||
t('site.preview.showing', { count: previewData.length, total: importData.length })
|
||||
}})
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(site, index) in previewData"
|
||||
:key="index"
|
||||
:class="{ 'border-error': !validateSiteData(site) }"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
:icon="validateSiteData(site) ? 'mdi-check-circle' : 'mdi-alert-circle'"
|
||||
:color="validateSiteData(site) ? 'success' : 'error'"
|
||||
/>
|
||||
</template>
|
||||
<VListItemTitle>{{ site.name || t('site.preview.unnamed') }}</VListItemTitle>
|
||||
<VListItemSubtitle>{{ site.url || t('site.preview.noUrl') }}</VListItemSubtitle>
|
||||
<template #append>
|
||||
<VChip v-if="!validateSiteData(site)" size="small" color="error" variant="tonal">
|
||||
{{ t('site.preview.invalid') }}
|
||||
</VChip>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="d-flex justify-end gap-2">
|
||||
<VBtn variant="text" @click="resetToFileSelection">
|
||||
{{ t('common.reset') }}
|
||||
</VBtn>
|
||||
<VBtn color="primary" @click="importSites" :disabled="importData.length === 0">
|
||||
{{ t('site.actions.startImport') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阶段3:正在导入阶段 -->
|
||||
<div v-if="currentStage === ImportStage.IMPORTING" class="importing-area">
|
||||
<VAlert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
:text="t('site.messages.importing', { progress: importProgress })"
|
||||
/>
|
||||
|
||||
<!-- 导入进度 -->
|
||||
<VCard variant="outlined" class="mb-4">
|
||||
<VCardTitle class="text-subtitle-1">
|
||||
{{ t('site.messages.importing', { progress: importProgress }) }}
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<VProgressLinear v-model="importProgress" color="primary" height="8" rounded class="mb-2" />
|
||||
<p class="text-caption text-center">{{ importProgress }}%</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
|
||||
<!-- 阶段4:导入完成阶段 -->
|
||||
<div v-if="currentStage === ImportStage.IMPORT_COMPLETE" class="result-area">
|
||||
<!-- 成功导入的站点 -->
|
||||
<div v-if="importSuccesses.length > 0" class="success-sites mb-4">
|
||||
<VAlert
|
||||
type="success"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
:text="t('site.messages.importSuccess', { count: importSuccesses.length })"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 错误详情 -->
|
||||
<div v-if="showErrorDetails && importErrors.length > 0" class="error-details">
|
||||
<VAlert
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
:text="t('site.messages.importErrors', { count: importErrors.length })"
|
||||
/>
|
||||
|
||||
<VCard variant="outlined" class="mb-4">
|
||||
<VCardTitle class="text-subtitle-1 d-flex align-center justify-space-between">
|
||||
{{ t('site.errors.title') }}
|
||||
</VCardTitle>
|
||||
<!-- 错误信息详情 -->
|
||||
<VExpansionPanels class="mt-4">
|
||||
<VExpansionPanel v-for="(error, index) in importErrors" :key="index">
|
||||
<VExpansionPanelTitle>
|
||||
{{ error.site.name || t('site.preview.unnamed') }} - {{ t('site.errors.details') }}
|
||||
</VExpansionPanelTitle>
|
||||
<VExpansionPanelText>
|
||||
<VAlert type="error" variant="text" :text="error.error" class="mb-0" />
|
||||
</VExpansionPanelText>
|
||||
</VExpansionPanel>
|
||||
</VExpansionPanels>
|
||||
</VCard>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="d-flex justify-end gap-2">
|
||||
<VBtn variant="text" @click="resetToFileSelection">
|
||||
{{ t('common.reset') }}
|
||||
</VBtn>
|
||||
<VBtn color="primary" @click="closeDialog(false)">
|
||||
{{ t('common.close') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.upload-area {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.upload-zone {
|
||||
padding: 2rem;
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-zone.dragging {
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
background-color: rgba(var(--v-theme-primary), 0.05);
|
||||
}
|
||||
|
||||
.error-details {
|
||||
margin-block: 1rem;
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
.error-details .v-expansion-panels {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.border-success {
|
||||
border-inline-start: 4px solid rgb(var(--v-theme-success));
|
||||
}
|
||||
|
||||
.border-error {
|
||||
border-inline-start: 4px solid rgb(var(--v-theme-error));
|
||||
}
|
||||
</style>
|
||||
@@ -130,7 +130,7 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<DialogWrapper scrollable fullscreen :scrim="false" transition="dialog-bottom-transition">
|
||||
<VDialog scrollable fullscreen :scrim="false" transition="dialog-bottom-transition">
|
||||
<VCard>
|
||||
<!-- Toolbar -->
|
||||
<div>
|
||||
@@ -281,7 +281,7 @@ onMounted(() => {
|
||||
@error="addDownloadError"
|
||||
@close="addDownloadDialog = false"
|
||||
/>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
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>
|
||||
<VDialog 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>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<VDialog 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>
|
||||
</VDialog>
|
||||
</VDialog>
|
||||
</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>
|
||||
@@ -287,11 +287,11 @@ onBeforeMount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper scrollable eager max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog 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')" />
|
||||
@@ -484,5 +484,5 @@ onBeforeMount(() => {
|
||||
</VCard>
|
||||
<!-- 进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="t('dialog.siteUserData.refreshing')" />
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -50,7 +50,7 @@ async function saveSmbConfig() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardItem>
|
||||
@@ -127,5 +127,5 @@ async function saveSmbConfig() {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -284,7 +284,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
@@ -543,5 +543,5 @@ onMounted(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -85,7 +85,7 @@ onBeforeMount(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<DialogWrapper scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem class="my-2">
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
@@ -206,7 +206,7 @@ onBeforeMount(() => {
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -146,7 +146,7 @@ function getMediaTypeText(type: string | undefined) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard class="mx-auto" width="100%">
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('dialog.subscribeHistory.title', { type: getMediaTypeText(props.type) }) }}</VCardTitle>
|
||||
@@ -220,5 +220,5 @@ function getMediaTypeText(type: string | undefined) {
|
||||
</VCard>
|
||||
<!-- 进度框 -->
|
||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -55,7 +55,7 @@ const $toast = useToast()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper scrollable max-width="30rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable max-width="30rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
@@ -112,5 +112,5 @@ const $toast = useToast()
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -118,7 +118,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper scrollable max-width="40rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable max-width="40rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
@@ -331,7 +331,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { formatFileSize } from '@/@core/utils/formatters'
|
||||
import api from '@/api'
|
||||
import { FileItem, TransferQueue } from '@/api/types'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
@@ -18,11 +20,14 @@ const emit = defineEmits(['close'])
|
||||
// 数据列表
|
||||
const dataList = ref<TransferQueue[]>([])
|
||||
|
||||
// 整理进度文本
|
||||
const progressText = ref(t('dialog.transferQueue.processing'))
|
||||
// 整体进度相关 - 根据完成的文件计算
|
||||
const overallProgress = ref({
|
||||
value: 0,
|
||||
text: t('dialog.transferQueue.processing'),
|
||||
})
|
||||
|
||||
// 整理进度
|
||||
const progressValue = ref(0)
|
||||
// 文件进度映射
|
||||
const fileProgressMap = ref<Map<string, { enable: boolean; value: number }>>(new Map())
|
||||
|
||||
// 数据可刷新标志
|
||||
const refreshFlag = ref(false)
|
||||
@@ -33,6 +38,9 @@ const progressActive = ref(false)
|
||||
// 活动标签
|
||||
const activeTab = ref('')
|
||||
|
||||
// 定时器引用
|
||||
const queueTimer = ref<NodeJS.Timeout | null>(null)
|
||||
|
||||
// 状态标签
|
||||
const stateDict: { [key: string]: string } = {
|
||||
'waiting': t('dialog.transferQueue.waitingState'),
|
||||
@@ -50,9 +58,18 @@ function getStateColor(state: string) {
|
||||
else return 'error'
|
||||
}
|
||||
|
||||
// 从dataList中提取所有的媒体信息
|
||||
// 从dataList中提取所有的媒体信息,合并相同title_year的记录
|
||||
const mediaList = computed(() => {
|
||||
return dataList.value.map(item => item.media)
|
||||
const mediaMap = new Map<string, any>()
|
||||
|
||||
dataList.value.forEach(item => {
|
||||
const titleYear = item.media.title_year || ''
|
||||
if (!mediaMap.has(titleYear)) {
|
||||
mediaMap.set(titleYear, item.media)
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(mediaMap.values())
|
||||
})
|
||||
|
||||
// 按media计算总数和完成数,返回 x/x
|
||||
@@ -66,17 +83,49 @@ function getMediaCount(title_year: string) {
|
||||
return `${completed} / ${total}`
|
||||
}
|
||||
|
||||
// 根据媒体信息获取对应的整理任务
|
||||
// 根据媒体信息获取对应的整理任务,合并相同title_year的所有任务
|
||||
const activeTasks = computed(() => {
|
||||
return dataList.value.find(item => item.media.title_year === activeTab.value)?.tasks
|
||||
const tasks = dataList.value.filter(item => item.media.title_year === activeTab.value).flatMap(item => item.tasks)
|
||||
return tasks
|
||||
})
|
||||
|
||||
// 根据媒体title_year获取对应的任务列表
|
||||
function getTasksByMedia(title_year: string) {
|
||||
return dataList.value.filter(item => item.media.title_year === title_year).flatMap(item => item.tasks)
|
||||
}
|
||||
|
||||
// 计算整体进度
|
||||
const overallProgressComputed = computed(() => {
|
||||
if (dataList.value.length === 0) return 0
|
||||
|
||||
const allTasks = dataList.value.flatMap(item => item.tasks)
|
||||
const totalTasks = allTasks.length
|
||||
const completedTasks = allTasks.filter(task => task.state === 'completed').length
|
||||
|
||||
return totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0
|
||||
})
|
||||
|
||||
// 获取文件进度
|
||||
function getFileProgress(filePath: string) {
|
||||
return fileProgressMap.value.get(filePath) || { enable: false, value: 0 }
|
||||
}
|
||||
|
||||
// 调用API获取队列信息
|
||||
async function get_transfer_queue() {
|
||||
try {
|
||||
dataList.value = await api.get('transfer/queue')
|
||||
if (dataList.value.length > 0) {
|
||||
if (!activeTab.value || activeTasks.value?.length == 0) activeTab.value = dataList.value[0].media.title_year || ''
|
||||
|
||||
// 如果有数据且SSE未启动,则启动SSE监听
|
||||
if (!progressActive.value) {
|
||||
startLoadingProgress()
|
||||
}
|
||||
} else {
|
||||
// 如果没有数据,停止SSE监听
|
||||
if (progressActive.value) {
|
||||
stopLoadingProgress()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -93,85 +142,164 @@ async function remove_queue_task(fileitem: FileItem) {
|
||||
}
|
||||
}
|
||||
|
||||
// 进度SSE消息处理函数
|
||||
function handleProgressMessage(event: MessageEvent) {
|
||||
const progress = JSON.parse(event.data)
|
||||
if (progress) {
|
||||
if (!progress.enable) {
|
||||
progressText.value = t('dialog.transferQueue.processing')
|
||||
progressValue.value = 0
|
||||
if (refreshFlag.value) {
|
||||
refreshFlag.value = false
|
||||
get_transfer_queue()
|
||||
}
|
||||
return
|
||||
}
|
||||
progressText.value = progress.text
|
||||
progressValue.value = progress.value
|
||||
if (progress.value >= 100 && refreshFlag.value) {
|
||||
refreshFlag.value = false
|
||||
get_transfer_queue()
|
||||
} else {
|
||||
if (progress.value > 0 && refreshFlag.value && progress.text?.includes('整理完成')) {
|
||||
refreshFlag.value = false
|
||||
get_transfer_queue()
|
||||
} else {
|
||||
refreshFlag.value = true
|
||||
// 文件进度SSE消息处理函数
|
||||
function createFileProgressHandler(filePath: string) {
|
||||
return function handleFileProgressMessage(event: MessageEvent) {
|
||||
try {
|
||||
const progress = JSON.parse(event.data)
|
||||
if (progress) {
|
||||
fileProgressMap.value.set(filePath, {
|
||||
enable: progress.enable || false,
|
||||
value: progress.value || 0,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析文件进度消息失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用优化的进度SSE连接
|
||||
const progressSSE = useProgressSSE(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/progress/filetransfer`,
|
||||
handleProgressMessage,
|
||||
'transfer-queue-progress',
|
||||
progressActive
|
||||
// 文件进度SSE连接映射
|
||||
const fileProgressSSEMap = ref<Map<string, any>>(new Map())
|
||||
|
||||
// 启动文件进度监听
|
||||
function startFileProgress(filePath: string) {
|
||||
if (fileProgressSSEMap.value.has(filePath)) {
|
||||
return // 已经存在连接
|
||||
}
|
||||
|
||||
// filePath计算md5
|
||||
const filePathMd5 = CryptoJS.MD5(filePath).toString()
|
||||
// 使用包含文件路径的唯一监听器ID
|
||||
const uniqueListenerId = `transfer-queue-file-progress-${filePathMd5}`
|
||||
const fileProgressUrl = `${import.meta.env.VITE_API_BASE_URL}system/progress/${filePathMd5}`
|
||||
|
||||
const fileProgressSSE = useProgressSSE(
|
||||
fileProgressUrl,
|
||||
createFileProgressHandler(filePath),
|
||||
uniqueListenerId,
|
||||
progressActive,
|
||||
)
|
||||
|
||||
fileProgressSSE.start()
|
||||
fileProgressSSEMap.value.set(filePath, fileProgressSSE)
|
||||
}
|
||||
|
||||
// 停止所有文件进度监听
|
||||
function stopAllFileProgress() {
|
||||
fileProgressSSEMap.value.forEach((sse, filePath) => {
|
||||
sse.stop()
|
||||
})
|
||||
fileProgressSSEMap.value.clear()
|
||||
fileProgressMap.value.clear()
|
||||
}
|
||||
|
||||
// 监听队列变化,自动管理文件进度SSE
|
||||
watch(
|
||||
dataList,
|
||||
newDataList => {
|
||||
// 获取当前正在运行的文件路径集合
|
||||
const currentRunningFiles = new Set<string>()
|
||||
newDataList.forEach(item => {
|
||||
item.tasks.forEach(task => {
|
||||
if (task.state === 'running') {
|
||||
currentRunningFiles.add(task.fileitem.path)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 获取当前已建立SSE连接的文件路径集合
|
||||
const currentSSEFiles = new Set(fileProgressSSEMap.value.keys())
|
||||
|
||||
// 停止不再需要的SSE连接
|
||||
currentSSEFiles.forEach(filePath => {
|
||||
if (!currentRunningFiles.has(filePath)) {
|
||||
const sse = fileProgressSSEMap.value.get(filePath)
|
||||
if (sse) {
|
||||
sse.stop()
|
||||
fileProgressSSEMap.value.delete(filePath)
|
||||
}
|
||||
// 清除对应的进度数据
|
||||
fileProgressMap.value.delete(filePath)
|
||||
}
|
||||
})
|
||||
|
||||
// 为新的运行中文件建立SSE连接
|
||||
currentRunningFiles.forEach(filePath => {
|
||||
if (!fileProgressSSEMap.value.has(filePath)) {
|
||||
startFileProgress(filePath)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// 使用SSE监听加载进度
|
||||
function startLoadingProgress() {
|
||||
progressText.value = t('dialog.transferQueue.processing')
|
||||
overallProgress.value.text = t('dialog.transferQueue.processing')
|
||||
progressActive.value = true
|
||||
progressSSE.start()
|
||||
}
|
||||
|
||||
// 停止监听加载进度
|
||||
function stopLoadingProgress() {
|
||||
progressActive.value = false
|
||||
progressSSE.stop()
|
||||
// 只有在没有数据时才停止所有文件进度监听
|
||||
if (dataList.value.length === 0) {
|
||||
stopAllFileProgress()
|
||||
}
|
||||
}
|
||||
|
||||
// 启动定时获取队列
|
||||
function startQueueTimer() {
|
||||
// 清除可能存在的定时器
|
||||
if (queueTimer.value) {
|
||||
clearInterval(queueTimer.value)
|
||||
}
|
||||
|
||||
// 立即执行一次
|
||||
get_transfer_queue()
|
||||
|
||||
// 设置3秒定时器
|
||||
queueTimer.value = setInterval(() => {
|
||||
get_transfer_queue()
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// 停止定时获取队列
|
||||
function stopQueueTimer() {
|
||||
if (queueTimer.value) {
|
||||
clearInterval(queueTimer.value)
|
||||
queueTimer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
get_transfer_queue()
|
||||
startLoadingProgress()
|
||||
startQueueTimer()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopQueueTimer()
|
||||
stopLoadingProgress()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard class="mx-auto" width="100%">
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('dialog.transferQueue.title') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VDivider />
|
||||
<VProgressLinear
|
||||
v-if="dataList.length > 0 && progressValue > 0"
|
||||
:value="progressValue"
|
||||
color="primary"
|
||||
indeterminate
|
||||
/>
|
||||
<VCardItem v-if="dataList.length > 0 && progressValue > 0" class="text-center pt-2">
|
||||
<span class="text-sm">{{ progressText }}</span>
|
||||
</VCardItem>
|
||||
<VCardText v-if="dataList.length === 0" class="text-center"> {{ t('dialog.transferQueue.noTasks') }} </VCardText>
|
||||
<VCardText>
|
||||
|
||||
<!-- 整体进度显示 -->
|
||||
<VProgressLinear v-if="dataList.length > 0" :model-value="overallProgressComputed" color="primary" />
|
||||
<VDivider v-else />
|
||||
|
||||
<VCardText v-if="dataList.length === 0" class="text-center">
|
||||
{{ t('dialog.transferQueue.noTasks') }}
|
||||
</VCardText>
|
||||
|
||||
<VCardText v-if="dataList.length > 0">
|
||||
<VTabs v-model="activeTab" show-arrows class="v-tabs-pill" stacked>
|
||||
<VTab
|
||||
v-for="media in mediaList"
|
||||
@@ -185,16 +313,34 @@ onUnmounted(() => {
|
||||
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
|
||||
<VWindowItem v-for="media in mediaList" :value="media.title_year">
|
||||
<VList>
|
||||
<VListItem v-for="task in activeTasks">
|
||||
<VListItem v-for="task in getTasksByMedia(media.title_year || '')" :key="task.fileitem.path">
|
||||
<VListItemTitle>{{ task.fileitem.name }}</VListItemTitle>
|
||||
<VListItemSubtitle>
|
||||
<VListItemSubtitle class="py-1">
|
||||
{{ t('dialog.transferQueue.sizeTitle') }}:{{ formatFileSize(task.fileitem.size || 0) }}
|
||||
<VChip size="small" :color="getStateColor(task.state)" class="ms-2">
|
||||
<VChip size="small" :color="getStateColor(task.state)" class="mx-2">
|
||||
{{ stateDict[task.state] }}
|
||||
</VChip>
|
||||
</VListItemSubtitle>
|
||||
|
||||
<!-- 文件进度显示 -->
|
||||
<div v-if="task.state === 'running' && getFileProgress(task.fileitem.path).enable" class="mt-2">
|
||||
<VProgressLinear
|
||||
:model-value="getFileProgress(task.fileitem.path).value"
|
||||
color="success"
|
||||
class="mb-1"
|
||||
:height="3"
|
||||
/>
|
||||
<div class="text-xs text-medium-emphasis text-center">
|
||||
{{ getFileProgress(task.fileitem.path).value.toFixed(1) }}%
|
||||
</div>
|
||||
</div>
|
||||
<template #append>
|
||||
<IconBtn size="small" icon="mdi-cancel" @click="remove_queue_task(task.fileitem)" />
|
||||
<IconBtn
|
||||
size="small"
|
||||
icon="mdi-cancel"
|
||||
@click="remove_queue_task(task.fileitem)"
|
||||
:disabled="task.state === 'completed'"
|
||||
/>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
@@ -202,5 +348,5 @@ onUnmounted(() => {
|
||||
</VWindow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -115,7 +115,7 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog width="40rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardItem>
|
||||
@@ -147,5 +147,5 @@ onUnmounted(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -366,7 +366,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper scrollable max-width="40rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable max-width="40rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem :class="props.oper === 'add' ? 'py-3' : 'py-2'">
|
||||
<template #prepend>
|
||||
@@ -619,5 +619,5 @@ onMounted(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -4,7 +4,6 @@ import api from '@/api'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -134,7 +133,7 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper width="40rem" scrollable>
|
||||
<VDialog width="40rem" scrollable>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
@@ -179,5 +178,5 @@ onMounted(async () => {
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -197,7 +197,7 @@ const isMacOS = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper scrollable fullscreen :scrim="false" transition="dialog-bottom-transition">
|
||||
<VDialog scrollable fullscreen :scrim="false" transition="dialog-bottom-transition">
|
||||
<VCard class="workflow-dialog">
|
||||
<!-- Toolbar -->
|
||||
<VToolbar color="primary" density="comfortable">
|
||||
@@ -256,7 +256,7 @@ const isMacOS = computed(() => {
|
||||
@close="importCodeDialog = false"
|
||||
@save="saveCodeString"
|
||||
/>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -182,7 +182,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper scrollable :close-on-back="false" eager max-width="30rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable :close-on-back="false" eager max-width="30rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
@@ -269,5 +269,5 @@ onMounted(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -68,7 +68,7 @@ const $toast = useToast()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogWrapper scrollable max-width="30rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog scrollable max-width="30rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
@@ -132,5 +132,5 @@ const $toast = useToast()
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -749,7 +749,7 @@ onMounted(() => {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<!-- 重命名弹窗 -->
|
||||
<DialogWrapper v-if="renamePopper" v-model="renamePopper" max-width="35rem">
|
||||
<VDialog v-if="renamePopper" v-model="renamePopper" max-width="35rem">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<template #prepend>
|
||||
@@ -783,7 +783,7 @@ onMounted(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
<!-- 文件整理弹窗 -->
|
||||
<ReorganizeDialog
|
||||
v-if="transferPopper"
|
||||
|
||||
@@ -166,7 +166,7 @@ const sortIcon = computed(() => {
|
||||
<VIcon icon="mdi-arrow-up-bold-outline" />
|
||||
</IconBtn>
|
||||
<!-- 新建文件夹 -->
|
||||
<DialogWrapper v-model="newFolderPopper" max-width="35rem">
|
||||
<VDialog v-model="newFolderPopper" max-width="35rem">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn>
|
||||
<VIcon v-bind="props" icon="mdi-folder-plus-outline" />
|
||||
@@ -191,6 +191,6 @@ const sortIcon = computed(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</VToolbar>
|
||||
</template>
|
||||
|
||||
@@ -22,22 +22,39 @@ export function useBackgroundOptimization() {
|
||||
backgroundCloseDelay?: number
|
||||
reconnectDelay?: number
|
||||
maxReconnectAttempts?: number
|
||||
connectDelay?: number // 新增:连接延迟
|
||||
},
|
||||
) => {
|
||||
const manager = sseManagerSingleton.getManager(url, options)
|
||||
// 使用独立的SSE管理器,确保每个监听器都有独立的连接
|
||||
const manager = sseManagerSingleton.getIndependentManager(url, listenerId, 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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +102,8 @@ export function useBackgroundOptimization() {
|
||||
delay: number = 3000,
|
||||
options?: Parameters<typeof useSSE>[3],
|
||||
) => {
|
||||
const manager = sseManagerSingleton.getManager(url, options)
|
||||
// 使用独立的SSE管理器,确保每个监听器都有独立的连接
|
||||
const manager = sseManagerSingleton.getIndependentManager(url, listenerId, options)
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
@@ -117,7 +135,8 @@ export function useBackgroundOptimization() {
|
||||
listenerId: string,
|
||||
isActive: Ref<boolean>,
|
||||
) => {
|
||||
const manager = sseManagerSingleton.getManager(url, {
|
||||
// 使用独立的SSE管理器,确保每个监听器都有独立的连接
|
||||
const manager = sseManagerSingleton.getIndependentManager(url, listenerId, {
|
||||
backgroundCloseDelay: 1000, // 进度SSE更快关闭
|
||||
reconnectDelay: 1000,
|
||||
maxReconnectAttempts: 5,
|
||||
|
||||
@@ -1,383 +0,0 @@
|
||||
import { ref, watch, onBeforeUnmount, readonly } from 'vue'
|
||||
|
||||
/**
|
||||
* 滚动锁定 Composable
|
||||
*
|
||||
* 使用示例:
|
||||
*
|
||||
* // 基本用法
|
||||
* const { isLocked, lockScroll, restoreScroll } = useScrollLock()
|
||||
*
|
||||
* // 带配置的用法
|
||||
* const { isLocked, lockScroll, restoreScroll } = useScrollLock({
|
||||
* preventTouchScroll: true,
|
||||
* preserveScrollPosition: true,
|
||||
* allowScrollSelectors: ['.my-modal', '.scrollable-content'],
|
||||
* allowScrollContainerSelectors: ['.modal-content'],
|
||||
* customScrollCheck: (element) => {
|
||||
* // 自定义逻辑
|
||||
* return element.classList.contains('allow-scroll')
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* // 自动监听版本
|
||||
* const { isLocked, lockScroll, restoreScroll } = useScrollLockWithWatch(
|
||||
* showModal, // 响应式布尔值
|
||||
* {
|
||||
* allowScrollSelectors: ['.modal-content'],
|
||||
* allowScrollContainerSelectors: ['.scrollable-area']
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
|
||||
// 滚动锁定配置
|
||||
export interface ScrollLockOptions {
|
||||
// 是否在组件卸载时自动恢复滚动
|
||||
autoRestore?: boolean
|
||||
// 是否保存和恢复滚动位置
|
||||
preserveScrollPosition?: boolean
|
||||
// 是否阻止触摸事件穿透
|
||||
preventTouchScroll?: boolean
|
||||
// 自定义锁定时的样式
|
||||
lockStyles?: {
|
||||
overflow?: string
|
||||
position?: string
|
||||
width?: string
|
||||
}
|
||||
// 允许滚动的选择器列表(CSS选择器)
|
||||
// 例如:['.my-modal', '.scrollable-content']
|
||||
allowScrollSelectors?: string[]
|
||||
// 允许滚动的容器选择器列表(CSS选择器)
|
||||
// 这些容器内的可滚动元素将被允许滚动
|
||||
// 例如:['.modal-content', '.scroll-container']
|
||||
allowScrollContainerSelectors?: string[]
|
||||
// 自定义滚动检查函数
|
||||
// 返回 true 表示允许滚动,false 表示阻止滚动
|
||||
customScrollCheck?: (element: Element) => boolean
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
const DEFAULT_OPTIONS: Required<
|
||||
Omit<ScrollLockOptions, 'allowScrollSelectors' | 'allowScrollContainerSelectors' | 'customScrollCheck'>
|
||||
> = {
|
||||
autoRestore: true,
|
||||
preserveScrollPosition: true,
|
||||
preventTouchScroll: true,
|
||||
lockStyles: {
|
||||
overflow: 'hidden',
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
},
|
||||
}
|
||||
|
||||
// 全局状态管理
|
||||
const globalLockCount = ref(0)
|
||||
const globalOriginalStyles = ref<{
|
||||
body: { [key: string]: string }
|
||||
documentElement: { [key: string]: string }
|
||||
html: { [key: string]: string }
|
||||
} | null>(null)
|
||||
const globalSavedScrollPosition = ref(0)
|
||||
const globalTouchEventListeners = new Set<(event: TouchEvent) => void>()
|
||||
|
||||
// 保存全局原始样式(只在第一次锁定时保存)
|
||||
const saveGlobalOriginalStyles = () => {
|
||||
if (globalOriginalStyles.value === null) {
|
||||
globalOriginalStyles.value = {
|
||||
body: {
|
||||
overflow: document.body.style.overflow,
|
||||
},
|
||||
documentElement: {
|
||||
overflow: document.documentElement.style.overflow,
|
||||
},
|
||||
html: {
|
||||
overflow: document.documentElement.style.overflow,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存全局滚动位置(只在第一次锁定时保存)
|
||||
const saveGlobalScrollPosition = () => {
|
||||
if (globalLockCount.value === 0) {
|
||||
globalSavedScrollPosition.value =
|
||||
window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
|
||||
}
|
||||
}
|
||||
|
||||
// 应用全局锁定样式
|
||||
const applyGlobalLockStyles = (config: any) => {
|
||||
if (globalLockCount.value === 1) {
|
||||
// 第一次锁定时应用样式
|
||||
document.body.style.overflow = config.lockStyles.overflow || 'hidden'
|
||||
document.documentElement.style.overflow = config.lockStyles.overflow || 'hidden'
|
||||
document.documentElement.classList.add('v-overlay-scroll-blocked')
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复全局样式(只在最后一个锁定时恢复)
|
||||
const restoreGlobalStyles = (config: any) => {
|
||||
if (globalLockCount.value === 0 && globalOriginalStyles.value) {
|
||||
// 最后一个锁定时恢复样式
|
||||
document.body.style.overflow = globalOriginalStyles.value.body.overflow || ''
|
||||
document.documentElement.style.overflow = globalOriginalStyles.value.documentElement.overflow || ''
|
||||
|
||||
// 移除 CSS 类名
|
||||
document.documentElement.classList.remove('v-overlay-scroll-blocked')
|
||||
|
||||
// 重置全局状态
|
||||
globalOriginalStyles.value = null
|
||||
globalSavedScrollPosition.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 添加全局触摸事件监听器
|
||||
const addGlobalTouchEventListener = (listener: (event: TouchEvent) => void) => {
|
||||
globalTouchEventListeners.add(listener)
|
||||
if (globalTouchEventListeners.size === 1) {
|
||||
// 第一次添加监听器时绑定到document
|
||||
document.addEventListener('touchmove', listener, { passive: false })
|
||||
}
|
||||
}
|
||||
|
||||
// 移除全局触摸事件监听器
|
||||
const removeGlobalTouchEventListener = (listener: (event: TouchEvent) => void) => {
|
||||
globalTouchEventListeners.delete(listener)
|
||||
if (globalTouchEventListeners.size === 0) {
|
||||
// 最后一个监听器被移除时解绑
|
||||
document.removeEventListener('touchmove', listener)
|
||||
}
|
||||
}
|
||||
|
||||
export function useScrollLock(options: ScrollLockOptions = {}) {
|
||||
const config = {
|
||||
...DEFAULT_OPTIONS,
|
||||
allowScrollSelectors: options.allowScrollSelectors || [],
|
||||
allowScrollContainerSelectors: options.allowScrollContainerSelectors || [],
|
||||
customScrollCheck: options.customScrollCheck,
|
||||
...options,
|
||||
}
|
||||
|
||||
// 状态管理
|
||||
const isLocked = ref(false)
|
||||
const savedScrollPosition = ref(0)
|
||||
|
||||
// 保存当前滚动位置
|
||||
const saveScrollPosition = () => {
|
||||
if (config.preserveScrollPosition) {
|
||||
savedScrollPosition.value =
|
||||
window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
|
||||
}
|
||||
}
|
||||
|
||||
// 检查元素是否应该允许滚动
|
||||
const shouldAllowScroll = (element: Element): boolean => {
|
||||
// 1. 检查是否匹配允许滚动的选择器
|
||||
for (const selector of config.allowScrollSelectors) {
|
||||
if (element.matches(selector) || element.closest(selector)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查是否在允许滚动的容器内
|
||||
for (const selector of config.allowScrollContainerSelectors) {
|
||||
const container = element.closest(selector)
|
||||
if (container) {
|
||||
// 检查容器是否可滚动
|
||||
const style = getComputedStyle(container)
|
||||
const isScrollable =
|
||||
container.scrollHeight > container.clientHeight &&
|
||||
style.overflow !== 'hidden' &&
|
||||
(style.overflow === 'auto' ||
|
||||
style.overflow === 'scroll' ||
|
||||
style.overflowY === 'auto' ||
|
||||
style.overflowY === 'scroll')
|
||||
if (isScrollable) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查是否在弹窗、菜单或其他覆盖层内
|
||||
const isInDialog = element.closest(
|
||||
'.v-dialog, .v-menu, .v-bottom-sheet, .v-snackbar, [role="dialog"], .v-overlay__content',
|
||||
)
|
||||
|
||||
// 4. 检查是否是可滚动的内容区域
|
||||
const isScrollableContent = element.closest(
|
||||
'.v-card-text, .v-list, .v-table__wrapper, .v-data-table__wrapper, .v-sheet, .v-card__content, .v-data-table, .v-table',
|
||||
)
|
||||
|
||||
// 5. 检查是否在可滚动的容器内
|
||||
const scrollableContainer = element.closest('[style*="overflow"], [class*="overflow"]')
|
||||
const isInScrollableContainer =
|
||||
scrollableContainer &&
|
||||
(scrollableContainer.scrollHeight > scrollableContainer.clientHeight ||
|
||||
getComputedStyle(scrollableContainer).overflow !== 'hidden')
|
||||
|
||||
// 6. 使用自定义检查函数
|
||||
if (config.customScrollCheck && config.customScrollCheck(element)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 如果不在弹窗内且不是可滚动内容且不在可滚动容器内,则不允许滚动
|
||||
return !!(isInDialog || isScrollableContent || isInScrollableContainer)
|
||||
}
|
||||
|
||||
// 阻止触摸滚动事件
|
||||
const preventTouchScroll = (event: TouchEvent) => {
|
||||
if (isLocked.value && config.preventTouchScroll) {
|
||||
// 检查触摸事件的目标元素
|
||||
const target = event.target as Element
|
||||
if (target) {
|
||||
// 如果元素应该允许滚动,则不阻止事件
|
||||
if (shouldAllowScroll(target)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 否则阻止滚动
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
// 锁定滚动
|
||||
const lockScroll = () => {
|
||||
if (isLocked.value) return
|
||||
|
||||
// 增加全局锁定计数
|
||||
globalLockCount.value++
|
||||
|
||||
// 保存当前状态(只在第一次锁定时)
|
||||
if (globalLockCount.value === 1) {
|
||||
saveGlobalOriginalStyles()
|
||||
saveGlobalScrollPosition()
|
||||
}
|
||||
|
||||
// 应用锁定样式
|
||||
applyGlobalLockStyles(config)
|
||||
|
||||
// 添加触摸事件监听器
|
||||
if (config.preventTouchScroll) {
|
||||
addGlobalTouchEventListener(preventTouchScroll)
|
||||
}
|
||||
|
||||
isLocked.value = true
|
||||
}
|
||||
|
||||
// 恢复滚动
|
||||
const restoreScroll = () => {
|
||||
if (!isLocked.value) return
|
||||
|
||||
// 减少全局锁定计数
|
||||
globalLockCount.value--
|
||||
|
||||
// 移除触摸事件监听器
|
||||
if (config.preventTouchScroll) {
|
||||
removeGlobalTouchEventListener(preventTouchScroll)
|
||||
}
|
||||
|
||||
// 恢复样式(只在最后一个锁定时)
|
||||
restoreGlobalStyles(config)
|
||||
|
||||
isLocked.value = false
|
||||
}
|
||||
|
||||
// 切换滚动锁定状态
|
||||
const toggleScrollLock = (lock?: boolean) => {
|
||||
const shouldLock = lock !== undefined ? lock : !isLocked.value
|
||||
|
||||
if (shouldLock) {
|
||||
lockScroll()
|
||||
} else {
|
||||
restoreScroll()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听响应式值的变化
|
||||
const watchTarget = (target: any) => {
|
||||
return watch(
|
||||
target,
|
||||
newValue => {
|
||||
toggleScrollLock(!!newValue)
|
||||
},
|
||||
{ immediate: false },
|
||||
)
|
||||
}
|
||||
|
||||
// 生命周期清理
|
||||
onBeforeUnmount(() => {
|
||||
if (config.autoRestore && isLocked.value) {
|
||||
restoreScroll()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
// 状态
|
||||
isLocked: readonly(isLocked),
|
||||
savedScrollPosition: readonly(savedScrollPosition),
|
||||
|
||||
// 方法
|
||||
lockScroll,
|
||||
restoreScroll,
|
||||
toggleScrollLock,
|
||||
watchTarget,
|
||||
|
||||
// 工具方法
|
||||
saveScrollPosition,
|
||||
}
|
||||
}
|
||||
|
||||
// 便捷的自动监听版本
|
||||
export function useScrollLockWithWatch(target: any, options: ScrollLockOptions = {}) {
|
||||
const scrollLock = useScrollLock(options)
|
||||
|
||||
// 自动监听目标值的变化
|
||||
const stopWatcher = scrollLock.watchTarget(target)
|
||||
|
||||
// 返回所有功能 + 停止监听的方法
|
||||
return {
|
||||
...scrollLock,
|
||||
stopWatcher,
|
||||
}
|
||||
}
|
||||
|
||||
// 全局弹窗检测和管理
|
||||
export function useGlobalDialogScrollLock() {
|
||||
const activeDialogs = ref<Set<string>>(new Set())
|
||||
|
||||
const registerDialog = (dialogId: string) => {
|
||||
activeDialogs.value.add(dialogId)
|
||||
if (activeDialogs.value.size === 1) {
|
||||
// 第一个弹窗时锁定滚动
|
||||
lockGlobalScroll()
|
||||
}
|
||||
}
|
||||
|
||||
const unregisterDialog = (dialogId: string) => {
|
||||
activeDialogs.value.delete(dialogId)
|
||||
if (activeDialogs.value.size === 0) {
|
||||
// 没有弹窗时恢复滚动
|
||||
unlockGlobalScroll()
|
||||
}
|
||||
}
|
||||
|
||||
const lockGlobalScroll = () => {
|
||||
document.body.style.overflow = 'hidden'
|
||||
document.documentElement.classList.add('v-overlay-scroll-blocked')
|
||||
}
|
||||
|
||||
const unlockGlobalScroll = () => {
|
||||
document.body.style.overflow = ''
|
||||
document.documentElement.classList.remove('v-overlay-scroll-blocked')
|
||||
}
|
||||
|
||||
return {
|
||||
activeDialogs: readonly(activeDialogs),
|
||||
registerDialog,
|
||||
unregisterDialog,
|
||||
lockGlobalScroll,
|
||||
unlockGlobalScroll,
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import { useRoute } from 'vue-router'
|
||||
import { filterMenusByPermission } from '@/utils/permission'
|
||||
import { onUnreadMessage } from '@/utils/badge'
|
||||
import { usePullDownGesture } from '@/composables/usePullDownGesture'
|
||||
import { useScrollLockWithWatch } from '@/composables/useScrollLock'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import OfflinePage from '@/layouts/components/OfflinePage.vue'
|
||||
import { useGlobalOfflineStatus } from '@/composables/useOfflineStatus'
|
||||
@@ -163,17 +162,6 @@ const handleServiceWorkerMessage = (event: MessageEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用滚动锁定 composable(自动监听showPluginQuickAccess的变化)
|
||||
useScrollLockWithWatch(showPluginQuickAccess, {
|
||||
preventTouchScroll: true,
|
||||
preserveScrollPosition: true,
|
||||
autoRestore: true,
|
||||
// 允许快速访问面板内的滚动
|
||||
allowScrollSelectors: ['.plugin-quick-access'],
|
||||
// 允许快速访问面板内的可滚动容器
|
||||
allowScrollContainerSelectors: ['.plugin-grid'],
|
||||
})
|
||||
|
||||
// 检查是否可以使用下拉手势
|
||||
const canUsePullGesture = () => {
|
||||
// 检查是否在dashboard页面
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useRecentPlugins } from '@/composables/useRecentPlugins'
|
||||
import PluginDataDialog from '@/components/dialog/PluginDataDialog.vue'
|
||||
import { VCard } from 'vuetify/components'
|
||||
import { getDominantColor } from '@/@core/utils/image'
|
||||
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -212,6 +213,18 @@ watch(
|
||||
if (visible) {
|
||||
fetchPluginsWithPage()
|
||||
loadRecentPlugins()
|
||||
// 禁用背景滚动,但允许面板内部滚动
|
||||
// 注意:参数是要允许滚动的目标元素,即面板本身
|
||||
const panelElement = document.querySelector('.plugin-quick-access')
|
||||
if (panelElement) {
|
||||
disableBodyScroll(panelElement as HTMLElement)
|
||||
}
|
||||
} else {
|
||||
// 恢复背景滚动
|
||||
const panelElement = document.querySelector('.plugin-quick-access')
|
||||
if (panelElement) {
|
||||
enableBodyScroll(panelElement as HTMLElement)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -224,6 +237,14 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 组件卸载时确保恢复背景滚动
|
||||
onUnmounted(() => {
|
||||
const panelElement = document.querySelector('.plugin-quick-access')
|
||||
if (panelElement) {
|
||||
enableBodyScroll(panelElement as HTMLElement)
|
||||
}
|
||||
})
|
||||
|
||||
// 处理触摸开始
|
||||
function handleTouchStart(event: TouchEvent) {
|
||||
if (!props.visible) return
|
||||
|
||||
@@ -50,6 +50,9 @@ const sendButtonDisabled = ref(false)
|
||||
// 消息对话框引用
|
||||
const messageDialogRef = ref<any>(null)
|
||||
|
||||
// 消息视图引用
|
||||
const messageViewRef = ref<any>(null)
|
||||
|
||||
// 滚动容器引用
|
||||
const messageContentRef = ref<any>()
|
||||
|
||||
@@ -115,6 +118,12 @@ async function openMessageDialog() {
|
||||
setTimeout(() => {
|
||||
forceScrollToEnd()
|
||||
}, 600)
|
||||
// 等待对话框打开后恢复SSE连接
|
||||
nextTick(() => {
|
||||
if (messageViewRef.value && typeof messageViewRef.value.resumeSSE === 'function') {
|
||||
messageViewRef.value.resumeSSE()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 智能滚动到底部(只有用户在底部附近时才滚动)
|
||||
@@ -184,6 +193,14 @@ defineExpose({
|
||||
openMessageDialog: openMessageDialogFromExternal,
|
||||
})
|
||||
|
||||
// 监听消息对话框状态变化
|
||||
watch(messageDialog, newValue => {
|
||||
if (!newValue && messageViewRef.value && typeof messageViewRef.value.pauseSSE === 'function') {
|
||||
// 对话框关闭时暂停SSE连接
|
||||
messageViewRef.value.pauseSSE()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const shortcut = getQueryValue('shortcut')
|
||||
if (shortcut) {
|
||||
@@ -248,7 +265,7 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VMenu>
|
||||
<!-- 名称测试弹窗 -->
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="nameTestDialog"
|
||||
v-model="nameTestDialog"
|
||||
max-width="45rem"
|
||||
@@ -268,9 +285,9 @@ onMounted(() => {
|
||||
<NameTestView />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
<!-- 网络测试弹窗 -->
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="netTestDialog"
|
||||
v-model="netTestDialog"
|
||||
max-width="35rem"
|
||||
@@ -290,9 +307,9 @@ onMounted(() => {
|
||||
<NetTestView />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
<!-- 实时日志弹窗 -->
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="loggingDialog"
|
||||
v-model="loggingDialog"
|
||||
scrollable
|
||||
@@ -318,9 +335,9 @@ onMounted(() => {
|
||||
<LoggingView logfile="moviepilot.log" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
<!-- 过滤规则弹窗 -->
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="ruleTestDialog"
|
||||
v-model="ruleTestDialog"
|
||||
max-width="35rem"
|
||||
@@ -340,9 +357,9 @@ onMounted(() => {
|
||||
<RuleTestView />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
<!-- 系统健康检查弹窗 -->
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="systemTestDialog"
|
||||
v-model="systemTestDialog"
|
||||
max-width="35rem"
|
||||
@@ -362,9 +379,9 @@ onMounted(() => {
|
||||
<ModuleTestView />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
<!-- 消息中心弹窗 -->
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="messageDialog"
|
||||
v-model="messageDialog"
|
||||
max-width="50rem"
|
||||
@@ -407,5 +424,5 @@ onMounted(() => {
|
||||
</div>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -650,7 +650,7 @@ onUnmounted(() => {
|
||||
<!-- 用户认证对话框 -->
|
||||
<UserAuthDialog v-if="siteAuthDialog" v-model="siteAuthDialog" @done="siteAuthDone" @close="siteAuthDialog = false" />
|
||||
<!-- 自定义 CSS -->
|
||||
<DialogWrapper v-if="cssDialog" v-model="cssDialog" max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog v-if="cssDialog" v-model="cssDialog" max-width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
@@ -671,10 +671,10 @@ onUnmounted(() => {
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
|
||||
<!-- 透明度调整对话框 -->
|
||||
<DialogWrapper v-if="showTransparencyDialog" v-model="showTransparencyDialog" max-width="30rem">
|
||||
<VDialog v-if="showTransparencyDialog" v-model="showTransparencyDialog" max-width="30rem">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
@@ -763,7 +763,7 @@ onUnmounted(() => {
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -833,6 +833,23 @@ export default {
|
||||
notStarted: 'Not Started',
|
||||
pending: 'Pending',
|
||||
paused: 'Paused',
|
||||
selectedCount: 'Selected {count}/{total} items',
|
||||
noSelectedItems: 'Please select subscriptions to operate',
|
||||
batchEnable: 'Batch Enable',
|
||||
batchPause: 'Batch Pause',
|
||||
batchDelete: 'Batch Delete',
|
||||
batchEnableConfirm: 'Are you sure you want to enable {count} selected subscriptions?',
|
||||
batchPauseConfirm: 'Are you sure you want to pause {count} selected subscriptions?',
|
||||
batchDeleteConfirm: 'Are you sure you want to delete {count} selected subscriptions? This action cannot be undone!',
|
||||
batchEnableSuccess: 'Successfully enabled {count} subscriptions',
|
||||
batchPauseSuccess: 'Successfully paused {count} subscriptions',
|
||||
batchDeleteSuccess: 'Successfully deleted {count} subscriptions',
|
||||
batchEnableFailed: 'Failed to enable {count} subscriptions',
|
||||
batchPauseFailed: 'Failed to pause {count} subscriptions',
|
||||
batchDeleteFailed: 'Failed to delete {count} subscriptions',
|
||||
batchEnableError: 'Batch enable operation failed',
|
||||
batchPauseError: 'Batch pause operation failed',
|
||||
batchDeleteError: 'Batch delete operation failed',
|
||||
},
|
||||
recommend: {
|
||||
all: 'All',
|
||||
@@ -1007,6 +1024,7 @@ export default {
|
||||
limitSeconds: 'Access Interval (seconds)',
|
||||
useProxy: 'Use Proxy',
|
||||
browserSimulation: 'Browser Simulation',
|
||||
selectFile: 'Select File',
|
||||
},
|
||||
hints: {
|
||||
url: 'Format: http://www.example.com/',
|
||||
@@ -1024,19 +1042,48 @@ export default {
|
||||
limitSeconds: 'Minimum interval between each access',
|
||||
useProxy: 'Use proxy server to access this site',
|
||||
browserSimulation: 'Use browser simulation for authentic site access',
|
||||
import: 'Batch import site data, supports JSON format files',
|
||||
selectFile: 'Select JSON file',
|
||||
dragDropFile: 'Drag and drop file here or click to select file',
|
||||
supportedFormat: 'Supports JSON format site configuration files',
|
||||
},
|
||||
actions: {
|
||||
add: 'Add Site',
|
||||
edit: 'Edit Site',
|
||||
import: 'Import',
|
||||
export: 'Export',
|
||||
startImport: 'Start Import',
|
||||
},
|
||||
messages: {
|
||||
addSuccess: 'Site added successfully',
|
||||
addFailed: 'Failed to add site',
|
||||
updateSuccess: 'Updated successfully',
|
||||
updateFailed: 'Update failed',
|
||||
exportSuccess: 'Sites exported successfully',
|
||||
exportFailed: 'Failed to export sites',
|
||||
importSuccess: 'Successfully imported {count} sites',
|
||||
importFailed: 'Failed to import sites',
|
||||
importPartialFailed: 'Import completed, {success} successful, {failed} failed',
|
||||
importAllFailed: 'Import failed, all {count} sites failed to import',
|
||||
noDataToImport: 'No data to import',
|
||||
noValidData: 'No valid data',
|
||||
someInvalidData: 'Some data is invalid, valid data: {valid}/{total}',
|
||||
invalidFileType: 'Unsupported file type, please select a JSON file',
|
||||
invalidFileFormat: 'Invalid file format, please check file content',
|
||||
parseFileError: 'Failed to parse file, please check file format',
|
||||
previewData: 'Preview data ({count} sites)',
|
||||
importing: 'Importing... ({progress}%)',
|
||||
importErrors: 'Import encountered {count} errors',
|
||||
},
|
||||
errors: {
|
||||
loadDownloader: 'Failed to load downloader settings',
|
||||
title: 'Import Error Details',
|
||||
failed: 'Import Failed',
|
||||
details: 'Error Details',
|
||||
},
|
||||
results: {
|
||||
successTitle: 'Successfully Imported Sites',
|
||||
success: 'Import Success',
|
||||
},
|
||||
testConnectivity: 'Test Connectivity',
|
||||
testing: 'Testing ...',
|
||||
@@ -1053,6 +1100,28 @@ 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',
|
||||
preview: {
|
||||
title: 'Preview Sites',
|
||||
showing: 'Showing {count}/{total}',
|
||||
unnamed: 'Unnamed Site',
|
||||
noUrl: 'No Site URL',
|
||||
invalid: 'Invalid Data',
|
||||
},
|
||||
},
|
||||
message: {
|
||||
loadMore: 'Load More',
|
||||
@@ -1064,6 +1133,7 @@ export default {
|
||||
program: 'Program',
|
||||
content: 'Content',
|
||||
refreshing: 'Refreshing',
|
||||
initializing: 'Initializing',
|
||||
},
|
||||
moduleTest: {
|
||||
normal: 'Normal',
|
||||
@@ -1192,7 +1262,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',
|
||||
@@ -1353,7 +1423,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',
|
||||
@@ -2015,6 +2090,10 @@ export default {
|
||||
startAll: 'Start All',
|
||||
refresh: 'Refresh',
|
||||
close: 'Close',
|
||||
processingFile: 'Processing',
|
||||
overallProgress: 'Overall Progress',
|
||||
currentFileProgress: 'Current File Progress',
|
||||
processingStatus: 'Processing',
|
||||
},
|
||||
reorganize: {
|
||||
title: 'Organize',
|
||||
|
||||
@@ -829,6 +829,23 @@ export default {
|
||||
notStarted: '未开始',
|
||||
pending: '待定',
|
||||
paused: '暂停',
|
||||
selectedCount: '已选择 {count}/{total} 项',
|
||||
noSelectedItems: '请先选择要操作的订阅',
|
||||
batchEnable: '批量启用',
|
||||
batchPause: '批量暂停',
|
||||
batchDelete: '批量删除',
|
||||
batchEnableConfirm: '确定要启用选中的 {count} 个订阅吗?',
|
||||
batchPauseConfirm: '确定要暂停选中的 {count} 个订阅吗?',
|
||||
batchDeleteConfirm: '确定要删除选中的 {count} 个订阅吗?此操作不可恢复!',
|
||||
batchEnableSuccess: '成功启用 {count} 个订阅',
|
||||
batchPauseSuccess: '成功暂停 {count} 个订阅',
|
||||
batchDeleteSuccess: '成功删除 {count} 个订阅',
|
||||
batchEnableFailed: '启用失败 {count} 个订阅',
|
||||
batchPauseFailed: '暂停失败 {count} 个订阅',
|
||||
batchDeleteFailed: '删除失败 {count} 个订阅',
|
||||
batchEnableError: '批量启用操作失败',
|
||||
batchPauseError: '批量暂停操作失败',
|
||||
batchDeleteError: '批量删除操作失败',
|
||||
},
|
||||
recommend: {
|
||||
all: '全部',
|
||||
@@ -1003,6 +1020,7 @@ export default {
|
||||
limitSeconds: '访问间隔(秒)',
|
||||
useProxy: '使用代理访问',
|
||||
browserSimulation: '浏览器仿真',
|
||||
selectFile: '选择文件',
|
||||
},
|
||||
hints: {
|
||||
url: '格式:http://www.example.com/',
|
||||
@@ -1020,19 +1038,48 @@ export default {
|
||||
limitSeconds: '每次访问需要间隔的最小时间',
|
||||
useProxy: '使用代理服务器访问该站点',
|
||||
browserSimulation: '使用浏览器模拟真实访问该站点',
|
||||
import: '批量导入站点数据,支持JSON格式文件',
|
||||
selectFile: '选择JSON文件',
|
||||
dragDropFile: '拖拽文件到此处或点击选择文件',
|
||||
supportedFormat: '支持JSON格式的站点配置文件',
|
||||
},
|
||||
actions: {
|
||||
add: '新增站点',
|
||||
edit: '编辑站点',
|
||||
import: '导入',
|
||||
export: '导出',
|
||||
startImport: '开始导入',
|
||||
},
|
||||
messages: {
|
||||
addSuccess: '新增站点成功',
|
||||
addFailed: '新增站点失败',
|
||||
updateSuccess: '更新成功',
|
||||
updateFailed: '更新失败',
|
||||
exportSuccess: '站点导出成功',
|
||||
exportFailed: '站点导出失败',
|
||||
importSuccess: '成功导入 {count} 个站点',
|
||||
importFailed: '站点导入失败',
|
||||
importPartialFailed: '导入完成,成功 {success} 个,失败 {failed} 个',
|
||||
importAllFailed: '导入失败,{count} 个站点全部导入失败',
|
||||
noDataToImport: '没有数据可导入',
|
||||
noValidData: '没有有效的数据',
|
||||
someInvalidData: '部分数据无效,有效数据 {valid}/{total} 个',
|
||||
invalidFileType: '不支持的文件类型,请选择JSON文件',
|
||||
invalidFileFormat: '文件格式无效,请检查文件内容',
|
||||
parseFileError: '文件解析失败,请检查文件格式',
|
||||
previewData: '预览数据 ({count} 个站点)',
|
||||
importing: '正在导入... ({progress}%)',
|
||||
importErrors: '导入过程中出现 {count} 个错误',
|
||||
},
|
||||
errors: {
|
||||
loadDownloader: '加载下载器设置失败',
|
||||
title: '导入错误详情',
|
||||
failed: '导入失败',
|
||||
details: '错误详情',
|
||||
},
|
||||
results: {
|
||||
successTitle: '成功导入的站点',
|
||||
success: '导入成功',
|
||||
},
|
||||
testConnectivity: '测试连通性',
|
||||
testing: '测试中 ...',
|
||||
@@ -1049,6 +1096,28 @@ export default {
|
||||
deleteSite: '删除站点',
|
||||
updateCookie: '更新Cookie',
|
||||
viewUserData: '查看用户数据',
|
||||
statistics: '统计信息',
|
||||
totalSites: '总站点数',
|
||||
normalSites: '正常站点',
|
||||
slowSites: '缓慢站点',
|
||||
failedSites: '失败站点',
|
||||
averageTime: '平均耗时',
|
||||
successRate: '成功率',
|
||||
successCount: '成功次数',
|
||||
failCount: '失败次数',
|
||||
lastAccess: '最后访问',
|
||||
timeRecords: '耗时记录',
|
||||
recentTimeRecords: '最近耗时记录',
|
||||
accessTime: '访问时间',
|
||||
responseTime: '响应时间',
|
||||
noTimeRecords: '暂无耗时记录',
|
||||
preview: {
|
||||
title: '预览站点',
|
||||
showing: '显示 {count}/{total}',
|
||||
unnamed: '未命名站点',
|
||||
noUrl: '无站点地址',
|
||||
invalid: '数据无效',
|
||||
},
|
||||
},
|
||||
message: {
|
||||
loadMore: '加载更多',
|
||||
@@ -1060,6 +1129,7 @@ export default {
|
||||
program: '程序',
|
||||
content: '内容',
|
||||
refreshing: '正在刷新',
|
||||
initializing: '正在初始化',
|
||||
},
|
||||
moduleTest: {
|
||||
normal: '正常',
|
||||
@@ -1187,7 +1257,7 @@ export default {
|
||||
workflowStatisticShareHint: '分享工作流统计数据到热门工作流,供其他MPer参考',
|
||||
bigMemoryMode: '大内存模式',
|
||||
bigMemoryModeHint: '使用更大的内存缓存数据,提升系统性能',
|
||||
dbWalEnable: 'WAL模式',
|
||||
dbWalEnable: '数据库WAL模式',
|
||||
dbWalEnableHint: '可提升读写并发性能,但可能在异常情况下增加数据丢失风险,更改后需重启生效',
|
||||
tmdbApiDomain: 'TMDB API服务地址',
|
||||
tmdbApiDomainPlaceholder: 'api.themoviedb.org',
|
||||
@@ -1341,6 +1411,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: '阅读站点消息',
|
||||
@@ -1989,6 +2064,10 @@ export default {
|
||||
startAll: '全部开始',
|
||||
refresh: '刷新',
|
||||
close: '关闭',
|
||||
processingFile: '正在整理',
|
||||
overallProgress: '整体进度',
|
||||
currentFileProgress: '当前文件进度',
|
||||
processingStatus: '整理中',
|
||||
},
|
||||
reorganize: {
|
||||
title: '整理',
|
||||
|
||||
@@ -827,6 +827,23 @@ export default {
|
||||
notStarted: '未開始',
|
||||
pending: '待定',
|
||||
paused: '暫停',
|
||||
selectedCount: '已選擇 {count}/{total} 項',
|
||||
noSelectedItems: '請先選擇要操作的訂閱',
|
||||
batchEnable: '批量啟用',
|
||||
batchPause: '批量暫停',
|
||||
batchDelete: '批量刪除',
|
||||
batchEnableConfirm: '確定要啟用選中的 {count} 個訂閱嗎?',
|
||||
batchPauseConfirm: '確定要暫停選中的 {count} 個訂閱嗎?',
|
||||
batchDeleteConfirm: '確定要刪除選中的 {count} 個訂閱嗎?此操作不可恢復!',
|
||||
batchEnableSuccess: '成功啟用 {count} 個訂閱',
|
||||
batchPauseSuccess: '成功暫停 {count} 個訂閱',
|
||||
batchDeleteSuccess: '成功刪除 {count} 個訂閱',
|
||||
batchEnableFailed: '啟用失敗 {count} 個訂閱',
|
||||
batchPauseFailed: '暫停失敗 {count} 個訂閱',
|
||||
batchDeleteFailed: '刪除失敗 {count} 個訂閱',
|
||||
batchEnableError: '批量啟用操作失敗',
|
||||
batchPauseError: '批量暫停操作失敗',
|
||||
batchDeleteError: '批量刪除操作失敗',
|
||||
},
|
||||
recommend: {
|
||||
all: '全部',
|
||||
@@ -1002,6 +1019,7 @@ export default {
|
||||
limitSeconds: '訪問間隔(秒)',
|
||||
useProxy: '使用代理訪問',
|
||||
browserSimulation: '瀏覽器仿真',
|
||||
selectFile: '選擇文件',
|
||||
},
|
||||
hints: {
|
||||
url: '格式:http://www.example.com/',
|
||||
@@ -1019,19 +1037,48 @@ export default {
|
||||
limitSeconds: '每次訪問需要間隔的最小時間',
|
||||
useProxy: '使用代理服務器訪問該站點',
|
||||
browserSimulation: '使用瀏覽器模擬真實訪問該站點',
|
||||
import: '批量導入站點數據,支持JSON格式文件',
|
||||
selectFile: '選擇JSON文件',
|
||||
dragDropFile: '拖拽文件到此處或點擊選擇文件',
|
||||
supportedFormat: '支持JSON格式的站點配置文件',
|
||||
},
|
||||
actions: {
|
||||
add: '新增站點',
|
||||
edit: '編輯站點',
|
||||
import: '導入',
|
||||
export: '導出',
|
||||
startImport: '開始導入',
|
||||
},
|
||||
messages: {
|
||||
addSuccess: '新增站點成功',
|
||||
addFailed: '新增站點失敗',
|
||||
updateSuccess: '更新成功',
|
||||
updateFailed: '更新失敗',
|
||||
exportSuccess: '站點導出成功',
|
||||
exportFailed: '站點導出失敗',
|
||||
importSuccess: '成功導入 {count} 個站點',
|
||||
importFailed: '站點導入失敗',
|
||||
importPartialFailed: '導入完成,成功 {success} 個,失敗 {failed} 個',
|
||||
importAllFailed: '導入失敗,{count} 個站點全部導入失敗',
|
||||
noDataToImport: '沒有數據可導入',
|
||||
noValidData: '沒有有效的數據',
|
||||
someInvalidData: '部分數據無效,有效數據 {valid}/{total} 個',
|
||||
invalidFileType: '不支持的文件類型,請選擇JSON文件',
|
||||
invalidFileFormat: '文件格式無效,請檢查文件內容',
|
||||
parseFileError: '文件解析失敗,請檢查文件格式',
|
||||
previewData: '預覽數據 ({count} 個站點)',
|
||||
importing: '正在導入... ({progress}%)',
|
||||
importErrors: '導入過程中出現 {count} 個錯誤',
|
||||
},
|
||||
errors: {
|
||||
loadDownloader: '加載下載器設置失敗',
|
||||
title: '導入錯誤詳情',
|
||||
failed: '導入失敗',
|
||||
details: '錯誤詳情',
|
||||
},
|
||||
results: {
|
||||
successTitle: '成功導入的站點',
|
||||
success: '導入成功',
|
||||
},
|
||||
testConnectivity: '測試連通性',
|
||||
testing: '測試中 ...',
|
||||
@@ -1048,6 +1095,28 @@ export default {
|
||||
deleteSite: '刪除站點',
|
||||
updateCookie: '更新Cookie',
|
||||
viewUserData: '查看用戶數據',
|
||||
statistics: '統計信息',
|
||||
totalSites: '總站點數',
|
||||
normalSites: '正常站點',
|
||||
slowSites: '緩慢站點',
|
||||
failedSites: '失敗站點',
|
||||
averageTime: '平均耗時',
|
||||
successRate: '成功率',
|
||||
successCount: '成功次數',
|
||||
failCount: '失敗次數',
|
||||
lastAccess: '最後訪問',
|
||||
timeRecords: '耗時記錄',
|
||||
recentTimeRecords: '最近耗時記錄',
|
||||
accessTime: '訪問時間',
|
||||
responseTime: '響應時間',
|
||||
noTimeRecords: '暫無耗時記錄',
|
||||
preview: {
|
||||
title: '預覽站點',
|
||||
showing: '顯示 {count}/{total}',
|
||||
unnamed: '未命名站點',
|
||||
noUrl: '無站點地址',
|
||||
invalid: '數據無效',
|
||||
},
|
||||
},
|
||||
message: {
|
||||
loadMore: '加載更多',
|
||||
@@ -1059,6 +1128,7 @@ export default {
|
||||
program: '程序',
|
||||
content: '內容',
|
||||
refreshing: '正在刷新',
|
||||
initializing: '正在初始化',
|
||||
},
|
||||
moduleTest: {
|
||||
normal: '正常',
|
||||
@@ -1186,7 +1256,7 @@ export default {
|
||||
workflowStatisticShareHint: '分享工作流統計數據到熱門工作流,供其他MPer參考',
|
||||
bigMemoryMode: '大內存模式',
|
||||
bigMemoryModeHint: '使用更大的內存緩存數據,提升系統性能',
|
||||
dbWalEnable: 'WAL模式',
|
||||
dbWalEnable: '數據庫WAL模式',
|
||||
dbWalEnableHint: '可提升讀寫併發性能,但可能在異常情況下增加數據丟失風險,更改後需重啟生效',
|
||||
tmdbApiDomain: 'TMDB API服務地址',
|
||||
tmdbApiDomainPlaceholder: 'api.themoviedb.org',
|
||||
@@ -1340,6 +1410,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: '閱讀站點消息',
|
||||
@@ -1988,6 +2063,10 @@ export default {
|
||||
startAll: '全部開始',
|
||||
refresh: '刷新',
|
||||
close: '關閉',
|
||||
processingFile: '正在整理',
|
||||
overallProgress: '整體進度',
|
||||
currentFileProgress: '當前文件進度',
|
||||
processingStatus: '整理中',
|
||||
},
|
||||
reorganize: {
|
||||
title: '整理',
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -370,7 +389,7 @@ onDeactivated(() => {
|
||||
</Teleport>
|
||||
|
||||
<!-- 弹窗,根据配置生成选项 -->
|
||||
<DialogWrapper v-if="dialog" v-model="dialog" max-width="35rem" :fullscreen="!display.mdAndUp.value" scrollable>
|
||||
<VDialog v-if="dialog" v-model="dialog" max-width="35rem" :fullscreen="!display.mdAndUp.value" scrollable>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
@@ -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)]
|
||||
@@ -423,7 +443,7 @@ onDeactivated(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.settings-card-header {
|
||||
@@ -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()
|
||||
@@ -199,7 +216,7 @@ onActivated(async () => {
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
<!-- 弹窗,根据配置生成选项 -->
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="orderConfigDialog"
|
||||
v-model="orderConfigDialog"
|
||||
max-width="35rem"
|
||||
@@ -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>
|
||||
@@ -243,7 +265,7 @@ onActivated(async () => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
<!-- 快速滚动到顶部按钮 -->
|
||||
<Teleport to="body" v-if="route.path === '/discover'">
|
||||
<VScrollToTopBtn />
|
||||
@@ -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,8 +139,8 @@ 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)}`;
|
||||
const querySeparator = source.api_path.includes('?') ? '&' : '?'
|
||||
const linkUrl = `/browse/${source.api_path}${querySeparator}title=${encodeURIComponent(source.name)}`
|
||||
viewList.push({
|
||||
apipath: source.api_path,
|
||||
linkurl: linkUrl,
|
||||
@@ -221,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 () => {
|
||||
@@ -250,13 +269,7 @@ onActivated(async () => {
|
||||
</div>
|
||||
|
||||
<!-- 设置面板 -->
|
||||
<DialogWrapper
|
||||
v-model="dialog"
|
||||
width="35rem"
|
||||
class="settings-dialog"
|
||||
scrollable
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
>
|
||||
<VDialog v-model="dialog" width="35rem" class="settings-dialog" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard class="settings-card">
|
||||
<VCardItem class="settings-card-header">
|
||||
<VCardTitle>
|
||||
@@ -275,8 +288,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">
|
||||
@@ -308,7 +321,7 @@ onActivated(async () => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
|
||||
<!-- 快速滚动到顶部按钮 -->
|
||||
<Teleport to="body" v-if="route.path === '/recommend'">
|
||||
@@ -394,7 +407,7 @@ onActivated(async () => {
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
background-color: var(--item-color, #4caf50);
|
||||
block-size: 100%;
|
||||
content: '';
|
||||
inline-size: 4px;
|
||||
@@ -403,19 +416,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);
|
||||
@@ -452,7 +452,7 @@ onActivated(async () => {
|
||||
|
||||
@media (width <= 600px) {
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -113,6 +113,18 @@ registerHeaderTab({
|
||||
},
|
||||
show: computed(() => activeTab.value === 'mysub'),
|
||||
},
|
||||
{
|
||||
icon: 'mdi-checkbox-multiple-marked-outline',
|
||||
variant: 'text',
|
||||
color: 'gray',
|
||||
class: 'settings-icon-button',
|
||||
action: () => {
|
||||
// 触发批量管理模式
|
||||
const event = new CustomEvent('toggle-batch-mode')
|
||||
window.dispatchEvent(event)
|
||||
},
|
||||
show: computed(() => activeTab.value === 'mysub'),
|
||||
},
|
||||
{
|
||||
icon: 'mdi-chart-line',
|
||||
variant: 'text',
|
||||
|
||||
@@ -6,7 +6,7 @@ declare let self: ServiceWorkerGlobalScope & {
|
||||
}
|
||||
|
||||
// 缓存版本控制
|
||||
const CACHE_VERSION = 'v1.0.5'
|
||||
const CACHE_VERSION = 'v1.1.0'
|
||||
const CACHE_NAMES = {
|
||||
appShell: `app-shell-${CACHE_VERSION}`,
|
||||
static: `static-resources-${CACHE_VERSION}`,
|
||||
|
||||
@@ -3,16 +3,15 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
// 基础样式
|
||||
html.v-overlay-scroll-blocked {
|
||||
position: fixed;
|
||||
position: relative;
|
||||
|
||||
--v-body-scroll-y: 0px !important;
|
||||
position: static;
|
||||
}
|
||||
|
||||
body {
|
||||
overscroll-behavior: none;
|
||||
html.v-overlay-scroll-blocked body {
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
inset: 0;
|
||||
inset-block-start: var(--v-body-scroll-y);
|
||||
}
|
||||
|
||||
@mixin hide-scrollbar {
|
||||
|
||||
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,63 +62,85 @@ 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: 前台恢复连接')
|
||||
|
||||
// 只有在有活跃监听器时才重新建立连接
|
||||
if (this.listeners.size > 0 && (!this.eventSource || this.eventSource.readyState === EventSource.CLOSED)) {
|
||||
this.reconnectSSE()
|
||||
}
|
||||
}
|
||||
|
||||
private reconnectSSE(attemptCount = 0) {
|
||||
if (attemptCount >= this.options.maxReconnectAttempts) {
|
||||
console.warn('SSE: 达到最大重连次数')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isConnecting) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有活跃的监听器,不进行重连
|
||||
if (this.listeners.size === 0) {
|
||||
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)
|
||||
if (!this.isBackground && this.listeners.size > 0) {
|
||||
this.reconnectSSE(this.reconnectAttempts + 1)
|
||||
}
|
||||
}, this.options.reconnectDelay)
|
||||
}
|
||||
}
|
||||
|
||||
this.eventSource.onmessage = (event) => {
|
||||
|
||||
this.eventSource.onmessage = event => {
|
||||
// 分发消息给所有监听器
|
||||
this.listeners.forEach(listener => {
|
||||
this.listeners.forEach((listener, listenerId) => {
|
||||
try {
|
||||
// 为每个监听器提供独立的错误处理
|
||||
listener(event)
|
||||
} catch (error) {
|
||||
console.error('SSE: 监听器错误', error)
|
||||
console.error(`SSE: 监听器错误 [${listenerId}]`, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('SSE: 创建连接失败', error)
|
||||
this.isConnecting = false
|
||||
|
||||
// 连接创建失败,尝试重连
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
}
|
||||
|
||||
this.reconnectTimer = window.setTimeout(() => {
|
||||
if (!this.isBackground && this.listeners.size > 0) {
|
||||
this.reconnectSSE(this.reconnectAttempts + 1)
|
||||
}
|
||||
}, this.options.reconnectDelay)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,9 +149,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 +161,7 @@ export class SSEManager {
|
||||
*/
|
||||
removeMessageListener(id: string) {
|
||||
this.listeners.delete(id)
|
||||
|
||||
|
||||
// 如果没有监听器了,关闭连接
|
||||
if (this.listeners.size === 0) {
|
||||
this.close()
|
||||
@@ -153,18 +176,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 +205,37 @@ export class SSEManager {
|
||||
get connectionUrl(): string {
|
||||
return this.url
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制重新连接
|
||||
*/
|
||||
forceReconnect() {
|
||||
this.close()
|
||||
if (!this.isBackground && this.listeners.size > 0) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,12 +246,37 @@ class SSEManagerSingleton {
|
||||
|
||||
/**
|
||||
* 获取或创建SSE管理器
|
||||
* @param url SSE连接URL
|
||||
* @param options SSE选项
|
||||
* @returns SSE管理器实例
|
||||
*/
|
||||
getManager(url: string, options?: ConstructorParameters<typeof SSEManager>[1]): SSEManager {
|
||||
if (!this.managers.has(url)) {
|
||||
this.managers.set(url, new SSEManager(url, options))
|
||||
// 使用完整的URL作为key,确保不同路径的SSE连接不会复用
|
||||
const managerKey = url
|
||||
if (!this.managers.has(managerKey)) {
|
||||
this.managers.set(managerKey, new SSEManager(url, options))
|
||||
}
|
||||
return this.managers.get(url)!
|
||||
return this.managers.get(managerKey)!
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建独立的SSE管理器(为每个监听器创建独立连接)
|
||||
* @param url SSE连接URL
|
||||
* @param listenerId 监听器ID
|
||||
* @param options SSE选项
|
||||
* @returns SSE管理器实例
|
||||
*/
|
||||
getIndependentManager(
|
||||
url: string,
|
||||
listenerId: string,
|
||||
options?: ConstructorParameters<typeof SSEManager>[1],
|
||||
): SSEManager {
|
||||
// 使用URL + 监听器ID作为key,确保每个监听器都有独立的连接
|
||||
const managerKey = `${url}::${listenerId}`
|
||||
if (!this.managers.has(managerKey)) {
|
||||
this.managers.set(managerKey, new SSEManager(url, options))
|
||||
}
|
||||
return this.managers.get(managerKey)!
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,4 +299,4 @@ class SSEManagerSingleton {
|
||||
}
|
||||
}
|
||||
|
||||
export const sseManagerSingleton = new SSEManagerSingleton()
|
||||
export const sseManagerSingleton = new SSEManagerSingleton()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -215,10 +215,7 @@ const defaultColor = '#2196F3'
|
||||
// 计算过滤表单是否全部为空
|
||||
const isFilterFormEmpty = computed(() => {
|
||||
return (
|
||||
filterForm.name === '' &&
|
||||
filterForm.author.length === 0 &&
|
||||
filterForm.label.length === 0 &&
|
||||
filterForm.repo.length === 0
|
||||
!filterForm.name && filterForm.author.length === 0 && filterForm.label.length === 0 && filterForm.repo.length === 0
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1552,7 +1549,7 @@ function onDragStartPlugin(evt: any) {
|
||||
/>
|
||||
|
||||
<!-- 插件搜索窗口 -->
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="SearchDialog"
|
||||
v-model="SearchDialog"
|
||||
scrollable
|
||||
@@ -1611,20 +1608,20 @@ function onDragStartPlugin(evt: any) {
|
||||
</VVirtualScroll>
|
||||
</VList>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
|
||||
<!-- 安装插件进度框 -->
|
||||
<DialogWrapper v-if="progressDialog" v-model="progressDialog" :scrim="false" width="25rem">
|
||||
<VDialog v-if="progressDialog" v-model="progressDialog" :scrim="false" width="25rem">
|
||||
<VCard color="primary">
|
||||
<VCardText class="text-center">
|
||||
{{ progressText }}
|
||||
<VProgressLinear indeterminate color="white" class="mb-0 mt-1" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
|
||||
<!-- 新建文件夹对话框 -->
|
||||
<DialogWrapper v-if="newFolderDialog" v-model="newFolderDialog" max-width="400">
|
||||
<VDialog v-if="newFolderDialog" v-model="newFolderDialog" max-width="400">
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="newFolderDialog = false" />
|
||||
<VCardItem>
|
||||
@@ -1646,5 +1643,5 @@ function onDragStartPlugin(evt: any) {
|
||||
}}</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -338,7 +338,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogWrapper v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||
<VDialog v-if="releaseDialog" v-model="releaseDialog" width="600" scrollable>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VDialogCloseBtn @click="releaseDialog = false" />
|
||||
@@ -346,7 +346,7 @@ onMounted(() => {
|
||||
</VCardItem>
|
||||
<VCardText v-html="releaseDialogBody" />
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style type="scss" scoped>
|
||||
|
||||
@@ -11,7 +11,6 @@ import { usePWA } from '@/composables/usePWA'
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
// PWA模式检测
|
||||
const { appMode } = usePWA()
|
||||
|
||||
@@ -423,7 +422,7 @@ onMounted(() => {
|
||||
</VCard>
|
||||
|
||||
<!-- 重新识别对话框 -->
|
||||
<DialogWrapper v-model="reidentifyDialog" scrollable max-width="35rem">
|
||||
<VDialog v-model="reidentifyDialog" scrollable max-width="35rem">
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
@@ -469,5 +468,5 @@ onMounted(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -444,7 +444,7 @@ onMounted(() => {
|
||||
:indeterminate="true"
|
||||
/>
|
||||
<!-- 模板编辑器对话框 -->
|
||||
<DialogWrapper v-model="editorVisible" v-if="editorVisible" max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VDialog v-model="editorVisible" v-if="editorVisible" max-width="50rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem class="py-2">
|
||||
<template #prepend>
|
||||
@@ -472,7 +472,7 @@ onMounted(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
<style scoped>
|
||||
/* Monaco编辑器容器样式 */
|
||||
|
||||
@@ -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',
|
||||
@@ -732,7 +733,7 @@ onDeactivated(() => {
|
||||
</VRow>
|
||||
|
||||
<!-- 高级系统设置 -->
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-if="advancedDialog"
|
||||
v-model="advancedDialog"
|
||||
scrollable
|
||||
@@ -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"
|
||||
/>
|
||||
@@ -1328,5 +1329,5 @@ onDeactivated(() => {
|
||||
</VForm>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
@@ -5,14 +5,20 @@ 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 SiteImportDialog from '@/components/dialog/SiteImportDialog.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useToast } from 'vue-toastification'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 路由
|
||||
const route = useRoute()
|
||||
|
||||
@@ -39,6 +45,12 @@ const loading = ref(false)
|
||||
// 新增站点对话框
|
||||
const siteAddDialog = ref(false)
|
||||
|
||||
// 统计信息对话框
|
||||
const siteStatsDialog = ref(false)
|
||||
|
||||
// 导入站点对话框
|
||||
const siteImportDialog = ref(false)
|
||||
|
||||
// 筛选相关
|
||||
const filterMenu = ref(false)
|
||||
const filterOption = ref('all') // all, active, inactive, connected, slow, failed, unknown
|
||||
@@ -208,6 +220,57 @@ function selectFilter(value: string) {
|
||||
filterMenu.value = false
|
||||
}
|
||||
|
||||
// 导出站点数据
|
||||
async function exportSites() {
|
||||
try {
|
||||
// 获取所有站点数据
|
||||
const sites: Site[] = await api.get('site/')
|
||||
|
||||
// 创建导出数据,只包含必要的字段
|
||||
const exportData = sites.map((site: Site) => ({
|
||||
name: site.name,
|
||||
domain: site.domain,
|
||||
url: site.url,
|
||||
rss: site.rss,
|
||||
downloader: site.downloader,
|
||||
cookie: site.cookie,
|
||||
apikey: site.apikey,
|
||||
token: site.token,
|
||||
ua: site.ua,
|
||||
proxy: site.proxy,
|
||||
filter: site.filter,
|
||||
render: site.render,
|
||||
public: site.public,
|
||||
note: site.note,
|
||||
timeout: site.timeout,
|
||||
limit_interval: site.limit_interval,
|
||||
limit_count: site.limit_count,
|
||||
limit_seconds: site.limit_seconds,
|
||||
is_active: site.is_active,
|
||||
pri: site.pri,
|
||||
}))
|
||||
|
||||
// 创建Blob对象
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
|
||||
|
||||
// 创建下载链接
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `sites_export_${new Date().toISOString().split('T')[0]}.json`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
// 显示成功提示
|
||||
$toast.success(t('site.messages.exportSuccess'))
|
||||
} catch (error) {
|
||||
console.error('Export sites failed:', error)
|
||||
$toast.error(t('site.messages.exportFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onBeforeMount(() => {
|
||||
fetchData()
|
||||
@@ -235,44 +298,68 @@ 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="success" @click="siteImportDialog = true">
|
||||
<VIcon icon="mdi-import" />
|
||||
<span v-if="!display.smAndDown.value" class="ml-2">
|
||||
{{ t('site.actions.import') }}
|
||||
</span>
|
||||
</VBtn>
|
||||
<!-- 导出按钮 -->
|
||||
<VBtn :icon="display.smAndDown.value" variant="text" color="warning" @click="exportSites">
|
||||
<VIcon icon="mdi-export" />
|
||||
<span v-if="!display.smAndDown.value" class="ml-2">
|
||||
{{ t('site.actions.export') }}
|
||||
</span>
|
||||
</VBtn>
|
||||
<!-- 统计信息按钮 -->
|
||||
<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 +413,10 @@ useDynamicButton({
|
||||
@save="onSiteSave"
|
||||
@close="siteAddDialog = false"
|
||||
/>
|
||||
|
||||
<!-- 统计信息弹窗 -->
|
||||
<SiteStatisticsDialog v-if="siteStatsDialog" v-model="siteStatsDialog" :sites="siteList" />
|
||||
|
||||
<!-- 导入站点弹窗 -->
|
||||
<SiteImportDialog v-if="siteImportDialog" v-model="siteImportDialog" @import-success="fetchData" />
|
||||
</template>
|
||||
|
||||
@@ -9,6 +9,8 @@ import { useUserStore } from '@/stores'
|
||||
import { useDynamicButton } from '@/composables/useDynamicButton'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -22,6 +24,12 @@ const { appMode } = usePWA()
|
||||
// 用户 Store
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 确认框
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 从 Store 中获取用户信息
|
||||
const superUser = userStore.superUser
|
||||
const userName = userStore.userName
|
||||
@@ -52,6 +60,10 @@ const orderConfig = ref<{ id: number }[]>([])
|
||||
// 显示的订阅列表
|
||||
const displayList = ref<Subscribe[]>([])
|
||||
|
||||
// 批量管理相关状态
|
||||
const isBatchMode = ref(false)
|
||||
const selectedSubscribes = ref<number[]>([])
|
||||
|
||||
// 根据订阅数据判断订阅状态
|
||||
function getSubscribeStatus(subscribe: Subscribe) {
|
||||
// 洗版中
|
||||
@@ -173,6 +185,160 @@ function historyDone() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 批量管理相关函数
|
||||
// 切换批量模式
|
||||
function toggleBatchMode() {
|
||||
isBatchMode.value = !isBatchMode.value
|
||||
if (!isBatchMode.value) {
|
||||
selectedSubscribes.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 全选/取消全选
|
||||
function toggleSelectAll() {
|
||||
if (selectedSubscribes.value.length === displayList.value.length) {
|
||||
selectedSubscribes.value = []
|
||||
} else {
|
||||
selectedSubscribes.value = displayList.value.map(item => item.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择单个订阅
|
||||
function toggleSelectSubscribe(id: number) {
|
||||
const index = selectedSubscribes.value.indexOf(id)
|
||||
if (index > -1) {
|
||||
selectedSubscribes.value.splice(index, 1)
|
||||
} else {
|
||||
selectedSubscribes.value.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除订阅
|
||||
async function batchDeleteSubscribes() {
|
||||
if (selectedSubscribes.value.length === 0) {
|
||||
$toast.warning(t('subscribe.noSelectedItems'))
|
||||
return
|
||||
}
|
||||
|
||||
const isConfirmed = await createConfirm({
|
||||
title: t('common.confirm'),
|
||||
content: t('subscribe.batchDeleteConfirm', { count: selectedSubscribes.value.length }),
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const promises = selectedSubscribes.value.map(id => api.delete(`subscribe/${id}`))
|
||||
const results = await Promise.allSettled(promises)
|
||||
|
||||
const successCount = results.filter(result => result.status === 'fulfilled').length
|
||||
const failedCount = results.length - successCount
|
||||
|
||||
if (successCount > 0) {
|
||||
$toast.success(t('subscribe.batchDeleteSuccess', { count: successCount }))
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
$toast.error(t('subscribe.batchDeleteFailed', { count: failedCount }))
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
await fetchData()
|
||||
// 退出批量模式
|
||||
isBatchMode.value = false
|
||||
selectedSubscribes.value = []
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('subscribe.batchDeleteError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 批量启用订阅
|
||||
async function batchEnableSubscribes() {
|
||||
if (selectedSubscribes.value.length === 0) {
|
||||
$toast.warning(t('subscribe.noSelectedItems'))
|
||||
return
|
||||
}
|
||||
|
||||
const isConfirmed = await createConfirm({
|
||||
title: t('common.confirm'),
|
||||
content: t('subscribe.batchEnableConfirm', { count: selectedSubscribes.value.length }),
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const promises = selectedSubscribes.value.map(id => api.put(`subscribe/status/${id}?state=R`))
|
||||
const results = await Promise.allSettled(promises)
|
||||
|
||||
const successCount = results.filter(result => result.status === 'fulfilled').length
|
||||
const failedCount = results.length - successCount
|
||||
|
||||
if (successCount > 0) {
|
||||
$toast.success(t('subscribe.batchEnableSuccess', { count: successCount }))
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
$toast.error(t('subscribe.batchEnableFailed', { count: failedCount }))
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
await fetchData()
|
||||
// 退出批量模式
|
||||
isBatchMode.value = false
|
||||
selectedSubscribes.value = []
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('subscribe.batchEnableError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 批量暂停订阅
|
||||
async function batchPauseSubscribes() {
|
||||
if (selectedSubscribes.value.length === 0) {
|
||||
$toast.warning(t('subscribe.noSelectedItems'))
|
||||
return
|
||||
}
|
||||
|
||||
const isConfirmed = await createConfirm({
|
||||
title: t('common.confirm'),
|
||||
content: t('subscribe.batchPauseConfirm', { count: selectedSubscribes.value.length }),
|
||||
})
|
||||
|
||||
if (!isConfirmed) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const promises = selectedSubscribes.value.map(id => api.put(`subscribe/status/${id}?state=S`))
|
||||
const results = await Promise.allSettled(promises)
|
||||
|
||||
const successCount = results.filter(result => result.status === 'fulfilled').length
|
||||
const failedCount = results.length - successCount
|
||||
|
||||
if (successCount > 0) {
|
||||
$toast.success(t('subscribe.batchPauseSuccess', { count: successCount }))
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
$toast.error(t('subscribe.batchPauseFailed', { count: failedCount }))
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
await fetchData()
|
||||
// 退出批量模式
|
||||
isBatchMode.value = false
|
||||
selectedSubscribes.value = []
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('subscribe.batchPauseError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 错误描述
|
||||
const errorDescription = computed(() => {
|
||||
if ((props.statusFilter && props.statusFilter !== 'all') || props.keyword) {
|
||||
@@ -199,6 +365,14 @@ onMounted(async () => {
|
||||
sub.page_open = true
|
||||
}
|
||||
}
|
||||
|
||||
// 监听批量管理模式切换事件
|
||||
window.addEventListener('toggle-batch-mode', toggleBatchMode)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除事件监听器
|
||||
window.removeEventListener('toggle-batch-mode', toggleBatchMode)
|
||||
})
|
||||
|
||||
onActivated(async () => {
|
||||
@@ -218,6 +392,63 @@ useDynamicButton({
|
||||
|
||||
<template>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
|
||||
|
||||
<!-- 批量管理工具栏 -->
|
||||
<div v-if="isBatchMode" class="mb-4 px-2">
|
||||
<VCard class="pa-4">
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div class="d-flex align-center">
|
||||
<VCheckbox
|
||||
:model-value="selectedSubscribes.length === displayList.length"
|
||||
:indeterminate="selectedSubscribes.length > 0 && selectedSubscribes.length < displayList.length"
|
||||
@update:model-value="toggleSelectAll"
|
||||
hide-details
|
||||
class="me-4"
|
||||
/>
|
||||
<span class="text-body-1 font-weight-medium">
|
||||
{{ t('subscribe.selectedCount', { count: selectedSubscribes.length, total: displayList.length }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<VBtn
|
||||
color="success"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
:disabled="selectedSubscribes.length === 0"
|
||||
@click="batchEnableSubscribes"
|
||||
>
|
||||
<VIcon icon="mdi-play" class="me-sm-1" />
|
||||
<span class="d-none d-sm-inline">{{ t('subscribe.batchEnable') }}</span>
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="info"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
:disabled="selectedSubscribes.length === 0"
|
||||
@click="batchPauseSubscribes"
|
||||
>
|
||||
<VIcon icon="mdi-pause" class="me-sm-1" />
|
||||
<span class="d-none d-sm-inline">{{ t('subscribe.batchPause') }}</span>
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
:disabled="selectedSubscribes.length === 0"
|
||||
@click="batchDeleteSubscribes"
|
||||
>
|
||||
<VIcon icon="mdi-delete" class="me-sm-1" />
|
||||
<span class="d-none d-sm-inline">{{ t('subscribe.batchDelete') }}</span>
|
||||
</VBtn>
|
||||
<VBtn color="secondary" variant="outlined" size="small" @click="toggleBatchMode">
|
||||
<VIcon icon="mdi-close" class="me-sm-1" />
|
||||
<span class="d-none d-sm-inline">{{ t('common.cancel') }}</span>
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
|
||||
<draggable
|
||||
v-if="displayList.length > 0"
|
||||
v-model="displayList"
|
||||
@@ -226,10 +457,18 @@ useDynamicButton({
|
||||
item-key="id"
|
||||
tag="div"
|
||||
:component-data="{ class: 'grid gap-4 grid-subscribe-card px-2' }"
|
||||
:disabled="props.keyword || (props.statusFilter && props.statusFilter !== 'all')"
|
||||
:disabled="props.keyword || (props.statusFilter && props.statusFilter !== 'all') || isBatchMode"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<SubscribeCard :key="element.id" :media="element" @remove="fetchData" @save="fetchData" />
|
||||
<SubscribeCard
|
||||
:key="element.id"
|
||||
:media="element"
|
||||
:batch-mode="isBatchMode"
|
||||
:selected="selectedSubscribes.includes(element.id)"
|
||||
@remove="fetchData"
|
||||
@save="fetchData"
|
||||
@select="toggleSelectSubscribe(element.id)"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
<NoDataFound
|
||||
|
||||
@@ -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') // 新消息到达时触发智能滚动
|
||||
@@ -43,11 +45,16 @@ function handleSSEMessage(event: MessageEvent) {
|
||||
}
|
||||
|
||||
// 使用优化的SSE连接
|
||||
useSSE(`${import.meta.env.VITE_API_BASE_URL}system/message?role=user`, handleSSEMessage, 'message-view', {
|
||||
backgroundCloseDelay: 5000,
|
||||
reconnectDelay: 3000,
|
||||
maxReconnectAttempts: 3,
|
||||
})
|
||||
const { manager, isConnected } = useSSE(
|
||||
`${import.meta.env.VITE_API_BASE_URL}system/message?role=user`,
|
||||
handleSSEMessage,
|
||||
'message-view',
|
||||
{
|
||||
backgroundCloseDelay: 5000,
|
||||
reconnectDelay: 3000,
|
||||
maxReconnectAttempts: 3,
|
||||
},
|
||||
)
|
||||
|
||||
// 调用API加载存量消息
|
||||
async function loadMessages({ done }: { done: any }) {
|
||||
@@ -76,14 +83,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 +112,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
|
||||
}
|
||||
}
|
||||
|
||||
// 图片加载完成时触发智能滚动
|
||||
@@ -112,6 +150,26 @@ function handleImageLoad() {
|
||||
emit('scroll')
|
||||
}
|
||||
|
||||
// 暂停SSE连接
|
||||
function pauseSSE() {
|
||||
if (manager) {
|
||||
manager.removeMessageListener('message-view')
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复SSE连接
|
||||
function resumeSSE() {
|
||||
if (manager) {
|
||||
manager.addMessageListener('message-view', handleSSEMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
pauseSSE,
|
||||
resumeSSE,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 组件挂载后触发一次滚动事件
|
||||
nextTick(() => {
|
||||
|
||||
@@ -617,7 +617,7 @@ const handleSortIconClick = () => {
|
||||
</VCard>
|
||||
|
||||
<!-- 全部筛选弹窗 -->
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-model="allFilterMenuOpen"
|
||||
max-width="50rem"
|
||||
location="center"
|
||||
@@ -690,10 +690,10 @@ const handleSortIconClick = () => {
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
|
||||
<!-- 筛选弹窗 -->
|
||||
<DialogWrapper v-model="filterMenuOpen" max-width="25rem" location="center" max-height="85vh">
|
||||
<VDialog 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>
|
||||
@@ -735,7 +735,7 @@ const handleSortIconClick = () => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
|
||||
<!-- 资源列表 -->
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="displayDataList" class="overflow-visible" @load="loadMore">
|
||||
|
||||
@@ -597,7 +597,7 @@ onMounted(() => {
|
||||
</VCard>
|
||||
|
||||
<!-- 全部筛选弹窗 -->
|
||||
<DialogWrapper
|
||||
<VDialog
|
||||
v-model="allFilterMenuOpen"
|
||||
max-width="50rem"
|
||||
location="center"
|
||||
@@ -670,10 +670,10 @@ onMounted(() => {
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
|
||||
<!-- 筛选弹窗 -->
|
||||
<DialogWrapper v-model="filterMenuOpen" max-width="25rem" max-height="85vh" location="center">
|
||||
<VDialog 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>
|
||||
@@ -715,7 +715,7 @@ onMounted(() => {
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
|
||||
<!-- 资源列表容器 -->
|
||||
<VCard class="resource-list-container">
|
||||
|
||||
@@ -454,7 +454,7 @@ watch(
|
||||
</VRow>
|
||||
|
||||
<!-- 双重验证弹窗 -->
|
||||
<DialogWrapper v-if="otpDialog" v-model="otpDialog" max-width="45rem" scrollable>
|
||||
<VDialog v-if="otpDialog" v-model="otpDialog" max-width="45rem" scrollable>
|
||||
<!-- 开启双重验证弹窗内容 -->
|
||||
<VCard>
|
||||
<VDialogCloseBtn @click="otpDialog = false" />
|
||||
@@ -492,6 +492,6 @@ watch(
|
||||
</VForm>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</DialogWrapper>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
90
yarn.lock
90
yarn.lock
@@ -861,24 +861,24 @@
|
||||
integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==
|
||||
|
||||
"@emnapi/core@^1.4.3":
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.4.tgz#76620673f3033626c6d79b1420d69f06a6bb153c"
|
||||
integrity sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==
|
||||
version "1.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.5.tgz#bfbb0cbbbb9f96ec4e2c4fd917b7bbe5495ceccb"
|
||||
integrity sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==
|
||||
dependencies:
|
||||
"@emnapi/wasi-threads" "1.0.3"
|
||||
"@emnapi/wasi-threads" "1.0.4"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/runtime@^1.2.0", "@emnapi/runtime@^1.4.3":
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.4.tgz#19a8f00719c51124e2d0fbf4aaad3fa7b0c92524"
|
||||
integrity sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==
|
||||
version "1.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.5.tgz#c67710d0661070f38418b6474584f159de38aba9"
|
||||
integrity sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/wasi-threads@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.0.3.tgz#83fa228bde0e71668aad6db1af4937473d1d3ab1"
|
||||
integrity sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==
|
||||
"@emnapi/wasi-threads@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz#703fc094d969e273b1b71c292523b2f792862bf4"
|
||||
integrity sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
@@ -1246,7 +1246,7 @@
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff"
|
||||
integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==
|
||||
|
||||
"@img/sharp-linux-arm64@0.33.5":
|
||||
@@ -1286,7 +1286,7 @@
|
||||
|
||||
"@img/sharp-linuxmusl-x64@0.33.5":
|
||||
version "0.33.5"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48"
|
||||
integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linuxmusl-x64" "1.0.4"
|
||||
@@ -1454,13 +1454,13 @@
|
||||
integrity sha512-+//cqVWKis//t0YH62EDtwaFSPG/CDtYNg4CZmzNmG2d5W17Iu3fuDAdpQXCDHUDrrU9q0veze4A7tPZXlR/mg==
|
||||
|
||||
"@napi-rs/wasm-runtime@^0.2.9":
|
||||
version "0.2.11"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz#192c1610e1625048089ab4e35bc0649ce478500e"
|
||||
integrity sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==
|
||||
version "0.2.12"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2"
|
||||
integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==
|
||||
dependencies:
|
||||
"@emnapi/core" "^1.4.3"
|
||||
"@emnapi/runtime" "^1.4.3"
|
||||
"@tybys/wasm-util" "^0.9.0"
|
||||
"@tybys/wasm-util" "^0.10.0"
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
@@ -1571,7 +1571,7 @@
|
||||
|
||||
"@parcel/watcher-linux-x64-musl@2.5.1":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz#277b346b05db54f55657301dd77bdf99d63606ee"
|
||||
integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==
|
||||
|
||||
"@parcel/watcher-win32-arm64@2.5.1":
|
||||
@@ -1759,7 +1759,7 @@
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.40.1":
|
||||
version "4.40.1"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz#c76fd593323c60ea219439a00da6c6d33ffd0ea6"
|
||||
integrity sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.40.1":
|
||||
@@ -1856,7 +1856,7 @@
|
||||
|
||||
"@swc/core-linux-x64-musl@1.12.9":
|
||||
version "1.12.9"
|
||||
resolved "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.9.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.9.tgz#54bffa601a6b84b0de9116bdaaabd6ae6fa61ee8"
|
||||
integrity sha512-9FB0wM+6idCGTI20YsBNBg9xSWtkDBymnpaTCsZM3qDc0l4uOpJMqbfWhQvp17x7r/ulZfb2QY8RDvQmCL6AcQ==
|
||||
|
||||
"@swc/core-win32-arm64-msvc@1.12.9":
|
||||
@@ -1920,13 +1920,23 @@
|
||||
resolved "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz"
|
||||
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
|
||||
|
||||
"@tybys/wasm-util@^0.9.0":
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.9.0.tgz#3e75eb00604c8d6db470bf18c37b7d984a0e3355"
|
||||
integrity sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==
|
||||
"@tybys/wasm-util@^0.10.0":
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.0.tgz#2fd3cd754b94b378734ce17058d0507c45c88369"
|
||||
integrity sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@types/body-scroll-lock@^3.1.2":
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-scroll-lock/-/body-scroll-lock-3.1.2.tgz#1ae7857d98180dbe6c3b05abbe7ec1fa67b614e3"
|
||||
integrity sha512-ELhtuphE/YbhEcpBf/rIV9Tl3/O0A0gpCVD+oYFSS8bWstHFJUgA4nNw1ZakVlRC38XaQEIsBogUZKWIPBvpfQ==
|
||||
|
||||
"@types/crypto-js@^4.2.2":
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea"
|
||||
integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==
|
||||
|
||||
"@types/debug@^4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz"
|
||||
@@ -2193,7 +2203,7 @@
|
||||
|
||||
"@unrs/resolver-binding-linux-x64-musl@1.7.2":
|
||||
version "1.7.2"
|
||||
resolved "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.2.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.2.tgz#684e576557d20deb4ac8ea056dcbe79739ca2870"
|
||||
integrity sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw==
|
||||
|
||||
"@unrs/resolver-binding-wasm32-wasi@1.7.2":
|
||||
@@ -2874,6 +2884,11 @@ body-parser@1.20.3:
|
||||
type-is "~1.6.18"
|
||||
unpipe "1.0.0"
|
||||
|
||||
body-scroll-lock@^3.1.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz#c1392d9217ed2c3e237fee1e910f6cdd80b7aaec"
|
||||
integrity sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg==
|
||||
|
||||
boolbase@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz"
|
||||
@@ -3246,6 +3261,11 @@ cross-spawn@^7.0.6:
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
crypto-js@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
|
||||
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
|
||||
|
||||
crypto-random-string@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz"
|
||||
@@ -6835,16 +6855,7 @@ std-env@^3.9.0:
|
||||
resolved "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz"
|
||||
integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -6929,14 +6940,7 @@ stringify-object@^3.3.0:
|
||||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
|
||||
Reference in New Issue
Block a user