Appearance
列表页开发规范
版本说明
| 时间 | 修改人 | 备注 |
|---|---|---|
| 2025-04-23 | YG | 初始化文档 |
| 2025-10-20 | YG | 优化文档结构,增加通用模板和规范 |
一、概述
本文档定义了列表页的标准开发规范,包括页面加载流程、交互规范、代码实现模板等,适用于新闻列表、商品列表、用户列表等各种类型的列表页开发。
二、页面加载流程
标准的页面加载流程应遵循以下步骤:
- 打开loading动画,调用
nxhSDKLib.$init()包裹页面逻辑 - 获取页面模版配置(如适用)
nxhSDK.getData("[page_config_api]", { template_type: [type] }) - 获取分类/筛选条件数据(如适用)
nxhSDK.getData("[category_api]") - 获取列表数据
nxhSDK.getData("[list_api]", { }) - 关闭loading动画
三、页面交互规范
- 分类点击:点击分类更新当前选中项,重置分页,重新加载列表
- 下拉刷新:清空列表,分页重置,重新请求接口
- 上拉加载:分页累加,拼接列表
- 列表需要
分页,不能一次性加载所有数据 - 列表可滚动,需要用
<scroll-view scroll-y>标签或<n-list />包裹列表内容 相关文档 。 - 默认进入不能先出现空状态,要先展示loading动画,接口调用结束后:
- 有数据则展示数据内容
- 无数据时,使用
<n-empty-data />组件占位。 相关文档
- 接口请求成功或失败后,都要关闭
loading动画
四、通用代码模板
4.1 基础页面结构
vue
<script setup>
import { ref, computed, reactive } from 'vue'
import { nxhSDK, nxhSDKLib } from "@/nxhsdk.module.min";
import { onLoad, onShareAppMessage } from "@dcloudio/uni-app";
// 页面基础数据
const dataList = ref([]); // 列表数据
const store = nxhSDK.useSDKStore();
const styleJson = computed(() => store.state?.appConfig?.style)
// 分页相关
const page = ref(0);
const length = ref(10);
const total = ref(0);
const loading = ref(true);
// 刷新和加载更多状态
const isRefreshing = ref(false);
const isLoadingMore = ref(false);
// 计算属性
const hasMore = computed(() => total.value > dataList.value.length)
// URL参数
const queryData = reactive({});
// 页面初始化
onLoad((query) => {
uni.showLoading({
title: '加载中...',
mask: true
})
Object.assign(queryData, query)
nxhSDKLib.$init(async () => {
// 如有页面配置,获取页面配置
// getPageInfo()
// 获取分类数据
// getCategoryList()
// 获取列表数据
getList(1)
})
})
</script>
<template>
<view>
<!-- 分类导航(如适用) -->
<!-- <Tabs :list="categoryList" :activeTab="curTabIdx" @changeTab="handleChangeTab" /> -->
<!-- 列表容器 -->
<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="list-item" v-for="item in dataList" :key="item.id">
<!-- 列表项内容 -->
</view>
</view>
<!-- 空状态 -->
<n-empty-data v-if="!dataList.length && !loading" />
</scroll-view>
</view>
</template>
<style lang="scss" scoped>
.list-wrap {
height: calc(100vh - 40px);
}
</style>4.2 获取列表数据方法
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("[list_api]", {
// 请求参数
start: page.value * length.value,
length: length.value
// 其他参数如分类ID等
})
total.value = res.result.total
const data = res?.result?.data
if (data?.length) {
// 数据处理逻辑
const processedData = data.map(item => {
// 特殊数据处理
return item
}) || []
// 根据类型决定是刷新还是加载更多
dataList.value = type === 1 ? processedData : [...dataList.value, ...processedData]
} else {
dataList.value = []
}
} catch (error) {
console.log(error)
// 错误处理
} finally {
// 重置状态
isRefreshing.value = false;
isLoadingMore.value = false;
loading.value = false
uni.hideLoading();
}
}
</script>4.3 刷新和加载更多方法
vue
<script setup>
// 下拉刷新
const onRefresh = () => {
isRefreshing.value = true;
page.value = 0;
getList(1);
};
// 上拉加载更多
const onLoadMore = () => {
if (!hasMore.value || isLoadingMore.value) {
return;
}
page.value += 1;
getList();
};
</script>4.4 分类切换方法(如适用)
vue
<script setup>
// 分类切换
const handleChangeTab = async (item) => {
page.value = 0;
// 更新当前分类
// curCategory.value = item;
// curCategoryId.value = item.id;
// 如需要更新导航栏标题
// uni.setNavigationBarTitle({
// title: curCategory.value?.name
// })
getList(1)
}
</script>五、特定场景实现示例
5.1 新闻分类列表页
页面加载流程
- 打开loading动画,调用
nxhSDKLib.$init()包裹页面逻辑 - 获取页面模版配置
nxhSDK.getData("cms_page_info", { template_type: 2 }) - 获取新闻栏目列表
nxhSDK.getData("cms_channel_list") - 获取新闻列表
nxhSDK.getData("cms_article_list", { }) - 关闭loading动画
核心代码实现
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 hasMore = computed(() => total.value > dataList.value.length)
// 获取页面配置
const getPageInfo = () => {
nxhSDK.getData("cms_page_info", { template_type: 2 }).then((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")
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)
}
}
// 获取列表数据
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
})
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 = []
}
} 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 onRefresh = () => {
isRefreshing.value = true;
page.value = 0;
getList(1);
};
// 上拉加载更多
const onLoadMore = () => {
if (!hasMore.value || isLoadingMore.value) {
return;
}
page.value += 1;
getList();
};
// 跳转到详情页
const handleGoListDetail = (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>