vue-stack-tabs 1.3.1 → 1.4.1
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.
- package/README.en.md +119 -52
- package/README.md +103 -42
- package/README.ru.md +125 -54
- package/dist/{src/lib/StackTabs.vue.d.ts → StackTabs.vue.d.ts} +5 -5
- package/dist/{src/lib/components → components}/ContextMenu/index.vue.d.ts +1 -1
- package/dist/components/StackKeepAlive/emptyPlaceholder.d.ts +3 -0
- package/dist/{src/lib/components → components}/TabHeader/TabHeaderItem.vue.d.ts +9 -0
- package/dist/{src/lib/components → components}/TabHeader/index.vue.d.ts +3 -3
- package/dist/{src/lib/hooks → hooks}/stackTabsContext.d.ts +5 -2
- package/dist/hooks/tabPanel/pageComponentFactory.d.ts +20 -0
- package/dist/{src/lib/hooks → hooks}/tabPanel/scroll.d.ts +2 -0
- package/dist/hooks/useScrollShadow.d.ts +20 -0
- package/dist/{src/lib/hooks → hooks}/useTabActions.d.ts +7 -2
- package/dist/{src/lib/hooks → hooks}/useTabEventBus.d.ts +2 -1
- package/dist/{src/lib/hooks → hooks}/useTabPanel.d.ts +3 -1
- package/dist/iframe-bridge.mjs +1 -1
- package/dist/index.d.ts +22 -6
- package/dist/{src/lib/model → model}/TabModel.d.ts +21 -0
- package/dist/nuxt/module.mjs +10 -6
- package/dist/nuxt/runtime/plugin.mjs +1600 -4
- package/dist/style.css +1 -1
- package/dist/utils/iframeBridge.d.ts +7 -1
- package/dist/{src/lib/utils → utils}/scrollUtils.d.ts +7 -1
- package/dist/{src/lib/versionLogger.d.ts → versionLogger.d.ts} +1 -1
- package/dist/vue-stack-tabs.css +1 -1
- package/dist/vue-stack-tabs.es.js +1 -1
- package/package.json +64 -40
- package/src/lib/StackTabs.vue +16 -3
- package/src/lib/assets/style/common/var.scss +2 -0
- package/src/lib/assets/style/stackTab.scss +17 -3
- package/src/lib/components/ContextMenu/index.vue +12 -3
- package/src/lib/components/PageLoading.vue +10 -2
- package/src/lib/components/StackKeepAlive/StackCacheRenderer.vue +3 -9
- package/src/lib/components/StackKeepAlive/emptyPlaceholder.tsx +18 -0
- package/src/lib/components/TabHeader/index.vue +4 -3
- package/src/lib/hooks/stackTabsContext.ts +23 -18
- package/src/lib/hooks/tabPanel/pageComponentFactory.tsx +142 -0
- package/src/lib/hooks/tabPanel/refresh.ts +3 -0
- package/src/lib/hooks/tabPanel/scroll.ts +110 -7
- package/src/lib/hooks/tabPanel/session.ts +29 -3
- package/src/lib/hooks/useContextMenu.ts +57 -11
- package/src/lib/hooks/useIframeManager.ts +18 -2
- package/src/lib/hooks/useScrollShadow.ts +124 -0
- package/src/lib/hooks/useTabActions.ts +18 -8
- package/src/lib/hooks/useTabEventBus.ts +5 -3
- package/src/lib/hooks/useTabPanel.tsx +108 -180
- package/src/lib/hooks/useTabRouter.ts +11 -2
- package/src/lib/i18n/index.ts +3 -3
- package/src/lib/model/TabModel.ts +40 -1
- package/src/lib/nuxt/module.ts +10 -3
- package/src/lib/nuxt/runtime/plugin.ts +13 -2
- package/src/lib/utils/iframeBridge.ts +9 -1
- package/src/lib/utils/scrollUtils.ts +35 -2
- package/src/lib/utils/tabInfoEncoder.ts +21 -8
- package/src/lib/utils/urlParser.ts +3 -1
- package/src/lib/versionLogger.ts +6 -2
- package/dist/favicon.svg +0 -10
- package/dist/iframe-bridge.html +0 -77
- package/dist/iframe-child.html +0 -88
- package/dist/nuxt/favicon.svg +0 -10
- package/dist/nuxt/iframe-bridge.html +0 -77
- package/dist/nuxt/iframe-child.html +0 -88
- package/dist/nuxt/plugin.mjs +0 -9
- package/dist/src/lib/iframe-bridge.d.ts +0 -8
- package/dist/src/lib/index.d.ts +0 -22
- package/dist/src/lib/nuxt/module.d.ts +0 -6
- package/dist/src/lib/nuxt/runtime/plugin.client.d.ts +0 -2
- package/dist/src/lib/nuxt/runtime/plugin.d.ts +0 -2
- package/dist/src/lib/utils/iframeBridge.d.ts +0 -39
- package/src/lib/nuxt/runtime/plugin.client.ts +0 -10
- /package/dist/{src/lib/components → components}/ContextMenu/ContextMenuItem.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/PageLoading.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/StackKeepAlive/StackCacheRenderer.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/StackKeepAlive/StackKeepAlive.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/TabHeader/TabHeaderButton.vue.d.ts +0 -0
- /package/dist/{src/lib/components → components}/TabHeader/TabHeaderScroll.vue.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/evict.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/navigationTransaction.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/refresh.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/session.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/tabPanel/state.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useContextMenu.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useIframeManager.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useTabLoading.d.ts +0 -0
- /package/dist/{src/lib/hooks → hooks}/useTabRouter.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/index.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/lang/en.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/lang/zh-CN.d.ts +0 -0
- /package/dist/{src/lib/i18n → i18n}/stackTabsLocale.d.ts +0 -0
- /package/dist/{src/lib/iframe.vue.d.ts → iframe.vue.d.ts} +0 -0
- /package/dist/{src/lib/utils → utils}/stackTabsA11y.d.ts +0 -0
- /package/dist/{src/lib/utils → utils}/stackTabsMessage.d.ts +0 -0
- /package/dist/{src/lib/utils → utils}/tabInfoEncoder.d.ts +0 -0
- /package/dist/{src/lib/utils → utils}/typeGuards.d.ts +0 -0
- /package/dist/{src/lib/utils → utils}/urlParser.d.ts +0 -0
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vue-stack-tabs",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"packageManager": "pnpm@11.21.0+sha512.521705bce689924eac72f5a3587122f362689ef6571e55ba80076fd637c11132ecffada26fad4ea79c485bfddbfd3d5a2a5b05805a77e893de71ec8a6cca3bb1",
|
|
3
|
+
"version": "1.4.1",
|
|
5
4
|
"private": false,
|
|
6
5
|
"type": "module",
|
|
6
|
+
"description": "Vue 3 multi-tab manager built on Vue Router with iframe-like tab isolation: per-tab component scope, stack-based forward/backward navigation, keep-alive page caching, scroll position memory, i18n and a Nuxt 3/4 module.",
|
|
7
7
|
"files": [
|
|
8
8
|
"src/lib",
|
|
9
9
|
"dist"
|
|
10
10
|
],
|
|
11
|
+
"sideEffects": [
|
|
12
|
+
"**/*.css",
|
|
13
|
+
"**/*.scss"
|
|
14
|
+
],
|
|
11
15
|
"exports": {
|
|
12
16
|
".": {
|
|
13
17
|
"types": "./dist/index.d.ts",
|
|
@@ -25,7 +29,9 @@
|
|
|
25
29
|
"./dist/vue-stack-tabs.css": "./dist/vue-stack-tabs.css"
|
|
26
30
|
},
|
|
27
31
|
"peerDependencies": {
|
|
28
|
-
"nuxt": "^3.0.0 || ^4.0.0"
|
|
32
|
+
"nuxt": "^3.0.0 || ^4.0.0",
|
|
33
|
+
"vue": "^3.5.43",
|
|
34
|
+
"vue-router": "^5.4.0"
|
|
29
35
|
},
|
|
30
36
|
"peerDependenciesMeta": {
|
|
31
37
|
"nuxt": {
|
|
@@ -34,6 +40,7 @@
|
|
|
34
40
|
},
|
|
35
41
|
"main": "./dist/vue-stack-tabs.es.js",
|
|
36
42
|
"module": "./dist/vue-stack-tabs.es.js",
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
37
44
|
"typings": "./dist/index.d.ts",
|
|
38
45
|
"repository": {
|
|
39
46
|
"type": "git",
|
|
@@ -41,20 +48,39 @@
|
|
|
41
48
|
},
|
|
42
49
|
"keywords": [
|
|
43
50
|
"vue",
|
|
44
|
-
"
|
|
51
|
+
"vue3",
|
|
52
|
+
"vue-router",
|
|
53
|
+
"router",
|
|
45
54
|
"tabs",
|
|
46
|
-
"
|
|
55
|
+
"tab",
|
|
56
|
+
"tab-panel",
|
|
57
|
+
"multi-tab",
|
|
58
|
+
"tabs-router",
|
|
59
|
+
"keep-alive",
|
|
60
|
+
"cache",
|
|
61
|
+
"iframe",
|
|
62
|
+
"nuxt",
|
|
63
|
+
"nuxt3",
|
|
64
|
+
"nuxt-module",
|
|
65
|
+
"navigation",
|
|
66
|
+
"vue-component",
|
|
67
|
+
"typescript"
|
|
47
68
|
],
|
|
48
69
|
"author": "tantan",
|
|
49
|
-
"license": "Apache
|
|
70
|
+
"license": "Apache-2.0",
|
|
50
71
|
"bugs": {
|
|
51
72
|
"url": "https://github.com/jktantan/vue-stack-tabs/issues"
|
|
52
73
|
},
|
|
53
|
-
"homepage": "https://github.
|
|
74
|
+
"homepage": "https://jktantan.github.io/vue-stack-tabs/",
|
|
75
|
+
"publishConfig": {
|
|
76
|
+
"access": "public"
|
|
77
|
+
},
|
|
54
78
|
"scripts": {
|
|
55
79
|
"clean:ports": "node -e \"const { execSync } = require('child_process'); try { if (process.platform === 'win32') { execSync('netstat -ano | findstr \":5173\" | findstr \"LISTENING\"').toString().split('\\n').forEach(line => { const pid = line.trim().split(/\\s+/)[4]; if (pid) try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' }); } catch(e) {} }); execSync('netstat -ano | findstr \":5174\" | findstr \"LISTENING\"').toString().split('\\n').forEach(line => { const pid = line.trim().split(/\\s+/)[4]; if (pid) try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' }); } catch(e) {} }); } else { execSync('lsof -ti:5173 | xargs kill -9 || true', { shell: '/bin/bash' }); execSync('lsof -ti:5174 | xargs kill -9 || true', { shell: '/bin/bash' }); } console.log('✅ 端口清理完成'); } catch(e) { console.log('✅ 端口已经空闲或清理完成'); }\"",
|
|
56
|
-
"docs:dev": "
|
|
57
|
-
"docs:
|
|
80
|
+
"docs:dev": "run-p docs:dev:docs docs:dev:demo",
|
|
81
|
+
"docs:dev:docs": "vitepress dev docs",
|
|
82
|
+
"docs:dev:demo": "vite --config vite.config.docs-demo.ts",
|
|
83
|
+
"docs:build": "vitepress build docs && vite build --config vite.config.docs-demo.ts",
|
|
58
84
|
"dev": "vite --host ",
|
|
59
85
|
"build": "run-p type-check \"build-only {@}\" --",
|
|
60
86
|
"preview": "vite preview",
|
|
@@ -64,64 +90,60 @@
|
|
|
64
90
|
"lint:check": "eslint .",
|
|
65
91
|
"format": "prettier --write .",
|
|
66
92
|
"format:check": "prettier --check .",
|
|
67
|
-
"lib:build": "vite --config vite.config.lib.ts build && vite --config vite.config.iframe-bridge.ts build && vite --config vite.config.nuxt.ts build && node -e \"const fs=require('fs');fs.copyFileSync('dist/vue-stack-tabs.css','dist/style.css');fs.
|
|
93
|
+
"lib:build": "vite --config vite.config.lib.ts build && vite --config vite.config.iframe-bridge.ts build && vite --config vite.config.nuxt.ts build && node -e \"const fs=require('fs');fs.copyFileSync('dist/vue-stack-tabs.css','dist/style.css');fs.rmSync('dist/src',{recursive:true,force:true});\"",
|
|
68
94
|
"test": "vitest run",
|
|
69
95
|
"test:watch": "vitest",
|
|
70
|
-
"test:coverage": "vitest run --coverage"
|
|
71
|
-
"test:prepack": "node scripts/verify-prepack.mjs",
|
|
72
|
-
"test:packaged": "pnpm run lib:build && node scripts/verify-packaged.mjs"
|
|
96
|
+
"test:coverage": "vitest run --coverage"
|
|
73
97
|
},
|
|
74
98
|
"dependencies": {
|
|
75
99
|
"defu": "^6.1.7",
|
|
76
100
|
"mitt": "^3.0.1",
|
|
77
|
-
"nostics": "^1.
|
|
101
|
+
"nostics": "^1.3.0",
|
|
78
102
|
"throttle-debounce": "^5.0.2",
|
|
79
|
-
"vue": "^
|
|
80
|
-
"vue-i18n-lite": "^1.0.2",
|
|
81
|
-
"vue-router": "^5.3.0"
|
|
103
|
+
"vue-i18n-lite": "^1.0.2"
|
|
82
104
|
},
|
|
83
105
|
"devDependencies": {
|
|
84
|
-
"@ctrl/tinycolor": "^4.2.
|
|
106
|
+
"@ctrl/tinycolor": "^4.2.1",
|
|
85
107
|
"@element-plus/icons-vue": "^2.3.2",
|
|
86
108
|
"@eslint/js": "^10.0.1",
|
|
87
109
|
"@floating-ui/core": "^1.8.0",
|
|
88
110
|
"@floating-ui/dom": "^1.8.0",
|
|
89
111
|
"@floating-ui/utils": "^0.2.12",
|
|
90
|
-
"@mdit/plugin-tasklist": "^1.1.
|
|
112
|
+
"@mdit/plugin-tasklist": "^1.1.3",
|
|
91
113
|
"@popperjs/core": "^2.11.8",
|
|
92
114
|
"@rushstack/eslint-patch": "^1.16.1",
|
|
93
115
|
"@tsconfig/node26": "^26.0.1",
|
|
94
116
|
"@types/lodash-es": "^4.17.12",
|
|
95
|
-
"@types/node": "^26.
|
|
117
|
+
"@types/node": "^26.6.5",
|
|
96
118
|
"@types/throttle-debounce": "^5.0.2",
|
|
97
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
119
|
+
"@vitejs/plugin-vue": "^6.0.10",
|
|
98
120
|
"@vitejs/plugin-vue-jsx": "^5.1.6",
|
|
99
121
|
"@vitest/coverage-v8": "4.1.10",
|
|
100
|
-
"@vue/compiler-core": "^3.5.
|
|
101
|
-
"@vue/compiler-dom": "^3.5.
|
|
122
|
+
"@vue/compiler-core": "^3.5.43",
|
|
123
|
+
"@vue/compiler-dom": "^3.5.43",
|
|
102
124
|
"@vue/devtools-api": "^8.2.1",
|
|
103
125
|
"@vue/devtools-kit": "^8.2.1",
|
|
104
126
|
"@vue/devtools-shared": "^8.2.1",
|
|
105
127
|
"@vue/eslint-config-prettier": "^10.2.0",
|
|
106
128
|
"@vue/eslint-config-typescript": "^14.9.0",
|
|
107
|
-
"@vue/reactivity": "^3.5.
|
|
108
|
-
"@vue/runtime-core": "^3.5.
|
|
109
|
-
"@vue/runtime-dom": "^3.5.
|
|
110
|
-
"@vue/shared": "^3.5.
|
|
111
|
-
"@vue/test-utils": "^2.5.
|
|
129
|
+
"@vue/reactivity": "^3.5.43",
|
|
130
|
+
"@vue/runtime-core": "^3.5.43",
|
|
131
|
+
"@vue/runtime-dom": "^3.5.43",
|
|
132
|
+
"@vue/shared": "^3.5.43",
|
|
133
|
+
"@vue/test-utils": "^2.5.1",
|
|
112
134
|
"@vue/tsconfig": "^0.9.1",
|
|
113
135
|
"@vueuse/core": "^14.4.0",
|
|
114
136
|
"@vueuse/shared": "^14.4.0",
|
|
115
137
|
"async-validator": "^4.2.5",
|
|
116
138
|
"birpc": "^4.2.0",
|
|
117
139
|
"dayjs": "^1.11.23",
|
|
118
|
-
"element-plus": "^2.14.
|
|
119
|
-
"eslint": "^10.
|
|
140
|
+
"element-plus": "^2.14.7",
|
|
141
|
+
"eslint": "^10.12.0",
|
|
120
142
|
"eslint-config-prettier": "^10.1.8",
|
|
121
|
-
"eslint-plugin-vue": "^10.
|
|
122
|
-
"globals": "^17.
|
|
123
|
-
"happy-dom": "^20.
|
|
124
|
-
"hookable": "^6.1.
|
|
143
|
+
"eslint-plugin-vue": "^10.11.1",
|
|
144
|
+
"globals": "^17.13.0",
|
|
145
|
+
"happy-dom": "^20.14.6",
|
|
146
|
+
"hookable": "^6.1.2",
|
|
125
147
|
"lodash-es": "^4.18.1",
|
|
126
148
|
"lodash-unified": "^1.0.3",
|
|
127
149
|
"memoize-one": "^6.0.0",
|
|
@@ -129,18 +151,20 @@
|
|
|
129
151
|
"normalize.css": "^8.0.1",
|
|
130
152
|
"npm-run-all2": "^9.0.3",
|
|
131
153
|
"perfect-debounce": "^2.1.0",
|
|
132
|
-
"prettier": "^3.9.
|
|
133
|
-
"sass-embedded": "^1.
|
|
154
|
+
"prettier": "^3.9.10",
|
|
155
|
+
"sass-embedded": "^1.105.1",
|
|
134
156
|
"terser": "^5.51.2",
|
|
135
|
-
"typescript": "
|
|
136
|
-
"typescript-eslint": "^8.
|
|
157
|
+
"typescript": "^6.0.3",
|
|
158
|
+
"typescript-eslint": "^8.71.1",
|
|
137
159
|
"unplugin-auto-import": "^21.1.0",
|
|
138
160
|
"unplugin-vue-components": "^32.1.0",
|
|
139
|
-
"vite": "^8.
|
|
161
|
+
"vite": "^8.3.4",
|
|
140
162
|
"vite-plugin-dts": "5.0.3",
|
|
141
163
|
"vite-plugin-package-version": "^1.1.0",
|
|
142
164
|
"vitepress": "^1.6.4",
|
|
143
165
|
"vitest": "^4.1.11",
|
|
144
|
-
"vue
|
|
166
|
+
"vue": "^3.5.43",
|
|
167
|
+
"vue-router": "^5.4.0",
|
|
168
|
+
"vue-tsc": "^3.3.13"
|
|
145
169
|
}
|
|
146
170
|
}
|
package/src/lib/StackTabs.vue
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
watch
|
|
23
23
|
} from 'vue'
|
|
24
24
|
import type { TransitionProps } from 'vue'
|
|
25
|
-
import { getMaxZIndex } from './utils/scrollUtils'
|
|
25
|
+
import { getMaxZIndex, invalidateZIndexCache } from './utils/scrollUtils'
|
|
26
26
|
import { isAllowedTabUrl } from './utils/urlParser'
|
|
27
27
|
import { applyStackTabsLocale } from './i18n/stackTabsLocale'
|
|
28
28
|
import { type ITabData, TabScrollMode } from './model/TabModel'
|
|
@@ -96,6 +96,17 @@ const props = withDefaults(
|
|
|
96
96
|
iframeLoadTimeout: 15000
|
|
97
97
|
}
|
|
98
98
|
)
|
|
99
|
+
|
|
100
|
+
// iframePath 必填:用于 iframe 占位页(StackTabs 把 iframe 标签路由到这个 path 渲染)。
|
|
101
|
+
// 这里在开发期给出明确错误,避免空字符串导致 router.push({ path: '' }) 跳到首页的隐式行为。
|
|
102
|
+
if (typeof props.iframePath !== 'string' || props.iframePath.trim() === '') {
|
|
103
|
+
const message = '[vue-stack-tabs] <VueStackTabs> requires the "iframePath" prop (e.g. "/iframe").'
|
|
104
|
+
if (!import.meta.env.PROD) {
|
|
105
|
+
throw new Error(message)
|
|
106
|
+
}
|
|
107
|
+
console.error(message)
|
|
108
|
+
}
|
|
109
|
+
|
|
99
110
|
const { changeLocale, t } = useI18n()
|
|
100
111
|
const runtimeContext =
|
|
101
112
|
inject(stackTabsContextKey, null) ??
|
|
@@ -120,10 +131,12 @@ if (isRuntimeContextOwner) {
|
|
|
120
131
|
provide(maximumKey, maximum)
|
|
121
132
|
}
|
|
122
133
|
watch(maximum, (isMaximum) => {
|
|
134
|
+
// 最大化切换会显著改变 body 下元素层叠顺序,先失效缓存再读取避免过期结果。
|
|
135
|
+
invalidateZIndexCache()
|
|
123
136
|
maximumZIndex.value = isMaximum ? getMaxZIndex('body *:not(.stack-tab,.stack-tab *)') : undefined
|
|
124
137
|
})
|
|
125
138
|
const panelApi = isRuntimeContextOwner
|
|
126
|
-
? useTabPanel()
|
|
139
|
+
? useTabPanel(runtimeContext)
|
|
127
140
|
: {
|
|
128
141
|
tabs: runtimeContext.tabs,
|
|
129
142
|
iframeRefreshKeys: runtimeContext.iframeRefreshKeys,
|
|
@@ -143,7 +156,7 @@ const {
|
|
|
143
156
|
setSessionPrefix
|
|
144
157
|
} = panelApi
|
|
145
158
|
const tabActions = isRuntimeContextOwner
|
|
146
|
-
? useTabActions()
|
|
159
|
+
? useTabActions(runtimeContext)
|
|
147
160
|
: {
|
|
148
161
|
setIFramePath: () => undefined,
|
|
149
162
|
openTab: () => Promise.resolve(undefined),
|
|
@@ -63,4 +63,6 @@ $stack-tab-shadow-left: -5px 0 5px -5px rgb(0 0 0 / 60%); //#ccc;
|
|
|
63
63
|
$stack-tab-shadow-right: 5px 0 5px -5px rgb(0 0 0 / 60%); //#ccc;
|
|
64
64
|
$stack-tab-shadow-right-inset: inset -5px 0 5px -5px rgb(0 0 0 / 60%); //#ccc;
|
|
65
65
|
$stack-tab-shadow-left-inset: inset 5px 0 5px -5px rgb(0 0 0 / 60%); //#ccc;
|
|
66
|
+
$stack-tab-shadow-top-inset: inset 0 5px 5px -5px rgb(0 0 0 / 60%); //#ccc;
|
|
67
|
+
$stack-tab-shadow-bottom-inset: inset 0 -5px 5px -5px rgb(0 0 0 / 60%); //#ccc;
|
|
66
68
|
$stack-tab-shadow-context-menu: 5px 5px 5px rgb(0 0 0 / 10%);
|
|
@@ -443,7 +443,6 @@
|
|
|
443
443
|
border-radius: 10px 0;
|
|
444
444
|
box-shadow: $stack-tab-shadow-context-menu;
|
|
445
445
|
transform-origin: left top;
|
|
446
|
-
transition: all 0.25s ease-in;
|
|
447
446
|
|
|
448
447
|
&-item {
|
|
449
448
|
display: flex;
|
|
@@ -464,7 +463,10 @@
|
|
|
464
463
|
background: transparent;
|
|
465
464
|
border: 0;
|
|
466
465
|
border-radius: 10px 0;
|
|
467
|
-
|
|
466
|
+
// 仅过渡悬停态实际会改变的颜色,避免触发菜单布局与合成层的无关动画。
|
|
467
|
+
transition:
|
|
468
|
+
color 0.2s ease-in-out,
|
|
469
|
+
background-color 0.2s ease-in-out;
|
|
468
470
|
user-select: none;
|
|
469
471
|
|
|
470
472
|
&:hover,
|
|
@@ -497,7 +499,7 @@
|
|
|
497
499
|
&-icon {
|
|
498
500
|
width: $stack-tab-font-size;
|
|
499
501
|
height: $stack-tab-font-size;
|
|
500
|
-
transition:
|
|
502
|
+
transition: background-color 0.2s ease-in-out;
|
|
501
503
|
|
|
502
504
|
&.svg-mask {
|
|
503
505
|
width: $stack-tab-font-size;
|
|
@@ -532,4 +534,16 @@
|
|
|
532
534
|
@include mixins.insetShadow();
|
|
533
535
|
box-shadow: $stack-tab-shadow-left-inset, $stack-tab-shadow-right-inset;
|
|
534
536
|
}
|
|
537
|
+
&-top::after {
|
|
538
|
+
@include mixins.insetShadow();
|
|
539
|
+
box-shadow: $stack-tab-shadow-top-inset;
|
|
540
|
+
}
|
|
541
|
+
&-bottom::after {
|
|
542
|
+
@include mixins.insetShadow();
|
|
543
|
+
box-shadow: $stack-tab-shadow-bottom-inset;
|
|
544
|
+
}
|
|
545
|
+
&-vertical::after {
|
|
546
|
+
@include mixins.insetShadow();
|
|
547
|
+
box-shadow: $stack-tab-shadow-top-inset, $stack-tab-shadow-bottom-inset;
|
|
548
|
+
}
|
|
535
549
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
:style="{
|
|
14
14
|
left: `${menuPosition.left}px`,
|
|
15
15
|
top: `${menuPosition.top}px`,
|
|
16
|
-
'z-index':
|
|
16
|
+
'z-index': menuZIndex()
|
|
17
17
|
}"
|
|
18
18
|
@keydown="handleMenuKeydown"
|
|
19
19
|
>
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
import { ref, onMounted, reactive, nextTick } from 'vue'
|
|
85
85
|
import type { ITabItem, IContextMenu } from '../../model/TabModel'
|
|
86
86
|
import { useI18n } from 'vue-i18n-lite'
|
|
87
|
-
import { getMaxZIndex } from '../../utils/scrollUtils'
|
|
87
|
+
import { getMaxZIndex, invalidateZIndexCache } from '../../utils/scrollUtils'
|
|
88
88
|
import useTabActions from '../../hooks/useTabActions'
|
|
89
89
|
import useTabPanel from '../../hooks/useTabPanel'
|
|
90
90
|
import ContextMenuItem from './ContextMenuItem.vue'
|
|
@@ -115,6 +115,14 @@ const menuPosition = reactive({ left: props.left, top: props.top })
|
|
|
115
115
|
const { closeTab, closeAllTabs, refreshTab, refreshAllTabs, openInNewWindow } = useTabActions()
|
|
116
116
|
const { removeLeftTabs, removeRightTabs, removeOtherTabs } = useTabPanel()
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* 菜单挂载时 z-index 必须重新计算(DOM 层叠已变化),先用 invalidateZIndexCache 失效再读取。
|
|
120
|
+
*/
|
|
121
|
+
const menuZIndex = () => {
|
|
122
|
+
invalidateZIndexCache('.stack-tab,.stack-tab *')
|
|
123
|
+
return getMaxZIndex('.stack-tab,.stack-tab *')
|
|
124
|
+
}
|
|
125
|
+
|
|
118
126
|
const focusActiveTabFallback = () => {
|
|
119
127
|
const activeTab = document.querySelector<HTMLElement>('[role="tab"][aria-selected="true"]')
|
|
120
128
|
activeTab?.focus()
|
|
@@ -193,7 +201,8 @@ const handleMenuKeydown = (event: KeyboardEvent) => {
|
|
|
193
201
|
}
|
|
194
202
|
}
|
|
195
203
|
|
|
196
|
-
const getCustomMenuKey = (item: IContextMenu): string =>
|
|
204
|
+
const getCustomMenuKey = (item: IContextMenu): string =>
|
|
205
|
+
item.key ?? `${item.title}-${item.icon ?? ''}`
|
|
197
206
|
|
|
198
207
|
/** 挂载后修正菜单位置,避免超出右边界 */
|
|
199
208
|
onMounted(() => {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
role="status"
|
|
12
12
|
aria-live="polite"
|
|
13
13
|
:aria-label="t('VueStackTab.loading')"
|
|
14
|
-
:style="{ zIndex:
|
|
14
|
+
:style="{ zIndex: loadingZIndex() }"
|
|
15
15
|
>
|
|
16
16
|
<div class="stack-tab-loading--spin turn" aria-hidden="true" />
|
|
17
17
|
</div>
|
|
@@ -20,7 +20,15 @@
|
|
|
20
20
|
import { ref, onUnmounted } from 'vue'
|
|
21
21
|
import { useI18n } from 'vue-i18n-lite'
|
|
22
22
|
import { TabEventType, useTabEmitter } from '../hooks/useTabEventBus'
|
|
23
|
-
import { getMaxZIndex } from '../utils/scrollUtils'
|
|
23
|
+
import { getMaxZIndex, invalidateZIndexCache } from '../utils/scrollUtils'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* loading 遮罩每次显隐都会改变 .cache-page-wrapper 子树层叠,先失效缓存再读取避免旧值。
|
|
27
|
+
*/
|
|
28
|
+
const loadingZIndex = () => {
|
|
29
|
+
invalidateZIndexCache('.cache-page-wrapper *')
|
|
30
|
+
return getMaxZIndex('.cache-page-wrapper *')
|
|
31
|
+
}
|
|
24
32
|
const props = defineProps<{
|
|
25
33
|
/** 当前标签 id,用于过滤 PAGE_LOADING 事件 */
|
|
26
34
|
tabId: string
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
- 不负责 transition、keep-alive、TabHeader、iframe 或 session 初始化
|
|
12
12
|
-->
|
|
13
13
|
<script lang="ts" setup>
|
|
14
|
-
import {
|
|
14
|
+
import { shallowRef, watch } from 'vue'
|
|
15
15
|
import type { DefineComponent, VNode } from 'vue'
|
|
16
16
|
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
|
17
17
|
import useTabPanel from '../../hooks/useTabPanel'
|
|
18
|
+
import { getEmptyPlaceholderComponent } from './emptyPlaceholder'
|
|
18
19
|
|
|
19
20
|
interface StackCacheRendererProps {
|
|
20
21
|
route: RouteLocationNormalizedLoaded
|
|
@@ -37,14 +38,7 @@ defineSlots<{
|
|
|
37
38
|
|
|
38
39
|
const { refreshKey, activeCacheKey, activePageRefreshVersion, addPage } = useTabPanel()
|
|
39
40
|
|
|
40
|
-
const
|
|
41
|
-
name: 'StackCacheRendererEmpty',
|
|
42
|
-
setup() {
|
|
43
|
-
return () => null
|
|
44
|
-
}
|
|
45
|
-
}) as DefineComponent
|
|
46
|
-
|
|
47
|
-
const wrappedComponent = shallowRef<DefineComponent>(EmptyRendererComponent)
|
|
41
|
+
const wrappedComponent = shallowRef<DefineComponent>(getEmptyPlaceholderComponent())
|
|
48
42
|
|
|
49
43
|
const getComponentIdentity = (component?: VNode | null): unknown => {
|
|
50
44
|
return component?.type ?? component
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 共享的 Empty 占位组件。
|
|
3
|
+
*
|
|
4
|
+
* useTabPanel 在「empty」/「recovered」分支会返回一个不渲染任何内容的占位组件。
|
|
5
|
+
* 原先在 pageComponentFactory.tsx 和 StackCacheRenderer.vue 各自定义了一份,
|
|
6
|
+
* 这里抽出唯一来源,避免两份略有差异的副本带来维护负担。
|
|
7
|
+
*/
|
|
8
|
+
import { defineComponent } from 'vue'
|
|
9
|
+
import type { DefineComponent } from 'vue'
|
|
10
|
+
|
|
11
|
+
export const EmptyPlaceholderComponent = defineComponent({
|
|
12
|
+
name: 'StackTabEmptyPlaceholder',
|
|
13
|
+
setup() {
|
|
14
|
+
return () => null
|
|
15
|
+
}
|
|
16
|
+
}) as DefineComponent
|
|
17
|
+
|
|
18
|
+
export const getEmptyPlaceholderComponent = (): DefineComponent => EmptyPlaceholderComponent
|
|
@@ -243,17 +243,18 @@ const handleTabListKeydown = (event: KeyboardEvent) => {
|
|
|
243
243
|
if (count <= 0) return
|
|
244
244
|
|
|
245
245
|
const currentIndex = getActiveTabIndex()
|
|
246
|
-
const safeIndex = currentIndex >= 0 ? currentIndex : 0
|
|
247
246
|
|
|
248
247
|
if (event.key === 'ArrowRight') {
|
|
249
248
|
event.preventDefault()
|
|
250
|
-
|
|
249
|
+
// 无 active 标签时从 -1 起步,按右方向键跳到第 0 个;正常情况环绕到下一个。
|
|
250
|
+
activateTabByIndex(currentIndex === -1 ? 0 : (currentIndex + 1) % count)
|
|
251
251
|
return
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
if (event.key === 'ArrowLeft') {
|
|
255
255
|
event.preventDefault()
|
|
256
|
-
|
|
256
|
+
// 无 active 标签时按左方向键跳到最后一个;正常情况环绕到上一个。
|
|
257
|
+
activateTabByIndex(currentIndex === -1 ? count - 1 : (currentIndex - 1 + count) % count)
|
|
257
258
|
return
|
|
258
259
|
}
|
|
259
260
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* VueStackTabs 全局运行时上下文管理。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 负责创建、注册、解析 StackTabsRuntimeContext。
|
|
5
|
+
* 业务运行时通过 Vue provide/inject 隔离;模块级回退仅保留给没有 App
|
|
6
|
+
* 容器的兼容调用(例如旧的单元测试),绝不能承载某个 Vue App 的状态。
|
|
6
7
|
* 每个 Vue app 只允许注册一个 <VueStackTabs> 实例。
|
|
7
8
|
*/
|
|
8
9
|
import type { App, DefineComponent, InjectionKey, Ref, ShallowRef } from 'vue'
|
|
@@ -77,7 +78,10 @@ export const createStackTabsRuntimeContext = (
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* 获取非 App 容器下注册的兼容上下文。
|
|
83
|
+
* @deprecated App 内的 composable 必须依赖 provide/inject,不应使用这个回退。
|
|
84
|
+
*/
|
|
81
85
|
export const getActiveStackTabsRuntimeContext = (): StackTabsRuntimeContext | null =>
|
|
82
86
|
activeRuntimeContext
|
|
83
87
|
|
|
@@ -86,22 +90,21 @@ export const registerStackTabsRuntimeContext = (
|
|
|
86
90
|
context: StackTabsRuntimeContext,
|
|
87
91
|
options: RegisterStackTabsRuntimeContextOptions = {}
|
|
88
92
|
): boolean => {
|
|
89
|
-
if (!activeRuntimeContext) {
|
|
90
|
-
activeRuntimeContext = context
|
|
91
|
-
}
|
|
92
|
-
|
|
93
93
|
if (options.app && !runtimeContextOwnersByApp.has(options.app)) {
|
|
94
94
|
runtimeContextOwnersByApp.add(options.app)
|
|
95
95
|
return true
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
if (
|
|
99
|
-
!
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
98
|
+
if (!options.app) {
|
|
99
|
+
if (!activeRuntimeContext) {
|
|
100
|
+
activeRuntimeContext = context
|
|
101
|
+
isFallbackRuntimeContextOwnerRegistered = true
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
if (activeRuntimeContext === context && !isFallbackRuntimeContextOwnerRegistered) {
|
|
105
|
+
isFallbackRuntimeContextOwnerRegistered = true
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
const isProduction = options.isProduction ?? import.meta.env.PROD
|
|
@@ -118,13 +121,15 @@ export const unregisterStackTabsRuntimeContext = (
|
|
|
118
121
|
context: StackTabsRuntimeContext,
|
|
119
122
|
options: UnregisterStackTabsRuntimeContextOptions = {}
|
|
120
123
|
): void => {
|
|
121
|
-
if (options.app)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
if (options.app) {
|
|
125
|
+
runtimeContextOwnersByApp.delete(options.app)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
if (activeRuntimeContext === context) isFallbackRuntimeContextOwnerRegistered = false
|
|
124
129
|
if (activeRuntimeContext === context) activeRuntimeContext = null
|
|
125
130
|
}
|
|
126
131
|
|
|
127
|
-
/** 解析当前可用的运行时上下文,优先使用 inject
|
|
132
|
+
/** 解析当前可用的运行时上下文,优先使用 inject,兼容回退仅限非 App 容器。 */
|
|
128
133
|
export const resolveStackTabsRuntimeContext = (): StackTabsRuntimeContext => {
|
|
129
134
|
const injected = hasInjectionContext() ? inject(stackTabsContextKey, null) : null
|
|
130
135
|
const context = injected ?? activeRuntimeContext
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 页面缓存组件工厂。
|
|
3
|
+
*
|
|
4
|
+
* 将 keep-alive 包装、VNode 注入和页面激活生命周期从 tab 状态编排中隔离,
|
|
5
|
+
* 使 useTabPanel 只负责领域状态与路由协调。
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
cloneVNode,
|
|
9
|
+
defineComponent,
|
|
10
|
+
onActivated,
|
|
11
|
+
onDeactivated,
|
|
12
|
+
onMounted,
|
|
13
|
+
onUnmounted,
|
|
14
|
+
ref,
|
|
15
|
+
shallowRef
|
|
16
|
+
} from 'vue'
|
|
17
|
+
import type { DefineComponent, PropType, VNode } from 'vue'
|
|
18
|
+
import type { ITabBase, ITabItem } from '../../model/TabModel'
|
|
19
|
+
import PageLoading from '../../components/PageLoading.vue'
|
|
20
|
+
import type { StackTabsRuntimeContext } from '../stackTabsContext'
|
|
21
|
+
import { useScrollShadow } from '../useScrollShadow'
|
|
22
|
+
|
|
23
|
+
interface PageComponentFactoryOptions {
|
|
24
|
+
runtimeContext: StackTabsRuntimeContext
|
|
25
|
+
getTab: (id: string) => ITabItem | null
|
|
26
|
+
addCache: (cacheName: string) => void
|
|
27
|
+
evictMarkedCaches: () => void
|
|
28
|
+
addPageScroller: (pageId: string, ...selectors: string[]) => void
|
|
29
|
+
saveScroller: (pageId: string) => void
|
|
30
|
+
restoreScroller: (pageId: string) => void
|
|
31
|
+
removeScroller: (pageId: string) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const createPageComponentFactory = (options: PageComponentFactoryOptions) => {
|
|
35
|
+
const {
|
|
36
|
+
runtimeContext,
|
|
37
|
+
getTab,
|
|
38
|
+
addCache,
|
|
39
|
+
evictMarkedCaches,
|
|
40
|
+
addPageScroller,
|
|
41
|
+
saveScroller,
|
|
42
|
+
restoreScroller,
|
|
43
|
+
removeScroller
|
|
44
|
+
} = options
|
|
45
|
+
const { components, tabIdsToEvict } = runtimeContext
|
|
46
|
+
|
|
47
|
+
const resolvePageComponent = (ctx: { cacheName: string; tabInfo: ITabBase }): DefineComponent => {
|
|
48
|
+
const { cacheName, tabInfo } = ctx
|
|
49
|
+
const existing = components.get(cacheName)
|
|
50
|
+
if (existing) {
|
|
51
|
+
addCache(cacheName)
|
|
52
|
+
return existing
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const cacheComponent = defineComponent<{ vnode?: VNode }>({
|
|
56
|
+
name: cacheName,
|
|
57
|
+
props: {
|
|
58
|
+
vnode: {
|
|
59
|
+
type: Object as PropType<VNode>,
|
|
60
|
+
required: false,
|
|
61
|
+
default: undefined
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
emits: ['onLoaded'],
|
|
65
|
+
setup(props, context) {
|
|
66
|
+
const localBackParams = ref<Record<string, unknown> | null>(null)
|
|
67
|
+
const lastVnode = shallowRef<VNode | null | undefined>(null)
|
|
68
|
+
const lastCloned = shallowRef<VNode | null>(null)
|
|
69
|
+
const lastBackParams = shallowRef<Record<string, unknown> | null>(null)
|
|
70
|
+
const pageRoot = ref<HTMLElement | null>(null)
|
|
71
|
+
|
|
72
|
+
const checkAndConsumeBackParams = () => {
|
|
73
|
+
try {
|
|
74
|
+
const tab = getTab(tabInfo.id!)
|
|
75
|
+
const top = tab?.pages.peek()
|
|
76
|
+
if (top && top.id === cacheName && top._backParams) {
|
|
77
|
+
localBackParams.value = { ...top._backParams }
|
|
78
|
+
delete top._backParams
|
|
79
|
+
} else {
|
|
80
|
+
localBackParams.value = null
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
localBackParams.value = null
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onMounted(() => {
|
|
88
|
+
context.emit('onLoaded')
|
|
89
|
+
addPageScroller(cacheName, `#W-${cacheName}`)
|
|
90
|
+
checkAndConsumeBackParams()
|
|
91
|
+
})
|
|
92
|
+
onDeactivated(() => saveScroller(cacheName))
|
|
93
|
+
onActivated(() => {
|
|
94
|
+
context.emit('onLoaded')
|
|
95
|
+
restoreScroller(cacheName)
|
|
96
|
+
evictMarkedCaches()
|
|
97
|
+
tabIdsToEvict.clear()
|
|
98
|
+
checkAndConsumeBackParams()
|
|
99
|
+
})
|
|
100
|
+
onUnmounted(() => removeScroller(cacheName))
|
|
101
|
+
|
|
102
|
+
// 给 .cache-page-wrapper 挂滚动阴影,确保 keep-alive 的 suspended/deactivated 生命周期下表现一致
|
|
103
|
+
useScrollShadow({ target: pageRoot })
|
|
104
|
+
|
|
105
|
+
return () => {
|
|
106
|
+
const vnode = props.vnode
|
|
107
|
+
const backParams = localBackParams.value
|
|
108
|
+
if (vnode !== lastVnode.value || backParams !== lastBackParams.value) {
|
|
109
|
+
lastCloned.value = vnode
|
|
110
|
+
? cloneVNode(vnode, {
|
|
111
|
+
tId: tabInfo.id,
|
|
112
|
+
pId: cacheName,
|
|
113
|
+
...(backParams ? { _back: backParams } : {})
|
|
114
|
+
})
|
|
115
|
+
: null
|
|
116
|
+
lastVnode.value = vnode
|
|
117
|
+
lastBackParams.value = backParams
|
|
118
|
+
}
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
ref={pageRoot}
|
|
122
|
+
class="cache-page-wrapper"
|
|
123
|
+
id={`W-${cacheName}`}
|
|
124
|
+
style={[
|
|
125
|
+
runtimeContext.useGlobalScroll.value ? 'overflow:auto' : 'overflow:hidden',
|
|
126
|
+
'height: 100%'
|
|
127
|
+
]}
|
|
128
|
+
>
|
|
129
|
+
{lastCloned.value}
|
|
130
|
+
<PageLoading tabId={tabInfo.id!} />
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}) as DefineComponent
|
|
136
|
+
components.set(cacheName, cacheComponent)
|
|
137
|
+
addCache(cacheName)
|
|
138
|
+
return cacheComponent
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { resolvePageComponent }
|
|
142
|
+
}
|