hookehuyr

feat(home): 联调首页接口并适配真实数据结构

- 更新首页 API 地址及 mock 处理器,对齐 Apifox 接口规范
- 重构首页页面,消费 `volunteer_top_banner`、`volunteer_bottom_banner`、`volunteer_home_icon` 字段
- 为顶部 banner 添加 Swiper 轮播支持,单图保持现状,多图自动切换
- 在 mock 环境下添加状态提示,便于后续真实数据联调
- 调整全局配置,移除默认 `f` 参数,改为各接口单独传递
- 更新组件类型声明,引入 NutUI Swiper 组件
- 同步更新 TODO 清单,标记已完成项并细化后续任务
1 # TODO 清单 1 # TODO 清单
2 2
3 -## 未实现与待联调功能 3 +## 首页与内容接口
4 - 4 +
5 -1. 首页当前使用的还是假数据,还缺一个首页内容接口,需要替换成真实返回的 banner、导航和图片入口数据。 5 +- [x] 首页页面已按 Apifox“首页”接口结构改造,当前消费 `volunteer_top_banner``volunteer_bottom_banner``volunteer_home_icon` 三组字段。
6 -2. 底部导航栏的显示还需要联调,还缺一个底部导航配置接口,用来确认各 tab 的标题、显隐、跳转页和 WebView 地址。 6 +- [x] 首页 mock 已按真实接口字段结构补齐,当前图片和图标先复用现有页面假数据,避免真实接口到位后再次大范围改页面。
7 -3. 资讯页面还需要一个接口获取资讯信息列表,后续要和详情页、已读状态这条链路一起确认真实字段。 7 +- [x] 首页页面已增加“当前为 mock 接口结构”的状态提示,后续真实接口出数后可直接去掉该提示。
8 -4. “应用”和“我的”页面还缺 2 个 WebView URL,需要分别提供可用地址后再完成联调。 8 +- [ ] 首页真实接口还没有返回业务数据,后续需要把 mock 环境切到真实返回值联调。
9 -5. 授权功能还没有联调,需要确认静默授权、授权失败跳转、授权成功回跳这条链路是否正常。 9 +- [ ] 首页接口正式 `client_id` 还未确认,当前继续沿用全局默认请求参数,待后端给出正式值后补配置。
10 -6. 支付确认页还没有联调,需要确认页面入参、金额展示、点击支付和状态提示是否正常。 10 +- [ ] 继续确认 `volunteer_top_banner` 实际返回是一组还是多组;当前页面已兼容“单图保持现状,多图自动切 Swiper 轮播”。
11 -7. 支付功能还没有联调,需要确认支付参数获取、小程序拉起支付、支付结果返回和回跳流程是否正常。 11 +- [ ] 确认首页接口里的 `title``color` 后续是否启用;当前已保留字段但页面暂未消费。
12 +
13 +## 底部导航与 WebView
14 +
15 +- [ ] 底部导航栏还需要联调真实配置接口,继续确认各 tab 的标题、显隐、跳转页和 WebView 地址。
16 +- [ ] “应用”和“我的”页面还缺 2 个可用 WebView URL,提供后再完成联调。
17 +
18 +## 资讯链路
19 +
20 +- [ ] 资讯页面还需要真实接口获取列表数据,后续要和详情页、已读状态链路一起确认真实字段。
21 +
22 +## 授权与支付
23 +
24 +- [ ] 授权功能还没有完整联调,需要确认静默授权、授权失败跳转、授权成功回跳这条链路是否正常。
25 +- [ ] 支付确认页还没有联调,需要确认页面入参、金额展示、点击支付和状态提示是否正常。
26 +- [ ] 支付功能还没有联调,需要确认支付参数获取、小程序拉起支付、支付结果返回和回跳流程是否正常。
......
...@@ -9,6 +9,8 @@ declare module 'vue' { ...@@ -9,6 +9,8 @@ declare module 'vue' {
9 export interface GlobalComponents { 9 export interface GlobalComponents {
10 AppTabbar: typeof import('./src/components/AppTabbar.vue')['default'] 10 AppTabbar: typeof import('./src/components/AppTabbar.vue')['default']
11 IndexNav: typeof import('./src/components/indexNav.vue')['default'] 11 IndexNav: typeof import('./src/components/indexNav.vue')['default']
12 + NutSwiper: typeof import('@nutui/nutui-taro')['Swiper']
13 + NutSwiperItem: typeof import('@nutui/nutui-taro')['SwiperItem']
12 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] 14 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
13 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] 15 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
14 QrCode: typeof import('./src/components/qrCode.vue')['default'] 16 QrCode: typeof import('./src/components/qrCode.vue')['default']
......
1 import { fn, fetch } from './fn' 1 import { fn, fetch } from './fn'
2 2
3 const Api = { 3 const Api = {
4 - HOME_CONTENT: '/srv/?a=home&t=content', 4 + HOME_CONTENT: '/srv/?a=app_list&t=volunteer&f=customize',
5 PAY_TEST: '/srv/?a=pay', 5 PAY_TEST: '/srv/?a=pay',
6 } 6 }
7 7
8 +/**
9 + * @description 获取首页内容
10 + * - 当前先按 Apifox 首页接口结构联调
11 + * - client_id 继续走全局默认参数,正式值待后端确认
12 + */
8 export const getHomeContentAPI = () => fn(fetch.get(Api.HOME_CONTENT)) 13 export const getHomeContentAPI = () => fn(fetch.get(Api.HOME_CONTENT))
9 14
10 /** 15 /**
......
1 -const banner_image = 'https://cdn.ipadbiz.cn/jls_weapp/images/banner01@2x.png' 1 +const top_banner_image = 'https://cdn.ipadbiz.cn/jls_weapp/images/banner01@2x.png'
2 -const activity_image_01 = 'https://cdn.ipadbiz.cn/jls_weapp/images/banner02@2x.png' 2 +const top_banner_image_02 = 'https://cdn.ipadbiz.cn/jls_weapp/images/banner02@2x.png'
3 -const activity_image_02 = 'https://cdn.ipadbiz.cn/jls_weapp/images/banner03@2x.png' 3 +const top_banner_image_03 = 'https://cdn.ipadbiz.cn/jls_weapp/images/banner03@2x.png'
4 +const bottom_banner_image_01 = 'https://cdn.ipadbiz.cn/jls_weapp/images/banner02@2x.png'
5 +const bottom_banner_image_02 = 'https://cdn.ipadbiz.cn/jls_weapp/images/banner03@2x.png'
4 6
7 +/**
8 + * @description 首页接口 mock 样本
9 + * - 字段结构对齐 Apifox“首页”接口
10 + * - 当前图片与图标先复用现有首页假数据,等真实接口出数后可直接替换
11 + */
5 export const getHomeContentFixture = () => ({ 12 export const getHomeContentFixture = () => ({
6 - banner: { 13 + volunteer_top_banner: [
7 - id: 'home-banner-01',
8 - image_url: banner_image,
9 - title: '苏州觉林寺',
10 - link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list',
11 - link_title: '苏州觉林寺',
12 - },
13 - nav_list: [
14 { 14 {
15 - id: 'map-guide', 15 + id: 2770328,
16 + title: '苏州觉林寺',
17 + link: top_banner_image,
18 + value: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list',
19 + },
20 + {
21 + id: 2770329,
22 + title: '法讯动态',
23 + link: top_banner_image_02,
24 + value: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=news_list',
25 + },
26 + {
27 + id: 2770331,
28 + title: '共修活动',
29 + link: top_banner_image_03,
30 + value: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=practice',
31 + },
32 + ],
33 + volunteer_bottom_banner: [
34 + {
35 + id: 2770330,
36 + title: '新春祈福法会',
37 + link: bottom_banner_image_01,
38 + value: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=activity_spring',
39 + },
40 + {
41 + id: 2770332,
42 + title: '中秋月光茶会',
43 + link: bottom_banner_image_02,
44 + value: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=activity_moon',
45 + },
46 + {
47 + id: 2770333,
48 + title: '禅修静心体验',
49 + link: bottom_banner_image_01,
50 + value: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=activity_meditation',
51 + },
52 + {
53 + id: 2770334,
54 + title: '法物流通',
55 + link: bottom_banner_image_02,
56 + value: '',
57 + },
58 + ],
59 + volunteer_home_icon: [
60 + {
61 + id: 2770335,
16 title: '地图导览', 62 title: '地图导览',
63 + link: '/pages/map-guide/index',
17 icon: 'fa-odnoklassniki-square', 64 icon: 'fa-odnoklassniki-square',
18 - link_url: '',
19 - page_url: '/pages/map-guide/index',
20 }, 65 },
21 { 66 {
22 - id: 'news', 67 + id: 2770336,
23 title: '法讯-最新动态', 68 title: '法讯-最新动态',
69 + link: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=news_list',
24 icon: 'icon-jingxiuying', 70 icon: 'icon-jingxiuying',
25 - link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=news_list',
26 - link_title: '法讯-最新动态',
27 }, 71 },
28 { 72 {
29 - id: 'intro', 73 + id: 2770337,
30 title: '寺院介绍', 74 title: '寺院介绍',
75 + link: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=temple_intro',
31 icon: 'icon-jingxiuying', 76 icon: 'icon-jingxiuying',
32 - link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=temple_intro',
33 - link_title: '寺院介绍',
34 }, 77 },
35 { 78 {
36 - id: 'practice', 79 + id: 2770338,
37 title: '共修活动', 80 title: '共修活动',
81 + link: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=practice',
38 icon: 'icon-jingxiuying', 82 icon: 'icon-jingxiuying',
39 - link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=practice',
40 - link_title: '共修活动',
41 }, 83 },
42 { 84 {
43 - id: 'volunteer', 85 + id: 2770339,
44 title: '义工报名', 86 title: '义工报名',
87 + link: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=volunteer',
45 icon: 'icon-jingxiuying', 88 icon: 'icon-jingxiuying',
46 - link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=volunteer',
47 - link_title: '义工报名',
48 }, 89 },
49 { 90 {
50 - id: 'contact', 91 + id: 2770340,
51 title: '联系我们', 92 title: '联系我们',
93 + link: '',
52 icon: 'icon-jingxiuying', 94 icon: 'icon-jingxiuying',
53 - link_url: '',
54 - },
55 - ],
56 - image_links: [
57 - {
58 - id: 'activity-01',
59 - title: '新春祈福法会',
60 - tag: '活动报名',
61 - image_url: activity_image_01,
62 - link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=activity_spring',
63 - link_title: '新春祈福法会',
64 - },
65 - {
66 - id: 'activity-02',
67 - title: '中秋月光茶会',
68 - tag: '活动报名',
69 - image_url: activity_image_02,
70 - link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=activity_moon',
71 - link_title: '中秋月光茶会',
72 - },
73 - {
74 - id: 'activity-03',
75 - title: '禅修静心体验',
76 - tag: '预约参加',
77 - image_url: activity_image_01,
78 - link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=activity_meditation',
79 - link_title: '禅修静心体验',
80 - },
81 - {
82 - id: 'activity-04',
83 - title: '法物流通',
84 - tag: '查看详情',
85 - image_url: activity_image_02,
86 - link_url: '',
87 }, 95 },
88 ], 96 ],
97 + title: '苏州觉林寺',
98 + color: '#d75b5b',
89 }) 99 })
......
...@@ -3,9 +3,9 @@ import { buildMockSuccess } from '../shared/response' ...@@ -3,9 +3,9 @@ import { buildMockSuccess } from '../shared/response'
3 3
4 export const homeMockHandlers = [ 4 export const homeMockHandlers = [
5 { 5 {
6 - action: 'home', 6 + action: 'app_list',
7 - type: 'content', 7 + type: 'volunteer',
8 method: 'GET', 8 method: 'GET',
9 - handle: () => buildMockSuccess(getHomeContentFixture(), '首页内容获取成功 (mock)'), 9 + handle: () => buildMockSuccess(getHomeContentFixture(), '首页内容获取成功(当前为 mock 结构)'),
10 }, 10 },
11 ] 11 ]
......
1 <template> 1 <template>
2 <view class="index-page"> 2 <view class="index-page">
3 <view class="page-content"> 3 <view class="page-content">
4 - <view 4 + <view v-if="hasTopBanner" class="home-top-banner">
5 - v-if="banner.image_url" 5 + <view v-if="isMockHomeData" class="mock-state-badge">
6 - class="home-banner" 6 + mock
7 - :class="{ clickable: hasLink(banner) }" 7 + </view>
8 - @tap="handleLinkedItemTap(banner)" 8 +
9 - > 9 + <nut-swiper
10 - <image class="home-banner__image" :src="banner.image_url" mode="aspectFill" /> 10 + v-if="shouldUseTopBannerSwiper"
11 + :auto-play="3000"
12 + :loop="true"
13 + :pagination-visible="true"
14 + pagination-color="#ffffff"
15 + pagination-unselected-color="rgba(255, 255, 255, 0.45)"
16 + height="210"
17 + >
18 + <nut-swiper-item
19 + v-for="item in topBanners"
20 + :key="item.id"
21 + >
22 + <view
23 + class="home-banner"
24 + :class="{ clickable: hasLink(item) }"
25 + @tap="handleLinkedItemTap(item)"
26 + >
27 + <image class="home-banner__image" :src="item.image_url" mode="aspectFill" />
28 + </view>
29 + </nut-swiper-item>
30 + </nut-swiper>
31 +
32 + <view
33 + v-else
34 + class="home-banner"
35 + :class="{ clickable: hasLink(topBanners[0]) }"
36 + @tap="handleLinkedItemTap(topBanners[0])"
37 + >
38 + <image class="home-banner__image" :src="topBanners[0].image_url" mode="aspectFill" />
39 + </view>
11 </view> 40 </view>
12 41
13 <view class="nav-panel"> 42 <view class="nav-panel">
14 <view 43 <view
15 - v-for="item in nav_list" 44 + v-for="item in homeIcons"
16 :key="item.id" 45 :key="item.id"
17 class="nav-entry" 46 class="nav-entry"
18 :class="{ clickable: hasLink(item) }" 47 :class="{ clickable: hasLink(item) }"
...@@ -30,7 +59,7 @@ ...@@ -30,7 +59,7 @@
30 59
31 <view class="image-link-list"> 60 <view class="image-link-list">
32 <view 61 <view
33 - v-for="item in image_links" 62 + v-for="item in bottomBanners"
34 :key="item.id" 63 :key="item.id"
35 class="image-link-card" 64 class="image-link-card"
36 :class="{ clickable: hasLink(item) }" 65 :class="{ clickable: hasLink(item) }"
...@@ -46,42 +75,83 @@ ...@@ -46,42 +75,83 @@
46 </template> 75 </template>
47 76
48 <script setup> 77 <script setup>
49 -import { ref } from 'vue' 78 +import { computed, ref } from 'vue'
50 import Taro, { useLoad } from '@tarojs/taro' 79 import Taro, { useLoad } from '@tarojs/taro'
51 import AppTabbar from '@/components/AppTabbar.vue' 80 import AppTabbar from '@/components/AppTabbar.vue'
52 import { getHomeContentAPI } from '@/api' 81 import { getHomeContentAPI } from '@/api'
53 import { buildWebviewPreviewUrl } from '@/utils/webview' 82 import { buildWebviewPreviewUrl } from '@/utils/webview'
83 +import { isMockEnabled } from '@/utils/config'
54 84
55 -const default_icon = 'icon-jingxiuying' 85 +const defaultIcon = 'icon-jingxiuying'
56 -const banner = ref({}) 86 +const homeContent = ref({
57 -const nav_list = ref([]) 87 + volunteer_top_banner: [],
58 -const image_links = ref([]) 88 + volunteer_bottom_banner: [],
89 + volunteer_home_icon: [],
90 + title: '',
91 + color: '',
92 +})
93 +
94 +const topBanners = computed(() => homeContent.value.volunteer_top_banner || [])
95 +const bottomBanners = computed(() => homeContent.value.volunteer_bottom_banner || [])
96 +const homeIcons = computed(() => homeContent.value.volunteer_home_icon || [])
97 +const hasTopBanner = computed(() => topBanners.value.some((item) => item?.image_url))
98 +const shouldUseTopBannerSwiper = computed(() => topBanners.value.filter((item) => item?.image_url).length > 1)
99 +const isMockHomeData = computed(() => isMockEnabled())
100 +
101 +const getItemTargetUrl = (item) => String(item?.target_url || '').trim()
59 102
60 -const hasLink = (item) => !!(item?.page_url || item?.link_url) 103 +const hasLink = (item) => !!getItemTargetUrl(item)
104 +
105 +const isInternalMiniProgramPath = (url) => String(url || '').startsWith('/pages/')
61 106
62 const getIconClass = (item) => { 107 const getIconClass = (item) => {
63 - const icon = item?.icon || default_icon 108 + const icon = item?.icon || defaultIcon
64 - const font_class = icon.startsWith('fa-') ? 'fa' : 'iconfont' 109 + const fontClass = icon.startsWith('fa-') ? 'fa' : 'iconfont'
65 - return [font_class, icon] 110 + return [fontClass, icon]
66 } 111 }
67 112
68 const handleLinkedItemTap = (item) => { 113 const handleLinkedItemTap = (item) => {
69 - if (!item) return 114 + if (!item || !hasLink(item)) return
115 +
116 + const targetUrl = getItemTargetUrl(item)
70 117
71 - if (item.page_url) { 118 + if (isInternalMiniProgramPath(targetUrl)) {
72 Taro.navigateTo({ 119 Taro.navigateTo({
73 - url: item.page_url, 120 + url: targetUrl,
74 }) 121 })
75 return 122 return
76 } 123 }
77 124
78 - if (item.link_url) { 125 + Taro.navigateTo({
79 - Taro.navigateTo({ 126 + url: buildWebviewPreviewUrl(targetUrl, item?.title || ''),
80 - url: buildWebviewPreviewUrl(item.link_url, item.link_title || item.title || ''), 127 + })
81 - })
82 - }
83 } 128 }
84 129
130 +const normalizeHomeContent = (data = {}) => ({
131 + volunteer_top_banner: Array.isArray(data.volunteer_top_banner)
132 + ? data.volunteer_top_banner.map((item) => ({
133 + ...item,
134 + image_url: item?.link || '',
135 + target_url: item?.value || '',
136 + }))
137 + : [],
138 + volunteer_bottom_banner: Array.isArray(data.volunteer_bottom_banner)
139 + ? data.volunteer_bottom_banner.map((item) => ({
140 + ...item,
141 + image_url: item?.link || '',
142 + target_url: item?.value || '',
143 + }))
144 + : [],
145 + volunteer_home_icon: Array.isArray(data.volunteer_home_icon)
146 + ? data.volunteer_home_icon.map((item) => ({
147 + ...item,
148 + target_url: item?.link || '',
149 + }))
150 + : [],
151 + title: data.title || '',
152 + color: data.color || '',
153 +})
154 +
85 const fetchHomeContent = async () => { 155 const fetchHomeContent = async () => {
86 const response = await getHomeContentAPI() 156 const response = await getHomeContentAPI()
87 157
...@@ -93,10 +163,7 @@ const fetchHomeContent = async () => { ...@@ -93,10 +163,7 @@ const fetchHomeContent = async () => {
93 return 163 return
94 } 164 }
95 165
96 - const data = response?.data || {} 166 + homeContent.value = normalizeHomeContent(response?.data)
97 - banner.value = data.banner || {}
98 - nav_list.value = data.nav_list || []
99 - image_links.value = data.image_links || []
100 } 167 }
101 168
102 useLoad(() => { 169 useLoad(() => {
...@@ -114,8 +181,28 @@ useLoad(() => { ...@@ -114,8 +181,28 @@ useLoad(() => {
114 box-sizing: border-box; 181 box-sizing: border-box;
115 } 182 }
116 183
117 - .home-banner { 184 + .home-top-banner {
185 + position: relative;
118 margin: -32rpx -32rpx 0; 186 margin: -32rpx -32rpx 0;
187 + }
188 +
189 + .mock-state-badge {
190 + position: absolute;
191 + top: 20rpx;
192 + right: 20rpx;
193 + z-index: 3;
194 + padding: 8rpx 16rpx;
195 + border-radius: 999rpx;
196 + background: rgba(0, 0, 0, 0.42);
197 + font-size: 20rpx;
198 + line-height: 1;
199 + color: #ffffff;
200 + letter-spacing: 1rpx;
201 + text-transform: uppercase;
202 + pointer-events: none;
203 + }
204 +
205 + .home-banner {
119 height: 420rpx; 206 height: 420rpx;
120 overflow: hidden; 207 overflow: hidden;
121 background: #e5e7eb; 208 background: #e5e7eb;
......
1 /* 1 /*
2 * @Description: API 环境配置 2 * @Description: API 环境配置
3 + * 现在接口有的没有 f, 有的 f 不一样, 每个接口单独传 f 参数
3 * @Note: 当前环境只由构建配置控制;本地开发默认 mock,生产构建默认正式环境 4 * @Note: 当前环境只由构建配置控制;本地开发默认 mock,生产构建默认正式环境
4 */ 5 */
5 6
...@@ -9,7 +10,7 @@ export const API_ENVIRONMENTS = { ...@@ -9,7 +10,7 @@ export const API_ENVIRONMENTS = {
9 label: '正式环境', 10 label: '正式环境',
10 baseURL: 'https://oa.onwall.cn', 11 baseURL: 'https://oa.onwall.cn',
11 requestDefaultParams: { 12 requestDefaultParams: {
12 - f: 'room', 13 + // f: 'room',
13 client_id: '772428', 14 client_id: '772428',
14 }, 15 },
15 useMock: false, 16 useMock: false,
...@@ -19,7 +20,7 @@ export const API_ENVIRONMENTS = { ...@@ -19,7 +20,7 @@ export const API_ENVIRONMENTS = {
19 label: '本地 Mock 环境', 20 label: '本地 Mock 环境',
20 baseURL: 'https://oa.onwall.cn', 21 baseURL: 'https://oa.onwall.cn',
21 requestDefaultParams: { 22 requestDefaultParams: {
22 - f: 'room', 23 + // f: 'room',
23 client_id: '772428', 24 client_id: '772428',
24 }, 25 },
25 useMock: true, 26 useMock: true,
......