hookehuyr

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

将 index.vue 中的分页相关逻辑抽离到独立的组合式函数 usePagination.js 中
提高代码复用性和可维护性,减少组件文件体积
1 +/**
2 + * 分页逻辑组合式函数
3 + * @description 提供分页的原始分组、过滤后分组、当前页索引、是否启用分页、可见字段等
4 + * @param {import('vue').Ref<Array>} formDataRef 表单字段数据的 ref
5 + * @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>}}
6 + */
7 +import { ref, computed, watch, nextTick } from 'vue'
8 +import { showToast } from 'vant'
9 +
10 +export function usePagination(formDataRef, options = {}) {
11 + const { myFormRef, validOther, afterSwitch } = options
12 + // 分页原始分组
13 + const pages_raw = ref([])
14 + // 是否启用分页
15 + const enable_pagination = ref(false)
16 + // 当前页索引
17 + const current_page_index = ref(0)
18 +
19 + // 过滤后的分页:剔除被隐藏(disabled)的字段,以及空页
20 + const filtered_pages = computed(() => {
21 + if (!pages_raw.value.length) {
22 + const keys = formDataRef.value.filter(i => !i.component_props?.disabled).map(i => i.key)
23 + return [keys]
24 + }
25 + const result = pages_raw.value
26 + .map(keys => keys.filter(k => {
27 + const f = formDataRef.value.find(i => i.key === k)
28 + return f && !f.component_props?.disabled
29 + }))
30 + .filter(keys => keys.length > 0)
31 + return result.length ? result : [[]]
32 + })
33 +
34 + // 当前页可见的 key 列表
35 + const visible_keys = computed(() => filtered_pages.value[current_page_index.value] || [])
36 +
37 + // 当前页可见的字段数据
38 + const visible_form_data = computed(() => {
39 + const set = new Set(visible_keys.value)
40 + return formDataRef.value.filter(i => set.has(i.key) && !i.component_props?.disabled)
41 + })
42 +
43 + /**
44 + * 构建分页分组
45 + * @description 优先按 divider 分组;若仅一组则按固定大小分片
46 + */
47 + const buildPages = () => {
48 + const result = []
49 + let cur = []
50 + formDataRef.value.forEach(item => {
51 + const tag = item.component_props?.tag
52 + // 分隔符 divider 作为分页分组边界
53 + if (tag === 'divider') {
54 + if (cur.length) result.push(cur)
55 + cur = []
56 + return
57 + }
58 + cur.push(item.key)
59 + })
60 + if (cur.length) result.push(cur)
61 + if (result.length <= 1) {
62 + const size = 8
63 + const keys = formDataRef.value.filter(i => i.component_props?.tag !== 'divider').map(i => i.key)
64 + const chunked = []
65 + for (let i = 0; i < keys.length; i += size) {
66 + chunked.push(keys.slice(i, i + size))
67 + }
68 + pages_raw.value = chunked.length ? chunked : [keys]
69 + } else {
70 + pages_raw.value = result
71 + }
72 + }
73 +
74 + // 监听过滤后的分页变化,纠正当前页索引并设置启用状态
75 + watch(
76 + () => filtered_pages.value,
77 + (newPages) => {
78 + const last = newPages.length - 1
79 + if (current_page_index.value > last) {
80 + current_page_index.value = last >= 0 ? last : 0
81 + }
82 + enable_pagination.value = newPages.length > 1
83 + },
84 + { flush: 'post' }
85 + )
86 +
87 + // 分页导航文案与禁用状态(按当前页字段的 component_props 提供的 mock 值)
88 + const page_nav = computed(() => {
89 + const idx = current_page_index.value
90 + const keys = filtered_pages.value[idx] || []
91 + let prev_text = '上一页'
92 + let next_text = '下一页'
93 + let prev_disabled = false
94 + for (let k of keys) {
95 + const item = formDataRef.value.find(i => i.key === k)
96 + if (item && item.component_props) {
97 + if (item.component_props.page_prev_text) prev_text = item.component_props.page_prev_text
98 + if (item.component_props.page_next_text) next_text = item.component_props.page_next_text
99 + if (typeof item.component_props.page_prev_disabled === 'boolean') prev_disabled = item.component_props.page_prev_disabled
100 + }
101 + }
102 + return { prev_text, next_text, prev_disabled }
103 + })
104 +
105 + /**
106 + * 校验当前页
107 + * @returns {Promise<boolean>} 是否通过校验
108 + */
109 + const validateCurrentPage = async () => {
110 + try {
111 + await myFormRef?.value?.validate()
112 + } catch (e) {
113 + const err = Array.isArray(e?.errors) ? e.errors[0] : null
114 + const name = err?.name || ''
115 + let error_label = ''
116 + formDataRef.value.forEach(item => { if (item.key === name) { error_label = item.component_props?.label || name } })
117 + const msg = err?.message || '验证失败'
118 + showToast((error_label || '表单') + ': ' + msg)
119 + return false
120 + }
121 + const other = typeof validOther === 'function' ? validOther() : { status: true }
122 + if (!other.status) {
123 + showToast('验证失败')
124 + return false
125 + }
126 + return true
127 + }
128 +
129 + /**
130 + * 上一页
131 + * @description 切换到上一页并执行页面切换后的回调
132 + */
133 + const handlePrev = async () => {
134 + if (current_page_index.value === 0) return
135 + current_page_index.value -= 1
136 + if (typeof afterSwitch === 'function') await afterSwitch()
137 + }
138 +
139 + /**
140 + * 下一页
141 + * @description 验证当前页,通过后切换到下一页并执行页面切换后的回调
142 + */
143 + const handleNext = async () => {
144 + const ok = await validateCurrentPage()
145 + if (!ok) return
146 + if (current_page_index.value < filtered_pages.value.length - 1) {
147 + current_page_index.value += 1
148 + if (typeof afterSwitch === 'function') await afterSwitch()
149 + await nextTick()
150 + window.scrollTo({ top: 0 })
151 + }
152 + }
153 +
154 + /**
155 + * 最后一页提交
156 + * @description 触发表单提交
157 + */
158 + const handleSubmit = () => {
159 + myFormRef?.value?.submit()
160 + }
161 +
162 + return {
163 + pages_raw,
164 + filtered_pages,
165 + visible_keys,
166 + visible_form_data,
167 + current_page_index,
168 + enable_pagination,
169 + buildPages,
170 + page_nav,
171 + validateCurrentPage,
172 + handlePrev,
173 + handleNext,
174 + handleSubmit,
175 + }
176 +}
...\ No newline at end of file ...\ No newline at end of file
1 <!-- 1 <!--
2 * @Date: 2022-07-18 10:22:22 2 * @Date: 2022-07-18 10:22:22
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-11-18 22:35:35 4 + * @LastEditTime: 2025-11-19 00:06:00
5 * @FilePath: /data-table/src/views/index.vue 5 * @FilePath: /data-table/src/views/index.vue
6 * @Description: 首页 6 * @Description: 首页
7 --> 7 -->
...@@ -217,6 +217,7 @@ import { sharePage } from '@/composables/useShare.js' ...@@ -217,6 +217,7 @@ import { sharePage } from '@/composables/useShare.js'
217 import wx from 'weixin-js-sdk' 217 import wx from 'weixin-js-sdk'
218 import LoginBox from '@/components/LoginBox/index.vue'; 218 import LoginBox from '@/components/LoginBox/index.vue';
219 import PaginationField from '@/components/PaginationField/index.vue'; 219 import PaginationField from '@/components/PaginationField/index.vue';
220 +import { usePagination } from '@/composables/usePagination.js';
220 221
221 const $route = useRoute(); 222 const $route = useRoute();
222 const $router = useRouter(); 223 const $router = useRouter();
...@@ -253,52 +254,6 @@ const PCommit = ref({}); ...@@ -253,52 +254,6 @@ const PCommit = ref({});
253 const formData = ref([]); 254 const formData = ref([]);
254 255
255 /** 256 /**
256 - * 分页组件原始数据
257 - */
258 -const pages_raw = ref([]);
259 -// 分页配置
260 -const enable_pagination = ref(false);
261 -const current_page_index = ref(0);
262 -const filtered_pages = computed(() => {
263 - if (!pages_raw.value.length) {
264 - const keys = formData.value.filter(i => !i.component_props?.disabled).map(i => i.key);
265 - return [keys];
266 - }
267 - const result = pages_raw.value.map(keys => keys.filter(k => {
268 - const f = formData.value.find(i => i.key === k);
269 - return f && !f.component_props?.disabled;
270 - })).filter(keys => keys.length > 0);
271 - return result.length ? result : [[]];
272 -});
273 -const visible_keys = computed(() => {
274 - return filtered_pages.value[current_page_index.value] || [];
275 -});
276 -const visible_form_data = computed(() => {
277 - const set = new Set(visible_keys.value);
278 - return formData.value.filter(i => set.has(i.key) && !i.component_props?.disabled);
279 -});
280 -
281 -// 分页组件导航
282 -const page_nav = computed(() => {
283 - const idx = current_page_index.value;
284 - const keys = filtered_pages.value[idx] || [];
285 - let prev_text = '上一页';
286 - let next_text = '下一页';
287 - let prev_disabled = false;
288 - for (let k of keys) {
289 - const item = formData.value.find(i => i.key === k);
290 - if (item && item.component_props) {
291 - // TODO: MOCK 数据 等待真实字段, 分页组件相关属性
292 - if (item.component_props.page_prev_text) prev_text = item.component_props.page_prev_text;
293 - if (item.component_props.page_next_text) next_text = item.component_props.page_next_text;
294 - if (typeof item.component_props.page_prev_disabled === 'boolean') prev_disabled = item.component_props.page_prev_disabled;
295 - }
296 - }
297 - return { prev_text, next_text, prev_disabled };
298 -});
299 -/******* END: 分页组件相关 *******/
300 -
301 -/**
302 * 格式化表单数据 257 * 格式化表单数据
303 */ 258 */
304 const formatData = (data) => { 259 const formatData = (data) => {
...@@ -392,6 +347,7 @@ const onApprovalSelect = (item) => { ...@@ -392,6 +347,7 @@ const onApprovalSelect = (item) => {
392 myForm.value.submit(); 347 myForm.value.submit();
393 } 348 }
394 }; 349 };
350 +
395 const onApprovalCancel = () => { 351 const onApprovalCancel = () => {
396 console.warn('取消'); 352 console.warn('取消');
397 } 353 }
...@@ -1420,18 +1376,6 @@ watch( ...@@ -1420,18 +1376,6 @@ watch(
1420 } 1376 }
1421 ); 1377 );
1422 1378
1423 -// 监听分页变化,移除空页并纠正当前页索引与分页状态
1424 -watch(
1425 - () => filtered_pages.value,
1426 - (newPages) => {
1427 - const last = newPages.length - 1;
1428 - if (current_page_index.value > last) {
1429 - current_page_index.value = last >= 0 ? last : 0;
1430 - }
1431 - enable_pagination.value = newPages.length > 1;
1432 - },
1433 - { flush: 'post' }
1434 -);
1435 1379
1436 // 为每个表单字段创建单独的监听器 1380 // 为每个表单字段创建单独的监听器
1437 // 可以监听到简单组件的字段的值变化,自定义的组件无法监听到 1381 // 可以监听到简单组件的字段的值变化,自定义的组件无法监听到
...@@ -1485,83 +1429,29 @@ const setVolunteerData = async (volunteer_phone) => { ...@@ -1485,83 +1429,29 @@ const setVolunteerData = async (volunteer_phone) => {
1485 } 1429 }
1486 } 1430 }
1487 1431
1488 -// 构建分页组件 1432 +// TAG: 分页逻辑抽离为组合式函数
1489 -const buildPages = () => { 1433 +const {
1490 - const result = []; 1434 + pages_raw,
1491 - let cur = []; 1435 + enable_pagination,
1492 - formData.value.forEach(item => { 1436 + current_page_index,
1493 - const tag = item.component_props?.tag; 1437 + filtered_pages,
1494 - // TODO: 分页组件标识暂时不确定 1438 + visible_keys,
1495 - if (tag === 'divider') { 1439 + visible_form_data,
1496 - if (cur.length) result.push(cur); 1440 + buildPages,
1497 - cur = []; 1441 + page_nav,
1498 - return; 1442 + handlePrev,
1499 - } 1443 + handleNext,
1500 - cur.push(item.key); 1444 + handleSubmit,
1501 - }); 1445 +} = usePagination(formData, {
1502 - if (cur.length) result.push(cur); 1446 + myFormRef: myForm,
1503 - if (result.length <= 1) { 1447 + validOther,
1504 - const size = 8; 1448 + afterSwitch: async () => {
1505 - const keys = formData.value.filter(i => i.component_props?.tag !== 'divider').map(i => i.key); 1449 + image_uploader.value = [];
1506 - const chunked = []; 1450 + file_uploader.value = [];
1507 - for (let i = 0; i < keys.length; i += size) { 1451 + table_editor.value = [];
1508 - chunked.push(keys.slice(i, i + size)); 1452 + await nextTick();
1509 } 1453 }
1510 - pages_raw.value = chunked.length ? chunked : [keys]; 1454 +});
1511 - } else {
1512 - pages_raw.value = result;
1513 - }
1514 -};
1515 -
1516 -// 上一页
1517 -const handlePrev = async () => {
1518 - if (current_page_index.value === 0) return;
1519 - current_page_index.value -= 1;
1520 - image_uploader.value = [];
1521 - file_uploader.value = [];
1522 - table_editor.value = [];
1523 - await nextTick();
1524 -};
1525 -
1526 -// 校验当前页
1527 -const validateCurrentPage = async () => {
1528 - try {
1529 - await myForm.value.validate();
1530 - } catch (e) {
1531 - const err = Array.isArray(e?.errors) ? e.errors[0] : null;
1532 - const name = err?.name || '';
1533 - let error_label = '';
1534 - formData.value.forEach(item => { if (item.key === name) { error_label = item.component_props?.label || name; } });
1535 - const msg = err?.message || '验证失败';
1536 - showToast((error_label || '表单') + ': ' + msg);
1537 - return false;
1538 - }
1539 - const other = validOther();
1540 - if (!other.status) {
1541 - showToast('验证失败');
1542 - return false;
1543 - }
1544 - return true;
1545 -};
1546 -
1547 -// 下一页
1548 -const handleNext = async () => {
1549 - const ok = await validateCurrentPage();
1550 - if (!ok) return;
1551 - if (current_page_index.value < filtered_pages.value.length - 1) {
1552 - current_page_index.value += 1;
1553 - image_uploader.value = [];
1554 - file_uploader.value = [];
1555 - table_editor.value = [];
1556 - await nextTick();
1557 - window.scrollTo({ top: 0 });
1558 - }
1559 -};
1560 -
1561 -// 最后一页提交
1562 -const handleSubmit = () => {
1563 - myForm.value.submit();
1564 -};
1565 </script> 1455 </script>
1566 1456
1567 <style lang="less"> 1457 <style lang="less">
......