Skip to content

Commit d6ee324

Browse files
authored
Merge pull request #2530 from didi/feat-web-title-bar-mackwang
Feat web title bar
2 parents 0a0c1b9 + 2f08ff9 commit d6ee324

3 files changed

Lines changed: 282 additions & 0 deletions

File tree

docs-vitepress/api/app-config.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,32 @@ mpx.config.webConfig.routeConfig = {
178178
mpx.config.webConfig.disablePageTransition = true
179179
```
180180
此处的 `disablePageTransition` 后续将被废弃,请使用编译阶段的[disablePageTransition](compile.md#webconfig)进行配置
181+
182+
### webConfig.enableTitleBar
183+
184+
`boolean = false`
185+
186+
是否在 Web 输出时启用内置标题栏(`mpx-titlebar`)组件。默认为 `false`,即不渲染标题栏。
187+
188+
设为 `true` 后,Mpx 会在每个页面的根节点外层自动注入 `<mpx-titlebar>` 组件,读取 `app.json``window` 配置及当前页面 json 配置,渲染与小程序视觉一致的 fixed 标题栏(含安全区适配与返回按钮)。
189+
190+
> **注意**:若页面 json 中将 `navigationStyle` 配置为 `'custom'`,则该页面的标题栏会自动隐藏,以支持自定义导航栏场景。
191+
192+
```js
193+
import mpx from '@mpxjs/core'
194+
195+
mpx.config.webConfig.enableTitleBar = true
196+
```
197+
198+
### webConfig.safeAreaInsetTop
199+
200+
`number = 24`
201+
202+
**Android** 设备上,标题栏顶部安全区(状态栏)的高度(单位:px)。仅在 `enableTitleBar``true` 且非 iOS 设备时生效;iOS 设备会自动使用 `env(safe-area-inset-top)` 获取安全区高度,无需手动配置。
203+
204+
```js
205+
import mpx from '@mpxjs/core'
206+
207+
// 将 Android 状态栏高度设为 28px(默认 24px)
208+
mpx.config.webConfig.safeAreaInsetTop = 28
209+
```
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
<script>
2+
import mpx from '@mpxjs/core'
3+
4+
const safeStyle = { paddingTop: 'var(--safe-area-inset-top)' }
5+
6+
export default {
7+
name: 'mpx-titlebar',
8+
props: {
9+
// 已合并 app.json window 配置与页面 json 配置的最终配置
10+
pageConfig: {
11+
type: Object,
12+
default: () => ({})
13+
}
14+
},
15+
computed: {
16+
// 标题文本
17+
titleText() {
18+
return this.pageConfig.navigationBarTitleText || ''
19+
},
20+
// 背景色(兼容常见字段)
21+
backgroundColor() {
22+
return this.pageConfig.navigationBarBackgroundColor || '#ffffff'
23+
},
24+
// 文本颜色,微信小程序中 navigationBarTextStyle 为 white 或 black
25+
textColor() {
26+
const style = this.pageConfig.navigationBarTextStyle || 'black'
27+
return style === 'white' ? '#ffffff' : '#000000'
28+
},
29+
// navigationStyle: 'default' | 'custom',custom 表示需要自定义绘制
30+
navigationStyle() {
31+
return this.pageConfig.navigationStyle || 'default'
32+
},
33+
// 是否隐藏(navigationStyle 为 'custom' 时也应隐藏)
34+
hidden() {
35+
return mpx.config?.webConfig?.enableTitleBar !== true || this.navigationStyle === 'custom'
36+
},
37+
// 是否展示返回按钮:根据浏览器历史判断(不依赖额外 page 配置)
38+
showBack() {
39+
try {
40+
return this.$router.stack.length > 1
41+
} catch (e) {
42+
return false
43+
}
44+
},
45+
// 安卓安全高度
46+
safeAreaInsetTop() {
47+
if (typeof mpx.config.webConfig.safeAreaInsetTop === 'number' && mpx.config.webConfig.safeAreaInsetTop >= 0) {
48+
return mpx.config.webConfig.safeAreaInsetTop
49+
}
50+
return 24
51+
},
52+
isIOS() {
53+
return /iP(hone|od|ad)/.test(navigator.userAgent)
54+
},
55+
innerHeight() {
56+
return (this.isIOS ? 44 : 48) + 'px'
57+
},
58+
warpStyle() {
59+
return {
60+
'--titlebar-height': this.innerHeight,
61+
'--safe-area-inset-top': `${this.isIOS ? 'env(safe-area-inset-top, constant(safe-area-inset-top), 0px)' : this.safeAreaInsetTop + 'px'}`,
62+
paddingTop: 'calc(var(--safe-area-inset-top) + var(--titlebar-height))',
63+
}
64+
},
65+
rootStyle() {
66+
return {
67+
background: this.backgroundColor,
68+
color: this.textColor
69+
}
70+
},
71+
innerStyle() {
72+
return {
73+
height: this.innerHeight
74+
}
75+
}
76+
},
77+
methods: {
78+
// 左侧点击:派发事件并在可回退时回退
79+
onLeftClick(e) {
80+
this.$emit('click-left', e)
81+
if (this.showBack) {
82+
try { window.history.back() } catch (err) { }
83+
}
84+
}
85+
},
86+
render(h) {
87+
const leftChildren = []
88+
89+
// default back button (SVG) — no left slot support
90+
if (this.showBack) {
91+
leftChildren.push(
92+
h('button', {
93+
class: 'mpx-titlebar__back',
94+
attrs: { 'aria-label': 'back', type: 'button' }
95+
}, [
96+
h('svg', {
97+
attrs: {
98+
viewBox: '0 0 24 24',
99+
width: '20',
100+
height: '20',
101+
fill: 'none',
102+
xmlns: 'http://www.w3.org/2000/svg',
103+
focusable: 'false',
104+
'aria-hidden': 'true'
105+
}
106+
}, [
107+
h('path', {
108+
attrs: {
109+
d: 'M15 18l-6-6 6-6',
110+
stroke: 'currentColor',
111+
'stroke-width': '2',
112+
'stroke-linecap': 'round',
113+
'stroke-linejoin': 'round'
114+
}
115+
})
116+
])
117+
])
118+
)
119+
}
120+
121+
// center shows title; only default slot (page content) is supported
122+
const centerChildren = [
123+
h('div', { class: 'mpx-titlebar__title', style: { color: this.textColor } }, [this.titleText])
124+
]
125+
126+
// top-level wrapper: contains fixed titlebar and page content wrapper
127+
return h('div', { class: 'mpx-page-warp', style: this.hidden ? {} : this.warpStyle }, [
128+
this.hidden ? null : h('div', {
129+
class: ['mpx-titlebar'],
130+
style: this.rootStyle
131+
}, [
132+
h('div', { class: 'mpx-titlebar__safe', style: safeStyle }, [
133+
h('div', { class: 'mpx-titlebar__inner', style: this.innerStyle }, [
134+
h('div', { class: 'mpx-titlebar__left', on: { click: this.onLeftClick } }, leftChildren),
135+
h('div', { class: 'mpx-titlebar__center' }, centerChildren),
136+
h('div', { class: 'mpx-titlebar__right' }, [])
137+
])
138+
])
139+
]),
140+
h('page', { style: { position: 'relative'} }, [this.$slots.default])
141+
])
142+
}
143+
}
144+
</script>
145+
146+
<style scoped>
147+
.mpx-page-warp {
148+
width: 100%;
149+
height: 100%;
150+
box-sizing: border-box;
151+
}
152+
153+
154+
.mpx-titlebar--hidden {
155+
display: none;
156+
}
157+
158+
.mpx-titlebar__safe {
159+
padding-top: var(--safe-area-inset-top);
160+
}
161+
162+
.mpx-titlebar__inner {
163+
display: flex;
164+
align-items: center;
165+
justify-content: space-between;
166+
padding: 0 12px;
167+
box-sizing: border-box;
168+
}
169+
170+
.mpx-titlebar__left,
171+
.mpx-titlebar__right {
172+
flex: 0 0 auto;
173+
min-width: 44px;
174+
display: flex;
175+
align-items: center;
176+
}
177+
178+
.mpx-titlebar__center {
179+
flex: 1 1 auto;
180+
display: flex;
181+
align-items: center;
182+
justify-content: center;
183+
overflow: hidden;
184+
padding: 0 8px;
185+
}
186+
187+
.mpx-titlebar__title {
188+
font-size: 17px;
189+
white-space: nowrap;
190+
text-overflow: ellipsis;
191+
overflow: hidden;
192+
font-weight: 500;
193+
}
194+
195+
.mpx-titlebar__back {
196+
background: none;
197+
border: none;
198+
font-size: 20px;
199+
color: inherit;
200+
padding: 6px;
201+
cursor: pointer;
202+
}
203+
204+
.mpx-titlebar {
205+
/* flex-shrink: 0; */
206+
height: calc(var(--safe-area-inset-top) + var(--titlebar-height));
207+
width: 100%;
208+
box-sizing: border-box;
209+
-webkit-font-smoothing: antialiased;
210+
position: fixed;
211+
top: 0;
212+
left: 0;
213+
right: 0;
214+
z-index: 99999;
215+
/* 脱离上下文,单独渲染加速,修复安卓低端机型在页面滚动时titlebar闪烁问题 */
216+
transform: translateZ(0);
217+
}
218+
219+
.mpx-titlebar__content {
220+
flex: 1;
221+
overflow: hidden;
222+
transform: translateZ(0);
223+
}
224+
225+
.mpx-titlebar__scroll {
226+
width: 100%;
227+
height: 100%;
228+
overflow: auto;
229+
}
230+
231+
/* SVG icon sizing and inherit color */
232+
.mpx-titlebar__back svg {
233+
display: block;
234+
width: 20px;
235+
height: 20px;
236+
}
237+
238+
.mpx-titlebar__back path {
239+
stroke: currentColor;
240+
}
241+
</style>

packages/webpack-plugin/lib/web/processTemplate.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
const templateCompiler = require('../template-compiler/compiler')
22
const genComponentTag = require('../utils/gen-component-tag')
33
const addQuery = require('../utils/add-query')
4+
const normalize = require('../utils/normalize')
45
const parseRequest = require('../utils/parse-request')
56
const { matchCondition } = require('../utils/match-condition')
67
const { getWxTemplateComponentName, serializeWxTemplateDefinition, buildWebTemplateImportMergeExpr } = require('./template-shared')
78

9+
const titleBarPath = normalize.lib('runtime/components/web/mpx-titlebar.vue')
10+
811
module.exports = function (template, {
912
loaderContext,
1013
hasScoped,
@@ -161,6 +164,15 @@ module.exports = function (template, {
161164
}
162165
}
163166

167+
// Page 在 web 模式下插入 titlebar 组件
168+
if (ctorType === 'page') {
169+
builtInComponentsMap['mpx-titlebar'] = {
170+
resource: addQuery(titleBarPath, { isComponent: true })
171+
}
172+
const serialized = templateCompiler.serialize(root)
173+
return `<mpx-titlebar :page-config="$options.__mpxPageConfig">${serialized}</mpx-titlebar>`
174+
}
175+
164176
return templateCompiler.serialize(root)
165177
}
166178
})

0 commit comments

Comments
 (0)