feat(tabbar): 实现动态底部导航及页面配置
- 新增应用页和我的备份页,支持 WebView 模式 - 重构我的页面为 WebView 承接页,移除静态 UI - 新增底部导航配置 API、Mock 数据及 Pinia 状态管理 - 重构 AppTabbar 组件以使用动态配置,支持图标映射和页面路由 - 在应用启动时预加载导航配置,优化用户体验 - 将“消息”统一更名为“资讯”,更新相关页面文案
Showing
18 changed files
with
1079 additions
and
688 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
| 1 | +<template> | ||
| 2 | + <view class="mine-page"> | ||
| 3 | + <view class="header-bg"> | ||
| 4 | + <view class="header-pattern" /> | ||
| 5 | + </view> | ||
| 6 | + | ||
| 7 | + <view class="page-content"> | ||
| 8 | + <view class="hero-banner"> | ||
| 9 | + <view class="hero-banner__ornament"> | ||
| 10 | + <view class="hero-banner__orb hero-banner__orb--lg" /> | ||
| 11 | + <view class="hero-banner__orb hero-banner__orb--sm" /> | ||
| 12 | + </view> | ||
| 13 | + <view class="profile-panel" @tap="handleProfileTap"> | ||
| 14 | + <view class="profile-panel__avatar"> | ||
| 15 | + <view class="avatar-ring"> | ||
| 16 | + <button | ||
| 17 | + class="avatar-btn" | ||
| 18 | + open-type="chooseAvatar" | ||
| 19 | + @chooseavatar="onChooseAvatar" | ||
| 20 | + > | ||
| 21 | + <image | ||
| 22 | + class="avatar-img" | ||
| 23 | + :src="avatarUrl || defaultAvatar" | ||
| 24 | + mode="aspectFill" | ||
| 25 | + /> | ||
| 26 | + </button> | ||
| 27 | + </view> | ||
| 28 | + </view> | ||
| 29 | + <view class="profile-panel__body"> | ||
| 30 | + <view class="profile-panel__head" @tap.stop="openNicknameDialog"> | ||
| 31 | + <view class="profile-panel__name-block"> | ||
| 32 | + <text class="nickname-text">{{ nickname || defaultNickname }}</text> | ||
| 33 | + <text class="profile-panel__subtext">点击修改昵称</text> | ||
| 34 | + </view> | ||
| 35 | + <text class="profile-panel__action">›</text> | ||
| 36 | + </view> | ||
| 37 | + <text class="profile-panel__tip"> | ||
| 38 | + 当前还没接微信昵称接口,先支持手动输入并直接显示在这里。 | ||
| 39 | + </text> | ||
| 40 | + </view> | ||
| 41 | + </view> | ||
| 42 | + </view> | ||
| 43 | + | ||
| 44 | + <view class="menu-section"> | ||
| 45 | + <view class="section-title"> | ||
| 46 | + <text class="section-title-text">常用服务</text> | ||
| 47 | + </view> | ||
| 48 | + <view class="menu-grid"> | ||
| 49 | + <view | ||
| 50 | + v-for="item in menuItems" | ||
| 51 | + :key="item.key" | ||
| 52 | + class="menu-item" | ||
| 53 | + @tap="handleMenuTap(item)" | ||
| 54 | + > | ||
| 55 | + <view class="menu-item__main"> | ||
| 56 | + <view class="menu-icon-wrap" :class="`menu-icon--${item.key}`"> | ||
| 57 | + <component | ||
| 58 | + :is="item.icon" | ||
| 59 | + size="20" | ||
| 60 | + color="#a67939" | ||
| 61 | + /> | ||
| 62 | + </view> | ||
| 63 | + <view class="menu-item__content"> | ||
| 64 | + <text class="menu-item-title">{{ item.title }}</text> | ||
| 65 | + <text class="menu-item-desc">{{ item.desc }}</text> | ||
| 66 | + </view> | ||
| 67 | + </view> | ||
| 68 | + <text class="menu-item-arrow">›</text> | ||
| 69 | + </view> | ||
| 70 | + </view> | ||
| 71 | + </view> | ||
| 72 | + | ||
| 73 | + <view class="menu-section"> | ||
| 74 | + <view class="section-title"> | ||
| 75 | + <text class="section-title-text">更多</text> | ||
| 76 | + </view> | ||
| 77 | + <view class="menu-list"> | ||
| 78 | + <view | ||
| 79 | + v-for="(item, idx) in bottomMenuItems" | ||
| 80 | + :key="item.key" | ||
| 81 | + class="menu-list-item" | ||
| 82 | + :class="{ 'menu-list-item--last': idx === bottomMenuItems.length - 1 }" | ||
| 83 | + @tap="handleMenuTap(item)" | ||
| 84 | + > | ||
| 85 | + <view class="menu-list-icon" :class="`menu-icon--${item.key}`"> | ||
| 86 | + <component | ||
| 87 | + :is="item.icon" | ||
| 88 | + size="18" | ||
| 89 | + color="#a67939" | ||
| 90 | + /> | ||
| 91 | + </view> | ||
| 92 | + <view class="menu-list-content"> | ||
| 93 | + <text class="menu-list-title">{{ item.title }}</text> | ||
| 94 | + <text class="menu-list-desc">{{ item.desc }}</text> | ||
| 95 | + </view> | ||
| 96 | + <text class="arrow-text arrow-text--light">›</text> | ||
| 97 | + </view> | ||
| 98 | + </view> | ||
| 99 | + </view> | ||
| 100 | + | ||
| 101 | + <view class="page-footer-tip"> | ||
| 102 | + <text class="page-footer-tip__text">山门清净,信息常新</text> | ||
| 103 | + </view> | ||
| 104 | + </view> | ||
| 105 | + | ||
| 106 | + <view | ||
| 107 | + v-if="nicknameDialogVisible" | ||
| 108 | + class="nickname-dialog-mask" | ||
| 109 | + @tap="closeNicknameDialog" | ||
| 110 | + > | ||
| 111 | + <view class="nickname-dialog" @tap.stop> | ||
| 112 | + <text class="nickname-dialog__title">设置昵称</text> | ||
| 113 | + <text class="nickname-dialog__desc">当前版本先手动输入昵称,留空时显示默认昵称。</text> | ||
| 114 | + <input | ||
| 115 | + class="nickname-dialog__input" | ||
| 116 | + :value="nicknameDraft" | ||
| 117 | + maxlength="20" | ||
| 118 | + placeholder="请输入昵称" | ||
| 119 | + focus | ||
| 120 | + @input="onNicknameDraftInput" | ||
| 121 | + /> | ||
| 122 | + <view class="nickname-dialog__actions"> | ||
| 123 | + <view | ||
| 124 | + class="nickname-dialog__btn nickname-dialog__btn--ghost" | ||
| 125 | + @tap="closeNicknameDialog" | ||
| 126 | + > | ||
| 127 | + 取消 | ||
| 128 | + </view> | ||
| 129 | + <view | ||
| 130 | + class="nickname-dialog__btn" | ||
| 131 | + @tap="confirmNickname" | ||
| 132 | + > | ||
| 133 | + 确定 | ||
| 134 | + </view> | ||
| 135 | + </view> | ||
| 136 | + </view> | ||
| 137 | + </view> | ||
| 138 | + | ||
| 139 | + <AppTabbar current="mine" /> | ||
| 140 | + </view> | ||
| 141 | +</template> | ||
| 142 | + | ||
| 143 | +<script setup> | ||
| 144 | +import { ref } from 'vue' | ||
| 145 | +import Taro from '@tarojs/taro' | ||
| 146 | +import AppTabbar from '@/components/AppTabbar.vue' | ||
| 147 | +import { | ||
| 148 | + Footprint, | ||
| 149 | + Location, | ||
| 150 | + Notice, | ||
| 151 | + Order, | ||
| 152 | + Service, | ||
| 153 | + Setting, | ||
| 154 | + Star, | ||
| 155 | +} from '@nutui/icons-vue-taro' | ||
| 156 | + | ||
| 157 | +const avatarUrl = ref('') | ||
| 158 | +const nickname = ref('') | ||
| 159 | +const nicknameDraft = ref('') | ||
| 160 | +const nicknameDialogVisible = ref(false) | ||
| 161 | +const defaultNickname = '觉林寺访客' | ||
| 162 | +const defaultAvatar = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEyMCIgdmlld0JveD0iMCAwIDEyMCAxMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMTgiIHkxPSIxNCIgeDI9IjEwMSIgeTI9IjEwNiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiNDRkE4NUEiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM5RTcxMkQiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48cmVjdCB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEyMCIgcng9IjYwIiBmaWxsPSJ1cmwoI2cpIi8+PGNpcmNsZSBjeD0iNjAiIGN5PSI0MiIgcj0iMTgiIGZpbGw9IiNGRkY3RTYiIGZpbGwtb3BhY2l0eT0iMC45NiIvPjxwYXRoIGQ9Ik0yNSA5MEMyNSA3NC41MzYgMzcuNTM2IDYyIDUzIDYySDY3QzgyLjQ2NCA2MiA5NSA3NC41MzYgOTUgOTBWOTJIMjVWOTBaIiBmaWxsPSIjRkZGN0U2IiBmaWxsLW9wYWNpdHk9IjAuOTYiLz48Y2lyY2xlIGN4PSI4OSIgY3k9IjI5IiByPSIxMCIgZmlsbD0iI0Y3REU5RCIgZmlsbC1vcGFjaXR5PSIwLjcyIi8+PC9zdmc+' | ||
| 163 | + | ||
| 164 | +const menuItems = [ | ||
| 165 | + { key: 'orders', title: '我的订单', desc: '查看法物订单', icon: Order }, | ||
| 166 | + { key: 'favorites', title: '我的收藏', desc: '收藏的法物', icon: Star }, | ||
| 167 | + { key: 'history', title: '浏览记录', desc: '最近浏览', icon: Footprint }, | ||
| 168 | + { key: 'address', title: '收货地址', desc: '管理地址', icon: Location }, | ||
| 169 | +] | ||
| 170 | + | ||
| 171 | +const bottomMenuItems = [ | ||
| 172 | + { key: 'settings', title: '设置', desc: '通知与隐私', icon: Setting }, | ||
| 173 | + { key: 'about', title: '关于觉林寺', desc: '了解寺院历史', icon: Service }, | ||
| 174 | + { key: 'help', title: '联系客堂', desc: '在线咨询', icon: Notice }, | ||
| 175 | +] | ||
| 176 | + | ||
| 177 | +const onChooseAvatar = (e) => { | ||
| 178 | + const { avatarUrl: url } = e.detail | ||
| 179 | + avatarUrl.value = url | ||
| 180 | +} | ||
| 181 | + | ||
| 182 | +const openNicknameDialog = () => { | ||
| 183 | + nicknameDraft.value = nickname.value | ||
| 184 | + nicknameDialogVisible.value = true | ||
| 185 | +} | ||
| 186 | + | ||
| 187 | +const closeNicknameDialog = () => { | ||
| 188 | + nicknameDialogVisible.value = false | ||
| 189 | +} | ||
| 190 | + | ||
| 191 | +const onNicknameDraftInput = (e) => { | ||
| 192 | + nicknameDraft.value = e.detail.value | ||
| 193 | +} | ||
| 194 | + | ||
| 195 | +const confirmNickname = () => { | ||
| 196 | + nickname.value = nicknameDraft.value.trim() | ||
| 197 | + closeNicknameDialog() | ||
| 198 | +} | ||
| 199 | + | ||
| 200 | +const handleProfileTap = () => {} | ||
| 201 | + | ||
| 202 | +const handleMenuTap = ({ title }) => { | ||
| 203 | + Taro.showToast({ title: `${title}功能开发中`, icon: 'none' }) | ||
| 204 | +} | ||
| 205 | +</script> | ||
| 206 | + | ||
| 207 | +<style lang="less"> | ||
| 208 | +.mine-page { | ||
| 209 | + min-height: 100vh; | ||
| 210 | + background: | ||
| 211 | + radial-gradient(circle at top right, rgba(166, 121, 57, 0.1), transparent 32%), | ||
| 212 | + linear-gradient(180deg, #fffaf3 0%, #f6f7fb 100%); | ||
| 213 | + position: relative; | ||
| 214 | + | ||
| 215 | + .header-bg { | ||
| 216 | + position: absolute; | ||
| 217 | + top: 0; | ||
| 218 | + left: 0; | ||
| 219 | + right: 0; | ||
| 220 | + height: 304rpx; | ||
| 221 | + background: linear-gradient(180deg, rgba(214, 184, 124, 0.18) 0%, rgba(214, 184, 124, 0.04) 72%, rgba(214, 184, 124, 0) 100%); | ||
| 222 | + border-radius: 0 0 48rpx 48rpx; | ||
| 223 | + overflow: hidden; | ||
| 224 | + } | ||
| 225 | + | ||
| 226 | + .header-pattern { | ||
| 227 | + position: absolute; | ||
| 228 | + top: 0; | ||
| 229 | + left: 0; | ||
| 230 | + right: 0; | ||
| 231 | + bottom: 0; | ||
| 232 | + background-image: | ||
| 233 | + radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.05) 0%, transparent 50%), | ||
| 234 | + radial-gradient(circle at 80% 60%, rgba(255, 255, 255, 0.04) 0%, transparent 40%); | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | + .page-content { | ||
| 238 | + position: relative; | ||
| 239 | + padding: 32rpx 28rpx 176rpx; | ||
| 240 | + box-sizing: border-box; | ||
| 241 | + z-index: 1; | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + .hero-banner { | ||
| 245 | + position: relative; | ||
| 246 | + margin-top: 10rpx; | ||
| 247 | + padding: 28rpx; | ||
| 248 | + border-radius: 32rpx; | ||
| 249 | + background: linear-gradient(135deg, rgba(255, 251, 246, 0.72), rgba(255, 255, 255, 0.52)); | ||
| 250 | + border: 2rpx solid rgba(214, 184, 124, 0.08); | ||
| 251 | + box-shadow: | ||
| 252 | + inset 0 2rpx 0 rgba(255, 255, 255, 0.22), | ||
| 253 | + 0 12rpx 36rpx rgba(15, 23, 42, 0.03); | ||
| 254 | + backdrop-filter: blur(20rpx) saturate(118%); | ||
| 255 | + overflow: hidden; | ||
| 256 | + box-sizing: border-box; | ||
| 257 | + | ||
| 258 | + &__ornament { | ||
| 259 | + position: absolute; | ||
| 260 | + top: 0; | ||
| 261 | + right: 0; | ||
| 262 | + bottom: 0; | ||
| 263 | + width: 200rpx; | ||
| 264 | + pointer-events: none; | ||
| 265 | + } | ||
| 266 | + | ||
| 267 | + &__orb { | ||
| 268 | + position: absolute; | ||
| 269 | + border-radius: 50%; | ||
| 270 | + background: radial-gradient(circle, rgba(214, 184, 124, 0.12) 0%, rgba(214, 184, 124, 0.02) 70%, transparent 100%); | ||
| 271 | + | ||
| 272 | + &--lg { | ||
| 273 | + width: 180rpx; | ||
| 274 | + height: 180rpx; | ||
| 275 | + top: -24rpx; | ||
| 276 | + right: -22rpx; | ||
| 277 | + } | ||
| 278 | + | ||
| 279 | + &--sm { | ||
| 280 | + width: 92rpx; | ||
| 281 | + height: 92rpx; | ||
| 282 | + right: 42rpx; | ||
| 283 | + bottom: 22rpx; | ||
| 284 | + } | ||
| 285 | + } | ||
| 286 | + } | ||
| 287 | + | ||
| 288 | + .profile-panel { | ||
| 289 | + position: relative; | ||
| 290 | + z-index: 1; | ||
| 291 | + display: flex; | ||
| 292 | + align-items: center; | ||
| 293 | + gap: 24rpx; | ||
| 294 | + padding: 30rpx 28rpx; | ||
| 295 | + min-height: 176rpx; | ||
| 296 | + box-sizing: border-box; | ||
| 297 | + | ||
| 298 | + &__avatar { | ||
| 299 | + flex-shrink: 0; | ||
| 300 | + } | ||
| 301 | + | ||
| 302 | + &__body { | ||
| 303 | + flex: 1; | ||
| 304 | + min-width: 0; | ||
| 305 | + display: flex; | ||
| 306 | + flex-direction: column; | ||
| 307 | + gap: 12rpx; | ||
| 308 | + } | ||
| 309 | + | ||
| 310 | + &__head { | ||
| 311 | + display: flex; | ||
| 312 | + align-items: center; | ||
| 313 | + justify-content: space-between; | ||
| 314 | + gap: 20rpx; | ||
| 315 | + } | ||
| 316 | + | ||
| 317 | + &__action { | ||
| 318 | + flex-shrink: 0; | ||
| 319 | + font-size: 30rpx; | ||
| 320 | + line-height: 1; | ||
| 321 | + color: rgba(125, 109, 82, 0.64); | ||
| 322 | + } | ||
| 323 | + | ||
| 324 | + &__name-block { | ||
| 325 | + min-width: 0; | ||
| 326 | + display: flex; | ||
| 327 | + flex-direction: column; | ||
| 328 | + gap: 6rpx; | ||
| 329 | + } | ||
| 330 | + | ||
| 331 | + &__subtext { | ||
| 332 | + font-size: 22rpx; | ||
| 333 | + line-height: 1.4; | ||
| 334 | + color: #948b7d; | ||
| 335 | + } | ||
| 336 | + | ||
| 337 | + &__tip { | ||
| 338 | + font-size: 22rpx; | ||
| 339 | + line-height: 1.6; | ||
| 340 | + color: #948b7d; | ||
| 341 | + } | ||
| 342 | + } | ||
| 343 | + | ||
| 344 | + .avatar-ring { | ||
| 345 | + width: 128rpx; | ||
| 346 | + height: 128rpx; | ||
| 347 | + border-radius: 50%; | ||
| 348 | + padding: 4rpx; | ||
| 349 | + background: linear-gradient(135deg, #e6dcc1, #cdb991); | ||
| 350 | + } | ||
| 351 | + | ||
| 352 | + .avatar-btn { | ||
| 353 | + padding: 0; | ||
| 354 | + margin: 0; | ||
| 355 | + width: 120rpx; | ||
| 356 | + height: 120rpx; | ||
| 357 | + border-radius: 50%; | ||
| 358 | + overflow: hidden; | ||
| 359 | + background: transparent; | ||
| 360 | + border: none; | ||
| 361 | + line-height: 1; | ||
| 362 | + | ||
| 363 | + &::after { | ||
| 364 | + border: none; | ||
| 365 | + } | ||
| 366 | + } | ||
| 367 | + | ||
| 368 | + .avatar-img { | ||
| 369 | + width: 120rpx; | ||
| 370 | + height: 120rpx; | ||
| 371 | + border-radius: 50%; | ||
| 372 | + display: block; | ||
| 373 | + } | ||
| 374 | + | ||
| 375 | + .profile-info { | ||
| 376 | + display: none; | ||
| 377 | + } | ||
| 378 | + | ||
| 379 | + .nickname-text { | ||
| 380 | + font-size: 36rpx; | ||
| 381 | + font-weight: 700; | ||
| 382 | + color: #3b352d; | ||
| 383 | + line-height: 1.4; | ||
| 384 | + } | ||
| 385 | + | ||
| 386 | + .arrow-text { | ||
| 387 | + font-size: 28rpx; | ||
| 388 | + color: #b9ac96; | ||
| 389 | + line-height: 1; | ||
| 390 | + | ||
| 391 | + &--light { | ||
| 392 | + color: #d1c9b5; | ||
| 393 | + } | ||
| 394 | + } | ||
| 395 | + | ||
| 396 | + .section-title { | ||
| 397 | + padding: 34rpx 8rpx 18rpx; | ||
| 398 | + display: flex; | ||
| 399 | + align-items: center; | ||
| 400 | + justify-content: space-between; | ||
| 401 | + } | ||
| 402 | + | ||
| 403 | + .section-title-text { | ||
| 404 | + font-size: 26rpx; | ||
| 405 | + font-weight: 600; | ||
| 406 | + color: #847b70; | ||
| 407 | + letter-spacing: 2rpx; | ||
| 408 | + position: relative; | ||
| 409 | + padding-left: 18rpx; | ||
| 410 | + | ||
| 411 | + &::before { | ||
| 412 | + content: ''; | ||
| 413 | + position: absolute; | ||
| 414 | + left: 0; | ||
| 415 | + top: 8rpx; | ||
| 416 | + bottom: 8rpx; | ||
| 417 | + width: 6rpx; | ||
| 418 | + border-radius: 999rpx; | ||
| 419 | + background: linear-gradient(180deg, #d8c7a4, #b8a489); | ||
| 420 | + } | ||
| 421 | + } | ||
| 422 | + | ||
| 423 | + .menu-grid { | ||
| 424 | + display: grid; | ||
| 425 | + grid-template-columns: repeat(2, minmax(0, 1fr)); | ||
| 426 | + gap: 18rpx; | ||
| 427 | + background: rgba(255, 255, 255, 0.98); | ||
| 428 | + border-radius: 24rpx; | ||
| 429 | + border: 2rpx solid rgba(166, 121, 57, 0.05); | ||
| 430 | + box-shadow: 0 14rpx 36rpx rgba(15, 23, 42, 0.04); | ||
| 431 | + padding: 22rpx; | ||
| 432 | + box-sizing: border-box; | ||
| 433 | + } | ||
| 434 | + | ||
| 435 | + .menu-item { | ||
| 436 | + display: flex; | ||
| 437 | + align-items: center; | ||
| 438 | + justify-content: space-between; | ||
| 439 | + gap: 16rpx; | ||
| 440 | + padding: 22rpx 20rpx; | ||
| 441 | + min-height: 0; | ||
| 442 | + border-radius: 20rpx; | ||
| 443 | + background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(250, 249, 246, 0.96) 100%); | ||
| 444 | + border: 1rpx solid rgba(166, 121, 57, 0.05); | ||
| 445 | + box-sizing: border-box; | ||
| 446 | + } | ||
| 447 | + | ||
| 448 | + .menu-item__main { | ||
| 449 | + min-width: 0; | ||
| 450 | + flex: 1; | ||
| 451 | + display: flex; | ||
| 452 | + align-items: center; | ||
| 453 | + gap: 16rpx; | ||
| 454 | + } | ||
| 455 | + | ||
| 456 | + .menu-item__content { | ||
| 457 | + min-width: 0; | ||
| 458 | + display: flex; | ||
| 459 | + flex-direction: column; | ||
| 460 | + gap: 6rpx; | ||
| 461 | + } | ||
| 462 | + | ||
| 463 | + .menu-item-arrow { | ||
| 464 | + flex-shrink: 0; | ||
| 465 | + font-size: 26rpx; | ||
| 466 | + color: #c0b8ac; | ||
| 467 | + line-height: 1; | ||
| 468 | + } | ||
| 469 | + | ||
| 470 | + .menu-icon-wrap { | ||
| 471 | + width: 72rpx; | ||
| 472 | + height: 72rpx; | ||
| 473 | + border-radius: 20rpx; | ||
| 474 | + display: flex; | ||
| 475 | + align-items: center; | ||
| 476 | + justify-content: center; | ||
| 477 | + box-shadow: inset 0 2rpx 0 rgba(255, 255, 255, 0.45); | ||
| 478 | + | ||
| 479 | + &.menu-icon--orders { | ||
| 480 | + background: linear-gradient(135deg, #faf4ea, #f4ead7); | ||
| 481 | + } | ||
| 482 | + &.menu-icon--favorites { | ||
| 483 | + background: linear-gradient(135deg, #faf1ec, #f3e4da); | ||
| 484 | + } | ||
| 485 | + &.menu-icon--history { | ||
| 486 | + background: linear-gradient(135deg, #f0f5f0, #e3ece3); | ||
| 487 | + } | ||
| 488 | + &.menu-icon--address { | ||
| 489 | + background: linear-gradient(135deg, #eff2f6, #e4e9ef); | ||
| 490 | + } | ||
| 491 | + &.menu-icon--settings { | ||
| 492 | + background: linear-gradient(135deg, #f3f3f3, #e9e9e9); | ||
| 493 | + } | ||
| 494 | + &.menu-icon--about { | ||
| 495 | + background: linear-gradient(135deg, #faf4ea, #f4ead7); | ||
| 496 | + } | ||
| 497 | + &.menu-icon--help { | ||
| 498 | + background: linear-gradient(135deg, #f0f5f0, #e3ece3); | ||
| 499 | + } | ||
| 500 | + } | ||
| 501 | + | ||
| 502 | + .menu-item-title { | ||
| 503 | + font-size: 24rpx; | ||
| 504 | + font-weight: 600; | ||
| 505 | + color: #45403a; | ||
| 506 | + line-height: 1.2; | ||
| 507 | + } | ||
| 508 | + | ||
| 509 | + .menu-item-desc { | ||
| 510 | + font-size: 20rpx; | ||
| 511 | + color: #9a9184; | ||
| 512 | + line-height: 1.3; | ||
| 513 | + } | ||
| 514 | + | ||
| 515 | + .menu-list { | ||
| 516 | + background: rgba(255, 255, 255, 0.98); | ||
| 517 | + border-radius: 24rpx; | ||
| 518 | + border: 2rpx solid rgba(166, 121, 57, 0.05); | ||
| 519 | + box-shadow: 0 14rpx 36rpx rgba(15, 23, 42, 0.04); | ||
| 520 | + overflow: hidden; | ||
| 521 | + } | ||
| 522 | + | ||
| 523 | + .menu-list-item { | ||
| 524 | + display: flex; | ||
| 525 | + align-items: center; | ||
| 526 | + padding: 28rpx 32rpx; | ||
| 527 | + gap: 24rpx; | ||
| 528 | + position: relative; | ||
| 529 | + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(249, 249, 247, 0.98)); | ||
| 530 | + | ||
| 531 | + &::after { | ||
| 532 | + content: ''; | ||
| 533 | + position: absolute; | ||
| 534 | + bottom: 0; | ||
| 535 | + left: 100rpx; | ||
| 536 | + right: 32rpx; | ||
| 537 | + height: 1rpx; | ||
| 538 | + background: rgba(0, 0, 0, 0.05); | ||
| 539 | + } | ||
| 540 | + | ||
| 541 | + &--last::after { | ||
| 542 | + display: none; | ||
| 543 | + } | ||
| 544 | + } | ||
| 545 | + | ||
| 546 | + .menu-list-icon { | ||
| 547 | + width: 64rpx; | ||
| 548 | + height: 64rpx; | ||
| 549 | + border-radius: 16rpx; | ||
| 550 | + display: flex; | ||
| 551 | + align-items: center; | ||
| 552 | + justify-content: center; | ||
| 553 | + flex-shrink: 0; | ||
| 554 | + } | ||
| 555 | + | ||
| 556 | + .menu-list-content { | ||
| 557 | + flex: 1; | ||
| 558 | + min-width: 0; | ||
| 559 | + display: flex; | ||
| 560 | + flex-direction: column; | ||
| 561 | + gap: 6rpx; | ||
| 562 | + } | ||
| 563 | + | ||
| 564 | + .menu-list-title { | ||
| 565 | + font-size: 28rpx; | ||
| 566 | + font-weight: 500; | ||
| 567 | + color: #45403a; | ||
| 568 | + line-height: 1.3; | ||
| 569 | + } | ||
| 570 | + | ||
| 571 | + .menu-list-desc { | ||
| 572 | + font-size: 22rpx; | ||
| 573 | + color: #9a9184; | ||
| 574 | + line-height: 1.2; | ||
| 575 | + } | ||
| 576 | + | ||
| 577 | + .page-footer-tip { | ||
| 578 | + display: flex; | ||
| 579 | + justify-content: center; | ||
| 580 | + padding: 28rpx 0 8rpx; | ||
| 581 | + | ||
| 582 | + &__text { | ||
| 583 | + font-size: 22rpx; | ||
| 584 | + letter-spacing: 4rpx; | ||
| 585 | + color: rgba(147, 141, 131, 0.9); | ||
| 586 | + } | ||
| 587 | + } | ||
| 588 | + | ||
| 589 | + .nickname-dialog-mask { | ||
| 590 | + position: fixed; | ||
| 591 | + inset: 0; | ||
| 592 | + z-index: 20; | ||
| 593 | + display: flex; | ||
| 594 | + align-items: center; | ||
| 595 | + justify-content: center; | ||
| 596 | + padding: 32rpx; | ||
| 597 | + background: rgba(33, 24, 12, 0.32); | ||
| 598 | + box-sizing: border-box; | ||
| 599 | + } | ||
| 600 | + | ||
| 601 | + .nickname-dialog { | ||
| 602 | + width: 100%; | ||
| 603 | + padding: 34rpx 30rpx 30rpx; | ||
| 604 | + border-radius: 28rpx; | ||
| 605 | + background: #fffefd; | ||
| 606 | + box-shadow: 0 24rpx 64rpx rgba(15, 23, 42, 0.12); | ||
| 607 | + box-sizing: border-box; | ||
| 608 | + | ||
| 609 | + &__title { | ||
| 610 | + display: block; | ||
| 611 | + font-size: 32rpx; | ||
| 612 | + font-weight: 600; | ||
| 613 | + color: #3b352d; | ||
| 614 | + line-height: 1.3; | ||
| 615 | + } | ||
| 616 | + | ||
| 617 | + &__desc { | ||
| 618 | + display: block; | ||
| 619 | + margin-top: 12rpx; | ||
| 620 | + font-size: 24rpx; | ||
| 621 | + line-height: 1.6; | ||
| 622 | + color: #948b7d; | ||
| 623 | + } | ||
| 624 | + | ||
| 625 | + &__input { | ||
| 626 | + margin-top: 24rpx; | ||
| 627 | + height: 88rpx; | ||
| 628 | + padding: 0 24rpx; | ||
| 629 | + border-radius: 18rpx; | ||
| 630 | + background: #f7f5f1; | ||
| 631 | + border: 1rpx solid rgba(166, 121, 57, 0.08); | ||
| 632 | + font-size: 28rpx; | ||
| 633 | + color: #3b352d; | ||
| 634 | + box-sizing: border-box; | ||
| 635 | + } | ||
| 636 | + | ||
| 637 | + &__actions { | ||
| 638 | + display: grid; | ||
| 639 | + grid-template-columns: repeat(2, minmax(0, 1fr)); | ||
| 640 | + gap: 18rpx; | ||
| 641 | + margin-top: 28rpx; | ||
| 642 | + } | ||
| 643 | + | ||
| 644 | + &__btn { | ||
| 645 | + display: flex; | ||
| 646 | + align-items: center; | ||
| 647 | + justify-content: center; | ||
| 648 | + height: 84rpx; | ||
| 649 | + border-radius: 18rpx; | ||
| 650 | + font-size: 28rpx; | ||
| 651 | + font-weight: 600; | ||
| 652 | + color: #fffefd; | ||
| 653 | + background: linear-gradient(135deg, #ac9a7d, #948269); | ||
| 654 | + | ||
| 655 | + &--ghost { | ||
| 656 | + color: #847b70; | ||
| 657 | + background: #f4f2ee; | ||
| 658 | + } | ||
| 659 | + } | ||
| 660 | + } | ||
| 661 | +} | ||
| 662 | +</style> |
| 1 | <template> | 1 | <template> |
| 2 | - <view class="mine-page"> | 2 | + <web-view v-if="preview_url" :src="preview_url" /> |
| 3 | - <!-- 顶部装饰背景 --> | 3 | + <view v-else class="tab-webview-page"> |
| 4 | - <view class="header-bg"> | 4 | + <view class="empty-card"> |
| 5 | - <view class="header-pattern" /> | 5 | + <text class="empty-title">暂未配置我的页地址</text> |
| 6 | + <text class="empty-desc"> | ||
| 7 | + 当前“我的”按钮已经切到 WebView 承接页,但接口还没有返回可用地址。 | ||
| 8 | + </text> | ||
| 6 | </view> | 9 | </view> |
| 7 | - | ||
| 8 | - <view class="page-content"> | ||
| 9 | - <view class="hero-banner"> | ||
| 10 | - <view class="hero-banner__ornament"> | ||
| 11 | - <view class="hero-banner__orb hero-banner__orb--lg" /> | ||
| 12 | - <view class="hero-banner__orb hero-banner__orb--sm" /> | ||
| 13 | - </view> | ||
| 14 | - <view class="profile-panel" @tap="handleProfileTap"> | ||
| 15 | - <view class="profile-panel__avatar"> | ||
| 16 | - <view class="avatar-ring"> | ||
| 17 | - <button | ||
| 18 | - class="avatar-btn" | ||
| 19 | - open-type="chooseAvatar" | ||
| 20 | - @chooseavatar="onChooseAvatar" | ||
| 21 | - > | ||
| 22 | - <image | ||
| 23 | - class="avatar-img" | ||
| 24 | - :src="avatarUrl || defaultAvatar" | ||
| 25 | - mode="aspectFill" | ||
| 26 | - /> | ||
| 27 | - </button> | ||
| 28 | - </view> | ||
| 29 | - </view> | ||
| 30 | - <view class="profile-panel__body"> | ||
| 31 | - <view class="profile-panel__head" @tap.stop="openNicknameDialog"> | ||
| 32 | - <view class="profile-panel__name-block"> | ||
| 33 | - <text class="nickname-text">{{ nickname || defaultNickname }}</text> | ||
| 34 | - <text class="profile-panel__subtext">点击修改昵称</text> | ||
| 35 | - </view> | ||
| 36 | - <text class="profile-panel__action">›</text> | ||
| 37 | - </view> | ||
| 38 | - <text class="profile-panel__tip"> | ||
| 39 | - 当前还没接微信昵称接口,先支持手动输入并直接显示在这里。 | ||
| 40 | - </text> | ||
| 41 | - </view> | ||
| 42 | - </view> | ||
| 43 | - </view> | ||
| 44 | - | ||
| 45 | - <!-- 功能菜单 --> | ||
| 46 | - <view class="menu-section"> | ||
| 47 | - <view class="section-title"> | ||
| 48 | - <text class="section-title-text">常用服务</text> | ||
| 49 | - </view> | ||
| 50 | - <view class="menu-grid"> | ||
| 51 | - <view | ||
| 52 | - v-for="item in menuItems" | ||
| 53 | - :key="item.key" | ||
| 54 | - class="menu-item" | ||
| 55 | - @tap="handleMenuTap(item)" | ||
| 56 | - > | ||
| 57 | - <view class="menu-item__main"> | ||
| 58 | - <view class="menu-icon-wrap" :class="`menu-icon--${item.key}`"> | ||
| 59 | - <component | ||
| 60 | - :is="item.icon" | ||
| 61 | - size="20" | ||
| 62 | - color="#a67939" | ||
| 63 | - /> | ||
| 64 | - </view> | ||
| 65 | - <view class="menu-item__content"> | ||
| 66 | - <text class="menu-item-title">{{ item.title }}</text> | ||
| 67 | - <text class="menu-item-desc">{{ item.desc }}</text> | ||
| 68 | - </view> | ||
| 69 | - </view> | ||
| 70 | - <text class="menu-item-arrow">›</text> | ||
| 71 | - </view> | ||
| 72 | - </view> | ||
| 73 | - </view> | ||
| 74 | - | ||
| 75 | - <!-- 更多菜单 --> | ||
| 76 | - <view class="menu-section"> | ||
| 77 | - <view class="section-title"> | ||
| 78 | - <text class="section-title-text">更多</text> | ||
| 79 | - </view> | ||
| 80 | - <view class="menu-list"> | ||
| 81 | - <view | ||
| 82 | - v-for="(item, idx) in bottomMenuItems" | ||
| 83 | - :key="item.key" | ||
| 84 | - class="menu-list-item" | ||
| 85 | - :class="{ 'menu-list-item--last': idx === bottomMenuItems.length - 1 }" | ||
| 86 | - @tap="handleMenuTap(item)" | ||
| 87 | - > | ||
| 88 | - <view class="menu-list-icon" :class="`menu-icon--${item.key}`"> | ||
| 89 | - <component | ||
| 90 | - :is="item.icon" | ||
| 91 | - size="18" | ||
| 92 | - color="#a67939" | ||
| 93 | - /> | ||
| 94 | - </view> | ||
| 95 | - <view class="menu-list-content"> | ||
| 96 | - <text class="menu-list-title">{{ item.title }}</text> | ||
| 97 | - <text class="menu-list-desc">{{ item.desc }}</text> | ||
| 98 | - </view> | ||
| 99 | - <text class="arrow-text arrow-text--light">›</text> | ||
| 100 | - </view> | ||
| 101 | - </view> | ||
| 102 | - </view> | ||
| 103 | - | ||
| 104 | - <view class="page-footer-tip"> | ||
| 105 | - <text class="page-footer-tip__text">山门清净,信息常新</text> | ||
| 106 | - </view> | ||
| 107 | - </view> | ||
| 108 | - | ||
| 109 | - <view | ||
| 110 | - v-if="nicknameDialogVisible" | ||
| 111 | - class="nickname-dialog-mask" | ||
| 112 | - @tap="closeNicknameDialog" | ||
| 113 | - > | ||
| 114 | - <view class="nickname-dialog" @tap.stop> | ||
| 115 | - <text class="nickname-dialog__title">设置昵称</text> | ||
| 116 | - <text class="nickname-dialog__desc">当前版本先手动输入昵称,留空时显示默认昵称。</text> | ||
| 117 | - <input | ||
| 118 | - class="nickname-dialog__input" | ||
| 119 | - :value="nicknameDraft" | ||
| 120 | - maxlength="20" | ||
| 121 | - placeholder="请输入昵称" | ||
| 122 | - focus | ||
| 123 | - @input="onNicknameDraftInput" | ||
| 124 | - /> | ||
| 125 | - <view class="nickname-dialog__actions"> | ||
| 126 | - <view | ||
| 127 | - class="nickname-dialog__btn nickname-dialog__btn--ghost" | ||
| 128 | - @tap="closeNicknameDialog" | ||
| 129 | - > | ||
| 130 | - 取消 | ||
| 131 | - </view> | ||
| 132 | - <view | ||
| 133 | - class="nickname-dialog__btn" | ||
| 134 | - @tap="confirmNickname" | ||
| 135 | - > | ||
| 136 | - 确定 | ||
| 137 | - </view> | ||
| 138 | - </view> | ||
| 139 | - </view> | ||
| 140 | - </view> | ||
| 141 | - | ||
| 142 | - <AppTabbar current="mine" /> | ||
| 143 | </view> | 10 | </view> |
| 144 | </template> | 11 | </template> |
| 145 | 12 | ||
| 146 | <script setup> | 13 | <script setup> |
| 147 | import { ref } from 'vue' | 14 | import { ref } from 'vue' |
| 148 | -import Taro from '@tarojs/taro' | 15 | +import Taro, { useLoad } from '@tarojs/taro' |
| 149 | -import AppTabbar from '@/components/AppTabbar.vue' | 16 | +import { useTabbarStore } from '@/stores/tabbar' |
| 150 | -import { | ||
| 151 | - Footprint, | ||
| 152 | - Location, | ||
| 153 | - Notice, | ||
| 154 | - Order, | ||
| 155 | - Service, | ||
| 156 | - Setting, | ||
| 157 | - Star, | ||
| 158 | -} from '@nutui/icons-vue-taro' | ||
| 159 | - | ||
| 160 | -const avatarUrl = ref('') | ||
| 161 | -const nickname = ref('') | ||
| 162 | -const nicknameDraft = ref('') | ||
| 163 | -const nicknameDialogVisible = ref(false) | ||
| 164 | -const defaultNickname = '觉林寺访客' | ||
| 165 | -const defaultAvatar = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEyMCIgdmlld0JveD0iMCAwIDEyMCAxMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMTgiIHkxPSIxNCIgeDI9IjEwMSIgeTI9IjEwNiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiNDRkE4NUEiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM5RTcxMkQiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48cmVjdCB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEyMCIgcng9IjYwIiBmaWxsPSJ1cmwoI2cpIi8+PGNpcmNsZSBjeD0iNjAiIGN5PSI0MiIgcj0iMTgiIGZpbGw9IiNGRkY3RTYiIGZpbGwtb3BhY2l0eT0iMC45NiIvPjxwYXRoIGQ9Ik0yNSA5MEMyNSA3NC41MzYgMzcuNTM2IDYyIDUzIDYySDY3QzgyLjQ2NCA2MiA5NSA3NC41MzYgOTUgOTBWOTJIMjVWOTBaIiBmaWxsPSIjRkZGN0U2IiBmaWxsLW9wYWNpdHk9IjAuOTYiLz48Y2lyY2xlIGN4PSI4OSIgY3k9IjI5IiByPSIxMCIgZmlsbD0iI0Y3REU5RCIgZmlsbC1vcGFjaXR5PSIwLjcyIi8+PC9zdmc+' | ||
| 166 | - | ||
| 167 | -const menuItems = [ | ||
| 168 | - { key: 'orders', title: '我的订单', desc: '查看法物订单', icon: Order }, | ||
| 169 | - { key: 'favorites', title: '我的收藏', desc: '收藏的法物', icon: Star }, | ||
| 170 | - { key: 'history', title: '浏览记录', desc: '最近浏览', icon: Footprint }, | ||
| 171 | - { key: 'address', title: '收货地址', desc: '管理地址', icon: Location }, | ||
| 172 | -] | ||
| 173 | 17 | ||
| 174 | -const bottomMenuItems = [ | 18 | +const tabbarStore = useTabbarStore() |
| 175 | - { key: 'settings', title: '设置', desc: '通知与隐私', icon: Setting }, | 19 | +const preview_url = ref('') |
| 176 | - { key: 'about', title: '关于觉林寺', desc: '了解寺院历史', icon: Service }, | 20 | +const default_page_title = '我的' |
| 177 | - { key: 'help', title: '联系客堂', desc: '在线咨询', icon: Notice }, | ||
| 178 | -] | ||
| 179 | 21 | ||
| 180 | -const onChooseAvatar = (e) => { | 22 | +const initPage = async () => { |
| 181 | - const { avatarUrl: url } = e.detail | 23 | + await tabbarStore.ensureLoaded() |
| 182 | - avatarUrl.value = url | ||
| 183 | -} | ||
| 184 | - | ||
| 185 | -const openNicknameDialog = () => { | ||
| 186 | - nicknameDraft.value = nickname.value | ||
| 187 | - nicknameDialogVisible.value = true | ||
| 188 | -} | ||
| 189 | 24 | ||
| 190 | -const closeNicknameDialog = () => { | 25 | + const currentTab = tabbarStore.getTabItem('mine') |
| 191 | - nicknameDialogVisible.value = false | 26 | + preview_url.value = currentTab?.webview_url || '' |
| 192 | -} | ||
| 193 | 27 | ||
| 194 | -const onNicknameDraftInput = (e) => { | 28 | + Taro.setNavigationBarTitle({ |
| 195 | - nicknameDraft.value = e.detail.value | 29 | + title: currentTab?.webview_title || currentTab?.title || default_page_title, |
| 196 | -} | 30 | + }) |
| 197 | 31 | ||
| 198 | -const confirmNickname = () => { | 32 | + if (!preview_url.value) { |
| 199 | - nickname.value = nicknameDraft.value.trim() | 33 | + Taro.showToast({ |
| 200 | - closeNicknameDialog() | 34 | + title: '暂未配置我的页地址', |
| 35 | + icon: 'none', | ||
| 36 | + }) | ||
| 37 | + } | ||
| 201 | } | 38 | } |
| 202 | 39 | ||
| 203 | -const handleProfileTap = () => {} | 40 | +useLoad(() => { |
| 204 | - | 41 | + initPage() |
| 205 | -const handleMenuTap = ({ title }) => { | 42 | +}) |
| 206 | - Taro.showToast({ title: `${title}功能开发中`, icon: 'none' }) | ||
| 207 | -} | ||
| 208 | </script> | 43 | </script> |
| 209 | 44 | ||
| 210 | <style lang="less"> | 45 | <style lang="less"> |
| 211 | -.mine-page { | 46 | +.tab-webview-page { |
| 212 | min-height: 100vh; | 47 | min-height: 100vh; |
| 48 | + padding: 32rpx 24rpx; | ||
| 49 | + box-sizing: border-box; | ||
| 213 | background: | 50 | background: |
| 214 | - radial-gradient(circle at top right, rgba(166, 121, 57, 0.1), transparent 32%), | 51 | + radial-gradient(circle at top right, rgba(166, 121, 57, 0.16), transparent 30%), |
| 215 | linear-gradient(180deg, #fffaf3 0%, #f6f7fb 100%); | 52 | linear-gradient(180deg, #fffaf3 0%, #f6f7fb 100%); |
| 216 | - position: relative; | 53 | +} |
| 217 | - | ||
| 218 | - // 顶部装饰背景 | ||
| 219 | - .header-bg { | ||
| 220 | - position: absolute; | ||
| 221 | - top: 0; | ||
| 222 | - left: 0; | ||
| 223 | - right: 0; | ||
| 224 | - height: 304rpx; | ||
| 225 | - background: linear-gradient(180deg, rgba(214, 184, 124, 0.18) 0%, rgba(214, 184, 124, 0.04) 72%, rgba(214, 184, 124, 0) 100%); | ||
| 226 | - border-radius: 0 0 48rpx 48rpx; | ||
| 227 | - overflow: hidden; | ||
| 228 | - } | ||
| 229 | - | ||
| 230 | - .header-pattern { | ||
| 231 | - position: absolute; | ||
| 232 | - top: 0; | ||
| 233 | - left: 0; | ||
| 234 | - right: 0; | ||
| 235 | - bottom: 0; | ||
| 236 | - background-image: | ||
| 237 | - radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.05) 0%, transparent 50%), | ||
| 238 | - radial-gradient(circle at 80% 60%, rgba(255, 255, 255, 0.04) 0%, transparent 40%); | ||
| 239 | - } | ||
| 240 | - | ||
| 241 | - .page-content { | ||
| 242 | - position: relative; | ||
| 243 | - padding: 32rpx 28rpx 176rpx; | ||
| 244 | - box-sizing: border-box; | ||
| 245 | - z-index: 1; | ||
| 246 | - } | ||
| 247 | - | ||
| 248 | - .hero-banner { | ||
| 249 | - position: relative; | ||
| 250 | - margin-top: 10rpx; | ||
| 251 | - padding: 28rpx; | ||
| 252 | - border-radius: 32rpx; | ||
| 253 | - background: linear-gradient(135deg, rgba(255, 251, 246, 0.72), rgba(255, 255, 255, 0.52)); | ||
| 254 | - border: 2rpx solid rgba(214, 184, 124, 0.08); | ||
| 255 | - box-shadow: | ||
| 256 | - inset 0 2rpx 0 rgba(255, 255, 255, 0.22), | ||
| 257 | - 0 12rpx 36rpx rgba(15, 23, 42, 0.03); | ||
| 258 | - backdrop-filter: blur(20rpx) saturate(118%); | ||
| 259 | - overflow: hidden; | ||
| 260 | - box-sizing: border-box; | ||
| 261 | - | ||
| 262 | - &__ornament { | ||
| 263 | - position: absolute; | ||
| 264 | - top: 0; | ||
| 265 | - right: 0; | ||
| 266 | - bottom: 0; | ||
| 267 | - width: 200rpx; | ||
| 268 | - pointer-events: none; | ||
| 269 | - } | ||
| 270 | - | ||
| 271 | - &__orb { | ||
| 272 | - position: absolute; | ||
| 273 | - border-radius: 50%; | ||
| 274 | - background: radial-gradient(circle, rgba(214, 184, 124, 0.12) 0%, rgba(214, 184, 124, 0.02) 70%, transparent 100%); | ||
| 275 | - | ||
| 276 | - &--lg { | ||
| 277 | - width: 180rpx; | ||
| 278 | - height: 180rpx; | ||
| 279 | - top: -24rpx; | ||
| 280 | - right: -22rpx; | ||
| 281 | - } | ||
| 282 | - | ||
| 283 | - &--sm { | ||
| 284 | - width: 92rpx; | ||
| 285 | - height: 92rpx; | ||
| 286 | - right: 42rpx; | ||
| 287 | - bottom: 22rpx; | ||
| 288 | - } | ||
| 289 | - } | ||
| 290 | - } | ||
| 291 | - | ||
| 292 | - .profile-panel { | ||
| 293 | - position: relative; | ||
| 294 | - z-index: 1; | ||
| 295 | - display: flex; | ||
| 296 | - align-items: center; | ||
| 297 | - gap: 24rpx; | ||
| 298 | - padding: 30rpx 28rpx; | ||
| 299 | - min-height: 176rpx; | ||
| 300 | - box-sizing: border-box; | ||
| 301 | - | ||
| 302 | - &__avatar { | ||
| 303 | - flex-shrink: 0; | ||
| 304 | - } | ||
| 305 | - | ||
| 306 | - &__body { | ||
| 307 | - flex: 1; | ||
| 308 | - min-width: 0; | ||
| 309 | - display: flex; | ||
| 310 | - flex-direction: column; | ||
| 311 | - gap: 12rpx; | ||
| 312 | - } | ||
| 313 | - | ||
| 314 | - &__head { | ||
| 315 | - display: flex; | ||
| 316 | - align-items: center; | ||
| 317 | - justify-content: space-between; | ||
| 318 | - gap: 20rpx; | ||
| 319 | - } | ||
| 320 | - | ||
| 321 | - &__action { | ||
| 322 | - flex-shrink: 0; | ||
| 323 | - font-size: 30rpx; | ||
| 324 | - line-height: 1; | ||
| 325 | - color: rgba(125, 109, 82, 0.64); | ||
| 326 | - } | ||
| 327 | - | ||
| 328 | - &__name-block { | ||
| 329 | - min-width: 0; | ||
| 330 | - display: flex; | ||
| 331 | - flex-direction: column; | ||
| 332 | - gap: 6rpx; | ||
| 333 | - } | ||
| 334 | - | ||
| 335 | - &__subtext { | ||
| 336 | - font-size: 22rpx; | ||
| 337 | - line-height: 1.4; | ||
| 338 | - color: #948b7d; | ||
| 339 | - } | ||
| 340 | - | ||
| 341 | - &__tip { | ||
| 342 | - font-size: 22rpx; | ||
| 343 | - line-height: 1.6; | ||
| 344 | - color: #948b7d; | ||
| 345 | - } | ||
| 346 | - } | ||
| 347 | - | ||
| 348 | - .avatar-ring { | ||
| 349 | - width: 128rpx; | ||
| 350 | - height: 128rpx; | ||
| 351 | - border-radius: 50%; | ||
| 352 | - padding: 4rpx; | ||
| 353 | - background: linear-gradient(135deg, #e6dcc1, #cdb991); | ||
| 354 | - } | ||
| 355 | - | ||
| 356 | - .avatar-btn { | ||
| 357 | - padding: 0; | ||
| 358 | - margin: 0; | ||
| 359 | - width: 120rpx; | ||
| 360 | - height: 120rpx; | ||
| 361 | - border-radius: 50%; | ||
| 362 | - overflow: hidden; | ||
| 363 | - background: transparent; | ||
| 364 | - border: none; | ||
| 365 | - line-height: 1; | ||
| 366 | - | ||
| 367 | - &::after { | ||
| 368 | - border: none; | ||
| 369 | - } | ||
| 370 | - } | ||
| 371 | - | ||
| 372 | - .avatar-img { | ||
| 373 | - width: 120rpx; | ||
| 374 | - height: 120rpx; | ||
| 375 | - border-radius: 50%; | ||
| 376 | - display: block; | ||
| 377 | - } | ||
| 378 | - | ||
| 379 | - .profile-info { | ||
| 380 | - display: none; | ||
| 381 | - } | ||
| 382 | - | ||
| 383 | - .nickname-text { | ||
| 384 | - font-size: 36rpx; | ||
| 385 | - font-weight: 700; | ||
| 386 | - color: #3b352d; | ||
| 387 | - line-height: 1.4; | ||
| 388 | - } | ||
| 389 | - | ||
| 390 | - .arrow-text { | ||
| 391 | - font-size: 28rpx; | ||
| 392 | - color: #b9ac96; | ||
| 393 | - line-height: 1; | ||
| 394 | - | ||
| 395 | - &--light { | ||
| 396 | - color: #d1c9b5; | ||
| 397 | - } | ||
| 398 | - } | ||
| 399 | - | ||
| 400 | - // 菜单分区 | ||
| 401 | - .section-title { | ||
| 402 | - padding: 34rpx 8rpx 18rpx; | ||
| 403 | - display: flex; | ||
| 404 | - align-items: center; | ||
| 405 | - justify-content: space-between; | ||
| 406 | - } | ||
| 407 | - | ||
| 408 | - .section-title-text { | ||
| 409 | - font-size: 26rpx; | ||
| 410 | - font-weight: 600; | ||
| 411 | - color: #847b70; | ||
| 412 | - letter-spacing: 2rpx; | ||
| 413 | - position: relative; | ||
| 414 | - padding-left: 18rpx; | ||
| 415 | - | ||
| 416 | - &::before { | ||
| 417 | - content: ''; | ||
| 418 | - position: absolute; | ||
| 419 | - left: 0; | ||
| 420 | - top: 8rpx; | ||
| 421 | - bottom: 8rpx; | ||
| 422 | - width: 6rpx; | ||
| 423 | - border-radius: 999rpx; | ||
| 424 | - background: linear-gradient(180deg, #d8c7a4, #b8a489); | ||
| 425 | - } | ||
| 426 | - } | ||
| 427 | - | ||
| 428 | - // 网格菜单(常用服务) | ||
| 429 | - .menu-grid { | ||
| 430 | - display: grid; | ||
| 431 | - grid-template-columns: repeat(2, minmax(0, 1fr)); | ||
| 432 | - gap: 18rpx; | ||
| 433 | - background: rgba(255, 255, 255, 0.98); | ||
| 434 | - border-radius: 24rpx; | ||
| 435 | - border: 2rpx solid rgba(166, 121, 57, 0.05); | ||
| 436 | - box-shadow: 0 14rpx 36rpx rgba(15, 23, 42, 0.04); | ||
| 437 | - padding: 22rpx; | ||
| 438 | - box-sizing: border-box; | ||
| 439 | - } | ||
| 440 | - | ||
| 441 | - .menu-item { | ||
| 442 | - display: flex; | ||
| 443 | - align-items: center; | ||
| 444 | - justify-content: space-between; | ||
| 445 | - gap: 16rpx; | ||
| 446 | - padding: 22rpx 20rpx; | ||
| 447 | - min-height: 0; | ||
| 448 | - border-radius: 20rpx; | ||
| 449 | - background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(250, 249, 246, 0.96) 100%); | ||
| 450 | - border: 1rpx solid rgba(166, 121, 57, 0.05); | ||
| 451 | - box-sizing: border-box; | ||
| 452 | - } | ||
| 453 | - | ||
| 454 | - .menu-item__main { | ||
| 455 | - min-width: 0; | ||
| 456 | - flex: 1; | ||
| 457 | - display: flex; | ||
| 458 | - align-items: center; | ||
| 459 | - gap: 16rpx; | ||
| 460 | - } | ||
| 461 | - | ||
| 462 | - .menu-item__content { | ||
| 463 | - min-width: 0; | ||
| 464 | - display: flex; | ||
| 465 | - flex-direction: column; | ||
| 466 | - gap: 6rpx; | ||
| 467 | - } | ||
| 468 | - | ||
| 469 | - .menu-item-arrow { | ||
| 470 | - flex-shrink: 0; | ||
| 471 | - font-size: 26rpx; | ||
| 472 | - color: #c0b8ac; | ||
| 473 | - line-height: 1; | ||
| 474 | - } | ||
| 475 | - | ||
| 476 | - .menu-icon-wrap { | ||
| 477 | - width: 72rpx; | ||
| 478 | - height: 72rpx; | ||
| 479 | - border-radius: 20rpx; | ||
| 480 | - display: flex; | ||
| 481 | - align-items: center; | ||
| 482 | - justify-content: center; | ||
| 483 | - box-shadow: inset 0 2rpx 0 rgba(255, 255, 255, 0.45); | ||
| 484 | - | ||
| 485 | - &.menu-icon--orders { | ||
| 486 | - background: linear-gradient(135deg, #faf4ea, #f4ead7); | ||
| 487 | - } | ||
| 488 | - &.menu-icon--favorites { | ||
| 489 | - background: linear-gradient(135deg, #faf1ec, #f3e4da); | ||
| 490 | - } | ||
| 491 | - &.menu-icon--history { | ||
| 492 | - background: linear-gradient(135deg, #f0f5f0, #e3ece3); | ||
| 493 | - } | ||
| 494 | - &.menu-icon--address { | ||
| 495 | - background: linear-gradient(135deg, #eff2f6, #e4e9ef); | ||
| 496 | - } | ||
| 497 | - &.menu-icon--settings { | ||
| 498 | - background: linear-gradient(135deg, #f3f3f3, #e9e9e9); | ||
| 499 | - } | ||
| 500 | - &.menu-icon--about { | ||
| 501 | - background: linear-gradient(135deg, #faf4ea, #f4ead7); | ||
| 502 | - } | ||
| 503 | - &.menu-icon--help { | ||
| 504 | - background: linear-gradient(135deg, #f0f5f0, #e3ece3); | ||
| 505 | - } | ||
| 506 | - } | ||
| 507 | - | ||
| 508 | - .menu-item-title { | ||
| 509 | - font-size: 24rpx; | ||
| 510 | - font-weight: 600; | ||
| 511 | - color: #45403a; | ||
| 512 | - line-height: 1.2; | ||
| 513 | - } | ||
| 514 | - | ||
| 515 | - .menu-item-desc { | ||
| 516 | - font-size: 20rpx; | ||
| 517 | - color: #9a9184; | ||
| 518 | - line-height: 1.3; | ||
| 519 | - } | ||
| 520 | - | ||
| 521 | - // 列表菜单(更多) | ||
| 522 | - .menu-list { | ||
| 523 | - background: rgba(255, 255, 255, 0.98); | ||
| 524 | - border-radius: 24rpx; | ||
| 525 | - border: 2rpx solid rgba(166, 121, 57, 0.05); | ||
| 526 | - box-shadow: 0 14rpx 36rpx rgba(15, 23, 42, 0.04); | ||
| 527 | - overflow: hidden; | ||
| 528 | - } | ||
| 529 | - | ||
| 530 | - .menu-list-item { | ||
| 531 | - display: flex; | ||
| 532 | - align-items: center; | ||
| 533 | - padding: 28rpx 32rpx; | ||
| 534 | - gap: 24rpx; | ||
| 535 | - position: relative; | ||
| 536 | - background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(249, 249, 247, 0.98)); | ||
| 537 | - | ||
| 538 | - &::after { | ||
| 539 | - content: ''; | ||
| 540 | - position: absolute; | ||
| 541 | - bottom: 0; | ||
| 542 | - left: 100rpx; | ||
| 543 | - right: 32rpx; | ||
| 544 | - height: 1rpx; | ||
| 545 | - background: rgba(0, 0, 0, 0.05); | ||
| 546 | - } | ||
| 547 | - | ||
| 548 | - &--last::after { | ||
| 549 | - display: none; | ||
| 550 | - } | ||
| 551 | - } | ||
| 552 | - | ||
| 553 | - .menu-list-icon { | ||
| 554 | - width: 64rpx; | ||
| 555 | - height: 64rpx; | ||
| 556 | - border-radius: 16rpx; | ||
| 557 | - display: flex; | ||
| 558 | - align-items: center; | ||
| 559 | - justify-content: center; | ||
| 560 | - flex-shrink: 0; | ||
| 561 | - } | ||
| 562 | - | ||
| 563 | - .menu-list-content { | ||
| 564 | - flex: 1; | ||
| 565 | - min-width: 0; | ||
| 566 | - display: flex; | ||
| 567 | - flex-direction: column; | ||
| 568 | - gap: 6rpx; | ||
| 569 | - } | ||
| 570 | - | ||
| 571 | - .menu-list-title { | ||
| 572 | - font-size: 28rpx; | ||
| 573 | - font-weight: 500; | ||
| 574 | - color: #45403a; | ||
| 575 | - line-height: 1.3; | ||
| 576 | - } | ||
| 577 | - | ||
| 578 | - .menu-list-desc { | ||
| 579 | - font-size: 22rpx; | ||
| 580 | - color: #9a9184; | ||
| 581 | - line-height: 1.2; | ||
| 582 | - } | ||
| 583 | - | ||
| 584 | - .page-footer-tip { | ||
| 585 | - display: flex; | ||
| 586 | - justify-content: center; | ||
| 587 | - padding: 28rpx 0 8rpx; | ||
| 588 | - | ||
| 589 | - &__text { | ||
| 590 | - font-size: 22rpx; | ||
| 591 | - letter-spacing: 4rpx; | ||
| 592 | - color: rgba(147, 141, 131, 0.9); | ||
| 593 | - } | ||
| 594 | - } | ||
| 595 | - | ||
| 596 | - .nickname-dialog-mask { | ||
| 597 | - position: fixed; | ||
| 598 | - inset: 0; | ||
| 599 | - z-index: 20; | ||
| 600 | - display: flex; | ||
| 601 | - align-items: center; | ||
| 602 | - justify-content: center; | ||
| 603 | - padding: 32rpx; | ||
| 604 | - background: rgba(33, 24, 12, 0.32); | ||
| 605 | - box-sizing: border-box; | ||
| 606 | - } | ||
| 607 | - | ||
| 608 | - .nickname-dialog { | ||
| 609 | - width: 100%; | ||
| 610 | - padding: 34rpx 30rpx 30rpx; | ||
| 611 | - border-radius: 28rpx; | ||
| 612 | - background: #fffefd; | ||
| 613 | - box-shadow: 0 24rpx 64rpx rgba(15, 23, 42, 0.12); | ||
| 614 | - box-sizing: border-box; | ||
| 615 | - | ||
| 616 | - &__title { | ||
| 617 | - display: block; | ||
| 618 | - font-size: 32rpx; | ||
| 619 | - font-weight: 600; | ||
| 620 | - color: #3b352d; | ||
| 621 | - line-height: 1.3; | ||
| 622 | - } | ||
| 623 | - | ||
| 624 | - &__desc { | ||
| 625 | - display: block; | ||
| 626 | - margin-top: 12rpx; | ||
| 627 | - font-size: 24rpx; | ||
| 628 | - line-height: 1.6; | ||
| 629 | - color: #948b7d; | ||
| 630 | - } | ||
| 631 | - | ||
| 632 | - &__input { | ||
| 633 | - margin-top: 24rpx; | ||
| 634 | - height: 88rpx; | ||
| 635 | - padding: 0 24rpx; | ||
| 636 | - border-radius: 18rpx; | ||
| 637 | - background: #f7f5f1; | ||
| 638 | - border: 1rpx solid rgba(166, 121, 57, 0.08); | ||
| 639 | - font-size: 28rpx; | ||
| 640 | - color: #3b352d; | ||
| 641 | - box-sizing: border-box; | ||
| 642 | - } | ||
| 643 | 54 | ||
| 644 | - &__actions { | 55 | +.empty-card { |
| 645 | - display: grid; | 56 | + background: rgba(255, 255, 255, 0.94); |
| 646 | - grid-template-columns: repeat(2, minmax(0, 1fr)); | 57 | + border: 2rpx solid rgba(166, 121, 57, 0.08); |
| 647 | - gap: 18rpx; | 58 | + border-radius: 28rpx; |
| 648 | - margin-top: 28rpx; | 59 | + padding: 32rpx; |
| 649 | - } | 60 | + box-sizing: border-box; |
| 61 | + box-shadow: 0 20rpx 60rpx rgba(15, 23, 42, 0.06); | ||
| 62 | +} | ||
| 650 | 63 | ||
| 651 | - &__btn { | 64 | +.empty-title { |
| 652 | - display: flex; | 65 | + display: block; |
| 653 | - align-items: center; | 66 | + font-size: 36rpx; |
| 654 | - justify-content: center; | 67 | + font-weight: 700; |
| 655 | - height: 84rpx; | 68 | + color: #111827; |
| 656 | - border-radius: 18rpx; | 69 | +} |
| 657 | - font-size: 28rpx; | ||
| 658 | - font-weight: 600; | ||
| 659 | - color: #fffefd; | ||
| 660 | - background: linear-gradient(135deg, #ac9a7d, #948269); | ||
| 661 | 70 | ||
| 662 | - &--ghost { | 71 | +.empty-desc { |
| 663 | - color: #847b70; | 72 | + display: block; |
| 664 | - background: #f4f2ee; | 73 | + margin-top: 16rpx; |
| 665 | - } | 74 | + font-size: 26rpx; |
| 666 | - } | 75 | + line-height: 1.7; |
| 667 | - } | 76 | + color: #6b7280; |
| 668 | } | 77 | } |
| 669 | </style> | 78 | </style> | ... | ... |
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