mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-22 08:49:47 +08:00
feat: 添加插件名称计算属性,优化未安装插件列表显示逻辑
This commit is contained in:
@@ -357,7 +357,7 @@ watch(
|
||||
{{ props.plugin?.plugin_desc }}
|
||||
</VCardText>
|
||||
</div>
|
||||
<div class="relative flex-shrink-0 self-center">
|
||||
<div class="relative flex-shrink-0 self-center cursor-move">
|
||||
<VAvatar size="64">
|
||||
<VImg
|
||||
ref="imageRef"
|
||||
@@ -406,10 +406,7 @@ watch(
|
||||
</IconBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
<div v-if="hover.isHovering" class="me-n3 absolute top-0 right-5">
|
||||
<VIcon class="cursor-move text-white">mdi-drag</VIcon>
|
||||
</div>
|
||||
<div v-else-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
|
||||
<div v-if="props.plugin?.has_update" class="me-n3 absolute top-0 right-5">
|
||||
<VIcon icon="mdi-new-box" class="text-white" />
|
||||
</div>
|
||||
</VCard>
|
||||
|
||||
@@ -334,8 +334,11 @@ function onSubscribeEditRemove() {
|
||||
<div v-if="subscribeState === 'P'" class="absolute inset-0 bg-yellow-900 opacity-80 pointer-events-none" />
|
||||
</template>
|
||||
<div>
|
||||
<VCardText class="flex items-center">
|
||||
<div class="h-auto w-16 flex-shrink-0 overflow-hidden rounded-md shadow-lg" v-if="imageLoaded">
|
||||
<VCardText class="flex items-center py-3">
|
||||
<div
|
||||
class="h-auto w-16 flex-shrink-0 overflow-hidden rounded-md shadow-lg cursor-move"
|
||||
v-if="imageLoaded"
|
||||
>
|
||||
<VImg :src="posterUrl" aspect-ratio="2/3" cover>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
@@ -352,7 +355,7 @@ function onSubscribeEditRemove() {
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap">
|
||||
<VCardText class="flex justify-space-between align-center flex-wrap py-3">
|
||||
<div class="flex align-center">
|
||||
<IconBtn
|
||||
v-if="props.media?.total_episode"
|
||||
@@ -383,9 +386,6 @@ function onSubscribeEditRemove() {
|
||||
color="success"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hover.isHovering" class="me-n3 absolute top-1 right-10">
|
||||
<IconBtn><VIcon class="cursor-move text-white">mdi-drag</VIcon></IconBtn>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
@@ -288,6 +288,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.user-card-admin {
|
||||
border: 1px solid transparent;
|
||||
background-clip: content-box, border-box;
|
||||
background-image: linear-gradient(rgb(var(--v-theme-surface)), rgb(var(--v-theme-surface))),
|
||||
linear-gradient(120deg, rgba(var(--v-theme-warning), 0.5), rgba(var(--v-theme-error), 0.5));
|
||||
|
||||
@@ -28,6 +28,7 @@ import VueApexCharts from 'vue3-apexcharts'
|
||||
// 6. 注册自定义组件
|
||||
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
|
||||
import ScrollToTopBtn from '@/@core/components/ScrollToTopBtn.vue'
|
||||
import PageContentTitle from './@core/components/PageContentTitle.vue'
|
||||
import MediaCard from './components/cards/MediaCard.vue'
|
||||
import PosterCard from './components/cards/PosterCard.vue'
|
||||
import BackdropCard from './components/cards/BackdropCard.vue'
|
||||
@@ -38,7 +39,6 @@ import MediaIdSelector from './components/misc/MediaIdSelector.vue'
|
||||
import CronField from './components/field/CronField.vue'
|
||||
import PathField from './components/field/PathField.vue'
|
||||
import HeaderTab from './layouts/components/HeaderTab.vue'
|
||||
import PageContentTitle from './layouts/components/PageContentTitle.vue'
|
||||
|
||||
// 7. 样式文件
|
||||
import '@core/scss/template/libs/vuetify/index.scss'
|
||||
|
||||
@@ -12,7 +12,6 @@ const subType = route.meta.subType?.toString()
|
||||
const subId = ref(route.query.id as string)
|
||||
const activeTab = ref(route.query.tab)
|
||||
const shareViewKey = ref(0)
|
||||
const subscribeViewKey = ref(0)
|
||||
|
||||
// 默认订阅设置弹窗
|
||||
const subscribeEditDialog = ref(false)
|
||||
@@ -29,12 +28,6 @@ const subscribeFilter = ref('')
|
||||
// 分享搜索词
|
||||
const shareKeyword = ref('')
|
||||
|
||||
// 过滤订阅
|
||||
const filterSubscribes = () => {
|
||||
filterSubscribeDialog.value = false
|
||||
subscribeViewKey.value++
|
||||
}
|
||||
|
||||
// 搜索分享
|
||||
const searchShares = () => {
|
||||
searchShareDialog.value = false
|
||||
@@ -49,12 +42,12 @@ const searchShares = () => {
|
||||
<VMenu
|
||||
v-if="activeTab === '我的订阅'"
|
||||
v-model="filterSubscribeDialog"
|
||||
width="25rem"
|
||||
width="20rem"
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<VBtn
|
||||
icon="mdi-filter-cog-outline"
|
||||
icon="mdi-filter-multiple-outline"
|
||||
variant="text"
|
||||
:color="subscribeFilter ? 'primary' : 'gray'"
|
||||
size="default"
|
||||
@@ -65,17 +58,13 @@ const searchShares = () => {
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-filter-cog-outline" class="mr-2" />
|
||||
<VIcon icon="mdi-filter-multiple-outline" class="mr-2" />
|
||||
筛选订阅
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="filterSubscribeDialog = false" />
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextField v-model="subscribeFilter" label="名称" clearable density="comfortable">
|
||||
<template #append>
|
||||
<VBtn prepend-icon="mdi-check" color="primary" @click="filterSubscribes">确定</VBtn>
|
||||
</template>
|
||||
</VTextField>
|
||||
<VTextField v-model="subscribeFilter" label="名称" clearable density="comfortable" />
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VMenu>
|
||||
@@ -128,7 +117,7 @@ const searchShares = () => {
|
||||
<VWindowItem value="我的订阅">
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<SubscribeListView :type="subType" :subid="subId" :key="subscribeViewKey" :keyword="subscribeFilter" />
|
||||
<SubscribeListView :type="subType" :subid="subId" :keyword="subscribeFilter" />
|
||||
</div>
|
||||
</transition>
|
||||
</VWindowItem>
|
||||
|
||||
@@ -47,6 +47,11 @@ const loading = ref(false)
|
||||
// 已安装插件列表
|
||||
const dataList = ref<Plugin[]>([])
|
||||
|
||||
// 计算已安装插件的名称列表
|
||||
const installedPluginNames = computed(() => {
|
||||
return dataList.value.map(item => item.plugin_name)
|
||||
})
|
||||
|
||||
// 过滤后的已安装插件列表
|
||||
const filteredDataList = ref<Plugin[]>([])
|
||||
|
||||
@@ -56,6 +61,12 @@ const uninstalledList = ref<Plugin[]>([])
|
||||
// 插件市场插件列表
|
||||
const marketList = ref<Plugin[]>([])
|
||||
|
||||
// 排序后的未安装插件列表
|
||||
const sortedUninstalledList = ref<Plugin[]>([])
|
||||
|
||||
// 显示的未安装插件列表
|
||||
const displayUninstalledList = ref<Plugin[]>([])
|
||||
|
||||
// 是否刷新过
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
@@ -163,7 +174,7 @@ function sortPluginOrder() {
|
||||
// 保存顺序设置
|
||||
async function savePluginOrder() {
|
||||
// 顺序配置
|
||||
const orderObj = dataList.value.map(item => ({ id: item.id || '' }))
|
||||
const orderObj = filteredDataList.value.map(item => ({ id: item.id || '' }))
|
||||
orderConfig.value = orderObj
|
||||
const orderString = JSON.stringify(orderObj)
|
||||
localStorage.setItem('MP_PLUGIN_ORDER', orderString)
|
||||
@@ -337,8 +348,8 @@ async function refreshData() {
|
||||
fetchUninstalledPlugins()
|
||||
}
|
||||
|
||||
// 对uninstalledList进行排序,按PluginStatistics倒序
|
||||
const sortedUninstalledList = computed(() => {
|
||||
// 对uninstalledList进行排序到sortedUninstalledList
|
||||
watch([marketList, filterForm], () => {
|
||||
// 匹配过滤函数
|
||||
const match = (filter: Array<string>, value: string | undefined) =>
|
||||
filter.length === 0 || (value && filter.includes(value))
|
||||
@@ -347,9 +358,6 @@ const sortedUninstalledList = computed(() => {
|
||||
const filterText = (filter: string, value: string | undefined) =>
|
||||
!filter || (value && value.toLowerCase().includes(filter.toLowerCase()))
|
||||
|
||||
// 过滤后的数据列表
|
||||
const ret_list: Plugin[] = []
|
||||
|
||||
// 过滤
|
||||
marketList.value.forEach(value => {
|
||||
if (value) {
|
||||
@@ -359,22 +367,26 @@ const sortedUninstalledList = computed(() => {
|
||||
matchMultiple(filterForm.label, value.plugin_label) &&
|
||||
match(filterForm.repo, handleRepoUrl(value.repo_url))
|
||||
) {
|
||||
ret_list.push(value)
|
||||
sortedUninstalledList.value.push(value)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (isNullOrEmptyObject(PluginStatistics.value)) return ret_list
|
||||
// 数据排序
|
||||
if (!activeSort.value || activeSort.value === 'count') {
|
||||
return ret_list.sort((a, b) => {
|
||||
return PluginStatistics.value[b.id || '0'] - PluginStatistics.value[a.id || '0']
|
||||
})
|
||||
} else if (activeSort.value) {
|
||||
return ret_list.sort((a: any, b: any) => {
|
||||
return a[activeSort.value ?? ''] > b[activeSort.value ?? ''] ? 1 : -1
|
||||
})
|
||||
// 排序
|
||||
if (!isNullOrEmptyObject(PluginStatistics.value)) {
|
||||
if (!activeSort.value || activeSort.value === 'count') {
|
||||
sortedUninstalledList.value = sortedUninstalledList.value.sort((a, b) => {
|
||||
return PluginStatistics.value[b.id || '0'] - PluginStatistics.value[a.id || '0']
|
||||
})
|
||||
} else if (activeSort.value) {
|
||||
sortedUninstalledList.value = sortedUninstalledList.value.sort((a: any, b: any) => {
|
||||
return a[activeSort.value ?? ''] > b[activeSort.value ?? ''] ? 1 : -1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 显示前20个
|
||||
displayUninstalledList.value = sortedUninstalledList.value.slice(0, 20)
|
||||
})
|
||||
|
||||
// 标签转换
|
||||
@@ -405,10 +417,19 @@ function handleRepoUrl(url: string | undefined) {
|
||||
// 监测dataList变化或installedFilter变化时更新filteredDataList
|
||||
watch([dataList, installedFilter], () => {
|
||||
filteredDataList.value = dataList.value.filter(item => {
|
||||
if (!installedFilter.value) return true
|
||||
return item.plugin_name?.toLowerCase().includes(installedFilter.value.toLowerCase())
|
||||
})
|
||||
})
|
||||
|
||||
// 插件市场加载更多数据
|
||||
function loadMarketMore({ done }: { done: any }) {
|
||||
// 从 dataList 中获取最前面的 20 个元素
|
||||
const itemsToMove = sortedUninstalledList.value.splice(0, 20)
|
||||
displayUninstalledList.value.push(...itemsToMove)
|
||||
done('ok')
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onMounted(async () => {
|
||||
await loadPluginOrderConfig()
|
||||
@@ -436,7 +457,7 @@ onMounted(async () => {
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<VBtn
|
||||
icon="mdi-filter-cog-outline"
|
||||
icon="mdi-filter-multiple-outline"
|
||||
variant="text"
|
||||
:color="installedFilter ? 'primary' : 'gray'"
|
||||
size="default"
|
||||
@@ -447,13 +468,19 @@ onMounted(async () => {
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-filter-cog-outline" class="mr-2" />
|
||||
<VIcon icon="mdi-filter-multiple-outline" class="mr-2" />
|
||||
筛选插件
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="filterInstalledPluginDialog = false" />
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VTextField v-model="installedFilter" label="名称" density="comfortable" clearable />
|
||||
<VCombobox
|
||||
v-model="installedFilter"
|
||||
:items="installedPluginNames"
|
||||
label="名称"
|
||||
density="comfortable"
|
||||
clearable
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VMenu>
|
||||
@@ -465,7 +492,7 @@ onMounted(async () => {
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<VBtn
|
||||
icon="mdi-filter-cog-outline"
|
||||
icon="mdi-filter-multiple-outline"
|
||||
variant="text"
|
||||
:color="isFilterFormEmpty ? 'gray' : 'primary'"
|
||||
size="default"
|
||||
@@ -476,7 +503,7 @@ onMounted(async () => {
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-filter-cog-outline" class="mr-2" />
|
||||
<VIcon icon="mdi-filter-multiple-outline" class="mr-2" />
|
||||
筛选插件
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn @click="filterMarketPluginDialog = false" />
|
||||
@@ -584,13 +611,25 @@ onMounted(async () => {
|
||||
<transition name="fade-slide" appear>
|
||||
<div>
|
||||
<LoadingBanner v-if="!isAppMarketLoaded" class="mt-12" />
|
||||
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card">
|
||||
<template v-for="(data, index) in sortedUninstalledList" :key="`${data.id}_v${data.plugin_version}`">
|
||||
<PluginAppCard :plugin="data" :count="PluginStatistics[data.id || '0']" @install="pluginInstalled" />
|
||||
</template>
|
||||
</div>
|
||||
<!-- 资源列表 -->
|
||||
<VInfiniteScroll
|
||||
v-if="isAppMarketLoaded"
|
||||
mode="intersect"
|
||||
side="end"
|
||||
:items="displayUninstalledList"
|
||||
@load="loadMarketMore"
|
||||
class="overflow-hidden"
|
||||
>
|
||||
<template #loading />
|
||||
<template #empty />
|
||||
<div class="grid gap-4 grid-plugin-card">
|
||||
<template v-for="data in displayUninstalledList" :key="`${data.id}_v${data.plugin_version}`">
|
||||
<PluginAppCard :plugin="data" :count="PluginStatistics[data.id || '0']" @install="pluginInstalled" />
|
||||
</template>
|
||||
</div>
|
||||
</VInfiniteScroll>
|
||||
<NoDataFound
|
||||
v-if="uninstalledList.length === 0 && isAppMarketLoaded"
|
||||
v-if="displayUninstalledList.length === 0 && isAppMarketLoaded"
|
||||
error-code="404"
|
||||
error-title="没有未安装插件"
|
||||
error-description="所有可用插件均已安装。"
|
||||
|
||||
@@ -15,6 +15,10 @@ const appMode = inject('pwaMode') && display.mdAndDown.value
|
||||
// 用户 Store
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 从 Store 中获取用户信息
|
||||
const superUser = userStore.superUser
|
||||
const userName = userStore.userName
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
type: String,
|
||||
@@ -25,9 +29,6 @@ const props = defineProps({
|
||||
// 是否刷新过
|
||||
let isRefreshed = ref(false)
|
||||
|
||||
// 搜索关键字
|
||||
const keyword = ref(props.keyword || '')
|
||||
|
||||
// 顺序存储键值
|
||||
const localOrderKey = props.type === '电影' ? 'MP_SUBSCRIBE_MOVIE_ORDER' : 'MP_SUBSCRIBE_TV_ORDER'
|
||||
const orderRequestKey = props.type === '电影' ? 'SubscribeMovieOrder' : 'SubscribeTvOrder'
|
||||
@@ -48,16 +49,19 @@ const orderConfig = ref<{ id: number }[]>([])
|
||||
const displayList = ref<Subscribe[]>([])
|
||||
|
||||
// 监听dataList变化,同步更新displayList
|
||||
watch(dataList, () => {
|
||||
// 从 Store 中获取用户信息
|
||||
const superUser = userStore.superUser
|
||||
const userName = userStore.userName
|
||||
if (superUser) displayList.value = dataList.value.filter(data => data.type === props.type)
|
||||
else displayList.value = dataList.value.filter(data => data.type === props.type && data.username === userName)
|
||||
// 过滤关键字
|
||||
if (keyword.value) {
|
||||
displayList.value = displayList.value.filter(data => data.name.toLowerCase().includes(keyword.value.toLowerCase()))
|
||||
}
|
||||
watch([dataList, () => props.keyword], () => {
|
||||
if (superUser)
|
||||
displayList.value = dataList.value.filter(
|
||||
data =>
|
||||
data.type === props.type && (!props.keyword || data.name.toLowerCase().includes(props.keyword.toLowerCase())),
|
||||
)
|
||||
else
|
||||
displayList.value = dataList.value.filter(
|
||||
data =>
|
||||
data.type === props.type &&
|
||||
data.username === userName &&
|
||||
(!props.keyword || data.name.toLowerCase().includes(props.keyword.toLowerCase())),
|
||||
)
|
||||
// 排序
|
||||
sortSubscribeOrder()
|
||||
})
|
||||
@@ -166,7 +170,7 @@ onActivated(async () => {
|
||||
v-if="displayList.length === 0 && isRefreshed"
|
||||
error-code="404"
|
||||
error-title="没有数据"
|
||||
:error-description="keyword ? '没有搜索到相关内容,请更换搜索关键词。' : '请通过搜索添加电影、电视剧订阅。'"
|
||||
:error-description="keyword ? '没有筛选到相关内容,请更换筛选条件。' : '请通过搜索添加电影、电视剧订阅。'"
|
||||
/>
|
||||
<!-- 底部操作按钮 -->
|
||||
<div v-if="isRefreshed">
|
||||
|
||||
Reference in New Issue
Block a user