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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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="所有可用插件均已安装"

View File

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