jobsys-explore
Version:
Enhanced component based on vant
460 lines (394 loc) • 11 kB
JSX
import { defineComponent, inject, reactive, nextTick, ref, onMounted, onUnmounted } from "vue"
import { useFetch, useCache, useSm3 } from "../../hooks"
import { closeToast, Empty, List, Pagination, PullRefresh, showLoadingToast, Sticky } from "vant"
import ExSearch from "../search/ExSearch.jsx"
import "./index.less"
import { EX_PAGINATION } from "../provider/ExProvider.jsx"
import { isBoolean, isFunction, isObject, isUndefined, pick } from "lodash-es"
/**
* ExPagination 翻页
*
*/
export default defineComponent({
name: "ExPagination",
props: {
/**
* 下拉刷新的配置
* true:表示启用下拉刷新
* Object:用于PullRefresh组件的配置
* [PullRefresh配置](https://vant-contrib.gitee.io/vant/#/zh-CN/pull-refresh)
*/
pullRefresh: { type: [Boolean, Object], default: true },
/**
* ExSearch 搜索配置
*/
search: { type: Object, default: null },
/**
* 是否Fix搜索栏
* true:表示自动吸顶
* Object:用于Sticky组件的配置,常用属性`offsetTop`
* [Sticky配置](https://vant-contrib.gitee.io/vant/#/zh-CN/sticky)
*/
searchFixed: { type: [Boolean, Object], default: true },
/**
* 请求数据URL
*
*/
url: { type: String, default: "" },
/**
* 请求方式,默认为 GET
*
* @values get, post
*/
method: { type: String, default: "get" },
/**
* 请求时的附带参数
*/
extraData: { type: Object, default: () => ({}) },
/**
* 是否自动加载
*/
autoLoad: { type: Boolean, default: true },
/**
* 每页条数
*/
pageSize: { type: Number, default: 10 },
/**
* 加载后数据的回调函数
*/
afterFetched: { type: Function, default: null },
/**
* 数据为空文案
*/
emptyText: { type: String, default: "暂无内容" },
/**
* 加载完毕文案
*/
finishText: { type: String, default: "加载完毕" },
/**
* 加载错误文案
*/
errorText: { type: String, default: "加载失败,点击重新加载" },
/**
* 滚动条与底部距离小于 offset 时触发 load 事件
*/
offset: { type: [Number, String], default: 300 },
/**
* 持久化,传入 localStorage 的 key,如果为 true, 将会以 URL Hash 为 key
*/
persistence: { type: [Boolean, String], default: false },
/**
* 是否单页模式
*/
singlePage: { type: Boolean, default: false },
/**
* [原生配置](https://vant-contrib.gitee.io/vant/#/zh-CN/list)
*/
defaultProps: { type: Object, default: () => ({}) },
},
setup(props, { expose, slots }) {
const paginationProvider = inject(EX_PAGINATION, () => ({}))
const searchRef = ref(null)
const { currentPage: currentPageKey, pageSize: pageSizeKey } = paginationProvider.requestKeys
const genPersistenceKey = (prefix) => {
if (!props.persistence) {
return null
}
prefix = prefix || ""
if (isBoolean(props.persistence)) {
return `exPagination_${prefix}` + useSm3(location.href)
}
return `exPagination_${prefix}` + useSm3(location.pathname + "_" + props.persistence)
}
let persistencePagination = props.persistence ? useCache(genPersistenceKey()).get({}) : {}
const state = reactive({
items: props.singlePage ? [] : persistencePagination.items || [],
loading: false,
error: persistencePagination.error || false,
errorMessage: "",
totalSize: persistencePagination.totalSize || 0,
currentPage: persistencePagination.currentPage || 1,
finished: persistencePagination.finished || false,
empty: persistencePagination.empty || false,
searchParams: {},
offsetTop: 0,
refreshLoading: false,
isFirstLoad: true,
})
/**
* 持久化翻页与滚动
*/
const onPersistence = () => {
if (!props.persistence) {
return
}
const data = {
empty: state.empty,
finished: state.finished,
currentPage: state.currentPage,
totalSize: state.totalSize,
error: state.error,
}
if (!props.singlePage) {
data.items = state.items
}
useCache(genPersistenceKey()).set(data)
}
const onScroll = () => {
if (props.persistence && !state.loading) {
useCache(genPersistenceKey("scroll")).set(window.scrollY)
}
}
onMounted(() => {
window.addEventListener("scroll", onScroll)
})
onUnmounted(() => {
window.removeEventListener("scroll", onScroll)
})
const loadMore = async (refresh) => {
if (refresh) {
state.items = []
state.currentPage = 1
state.error = false
state.errorMessage = ""
if (props.persistence) {
useCache(genPersistenceKey()).remove()
useCache(genPersistenceKey("scroll")).remove()
}
} else if (state.finished) {
return
}
let currentPage = state.currentPage ? state.currentPage : 1
//如果是错误状态,重新加载时,从当前页开始加载
if (state.error) {
currentPage = state.currentPage || 1
}
if (props.singlePage) {
showLoadingToast({
message: "加载中...",
forbidClick: true,
})
}
let res,
data = {}
try {
state.loading = true
const method = props.method
const params = {
[currentPageKey]: currentPage,
[pageSizeKey]: props.pageSize,
...props.extraData,
...state.searchParams,
}
if (method === "get") {
data = { params }
} else if (method === "post") {
data = { ...params }
}
res = await useFetch()[method](props.url, data)
} catch (e) {
state.error = true
state.errorMessage = "加载失败"
if (props.persistence) {
onPersistence()
}
return
}
/**
* @var {PaginationRes} res
*/
if (props.afterFetched && isFunction(props.afterFetched)) {
res = props.afterFetched(res)
} else if (paginationProvider.afterFetched && isFunction(paginationProvider.afterFetched)) {
res = paginationProvider.afterFetched(res)
}
if (res.errorMessage) {
state.error = true
state.errorMessage = res.errorMessage
if (props.persistence) {
onPersistence()
}
return
} else {
state.error = false
state.errorMessage = ""
}
state.items = props.singlePage ? res.items : [...state.items, ...res.items]
state.currentPage = res.currentPage
state.empty = res.totalSize === 0
state.finished = res.totalSize <= state.items.length
state.totalSize = res.totalSize
if (props.persistence) {
onPersistence()
}
if (props.singlePage) {
if (!state.isFirstLoad) {
//如果有 persistence 则重置 scroll 来回到顶部
useCache(genPersistenceKey("scroll")).set(0)
window.scrollTo({ top: 0 })
}
closeToast()
}
nextTick(() => {
state.isFirstLoad = false
state.refreshLoading = false
state.loading = false
})
}
const onSearch = (searchData) => {
const isPersistence = !isUndefined(searchData.persistence) && searchData.persistence
if (isPersistence) {
delete searchData.persistence
}
state.searchParams = searchData
loadMore(!isPersistence)
}
/**
* 是否自动加载第一页数据
* @returns {boolean}
*/
const onImmediateCheck = () => {
if (!props.autoLoad) {
return false
}
if (state.isFirstLoad && props.persistence && state.items.length) {
return false
}
return true
}
/**
* 瀑布形式自动加载
*/
const onLoad = () => {
//state.searchParams = searchRef.value?.getQueryForm() || {}
if (!state.isFirstLoad || (state.isFirstLoad && state.currentPage !== 1)) {
state.currentPage += 1
}
loadMore(false)
}
/**
* 如果没有下拉刷新,并且自动加载,则手动加载一次
* 如果是带 Search ,则由 Search 挂载后加载
*/
if (props.singlePage && props.autoLoad && !props.search) {
loadMore(!props.persistence)
}
/****************** exposed ******************/
/**
* 获取列表数据
* @returns Array
*/
const items = () => {
return state.items || []
}
/**
* 设置列表数据
* @param items
*/
const setItems = (items) => {
state.items = items
}
expose({
loadMore,
items,
setItems,
pagination: pick(state, ["loading", "finished", "empty", "error", "currentPage"]),
})
/****************** render ******************/
const searchElem = () => {
if (props.search) {
if (props.searchFixed) {
const stickyObj = isObject(props.searchFixed) ? props.searchFixed : {}
//给list添加一个top属性,用于计算顶部距离
if (isObject(props.searchFixed) && props.searchFixed.offsetTop) {
state.offsetTop = props.searchFixed.offsetTop
}
return (
<Sticky {...stickyObj}>
<div class={`ex-pagination__search`}>
<ExSearch ref={searchRef} persistence={props.persistence} {...props.search} onSearch={onSearch}></ExSearch>
</div>
</Sticky>
)
} else {
return (
<div class={`ex-pagination__search`}>
<ExSearch ref={searchRef} persistence={props.persistence} {...props.search} onSearch={onSearch}></ExSearch>
</div>
)
}
} else {
return null
}
}
const renderItems = () => {
const elems = state.items
? state.items.map((item, index) =>
slots.renderItem
? slots.renderItem({
item,
index,
})
: null,
)
: null
nextTick(() => {
if (props.persistence) {
const scrollY = useCache(genPersistenceKey("scroll")).get(0)
window.scrollTo({ top: scrollY })
}
})
return elems
}
const paginationElem = () => {
return (
<div class={`ex-pagination`}>
{searchElem()}
{slots.prepend?.()}
<div class={`ex-pagination__list`} style={{ paddingTop: `${state.offsetTop}px` }}>
{state.empty ? (
<Empty description={props.emptyText}></Empty>
) : (
<div>
<List
v-model:loading={state.loading}
v-model:error={state.error}
immediateCheck={onImmediateCheck()}
disabled={props.singlePage}
finished={state.finished}
finishedText={props.finishText}
errorText={state.errorMessage || props.errorText}
onLoad={onLoad}
offset={props.offset}
{...props.defaultProps}
>
{() => renderItems()}
</List>
{props.singlePage ? (
<Pagination
v-model={state.currentPage}
totalItems={state.totalSize}
onChange={() => loadMore(false)}
></Pagination>
) : null}
</div>
)}
</div>
</div>
)
}
const refreshElem = () => {
if (props.pullRefresh) {
const pullRefreshObj = isObject(props.pullRefresh) ? props.pullRefresh : {}
return (
<PullRefresh v-model={state.refreshLoading} class={`ex-pagination__refresh`} onRefresh={() => loadMore(true)} {...pullRefreshObj}>
{() => paginationElem()}
</PullRefresh>
)
} else {
return paginationElem()
}
}
return () => refreshElem()
},
})