feat(tabbar): 实现动态底部导航及页面配置
- 新增应用页和我的备份页,支持 WebView 模式 - 重构我的页面为 WebView 承接页,移除静态 UI - 新增底部导航配置 API、Mock 数据及 Pinia 状态管理 - 重构 AppTabbar 组件以使用动态配置,支持图标映射和页面路由 - 在应用启动时预加载导航配置,优化用户体验 - 将“消息”统一更名为“资讯”,更新相关页面文案
Showing
18 changed files
with
364 additions
and
44 deletions
src/api/tabbar.js
0 → 100644
| ... | @@ -4,7 +4,9 @@ export default { | ... | @@ -4,7 +4,9 @@ export default { |
| 4 | 'pages/map-guide/index', | 4 | 'pages/map-guide/index', |
| 5 | 'pages/message/index', | 5 | 'pages/message/index', |
| 6 | 'pages/message-detail/index', | 6 | 'pages/message-detail/index', |
| 7 | + 'pages/application/index', | ||
| 7 | 'pages/mine/index', | 8 | 'pages/mine/index', |
| 9 | + 'pages/mine-backup/index', | ||
| 8 | 'pages/pay-test/index', | 10 | 'pages/pay-test/index', |
| 9 | 'pages/pay-bridge/index', | 11 | 'pages/pay-bridge/index', |
| 10 | 'pages/webview-preview/index', | 12 | 'pages/webview-preview/index', | ... | ... |
| ... | @@ -3,6 +3,9 @@ import { createPinia } from 'pinia' | ... | @@ -3,6 +3,9 @@ import { createPinia } from 'pinia' |
| 3 | import './utils/polyfill' | 3 | import './utils/polyfill' |
| 4 | import './app.less' | 4 | import './app.less' |
| 5 | import { saveCurrentPagePath, hasAuth, silentAuth, navigateToAuth } from '@/utils/authRedirect' | 5 | import { saveCurrentPagePath, hasAuth, silentAuth, navigateToAuth } from '@/utils/authRedirect' |
| 6 | +import { useTabbarStore } from '@/stores/tabbar' | ||
| 7 | + | ||
| 8 | +const pinia = createPinia() | ||
| 6 | 9 | ||
| 7 | const App = createApp({ | 10 | const App = createApp({ |
| 8 | async onLaunch(options) { | 11 | async onLaunch(options) { |
| ... | @@ -26,13 +29,21 @@ const App = createApp({ | ... | @@ -26,13 +29,21 @@ const App = createApp({ |
| 26 | } catch (error) { | 29 | } catch (error) { |
| 27 | console.error('静默授权失败:', error) | 30 | console.error('静默授权失败:', error) |
| 28 | navigateToAuth(full_path || undefined) | 31 | navigateToAuth(full_path || undefined) |
| 32 | + return | ||
| 29 | } | 33 | } |
| 30 | } | 34 | } |
| 35 | + | ||
| 36 | + try { | ||
| 37 | + const tabbarStore = useTabbarStore(pinia) | ||
| 38 | + await tabbarStore.ensureLoaded() | ||
| 39 | + } catch (error) { | ||
| 40 | + console.error('预加载底部导航配置失败:', error) | ||
| 41 | + } | ||
| 31 | }, | 42 | }, |
| 32 | onShow() { | 43 | onShow() { |
| 33 | }, | 44 | }, |
| 34 | }) | 45 | }) |
| 35 | 46 | ||
| 36 | -App.use(createPinia()) | 47 | +App.use(pinia) |
| 37 | 48 | ||
| 38 | export default App | 49 | export default App | ... | ... |
| ... | @@ -6,9 +6,9 @@ | ... | @@ -6,9 +6,9 @@ |
| 6 | <view class="app-tabbar__panel"> | 6 | <view class="app-tabbar__panel"> |
| 7 | <view | 7 | <view |
| 8 | v-for="item in tabItems" | 8 | v-for="item in tabItems" |
| 9 | - :key="item.name" | 9 | + :key="item.key" |
| 10 | class="app-tabbar__item" | 10 | class="app-tabbar__item" |
| 11 | - :class="{ 'is-active': isActive(item.name) }" | 11 | + :class="{ 'is-active': isActive(item.key) }" |
| 12 | @tap="handleTabClick(item)" | 12 | @tap="handleTabClick(item)" |
| 13 | > | 13 | > |
| 14 | <view class="app-tabbar__item-inner"> | 14 | <view class="app-tabbar__item-inner"> |
| ... | @@ -16,7 +16,7 @@ | ... | @@ -16,7 +16,7 @@ |
| 16 | <component | 16 | <component |
| 17 | :is="item.icon" | 17 | :is="item.icon" |
| 18 | size="18" | 18 | size="18" |
| 19 | - :color="isActive(item.name) ? activeColor : inactiveColor" | 19 | + :color="isActive(item.key) ? activeColor : inactiveColor" |
| 20 | /> | 20 | /> |
| 21 | </view> | 21 | </view> |
| 22 | <text class="app-tabbar__label">{{ item.title }}</text> | 22 | <text class="app-tabbar__label">{{ item.title }}</text> |
| ... | @@ -28,8 +28,10 @@ | ... | @@ -28,8 +28,10 @@ |
| 28 | </template> | 28 | </template> |
| 29 | 29 | ||
| 30 | <script setup> | 30 | <script setup> |
| 31 | +import { computed, onMounted } from 'vue' | ||
| 31 | import Taro from '@tarojs/taro' | 32 | import Taro from '@tarojs/taro' |
| 32 | -import { Home, Message, My } from '@nutui/icons-vue-taro' | 33 | +import { Category, Home, My, Notice, Service } from '@nutui/icons-vue-taro' |
| 34 | +import { useTabbarStore } from '@/stores/tabbar' | ||
| 33 | 35 | ||
| 34 | const props = defineProps({ | 36 | const props = defineProps({ |
| 35 | current: { | 37 | current: { |
| ... | @@ -41,36 +43,35 @@ const props = defineProps({ | ... | @@ -41,36 +43,35 @@ const props = defineProps({ |
| 41 | const activeColor = '#a67939' | 43 | const activeColor = '#a67939' |
| 42 | const inactiveColor = '#8b95a7' | 44 | const inactiveColor = '#8b95a7' |
| 43 | 45 | ||
| 44 | -const tabItems = [ | 46 | +const tabbarStore = useTabbarStore() |
| 45 | - { | 47 | + |
| 46 | - name: 'home', | 48 | +const tabIconMap = { |
| 47 | - title: '首页', | 49 | + home: Home, |
| 48 | - icon: Home, | 50 | + message: Notice, |
| 49 | - url: '/pages/index/index', | 51 | + application: Category, |
| 50 | - }, | 52 | + mine: My, |
| 51 | - { | 53 | +} |
| 52 | - name: 'message', | 54 | + |
| 53 | - title: '消息', | 55 | +const tabItems = computed(() => ( |
| 54 | - icon: Message, | 56 | + tabbarStore.visibleTabItems.map((item) => ({ |
| 55 | - url: '/pages/message/index', | 57 | + ...item, |
| 56 | - }, | 58 | + icon: tabIconMap[item.key] || Service, |
| 57 | - { | 59 | + })) |
| 58 | - name: 'mine', | 60 | +)) |
| 59 | - title: '我的', | ||
| 60 | - icon: My, | ||
| 61 | - url: '/pages/mine/index', | ||
| 62 | - }, | ||
| 63 | -] | ||
| 64 | 61 | ||
| 65 | const isActive = (name) => name === props.current | 62 | const isActive = (name) => name === props.current |
| 66 | 63 | ||
| 67 | const handleTabClick = (item) => { | 64 | const handleTabClick = (item) => { |
| 68 | - if (!item?.url || item.name === props.current) { | 65 | + if (!item?.page_url || item.key === props.current) { |
| 69 | return | 66 | return |
| 70 | } | 67 | } |
| 71 | 68 | ||
| 72 | - Taro.redirectTo({ url: item.url }) | 69 | + Taro.redirectTo({ url: item.page_url }) |
| 73 | } | 70 | } |
| 71 | + | ||
| 72 | +onMounted(() => { | ||
| 73 | + tabbarStore.ensureLoaded() | ||
| 74 | +}) | ||
| 74 | </script> | 75 | </script> |
| 75 | 76 | ||
| 76 | <style lang="less"> | 77 | <style lang="less"> | ... | ... |
src/mock/fixtures/tabbar.fixture.js
0 → 100644
| 1 | +export const getTabbarConfigFixture = () => ({ | ||
| 2 | + tab_items: [ | ||
| 3 | + { | ||
| 4 | + key: 'home', | ||
| 5 | + title: '首页', | ||
| 6 | + visible: true, | ||
| 7 | + }, | ||
| 8 | + { | ||
| 9 | + key: 'message', | ||
| 10 | + title: '资讯', | ||
| 11 | + visible: true, | ||
| 12 | + }, | ||
| 13 | + { | ||
| 14 | + key: 'application', | ||
| 15 | + title: '应用', | ||
| 16 | + visible: true, | ||
| 17 | + webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list', | ||
| 18 | + webview_title: '应用', | ||
| 19 | + }, | ||
| 20 | + { | ||
| 21 | + key: 'mine', | ||
| 22 | + title: '我的', | ||
| 23 | + visible: true, | ||
| 24 | + webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list', | ||
| 25 | + webview_title: '我的', | ||
| 26 | + }, | ||
| 27 | + ], | ||
| 28 | +}) |
| ... | @@ -3,6 +3,7 @@ import { paymentMockHandlers } from './payment.mock' | ... | @@ -3,6 +3,7 @@ import { paymentMockHandlers } from './payment.mock' |
| 3 | import { commonMockHandlers } from './common.mock' | 3 | import { commonMockHandlers } from './common.mock' |
| 4 | import { messageMockHandlers } from './message.mock' | 4 | import { messageMockHandlers } from './message.mock' |
| 5 | import { homeMockHandlers } from './home.mock' | 5 | import { homeMockHandlers } from './home.mock' |
| 6 | +import { tabbarMockHandlers } from './tabbar.mock' | ||
| 6 | 7 | ||
| 7 | export const mockHandlers = [ | 8 | export const mockHandlers = [ |
| 8 | ...authMockHandlers, | 9 | ...authMockHandlers, |
| ... | @@ -10,4 +11,5 @@ export const mockHandlers = [ | ... | @@ -10,4 +11,5 @@ export const mockHandlers = [ |
| 10 | ...commonMockHandlers, | 11 | ...commonMockHandlers, |
| 11 | ...messageMockHandlers, | 12 | ...messageMockHandlers, |
| 12 | ...homeMockHandlers, | 13 | ...homeMockHandlers, |
| 14 | + ...tabbarMockHandlers, | ||
| 13 | ] | 15 | ] | ... | ... |
src/mock/modules/tabbar.mock.js
0 → 100644
| 1 | +import { getTabbarConfigFixture } from '../fixtures/tabbar.fixture' | ||
| 2 | +import { buildMockSuccess } from '../shared/response' | ||
| 3 | + | ||
| 4 | +export const tabbarMockHandlers = [ | ||
| 5 | + { | ||
| 6 | + action: 'tabbar', | ||
| 7 | + type: 'config', | ||
| 8 | + method: 'GET', | ||
| 9 | + handle: () => buildMockSuccess(getTabbarConfigFixture(), '底部导航配置获取成功 (mock)'), | ||
| 10 | + }, | ||
| 11 | +] |
src/pages/application/index.config.js
0 → 100644
src/pages/application/index.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <web-view v-if="preview_url" :src="preview_url" /> | ||
| 3 | + <view v-else class="tab-webview-page"> | ||
| 4 | + <view class="empty-card"> | ||
| 5 | + <text class="empty-title">暂未配置应用地址</text> | ||
| 6 | + <text class="empty-desc"> | ||
| 7 | + 当前应用页还没有收到可用的外部链接,请先检查底部导航配置接口返回。 | ||
| 8 | + </text> | ||
| 9 | + </view> | ||
| 10 | + </view> | ||
| 11 | +</template> | ||
| 12 | + | ||
| 13 | +<script setup> | ||
| 14 | +import { ref } from 'vue' | ||
| 15 | +import Taro, { useLoad } from '@tarojs/taro' | ||
| 16 | +import { useTabbarStore } from '@/stores/tabbar' | ||
| 17 | + | ||
| 18 | +const tabbarStore = useTabbarStore() | ||
| 19 | +const preview_url = ref('') | ||
| 20 | +const default_page_title = '应用' | ||
| 21 | + | ||
| 22 | +const initPage = async () => { | ||
| 23 | + await tabbarStore.ensureLoaded() | ||
| 24 | + | ||
| 25 | + const currentTab = tabbarStore.getTabItem('application') | ||
| 26 | + preview_url.value = currentTab?.webview_url || '' | ||
| 27 | + | ||
| 28 | + Taro.setNavigationBarTitle({ | ||
| 29 | + title: currentTab?.webview_title || currentTab?.title || default_page_title, | ||
| 30 | + }) | ||
| 31 | + | ||
| 32 | + if (!preview_url.value) { | ||
| 33 | + Taro.showToast({ | ||
| 34 | + title: '暂未配置应用地址', | ||
| 35 | + icon: 'none', | ||
| 36 | + }) | ||
| 37 | + } | ||
| 38 | +} | ||
| 39 | + | ||
| 40 | +useLoad(() => { | ||
| 41 | + initPage() | ||
| 42 | +}) | ||
| 43 | +</script> | ||
| 44 | + | ||
| 45 | +<style lang="less"> | ||
| 46 | +.tab-webview-page { | ||
| 47 | + min-height: 100vh; | ||
| 48 | + padding: 32rpx 24rpx; | ||
| 49 | + box-sizing: border-box; | ||
| 50 | + background: | ||
| 51 | + radial-gradient(circle at top right, rgba(166, 121, 57, 0.16), transparent 30%), | ||
| 52 | + linear-gradient(180deg, #fffaf3 0%, #f6f7fb 100%); | ||
| 53 | +} | ||
| 54 | + | ||
| 55 | +.empty-card { | ||
| 56 | + background: rgba(255, 255, 255, 0.94); | ||
| 57 | + border: 2rpx solid rgba(166, 121, 57, 0.08); | ||
| 58 | + border-radius: 28rpx; | ||
| 59 | + padding: 32rpx; | ||
| 60 | + box-sizing: border-box; | ||
| 61 | + box-shadow: 0 20rpx 60rpx rgba(15, 23, 42, 0.06); | ||
| 62 | +} | ||
| 63 | + | ||
| 64 | +.empty-title { | ||
| 65 | + display: block; | ||
| 66 | + font-size: 36rpx; | ||
| 67 | + font-weight: 700; | ||
| 68 | + color: #111827; | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | +.empty-desc { | ||
| 72 | + display: block; | ||
| 73 | + margin-top: 16rpx; | ||
| 74 | + font-size: 26rpx; | ||
| 75 | + line-height: 1.7; | ||
| 76 | + color: #6b7280; | ||
| 77 | +} | ||
| 78 | +</style> |
| ... | @@ -2,15 +2,15 @@ | ... | @@ -2,15 +2,15 @@ |
| 2 | <view class="message-detail-page"> | 2 | <view class="message-detail-page"> |
| 3 | <view class="page-content"> | 3 | <view class="page-content"> |
| 4 | <view class="hero-card"> | 4 | <view class="hero-card"> |
| 5 | - <text class="hero-title">消息详情</text> | 5 | + <text class="hero-title">资讯详情</text> |
| 6 | <text class="hero-desc"> | 6 | <text class="hero-desc"> |
| 7 | - 进入详情页后,当前消息会在 mock 中自动标记为已读,返回列表即可看到状态变化。 | 7 | + 进入详情页后,当前资讯会在 mock 中自动标记为已读,返回列表即可看到状态变化。 |
| 8 | </text> | 8 | </text> |
| 9 | </view> | 9 | </view> |
| 10 | 10 | ||
| 11 | <view v-if="loading" class="placeholder-card"> | 11 | <view v-if="loading" class="placeholder-card"> |
| 12 | <text class="section-title">加载中</text> | 12 | <text class="section-title">加载中</text> |
| 13 | - <text class="section-desc">正在获取消息详情...</text> | 13 | + <text class="section-desc">正在获取资讯详情...</text> |
| 14 | </view> | 14 | </view> |
| 15 | 15 | ||
| 16 | <view v-else-if="detail" class="detail-card"> | 16 | <view v-else-if="detail" class="detail-card"> |
| ... | @@ -23,13 +23,13 @@ | ... | @@ -23,13 +23,13 @@ |
| 23 | </view> | 23 | </view> |
| 24 | 24 | ||
| 25 | <view v-else class="placeholder-card"> | 25 | <view v-else class="placeholder-card"> |
| 26 | - <text class="section-title">未找到消息</text> | 26 | + <text class="section-title">未找到资讯</text> |
| 27 | <text class="section-desc"> | 27 | <text class="section-desc"> |
| 28 | - 当前消息不存在,或者真实接口还没有返回这条详情数据。 | 28 | + 当前资讯不存在,或者真实接口还没有返回这条详情数据。 |
| 29 | </text> | 29 | </text> |
| 30 | </view> | 30 | </view> |
| 31 | 31 | ||
| 32 | - <button class="back-btn" @tap="goBack">返回消息列表</button> | 32 | + <button class="back-btn" @tap="goBack">返回资讯列表</button> |
| 33 | </view> | 33 | </view> |
| 34 | </view> | 34 | </view> |
| 35 | </template> | 35 | </template> |
| ... | @@ -54,11 +54,11 @@ const fetchMessageDetail = async (id) => { | ... | @@ -54,11 +54,11 @@ const fetchMessageDetail = async (id) => { |
| 54 | 54 | ||
| 55 | detail.value = null | 55 | detail.value = null |
| 56 | Taro.showToast({ | 56 | Taro.showToast({ |
| 57 | - title: response?.msg || '获取详情失败', | 57 | + title: response?.msg || '获取资讯详情失败', |
| 58 | icon: 'none', | 58 | icon: 'none', |
| 59 | }) | 59 | }) |
| 60 | } catch (error) { | 60 | } catch (error) { |
| 61 | - console.error('获取消息详情失败:', error) | 61 | + console.error('获取资讯详情失败:', error) |
| 62 | detail.value = null | 62 | detail.value = null |
| 63 | } finally { | 63 | } finally { |
| 64 | loading.value = false | 64 | loading.value = false | ... | ... |
| ... | @@ -3,7 +3,7 @@ | ... | @@ -3,7 +3,7 @@ |
| 3 | <view class="page-content"> | 3 | <view class="page-content"> |
| 4 | <view class="hero-card"> | 4 | <view class="hero-card"> |
| 5 | <view class="hero-head"> | 5 | <view class="hero-head"> |
| 6 | - <text class="hero-title">消息</text> | 6 | + <text class="hero-title">资讯</text> |
| 7 | <text class="env-tag" :class="{ mock: current_env_use_mock }"> | 7 | <text class="env-tag" :class="{ mock: current_env_use_mock }"> |
| 8 | {{ current_env_label }} | 8 | {{ current_env_label }} |
| 9 | </text> | 9 | </text> |
| ... | @@ -17,7 +17,7 @@ | ... | @@ -17,7 +17,7 @@ |
| 17 | <view> | 17 | <view> |
| 18 | <text class="section-title">列表状态</text> | 18 | <text class="section-title">列表状态</text> |
| 19 | <text class="section-desc"> | 19 | <text class="section-desc"> |
| 20 | - 共 {{ total }} 条消息,未读 {{ unread_count }} 条。进入详情页后,当前消息会在 mock 中自动变成已读。 | 20 | + 共 {{ total }} 条资讯,未读 {{ unread_count }} 条。进入详情页后,当前资讯会在 mock 中自动变成已读。 |
| 21 | </text> | 21 | </text> |
| 22 | </view> | 22 | </view> |
| 23 | <button class="refresh-btn" :loading="loading" @tap="handleRefresh"> | 23 | <button class="refresh-btn" :loading="loading" @tap="handleRefresh"> |
| ... | @@ -28,14 +28,14 @@ | ... | @@ -28,14 +28,14 @@ |
| 28 | <view v-if="loading && !message_list.length" class="placeholder-card"> | 28 | <view v-if="loading && !message_list.length" class="placeholder-card"> |
| 29 | <text class="section-title">加载中</text> | 29 | <text class="section-title">加载中</text> |
| 30 | <text class="section-desc"> | 30 | <text class="section-desc"> |
| 31 | - 正在拉取消息列表... | 31 | + 正在拉取资讯列表... |
| 32 | </text> | 32 | </text> |
| 33 | </view> | 33 | </view> |
| 34 | 34 | ||
| 35 | <view v-else-if="!message_list.length" class="placeholder-card"> | 35 | <view v-else-if="!message_list.length" class="placeholder-card"> |
| 36 | - <text class="section-title">暂无消息</text> | 36 | + <text class="section-title">暂无资讯</text> |
| 37 | <text class="section-desc"> | 37 | <text class="section-desc"> |
| 38 | - 当前接口还没有返回消息内容,可以先在 mock 里补结构,再回到这个页面验证展示效果。 | 38 | + 当前接口还没有返回资讯内容,可以先在 mock 里补结构,再回到这个页面验证展示效果。 |
| 39 | </text> | 39 | </text> |
| 40 | </view> | 40 | </view> |
| 41 | 41 | ||
| ... | @@ -110,7 +110,7 @@ const fetchMessageList = async (nextPage = 0, append = false) => { | ... | @@ -110,7 +110,7 @@ const fetchMessageList = async (nextPage = 0, append = false) => { |
| 110 | 110 | ||
| 111 | if (response?.code !== 1) { | 111 | if (response?.code !== 1) { |
| 112 | Taro.showToast({ | 112 | Taro.showToast({ |
| 113 | - title: response?.msg || '获取消息失败', | 113 | + title: response?.msg || '获取资讯失败', |
| 114 | icon: 'none', | 114 | icon: 'none', |
| 115 | }) | 115 | }) |
| 116 | return | 116 | return |
| ... | @@ -122,7 +122,7 @@ const fetchMessageList = async (nextPage = 0, append = false) => { | ... | @@ -122,7 +122,7 @@ const fetchMessageList = async (nextPage = 0, append = false) => { |
| 122 | has_more.value = !!response?.data?.has_more | 122 | has_more.value = !!response?.data?.has_more |
| 123 | page.value = nextPage | 123 | page.value = nextPage |
| 124 | } catch (error) { | 124 | } catch (error) { |
| 125 | - console.error('获取消息列表失败:', error) | 125 | + console.error('获取资讯列表失败:', error) |
| 126 | } finally { | 126 | } finally { |
| 127 | loading.value = false | 127 | loading.value = false |
| 128 | loading_more.value = false | 128 | loading_more.value = false | ... | ... |
src/pages/mine-backup/index.config.js
0 → 100644
src/pages/mine-backup/index.vue
0 → 100644
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
src/stores/tabbar.js
0 → 100644
| 1 | +import { defineStore } from 'pinia' | ||
| 2 | +import { getTabbarConfigAPI } from '@/api/tabbar' | ||
| 3 | +import { | ||
| 4 | + getDefaultTabbarItem, | ||
| 5 | + getDefaultTabbarItems, | ||
| 6 | + normalizeTabbarItems, | ||
| 7 | +} from '@/utils/tabbar' | ||
| 8 | + | ||
| 9 | +let tabbarRequestPromise = null | ||
| 10 | + | ||
| 11 | +export const useTabbarStore = defineStore('tabbar', { | ||
| 12 | + state: () => ({ | ||
| 13 | + tabItems: getDefaultTabbarItems(), | ||
| 14 | + loaded: false, | ||
| 15 | + loading: false, | ||
| 16 | + }), | ||
| 17 | + getters: { | ||
| 18 | + visibleTabItems: (state) => state.tabItems.filter((item) => item.visible !== false), | ||
| 19 | + getTabItem: (state) => (key) => { | ||
| 20 | + const normalizedKey = String(key || '').trim() | ||
| 21 | + return state.tabItems.find((item) => item.key === normalizedKey) || getDefaultTabbarItem(normalizedKey) | ||
| 22 | + }, | ||
| 23 | + }, | ||
| 24 | + actions: { | ||
| 25 | + async fetchTabbarConfig(force = false) { | ||
| 26 | + if (!force && this.loaded) { | ||
| 27 | + return this.tabItems | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + if (this.loading && tabbarRequestPromise) { | ||
| 31 | + return tabbarRequestPromise | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + this.loading = true | ||
| 35 | + | ||
| 36 | + tabbarRequestPromise = (async () => { | ||
| 37 | + try { | ||
| 38 | + const response = await getTabbarConfigAPI() | ||
| 39 | + const rawTabItems = response?.data?.tab_items || response?.data?.tabs || response?.data?.list || [] | ||
| 40 | + | ||
| 41 | + if (response?.code === 1) { | ||
| 42 | + this.tabItems = normalizeTabbarItems(rawTabItems) | ||
| 43 | + } else { | ||
| 44 | + this.tabItems = getDefaultTabbarItems() | ||
| 45 | + } | ||
| 46 | + } catch (error) { | ||
| 47 | + console.error('获取底部导航配置失败:', error) | ||
| 48 | + this.tabItems = getDefaultTabbarItems() | ||
| 49 | + } finally { | ||
| 50 | + this.loaded = true | ||
| 51 | + this.loading = false | ||
| 52 | + tabbarRequestPromise = null | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + return this.tabItems | ||
| 56 | + })() | ||
| 57 | + | ||
| 58 | + return tabbarRequestPromise | ||
| 59 | + }, | ||
| 60 | + async ensureLoaded() { | ||
| 61 | + if (this.loaded) { | ||
| 62 | + return this.tabItems | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + return this.fetchTabbarConfig() | ||
| 66 | + }, | ||
| 67 | + }, | ||
| 68 | +}) |
src/utils/tabbar.js
0 → 100644
| 1 | +const defaultTabbarItemMap = { | ||
| 2 | + home: { | ||
| 3 | + key: 'home', | ||
| 4 | + title: '首页', | ||
| 5 | + visible: true, | ||
| 6 | + page_url: '/pages/index/index', | ||
| 7 | + webview_url: '', | ||
| 8 | + webview_title: '首页', | ||
| 9 | + }, | ||
| 10 | + message: { | ||
| 11 | + key: 'message', | ||
| 12 | + title: '资讯', | ||
| 13 | + visible: true, | ||
| 14 | + page_url: '/pages/message/index', | ||
| 15 | + webview_url: '', | ||
| 16 | + webview_title: '资讯', | ||
| 17 | + }, | ||
| 18 | + application: { | ||
| 19 | + key: 'application', | ||
| 20 | + title: '应用', | ||
| 21 | + visible: true, | ||
| 22 | + page_url: '/pages/application/index', | ||
| 23 | + webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list', | ||
| 24 | + webview_title: '应用', | ||
| 25 | + }, | ||
| 26 | + mine: { | ||
| 27 | + key: 'mine', | ||
| 28 | + title: '我的', | ||
| 29 | + visible: true, | ||
| 30 | + page_url: '/pages/mine/index', | ||
| 31 | + webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list', | ||
| 32 | + webview_title: '我的', | ||
| 33 | + }, | ||
| 34 | +} | ||
| 35 | + | ||
| 36 | +export const TABBAR_ORDER = ['home', 'message', 'application', 'mine'] | ||
| 37 | + | ||
| 38 | +const normalizeVisibleValue = (rawValue, fallbackValue = true) => { | ||
| 39 | + if (typeof rawValue === 'boolean') { | ||
| 40 | + return rawValue | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + if (rawValue === 1 || rawValue === '1') { | ||
| 44 | + return true | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + if (rawValue === 0 || rawValue === '0') { | ||
| 48 | + return false | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + return fallbackValue | ||
| 52 | +} | ||
| 53 | + | ||
| 54 | +export const getDefaultTabbarItem = (key) => { | ||
| 55 | + const normalizedKey = String(key || '').trim() | ||
| 56 | + const fallbackItem = defaultTabbarItemMap[normalizedKey] | ||
| 57 | + | ||
| 58 | + if (!fallbackItem) { | ||
| 59 | + return null | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + return { | ||
| 63 | + ...fallbackItem, | ||
| 64 | + } | ||
| 65 | +} | ||
| 66 | + | ||
| 67 | +export const getDefaultTabbarItems = () => ( | ||
| 68 | + TABBAR_ORDER | ||
| 69 | + .map((key) => getDefaultTabbarItem(key)) | ||
| 70 | + .filter(Boolean) | ||
| 71 | +) | ||
| 72 | + | ||
| 73 | +export const normalizeTabbarItem = (rawItem = {}) => { | ||
| 74 | + const normalizedKey = String(rawItem.key || rawItem.name || '').trim() | ||
| 75 | + const fallbackItem = getDefaultTabbarItem(normalizedKey) | ||
| 76 | + | ||
| 77 | + if (!fallbackItem) { | ||
| 78 | + return null | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + return { | ||
| 82 | + ...fallbackItem, | ||
| 83 | + ...rawItem, | ||
| 84 | + key: fallbackItem.key, | ||
| 85 | + title: String(rawItem.title || fallbackItem.title), | ||
| 86 | + page_url: String(rawItem.page_url || fallbackItem.page_url || ''), | ||
| 87 | + webview_url: String(rawItem.webview_url || rawItem.link_url || rawItem.url || fallbackItem.webview_url || ''), | ||
| 88 | + webview_title: String(rawItem.webview_title || rawItem.link_title || rawItem.title || fallbackItem.webview_title || ''), | ||
| 89 | + visible: normalizeVisibleValue( | ||
| 90 | + rawItem.visible ?? rawItem.is_show ?? rawItem.show, | ||
| 91 | + fallbackItem.visible, | ||
| 92 | + ), | ||
| 93 | + } | ||
| 94 | +} | ||
| 95 | + | ||
| 96 | +export const normalizeTabbarItems = (rawItems = []) => { | ||
| 97 | + if (!Array.isArray(rawItems) || !rawItems.length) { | ||
| 98 | + return getDefaultTabbarItems() | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + const normalizedItems = rawItems | ||
| 102 | + .map((item) => normalizeTabbarItem(item)) | ||
| 103 | + .filter(Boolean) | ||
| 104 | + | ||
| 105 | + return normalizedItems.length ? normalizedItems : getDefaultTabbarItems() | ||
| 106 | +} |
-
Please register or login to post a comment