usePagination.js 11.1 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'
import { styleColor } from "@/constant.js";

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([])
    // 下一页意图标记:通过 onSubmit 校验通过后再真正切页
    const navigate_next_pending = ref(false)

    /**
     * 过滤后的分页
     * @description 剔除被隐藏(disabled)的字段,并移除过滤后为空的页,确保分页总数只计算有内容的页
     */
    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
            }))
            .filter(keys => keys.length > 0)
        // 兜底:若全部为空,则保留一个空页以维持形态
        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, prev_btn_color: string, next_btn_color: string, prev_text_color: string, next_text_color: string}} 导航配置
     */
    const normalizePaginatorProps = (props = {}) => {
        // 中文文案默认值
        const nav = { prev_text: '上一页', next_text: '下一页', prev_disabled: false, prev_btn_color: styleColor.baseColor, next_btn_color: styleColor.baseColor, prev_text_color: '#fff', next_text_color: '#fff' }
        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
        // 导航颜色
        if (props.back_background_color) nav.prev_btn_color = props.back_background_color
        if (props.next_background_color) nav.next_btn_color = props.next_background_color
        if (props.back_color) nav.prev_text_color = props.back_color
        if (props.next_color) nav.next_text_color = props.next_color
        return nav
    }

    /**
     * 构建分页分组
     * @description 优先按 paginator 分组;并为每一页记录其对应的导航文案与返回策略
     */
    const buildPages = () => {
        const pages = []
        const navs = []
        let cur = []
        // 连续的 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(normalizePaginatorProps(item.component_props))
                    cur = []
                }
                last_was_paginator = true
                return
            }
            // 普通字段加入当前页
            cur.push(item.key)
            last_was_paginator = false
        })

        // 收尾:最后一页(没有尾随 paginator 时使用默认导航)
        if (cur.length) {
            pages.push(cur)
            navs.push(normalizePaginatorProps())
        }

        // 无显式分页符时,不启用分页(单页展现,不再进行固定大小分片)
        // 这样可确保没有 paginator 控件时,分页组件不会显示
        const has_paginator = formDataRef.value.some(i => i.component_props?.tag === 'paginator')
        if (!has_paginator) {
            const keys = formDataRef.value
                .filter(i => i.component_props?.tag !== 'paginator')
                .map(i => i.key)
            pages_raw.value = [keys]
            // 单页仍提供默认导航配置,供最后页按钮样式读取,但不会显示分页组件
            page_nav_props_by_index.value = [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}}
     */
    /**
     * 过滤后的分页导航
     * @description 与 filtered_pages 同步,仅为有内容的页保留对应的导航配置
     */
    const filtered_nav_props_by_index = computed(() => {
        // 未构建分页时使用默认导航
        if (!pages_raw.value.length) {
            return [normalizePaginatorProps()]
        }
        // 与过滤页一一对应:仅保留含可见字段的页的导航
        const visible_navs = []
        pages_raw.value.forEach((keys, idx) => {
            const hasVisible = keys.some(k => {
                const f = formDataRef.value.find(i => i.key === k)
                return f && !f.component_props?.disabled
            })
            if (hasVisible) visible_navs.push(page_nav_props_by_index.value[idx] || normalizePaginatorProps())
        })
        return visible_navs.length ? visible_navs : [normalizePaginatorProps()]
    })

    /**
     * 分页导航配置(与过滤后页索引一致)
     * @returns {{prev_text: string, next_text: string, prev_disabled: boolean}}
     */
    const page_nav = computed(() => {
        const def = normalizePaginatorProps()
        return filtered_nav_props_by_index.value[current_page_index.value] || def
    })

    /**
     * 是否为最后一页
     * @description 判断当前页索引是否位于过滤后的最后一页
     * @returns {import('vue').ComputedRef<boolean>}
     */
    const is_last_page = computed(() => {
        const last = filtered_pages.value.length - 1
        return current_page_index.value === last
    })

    /**
     * 校验当前页
     * @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 验证当前页,通过后切换到下一页并执行页面切换后的回调
     */
    /**
     * 下一页
     * @description 不直接调用 validate,而是触发表单 submit,让视图层的 onSubmit 统一处理校验
     */
    const handleNext = async () => {
        // 标记为“下一页校验意图”,供 onSubmit 判定
        navigate_next_pending.value = true
        // 触发表单校验(验证失败将触发 onFailed,成功将触发 onSubmit)
        await myFormRef?.value?.submit?.()
    }

    /**
     * 校验通过后的翻页动作
     * @description onSubmit 检验通过且为“下一页意图”时调用;同时重置后续页面的错误状态
     */
    const afterValidatedNavigateNext = async () => {
        navigate_next_pending.value = false
        if (current_page_index.value < filtered_pages.value.length - 1) {
            current_page_index.value += 1
            // 重置所有校验状态,避免新页显示历史错误
            try { myFormRef?.value?.resetValidation?.() } catch (e) {}
            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,
        is_last_page,
        navigate_next_pending,
        afterValidatedNavigateNext,
        validateCurrentPage,
        handlePrev,
        handleNext,
        handleSubmit,
    }
}