hookehuyr

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

- 移除资讯页原有的原生列表/详情逻辑,改为与“应用/我的”一致的 WebView 容器
- 在 tabbar 配置和 mock 数据中为资讯项添加 webview_url 和 webview_title
- 更新项目文档,说明资讯链路已统一为 WebView 承接,旧版原生资讯页转为参考链路
# 仓库指南
## 项目功能概览
当前项目不是一个只有单页示例的小程序壳,而是一套已经串起“首页内容分发 + 动态底部导航 + 资讯列表/详情 + WebView 容器页 + 授权 + 微信支付桥接”的业务骨架。
当前项目不是一个只有单页示例的小程序壳,而是一套已经串起“首页内容分发 + 动态底部导航 + 资讯/应用/我的 WebView 承接 + 授权 + 微信支付桥接”的业务骨架。
- 首页主链路在 `src/pages/index/`,通过 `src/api/index.js` 拉取 banner、宫格导航和图片入口;点击后要么跳内部页面,要么通过 `src/utils/webview.js` 组装参数进入通用 `WebView` 容器。
- 底部导航不是写死在页面里的,核心在 `src/components/AppTabbar.vue``src/stores/tabbar.js``src/api/tabbar.js``src/utils/tabbar.js`。应用启动时会预加载 tabbar 配置,`application``mine` 现在都是“按接口返回地址承接 WebView”的容器页
- 资讯演示链路在 `src/pages/message/``src/pages/message-detail/`,既是当前业务页,也是本地 mock 是否支持“列表 -> 详情 -> 已读状态变化”的验证入口
- 底部导航不是写死在页面里的,核心在 `src/components/AppTabbar.vue``src/stores/tabbar.js``src/api/tabbar.js``src/utils/tabbar.js`。应用启动时会预加载 tabbar 配置,`message``application``mine` 现在都按接口返回地址承接 WebView
- `src/pages/message/` 现在已经切到和导航栏“应用 / 我的”一致的 WebView 容器模式;`src/pages/message-detail/``src/api/message.js` 更适合作为旧版原生资讯列表/详情演示链路参考,不应再默认当作线上资讯主入口继续扩展
- 支付相关目前有三类页面:`src/pages/pay-test/` 用于手工调试授权和支付参数;`src/pages/pay-confirm/` 是用户确认金额后点击支付的正式按钮页;`src/pages/pay-bridge/` 是给 H5/WebView 调起小程序支付用的桥页,负责自动授权、拉起支付、展示结果并返回上一页。
- `src/pages/webview-preview/` 是通用外链承接页,`src/pages/application/``src/pages/mine/`、首页外链入口都会复用这类能力;`src/pages/map-guide/` 是固定地图签到 H5 页;`src/pages/auth/` 是统一授权页,不能绕过。
- `src/pages/mine-backup/` 目前更适合作为旧版“我的”页视觉与交互备份参考,不应默认当作线上主链路去扩展新业务。
......@@ -13,11 +13,11 @@
## 项目结构与模块组织
源码位于 `src/`,应用入口为 `src/app.js``src/app.config.js`。当前目录分工建议按下面理解,而不是只把它当成普通 Taro 模板:
- `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` 是备份页。
- `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` 更偏旧链路参考页。
- `src/components/`:通用组件目录。当前最关键的是 `AppTabbar.vue``PosterBuilder/`、二维码组件、时间选择器等属于可复用能力模块。
- `src/composables/`:组合式业务逻辑目录。`useWechatMiniPay.js` 是当前支付链路核心,和页面解耦较强;离线预约缓存相关逻辑也集中在这里。
- `src/hooks/`:较轻量的通用 hooks,目前主要是 `useGo.js` 这类导航辅助。
- `src/api/`:接口封装层。`fn.js` 是统一返回格式与 mock 接入的总入口,`index.js``message.js``tabbar.js``wx/pay.js` 分别承接首页、资讯、底部导航、微信支付等接口。
- `src/api/`:接口封装层。`fn.js` 是统一返回格式与 mock 接入的总入口,`index.js``tabbar.js``wx/pay.js` 分别承接首页、底部导航、微信支付等当前主链路接口;`message.js` 目前更偏旧版原生资讯列表/详情演示接口。
- `src/mock/`:本地 mock 体系。目录拆分规则是 `index.js` 统一入口、`modules/` 放 handler、`shared/` 放公共解析能力、`stores/` 放有状态 mock 数据、`fixtures/` 放静态样本。
- `src/utils/`:公共工具层。当前高频核心文件是 `authRedirect.js``request.js``config.js``webview.js``tabbar.js``wechatPay.js`;改动授权、环境、WebView 路由或支付时优先先看这里。
- `src/stores/`:Pinia 状态目录。当前重点是 `tabbar.js`(底部导航配置)和 `router.js`(授权回跳来源页),其他 store 多为基础能力或历史保留。
......
......@@ -17,7 +17,9 @@
## 资讯链路
- [ ] 资讯页面还需要真实接口获取列表数据,后续要和详情页、已读状态链路一起确认真实字段。
- [x] 资讯 tab 已调整为和“应用 / 我的”一致的 WebView 承接模式,当前地址由底部导航配置统一控制。
- [ ] 资讯 tab 还需要联调真实配置接口,继续确认标题、显隐和 WebView 地址是否正确。
- [ ] 旧版 `message-detail``src/api/message.js` 原生列表/详情演示链路后续再决定是继续保留为参考页,还是统一下线清理。
## 授权与支付
......
......@@ -9,6 +9,8 @@ export const getTabbarConfigFixture = () => ({
key: 'message',
title: '资讯',
visible: true,
webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=news_list',
webview_title: '资讯',
},
{
key: 'application',
......
<template>
<view class="message-page">
<view class="page-content">
<view class="hero-card">
<view class="hero-head">
<text class="hero-title">资讯</text>
<text class="env-tag" :class="{ mock: current_env_use_mock }">
{{ current_env_label }}
</text>
</view>
<text class="hero-desc">
当前环境由配置文件统一控制。本地如果要让所有接口都走 mock,只需要改 `config/dev.js` 里的 `API_RUNTIME_ENV`。
</text>
</view>
<view class="toolbar-card">
<view>
<text class="section-title">列表状态</text>
<text class="section-desc">
共 {{ total }} 条资讯,未读 {{ unread_count }} 条。进入详情页后,当前资讯会在 mock 中自动变成已读。
</text>
</view>
<button class="refresh-btn" :loading="loading" @tap="handleRefresh">
刷新
</button>
</view>
<view v-if="loading && !message_list.length" class="placeholder-card">
<text class="section-title">加载中</text>
<text class="section-desc">
正在拉取资讯列表...
</text>
</view>
<view v-else-if="!message_list.length" class="placeholder-card">
<text class="section-title">暂无资讯</text>
<text class="section-desc">
当前接口还没有返回资讯内容,可以先在 mock 里补结构,再回到这个页面验证展示效果。
</text>
</view>
<view
v-for="item in message_list"
:key="item.id"
class="message-card"
@tap="goToDetail(item.id)"
>
<view class="message-top">
<view class="message-meta">
<text class="message-category">{{ item.category }}</text>
<text class="message-time">{{ item.created_time }}</text>
</view>
<text class="message-status" :class="{ unread: item.status === 'send' }">
{{ item.status === 'send' ? '未读' : '已读' }}
</text>
</view>
<text class="message-title">{{ item.title }}</text>
<text class="message-summary">{{ item.summary }}</text>
</view>
<button
v-if="has_more && message_list.length"
class="load-more-btn"
:loading="loading_more"
@tap="handleLoadMore"
>
加载更多
</button>
<web-view v-if="preview_url" :src="preview_url" />
<view v-else class="tab-webview-page">
<view class="empty-card">
<text class="empty-title">暂未配置资讯地址</text>
<text class="empty-desc">
当前资讯页已经改成 WebView 承接模式,请先检查底部导航配置接口是否返回了可用地址。
</text>
</view>
<AppTabbar current="message" />
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import Taro, { useDidShow, useLoad } from '@tarojs/taro'
import AppTabbar from '@/components/AppTabbar.vue'
import { messageListAPI } from '@/api/message'
import { getCurrentApiConfig } from '@/utils/config'
const api_config = getCurrentApiConfig()
const current_env_label = api_config.label
const current_env_use_mock = api_config.useMock
const message_list = ref([])
const page = ref(0)
const page_size = 6
const total = ref(0)
const has_more = ref(true)
const loading = ref(false)
const loading_more = ref(false)
const has_loaded_once = ref(false)
import { ref } from 'vue'
import Taro, { useLoad } from '@tarojs/taro'
import { useTabbarStore } from '@/stores/tabbar'
const unread_count = computed(() => (
message_list.value.filter((item) => item.status === 'send').length
))
const tabbarStore = useTabbarStore()
const preview_url = ref('')
const default_page_title = '资讯'
const fetchMessageList = async (nextPage = 0, append = false) => {
if (append) {
loading_more.value = true
} else {
loading.value = true
}
const initPage = async () => {
await tabbarStore.ensureLoaded()
try {
const response = await messageListAPI({
page: nextPage,
limit: page_size,
})
const currentTab = tabbarStore.getTabItem('message')
preview_url.value = currentTab?.webview_url || ''
if (response?.code !== 1) {
Taro.showToast({
title: response?.msg || '获取资讯失败',
icon: 'none',
})
return
}
Taro.setNavigationBarTitle({
title: currentTab?.webview_title || currentTab?.title || default_page_title,
})
const list = response?.data?.list || []
message_list.value = append ? [...message_list.value, ...list] : list
total.value = Number(response?.data?.total || list.length)
has_more.value = !!response?.data?.has_more
page.value = nextPage
} catch (error) {
console.error('获取资讯列表失败:', error)
} finally {
loading.value = false
loading_more.value = false
if (!preview_url.value) {
Taro.showToast({
title: '暂未配置资讯地址',
icon: 'none',
})
}
}
const handleRefresh = async () => {
await fetchMessageList(0, false)
}
const handleLoadMore = async () => {
if (!has_more.value || loading_more.value) return
await fetchMessageList(page.value + 1, true)
}
const goToDetail = (id) => {
Taro.navigateTo({
url: `/pages/message-detail/index?id=${encodeURIComponent(id)}`,
})
}
useLoad(async () => {
await fetchMessageList(0, false)
has_loaded_once.value = true
})
useDidShow(async () => {
if (!has_loaded_once.value) return
await fetchMessageList(0, false)
useLoad(() => {
initPage()
})
</script>
<style lang="less">
.message-page {
.tab-webview-page {
min-height: 100vh;
padding: 32rpx 24rpx;
box-sizing: border-box;
background:
radial-gradient(circle at top left, rgba(166, 121, 57, 0.12), transparent 28%),
linear-gradient(180deg, #fffaf4 0%, #f4f6fb 100%);
.page-content {
padding: 32rpx 24rpx 0;
box-sizing: border-box;
}
.hero-card,
.toolbar-card,
.message-card,
.placeholder-card {
padding: 32rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.94);
border: 2rpx solid rgba(166, 121, 57, 0.08);
box-shadow: 0 20rpx 60rpx rgba(15, 23, 42, 0.06);
box-sizing: border-box;
}
.hero-head,
.toolbar-card,
.message-top,
.message-meta {
display: flex;
align-items: center;
}
.hero-head,
.toolbar-card,
.message-top {
justify-content: space-between;
}
.hero-title,
.section-title {
display: block;
font-size: 40rpx;
font-weight: 700;
color: #111827;
}
.hero-desc,
.section-desc {
display: block;
margin-top: 16rpx;
font-size: 26rpx;
line-height: 1.8;
color: #6b7280;
}
.placeholder-card {
margin-top: 24rpx;
}
.toolbar-card,
.message-card {
margin-top: 24rpx;
}
.section-title {
font-size: 30rpx;
}
.env-tag {
padding: 10rpx 18rpx;
border-radius: 999rpx;
font-size: 22rpx;
color: #1d4ed8;
background: #dbeafe;
}
.env-tag.mock {
color: #166534;
background: #dcfce7;
}
.refresh-btn,
.load-more-btn {
border-radius: 999rpx;
font-size: 26rpx;
line-height: 80rpx;
}
.refresh-btn {
min-width: 180rpx;
color: #0f172a;
background: #fff;
border: 2rpx solid #d1d5db;
}
.message-card {
display: flex;
flex-direction: column;
gap: 18rpx;
}
.message-meta {
gap: 14rpx;
}
.message-category,
.message-time,
.message-status {
font-size: 22rpx;
}
.message-category {
padding: 8rpx 14rpx;
border-radius: 999rpx;
color: #92400e;
background: #fef3c7;
}
.message-time {
color: #9ca3af;
}
.message-status {
color: #94a3b8;
}
.message-status.unread {
color: #dc2626;
}
radial-gradient(circle at top right, rgba(166, 121, 57, 0.16), transparent 30%),
linear-gradient(180deg, #fffaf3 0%, #f6f7fb 100%);
}
.message-title {
font-size: 32rpx;
font-weight: 700;
color: #111827;
}
.empty-card {
background: rgba(255, 255, 255, 0.94);
border: 2rpx solid rgba(166, 121, 57, 0.08);
border-radius: 28rpx;
padding: 32rpx;
box-sizing: border-box;
box-shadow: 0 20rpx 60rpx rgba(15, 23, 42, 0.06);
}
.message-summary {
font-size: 25rpx;
line-height: 1.7;
color: #6b7280;
}
.empty-title {
display: block;
font-size: 36rpx;
font-weight: 700;
color: #111827;
}
.load-more-btn {
margin-top: 24rpx;
color: #0f172a;
background: #ffffff;
border: 2rpx solid #d1d5db;
}
.empty-desc {
display: block;
margin-top: 16rpx;
font-size: 26rpx;
line-height: 1.7;
color: #6b7280;
}
</style>
......
......@@ -12,7 +12,7 @@ const defaultTabbarItemMap = {
title: '资讯',
visible: true,
page_url: '/pages/message/index',
webview_url: '',
webview_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=news_list',
webview_title: '资讯',
},
application: {
......