feat: Enhance plugin detail and installation experience with new UI elements and internationalization support

This commit is contained in:
Soulter
2026-05-01 00:16:42 +08:00
parent 2e49eb8455
commit 6f83917336
10 changed files with 383 additions and 151 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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