hookehuyr

refactor(分页): 将分页逻辑抽离为组合式函数 usePagination

将 index.vue 中的分页相关逻辑抽离到独立的组合式函数 usePagination.js 中
提高代码复用性和可维护性,减少组件文件体积
/**
* 分页逻辑组合式函数
* @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)的字段,以及空页
const filtered_pages = computed(() => {
if (!pages_raw.value.length) {
const keys = formDataRef.value.filter(i => !i.component_props?.disabled).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)
})
/**
* 构建分页分组
* @description 优先按 divider 分组;若仅一组则按固定大小分片
*/
const buildPages = () => {
const result = []
let cur = []
formDataRef.value.forEach(item => {
const tag = item.component_props?.tag
// 分隔符 divider 作为分页分组边界
if (tag === 'divider') {
if (cur.length) result.push(cur)
cur = []
return
}
cur.push(item.key)
})
if (cur.length) result.push(cur)
if (result.length <= 1) {
const size = 8
const keys = formDataRef.value.filter(i => i.component_props?.tag !== 'divider').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' }
)
// 分页导航文案与禁用状态(按当前页字段的 component_props 提供的 mock 值)
const page_nav = computed(() => {
const idx = current_page_index.value
const keys = filtered_pages.value[idx] || []
let prev_text = '上一页'
let next_text = '下一页'
let prev_disabled = false
for (let k of keys) {
const item = formDataRef.value.find(i => i.key === k)
if (item && item.component_props) {
if (item.component_props.page_prev_text) prev_text = item.component_props.page_prev_text
if (item.component_props.page_next_text) next_text = item.component_props.page_next_text
if (typeof item.component_props.page_prev_disabled === 'boolean') prev_disabled = item.component_props.page_prev_disabled
}
}
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,
}
}
\ No newline at end of file
<!--
* @Date: 2022-07-18 10:22:22
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-11-18 22:35:35
* @LastEditTime: 2025-11-19 00:06:00
* @FilePath: /data-table/src/views/index.vue
* @Description: 首页
-->
......@@ -217,6 +217,7 @@ import { sharePage } from '@/composables/useShare.js'
import wx from 'weixin-js-sdk'
import LoginBox from '@/components/LoginBox/index.vue';
import PaginationField from '@/components/PaginationField/index.vue';
import { usePagination } from '@/composables/usePagination.js';
const $route = useRoute();
const $router = useRouter();
......@@ -253,52 +254,6 @@ const PCommit = ref({});
const formData = ref([]);
/**
* 分页组件原始数据
*/
const pages_raw = ref([]);
// 分页配置
const enable_pagination = ref(false);
const current_page_index = ref(0);
const filtered_pages = computed(() => {
if (!pages_raw.value.length) {
const keys = formData.value.filter(i => !i.component_props?.disabled).map(i => i.key);
return [keys];
}
const result = pages_raw.value.map(keys => keys.filter(k => {
const f = formData.value.find(i => i.key === k);
return f && !f.component_props?.disabled;
})).filter(keys => keys.length > 0);
return result.length ? result : [[]];
});
const visible_keys = computed(() => {
return filtered_pages.value[current_page_index.value] || [];
});
const visible_form_data = computed(() => {
const set = new Set(visible_keys.value);
return formData.value.filter(i => set.has(i.key) && !i.component_props?.disabled);
});
// 分页组件导航
const page_nav = computed(() => {
const idx = current_page_index.value;
const keys = filtered_pages.value[idx] || [];
let prev_text = '上一页';
let next_text = '下一页';
let prev_disabled = false;
for (let k of keys) {
const item = formData.value.find(i => i.key === k);
if (item && item.component_props) {
// TODO: MOCK 数据 等待真实字段, 分页组件相关属性
if (item.component_props.page_prev_text) prev_text = item.component_props.page_prev_text;
if (item.component_props.page_next_text) next_text = item.component_props.page_next_text;
if (typeof item.component_props.page_prev_disabled === 'boolean') prev_disabled = item.component_props.page_prev_disabled;
}
}
return { prev_text, next_text, prev_disabled };
});
/******* END: 分页组件相关 *******/
/**
* 格式化表单数据
*/
const formatData = (data) => {
......@@ -392,6 +347,7 @@ const onApprovalSelect = (item) => {
myForm.value.submit();
}
};
const onApprovalCancel = () => {
console.warn('取消');
}
......@@ -1420,18 +1376,6 @@ watch(
}
);
// 监听分页变化,移除空页并纠正当前页索引与分页状态
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' }
);
// 为每个表单字段创建单独的监听器
// 可以监听到简单组件的字段的值变化,自定义的组件无法监听到
......@@ -1485,83 +1429,29 @@ const setVolunteerData = async (volunteer_phone) => {
}
}
// 构建分页组件
const buildPages = () => {
const result = [];
let cur = [];
formData.value.forEach(item => {
const tag = item.component_props?.tag;
// TODO: 分页组件标识暂时不确定
if (tag === 'divider') {
if (cur.length) result.push(cur);
cur = [];
return;
}
cur.push(item.key);
});
if (cur.length) result.push(cur);
if (result.length <= 1) {
const size = 8;
const keys = formData.value.filter(i => i.component_props?.tag !== 'divider').map(i => i.key);
const chunked = [];
for (let i = 0; i < keys.length; i += size) {
chunked.push(keys.slice(i, i + size));
// TAG: 分页逻辑抽离为组合式函数
const {
pages_raw,
enable_pagination,
current_page_index,
filtered_pages,
visible_keys,
visible_form_data,
buildPages,
page_nav,
handlePrev,
handleNext,
handleSubmit,
} = usePagination(formData, {
myFormRef: myForm,
validOther,
afterSwitch: async () => {
image_uploader.value = [];
file_uploader.value = [];
table_editor.value = [];
await nextTick();
}
pages_raw.value = chunked.length ? chunked : [keys];
} else {
pages_raw.value = result;
}
};
// 上一页
const handlePrev = async () => {
if (current_page_index.value === 0) return;
current_page_index.value -= 1;
image_uploader.value = [];
file_uploader.value = [];
table_editor.value = [];
await nextTick();
};
// 校验当前页
const validateCurrentPage = async () => {
try {
await myForm.value.validate();
} catch (e) {
const err = Array.isArray(e?.errors) ? e.errors[0] : null;
const name = err?.name || '';
let error_label = '';
formData.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 = validOther();
if (!other.status) {
showToast('验证失败');
return false;
}
return true;
};
// 下一页
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;
image_uploader.value = [];
file_uploader.value = [];
table_editor.value = [];
await nextTick();
window.scrollTo({ top: 0 });
}
};
// 最后一页提交
const handleSubmit = () => {
myForm.value.submit();
};
});
</script>
<style lang="less">
......