vue-stack-tabs 2.3.0 → 2.4.0

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.
@@ -8,6 +8,9 @@
8
8
  $stack-tab-color-white: #fff !default;
9
9
  $stack-tab-color-black: #000 !default;
10
10
 
11
+ // 主色上的文字/图标(深色主题下会反转为深色)
12
+ $stack-tab-color-accent-contrast: $stack-tab-color-white !default;
13
+
11
14
  // 主色
12
15
  $stack-tab-color-primary: #409eff !default;
13
16
  $stack-tab-color-primary-invert: #ffac30 !default;
@@ -42,6 +45,7 @@ $stack-tab-iframe-error-bg: rgb(255 255 255 / 92%) !default;
42
45
 
43
46
  $stack-tab-border-color: #f0f0f0 !default;
44
47
  $stack-tab-border: 1px solid $stack-tab-border-color !default;
48
+ $stack-tab-color-divider: #a9a9a9 !default;
45
49
  $stack-tab-tab-radius: 8px 8px 0 0 !default;
46
50
  $stack-tab-contextmenu-radius: 10px 0 !default;
47
51
 
@@ -88,13 +92,14 @@ $stack-tab-transition-slide:
88
92
  // ============================================================
89
93
 
90
94
  $stack-tab-shadow-color: rgb(0 0 0 / 60%) !default;
95
+ $stack-tab-shadow-soft-color: rgb(0 0 0 / 10%) !default;
91
96
  $stack-tab-shadow-left: -5px 0 5px -5px $stack-tab-shadow-color !default;
92
97
  $stack-tab-shadow-right: 5px 0 5px -5px $stack-tab-shadow-color !default;
93
98
  $stack-tab-shadow-right-inset: inset -5px 0 5px -5px $stack-tab-shadow-color !default;
94
99
  $stack-tab-shadow-left-inset: inset 5px 0 5px -5px $stack-tab-shadow-color !default;
95
100
  $stack-tab-shadow-top-inset: inset 0 5px 5px -5px $stack-tab-shadow-color !default;
96
101
  $stack-tab-shadow-bottom-inset: inset 0 -5px 5px -5px $stack-tab-shadow-color !default;
97
- $stack-tab-shadow-context-menu: 5px 5px 5px rgb(0 0 0 / 10%) !default;
102
+ $stack-tab-shadow-context-menu: 5px 5px 5px $stack-tab-shadow-soft-color !default;
98
103
 
99
104
  // ============================================================
100
105
  // 加载态
@@ -106,3 +111,101 @@ $stack-tab-loading-size: 50px !default;
106
111
  $stack-tab-loading-border-width: 8px !default;
107
112
  $stack-tab-loading-color: #007bff97 !default;
108
113
  $stack-tab-loading-duration: 1s !default;
114
+
115
+ // ============================================================
116
+ // 主题令牌(运行时 CSS 变量)
117
+ //
118
+ // 规则统一消费这一组变量;默认值定义在 theme.scss 的 :root 中。
119
+ // var() 的 fallback 使用上面的编译期变量,保证单独编译的 scoped
120
+ // 样式或未加载主题入口时仍有正确颜色,同时保留 SCSS 变量定制能力。
121
+ // ============================================================
122
+
123
+ // accent
124
+ $stack-tab-accent: var(--stack-tab-accent, #{$stack-tab-color-primary}) !default;
125
+ $stack-tab-accent-active: var(
126
+ --stack-tab-accent-active,
127
+ #{$stack-tab-color-primary-activate}
128
+ ) !default;
129
+ $stack-tab-accent-contrast: var(
130
+ --stack-tab-accent-contrast,
131
+ #{$stack-tab-color-accent-contrast}
132
+ ) !default;
133
+ $stack-tab-accent-shadow: var(
134
+ --stack-tab-accent-shadow,
135
+ #{rgba($stack-tab-color-primary, 0.8)}
136
+ ) !default;
137
+
138
+ // surface
139
+ $stack-tab-surface: var(--stack-tab-surface, #{$stack-tab-background-color-base}) !default;
140
+ $stack-tab-surface-muted: var(
141
+ --stack-tab-surface-muted,
142
+ #{$stack-tab-background-color-inactive}
143
+ ) !default;
144
+ $stack-tab-overlay: var(--stack-tab-overlay, #{$stack-tab-loading-mask-color}) !default;
145
+
146
+ // text / icon
147
+ $stack-tab-text: var(--stack-tab-text, #{$stack-tab-text-color}) !default;
148
+ $stack-tab-text-strong: var(--stack-tab-text-strong, #{$stack-tab-color-black}) !default;
149
+ $stack-tab-text-disabled: var(--stack-tab-text-disabled, #{$stack-tab-color-disabled}) !default;
150
+ $stack-tab-icon-mask: var(--stack-tab-mask, #{$stack-tab-text-color}) !default;
151
+ $stack-tab-font-size-token: var(--stack-tab-font-size, #{$stack-tab-font-size}) !default;
152
+
153
+ // feedback
154
+ $stack-tab-scrollbar: var(--stack-tab-scrollbar, #{$stack-tab-color-scrollbar}) !default;
155
+
156
+ // border
157
+ $stack-tab-border-color-token: var(--stack-tab-border, #{$stack-tab-border-color}) !default;
158
+ $stack-tab-border-token: 1px solid $stack-tab-border-color-token !default;
159
+ $stack-tab-divider: var(--stack-tab-divider, #{$stack-tab-color-divider}) !default;
160
+
161
+ // shadow:颜色层
162
+ $stack-tab-shadow-color-token: var(--stack-tab-shadow-color, #{$stack-tab-shadow-color}) !default;
163
+
164
+ // shadow:整体层
165
+ $stack-tab-shadow-tab: var(
166
+ --stack-tab-shadow-tab,
167
+ #{$stack-tab-shadow-left},
168
+ #{$stack-tab-shadow-right}
169
+ ) !default;
170
+ $stack-tab-shadow-tab-left: var(--stack-tab-shadow-tab-left, #{$stack-tab-shadow-left}) !default;
171
+ $stack-tab-shadow-tab-right: var(--stack-tab-shadow-tab-right, #{$stack-tab-shadow-right}) !default;
172
+ $stack-tab-shadow-menu: var(
173
+ --stack-tab-shadow-menu,
174
+ 5px 5px 5px #{$stack-tab-shadow-soft-color}
175
+ ) !default;
176
+ $stack-tab-shadow-drop: var(--stack-tab-shadow-drop, 0 0 6px #{$stack-tab-accent-shadow}) !default;
177
+
178
+ // shadow:inset 工具类
179
+ $stack-tab-shadow-inset-left: var(
180
+ --stack-tab-shadow-inset-left,
181
+ #{$stack-tab-shadow-left-inset}
182
+ ) !default;
183
+ $stack-tab-shadow-inset-right: var(
184
+ --stack-tab-shadow-inset-right,
185
+ #{$stack-tab-shadow-right-inset}
186
+ ) !default;
187
+ $stack-tab-shadow-inset-top: var(
188
+ --stack-tab-shadow-inset-top,
189
+ #{$stack-tab-shadow-top-inset}
190
+ ) !default;
191
+ $stack-tab-shadow-inset-bottom: var(
192
+ --stack-tab-shadow-inset-bottom,
193
+ #{$stack-tab-shadow-bottom-inset}
194
+ ) !default;
195
+ $stack-tab-shadow-inset-all: var(
196
+ --stack-tab-shadow-inset-all,
197
+ #{$stack-tab-shadow-left-inset},
198
+ #{$stack-tab-shadow-right-inset}
199
+ ) !default;
200
+ $stack-tab-shadow-inset-vertical: var(
201
+ --stack-tab-shadow-inset-vertical,
202
+ #{$stack-tab-shadow-top-inset},
203
+ #{$stack-tab-shadow-bottom-inset}
204
+ ) !default;
205
+
206
+ // badge
207
+ $stack-tab-badge-default: var(--stack-tab-badge-default, #{$stack-tab-color-danger}) !default;
208
+ $stack-tab-badge-primary: var(--stack-tab-badge-primary, #{$stack-tab-color-primary}) !default;
209
+
210
+ // loading
211
+ $stack-tab-loading-accent: var(--stack-tab-loading-accent, #{$stack-tab-loading-color}) !default;
@@ -1,3 +1,4 @@
1
+ @use 'common/theme.scss';
1
2
  @use 'stack-tab.scss';
2
3
  @use 'icon.scss';
3
4
  @use 'transition.scss';
@@ -6,7 +6,7 @@
6
6
  &__mask {
7
7
  @include absolute-fill;
8
8
  @include flex-center;
9
- background-color: $stack-tab-loading-mask-color;
9
+ background-color: $stack-tab-overlay;
10
10
  opacity: $stack-tab-loading-mask-opacity;
11
11
  }
12
12
 
@@ -16,7 +16,7 @@
16
16
  width: $stack-tab-loading-size;
17
17
  aspect-ratio: 1;
18
18
  border: $stack-tab-loading-border-width solid transparent;
19
- border-right-color: $stack-tab-loading-color;
19
+ border-right-color: $stack-tab-loading-accent;
20
20
  border-radius: 50%;
21
21
  animation: stack-tab-loading-rotate $stack-tab-loading-duration infinite linear;
22
22
 
@@ -26,7 +26,7 @@
26
26
  position: absolute;
27
27
  inset: -$stack-tab-loading-border-width;
28
28
  border: $stack-tab-loading-border-width solid transparent;
29
- border-right-color: $stack-tab-loading-color;
29
+ border-right-color: $stack-tab-loading-accent;
30
30
  border-radius: 50%;
31
31
  animation: stack-tab-loading-rotate $stack-tab-loading-duration * 2 infinite linear;
32
32
  }
@@ -5,7 +5,7 @@
5
5
  .stack-tab {
6
6
  display: flex;
7
7
  min-height: 300px;
8
- background-color: $stack-tab-background-color-base;
8
+ background-color: $stack-tab-surface;
9
9
  flex-direction: column;
10
10
  gap: 5px;
11
11
 
@@ -41,7 +41,7 @@
41
41
 
42
42
  .stack-tab__header-button[disabled] & {
43
43
  cursor: not-allowed;
44
- background-color: $stack-tab-color-disabled;
44
+ background-color: $stack-tab-text-disabled;
45
45
  transition: none;
46
46
  }
47
47
  }
@@ -57,7 +57,7 @@
57
57
  display: flex;
58
58
  width: 100%;
59
59
  height: $stack-tab-head-height;
60
- border-bottom: 1px solid $stack-tab-border-color;
60
+ border-bottom: 1px solid $stack-tab-border-color-token;
61
61
  box-sizing: border-box;
62
62
 
63
63
  // tab header 上面的按钮
@@ -66,7 +66,7 @@
66
66
  height: $stack-tab-head-height;
67
67
  width: fit-content;
68
68
  overflow: hidden;
69
- background-color: $stack-tab-background-color-base;
69
+ background-color: $stack-tab-surface;
70
70
  cursor: pointer;
71
71
  display: flex;
72
72
  align-items: center;
@@ -76,7 +76,7 @@
76
76
 
77
77
  // 按钮禁用样式
78
78
  &[disabled] {
79
- background-color: $stack-tab-background-color-inactive;
79
+ background-color: $stack-tab-surface-muted;
80
80
  box-shadow: none;
81
81
  cursor: not-allowed;
82
82
  }
@@ -113,7 +113,7 @@
113
113
  bottom: 0;
114
114
  left: 0;
115
115
  height: $stack-tab-scroll-height;
116
- background-color: $stack-tab-color-scrollbar;
116
+ background-color: $stack-tab-scrollbar;
117
117
  border-radius: $stack-tab-scroll-border-radius;
118
118
  opacity: 0;
119
119
  transition: $stack-tab-transition-fast;
@@ -129,7 +129,7 @@
129
129
  top: 0;
130
130
  left: 0;
131
131
  height: 100%;
132
- background-color: $stack-tab-color-scrollbar;
132
+ background-color: $stack-tab-scrollbar;
133
133
  border-radius: $stack-tab-scroll-border-radius;
134
134
  transition: background-color $stack-tab-transition-duration ease-in-out;
135
135
  // 拖拽由 Pointer Events 接管,避免触摸时被浏览器手势打断
@@ -145,7 +145,7 @@
145
145
 
146
146
  .stack-tab__scrollbar--dragging &,
147
147
  &:hover {
148
- background-color: $stack-tab-color-primary !important;
148
+ background-color: $stack-tab-accent !important;
149
149
  }
150
150
  }
151
151
 
@@ -166,11 +166,11 @@
166
166
  position: relative;
167
167
  display: flex;
168
168
  padding: 0 $stack-tab-padding;
169
- font-size: $stack-tab-font-size;
170
- color: $stack-tab-text-color;
169
+ font-size: $stack-tab-font-size-token;
170
+ color: $stack-tab-text;
171
171
  cursor: pointer;
172
- background-color: $stack-tab-background-color-inactive;
173
- border: $stack-tab-border;
172
+ background-color: $stack-tab-surface-muted;
173
+ border: $stack-tab-border-token;
174
174
  border-left: none;
175
175
  border-radius: $stack-tab-tab-radius;
176
176
  transition: $stack-tab-transition-fast;
@@ -182,7 +182,7 @@
182
182
  margin-top: 5px;
183
183
 
184
184
  &:first-child {
185
- border-left: $stack-tab-border;
185
+ border-left: $stack-tab-border-token;
186
186
  }
187
187
 
188
188
  &--dragging {
@@ -197,9 +197,9 @@
197
197
  bottom: 2px;
198
198
  width: 5px;
199
199
  content: '';
200
- background: $stack-tab-color-primary;
200
+ background: $stack-tab-accent;
201
201
  border-radius: 2px;
202
- box-shadow: 0 0 6px rgba($stack-tab-color-primary, 0.8);
202
+ box-shadow: $stack-tab-shadow-drop;
203
203
  }
204
204
 
205
205
  &--drop-before::before {
@@ -212,21 +212,21 @@
212
212
 
213
213
  // 激活右键菜单的页签
214
214
  &--context-menu {
215
- color: $stack-tab-color-black;
215
+ color: $stack-tab-text-strong;
216
216
  }
217
217
 
218
218
  // 激活的页签
219
219
  &--active {
220
220
  z-index: 1;
221
- background-color: $stack-tab-background-color-base;
222
- border-bottom-color: $stack-tab-background-color-base;
223
- box-shadow: $stack-tab-shadow-left, $stack-tab-shadow-right;
221
+ background-color: $stack-tab-surface;
222
+ border-bottom-color: $stack-tab-surface;
223
+ box-shadow: $stack-tab-shadow-tab;
224
224
  margin-top: unset;
225
225
  }
226
226
 
227
227
  &:hover,
228
228
  &--active {
229
- color: $stack-tab-color-primary;
229
+ color: $stack-tab-accent;
230
230
  }
231
231
 
232
232
  // 可关闭标签在悬停/激活时给关闭按钮腾出空间
@@ -300,19 +300,19 @@
300
300
  padding: 0.3rem;
301
301
  font-size: 0.5rem;
302
302
  line-height: 0.4rem;
303
- color: $stack-tab-color-white;
303
+ color: $stack-tab-accent-contrast;
304
304
  text-align: center;
305
- border: 1px solid $stack-tab-background-color-base;
305
+ border: 1px solid $stack-tab-surface;
306
306
  border-radius: 0.5rem;
307
307
  transform: translateX(100%) translateY(-50%);
308
308
  box-sizing: border-box;
309
309
 
310
310
  &--default {
311
- background: $stack-tab-color-danger;
311
+ background: $stack-tab-badge-default;
312
312
  }
313
313
 
314
314
  &--primary {
315
- background: $stack-tab-color-primary;
315
+ background: $stack-tab-badge-primary;
316
316
  }
317
317
  }
318
318
  }
@@ -361,11 +361,11 @@
361
361
 
362
362
  &-retry,
363
363
  &-open {
364
- border: $stack-tab-border;
364
+ border: $stack-tab-border-token;
365
365
  padding: 4px 10px;
366
- color: $stack-tab-text-color;
366
+ color: $stack-tab-text;
367
367
  cursor: pointer;
368
- background: $stack-tab-background-color-base;
368
+ background: $stack-tab-surface;
369
369
  border-radius: 6px;
370
370
  font: inherit;
371
371
  }
@@ -377,11 +377,11 @@
377
377
  z-index: 999;
378
378
  min-width: 120px;
379
379
  padding: 8px;
380
- font-size: $stack-tab-font-size;
381
- background: $stack-tab-background-color-base;
382
- border: $stack-tab-border;
380
+ font-size: $stack-tab-font-size-token;
381
+ background: $stack-tab-surface;
382
+ border: $stack-tab-border-token;
383
383
  border-radius: $stack-tab-contextmenu-radius;
384
- box-shadow: $stack-tab-shadow-context-menu;
384
+ box-shadow: $stack-tab-shadow-menu;
385
385
  transform-origin: left top;
386
386
 
387
387
  &-item {
@@ -396,7 +396,7 @@
396
396
  padding: 0 10px;
397
397
  line-height: 30px;
398
398
  box-sizing: border-box;
399
- color: $stack-tab-text-color;
399
+ color: $stack-tab-text;
400
400
  cursor: pointer;
401
401
  background: transparent;
402
402
  border-radius: $stack-tab-contextmenu-radius;
@@ -409,11 +409,11 @@
409
409
  &:hover,
410
410
  &:active,
411
411
  &:focus-visible {
412
- color: $stack-tab-color-white;
413
- background-color: $stack-tab-color-primary;
412
+ color: $stack-tab-accent-contrast;
413
+ background-color: $stack-tab-accent;
414
414
 
415
415
  .stack-tab__icon--mask {
416
- background-color: $stack-tab-color-white;
416
+ background-color: $stack-tab-accent-contrast;
417
417
  }
418
418
  }
419
419
 
@@ -422,62 +422,62 @@
422
422
  }
423
423
 
424
424
  &[disabled] {
425
- color: $stack-tab-color-disabled;
425
+ color: $stack-tab-text-disabled;
426
426
  pointer-events: none;
427
427
  cursor: default;
428
428
  background: none;
429
429
 
430
430
  .stack-tab__icon--mask {
431
- background-color: $stack-tab-color-disabled;
431
+ background-color: $stack-tab-text-disabled;
432
432
  }
433
433
  }
434
434
  }
435
435
 
436
436
  &-icon {
437
- width: $stack-tab-font-size;
438
- height: $stack-tab-font-size;
437
+ width: $stack-tab-font-size-token;
438
+ height: $stack-tab-font-size-token;
439
439
  transition: background-color 0.2s ease-in-out;
440
440
 
441
441
  &.stack-tab__icon--mask {
442
- width: $stack-tab-font-size;
443
- height: $stack-tab-font-size;
444
- background-color: $stack-tab-text-color;
442
+ width: $stack-tab-font-size-token;
443
+ height: $stack-tab-font-size-token;
444
+ background-color: $stack-tab-icon-mask;
445
445
  }
446
446
  }
447
447
  }
448
448
  }
449
449
 
450
450
  .stack-tab__shadow-left {
451
- box-shadow: $stack-tab-shadow-left;
451
+ box-shadow: $stack-tab-shadow-tab-left;
452
452
  }
453
453
 
454
454
  .stack-tab__shadow-right {
455
- box-shadow: $stack-tab-shadow-right;
455
+ box-shadow: $stack-tab-shadow-tab-right;
456
456
  }
457
457
 
458
458
  // 内阴影
459
459
  .stack-tab__shadow-inset {
460
460
  &-left {
461
- @include inset-shadow($stack-tab-shadow-left-inset);
461
+ @include inset-shadow($stack-tab-shadow-inset-left);
462
462
  }
463
463
 
464
464
  &-right {
465
- @include inset-shadow($stack-tab-shadow-right-inset);
465
+ @include inset-shadow($stack-tab-shadow-inset-right);
466
466
  }
467
467
 
468
468
  &-all {
469
- @include inset-shadow($stack-tab-shadow-left-inset, $stack-tab-shadow-right-inset);
469
+ @include inset-shadow($stack-tab-shadow-inset-all);
470
470
  }
471
471
 
472
472
  &-top {
473
- @include inset-shadow($stack-tab-shadow-top-inset);
473
+ @include inset-shadow($stack-tab-shadow-inset-top);
474
474
  }
475
475
 
476
476
  &-bottom {
477
- @include inset-shadow($stack-tab-shadow-bottom-inset);
477
+ @include inset-shadow($stack-tab-shadow-inset-bottom);
478
478
  }
479
479
 
480
480
  &-vertical {
481
- @include inset-shadow($stack-tab-shadow-top-inset, $stack-tab-shadow-bottom-inset);
481
+ @include inset-shadow($stack-tab-shadow-inset-vertical);
482
482
  }
483
483
  }
@@ -218,6 +218,8 @@ onMounted(() => {
218
218
  </script>
219
219
 
220
220
  <style lang="scss" scoped>
221
+ @use '../../assets/style/common/var' as *;
222
+
221
223
  .divider {
222
224
  position: relative;
223
225
  height: 1px;
@@ -231,7 +233,7 @@ onMounted(() => {
231
233
  left: 0;
232
234
  width: 100%;
233
235
  height: 1px;
234
- background-image: linear-gradient(to right, transparent, darkgrey, transparent);
236
+ background-image: linear-gradient(to right, transparent, $stack-tab-divider, transparent);
235
237
  content: '';
236
238
  }
237
239
  </style>
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 页面级运行时上下文。
3
+ *
4
+ * 每个缓存页面(cacheName / pId)都归属于一个标签(tabId / tId)。
5
+ * 这组信息由页面缓存包装组件通过 provide 提供,页面内的 useTabRouter、
6
+ * useTabLoading 通过 inject 读取,不再作为 attrs 注入到用户页面组件。
7
+ *
8
+ * 这样做的原因:
9
+ * - 多根节点(Fragment)页面无法继承 attrs,Vue 会在开发环境提示
10
+ * “Extraneous non-props attributes”;
11
+ * - 单根节点页面会把 tId / pId 透传到根 DOM 上,造成内部 ID 泄漏;
12
+ * - provide/inject 的边界与缓存包装组件一致,也允许页面内的嵌套组件复用。
13
+ */
14
+ import type { InjectionKey } from 'vue'
15
+ import { getCurrentInstance, hasInjectionContext, inject } from 'vue'
16
+
17
+ export interface TabPageContext {
18
+ /** 当前页面所属的标签 id(旧实现中的 attrs.tId) */
19
+ tabId: string
20
+ /** 当前页面的缓存 id,即 cacheName(旧实现中的 attrs.pId) */
21
+ pageId: string
22
+ }
23
+
24
+ export const tabPageContextKey: InjectionKey<TabPageContext> = Symbol('tabPageContext')
25
+
26
+ /**
27
+ * 解析当前页面上下文。
28
+ *
29
+ * 优先读取缓存包装组件 provide 的上下文;attrs / props 仅作为旧版
30
+ * cloneVNode 注入方式以及手工挂载场景的兼容回退。
31
+ *
32
+ * 返回值允许只包含其中一个字段:useTabLoading 只依赖 tabId,
33
+ * 旧代码或手工挂载可能只传了其中一个 id。
34
+ */
35
+ export const resolveTabPageContext = (): Partial<TabPageContext> | null => {
36
+ if (hasInjectionContext()) {
37
+ const injected = inject(tabPageContextKey, null)
38
+ if (injected) return injected
39
+ }
40
+
41
+ const instance = getCurrentInstance()
42
+ if (!instance) return null
43
+
44
+ const tabId = instance.attrs.tId ?? instance.props.tId
45
+ const pageId = instance.attrs.pId ?? instance.props.pId
46
+ const context: Partial<TabPageContext> = {}
47
+ if (typeof tabId === 'string') context.tabId = tabId
48
+ if (typeof pageId === 'string') context.pageId = pageId
49
+ return context.tabId !== undefined || context.pageId !== undefined ? context : null
50
+ }
@@ -13,6 +13,7 @@ import {
13
13
  onDeactivated,
14
14
  onMounted,
15
15
  onUnmounted,
16
+ provide,
16
17
  ref,
17
18
  shallowRef,
18
19
  watch
@@ -21,6 +22,7 @@ import type { DefineComponent, PropType, VNode } from 'vue'
21
22
  import type { ITabBase } from '../../model/TabModel'
22
23
  import PageLoading from '../../components/PageLoading.vue'
23
24
  import type { StackTabsRuntimeContext } from '../stackTabsContext'
25
+ import { tabPageContextKey } from '../tabPageContext'
24
26
  import { useScrollShadow } from '../useScrollShadow'
25
27
 
26
28
  interface PageComponentFactoryOptions {
@@ -139,6 +141,14 @@ export const createPageComponentFactory = (options: PageComponentFactoryOptions)
139
141
  },
140
142
  emits: ['onLoaded'],
141
143
  setup(props, context) {
144
+ // 页面上下文只在缓存包装组件这一层 provide,页面组件及其嵌套组件都可 inject。
145
+ // 不再把 tId / pId 作为 attrs 注入页面:多根节点页面会触发 Vue 的
146
+ // “Extraneous non-props attributes”警告,单根节点页面则会把内部 ID 透传到 DOM。
147
+ provide(tabPageContextKey, {
148
+ tabId: tabInfo.id!,
149
+ pageId: cacheName
150
+ })
151
+
142
152
  const localBackParams = ref<Record<string, unknown> | null>(null)
143
153
  const lastVnode = shallowRef<VNode | null | undefined>(null)
144
154
  const lastCloned = shallowRef<VNode | null>(null)
@@ -337,12 +347,10 @@ export const createPageComponentFactory = (options: PageComponentFactoryOptions)
337
347
  const vnode = props.vnode
338
348
  const backParams = localBackParams.value
339
349
  if (vnode !== lastVnode.value || backParams !== lastBackParams.value) {
350
+ // _back 是页面需要读取的公开 prop,仍通过 cloneVNode 注入;
351
+ // tId / pId 已改为 provide/inject,不再污染页面 attrs。
340
352
  lastCloned.value = vnode
341
- ? cloneVNode(vnode, {
342
- tId: tabInfo.id,
343
- pId: cacheName,
344
- ...(backParams ? { _back: backParams } : {})
345
- })
353
+ ? cloneVNode(vnode, backParams ? { _back: backParams } : null)
346
354
  : null
347
355
  lastVnode.value = vnode
348
356
  lastBackParams.value = backParams
@@ -1,16 +1,18 @@
1
- import { type ComponentInternalInstance, getCurrentInstance, onUnmounted } from 'vue'
1
+ import { onUnmounted } from 'vue'
2
2
  import { TabEventType, useTabEmitter } from '../hooks/useTabEventBus'
3
+ import { resolveTabPageContext } from './tabPageContext'
3
4
 
4
5
  /**
5
6
  * useTabLoading - 标签内页面加载状态 Hook
6
7
  *
7
8
  * 职责:通过 PAGE_LOADING 事件控制 PageLoading 组件的显示/隐藏
8
- * 使用范围:标签内的页面组件(需有 tId 通过 attrs 传入)
9
+ * 使用范围:标签内的页面组件;tabId 由缓存包装组件 provide,
10
+ * 页面通过 tabPageContext 解析(兼容旧版 attrs / props 注入)。
9
11
  */
10
12
  export default () => {
11
13
  const emitter = useTabEmitter()
12
- const { attrs } = getCurrentInstance() as ComponentInternalInstance
13
- const getTabId = () => String(attrs.tId ?? '')
14
+ const pageContext = resolveTabPageContext()
15
+ const getTabId = () => pageContext?.tabId ?? ''
14
16
  onUnmounted(() => {
15
17
  closeTabLoading()
16
18
  })
@@ -1,9 +1,8 @@
1
- import { getCurrentInstance } from 'vue'
2
- import type { ComponentInternalInstance } from 'vue'
3
1
  import type { LocationQueryRaw, RouteLocationPathRaw } from 'vue-router'
4
2
  import { isNavigationFailure, useRoute, useRouter } from 'vue-router'
5
3
 
6
4
  import useTabPanel from './useTabPanel'
5
+ import { resolveTabPageContext } from './tabPageContext'
7
6
  import {
8
7
  parseUrl,
9
8
  cloneLocationQuery,
@@ -22,30 +21,26 @@ import { createPageId } from '../utils/tabInfoEncoder'
22
21
  * useTabRouter - 标签内路由 Hook
23
22
  *
24
23
  * 职责:在单个标签内实现「前进/后退」栈式导航,以及滚动位置的记录与恢复
25
- * 使用范围:仅在标签内的页面组件中使用,依赖 StackTabs 通过 cloneVNode 注入的 tId / pId
24
+ * 使用范围:仅在标签内的页面组件中使用;页面上下文由缓存包装组件 provide,
25
+ * 通过 tabPageContext 解析(兼容旧版 attrs / props 注入)。
26
26
  *
27
27
  * 主要 API:
28
28
  * - forward(to) 前进:push 新页面到该标签的 pages 栈
29
29
  * - backward(to, backQuery) 后退:pop 栈顶、驱逐缓存,跳转到新栈顶
30
30
  * - addScrollTarget(...) 注册需要记录滚动位置的选择器
31
+ * - tabId / pageId 当前页面所属标签与缓存页 id(只读)
31
32
  */
32
33
  export default function useTabRouter() {
33
- const instance = getCurrentInstance() as ComponentInternalInstance
34
- const { attrs, props } = instance
34
+ const pageContext = resolveTabPageContext()
35
35
 
36
- /**
37
- * 当前标签 ID。
38
- * StackTabs 在 StackTabs.vue 的 tabWrapper 里通过 cloneVNode(Node, { tId, pId }) 注入,
39
- * 未在子组件 setup 中声明为 prop 的属性会落入 attrs,
40
- * 因此优先读 attrs.tId,再 fallback 到 props.tId。
41
- */
42
- const tabId = (attrs.tId ?? props.tId) as string
36
+ /** 当前标签 ID,用于定位页面栈与标签激活状态。 */
37
+ const tabId = pageContext?.tabId ?? ''
43
38
 
44
39
  /**
45
40
  * 当前页面的缓存 ID(即 cacheName / keep-alive include 中的名称)。
46
41
  * 用于 addScrollTarget 向 useTabPanel 绑定该页面的可滚动容器选择器。
47
42
  */
48
- const pageId = (attrs.pId ?? props.pId) as string
43
+ const pageId = pageContext?.pageId ?? ''
49
44
 
50
45
  const route = useRoute()
51
46
  const router = useRouter()
@@ -249,6 +244,8 @@ export default function useTabRouter() {
249
244
  return {
250
245
  forward,
251
246
  backward,
252
- addScrollTarget
247
+ addScrollTarget,
248
+ tabId,
249
+ pageId
253
250
  }
254
251
  }