feat(dashboard): improve plugin platform support display and mobile accessibility (#5271)

* feat(dashboard): improve plugin platform support display and mobile accessibility

- Replace hover-based tooltips with interactive click menus for platform support information.
- Fix mobile touch issues by introducing explicit state control for status capsules.
- Enhance UI aesthetics with platform-specific icons and a structured vertical list layout.
- Add dynamic chevron icons to provide clear visual cues for expandable content.

* refactor(dashboard): refactor market card with computed properties for performance

* refactor(dashboard): unify plugin platform support UI with new reusable chip component

- Create shared 'PluginPlatformChip' component to encapsulate platform meta display.
- Fix mobile interaction bugs by simplifying menu triggers and event handling.
- Add stacked platform icon previews and dynamic chevron indicators within capsules.
- Improve information hierarchy using structured vertical lists for platform details.
- Optimize rendering efficiency with computed properties across both card views.
This commit is contained in:
Helian Nuits
2026-02-21 17:22:22 +08:00
committed by GitHub
parent a404436f2c
commit fa1d1e6034
3 changed files with 138 additions and 50 deletions
@@ -1,10 +1,11 @@
<script setup>
import { ref, computed } from "vue";
import { useModuleI18n } from "@/i18n/composables";
import { getPlatformDisplayName } from "@/utils/platformUtils";
import PluginPlatformChip from "@/components/shared/PluginPlatformChip.vue";
const { tm } = useModuleI18n("features/extension");
defineProps({
const props = defineProps({
plugin: {
type: Object,
required: true,
@@ -26,12 +27,6 @@ const normalizePlatformList = (platforms) => {
return platforms.filter((item) => typeof item === "string");
};
const getPlatformDisplayList = (platforms) => {
return normalizePlatformList(platforms).map((platformId) =>
getPlatformDisplayName(platformId),
);
};
const handleInstall = (plugin) => {
emit("install", plugin);
};
@@ -165,9 +160,9 @@ const handleInstall = (plugin) => {
</div>
<div
v-if="plugin.astrbot_version || normalizePlatformList(plugin.support_platforms).length"
v-if="plugin.astrbot_version || platformDisplayList.length"
class="d-flex align-center flex-wrap"
style="gap: 4px; margin-top: 4px; margin-bottom: 4px;"
style="gap: 4px; margin-top: 4px; margin-bottom: 4px"
>
<v-chip
v-if="plugin.astrbot_version"
@@ -178,26 +173,11 @@ const handleInstall = (plugin) => {
>
AstrBot: {{ plugin.astrbot_version }}
</v-chip>
<v-chip
v-if="normalizePlatformList(plugin.support_platforms).length"
<PluginPlatformChip
:platforms="plugin.support_platforms"
size="x-small"
color="info"
variant="outlined"
style="height: 20px"
>
<v-tooltip location="top">
<template v-slot:activator="{ props: tooltipProps }">
<span v-bind="tooltipProps">
{{
tm("card.status.supportPlatformsCount", {
count: getPlatformDisplayList(plugin.support_platforms).length,
})
}}
</span>
</template>
<span>{{ getPlatformDisplayList(plugin.support_platforms).join(", ") }}</span>
</v-tooltip>
</v-chip>
:chip-style="{ height: '20px' }"
/>
</div>
<div class="d-flex align-center" style="gap: 8px; margin-top: auto">
@@ -2,8 +2,9 @@
import { ref, computed, inject } from "vue";
import { useCustomizerStore } from "@/stores/customizer";
import { useModuleI18n } from "@/i18n/composables";
import { getPlatformDisplayName } from "@/utils/platformUtils";
import { getPlatformDisplayName, getPlatformIcon } from "@/utils/platformUtils";
import UninstallConfirmDialog from "./UninstallConfirmDialog.vue";
import PluginPlatformChip from "./PluginPlatformChip.vue";
const props = defineProps({
extension: {
@@ -336,27 +337,10 @@ const viewChangelog = () => {
>
{{ tag === "danger" ? tm("tags.danger") : tag }}
</v-chip>
<v-chip
v-if="supportPlatforms.length"
color="info"
variant="outlined"
label
size="small"
<PluginPlatformChip
:platforms="supportPlatforms"
class="ml-2"
>
<v-tooltip location="top">
<template v-slot:activator="{ props: tooltipProps }">
<span v-bind="tooltipProps">
{{
tm("card.status.supportPlatformsCount", {
count: supportPlatformDisplayNames.length,
})
}}
</span>
</template>
<span>{{ supportPlatformDisplayNames.join(", ") }}</span>
</v-tooltip>
</v-chip>
/>
<v-chip
v-if="astrbotVersionRequirement"
color="secondary"
@@ -0,0 +1,124 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { getPlatformDisplayName, getPlatformIcon } from "@/utils/platformUtils";
import { useModuleI18n } from "@/i18n/composables";
const props = defineProps({
platforms: {
type: Array,
default: () => [],
},
size: {
type: String,
default: "small",
},
chipStyle: {
type: Object,
default: () => ({}),
},
});
const { tm } = useModuleI18n("features/extension");
const showMenu = ref(false);
const platformDetails = computed(() => {
if (!Array.isArray(props.platforms)) return [];
return props.platforms
.filter((item) => typeof item === "string")
.map((platformId) => ({
name: getPlatformDisplayName(platformId as string),
icon: getPlatformIcon(platformId as string),
}));
});
</script>
<template>
<div class="d-inline-block">
<v-chip
v-if="platformDetails.length"
color="info"
variant="outlined"
label
:size="size"
class="plugin-platform-chip"
:style="{ cursor: 'pointer', ...chipStyle }"
@click.stop="showMenu = !showMenu"
>
<div class="d-flex align-center" style="gap: 2px">
<!-- 显示图标最多 5 -->
<div class="d-flex align-center mr-1" v-if="platformDetails.some(p => p.icon)">
<v-avatar
v-for="(platform, index) in platformDetails.slice(0, 5)"
:key="index"
:size="size === 'x-small' ? 12 : 14"
class="platform-mini-icon"
:style="{ marginLeft: index > 0 ? '-4px' : '0', zIndex: 10 - index }"
>
<v-img v-if="platform.icon" :src="platform.icon"></v-img>
<v-icon v-else icon="mdi-circle-small" :size="size === 'x-small' ? 8 : 10"></v-icon>
</v-avatar>
</div>
<span class="text-caption font-weight-bold">
{{
tm("card.status.supportPlatformsCount", {
count: platformDetails.length,
})
}}
</span>
<v-icon
:icon="showMenu ? 'mdi-chevron-up' : 'mdi-chevron-down'"
:size="size === 'x-small' ? 14 : 16"
class="ml-n1"
></v-icon>
</div>
<v-menu
v-model="showMenu"
activator="parent"
location="top"
:close-on-content-click="false"
transition="scale-transition"
open-on-hover
>
<v-list density="compact" border elevation="12" class="rounded-lg pa-1">
<v-list-item
v-for="platform in platformDetails"
:key="platform.name"
min-height="24"
class="px-2"
>
<template v-slot:prepend>
<v-avatar size="14" class="mr-2" v-if="platform.icon">
<v-img :src="platform.icon"></v-img>
</v-avatar>
<v-icon v-else icon="mdi-platform" size="12" class="mr-2"></v-icon>
</template>
<v-list-item-title class="text-caption font-weight-bold" style="font-size: 0.75rem !important">
{{ platform.name }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-chip>
</div>
</template>
<style scoped>
.plugin-platform-chip {
padding-left: 6px !important;
padding-right: 4px !important;
transition: all 0.2s ease;
}
.platform-mini-icon {
border: 1px solid rgba(var(--v-theme-info), 0.3);
background: rgba(var(--v-theme-surface));
}
.plugin-platform-chip:hover {
background: rgba(var(--v-theme-info), 0.08);
}
</style>