feat: 添加插件名称计算属性,优化未安装插件列表显示逻辑

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