mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 01:40:15 +08:00
feat: Enhance plugin detail and installation experience with new UI elements and internationalization support
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useModuleI18n } from "@/i18n/composables";
|
||||
import PluginPlatformChip from "@/components/shared/PluginPlatformChip.vue";
|
||||
|
||||
@@ -20,7 +20,7 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["install"]);
|
||||
const emit = defineEmits(["install", "open"]);
|
||||
|
||||
const normalizePlatformList = (platforms) => {
|
||||
if (!Array.isArray(platforms)) return [];
|
||||
@@ -35,6 +35,10 @@ const handleInstall = (plugin) => {
|
||||
emit("install", plugin);
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
emit("open", props.plugin);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -42,6 +46,8 @@ const handleInstall = (plugin) => {
|
||||
class="rounded-lg d-flex flex-column plugin-card"
|
||||
variant="outlined"
|
||||
elevation="0"
|
||||
:ripple="false"
|
||||
@click="handleOpen"
|
||||
>
|
||||
|
||||
<v-card-text
|
||||
@@ -111,17 +117,6 @@ const handleInstall = (plugin) => {
|
||||
>
|
||||
{{ plugin.author }}
|
||||
</span>
|
||||
<div
|
||||
class="d-flex align-center text-subtitle-2 ml-2"
|
||||
style="color: rgba(var(--v-theme-on-surface), 0.7)"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-source-branch"
|
||||
size="x-small"
|
||||
style="margin-right: 2px"
|
||||
></v-icon>
|
||||
<span>{{ plugin.version }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="plugin.stars !== undefined"
|
||||
class="d-flex align-center text-subtitle-2 ml-2"
|
||||
@@ -141,18 +136,9 @@ const handleInstall = (plugin) => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="plugin.astrbot_version || platformDisplayList.length"
|
||||
v-if="platformDisplayList.length"
|
||||
class="plugin-badges"
|
||||
>
|
||||
<v-chip
|
||||
v-if="plugin.astrbot_version"
|
||||
size="x-small"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
style="height: 20px"
|
||||
>
|
||||
AstrBot: {{ plugin.astrbot_version }}
|
||||
</v-chip>
|
||||
<PluginPlatformChip
|
||||
:platforms="plugin.support_platforms"
|
||||
size="x-small"
|
||||
@@ -168,36 +154,6 @@ const handleInstall = (plugin) => {
|
||||
style="gap: 6px; padding: 8px 12px; padding-top: 0"
|
||||
@click.stop
|
||||
>
|
||||
<v-chip
|
||||
v-for="tag in plugin.tags?.slice(0, 2)"
|
||||
:key="tag"
|
||||
:color="tag === 'danger' ? 'error' : 'primary'"
|
||||
label
|
||||
size="x-small"
|
||||
style="height: 20px"
|
||||
>
|
||||
{{ tag === "danger" ? tm("tags.danger") : tag }}
|
||||
</v-chip>
|
||||
<v-menu v-if="plugin.tags && plugin.tags.length > 2" open-on-hover offset-y>
|
||||
<template v-slot:activator="{ props: menuProps }">
|
||||
<v-chip
|
||||
v-bind="menuProps"
|
||||
color="grey"
|
||||
label
|
||||
size="x-small"
|
||||
style="height: 20px; cursor: pointer"
|
||||
>
|
||||
+{{ plugin.tags.length - 2 }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item v-for="tag in plugin.tags.slice(2)" :key="tag">
|
||||
<v-chip :color="tag === 'danger' ? 'error' : 'primary'" label size="small">
|
||||
{{ tag === "danger" ? tm("tags.danger") : tag }}
|
||||
</v-chip>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
v-if="plugin?.repo"
|
||||
@@ -241,6 +197,7 @@ const handleInstall = (plugin) => {
|
||||
<style scoped>
|
||||
.plugin-card {
|
||||
background: rgb(var(--v-theme-surface));
|
||||
cursor: pointer;
|
||||
transition: background-color 0.16s ease;
|
||||
}
|
||||
|
||||
@@ -249,6 +206,11 @@ const handleInstall = (plugin) => {
|
||||
background: rgba(var(--v-theme-on-surface), 0.04);
|
||||
}
|
||||
|
||||
.plugin-card :deep(.v-card__overlay),
|
||||
.plugin-card :deep(.v-ripple__container) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.plugin-card-content {
|
||||
padding: 12px;
|
||||
padding-bottom: 8px;
|
||||
@@ -318,11 +280,11 @@ const handleInstall = (plugin) => {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
min-height: calc(1.3em * 3);
|
||||
max-height: calc(1.3em * 3);
|
||||
min-height: calc(1.3em * 2);
|
||||
max-height: calc(1.3em * 2);
|
||||
}
|
||||
|
||||
.plugin-badges {
|
||||
|
||||
@@ -109,8 +109,12 @@
|
||||
},
|
||||
"info": {
|
||||
"title": "Info",
|
||||
"version": "Version",
|
||||
"author": "Author",
|
||||
"category": "Category",
|
||||
"stars": "Stars",
|
||||
"tags": "Tags",
|
||||
"astrbotVersion": "AstrBot Version Requirement",
|
||||
"authorWebsite": "Author Website",
|
||||
"repository": "Repository"
|
||||
}
|
||||
@@ -152,8 +156,6 @@
|
||||
"sourceExists": "This source already exists",
|
||||
"installPlugin": "Install Plugin",
|
||||
"randomPlugins": "🎲 Random Plugins",
|
||||
"showRandomPlugins": "Show Random Plugins",
|
||||
"hideRandomPlugins": "Hide Random Plugins",
|
||||
"sourceSafetyWarning": "Even with the default source, plugin stability and security cannot be fully guaranteed. Please verify carefully before use."
|
||||
},
|
||||
"sort": {
|
||||
|
||||
@@ -109,8 +109,12 @@
|
||||
},
|
||||
"info": {
|
||||
"title": "Информация",
|
||||
"version": "Версия",
|
||||
"author": "Автор",
|
||||
"category": "Категория",
|
||||
"stars": "Звезды",
|
||||
"tags": "Теги",
|
||||
"astrbotVersion": "Требуемая версия AstrBot",
|
||||
"authorWebsite": "Сайт автора",
|
||||
"repository": "Репозиторий"
|
||||
}
|
||||
@@ -151,8 +155,6 @@
|
||||
"sourceExists": "Этот источник уже есть в списке",
|
||||
"installPlugin": "Установить плагин",
|
||||
"randomPlugins": "🎲 Случайные плагины",
|
||||
"showRandomPlugins": "Показать случайные",
|
||||
"hideRandomPlugins": "Скрыть случайные",
|
||||
"sourceSafetyWarning": "Даже при использовании источников по умолчанию мы не можем гарантировать 100% безопасность и стабильность сторонних плагинов. Пожалуйста, будьте внимательны."
|
||||
},
|
||||
"sort": {
|
||||
|
||||
@@ -109,8 +109,12 @@
|
||||
},
|
||||
"info": {
|
||||
"title": "信息",
|
||||
"version": "版本",
|
||||
"author": "作者",
|
||||
"category": "类别",
|
||||
"stars": "Star数",
|
||||
"tags": "标签",
|
||||
"astrbotVersion": "AstrBot 版本要求",
|
||||
"authorWebsite": "作者网站",
|
||||
"repository": "仓库"
|
||||
}
|
||||
@@ -152,8 +156,6 @@
|
||||
"sourceExists": "该插件源已存在",
|
||||
"installPlugin": "安装插件",
|
||||
"randomPlugins": "🎲 随机插件",
|
||||
"showRandomPlugins": "显示随机插件",
|
||||
"hideRandomPlugins": "隐藏随机插件",
|
||||
"sourceSafetyWarning": "即使是默认插件源,我们也不能完全保证插件的稳定性和安全性,使用前请谨慎核查。"
|
||||
},
|
||||
"sort": {
|
||||
|
||||
@@ -163,6 +163,7 @@ export const useCommonStore = defineStore("common", {
|
||||
const pluginData = res.data.data[key];
|
||||
|
||||
data.push({
|
||||
...pluginData,
|
||||
"name": pluginData.name || key, // 优先使用插件数据中的name字段,否则使用键名
|
||||
"desc": pluginData.desc,
|
||||
"author": pluginData.author,
|
||||
|
||||
@@ -12,8 +12,11 @@ import MarketPluginsTab from "./extension/MarketPluginsTab.vue";
|
||||
import PluginDetailPage from "./extension/PluginDetailPage.vue";
|
||||
import { useExtensionPage } from "./extension/useExtensionPage";
|
||||
import { computed } from "vue";
|
||||
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
|
||||
import { usePluginI18n } from "@/utils/pluginI18n";
|
||||
|
||||
const pageState = useExtensionPage();
|
||||
const { pluginName, pluginDesc } = usePluginI18n();
|
||||
|
||||
const {
|
||||
commonStore,
|
||||
@@ -124,6 +127,8 @@ const {
|
||||
reloadPlugin,
|
||||
viewReadme,
|
||||
viewChangelog,
|
||||
openInstallDialog,
|
||||
closeInstallDialog,
|
||||
handleInstallPlugin,
|
||||
confirmDangerInstall,
|
||||
cancelDangerInstall,
|
||||
@@ -158,6 +163,8 @@ const selectedPluginId = computed(() => {
|
||||
return Array.isArray(pluginId) ? pluginId[0] : pluginId || "";
|
||||
});
|
||||
|
||||
const selectedDetailTab = computed(() => extractTabFromHash(route.hash) || "installed");
|
||||
|
||||
const selectedInstalledPlugin = computed(() => {
|
||||
if (!selectedPluginId.value) return null;
|
||||
const data = Array.isArray(extension_data?.data) ? extension_data.data : [];
|
||||
@@ -165,23 +172,59 @@ const selectedInstalledPlugin = computed(() => {
|
||||
});
|
||||
|
||||
const selectedMarketPlugin = computed(() => {
|
||||
const plugin = selectedInstalledPlugin.value;
|
||||
if (!plugin) return null;
|
||||
const market = Array.isArray(pluginMarketData.value) ? pluginMarketData.value : [];
|
||||
const repo = plugin.repo?.toLowerCase();
|
||||
const installedPlugin = selectedInstalledPlugin.value;
|
||||
const repo = installedPlugin?.repo?.toLowerCase();
|
||||
return (
|
||||
market.find((item) => item.name === selectedPluginId.value) ||
|
||||
market.find((item) => repo && item.repo?.toLowerCase() === repo) ||
|
||||
market.find((item) => item.name === plugin.name) ||
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
const selectedDetailPlugin = computed(() => {
|
||||
if (selectedDetailTab.value === "market" && selectedMarketPlugin.value) {
|
||||
return selectedMarketPlugin.value;
|
||||
}
|
||||
return selectedInstalledPlugin.value || selectedMarketPlugin.value;
|
||||
});
|
||||
|
||||
const installDialogPluginName = computed(() =>
|
||||
selectedInstallPlugin.value ? pluginName(selectedInstallPlugin.value) : "",
|
||||
);
|
||||
|
||||
const installDialogPluginDesc = computed(() =>
|
||||
String(
|
||||
selectedInstallPlugin.value
|
||||
? pluginDesc(
|
||||
selectedInstallPlugin.value,
|
||||
selectedInstallPlugin.value.desc ||
|
||||
selectedInstallPlugin.value.description ||
|
||||
"",
|
||||
)
|
||||
: "",
|
||||
).trim(),
|
||||
);
|
||||
|
||||
const installDialogPluginAuthor = computed(() => {
|
||||
const author = selectedInstallPlugin.value?.author;
|
||||
if (Array.isArray(author)) return author.join(", ");
|
||||
if (author && typeof author === "object") return author.name || "";
|
||||
return typeof author === "string" ? author.trim() : "";
|
||||
});
|
||||
|
||||
const installDialogPluginLogo = computed(() => {
|
||||
const logo = selectedInstallPlugin.value?.logo;
|
||||
return typeof logo === "string" && logo.trim() ? logo : defaultPluginIcon;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PluginDetailPage
|
||||
v-if="selectedPluginId && selectedInstalledPlugin"
|
||||
:plugin="selectedInstalledPlugin"
|
||||
v-if="selectedPluginId && selectedDetailPlugin"
|
||||
:plugin="selectedDetailPlugin"
|
||||
:market-plugin="selectedMarketPlugin"
|
||||
:source-tab="selectedDetailTab"
|
||||
:state="pageState"
|
||||
/>
|
||||
|
||||
@@ -195,10 +238,10 @@ const selectedMarketPlugin = computed(() => {
|
||||
icon="mdi-arrow-left"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
@click="router.push({ name: 'Extensions', hash: '#installed' })"
|
||||
@click="router.push({ name: 'Extensions', hash: `#${selectedDetailTab}` })"
|
||||
/>
|
||||
<h2 class="text-h3 mb-0 ml-2">
|
||||
{{ tm("titles.installedAstrBotPlugins") }}
|
||||
{{ selectedDetailTab === "market" ? tm("tabs.market") : tm("titles.installedAstrBotPlugins") }}
|
||||
<v-icon icon="mdi-chevron-right" size="24" class="mx-1" />
|
||||
{{ selectedPluginId }}
|
||||
</h2>
|
||||
@@ -607,12 +650,87 @@ const selectedMarketPlugin = computed(() => {
|
||||
</v-card-title>
|
||||
|
||||
<div class="v-card-text">
|
||||
<v-tabs v-model="uploadTab" color="primary">
|
||||
<div v-if="selectedMarketInstallPlugin" class="market-install-confirm">
|
||||
<div class="market-install-confirm__header">
|
||||
<img
|
||||
:src="installDialogPluginLogo"
|
||||
:alt="installDialogPluginName"
|
||||
class="market-install-confirm__logo"
|
||||
/>
|
||||
<div class="market-install-confirm__meta">
|
||||
<div class="market-install-confirm__name">
|
||||
{{ installDialogPluginName }}
|
||||
</div>
|
||||
<div
|
||||
v-if="installDialogPluginAuthor"
|
||||
class="market-install-confirm__author"
|
||||
>
|
||||
{{ tm("detail.info.author") }}: {{ installDialogPluginAuthor }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<div v-if="installDialogPluginDesc" class="market-install-confirm__section">
|
||||
<div class="market-install-confirm__section-title">
|
||||
{{ tm("table.headers.description") }}
|
||||
</div>
|
||||
<div class="market-install-confirm__desc">
|
||||
{{ installDialogPluginDesc }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedInstallPlugin" class="mt-4">
|
||||
<v-chip
|
||||
v-if="selectedInstallPlugin.astrbot_version"
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
class="mr-2 mb-2"
|
||||
>
|
||||
{{ tm("card.status.astrbotVersion") }}:
|
||||
{{ selectedInstallPlugin.astrbot_version }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="normalizePlatformList(selectedInstallPlugin.support_platforms).length"
|
||||
size="small"
|
||||
color="info"
|
||||
variant="outlined"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ tm("card.status.supportPlatform") }}:
|
||||
{{
|
||||
getPlatformDisplayList(selectedInstallPlugin.support_platforms).join(
|
||||
", ",
|
||||
)
|
||||
}}
|
||||
</v-chip>
|
||||
<v-alert
|
||||
v-if="
|
||||
selectedInstallPlugin.astrbot_version &&
|
||||
installCompat.checked &&
|
||||
!installCompat.compatible
|
||||
"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
density="comfortable"
|
||||
class="mt-2"
|
||||
>
|
||||
{{ installCompat.message }}
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<ProxySelector class="mt-4" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<v-tabs v-model="uploadTab" color="primary">
|
||||
<v-tab value="file">{{ tm("dialogs.install.fromFile") }}</v-tab>
|
||||
<v-tab value="url">{{ tm("dialogs.install.fromUrl") }}</v-tab>
|
||||
</v-tabs>
|
||||
</v-tabs>
|
||||
|
||||
<v-window v-model="uploadTab" class="mt-4">
|
||||
<v-window v-model="uploadTab" class="mt-4">
|
||||
<v-window-item value="file">
|
||||
<div class="d-flex flex-column align-center justify-center pa-4">
|
||||
<v-file-input
|
||||
@@ -712,12 +830,13 @@ const selectedMarketPlugin = computed(() => {
|
||||
<ProxySelector></ProxySelector>
|
||||
</div>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-window>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="v-card-actions">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" variant="text" @click="dialog = false">{{
|
||||
<v-btn color="grey" variant="text" @click="closeInstallDialog">{{
|
||||
tm("buttons.cancel")
|
||||
}}</v-btn>
|
||||
<v-btn color="primary" variant="text" @click="newExtension">{{
|
||||
@@ -929,6 +1048,50 @@ const selectedMarketPlugin = computed(() => {
|
||||
transform: translateY(-4px) scale(1.05);
|
||||
box-shadow: 0 12px 20px rgba(var(--v-theme-primary), 0.4);
|
||||
}
|
||||
|
||||
.market-install-confirm {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.market-install-confirm__header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.market-install-confirm__logo {
|
||||
border-radius: 14px;
|
||||
height: 64px;
|
||||
object-fit: cover;
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.market-install-confirm__meta {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.market-install-confirm__name {
|
||||
color: rgba(var(--v-theme-on-surface), 0.92);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.market-install-confirm__author,
|
||||
.market-install-confirm__desc {
|
||||
color: rgba(var(--v-theme-on-surface), 0.64);
|
||||
line-height: 1.55;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.market-install-confirm__section-title {
|
||||
color: rgba(var(--v-theme-on-surface), 0.92);
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -116,6 +116,7 @@ const {
|
||||
reloadPlugin,
|
||||
viewReadme,
|
||||
viewChangelog,
|
||||
openInstallDialog,
|
||||
handleInstallPlugin,
|
||||
confirmDangerInstall,
|
||||
cancelDangerInstall,
|
||||
@@ -365,7 +366,7 @@ const togglePinnedExtension = (extension) => {
|
||||
z-index: 10000;
|
||||
border-radius: 16px;
|
||||
"
|
||||
@click="dialog = true"
|
||||
@click="openInstallDialog"
|
||||
>
|
||||
<span class="v-btn__overlay"></span>
|
||||
<span class="v-btn__underlay"></span>
|
||||
|
||||
@@ -80,7 +80,6 @@ const {
|
||||
sortBy,
|
||||
sortOrder,
|
||||
randomPluginNames,
|
||||
showRandomPlugins,
|
||||
marketCategoryFilter,
|
||||
marketCategoryItems,
|
||||
normalizeStr,
|
||||
@@ -96,7 +95,6 @@ const {
|
||||
randomPlugins,
|
||||
shufflePlugins,
|
||||
refreshRandomPlugins,
|
||||
toggleRandomPluginsVisibility,
|
||||
displayItemsPerPage,
|
||||
totalPages,
|
||||
paginatedPlugins,
|
||||
@@ -125,6 +123,7 @@ const {
|
||||
reloadPlugin,
|
||||
viewReadme,
|
||||
viewChangelog,
|
||||
openInstallDialog,
|
||||
handleInstallPlugin,
|
||||
confirmDangerInstall,
|
||||
cancelDangerInstall,
|
||||
@@ -175,6 +174,15 @@ const marketCategorySelectItems = computed(() =>
|
||||
value: item.value,
|
||||
})),
|
||||
);
|
||||
|
||||
const openMarketPluginDetail = (plugin) => {
|
||||
if (!plugin?.name) return;
|
||||
router.push({
|
||||
name: "ExtensionDetails",
|
||||
params: { pluginId: plugin.name },
|
||||
hash: "#market",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -205,20 +213,6 @@ const marketCategorySelectItems = computed(() =>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
rounded="md"
|
||||
class="text-none px-2"
|
||||
:prepend-icon="showRandomPlugins ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
@click="toggleRandomPluginsVisibility"
|
||||
>
|
||||
{{
|
||||
showRandomPlugins
|
||||
? tm("market.hideRandomPlugins")
|
||||
: tm("market.showRandomPlugins")
|
||||
}}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-text-field
|
||||
@@ -263,7 +257,7 @@ const marketCategorySelectItems = computed(() =>
|
||||
z-index: 10000;
|
||||
border-radius: 16px;
|
||||
"
|
||||
@click="dialog = true"
|
||||
@click="openInstallDialog"
|
||||
>
|
||||
<span class="v-btn__overlay"></span>
|
||||
<span class="v-btn__underlay"></span>
|
||||
@@ -347,6 +341,7 @@ const marketCategorySelectItems = computed(() =>
|
||||
:default-plugin-icon="defaultPluginIcon"
|
||||
:show-plugin-full-name="showPluginFullName"
|
||||
@install="handleInstallPlugin"
|
||||
@open="openMarketPluginDetail"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -364,7 +359,7 @@ const marketCategorySelectItems = computed(() =>
|
||||
</div>
|
||||
|
||||
<v-expand-transition>
|
||||
<div v-if="showRandomPlugins">
|
||||
<div v-if="randomPlugins.length > 0">
|
||||
<div
|
||||
class="d-flex align-center mb-2 mt-4"
|
||||
style="justify-content: space-between; flex-wrap: wrap; gap: 8px"
|
||||
@@ -397,6 +392,7 @@ const marketCategorySelectItems = computed(() =>
|
||||
:default-plugin-icon="defaultPluginIcon"
|
||||
:show-plugin-full-name="showPluginFullName"
|
||||
@install="handleInstallPlugin"
|
||||
@open="openMarketPluginDetail"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@@ -15,6 +15,10 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
sourceTab: {
|
||||
type: String,
|
||||
default: "installed",
|
||||
},
|
||||
state: {
|
||||
type: Object,
|
||||
required: true,
|
||||
@@ -78,6 +82,13 @@ const detailPageRef = ref(null);
|
||||
const isHeaderStuck = ref(false);
|
||||
|
||||
const displayName = computed(() => pluginName(props.plugin));
|
||||
const detailSourceTab = computed(() =>
|
||||
props.sourceTab === "market" ? "market" : "installed",
|
||||
);
|
||||
const isMarketDetail = computed(() => detailSourceTab.value === "market");
|
||||
const detailParentTitle = computed(() =>
|
||||
isMarketDetail.value ? tm("tabs.market") : tm("titles.installedAstrBotPlugins"),
|
||||
);
|
||||
|
||||
const pluginDesc = computed(() => {
|
||||
const desc =
|
||||
@@ -143,10 +154,53 @@ const authorWebsite = computed(() => {
|
||||
|
||||
const repoUrl = computed(() => props.plugin.repo || props.marketPlugin?.repo || "");
|
||||
|
||||
const firstPresentValue = (...values) =>
|
||||
values.find(
|
||||
(value) =>
|
||||
value !== undefined &&
|
||||
value !== null &&
|
||||
value !== "" &&
|
||||
(!Array.isArray(value) || value.length > 0),
|
||||
);
|
||||
|
||||
const versionDisplay = computed(() =>
|
||||
String(firstPresentValue(props.plugin.version, props.marketPlugin?.version) || "").trim(),
|
||||
);
|
||||
|
||||
const starsDisplay = computed(() => {
|
||||
const value = firstPresentValue(props.plugin.stars, props.marketPlugin?.stars);
|
||||
return value === undefined ? "" : String(value);
|
||||
});
|
||||
|
||||
const tagsDisplay = computed(() => {
|
||||
const tags = firstPresentValue(props.plugin.tags, props.marketPlugin?.tags);
|
||||
if (!Array.isArray(tags)) return [];
|
||||
return tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0);
|
||||
});
|
||||
|
||||
const astrbotVersionDisplay = computed(() =>
|
||||
String(
|
||||
firstPresentValue(props.plugin.astrbot_version, props.marketPlugin?.astrbot_version) || "",
|
||||
).trim(),
|
||||
);
|
||||
|
||||
const infoRows = computed(() => {
|
||||
const rows = [
|
||||
{ label: tm("detail.info.version"), value: versionDisplay.value, optional: true },
|
||||
{ label: tm("detail.info.author"), value: authorDisplay.value },
|
||||
{ label: tm("detail.info.category"), value: categoryDisplay.value, optional: true },
|
||||
{ label: tm("detail.info.stars"), value: starsDisplay.value, optional: true },
|
||||
{
|
||||
label: tm("detail.info.tags"),
|
||||
value: tagsDisplay.value,
|
||||
kind: "tags",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
label: tm("detail.info.astrbotVersion"),
|
||||
value: astrbotVersionDisplay.value,
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
label: tm("detail.info.authorWebsite"),
|
||||
value: authorWebsite.value,
|
||||
@@ -161,12 +215,38 @@ const infoRows = computed(() => {
|
||||
},
|
||||
];
|
||||
|
||||
return rows.filter((row) => !row.optional || row.value);
|
||||
return rows.filter(
|
||||
(row) => !row.optional || (Array.isArray(row.value) ? row.value.length > 0 : row.value),
|
||||
);
|
||||
});
|
||||
|
||||
const handlers = computed(() =>
|
||||
Array.isArray(props.plugin.handlers) ? props.plugin.handlers : [],
|
||||
);
|
||||
const normalizeHandlerList = (source) => {
|
||||
if (!source || typeof source !== "object") return [];
|
||||
if (Array.isArray(source.handlers)) {
|
||||
return source.handlers.filter((handler) => handler && typeof handler === "object");
|
||||
}
|
||||
if (Array.isArray(source.command_handlers)) {
|
||||
return source.command_handlers.filter(
|
||||
(handler) => handler && typeof handler === "object",
|
||||
);
|
||||
}
|
||||
if (Array.isArray(source.commands)) {
|
||||
return source.commands
|
||||
.filter((command) => command && (typeof command === "string" || typeof command === "object"))
|
||||
.map((command) =>
|
||||
typeof command === "string"
|
||||
? { cmd: command, type: "指令" }
|
||||
: { type: command.type || "指令", ...command },
|
||||
);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const handlers = computed(() => {
|
||||
const pluginHandlers = normalizeHandlerList(props.plugin);
|
||||
if (pluginHandlers.length > 0) return pluginHandlers;
|
||||
return normalizeHandlerList(props.marketPlugin);
|
||||
});
|
||||
|
||||
const handlerGroupOrder = [
|
||||
"commands",
|
||||
@@ -318,7 +398,7 @@ const openExternal = (url) => {
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
router.push({ name: "Extensions", hash: "#installed" });
|
||||
router.push({ name: "Extensions", hash: `#${detailSourceTab.value}` });
|
||||
};
|
||||
|
||||
const renderMarkdown = (source) => {
|
||||
@@ -358,6 +438,28 @@ const fetchReadme = async () => {
|
||||
readmeEmpty.value = false;
|
||||
renderedReadme.value = "";
|
||||
|
||||
if (isMarketDetail.value) {
|
||||
readmeLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const inlineReadme =
|
||||
props.plugin.readme ||
|
||||
props.plugin.README ||
|
||||
props.plugin.readme_content ||
|
||||
props.plugin.docs ||
|
||||
props.marketPlugin?.readme ||
|
||||
props.marketPlugin?.README ||
|
||||
props.marketPlugin?.readme_content ||
|
||||
props.marketPlugin?.docs ||
|
||||
"";
|
||||
|
||||
if (inlineReadme) {
|
||||
renderedReadme.value = renderMarkdown(inlineReadme);
|
||||
readmeLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios.get("/api/plugin/readme", {
|
||||
params: { name: props.plugin.name },
|
||||
@@ -382,6 +484,8 @@ const fetchReadme = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const showDocsSection = computed(() => !isMarketDetail.value);
|
||||
|
||||
watch(
|
||||
() => props.plugin?.name,
|
||||
() => {
|
||||
@@ -416,7 +520,7 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<h2 class="detail-title">
|
||||
<button class="detail-title__parent" type="button" @click="goBack">
|
||||
{{ tm("titles.installedAstrBotPlugins") }}
|
||||
{{ detailParentTitle }}
|
||||
</button>
|
||||
<v-icon icon="mdi-chevron-right" size="24" class="mx-1" />
|
||||
<span class="detail-title__current">{{ displayName }}</span>
|
||||
@@ -438,9 +542,9 @@ onBeforeUnmount(() => {
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<section class="detail-section">
|
||||
<section v-if="groupedHandlerSections.length" class="detail-section">
|
||||
<h3 class="detail-section__title">{{ tm("detail.contents") }}</h3>
|
||||
<div v-if="groupedHandlerSections.length" class="handler-groups">
|
||||
<div class="handler-groups">
|
||||
<div
|
||||
v-for="group in groupedHandlerSections"
|
||||
:key="group.key"
|
||||
@@ -539,11 +643,6 @@ onBeforeUnmount(() => {
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
<v-card v-else class="rounded-lg handler-card" variant="outlined">
|
||||
<v-card-text class="pa-4 text-medium-emphasis">
|
||||
{{ tm("detail.noContents") }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</section>
|
||||
|
||||
<section class="detail-section">
|
||||
@@ -564,6 +663,18 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
{{ row.actionText }}
|
||||
</v-btn>
|
||||
<div v-else-if="row.kind === 'tags'" class="detail-tags">
|
||||
<v-chip
|
||||
v-for="tag in row.value"
|
||||
:key="tag"
|
||||
:color="tag === 'danger' ? 'error' : 'primary'"
|
||||
label
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ tag === "danger" ? tm("tags.danger") : tag }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<button
|
||||
v-else-if="row.href"
|
||||
class="detail-link"
|
||||
@@ -581,7 +692,7 @@ onBeforeUnmount(() => {
|
||||
</v-card>
|
||||
</section>
|
||||
|
||||
<section class="detail-section">
|
||||
<section v-if="showDocsSection" class="detail-section">
|
||||
<h3 class="detail-section__title">{{ tm("detail.docsTitle") }}</h3>
|
||||
<v-card class="rounded-lg docs-card" variant="outlined">
|
||||
<v-card-text>
|
||||
@@ -827,6 +938,12 @@ onBeforeUnmount(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.docs-card {
|
||||
background: rgb(var(--v-theme-surface));
|
||||
color: rgba(var(--v-theme-on-surface), 0.9);
|
||||
|
||||
@@ -17,36 +17,6 @@ import {
|
||||
import { ref, computed, onMounted, onUnmounted, reactive, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
const useRandomPluginsDisplay = ({ activeTab, marketSearch, currentPage }) => {
|
||||
const showRandomPlugins = ref(true);
|
||||
|
||||
const toggleRandomPluginsVisibility = () => {
|
||||
showRandomPlugins.value = !showRandomPlugins.value;
|
||||
};
|
||||
|
||||
const collapseRandomPlugins = () => {
|
||||
showRandomPlugins.value = false;
|
||||
};
|
||||
|
||||
watch(marketSearch, () => {
|
||||
if (activeTab.value === "market") {
|
||||
collapseRandomPlugins();
|
||||
}
|
||||
});
|
||||
|
||||
watch(currentPage, (newPage, oldPage) => {
|
||||
if (newPage === oldPage) return;
|
||||
if (activeTab.value !== "market") return;
|
||||
collapseRandomPlugins();
|
||||
});
|
||||
|
||||
return {
|
||||
showRandomPlugins,
|
||||
toggleRandomPluginsVisibility,
|
||||
collapseRandomPlugins,
|
||||
};
|
||||
};
|
||||
|
||||
const buildFailedPluginItems = (raw) => {
|
||||
return Object.entries(raw || {}).map(([dirName, info]) => {
|
||||
const detail = info && typeof info === "object" ? info : {};
|
||||
@@ -217,15 +187,6 @@ export const useExtensionPage = () => {
|
||||
const sortOrder = ref("desc"); // desc (降序) or asc (升序)
|
||||
const randomPluginNames = ref([]);
|
||||
const marketCategoryFilter = ref("all");
|
||||
const {
|
||||
showRandomPlugins,
|
||||
toggleRandomPluginsVisibility,
|
||||
collapseRandomPlugins,
|
||||
} = useRandomPluginsDisplay({
|
||||
activeTab,
|
||||
marketSearch,
|
||||
currentPage,
|
||||
});
|
||||
|
||||
// 插件市场拼音搜索
|
||||
|
||||
@@ -923,6 +884,26 @@ export const useExtensionPage = () => {
|
||||
changelogDialog.repoUrl = plugin.repo;
|
||||
changelogDialog.show = true;
|
||||
};
|
||||
|
||||
const resetInstallDialogState = () => {
|
||||
selectedMarketInstallPlugin.value = null;
|
||||
extension_url.value = "";
|
||||
upload_file.value = null;
|
||||
uploadTab.value = "file";
|
||||
installCompat.checked = false;
|
||||
installCompat.compatible = true;
|
||||
installCompat.message = "";
|
||||
};
|
||||
|
||||
const openInstallDialog = () => {
|
||||
resetInstallDialogState();
|
||||
dialog.value = true;
|
||||
};
|
||||
|
||||
const closeInstallDialog = () => {
|
||||
dialog.value = false;
|
||||
resetInstallDialogState();
|
||||
};
|
||||
|
||||
// 为表格视图创建一个处理安装插件的函数
|
||||
const handleInstallPlugin = async (plugin) => {
|
||||
@@ -932,6 +913,7 @@ export const useExtensionPage = () => {
|
||||
} else {
|
||||
selectedMarketInstallPlugin.value = plugin;
|
||||
extension_url.value = plugin.repo;
|
||||
upload_file.value = null;
|
||||
dialog.value = true;
|
||||
uploadTab.value = "url";
|
||||
}
|
||||
@@ -942,6 +924,7 @@ export const useExtensionPage = () => {
|
||||
if (selectedDangerPlugin.value) {
|
||||
selectedMarketInstallPlugin.value = selectedDangerPlugin.value;
|
||||
extension_url.value = selectedDangerPlugin.value.repo;
|
||||
upload_file.value = null;
|
||||
dialog.value = true;
|
||||
uploadTab.value = "url";
|
||||
}
|
||||
@@ -1258,6 +1241,7 @@ export const useExtensionPage = () => {
|
||||
|
||||
onLoadingDialogResult(1, resData.message);
|
||||
dialog.value = false;
|
||||
selectedMarketInstallPlugin.value = null;
|
||||
await getExtensions();
|
||||
checkAlreadyInstalled();
|
||||
|
||||
@@ -1459,6 +1443,9 @@ export const useExtensionPage = () => {
|
||||
installCompat.checked = false;
|
||||
installCompat.compatible = true;
|
||||
installCompat.message = "";
|
||||
if (!dialogOpen) {
|
||||
selectedMarketInstallPlugin.value = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
await checkInstallCompatibility();
|
||||
@@ -1565,7 +1552,6 @@ export const useExtensionPage = () => {
|
||||
sortBy,
|
||||
sortOrder,
|
||||
randomPluginNames,
|
||||
showRandomPlugins,
|
||||
normalizeStr,
|
||||
toPinyinText,
|
||||
toInitials,
|
||||
@@ -1578,8 +1564,6 @@ export const useExtensionPage = () => {
|
||||
randomPlugins,
|
||||
shufflePlugins,
|
||||
refreshRandomPlugins,
|
||||
toggleRandomPluginsVisibility,
|
||||
collapseRandomPlugins,
|
||||
displayItemsPerPage,
|
||||
totalPages,
|
||||
paginatedPlugins,
|
||||
@@ -1611,6 +1595,8 @@ export const useExtensionPage = () => {
|
||||
reloadPlugin,
|
||||
viewReadme,
|
||||
viewChangelog,
|
||||
openInstallDialog,
|
||||
closeInstallDialog,
|
||||
handleInstallPlugin,
|
||||
confirmDangerInstall,
|
||||
cancelDangerInstall,
|
||||
|
||||
Reference in New Issue
Block a user