hookehuyr

feat(message): 将资讯页改为 WebView 容器模式

- 移除资讯页原有的原生列表/详情逻辑,改为与“应用/我的”一致的 WebView 容器
- 在 tabbar 配置和 mock 数据中为资讯项添加 webview_url 和 webview_title
- 更新项目文档,说明资讯链路已统一为 WebView 承接,旧版原生资讯页转为参考链路
1 # 仓库指南 1 # 仓库指南
2 2
3 ## 项目功能概览 3 ## 项目功能概览
4 -当前项目不是一个只有单页示例的小程序壳,而是一套已经串起“首页内容分发 + 动态底部导航 + 资讯列表/详情 + WebView 容器页 + 授权 + 微信支付桥接”的业务骨架。 4 +当前项目不是一个只有单页示例的小程序壳,而是一套已经串起“首页内容分发 + 动态底部导航 + 资讯/应用/我的 WebView 承接 + 授权 + 微信支付桥接”的业务骨架。
5 5
6 - 首页主链路在 `src/pages/index/`,通过 `src/api/index.js` 拉取 banner、宫格导航和图片入口;点击后要么跳内部页面,要么通过 `src/utils/webview.js` 组装参数进入通用 `WebView` 容器。 6 - 首页主链路在 `src/pages/index/`,通过 `src/api/index.js` 拉取 banner、宫格导航和图片入口;点击后要么跳内部页面,要么通过 `src/utils/webview.js` 组装参数进入通用 `WebView` 容器。
7 -- 底部导航不是写死在页面里的,核心在 `src/components/AppTabbar.vue``src/stores/tabbar.js``src/api/tabbar.js``src/utils/tabbar.js`。应用启动时会预加载 tabbar 配置,`application``mine` 现在都是“按接口返回地址承接 WebView”的容器页 7 +- 底部导航不是写死在页面里的,核心在 `src/components/AppTabbar.vue``src/stores/tabbar.js``src/api/tabbar.js``src/utils/tabbar.js`。应用启动时会预加载 tabbar 配置,`message``application``mine` 现在都按接口返回地址承接 WebView
8 -- 资讯演示链路在 `src/pages/message/``src/pages/message-detail/`,既是当前业务页,也是本地 mock 是否支持“列表 -> 详情 -> 已读状态变化”的验证入口 8 +- `src/pages/message/` 现在已经切到和导航栏“应用 / 我的”一致的 WebView 容器模式;`src/pages/message-detail/``src/api/message.js` 更适合作为旧版原生资讯列表/详情演示链路参考,不应再默认当作线上资讯主入口继续扩展
9 - 支付相关目前有三类页面:`src/pages/pay-test/` 用于手工调试授权和支付参数;`src/pages/pay-confirm/` 是用户确认金额后点击支付的正式按钮页;`src/pages/pay-bridge/` 是给 H5/WebView 调起小程序支付用的桥页,负责自动授权、拉起支付、展示结果并返回上一页。 9 - 支付相关目前有三类页面:`src/pages/pay-test/` 用于手工调试授权和支付参数;`src/pages/pay-confirm/` 是用户确认金额后点击支付的正式按钮页;`src/pages/pay-bridge/` 是给 H5/WebView 调起小程序支付用的桥页,负责自动授权、拉起支付、展示结果并返回上一页。
10 - `src/pages/webview-preview/` 是通用外链承接页,`src/pages/application/``src/pages/mine/`、首页外链入口都会复用这类能力;`src/pages/map-guide/` 是固定地图签到 H5 页;`src/pages/auth/` 是统一授权页,不能绕过。 10 - `src/pages/webview-preview/` 是通用外链承接页,`src/pages/application/``src/pages/mine/`、首页外链入口都会复用这类能力;`src/pages/map-guide/` 是固定地图签到 H5 页;`src/pages/auth/` 是统一授权页,不能绕过。
11 - `src/pages/mine-backup/` 目前更适合作为旧版“我的”页视觉与交互备份参考,不应默认当作线上主链路去扩展新业务。 11 - `src/pages/mine-backup/` 目前更适合作为旧版“我的”页视觉与交互备份参考,不应默认当作线上主链路去扩展新业务。
...@@ -13,11 +13,11 @@ ...@@ -13,11 +13,11 @@
13 ## 项目结构与模块组织 13 ## 项目结构与模块组织
14 源码位于 `src/`,应用入口为 `src/app.js``src/app.config.js`。当前目录分工建议按下面理解,而不是只把它当成普通 Taro 模板: 14 源码位于 `src/`,应用入口为 `src/app.js``src/app.config.js`。当前目录分工建议按下面理解,而不是只把它当成普通 Taro 模板:
15 15
16 -- `src/pages/`:页面路由主目录。当前重点页面包括 `index``message``message-detail``application``mine``pay-test``pay-confirm``pay-bridge``webview-preview``map-guide``auth`;其中 `application` / `mine` 更偏 WebView 容器,`pay-bridge` 更偏桥接页,`mine-backup` 是备份页。 16 +- `src/pages/`:页面路由主目录。当前重点页面包括 `index``message``message-detail``application``mine``pay-test``pay-confirm``pay-bridge``webview-preview``map-guide``auth`;其中 `message` / `application` / `mine` 都更偏 WebView 容器,`pay-bridge` 更偏桥接页,`message-detail``mine-backup` 更偏旧链路参考页。
17 - `src/components/`:通用组件目录。当前最关键的是 `AppTabbar.vue``PosterBuilder/`、二维码组件、时间选择器等属于可复用能力模块。 17 - `src/components/`:通用组件目录。当前最关键的是 `AppTabbar.vue``PosterBuilder/`、二维码组件、时间选择器等属于可复用能力模块。
18 - `src/composables/`:组合式业务逻辑目录。`useWechatMiniPay.js` 是当前支付链路核心,和页面解耦较强;离线预约缓存相关逻辑也集中在这里。 18 - `src/composables/`:组合式业务逻辑目录。`useWechatMiniPay.js` 是当前支付链路核心,和页面解耦较强;离线预约缓存相关逻辑也集中在这里。
19 - `src/hooks/`:较轻量的通用 hooks,目前主要是 `useGo.js` 这类导航辅助。 19 - `src/hooks/`:较轻量的通用 hooks,目前主要是 `useGo.js` 这类导航辅助。
20 -- `src/api/`:接口封装层。`fn.js` 是统一返回格式与 mock 接入的总入口,`index.js``message.js``tabbar.js``wx/pay.js` 分别承接首页、资讯、底部导航、微信支付等接口。 20 +- `src/api/`:接口封装层。`fn.js` 是统一返回格式与 mock 接入的总入口,`index.js``tabbar.js``wx/pay.js` 分别承接首页、底部导航、微信支付等当前主链路接口;`message.js` 目前更偏旧版原生资讯列表/详情演示接口。
21 - `src/mock/`:本地 mock 体系。目录拆分规则是 `index.js` 统一入口、`modules/` 放 handler、`shared/` 放公共解析能力、`stores/` 放有状态 mock 数据、`fixtures/` 放静态样本。 21 - `src/mock/`:本地 mock 体系。目录拆分规则是 `index.js` 统一入口、`modules/` 放 handler、`shared/` 放公共解析能力、`stores/` 放有状态 mock 数据、`fixtures/` 放静态样本。
22 - `src/utils/`:公共工具层。当前高频核心文件是 `authRedirect.js``request.js``config.js``webview.js``tabbar.js``wechatPay.js`;改动授权、环境、WebView 路由或支付时优先先看这里。 22 - `src/utils/`:公共工具层。当前高频核心文件是 `authRedirect.js``request.js``config.js``webview.js``tabbar.js``wechatPay.js`;改动授权、环境、WebView 路由或支付时优先先看这里。
23 - `src/stores/`:Pinia 状态目录。当前重点是 `tabbar.js`(底部导航配置)和 `router.js`(授权回跳来源页),其他 store 多为基础能力或历史保留。 23 - `src/stores/`:Pinia 状态目录。当前重点是 `tabbar.js`(底部导航配置)和 `router.js`(授权回跳来源页),其他 store 多为基础能力或历史保留。
......
...@@ -17,7 +17,9 @@ ...@@ -17,7 +17,9 @@
17 17
18 ## 资讯链路 18 ## 资讯链路
19 19
20 -- [ ] 资讯页面还需要真实接口获取列表数据,后续要和详情页、已读状态链路一起确认真实字段。 20 +- [x] 资讯 tab 已调整为和“应用 / 我的”一致的 WebView 承接模式,当前地址由底部导航配置统一控制。
21 +- [ ] 资讯 tab 还需要联调真实配置接口,继续确认标题、显隐和 WebView 地址是否正确。
22 +- [ ] 旧版 `message-detail``src/api/message.js` 原生列表/详情演示链路后续再决定是继续保留为参考页,还是统一下线清理。
21 23
22 ## 授权与支付 24 ## 授权与支付
23 25
......
...@@ -9,6 +9,8 @@ export const getTabbarConfigFixture = () => ({ ...@@ -9,6 +9,8 @@ export const getTabbarConfigFixture = () => ({
9 key: 'message', 9 key: 'message',
10 title: '资讯', 10 title: '资讯',
11 visible: true, 11 visible: true,
12 + webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=news_list',
13 + webview_title: '资讯',
12 }, 14 },
13 { 15 {
14 key: 'application', 16 key: 'application',
......
1 <template> 1 <template>
2 - <view class="message-page"> 2 + <web-view v-if="preview_url" :src="preview_url" />
3 - <view class="page-content"> 3 + <view v-else class="tab-webview-page">
4 - <view class="hero-card"> 4 + <view class="empty-card">
5 - <view class="hero-head"> 5 + <text class="empty-title">暂未配置资讯地址</text>
6 - <text class="hero-title">资讯</text> 6 + <text class="empty-desc">
7 - <text class="env-tag" :class="{ mock: current_env_use_mock }"> 7 + 当前资讯页已经改成 WebView 承接模式,请先检查底部导航配置接口是否返回了可用地址。
8 - {{ current_env_label }}
9 </text> 8 </text>
10 </view> 9 </view>
11 - <text class="hero-desc">
12 - 当前环境由配置文件统一控制。本地如果要让所有接口都走 mock,只需要改 `config/dev.js` 里的 `API_RUNTIME_ENV`。
13 - </text>
14 - </view>
15 -
16 - <view class="toolbar-card">
17 - <view>
18 - <text class="section-title">列表状态</text>
19 - <text class="section-desc">
20 - 共 {{ total }} 条资讯,未读 {{ unread_count }} 条。进入详情页后,当前资讯会在 mock 中自动变成已读。
21 - </text>
22 - </view>
23 - <button class="refresh-btn" :loading="loading" @tap="handleRefresh">
24 - 刷新
25 - </button>
26 - </view>
27 -
28 - <view v-if="loading && !message_list.length" class="placeholder-card">
29 - <text class="section-title">加载中</text>
30 - <text class="section-desc">
31 - 正在拉取资讯列表...
32 - </text>
33 - </view>
34 -
35 - <view v-else-if="!message_list.length" class="placeholder-card">
36 - <text class="section-title">暂无资讯</text>
37 - <text class="section-desc">
38 - 当前接口还没有返回资讯内容,可以先在 mock 里补结构,再回到这个页面验证展示效果。
39 - </text>
40 - </view>
41 -
42 - <view
43 - v-for="item in message_list"
44 - :key="item.id"
45 - class="message-card"
46 - @tap="goToDetail(item.id)"
47 - >
48 - <view class="message-top">
49 - <view class="message-meta">
50 - <text class="message-category">{{ item.category }}</text>
51 - <text class="message-time">{{ item.created_time }}</text>
52 - </view>
53 - <text class="message-status" :class="{ unread: item.status === 'send' }">
54 - {{ item.status === 'send' ? '未读' : '已读' }}
55 - </text>
56 - </view>
57 - <text class="message-title">{{ item.title }}</text>
58 - <text class="message-summary">{{ item.summary }}</text>
59 - </view>
60 -
61 - <button
62 - v-if="has_more && message_list.length"
63 - class="load-more-btn"
64 - :loading="loading_more"
65 - @tap="handleLoadMore"
66 - >
67 - 加载更多
68 - </button>
69 - </view>
70 -
71 - <AppTabbar current="message" />
72 </view> 10 </view>
73 </template> 11 </template>
74 12
75 <script setup> 13 <script setup>
76 -import { computed, ref } from 'vue' 14 +import { ref } from 'vue'
77 -import Taro, { useDidShow, useLoad } from '@tarojs/taro' 15 +import Taro, { useLoad } from '@tarojs/taro'
78 -import AppTabbar from '@/components/AppTabbar.vue' 16 +import { useTabbarStore } from '@/stores/tabbar'
79 -import { messageListAPI } from '@/api/message'
80 -import { getCurrentApiConfig } from '@/utils/config'
81 17
82 -const api_config = getCurrentApiConfig() 18 +const tabbarStore = useTabbarStore()
83 -const current_env_label = api_config.label 19 +const preview_url = ref('')
84 -const current_env_use_mock = api_config.useMock 20 +const default_page_title = '资讯'
85 -const message_list = ref([])
86 -const page = ref(0)
87 -const page_size = 6
88 -const total = ref(0)
89 -const has_more = ref(true)
90 -const loading = ref(false)
91 -const loading_more = ref(false)
92 -const has_loaded_once = ref(false)
93 21
94 -const unread_count = computed(() => ( 22 +const initPage = async () => {
95 - message_list.value.filter((item) => item.status === 'send').length 23 + await tabbarStore.ensureLoaded()
96 -))
97 24
98 -const fetchMessageList = async (nextPage = 0, append = false) => { 25 + const currentTab = tabbarStore.getTabItem('message')
99 - if (append) { 26 + preview_url.value = currentTab?.webview_url || ''
100 - loading_more.value = true
101 - } else {
102 - loading.value = true
103 - }
104 27
105 - try { 28 + Taro.setNavigationBarTitle({
106 - const response = await messageListAPI({ 29 + title: currentTab?.webview_title || currentTab?.title || default_page_title,
107 - page: nextPage,
108 - limit: page_size,
109 }) 30 })
110 31
111 - if (response?.code !== 1) { 32 + if (!preview_url.value) {
112 Taro.showToast({ 33 Taro.showToast({
113 - title: response?.msg || '获取资讯失败', 34 + title: '暂未配置资讯地址',
114 icon: 'none', 35 icon: 'none',
115 }) 36 })
116 - return
117 - }
118 -
119 - const list = response?.data?.list || []
120 - message_list.value = append ? [...message_list.value, ...list] : list
121 - total.value = Number(response?.data?.total || list.length)
122 - has_more.value = !!response?.data?.has_more
123 - page.value = nextPage
124 - } catch (error) {
125 - console.error('获取资讯列表失败:', error)
126 - } finally {
127 - loading.value = false
128 - loading_more.value = false
129 } 37 }
130 } 38 }
131 39
132 -const handleRefresh = async () => { 40 +useLoad(() => {
133 - await fetchMessageList(0, false) 41 + initPage()
134 -}
135 -
136 -const handleLoadMore = async () => {
137 - if (!has_more.value || loading_more.value) return
138 - await fetchMessageList(page.value + 1, true)
139 -}
140 -
141 -const goToDetail = (id) => {
142 - Taro.navigateTo({
143 - url: `/pages/message-detail/index?id=${encodeURIComponent(id)}`,
144 - })
145 -}
146 -
147 -useLoad(async () => {
148 - await fetchMessageList(0, false)
149 - has_loaded_once.value = true
150 -})
151 -
152 -useDidShow(async () => {
153 - if (!has_loaded_once.value) return
154 - await fetchMessageList(0, false)
155 }) 42 })
156 </script> 43 </script>
157 44
158 <style lang="less"> 45 <style lang="less">
159 -.message-page { 46 +.tab-webview-page {
160 min-height: 100vh; 47 min-height: 100vh;
161 - background: 48 + padding: 32rpx 24rpx;
162 - radial-gradient(circle at top left, rgba(166, 121, 57, 0.12), transparent 28%),
163 - linear-gradient(180deg, #fffaf4 0%, #f4f6fb 100%);
164 -
165 - .page-content {
166 - padding: 32rpx 24rpx 0;
167 box-sizing: border-box; 49 box-sizing: border-box;
168 - } 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 +}
169 54
170 - .hero-card, 55 +.empty-card {
171 - .toolbar-card,
172 - .message-card,
173 - .placeholder-card {
174 - padding: 32rpx;
175 - border-radius: 28rpx;
176 background: rgba(255, 255, 255, 0.94); 56 background: rgba(255, 255, 255, 0.94);
177 border: 2rpx solid rgba(166, 121, 57, 0.08); 57 border: 2rpx solid rgba(166, 121, 57, 0.08);
178 - box-shadow: 0 20rpx 60rpx rgba(15, 23, 42, 0.06); 58 + border-radius: 28rpx;
59 + padding: 32rpx;
179 box-sizing: border-box; 60 box-sizing: border-box;
180 - } 61 + box-shadow: 0 20rpx 60rpx rgba(15, 23, 42, 0.06);
181 - 62 +}
182 - .hero-head,
183 - .toolbar-card,
184 - .message-top,
185 - .message-meta {
186 - display: flex;
187 - align-items: center;
188 - }
189 -
190 - .hero-head,
191 - .toolbar-card,
192 - .message-top {
193 - justify-content: space-between;
194 - }
195 63
196 - .hero-title, 64 +.empty-title {
197 - .section-title {
198 display: block; 65 display: block;
199 - font-size: 40rpx; 66 + font-size: 36rpx;
200 font-weight: 700; 67 font-weight: 700;
201 color: #111827; 68 color: #111827;
202 - } 69 +}
203 70
204 - .hero-desc, 71 +.empty-desc {
205 - .section-desc {
206 display: block; 72 display: block;
207 margin-top: 16rpx; 73 margin-top: 16rpx;
208 font-size: 26rpx; 74 font-size: 26rpx;
209 - line-height: 1.8;
210 - color: #6b7280;
211 - }
212 -
213 - .placeholder-card {
214 - margin-top: 24rpx;
215 - }
216 -
217 - .toolbar-card,
218 - .message-card {
219 - margin-top: 24rpx;
220 - }
221 -
222 - .section-title {
223 - font-size: 30rpx;
224 - }
225 -
226 - .env-tag {
227 - padding: 10rpx 18rpx;
228 - border-radius: 999rpx;
229 - font-size: 22rpx;
230 - color: #1d4ed8;
231 - background: #dbeafe;
232 - }
233 -
234 - .env-tag.mock {
235 - color: #166534;
236 - background: #dcfce7;
237 - }
238 -
239 - .refresh-btn,
240 - .load-more-btn {
241 - border-radius: 999rpx;
242 - font-size: 26rpx;
243 - line-height: 80rpx;
244 - }
245 -
246 - .refresh-btn {
247 - min-width: 180rpx;
248 - color: #0f172a;
249 - background: #fff;
250 - border: 2rpx solid #d1d5db;
251 - }
252 -
253 - .message-card {
254 - display: flex;
255 - flex-direction: column;
256 - gap: 18rpx;
257 - }
258 -
259 - .message-meta {
260 - gap: 14rpx;
261 - }
262 -
263 - .message-category,
264 - .message-time,
265 - .message-status {
266 - font-size: 22rpx;
267 - }
268 -
269 - .message-category {
270 - padding: 8rpx 14rpx;
271 - border-radius: 999rpx;
272 - color: #92400e;
273 - background: #fef3c7;
274 - }
275 -
276 - .message-time {
277 - color: #9ca3af;
278 - }
279 -
280 - .message-status {
281 - color: #94a3b8;
282 - }
283 -
284 - .message-status.unread {
285 - color: #dc2626;
286 - }
287 -
288 - .message-title {
289 - font-size: 32rpx;
290 - font-weight: 700;
291 - color: #111827;
292 - }
293 -
294 - .message-summary {
295 - font-size: 25rpx;
296 line-height: 1.7; 75 line-height: 1.7;
297 color: #6b7280; 76 color: #6b7280;
298 - }
299 -
300 - .load-more-btn {
301 - margin-top: 24rpx;
302 - color: #0f172a;
303 - background: #ffffff;
304 - border: 2rpx solid #d1d5db;
305 - }
306 } 77 }
307 </style> 78 </style>
......
...@@ -12,7 +12,7 @@ const defaultTabbarItemMap = { ...@@ -12,7 +12,7 @@ const defaultTabbarItemMap = {
12 title: '资讯', 12 title: '资讯',
13 visible: true, 13 visible: true,
14 page_url: '/pages/message/index', 14 page_url: '/pages/message/index',
15 - webview_url: '', 15 + webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=news_list',
16 webview_title: '资讯', 16 webview_title: '资讯',
17 }, 17 },
18 application: { 18 application: {
......