hookehuyr

feat(home): 实现首页内容动态渲染

- 新增首页内容 API 接口及 mock 数据
- 重构首页 UI,移除静态占位内容,改为动态渲染 banner、导航和图文链接
- 添加点击跳转逻辑,支持页面内跳转和 WebView 打开外部链接
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 PAY_TEST: '/srv/?a=pay', 5 PAY_TEST: '/srv/?a=pay',
5 } 6 }
6 7
8 +export const getHomeContentAPI = () => fn(fetch.get(Api.HOME_CONTENT))
9 +
7 /** 10 /**
8 * @description 获取微信支付参数(对齐 meihuaApp 的支付接口) 11 * @description 获取微信支付参数(对齐 meihuaApp 的支付接口)
9 * @param {Object} params 请求参数 12 * @param {Object} params 请求参数
......
1 +const 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'
3 +const activity_image_02 = 'https://cdn.ipadbiz.cn/jls_weapp/images/banner03@2x.png'
4 +
5 +export const getHomeContentFixture = () => ({
6 + 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 + {
15 + id: 'map-guide',
16 + title: '地图导览',
17 + icon: 'icon-jingxiuying',
18 + link_url: '',
19 + page_url: '/pages/map-guide/index',
20 + },
21 + {
22 + id: 'news',
23 + title: '法讯-最新动态',
24 + icon: 'icon-jingxiuying',
25 + link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=news_list',
26 + link_title: '法讯-最新动态',
27 + },
28 + {
29 + id: 'intro',
30 + title: '寺院介绍',
31 + icon: 'icon-jingxiuying',
32 + link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=temple_intro',
33 + link_title: '寺院介绍',
34 + },
35 + {
36 + id: 'practice',
37 + title: '共修活动',
38 + icon: 'icon-jingxiuying',
39 + link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=practice',
40 + link_title: '共修活动',
41 + },
42 + {
43 + id: 'volunteer',
44 + title: '义工报名',
45 + icon: 'icon-jingxiuying',
46 + link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=volunteer',
47 + link_title: '义工报名',
48 + },
49 + {
50 + id: 'contact',
51 + title: '联系我们',
52 + 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 + },
88 + ],
89 +})
1 +import { getHomeContentFixture } from '../fixtures/home.fixture'
2 +import { buildMockSuccess } from '../shared/response'
3 +
4 +export const homeMockHandlers = [
5 + {
6 + action: 'home',
7 + type: 'content',
8 + method: 'GET',
9 + handle: () => buildMockSuccess(getHomeContentFixture(), '首页内容获取成功 (mock)'),
10 + },
11 +]
...@@ -2,10 +2,12 @@ import { authMockHandlers } from './auth.mock' ...@@ -2,10 +2,12 @@ import { authMockHandlers } from './auth.mock'
2 import { paymentMockHandlers } from './payment.mock' 2 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 6
6 export const mockHandlers = [ 7 export const mockHandlers = [
7 ...authMockHandlers, 8 ...authMockHandlers,
8 ...paymentMockHandlers, 9 ...paymentMockHandlers,
9 ...commonMockHandlers, 10 ...commonMockHandlers,
10 ...messageMockHandlers, 11 ...messageMockHandlers,
12 + ...homeMockHandlers,
11 ] 13 ]
......
1 -import { createMessageFixtureList } from '../fixtures/message.fixture' 1 +import { MESSAGE_STATUS, createMessageFixtureList } from '../fixtures/message.fixture'
2 2
3 let messageList = null 3 let messageList = null
4 4
......
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 class="hero-card"> 4 + <view
5 - <text class="hero-eyebrow">觉林寺小程序</text> 5 + v-if="banner.image_url"
6 - <text class="hero-title">首页</text> 6 + class="home-banner"
7 - <text class="hero-desc"> 7 + :class="{ clickable: hasLink(banner) }"
8 - 当前先完成首页、消息、我的三栏结构,测试能力统一收口到测试中心,避免首页继续堆放调试按钮。 8 + @tap="handleLinkedItemTap(banner)"
9 - </text> 9 + >
10 + <image class="home-banner__image" :src="banner.image_url" mode="aspectFill" />
10 </view> 11 </view>
11 12
12 - <view class="status-card"> 13 + <view class="nav-panel">
13 - <view> 14 + <view
14 - <text class="section-label">当前授权状态</text> 15 + v-for="item in nav_list"
15 - <text class="status-text">应用启动时会优先尝试静默授权</text> 16 + :key="item.id"
16 - </view> 17 + class="nav-entry"
17 - <text class="status-tag" :class="{ authed: isAuthed }"> 18 + :class="{ clickable: hasLink(item) }"
18 - {{ isAuthed ? '已授权' : '未授权' }} 19 + @tap="handleLinkedItemTap(item)"
19 - </text> 20 + >
20 - </view> 21 + <view class="nav-entry__icon-wrap">
21 - 22 + <i
22 - <view class="overview-grid"> 23 + class="fa iconfont iconfont-demo"
23 - <view class="overview-card"> 24 + :class="item.icon || default_icon"
24 - <view class="card-title-row"> 25 + ></i>
25 - <text class="card-title">首页</text>
26 - <i class="fa iconfont icon-jingxiuying iconfont-demo"></i>
27 </view> 26 </view>
28 - <text class="card-desc">展示当前项目概览、地图导览与测试入口。</text> 27 + <text class="nav-entry__title">{{ item.title }}</text>
29 - </view>
30 - <view class="overview-card">
31 - <text class="card-title">消息</text>
32 - <text class="card-desc">后续承接通知、订单提醒与系统消息。</text>
33 </view> 28 </view>
34 - <view class="overview-card">
35 - <text class="card-title">我的</text>
36 - <text class="card-desc">后续承接个人信息、授权状态与常用功能。</text>
37 - </view>
38 - </view>
39 -
40 - <view class="map-entry-card">
41 - <text class="section-label">地图导览</text>
42 - <text class="map-entry-desc">
43 - 当前通过小程序 WebView 承载地图项目,方便从首页直接进入步行导览页面。
44 - </text>
45 - <button class="primary-btn" @tap="goToMapGuide">打开地图导览</button>
46 </view> 29 </view>
47 30
48 - <view class="webview-entry-card"> 31 + <view class="image-link-list">
49 - <text class="section-label">模拟 WebView</text> 32 + <view
50 - <text class="webview-entry-desc"> 33 + v-for="item in image_links"
51 - 这里提供一个首页直达的测试 WebView 入口,用于验证通用 WebView 承载能力。 34 + :key="item.id"
52 - </text> 35 + class="image-link-card"
53 - <button class="outline-btn" @tap="goToMockWebview">打开模拟 WebView</button> 36 + :class="{ clickable: hasLink(item) }"
54 - </view> 37 + @tap="handleLinkedItemTap(item)"
55 - 38 + >
56 - <view class="test-entry-card"> 39 + <image class="image-link-card__image" :src="item.image_url" mode="aspectFill" />
57 - <text class="section-label">测试入口</text> 40 + </view>
58 - <text class="test-entry-desc">
59 - 支付测试等调试能力仍统一收口到测试中心,方便集中联调。
60 - </text>
61 - <button class="outline-btn" @tap="goToTestCenter">进入测试中心</button>
62 </view> 41 </view>
63 </view> 42 </view>
64 43
...@@ -68,197 +47,171 @@ ...@@ -68,197 +47,171 @@
68 47
69 <script setup> 48 <script setup>
70 import { ref } from 'vue' 49 import { ref } from 'vue'
71 -import Taro, { useDidShow } from '@tarojs/taro' 50 +import Taro, { useLoad } from '@tarojs/taro'
72 import AppTabbar from '@/components/AppTabbar.vue' 51 import AppTabbar from '@/components/AppTabbar.vue'
73 -import { hasAuth } from '@/utils/authRedirect' 52 +import { getHomeContentAPI } from '@/api'
74 import { buildWebviewPreviewUrl } from '@/utils/webview' 53 import { buildWebviewPreviewUrl } from '@/utils/webview'
75 54
76 -const mock_webview_url = 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list' 55 +const default_icon = 'icon-jingxiuying'
77 -const mock_webview_title = '福田首页' 56 +const banner = ref({})
57 +const nav_list = ref([])
58 +const image_links = ref([])
78 59
79 -const isAuthed = ref(false) 60 +const hasLink = (item) => !!(item?.page_url || item?.link_url)
80 61
81 -const refreshAuthStatus = () => { 62 +const handleLinkedItemTap = (item) => {
82 - isAuthed.value = hasAuth() 63 + if (!item) return
83 -}
84 64
85 -const goToMapGuide = () => { 65 + if (item.page_url) {
86 - Taro.navigateTo({ 66 + Taro.navigateTo({
87 - url: '/pages/map-guide/index', 67 + url: item.page_url,
88 - }) 68 + })
89 -} 69 + return
70 + }
90 71
91 -const goToMockWebview = () => { 72 + if (item.link_url) {
92 - Taro.navigateTo({ 73 + Taro.navigateTo({
93 - url: buildWebviewPreviewUrl(mock_webview_url, mock_webview_title), 74 + url: buildWebviewPreviewUrl(item.link_url, item.link_title || item.title || ''),
94 - }) 75 + })
76 + }
95 } 77 }
96 78
97 -const goToTestCenter = () => { 79 +const fetchHomeContent = async () => {
98 - Taro.navigateTo({ 80 + const response = await getHomeContentAPI()
99 - url: '/pages/pay-test/index', 81 +
100 - }) 82 + if (response?.code !== 1) {
83 + Taro.showToast({
84 + title: response?.msg || '获取首页内容失败',
85 + icon: 'none',
86 + })
87 + return
88 + }
89 +
90 + const data = response?.data || {}
91 + banner.value = data.banner || {}
92 + nav_list.value = data.nav_list || []
93 + image_links.value = data.image_links || []
101 } 94 }
102 95
103 -useDidShow(() => { 96 +useLoad(() => {
104 - refreshAuthStatus() 97 + fetchHomeContent()
105 }) 98 })
106 </script> 99 </script>
107 100
108 <style lang="less"> 101 <style lang="less">
109 .index-page { 102 .index-page {
110 min-height: 100vh; 103 min-height: 100vh;
111 - background: 104 + background: #f1f1f1;
112 - radial-gradient(circle at top right, rgba(166, 121, 57, 0.18), transparent 32%),
113 - linear-gradient(180deg, #fffaf3 0%, #f6f7fb 100%);
114 105
115 .page-content { 106 .page-content {
116 - padding: 32rpx 24rpx 0; 107 + padding: 32rpx 32rpx 0;
117 - box-sizing: border-box;
118 - }
119 -
120 - .hero-card,
121 - .status-card,
122 - .overview-card,
123 - .map-entry-card,
124 - .webview-entry-card,
125 - .test-entry-card {
126 - background: rgba(255, 255, 255, 0.94);
127 - border: 2rpx solid rgba(166, 121, 57, 0.08);
128 - border-radius: 28rpx;
129 - box-shadow: 0 20rpx 60rpx rgba(15, 23, 42, 0.06);
130 box-sizing: border-box; 108 box-sizing: border-box;
131 } 109 }
132 110
133 - .hero-card { 111 + .home-banner {
134 - display: flex; 112 + margin: -32rpx -32rpx 0;
135 - flex-direction: column; 113 + height: 420rpx;
136 - padding: 36rpx 32rpx; 114 + overflow: hidden;
137 - } 115 + background: #e5e7eb;
138 -
139 - .hero-eyebrow {
140 - font-size: 24rpx;
141 - font-weight: 600;
142 - letter-spacing: 4rpx;
143 - color: #a67939;
144 } 116 }
145 117
146 - .hero-title { 118 + .home-banner__image,
147 - margin-top: 12rpx; 119 + .image-link-card__image {
148 - font-size: 52rpx; 120 + display: block;
149 - font-weight: 700; 121 + width: 100%;
150 - color: #1f2937; 122 + height: 100%;
151 } 123 }
152 124
153 - .hero-desc { 125 + .nav-panel {
154 - margin-top: 18rpx; 126 + display: grid;
155 - font-size: 26rpx; 127 + grid-template-columns: repeat(3, minmax(0, 1fr));
156 - line-height: 1.8; 128 + margin: 32rpx 0 0;
157 - color: #6b7280; 129 + overflow: hidden;
130 + border-radius: 16rpx;
131 + background: #fff;
132 + box-shadow: 0 16rpx 40rpx rgba(31, 41, 55, 0.06);
158 } 133 }
159 134
160 - .status-card { 135 + .nav-entry {
161 - margin-top: 24rpx; 136 + position: relative;
162 - padding: 28rpx 32rpx;
163 display: flex; 137 display: flex;
138 + flex-direction: column;
164 align-items: center; 139 align-items: center;
165 - justify-content: space-between; 140 + justify-content: center;
166 - gap: 24rpx; 141 + min-width: 0;
167 - } 142 + min-height: 184rpx;
168 - 143 + padding: 28rpx 8rpx 24rpx;
169 - .section-label { 144 + box-sizing: border-box;
170 - display: block; 145 + color: #98271d;
171 - font-size: 30rpx;
172 - font-weight: 600;
173 - color: #111827;
174 - }
175 -
176 - .status-text,
177 - .card-desc,
178 - .map-entry-desc,
179 - .webview-entry-desc,
180 - .test-entry-desc {
181 - display: block;
182 - margin-top: 12rpx;
183 - font-size: 24rpx;
184 - line-height: 1.7;
185 - color: #6b7280;
186 - }
187 -
188 - .status-tag {
189 - flex-shrink: 0;
190 - padding: 12rpx 22rpx;
191 - border-radius: 999rpx;
192 - font-size: 24rpx;
193 - font-weight: 600;
194 - color: #b45309;
195 - background: #fef3c7;
196 - }
197 -
198 - .status-tag.authed {
199 - color: #166534;
200 - background: #dcfce7;
201 - }
202 -
203 - .overview-grid {
204 - margin-top: 24rpx;
205 - display: grid;
206 - grid-template-columns: repeat(2, minmax(0, 1fr));
207 - gap: 20rpx;
208 } 146 }
209 147
210 - .overview-card { 148 + .nav-entry::after {
211 - padding: 28rpx; 149 + position: absolute;
150 + top: 0;
151 + right: 0;
152 + width: 1rpx;
153 + height: 100%;
154 + background: #ececec;
155 + content: '';
212 } 156 }
213 157
214 - .overview-card:last-child { 158 + .nav-entry:nth-child(3n)::after {
215 - grid-column: 1 / span 2; 159 + display: none;
216 } 160 }
217 161
218 - .card-title { 162 + .nav-entry:nth-child(n + 4)::before {
219 - display: block; 163 + position: absolute;
220 - font-size: 30rpx; 164 + top: 0;
221 - font-weight: 600; 165 + left: 0;
222 - color: #111827; 166 + width: 100%;
167 + height: 1rpx;
168 + background: #ececec;
169 + content: '';
223 } 170 }
224 171
225 - .card-title-row { 172 + .nav-entry__icon-wrap {
226 display: flex; 173 display: flex;
227 align-items: center; 174 align-items: center;
228 - justify-content: space-between; 175 + justify-content: center;
229 - gap: 16rpx; 176 + width: 76rpx;
177 + height: 76rpx;
178 + line-height: 1;
230 } 179 }
231 180
232 .iconfont-demo { 181 .iconfont-demo {
233 - flex-shrink: 0; 182 + font-size: 64rpx;
234 - font-size: 42rpx; 183 + color: #98271d;
235 - color: #a67939;
236 } 184 }
237 185
238 - .map-entry-card, 186 + .nav-entry__title {
239 - .webview-entry-card, 187 + display: block;
240 - .test-entry-card { 188 + width: 100%;
241 - margin-top: 24rpx; 189 + margin-top: 18rpx;
242 - padding: 32rpx; 190 + font-size: 28rpx;
191 + line-height: 1.25;
192 + text-align: center;
193 + color: #98271d;
194 + word-break: break-all;
243 } 195 }
244 196
245 - .primary-btn, 197 + .image-link-list {
246 - .outline-btn { 198 + display: flex;
247 - margin-top: 24rpx; 199 + flex-direction: column;
248 - border-radius: 999rpx; 200 + gap: 32rpx;
249 - font-size: 30rpx; 201 + margin-top: 40rpx;
250 - line-height: 88rpx; 202 + padding: 0 4rpx 40rpx;
251 } 203 }
252 204
253 - .primary-btn { 205 + .image-link-card {
254 - color: #fff; 206 + position: relative;
255 - background: linear-gradient(135deg, #a67939, #8f5e20); 207 + height: 252rpx;
208 + overflow: hidden;
209 + border-radius: 12rpx;
210 + background: #d1d5db;
256 } 211 }
257 212
258 - .outline-btn { 213 + .clickable {
259 - color: #0f172a; 214 + cursor: pointer;
260 - background: #fff;
261 - border: 2rpx solid #d1d5db;
262 } 215 }
263 } 216 }
264 </style> 217 </style>
......