UNPKG

jobsys-explore

Version:

Enhanced component based on vant

460 lines (394 loc) 11 kB
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() }, })