mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-09 22:22:58 +08:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d997dc0394 | ||
|
|
6b6353ed41 | ||
|
|
e73d906564 | ||
|
|
7e3e850e21 | ||
|
|
56b2dc4ebf | ||
|
|
9444b0e518 | ||
|
|
bcb72118f5 | ||
|
|
c59be8d981 | ||
|
|
8466a40455 | ||
|
|
f435b4fc52 | ||
|
|
5686c6fe65 | ||
|
|
6810112eda | ||
|
|
11a2d07935 | ||
|
|
02cd2f1570 | ||
|
|
924c1d72ea | ||
|
|
5d9b2e1919 | ||
|
|
f7fa440f9a | ||
|
|
d4aaa46968 | ||
|
|
93ac5e1b3b | ||
|
|
c7a8c68e14 | ||
|
|
77afb4d736 | ||
|
|
141796ab24 | ||
|
|
30d733f55d | ||
|
|
6a39e65b6b | ||
|
|
c27013b7ad | ||
|
|
582ce496fa | ||
|
|
5b4dbb82d5 | ||
|
|
011a0d16ab | ||
|
|
ac5539194d | ||
|
|
6b7e1b3c4e | ||
|
|
30c3d00139 | ||
|
|
36d460cd74 | ||
|
|
11cb2eb0f8 | ||
|
|
4dce1c94a3 |
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -44,6 +44,7 @@ jobs:
|
||||
|
||||
- name: Delete Release
|
||||
uses: dev-drprasad/delete-tag-and-release@v1.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
tag_name: ${{ env.frontend_version }}
|
||||
delete_release: true
|
||||
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -106,5 +106,8 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"vue3snippets.enable-compile-vue-file-on-did-save-code": false
|
||||
}
|
||||
"vue3snippets.enable-compile-vue-file-on-did-save-code": false,
|
||||
"i18n-ally.localesPaths": [
|
||||
"src/locales"
|
||||
]
|
||||
}
|
||||
@@ -9,7 +9,7 @@ MoviePilot前端采用模块联邦(Module Federation)技术实现插件的动态
|
||||
|
||||
## 2. 技术要求
|
||||
|
||||
- Node.js 16+
|
||||
- Node.js 20+
|
||||
- Vue 3
|
||||
- Vite 4+
|
||||
- TypeScript 5+
|
||||
@@ -80,13 +80,6 @@ export default defineConfig({
|
||||
target: 'esnext', // 必须设置为esnext以支持顶层await
|
||||
minify: false, // 开发阶段建议关闭混淆
|
||||
cssCodeSplit: true, // 改为true以便能分离样式文件
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'vuetify-lib': ['vuetify'] // 将vuetify单独分离出来
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.4.5",
|
||||
"version": "2.4.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": "dist/service.js",
|
||||
|
||||
@@ -7,7 +7,7 @@ const props = defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full text-center text-gray-500 text-sm flex flex-col items-center">
|
||||
<div class="w-full text-center text-gray-500 text-sm flex flex-col items-center mb-5">
|
||||
<VProgressCircular v-if="!props.text || !props.progress" class="mb-3" size="64" indeterminate color="primary" />
|
||||
<VProgressCircular v-if="props.progress" class="mb-3" color="primary" :model-value="props.progress" size="64" />
|
||||
<span>{{ props.text }}</span>
|
||||
|
||||
109
src/App.vue
109
src/App.vue
@@ -4,13 +4,9 @@ import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
|
||||
import { ensureRenderComplete, removeEl } from './@core/utils/dom'
|
||||
import api from '@/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getBrowserLocale, setI18nLanguage } from './plugins/i18n'
|
||||
import { SupportedLocale } from '@/types/i18n'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 生效主题
|
||||
const { global: globalTheme } = useTheme()
|
||||
let themeValue = localStorage.getItem('theme') || 'light'
|
||||
@@ -21,9 +17,6 @@ globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
|
||||
const localeValue = getBrowserLocale()
|
||||
setI18nLanguage(localeValue as SupportedLocale)
|
||||
|
||||
// 从 provide 中获取全局设置
|
||||
const globalSettings: any = inject('globalSettings')
|
||||
|
||||
// 显示状态
|
||||
const show = ref(false)
|
||||
|
||||
@@ -31,6 +24,9 @@ const show = ref(false)
|
||||
const authStore = useAuthStore()
|
||||
const isLogin = computed(() => authStore.token)
|
||||
|
||||
// 生成背景图片key
|
||||
const loginStateKey = computed(() => (isLogin.value ? 'logged-in' : 'logged-out'))
|
||||
|
||||
// 背景图片
|
||||
const backgroundImages = ref<string[]>([])
|
||||
const activeImageIndex = ref(0)
|
||||
@@ -78,6 +74,7 @@ function updateHtmlThemeAttribute(themeName: string) {
|
||||
async function fetchBackgroundImages() {
|
||||
try {
|
||||
backgroundImages.value = await api.get(`/login/wallpapers`)
|
||||
activeImageIndex.value = 0
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
@@ -85,6 +82,7 @@ async function fetchBackgroundImages() {
|
||||
|
||||
// 开始背景图片轮换
|
||||
function startBackgroundRotation() {
|
||||
// 清除轮换定时器
|
||||
if (backgroundRotationTimer) clearInterval(backgroundRotationTimer)
|
||||
|
||||
if (backgroundImages.value.length > 1) {
|
||||
@@ -106,7 +104,6 @@ function startBackgroundRotation() {
|
||||
function preloadImage(url: string): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const img = new Image()
|
||||
const imageUrl = getImgUrl(url)
|
||||
|
||||
img.onload = () => resolve(true)
|
||||
img.onerror = () => resolve(false)
|
||||
@@ -117,7 +114,7 @@ function preloadImage(url: string): Promise<boolean> {
|
||||
resolve(false)
|
||||
}, 5000) // 5秒超时
|
||||
|
||||
img.src = imageUrl
|
||||
img.src = url
|
||||
|
||||
// 如果图片已经缓存,onload可能不会触发
|
||||
if (img.complete) {
|
||||
@@ -127,28 +124,6 @@ function preloadImage(url: string): Promise<boolean> {
|
||||
})
|
||||
}
|
||||
|
||||
// 计算图片地址
|
||||
function getImgUrl(url: string) {
|
||||
// 使用图片缓存
|
||||
if (globalSettings.GLOBAL_IMAGE_CACHE && isLogin.value)
|
||||
return `${import.meta.env.VITE_API_BASE_URL}system/cache/image?url=${encodeURIComponent(url)}`
|
||||
return url
|
||||
}
|
||||
|
||||
// 处理页面可见性变化
|
||||
function handleVisibilityChange() {
|
||||
if (document.visibilityState === 'visible') {
|
||||
// 如果已有背景图片数据,直接重启轮换
|
||||
if (backgroundImages.value.length > 0) {
|
||||
startBackgroundRotation()
|
||||
}
|
||||
// 如果没有背景图片数据,重新获取
|
||||
else {
|
||||
fetchBackgroundImages().then(() => startBackgroundRotation())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加logo动画效果并延迟移除加载界面
|
||||
function animateAndRemoveLoader() {
|
||||
const loadingBg = document.querySelector('#loading-bg') as HTMLElement
|
||||
@@ -167,29 +142,61 @@ function animateAndRemoveLoader() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 加载背景图片
|
||||
async function loadBackgroundImages() {
|
||||
await fetchBackgroundImages()
|
||||
.then(() => {
|
||||
startBackgroundRotation()
|
||||
})
|
||||
.catch(() => {
|
||||
// 3秒后重试
|
||||
setTimeout(() => {
|
||||
loadBackgroundImages()
|
||||
}, 3000)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 初始化data-theme属性
|
||||
updateHtmlThemeAttribute(globalTheme.name.value)
|
||||
|
||||
// 加载背景图片并开始轮换
|
||||
fetchBackgroundImages().then(() => startBackgroundRotation())
|
||||
// 默认隐藏页面
|
||||
show.value = false
|
||||
|
||||
// 添加页面可见性变化监听
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
// 加载背景图片
|
||||
await loadBackgroundImages()
|
||||
|
||||
// 移除加载动画
|
||||
ensureRenderComplete(() => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
// 移除加载动画
|
||||
// 移除加载动画,显示页面
|
||||
animateAndRemoveLoader()
|
||||
}, 1500)
|
||||
})
|
||||
})
|
||||
|
||||
// 添加页面可见性变化监听
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
loadBackgroundImages()
|
||||
}
|
||||
})
|
||||
|
||||
// 添加PWA的页面恢复事件监听
|
||||
window.addEventListener('pageshow', event => {
|
||||
// persisted属性为true表示页面是从bfcache中恢复的
|
||||
if (event.persisted) {
|
||||
loadBackgroundImages()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除页面可见性监听
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
document.removeEventListener('visibilitychange', () => {})
|
||||
// 移除PWA的页面恢复事件监听
|
||||
window.removeEventListener('pageshow', () => {})
|
||||
|
||||
// 清除轮换定时器
|
||||
if (backgroundRotationTimer) {
|
||||
@@ -202,20 +209,18 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<div class="app-wrapper">
|
||||
<!-- 透明主题背景 -->
|
||||
<template v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)">
|
||||
<div class="background-container">
|
||||
<div
|
||||
v-for="(imageUrl, index) in backgroundImages"
|
||||
:key="index"
|
||||
class="background-image"
|
||||
:class="{ 'active': index === activeImageIndex }"
|
||||
:style="{ backgroundImage: `url(${getImgUrl(imageUrl)})` }"
|
||||
></div>
|
||||
<!-- 全局磨砂层 -->
|
||||
<div v-if="isLogin" class="global-blur-layer"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)" class="background-container">
|
||||
<div
|
||||
v-for="(imageUrl, index) in backgroundImages"
|
||||
:key="`bg-${index}-${loginStateKey}`"
|
||||
class="background-image"
|
||||
:class="{ 'active': index === activeImageIndex }"
|
||||
:style="{ 'backgroundImage': `url(${imageUrl})` }"
|
||||
></div>
|
||||
<!-- 全局磨砂层 -->
|
||||
<div v-if="isLogin && isTransparentTheme" class="global-blur-layer"></div>
|
||||
</div>
|
||||
<!-- 页面内容 -->
|
||||
<VApp v-show="show" :class="{ 'transparent-app': isTransparentTheme }">
|
||||
<RouterView />
|
||||
</VApp>
|
||||
|
||||
@@ -82,9 +82,7 @@ function goPersonDetail() {
|
||||
}"
|
||||
@click.stop="goPersonDetail"
|
||||
>
|
||||
<div
|
||||
class="person-card relative transform-gpu cursor-pointer rounded transition duration-150 ease-in-out scale-100 ring-gray-700"
|
||||
>
|
||||
<div class="person-card relative cursor-pointer ring-gray-700">
|
||||
<div style="padding-block-end: 150%">
|
||||
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2">
|
||||
<div class="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
|
||||
|
||||
@@ -168,12 +168,8 @@ const dropdownItems = ref([
|
||||
>
|
||||
<div
|
||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
:style="`background: linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.5) 100%), linear-gradient(${backgroundColor} 0%, ${backgroundColor} 100%)`"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center"
|
||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
||||
></div>
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<VCardTitle
|
||||
class="text-white text-lg px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis ..."
|
||||
|
||||
@@ -345,12 +345,8 @@ watch(
|
||||
>
|
||||
<div
|
||||
class="relative flex flex-row items-start pa-3 justify-between grow"
|
||||
:style="{ background: `${backgroundColor}` }"
|
||||
:style="`background: linear-gradient(rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.5) 100%), linear-gradient(${backgroundColor} 0%, ${backgroundColor} 100%)`"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center"
|
||||
:style="{ background: `${backgroundColor}`, filter: 'brightness(0.5)' }"
|
||||
/>
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<VCardTitle class="text-white text-lg px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<VBadge v-if="props.plugin?.state" dot inline color="success" />
|
||||
|
||||
@@ -296,99 +296,109 @@ function onSubscribeEditRemove() {
|
||||
<div>
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:key="props.media?.id"
|
||||
class="flex flex-col h-full"
|
||||
<div
|
||||
class="w-full h-full rounded-lg overflow-hidden"
|
||||
:class="{
|
||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
'opacity-70': subscribeState === 'S',
|
||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||
}"
|
||||
min-height="170"
|
||||
@click="editSubscribeDialog"
|
||||
:ripple="false"
|
||||
>
|
||||
<div class="me-n3 absolute top-1 right-2">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" color="white" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<template v-for="(item, i) in dropdownItems" :key="i">
|
||||
<VListItem v-if="item.show !== false" :base-color="item.props.color" @click="item.props.click">
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</template>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<template #image>
|
||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||
</VImg>
|
||||
<div v-if="subscribeState === 'P'" class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none" />
|
||||
</template>
|
||||
<div>
|
||||
<VCardText class="flex items-center py-3">
|
||||
<div class="h-auto w-16 flex-shrink-0 overflow-hidden rounded-md cursor-move" v-if="imageLoaded">
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
||||
<div class="mr-2 min-w-0 text-lg font-bold text-white">
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap py-3">
|
||||
<div class="flex align-center">
|
||||
<IconBtn
|
||||
v-if="props.media?.total_episode"
|
||||
v-bind="props"
|
||||
icon="mdi-progress-download"
|
||||
color="white"
|
||||
class="me-1"
|
||||
/>
|
||||
<div v-if="props.media?.season" class="text-subtitle-2 me-4 text-white">
|
||||
{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
|
||||
{{ props.media?.total_episode }}
|
||||
</div>
|
||||
<IconBtn v-if="props.media?.username" icon="mdi-account" color="white" class="me-1" />
|
||||
<span v-if="props.media?.username" class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.username }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText v-if="lastUpdateText" class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
||||
<VIcon icon="mdi-download" class="me-1" />
|
||||
{{ lastUpdateText }}
|
||||
</VCardText>
|
||||
<div class="w-full absolute bottom-0">
|
||||
<VProgressLinear
|
||||
v-if="getPercentage() > 0"
|
||||
:model-value="getPercentage()"
|
||||
bg-color="success"
|
||||
color="success"
|
||||
/>
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:key="props.media?.id"
|
||||
class="flex flex-col h-full"
|
||||
:class="{
|
||||
'opacity-70': subscribeState === 'S',
|
||||
}"
|
||||
rounded="0"
|
||||
min-height="170"
|
||||
@click="editSubscribeDialog"
|
||||
:ripple="false"
|
||||
>
|
||||
<div class="me-n3 absolute top-1 right-4">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" color="white" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<template v-for="(item, i) in dropdownItems" :key="i">
|
||||
<VListItem v-if="item.show !== false" :base-color="item.props.color" @click="item.props.click">
|
||||
<template #prepend>
|
||||
<VIcon :icon="item.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="item.title" />
|
||||
</VListItem>
|
||||
</template>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
<template #image>
|
||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="absolute inset-0 outline-none subscribe-card-background"></div>
|
||||
</VImg>
|
||||
<div
|
||||
v-if="subscribeState === 'P'"
|
||||
class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none"
|
||||
/>
|
||||
</template>
|
||||
<div>
|
||||
<VCardText class="flex items-center py-3">
|
||||
<div class="h-auto w-16 flex-shrink-0 overflow-hidden rounded-md cursor-move" v-if="imageLoaded">
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
||||
<div class="mr-2 min-w-0 text-lg font-bold text-white">
|
||||
{{ props.media?.name }}
|
||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap py-3">
|
||||
<div class="flex align-center">
|
||||
<IconBtn
|
||||
v-if="props.media?.total_episode"
|
||||
v-bind="props"
|
||||
icon="mdi-progress-download"
|
||||
color="white"
|
||||
class="me-1"
|
||||
/>
|
||||
<div v-if="props.media?.season" class="text-subtitle-2 me-4 text-white">
|
||||
{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
|
||||
{{ props.media?.total_episode }}
|
||||
</div>
|
||||
<IconBtn v-if="props.media?.username" icon="mdi-account" color="white" class="me-1" />
|
||||
<span v-if="props.media?.username" class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.username }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText v-if="lastUpdateText" class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
||||
<VIcon icon="mdi-download" class="me-1" />
|
||||
{{ lastUpdateText }}
|
||||
</VCardText>
|
||||
<div class="w-full absolute bottom-0">
|
||||
<VProgressLinear
|
||||
v-if="getPercentage() > 0"
|
||||
:model-value="getPercentage()"
|
||||
bg-color="success"
|
||||
color="success"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
</VHover>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
|
||||
@@ -97,64 +97,69 @@ function doDelete() {
|
||||
<div class="h-full">
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:key="props.media?.id"
|
||||
class="flex flex-col h-full"
|
||||
<div
|
||||
class="w-full h-full rounded-lg overflow-hidden"
|
||||
:class="{
|
||||
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
|
||||
}"
|
||||
min-height="170"
|
||||
@click="showForkSubscribe"
|
||||
>
|
||||
<template #image>
|
||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
<VCard
|
||||
v-bind="hover.props"
|
||||
:key="props.media?.id"
|
||||
class="flex flex-col h-full"
|
||||
rounded="0"
|
||||
min-height="170"
|
||||
@click="showForkSubscribe"
|
||||
>
|
||||
<template #image>
|
||||
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||
</VImg>
|
||||
</template>
|
||||
<div class="h-full flex flex-col">
|
||||
<VCardText class="flex items-center pb-1 grow">
|
||||
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md" v-if="imageLoaded">
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
</template>
|
||||
<div class="absolute inset-0 subscribe-card-background"></div>
|
||||
</VImg>
|
||||
</template>
|
||||
<div class="h-full flex flex-col">
|
||||
<VCardText class="flex items-center pb-1 grow">
|
||||
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md" v-if="imageLoaded">
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center pl-2 xl:pl-4">
|
||||
<div class="mr-2 min-w-0 text-lg font-bold text-white line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.share_title }}
|
||||
<div class="flex flex-col justify-center pl-2 xl:pl-4">
|
||||
<div class="mr-2 min-w-0 text-lg font-bold text-white line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.share_title }}
|
||||
</div>
|
||||
<div class="text-sm font-medium text-gray-200 sm:pt-1 line-clamp-3 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.share_comment }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm font-medium text-gray-200 sm:pt-1 line-clamp-3 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.share_comment }}
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap">
|
||||
<div class="flex align-center">
|
||||
<IconBtn v-bind="props" icon="mdi-account" color="white" class="me-1" />
|
||||
<div class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.share_user }}
|
||||
</div>
|
||||
<IconBtn v-if="props.media?.count" icon="mdi-fire" color="white" class="me-1" />
|
||||
<span v-if="props.media?.count" class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.count.toLocaleString() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap">
|
||||
<div class="flex align-center">
|
||||
<IconBtn v-bind="props" icon="mdi-account" color="white" class="me-1" />
|
||||
<div class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.share_user }}
|
||||
</div>
|
||||
<IconBtn v-if="props.media?.count" icon="mdi-fire" color="white" class="me-1" />
|
||||
<span v-if="props.media?.count" class="text-subtitle-2 me-4 text-white">
|
||||
{{ props.media?.count.toLocaleString() }}
|
||||
</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
||||
<VIcon icon="mdi-calcdar" class="me-1" />
|
||||
{{ dateText }}
|
||||
</VCardText>
|
||||
</div>
|
||||
</VCard>
|
||||
</VCardText>
|
||||
<VCardText class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
|
||||
<VIcon icon="mdi-calcdar" class="me-1" />
|
||||
{{ dateText }}
|
||||
</VCardText>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
</VHover>
|
||||
<!-- 订阅编辑弹窗 -->
|
||||
|
||||
@@ -28,17 +28,33 @@ async function handleReset() {
|
||||
const result: { [key: string]: any } = await api.get('/storage/reset/alist')
|
||||
if (result.success) {
|
||||
// 重置成功
|
||||
alertType.value = 'success'
|
||||
handleDone()
|
||||
} else {
|
||||
alertType.value = 'error'
|
||||
text.value = result.message
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 登录类型
|
||||
let loginType = ref('username')
|
||||
if (props.conf.token) {
|
||||
loginType = ref('token')
|
||||
} else if (props.conf.username) {
|
||||
loginType = ref('username')
|
||||
} else {
|
||||
loginType = ref('guest')
|
||||
}
|
||||
|
||||
// 数据源
|
||||
const sourceItems = [
|
||||
{
|
||||
'title': t('dialog.alistConfig.loginTypeOptions.username'),
|
||||
'value': 'username',
|
||||
},
|
||||
{ 'title': t('dialog.alistConfig.loginTypeOptions.token'), 'value': 'token' },
|
||||
{ 'title': t('dialog.alistConfig.loginTypeOptions.guest'), 'value': 'guest' },
|
||||
]
|
||||
|
||||
// 保存alist设置
|
||||
async function savaAlistConfig() {
|
||||
try {
|
||||
@@ -63,7 +79,16 @@ async function savaAlistConfig() {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
v-model="loginType"
|
||||
:items="sourceItems"
|
||||
:label="t('dialog.alistConfig.loginType')"
|
||||
:hint="t('dialog.alistConfig.loginType')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4" v-if="loginType == 'username'">
|
||||
<VTextField
|
||||
v-model="props.conf.username"
|
||||
:hint="t('dialog.alistConfig.username')"
|
||||
@@ -71,7 +96,7 @@ async function savaAlistConfig() {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VCol cols="12" md="4" v-if="loginType == 'username'">
|
||||
<VTextField
|
||||
type="password"
|
||||
v-model="props.conf.password"
|
||||
@@ -80,6 +105,14 @@ async function savaAlistConfig() {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="8" v-if="loginType == 'token'">
|
||||
<VTextField
|
||||
v-model="props.conf.token"
|
||||
:hint="t('dialog.alistConfig.loginTypeOptions.token')"
|
||||
:label="t('dialog.alistConfig.loginTypeOptions.token')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
|
||||
@@ -23,6 +23,9 @@ const emit = defineEmits(['close'])
|
||||
// 订阅文件信息
|
||||
const subScribeInfo = ref<SubscrbieInfo>()
|
||||
|
||||
// 是否加载中
|
||||
const loading = ref(false)
|
||||
|
||||
// 下载文件表头
|
||||
const downloadHeaders = [
|
||||
{ title: t('dialog.subscribeFiles.episodeColumn'), key: 'episode_number', sortable: true },
|
||||
@@ -39,9 +42,12 @@ const libraryHeaders = [
|
||||
// 调用API查询订阅文件信息
|
||||
async function loadSubscribeFilesInfo() {
|
||||
try {
|
||||
loading.value = true
|
||||
subScribeInfo.value = await api.get(`subscribe/files/${props.subid}`)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +90,8 @@ onBeforeMount(() => {
|
||||
<VCardItem class="my-2">
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<LoadingBanner v-if="loading" />
|
||||
<VCardText v-else>
|
||||
<div class="media-page">
|
||||
<div class="media-header">
|
||||
<div class="media-poster">
|
||||
|
||||
@@ -203,7 +203,7 @@ onMounted(() => {
|
||||
</VCard>
|
||||
</VMenu>
|
||||
<!-- 名称测试弹窗 -->
|
||||
<VDialog v-if="nameTestDialog" v-model="nameTestDialog" max-width="35rem" scrollable>
|
||||
<VDialog v-if="nameTestDialog" v-model="nameTestDialog" max-width="45rem" scrollable>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
@@ -298,7 +298,7 @@ onMounted(() => {
|
||||
<VDialog
|
||||
v-if="messageDialog"
|
||||
v-model="messageDialog"
|
||||
max-width="40rem"
|
||||
max-width="50rem"
|
||||
scrollable
|
||||
:fullscreen="!display.mdAndUp.value"
|
||||
ref="messageDialogRef"
|
||||
|
||||
@@ -1011,6 +1011,8 @@ export default {
|
||||
scrapFollowTmdb: 'Follow TMDB Recognition',
|
||||
scrapFollowTmdbHint:
|
||||
'When turned off, organization history will be used (if available) to avoid TMDB data changes during subscription',
|
||||
scrapOriginalImage: 'Scrap TheMovieDb Original Language Image',
|
||||
scrapOriginalImageHint: 'Scrap original language image from themoviedb, otherwise scrap metadata language image',
|
||||
fanartEnable: 'Fanart Image Data Source',
|
||||
fanartEnableHint: 'Use image data from fanart.tv',
|
||||
githubProxy: 'Github Acceleration Proxy',
|
||||
|
||||
@@ -1005,6 +1005,8 @@ export default {
|
||||
metaCacheExpireMin: '元数据缓存时间必须大于等于0',
|
||||
scrapFollowTmdb: '跟随TMDB识别整理',
|
||||
scrapFollowTmdbHint: '关闭时以整理历史记录为准(如有),避免TMDB数据在订阅中途修改',
|
||||
scrapOriginalImage: 'TMDB 刮削原语种图片',
|
||||
scrapOriginalImageHint: '刮削原语种图片,否则刮削元数据语种图片',
|
||||
fanartEnable: 'Fanart图片数据源',
|
||||
fanartEnableHint: '使用 fanart.tv 的图片数据',
|
||||
githubProxy: 'Github加速代理',
|
||||
@@ -1491,7 +1493,7 @@ export default {
|
||||
loginTypeOptions: {
|
||||
guest: '访客',
|
||||
username: '用户名密码',
|
||||
token: 'Token',
|
||||
token: '令牌',
|
||||
},
|
||||
complete: '完成',
|
||||
reset: '重置',
|
||||
|
||||
@@ -1007,6 +1007,8 @@ export default {
|
||||
metaCacheExpireMin: '元數據緩存時間必須大於等於0',
|
||||
scrapFollowTmdb: '跟隨TMDB識別整理',
|
||||
scrapFollowTmdbHint: '關閉時以整理歷史記錄為準(如有),避免TMDB數據在訂閱中途修改',
|
||||
scrapOriginalImage: 'TMDB 刮削原語种圖片',
|
||||
scrapOriginalImageHint: '刮削原語种圖片,否则數據元数据語种圖片',
|
||||
fanartEnable: 'Fanart圖片數據源',
|
||||
fanartEnableHint: '使用 fanart.tv 的圖片數據',
|
||||
githubProxy: 'Github加速代理',
|
||||
|
||||
@@ -48,6 +48,9 @@ const currentLocale = ref(getCurrentLocale())
|
||||
// 可用的语言列表
|
||||
const locales = Object.values(SUPPORTED_LOCALES)
|
||||
|
||||
// 登录按钮 loading
|
||||
const loading = ref(false)
|
||||
|
||||
// 切换语言
|
||||
async function switchLanguage(locale: SupportedLocale) {
|
||||
await setI18nLanguage(locale)
|
||||
@@ -103,6 +106,8 @@ async function afterLogin(superuser: boolean) {
|
||||
router.push(authStore.originalPath ?? '/')
|
||||
// 订阅推送通知
|
||||
if (superuser) await subscribeForPushNotifications()
|
||||
// 登录按钮 loading
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 登录获取token事件
|
||||
@@ -113,6 +118,10 @@ function login() {
|
||||
if (!form.value.username || !form.value.password || (isOTP.value && !form.value.otp_password)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 登录按钮 loading
|
||||
loading.value = true
|
||||
|
||||
// 用户名密码
|
||||
const formData = new FormData()
|
||||
|
||||
@@ -155,6 +164,8 @@ function login() {
|
||||
else if (error.response.status === 403) errorMessage.value = t('login.permissionDenied')
|
||||
else if (error.response.status === 500) errorMessage.value = t('login.serverError')
|
||||
else errorMessage.value = `${t('login.loginFailed')} ${error.response.status},${t('login.checkCredentials')}`
|
||||
// 登录按钮 loading
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -252,7 +263,9 @@ onMounted(async () => {
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<!-- login button -->
|
||||
<VBtn block type="submit" @click="login" prepend-icon="mdi-login"> {{ t('login.login') }} </VBtn>
|
||||
<VBtn block type="submit" @click="login" prepend-icon="mdi-login" :loading="loading">
|
||||
{{ t('login.login') }}
|
||||
</VBtn>
|
||||
<VAlert v-if="errorMessage" type="error" variant="tonal" class="mt-3">
|
||||
{{ errorMessage }}
|
||||
</VAlert>
|
||||
|
||||
@@ -115,12 +115,14 @@ html.v-overlay-scroll-blocked {
|
||||
|
||||
// 美化滚动条
|
||||
::-webkit-scrollbar {
|
||||
block-size: 8px;
|
||||
inline-size: 8px;
|
||||
block-size: 4px;
|
||||
inline-size: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
border-radius: 2px;
|
||||
background: rgb(var(--v-theme-perfect-scrollbar-thumb));
|
||||
box-shadow: inset 0 0 10px rgba(0,0,0,20%);
|
||||
|
||||
@@ -131,6 +133,16 @@ html.v-overlay-scroll-blocked {
|
||||
}
|
||||
}
|
||||
|
||||
// 当鼠标悬停在可滚动元素上时显示滚动条
|
||||
*:hover::-webkit-scrollbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 当元素正在滚动时显示滚动条
|
||||
*:active::-webkit-scrollbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.v-alert--variant-elevated, .v-alert--variant-flat {
|
||||
background: rgb(var(--v-table-header-background));
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
|
||||
@@ -12,6 +12,20 @@ interface RemoteModule {
|
||||
url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个远程模块信息
|
||||
* @param id 远程模块ID
|
||||
*/
|
||||
async function fetchSingleRemoteModule(id: string): Promise<RemoteModule | null> {
|
||||
try {
|
||||
const modules = await fetchRemoteModules()
|
||||
return modules.find(module => module.id === id) || null
|
||||
} catch (error) {
|
||||
console.error(`获取远程模块信息失败: ${id}`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载远程组件
|
||||
* @param id 远程模块ID
|
||||
@@ -22,8 +36,24 @@ export async function loadRemoteComponent(id: string, componentName: string = 'P
|
||||
const module = await __federation_method_getRemote(id, `./${componentName}`)
|
||||
return __federation_method_unwrapDefault(module)
|
||||
} catch (error) {
|
||||
console.error(`加载远程组件失败: ${id}/${componentName}`, error)
|
||||
throw error
|
||||
// 组件未注册,尝试重新注册
|
||||
try {
|
||||
const moduleInfo = await fetchSingleRemoteModule(id)
|
||||
if (moduleInfo) {
|
||||
console.log(`组件未注册,正在重新注册: ${id}`)
|
||||
injectRemoteModule(moduleInfo)
|
||||
|
||||
// 重新尝试加载组件
|
||||
const module = await __federation_method_getRemote(id, `./${componentName}`)
|
||||
return __federation_method_unwrapDefault(module)
|
||||
} else {
|
||||
console.error(`无法找到远程模块信息: ${id}`)
|
||||
throw new Error(`无法找到远程模块信息: ${id}`)
|
||||
}
|
||||
} catch (retryError) {
|
||||
console.error(`重新注册并加载组件失败: ${id}/${componentName}`, retryError)
|
||||
throw retryError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ const SystemSettings = ref<any>({
|
||||
META_CACHE_EXPIRE: 0,
|
||||
SCRAP_FOLLOW_TMDB: true,
|
||||
FANART_ENABLE: false,
|
||||
TMDB_SCRAP_ORIGINAL_IMAGE: null,
|
||||
// 网络
|
||||
PROXY_HOST: null,
|
||||
GITHUB_PROXY: null,
|
||||
@@ -740,6 +741,14 @@ onDeactivated(() => {
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.TMDB_SCRAP_ORIGINAL_IMAGE"
|
||||
:label="t('setting.system.scrapOriginalImage')"
|
||||
:hint="t('setting.system.scrapOriginalImageHint')"
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Advanced.FANART_ENABLE"
|
||||
|
||||
@@ -37,6 +37,9 @@ const filterForm: Record<string, string[]> = reactive({
|
||||
|
||||
// 排序选项
|
||||
const sortField = ref('default')
|
||||
// 降序
|
||||
const sortType = ref<'asc' | 'desc'>('desc')
|
||||
|
||||
const sortTitles: Record<string, string> = {
|
||||
default: t('torrent.sortDefault'),
|
||||
site: t('torrent.sortSite'),
|
||||
@@ -212,7 +215,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
// 修改watch监听,同时监听排序字段的变化
|
||||
watch([filterForm, groupedDataList, sortField], filterData)
|
||||
watch([filterForm, groupedDataList, sortField, sortType], filterData)
|
||||
|
||||
function filterData() {
|
||||
// 清空列表
|
||||
@@ -258,16 +261,30 @@ function filterData() {
|
||||
// 排序数据
|
||||
if (sortField.value !== 'default') {
|
||||
filteredData.sort((a, b) => {
|
||||
if (sortField.value === 'site') {
|
||||
// 按站点名称排序
|
||||
return (a.torrent_info.site_name || '').localeCompare(b.torrent_info.site_name || '')
|
||||
} else if (sortField.value === 'size') {
|
||||
// 按文件大小排序(降序)
|
||||
return (Number(b.torrent_info.size) || 0) - (Number(a.torrent_info.size) || 0)
|
||||
} else if (sortField.value === 'seeder') {
|
||||
// 按做种数排序(降序)
|
||||
return (Number(b.torrent_info.seeders) || 0) - (Number(a.torrent_info.seeders) || 0)
|
||||
if (sortType.value === 'desc') {
|
||||
if (sortField.value === 'site') {
|
||||
// 按站点名称排序
|
||||
return (a.torrent_info.site_name || '').localeCompare(b.torrent_info.site_name || '')
|
||||
} else if (sortField.value === 'size') {
|
||||
// 按文件大小排序(降序)
|
||||
return (Number(b.torrent_info.size) || 0) - (Number(a.torrent_info.size) || 0)
|
||||
} else if (sortField.value === 'seeder') {
|
||||
// 按做种数排序(降序)
|
||||
return (Number(b.torrent_info.seeders) || 0) - (Number(a.torrent_info.seeders) || 0)
|
||||
}
|
||||
} else {
|
||||
if (sortField.value === 'site') {
|
||||
// 按站点名称排序
|
||||
return (b.torrent_info.site_name || '').localeCompare(a.torrent_info.site_name || '')
|
||||
} else if (sortField.value === 'size') {
|
||||
// 按文件大小排序(降序)
|
||||
return (Number(a.torrent_info.size) || 0) - (Number(b.torrent_info.size) || 0)
|
||||
} else if (sortField.value === 'seeder') {
|
||||
// 按做种数排序(降序)
|
||||
return (Number(a.torrent_info.seeders) || 0) - (Number(b.torrent_info.seeders) || 0)
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
}
|
||||
@@ -364,6 +381,12 @@ function loadMore({ done }: { done: any }) {
|
||||
displayDataList.value.push(...itemsToMove)
|
||||
done('ok')
|
||||
}
|
||||
|
||||
// 处理图标点击
|
||||
const handleSortIconClick = () => {
|
||||
// 切换排序方向
|
||||
sortType.value = sortType.value === 'asc' ? 'desc' : 'asc'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -384,9 +407,15 @@ function loadMore({ done }: { done: any }) {
|
||||
density="compact"
|
||||
hide-details
|
||||
class="sort-select"
|
||||
prepend-icon="mdi-sort"
|
||||
variant="plain"
|
||||
></VSelect>
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<!-- 添加排序点击事件 -->
|
||||
<VIcon @mousedown.stop.prevent="handleSortIconClick">
|
||||
{{ sortType === 'asc' ? 'mdi-sort-ascending' : 'mdi-sort-descending' }}
|
||||
</VIcon>
|
||||
</template>
|
||||
</VSelect>
|
||||
</div>
|
||||
|
||||
<!-- 筛选按钮组 -->
|
||||
@@ -521,9 +550,15 @@ function loadMore({ done }: { done: any }) {
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mobile-sort-select"
|
||||
prepend-icon="mdi-sort"
|
||||
variant="plain"
|
||||
></VSelect>
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<!-- 添加排序点击事件 -->
|
||||
<VIcon @mousedown.stop.prevent="handleSortIconClick">
|
||||
{{ sortType === 'asc' ? 'mdi-sort-ascending' : 'mdi-sort-descending' }}
|
||||
</VIcon>
|
||||
</template>
|
||||
</VSelect>
|
||||
</div>
|
||||
|
||||
<!-- 筛选图标按钮区域 -->
|
||||
@@ -813,7 +848,7 @@ function loadMore({ done }: { done: any }) {
|
||||
|
||||
.mobile-sort-select {
|
||||
max-inline-size: 130px;
|
||||
min-inline-size: 110px;
|
||||
min-inline-size: 80px;
|
||||
}
|
||||
|
||||
.filter-buttons-grid {
|
||||
|
||||
@@ -61,6 +61,8 @@ const filterOptions: Record<string, string[]> = reactive({
|
||||
|
||||
// 排序字段
|
||||
const sortField = ref('default')
|
||||
// 降序
|
||||
const sortType = ref<'asc' | 'desc'>('desc')
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<Array<Context>>([])
|
||||
@@ -202,7 +204,7 @@ function sortSeasonOptions() {
|
||||
}
|
||||
|
||||
// 修改watch监听,同时监听排序字段的变化
|
||||
watch([filterForm, sortField], filterData)
|
||||
watch([filterForm, sortField, sortType], filterData)
|
||||
|
||||
// 计算过滤后的列表
|
||||
function filterData() {
|
||||
@@ -247,16 +249,30 @@ function filterData() {
|
||||
})
|
||||
|
||||
// 排序
|
||||
if (sortField.value === 'default') {
|
||||
filteredData = filteredData.sort((a, b) => b.torrent_info.pri_order - a.torrent_info.pri_order)
|
||||
} else if (sortField.value === 'site') {
|
||||
filteredData = filteredData.sort((a, b) =>
|
||||
(a.torrent_info.site_name || '').localeCompare(b.torrent_info.site_name || ''),
|
||||
)
|
||||
} else if (sortField.value === 'size') {
|
||||
filteredData = filteredData.sort((a, b) => b.torrent_info.size - a.torrent_info.size)
|
||||
} else if (sortField.value === 'seeder') {
|
||||
filteredData = filteredData.sort((a, b) => b.torrent_info.seeders - a.torrent_info.seeders)
|
||||
if (sortType.value === 'desc') {
|
||||
if (sortField.value === 'default') {
|
||||
filteredData = filteredData.sort((a, b) => b.torrent_info.pri_order - a.torrent_info.pri_order)
|
||||
} else if (sortField.value === 'site') {
|
||||
filteredData = filteredData.sort((a, b) =>
|
||||
(a.torrent_info.site_name || '').localeCompare(b.torrent_info.site_name || ''),
|
||||
)
|
||||
} else if (sortField.value === 'size') {
|
||||
filteredData = filteredData.sort((a, b) => b.torrent_info.size - a.torrent_info.size)
|
||||
} else if (sortField.value === 'seeder') {
|
||||
filteredData = filteredData.sort((a, b) => b.torrent_info.seeders - a.torrent_info.seeders)
|
||||
}
|
||||
} else {
|
||||
if (sortField.value === 'default') {
|
||||
filteredData = filteredData.sort((a, b) => a.torrent_info.pri_order - b.torrent_info.pri_order)
|
||||
} else if (sortField.value === 'site') {
|
||||
filteredData = filteredData.sort((a, b) =>
|
||||
(b.torrent_info.site_name || '').localeCompare(a.torrent_info.site_name || ''),
|
||||
)
|
||||
} else if (sortField.value === 'size') {
|
||||
filteredData = filteredData.sort((a, b) => a.torrent_info.size - b.torrent_info.size)
|
||||
} else if (sortField.value === 'seeder') {
|
||||
filteredData = filteredData.sort((a, b) => a.torrent_info.seeders - b.torrent_info.seeders)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示前20个
|
||||
@@ -338,6 +354,12 @@ function loadMore({ done }: { done: any }) {
|
||||
done('ok')
|
||||
}
|
||||
|
||||
// 处理图标点击
|
||||
const handleSortIconClick = () => {
|
||||
// 切换排序方向
|
||||
sortType.value = sortType.value === 'asc' ? 'desc' : 'asc'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
filterData()
|
||||
})
|
||||
@@ -363,9 +385,14 @@ onMounted(() => {
|
||||
density="compact"
|
||||
hide-details
|
||||
class="sort-select"
|
||||
prepend-icon="mdi-sort"
|
||||
variant="plain"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<!-- 添加排序点击事件 -->
|
||||
<VIcon @mousedown.stop.prevent="handleSortIconClick">
|
||||
{{ sortType === 'asc' ? 'mdi-sort-ascending' : 'mdi-sort-descending' }}
|
||||
</VIcon>
|
||||
</template>
|
||||
</VSelect>
|
||||
<div class="filter-divider"></div>
|
||||
|
||||
@@ -499,9 +526,15 @@ onMounted(() => {
|
||||
density="compact"
|
||||
hide-details
|
||||
class="mobile-sort-select"
|
||||
prepend-icon="mdi-sort"
|
||||
variant="plain"
|
||||
></VSelect>
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<!-- 添加排序点击事件 -->
|
||||
<VIcon @mousedown.stop.prevent="handleSortIconClick">
|
||||
{{ sortType === 'asc' ? 'mdi-sort-ascending' : 'mdi-sort-descending' }}
|
||||
</VIcon>
|
||||
</template>
|
||||
</VSelect>
|
||||
</div>
|
||||
|
||||
<!-- 筛选图标按钮区域 -->
|
||||
@@ -841,7 +874,7 @@ onMounted(() => {
|
||||
|
||||
.mobile-sort-select {
|
||||
max-inline-size: 130px;
|
||||
min-inline-size: 110px;
|
||||
min-inline-size: 80px;
|
||||
}
|
||||
|
||||
.all-filters-grid {
|
||||
|
||||
@@ -35,6 +35,7 @@ export default defineConfig({
|
||||
federation({
|
||||
name: 'MoviePilot',
|
||||
filename: 'remoteEntry.js',
|
||||
// @ts-ignore
|
||||
remotes: {
|
||||
// 动态remotes将在运行时注入
|
||||
dummy: {
|
||||
@@ -171,8 +172,8 @@ export default defineConfig({
|
||||
minify: 'terser',
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: false,
|
||||
drop_debugger: false,
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
},
|
||||
},
|
||||
chunkSizeWarningLimit: 5000,
|
||||
|
||||
Reference in New Issue
Block a user