diff --git a/src/components/DebugTab/DebugTab.vue b/src/components/DebugTab/DebugTab.vue
index 7e9043da..7c890826 100644
--- a/src/components/DebugTab/DebugTab.vue
+++ b/src/components/DebugTab/DebugTab.vue
@@ -1,7 +1,7 @@
+
+
+
+
+
+
+
+
diff --git a/src/components/UI/SubTabs.vue b/src/components/navigation/NavigationTabsBase.vue
similarity index 64%
rename from src/components/UI/SubTabs.vue
rename to src/components/navigation/NavigationTabsBase.vue
index 3c596ae7..87e238e7 100644
--- a/src/components/UI/SubTabs.vue
+++ b/src/components/navigation/NavigationTabsBase.vue
@@ -1,32 +1,16 @@
@@ -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"
>
{{ tab.label }}
-
-
-
-
+
+
+
diff --git a/src/components/navigation/PageNavigation.vue b/src/components/navigation/PageTabs.vue
similarity index 91%
rename from src/components/navigation/PageNavigation.vue
rename to src/components/navigation/PageTabs.vue
index 759401b7..536c372f 100644
--- a/src/components/navigation/PageNavigation.vue
+++ b/src/components/navigation/PageTabs.vue
@@ -1,17 +1,15 @@
+
+
+
+
+
+
+
+
diff --git a/src/components/navigation/index.ts b/src/components/navigation/index.ts
new file mode 100644
index 00000000..5b83920c
--- /dev/null
+++ b/src/components/navigation/index.ts
@@ -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'
diff --git a/src/components/navigation/types.ts b/src/components/navigation/types.ts
new file mode 100644
index 00000000..da0d3ed6
--- /dev/null
+++ b/src/components/navigation/types.ts
@@ -0,0 +1,5 @@
+export interface NavigationTabItem {
+ id: string
+ label: string
+ icon?: string
+}
diff --git a/src/composables/useAnchorNavigation.ts b/src/composables/useAnchorNavigation.ts
new file mode 100644
index 00000000..99c7cebe
--- /dev/null
+++ b/src/composables/useAnchorNavigation.ts
@@ -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 | Ref,
+ 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(null)
+ const sectionRefs = ref>({})
+
+ 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,
+ }
+}
diff --git a/src/composables/useSubTabsScroll.ts b/src/composables/useSubTabsScroll.ts
deleted file mode 100644
index 8c07676b..00000000
--- a/src/composables/useSubTabsScroll.ts
+++ /dev/null
@@ -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 | Ref,
- 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(null)
-
- // Section 引用
- const sectionRefs = ref>({})
-
- /**
- * 设置 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,
- }
-}
diff --git a/src/pages/group-chat/components/QuotesTab.vue b/src/pages/group-chat/components/QuotesTab.vue
index 06d3c3dc..94869d02 100644
--- a/src/pages/group-chat/components/QuotesTab.vue
+++ b/src/pages/group-chat/components/QuotesTab.vue
@@ -1,7 +1,7 @@