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 @@ + + + + + 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.author }} - - - {{ plugin.author }} - -
- - {{ plugin.version }} -
-
- - -
- {{ plugin.desc }} -
- - -
-
- - {{ plugin.stars }} -
-
- - {{ - new Date(plugin.updated_at).toLocaleString() - }} -
-
-
-
- - - - - {{ tag === "danger" ? tm("tags.danger") : tag }} - - - - - - - {{ 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);