usePagination.js
11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/**
* 分页逻辑组合式函数
* @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,
}
}