@@ -170,8 +149,7 @@ const handleInstall = (plugin) => {
{
style="height: 20px"
>
✓ {{ tm("status.installed") }}
-
+
diff --git a/dashboard/src/components/extension/PluginSortControl.vue b/dashboard/src/components/extension/PluginSortControl.vue
index 87410066d..2d4d78dd3 100644
--- a/dashboard/src/components/extension/PluginSortControl.vue
+++ b/dashboard/src/components/extension/PluginSortControl.vue
@@ -92,6 +92,7 @@ const toggleOrder = () => {
.plugin-sort-control__select {
min-width: 180px;
+ width: 190px;
max-width: 220px;
}
diff --git a/dashboard/src/components/shared/ExtensionCard.vue b/dashboard/src/components/shared/ExtensionCard.vue
index 49f330a09..3de3d913d 100644
--- a/dashboard/src/components/shared/ExtensionCard.vue
+++ b/dashboard/src/components/shared/ExtensionCard.vue
@@ -1,5 +1,5 @@
+
+
diff --git a/dashboard/src/i18n/locales/en-US/features/config-metadata.json b/dashboard/src/i18n/locales/en-US/features/config-metadata.json
index 8d47eeaaa..7332d33b1 100644
--- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json
+++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json
@@ -634,6 +634,30 @@
"description": "Send Replies via Webhook Only",
"hint": "When enabled, all WeCom AI Bot replies are sent through msg_push_webhook_url. The message push webhook supports more message types (such as images, files, etc.). If you do not need the typing effect, it is strongly recommended to use this option. "
},
+ "weixin_oc_base_url": {
+ "description": "API Base URL",
+ "hint": "Default: https://ilinkai.weixin.qq.com"
+ },
+ "weixin_oc_bot_type": {
+ "description": "bot_type (QR login parameter)",
+ "hint": "Default: 3"
+ },
+ "weixin_oc_qr_poll_interval": {
+ "description": "QR status poll interval (seconds)",
+ "hint": "Polling interval in seconds for QR code status."
+ },
+ "weixin_oc_long_poll_timeout_ms": {
+ "description": "getUpdates long-poll timeout (ms)",
+ "hint": "Timeout parameter for polling messages."
+ },
+ "weixin_oc_api_timeout_ms": {
+ "description": "HTTP timeout (ms)",
+ "hint": "Generic API request timeout."
+ },
+ "weixin_oc_token": {
+ "description": "Token after login (optional)",
+ "hint": "Automatically written after QR login; can be filled manually for advanced scenarios."
+ },
"kook_bot_token": {
"description": "Bot Token",
"type": "string",
diff --git a/dashboard/src/i18n/locales/en-US/features/extension.json b/dashboard/src/i18n/locales/en-US/features/extension.json
index 07affcd62..9ad049db5 100644
--- a/dashboard/src/i18n/locales/en-US/features/extension.json
+++ b/dashboard/src/i18n/locales/en-US/features/extension.json
@@ -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",
diff --git a/dashboard/src/i18n/locales/en-US/features/platform.json b/dashboard/src/i18n/locales/en-US/features/platform.json
index 64448ca1e..a62b2f680 100644
--- a/dashboard/src/i18n/locales/en-US/features/platform.json
+++ b/dashboard/src/i18n/locales/en-US/features/platform.json
@@ -123,6 +123,13 @@
"unknown": "Unknown",
"errors": "error(s)"
},
+ "platformQr": {
+ "title": "QR Login",
+ "show": "Show QR",
+ "status": "QR Status",
+ "waiting": "Waiting for QR",
+ "close": "Close"
+ },
"errorDialog": {
"title": "Error Details",
"platformId": "Platform ID",
diff --git a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json
index afffef80c..a204118db 100644
--- a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json
+++ b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json
@@ -630,6 +630,30 @@
"description": "Отправлять ответы только через Webhook",
"hint": "Все ответы WeCom AI Bot будут идти через вебхук пуш-сообщений. Поддерживает больше типов контента."
},
+ "weixin_oc_base_url": {
+ "description": "URL API iLink",
+ "hint": "Значение по умолчанию: https://ilinkai.weixin.qq.com"
+ },
+ "weixin_oc_bot_type": {
+ "description": "bot_type (параметр входа по QR)",
+ "hint": "Значение по умолчанию: 3"
+ },
+ "weixin_oc_qr_poll_interval": {
+ "description": "Интервал опроса статуса QR (сек)",
+ "hint": "Пауза между запросами статуса QR-кода."
+ },
+ "weixin_oc_long_poll_timeout_ms": {
+ "description": "Таймаут long-poll getUpdates (мс)",
+ "hint": "Параметр таймаута запроса получения сообщений."
+ },
+ "weixin_oc_api_timeout_ms": {
+ "description": "Таймаут HTTP запроса (мс)",
+ "hint": "Общий таймаут для HTTP запросов."
+ },
+ "weixin_oc_token": {
+ "description": "Token после входа (опционально)",
+ "hint": "Автоматически сохраняется после QR логина; для сложных сценариев можно ввести вручную."
+ },
"kook_bot_token": {
"description": "Токен бота",
"type": "string",
diff --git a/dashboard/src/i18n/locales/ru-RU/features/extension.json b/dashboard/src/i18n/locales/ru-RU/features/extension.json
index 2ff06f573..7d3e0602f 100644
--- a/dashboard/src/i18n/locales/ru-RU/features/extension.json
+++ b/dashboard/src/i18n/locales/ru-RU/features/extension.json
@@ -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": "Журнал изменений"
}
-}
\ No newline at end of file
+}
diff --git a/dashboard/src/i18n/locales/ru-RU/features/platform.json b/dashboard/src/i18n/locales/ru-RU/features/platform.json
index 04c7613a9..42bcae42e 100644
--- a/dashboard/src/i18n/locales/ru-RU/features/platform.json
+++ b/dashboard/src/i18n/locales/ru-RU/features/platform.json
@@ -116,14 +116,21 @@
"error": "Ошибка"
},
"runtimeStatus": {
- "running": "Работает",
- "error": "Ошибка",
- "pending": "Ожидание",
- "stopped": "Остановлен",
- "unknown": "Неизвестно",
- "errors": "ошибок"
- },
- "errorDialog": {
+ "running": "Работает",
+ "error": "Ошибка",
+ "pending": "Ожидание",
+ "stopped": "Остановлен",
+ "unknown": "Неизвестно",
+ "errors": "ошибок"
+ },
+ "platformQr": {
+ "title": "QR вход",
+ "show": "Показать QR",
+ "status": "Статус QR",
+ "waiting": "Ожидание QR",
+ "close": "Закрыть"
+ },
+ "errorDialog": {
"title": "Детали ошибки",
"platformId": "ID платформы",
"errorCount": "Кол-во ошибок",
@@ -132,4 +139,4 @@
"traceback": "Стек вызовов",
"close": "Закрыть"
}
-}
\ No newline at end of file
+}
diff --git a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
index 27bafd0dc..de22ecffb 100644
--- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
+++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
@@ -636,6 +636,30 @@
"description": "仅使用 Webhook 发送消息",
"hint": "可选。启用后,企业微信智能机器人的所有回复都改为通过消息推送 Webhook 发送。消息推送 Webhook 支持更多的消息类型(如图片、文件等)。如果不需要打字机效果,强烈建议使用此选项。"
},
+ "weixin_oc_base_url": {
+ "description": "Base URL 地址",
+ "hint": "默认值: https://ilinkai.weixin.qq.com"
+ },
+ "weixin_oc_bot_type": {
+ "description": "扫码参数 bot_type",
+ "hint": "默认值: 3"
+ },
+ "weixin_oc_qr_poll_interval": {
+ "description": "二维码状态轮询间隔(秒)",
+ "hint": "每隔多少秒轮询一次二维码状态。"
+ },
+ "weixin_oc_long_poll_timeout_ms": {
+ "description": "getUpdates 长轮询超时时间(毫秒)",
+ "hint": "会话消息拉取接口超时参数。"
+ },
+ "weixin_oc_api_timeout_ms": {
+ "description": "HTTP 请求超时(毫秒)",
+ "hint": "通用 API 请求超时参数。"
+ },
+ "weixin_oc_token": {
+ "description": "登录后 token(可留空)",
+ "hint": "扫码登录成功后会自动写入;高级场景可手动填写。"
+ },
"kook_bot_token": {
"description": "机器人 Token",
"type": "string",
diff --git a/dashboard/src/i18n/locales/zh-CN/features/extension.json b/dashboard/src/i18n/locales/zh-CN/features/extension.json
index f42173ffa..6f38591f4 100644
--- a/dashboard/src/i18n/locales/zh-CN/features/extension.json
+++ b/dashboard/src/i18n/locales/zh-CN/features/extension.json
@@ -9,7 +9,8 @@
"handlersOperation": "管理行为"
},
"titles": {
- "installedAstrBotPlugins": "已安装的 AstrBot 插件"
+ "installedAstrBotPlugins": "已安装的 AstrBot 插件",
+ "pinnedPlugins": "置顶插件"
},
"failedPlugins": {
"title": "加载失败插件({count})",
@@ -53,6 +54,8 @@
"refresh": "刷新",
"updateAll": "更新全部插件",
"deleteSource": "删除源",
+ "pin": "置顶",
+ "unpin": "取消置顶",
"reshuffle": "随机一发"
},
"status": {
@@ -96,6 +99,16 @@
"market": {
"recommended": "🥳 推荐",
"allPlugins": "📦 全部插件",
+ "category": "分类",
+ "categories": {
+ "all": "全部",
+ "ai_tools": "AI 增强",
+ "entertainment": "娱乐",
+ "productivity": "效率",
+ "integrations": "外部集成",
+ "utilities": "生活实用",
+ "other": "其他"
+ },
"showFullName": "完整名称",
"devDocs": "插件开发文档",
"submitRepo": "提交插件仓库",
diff --git a/dashboard/src/i18n/locales/zh-CN/features/platform.json b/dashboard/src/i18n/locales/zh-CN/features/platform.json
index f26d9507d..75dc5a2a6 100644
--- a/dashboard/src/i18n/locales/zh-CN/features/platform.json
+++ b/dashboard/src/i18n/locales/zh-CN/features/platform.json
@@ -123,6 +123,13 @@
"unknown": "未知",
"errors": "个错误"
},
+ "platformQr": {
+ "title": "二维码登录",
+ "show": "查看二维码",
+ "status": "二维码状态",
+ "waiting": "等待二维码",
+ "close": "关闭"
+ },
"errorDialog": {
"title": "错误详情",
"platformId": "平台 ID",
diff --git a/dashboard/src/stores/common.js b/dashboard/src/stores/common.js
index f75790e0a..9d1322533 100644
--- a/dashboard/src/stores/common.js
+++ b/dashboard/src/stores/common.js
@@ -177,6 +177,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)
diff --git a/dashboard/src/utils/platformUtils.js b/dashboard/src/utils/platformUtils.js
index 87523f3ad..04cb226fa 100644
--- a/dashboard/src/utils/platformUtils.js
+++ b/dashboard/src/utils/platformUtils.js
@@ -12,6 +12,8 @@ export function getPlatformIcon(name) {
return new URL('@/assets/images/platform_logos/onebot.png', import.meta.url).href
} else if (name === 'qq_official' || name === 'qq_official_webhook') {
return new URL('@/assets/images/platform_logos/qq.png', import.meta.url).href
+ } else if (name === 'weixin_oc' || name === 'weixin_oc') {
+ return new URL('@/assets/images/platform_logos/wechat.png', import.meta.url).href
} else if (name === 'wecom' || name === 'wecom_ai_bot') {
return new URL('@/assets/images/platform_logos/wecom.png', import.meta.url).href
} else if (name === 'weixin_official_account') {
@@ -50,6 +52,7 @@ export function getTutorialLink(platformType) {
"qq_official": "https://docs.astrbot.app/platform/qqofficial/websockets.html",
"aiocqhttp": "https://docs.astrbot.app/platform/aiocqhttp/napcat.html",
"wecom": "https://docs.astrbot.app/platform/wecom.html",
+ "weixin_oc": "https://docs.astrbot.app/platform/weixin_oc.html",
"wecom_ai_bot": "https://docs.astrbot.app/platform/wecom_ai_bot.html",
"lark": "https://docs.astrbot.app/platform/lark.html",
"telegram": "https://docs.astrbot.app/platform/telegram.html",
diff --git a/dashboard/src/views/PlatformPage.vue b/dashboard/src/views/PlatformPage.vue
index 59b9d6be0..be173ab30 100644
--- a/dashboard/src/views/PlatformPage.vue
+++ b/dashboard/src/views/PlatformPage.vue
@@ -230,6 +230,30 @@
+
+
+
+ mdi-qrcode
+ {{ tm('platformQr.title') }}
+
+
+
+ {{ tm('platformQr.status') }}: {{ getPlatformQrLoginStat(currentQrPlatformId)?.qr_status || tm('platformQr.waiting') }}
+
+
+
+
+
+
+ {{ tm('platformQr.close') }}
+
+
+
+
+
{
this.getPlatformStats();
- }, 10000);
+ }, 5000);
// 监听语言切换事件,重新加载配置以获取插件的 i18n 数据
window.addEventListener('astrbot-locale-changed', this.handleLocaleChange);
@@ -463,8 +491,8 @@ export default {
});
},
- getPlatformStats() {
- axios.get('/api/platform/stats').then((res) => {
+ async getPlatformStats() {
+ await axios.get('/api/platform/stats').then((res) => {
if (res.data.status === 'ok') {
// 将数组转换为以 id 为 key 的对象,方便查找
const stats = {};
@@ -482,6 +510,31 @@ export default {
return this.platformStats[platformId] || null;
},
+ hasQrPayload(platformId) {
+ const stat = this.getPlatformQrLoginStat(platformId);
+ return Boolean(stat?.qrcode_img_content || stat?.qrcode);
+ },
+
+ getPlatformQrLoginStat(platformId) {
+ const stat = this.getPlatformStat(platformId);
+ if (stat?.weixin_oc) {
+ return stat.weixin_oc;
+ }
+ if (stat && typeof stat === "object") {
+ for (const value of Object.values(stat)) {
+ if (value && typeof value === "object" && ("qrcode_img_content" in value || "qrcode" in value)) {
+ return value;
+ }
+ }
+ }
+ return null;
+ },
+
+ openPlatformQrDialog(platformId) {
+ this.currentQrPlatformId = platformId;
+ this.showQrDialog = true;
+ },
+
getStatusColor(status) {
switch (status) {
case 'running': return 'success';
@@ -740,4 +793,14 @@ export default {
max-height: 300px;
overflow-y: auto;
}
+
+.platform-qr-chip {
+ margin-top: 4px;
+}
+
+.platform-qr-status {
+ font-size: 13px;
+ margin-bottom: 10px;
+ color: rgba(0, 0, 0, 0.7);
+}
diff --git a/dashboard/src/views/extension/InstalledPluginsTab.vue b/dashboard/src/views/extension/InstalledPluginsTab.vue
index 2d36ea69d..804767a8a 100644
--- a/dashboard/src/views/extension/InstalledPluginsTab.vue
+++ b/dashboard/src/views/extension/InstalledPluginsTab.vue
@@ -1,9 +1,11 @@
@@ -846,4 +950,94 @@ const {
transform: translateY(-4px) scale(1.05);
box-shadow: 0 12px 20px rgba(var(--v-theme-primary), 0.4);
}
+
+.pinned-plugins h3 {
+ font-weight: 600;
+}
+
+.pinned-list {
+ gap: 12px;
+}
+
+.pinned-item {
+ flex: 1 1 180px;
+ max-width: 320px;
+ height: 76px;
+ border-radius: 10px;
+ background: rgba(0, 0, 0, 0.04);
+ box-shadow: 0 1px 4px rgba(16,24,40,0.04);
+}
+
+.pinned-avatar {
+ display: inline-flex;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ cursor: pointer;
+ border-radius: 12px;
+}
+
+.pinned-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+.pinned-card-wrapper {
+ position: relative;
+ display: inline-block;
+ width: 72px;
+ height: 72px;
+}
+
+.pin-btn {
+ position: absolute;
+ top: 6px;
+ right: 6px;
+ z-index: 5;
+}
+
+.pinned-item-skeleton {
+ width: 72px;
+ height: 72px;
+ border-radius: 10px;
+}
+
+.pinned-item {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: transform 0.2s ease, opacity 0.2s ease;
+}
+
+.is-dragging {
+ opacity: 0.5;
+ transform: scale(0.95);
+ cursor: grabbing;
+}
+
+[draggable="true"] {
+ cursor: grab;
+}
+
+[draggable="true"]:active {
+ cursor: grabbing;
+}
+
+.list-move,
+.list-enter-active,
+.list-leave-active {
+ transition: all 0.3s ease;
+}
+
+.list-enter-from,
+.list-leave-to {
+ opacity: 0;
+ transform: scale(0.6);
+}
+
+.list-leave-active {
+ position: absolute;
+}
diff --git a/dashboard/src/views/extension/MarketPluginsTab.vue b/dashboard/src/views/extension/MarketPluginsTab.vue
index 1ad5f843a..b4715994b 100644
--- a/dashboard/src/views/extension/MarketPluginsTab.vue
+++ b/dashboard/src/views/extension/MarketPluginsTab.vue
@@ -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,
+ })),
+);
@@ -418,3 +427,17 @@ const marketSortItems = computed(() => [
+
+
diff --git a/dashboard/src/views/extension/useExtensionPage.js b/dashboard/src/views/extension/useExtensionPage.js
index 5d8c8778c..932eafced 100644
--- a/dashboard/src/views/extension/useExtensionPage.js
+++ b/dashboard/src/views/extension/useExtensionPage.js
@@ -238,6 +238,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,
@@ -257,6 +258,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" },
@@ -439,13 +524,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;
+ });
});
// 所有插件列表,推荐插件排在前面
@@ -571,8 +667,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);
@@ -584,7 +682,9 @@ export const useExtensionPage = () => {
} catch (err) {
toast(err, "error");
} finally {
- loading_.value = false;
+ if (withLoading) {
+ loading_.value = false;
+ }
}
};
@@ -1412,6 +1512,7 @@ export const useExtensionPage = () => {
// 刷新插件市场数据
const refreshPluginMarket = async () => {
refreshingMarket.value = true;
+ loading_.value = true;
try {
// 强制刷新插件市场数据
const data = await commonStore.getPluginCollections(
@@ -1430,6 +1531,7 @@ export const useExtensionPage = () => {
toast(tm("messages.refreshFailed") + " " + err, "error");
} finally {
refreshingMarket.value = false;
+ loading_.value = false;
}
};
@@ -1438,21 +1540,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,
@@ -1464,6 +1567,8 @@ export const useExtensionPage = () => {
refreshRandomPlugins();
} catch (err) {
toast(tm("messages.getMarketDataFailed") + " " + err, "error");
+ } finally {
+ loading_.value = false;
}
});
@@ -1533,6 +1638,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,
@@ -1576,6 +1698,9 @@ export const useExtensionPage = () => {
installedSortOrder,
loading_,
currentPage,
+ marketCategoryFilter,
+ marketCategoryItems,
+ marketCategoryCounts,
dangerConfirmDialog,
selectedDangerPlugin,
selectedMarketInstallPlugin,
diff --git a/dashboard/src/views/persona/FolderBreadcrumb.vue b/dashboard/src/views/persona/FolderBreadcrumb.vue
index 4550bc590..1b65a159c 100644
--- a/dashboard/src/views/persona/FolderBreadcrumb.vue
+++ b/dashboard/src/views/persona/FolderBreadcrumb.vue
@@ -40,6 +40,7 @@ import { defineComponent } from 'vue';
import { useModuleI18n } from '@/i18n/composables';
import { usePersonaStore } from '@/stores/personaStore';
import { mapState, mapActions } from 'pinia';
+import BaseFolderBreadcrumb from '@/components/folder/BaseFolderBreadcrumb.vue';
import type { FolderTreeNode } from '@/components/folder/types';
interface BreadcrumbItem {
@@ -51,33 +52,15 @@ interface BreadcrumbItem {
export default defineComponent({
name: 'FolderBreadcrumb',
+ components: { BaseFolderBreadcrumb },
setup() {
const { tm } = useModuleI18n('features/persona');
return { tm };
},
computed: {
...mapState(usePersonaStore, ['breadcrumbPath', 'currentFolderId']),
-
- breadcrumbItems(): BreadcrumbItem[] {
- const items: BreadcrumbItem[] = [
- {
- title: this.tm('folder.rootFolder'),
- folderId: null,
- disabled: this.currentFolderId === null,
- isRoot: true
- }
- ];
-
- (this.breadcrumbPath as FolderTreeNode[]).forEach((folder, index) => {
- items.push({
- title: folder.name,
- folderId: folder.folder_id,
- disabled: index === (this.breadcrumbPath as FolderTreeNode[]).length - 1,
- isRoot: false
- });
- });
-
- return items;
+ rootName(): string {
+ return this.tm('folder.rootFolder');
}
},
methods: {
diff --git a/dashboard/src/views/persona/FolderCard.vue b/dashboard/src/views/persona/FolderCard.vue
index 506a0b5fb..27cfee5ad 100644
--- a/dashboard/src/views/persona/FolderCard.vue
+++ b/dashboard/src/views/persona/FolderCard.vue
@@ -89,10 +89,12 @@