Skip to content

list view

list view 是对 后端分页 请求对统一封装

最终效果:

vue
<template>
  <ATLayout title="广告招商">
    <ListView
      :loading="loading"
      :list="list"
      :total="total"
      :itemView="AdItem"
      @tapItem="toDetail"
      @tapNoData="reLoad"
      @loadNext="loadNext"
    />
  </ATLayout>
</template>

<script setup lang="ts">
import { onMounted } from 'vue'
import ATLayout from '@/components/base/ATLayout.vue'
import AdItem from '@/components/page/ad/AdItem.vue'
import ListView from '@/components/base/ListView'

import type { IAdItem } from '@/types/ad'
import { toIAdItem } from '@/api/type'
import { reqAdList } from '@/api/home'
import { useRouter } from 'vue-router'

import { useGenericRichTextDetailStore } from '@/stores/other/promotion.store'
import { useListView } from '@/hook/useListView'

const richTextDetailStore = useGenericRichTextDetailStore()
const router = useRouter()

const { loading, total, list, reLoad, loadNext } = useListView<IAdItem>({
  loadAPI: (pageNum, pageSize) =>
    reqAdList({ pageSize, pageNum }).then((res) => {
      if (!Array.isArray(res.rows)) throw new Error(res.msg)

      const list = res.rows.map(toIAdItem)

      const total = res.total

      return { list, total }
    })
})

onMounted(() => {
  reLoad()
})

function toDetail(item: IAdItem) {
  richTextDetailStore.promotionToDetail(item)
  router.push({
    name: 'ad-detail'
  })
}
</script>

<style scoped lang="less"></style>
vue
<template>
  <div class="at-activity-list">
    <BackTitleBar title="赛事动态" />
    <van-tabs
      :active="routeTag"
      @update:active="updateTag"
      color="#2975FF"
      background="transparent"
    >
      <van-tab title="报名中" name="active"> </van-tab>
      <van-tab title="全部" name="all"> </van-tab>
    </van-tabs>
    <div class="list">
      <ListView
        v-if="routeTag === 'active'"
        :list="active.list.value"
        :itemView="ActivityItem"
        :loading="active.loading.value"
        :total="active.total.value"
        @tapItem="toActivity"
        @tapNoData="active.reLoad"
        @loadNext="active.loadNext"
        gap="12px"
      />
      <ListView
        v-if="routeTag === 'all'"
        :list="all.list.value"
        :itemView="ActivityItem"
        :loading="all.loading.value"
        :total="all.total.value"
        @tapItem="toActivity"
        @tapNoData="all.reLoad"
        @loadNext="all.loadNext"
        gap="12px"
      />
    </div>
  </div>
</template>
<script setup lang="ts">
import BackTitleBar from '@/components/base/TitleBar/BackTitleBar.vue'
import ActivityItem from '@/components/page/activity/ActivityItem.vue'
import ListView from '@/components/base/ListView'
import type { IActivity } from '@/types/home'
import { computed, onMounted, reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { reqActivityList } from '@/api/other/activity'
import { toIActivity } from '@/api/type'
import { useActivityStore, useUserInputActivity } from '@/stores/other/activity'
import { useListView } from '@/hook/useListView'

const router = useRouter()
const route = useRoute()

const activityUserInput = useUserInputActivity()
const activityStore = useActivityStore()

type ISelectType = 'active' | 'all'

const routeTag = computed(() => route.query.tag as ISelectType)

const updateTag = (tag: ISelectType) => {
  switch (tag) {
    case 'active':
      // vant 会在没数据的时候自动触发 load
      active.reset()
      break
    case 'all':
      // vant 会在没数据的时候自动触发 load
      all.reset()
      break
  }

  router.replace({
    name: 'activity-list',
    query: {
      tag
    }
  })
}

const active = useListView<IActivity>({
  loadAPI: (page, size) =>
    reqActivityList({ eventStatus: 1, pageNum: page, pageSize: size }).then((res) => {
      if (res.code !== 200) throw new Error('获取数据失败')
      if (!Array.isArray(res.rows)) throw new Error('获取数据失败')

      return { list: res.rows.map(toIActivity), total: res.total }
    })
})

const all = useListView<IActivity>({
  loadAPI: (page, size) =>
    reqActivityList({ pageNum: page, pageSize: size }).then((res) => {
      if (res.code !== 200) throw new Error('获取数据失败')
      if (!Array.isArray(res.rows)) throw new Error('获取数据失败')

      return { list: res.rows.map(toIActivity), total: res.total }
    })
})

onMounted(() => {
  if (route.query.tag) {
    updateTag(route.query.tag as ISelectType)
  } else {
    updateTag('active')
  }
})

const toActivity = ({ id }: IActivity) => {
  activityUserInput.reset()
  activityStore.$reset()
  router.push({ name: 'activity-sign-up', params: { id } })
}
</script>

<style scoped lang="less">
.at-activity-list {
  height: 100vh;
  width: 100vw;

  display: flex;
  flex-direction: column;

  background-color: @c-bg;

  .list {
    flex: 1;
    overflow: scroll;
    padding: 12px;
  }
}
</style>
vue
<template>
  <div class="at-order">
    <BackTitleBar title="订单" />
    <van-tabs
      :active="routeTag"
      @update:active="updateTag"
      color="#2975FF"
      background="transparent"
    >
      <van-tab title="全部" name="all"> </van-tab>
      <van-tab title="待付款" name="unpaid"> </van-tab>
      <van-tab title="已付款" name="paid"> </van-tab>
      <van-tab title="已取消" name="cancel"> </van-tab>
      <van-tab v-if="false" title="已完成" name="completed"> </van-tab>
    </van-tabs>

    <div class="list">
      <ListView
        v-if="routeTag === 'all'"
        :itemView="OrderItemCard"
        :list="all.list.value"
        :loading="all.loading.value"
        :total="all.total.value"
        @load="all.loadNext"
      />
      <ListView
        v-if="routeTag === 'unpaid'"
        :itemView="OrderItemCard"
        :list="unpaid.list.value"
        :loading="unpaid.loading.value"
        :total="unpaid.total.value"
        @load="unpaid.loadNext"
      />
      <ListView
        v-if="routeTag === 'paid'"
        :itemView="OrderItemCard"
        :list="paid.list.value"
        :loading="paid.loading.value"
        :total="paid.total.value"
        @load="paid.loadNext"
      />
      <ListView
        v-if="routeTag === 'cancel'"
        :itemView="OrderItemCard"
        :list="cancel.list.value"
        :loading="cancel.loading.value"
        :total="cancel.total.value"
        @load="cancel.loadNext"
      />
    </div>
  </div>
</template>
<script setup lang="tsx">
import { computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'

import BackTitleBar from '@/components/base/TitleBar/BackTitleBar.vue'
import OrderItem from '@/components/page/order/item/index.vue'
import ListView from '@/components/base/ListView'

import type { IOrder } from '@/types/order'
import { IResOrderListRowsToIOrder, orderCancel, useRePay, type TagType } from './hooks'
import { showError, showOk } from '@/util/show'
import { useListView } from '@/hook/useListView'
import { reqOrderList } from '@/api/order'
import { onError2Toast } from '@/onError'

const { rePay } = useRePay()

const all = useListView<IOrder>({
  loadAPI: loadDataFromServer()
})
const unpaid = useListView<IOrder>({
  loadAPI: loadDataFromServer(1)
})
const paid = useListView<IOrder>({
  loadAPI: loadDataFromServer(2)
})
const cancel = useListView<IOrder>({
  loadAPI: loadDataFromServer(3)
})

// const completed = useListView<IOrder>({
//   loadAPI: loadDataFromServer(2)
// })

const OrderItemCard = (props: IOrder) => (
  <OrderItem
    {...props}
    onBase={() => handleBaseInfo(props)}
    onCancel={() => handleCancel(props)}
    onPay={() => handlePay(props)}
  />
)

const router = useRouter()
const route = useRoute()

const routeTag = computed(() => route.query.tag as TagType)

const getListController = (tag: TagType) => {
  switch (tag) {
    case 'all':
      return all
    case 'unpaid':
      return unpaid
    case 'paid':
      return paid
    case 'cancel':
      return cancel
    // case 'completed':
  }
}

const updateTag = (tag: TagType) => {
  console.log('updateTag', routeTag.value, '>', tag)

  const listController = getListController(tag)
  // 这里只需要reset,因为 vant 会在没数据的时候自动触发 load
  // 而 load 可以正确的加载数据
  if (listController) listController.reset()

  router.replace({
    name: 'order',
    query: {
      tag
    }
  })
}

const handleBaseInfo = (item: IOrder) => {
  router.push({
    name: 'order-info',
    params: {
      id: item.id
    }
  })
}

const handleCancel = (item: IOrder) => {
  console.log('handleCancel > ', item)
  orderCancel(item).then((res) => {
    if (res.code === 200) {
      // showOk('取消成功')

      const listController = getListController(routeTag.value)

      const orderData = listController?.list.value.find((i) => i.id === item.id)

      if (orderData) {
        orderData.type = 'cancel'
      }
    }
  })
}

const handlePay = (item: IOrder) => {
  return rePay(item)
    .then(({ message, isCanPaid, isPaySuccess }) => {
      if (!isCanPaid || !isPaySuccess) throw new Error(message)

      // orderStore.refresh()
    })
    .catch(onError2Toast('支付失败'))
}

function loadDataFromServer(status?: 1 | 2 | 3) {
  return function (pageNum: number, pageSize: number) {
    return reqOrderList({
      pageNum,
      pageSize,
      status
    }).then((res) => {
      if (res.code !== 200) throw new Error('获取数据失败')
      if (!Array.isArray(res.rows)) throw new Error('获取数据失败')

      return {
        list: res.rows.map(IResOrderListRowsToIOrder),
        total: res.total
      }
    })
  }
}
</script>
<style scoped lang="less">
.at-order {
  height: 100vh;
  width: 100vw;
  display: flex;
  flex-direction: column;
  background-color: @c-bg;

  position: relative;

  .list {
    flex: 1;
    overflow: scroll;
    padding: 12px;
  }
}
</style>

就算最复杂的 订单,在封装后,也才 200 行代码。

封装 与 可维护性都很高。

ListView.tsx

注意,是 tsx!

tsx
import { computed, type SetupContext } from 'vue'

import { List as VantList } from 'vant'

import IconNoData from '@/assets/icon/no-data.svg'


interface ListViewProps {
  list: Array<any>
  itemView: any
  loading: boolean
  total?: number
  gap?: string
}

interface ListViewEvents {
  tapItem: (item: any) => void
  loadNext: () => void
  tapNoData: () => void
}

export default function (props: ListViewProps, { emit }: SetupContext<ListViewEvents>) {
  const isFinished = computed(() => {
    if (typeof props.total === 'undefined') return false

    // state.page * state.size >= state.total,
    return props.list.length >= props.total
  })

  const isNoData = computed(() => isFinished.value && props.total === 0)

  const finishedText = computed(() => (isNoData.value ? '' : '没有更多了'))

  const ItemView = props.itemView

  const listView = props.list.map((item) => (
    <ItemView {...item} onClick={() => handleItemClick(item)} />
  ))

  const noData = (
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        color: '#666',
        gap: '12px',
        paddingTop: '100px'
      }}
      onClick={() => emit('tapNoData')}
    >
      <img src={IconNoData} />
      <div>无数据</div>
    </div>
  )

  const handleItemClick = (item: any) => {
    // console.log('tapItem', item)
    emit('tapItem', item)
  }

  return (
    <VantList
      style={{ display: 'flex', flexDirection: 'column', gap: props.gap || '20px' }}
      loading={props.loading}
      finished={isFinished.value}
      finishedText={finishedText.value}
      onLoad={() => emit('loadNext')}
    >
      {isNoData.value ? noData : listView}
    </VantList>
  )
}
useListView.ts
ts
import { showInfo } from '@/util/show'
import { ref } from 'vue'

interface IListViewOptions<X> {
  loadAPI: (page: number, size: number) => Promise<{ list: X[]; total: number }>
  pageSize?: number
}

export const useListView = <T = any>({ loadAPI, pageSize }: IListViewOptions<T>) => {
  const page = ref(0)

  const size = ref(pageSize || 10)
  const total = ref<number | undefined>(undefined)

  const loading = ref(false)
  const list = ref<T[]>([])

  const reLoad = async () => {
    page.value = 0
    list.value = []

    await loadNext()
  }

  const loadNext = async () => {
    if (loading.value) return

    const isNoMore =
      typeof total.value !== 'undefined' && total.value > 0 && list.value.length >= total.value
    if (isNoMore) {
      return console.log('没有更多了')
    }

    loading.value = true

    try {
      const res = await loadAPI(page.value + 1, size.value)

      // @ts-ignore
      list.value.push(...res.list)
      total.value = res.total
      page.value++
    } catch (error: any) {
      showInfo(error.message)
    } finally {
      loading.value = false
    }
  }

  return { reLoad, loadNext, list, loading, total, page: { index: page, size } }
}

奥体业务 中 需要整改的 list

✅订单

✅广告招商

✅活动承接

✅场地租用

✅赛事活动

✅奥体动态

✅首页-消息列表

✅意见反馈-历史

分页 注意事项

思考

  1. 重新加载,数据清空
  • list clear
  • page = 1
  1. 加载下一页
  • page + 1
  • list push
  • page++(需要成功才真的++)
  1. 无数据的时候
  • 需要 更具 total 判断
  • total 默认为 undefined