mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 01:40:15 +08:00
feat(extension): add category filtering for market plugins and enhance UI components (#6762)
This commit is contained in:
@@ -41,69 +41,22 @@ const handleInstall = (plugin) => {
|
||||
<v-card
|
||||
class="rounded-lg d-flex flex-column plugin-card"
|
||||
elevation="0"
|
||||
style="height: 13rem; position: relative"
|
||||
>
|
||||
<v-chip
|
||||
v-if="plugin?.pinned"
|
||||
color="warning"
|
||||
size="x-small"
|
||||
label
|
||||
style="
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
z-index: 10;
|
||||
height: 20px;
|
||||
font-weight: bold;
|
||||
"
|
||||
>
|
||||
{{ tm("market.recommended") }}
|
||||
</v-chip>
|
||||
|
||||
<v-card-text
|
||||
style="
|
||||
padding: 12px;
|
||||
padding-bottom: 8px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
"
|
||||
class="plugin-card-content"
|
||||
>
|
||||
<div style="flex-shrink: 0">
|
||||
<div class="plugin-cover">
|
||||
<img
|
||||
:src="plugin?.logo || defaultPluginIcon"
|
||||
:alt="plugin.name"
|
||||
style="
|
||||
height: 75px;
|
||||
width: 75px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
"
|
||||
class="plugin-cover__image"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style="
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="font-weight-bold"
|
||||
style="
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.3;
|
||||
font-size: 1.2rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
"
|
||||
>
|
||||
<span style="overflow: hidden; text-overflow: ellipsis">
|
||||
<div class="plugin-info">
|
||||
<div class="d-flex align-center plugin-title-row">
|
||||
<div class="font-weight-bold plugin-title">
|
||||
{{
|
||||
plugin.display_name?.length
|
||||
? plugin.display_name
|
||||
@@ -111,10 +64,19 @@ const handleInstall = (plugin) => {
|
||||
? plugin.name
|
||||
: plugin.trimmedName
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<v-chip
|
||||
v-if="plugin?.pinned"
|
||||
color="warning"
|
||||
size="x-small"
|
||||
label
|
||||
class="market-recommended-chip"
|
||||
>
|
||||
{{ tm("market.recommended") }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center" style="gap: 4px; margin-bottom: 6px">
|
||||
<div class="d-flex align-center plugin-meta">
|
||||
<v-icon
|
||||
icon="mdi-account"
|
||||
size="x-small"
|
||||
@@ -159,6 +121,18 @@ const handleInstall = (plugin) => {
|
||||
></v-icon>
|
||||
<span>{{ plugin.version }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="plugin.stars !== undefined"
|
||||
class="d-flex align-center text-subtitle-2 ml-2"
|
||||
style="color: rgba(var(--v-theme-on-surface), 0.7)"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-star"
|
||||
size="x-small"
|
||||
style="margin-right: 2px"
|
||||
></v-icon>
|
||||
<span>{{ plugin.stars }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-caption plugin-description">
|
||||
@@ -167,8 +141,7 @@ const handleInstall = (plugin) => {
|
||||
|
||||
<div
|
||||
v-if="plugin.astrbot_version || platformDisplayList.length"
|
||||
class="d-flex align-center flex-wrap"
|
||||
style="gap: 4px; margin-top: 4px; margin-bottom: 4px"
|
||||
class="plugin-badges"
|
||||
>
|
||||
<v-chip
|
||||
v-if="plugin.astrbot_version"
|
||||
@@ -186,32 +159,7 @@ const handleInstall = (plugin) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center" style="gap: 8px; margin-top: auto">
|
||||
<div
|
||||
v-if="plugin.stars !== undefined"
|
||||
class="d-flex align-center text-subtitle-2"
|
||||
style="color: rgba(var(--v-theme-on-surface), 0.7)"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-star"
|
||||
size="x-small"
|
||||
style="margin-right: 2px"
|
||||
></v-icon>
|
||||
<span>{{ plugin.stars }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="plugin.updated_at"
|
||||
class="d-flex align-center text-subtitle-2"
|
||||
style="color: rgba(var(--v-theme-on-surface), 0.7)"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-clock-outline"
|
||||
size="x-small"
|
||||
style="margin-right: 2px"
|
||||
></v-icon>
|
||||
<span>{{ new Date(plugin.updated_at).toLocaleString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="plugin-stats"></div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
@@ -274,24 +222,112 @@ const handleInstall = (plugin) => {
|
||||
>
|
||||
{{ tm("buttons.install") }}
|
||||
</v-btn>
|
||||
<v-chip v-else color="success" size="x-small" label style="height: 20px">
|
||||
<v-btn
|
||||
v-else
|
||||
color="success"
|
||||
size="small"
|
||||
variant="flat"
|
||||
disabled
|
||||
class="market-action-btn"
|
||||
style="height: 32px"
|
||||
>
|
||||
✓ {{ tm("status.installed") }}
|
||||
</v-chip>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.plugin-card-content {
|
||||
padding: 12px;
|
||||
padding-bottom: 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.plugin-cover {
|
||||
flex-shrink: 0;
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.plugin-cover__image {
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.plugin-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plugin-title-row {
|
||||
margin-bottom: 4px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.market-recommended-chip {
|
||||
flex-shrink: 0;
|
||||
font-weight: bold;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.plugin-title {
|
||||
line-height: 1.3;
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.plugin-meta {
|
||||
gap: 4px;
|
||||
margin-bottom: 6px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.plugin-description {
|
||||
color: rgba(var(--v-theme-on-surface), 0.6);
|
||||
line-height: 1.3;
|
||||
margin-bottom: 6px;
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
min-height: calc(1.3em * 3);
|
||||
max-height: calc(1.3em * 3);
|
||||
}
|
||||
|
||||
.plugin-card:hover .plugin-description {
|
||||
overflow-y: auto;
|
||||
.plugin-badges {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.plugin-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.plugin-description::-webkit-scrollbar {
|
||||
|
||||
@@ -85,6 +85,7 @@ const toggleOrder = () => {
|
||||
|
||||
.plugin-sort-control__select {
|
||||
min-width: 180px;
|
||||
width: 190px;
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,16 @@
|
||||
"market": {
|
||||
"recommended": "🥳 Recommended",
|
||||
"allPlugins": "📦 All Extensions",
|
||||
"category": "Category",
|
||||
"categories": {
|
||||
"all": "All",
|
||||
"ai_tools": "AI Tools",
|
||||
"entertainment": "Entertainment",
|
||||
"productivity": "Productivity",
|
||||
"integrations": "Integrations",
|
||||
"utilities": "Utilities",
|
||||
"other": "Other"
|
||||
},
|
||||
"showFullName": "Full Name",
|
||||
"devDocs": "Extension Development Docs",
|
||||
"submitRepo": "Submit Extension Repository",
|
||||
|
||||
@@ -96,6 +96,15 @@
|
||||
"market": {
|
||||
"recommended": "🥳 Рекомендуем",
|
||||
"allPlugins": "📦 Все плагины",
|
||||
"categories": {
|
||||
"all": "Все",
|
||||
"ai_tools": "AI Tools",
|
||||
"entertainment": "Развлечения",
|
||||
"productivity": "Продуктивность",
|
||||
"integrations": "Интеграции",
|
||||
"utilities": "Утилиты",
|
||||
"other": "Другое"
|
||||
},
|
||||
"showFullName": "Полное имя",
|
||||
"devDocs": "Документация для разработчиков",
|
||||
"submitRepo": "Добавить репозиторий",
|
||||
@@ -355,4 +364,4 @@
|
||||
"pluginChangelog": {
|
||||
"menuTitle": "Журнал изменений"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,16 @@
|
||||
"market": {
|
||||
"recommended": "🥳 推荐",
|
||||
"allPlugins": "📦 全部插件",
|
||||
"category": "分类",
|
||||
"categories": {
|
||||
"all": "全部",
|
||||
"ai_tools": "AI 增强",
|
||||
"entertainment": "娱乐",
|
||||
"productivity": "效率",
|
||||
"integrations": "外部集成",
|
||||
"utilities": "生活实用",
|
||||
"other": "其他"
|
||||
},
|
||||
"showFullName": "完整名称",
|
||||
"devDocs": "插件开发文档",
|
||||
"submitRepo": "提交插件仓库",
|
||||
|
||||
@@ -178,6 +178,7 @@ export const useCommonStore = defineStore({
|
||||
"updated_at": pluginData?.updated_at ? pluginData.updated_at : "",
|
||||
"display_name": pluginData?.display_name ? pluginData.display_name : "",
|
||||
"astrbot_version": pluginData?.astrbot_version ? pluginData.astrbot_version : "",
|
||||
"category": pluginData?.category ? pluginData.category : "",
|
||||
"support_platforms": Array.isArray(pluginData?.support_platforms)
|
||||
? pluginData.support_platforms
|
||||
: Array.isArray(pluginData?.support_platform)
|
||||
|
||||
@@ -218,15 +218,6 @@ const {
|
||||
<!-- 插件市场标签页内容 -->
|
||||
<MarketPluginsTab :state="pageState" />
|
||||
|
||||
<v-row v-if="loading_">
|
||||
<v-col cols="12" class="d-flex justify-center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="48"
|
||||
></v-progress-circular>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
@@ -81,6 +81,8 @@ const {
|
||||
sortOrder,
|
||||
randomPluginNames,
|
||||
showRandomPlugins,
|
||||
marketCategoryFilter,
|
||||
marketCategoryItems,
|
||||
normalizeStr,
|
||||
toPinyinText,
|
||||
toInitials,
|
||||
@@ -166,6 +168,13 @@ const marketSortItems = computed(() => [
|
||||
{ title: tm("sort.author"), value: "author" },
|
||||
{ title: tm("sort.updated"), value: "updated" },
|
||||
]);
|
||||
|
||||
const marketCategorySelectItems = computed(() =>
|
||||
marketCategoryItems.value.map((item) => ({
|
||||
title: `${item.label || ""} (${item.count || 0})`,
|
||||
value: item.value,
|
||||
})),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -270,10 +279,94 @@ const marketSortItems = computed(() => [
|
||||
</v-tooltip>
|
||||
|
||||
<div class="mt-4">
|
||||
<div
|
||||
class="d-flex align-center mb-2"
|
||||
style="
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
"
|
||||
>
|
||||
<div class="d-flex align-center" style="gap: 6px">
|
||||
<h2>
|
||||
{{ tm("market.allPlugins") }}
|
||||
</h2>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="refreshPluginMarket"
|
||||
:loading="loading_ || refreshingMarket"
|
||||
:disabled="loading_ || refreshingMarket"
|
||||
>
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="d-flex align-center"
|
||||
style="gap: 8px; flex-wrap: wrap"
|
||||
>
|
||||
<v-select
|
||||
v-if="marketCategoryItems.length > 0"
|
||||
v-model="marketCategoryFilter"
|
||||
:items="marketCategorySelectItems"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
:label="tm('market.category')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="market-filter-control"
|
||||
:menu-props="{ openOnHover: true, closeOnContentClick: false }"
|
||||
></v-select>
|
||||
|
||||
<PluginSortControl
|
||||
v-model="sortBy"
|
||||
:items="marketSortItems"
|
||||
:label="tm('sort.by')"
|
||||
:order="sortOrder"
|
||||
:ascending-label="tm('sort.ascending')"
|
||||
:descending-label="tm('sort.descending')"
|
||||
:show-order="sortBy !== 'default'"
|
||||
@update:order="sortOrder = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-row style="min-height: 26rem" dense>
|
||||
<v-col
|
||||
v-for="plugin in paginatedPlugins"
|
||||
:key="plugin.name"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
class="pb-2"
|
||||
>
|
||||
<MarketPluginCard
|
||||
:plugin="plugin"
|
||||
:default-plugin-icon="defaultPluginIcon"
|
||||
:show-plugin-full-name="showPluginFullName"
|
||||
@install="handleInstallPlugin"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div
|
||||
class="d-flex justify-center mt-4"
|
||||
v-if="totalPages > 1"
|
||||
>
|
||||
<v-pagination
|
||||
v-model="currentPage"
|
||||
:length="totalPages"
|
||||
:total-visible="7"
|
||||
size="small"
|
||||
></v-pagination>
|
||||
</div>
|
||||
|
||||
<v-expand-transition>
|
||||
<div v-if="showRandomPlugins">
|
||||
<div
|
||||
class="d-flex align-center mb-2"
|
||||
class="d-flex align-center mb-2 mt-4"
|
||||
style="justify-content: space-between; flex-wrap: wrap; gap: 8px"
|
||||
>
|
||||
<h2>
|
||||
@@ -309,74 +402,22 @@ const marketSortItems = computed(() => [
|
||||
</v-row>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
|
||||
<div
|
||||
class="d-flex align-center mb-2"
|
||||
style="
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
"
|
||||
>
|
||||
<div class="d-flex align-center" style="gap: 6px">
|
||||
<h2>
|
||||
{{ tm("market.allPlugins") }}({{
|
||||
filteredMarketPlugins.length
|
||||
}})
|
||||
</h2>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="refreshPluginMarket"
|
||||
:loading="refreshingMarket"
|
||||
>
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="d-flex align-center"
|
||||
style="gap: 8px; flex-wrap: wrap"
|
||||
>
|
||||
<PluginSortControl
|
||||
v-model="sortBy"
|
||||
:items="marketSortItems"
|
||||
:label="tm('sort.by')"
|
||||
:order="sortOrder"
|
||||
:ascending-label="tm('sort.ascending')"
|
||||
:descending-label="tm('sort.descending')"
|
||||
:show-order="sortBy !== 'default'"
|
||||
@update:order="sortOrder = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-row style="min-height: 26rem" dense>
|
||||
<v-col
|
||||
v-for="plugin in paginatedPlugins"
|
||||
:key="plugin.name"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
class="pb-2"
|
||||
>
|
||||
<MarketPluginCard
|
||||
:plugin="plugin"
|
||||
:default-plugin-icon="defaultPluginIcon"
|
||||
:show-plugin-full-name="showPluginFullName"
|
||||
@install="handleInstallPlugin"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="d-flex justify-center mt-4" v-if="totalPages > 1">
|
||||
<v-pagination
|
||||
v-model="currentPage"
|
||||
:length="totalPages"
|
||||
:total-visible="7"
|
||||
size="small"
|
||||
></v-pagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</v-tab-item>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.market-filter-control {
|
||||
min-width: 190px;
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.market-filter-control :deep(.v-field__input),
|
||||
.market-filter-control :deep(.v-field-label),
|
||||
.market-filter-control :deep(.v-select__selection-text),
|
||||
.market-filter-control :deep(.v-field__prepend-inner) {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -237,6 +237,7 @@ export const useExtensionPage = () => {
|
||||
const sortBy = ref("default"); // default, stars, author, updated
|
||||
const sortOrder = ref("desc"); // desc (降序) or asc (升序)
|
||||
const randomPluginNames = ref([]);
|
||||
const marketCategoryFilter = ref("all");
|
||||
const {
|
||||
showRandomPlugins,
|
||||
toggleRandomPluginsVisibility,
|
||||
@@ -256,6 +257,90 @@ export const useExtensionPage = () => {
|
||||
{ title: tm("table.headers.trigger"), key: "cmd" },
|
||||
]);
|
||||
|
||||
const normalizeMarketCategory = (rawCategory) => {
|
||||
const normalized = String(rawCategory || "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return "other";
|
||||
}
|
||||
return normalized.replace(/[\s-]+/g, "_");
|
||||
};
|
||||
|
||||
const getMarketCategoryLabel = (key, rawCategory = "") => {
|
||||
const fallbackMap = {
|
||||
all: "All",
|
||||
ai_tools: "AI Tools",
|
||||
entertainment: "Entertainment",
|
||||
productivity: "Productivity",
|
||||
integrations: "Integrations",
|
||||
utilities: "Utilities",
|
||||
other: "Other",
|
||||
};
|
||||
const i18nKey = `market.categories.${key}`;
|
||||
const translated = tm(i18nKey);
|
||||
if (translated && !translated.includes("[MISSING:")) {
|
||||
return translated;
|
||||
}
|
||||
if (fallbackMap[key]) {
|
||||
return fallbackMap[key];
|
||||
}
|
||||
const normalizedRaw = String(rawCategory || "").trim();
|
||||
if (normalizedRaw) {
|
||||
return normalizedRaw;
|
||||
}
|
||||
return key
|
||||
.split(/[_-]+/)
|
||||
.filter(Boolean)
|
||||
.map((part) => part[0].toUpperCase() + part.slice(1))
|
||||
.join(" ");
|
||||
};
|
||||
|
||||
const marketCategoryMeta = computed(() => {
|
||||
const categories = new Map();
|
||||
|
||||
for (const plugin of pluginMarketData.value) {
|
||||
const categoryKey = normalizeMarketCategory(plugin?.category);
|
||||
const categoryData = categories.get(categoryKey);
|
||||
if (categoryData) {
|
||||
categoryData.count += 1;
|
||||
continue;
|
||||
}
|
||||
categories.set(categoryKey, {
|
||||
count: 1,
|
||||
rawLabel: String(plugin?.category || "").trim(),
|
||||
});
|
||||
}
|
||||
|
||||
return categories;
|
||||
});
|
||||
|
||||
const marketCategoryCounts = computed(() => {
|
||||
const counts = { all: pluginMarketData.value.length };
|
||||
for (const [categoryKey, categoryData] of marketCategoryMeta.value.entries()) {
|
||||
counts[categoryKey] = categoryData.count;
|
||||
}
|
||||
return counts;
|
||||
});
|
||||
|
||||
const marketCategoryItems = computed(() => {
|
||||
const items = [
|
||||
{
|
||||
value: "all",
|
||||
label: getMarketCategoryLabel("all"),
|
||||
count: marketCategoryCounts.value.all || 0,
|
||||
},
|
||||
];
|
||||
|
||||
for (const [categoryKey, categoryData] of marketCategoryMeta.value.entries()) {
|
||||
items.push({
|
||||
value: categoryKey,
|
||||
label: getMarketCategoryLabel(categoryKey, categoryData.rawLabel),
|
||||
count: categoryData.count,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
const installedSortItems = computed(() => [
|
||||
{ title: tm("sort.default"), value: "default" },
|
||||
{ title: tm("sort.installTime"), value: "install_time" },
|
||||
@@ -438,13 +523,24 @@ export const useExtensionPage = () => {
|
||||
// 过滤后的插件市场数据(带搜索)
|
||||
const filteredMarketPlugins = computed(() => {
|
||||
const query = buildSearchQuery(debouncedMarketSearch.value);
|
||||
const targetCategory = normalizeMarketCategory(marketCategoryFilter.value);
|
||||
const shouldFilterByCategory = marketCategoryFilter.value !== "all";
|
||||
if (!query) {
|
||||
return pluginMarketData.value;
|
||||
if (!shouldFilterByCategory) {
|
||||
return pluginMarketData.value;
|
||||
}
|
||||
return pluginMarketData.value.filter(
|
||||
(plugin) => normalizeMarketCategory(plugin?.category) === targetCategory,
|
||||
);
|
||||
}
|
||||
|
||||
return pluginMarketData.value.filter((plugin) =>
|
||||
matchesPluginSearch(plugin, query),
|
||||
);
|
||||
return pluginMarketData.value.filter((plugin) => {
|
||||
const matchesSearch = matchesPluginSearch(plugin, query);
|
||||
const matchesCategory = shouldFilterByCategory
|
||||
? normalizeMarketCategory(plugin?.category) === targetCategory
|
||||
: true;
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
});
|
||||
|
||||
// 所有插件列表,推荐插件排在前面
|
||||
@@ -570,8 +666,10 @@ export const useExtensionPage = () => {
|
||||
buildFailedPluginItems(failedPluginsDict.value),
|
||||
);
|
||||
|
||||
const getExtensions = async () => {
|
||||
loading_.value = true;
|
||||
const getExtensions = async ({ withLoading = true } = {}) => {
|
||||
if (withLoading) {
|
||||
loading_.value = true;
|
||||
}
|
||||
try {
|
||||
const res = await axios.get("/api/plugin/get");
|
||||
Object.assign(extension_data, res.data);
|
||||
@@ -583,7 +681,9 @@ export const useExtensionPage = () => {
|
||||
} catch (err) {
|
||||
toast(err, "error");
|
||||
} finally {
|
||||
loading_.value = false;
|
||||
if (withLoading) {
|
||||
loading_.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1411,6 +1511,7 @@ export const useExtensionPage = () => {
|
||||
// 刷新插件市场数据
|
||||
const refreshPluginMarket = async () => {
|
||||
refreshingMarket.value = true;
|
||||
loading_.value = true;
|
||||
try {
|
||||
// 强制刷新插件市场数据
|
||||
const data = await commonStore.getPluginCollections(
|
||||
@@ -1429,6 +1530,7 @@ export const useExtensionPage = () => {
|
||||
toast(tm("messages.refreshFailed") + " " + err, "error");
|
||||
} finally {
|
||||
refreshingMarket.value = false;
|
||||
loading_.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1437,21 +1539,22 @@ export const useExtensionPage = () => {
|
||||
if (!syncTabFromHash(getLocationHash())) {
|
||||
await replaceTabRoute(router, route, activeTab.value);
|
||||
}
|
||||
await getExtensions();
|
||||
|
||||
// 加载自定义插件源
|
||||
loadCustomSources();
|
||||
|
||||
// 检查是否有 open_config 参数
|
||||
const plugin_name = Array.isArray(route.query.open_config)
|
||||
? route.query.open_config[0]
|
||||
: route.query.open_config;
|
||||
if (plugin_name) {
|
||||
console.log(`Opening config for plugin: ${plugin_name}`);
|
||||
openExtensionConfig(plugin_name);
|
||||
}
|
||||
|
||||
loading_.value = true;
|
||||
try {
|
||||
await getExtensions({ withLoading: false });
|
||||
|
||||
// 加载自定义插件源
|
||||
loadCustomSources();
|
||||
|
||||
// 检查是否有 open_config 参数
|
||||
const plugin_name = Array.isArray(route.query.open_config)
|
||||
? route.query.open_config[0]
|
||||
: route.query.open_config;
|
||||
if (plugin_name) {
|
||||
console.log(`Opening config for plugin: ${plugin_name}`);
|
||||
openExtensionConfig(plugin_name);
|
||||
}
|
||||
|
||||
const data = await commonStore.getPluginCollections(
|
||||
false,
|
||||
selectedSource.value,
|
||||
@@ -1463,6 +1566,8 @@ export const useExtensionPage = () => {
|
||||
refreshRandomPlugins();
|
||||
} catch (err) {
|
||||
toast(tm("messages.getMarketDataFailed") + " " + err, "error");
|
||||
} finally {
|
||||
loading_.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1532,6 +1637,23 @@ export const useExtensionPage = () => {
|
||||
void replaceTabRoute(router, route, newTab);
|
||||
});
|
||||
|
||||
watch(marketCategoryFilter, () => {
|
||||
if (activeTab.value === "market") {
|
||||
currentPage.value = 1;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
marketCategoryItems,
|
||||
(newItems) => {
|
||||
const validValues = new Set(newItems.map((item) => item.value));
|
||||
if (!validValues.has(marketCategoryFilter.value)) {
|
||||
marketCategoryFilter.value = "all";
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return {
|
||||
commonStore,
|
||||
t,
|
||||
@@ -1575,6 +1697,9 @@ export const useExtensionPage = () => {
|
||||
installedSortOrder,
|
||||
loading_,
|
||||
currentPage,
|
||||
marketCategoryFilter,
|
||||
marketCategoryItems,
|
||||
marketCategoryCounts,
|
||||
dangerConfirmDialog,
|
||||
selectedDangerPlugin,
|
||||
selectedMarketInstallPlugin,
|
||||
|
||||
Reference in New Issue
Block a user