Appearance
列表页加载逻辑
说明: 以新闻分类页为例。
版本说明
时间 | 修改人 | 备注 |
---|---|---|
2025-04-23 | YG | 初始化文档 |
一、页面加载流程
标准的页面加载流程应遵循以下步骤:
- 打开loading动画,调用
nxhSDKLib.$init()
包裹页面逻辑 - 获取主题色配置
store.dispatch("getAppConfig", query)
- 获取页面模版配置
nxhSDK.getData("cms_page_info", { template_type: 2 })
- 获取新闻栏目列表
nxhSDK.getData("cms_channel_list")
- 获取新闻列表
nxhSDK.getData("cms_article_list", { })
- 关闭loading动画
二、页面交互规范
- 分类点击:点击分类更新
curCategoryId
,重置分页,重新加载列表 - 下拉刷新:清空列表,分页重置,重新请求接口
- 上拉加载:分页累加,拼接列表
- 列表需要
分页
,不能一次性加载所有数据 - 列表可滚动,需要用
<scroll-view scroll-y>
标签或<n-list />
包裹列表内容 相关文档 。 - 默认进入不能先出现空状态,要先展示loading动画,接口调用结束后,有数据则展示数据内容,无数据时,使用
<n-empty-data />
组件占位。 相关文档 - 接口请求成功或失败后,都要关闭
loading
动画
三、代码实现与解析
1. 打开loading动画,调用 nxhSDKLib.$init()
包裹页面逻辑
vue
<script setup>
onLoad((query) => {
uni.showLoading({
title: '加载中...',
mask: true
})
nxhSDKLib.$init(() => {
// 写你的页面接口调用
})
})
</script>
2. 获取主题色配置 store.dispatch("getAppConfig", query)
调用了该代码后,会自动获取主题色,并存储在 store 中,方便其他页面调用。
vue
<script setup>
const styleJson = computed(() => store.state?.appConfig?.style)
onLoad((query) => {
nxhSDKLib.$init(() => {
store.dispatch("getAppConfig", query)
})
})
</script>
3. 获取页面模版配置 nxhSDK.getData("cms_page_info", { template_type: 2 })
- 调用SDK接口获取页面配置信息
- 解析返回的配置数据,更新列表配置和列表类型
vue
<script setup>
const getPageInfo = () => {
nxhSDK.getData("cms_page_info", { template_type: 2 }).then((res) => {
console.log('getPageInfo', res)
const { page_config } = res?.result?.info
if (page_config) {
listConfig.value = JSON.parse(page_config || '{}')
listType.value = listConfig.value.listType
}
})
}
</script>
4. 获取新闻栏目列表 nxhSDK.getData("cms_channel_list")
- 获取新闻栏目列表数据
- 根据URL参数确定当前选中的栏目
- 设置导航栏标题为当前栏目名称
- 获取当前栏目的文章列表
vue
<script setup>
const getChannelList = async () => {
try {
const res = await nxhSDK.getData("cms_channel_list")
console.log('fetchChannelList', res)
const list = res.result.data
channelList.value = list
// 处理URL参数中的栏目ID
if (queryData.id) {
curChannel.value = list.find(i => i.id == queryData.id)
uni.setNavigationBarTitle({
title: curChannel.value?.name
})
curChannelId.value = curChannel.value?.id
curTabIdx.value = list.findIndex(i => i.id == queryData.id)
} else {
curChannelId.value = list[0]?.id
}
// 获取当前栏目的文章列表
if (curChannelId.value) {
getList(1)
}
} catch (error) {
console.log(error)
}
}
</script>
5. 获取新闻列表 nxhSDK.getData("cms_article_list", { })
- 根据参数type区分是刷新列表还是加载更多
- 显示加载动画,调用接口获取文章列表
- 处理特殊类型的文章数据(如视频类型)
- 更新列表数据,并重置相关状态
vue
<script setup>
// 获取列表 type: 1-刷新 0-加载更多
const getList = async (type = 0) => {
try {
loading.value = true
uni.showLoading({
title: '加载中...',
mask: true
})
// 调用接口获取文章列表
const res = await nxhSDK.getData("cms_article_list", {
channel_id: curChannelId.value,
start: page.value * length.value,
length: length.value
})
console.log('fetchArticleList', res)
total.value = res.result.total
const data = res?.result?.data
// 处理返回的数据
if (data?.length) {
const arr = data.map(item => {
if (item.real_template_type === 5) {
// 处理视频类型的文章
try {
item.videoUrl = JSON.parse(item.attach)[0] && JSON.parse(item.attach)[0].url
} catch (error) {
console.log(error)
}
}
return item
}) || []
// 根据类型决定是刷新还是加载更多
dataList.value = type === 1 ? arr : [...dataList.value, ...arr]
} else {
dataList.value = defaultList
}
console.warn('dataList', dataList.value);
} catch (error) {
console.log(error)
} finally {
// 重置状态
isRefreshing.value = false;
isLoadingMore.value = false;
loading.value = false
uni.hideLoading();
}
}
</script>
四、完整代码示例
vue
<script setup>
import { ref, computed, reactive, } from 'vue'
import Tabs from '@/components/tabs'
import {nxhSDK, nxhSDKLib} from "@/nxhsdk.module.min";
import {onLoad, onShareAppMessage} from "@dcloudio/uni-app";
const cdnEnvUrl = uni.cdnEnvUrl;
const listType = ref(2);
const listConfig = ref({
color: "#2A3547",
date: 1,
divider: 1,
fontSize: 1,
fontWeight: 1,
imgLayout: 1,
imgStyle: 1,
listType: 2,
more: 1,
radius: 1,
row: 1,
tag: 1,
})
const dataList = ref([]);
const store = nxhSDK.useSDKStore();
const styleJson = computed(() => store.state?.appConfig?.style)
const channelList = ref([]);
const queryData = reactive({});
const curTabIdx = ref(0); // 当前选中的tab索引
const page = ref(0);
const length = ref(10);
const total = ref(0);
const loading = ref(true);
const curChannelId = ref('');
const curChannel = ref({});
// 添加下拉刷新和上拉加载状态
const isRefreshing = ref(false);
const isLoadingMore = ref(false);
const fontSizeConf = {
1: '16px',
2: '14px',
3: '12px',
}
const fontWeightConf = {
1: 'bold',
2: 'normal',
}
const defaultList = []
const hasMore = computed(() => total.value > dataList.value.length)
const getPageInfo = () => {
nxhSDK.getData("cms_page_info", { template_type: 2 }).then((res) => {
console.log('getPageInfo', res)
const { page_config } = res?.result?.info
if (page_config) {
listConfig.value = JSON.parse(page_config || '{}')
listType.value = listConfig.value.listType
}
})
}
const getChannelList = async () => {
try {
const res = await nxhSDK.getData("cms_channel_list")
console.log('fetchChannelList', res)
const list = res.result.data
channelList.value = list
if (queryData.id) {
curChannel.value = list.find(i => i.id == queryData.id)
uni.setNavigationBarTitle({
title: curChannel.value?.name
})
curChannelId.value = curChannel.value?.id
curTabIdx.value = list.findIndex(i => i.id == queryData.id)
} else {
curChannelId.value = list[0]?.id
}
if (curChannelId.value) {
getList(1)
}
} catch (error) {
console.log(error)
}
}
// 获取列表 type: 1-刷新 0-加载更多
const getList = async (type = 0) => {
try {
loading.value = true
uni.showLoading({
title: '加载中...',
mask: true
})
const res = await nxhSDK.getData("cms_article_list", {
channel_id: curChannelId.value,
start: page.value * length.value,
length: length.value
})
console.log('fetchArticleList', res)
total.value = res.result.total
const data = res?.result?.data
if (data?.length) {
const arr = data.map(item => {
if (item.real_template_type === 5) {
// 视频
try {
item.videoUrl = JSON.parse(item.attach)[0] && JSON.parse(item.attach)[0].url
} catch (error) {
console.log(error)
}
}
return item
}) || []
dataList.value = type === 1 ? arr : [...dataList.value, ...arr]
} else {
dataList.value = defaultList
}
console.warn('dataList', dataList.value);
} catch (error) {
console.log(error)
} finally {
isRefreshing.value = false;
isLoadingMore.value = false;
loading.value = false
uni.hideLoading();
}
}
const handleChangeTab = async (item) => {
page.value = 0;
curChannel.value = item;
curChannelId.value = item.id;
uni.setNavigationBarTitle({
title: curChannel.value?.name
})
getList(1)
}
const onLoadMore = () => {
console.log("load", page.value);
if (!hasMore.value || isLoadingMore.value) {
return;
}
page.value += 1;
getList();
};
const onRefresh = () => {
console.log("refresh");
isRefreshing.value = true;
page.value = 0;
getList(1);
};
const handleGoListDetail = (item) => {
console.log(item)
uni.navigateTo({
url: `/news/detail/index?did=${item.id}&appid=${queryData.appid}`,
})
}
const splitTags = (tags) => {
return tags ? tags?.replace(/,/g, ',').split(',') : []
}
onShareAppMessage(() => {
return {
title: `${curChannel.value?.name}`,
path: `/news/cate/index?id=${curChannel.value?.id}&appid=${queryData.appid}`,
}
})
onLoad((query) => {
uni.showLoading({
title: '加载中...',
mask: true
})
Object.assign(queryData, query)
nxhSDKLib.$init(async () => {
await store.dispatch("getAppConfig", query);
getPageInfo()
getChannelList()
})
})
</script>
<template>
<view v-if="channelList.length" :style="{
'--style-color-primary': styleJson?.color1,
'--style-color-secondary': styleJson?.color2,
'--style-radius': styleJson?.wind ? '18px' : (styleJson?.angle_style == 1 ? '0' : styleJson?.angle_style == 2 ? '18px' : '8px'),
'--style-tag': styleJson?.wind ? 'style-tag-bg' : (styleJson?.tag_style == 1 ? 'style-tag-border' : 'style-tag-bg'),
'--style-space': styleJson?.wind ? '0' : styleJson?.card_spacing + 'px',
}">
<Tabs :list="channelList" :activeTab="curTabIdx" @changeTab="handleChangeTab" labelKey="name"></Tabs>
</view>
<scroll-view class="list-wrap"
scroll-y
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore">
<view class="list-container" v-if="dataList.length">
<view class="type1-list" v-if="listType == 1">
<view class="type3">
<view :class="['type3-item', 'divider']" v-for="i in dataList" :key="i.id" @click="handleGoListDetail(i)">
<view :class="['title', 'over2']" :style="{ fontSize: fontSizeConf[listConfig.fontSize], fontWeight: fontWeightConf[listConfig.fontSize], color: '#2A3547' }">
<image class="title-icon" v-if="listConfig.icon" :src="i.icon || cdnEnvUrl + 'images/news/pdf.png'"/>
<span class="title-item">{{ i.title }}</span>
<image class="title-corner" v-if="listConfig.corner && i.real_template_type === 4" :src="cdnEnvUrl + 'images/news/download.png'" @click.stop="handleGoListDetail(i)" />
<image class="title-corner" v-if="listConfig.corner && i.real_template_type !== 4" :src="cdnEnvUrl + 'images/news/right.svg'" alt=""/>
</view>
<view class="bottom flc" v-if="listConfig.like || listConfig.view || listConfig.tag || listConfig.date">
<view class="left flc">
<view class="flc" v-if="listConfig.tag && i.tag_name">
<view class="tag" v-for="tag in splitTags(i.tag_name)">{{tag}}</view>
</view>
<view class="date" v-if="listConfig.date && !(!listConfig.like && !listConfig.view && !listConfig.tag)">{{ (i.updated_at && i.updated_at.split(' ')[0]) || '2024-03-20' }}</view>
</view>
<view class="right flc">
<view class="date" v-if="listConfig.date && (!listConfig.like && !listConfig.view && !listConfig.tag)">{{ (i.updated_at && i.updated_at.split(' ')[0]) || '2024-03-20' }}</view>
<view class="item flc" v-if="listConfig.like">
<image class="icon-img" :src="cdnEnvUrl + 'images/news/zan.png'" alt=""/>
<view>0</view>
</view>
<view class="item flc" v-if="listConfig.view">
<image class="icon-img" :src="cdnEnvUrl + 'images/news/view.png'" alt=""/>
<view>{{ i.view_num }}</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<n-empty-data v-if="!dataList.length && !loading" />
</scroll-view>
</template>
<style lang="scss" scoped>
.list-wrap {
height: calc(100vh - 40px);
}
</style>