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
uses: dev-drprasad/delete-tag-and-release@v1.1
continue-on-error: true
with:
tag_name: ${{ env.frontend_version }}
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. 技术要求
- 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: {

View File

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

View File

@@ -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>

View File

@@ -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>

View File

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

View File

@@ -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 ..."

View File

@@ -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" />

View File

@@ -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>
<!-- 订阅编辑弹窗 -->

View File

@@ -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>
<!-- 订阅编辑弹窗 -->

View File

@@ -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>

View File

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

View File

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

View File

@@ -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',

View File

@@ -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: '重置',

View File

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

View File

@@ -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>

View File

@@ -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));

View File

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

View File

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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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,