feat(tabbar): 支持后端 API 变更并重构配置处理逻辑
重构 tabbar 配置处理以适配新的后端 API 端点 `/srv/?a=app_menu`,该端点返回对象结构而非数组。 新增 `normalizeTabbarPayload` 函数统一处理数组、对象及嵌套数据结构,增强数据兼容性。 优化 tabbar 组件支持水平滚动,当项目超过 4 个时自动启用滚动布局并添加滚动提示动画。 标准化 tabbar 键名映射,统一处理 `message`、`application`、`mine` 等旧键名到新键名 `news`、`list`、`user`。 改进页面 URL 生成逻辑,非首页项目优先使用 webview 预览链接。
Showing
6 changed files
with
338 additions
and
70 deletions
| 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 | +} | ... | ... |
-
Please register or login to post a comment