From ea84b7b9838cd87f50e4f26f2ca08558d6d03854 Mon Sep 17 00:00:00 2001 From: wizardchen Date: Mon, 29 Jun 2026 11:06:38 +0800 Subject: [PATCH] feat(frontend): show stacked integration icons on sidebar hover Add a compact stacked icon preview when hovering the Publish & Integrations menu item so users can see available channels before opening the page. Centralize integration tab metadata in config for easier future additions. --- frontend/src/components/menu.vue | 53 +++++++++++++++++++ frontend/src/config/integrations.ts | 19 +++++++ .../views/integrations/IntegrationsModal.vue | 19 ++++--- 3 files changed, 81 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/menu.vue b/frontend/src/components/menu.vue index 8cac5986..1f35a33e 100644 --- a/frontend/src/components/menu.vue +++ b/frontend/src/components/menu.vue @@ -94,6 +94,15 @@ class="menu-pending-badge" :title="t('organization.settings.pendingJoinRequestsBadge')">{{ orgStore.totalPendingJoinRequestCount }} + @@ -248,8 +257,10 @@ import UserMenu from '@/components/UserMenu.vue'; import TenantSelector from '@/components/TenantSelector.vue'; import { useI18n } from 'vue-i18n'; import { getSystemInfo } from '@/api/system'; +import { INTEGRATION_PREVIEW_ITEMS } from '@/config/integrations'; const chatResources = useChatResourcesStore(); +const integrationPreviewItems = INTEGRATION_PREVIEW_ITEMS; // Platform logos reused from IMChannelsOverviewPanel — keeps the session list // visually consistent with the channels admin view. import wecomLogo from '@/assets/img/im/wecom.svg'; @@ -1887,6 +1898,48 @@ const onDragHandleMouseDown = (e: MouseEvent) => { flex-shrink: 0; } +.integration-preview { + display: inline-flex; + align-items: center; + margin-left: auto; + flex-shrink: 0; + width: 0; + overflow: hidden; + pointer-events: none; + + .menu_item:hover & { + width: auto; + } + + &__item { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + flex-shrink: 0; + border-radius: 50%; + background: var(--td-bg-color-container); + border: 2px solid var(--td-bg-color-sidebar); + box-sizing: border-box; + color: var(--td-text-color-primary); + + &:not(:first-child) { + margin-left: -5px; + } + + :deep(.t-icon) { + display: block; + } + } + + &__emoji { + font-size: 12px; + line-height: 1; + } +} + .menu_box { position: relative; } diff --git a/frontend/src/config/integrations.ts b/frontend/src/config/integrations.ts index 4a1e83f1..72e7aa7e 100644 --- a/frontend/src/config/integrations.ts +++ b/frontend/src/config/integrations.ts @@ -2,3 +2,22 @@ export const CHROME_EXTENSION_URL = 'https://chromewebstore.google.com/detail/jpemjbopikggjlmikmclgbmkhhopjdgd?utm_source=item-share-cb' export const CLAWHUB_SKILL_URL = 'https://clawhub.ai/lyingbug/weknora' + +export type IntegrationTab = 'im' | 'embed' | 'chrome' | 'claw' + +export const INTEGRATION_TABS: IntegrationTab[] = ['im', 'embed', 'chrome', 'claw'] + +export type IntegrationPreviewIcon = + | { type: 'icon'; name: string } + | { type: 'emoji'; value: string } + +/** Sidebar hover preview + Integrations modal nav — add new entries here. */ +export const INTEGRATION_PREVIEW_ITEMS: Array<{ + key: IntegrationTab + icon: IntegrationPreviewIcon +}> = [ + { key: 'im', icon: { type: 'icon', name: 'chat-message' } }, + { key: 'embed', icon: { type: 'icon', name: 'code' } }, + { key: 'chrome', icon: { type: 'icon', name: 'extension' } }, + { key: 'claw', icon: { type: 'emoji', value: '🦞' } }, +] diff --git a/frontend/src/views/integrations/IntegrationsModal.vue b/frontend/src/views/integrations/IntegrationsModal.vue index f2a7dc96..5f8befff 100644 --- a/frontend/src/views/integrations/IntegrationsModal.vue +++ b/frontend/src/views/integrations/IntegrationsModal.vue @@ -76,10 +76,7 @@ import IMChannelPanel from '@/components/IMChannelPanel.vue'; import AgentEmbedChannelPanel from '@/components/AgentEmbedChannelPanel.vue'; import ChromeExtensionLanding from '@/views/integrations/ChromeExtensionLanding.vue'; import ClawSkillLanding from '@/views/integrations/ClawSkillLanding.vue'; - -type IntegrationTab = 'im' | 'embed' | 'chrome' | 'claw'; - -const INTEGRATION_TABS: IntegrationTab[] = ['im', 'embed', 'chrome', 'claw']; +import { INTEGRATION_PREVIEW_ITEMS, INTEGRATION_TABS, type IntegrationTab } from '@/config/integrations'; const { t } = useI18n(); const route = useRoute(); @@ -94,12 +91,14 @@ const isLandingSection = computed( () => currentSection.value === 'chrome' || currentSection.value === 'claw', ); -const navItems = computed(() => [ - { key: 'im' as const, icon: 'chat-message', label: t('integrations.tabs.im') }, - { key: 'embed' as const, icon: 'code', label: t('integrations.tabs.embed') }, - { key: 'chrome' as const, icon: 'extension', label: t('integrations.tabs.chrome') }, - { key: 'claw' as const, icon: '', emoji: '🦞', label: t('integrations.tabs.claw') }, -]); +const navItems = computed(() => + INTEGRATION_PREVIEW_ITEMS.map((item) => ({ + key: item.key, + icon: item.icon.type === 'icon' ? item.icon.name : '', + emoji: item.icon.type === 'emoji' ? item.icon.value : undefined, + label: t(`integrations.tabs.${item.key}`), + })), +); function applyRouteQuery() { const tab = route.query.tab as string;