hookehuyr

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

- 新增首页内容 API 接口及 mock 数据
- 重构首页 UI,移除静态占位内容,改为动态渲染 banner、导航和图文链接
- 添加点击跳转逻辑,支持页面内跳转和 WebView 打开外部链接
import { fn, fetch } from './fn'
const Api = {
HOME_CONTENT: '/srv/?a=home&t=content',
PAY_TEST: '/srv/?a=pay',
}
export const getHomeContentAPI = () => fn(fetch.get(Api.HOME_CONTENT))
/**
* @description 获取微信支付参数(对齐 meihuaApp 的支付接口)
* @param {Object} params 请求参数
......
const banner_image = 'https://cdn.ipadbiz.cn/jls_weapp/images/banner01@2x.png'
const activity_image_01 = 'https://cdn.ipadbiz.cn/jls_weapp/images/banner02@2x.png'
const activity_image_02 = 'https://cdn.ipadbiz.cn/jls_weapp/images/banner03@2x.png'
export const getHomeContentFixture = () => ({
banner: {
id: 'home-banner-01',
image_url: banner_image,
title: '苏州觉林寺',
link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list',
link_title: '苏州觉林寺',
},
nav_list: [
{
id: 'map-guide',
title: '地图导览',
icon: 'icon-jingxiuying',
link_url: '',
page_url: '/pages/map-guide/index',
},
{
id: 'news',
title: '法讯-最新动态',
icon: 'icon-jingxiuying',
link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=news_list',
link_title: '法讯-最新动态',
},
{
id: 'intro',
title: '寺院介绍',
icon: 'icon-jingxiuying',
link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=temple_intro',
link_title: '寺院介绍',
},
{
id: 'practice',
title: '共修活动',
icon: 'icon-jingxiuying',
link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=practice',
link_title: '共修活动',
},
{
id: 'volunteer',
title: '义工报名',
icon: 'icon-jingxiuying',
link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=volunteer',
link_title: '义工报名',
},
{
id: 'contact',
title: '联系我们',
icon: 'icon-jingxiuying',
link_url: '',
},
],
image_links: [
{
id: 'activity-01',
title: '新春祈福法会',
tag: '活动报名',
image_url: activity_image_01,
link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=activity_spring',
link_title: '新春祈福法会',
},
{
id: 'activity-02',
title: '中秋月光茶会',
tag: '活动报名',
image_url: activity_image_02,
link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=activity_moon',
link_title: '中秋月光茶会',
},
{
id: 'activity-03',
title: '禅修静心体验',
tag: '预约参加',
image_url: activity_image_01,
link_url: 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=activity_meditation',
link_title: '禅修静心体验',
},
{
id: 'activity-04',
title: '法物流通',
tag: '查看详情',
image_url: activity_image_02,
link_url: '',
},
],
})
import { getHomeContentFixture } from '../fixtures/home.fixture'
import { buildMockSuccess } from '../shared/response'
export const homeMockHandlers = [
{
action: 'home',
type: 'content',
method: 'GET',
handle: () => buildMockSuccess(getHomeContentFixture(), '首页内容获取成功 (mock)'),
},
]
......@@ -2,10 +2,12 @@ import { authMockHandlers } from './auth.mock'
import { paymentMockHandlers } from './payment.mock'
import { commonMockHandlers } from './common.mock'
import { messageMockHandlers } from './message.mock'
import { homeMockHandlers } from './home.mock'
export const mockHandlers = [
...authMockHandlers,
...paymentMockHandlers,
...commonMockHandlers,
...messageMockHandlers,
...homeMockHandlers,
]
......
import { createMessageFixtureList } from '../fixtures/message.fixture'
import { MESSAGE_STATUS, createMessageFixtureList } from '../fixtures/message.fixture'
let messageList = null
......
<template>
<view class="index-page">
<view class="page-content">
<view class="hero-card">
<text class="hero-eyebrow">觉林寺小程序</text>
<text class="hero-title">首页</text>
<text class="hero-desc">
当前先完成首页、消息、我的三栏结构,测试能力统一收口到测试中心,避免首页继续堆放调试按钮。
</text>
<view
v-if="banner.image_url"
class="home-banner"
:class="{ clickable: hasLink(banner) }"
@tap="handleLinkedItemTap(banner)"
>
<image class="home-banner__image" :src="banner.image_url" mode="aspectFill" />
</view>
<view class="status-card">
<view>
<text class="section-label">当前授权状态</text>
<text class="status-text">应用启动时会优先尝试静默授权</text>
</view>
<text class="status-tag" :class="{ authed: isAuthed }">
{{ isAuthed ? '已授权' : '未授权' }}
</text>
</view>
<view class="overview-grid">
<view class="overview-card">
<view class="card-title-row">
<text class="card-title">首页</text>
<i class="fa iconfont icon-jingxiuying iconfont-demo"></i>
<view class="nav-panel">
<view
v-for="item in nav_list"
:key="item.id"
class="nav-entry"
:class="{ clickable: hasLink(item) }"
@tap="handleLinkedItemTap(item)"
>
<view class="nav-entry__icon-wrap">
<i
class="fa iconfont iconfont-demo"
:class="item.icon || default_icon"
></i>
</view>
<text class="card-desc">展示当前项目概览、地图导览与测试入口。</text>
</view>
<view class="overview-card">
<text class="card-title">消息</text>
<text class="card-desc">后续承接通知、订单提醒与系统消息。</text>
<text class="nav-entry__title">{{ item.title }}</text>
</view>
<view class="overview-card">
<text class="card-title">我的</text>
<text class="card-desc">后续承接个人信息、授权状态与常用功能。</text>
</view>
</view>
<view class="map-entry-card">
<text class="section-label">地图导览</text>
<text class="map-entry-desc">
当前通过小程序 WebView 承载地图项目,方便从首页直接进入步行导览页面。
</text>
<button class="primary-btn" @tap="goToMapGuide">打开地图导览</button>
</view>
<view class="webview-entry-card">
<text class="section-label">模拟 WebView</text>
<text class="webview-entry-desc">
这里提供一个首页直达的测试 WebView 入口,用于验证通用 WebView 承载能力。
</text>
<button class="outline-btn" @tap="goToMockWebview">打开模拟 WebView</button>
</view>
<view class="test-entry-card">
<text class="section-label">测试入口</text>
<text class="test-entry-desc">
支付测试等调试能力仍统一收口到测试中心,方便集中联调。
</text>
<button class="outline-btn" @tap="goToTestCenter">进入测试中心</button>
<view class="image-link-list">
<view
v-for="item in image_links"
:key="item.id"
class="image-link-card"
:class="{ clickable: hasLink(item) }"
@tap="handleLinkedItemTap(item)"
>
<image class="image-link-card__image" :src="item.image_url" mode="aspectFill" />
</view>
</view>
</view>
......@@ -68,197 +47,171 @@
<script setup>
import { ref } from 'vue'
import Taro, { useDidShow } from '@tarojs/taro'
import Taro, { useLoad } from '@tarojs/taro'
import AppTabbar from '@/components/AppTabbar.vue'
import { hasAuth } from '@/utils/authRedirect'
import { getHomeContentAPI } from '@/api'
import { buildWebviewPreviewUrl } from '@/utils/webview'
const mock_webview_url = 'https://oa-dev.onwall.cn/f/futian_home/?f=f&p=futian_list'
const mock_webview_title = '福田首页'
const default_icon = 'icon-jingxiuying'
const banner = ref({})
const nav_list = ref([])
const image_links = ref([])
const isAuthed = ref(false)
const hasLink = (item) => !!(item?.page_url || item?.link_url)
const refreshAuthStatus = () => {
isAuthed.value = hasAuth()
}
const handleLinkedItemTap = (item) => {
if (!item) return
const goToMapGuide = () => {
Taro.navigateTo({
url: '/pages/map-guide/index',
})
}
if (item.page_url) {
Taro.navigateTo({
url: item.page_url,
})
return
}
const goToMockWebview = () => {
Taro.navigateTo({
url: buildWebviewPreviewUrl(mock_webview_url, mock_webview_title),
})
if (item.link_url) {
Taro.navigateTo({
url: buildWebviewPreviewUrl(item.link_url, item.link_title || item.title || ''),
})
}
}
const goToTestCenter = () => {
Taro.navigateTo({
url: '/pages/pay-test/index',
})
const fetchHomeContent = async () => {
const response = await getHomeContentAPI()
if (response?.code !== 1) {
Taro.showToast({
title: response?.msg || '获取首页内容失败',
icon: 'none',
})
return
}
const data = response?.data || {}
banner.value = data.banner || {}
nav_list.value = data.nav_list || []
image_links.value = data.image_links || []
}
useDidShow(() => {
refreshAuthStatus()
useLoad(() => {
fetchHomeContent()
})
</script>
<style lang="less">
.index-page {
min-height: 100vh;
background:
radial-gradient(circle at top right, rgba(166, 121, 57, 0.18), transparent 32%),
linear-gradient(180deg, #fffaf3 0%, #f6f7fb 100%);
background: #f1f1f1;
.page-content {
padding: 32rpx 24rpx 0;
box-sizing: border-box;
}
.hero-card,
.status-card,
.overview-card,
.map-entry-card,
.webview-entry-card,
.test-entry-card {
background: rgba(255, 255, 255, 0.94);
border: 2rpx solid rgba(166, 121, 57, 0.08);
border-radius: 28rpx;
box-shadow: 0 20rpx 60rpx rgba(15, 23, 42, 0.06);
padding: 32rpx 32rpx 0;
box-sizing: border-box;
}
.hero-card {
display: flex;
flex-direction: column;
padding: 36rpx 32rpx;
}
.hero-eyebrow {
font-size: 24rpx;
font-weight: 600;
letter-spacing: 4rpx;
color: #a67939;
.home-banner {
margin: -32rpx -32rpx 0;
height: 420rpx;
overflow: hidden;
background: #e5e7eb;
}
.hero-title {
margin-top: 12rpx;
font-size: 52rpx;
font-weight: 700;
color: #1f2937;
.home-banner__image,
.image-link-card__image {
display: block;
width: 100%;
height: 100%;
}
.hero-desc {
margin-top: 18rpx;
font-size: 26rpx;
line-height: 1.8;
color: #6b7280;
.nav-panel {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin: 32rpx 0 0;
overflow: hidden;
border-radius: 16rpx;
background: #fff;
box-shadow: 0 16rpx 40rpx rgba(31, 41, 55, 0.06);
}
.status-card {
margin-top: 24rpx;
padding: 28rpx 32rpx;
.nav-entry {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
gap: 24rpx;
}
.section-label {
display: block;
font-size: 30rpx;
font-weight: 600;
color: #111827;
}
.status-text,
.card-desc,
.map-entry-desc,
.webview-entry-desc,
.test-entry-desc {
display: block;
margin-top: 12rpx;
font-size: 24rpx;
line-height: 1.7;
color: #6b7280;
}
.status-tag {
flex-shrink: 0;
padding: 12rpx 22rpx;
border-radius: 999rpx;
font-size: 24rpx;
font-weight: 600;
color: #b45309;
background: #fef3c7;
}
.status-tag.authed {
color: #166534;
background: #dcfce7;
}
.overview-grid {
margin-top: 24rpx;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20rpx;
justify-content: center;
min-width: 0;
min-height: 184rpx;
padding: 28rpx 8rpx 24rpx;
box-sizing: border-box;
color: #98271d;
}
.overview-card {
padding: 28rpx;
.nav-entry::after {
position: absolute;
top: 0;
right: 0;
width: 1rpx;
height: 100%;
background: #ececec;
content: '';
}
.overview-card:last-child {
grid-column: 1 / span 2;
.nav-entry:nth-child(3n)::after {
display: none;
}
.card-title {
display: block;
font-size: 30rpx;
font-weight: 600;
color: #111827;
.nav-entry:nth-child(n + 4)::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 1rpx;
background: #ececec;
content: '';
}
.card-title-row {
.nav-entry__icon-wrap {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
justify-content: center;
width: 76rpx;
height: 76rpx;
line-height: 1;
}
.iconfont-demo {
flex-shrink: 0;
font-size: 42rpx;
color: #a67939;
font-size: 64rpx;
color: #98271d;
}
.map-entry-card,
.webview-entry-card,
.test-entry-card {
margin-top: 24rpx;
padding: 32rpx;
.nav-entry__title {
display: block;
width: 100%;
margin-top: 18rpx;
font-size: 28rpx;
line-height: 1.25;
text-align: center;
color: #98271d;
word-break: break-all;
}
.primary-btn,
.outline-btn {
margin-top: 24rpx;
border-radius: 999rpx;
font-size: 30rpx;
line-height: 88rpx;
.image-link-list {
display: flex;
flex-direction: column;
gap: 32rpx;
margin-top: 40rpx;
padding: 0 4rpx 40rpx;
}
.primary-btn {
color: #fff;
background: linear-gradient(135deg, #a67939, #8f5e20);
.image-link-card {
position: relative;
height: 252rpx;
overflow: hidden;
border-radius: 12rpx;
background: #d1d5db;
}
.outline-btn {
color: #0f172a;
background: #fff;
border: 2rpx solid #d1d5db;
.clickable {
cursor: pointer;
}
}
</style>
......