vue-stack-tabs 1.3.1 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.en.md +119 -52
  2. package/README.md +103 -42
  3. package/README.ru.md +125 -54
  4. package/dist/{src/lib/StackTabs.vue.d.ts → StackTabs.vue.d.ts} +5 -5
  5. package/dist/{src/lib/components → components}/ContextMenu/index.vue.d.ts +1 -1
  6. package/dist/components/StackKeepAlive/emptyPlaceholder.d.ts +3 -0
  7. package/dist/{src/lib/components → components}/TabHeader/TabHeaderItem.vue.d.ts +9 -0
  8. package/dist/{src/lib/components → components}/TabHeader/index.vue.d.ts +3 -3
  9. package/dist/{src/lib/hooks → hooks}/stackTabsContext.d.ts +5 -2
  10. package/dist/hooks/tabPanel/pageComponentFactory.d.ts +23 -0
  11. package/dist/{src/lib/hooks → hooks}/tabPanel/refresh.d.ts +6 -3
  12. package/dist/{src/lib/hooks → hooks}/tabPanel/scroll.d.ts +2 -0
  13. package/dist/hooks/tabPanel/tabStore.d.ts +69 -0
  14. package/dist/hooks/useScrollShadow.d.ts +20 -0
  15. package/dist/{src/lib/hooks → hooks}/useTabActions.d.ts +7 -2
  16. package/dist/{src/lib/hooks → hooks}/useTabEventBus.d.ts +2 -1
  17. package/dist/{src/lib/hooks → hooks}/useTabPanel.d.ts +16 -4
  18. package/dist/iframe-bridge.mjs +1 -1
  19. package/dist/index.d.ts +22 -6
  20. package/dist/{src/lib/model → model}/TabModel.d.ts +21 -0
  21. package/dist/nuxt/module.mjs +10 -6
  22. package/dist/nuxt/runtime/plugin.mjs +1600 -4
  23. package/dist/style.css +1 -1
  24. package/dist/utils/iframeBridge.d.ts +7 -1
  25. package/dist/{src/lib/utils → utils}/scrollUtils.d.ts +7 -1
  26. package/dist/{src/lib/versionLogger.d.ts → versionLogger.d.ts} +1 -1
  27. package/dist/vue-stack-tabs.css +1 -1
  28. package/dist/vue-stack-tabs.es.js +1 -1
  29. package/package.json +64 -40
  30. package/src/lib/StackTabs.vue +16 -3
  31. package/src/lib/assets/style/common/var.scss +2 -0
  32. package/src/lib/assets/style/stackTab.scss +17 -3
  33. package/src/lib/components/ContextMenu/index.vue +12 -3
  34. package/src/lib/components/PageLoading.vue +10 -2
  35. package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +3 -9
  36. package/src/lib/components/StackKeepAlive/emptyPlaceholder.tsx +18 -0
  37. package/src/lib/components/TabHeader/index.vue +4 -3
  38. package/src/lib/hooks/stackTabsContext.ts +23 -18
  39. package/src/lib/hooks/tabPanel/pageComponentFactory.tsx +136 -0
  40. package/src/lib/hooks/tabPanel/refresh.ts +20 -13
  41. package/src/lib/hooks/tabPanel/scroll.ts +110 -7
  42. package/src/lib/hooks/tabPanel/session.ts +29 -3
  43. package/src/lib/hooks/tabPanel/tabStore.ts +595 -0
  44. package/src/lib/hooks/useContextMenu.ts +57 -11
  45. package/src/lib/hooks/useIframeManager.ts +18 -2
  46. package/src/lib/hooks/useScrollShadow.ts +124 -0
  47. package/src/lib/hooks/useTabActions.ts +18 -8
  48. package/src/lib/hooks/useTabEventBus.ts +5 -3
  49. package/src/lib/hooks/useTabPanel.tsx +183 -302
  50. package/src/lib/hooks/useTabRouter.ts +42 -38
  51. package/src/lib/i18n/index.ts +3 -3
  52. package/src/lib/model/TabModel.ts +40 -1
  53. package/src/lib/nuxt/module.ts +10 -3
  54. package/src/lib/nuxt/runtime/plugin.ts +13 -2
  55. package/src/lib/utils/iframeBridge.ts +9 -1
  56. package/src/lib/utils/scrollUtils.ts +35 -2
  57. package/src/lib/utils/tabInfoEncoder.ts +21 -8
  58. package/src/lib/utils/urlParser.ts +3 -1
  59. package/src/lib/versionLogger.ts +6 -2
  60. package/dist/favicon.svg +0 -10
  61. package/dist/iframe-bridge.html +0 -77
  62. package/dist/iframe-child.html +0 -88
  63. package/dist/nuxt/favicon.svg +0 -10
  64. package/dist/nuxt/iframe-bridge.html +0 -77
  65. package/dist/nuxt/iframe-child.html +0 -88
  66. package/dist/nuxt/plugin.mjs +0 -9
  67. package/dist/src/lib/iframe-bridge.d.ts +0 -8
  68. package/dist/src/lib/index.d.ts +0 -22
  69. package/dist/src/lib/nuxt/module.d.ts +0 -6
  70. package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +0 -2
  71. package/dist/src/lib/nuxt/runtime/plugin.d.ts +0 -2
  72. package/dist/src/lib/utils/iframeBridge.d.ts +0 -39
  73. package/src/lib/nuxt/runtime/plugin.client.ts +0 -10
  74. /package/dist/{src/lib/components → components}/ContextMenu/ContextMenuItem.vue.d.ts +0 -0
  75. /package/dist/{src/lib/components → components}/PageLoading.vue.d.ts +0 -0
  76. /package/dist/{src/lib/components → components}/StackKeepAlive/StackCacheRenderer.vue.d.ts +0 -0
  77. /package/dist/{src/lib/components → components}/StackKeepAlive/StackKeepAlive.vue.d.ts +0 -0
  78. /package/dist/{src/lib/components → components}/TabHeader/TabHeaderButton.vue.d.ts +0 -0
  79. /package/dist/{src/lib/components → components}/TabHeader/TabHeaderScroll.vue.d.ts +0 -0
  80. /package/dist/{src/lib/hooks → hooks}/tabPanel/evict.d.ts +0 -0
  81. /package/dist/{src/lib/hooks → hooks}/tabPanel/navigationTransaction.d.ts +0 -0
  82. /package/dist/{src/lib/hooks → hooks}/tabPanel/session.d.ts +0 -0
  83. /package/dist/{src/lib/hooks → hooks}/tabPanel/state.d.ts +0 -0
  84. /package/dist/{src/lib/hooks → hooks}/useContextMenu.d.ts +0 -0
  85. /package/dist/{src/lib/hooks → hooks}/useIframeManager.d.ts +0 -0
  86. /package/dist/{src/lib/hooks → hooks}/useTabLoading.d.ts +0 -0
  87. /package/dist/{src/lib/hooks → hooks}/useTabRouter.d.ts +0 -0
  88. /package/dist/{src/lib/i18n → i18n}/index.d.ts +0 -0
  89. /package/dist/{src/lib/i18n → i18n}/lang/en.d.ts +0 -0
  90. /package/dist/{src/lib/i18n → i18n}/lang/zh-CN.d.ts +0 -0
  91. /package/dist/{src/lib/i18n → i18n}/stackTabsLocale.d.ts +0 -0
  92. /package/dist/{src/lib/iframe.vue.d.ts → iframe.vue.d.ts} +0 -0
  93. /package/dist/{src/lib/utils → utils}/stackTabsA11y.d.ts +0 -0
  94. /package/dist/{src/lib/utils → utils}/stackTabsMessage.d.ts +0 -0
  95. /package/dist/{src/lib/utils → utils}/tabInfoEncoder.d.ts +0 -0
  96. /package/dist/{src/lib/utils → utils}/typeGuards.d.ts +0 -0
  97. /package/dist/{src/lib/utils → utils}/urlParser.d.ts +0 -0
@@ -4,7 +4,7 @@
4
4
  * 统一管理所有 iframe 标签的 src 计算、加载状态追踪、超时检测、
5
5
  * 刷新控制、DOM ref 收集,以及 postMessage 通信。
6
6
  */
7
- import { computed, onBeforeUnmount, reactive, watch, type Ref } from 'vue'
7
+ import { computed, nextTick, onBeforeUnmount, reactive, watch, type Ref } from 'vue'
8
8
  import type { ITabData, ITabItem } from '../model/TabModel'
9
9
  import { toSafeTabUrl } from '../utils/urlParser'
10
10
  import { getPostMessageTargetOrigin, isStackTabsOpenTabMessage } from '../utils/stackTabsMessage'
@@ -34,6 +34,9 @@ export function useIframeManager(options: UseIframeManagerOptions) {
34
34
  const iframeEverActivated = reactive<Record<string, boolean>>({})
35
35
 
36
36
  const appendIframeRefreshKey = (src: string, refreshKey: number): string => {
37
+ // 仅当 refreshKey > 0 时附加查询参数:
38
+ // - 初次激活(key=0)保留干净的 URL,便于用户阅读和书签
39
+ // - 后续 reload / 重试时附加,用于强制浏览器破坏 iframe 缓存
37
40
  if (refreshKey <= 0 || src === 'about:blank') return src
38
41
 
39
42
  const hashIndex = src.indexOf('#')
@@ -113,7 +116,11 @@ export function useIframeManager(options: UseIframeManagerOptions) {
113
116
  ...iframeRefreshKeys.value,
114
117
  [frameId]: (iframeRefreshKeys.value[frameId] ?? 0) + 1
115
118
  }
116
- setIframeLoading(frameId, { force: true })
119
+ // 等下一拍再设置 loading 状态,让 refreshKey watcher 先把新 src 反映到 DOM 上,
120
+ // 避免 loading 遮罩先于 src 变化显示,造成视觉抖动。
121
+ nextTick(() => {
122
+ setIframeLoading(frameId, { force: true })
123
+ })
117
124
  }
118
125
 
119
126
  const shouldShowIframeLoading = (frameId: string) =>
@@ -148,6 +155,15 @@ export function useIframeManager(options: UseIframeManagerOptions) {
148
155
  delete iframeElRefs[id]
149
156
  }
150
157
  }
158
+ // 同步清理已不再属于任何 iframe tab 的 refreshKey,避免长会话下 Map 持续膨胀。
159
+ if (activeIds.size < Object.keys(iframeRefreshKeys.value).length) {
160
+ const next: Record<string, number> = {}
161
+ for (const id of activeIds) {
162
+ const key = iframeRefreshKeys.value[id]
163
+ if (typeof key === 'number') next[id] = key
164
+ }
165
+ iframeRefreshKeys.value = next
166
+ }
151
167
  },
152
168
  { immediate: true }
153
169
  )
@@ -0,0 +1,124 @@
1
+ /**
2
+ * useScrollShadow - 为可滚动容器动态切换上下阴影类。
3
+ *
4
+ * 职责:在 scroll 事件触发时根据 scrollTop / scrollHeight 边界,
5
+ * 给目标元素动态加上/移除 stack-tab__shadow-inset-top / -bottom 类,
6
+ * 距离边界 > 阈值时显示阴影(提示用户"还能往这个方向滚"),
7
+ * 已贴边(距离边界 ≤ 阈值)时隐藏阴影,避免到顶/到底后阴影残留误导。
8
+ *
9
+ * 设计要点:
10
+ * - 使用 passive scroll 监听,不阻塞滚动
11
+ * - 使用 ResizeObserver 监视子页面缩放/尺寸变化,重新计算阴影边界
12
+ * - 使用 rAF 节流,避免一帧内多次写 class
13
+ * - 阈值(默认 5px)内视为已贴边(≤ 阈值)→ 隐藏阴影;超过阈值视为可继续滚动 → 显示阴影
14
+ * - SSR 安全:在 document 不存在时直接返回
15
+ * - 自动清理:onScopeDispose 移除监听并取消 rAF/disconnect observer
16
+ */
17
+ import { onScopeDispose, type Ref } from 'vue'
18
+
19
+ const SHADOW_TOP_CLASS = 'stack-tab__shadow-inset-top'
20
+ const SHADOW_BOTTOM_CLASS = 'stack-tab__shadow-inset-bottom'
21
+ const SHADOW_VERTICAL_CLASS = 'stack-tab__shadow-inset-vertical'
22
+ const DEFAULT_EDGE_THRESHOLD = 5
23
+
24
+ export interface UseScrollShadowOptions {
25
+ /** 容器元素的 ref,可以是 Ref<HTMLElement | null> 或 getter 函数 */
26
+ target: Ref<HTMLElement | null> | (() => HTMLElement | null)
27
+ /**
28
+ * 是否监听内容缩放/尺寸变化(默认 true)。
29
+ * 启用后会用 ResizeObserver 监视目标尺寸,容器或子树尺寸变化都会重新计算阴影边界。
30
+ */
31
+ watchResize?: boolean
32
+ /**
33
+ * 到顶/到底的判定阈值(px,默认 5)。
34
+ * 距离边界 ≤ 阈值视为已贴边(不显示阴影);距离边界 > 阈值视为可继续滚动(显示阴影)。
35
+ */
36
+ edgeThreshold?: number
37
+ }
38
+
39
+ export interface UseScrollShadowReturn {
40
+ /** 手动同步一次(用于内容变化/resize 后) */
41
+ sync: () => void
42
+ }
43
+
44
+ const resolveTarget = (
45
+ target: Ref<HTMLElement | null> | (() => HTMLElement | null)
46
+ ): HTMLElement | null => (typeof target === 'function' ? target() : target.value)
47
+
48
+ export const useScrollShadow = (options: UseScrollShadowOptions): UseScrollShadowReturn => {
49
+ if (typeof document === 'undefined') {
50
+ return { sync: () => undefined }
51
+ }
52
+
53
+ const watchResize = options.watchResize !== false
54
+ const edgeThreshold = Math.max(0, options.edgeThreshold ?? DEFAULT_EDGE_THRESHOLD)
55
+ let rafId: number | null = null
56
+ let attached: HTMLElement | null = null
57
+ let resizeObserver: ResizeObserver | null = null
58
+
59
+ const recompute = (): void => {
60
+ if (!attached) return
61
+ const target = attached
62
+ const { scrollTop, scrollHeight, clientHeight } = target
63
+ const distanceToTop = scrollTop
64
+ const distanceToBottom = scrollHeight - clientHeight - scrollTop
65
+ // 顶部阴影:表示上方还有内容,离开顶部(距离顶部 > 阈值)时显示。
66
+ // 底部阴影:表示下方还有内容,离开底部(距离底部 > 阈值)时显示。
67
+ const showTopShadow = distanceToTop > edgeThreshold
68
+ const showBottomShadow = distanceToBottom > edgeThreshold
69
+ // 注意:-top / -bottom 是两个独立 CSS 规则、作用在同一个 ::after 上,
70
+ // 同时加会互相覆盖(后定义的 -bottom 胜出)。因此这里互斥地只加一个类:
71
+ // 两侧都有内容用 -vertical,只有一侧用 -top / -bottom,贴边时都不加。
72
+ target.classList.toggle(SHADOW_VERTICAL_CLASS, showTopShadow && showBottomShadow)
73
+ target.classList.toggle(SHADOW_TOP_CLASS, showTopShadow && !showBottomShadow)
74
+ target.classList.toggle(SHADOW_BOTTOM_CLASS, !showTopShadow && showBottomShadow)
75
+ }
76
+
77
+ const schedule = (): void => {
78
+ if (rafId !== null) return
79
+ rafId = requestAnimationFrame(() => {
80
+ rafId = null
81
+ recompute()
82
+ })
83
+ }
84
+
85
+ const attach = (): void => {
86
+ const el = resolveTarget(options.target)
87
+ if (!el || el === attached) return
88
+ if (attached) {
89
+ attached.removeEventListener('scroll', schedule)
90
+ resizeObserver?.disconnect()
91
+ resizeObserver = null
92
+ }
93
+ attached = el
94
+ el.addEventListener('scroll', schedule, { passive: true })
95
+ if (watchResize && typeof ResizeObserver !== 'undefined') {
96
+ resizeObserver = new ResizeObserver(() => schedule())
97
+ resizeObserver.observe(el)
98
+ }
99
+ schedule()
100
+ }
101
+
102
+ const detach = (): void => {
103
+ if (attached) {
104
+ attached.removeEventListener('scroll', schedule)
105
+ attached.classList.remove(SHADOW_TOP_CLASS, SHADOW_BOTTOM_CLASS, SHADOW_VERTICAL_CLASS)
106
+ attached = null
107
+ }
108
+ resizeObserver?.disconnect()
109
+ resizeObserver = null
110
+ if (rafId !== null) {
111
+ cancelAnimationFrame(rafId)
112
+ rafId = null
113
+ }
114
+ }
115
+
116
+ // 初始挂载(target 可能是异步插入的 DOM,使用 microtask 等一帧)
117
+ queueMicrotask(attach)
118
+
119
+ onScopeDispose(detach)
120
+
121
+ return {
122
+ sync: () => schedule()
123
+ }
124
+ }
@@ -11,7 +11,7 @@ import {
11
11
  } from '../utils/urlParser'
12
12
  import type { ITabData, IframeRefreshMode } from '../model/TabModel'
13
13
  import { runNavigationTransaction } from './tabPanel/navigationTransaction'
14
- import { resolveStackTabsRuntimeContext } from './stackTabsContext'
14
+ import { resolveStackTabsRuntimeContext, type StackTabsRuntimeContext } from './stackTabsContext'
15
15
 
16
16
  /**
17
17
  * useTabActions - 标签操作对外 Hook
@@ -26,9 +26,9 @@ import { resolveStackTabsRuntimeContext } from './stackTabsContext'
26
26
  * - activeTab(id, isRoute?) 激活标签
27
27
  * - reset() 重置(关闭所有)
28
28
  */
29
- export default function useTabActions() {
29
+ export default function useTabActions(providedRuntimeContext?: StackTabsRuntimeContext) {
30
30
  const router = useRouter()
31
- const runtimeContext = resolveStackTabsRuntimeContext()
31
+ const runtimeContext = providedRuntimeContext ?? resolveStackTabsRuntimeContext()
32
32
  const {
33
33
  active,
34
34
  hasTab,
@@ -37,13 +37,14 @@ export default function useTabActions() {
37
37
  canAddTab,
38
38
  renewTab,
39
39
  getTab,
40
+ getTopPage,
40
41
  tabs,
41
42
  moveTab,
42
43
  removeTab,
43
44
  removeAllTabs,
44
45
  refreshTab,
45
46
  refreshAllTabs
46
- } = useTabPanel()
47
+ } = useTabPanel(runtimeContext)
47
48
 
48
49
  /** 合并默认值,生成完整 tabInfo */
49
50
  const prepareTabInfo = (tab: ITabData): ITabData => {
@@ -80,7 +81,7 @@ export default function useTabActions() {
80
81
  let refreshKeySnapshot: number | undefined
81
82
 
82
83
  if (tabInfo.id && renew && isExistingTab) {
83
- const currentTab = getTab(tab.id!)
84
+ const currentTab = getTab(tabInfo.id!)
84
85
  rollbackRenew = renewTab(tab)
85
86
  if (currentTab?.active) {
86
87
  refreshKeySnapshot = refreshKey.value
@@ -143,9 +144,18 @@ export default function useTabActions() {
143
144
  runtimeContext.iframePath.value = path
144
145
  }
145
146
 
146
- /** 获取当前页面的缓存容器 DOM(用于某些需要直接操作 DOM 的场景) */
147
- const getWrapper = () => {
148
- return document.getElementsByClassName('cache-page-wrapper')[0]
147
+ /**
148
+ * 获取缓存页面容器。
149
+ * 不传参数时保留旧行为并返回第一个容器;多标签场景应传 tabId 或 pageId,
150
+ * 避免因 DOM 排序而取得错误页面。
151
+ */
152
+ const getWrapper = (
153
+ target: { tabId?: string; pageId?: string } = {}
154
+ ): HTMLElement | undefined => {
155
+ if (typeof document === 'undefined') return undefined
156
+ const pageId = target.pageId ?? (target.tabId ? getTopPage(target.tabId)?.id : undefined)
157
+ if (pageId) return document.getElementById(`W-${pageId}`) ?? undefined
158
+ return document.querySelector<HTMLElement>('.cache-page-wrapper') ?? undefined
149
159
  }
150
160
 
151
161
  /** iframe 标签在新窗口打开(无法嵌入时降级) */
@@ -7,7 +7,7 @@
7
7
  import type { InjectionKey } from 'vue'
8
8
  import { hasInjectionContext, inject } from 'vue'
9
9
  import type { Emitter, EventType } from 'mitt'
10
- import { resolveStackTabsRuntimeContext } from './stackTabsContext'
10
+ import { resolveStackTabsRuntimeContext, type StackTabsRuntimeContext } from './stackTabsContext'
11
11
 
12
12
  declare module '@vue/runtime-core' {
13
13
  export interface ComponentCustomProperties {
@@ -35,7 +35,9 @@ export interface TabEventPayloadMap extends Record<EventType, unknown> {
35
35
  export const tabEmitterKey: InjectionKey<Emitter<TabEventPayloadMap>> = Symbol('tabEmitter')
36
36
 
37
37
  /** 获取标签事件总线,用于组件间通信 */
38
- export const useTabEmitter = (): Emitter<TabEventPayloadMap> => {
38
+ export const useTabEmitter = (
39
+ runtimeContext?: StackTabsRuntimeContext
40
+ ): Emitter<TabEventPayloadMap> => {
39
41
  const injected = hasInjectionContext() ? inject(tabEmitterKey, null) : null
40
- return injected ?? resolveStackTabsRuntimeContext().eventBus
42
+ return injected ?? runtimeContext?.eventBus ?? resolveStackTabsRuntimeContext().eventBus
41
43
  }