hookehuyr

feat(底部导航): 重构小程序 WebView 底部导航组件

- 新增 JlsBottomNav 组件目录,包含配置加载、状态管理、预览工具等模块
- 统一底部导航数据源,支持 API 动态配置与本地 mock 预览
- 重构 activeTab 解析逻辑,兼容新旧 key 映射关系
- 优化组件样式,支持滚动布局与响应式设计
- 更新组件类型声明,移除未使用的 Element Plus 组件
...@@ -13,10 +13,6 @@ declare module '@vue/runtime-core' { ...@@ -13,10 +13,6 @@ declare module '@vue/runtime-core' {
13 AudioBackground1: typeof import('./src/components/audioBackground1.vue')['default'] 13 AudioBackground1: typeof import('./src/components/audioBackground1.vue')['default']
14 AudioList: typeof import('./src/components/audioList.vue')['default'] 14 AudioList: typeof import('./src/components/audioList.vue')['default']
15 BottomNav: typeof import('./src/components/BottomNav.vue')['default'] 15 BottomNav: typeof import('./src/components/BottomNav.vue')['default']
16 - ElButton: typeof import('element-plus/es')['ElButton']
17 - ElInput: typeof import('element-plus/es')['ElInput']
18 - ElOption: typeof import('element-plus/es')['ElOption']
19 - ElSelect: typeof import('element-plus/es')['ElSelect']
20 Floor: typeof import('./src/components/Floor/index.vue')['default'] 16 Floor: typeof import('./src/components/Floor/index.vue')['default']
21 InfoPopup: typeof import('./src/components/InfoPopup.vue')['default'] 17 InfoPopup: typeof import('./src/components/InfoPopup.vue')['default']
22 InfoPopupLite: typeof import('./src/components/InfoPopupLite.vue')['default'] 18 InfoPopupLite: typeof import('./src/components/InfoPopupLite.vue')['default']
...@@ -25,25 +21,20 @@ declare module '@vue/runtime-core' { ...@@ -25,25 +21,20 @@ declare module '@vue/runtime-core' {
25 InfoWindowLite: typeof import('./src/components/InfoWindowLite.vue')['default'] 21 InfoWindowLite: typeof import('./src/components/InfoWindowLite.vue')['default']
26 InfoWindowWarn: typeof import('./src/components/InfoWindowWarn.vue')['default'] 22 InfoWindowWarn: typeof import('./src/components/InfoWindowWarn.vue')['default']
27 InfoWindowYard: typeof import('./src/components/InfoWindowYard.vue')['default'] 23 InfoWindowYard: typeof import('./src/components/InfoWindowYard.vue')['default']
28 - JlsBottomNav: typeof import('./src/components/JlsBottomNav.vue')['default'] 24 + JlsBottomNav: typeof import('./src/components/JlsBottomNav/JlsBottomNav.vue')['default']
29 RouterLink: typeof import('vue-router')['RouterLink'] 25 RouterLink: typeof import('vue-router')['RouterLink']
30 RouterView: typeof import('vue-router')['RouterView'] 26 RouterView: typeof import('vue-router')['RouterView']
31 SvgIcon: typeof import('./src/components/Floor/svgIcon.vue')['default'] 27 SvgIcon: typeof import('./src/components/Floor/svgIcon.vue')['default']
32 VanBackTop: typeof import('vant/es')['BackTop'] 28 VanBackTop: typeof import('vant/es')['BackTop']
33 VanButton: typeof import('vant/es')['Button'] 29 VanButton: typeof import('vant/es')['Button']
34 - VanCol: typeof import('vant/es')['Col']
35 VanConfigProvider: typeof import('vant/es')['ConfigProvider'] 30 VanConfigProvider: typeof import('vant/es')['ConfigProvider']
36 VanDialog: typeof import('vant/es')['Dialog'] 31 VanDialog: typeof import('vant/es')['Dialog']
37 - VanField: typeof import('vant/es')['Field']
38 VanFloatingPanel: typeof import('vant/es')['FloatingPanel'] 32 VanFloatingPanel: typeof import('vant/es')['FloatingPanel']
39 VanIcon: typeof import('vant/es')['Icon'] 33 VanIcon: typeof import('vant/es')['Icon']
40 VanImage: typeof import('vant/es')['Image'] 34 VanImage: typeof import('vant/es')['Image']
41 VanImagePreview: typeof import('vant/es')['ImagePreview'] 35 VanImagePreview: typeof import('vant/es')['ImagePreview']
42 VanLoading: typeof import('vant/es')['Loading'] 36 VanLoading: typeof import('vant/es')['Loading']
43 - VanOverlay: typeof import('vant/es')['Overlay']
44 VanPopup: typeof import('vant/es')['Popup'] 37 VanPopup: typeof import('vant/es')['Popup']
45 - VanRow: typeof import('vant/es')['Row']
46 - VanSlider: typeof import('vant/es')['Slider']
47 VanSwipe: typeof import('vant/es')['Swipe'] 38 VanSwipe: typeof import('vant/es')['Swipe']
48 VanSwipeItem: typeof import('vant/es')['SwipeItem'] 39 VanSwipeItem: typeof import('vant/es')['SwipeItem']
49 VanTab: typeof import('vant/es')['Tab'] 40 VanTab: typeof import('vant/es')['Tab']
......
1 -<template>
2 - <div v-if="isMiniProgramWebView" class="bottom-nav jls-bottom-nav">
3 - <div
4 - v-for="item in navItems"
5 - :key="item.path"
6 - @click="navigate(item.path)"
7 - :class="['nav-item', isActive(item.name) ? 'nav-item-active' : 'nav-item-inactive']"
8 - >
9 - <div class="nav-item-inner">
10 - <div class="nav-icon">
11 - <component :is="item.icon" size="18" :color="getIconColor(item.name)" />
12 - </div>
13 - <span class="nav-label">{{ item.label }}</span>
14 - </div>
15 - </div>
16 - </div>
17 -</template>
18 -
19 -<script setup>
20 -import { computed } from 'vue';
21 -import { useRoute } from 'vue-router';
22 -import HomeIcon from '@nutui/icons-vue/dist/es/icons/Home.js';
23 -import MessageIcon from '@nutui/icons-vue/dist/es/icons/Message.js';
24 -import MyIcon from '@nutui/icons-vue/dist/es/icons/My.js';
25 -import wx from 'weixin-js-sdk';
26 -import { resolveCheckinActiveTab } from '@/views/checkin/nav-mode.js';
27 -
28 -const ACTIVE_COLOR = '#a67939';
29 -const INACTIVE_COLOR = '#8b95a7';
30 -
31 -const route = useRoute();
32 -
33 -const isMiniProgramWebView = computed(() => {
34 - return navigator.userAgent.includes('miniProgram');
35 -});
36 -
37 -const navItems = [
38 - { name: 'home', path: '/pages/index/index', icon: HomeIcon, label: '首页' },
39 - { name: 'message', path: '/pages/message/index', icon: MessageIcon, label: '消息' },
40 - { name: 'mine', path: '/pages/mine/index', icon: MyIcon, label: '我的' },
41 -];
42 -
43 -const currentActiveTab = computed(() => {
44 - return resolveCheckinActiveTab(route.query || {});
45 -});
46 -
47 -const isActive = (name) => {
48 - return currentActiveTab.value === name;
49 -};
50 -
51 -const getIconColor = (name) => {
52 - return isActive(name) ? ACTIVE_COLOR : INACTIVE_COLOR;
53 -};
54 -
55 -const navigate = (path) => {
56 - if (!path) {
57 - return;
58 - }
59 -
60 - if (wx?.miniProgram?.redirectTo) {
61 - wx.miniProgram.redirectTo({
62 - url: path,
63 - fail: () => {
64 - wx?.miniProgram?.reLaunch?.({ url: path });
65 - },
66 - });
67 - return;
68 - }
69 -
70 - wx?.miniProgram?.reLaunch?.({ url: path });
71 -};
72 -</script>
73 -
74 -<style scoped>
75 -.bottom-nav {
76 - position: fixed;
77 - bottom: 0;
78 - left: 0;
79 - right: 0;
80 - display: flex;
81 - align-items: stretch;
82 - justify-content: space-around;
83 - box-sizing: border-box;
84 - height: 80px;
85 - padding: 8px 12px;
86 - z-index: 50;
87 - min-height: 80px;
88 - background: rgba(255, 255, 255, 0.98);
89 - backdrop-filter: blur(6px);
90 -}
91 -
92 -.nav-item {
93 - display: flex;
94 - flex: 1;
95 - min-width: 0;
96 - align-items: stretch;
97 - justify-content: center;
98 - padding: 0;
99 - position: relative;
100 - cursor: pointer;
101 - transition: all 0.2s ease;
102 -}
103 -
104 -.nav-item-inner {
105 - display: flex;
106 - flex-direction: column;
107 - align-items: center;
108 - justify-content: center;
109 - width: 100%;
110 - height: 100%;
111 - gap: 5px;
112 - border-radius: 10px;
113 - color: #8b95a7;
114 - transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
115 -}
116 -
117 -.nav-item-active {
118 - color: #a67939;
119 -}
120 -
121 -.nav-item-inactive {
122 - color: #8b95a7;
123 -}
124 -
125 -.nav-icon {
126 - display: flex;
127 - align-items: center;
128 - justify-content: center;
129 - width: 18px;
130 - height: 18px;
131 - line-height: 1;
132 - transition: transform 0.2s ease;
133 -}
134 -
135 -.nav-label {
136 - font-size: 11px;
137 - margin-top: 0;
138 - line-height: 1.2;
139 - font-weight: 600;
140 -}
141 -
142 -.jls-bottom-nav .nav-item-active .nav-item-inner {
143 - color: #a67939;
144 -}
145 -
146 -@media screen and (min-width: 768px) {
147 - .bottom-nav {
148 - height: 80px;
149 - min-height: 80px;
150 - padding: 8px 16px;
151 - }
152 -
153 - .nav-item-inner {
154 - gap: 6px;
155 - }
156 -
157 - .nav-icon {
158 - width: 20px;
159 - height: 20px;
160 - }
161 -
162 - .nav-label {
163 - font-size: 12px;
164 - }
165 -}
166 -
167 -@media (hover: hover) {
168 - .nav-item:hover .nav-item-inner {
169 - transform: translateY(-1px);
170 - }
171 -}
172 -</style>
1 +<template>
2 + <div v-if="visible" class="jls-bottom-nav">
3 + <div ref="panelRef" class="jls-bottom-nav__panel" @scroll="syncScrollState">
4 + <div class="jls-bottom-nav__content" :class="{ 'is-scrollable': isScrollable }">
5 + <button
6 + v-for="item in tabItems"
7 + :key="item.key"
8 + type="button"
9 + class="jls-bottom-nav__item"
10 + :class="{ 'is-active': isActive(item.key), 'is-scrollable': isScrollable }"
11 + :style="itemStyle"
12 + @click="navigate(item)"
13 + >
14 + <span class="jls-bottom-nav__item-inner">
15 + <span class="jls-bottom-nav__icon">
16 + <i class="jls-bottom-nav__icon-font" :class="getIconClasses(item)" aria-hidden="true"></i>
17 + </span>
18 + <span class="jls-bottom-nav__label">{{ item.title }}</span>
19 + </span>
20 + </button>
21 + </div>
22 + </div>
23 +
24 + <div v-if="showScrollFade" class="jls-bottom-nav__fade" />
25 + </div>
26 +</template>
27 +
28 +<script setup>
29 +import { computed, nextTick, onMounted, ref, watch } from 'vue';
30 +import { useRoute } from 'vue-router';
31 +import wx from 'weixin-js-sdk';
32 +import './iconfont.css';
33 +import { useJlsTabbar } from './useTabbar';
34 +import { getTabbarIconClasses } from './tabbar.utils';
35 +import { resolveJlsCheckinActiveTab } from './nav-state';
36 +
37 +const SCROLLABLE_ITEM_WIDTH = 92;
38 +const defaultLoadOptions = Object.freeze({
39 + useMock: false,
40 + mockData: null,
41 +});
42 +
43 +const props = defineProps({
44 + visible: {
45 + type: Boolean,
46 + default: false,
47 + },
48 + loadOptions: {
49 + type: Object,
50 + default: () => ({
51 + useMock: false,
52 + mockData: null,
53 + }),
54 + },
55 +});
56 +
57 +const route = useRoute();
58 +const panelRef = ref(null);
59 +const isScrolledToEnd = ref(false);
60 +const tabbar = useJlsTabbar();
61 +
62 +const tabItems = computed(() => tabbar.visibleTabItems.value);
63 +const isScrollable = computed(() => tabItems.value.length > 4);
64 +const itemStyle = computed(() => {
65 + if (!isScrollable.value) {
66 + return null;
67 + }
68 +
69 + return {
70 + width: `${SCROLLABLE_ITEM_WIDTH}px`,
71 + minWidth: `${SCROLLABLE_ITEM_WIDTH}px`,
72 + };
73 +});
74 +const showScrollFade = computed(() => isScrollable.value && !isScrolledToEnd.value);
75 +const currentActiveTab = computed(() => resolveJlsCheckinActiveTab(route.query || {}));
76 +
77 +const isActive = (key) => key === currentActiveTab.value;
78 +
79 +const getIconClasses = (item) => getTabbarIconClasses(item);
80 +
81 +const syncScrollState = () => {
82 + const panel = panelRef.value;
83 +
84 + if (!panel || !isScrollable.value) {
85 + isScrolledToEnd.value = false;
86 + return;
87 + }
88 +
89 + const maxScrollLeft = Math.max(panel.scrollWidth - panel.clientWidth, 0);
90 + isScrolledToEnd.value = panel.scrollLeft >= maxScrollLeft - 4;
91 +};
92 +
93 +const navigate = (item) => {
94 + const targetUrl = tabbar.resolveTargetUrl(item);
95 +
96 + if (!targetUrl || isActive(item?.key)) {
97 + return;
98 + }
99 +
100 + if (wx?.miniProgram?.redirectTo) {
101 + wx.miniProgram.redirectTo({
102 + url: targetUrl,
103 + fail: () => {
104 + wx?.miniProgram?.reLaunch?.({ url: targetUrl });
105 + },
106 + });
107 + return;
108 + }
109 +
110 + wx?.miniProgram?.reLaunch?.({ url: targetUrl });
111 +};
112 +
113 +watch(
114 + () => tabItems.value.length,
115 + async () => {
116 + await nextTick();
117 + syncScrollState();
118 + },
119 + { immediate: true },
120 +);
121 +
122 +onMounted(async () => {
123 + if (!props.visible) {
124 + return;
125 + }
126 +
127 + await tabbar.ensureLoaded(props.loadOptions || defaultLoadOptions);
128 + await nextTick();
129 + syncScrollState();
130 +});
131 +</script>
132 +
133 +<style scoped>
134 +.jls-bottom-nav {
135 + position: fixed;
136 + bottom: 0;
137 + left: 0;
138 + right: 0;
139 + z-index: 50;
140 + height: 80px;
141 + min-height: 80px;
142 + background: rgba(255, 255, 255, 0.98);
143 + border-top: 1px solid rgba(166, 121, 57, 0.12);
144 + backdrop-filter: blur(6px);
145 + overflow: hidden;
146 +}
147 +
148 +.jls-bottom-nav__panel {
149 + height: 100%;
150 + overflow-x: auto;
151 + overflow-y: hidden;
152 + scrollbar-width: none;
153 + -ms-overflow-style: none;
154 +}
155 +
156 +.jls-bottom-nav__panel::-webkit-scrollbar {
157 + display: none;
158 +}
159 +
160 +.jls-bottom-nav__content {
161 + display: flex;
162 + align-items: stretch;
163 + justify-content: space-around;
164 + box-sizing: border-box;
165 + height: 100%;
166 + padding: 8px 12px;
167 +}
168 +
169 +.jls-bottom-nav__content.is-scrollable {
170 + justify-content: flex-start;
171 + width: max-content;
172 + min-width: 100%;
173 +}
174 +
175 +.jls-bottom-nav__item {
176 + display: flex;
177 + flex: 1;
178 + min-width: 0;
179 + align-items: stretch;
180 + justify-content: center;
181 + padding: 0;
182 + border: 0;
183 + background: transparent;
184 + color: #8b95a7;
185 + cursor: pointer;
186 +}
187 +
188 +.jls-bottom-nav__item.is-scrollable {
189 + flex: 0 0 auto;
190 +}
191 +
192 +.jls-bottom-nav__item-inner {
193 + display: flex;
194 + flex-direction: column;
195 + align-items: center;
196 + justify-content: center;
197 + width: 100%;
198 + height: 100%;
199 + gap: 5px;
200 + border-radius: 10px;
201 + color: inherit;
202 + transition: color 0.2s ease, transform 0.2s ease;
203 +}
204 +
205 +.jls-bottom-nav__item.is-active {
206 + color: #a67939;
207 +}
208 +
209 +.jls-bottom-nav__icon {
210 + display: flex;
211 + align-items: center;
212 + justify-content: center;
213 + width: 18px;
214 + height: 18px;
215 + line-height: 1;
216 +}
217 +
218 +.jls-bottom-nav__icon-font {
219 + font-size: 18px;
220 + line-height: 1;
221 +}
222 +
223 +.jls-bottom-nav__label {
224 + font-size: 11px;
225 + line-height: 1.2;
226 + font-weight: 600;
227 +}
228 +
229 +.jls-bottom-nav__fade {
230 + position: absolute;
231 + top: 0;
232 + right: 0;
233 + bottom: 0;
234 + width: 48px;
235 + pointer-events: none;
236 + background: linear-gradient(
237 + 90deg,
238 + rgba(255, 255, 255, 0) 0%,
239 + rgba(255, 255, 255, 0.36) 42%,
240 + rgba(255, 255, 255, 0.98) 100%
241 + );
242 +}
243 +
244 +@media screen and (min-width: 768px) {
245 + .jls-bottom-nav__content {
246 + padding: 8px 16px;
247 + }
248 +
249 + .jls-bottom-nav__item-inner {
250 + gap: 6px;
251 + }
252 +
253 + .jls-bottom-nav__icon {
254 + width: 20px;
255 + height: 20px;
256 + }
257 +
258 + .jls-bottom-nav__icon-font {
259 + font-size: 20px;
260 + }
261 +
262 + .jls-bottom-nav__label {
263 + font-size: 12px;
264 + }
265 +}
266 +
267 +@media (hover: hover) {
268 + .jls-bottom-nav__item:hover .jls-bottom-nav__item-inner {
269 + transform: translateY(-1px);
270 + }
271 +}
272 +</style>
1 +# JlsBottomNav
2 +
3 +面向小程序 WebView 场景的底部导航组件,H5 端保留当前 `80px` 高度体系,并同步小程序侧的底栏数据来源、显示规则和 icon 使用方式。
4 +
5 +## 文件说明
6 +
7 +- [JlsBottomNav.vue](/Users/huyirui/program/itomix/git/map-demo/src/components/JlsBottomNav/JlsBottomNav.vue)
8 + 组件主文件,只负责渲染、激活态、高亮、跳转和滚动表现。
9 +- [tabbar.api.js](/Users/huyirui/program/itomix/git/map-demo/src/components/JlsBottomNav/tabbar.api.js)
10 + 底栏配置接口请求,当前使用 `/srv/?a=app_menu`
11 +- [tabbar.utils.js](/Users/huyirui/program/itomix/git/map-demo/src/components/JlsBottomNav/tabbar.utils.js)
12 + 底栏数据归一化、默认项、顺序、icon class 解析、目标地址解析。
13 +- [useTabbar.js](/Users/huyirui/program/itomix/git/map-demo/src/components/JlsBottomNav/useTabbar.js)
14 + 组件内状态和加载逻辑,支持 `api` / `mock` 两种加载模式。
15 +- [nav-state.js](/Users/huyirui/program/itomix/git/map-demo/src/components/JlsBottomNav/nav-state.js)
16 + `activeTab` 的新旧 key 兼容规则。
17 +- [iconfont.css](/Users/huyirui/program/itomix/git/map-demo/src/components/JlsBottomNav/iconfont.css)
18 + 小程序侧 `iconfont` 字体定义。
19 +- [preview.js](/Users/huyirui/program/itomix/git/map-demo/src/components/JlsBottomNav/preview.js)
20 + 预览辅助层,用于把路由 query 解析成预览参数。
21 +- [tabbar.mock.js](/Users/huyirui/program/itomix/git/map-demo/src/components/JlsBottomNav/tabbar.mock.js)
22 + 预览用 mock 数据。
23 +- [index.js](/Users/huyirui/program/itomix/git/map-demo/src/components/JlsBottomNav/index.js)
24 + 目录入口。
25 +
26 +## 正式接入
27 +
28 +### 1. 引入组件
29 +
30 +```js
31 +import JlsBottomNav from '@/components/JlsBottomNav';
32 +```
33 +
34 +### 2. 页面里控制显示
35 +
36 +```vue
37 +<JlsBottomNav
38 + :visible="shouldShowJlsBottomNav"
39 + :load-options="{
40 + useMock: false,
41 + mockData: null,
42 + }"
43 +/>
44 +```
45 +
46 +`visible``true` 时组件才会渲染并尝试加载数据。
47 +
48 +### 3. 激活态
49 +
50 +组件通过当前路由的 `activeTab` 识别高亮项,兼容以下写法:
51 +
52 +- `home`
53 +- `message` / `news`
54 +- `application` / `list`
55 +- `mine` / `user`
56 +
57 +例如:
58 +
59 +```txt
60 +?navMode=jls&activeTab=home
61 +?navMode=jls&activeTab=news
62 +?navMode=jls&activeTab=list
63 +?navMode=jls&activeTab=user
64 +```
65 +
66 +## Icon 规则
67 +
68 +组件和小程序保持一致:
69 +
70 +- `fa-` 开头:按 `font-awesome` 渲染
71 +- 其他 class:按 `iconfont` 渲染
72 +
73 +示例:
74 +
75 +```js
76 +{ icon: 'fa-home' }
77 +{ icon: 'icon-shijian' }
78 +```
79 +
80 +当前仓库已全局引入 `font-awesome`。如果移植到别的项目,对方项目也需要提供 `font-awesome`,或者自行改成别的图标方案。
81 +
82 +## 预览与 Mock
83 +
84 +如果只是本地看效果,可以配合 [preview.js](/Users/huyirui/program/itomix/git/map-demo/src/components/JlsBottomNav/preview.js) 使用。
85 +
86 +当前项目里的用法是:
87 +
88 +```js
89 +import { getJlsTabbarPreviewOptions } from '@/components/JlsBottomNav/preview';
90 +```
91 +
92 +它会读取:
93 +
94 +- `jlsTabbarPreview=1`
95 +- `jlsTabbarMock=1`
96 +
97 +并转换成组件需要的 `visible` / `loadOptions`
98 +
99 +示例地址:
100 +
101 +```txt
102 +/#/checkin/?navMode=jls&activeTab=home&jlsTabbarPreview=1&jlsTabbarMock=1
103 +```
104 +
105 +## 移植建议
106 +
107 +### 最小正式版本
108 +
109 +如果对方项目不需要预览辅助,建议带走这些文件:
110 +
111 +- `JlsBottomNav.vue`
112 +- `tabbar.api.js`
113 +- `tabbar.utils.js`
114 +- `useTabbar.js`
115 +- `nav-state.js`
116 +- `iconfont.css`
117 +- `index.js`
118 +
119 +### 可选开发辅助
120 +
121 +如果对方也希望保留本地预览,再额外带走:
122 +
123 +- `preview.js`
124 +- `tabbar.mock.js`
125 +
126 +## 注意点
127 +
128 +- 组件默认按小程序 WebView 跳转方式调用 `wx.miniProgram.redirectTo / reLaunch`
129 +- 当前跳转逻辑是:
130 + - `home` 默认跳回小程序页 `/pages/index/index`
131 + - 其他项不会直接跳裸 H5 URL,而是优先使用接口返回的 `page_url`
132 + - 如果接口没有返回 `page_url`,则会把 `webview_url` 包装成小程序承接页地址,例如 `/pages/webview-preview/index?url=...`
133 +- 也就是说,当前实现仍然是“首页回小程序页,其他项走小程序承接页再打开 H5”,不是“其他项直接在当前 H5 中跳转到接口 URL”。
134 +- 当前样式尺寸以 H5 版本为准,没有照搬小程序 `rpx` 布局。
1 +@font-face {
2 + font-family: 'iconfont';
3 + src:
4 + url('https://at.alicdn.com/t/c/font_4618760_7lrp6nlwv9y.woff2?t=1778124018667') format('woff2'),
5 + url('https://at.alicdn.com/t/c/font_4618760_7lrp6nlwv9y.woff?t=1778124018667') format('woff'),
6 + url('https://at.alicdn.com/t/c/font_4618760_7lrp6nlwv9y.ttf?t=1778124018667') format('truetype');
7 +}
8 +
9 +.iconfont {
10 + font-family: 'iconfont' !important;
11 + font-size: 16px;
12 + font-style: normal;
13 + -webkit-font-smoothing: antialiased;
14 + -moz-osx-font-smoothing: grayscale;
15 +}
16 +
17 +.icon-zanwuneirong:before {
18 + content: '\e64b';
19 +}
20 +
21 +.icon-zhiribiao:before {
22 + content: '\e6c2';
23 +}
24 +
25 +.icon-yanzhengma:before {
26 + content: '\e6c1';
27 +}
28 +
29 +.icon-changdigailan:before {
30 + content: '\e6bf';
31 +}
32 +
33 +.icon-queren:before {
34 + content: '\e6c0';
35 +}
36 +
37 +.icon-jichengmingdanguanli:before {
38 + content: '\e6bb';
39 +}
40 +
41 +.icon-shenqingbiao:before {
42 + content: '\e6bc';
43 +}
44 +
45 +.icon-weixintongzhimoban:before {
46 + content: '\e6bd';
47 +}
48 +
49 +.icon-gangweihuizong:before {
50 + content: '\e6be';
51 +}
52 +
53 +.icon-mima:before {
54 + content: '\e6b5';
55 +}
56 +
57 +.icon-shoucang:before {
58 + content: '\e6b6';
59 +}
60 +
61 +.icon-daiban:before {
62 + content: '\e6b7';
63 +}
64 +
65 +.icon-faqi:before {
66 + content: '\e6b8';
67 +}
68 +
69 +.icon-chaosong:before {
70 + content: '\e6b9';
71 +}
72 +
73 +.icon-geren:before {
74 + content: '\e6ba';
75 +}
76 +
77 +.icon-zhankai:before {
78 + content: '\e6b0';
79 +}
80 +
81 +.icon-fanhui:before {
82 + content: '\e6b1';
83 +}
84 +
85 +.icon-guanbi:before {
86 + content: '\e6b2';
87 +}
88 +
89 +.icon-guan:before {
90 + content: '\e6b3';
91 +}
92 +
93 +.icon-kai:before {
94 + content: '\e6b4';
95 +}
96 +
97 +.icon-bianji:before {
98 + content: '\e6a8';
99 +}
100 +
101 +.icon-tianjia:before {
102 + content: '\e6a9';
103 +}
104 +
105 +.icon-gouwuche:before {
106 + content: '\e6aa';
107 +}
108 +
109 +.icon-rili:before {
110 + content: '\e6ab';
111 +}
112 +
113 +.icon-shanchu:before {
114 + content: '\e6ac';
115 +}
116 +
117 +.icon-dianhua:before {
118 + content: '\e6ad';
119 +}
120 +
121 +.icon-right:before {
122 + content: '\e6ae';
123 +}
124 +
125 +.icon-dui:before {
126 + content: '\e6af';
127 +}
128 +
129 +.icon-left:before {
130 + content: '\e6a7';
131 +}
132 +
133 +.icon-nv-01:before {
134 + content: '\e6a5';
135 +}
136 +
137 +.icon-nan-01:before {
138 + content: '\e6a6';
139 +}
140 +
141 +.icon-sousuo:before {
142 + content: '\e69c';
143 +}
144 +
145 +.icon-nianfogongxiu:before {
146 + content: '\e69d';
147 +}
148 +
149 +.icon-qita:before {
150 + content: '\e69e';
151 +}
152 +
153 +.icon-huodonghaibao:before {
154 + content: '\e69f';
155 +}
156 +
157 +.icon-rusizhinan:before {
158 + content: '\e6a0';
159 +}
160 +
161 +.icon-renyuan:before {
162 + content: '\e6a1';
163 +}
164 +
165 +.icon-changdishenqing:before {
166 + content: '\e6a2';
167 +}
168 +
169 +.icon-huodongguanli:before {
170 + content: '\e6a3';
171 +}
172 +
173 +.icon-changguicanxue:before {
174 + content: '\e6a4';
175 +}
176 +
177 +.icon-yingyong:before {
178 + content: '\e68d';
179 +}
180 +
181 +.icon-renyuandingwei:before {
182 + content: '\e68e';
183 +}
184 +
185 +.icon-xiaoxi:before {
186 + content: '\e68f';
187 +}
188 +
189 +.icon-guiyigongxiu:before {
190 + content: '\e690';
191 +}
192 +
193 +.icon-wodebaoming:before {
194 + content: '\e691';
195 +}
196 +
197 +.icon-wodeyuyue:before {
198 + content: '\e692';
199 +}
200 +
201 +.icon-shaotouxiang:before {
202 + content: '\e693';
203 +}
204 +
205 +.icon-xinzhongmingdan:before {
206 + content: '\e694';
207 +}
208 +
209 +.icon-a-3Dloucengdaolan:before {
210 + content: '\e695';
211 +}
212 +
213 +.icon-huodongkanban:before {
214 + content: '\e696';
215 +}
216 +
217 +.icon-wode:before {
218 + content: '\e697';
219 +}
220 +
221 +.icon-dicangfahui:before {
222 + content: '\e698';
223 +}
224 +
225 +.icon-shijian:before {
226 + content: '\e699';
227 +}
228 +
229 +.icon-shouye:before {
230 + content: '\e69a';
231 +}
232 +
233 +.icon-nianduhuodongbiao:before {
234 + content: '\e69b';
235 +}
236 +
237 +.icon-yigongmingdan:before {
238 + content: '\e67f';
239 +}
240 +
241 +.icon-jingjinfoqi:before {
242 + content: '\e680';
243 +}
244 +
245 +.icon-yufojie:before {
246 + content: '\e681';
247 +}
248 +
249 +.icon-zhongyuanjie:before {
250 + content: '\e682';
251 +}
252 +
253 +.icon-shuhuazhan:before {
254 + content: '\e683';
255 +}
256 +
257 +.icon-zuchang:before {
258 + content: '\e684';
259 +}
260 +
261 +.icon-wuzishenqing:before {
262 + content: '\e685';
263 +}
264 +
265 +.icon-zuchangtongxunlu:before {
266 + content: '\e686';
267 +}
268 +
269 +.icon-zhusushenqing:before {
270 + content: '\e687';
271 +}
272 +
273 +.icon-shezhi:before {
274 + content: '\e688';
275 +}
276 +
277 +.icon-yijian:before {
278 + content: '\e689';
279 +}
280 +
281 +.icon-zhongyaoshebeibaojing:before {
282 + content: '\e68a';
283 +}
284 +
285 +.icon-yongcanshenqing:before {
286 + content: '\e68b';
287 +}
288 +
289 +.icon-shengtaifangsheng:before {
290 + content: '\e68c';
291 +}
292 +
293 +.icon-VRzhanshi:before {
294 + content: '\e67e';
295 +}
296 +
297 +.icon-ditudaolan:before {
298 + content: '\e677';
299 +}
300 +
301 +.icon-guanyindan:before {
302 + content: '\e678';
303 +}
304 +
305 +.icon-chanxiuying:before {
306 + content: '\e679';
307 +}
308 +
309 +.icon-sanguiwujie:before {
310 + content: '\e67a';
311 +}
312 +
313 +.icon-baguanzhaijie:before {
314 + content: '\e67b';
315 +}
316 +
317 +.icon-fangsheng:before {
318 + content: '\e67c';
319 +}
320 +
321 +.icon-jingxiuying:before {
322 + content: '\e67d';
323 +}
1 +export { default } from './JlsBottomNav.vue';
1 +export const JLS_CHECKIN_ACTIVE_TAB = {
2 + HOME: 'home',
3 + MESSAGE: 'news',
4 + APPLICATION: 'list',
5 + MINE: 'user',
6 +};
7 +
8 +const JLS_CHECKIN_ACTIVE_TAB_ALIAS_MAP = {
9 + home: JLS_CHECKIN_ACTIVE_TAB.HOME,
10 + message: JLS_CHECKIN_ACTIVE_TAB.MESSAGE,
11 + news: JLS_CHECKIN_ACTIVE_TAB.MESSAGE,
12 + application: JLS_CHECKIN_ACTIVE_TAB.APPLICATION,
13 + list: JLS_CHECKIN_ACTIVE_TAB.APPLICATION,
14 + mine: JLS_CHECKIN_ACTIVE_TAB.MINE,
15 + user: JLS_CHECKIN_ACTIVE_TAB.MINE,
16 +};
17 +
18 +export const resolveJlsCheckinActiveTab = (query = {}) => {
19 + const rawTab = Array.isArray(query.activeTab) ? query.activeTab[0] : query.activeTab;
20 + const normalizedTab = String(rawTab || '').trim().toLowerCase();
21 + return JLS_CHECKIN_ACTIVE_TAB_ALIAS_MAP[normalizedTab] || '';
22 +};
1 +import { getJlsTabbarMockData } from './tabbar.mock';
2 +
3 +const getQueryValue = (query = {}, key = '') => String(query?.[key] || '');
4 +
5 +export const getJlsTabbarPreviewOptions = (query = {}) => {
6 + const previewEnabled = getQueryValue(query, 'jlsTabbarPreview') === '1';
7 + const mockEnabled = getQueryValue(query, 'jlsTabbarMock') === '1';
8 +
9 + return {
10 + previewEnabled,
11 + loadOptions: mockEnabled
12 + ? {
13 + useMock: true,
14 + mockData: getJlsTabbarMockData(),
15 + }
16 + : {
17 + useMock: false,
18 + mockData: null,
19 + },
20 + };
21 +};
1 +import { fn, fetch } from '@/api/fn';
2 +
3 +const Api = {
4 + TABBAR_CONFIG: '/srv/?a=app_menu',
5 +};
6 +
7 +export const getTabbarConfigAPI = () => fn(fetch.get(Api.TABBAR_CONFIG));
1 +export const getJlsTabbarMockData = () => ({
2 + home: {
3 + title: '首页',
4 + icon: 'fa-home',
5 + link: '',
6 + },
7 + news: {
8 + title: '资讯',
9 + icon: 'fa-newspaper-o',
10 + link: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=news_list',
11 + },
12 + list: {
13 + title: '应用',
14 + icon: 'fa-th-large',
15 + link: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list',
16 + },
17 + user: {
18 + title: '我的',
19 + icon: 'fa-user',
20 + link: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list',
21 + },
22 + calendar: {
23 + title: '日程',
24 + icon: 'icon-shijian',
25 + link: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=schedule',
26 + },
27 + board: {
28 + title: '看板',
29 + icon: 'icon-huodongkanban',
30 + link: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=dashboard',
31 + },
32 +});
1 +const TABBAR_KEY_ALIAS_MAP = {
2 + message: 'news',
3 + application: 'list',
4 + mine: 'user',
5 +};
6 +
7 +const KNOWN_TABBAR_ITEM_MAP = {
8 + home: {
9 + key: 'home',
10 + title: '首页',
11 + class: 'fa-home',
12 + visible: true,
13 + page_url: '/pages/index/index',
14 + webview_url: '',
15 + webview_title: '首页',
16 + },
17 + news: {
18 + key: 'news',
19 + title: '资讯',
20 + class: 'fa-newspaper-o',
21 + visible: true,
22 + page_url: '',
23 + webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=news_list',
24 + webview_title: '资讯',
25 + },
26 + list: {
27 + key: 'list',
28 + title: '应用',
29 + class: 'fa-th-large',
30 + visible: true,
31 + page_url: '',
32 + webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list',
33 + webview_title: '应用',
34 + },
35 + user: {
36 + key: 'user',
37 + title: '我的',
38 + class: 'fa-user',
39 + visible: true,
40 + page_url: '',
41 + webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list',
42 + webview_title: '我的',
43 + },
44 +};
45 +
46 +const TABBAR_ORDER = ['home', 'news', 'list', 'user'];
47 +const DEFAULT_ICON_CLASS = 'fa-circle-o';
48 +
49 +const normalizeVisibleValue = (rawValue, fallbackValue = true) => {
50 + if (typeof rawValue === 'boolean') {
51 + return rawValue;
52 + }
53 +
54 + if (rawValue === 1 || rawValue === '1') {
55 + return true;
56 + }
57 +
58 + if (rawValue === 0 || rawValue === '0') {
59 + return false;
60 + }
61 +
62 + return fallbackValue;
63 +};
64 +
65 +export const normalizeTabbarKey = (key) => {
66 + const normalizedKey = String(key || '').trim().toLowerCase();
67 + return TABBAR_KEY_ALIAS_MAP[normalizedKey] || normalizedKey;
68 +};
69 +
70 +export const buildWebviewPreviewUrl = (targetUrl = '', pageTitle = '') => {
71 + const normalizedUrl = String(targetUrl || '').trim();
72 + const normalizedTitle = String(pageTitle || '').trim();
73 +
74 + if (!normalizedUrl) {
75 + return '/pages/webview-preview/index';
76 + }
77 +
78 + const queryList = [`url=${encodeURIComponent(normalizedUrl)}`];
79 +
80 + if (normalizedTitle) {
81 + queryList.push(`title=${encodeURIComponent(normalizedTitle)}`);
82 + }
83 +
84 + return `/pages/webview-preview/index?${queryList.join('&')}`;
85 +};
86 +
87 +const createFallbackTabbarItem = (key, title = '') => {
88 + const normalizedKey = normalizeTabbarKey(key);
89 +
90 + return {
91 + key: normalizedKey,
92 + title: title || '栏目',
93 + class: DEFAULT_ICON_CLASS,
94 + visible: true,
95 + page_url: normalizedKey === 'home' ? '/pages/index/index' : '',
96 + webview_url: '',
97 + webview_title: title || '栏目',
98 + };
99 +};
100 +
101 +export const getDefaultTabbarItem = (key) => {
102 + const normalizedKey = normalizeTabbarKey(key);
103 + const fallbackItem =
104 + KNOWN_TABBAR_ITEM_MAP[normalizedKey] || createFallbackTabbarItem(normalizedKey);
105 +
106 + return {
107 + ...fallbackItem,
108 + };
109 +};
110 +
111 +export const getDefaultTabbarItems = () =>
112 + TABBAR_ORDER.map((key) => getDefaultTabbarItem(key)).filter(Boolean);
113 +
114 +const buildTabbarPageUrl = (key, webviewUrl, webviewTitle, rawPageUrl = '') => {
115 + if (key === 'home') {
116 + return rawPageUrl || '/pages/index/index';
117 + }
118 +
119 + if (!webviewUrl) {
120 + return '';
121 + }
122 +
123 + return buildWebviewPreviewUrl(webviewUrl, webviewTitle);
124 +};
125 +
126 +export const normalizeTabbarItem = (rawItem = {}) => {
127 + const normalizedKey = normalizeTabbarKey(rawItem.key || rawItem.name);
128 + const fallbackItem = getDefaultTabbarItem(normalizedKey || rawItem.title);
129 + const title = String(rawItem.title || fallbackItem.title || '栏目');
130 + const webviewUrl = String(
131 + rawItem.webview_url ||
132 + rawItem.link_url ||
133 + rawItem.link ||
134 + rawItem.url ||
135 + fallbackItem.webview_url ||
136 + '',
137 + );
138 + const webviewTitle = String(
139 + rawItem.webview_title ||
140 + rawItem.link_title ||
141 + rawItem.title ||
142 + fallbackItem.webview_title ||
143 + title,
144 + );
145 +
146 + return {
147 + ...fallbackItem,
148 + ...rawItem,
149 + key: fallbackItem.key,
150 + title,
151 + class: String(
152 + rawItem.class ||
153 + rawItem.icon_class ||
154 + rawItem.icon ||
155 + fallbackItem.class ||
156 + DEFAULT_ICON_CLASS,
157 + ),
158 + webview_url: webviewUrl,
159 + webview_title: webviewTitle,
160 + page_url: buildTabbarPageUrl(
161 + fallbackItem.key,
162 + webviewUrl,
163 + webviewTitle,
164 + String(rawItem.page_url || fallbackItem.page_url || ''),
165 + ),
166 + visible: normalizeVisibleValue(
167 + rawItem.visible ?? rawItem.is_show ?? rawItem.show,
168 + fallbackItem.visible,
169 + ),
170 + };
171 +};
172 +
173 +const sortTabbarObjectEntries = (entries = []) => {
174 + const orderMap = TABBAR_ORDER.reduce((map, key, index) => {
175 + map[key] = index;
176 + return map;
177 + }, {});
178 +
179 + return entries
180 + .map((entry, index) => ({ entry, index }))
181 + .sort((left, right) => {
182 + const leftKey = normalizeTabbarKey(left.entry[0]);
183 + const rightKey = normalizeTabbarKey(right.entry[0]);
184 + const leftOrder = orderMap[leftKey];
185 + const rightOrder = orderMap[rightKey];
186 + const leftKnown = Number.isInteger(leftOrder);
187 + const rightKnown = Number.isInteger(rightOrder);
188 +
189 + if (leftKnown && rightKnown) {
190 + return leftOrder - rightOrder;
191 + }
192 +
193 + if (leftKnown) {
194 + return -1;
195 + }
196 +
197 + if (rightKnown) {
198 + return 1;
199 + }
200 +
201 + return left.index - right.index;
202 + })
203 + .map(({ entry }) => entry);
204 +};
205 +
206 +const normalizeTabbarObjectItems = (rawMap = {}) => {
207 + const objectEntries = Object.entries(rawMap).filter(
208 + ([, value]) => value && typeof value === 'object' && !Array.isArray(value),
209 + );
210 +
211 + if (!objectEntries.length) {
212 + return [];
213 + }
214 +
215 + return sortTabbarObjectEntries(objectEntries)
216 + .map(([key, value]) =>
217 + normalizeTabbarItem({
218 + ...value,
219 + key,
220 + }),
221 + )
222 + .filter(Boolean);
223 +};
224 +
225 +export const normalizeTabbarItems = (rawItems = []) => {
226 + if (!Array.isArray(rawItems) || !rawItems.length) {
227 + return getDefaultTabbarItems();
228 + }
229 +
230 + const normalizedItems = rawItems.map((item) => normalizeTabbarItem(item)).filter(Boolean);
231 +
232 + return normalizedItems.length ? normalizedItems : getDefaultTabbarItems();
233 +};
234 +
235 +export const normalizeTabbarPayload = (rawPayload = null) => {
236 + if (Array.isArray(rawPayload)) {
237 + return normalizeTabbarItems(rawPayload);
238 + }
239 +
240 + const arrayPayload = rawPayload?.tab_items || rawPayload?.tabs || rawPayload?.menus;
241 + if (Array.isArray(arrayPayload)) {
242 + return normalizeTabbarItems(arrayPayload);
243 + }
244 +
245 + const objectPayload = rawPayload?.tab_items || rawPayload?.tabs || rawPayload?.menus || rawPayload;
246 + const normalizedItems = normalizeTabbarObjectItems(objectPayload);
247 +
248 + return normalizedItems.length ? normalizedItems : getDefaultTabbarItems();
249 +};
250 +
251 +export const resolveTabbarTargetUrl = (item = {}) => {
252 + if (item?.page_url) {
253 + return item.page_url;
254 + }
255 +
256 + if (normalizeTabbarKey(item?.key) === 'home') {
257 + return '/pages/index/index';
258 + }
259 +
260 + return buildWebviewPreviewUrl(item?.webview_url, item?.webview_title || item?.title);
261 +};
262 +
263 +export const getTabbarIconClasses = (item = {}) => {
264 + const icon = String(item?.class || item?.icon || DEFAULT_ICON_CLASS).trim() || DEFAULT_ICON_CLASS;
265 + const fontClass = icon.startsWith('fa-') ? 'fa' : 'iconfont';
266 + return [fontClass, icon];
267 +};
1 +import { computed, reactive, readonly } from 'vue';
2 +import { getTabbarConfigAPI } from './tabbar.api';
3 +import {
4 + getDefaultTabbarItem,
5 + getDefaultTabbarItems,
6 + normalizeTabbarKey,
7 + normalizeTabbarPayload,
8 + resolveTabbarTargetUrl,
9 +} from './tabbar.utils';
10 +
11 +const state = reactive({
12 + tabItems: getDefaultTabbarItems(),
13 + loaded: false,
14 + loading: false,
15 + loadMode: 'api',
16 +});
17 +
18 +let tabbarRequestPromise = null;
19 +
20 +const applyFallbackItems = () => {
21 + state.tabItems = getDefaultTabbarItems();
22 +};
23 +
24 +const resolveLoadMode = (options = {}) => (options.useMock ? 'mock' : 'api');
25 +
26 +export const fetchTabbarConfig = async (force = false, options = {}) => {
27 + const loadMode = resolveLoadMode(options);
28 +
29 + if (!force && state.loaded && state.loadMode === loadMode) {
30 + return state.tabItems;
31 + }
32 +
33 + if (state.loading && tabbarRequestPromise) {
34 + return tabbarRequestPromise;
35 + }
36 +
37 + state.loading = true;
38 +
39 + tabbarRequestPromise = (async () => {
40 + try {
41 + if (loadMode === 'mock') {
42 + state.tabItems = normalizeTabbarPayload(options.mockData);
43 + state.loadMode = loadMode;
44 + return state.tabItems;
45 + }
46 +
47 + const response = await getTabbarConfigAPI();
48 +
49 + if (response?.code === 1) {
50 + state.tabItems = normalizeTabbarPayload(response?.data);
51 + } else {
52 + applyFallbackItems();
53 + }
54 +
55 + state.loadMode = loadMode;
56 + } catch (error) {
57 + console.error('获取底部导航配置失败:', error);
58 + applyFallbackItems();
59 + state.loadMode = loadMode;
60 + } finally {
61 + state.loaded = true;
62 + state.loading = false;
63 + tabbarRequestPromise = null;
64 + }
65 +
66 + return state.tabItems;
67 + })();
68 +
69 + return tabbarRequestPromise;
70 +};
71 +
72 +export const ensureTabbarLoaded = (options = {}) => {
73 + const loadMode = resolveLoadMode(options);
74 +
75 + if (state.loaded && state.loadMode === loadMode) {
76 + return Promise.resolve(state.tabItems);
77 + }
78 +
79 + return fetchTabbarConfig(false, options);
80 +};
81 +
82 +export const useJlsTabbar = () => {
83 + const visibleTabItems = computed(() => state.tabItems.filter((item) => item.visible !== false));
84 +
85 + const getTabItem = (key) => {
86 + const normalizedKey = normalizeTabbarKey(key);
87 + return state.tabItems.find((item) => item.key === normalizedKey) || getDefaultTabbarItem(normalizedKey);
88 + };
89 +
90 + return {
91 + state: readonly(state),
92 + visibleTabItems,
93 + getTabItem,
94 + ensureLoaded: ensureTabbarLoaded,
95 + fetchTabbarConfig,
96 + resolveTargetUrl: resolveTabbarTargetUrl,
97 + };
98 +};
...@@ -125,7 +125,11 @@ ...@@ -125,7 +125,11 @@
125 125
126 <!-- 底部导航组件 --> 126 <!-- 底部导航组件 -->
127 <BottomNav v-if="shouldShowLegacyBottomNav" /> 127 <BottomNav v-if="shouldShowLegacyBottomNav" />
128 - <JlsBottomNav v-else-if="shouldShowJlsBottomNav" /> 128 + <JlsBottomNav
129 + v-else-if="shouldShowJlsBottomNav"
130 + :visible="shouldShowJlsBottomNav"
131 + :load-options="jlsBottomNavLoadOptions"
132 + />
129 </div> 133 </div>
130 </template> 134 </template>
131 135
...@@ -142,13 +146,14 @@ import wx from 'weixin-js-sdk' ...@@ -142,13 +146,14 @@ import wx from 'weixin-js-sdk'
142 import pageInfo from '@/views/checkin/info.vue' 146 import pageInfo from '@/views/checkin/info.vue'
143 import audioBackground1 from '@/components/audioBackground1.vue' 147 import audioBackground1 from '@/components/audioBackground1.vue'
144 import BottomNav from '@/components/BottomNav.vue' 148 import BottomNav from '@/components/BottomNav.vue'
145 -import JlsBottomNav from '@/components/JlsBottomNav.vue' 149 +import JlsBottomNav from '@/components/JlsBottomNav'
146 import { mapState, mapActions } from 'pinia' 150 import { mapState, mapActions } from 'pinia'
147 import { mainStore } from '@/store' 151 import { mainStore } from '@/store'
148 import { parseQueryString, getAdaptiveFontSize, getAdaptivePadding } from '@/utils/tools' 152 import { parseQueryString, getAdaptiveFontSize, getAdaptivePadding } from '@/utils/tools'
149 import AMapLoader from '@amap/amap-jsapi-loader' 153 import AMapLoader from '@amap/amap-jsapi-loader'
150 import { mapAudioAPI } from '@/api/map.js' 154 import { mapAudioAPI } from '@/api/map.js'
151 import { CHECKIN_NAV_MODE, resolveCheckinNavMode, getCheckinMapHeight } from '@/views/checkin/nav-mode.js' 155 import { CHECKIN_NAV_MODE, resolveCheckinNavMode, getCheckinMapHeight } from '@/views/checkin/nav-mode.js'
156 +import { getJlsTabbarPreviewOptions } from '@/components/JlsBottomNav/preview'
152 157
153 // 地图缓存管理器 158 // 地图缓存管理器
154 class MapCacheManager { 159 class MapCacheManager {
...@@ -345,6 +350,12 @@ export default { ...@@ -345,6 +350,12 @@ export default {
345 isMiniProgramWebView() { 350 isMiniProgramWebView() {
346 return navigator.userAgent.includes('miniProgram'); 351 return navigator.userAgent.includes('miniProgram');
347 }, 352 },
353 + isJlsBottomNavPreview() {
354 + return getJlsTabbarPreviewOptions(this.$route?.query || {}).previewEnabled;
355 + },
356 + jlsBottomNavLoadOptions() {
357 + return getJlsTabbarPreviewOptions(this.$route?.query || {}).loadOptions;
358 + },
348 currentCheckinNavMode() { 359 currentCheckinNavMode() {
349 return resolveCheckinNavMode(this.$route?.query || {}); 360 return resolveCheckinNavMode(this.$route?.query || {});
350 }, 361 },
...@@ -352,7 +363,8 @@ export default { ...@@ -352,7 +363,8 @@ export default {
352 return this.isMiniProgramWebView && this.currentCheckinNavMode === CHECKIN_NAV_MODE.LEGACY; 363 return this.isMiniProgramWebView && this.currentCheckinNavMode === CHECKIN_NAV_MODE.LEGACY;
353 }, 364 },
354 shouldShowJlsBottomNav() { 365 shouldShowJlsBottomNav() {
355 - return this.isMiniProgramWebView && this.currentCheckinNavMode === CHECKIN_NAV_MODE.JLS; 366 + return (this.isMiniProgramWebView || this.isJlsBottomNavPreview)
367 + && this.currentCheckinNavMode === CHECKIN_NAV_MODE.JLS;
356 }, 368 },
357 /** 369 /**
358 * 动态计算地图高度 370 * 动态计算地图高度
...@@ -360,7 +372,7 @@ export default { ...@@ -360,7 +372,7 @@ export default {
360 */ 372 */
361 mapHeight() { 373 mapHeight() {
362 return getCheckinMapHeight({ 374 return getCheckinMapHeight({
363 - isMiniProgramWebView: this.isMiniProgramWebView, 375 + isMiniProgramWebView: this.isMiniProgramWebView || this.isJlsBottomNavPreview,
364 navMode: this.currentCheckinNavMode, 376 navMode: this.currentCheckinNavMode,
365 }); 377 });
366 }, 378 },
......
1 +import { JLS_CHECKIN_ACTIVE_TAB, resolveJlsCheckinActiveTab } from '@/components/JlsBottomNav/nav-state.js';
2 +
1 export const CHECKIN_NAV_MODE = { 3 export const CHECKIN_NAV_MODE = {
2 LEGACY: 'legacy', 4 LEGACY: 'legacy',
3 JLS: 'jls', 5 JLS: 'jls',
4 NONE: 'none', 6 NONE: 'none',
5 }; 7 };
6 8
7 -export const CHECKIN_ACTIVE_TAB = { 9 +export const CHECKIN_ACTIVE_TAB = JLS_CHECKIN_ACTIVE_TAB;
8 - HOME: 'home',
9 - MESSAGE: 'message',
10 - MINE: 'mine',
11 -};
12 10
13 const NAV_HEIGHT = '80px'; 11 const NAV_HEIGHT = '80px';
14 12
...@@ -40,20 +38,5 @@ export function getCheckinMapHeight({ ...@@ -40,20 +38,5 @@ export function getCheckinMapHeight({
40 } 38 }
41 39
42 export function resolveCheckinActiveTab(query = {}) { 40 export function resolveCheckinActiveTab(query = {}) {
43 - const rawTab = Array.isArray(query.activeTab) ? query.activeTab[0] : query.activeTab; 41 + return resolveJlsCheckinActiveTab(query);
44 - const normalizedTab = String(rawTab || '').trim().toLowerCase();
45 -
46 - if (normalizedTab === CHECKIN_ACTIVE_TAB.HOME) {
47 - return CHECKIN_ACTIVE_TAB.HOME;
48 - }
49 -
50 - if (normalizedTab === CHECKIN_ACTIVE_TAB.MESSAGE) {
51 - return CHECKIN_ACTIVE_TAB.MESSAGE;
52 - }
53 -
54 - if (normalizedTab === CHECKIN_ACTIVE_TAB.MINE) {
55 - return CHECKIN_ACTIVE_TAB.MINE;
56 - }
57 -
58 - return '';
59 } 42 }
......