Skip to content

列表页开发规范

版本说明

时间修改人备注
2025-04-23YG初始化文档
2025-10-20YG优化文档结构,增加通用模板和规范

一、概述

本文档定义了列表页的标准开发规范,包括页面加载流程、交互规范、代码实现模板等,适用于新闻列表、商品列表、用户列表等各种类型的列表页开发。

二、页面加载流程

标准的页面加载流程应遵循以下步骤:

  1. 打开loading动画,调用 nxhSDKLib.$init()包裹页面逻辑
  2. 获取页面模版配置(如适用)nxhSDK.getData("[page_config_api]", { template_type: [type] })
  3. 获取分类/筛选条件数据(如适用)nxhSDK.getData("[category_api]")
  4. 获取列表数据 nxhSDK.getData("[list_api]", { })
  5. 关闭loading动画

三、页面交互规范

  1. 分类点击:点击分类更新当前选中项,重置分页,重新加载列表
  2. 下拉刷新:清空列表,分页重置,重新请求接口
  3. 上拉加载:分页累加,拼接列表
  4. 列表需要分页,不能一次性加载所有数据
  5. 列表可滚动,需要用 <scroll-view scroll-y> 标签或 <n-list /> 包裹列表内容 相关文档
  6. 默认进入不能先出现空状态,要先展示loading动画,接口调用结束后:
    • 有数据则展示数据内容
    • 无数据时,使用<n-empty-data /> 组件占位。 相关文档
  7. 接口请求成功或失败后,都要关闭 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 新闻分类列表页

页面加载流程

  1. 打开loading动画,调用 nxhSDKLib.$init()包裹页面逻辑
  2. 获取页面模版配置 nxhSDK.getData("cms_page_info", { template_type: 2 })
  3. 获取新闻栏目列表 nxhSDK.getData("cms_channel_list")
  4. 获取新闻列表 nxhSDK.getData("cms_article_list", { })
  5. 关闭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>