refactor(navigation): separate page section and compact tabs

This commit is contained in:
digua committed 2026-07-12 01:44:52 +08:00
1 parent 7120d56851
commit 12c7fc2105
22 files changed
+234 -273

No files matched your search

+2 -2
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { SubTabs } from '@/components/UI'
import { SectionTabs } from '@/components/navigation'
import TableBrowser from './TableBrowser.vue'
import ToolTestTab from './ToolTestTab.vue'
@@ -21,7 +21,7 @@ const activeSubTab = ref('sql-tables')
<template>
<div class="flex h-full flex-col">
<SubTabs v-model="activeSubTab" :items="subTabs" persist-key="debugTab" />
<SectionTabs v-model="activeSubTab" :items="subTabs" persist-key="debugTab" />
<div class="min-h-0 flex-1 overflow-hidden">
<Transition name="fade" mode="out-in">
-1
View File
@@ -11,7 +11,6 @@ export { default as EmptyState } from './EmptyState.vue'
export { default as LoadingState } from './LoadingState.vue'
export { default as Tabs } from './Tabs.vue'
export { default as UITabs } from './Tabs.vue'
export { default as SubTabs } from './SubTabs.vue'
export { default as PageAnchorsNav } from './PageAnchorsNav.vue'
export { default as FileDropZone } from './FileDropZone.vue'
export { default as DatePicker } from './DatePicker.vue'
+2 -2
View File
@@ -2,7 +2,7 @@
import { ref, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { SubTabs } from '@/components/UI'
import { SectionTabs } from '@/components/navigation'
import { ChatExplorer } from '../AIChat'
import SQLLabTab from './SQLLabTab.vue'
import { KeywordAnalysis } from './quotes'
@@ -68,7 +68,7 @@ defineExpose({
<template>
<div class="flex h-full flex-col">
<!-- 子 Tab 导航 -->
<SubTabs v-model="activeSubTab" :items="subTabs" persist-key="aiTab" />
<SectionTabs v-model="activeSubTab" :items="subTabs" persist-key="aiTab" />
<!-- 子 Tab 内容 -->
<div class="flex-1 min-h-0 overflow-hidden">
@@ -8,8 +8,8 @@ import AIPreprocessTab from './AI/AIPreprocessTab.vue'
import AIExportSettingsTab from './AI/AIExportSettingsTab.vue'
import SessionIndexSection from './AI/SessionIndexSection.vue'
import SemanticIndexSection from './AI/SemanticIndexSection.vue'
import SubTabs from '@/components/UI/SubTabs.vue'
import { useSubTabsScroll } from '@/composables/useSubTabsScroll'
import { CompactTabs } from '@/components/navigation'
import { useAnchorNavigation } from '@/composables/useAnchorNavigation'
const { t } = useI18n()
@@ -31,8 +31,8 @@ const navItems = computed(() => [
{ id: 'export', label: t('settings.aiPrompt.exportSettings.title') },
])
// 使用二级导航滚动联动 composable
const { activeNav, scrollContainerRef, setSectionRef, handleNavChange, scrollToId } = useSubTabsScroll(navItems)
// 使用锚点导航滚动联动 composable
const { activeNav, scrollContainerRef, setSectionRef, handleNavChange, scrollToId } = useAnchorNavigation(navItems)
void scrollContainerRef // 在模板中通过 ref="scrollContainerRef" 使用
// AI 配置变更回调
@@ -61,7 +61,7 @@ void aiModelConfigRef.value
<div class="flex h-full gap-6">
<!-- 左侧锚点导航 -->
<div class="w-36 shrink-0">
<SubTabs v-model="activeNav" :items="navItems" orientation="vertical" @change="handleNavChange" />
<CompactTabs v-model="activeNav" :items="navItems" orientation="vertical" @change="handleNavChange" />
</div>
<!-- 右侧内容区域 -->
@@ -3,7 +3,7 @@ import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useApiServerStore, type DataSource, type ImportSession } from '@/stores/apiServer'
import { storeToRefs } from 'pinia'
import { SubTabs } from '@/components/UI'
import { CompactTabs } from '@/components/navigation'
import { useLayoutStore } from '@/stores/layout'
import DataSourceAddModal from './API/DataSourceAddModal.vue'
import DataSourceEditModal from './API/DataSourceEditModal.vue'
@@ -230,7 +230,7 @@ function subscribedRemoteIds(ds: DataSource): Set<string> {
<template>
<div class="flex h-full flex-col -mx-6 -mt-6">
<SubTabs v-model="activeSubTab" :items="subTabs" persist-key="apiSubTab" />
<CompactTabs v-model="activeSubTab" :items="subTabs" persist-key="apiSubTab" />
<div class="flex-1 min-h-0 overflow-auto">
<div class="space-y-6 px-6 pt-4 pb-6">
+2 -16
View File
@@ -8,6 +8,7 @@ import BatchManageTab from './Settings/BatchManageTab.vue'
import StorageTab from './Settings/StorageTab.vue'
import AboutTab from './Settings/AboutTab.vue'
import ApiSettingsTab from './Settings/ApiSettingsTab.vue'
import { PageTabs } from '@/components/navigation'
import { usePromptStore } from '@/stores/prompt'
import { useLayoutStore } from '@/stores/layout'
@@ -100,22 +101,7 @@ watch(showSettings, async (visible) => {
/>
</div>
<!-- Tabs -->
<div class="mt-4 flex items-center gap-1 overflow-x-auto pb-3 scrollbar-hide">
<button
v-for="tab in tabs"
:key="tab.id"
class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-all"
:class="[
activeTab === tab.id
? 'bg-pink-500 text-white dark:bg-pink-900/30 dark:text-pink-300'
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800',
]"
@click="switchTab(tab.id)"
>
<UIcon :name="tab.icon" class="h-4 w-4" />
<span class="whitespace-nowrap">{{ tab.label }}</span>
</button>
</div>
<PageTabs v-model="activeTab" class="mt-4 pb-3" :items="tabs" @change="switchTab" />
</div>
<div class="relative flex-1">
+3 -3
View File
@@ -17,7 +17,7 @@ import {
type UpdateNoticeCache,
type UpdateNoticeState,
} from './sidebar/updateNotice'
import SubTabs from '@/components/UI/SubTabs.vue'
import { CompactTabs } from '@/components/navigation'
import { useSessionStore } from '@/stores/session'
import { useLayoutStore } from '@/stores/layout'
import { usePlatformService } from '@/services'
@@ -481,7 +481,7 @@ function getAvatarColorClass(session: AnalysisSession, isActive: boolean) {
<div class="flex-1 relative min-h-0 flex flex-col">
<!-- 筛选与排序 - 固定在顶部,不随列表滚动 -->
<div v-if="!isCollapsed && sessions.length > 0" class="mb-2">
<SubTabs v-model="filterType" :items="filterTabItems" size="sm" :bordered="false">
<CompactTabs v-model="filterType" :items="filterTabItems" size="sm" :bordered="false">
<template #right>
<div class="flex items-center gap-0.5">
<UTooltip :text="t('layout.tooltip.search')" :content="{ side: 'bottom' }">
@@ -505,7 +505,7 @@ function getAvatarColorClass(session: AnalysisSession, isActive: boolean) {
</UTooltip>
</div>
</template>
</SubTabs>
</CompactTabs>
<!-- 搜索框 -->
<div v-if="showSearch" class="mt-2 px-4">
<UInput
@@ -5,7 +5,7 @@ import { useLayoutStore } from '@/stores/layout'
import CaptureButton from '@/components/common/CaptureButton.vue'
import TimeSelect from '@/components/common/TimeSelect.vue'
import PageHeader from '@/components/layout/PageHeader.vue'
import PageNavigation from '@/components/navigation/PageNavigation.vue'
import { PageTabs } from '@/components/navigation'
import type { TimeRangeValue, TimeSelectState } from '@/components/common/TimeSelect.vue'
interface SessionAnalysisTab {
@@ -99,7 +99,7 @@ const navigationItems = computed(() =>
</template>
<div class="mt-3 flex items-center justify-between gap-3">
<PageNavigation v-model="activeTab" class="min-w-0 shrink" :items="navigationItems" />
<PageTabs v-model="activeTab" class="min-w-0 shrink" :items="navigationItems" />
<!-- AI 对话和实验室都不使用这里的时间范围筛选,因此在这些一级 Tab 下隐藏。 -->
<TimeSelect
v-model="timeRangeValue"
+40
View File
@@ -0,0 +1,40 @@
<script setup lang="ts">
import NavigationTabsBase from './NavigationTabsBase.vue'
import type { NavigationTabItem } from './types'
withDefaults(
defineProps<{
items: NavigationTabItem[]
persistKey?: string
orientation?: 'horizontal' | 'vertical'
size?: 'sm' | 'md'
bordered?: boolean
}>(),
{
orientation: 'horizontal',
size: 'md',
}
)
const model = defineModel<string>({ required: true })
const emit = defineEmits<{
change: [value: string]
}>()
</script>
<template>
<NavigationTabsBase
v-model="model"
:items="items"
:persist-key="persistKey"
:orientation="orientation"
:size="size"
:bordered="bordered"
mode="compact"
@change="emit('change', $event)"
>
<template v-if="$slots.right" #right>
<slot name="right" />
</template>
</NavigationTabsBase>
</template>
@@ -1,32 +1,16 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
/**
* 轻量级子标签页组件
* 适用于页面内部的二级导航,使用原生样式
* 支持通过 persistKey 将选中状态同步到 URL 查询参数
* 支持水平和垂直两种方向
*/
interface TabItem {
id: string
label: string
icon?: string
}
import type { NavigationTabItem } from './types'
interface Props {
modelValue: string
items: TabItem[]
/** 持久化 key,设置后会将当前 tab 状态同步到 URL 查询参数 */
items: NavigationTabItem[]
persistKey?: string
/** 方向:horizontal(水平)或 vertical(垂直) */
orientation?: 'horizontal' | 'vertical'
/** 尺寸:sm 适用于侧边栏等紧凑场景 */
size?: 'sm' | 'md'
/** 是否显示底部边框线 */
bordered?: boolean
/** 页面级二级导航使用 page,嵌入式 Tab 保持 default */
variant?: 'default' | 'page'
mode: 'section' | 'compact'
}
interface Emits {
@@ -37,24 +21,16 @@ interface Emits {
const props = withDefaults(defineProps<Props>(), {
orientation: 'horizontal',
size: 'md',
variant: 'default',
})
const emit = defineEmits<Emits>()
const route = useRoute()
const router = useRouter()
// 是否为垂直模式
const isVertical = computed(() => props.orientation === 'vertical')
// Tab 按钮引用
const tabRefs = ref<Record<string, HTMLElement | null>>({})
const containerRef = ref<HTMLElement | null>(null)
// 激活指示器的样式
const indicatorStyle = ref<Record<string, string>>({})
// 计算内部值
const activeTab = computed({
get: () => props.modelValue,
set: (value) => {
@@ -63,64 +39,45 @@ const activeTab = computed({
},
})
// 更新指示器位置
function updateIndicator() {
const activeButton = tabRefs.value[activeTab.value]
if (activeButton && containerRef.value) {
const containerRect = containerRef.value.getBoundingClientRect()
const buttonRect = activeButton.getBoundingClientRect()
if (!activeButton || !containerRef.value) return
if (isVertical.value) {
// 垂直模式:指示器在右侧
indicatorStyle.value = {
const containerRect = containerRef.value.getBoundingClientRect()
const buttonRect = activeButton.getBoundingClientRect()
indicatorStyle.value = isVertical.value
? {
top: `${buttonRect.top - containerRect.top}px`,
height: `${buttonRect.height}px`,
right: '0px',
width: '2px',
}
} else {
// 水平模式:指示器在底部
indicatorStyle.value = {
: {
left: `${buttonRect.left - containerRect.left}px`,
width: `${buttonRect.width}px`,
bottom: '0px',
height: '2px',
}
}
}
}
// 点击标签
const handleTabClick = (tabId: string) => {
activeTab.value = tabId
}
// 设置 tab 引用
function setTabRef(id: string, el: HTMLElement | null) {
tabRefs.value[id] = el
}
// 从 URL 查询参数恢复 tab 状态
onMounted(() => {
if (props.persistKey) {
const savedTab = route.query[props.persistKey] as string
// 验证 savedTab 是否在 items 中存在
if (savedTab && props.items.some((item) => item.id === savedTab)) {
activeTab.value = savedTab
}
}
// 初始更新指示器位置
nextTick(() => {
updateIndicator()
})
nextTick(updateIndicator)
})
// 监听 tab 变化,同步到 URL 查询参数并更新指示器
watch(
() => props.modelValue,
(newValue) => {
if (props.persistKey && newValue) {
// 使用 replace 而不是 push,避免产生大量历史记录
router.replace({
query: {
...route.query,
@@ -128,21 +85,13 @@ watch(
},
})
}
// 更新指示器位置
nextTick(() => {
updateIndicator()
})
nextTick(updateIndicator)
}
)
// 监听 items 变化,更新指示器位置
watch(
() => props.items,
() => {
nextTick(() => {
updateIndicator()
})
},
() => nextTick(updateIndicator),
{ deep: true }
)
</script>
@@ -155,7 +104,7 @@ watch(
: [
'flex justify-between overflow-x-auto',
bordered !== false ? 'border-b border-gray-200/50 dark:border-gray-800/50' : '',
variant === 'page'
mode === 'section'
? 'flex-col items-stretch gap-2 pl-7 pr-6 pt-3 xl:flex-row xl:items-center'
: ['items-center', size === 'sm' ? 'px-3' : 'px-6'],
],
@@ -166,6 +115,7 @@ watch(
v-for="tab in items"
:key="tab.id"
:ref="(el) => setTabRef(tab.id, el as HTMLElement)"
type="button"
class="flex shrink-0 items-center whitespace-nowrap font-medium transition-colors"
:class="[
isVertical
@@ -177,17 +127,16 @@ watch(
? 'text-primary-600 dark:text-primary-400'
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300',
]"
@click="handleTabClick(tab.id)"
@click="activeTab = tab.id"
>
<UIcon v-if="tab.icon" :name="tab.icon" :class="size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
{{ tab.label }}
</button>
<!-- 滑动指示器 -->
<div class="absolute bg-primary-500 transition-all duration-300 ease-out" :style="indicatorStyle" />
</div>
<!-- 右侧插槽(水平模式可用) -->
<template v-if="!isVertical">
<div v-if="variant === 'page'" class="flex w-full min-w-0 items-center xl:w-auto xl:justify-end">
<template v-if="!isVertical && $slots.right">
<div v-if="mode === 'section'" class="flex w-full min-w-0 items-center xl:w-auto xl:justify-end">
<slot name="right" />
</div>
<slot v-else name="right" />
@@ -1,17 +1,15 @@
<script setup lang="ts">
import { RouterLink, type RouteLocationRaw } from 'vue-router'
import type { NavigationTabItem } from './types'
interface PageNavItem {
id: string
label: string
icon?: string
interface PageTabItem extends NavigationTabItem {
to?: RouteLocationRaw
}
const props = withDefaults(
defineProps<{
modelValue: string
items: PageNavItem[]
items: PageTabItem[]
ariaLabel?: string
}>(),
{
@@ -24,7 +22,7 @@ const emit = defineEmits<{
change: [value: string]
}>()
function selectItem(item: PageNavItem): void {
function selectItem(item: PageTabItem): void {
if (item.to || item.id === props.modelValue) return
emit('update:modelValue', item.id)
emit('change', item.id)
+30
View File
@@ -0,0 +1,30 @@
<script setup lang="ts">
import NavigationTabsBase from './NavigationTabsBase.vue'
import type { NavigationTabItem } from './types'
defineProps<{
items: NavigationTabItem[]
persistKey?: string
}>()
const model = defineModel<string>({ required: true })
const emit = defineEmits<{
change: [value: string]
}>()
</script>
<template>
<NavigationTabsBase
v-model="model"
:items="items"
:persist-key="persistKey"
mode="section"
size="sm"
:bordered="false"
@change="emit('change', $event)"
>
<template v-if="$slots.right" #right>
<slot name="right" />
</template>
</NavigationTabsBase>
</template>
+4
View File
@@ -0,0 +1,4 @@
export { default as PageTabs } from './PageTabs.vue'
export { default as SectionTabs } from './SectionTabs.vue'
export { default as CompactTabs } from './CompactTabs.vue'
export type { NavigationTabItem } from './types'
+5
View File
@@ -0,0 +1,5 @@
export interface NavigationTabItem {
id: string
label: string
icon?: string
}
+97
View File
@@ -0,0 +1,97 @@
import { ref, onMounted, onUnmounted, type Ref, type ComputedRef } from 'vue'
export interface AnchorNavigationItem {
id: string
label: string
icon?: string
}
export interface AnchorNavigationOptions {
scrollBehavior?: ScrollBehavior
scrollLockDuration?: number
}
/**
* 实现锚点导航与内容区域的滚动联动。
*/
export function useAnchorNavigation(
navItems: ComputedRef<AnchorNavigationItem[]> | Ref<AnchorNavigationItem[]>,
options: AnchorNavigationOptions = {}
) {
const scrollBehavior = options.scrollBehavior ?? 'smooth'
const scrollLockDuration = options.scrollLockDuration ?? 500
const activeNav = ref(navItems.value[0]?.id || '')
const isUserClick = ref(false)
const scrollContainerRef = ref<HTMLElement | null>(null)
const sectionRefs = ref<Record<string, HTMLElement | null>>({})
function setSectionRef(id: string, el: HTMLElement | null) {
sectionRefs.value[id] = el
}
function handleNavChange(id: string) {
const section = sectionRefs.value[id]
if (!section || !scrollContainerRef.value) return
isUserClick.value = true
section.scrollIntoView({ behavior: scrollBehavior, block: 'start' })
setTimeout(() => {
isUserClick.value = false
}, scrollLockDuration)
}
function handleScroll() {
if (isUserClick.value || !scrollContainerRef.value) return
const container = scrollContainerRef.value
const containerRect = container.getBoundingClientRect()
const offset = 50
const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 5
if (isAtBottom) {
const lastItem = navItems.value[navItems.value.length - 1]
if (lastItem) activeNav.value = lastItem.id
return
}
for (const item of navItems.value) {
const section = sectionRefs.value[item.id]
if (!section) continue
const rect = section.getBoundingClientRect()
if (rect.top <= containerRect.top + offset && rect.bottom > containerRect.top + offset) {
activeNav.value = item.id
break
}
}
}
onMounted(() => {
scrollContainerRef.value?.addEventListener('scroll', handleScroll)
})
onUnmounted(() => {
scrollContainerRef.value?.removeEventListener('scroll', handleScroll)
})
function scrollToId(id: string) {
const section = sectionRefs.value[id]
if (!section || !scrollContainerRef.value) return
isUserClick.value = true
section.scrollIntoView({ behavior: scrollBehavior, block: 'start' })
activeNav.value = id
setTimeout(() => {
isUserClick.value = false
}, scrollLockDuration)
}
return {
activeNav,
scrollContainerRef,
sectionRefs,
setSectionRef,
handleNavChange,
scrollToId,
}
}
-131
View File
@@ -1,131 +0,0 @@
import { ref, onMounted, onUnmounted, type Ref, type ComputedRef } from 'vue'
/**
* 导航项配置
*/
export interface SubTabNavItem {
id: string
label: string
icon?: string
}
export interface SubTabsScrollOptions {
scrollBehavior?: ScrollBehavior
scrollLockDuration?: number
}
/**
* 二级导航滚动联动 composable
* 实现左侧锚点导航与右侧内容区域的滚动联动
*/
export function useSubTabsScroll(
navItems: ComputedRef<SubTabNavItem[]> | Ref<SubTabNavItem[]>,
options: SubTabsScrollOptions = {}
) {
const scrollBehavior = options.scrollBehavior ?? 'smooth'
const scrollLockDuration = options.scrollLockDuration ?? 500
// 当前激活的导航项
const activeNav = ref(navItems.value[0]?.id || '')
// 是否由用户点击触发(用于区分点击滚动和手动滚动)
const isUserClick = ref(false)
// 滚动容器引用
const scrollContainerRef = ref<HTMLElement | null>(null)
// Section 引用
const sectionRefs = ref<Record<string, HTMLElement | null>>({})
/**
* 设置 section 引用
*/
function setSectionRef(id: string, el: HTMLElement | null) {
sectionRefs.value[id] = el
}
/**
* 处理导航点击(通过 @change 事件)
*/
function handleNavChange(id: string) {
const section = sectionRefs.value[id]
if (section && scrollContainerRef.value) {
// 标记为用户点击触发
isUserClick.value = true
section.scrollIntoView({ behavior: scrollBehavior, block: 'start' })
// 滚动动画结束后恢复
setTimeout(() => {
isUserClick.value = false
}, scrollLockDuration)
}
}
/**
* 监听滚动更新当前激活项
*/
function handleScroll() {
// 如果是用户点击触发的滚动,不更新 activeNav(避免冲突)
if (isUserClick.value || !scrollContainerRef.value) return
const container = scrollContainerRef.value
const containerRect = container.getBoundingClientRect()
const offset = 50 // 偏移量,提前触发
// 检查是否滚动到底部(误差范围 5px)
const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 5
if (isAtBottom) {
// 滚动到底部时,激活最后一个导航项
const lastItem = navItems.value[navItems.value.length - 1]
if (lastItem) {
activeNav.value = lastItem.id
}
return
}
// 检查每个 section 的位置
for (const item of navItems.value) {
const section = sectionRefs.value[item.id]
if (section) {
const rect = section.getBoundingClientRect()
// 如果 section 顶部在容器可视区域内
if (rect.top <= containerRect.top + offset && rect.bottom > containerRect.top + offset) {
activeNav.value = item.id
break
}
}
}
}
// 生命周期钩子
onMounted(() => {
scrollContainerRef.value?.addEventListener('scroll', handleScroll)
})
onUnmounted(() => {
scrollContainerRef.value?.removeEventListener('scroll', handleScroll)
})
/**
* 程序化滚动到指定 section(供外部调用)
*/
function scrollToId(id: string) {
const section = sectionRefs.value[id]
if (section && scrollContainerRef.value) {
isUserClick.value = true
section.scrollIntoView({ behavior: scrollBehavior, block: 'start' })
activeNav.value = id
setTimeout(() => {
isUserClick.value = false
}, scrollLockDuration)
}
}
return {
activeNav,
scrollContainerRef,
sectionRefs,
setSectionRef,
handleNavChange,
scrollToId,
}
}
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { SubTabs } from '@/components/UI'
import { SectionTabs } from '@/components/navigation'
import { CatchphraseTab, HotRepeatTab } from '@/components/analysis/quotes'
import type { TimeFilter } from '@openchatlab/shared-types'
@@ -28,7 +28,7 @@ const activeSubTab = ref('hot-repeat')
<template>
<div class="flex h-full flex-col">
<!-- 子 Tab 导航 -->
<SubTabs v-model="activeSubTab" :items="subTabs" persist-key="quotesTab" />
<SectionTabs v-model="activeSubTab" :items="subTabs" persist-key="quotesTab" />
<!-- 子 Tab 内容 -->
<div class="flex-1 min-h-0 overflow-auto">
+3 -10
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { SubTabs } from '@/components/UI'
import { SectionTabs } from '@/components/navigation'
import UserSelect from '@/components/common/UserSelect.vue'
import TypeAnalysisView from '@/components/analysis/message/TypeAnalysisView.vue'
import TimeAnalysisView from '@/components/analysis/message/TimeAnalysisView.vue'
@@ -50,18 +50,11 @@ const viewTimeFilter = computed(() => ({
<template>
<div class="flex h-full flex-col">
<SubTabs
v-model="activeSubTab"
:items="subTabs"
persist-key="groupViewTab"
size="sm"
variant="page"
:bordered="false"
>
<SectionTabs v-model="activeSubTab" :items="subTabs" persist-key="groupViewTab">
<template #right>
<UserSelect v-if="activeSubTab !== 'topic'" v-model="selectedMemberId" :session-id="props.sessionId" />
</template>
</SubTabs>
</SectionTabs>
<div class="flex-1 min-h-0 overflow-y-auto">
<Transition name="fade" mode="out-in">
@@ -5,7 +5,7 @@
*/
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { SubTabs } from '@/components/UI'
import { SectionTabs } from '@/components/navigation'
import InteractionView from '@/components/analysis/relationships/InteractionView.vue'
import Relationships from './Relationships.vue'
import ClusterView from '@/components/analysis/relationships/ClusterView.vue'
@@ -33,7 +33,7 @@ const subTabs = computed(() => [
<template>
<div class="flex h-full flex-col">
<SubTabs v-model="activeSubTab" :items="subTabs" persist-key="groupRelationshipsTab" size="sm" />
<SectionTabs v-model="activeSubTab" :items="subTabs" persist-key="groupRelationshipsTab" />
<div class="min-h-0 flex-1 overflow-y-auto">
<Transition name="fade" mode="out-in">
+5 -7
View File
@@ -14,7 +14,8 @@ import type {
import { useDataService } from '@/services'
import { useToast } from '@/composables/useToast'
import { useLayoutStore } from '@/stores/layout'
import { LoadingState, SubTabs, ThemeCard } from '@/components/UI'
import { LoadingState, ThemeCard } from '@/components/UI'
import { SectionTabs } from '@/components/navigation'
import { usePeoplePageHeader } from '../people-page-header'
import ContactDetailPanel from './components/ContactDetailPanel.vue'
import ContactsStatusBlocks from './components/ContactsStatusBlocks.vue'
@@ -848,13 +849,10 @@ function getGroupSectionScrollTop(): number | null {
<template>
<div class="flex min-h-0 flex-1 flex-col text-gray-900 dark:bg-page-dark dark:text-gray-100">
<SubTabs
<SectionTabs
v-model="activeContactSection"
:items="contactTabs"
persist-key="contactsTab"
size="sm"
variant="page"
:bordered="false"
@change="handleContactTabChange"
>
<template #right>
@@ -887,11 +885,11 @@ function getGroupSectionScrollTop(): number | null {
</UInput>
</div>
</template>
</SubTabs>
</SectionTabs>
<div class="flex min-h-0 flex-1 overflow-hidden">
<main class="min-h-0 min-w-0 flex-1 overflow-hidden">
<div class="flex h-full min-h-0 w-full flex-col gap-6 px-6 pb-6 pt-4">
<div class="flex h-full min-h-0 w-full flex-col gap-6 px-6 pb-3 pt-4">
<ContactsStatusBlocks
:show-disabled-notice="showDisabledNotice"
:active-private-session-count="diagnostics?.activePrivateSessionCount ?? 0"
+3 -3
View File
@@ -3,7 +3,7 @@ import { computed } from 'vue'
import { RouterView, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import PageHeader from '@/components/layout/PageHeader.vue'
import PageNavigation from '@/components/navigation/PageNavigation.vue'
import { PageTabs } from '@/components/navigation'
import { providePeoplePageHeader, type PeoplePageHeaderConfig, type PeopleSubpage } from './people-page-header'
const { t } = useI18n()
@@ -75,7 +75,7 @@ const { header } = providePeoplePageHeader(defaultHeader)
</UButton>
</template>
<PageNavigation
<PageTabs
class="mt-3 pb-1.5"
:model-value="activeSubpage"
:items="navigationItems"
@@ -92,7 +92,7 @@ const { header } = providePeoplePageHeader(defaultHeader)
</template>
</div>
</template>
</PageNavigation>
</PageTabs>
</PageHeader>
<RouterView v-slot="{ Component }">
+3 -10
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { SubTabs } from '@/components/UI'
import { SectionTabs } from '@/components/navigation'
import UserSelect from '@/components/common/UserSelect.vue'
import TypeAnalysisView from '@/components/analysis/message/TypeAnalysisView.vue'
import TimeAnalysisView from '@/components/analysis/message/TimeAnalysisView.vue'
@@ -38,14 +38,7 @@ const viewTimeFilter = computed(() => ({
<template>
<div class="flex h-full flex-col">
<SubTabs
v-model="activeSubTab"
:items="subTabs"
persist-key="privateViewTab"
size="sm"
variant="page"
:bordered="false"
>
<SectionTabs v-model="activeSubTab" :items="subTabs" persist-key="privateViewTab">
<template #right>
<UserSelect
v-if="activeSubTab === 'type-analysis' || activeSubTab === 'time-analysis'"
@@ -53,7 +46,7 @@ const viewTimeFilter = computed(() => ({
:session-id="props.sessionId"
/>
</template>
</SubTabs>
</SectionTabs>
<div class="flex-1 min-h-0 overflow-auto">
<Transition name="fade" mode="out-in">