Merge branch 'jxxghp:v2' into v2

This commit is contained in:
Seed680
2025-05-08 23:28:18 +08:00
committed by GitHub
36 changed files with 716 additions and 112 deletions

View File

@@ -107,7 +107,7 @@ function onClose() {
</VCardText>
</VCard>
<VDialog v-if="ruleInfoDialog" v-model="ruleInfoDialog" scrollable max-width="40rem">
<VCard :title="t('customRule.title', { id: props.rule.id })" class="rounded-t">
<VCard :title="t('customRule.title', { id: props.rule.id })">
<VDialogCloseBtn v-model="ruleInfoDialog" />
<VDivider />
<VCardText>

View File

@@ -220,7 +220,7 @@ function onClose() {
</VCardText>
</VCard>
<VDialog v-if="groupInfoDialog" v-model="groupInfoDialog" scrollable max-width="80rem">
<VCard :title="`${props.group.name} - ${t('filterRule.title')}`" class="rounded-t">
<VCard :title="`${props.group.name} - ${t('filterRule.title')}`">
<VDialogCloseBtn v-model="groupInfoDialog" />
<VDivider />
<VCardItem class="pt-1">

View File

@@ -200,7 +200,7 @@ onMounted(() => {
</VCardText>
</VCard>
<VDialog v-if="mediaServerInfoDialog" v-model="mediaServerInfoDialog" scrollable max-width="40rem">
<VCard :title="`${props.mediaserver.name} - ${t('common.config')}`" class="rounded-t">
<VCard :title="`${props.mediaserver.name} - ${t('common.config')}`">
<VDialogCloseBtn v-model="mediaServerInfoDialog" />
<VDivider />
<VCardText>

View File

@@ -135,7 +135,7 @@ function onClose() {
</VCardText>
</VCard>
<VDialog v-if="notificationInfoDialog" v-model="notificationInfoDialog" scrollable max-width="40rem">
<VCard :title="`${props.notification.name} - ${t('notification.config')}`" class="rounded-t">
<VCard :title="`${props.notification.name} - ${t('notification.config')}`">
<VDialogCloseBtn v-model="notificationInfoDialog" />
<VDivider />
<VCardText>

View File

@@ -293,21 +293,20 @@ onMounted(() => {
</div>
<!-- 右侧操作按钮区 -->
<VSheet
class="site-card-actions absolute inset-y-0 right-0 z-20 flex flex-col py-2 px-1 transform translate-x-full transition-transform duration-200"
>
<VSheet class="site-card-actions absolute inset-y-0 right-0 z-20 flex flex-col py-2 px-1">
<!-- 测试按钮 -->
<VBtn
icon
variant="text"
density="comfortable"
class="mb-1 relative w-10 h-10 min-w-10 flex items-center justify-center rounded-full"
class="mb-1 relative flex items-center justify-center rounded-full mx-auto"
:disabled="testButtonDisable"
@click.stop="testSite"
size="36"
>
<div class="relative flex items-center justify-center w-full h-full">
<div
class="w-[22px] h-[22px] rounded-full shadow-[inset_0_0_0_2px_rgba(var(--v-theme-on-surface),0.1)] pulse-dot"
class="w-[20px] h-[20px] rounded-full shadow-[inset_0_0_0_2px_rgba(var(--v-theme-on-surface),0.1)] pulse-dot"
:class="statColor"
></div>
</div>
@@ -322,29 +321,29 @@ onMounted(() => {
</VBtn>
<!-- 用户数据按钮 -->
<VBtn icon variant="text" @click.stop="handleSiteUserData">
<VIcon icon="mdi-chart-bell-curve" size="small" />
<VBtn icon variant="text" @click.stop="handleSiteUserData" size="36">
<VIcon icon="mdi-chart-bell-curve" size="20" />
</VBtn>
<!-- 更新按钮 -->
<VBtn icon variant="text" @click.stop="handleSiteUpdate">
<VIcon icon="mdi-refresh" size="small" />
<VBtn icon variant="text" @click.stop="handleSiteUpdate" size="36">
<VIcon icon="mdi-refresh" size="20" />
</VBtn>
<!-- 更多选项按钮 -->
<VBtn icon variant="text" class="mt-auto">
<VIcon icon="mdi-dots-vertical" size="small" />
<VBtn icon variant="text" class="mt-auto" size="36">
<VIcon icon="mdi-dots-vertical" size="20" />
<VMenu :activator="'parent'" :close-on-content-click="true" :location="'left'">
<VList>
<VListItem @click="handleResourceBrowse" base-color="info">
<template #prepend>
<VIcon icon="mdi-web" size="small" />
<VIcon icon="mdi-web" size="20" />
</template>
<VListItemTitle>{{ t('site.browseResources') }}</VListItemTitle>
</VListItem>
<VListItem @click="deleteSiteInfo">
<template #prepend>
<VIcon icon="mdi-delete-outline" size="small" color="error" />
<VIcon icon="mdi-delete-outline" size="20" color="error" />
</template>
<VListItemTitle class="text-error">{{ t('site.deleteSite') }}</VListItemTitle>
</VListItem>
@@ -386,12 +385,6 @@ onMounted(() => {
</template>
<style scoped>
.site-card:hover {
.site-card-actions {
transform: translateX(0);
}
}
.site-status-indicator {
position: absolute;
z-index: 1;
@@ -430,15 +423,15 @@ onMounted(() => {
/* 上传下载条样式 */
.upload-bar {
animation: pulse-width 2s infinite;
background: linear-gradient(90deg, #4d79ff, #07f);
box-shadow: 0 0 4px rgba(0, 119, 255, 50%);
animation: pulse-width 2s infinite;
}
.download-bar {
animation: pulse-width 2s infinite;
background: linear-gradient(90deg, #42d392, #00b77e);
box-shadow: 0 0 4px rgba(0, 183, 126, 50%);
animation: pulse-width 2s infinite;
}
/* 测试状态点样式 */
@@ -446,22 +439,22 @@ onMounted(() => {
position: absolute;
z-index: 1;
border-radius: 50%;
block-size: 70%;
content: '';
height: 70%;
width: 70%;
top: 15%;
left: 15%;
inline-size: 70%;
inset-block-start: 15%;
inset-inline-start: 15%;
}
.pulse-dot::after {
position: absolute;
z-index: 2;
border-radius: 50%;
block-size: 100%;
content: '';
height: 100%;
width: 100%;
top: 0;
left: 0;
inline-size: 100%;
inset-block-start: 0;
inset-inline-start: 0;
}
.pulse-dot.error::before {
@@ -508,11 +501,11 @@ onMounted(() => {
.spinner-circle {
position: absolute;
border: 1px solid rgba(var(--v-theme-primary), 0.2);
border-top-color: rgba(var(--v-theme-primary), 1);
border-radius: 50%;
width: 100%;
height: 100%;
animation: spin 0.8s linear infinite;
block-size: 100%;
border-block-start-color: rgba(var(--v-theme-primary), 1);
inline-size: 100%;
}
/* 动画关键帧 */
@@ -522,6 +515,7 @@ onMounted(() => {
opacity: 0.85;
transform: scaleX(0.95);
}
50% {
opacity: 1;
transform: scaleX(1.05);
@@ -532,9 +526,11 @@ onMounted(() => {
0% {
box-shadow: 0 0 0 0 rgba(var(--v-theme-error), 0.6);
}
70% {
box-shadow: 0 0 0 10px rgba(var(--v-theme-error), 0);
}
100% {
box-shadow: 0 0 0 0 rgba(var(--v-theme-error), 0);
}
@@ -544,9 +540,11 @@ onMounted(() => {
0% {
box-shadow: 0 0 0 0 rgba(var(--v-theme-warning), 0.6);
}
70% {
box-shadow: 0 0 0 10px rgba(var(--v-theme-warning), 0);
}
100% {
box-shadow: 0 0 0 0 rgba(var(--v-theme-warning), 0);
}
@@ -556,9 +554,11 @@ onMounted(() => {
0% {
box-shadow: 0 0 0 0 rgba(var(--v-theme-success), 0.6);
}
70% {
box-shadow: 0 0 0 10px rgba(var(--v-theme-success), 0);
}
100% {
box-shadow: 0 0 0 0 rgba(var(--v-theme-success), 0);
}
@@ -568,9 +568,11 @@ onMounted(() => {
0% {
box-shadow: 0 0 0 0 rgba(var(--v-theme-secondary), 0.6);
}
70% {
box-shadow: 0 0 0 10px rgba(var(--v-theme-secondary), 0);
}
100% {
box-shadow: 0 0 0 0 rgba(var(--v-theme-secondary), 0);
}
@@ -580,6 +582,7 @@ onMounted(() => {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
@@ -589,8 +592,22 @@ onMounted(() => {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.site-card-actions {
opacity: 0;
transform: translateX(100%);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
visibility: hidden;
}
.site-card:hover .site-card-actions {
opacity: 1;
transform: translateX(0);
visibility: visible;
}
</style>

View File

@@ -69,7 +69,7 @@ async function savaAlistConfig() {
<template>
<VDialog width="50rem" scrollable max-height="85vh">
<VCard :title="t('dialog.alistConfig.title')" class="rounded-t">
<VCard :title="t('dialog.alistConfig.title')">
<VDialogCloseBtn @click="emit('close')" />
<VCardText>
<VRow>
@@ -119,7 +119,7 @@ async function savaAlistConfig() {
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
{{ t('dialog.alistConfig.reset') }}
</VBtn>
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">

View File

@@ -107,7 +107,7 @@ onUnmounted(() => {
<template>
<VDialog width="40rem" scrollable max-height="85vh">
<VCard :title="t('dialog.aliyunAuth.loginTitle')" class="rounded-t">
<VCard :title="t('dialog.aliyunAuth.loginTitle')">
<VDialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2 flex flex-col items-center">
<div class="my-6 rounded text-center p-3 border">
@@ -125,7 +125,7 @@ onUnmounted(() => {
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
{{ t('dialog.aliyunAuth.reset') }}
</VBtn>
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">

View File

@@ -25,7 +25,7 @@ function handleImport() {
<template>
<VDialog width="40rem" scrollable max-height="85vh">
<VCard :title="props.title" class="rounded-t">
<VCard :title="props.title">
<VDialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2">
<VTextarea v-model="codeString" />

View File

@@ -150,11 +150,7 @@ onBeforeMount(async () => {
<template>
<VDialog scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
<!-- Vuetify 渲染模式 -->
<VCard
v-if="renderMode === 'vuetify'"
:title="`${props.plugin?.plugin_name} - ${t('dialog.pluginConfig.title')}`"
class="rounded-t"
>
<VCard v-if="renderMode === 'vuetify'" :title="`${props.plugin?.plugin_name} - ${t('dialog.pluginConfig.title')}`">
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<LoadingBanner v-if="!isRefreshed" class="mt-5" />
@@ -182,16 +178,19 @@ onBeforeMount(async () => {
</VCardActions>
</VCard>
<!-- Vue 渲染模式 -->
<div v-else-if="renderMode === 'vue'">
<component
:is="dynamicComponent"
:initial-config="pluginConfigForm"
:api="api"
@save="handleVueComponentSave"
@switch="emit('switch')"
@close="emit('close')"
/>
</div>
<VCard v-else-if="renderMode === 'vue'">
<VCardText class="pa-0">
<component
:is="dynamicComponent"
:initial-config="pluginConfigForm"
:api="api"
@save="handleVueComponentSave"
@switch="emit('switch')"
@close="emit('close')"
/>
</VCardText>
</VCard>
<!-- 进度框 -->
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
</VDialog>

View File

@@ -120,7 +120,7 @@ onMounted(() => {
<template>
<VDialog scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
<!-- Vuetify 渲染模式 -->
<VCard v-if="renderMode === 'vuetify'" :title="`${props.plugin?.plugin_name}`" class="rounded-t">
<VCard v-if="renderMode === 'vuetify'" :title="`${props.plugin?.plugin_name}`">
<VDialogCloseBtn @click="emit('close')" />
<LoadingBanner v-if="!isRefreshed" class="mt-5" />
<VCardText v-else class="min-h-40">
@@ -141,8 +141,16 @@ onMounted(() => {
/>
</VCard>
<!-- Vue 渲染模式 -->
<div v-else-if="renderMode === 'vue'">
<component :is="dynamicComponent" :api="api" @action="handleAction" @switch="emit('switch')" @close="emit('close')" />
</div>
<VCard v-else-if="renderMode === 'vue'">
<VCardText class="pa-0">
<component
:is="dynamicComponent"
:api="api"
@action="handleAction"
@switch="emit('switch')"
@close="emit('close')"
/>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -45,7 +45,7 @@ onMounted(() => {
<template>
<VDialog width="50rem" scrollable max-height="85vh">
<VCard class="rounded-t">
<VCard>
<VCardItem>
<VCardTitle>
<VIcon icon="mdi-store-cog" class="me-2" />

View File

@@ -44,12 +44,7 @@ async function handleReset() {
try {
const result: { [key: string]: any } = await api.get('/storage/reset/rclone')
if (result.success) {
// 重置成功
alertType.value = 'success'
handleDone()
} else {
alertType.value = 'error'
text.value = result.message
}
} catch (e) {
console.error(e)
@@ -59,7 +54,7 @@ async function handleReset() {
<template>
<VDialog width="50rem" scrollable max-height="85vh">
<VCard :title="t('dialog.rcloneConfig.title')" class="rounded-t">
<VCard :title="t('dialog.rcloneConfig.title')">
<VDialogCloseBtn @click="emit('close')" />
<VCardText>
<VRow>
@@ -80,7 +75,7 @@ async function handleReset() {
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
{{ t('dialog.rcloneConfig.reset') }}
</VBtn>
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">

View File

@@ -250,7 +250,7 @@ onUnmounted(() => {
<template>
<VDialog scrollable max-width="45rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="dialogTitle" class="rounded-t">
<VCard :title="dialogTitle">
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>

View File

@@ -152,7 +152,6 @@ onMounted(async () => {
:title="`${props.oper === 'add' ? t('site.actions.add') : t('site.actions.edit')}${t('site.title')}${
props.oper !== 'add' ? ` - ${siteForm.name}` : ''
}`"
class="rounded-t"
>
<VDialogCloseBtn @click="emit('close')" />
<VDivider />

View File

@@ -283,7 +283,7 @@ onBeforeMount(async () => {
<template>
<VDialog scrollable eager max-width="80rem" :fullscreen="!display.mdAndUp.value">
<VCard class="rounded-t">
<VCard>
<VCardItem>
<VCardTitle
>{{ t('dialog.siteUserData.title') }} - {{ props.site?.name }}

View File

@@ -292,7 +292,6 @@ onMounted(() => {
: '',
})
"
class="rounded-t"
>
<VCardText>
<VDialogCloseBtn @click="emit('close')" />

View File

@@ -80,7 +80,7 @@ onBeforeMount(() => {
</script>
<template>
<VDialog scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
<VCard class="rounded-t">
<VCard>
<VCardItem class="my-2">
<VDialogCloseBtn @click="emit('close')" />
</VCardItem>

View File

@@ -60,7 +60,6 @@ const $toast = useToast()
:title="`${t('dialog.subscribeShare.shareSubscription')} - ${props.sub?.name} ${
props.sub?.season ? t('dialog.subscribeShare.season', { number: props.sub?.season }) : ''
}`"
class="rounded-t"
>
<VCardText>
<VDialogCloseBtn @click="emit('close')" />

View File

@@ -112,7 +112,7 @@ onUnmounted(() => {
<template>
<VDialog width="40rem" scrollable max-height="85vh">
<VCard :title="t('dialog.u115Auth.loginTitle')" class="rounded-t">
<VCard :title="t('dialog.u115Auth.loginTitle')">
<VDialogCloseBtn @click="emit('close')" />
<VCardText class="pt-2 flex flex-col items-center">
<div class="my-6 rounded text-center p-3 border">
@@ -124,7 +124,7 @@ onUnmounted(() => {
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
<VBtn variant="tonal" color="error" @click="handleReset" prepend-icon="mdi-restore" class="px-5 me-3">
{{ t('dialog.u115Auth.reset') }}
</VBtn>
<VBtn variant="elevated" @click="handleDone" prepend-icon="mdi-check" class="px-5 me-3">

View File

@@ -295,7 +295,6 @@ onMounted(() => {
:title="`${props.oper === 'add' ? t('dialog.userAddEdit.add') : t('dialog.userAddEdit.edit')}${
props.oper !== 'add' ? ` - ${userName}` : ''
}`"
class="rounded-t"
>
<VDialogCloseBtn @click="emit('close')" />
<VDivider />

View File

@@ -134,7 +134,7 @@ onMounted(async () => {
<template>
<VDialog width="40rem" max-height="85vh">
<VCard :title="t('dialog.userAuth.title')" class="rounded-t">
<VCard :title="t('dialog.userAuth.title')">
<VDialogCloseBtn @click="emit('close')" />
<VCardText>
<VRow>

View File

@@ -86,7 +86,7 @@ async function editWorkflow() {
<template>
<VDialog scrollable :close-on-back="false" eager max-width="30rem" :fullscreen="!display.mdAndUp.value">
<VCard :title="title" class="rounded-t">
<VCard :title="title">
<VDialogCloseBtn @click="emit('close')" />
<VDivider />
<VCardText>

View File

@@ -162,7 +162,7 @@ const sortIcon = computed(() => {
<IconBtn @click="changeSort">
<VIcon :icon="sortIcon" />
</IconBtn>
<IconBtn @click="goUp">
<IconBtn v-if="pathSegments.length > 0" @click="goUp">
<VIcon icon="mdi-arrow-up-bold-outline" />
</IconBtn>
<VDialog v-model="newFolderPopper" max-width="35rem">

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, defineAsyncComponent } from 'vue'
import api from '@/api'
import { DashboardItem } from '@/api/types'
import AnalyticsMediaStatistic from '@/views/dashboard/AnalyticsMediaStatistic.vue'
import AnalyticsScheduler from '@/views/dashboard/AnalyticsScheduler.vue'
@@ -88,7 +88,7 @@ onUnmounted(() => {
<template v-else-if="!isNullOrEmptyObject(props.config)">
<!-- Vue 渲染模式 -->
<div v-if="pluginRenderMode === 'vue'">
<component :is="dynamicPluginComponent" :config="props.config" :allow-refresh="props.allowRefresh" />
<component :is="dynamicPluginComponent" :config="props.config" :allow-refresh="props.allowRefresh" :api="api" />
<!-- Vue 模式下也可以显示拖拽句柄 -->
<div class="absolute right-5 top-5">
<VIcon class="cursor-move">mdi-drag</VIcon>

View File

@@ -87,7 +87,7 @@ onBeforeUnmount(() => {
</VCardItem>
<VDivider />
<div class="notification-list-container">
<div v-if="notificationList.length > 0" class="notification-list">
<div v-if="notificationList.length > 0" class="h-full overflow-y-auto">
<VListItem v-for="(item, i) in notificationList" :key="i" lines="two" class="mb-1">
<template #prepend>
<VAvatar rounded>
@@ -120,12 +120,7 @@ onBeforeUnmount(() => {
<style scoped>
.notification-list-container {
max-height: 50vh;
overflow: hidden;
}
.notification-list {
max-height: 100%;
overflow-y: auto;
max-block-size: 50vh;
}
</style>

View File

@@ -6,12 +6,55 @@ import {
// @ts-ignore
} from 'virtual:__federation__'
// 扩展全局接口添加federation所需的共享作用域
declare global {
interface Window {
__rf_placeholder__shareScope?: Record<string, any>
vue?: any
vuetify?: any
pinia?: any
'vue-i18n'?: any
'vue-router'?: any
axios?: any
}
}
// 定义远程模块接口
interface RemoteModule {
id: string
url: string
}
/**
* 初始化共享作用域
*/
function initShareScope() {
// 确保全局共享作用域存在
if (!window.__rf_placeholder__shareScope) {
window.__rf_placeholder__shareScope = {}
}
// 为共享模块设置默认作用域
const shared = ['vue', 'vuetify', 'pinia', 'vue-i18n', 'vue-router', 'axios']
shared.forEach(lib => {
if (window.__rf_placeholder__shareScope) {
window.__rf_placeholder__shareScope[lib] = { default: { get: () => (window as any)[lib] } }
}
})
console.log('已初始化共享作用域:', window.__rf_placeholder__shareScope)
}
/**
* 添加一个dummy远程模块以解决生产环境中的共享作用域问题
*/
function addDummyRemote() {
__federation_method_setRemote('dummy', {
url: () => Promise.resolve(''),
format: 'esm',
from: 'vite',
shareScope: 'default',
})
}
/**
* 加载远程组件
* @param id 远程模块ID
@@ -40,15 +83,39 @@ async function fetchRemoteModules(): Promise<RemoteModule[]> {
}
}
/**
* 生成唯一的版本标记用于防止缓存
*/
function generateVersionTag(): string {
return `v=${Date.now()}`
}
/**
* 动态注入Federation Remote模块
* @param modules 远程模块列表
*/
function injectRemoteModule(module: RemoteModule): void {
// 从浏览器地址栏获取当前地址前缀
const baseUrl = new URL(window.location.href)
// 环境变量
let apiBase = import.meta.env.VITE_API_BASE_URL
if (apiBase.startsWith('/')) {
apiBase = apiBase.slice(1)
}
if (apiBase.endsWith('/')) {
apiBase = apiBase.slice(0, -1)
}
// 添加版本标记防止缓存
const versionTag = generateVersionTag()
const remoteUrl = `${baseUrl.origin}/${apiBase}${module.url}`
const urlWithVersion = remoteUrl.includes('?') ? `${remoteUrl}&${versionTag}` : `${remoteUrl}?${versionTag}`
__federation_method_setRemote(module.id, {
url: () => Promise.resolve(`${import.meta.env.VITE_API_BASE_URL}/${module.url}`),
url: () => Promise.resolve(urlWithVersion),
format: 'esm',
from: 'vite',
shareScope: 'default',
})
console.log('已注入远程模块:', module)
}
@@ -58,6 +125,12 @@ function injectRemoteModule(module: RemoteModule): void {
*/
export async function loadRemoteComponents(): Promise<void> {
try {
// 初始化共享作用域
initShareScope()
// 添加dummy远程模块解决生产环境问题
addDummyRemote()
// 获取远程模块列表
const modules = await fetchRemoteModules()

View File

@@ -34,10 +34,6 @@ const props = defineProps({
// 是否刷新过
let isRefreshed = ref(false)
// 顺序存储键值
const localOrderKey = props.type === t('media.movie') ? 'MP_SUBSCRIBE_MOVIE_ORDER' : 'MP_SUBSCRIBE_TV_ORDER'
const orderRequestKey = props.type === t('media.movie') ? 'SubscribeMovieOrder' : 'SubscribeTvOrder'
// 刷新状态
const loading = ref(false)
@@ -53,6 +49,10 @@ const orderConfig = ref<{ id: number }[]>([])
// 显示的订阅列表
const displayList = ref<Subscribe[]>([])
// 顺序存储键值(计算属性)
const localOrderKey = computed(() => (props.type === '电影' ? 'MP_SUBSCRIBE_MOVIE_ORDER' : 'MP_SUBSCRIBE_TV_ORDER'))
const orderRequestKey = computed(() => (props.type === '电影' ? 'SubscribeMovieOrder' : 'SubscribeTvOrder'))
// 监听dataList变化同步更新displayList
watch([dataList, () => props.keyword], () => {
if (superUser)
@@ -74,26 +74,27 @@ watch([dataList, () => props.keyword], () => {
// 加载顺序
async function loadSubscribeOrderConfig() {
// 顺序配置
const local_order = localStorage.getItem(localOrderKey)
const local_order = localStorage.getItem(localOrderKey.value)
if (local_order) {
orderConfig.value = JSON.parse(local_order)
} else {
const response = await api.get(`/user/config/${orderRequestKey}`)
if (response && response.data && response.data.value) {
orderConfig.value = response.data.value
localStorage.setItem(localOrderKey, JSON.stringify(orderConfig.value))
localStorage.setItem(localOrderKey.value, JSON.stringify(orderConfig.value))
}
}
}
// 按order的顺序排序
function sortSubscribeOrder() {
async function sortSubscribeOrder() {
if (!orderConfig.value) {
return
}
if (displayList.value.length === 0) {
return
}
await loadSubscribeOrderConfig()
displayList.value.sort((a, b) => {
const aIndex = orderConfig.value.findIndex((item: { id: number }) => item.id === a.id)
const bIndex = orderConfig.value.findIndex((item: { id: number }) => item.id === b.id)
@@ -107,7 +108,7 @@ async function saveSubscribeOrder() {
const orderObj = displayList.value.map(item => ({ id: item.id }))
orderConfig.value = orderObj
const orderString = JSON.stringify(orderObj)
localStorage.setItem(localOrderKey, orderString)
localStorage.setItem(localOrderKey.value, orderString)
// 保存到服务端
try {
@@ -136,7 +137,6 @@ function historyDone() {
}
onMounted(async () => {
await loadSubscribeOrderConfig()
await fetchData()
if (props.subid) {
// 找到这个订阅

View File

@@ -79,6 +79,11 @@ const groupedDataList = ref<Map<string, Context[]>>()
const filterMenuOpen = ref(false)
const currentFilter = ref('site')
const currentFilterTitle = computed(() => filterTitles[currentFilter.value])
const currentFilterOptions = computed(() => {
return filterOptions[currentFilter.value]
})
// 添加全部筛选菜单相关
const allFilterMenuOpen = ref(false)
@@ -522,7 +527,23 @@ function loadMore({ done }: { done: any }) {
</div>
<!-- 筛选图标按钮区域 -->
<div class="filter-buttons-grid w-100">
<div class="filter-buttons-grid w-100 mt-2">
<!-- 全部筛选按钮 -->
<VBtn variant="text" color="primary" class="filter-btn-mobile" @click="toggleAllFilterMenu">
<VIcon icon="mdi-filter-variant" class="filter-icon me-1"></VIcon>
<span class="filter-label">
{{ t('torrent.allFilters') }}
</span>
<VBadge
v-if="getFilterCount > 0"
:content="getFilterCount"
color="primary"
location="top end"
offset-x="-10"
offset-y="-10"
></VBadge>
</VBtn>
<VBtn
v-for="(title, key) in filterTitles"
v-show="filterOptions[key].length > 0"
@@ -550,7 +571,7 @@ function loadMore({ done }: { done: any }) {
</VCard>
<!-- 全部筛选弹窗 -->
<VDialog v-model="allFilterMenuOpen" max-width="50rem" location="center" scrollable>
<VDialog v-model="allFilterMenuOpen" max-width="50rem" max-height="90%" location="center" scrollable>
<VCard>
<VDialogCloseBtn @click="allFilterMenuOpen = false" />
<VCardTitle class="py-3 d-flex align-center">
@@ -619,6 +640,51 @@ function loadMore({ done }: { done: any }) {
</VCard>
</VDialog>
<!-- 筛选弹窗 -->
<VDialog v-model="filterMenuOpen" max-width="25rem" max-height="80%" location="center">
<VCard>
<VCardTitle class="py-3 d-flex align-center">
<VIcon :icon="getFilterIcon(currentFilter)" class="me-2"></VIcon>
<span>{{ currentFilterTitle }}</span>
<VSpacer />
<VBtn
v-if="filterForm[currentFilter].length > 0"
variant="text"
size="small"
color="error"
@click="clearFilter(currentFilter)"
>
{{ t('torrent.clear') }}
</VBtn>
<VBtn variant="text" size="small" color="primary" @click="selectAll(currentFilter)">
{{ t('torrent.selectAll') }}
</VBtn>
</VCardTitle>
<VDivider />
<VCardText>
<VChipGroup v-model="filterForm[currentFilter]" column multiple class="filter-options">
<VChip
v-for="option in currentFilterOptions"
:key="option"
:value="option"
filter
variant="elevated"
class="ma-1 filter-chip"
size="small"
>
{{ option }}
</VChip>
</VChipGroup>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="elevated" color="primary" @click="filterMenuOpen = false">
{{ t('torrent.confirm') }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- 资源列表 -->
<VInfiniteScroll mode="intersect" side="end" :items="displayDataList" class="overflow-visible" @load="loadMore">
<template #loading />

View File

@@ -549,7 +549,7 @@ onMounted(() => {
</VCard>
<!-- 全部筛选弹窗 -->
<VDialog v-model="allFilterMenuOpen" max-width="50rem" location="center" scrollable>
<VDialog v-model="allFilterMenuOpen" max-width="50rem" max-height="90%" location="center" scrollable>
<VCard>
<VDialogCloseBtn @click="allFilterMenuOpen = false" />
<VCardTitle class="py-3 d-flex align-center">