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.
This commit is contained in:
wizardchen
2026-06-29 11:06:38 +08:00
committed by lyingbug
parent 7d8a80ae8d
commit ea84b7b983
3 changed files with 81 additions and 10 deletions
+53
View File
@@ -94,6 +94,15 @@
class="menu-pending-badge"
:title="t('organization.settings.pendingJoinRequestsBadge')">{{
orgStore.totalPendingJoinRequestCount }}</span>
<span v-if="item.path === 'integrations'" class="integration-preview"
aria-hidden="true">
<span v-for="(preview, idx) in integrationPreviewItems" :key="preview.key"
class="integration-preview__item" :style="{ zIndex: idx + 1 }">
<t-icon v-if="preview.icon.type === 'icon'" :name="preview.icon.name"
size="13px" />
<span v-else class="integration-preview__emoji">{{ preview.icon.value }}</span>
</span>
</span>
</template>
</div>
</div>
@@ -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;
}
+19
View File
@@ -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: '🦞' } },
]
@@ -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;