usePagination.js 8.07 KB
/**
 * 分页逻辑组合式函数
 * @description 提供分页的原始分组、过滤后分组、当前页索引、是否启用分页、可见字段等
 * @param {import('vue').Ref<Array>} formDataRef 表单字段数据的 ref
 * @returns {{pages_raw: import('vue').Ref<Array>, filtered_pages: import('vue').ComputedRef<Array>, visible_keys: import('vue').ComputedRef<Array>, visible_form_data: import('vue').ComputedRef<Array>, current_page_index: import('vue').Ref<number>, enable_pagination: import('vue').Ref<boolean>, buildPages: Function, page_nav: import('vue').ComputedRef<Object>}}
 */
import { ref, computed, watch, nextTick } from 'vue'
import { showToast } from 'vant'

export function usePagination(formDataRef, options = {}) {
    const { myFormRef, validOther, afterSwitch } = options
    // 分页原始分组
    const pages_raw = ref([])
    // 是否启用分页
    const enable_pagination = ref(false)
    // 当前页索引
    const current_page_index = ref(0)
    // 每一页的导航配置(由相邻的 paginator 控件决定)
    const page_nav_props_by_index = ref([])

    // 过滤后的分页:剔除被隐藏(disabled)的字段,但保留空页占位,确保分页总数与 paginator 一致
    const filtered_pages = computed(() => {
        if (!pages_raw.value.length) {
            const keys = formDataRef.value
                .filter(i => !i.component_props?.disabled && i.component_props?.tag !== 'paginator')
                .map(i => i.key)
            return [keys]
        }
        const result = pages_raw.value
            .map(keys => keys.filter(k => {
                const f = formDataRef.value.find(i => i.key === k)
                return f && !f.component_props?.disabled
            }))
        // 不再删除空页,以保证总页数符合分页符定义
        return result.length ? result : [[]]
    })

    // 当前页可见的 key 列表
    const visible_keys = computed(() => filtered_pages.value[current_page_index.value] || [])

    // 当前页可见的字段数据
    const visible_form_data = computed(() => {
        const set = new Set(visible_keys.value)
        return formDataRef.value.filter(i => set.has(i.key) && !i.component_props?.disabled)
    })

    /**
     * 规范化分页符属性为导航配置
     * @param {Object} props 分页符的 component_props
     * @returns {{prev_text: string, next_text: string, prev_disabled: boolean}} 导航配置
     */
    const normalizePaginatorProps = (props = {}) => {
        // 中文文案默认值
        const nav = { prev_text: '上一页', next_text: '下一页', prev_disabled: false }
        if (props.back_title) nav.prev_text = props.back_title
        if (props.next_title) nav.next_text = props.next_title
        // 是否允许返回:true 允许,false 不允许
        if (typeof props.is_back === 'boolean') nav.prev_disabled = !props.is_back
        return nav
    }

    /**
     * 构建分页分组
     * @description 优先按 paginator 分组;并为每一页记录其对应的导航文案与返回策略
     */
    const buildPages = () => {
        const pages = []
        const navs = []
        let cur = []
        // 当前页导航配置,默认值;当遇到 paginator 时更新为“下一页”的导航
        let current_nav = normalizePaginatorProps()
        // 连续的 paginator 需要忽略,避免出现空分页
        let last_was_paginator = false
        formDataRef.value.forEach(item => {
            const tag = item.component_props?.tag
            // 分隔符 paginator 作为分页分组边界,同时决定后续一页的导航配置
            if (tag === 'paginator') {
                // 连续分页符:忽略后者,保留先前设置,避免空页与属性覆盖
                if (last_was_paginator) return
                // 如果当前已有内容,先把“这一页”收束,并使用当前导航配置
                if (cur.length) {
                    pages.push(cur)
                    navs.push(current_nav)
                    cur = []
                }
                // 更新“下一页”的导航配置为本分页符设置
                current_nav = normalizePaginatorProps(item.component_props)
                last_was_paginator = true
                return
            }
            // 普通字段加入当前页
            cur.push(item.key)
            last_was_paginator = false
        })

        // 收尾:最后一页
        if (cur.length) {
            pages.push(cur)
            navs.push(current_nav)
        }

        // 若未显式分页,仅按固定大小分片,并为每一页设置默认导航
        if (pages.length <= 1) {
            const size = 8
            const keys = formDataRef.value.filter(i => i.component_props?.tag !== 'paginator').map(i => i.key)
            const chunked = []
            for (let i = 0; i < keys.length; i += size) {
                chunked.push(keys.slice(i, i + size))
            }
            pages_raw.value = chunked.length ? chunked : [keys]
            page_nav_props_by_index.value = (chunked.length ? chunked : [keys]).map(() => normalizePaginatorProps())
        } else {
            pages_raw.value = pages
            page_nav_props_by_index.value = navs
        }
    }

    // 监听过滤后的分页变化,纠正当前页索引并设置启用状态
    watch(
        () => filtered_pages.value,
        (newPages) => {
            const last = newPages.length - 1
            if (current_page_index.value > last) {
                current_page_index.value = last >= 0 ? last : 0
            }
            enable_pagination.value = newPages.length > 1
        },
        { flush: 'post' }
    )

    /**
     * 分页导航配置
     * @description 根据当前页索引读取对应的分页符导航文案与返回控制
     * @returns {{prev_text: string, next_text: string, prev_disabled: boolean}}
     */
    const page_nav = computed(() => {
        const def = normalizePaginatorProps()
        return page_nav_props_by_index.value[current_page_index.value] || def
    })

    /**
     * 校验当前页
     * @returns {Promise<boolean>} 是否通过校验
     */
    const validateCurrentPage = async () => {
        try {
            await myFormRef?.value?.validate()
        } catch (e) {
            const err = Array.isArray(e?.errors) ? e.errors[0] : null
            const name = err?.name || ''
            let error_label = ''
            formDataRef.value.forEach(item => { if (item.key === name) { error_label = item.component_props?.label || name } })
            const msg = err?.message || '验证失败'
            showToast((error_label || '表单') + ': ' + msg)
            return false
        }
        const other = typeof validOther === 'function' ? validOther() : { status: true }
        if (!other.status) {
            showToast('验证失败')
            return false
        }
        return true
    }

    /**
     * 上一页
     * @description 切换到上一页并执行页面切换后的回调
     */
    const handlePrev = async () => {
        if (current_page_index.value === 0) return
        current_page_index.value -= 1
        if (typeof afterSwitch === 'function') await afterSwitch()
    }

    /**
     * 下一页
     * @description 验证当前页,通过后切换到下一页并执行页面切换后的回调
     */
    const handleNext = async () => {
        const ok = await validateCurrentPage()
        if (!ok) return
        if (current_page_index.value < filtered_pages.value.length - 1) {
            current_page_index.value += 1
            if (typeof afterSwitch === 'function') await afterSwitch()
            await nextTick()
            window.scrollTo({ top: 0 })
        }
    }

    /**
     * 最后一页提交
     * @description 触发表单提交
     */
    const handleSubmit = () => {
        myFormRef?.value?.submit()
    }

    return {
        pages_raw,
        filtered_pages,
        visible_keys,
        visible_form_data,
        current_page_index,
        enable_pagination,
        buildPages,
        page_nav,
        validateCurrentPage,
        handlePrev,
        handleNext,
        handleSubmit,
    }
}