hookehuyr

feat(tabbar): 支持后端 API 变更并重构配置处理逻辑

重构 tabbar 配置处理以适配新的后端 API 端点 `/srv/?a=app_menu`,该端点返回对象结构而非数组。
新增 `normalizeTabbarPayload` 函数统一处理数组、对象及嵌套数据结构,增强数据兼容性。
优化 tabbar 组件支持水平滚动,当项目超过 4 个时自动启用滚动布局并添加滚动提示动画。
标准化 tabbar 键名映射,统一处理 `message`、`application`、`mine` 等旧键名到新键名 `news`、`list`、`user`。
改进页面 URL 生成逻辑,非首页项目优先使用 webview 预览链接。
1 import { fn, fetch } from './fn' 1 import { fn, fetch } from './fn'
2 2
3 const Api = { 3 const Api = {
4 - TABBAR_CONFIG: '/srv/?a=tabbar&t=config', 4 + TABBAR_CONFIG: '/srv/?a=app_menu',
5 } 5 }
6 6
7 export const getTabbarConfigAPI = () => fn(fetch.get(Api.TABBAR_CONFIG)) 7 export const getTabbarConfigAPI = () => fn(fetch.get(Api.TABBAR_CONFIG))
......
...@@ -3,12 +3,22 @@ ...@@ -3,12 +3,22 @@
3 <view class="app-tabbar__placeholder" /> 3 <view class="app-tabbar__placeholder" />
4 4
5 <view class="app-tabbar__wrap"> 5 <view class="app-tabbar__wrap">
6 - <view class="app-tabbar__panel"> 6 + <scroll-view
7 + class="app-tabbar__panel"
8 + :scrollX="true"
9 + :enhanced="true"
10 + :showScrollbar="false"
11 + :mpScrollLeft="scrollLeft"
12 + :animated="true"
13 + @scroll="handlePanelScroll"
14 + >
15 + <view class="app-tabbar__content" :class="{ 'is-scrollable': isScrollable }" :style="contentStyle">
7 <view 16 <view
8 v-for="item in tabItems" 17 v-for="item in tabItems"
9 :key="item.key" 18 :key="item.key"
10 class="app-tabbar__item" 19 class="app-tabbar__item"
11 - :class="{ 'is-active': isActive(item.key) }" 20 + :class="{ 'is-active': isActive(item.key), 'is-scrollable': isScrollable }"
21 + :style="itemStyle"
12 @tap="handleTabClick(item)" 22 @tap="handleTabClick(item)"
13 > 23 >
14 <view class="app-tabbar__item-inner"> 24 <view class="app-tabbar__item-inner">
...@@ -22,14 +32,21 @@ ...@@ -22,14 +32,21 @@
22 </view> 32 </view>
23 </view> 33 </view>
24 </view> 34 </view>
35 + </scroll-view>
36 +
37 + <view v-if="showScrollHint" class="app-tabbar__fade">
38 + <view v-if="showScrollArrow" class="app-tabbar__hint-arrow" />
39 + </view>
25 </view> 40 </view>
26 </view> 41 </view>
27 </template> 42 </template>
28 43
29 <script setup> 44 <script setup>
30 -import { computed, onMounted } from 'vue' 45 +import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
31 import Taro from '@tarojs/taro' 46 import Taro from '@tarojs/taro'
32 import { useTabbarStore } from '@/stores/tabbar' 47 import { useTabbarStore } from '@/stores/tabbar'
48 +import { buildWebviewPreviewUrl } from '@/utils/webview'
49 +import { normalizeTabbarKey } from '@/utils/tabbar'
33 50
34 const props = defineProps({ 51 const props = defineProps({
35 current: { 52 current: {
...@@ -38,14 +55,48 @@ const props = defineProps({ ...@@ -38,14 +55,48 @@ const props = defineProps({
38 }, 55 },
39 }) 56 })
40 57
41 -const activeColor = '#a67939'
42 -const inactiveColor = '#8b95a7'
43 const defaultIcon = 'fa-circle-o' 58 const defaultIcon = 'fa-circle-o'
59 +const scrollHintStorageKey = 'app_tabbar_scroll_hint_seen_v1'
60 +const scrollHintOffset = 56
61 +const scrollableItemWidth = 168
62 +const scrollableSidePadding = 60
44 63
45 const tabbarStore = useTabbarStore() 64 const tabbarStore = useTabbarStore()
46 const tabItems = computed(() => tabbarStore.visibleTabItems) 65 const tabItems = computed(() => tabbarStore.visibleTabItems)
66 +const currentKey = computed(() => normalizeTabbarKey(props.current))
67 +const isScrollable = computed(() => tabItems.value.length > 4)
68 +const showScrollHint = computed(() => isScrollable.value)
69 +const showScrollArrow = computed(() => showScrollHint.value && !hasUserScrolled.value)
70 +const contentStyle = computed(() => {
71 + if (!isScrollable.value) {
72 + return null
73 + }
74 +
75 + return {
76 + width: `${(tabItems.value.length * scrollableItemWidth) + scrollableSidePadding}rpx`,
77 + }
78 +})
79 +const itemStyle = computed(() => {
80 + if (!isScrollable.value) {
81 + return null
82 + }
47 83
48 -const isActive = (name) => name === props.current 84 + const width = `${scrollableItemWidth}rpx`
85 +
86 + return {
87 + width,
88 + minWidth: width,
89 + }
90 +})
91 +const scrollLeft = ref(0)
92 +const hasPlayedScrollHint = ref(false)
93 +const hasUserScrolled = ref(false)
94 +const isPlayingAutoScrollHint = ref(false)
95 +
96 +let scrollHintForwardTimer = null
97 +let scrollHintResetTimer = null
98 +
99 +const isActive = (key) => key === currentKey.value
49 100
50 const getIconClass = (item) => { 101 const getIconClass = (item) => {
51 const icon = String(item?.class || item?.icon || defaultIcon).trim() || defaultIcon 102 const icon = String(item?.class || item?.icon || defaultIcon).trim() || defaultIcon
...@@ -54,15 +105,79 @@ const getIconClass = (item) => { ...@@ -54,15 +105,79 @@ const getIconClass = (item) => {
54 } 105 }
55 106
56 const handleTabClick = (item) => { 107 const handleTabClick = (item) => {
57 - if (!item?.page_url || item.key === props.current) { 108 + const targetUrl = item?.page_url
109 + || (item?.key !== 'home' ? buildWebviewPreviewUrl(item?.webview_url, item?.webview_title || item?.title) : '')
110 +
111 + if (!targetUrl || isActive(item?.key)) {
112 + return
113 + }
114 +
115 + Taro.redirectTo({ url: targetUrl })
116 +}
117 +
118 +const clearScrollHintTimers = () => {
119 + if (scrollHintForwardTimer) {
120 + clearTimeout(scrollHintForwardTimer)
121 + scrollHintForwardTimer = null
122 + }
123 +
124 + if (scrollHintResetTimer) {
125 + clearTimeout(scrollHintResetTimer)
126 + scrollHintResetTimer = null
127 + }
128 +
129 + isPlayingAutoScrollHint.value = false
130 +}
131 +
132 +const handlePanelScroll = (event) => {
133 + if (isPlayingAutoScrollHint.value || hasUserScrolled.value) {
58 return 134 return
59 } 135 }
60 136
61 - Taro.redirectTo({ url: item.page_url }) 137 + const currentScrollLeft = Number(event?.detail?.scrollLeft || 0)
138 + if (currentScrollLeft > 6) {
139 + hasUserScrolled.value = true
140 + }
62 } 141 }
63 142
64 -onMounted(() => { 143 +const playScrollHint = async () => {
65 - tabbarStore.ensureLoaded() 144 + if (!showScrollHint.value || hasPlayedScrollHint.value) {
145 + return
146 + }
147 +
148 + const hasSeenHint = Taro.getStorageSync(scrollHintStorageKey)
149 + if (hasSeenHint) {
150 + hasPlayedScrollHint.value = true
151 + return
152 + }
153 +
154 + hasPlayedScrollHint.value = true
155 + await nextTick()
156 + isPlayingAutoScrollHint.value = true
157 +
158 + scrollHintForwardTimer = setTimeout(() => {
159 + scrollLeft.value = scrollHintOffset
160 + }, 240)
161 +
162 + scrollHintResetTimer = setTimeout(() => {
163 + scrollLeft.value = 0
164 + isPlayingAutoScrollHint.value = false
165 + Taro.setStorageSync(scrollHintStorageKey, 1)
166 + }, 1080)
167 +}
168 +
169 +watch(showScrollHint, (value) => {
170 + if (value) {
171 + playScrollHint()
172 + }
173 +}, { immediate: true })
174 +
175 +onMounted(async () => {
176 + await tabbarStore.ensureLoaded()
177 +})
178 +
179 +onBeforeUnmount(() => {
180 + clearScrollHintTimers()
66 }) 181 })
67 </script> 182 </script>
68 183
...@@ -80,24 +195,43 @@ onMounted(() => { ...@@ -80,24 +195,43 @@ onMounted(() => {
80 bottom: 0; 195 bottom: 0;
81 z-index: 100; 196 z-index: 100;
82 box-sizing: border-box; 197 box-sizing: border-box;
198 + overflow: hidden;
83 } 199 }
84 200
85 .app-tabbar__panel { 201 .app-tabbar__panel {
86 - display: flex; 202 + height: 132rpx;
87 - align-items: stretch;
88 - min-height: 132rpx;
89 - padding: 16rpx 24rpx;
90 border-top: 2rpx solid rgba(166, 121, 57, 0.12); 203 border-top: 2rpx solid rgba(166, 121, 57, 0.12);
91 background: rgba(255, 255, 255, 0.98); 204 background: rgba(255, 255, 255, 0.98);
92 box-sizing: border-box; 205 box-sizing: border-box;
93 backdrop-filter: blur(12rpx); 206 backdrop-filter: blur(12rpx);
94 } 207 }
95 208
209 + .app-tabbar__content {
210 + display: flex;
211 + align-items: stretch;
212 + height: 132rpx;
213 + padding: 16rpx 28rpx 16rpx 52rpx;
214 + box-sizing: border-box;
215 + }
216 +
217 + .app-tabbar__content.is-scrollable {
218 + display: inline-block;
219 + white-space: nowrap;
220 + }
221 +
96 .app-tabbar__item { 222 .app-tabbar__item {
97 flex: 1; 223 flex: 1;
98 min-width: 0; 224 min-width: 0;
99 } 225 }
100 226
227 + .app-tabbar__item.is-scrollable {
228 + display: inline-flex;
229 + vertical-align: top;
230 + padding-right: 4rpx;
231 + box-sizing: border-box;
232 + white-space: normal;
233 + }
234 +
101 .app-tabbar__item-inner { 235 .app-tabbar__item-inner {
102 display: flex; 236 display: flex;
103 flex-direction: column; 237 flex-direction: column;
...@@ -136,5 +270,33 @@ onMounted(() => { ...@@ -136,5 +270,33 @@ onMounted(() => {
136 .app-tabbar__item.is-active .app-tabbar__icon { 270 .app-tabbar__item.is-active .app-tabbar__icon {
137 color: #a67939; 271 color: #a67939;
138 } 272 }
273 +
274 + .app-tabbar__fade {
275 + position: absolute;
276 + top: 0;
277 + right: 0;
278 + bottom: 0;
279 + width: 84rpx;
280 + display: flex;
281 + align-items: center;
282 + justify-content: center;
283 + pointer-events: none;
284 + background: linear-gradient(
285 + 90deg,
286 + rgba(255, 255, 255, 0) 0%,
287 + rgba(255, 255, 255, 0.2) 26%,
288 + rgba(255, 255, 255, 0.74) 68%,
289 + rgba(255, 255, 255, 0.98) 100%
290 + );
291 + }
292 +
293 + .app-tabbar__hint-arrow {
294 + width: 12rpx;
295 + height: 12rpx;
296 + margin-right: 12rpx;
297 + border-top: 2rpx solid rgba(166, 121, 57, 0.32);
298 + border-right: 2rpx solid rgba(166, 121, 57, 0.32);
299 + transform: rotate(45deg);
300 + }
139 } 301 }
140 </style> 302 </style>
......
1 export const getTabbarConfigFixture = () => ({ 1 export const getTabbarConfigFixture = () => ({
2 - tab_items: [ 2 + home: {
3 - {
4 - key: 'home',
5 title: '首页', 3 title: '首页',
6 - class: 'fa-home', 4 + icon: 'fa-home',
7 - visible: true, 5 + link: '',
8 }, 6 },
9 - { 7 + news: {
10 - key: 'message',
11 title: '资讯', 8 title: '资讯',
12 - class: 'fa-newspaper-o', 9 + icon: 'fa-newspaper-o',
13 - visible: true, 10 + link: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=news_list',
14 - webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=news_list',
15 - webview_title: '资讯',
16 }, 11 },
17 - { 12 + list: {
18 - key: 'application',
19 title: '应用', 13 title: '应用',
20 - class: 'fa-th-large', 14 + icon: 'fa-th-large',
21 - visible: true, 15 + link: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list',
22 - webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list',
23 - webview_title: '应用',
24 }, 16 },
25 - { 17 + user: {
26 - key: 'mine',
27 title: '我的', 18 title: '我的',
28 - class: 'fa-user', 19 + icon: 'fa-user',
29 - visible: true, 20 + link: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list',
30 - webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list',
31 - webview_title: '我的',
32 }, 21 },
33 - ],
34 }) 22 })
......
...@@ -3,8 +3,7 @@ import { buildMockSuccess } from '../shared/response' ...@@ -3,8 +3,7 @@ import { buildMockSuccess } from '../shared/response'
3 3
4 export const tabbarMockHandlers = [ 4 export const tabbarMockHandlers = [
5 { 5 {
6 - action: 'tabbar', 6 + action: 'app_menu',
7 - type: 'config',
8 method: 'GET', 7 method: 'GET',
9 handle: () => buildMockSuccess(getTabbarConfigFixture(), '底部导航配置获取成功 (mock)'), 8 handle: () => buildMockSuccess(getTabbarConfigFixture(), '底部导航配置获取成功 (mock)'),
10 }, 9 },
......
...@@ -3,7 +3,8 @@ import { getTabbarConfigAPI } from '@/api/tabbar' ...@@ -3,7 +3,8 @@ import { getTabbarConfigAPI } from '@/api/tabbar'
3 import { 3 import {
4 getDefaultTabbarItem, 4 getDefaultTabbarItem,
5 getDefaultTabbarItems, 5 getDefaultTabbarItems,
6 - normalizeTabbarItems, 6 + normalizeTabbarKey,
7 + normalizeTabbarPayload,
7 } from '@/utils/tabbar' 8 } from '@/utils/tabbar'
8 9
9 let tabbarRequestPromise = null 10 let tabbarRequestPromise = null
...@@ -17,7 +18,7 @@ export const useTabbarStore = defineStore('tabbar', { ...@@ -17,7 +18,7 @@ export const useTabbarStore = defineStore('tabbar', {
17 getters: { 18 getters: {
18 visibleTabItems: (state) => state.tabItems.filter((item) => item.visible !== false), 19 visibleTabItems: (state) => state.tabItems.filter((item) => item.visible !== false),
19 getTabItem: (state) => (key) => { 20 getTabItem: (state) => (key) => {
20 - const normalizedKey = String(key || '').trim() 21 + const normalizedKey = normalizeTabbarKey(key)
21 return state.tabItems.find((item) => item.key === normalizedKey) || getDefaultTabbarItem(normalizedKey) 22 return state.tabItems.find((item) => item.key === normalizedKey) || getDefaultTabbarItem(normalizedKey)
22 }, 23 },
23 }, 24 },
...@@ -36,10 +37,9 @@ export const useTabbarStore = defineStore('tabbar', { ...@@ -36,10 +37,9 @@ export const useTabbarStore = defineStore('tabbar', {
36 tabbarRequestPromise = (async () => { 37 tabbarRequestPromise = (async () => {
37 try { 38 try {
38 const response = await getTabbarConfigAPI() 39 const response = await getTabbarConfigAPI()
39 - const rawTabItems = response?.data?.tab_items || response?.data?.tabs || response?.data?.list || []
40 40
41 if (response?.code === 1) { 41 if (response?.code === 1) {
42 - this.tabItems = normalizeTabbarItems(rawTabItems) 42 + this.tabItems = normalizeTabbarPayload(response?.data)
43 } else { 43 } else {
44 this.tabItems = getDefaultTabbarItems() 44 this.tabItems = getDefaultTabbarItems()
45 } 45 }
......
1 -const defaultTabbarItemMap = { 1 +import { buildWebviewPreviewUrl } from '@/utils/webview'
2 +
3 +const TABBAR_KEY_ALIAS_MAP = {
4 + message: 'news',
5 + application: 'list',
6 + mine: 'user',
7 +}
8 +
9 +const KNOWN_TABBAR_ITEM_MAP = {
2 home: { 10 home: {
3 key: 'home', 11 key: 'home',
4 title: '首页', 12 title: '首页',
...@@ -8,36 +16,37 @@ const defaultTabbarItemMap = { ...@@ -8,36 +16,37 @@ const defaultTabbarItemMap = {
8 webview_url: '', 16 webview_url: '',
9 webview_title: '首页', 17 webview_title: '首页',
10 }, 18 },
11 - message: { 19 + news: {
12 - key: 'message', 20 + key: 'news',
13 title: '资讯', 21 title: '资讯',
14 class: 'fa-newspaper-o', 22 class: 'fa-newspaper-o',
15 visible: true, 23 visible: true,
16 - page_url: '/pages/message/index', 24 + page_url: '',
17 webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=news_list', 25 webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=news_list',
18 webview_title: '资讯', 26 webview_title: '资讯',
19 }, 27 },
20 - application: { 28 + list: {
21 - key: 'application', 29 + key: 'list',
22 title: '应用', 30 title: '应用',
23 class: 'fa-th-large', 31 class: 'fa-th-large',
24 visible: true, 32 visible: true,
25 - page_url: '/pages/application/index', 33 + page_url: '',
26 webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list', 34 webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list',
27 webview_title: '应用', 35 webview_title: '应用',
28 }, 36 },
29 - mine: { 37 + user: {
30 - key: 'mine', 38 + key: 'user',
31 title: '我的', 39 title: '我的',
32 class: 'fa-user', 40 class: 'fa-user',
33 visible: true, 41 visible: true,
34 - page_url: '/pages/mine/index', 42 + page_url: '',
35 webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list', 43 webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list',
36 webview_title: '我的', 44 webview_title: '我的',
37 }, 45 },
38 } 46 }
39 47
40 -export const TABBAR_ORDER = ['home', 'message', 'application', 'mine'] 48 +export const TABBAR_ORDER = ['home', 'news', 'list', 'user']
49 +const DEFAULT_ICON_CLASS = 'fa-circle-o'
41 50
42 const normalizeVisibleValue = (rawValue, fallbackValue = true) => { 51 const normalizeVisibleValue = (rawValue, fallbackValue = true) => {
43 if (typeof rawValue === 'boolean') { 52 if (typeof rawValue === 'boolean') {
...@@ -55,13 +64,28 @@ const normalizeVisibleValue = (rawValue, fallbackValue = true) => { ...@@ -55,13 +64,28 @@ const normalizeVisibleValue = (rawValue, fallbackValue = true) => {
55 return fallbackValue 64 return fallbackValue
56 } 65 }
57 66
58 -export const getDefaultTabbarItem = (key) => { 67 +export const normalizeTabbarKey = (key) => {
59 const normalizedKey = String(key || '').trim() 68 const normalizedKey = String(key || '').trim()
60 - const fallbackItem = defaultTabbarItemMap[normalizedKey] 69 + return TABBAR_KEY_ALIAS_MAP[normalizedKey] || normalizedKey
70 +}
71 +
72 +const createFallbackTabbarItem = (key, title = '') => {
73 + const normalizedKey = normalizeTabbarKey(key)
61 74
62 - if (!fallbackItem) { 75 + return {
63 - return null 76 + key: normalizedKey,
77 + title: title || '栏目',
78 + class: DEFAULT_ICON_CLASS,
79 + visible: true,
80 + page_url: normalizedKey === 'home' ? '/pages/index/index' : '',
81 + webview_url: '',
82 + webview_title: title || '栏目',
64 } 83 }
84 +}
85 +
86 +export const getDefaultTabbarItem = (key) => {
87 + const normalizedKey = normalizeTabbarKey(key)
88 + const fallbackItem = KNOWN_TABBAR_ITEM_MAP[normalizedKey] || createFallbackTabbarItem(normalizedKey)
65 89
66 return { 90 return {
67 ...fallbackItem, 91 ...fallbackItem,
...@@ -74,23 +98,52 @@ export const getDefaultTabbarItems = () => ( ...@@ -74,23 +98,52 @@ export const getDefaultTabbarItems = () => (
74 .filter(Boolean) 98 .filter(Boolean)
75 ) 99 )
76 100
77 -export const normalizeTabbarItem = (rawItem = {}) => { 101 +const buildTabbarPageUrl = (key, webviewUrl, webviewTitle, rawPageUrl = '') => {
78 - const normalizedKey = String(rawItem.key || rawItem.name || '').trim() 102 + if (key === 'home') {
79 - const fallbackItem = getDefaultTabbarItem(normalizedKey) 103 + return rawPageUrl || '/pages/index/index'
104 + }
80 105
81 - if (!fallbackItem) { 106 + if (!webviewUrl) {
82 - return null 107 + return ''
83 } 108 }
84 109
110 + return buildWebviewPreviewUrl(webviewUrl, webviewTitle)
111 +}
112 +
113 +export const normalizeTabbarItem = (rawItem = {}) => {
114 + const normalizedKey = normalizeTabbarKey(rawItem.key || rawItem.name)
115 + const fallbackItem = getDefaultTabbarItem(normalizedKey || rawItem.title)
116 + const title = String(rawItem.title || fallbackItem.title || '栏目')
117 + const webviewUrl = String(
118 + rawItem.webview_url
119 + || rawItem.link_url
120 + || rawItem.link
121 + || rawItem.url
122 + || fallbackItem.webview_url
123 + || '',
124 + )
125 + const webviewTitle = String(
126 + rawItem.webview_title
127 + || rawItem.link_title
128 + || rawItem.title
129 + || fallbackItem.webview_title
130 + || title,
131 + )
132 +
85 return { 133 return {
86 ...fallbackItem, 134 ...fallbackItem,
87 ...rawItem, 135 ...rawItem,
88 key: fallbackItem.key, 136 key: fallbackItem.key,
89 - title: String(rawItem.title || fallbackItem.title), 137 + title,
90 - class: String(rawItem.class || rawItem.icon_class || rawItem.icon || fallbackItem.class || ''), 138 + class: String(rawItem.class || rawItem.icon_class || rawItem.icon || fallbackItem.class || DEFAULT_ICON_CLASS),
91 - page_url: String(rawItem.page_url || fallbackItem.page_url || ''), 139 + webview_url: webviewUrl,
92 - webview_url: String(rawItem.webview_url || rawItem.link_url || rawItem.url || fallbackItem.webview_url || ''), 140 + webview_title: webviewTitle,
93 - webview_title: String(rawItem.webview_title || rawItem.link_title || rawItem.title || fallbackItem.webview_title || ''), 141 + page_url: buildTabbarPageUrl(
142 + fallbackItem.key,
143 + webviewUrl,
144 + webviewTitle,
145 + String(rawItem.page_url || fallbackItem.page_url || ''),
146 + ),
94 visible: normalizeVisibleValue( 147 visible: normalizeVisibleValue(
95 rawItem.visible ?? rawItem.is_show ?? rawItem.show, 148 rawItem.visible ?? rawItem.is_show ?? rawItem.show,
96 fallbackItem.visible, 149 fallbackItem.visible,
...@@ -98,6 +151,56 @@ export const normalizeTabbarItem = (rawItem = {}) => { ...@@ -98,6 +151,56 @@ export const normalizeTabbarItem = (rawItem = {}) => {
98 } 151 }
99 } 152 }
100 153
154 +const sortTabbarObjectEntries = (entries = []) => {
155 + const orderMap = TABBAR_ORDER.reduce((map, key, index) => {
156 + map[key] = index
157 + return map
158 + }, {})
159 +
160 + return entries
161 + .map((entry, index) => ({ entry, index }))
162 + .sort((left, right) => {
163 + const leftKey = normalizeTabbarKey(left.entry[0])
164 + const rightKey = normalizeTabbarKey(right.entry[0])
165 + const leftOrder = orderMap[leftKey]
166 + const rightOrder = orderMap[rightKey]
167 + const leftKnown = Number.isInteger(leftOrder)
168 + const rightKnown = Number.isInteger(rightOrder)
169 +
170 + if (leftKnown && rightKnown) {
171 + return leftOrder - rightOrder
172 + }
173 +
174 + if (leftKnown) {
175 + return -1
176 + }
177 +
178 + if (rightKnown) {
179 + return 1
180 + }
181 +
182 + return left.index - right.index
183 + })
184 + .map(({ entry }) => entry)
185 +}
186 +
187 +const normalizeTabbarObjectItems = (rawMap = {}) => {
188 + const objectEntries = Object.entries(rawMap).filter(([, value]) => (
189 + value && typeof value === 'object' && !Array.isArray(value)
190 + ))
191 +
192 + if (!objectEntries.length) {
193 + return []
194 + }
195 +
196 + return sortTabbarObjectEntries(objectEntries)
197 + .map(([key, value]) => normalizeTabbarItem({
198 + ...value,
199 + key,
200 + }))
201 + .filter(Boolean)
202 +}
203 +
101 export const normalizeTabbarItems = (rawItems = []) => { 204 export const normalizeTabbarItems = (rawItems = []) => {
102 if (!Array.isArray(rawItems) || !rawItems.length) { 205 if (!Array.isArray(rawItems) || !rawItems.length) {
103 return getDefaultTabbarItems() 206 return getDefaultTabbarItems()
...@@ -109,3 +212,19 @@ export const normalizeTabbarItems = (rawItems = []) => { ...@@ -109,3 +212,19 @@ export const normalizeTabbarItems = (rawItems = []) => {
109 212
110 return normalizedItems.length ? normalizedItems : getDefaultTabbarItems() 213 return normalizedItems.length ? normalizedItems : getDefaultTabbarItems()
111 } 214 }
215 +
216 +export const normalizeTabbarPayload = (rawPayload = null) => {
217 + if (Array.isArray(rawPayload)) {
218 + return normalizeTabbarItems(rawPayload)
219 + }
220 +
221 + const arrayPayload = rawPayload?.tab_items || rawPayload?.tabs || rawPayload?.menus
222 + if (Array.isArray(arrayPayload)) {
223 + return normalizeTabbarItems(arrayPayload)
224 + }
225 +
226 + const objectPayload = rawPayload?.tab_items || rawPayload?.tabs || rawPayload?.menus || rawPayload
227 + const normalizedItems = normalizeTabbarObjectItems(objectPayload)
228 +
229 + return normalizedItems.length ? normalizedItems : getDefaultTabbarItems()
230 +}
......