Skip to content

Commit 35c85fb

Browse files
committed
fix(web-vapor): 将自定义标签栏迁移为蒸汽组件
1 parent aa93299 commit 35c85fb

2 files changed

Lines changed: 276 additions & 50 deletions

File tree

‎packages/uni-components/lib-x-vapor/custom-tab-bar/custom-tab-bar.vue‎

Lines changed: 71 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -61,67 +61,88 @@
6161
</uni-tabbar>
6262
</template>
6363

64-
<script>
64+
<script setup lang="ts">
6565
import { computed, ref, watch } from 'vue'
6666
import { useRoute } from 'vue-router'
6767
import { getRealPath } from '@dcloudio/uni-h5'
6868
import { useTabBar } from '@dcloudio/uni-h5'
6969
70-
export default {
70+
defineOptions({
7171
name: 'CustomTabBar',
72-
props: {
73-
selected: {
74-
type: Number,
75-
default: 0
76-
},
77-
showIcon: {
78-
type: Boolean,
79-
default: true
80-
},
81-
direction: {
82-
type: String,
83-
default: 'horizontal'
84-
}
72+
})
73+
74+
const props = defineProps({
75+
selected: {
76+
type: Number,
77+
default: 0,
8578
},
86-
setup(props, { emit }) {
87-
const tabBar = useTabBar()
88-
const route = useRoute()
89-
const hasTabBar = computed(() => tabBar.list && tabBar.list.length)
90-
const selectedIndex = ref(props.selected)
91-
watch(() => props.selected, value => selectedIndex.value = value)
92-
watch(() => selectedIndex.value, value => tabBar.selectedIndex = value)
93-
watch(() => {
94-
const meta = route.meta
95-
return [meta.isTabBar, meta.route]
96-
}, ([isTabBar, pagePath]) => {
97-
if (isTabBar) {
98-
const index = tabBar.list.findIndex(item => pagePath === item.pagePath)
99-
if (index > -1) {
100-
selectedIndex.value = index
101-
}
102-
}
103-
})
104-
function switchTab(item, index) {
105-
selectedIndex.value = index
106-
const detail = {
107-
index,
108-
text: item.text,
109-
pagePath: item.pagePath,
79+
showIcon: {
80+
type: Boolean,
81+
default: true,
82+
},
83+
direction: {
84+
type: String,
85+
default: 'horizontal',
86+
},
87+
})
88+
89+
const emit = defineEmits()
90+
const tabBar = useTabBar()!
91+
const route = useRoute()
92+
const hasTabBar = computed(() => tabBar.list && tabBar.list.length)
93+
const selectedIndex = ref(props.selected)
94+
const showTabBar = ref(true)
95+
watch(
96+
() => props.selected,
97+
(value) => (selectedIndex.value = value)
98+
)
99+
watch(
100+
() => selectedIndex.value,
101+
(value) => (tabBar.selectedIndex = value)
102+
)
103+
watch(
104+
() => {
105+
const meta = route.meta
106+
return [meta.isTabBar, meta.route]
107+
},
108+
([isTabBar, pagePath]) => {
109+
if (isTabBar) {
110+
const index = tabBar.list.findIndex((item) => pagePath === item.pagePath)
111+
if (index > -1) {
112+
selectedIndex.value = index
110113
}
111-
emit('onTabItemTap', detail)
112114
}
115+
}
116+
)
113117
114-
return {
115-
tabBar,
116-
getRealPath,
117-
selectedIndex,
118-
hasTabBar,
119-
showTabBar: true,
120-
switchTab,
121-
}
118+
function switchTab(item: UniApp.TabBarItemOptions, index: number) {
119+
selectedIndex.value = index
120+
const detail = {
121+
index,
122+
text: item.text,
123+
pagePath: item.pagePath,
122124
}
125+
emit('onTabItemTap', detail)
123126
}
127+
128+
// 保留迁移前公开实例可访问的 props 和 setup 返回成员。
129+
defineExpose({
130+
get selected() {
131+
return props.selected
132+
},
133+
get showIcon() {
134+
return props.showIcon
135+
},
136+
get direction() {
137+
return props.direction
138+
},
139+
tabBar,
140+
getRealPath,
141+
selectedIndex,
142+
hasTabBar,
143+
showTabBar,
144+
switchTab,
145+
})
124146
</script>
125147

126-
<style>
127-
</style>
148+
<style></style>
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import path from 'path'
2+
import { execFileSync } from 'child_process'
3+
import ts from 'typescript'
4+
import { rollup } from 'rollup'
5+
import { checkWebVaporVNodes } from '../../../scripts/checkWebVaporVNodes'
6+
7+
const root = path.resolve(__dirname, '../../..')
8+
9+
describe('Web Vapor CustomTabBar', () => {
10+
let vue: any
11+
let Component: any
12+
let code: string
13+
let dom: any
14+
let app: any
15+
let host: HTMLElement
16+
let tabBar: any
17+
let route: any
18+
const globals = new Map<string, any>()
19+
20+
beforeAll(() => {
21+
const { JSDOM } = jest.requireActual('jsdom')
22+
dom = new JSDOM('<!DOCTYPE html><body></body>')
23+
for (const name of [
24+
'window',
25+
'document',
26+
'Node',
27+
'Element',
28+
'HTMLElement',
29+
'SVGElement',
30+
'Text',
31+
'Comment',
32+
'ShadowRoot',
33+
]) {
34+
globals.set(name, (global as any)[name])
35+
;(global as any)[name] = dom.window[name]
36+
}
37+
vue = jest.requireActual(
38+
path.join(root, 'packages/uni-h5-vue/dist-x-vapor/vue.runtime.cjs.js')
39+
)
40+
code = execFileSync(
41+
process.execPath,
42+
[
43+
'-e',
44+
`
45+
const fs = require('fs');
46+
require('./packages/uni-cli-shared/dist/hbx/alias').initWebVaporAliases();
47+
const compiler = require('@vue/compiler-sfc');
48+
const filename = 'packages/uni-components/lib-x-vapor/custom-tab-bar/custom-tab-bar.vue';
49+
const { descriptor, errors } = compiler.parse(fs.readFileSync(filename, 'utf8'), { filename });
50+
require('assert/strict').deepEqual(errors, []);
51+
require('assert/strict').ok(descriptor.scriptSetup, 'Vapor 内置组件必须使用 script setup');
52+
process.stdout.write(compiler.compileScript(descriptor, {
53+
id: 'custom-tab-bar', vapor: true, inlineTemplate: true,
54+
templateOptions: { compiler: require('@dcloudio/compiler-vapor-web'),
55+
compilerOptions: { isNativeTag: tag => ['uni-tabbar', 'div', 'img'].includes(tag) }
56+
}
57+
}).content);
58+
`,
59+
],
60+
{ cwd: root, encoding: 'utf8' }
61+
)
62+
code = ts.transpileModule(code, {
63+
compilerOptions: {
64+
module: ts.ModuleKind.ESNext,
65+
target: ts.ScriptTarget.ES2020,
66+
},
67+
}).outputText
68+
const module = { exports: {} as any }
69+
const compiled = ts.transpileModule(code, {
70+
compilerOptions: {
71+
module: ts.ModuleKind.CommonJS,
72+
target: ts.ScriptTarget.ES2020,
73+
},
74+
}).outputText
75+
new Function('require', 'module', 'exports', compiled)(
76+
(id: string) => {
77+
if (id === 'vue') return vue
78+
if (id === 'vue-router') return { useRoute: () => route }
79+
if (id === '@dcloudio/uni-h5')
80+
return {
81+
useTabBar: () => tabBar,
82+
getRealPath: (value: string) => '/web/' + value,
83+
}
84+
throw new Error('缺少测试依赖:' + id)
85+
},
86+
module,
87+
module.exports
88+
)
89+
Component = module.exports.default
90+
})
91+
92+
beforeEach(() => {
93+
host = document.createElement('div')
94+
document.body.appendChild(host)
95+
tabBar = vue.reactive({
96+
backgroundColor: '#fff',
97+
color: '#333',
98+
selectedColor: '#007aff',
99+
selectedIndex: 0,
100+
list: [
101+
{
102+
pagePath: 'pages/a',
103+
text: 'A',
104+
iconPath: 'a.png',
105+
selectedIconPath: 'a-active.png',
106+
},
107+
{
108+
pagePath: 'pages/b',
109+
text: 'B',
110+
iconPath: 'b.png',
111+
selectedIconPath: 'b-active.png',
112+
redDot: true,
113+
badge: '2',
114+
},
115+
{ pagePath: 'pages/hidden', text: 'Hidden', visible: false },
116+
],
117+
})
118+
route = vue.reactive({ meta: { isTabBar: false, route: '' } })
119+
})
120+
121+
afterEach(() => {
122+
app?.unmount()
123+
app = undefined
124+
host.remove()
125+
})
126+
127+
afterAll(() => {
128+
dom?.window.close()
129+
for (const [name, value] of globals) {
130+
if (value === undefined) delete (global as any)[name]
131+
else (global as any)[name] = value
132+
}
133+
})
134+
135+
test('延迟编译的组件同样通过框架 VNode 检测', async () => {
136+
expect(code).toContain('defineVaporComponent')
137+
expect(Component.name).toBe('CustomTabBar')
138+
expect(Component.emits).toBeUndefined()
139+
const bundle = await rollup({
140+
input: 'entry',
141+
external: (id) => id !== 'entry',
142+
plugins: [
143+
{
144+
name: 'test-entry',
145+
resolveId: (id) => (id === 'entry' ? id : null),
146+
load: (id) => (id === 'entry' ? code : null),
147+
},
148+
checkWebVaporVNodes(),
149+
],
150+
})
151+
try {
152+
await expect(bundle.generate({ format: 'es' })).resolves.toBeDefined()
153+
} finally {
154+
await bundle.close()
155+
}
156+
})
157+
158+
test('真实 Vapor 挂载、点击事件及路由同步保持原行为', async () => {
159+
const tap = jest.fn()
160+
app = vue.createVaporApp(Component, { onOnTabItemTap: () => tap })
161+
const vm = app.mount(host)
162+
expect(vm.selected).toBe(0)
163+
expect(vm.showIcon).toBe(true)
164+
expect(vm.direction).toBe('horizontal')
165+
expect(host.querySelectorAll('.uni-tabbar__item')).toHaveLength(2)
166+
expect(host.querySelector('img')!.getAttribute('src')).toBe(
167+
'/web/a-active.png'
168+
)
169+
;(host.querySelectorAll('.uni-tabbar__item')[1] as HTMLElement).click()
170+
await vue.nextTick()
171+
expect(tap).toHaveBeenCalledTimes(1)
172+
expect(tap.mock.calls[0][0]).toEqual({
173+
index: 1,
174+
text: 'B',
175+
pagePath: 'pages/b',
176+
})
177+
expect(tabBar.selectedIndex).toBe(1)
178+
route.meta.isTabBar = true
179+
route.meta.route = 'pages/a'
180+
await vue.nextTick()
181+
expect(vm.selectedIndex).toBe(0)
182+
expect(tabBar.selectedIndex).toBe(0)
183+
})
184+
185+
test('外部 selected 更新及横纵向、无图标模式', async () => {
186+
const selected = vue.ref(1)
187+
app = vue.createVaporApp(
188+
vue.defineVaporComponent(() =>
189+
vue.createComponent(Component, {
190+
selected: () => selected.value,
191+
direction: 'vertical',
192+
showIcon: false,
193+
})
194+
)
195+
)
196+
app.mount(host)
197+
expect(host.querySelector('img')).toBeNull()
198+
expect(
199+
(host.querySelector('.uni-tabbar') as HTMLElement).style.flexDirection
200+
).toBe('column')
201+
selected.value = 0
202+
await vue.nextTick()
203+
expect(tabBar.selectedIndex).toBe(0)
204+
})
205+
})

0 commit comments

Comments
 (0)