From b8d24994755ae92eaaf8679876518ba0d9628811 Mon Sep 17 00:00:00 2001
From: Soulter <37870767+Soulter@users.noreply.github.com>
Date: Wed, 18 Feb 2026 17:29:04 +0800
Subject: [PATCH] feat: add MarketPluginCard component and integrate random
plugin feature in ExtensionPage (#5190)
* feat: add MarketPluginCard component and integrate random plugin feature in ExtensionPage
* feat: update random plugin selection logic to use pluginMarketData and refresh on relevant events
---
.../components/extension/MarketPluginCard.vue | 277 ++++++++++++
.../locales/en-US/features/extension.json | 7 +-
.../locales/zh-CN/features/extension.json | 9 +-
dashboard/src/views/ExtensionPage.vue | 395 +++++-------------
4 files changed, 385 insertions(+), 303 deletions(-)
create mode 100644 dashboard/src/components/extension/MarketPluginCard.vue
diff --git a/dashboard/src/components/extension/MarketPluginCard.vue b/dashboard/src/components/extension/MarketPluginCard.vue
new file mode 100644
index 000000000..03425553d
--- /dev/null
+++ b/dashboard/src/components/extension/MarketPluginCard.vue
@@ -0,0 +1,277 @@
+
+
+
+
+
+ {{ tm("market.recommended") }}
+
+
+
+
+
![]()
+
+
+
+
+
+ {{
+ plugin.display_name?.length
+ ? plugin.display_name
+ : showPluginFullName
+ ? plugin.name
+ : plugin.trimmedName
+ }}
+
+
+
+
+
+
+ {{ plugin.desc }}
+
+
+
+
+
+ {{ plugin.stars }}
+
+
+
+ {{ new Date(plugin.updated_at).toLocaleString() }}
+
+
+
+
+
+
+
+ {{ tag === "danger" ? tm("tags.danger") : tag }}
+
+
+
+
+ +{{ plugin.tags.length - 2 }}
+
+
+
+
+
+ {{ tag === "danger" ? tm("tags.danger") : tag }}
+
+
+
+
+
+
+
+ {{ tm("buttons.viewRepo") }}
+
+
+ {{ tm("buttons.install") }}
+
+
+ ✓ {{ tm("status.installed") }}
+
+
+
+
+
+
diff --git a/dashboard/src/i18n/locales/en-US/features/extension.json b/dashboard/src/i18n/locales/en-US/features/extension.json
index 52d03827b..c184b7cf2 100644
--- a/dashboard/src/i18n/locales/en-US/features/extension.json
+++ b/dashboard/src/i18n/locales/en-US/features/extension.json
@@ -38,7 +38,8 @@
"selectFile": "Select File",
"refresh": "Refresh",
"updateAll": "Update All",
- "deleteSource": "Delete Source"
+ "deleteSource": "Delete Source",
+ "reshuffle": "Shuffle Again"
},
"status": {
"enabled": "Enabled",
@@ -103,7 +104,9 @@
"sourceUpdated": "Source updated successfully",
"defaultOfficialSource": "Default Official Source",
"sourceExists": "This source already exists",
- "installPlugin": "Install Plugin"
+ "installPlugin": "Install Plugin",
+ "randomPlugins": "🎲 Random Plugins",
+ "sourceSafetyWarning": "Even with the default source, plugin stability and security cannot be fully guaranteed. Please verify carefully before use."
},
"sort": {
"default": "Default",
diff --git a/dashboard/src/i18n/locales/zh-CN/features/extension.json b/dashboard/src/i18n/locales/zh-CN/features/extension.json
index 5f838789e..88ca6fc8b 100644
--- a/dashboard/src/i18n/locales/zh-CN/features/extension.json
+++ b/dashboard/src/i18n/locales/zh-CN/features/extension.json
@@ -3,7 +3,7 @@
"subtitle": "管理和配置系统插件",
"tabs": {
"installedPlugins": "AstrBot 插件",
- "market": "AstrBot 插件市场",
+ "market": "AstrBot 插件市场",
"installedMcpServers": "MCP",
"skills": "Skills",
"handlersOperation": "管理行为"
@@ -38,7 +38,8 @@
"selectFile": "选择文件",
"refresh": "刷新",
"updateAll": "更新全部插件",
- "deleteSource": "删除源"
+ "deleteSource": "删除源",
+ "reshuffle": "随机一发"
},
"status": {
"enabled": "启用",
@@ -103,7 +104,9 @@
"sourceUpdated": "插件源更新成功",
"defaultOfficialSource": "默认官方源",
"sourceExists": "该插件源已存在",
- "installPlugin": "安装插件"
+ "installPlugin": "安装插件",
+ "randomPlugins": "🎲 随机插件",
+ "sourceSafetyWarning": "即使是默认插件源,我们也不能完全保证插件的稳定性和安全性,使用前请谨慎核查。"
},
"sort": {
"default": "默认排序",
diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue
index fb6055d6a..e24053fd0 100644
--- a/dashboard/src/views/ExtensionPage.vue
+++ b/dashboard/src/views/ExtensionPage.vue
@@ -7,6 +7,7 @@ import ProxySelector from "@/components/shared/ProxySelector.vue";
import UninstallConfirmDialog from "@/components/shared/UninstallConfirmDialog.vue";
import McpServersSection from "@/components/extension/McpServersSection.vue";
import SkillsSection from "@/components/extension/SkillsSection.vue";
+import MarketPluginCard from "@/components/extension/MarketPluginCard.vue";
import ComponentPanel from "@/components/extension/componentPanel/index.vue";
import axios from "axios";
import { pinyin } from "pinyin-pro";
@@ -175,6 +176,7 @@ const debouncedMarketSearch = ref("");
const refreshingMarket = ref(false);
const sortBy = ref("default"); // default, stars, author, updated
const sortOrder = ref("desc"); // desc (降序) or asc (升序)
+const randomPluginNames = ref([]);
// 插件市场拼音搜索
const normalizeStr = (s) => (s ?? "").toString().toLowerCase().trim();
@@ -310,8 +312,42 @@ const sortedPlugins = computed(() => {
return plugins;
});
+const RANDOM_PLUGINS_COUNT = 6;
+
+const randomPlugins = computed(() => {
+ const allPlugins = pluginMarketData.value;
+ if (allPlugins.length === 0) return [];
+
+ const pluginsByName = new Map(allPlugins.map((plugin) => [plugin.name, plugin]));
+ const selected = randomPluginNames.value
+ .map((name) => pluginsByName.get(name))
+ .filter(Boolean);
+
+ if (selected.length > 0) {
+ return selected;
+ }
+
+ return allPlugins.slice(0, Math.min(RANDOM_PLUGINS_COUNT, allPlugins.length));
+});
+
+const shufflePlugins = (plugins) => {
+ const shuffled = [...plugins];
+ for (let i = shuffled.length - 1; i > 0; i -= 1) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
+ }
+ return shuffled;
+};
+
+const refreshRandomPlugins = () => {
+ const shuffled = shufflePlugins(pluginMarketData.value);
+ randomPluginNames.value = shuffled
+ .slice(0, Math.min(RANDOM_PLUGINS_COUNT, shuffled.length))
+ .map((plugin) => plugin.name);
+};
+
// 分页计算属性
-const displayItemsPerPage = 9; // 固定每页显示6个卡片(2行)
+const displayItemsPerPage = 9; // 固定每页显示9个卡片(3行)
const totalPages = computed(() => {
return Math.ceil(sortedPlugins.value.length / displayItemsPerPage);
@@ -1037,6 +1073,7 @@ const refreshPluginMarket = async () => {
trimExtensionName();
checkAlreadyInstalled();
checkUpdate();
+ refreshRandomPlugins();
currentPage.value = 1; // 重置到第一页
toast(tm("messages.refreshSuccess"), "success");
@@ -1085,6 +1122,7 @@ onMounted(async () => {
trimExtensionName();
checkAlreadyInstalled();
checkUpdate();
+ refreshRandomPlugins();
} catch (err) {
toast(tm("messages.getMarketDataFailed") + " " + err, "error");
}
@@ -1788,17 +1826,21 @@ watch(activeTab, (newTab) => {
-
-
-
+
+ mdi-alert-outline
+ {{ tm("market.sourceSafetyWarning") }}
+
+
@@ -1883,6 +1925,42 @@ watch(activeTab, (newTab) => {
+
+
+ {{ tm("market.randomPlugins") }}
+
+
+ {{ tm("buttons.reshuffle") }}
+
+
+
+
+
+
+
+
+
-
{
}}
-
-
+
-
-
-
- 🥳 推荐
-
-
-
-
-
![]()
-
-
-
-
-
-
- {{
- plugin.display_name?.length
- ? plugin.display_name
- : showPluginFullName
- ? plugin.name
- : plugin.trimmedName
- }}
-
-
-
-
-
-
-
-
- {{ plugin.desc }}
-
-
-
-
-
-
- {{ plugin.stars }}
-
-
-
- {{
- new Date(plugin.updated_at).toLocaleString()
- }}
-
-
-
-
-
-
-
-
- {{ tag === "danger" ? tm("tags.danger") : tag }}
-
-
-
-
- +{{ plugin.tags.length - 2 }}
-
-
-
-
-
- {{ tag === "danger" ? tm("tags.danger") : tag }}
-
-
-
-
-
-
-
- {{ tm("buttons.viewRepo") }}
-
-
- {{ tm("buttons.install") }}
-
-
- ✓ {{ tm("status.installed") }}
-
-
-
+
-
{
background-color: #f5f5f5;
}
-.plugin-description {
- color: rgba(var(--v-theme-on-surface), 0.6);
- line-height: 1.3;
- margin-bottom: 6px;
- flex: 1;
- overflow-y: hidden;
-}
-
-.plugin-card:hover .plugin-description {
- overflow-y: auto;
-}
-
-.plugin-description::-webkit-scrollbar {
- width: 8px;
- height: 8px;
-}
-
-.plugin-description::-webkit-scrollbar-track {
- background: transparent;
-}
-
-.plugin-description::-webkit-scrollbar-thumb {
- background-color: rgba(var(--v-theme-primary-rgb), 0.4);
- border-radius: 4px;
- border: 2px solid transparent;
- background-clip: content-box;
-}
-
-.plugin-description::-webkit-scrollbar-thumb:hover {
- background-color: rgba(var(--v-theme-primary-rgb), 0.6);
-}
-
.fab-button {
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);