Skip to content

列表页加载逻辑

说明: 以新闻分类页为例。

版本说明

时间修改人备注
2025-04-23YG初始化文档

一、页面加载流程

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

  1. 打开loading动画,调用 nxhSDKLib.$init()包裹页面逻辑
  2. 获取主题色配置 store.dispatch("getAppConfig", query)
  3. 获取页面模版配置 nxhSDK.getData("cms_page_info", { template_type: 2 })
  4. 获取新闻栏目列表 nxhSDK.getData("cms_channel_list")
  5. 获取新闻列表 nxhSDK.getData("cms_article_list", { })
  6. 关闭loading动画

二、页面交互规范

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