usePagination.js 6.92 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)

    // 过滤后的分页:剔除被隐藏(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)
    })

    /**
     * 构建分页分组
     * @description 优先按 paginator 分组;若仅一组则按固定大小分片
     */
    const buildPages = () => {
        const result = []
        let cur = []
        // 连续的 paginator 需要忽略,避免出现空分页
        let last_was_paginator = false
        formDataRef.value.forEach(item => {
            const tag = item.component_props?.tag
            // 分隔符 paginator 作为分页分组边界
            if (tag === 'paginator') {
                // 若前一个也是 paginator,则忽略本次(不产生空页)
                if (last_was_paginator) return
                if (cur.length) result.push(cur)
                cur = []
                last_was_paginator = true
                return
            }
            cur.push(item.key)
            last_was_paginator = false
        })
        if (cur.length) result.push(cur)
        if (result.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]
        } else {
            pages_raw.value = result
        }
    }

    // 监听过滤后的分页变化,纠正当前页索引并设置启用状态
    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 仅从 tag 为 paginator 的字段中读取导航文案与可返回状态
     * @returns {{prev_text: string, next_text: string, prev_disabled: boolean}}
     */
    const page_nav = computed(() => {
        // 默认文案与状态
        let prev_text = '上一页'
        let next_text = '下一页'
        let prev_disabled = false

        // 仅查找 tag=paginator 的字段并提取其 component_props
        const paginator_item = formDataRef.value.find(i => i?.component_props?.tag === 'paginator')
        if (paginator_item && paginator_item.component_props) {
            const props = paginator_item.component_props
            if (props.back_title) prev_text = props.back_title
            if (props.next_title) next_text = props.next_title
            // is_back 表示是否允许返回
            if (typeof props.is_back === 'boolean') prev_disabled = !props.is_back
        }

        return { prev_text, next_text, prev_disabled }
    })

    /**
     * 校验当前页
     * @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,
    }
}