Compare commits

...

34 Commits

Author SHA1 Message Date
jxxghp
d997dc0394 优化登录页面,添加登录按钮的加载状态管理,确保用户体验流畅。 2025-05-13 23:28:03 +08:00
jxxghp
6b6353ed41 优化 App.vue 中的背景图片加载逻辑,调整异步加载方式并简化图片地址获取逻辑。 2025-05-13 19:25:41 +08:00
jxxghp
e73d906564 fix #333 2025-05-13 19:14:13 +08:00
jxxghp
7e3e850e21 Merge pull request #333 from Seed680/v2 2025-05-13 18:46:23 +08:00
qiaoyun680
56b2dc4ebf 资源搜索结果页面增加排序切换 2025-05-13 16:25:22 +08:00
jxxghp
9444b0e518 优化 App.vue 中的背景图片加载逻辑,添加登录状态变化时清空背景图片数组的处理,并更新图片地址获取逻辑以支持缓存和原始地址的选择。 2025-05-13 13:37:37 +08:00
jxxghp
bcb72118f5 在背景图片加载失败时添加重试机制,3秒后自动重试加载背景图片 2025-05-13 08:18:44 +08:00
jxxghp
c59be8d981 更新 module-federation-guide.md 2025-05-12 18:03:09 +08:00
jxxghp
8466a40455 重构 App.vue 中的背景图片加载逻辑 2025-05-12 13:51:45 +08:00
jxxghp
f435b4fc52 在 fetchBackgroundImages 函数中初始化 activeImageIndex 为 0,以确保背景图片加载时的索引正确。 2025-05-12 11:23:48 +08:00
jxxghp
5686c6fe65 在构建工作流中移除了删除标签的选项,以简化发布流程。 2025-05-12 11:09:49 +08:00
jxxghp
6810112eda 在 App.vue 中添加登录状态变化的监听,确保登录后重新加载背景图片;同时更新 .vscode/settings.json,增加 i18n-ally.localesPaths 配置。 2025-05-12 10:44:01 +08:00
jxxghp
11a2d07935 优化 App.vue 中的国际化代码,调整 LoadingBanner 组件的样式,增加 SubscribeFilesDialog 组件的加载状态管理。 2025-05-12 07:56:52 +08:00
jxxghp
02cd2f1570 在构建工作流中添加了删除标签和发布的选项,并设置在出错时继续执行。 2025-05-11 08:44:30 +08:00
jxxghp
924c1d72ea 优化自定义滚动条样式 2025-05-11 08:41:48 +08:00
jxxghp
5d9b2e1919 更新 AlistConfigDialog.vue 2025-05-11 07:56:50 +08:00
jxxghp
f7fa440f9a 更新 AlistConfigDialog.vue 2025-05-10 23:31:49 +08:00
jxxghp
d4aaa46968 优化 SubscribeCard 和 SubscribeShareCard 组件的结构 2025-05-10 23:18:00 +08:00
jxxghp
93ac5e1b3b 优化 PluginAppCard 和 PluginCard 组件的背景样式,更新渐变效果以增强视觉层次感。 2025-05-10 22:38:19 +08:00
jxxghp
c7a8c68e14 调整 PluginCard 组件的背景样式,优化渐变效果以提升视觉效果。 2025-05-10 22:25:53 +08:00
jxxghp
77afb4d736 优化 PluginAppCard 和 PluginCard 组件的背景样式 2025-05-10 22:10:02 +08:00
jxxghp
141796ab24 更新 AccountSettingSystem.vue 中的 v-model,修改为 TMDB_SCRAP_ORIGINAL_IMAGE,以更准确地反映设置项。 2025-05-10 21:58:13 +08:00
jxxghp
30d733f55d v2.4.6 2025-05-10 21:54:32 +08:00
jxxghp
6a39e65b6b 添加 TMDB 刮削原语种选项。 2025-05-10 21:45:45 +08:00
jxxghp
c27013b7ad Merge pull request #332 from Seed680/v2 2025-05-10 21:24:45 +08:00
jxxghp
582ce496fa 添加 TMDB 刮削图片语言相关 2025-05-10 20:44:06 +08:00
jxxghp
5b4dbb82d5 调整 ShortcutBar 组件中的对话框最大宽度 2025-05-10 19:33:57 +08:00
jxxghp
011a0d16ab 加载远程组件时如未注册则重新注册 2025-05-10 08:40:14 +08:00
jxxghp
ac5539194d 优化 PersonCard 组件,移除多余的样式类以简化结构 2025-05-09 20:29:54 +08:00
Seed680
6b7e1b3c4e Merge branch 'jxxghp:v2' into v2 2025-05-09 09:21:10 +08:00
jxxghp
30c3d00139 移除 Vite 配置中的手动分块选项,简化配置以提升可读性和维护性。 2025-05-09 00:06:58 +08:00
jxxghp
36d460cd74 更新 Vite 配置,启用压缩选项以移除控制台日志和调试器, 2025-05-09 00:04:14 +08:00
Seed680
11cb2eb0f8 Merge branch 'jxxghp:v2' into v2 2025-05-08 23:28:18 +08:00
qiaoyun680
4dce1c94a3 feat(storge): 添加alist存储的登录方式(令牌、访客) 2025-05-08 23:26:54 +08:00
24 changed files with 450 additions and 264 deletions

View File

@@ -44,6 +44,7 @@ jobs:
- name: Delete Release - name: Delete Release
uses: dev-drprasad/delete-tag-and-release@v1.1 uses: dev-drprasad/delete-tag-and-release@v1.1
continue-on-error: true
with: with:
tag_name: ${{ env.frontend_version }} tag_name: ${{ env.frontend_version }}
delete_release: true delete_release: true

View File

@@ -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"
]
}

View File

@@ -9,7 +9,7 @@ MoviePilot前端采用模块联邦(Module Federation)技术实现插件的动态
## 2. 技术要求 ## 2. 技术要求
- Node.js 16+ - Node.js 20+
- Vue 3 - Vue 3
- Vite 4+ - Vite 4+
- TypeScript 5+ - TypeScript 5+
@@ -80,13 +80,6 @@ export default defineConfig({
target: 'esnext', // 必须设置为esnext以支持顶层await target: 'esnext', // 必须设置为esnext以支持顶层await
minify: false, // 开发阶段建议关闭混淆 minify: false, // 开发阶段建议关闭混淆
cssCodeSplit: true, // 改为true以便能分离样式文件 cssCodeSplit: true, // 改为true以便能分离样式文件
rollupOptions: {
output: {
manualChunks: {
'vuetify-lib': ['vuetify'] // 将vuetify单独分离出来
}
}
}
}, },
css: { css: {
preprocessorOptions: { preprocessorOptions: {

View File

@@ -1,6 +1,6 @@
{ {
"name": "moviepilot", "name": "moviepilot",
"version": "2.4.5", "version": "2.4.6",
"private": true, "private": true,
"type": "module", "type": "module",
"bin": "dist/service.js", "bin": "dist/service.js",

View File

@@ -7,7 +7,7 @@ const props = defineProps({
</script> </script>
<template> <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.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" /> <VProgressCircular v-if="props.progress" class="mb-3" color="primary" :model-value="props.progress" size="64" />
<span>{{ props.text }}</span> <span>{{ props.text }}</span>

View File

@@ -4,13 +4,9 @@ import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
import { ensureRenderComplete, removeEl } from './@core/utils/dom' import { ensureRenderComplete, removeEl } from './@core/utils/dom'
import api from '@/api' import api from '@/api'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useI18n } from 'vue-i18n'
import { getBrowserLocale, setI18nLanguage } from './plugins/i18n' import { getBrowserLocale, setI18nLanguage } from './plugins/i18n'
import { SupportedLocale } from '@/types/i18n' import { SupportedLocale } from '@/types/i18n'
// 国际化
const { t } = useI18n()
// 生效主题 // 生效主题
const { global: globalTheme } = useTheme() const { global: globalTheme } = useTheme()
let themeValue = localStorage.getItem('theme') || 'light' let themeValue = localStorage.getItem('theme') || 'light'
@@ -21,9 +17,6 @@ globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
const localeValue = getBrowserLocale() const localeValue = getBrowserLocale()
setI18nLanguage(localeValue as SupportedLocale) setI18nLanguage(localeValue as SupportedLocale)
// 从 provide 中获取全局设置
const globalSettings: any = inject('globalSettings')
// 显示状态 // 显示状态
const show = ref(false) const show = ref(false)
@@ -31,6 +24,9 @@ const show = ref(false)
const authStore = useAuthStore() const authStore = useAuthStore()
const isLogin = computed(() => authStore.token) const isLogin = computed(() => authStore.token)
// 生成背景图片key
const loginStateKey = computed(() => (isLogin.value ? 'logged-in' : 'logged-out'))
// 背景图片 // 背景图片
const backgroundImages = ref<string[]>([]) const backgroundImages = ref<string[]>([])
const activeImageIndex = ref(0) const activeImageIndex = ref(0)
@@ -78,6 +74,7 @@ function updateHtmlThemeAttribute(themeName: string) {
async function fetchBackgroundImages() { async function fetchBackgroundImages() {
try { try {
backgroundImages.value = await api.get(`/login/wallpapers`) backgroundImages.value = await api.get(`/login/wallpapers`)
activeImageIndex.value = 0
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@@ -85,6 +82,7 @@ async function fetchBackgroundImages() {
// 开始背景图片轮换 // 开始背景图片轮换
function startBackgroundRotation() { function startBackgroundRotation() {
// 清除轮换定时器
if (backgroundRotationTimer) clearInterval(backgroundRotationTimer) if (backgroundRotationTimer) clearInterval(backgroundRotationTimer)
if (backgroundImages.value.length > 1) { if (backgroundImages.value.length > 1) {
@@ -106,7 +104,6 @@ function startBackgroundRotation() {
function preloadImage(url: string): Promise<boolean> { function preloadImage(url: string): Promise<boolean> {
return new Promise(resolve => { return new Promise(resolve => {
const img = new Image() const img = new Image()
const imageUrl = getImgUrl(url)
img.onload = () => resolve(true) img.onload = () => resolve(true)
img.onerror = () => resolve(false) img.onerror = () => resolve(false)
@@ -117,7 +114,7 @@ function preloadImage(url: string): Promise<boolean> {
resolve(false) resolve(false)
}, 5000) // 5秒超时 }, 5000) // 5秒超时
img.src = imageUrl img.src = url
// 如果图片已经缓存onload可能不会触发 // 如果图片已经缓存onload可能不会触发
if (img.complete) { 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动画效果并延迟移除加载界面 // 添加logo动画效果并延迟移除加载界面
function animateAndRemoveLoader() { function animateAndRemoveLoader() {
const loadingBg = document.querySelector('#loading-bg') as HTMLElement 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属性 // 初始化data-theme属性
updateHtmlThemeAttribute(globalTheme.name.value) updateHtmlThemeAttribute(globalTheme.name.value)
// 加载背景图片并开始轮换 // 默认隐藏页面
fetchBackgroundImages().then(() => startBackgroundRotation()) show.value = false
// 添加页面可见性变化监听 // 加载背景图片
document.addEventListener('visibilitychange', handleVisibilityChange) await loadBackgroundImages()
// 移除加载动画
ensureRenderComplete(() => { ensureRenderComplete(() => {
nextTick(() => { nextTick(() => {
setTimeout(() => { setTimeout(() => {
// 移除加载动画 // 移除加载动画,显示页面
animateAndRemoveLoader() animateAndRemoveLoader()
}, 1500) }, 1500)
}) })
}) })
// 添加页面可见性变化监听
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
loadBackgroundImages()
}
})
// 添加PWA的页面恢复事件监听
window.addEventListener('pageshow', event => {
// persisted属性为true表示页面是从bfcache中恢复的
if (event.persisted) {
loadBackgroundImages()
}
})
}) })
onUnmounted(() => { onUnmounted(() => {
// 移除页面可见性监听 // 移除页面可见性监听
document.removeEventListener('visibilitychange', handleVisibilityChange) document.removeEventListener('visibilitychange', () => {})
// 移除PWA的页面恢复事件监听
window.removeEventListener('pageshow', () => {})
// 清除轮换定时器 // 清除轮换定时器
if (backgroundRotationTimer) { if (backgroundRotationTimer) {
@@ -202,20 +209,18 @@ onUnmounted(() => {
<template> <template>
<div class="app-wrapper"> <div class="app-wrapper">
<!-- 透明主题背景 --> <!-- 透明主题背景 -->
<template v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)"> <div v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)" class="background-container">
<div class="background-container"> <div
<div v-for="(imageUrl, index) in backgroundImages"
v-for="(imageUrl, index) in backgroundImages" :key="`bg-${index}-${loginStateKey}`"
:key="index" class="background-image"
class="background-image" :class="{ 'active': index === activeImageIndex }"
:class="{ 'active': index === activeImageIndex }" :style="{ 'backgroundImage': `url(${imageUrl})` }"
:style="{ backgroundImage: `url(${getImgUrl(imageUrl)})` }" ></div>
></div> <!-- 全局磨砂层 -->
<!-- 全局磨砂层 --> <div v-if="isLogin && isTransparentTheme" class="global-blur-layer"></div>
<div v-if="isLogin" class="global-blur-layer"></div> </div>
</div> <!-- 页面内容 -->
</template>
<VApp v-show="show" :class="{ 'transparent-app': isTransparentTheme }"> <VApp v-show="show" :class="{ 'transparent-app': isTransparentTheme }">
<RouterView /> <RouterView />
</VApp> </VApp>

View File

@@ -82,9 +82,7 @@ function goPersonDetail() {
}" }"
@click.stop="goPersonDetail" @click.stop="goPersonDetail"
> >
<div <div class="person-card relative cursor-pointer ring-gray-700">
class="person-card relative transform-gpu cursor-pointer rounded transition duration-150 ease-in-out scale-100 ring-gray-700"
>
<div style="padding-block-end: 150%"> <div style="padding-block-end: 150%">
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2"> <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"> <div class="relative mt-2 mb-4 flex h-1/2 w-full justify-center">

View File

@@ -168,12 +168,8 @@ const dropdownItems = ref([
> >
<div <div
class="relative flex flex-row items-start pa-3 justify-between grow" 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"> <div class="relative flex-1 min-w-0">
<VCardTitle <VCardTitle
class="text-white text-lg px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis ..." class="text-white text-lg px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis ..."

View File

@@ -345,12 +345,8 @@ watch(
> >
<div <div
class="relative flex flex-row items-start pa-3 justify-between grow" 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"> <div class="relative flex-1 min-w-0">
<VCardTitle class="text-white text-lg px-2 text-shadow whitespace-nowrap overflow-hidden text-ellipsis"> <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" /> <VBadge v-if="props.plugin?.state" dot inline color="success" />

View File

@@ -296,99 +296,109 @@ function onSubscribeEditRemove() {
<div> <div>
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <div
v-bind="hover.props" class="w-full h-full rounded-lg overflow-hidden"
:key="props.media?.id"
class="flex flex-col h-full"
:class="{ :class="{
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering, '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"> <VCard
<IconBtn> v-bind="hover.props"
<VIcon icon="mdi-dots-vertical" color="white" /> :key="props.media?.id"
<VMenu activator="parent" close-on-content-click> class="flex flex-col h-full"
<VList> :class="{
<template v-for="(item, i) in dropdownItems" :key="i"> 'opacity-70': subscribeState === 'S',
<VListItem v-if="item.show !== false" :base-color="item.props.color" @click="item.props.click"> }"
<template #prepend> rounded="0"
<VIcon :icon="item.props.prependIcon" /> min-height="170"
</template> @click="editSubscribeDialog"
<VListItemTitle v-text="item.title" /> :ripple="false"
</VListItem> >
</template> <div class="me-n3 absolute top-1 right-4">
</VList> <IconBtn>
</VMenu> <VIcon icon="mdi-dots-vertical" color="white" />
</IconBtn> <VMenu activator="parent" close-on-content-click>
</div> <VList>
<template #image> <template v-for="(item, i) in dropdownItems" :key="i">
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top"> <VListItem v-if="item.show !== false" :base-color="item.props.color" @click="item.props.click">
<template #placeholder> <template #prepend>
<div class="w-full h-full"> <VIcon :icon="item.props.prependIcon" />
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" /> </template>
</div> <VListItemTitle v-text="item.title" />
</template> </VListItem>
<div class="absolute inset-0 subscribe-card-background"></div> </template>
</VImg> </VList>
<div v-if="subscribeState === 'P'" class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none" /> </VMenu>
</template> </IconBtn>
<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>
</div> <template #image>
</VCard> <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> </template>
</VHover> </VHover>
<!-- 订阅编辑弹窗 --> <!-- 订阅编辑弹窗 -->

View File

@@ -97,64 +97,69 @@ function doDelete() {
<div class="h-full"> <div class="h-full">
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<VCard <div
v-bind="hover.props" class="w-full h-full rounded-lg overflow-hidden"
:key="props.media?.id"
class="flex flex-col h-full"
:class="{ :class="{
'transition transform-cpu duration-300 -translate-y-1': hover.isHovering, 'transition transform-cpu duration-300 -translate-y-1': hover.isHovering,
}" }"
min-height="170"
@click="showForkSubscribe"
> >
<template #image> <VCard
<VImg :src="backdropUrl || posterUrl" aspect-ratio="3/2" cover @load="imageLoadHandler" position="top"> v-bind="hover.props"
<template #placeholder> :key="props.media?.id"
<div class="w-full h-full"> class="flex flex-col h-full"
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" /> 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> </div>
</template> <div class="flex flex-col justify-center pl-2 xl:pl-4">
<div class="absolute inset-0 subscribe-card-background"></div> <div class="mr-2 min-w-0 text-lg font-bold text-white line-clamp-2 overflow-hidden text-ellipsis ...">
</VImg> {{ props.media?.share_title }}
</template> </div>
<div class="h-full flex flex-col"> <div class="text-sm font-medium text-gray-200 sm:pt-1 line-clamp-3 overflow-hidden text-ellipsis ...">
<VCardText class="flex items-center pb-1 grow"> {{ props.media?.share_comment }}
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md" v-if="imageLoaded"> </div>
<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> </div>
<div class="text-sm font-medium text-gray-200 sm:pt-1 line-clamp-3 overflow-hidden text-ellipsis ..."> </VCardText>
{{ props.media?.share_comment }} <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>
</div> </VCardText>
</VCardText> <VCardText class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
<VCardText class="flex justify-space-between align-center flex-wrap"> <VIcon icon="mdi-calcdar" class="me-1" />
<div class="flex align-center"> {{ dateText }}
<IconBtn v-bind="props" icon="mdi-account" color="white" class="me-1" /> </VCardText>
<div class="text-subtitle-2 me-4 text-white"> </div>
{{ props.media?.share_user }} </VCard>
</div> </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>
</template> </template>
</VHover> </VHover>
<!-- 订阅编辑弹窗 --> <!-- 订阅编辑弹窗 -->

View File

@@ -28,17 +28,33 @@ async function handleReset() {
const result: { [key: string]: any } = await api.get('/storage/reset/alist') const result: { [key: string]: any } = await api.get('/storage/reset/alist')
if (result.success) { if (result.success) {
// 重置成功 // 重置成功
alertType.value = 'success'
handleDone() handleDone()
} else {
alertType.value = 'error'
text.value = result.message
} }
} catch (e) { } catch (e) {
console.error(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设置 // 保存alist设置
async function savaAlistConfig() { async function savaAlistConfig() {
try { try {
@@ -63,7 +79,16 @@ async function savaAlistConfig() {
persistent-hint persistent-hint
/> />
</VCol> </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 <VTextField
v-model="props.conf.username" v-model="props.conf.username"
:hint="t('dialog.alistConfig.username')" :hint="t('dialog.alistConfig.username')"
@@ -71,7 +96,7 @@ async function savaAlistConfig() {
persistent-hint persistent-hint
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="4" v-if="loginType == 'username'">
<VTextField <VTextField
type="password" type="password"
v-model="props.conf.password" v-model="props.conf.password"
@@ -80,6 +105,14 @@ async function savaAlistConfig() {
persistent-hint persistent-hint
/> />
</VCol> </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> </VRow>
</VCardText> </VCardText>
<VCardActions> <VCardActions>

View File

@@ -23,6 +23,9 @@ const emit = defineEmits(['close'])
// 订阅文件信息 // 订阅文件信息
const subScribeInfo = ref<SubscrbieInfo>() const subScribeInfo = ref<SubscrbieInfo>()
// 是否加载中
const loading = ref(false)
// 下载文件表头 // 下载文件表头
const downloadHeaders = [ const downloadHeaders = [
{ title: t('dialog.subscribeFiles.episodeColumn'), key: 'episode_number', sortable: true }, { title: t('dialog.subscribeFiles.episodeColumn'), key: 'episode_number', sortable: true },
@@ -39,9 +42,12 @@ const libraryHeaders = [
// 调用API查询订阅文件信息 // 调用API查询订阅文件信息
async function loadSubscribeFilesInfo() { async function loadSubscribeFilesInfo() {
try { try {
loading.value = true
subScribeInfo.value = await api.get(`subscribe/files/${props.subid}`) subScribeInfo.value = await api.get(`subscribe/files/${props.subid}`)
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} finally {
loading.value = false
} }
} }
@@ -84,7 +90,8 @@ onBeforeMount(() => {
<VCardItem class="my-2"> <VCardItem class="my-2">
<VDialogCloseBtn @click="emit('close')" /> <VDialogCloseBtn @click="emit('close')" />
</VCardItem> </VCardItem>
<VCardText> <LoadingBanner v-if="loading" />
<VCardText v-else>
<div class="media-page"> <div class="media-page">
<div class="media-header"> <div class="media-header">
<div class="media-poster"> <div class="media-poster">

View File

@@ -203,7 +203,7 @@ onMounted(() => {
</VCard> </VCard>
</VMenu> </VMenu>
<!-- 名称测试弹窗 --> <!-- 名称测试弹窗 -->
<VDialog v-if="nameTestDialog" v-model="nameTestDialog" max-width="35rem" scrollable> <VDialog v-if="nameTestDialog" v-model="nameTestDialog" max-width="45rem" scrollable>
<VCard> <VCard>
<VCardItem> <VCardItem>
<VCardTitle> <VCardTitle>
@@ -298,7 +298,7 @@ onMounted(() => {
<VDialog <VDialog
v-if="messageDialog" v-if="messageDialog"
v-model="messageDialog" v-model="messageDialog"
max-width="40rem" max-width="50rem"
scrollable scrollable
:fullscreen="!display.mdAndUp.value" :fullscreen="!display.mdAndUp.value"
ref="messageDialogRef" ref="messageDialogRef"

View File

@@ -1011,6 +1011,8 @@ export default {
scrapFollowTmdb: 'Follow TMDB Recognition', scrapFollowTmdb: 'Follow TMDB Recognition',
scrapFollowTmdbHint: scrapFollowTmdbHint:
'When turned off, organization history will be used (if available) to avoid TMDB data changes during subscription', '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', fanartEnable: 'Fanart Image Data Source',
fanartEnableHint: 'Use image data from fanart.tv', fanartEnableHint: 'Use image data from fanart.tv',
githubProxy: 'Github Acceleration Proxy', githubProxy: 'Github Acceleration Proxy',

View File

@@ -1005,6 +1005,8 @@ export default {
metaCacheExpireMin: '元数据缓存时间必须大于等于0', metaCacheExpireMin: '元数据缓存时间必须大于等于0',
scrapFollowTmdb: '跟随TMDB识别整理', scrapFollowTmdb: '跟随TMDB识别整理',
scrapFollowTmdbHint: '关闭时以整理历史记录为准如有避免TMDB数据在订阅中途修改', scrapFollowTmdbHint: '关闭时以整理历史记录为准如有避免TMDB数据在订阅中途修改',
scrapOriginalImage: 'TMDB 刮削原语种图片',
scrapOriginalImageHint: '刮削原语种图片,否则刮削元数据语种图片',
fanartEnable: 'Fanart图片数据源', fanartEnable: 'Fanart图片数据源',
fanartEnableHint: '使用 fanart.tv 的图片数据', fanartEnableHint: '使用 fanart.tv 的图片数据',
githubProxy: 'Github加速代理', githubProxy: 'Github加速代理',
@@ -1491,7 +1493,7 @@ export default {
loginTypeOptions: { loginTypeOptions: {
guest: '访客', guest: '访客',
username: '用户名密码', username: '用户名密码',
token: 'Token', token: '令牌',
}, },
complete: '完成', complete: '完成',
reset: '重置', reset: '重置',

View File

@@ -1007,6 +1007,8 @@ export default {
metaCacheExpireMin: '元數據緩存時間必須大於等於0', metaCacheExpireMin: '元數據緩存時間必須大於等於0',
scrapFollowTmdb: '跟隨TMDB識別整理', scrapFollowTmdb: '跟隨TMDB識別整理',
scrapFollowTmdbHint: '關閉時以整理歷史記錄為準如有避免TMDB數據在訂閱中途修改', scrapFollowTmdbHint: '關閉時以整理歷史記錄為準如有避免TMDB數據在訂閱中途修改',
scrapOriginalImage: 'TMDB 刮削原語种圖片',
scrapOriginalImageHint: '刮削原語种圖片,否则數據元数据語种圖片',
fanartEnable: 'Fanart圖片數據源', fanartEnable: 'Fanart圖片數據源',
fanartEnableHint: '使用 fanart.tv 的圖片數據', fanartEnableHint: '使用 fanart.tv 的圖片數據',
githubProxy: 'Github加速代理', githubProxy: 'Github加速代理',

View File

@@ -48,6 +48,9 @@ const currentLocale = ref(getCurrentLocale())
// 可用的语言列表 // 可用的语言列表
const locales = Object.values(SUPPORTED_LOCALES) const locales = Object.values(SUPPORTED_LOCALES)
// 登录按钮 loading
const loading = ref(false)
// 切换语言 // 切换语言
async function switchLanguage(locale: SupportedLocale) { async function switchLanguage(locale: SupportedLocale) {
await setI18nLanguage(locale) await setI18nLanguage(locale)
@@ -103,6 +106,8 @@ async function afterLogin(superuser: boolean) {
router.push(authStore.originalPath ?? '/') router.push(authStore.originalPath ?? '/')
// 订阅推送通知 // 订阅推送通知
if (superuser) await subscribeForPushNotifications() if (superuser) await subscribeForPushNotifications()
// 登录按钮 loading
loading.value = false
} }
// 登录获取token事件 // 登录获取token事件
@@ -113,6 +118,10 @@ function login() {
if (!form.value.username || !form.value.password || (isOTP.value && !form.value.otp_password)) { if (!form.value.username || !form.value.password || (isOTP.value && !form.value.otp_password)) {
return return
} }
// 登录按钮 loading
loading.value = true
// 用户名密码 // 用户名密码
const formData = new FormData() 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 === 403) errorMessage.value = t('login.permissionDenied')
else if (error.response.status === 500) errorMessage.value = t('login.serverError') else if (error.response.status === 500) errorMessage.value = t('login.serverError')
else errorMessage.value = `${t('login.loginFailed')} ${error.response.status}${t('login.checkCredentials')}` else errorMessage.value = `${t('login.loginFailed')} ${error.response.status}${t('login.checkCredentials')}`
// 登录按钮 loading
loading.value = false
}) })
} }
@@ -252,7 +263,9 @@ onMounted(async () => {
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
<!-- login button --> <!-- 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"> <VAlert v-if="errorMessage" type="error" variant="tonal" class="mt-3">
{{ errorMessage }} {{ errorMessage }}
</VAlert> </VAlert>

View File

@@ -115,12 +115,14 @@ html.v-overlay-scroll-blocked {
// 美化滚动条 // 美化滚动条
::-webkit-scrollbar { ::-webkit-scrollbar {
block-size: 8px; block-size: 4px;
inline-size: 8px; inline-size: 4px;
opacity: 0;
transition: opacity 0.3s;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
border-radius: 3px; border-radius: 2px;
background: rgb(var(--v-theme-perfect-scrollbar-thumb)); background: rgb(var(--v-theme-perfect-scrollbar-thumb));
box-shadow: inset 0 0 10px rgba(0,0,0,20%); 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 { .v-alert--variant-elevated, .v-alert--variant-flat {
background: rgb(var(--v-table-header-background)); background: rgb(var(--v-table-header-background));
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)); color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));

View File

@@ -12,6 +12,20 @@ interface RemoteModule {
url: string 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 * @param id 远程模块ID
@@ -22,8 +36,24 @@ export async function loadRemoteComponent(id: string, componentName: string = 'P
const module = await __federation_method_getRemote(id, `./${componentName}`) const module = await __federation_method_getRemote(id, `./${componentName}`)
return __federation_method_unwrapDefault(module) return __federation_method_unwrapDefault(module)
} catch (error) { } 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
}
} }
} }

View File

@@ -43,6 +43,7 @@ const SystemSettings = ref<any>({
META_CACHE_EXPIRE: 0, META_CACHE_EXPIRE: 0,
SCRAP_FOLLOW_TMDB: true, SCRAP_FOLLOW_TMDB: true,
FANART_ENABLE: false, FANART_ENABLE: false,
TMDB_SCRAP_ORIGINAL_IMAGE: null,
// 网络 // 网络
PROXY_HOST: null, PROXY_HOST: null,
GITHUB_PROXY: null, GITHUB_PROXY: null,
@@ -740,6 +741,14 @@ onDeactivated(() => {
persistent-hint persistent-hint
/> />
</VCol> </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"> <VCol cols="12" md="6">
<VSwitch <VSwitch
v-model="SystemSettings.Advanced.FANART_ENABLE" v-model="SystemSettings.Advanced.FANART_ENABLE"

View File

@@ -37,6 +37,9 @@ const filterForm: Record<string, string[]> = reactive({
// 排序选项 // 排序选项
const sortField = ref('default') const sortField = ref('default')
// 降序
const sortType = ref<'asc' | 'desc'>('desc')
const sortTitles: Record<string, string> = { const sortTitles: Record<string, string> = {
default: t('torrent.sortDefault'), default: t('torrent.sortDefault'),
site: t('torrent.sortSite'), site: t('torrent.sortSite'),
@@ -212,7 +215,7 @@ onMounted(() => {
}) })
// 修改watch监听同时监听排序字段的变化 // 修改watch监听同时监听排序字段的变化
watch([filterForm, groupedDataList, sortField], filterData) watch([filterForm, groupedDataList, sortField, sortType], filterData)
function filterData() { function filterData() {
// 清空列表 // 清空列表
@@ -258,16 +261,30 @@ function filterData() {
// 排序数据 // 排序数据
if (sortField.value !== 'default') { if (sortField.value !== 'default') {
filteredData.sort((a, b) => { filteredData.sort((a, b) => {
if (sortField.value === 'site') { 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 (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.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) // 按做种数排序(降序)
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 return 0
}) })
} }
@@ -364,6 +381,12 @@ function loadMore({ done }: { done: any }) {
displayDataList.value.push(...itemsToMove) displayDataList.value.push(...itemsToMove)
done('ok') done('ok')
} }
// 处理图标点击
const handleSortIconClick = () => {
// 切换排序方向
sortType.value = sortType.value === 'asc' ? 'desc' : 'asc'
}
</script> </script>
<template> <template>
@@ -384,9 +407,15 @@ function loadMore({ done }: { done: any }) {
density="compact" density="compact"
hide-details hide-details
class="sort-select" class="sort-select"
prepend-icon="mdi-sort"
variant="plain" variant="plain"
></VSelect> >
<template #prepend-inner>
<!-- 添加排序点击事件 -->
<VIcon @mousedown.stop.prevent="handleSortIconClick">
{{ sortType === 'asc' ? 'mdi-sort-ascending' : 'mdi-sort-descending' }}
</VIcon>
</template>
</VSelect>
</div> </div>
<!-- 筛选按钮组 --> <!-- 筛选按钮组 -->
@@ -521,9 +550,15 @@ function loadMore({ done }: { done: any }) {
density="compact" density="compact"
hide-details hide-details
class="mobile-sort-select" class="mobile-sort-select"
prepend-icon="mdi-sort"
variant="plain" variant="plain"
></VSelect> >
<template #prepend-inner>
<!-- 添加排序点击事件 -->
<VIcon @mousedown.stop.prevent="handleSortIconClick">
{{ sortType === 'asc' ? 'mdi-sort-ascending' : 'mdi-sort-descending' }}
</VIcon>
</template>
</VSelect>
</div> </div>
<!-- 筛选图标按钮区域 --> <!-- 筛选图标按钮区域 -->
@@ -813,7 +848,7 @@ function loadMore({ done }: { done: any }) {
.mobile-sort-select { .mobile-sort-select {
max-inline-size: 130px; max-inline-size: 130px;
min-inline-size: 110px; min-inline-size: 80px;
} }
.filter-buttons-grid { .filter-buttons-grid {

View File

@@ -61,6 +61,8 @@ const filterOptions: Record<string, string[]> = reactive({
// 排序字段 // 排序字段
const sortField = ref('default') const sortField = ref('default')
// 降序
const sortType = ref<'asc' | 'desc'>('desc')
// 数据列表 // 数据列表
const dataList = ref<Array<Context>>([]) const dataList = ref<Array<Context>>([])
@@ -202,7 +204,7 @@ function sortSeasonOptions() {
} }
// 修改watch监听同时监听排序字段的变化 // 修改watch监听同时监听排序字段的变化
watch([filterForm, sortField], filterData) watch([filterForm, sortField, sortType], filterData)
// 计算过滤后的列表 // 计算过滤后的列表
function filterData() { function filterData() {
@@ -247,16 +249,30 @@ function filterData() {
}) })
// 排序 // 排序
if (sortField.value === 'default') { if (sortType.value === 'desc') {
filteredData = filteredData.sort((a, b) => b.torrent_info.pri_order - a.torrent_info.pri_order) if (sortField.value === 'default') {
} else if (sortField.value === 'site') { filteredData = filteredData.sort((a, b) => b.torrent_info.pri_order - a.torrent_info.pri_order)
filteredData = filteredData.sort((a, b) => } else if (sortField.value === 'site') {
(a.torrent_info.site_name || '').localeCompare(b.torrent_info.site_name || ''), 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 === 'size') {
} else if (sortField.value === 'seeder') { filteredData = filteredData.sort((a, b) => b.torrent_info.size - a.torrent_info.size)
filteredData = filteredData.sort((a, b) => b.torrent_info.seeders - a.torrent_info.seeders) } 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个 // 显示前20个
@@ -338,6 +354,12 @@ function loadMore({ done }: { done: any }) {
done('ok') done('ok')
} }
// 处理图标点击
const handleSortIconClick = () => {
// 切换排序方向
sortType.value = sortType.value === 'asc' ? 'desc' : 'asc'
}
onMounted(() => { onMounted(() => {
filterData() filterData()
}) })
@@ -363,9 +385,14 @@ onMounted(() => {
density="compact" density="compact"
hide-details hide-details
class="sort-select" class="sort-select"
prepend-icon="mdi-sort"
variant="plain" variant="plain"
> >
<template #prepend-inner>
<!-- 添加排序点击事件 -->
<VIcon @mousedown.stop.prevent="handleSortIconClick">
{{ sortType === 'asc' ? 'mdi-sort-ascending' : 'mdi-sort-descending' }}
</VIcon>
</template>
</VSelect> </VSelect>
<div class="filter-divider"></div> <div class="filter-divider"></div>
@@ -499,9 +526,15 @@ onMounted(() => {
density="compact" density="compact"
hide-details hide-details
class="mobile-sort-select" class="mobile-sort-select"
prepend-icon="mdi-sort"
variant="plain" variant="plain"
></VSelect> >
<template #prepend-inner>
<!-- 添加排序点击事件 -->
<VIcon @mousedown.stop.prevent="handleSortIconClick">
{{ sortType === 'asc' ? 'mdi-sort-ascending' : 'mdi-sort-descending' }}
</VIcon>
</template>
</VSelect>
</div> </div>
<!-- 筛选图标按钮区域 --> <!-- 筛选图标按钮区域 -->
@@ -841,7 +874,7 @@ onMounted(() => {
.mobile-sort-select { .mobile-sort-select {
max-inline-size: 130px; max-inline-size: 130px;
min-inline-size: 110px; min-inline-size: 80px;
} }
.all-filters-grid { .all-filters-grid {

View File

@@ -35,6 +35,7 @@ export default defineConfig({
federation({ federation({
name: 'MoviePilot', name: 'MoviePilot',
filename: 'remoteEntry.js', filename: 'remoteEntry.js',
// @ts-ignore
remotes: { remotes: {
// 动态remotes将在运行时注入 // 动态remotes将在运行时注入
dummy: { dummy: {
@@ -171,8 +172,8 @@ export default defineConfig({
minify: 'terser', minify: 'terser',
terserOptions: { terserOptions: {
compress: { compress: {
drop_console: false, drop_console: true,
drop_debugger: false, drop_debugger: true,
}, },
}, },
chunkSizeWarningLimit: 5000, chunkSizeWarningLimit: 5000,