Appearance
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
✅订单
✅广告招商
✅活动承接
✅场地租用
✅赛事活动
✅奥体动态
✅首页-消息列表
✅意见反馈-历史
分页 注意事项
思考
- 重新加载,数据清空
- list clear
- page = 1
- 加载下一页
- page + 1
- list push
- page++(需要成功才真的++)
- 无数据的时候
- 需要 更具 total 判断
- total 默认为 undefined