mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-18 19:11:42 +08:00
138 lines
4.0 KiB
Vue
138 lines
4.0 KiB
Vue
<script lang="ts" setup>
|
|
import { getLogoUrl } from '@/utils/imageUtils'
|
|
import type { Plugin } from '@/api/types'
|
|
import { useDisplay } from 'vuetify'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
const props = defineProps({
|
|
keyword: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
modelValue: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
plugins: {
|
|
type: Array as PropType<Plugin[]>,
|
|
default: () => [],
|
|
},
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
(event: 'close'): void
|
|
(event: 'open-plugin', plugin: Plugin): void
|
|
(event: 'update:keyword', value: string): void
|
|
(event: 'update:modelValue', value: boolean): void
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
const display = useDisplay()
|
|
const pluginIconLoaded = ref<Record<string, boolean>>({})
|
|
|
|
const dialogVisible = computed({
|
|
get: () => props.modelValue,
|
|
set: value => emit('update:modelValue', value),
|
|
})
|
|
|
|
const searchKeyword = computed({
|
|
get: () => props.keyword,
|
|
set: value => emit('update:keyword', value),
|
|
})
|
|
|
|
// 返回插件图标地址,并在远程图标失败后回退到默认图标。
|
|
function pluginIcon(item: Plugin) {
|
|
if (pluginIconLoaded.value[item.id || '0'] === false) return getLogoUrl('plugin')
|
|
if (item?.plugin_icon?.startsWith('http')) {
|
|
return `${import.meta.env.VITE_API_BASE_URL}system/img/1?imgurl=${encodeURIComponent(item?.plugin_icon)}&cache=true`
|
|
}
|
|
|
|
return `./plugin_icon/${item?.plugin_icon}`
|
|
}
|
|
|
|
// 标记指定插件图标加载失败。
|
|
function pluginIconError(item: Plugin) {
|
|
pluginIconLoaded.value[item.id || '0'] = false
|
|
}
|
|
|
|
// 获取插件标签列表。
|
|
function pluginLabels(label: string | undefined) {
|
|
if (!label) return []
|
|
return label.split(',')
|
|
}
|
|
|
|
// 关闭搜索弹窗并通知共享弹窗 Host 回收实例。
|
|
function closeDialog() {
|
|
emit('close')
|
|
emit('update:modelValue', false)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<VDialog
|
|
v-model="dialogVisible"
|
|
scrollable
|
|
max-width="40rem"
|
|
:max-height="!display.mdAndUp.value ? '' : '85vh'"
|
|
:fullscreen="!display.mdAndUp.value"
|
|
>
|
|
<VCard class="mx-auto" width="100%">
|
|
<VToolbar flat class="p-0">
|
|
<VTextField
|
|
v-model="searchKeyword"
|
|
:label="t('plugin.searchPlugins')"
|
|
single-line
|
|
:placeholder="t('plugin.searchPlaceholder')"
|
|
variant="solo"
|
|
prepend-inner-icon="mdi-magnify"
|
|
flat
|
|
class="mx-1"
|
|
/>
|
|
</VToolbar>
|
|
<VDialogCloseBtn @click="closeDialog" />
|
|
<VList v-if="plugins.length > 0" class="plugin-search-list" lines="two">
|
|
<VVirtualScroll :items="plugins">
|
|
<template #default="{ item }">
|
|
<VListItem @click="emit('open-plugin', item)">
|
|
<template #prepend>
|
|
<VAvatar>
|
|
<VImg :src="pluginIcon(item)" @error="pluginIconError(item)">
|
|
<template #placeholder>
|
|
<div class="w-full h-full">
|
|
<VSkeletonLoader class="object-cover aspect-w-1 aspect-h-1" />
|
|
</div>
|
|
</template>
|
|
</VImg>
|
|
</VAvatar>
|
|
</template>
|
|
<VListItemTitle>
|
|
{{ item.plugin_name }}<span class="text-sm ms-2 mt-1 text-gray-500">v{{ item?.plugin_version }}</span>
|
|
<VIcon v-if="item.installed" color="success" icon="mdi-check-circle" class="ms-2" size="small" />
|
|
</VListItemTitle>
|
|
<VListItemSubtitle>
|
|
<VChip
|
|
v-for="label in pluginLabels(item.plugin_label)"
|
|
:key="label"
|
|
variant="tonal"
|
|
size="small"
|
|
class="me-1 my-1"
|
|
color="info"
|
|
label
|
|
>
|
|
{{ label }}
|
|
</VChip>
|
|
{{ item.plugin_desc }}
|
|
</VListItemSubtitle>
|
|
</VListItem>
|
|
</template>
|
|
</VVirtualScroll>
|
|
</VList>
|
|
</VCard>
|
|
</VDialog>
|
|
</template>
|
|
<style lang="scss" scoped>
|
|
.plugin-search-list {
|
|
border-radius: 0 !important;
|
|
}
|
|
</style>
|