@antsoo-lib/core 2.0.5 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/core.css +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +6 -5
- package/dist/types/BaseSearch/index.d.ts +1 -1
- package/dist/types/BaseTable/index.d.ts +4 -480
- package/dist/types/Form/CoreForm.d.ts +82 -0
- package/dist/types/SSelectPage/index.d.ts +102 -0
- package/package.json +5 -5
- package/src/BaseSearch/index.vue +371 -371
- package/src/BaseTable/index.vue +910 -910
- package/src/Form/CoreForm.vue +782 -782
- package/src/Form/types.ts +86 -86
- package/src/SSelectPage/index.vue +607 -607
- package/src/index.ts +17 -17
- package/src/render/AreaCascader.tsx +64 -64
- package/src/render/AutoComplete.tsx +101 -101
- package/src/render/Button.tsx +62 -62
- package/src/render/Cascader.tsx +45 -45
- package/src/render/Checkbox.tsx +65 -65
- package/src/render/CheckboxGroup.tsx +57 -57
- package/src/render/Custom.tsx +19 -19
- package/src/render/DatePicker.tsx +83 -83
- package/src/render/Input.tsx +140 -140
- package/src/render/InputGroup.tsx +115 -115
- package/src/render/InputNumber.tsx +205 -205
- package/src/render/InputPassword.tsx +81 -81
- package/src/render/InputRange.tsx +154 -154
- package/src/render/RadioGroup.tsx +63 -63
- package/src/render/Select.tsx +96 -96
- package/src/render/SselectPage.tsx +107 -107
- package/src/render/Switch.tsx +60 -60
- package/src/render/Tree.tsx +136 -136
- package/src/render/TreeSelect.tsx +81 -81
- package/src/render/Upload.tsx +91 -91
- package/src/render/helper.tsx +221 -221
- package/src/render/index.ts +108 -108
- package/src/render/registry.ts +20 -20
- package/src/render/state.ts +37 -37
- package/src/render/types.ts +567 -567
- package/src/utils/attrMapping.ts +106 -106
- package/vite.config.ts +61 -61
- package/.turbo/turbo-build.log +0 -40
|
@@ -1,607 +1,607 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
import { Divider, Pagination, Select, SelectOption } from '@antsoo-lib/components'
|
|
3
|
-
import type { SelectProps } from '@antsoo-lib/components'
|
|
4
|
-
import type { AnyObject } from '@antsoo-lib/shared'
|
|
5
|
-
import { debounce, isEqual } from 'lodash-es'
|
|
6
|
-
|
|
7
|
-
import { computed, defineComponent, nextTick, reactive, ref, watch } from 'vue'
|
|
8
|
-
|
|
9
|
-
// 请求基础配置
|
|
10
|
-
interface RequestConfig {
|
|
11
|
-
method?: 'GET' | 'POST'
|
|
12
|
-
url: string
|
|
13
|
-
headers?: Record<string, string>
|
|
14
|
-
params?: AnyObject
|
|
15
|
-
data?: AnyObject
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// 外部注入请求函数类型
|
|
19
|
-
type Requester = (
|
|
20
|
-
config: Required<Pick<RequestConfig, 'method' | 'url'>> & RequestConfig,
|
|
21
|
-
) => Promise<any>
|
|
22
|
-
|
|
23
|
-
// 组件 API 配置
|
|
24
|
-
interface ApiConfig extends RequestConfig {
|
|
25
|
-
request?: Requester
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// 数据映射配置
|
|
29
|
-
interface DataMapping {
|
|
30
|
-
list?: string
|
|
31
|
-
total?: string
|
|
32
|
-
value?: string
|
|
33
|
-
label?: string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// 分页配置
|
|
37
|
-
interface PaginationConfig {
|
|
38
|
-
pageField?: string
|
|
39
|
-
pageSizeField?: string
|
|
40
|
-
searchField?: string
|
|
41
|
-
searchContains?: string
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// 组件 Props
|
|
45
|
-
interface Props extends /* @vue-ignore */ Omit<SelectProps, 'options' | 'loading'> {
|
|
46
|
-
placeholder?: string
|
|
47
|
-
allowClear?: boolean
|
|
48
|
-
showSearch?: boolean
|
|
49
|
-
disabled?: boolean
|
|
50
|
-
maxTagCount?: 'responsive' | number
|
|
51
|
-
size?: 'large' | 'middle' | 'small'
|
|
52
|
-
mode?: 'multiple' | 'tags'
|
|
53
|
-
value?: any
|
|
54
|
-
api: ApiConfig
|
|
55
|
-
dataMapping?: DataMapping
|
|
56
|
-
pagination?: boolean
|
|
57
|
-
paginationConfig?: PaginationConfig
|
|
58
|
-
pageSize?: number
|
|
59
|
-
searchDelay?: number
|
|
60
|
-
extraParams?: AnyObject
|
|
61
|
-
params?: AnyObject
|
|
62
|
-
transformData?: (data: any) => any
|
|
63
|
-
autoLoad?: boolean
|
|
64
|
-
attr?: Record<string, string>
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
type OptionItem = Record<string, unknown> & { disabled?: boolean }
|
|
68
|
-
type SelectValue = SelectProps['value']
|
|
69
|
-
type RequestPayload = Required<Pick<RequestConfig, 'method' | 'url'>> & RequestConfig
|
|
70
|
-
interface ResponseProcessResult {
|
|
71
|
-
listData: OptionItem[]
|
|
72
|
-
totalCount: number
|
|
73
|
-
shouldDisablePagination: boolean
|
|
74
|
-
}
|
|
75
|
-
interface ExposedMethods {
|
|
76
|
-
reload: () => void
|
|
77
|
-
fetchData: () => Promise<void> | void
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
81
|
-
pagination: true,
|
|
82
|
-
pageSize: 10,
|
|
83
|
-
searchDelay: 300,
|
|
84
|
-
extraParams: () => ({}),
|
|
85
|
-
params: () => ({}),
|
|
86
|
-
autoLoad: true,
|
|
87
|
-
allowClear: true,
|
|
88
|
-
showSearch: true,
|
|
89
|
-
placeholder: '请选择',
|
|
90
|
-
maxTagCount: 1,
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
const emit = defineEmits<{
|
|
94
|
-
change: [value: any, option: any, allOptions: any]
|
|
95
|
-
search: [value: string]
|
|
96
|
-
clear: []
|
|
97
|
-
'update:value': [value: any]
|
|
98
|
-
}>()
|
|
99
|
-
|
|
100
|
-
const selectedValue = ref<SelectValue>()
|
|
101
|
-
const loading = ref(false)
|
|
102
|
-
const options = ref<OptionItem[]>([])
|
|
103
|
-
const currentPage = ref(1)
|
|
104
|
-
const total = ref(0)
|
|
105
|
-
const searchKeyword = ref('')
|
|
106
|
-
const selectRef = ref<{ focus?: () => void } | null>(null)
|
|
107
|
-
const isSearching = ref(false)
|
|
108
|
-
const dynamicPagination = ref(true)
|
|
109
|
-
let lastRequestId = 0
|
|
110
|
-
|
|
111
|
-
// 临时查询参数(用于回显时按 value 拉取选项)
|
|
112
|
-
const seekParams = reactive<AnyObject>({})
|
|
113
|
-
|
|
114
|
-
const mergedDataMapping = computed<Required<DataMapping>>(() => {
|
|
115
|
-
const defaultMapping: Required<DataMapping> = {
|
|
116
|
-
list: 'list',
|
|
117
|
-
total: 'total',
|
|
118
|
-
value: 'id',
|
|
119
|
-
label: 'name',
|
|
120
|
-
}
|
|
121
|
-
if (!props.dataMapping) return defaultMapping
|
|
122
|
-
return {
|
|
123
|
-
list: props.dataMapping.list ?? defaultMapping.list,
|
|
124
|
-
total: props.dataMapping.total ?? defaultMapping.total,
|
|
125
|
-
value: props.dataMapping.value ?? defaultMapping.value,
|
|
126
|
-
label: props.dataMapping.label ?? defaultMapping.label,
|
|
127
|
-
}
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
const mergedPaginationConfig = computed<Required<PaginationConfig>>(() => {
|
|
131
|
-
const defaultPaginationConfig: Required<PaginationConfig> = {
|
|
132
|
-
pageField: 'pageNum',
|
|
133
|
-
pageSizeField: 'pageSize',
|
|
134
|
-
searchField: 'keyword',
|
|
135
|
-
searchContains: '',
|
|
136
|
-
}
|
|
137
|
-
if (!props.paginationConfig) return defaultPaginationConfig
|
|
138
|
-
return {
|
|
139
|
-
pageField: props.paginationConfig.pageField ?? defaultPaginationConfig.pageField,
|
|
140
|
-
pageSizeField: props.paginationConfig.pageSizeField ?? defaultPaginationConfig.pageSizeField,
|
|
141
|
-
searchField: props.paginationConfig.searchField ?? defaultPaginationConfig.searchField,
|
|
142
|
-
searchContains: props.paginationConfig.searchContains ?? defaultPaginationConfig.searchContains,
|
|
143
|
-
}
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
const requestParams = computed(() => {
|
|
147
|
-
const params: AnyObject = {}
|
|
148
|
-
if (props.pagination && dynamicPagination.value) {
|
|
149
|
-
params[mergedPaginationConfig.value.pageField] = currentPage.value
|
|
150
|
-
params[mergedPaginationConfig.value.pageSizeField] = props.pageSize
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const allParams = {
|
|
154
|
-
...props.extraParams,
|
|
155
|
-
...props.params,
|
|
156
|
-
...seekParams,
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (searchKeyword.value && mergedPaginationConfig.value.searchField) {
|
|
160
|
-
allParams[mergedPaginationConfig.value.searchField] = searchKeyword.value
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
...params,
|
|
165
|
-
...allParams,
|
|
166
|
-
}
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* 获取嵌套路径值
|
|
171
|
-
* @param obj 源对象
|
|
172
|
-
* @param path 使用点号分隔的路径
|
|
173
|
-
*/
|
|
174
|
-
const getNestedValue = (obj: unknown, path: string): any => {
|
|
175
|
-
const source = (obj ?? {}) as AnyObject
|
|
176
|
-
return path.split('.').reduce((current: AnyObject, key: string) => current?.[key], source)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const getOptionValue = (item: any) => {
|
|
180
|
-
return getNestedValue(item, mergedDataMapping.value.value)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const getOptionLabel = (item: any) => {
|
|
184
|
-
return getNestedValue(item, mergedDataMapping.value.label)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* 处理后端响应结构,统一抽取列表与总数,并判断是否需要关闭分页
|
|
189
|
-
* @param data 响应数据
|
|
190
|
-
*/
|
|
191
|
-
const processResponseData = (data: unknown): ResponseProcessResult => {
|
|
192
|
-
if (!data || typeof data !== 'object') {
|
|
193
|
-
return { listData: [], totalCount: 0, shouldDisablePagination: true }
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if ('code' in data && 'data' in data) {
|
|
197
|
-
const actualData = data.data
|
|
198
|
-
if (actualData && typeof actualData === 'object' && 'list' in actualData) {
|
|
199
|
-
const listData = actualData.list
|
|
200
|
-
if (Array.isArray(listData) && listData.length > 0) {
|
|
201
|
-
const totalCount = mergedDataMapping.value.total
|
|
202
|
-
? getNestedValue(actualData, mergedDataMapping.value.total)
|
|
203
|
-
: listData.length
|
|
204
|
-
return { listData, totalCount, shouldDisablePagination: false }
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (Array.isArray(actualData)) {
|
|
209
|
-
return { listData: actualData, totalCount: actualData.length, shouldDisablePagination: true }
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (actualData && typeof actualData === 'object') {
|
|
213
|
-
const listData = mergedDataMapping.value.list
|
|
214
|
-
? getNestedValue(actualData, mergedDataMapping.value.list)
|
|
215
|
-
: actualData
|
|
216
|
-
if (Array.isArray(listData)) {
|
|
217
|
-
const totalCount = mergedDataMapping.value.total
|
|
218
|
-
? getNestedValue(actualData, mergedDataMapping.value.total)
|
|
219
|
-
: listData.length
|
|
220
|
-
const shouldDisablePagination =
|
|
221
|
-
!mergedDataMapping.value.list || mergedDataMapping.value.list === 'list'
|
|
222
|
-
return { listData, totalCount, shouldDisablePagination }
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return { listData: [], totalCount: 0, shouldDisablePagination: true }
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if ('list' in data) {
|
|
230
|
-
const listData = data.list
|
|
231
|
-
if (Array.isArray(listData) && listData.length > 0) {
|
|
232
|
-
const totalCount = mergedDataMapping.value.total
|
|
233
|
-
? getNestedValue(data, mergedDataMapping.value.total)
|
|
234
|
-
: listData.length
|
|
235
|
-
return { listData, totalCount, shouldDisablePagination: false }
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (Array.isArray(data)) {
|
|
240
|
-
return { listData: data, totalCount: data.length, shouldDisablePagination: true }
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const listData = mergedDataMapping.value.list
|
|
244
|
-
? getNestedValue(data, mergedDataMapping.value.list)
|
|
245
|
-
: data
|
|
246
|
-
if (Array.isArray(listData)) {
|
|
247
|
-
const totalCount = mergedDataMapping.value.total
|
|
248
|
-
? getNestedValue(data, mergedDataMapping.value.total)
|
|
249
|
-
: listData.length
|
|
250
|
-
const shouldDisablePagination =
|
|
251
|
-
!mergedDataMapping.value.list || mergedDataMapping.value.list === 'list'
|
|
252
|
-
return { listData, totalCount, shouldDisablePagination }
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return { listData: [], totalCount: 0, shouldDisablePagination: true }
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* 统一兼容 axios 风格与直返数据风格
|
|
260
|
-
* @param response 请求返回值或 axios 响应对象
|
|
261
|
-
*/
|
|
262
|
-
const normalizeRequestData = (response: unknown): unknown => {
|
|
263
|
-
if (response && typeof response === 'object' && 'data' in response) {
|
|
264
|
-
return (response as { data: unknown }).data
|
|
265
|
-
}
|
|
266
|
-
return response
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* 发起请求并刷新选项
|
|
271
|
-
*/
|
|
272
|
-
const fetchData = async (): Promise<void> => {
|
|
273
|
-
if (!props.api.url) return
|
|
274
|
-
if (!props.api.request || typeof props.api.request !== 'function') {
|
|
275
|
-
options.value = []
|
|
276
|
-
total.value = 0
|
|
277
|
-
dynamicPagination.value = true
|
|
278
|
-
loading.value = false
|
|
279
|
-
console.error('SSelectPage: api.request 未提供,无法发起请求')
|
|
280
|
-
return
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const requestId = ++lastRequestId
|
|
284
|
-
loading.value = true
|
|
285
|
-
try {
|
|
286
|
-
const method = props.api.method || 'GET'
|
|
287
|
-
const request = props.api.request
|
|
288
|
-
const payload: RequestPayload = {
|
|
289
|
-
method,
|
|
290
|
-
url: props.api.url,
|
|
291
|
-
headers: props.api.headers || {},
|
|
292
|
-
params: { ...requestParams.value, ...props.api.params },
|
|
293
|
-
data: { ...requestParams.value, ...props.api.data },
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const response = await request(payload)
|
|
297
|
-
|
|
298
|
-
let data = normalizeRequestData(response)
|
|
299
|
-
if (props.transformData) {
|
|
300
|
-
data = props.transformData(data)
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const { listData, totalCount, shouldDisablePagination } = processResponseData(data)
|
|
304
|
-
if (requestId !== lastRequestId) return
|
|
305
|
-
|
|
306
|
-
options.value = listData
|
|
307
|
-
total.value = totalCount
|
|
308
|
-
dynamicPagination.value = !shouldDisablePagination
|
|
309
|
-
if (shouldDisablePagination) {
|
|
310
|
-
currentPage.value = 1
|
|
311
|
-
}
|
|
312
|
-
} catch (error) {
|
|
313
|
-
if (requestId !== lastRequestId) return
|
|
314
|
-
options.value = []
|
|
315
|
-
total.value = 0
|
|
316
|
-
dynamicPagination.value = true
|
|
317
|
-
console.error('SSelectPage 数据加载失败:', error)
|
|
318
|
-
} finally {
|
|
319
|
-
if (requestId === lastRequestId) {
|
|
320
|
-
loading.value = false
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
watch(
|
|
326
|
-
() => props.value,
|
|
327
|
-
(newValue) => {
|
|
328
|
-
let hasRequestedByValue = false
|
|
329
|
-
const canBackfillBySingleValue =
|
|
330
|
-
newValue !== undefined &&
|
|
331
|
-
newValue !== null &&
|
|
332
|
-
!Array.isArray(newValue) &&
|
|
333
|
-
typeof newValue !== 'object'
|
|
334
|
-
|
|
335
|
-
if (canBackfillBySingleValue && newValue !== selectedValue.value) {
|
|
336
|
-
const exists = Array.isArray(options.value)
|
|
337
|
-
? options.value.some((item) => getOptionValue(item) === newValue)
|
|
338
|
-
: false
|
|
339
|
-
if (!exists) {
|
|
340
|
-
hasRequestedByValue = true
|
|
341
|
-
seekParams[`${mergedDataMapping.value.value}`] = newValue
|
|
342
|
-
fetchData().finally(() => {
|
|
343
|
-
Object.keys(seekParams).forEach((key) => {
|
|
344
|
-
delete seekParams[key]
|
|
345
|
-
})
|
|
346
|
-
})
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (!hasRequestedByValue && props.autoLoad && (!options.value || options.value.length === 0)) {
|
|
351
|
-
fetchData()
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (
|
|
355
|
-
(props.mode === 'multiple' || props.mode === 'tags') &&
|
|
356
|
-
(newValue === null || newValue === undefined)
|
|
357
|
-
) {
|
|
358
|
-
selectedValue.value = []
|
|
359
|
-
} else {
|
|
360
|
-
selectedValue.value = newValue || undefined
|
|
361
|
-
}
|
|
362
|
-
},
|
|
363
|
-
{ immediate: true },
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* 分页变更
|
|
368
|
-
*/
|
|
369
|
-
const handlePageChange = (page: number): void => {
|
|
370
|
-
currentPage.value = page
|
|
371
|
-
fetchData()
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
watch(
|
|
375
|
-
() => props.extraParams,
|
|
376
|
-
(newParams, oldParams) => {
|
|
377
|
-
if (!isEqual(newParams, oldParams)) {
|
|
378
|
-
currentPage.value = 1
|
|
379
|
-
fetchData()
|
|
380
|
-
}
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
deep: true,
|
|
384
|
-
immediate: false,
|
|
385
|
-
},
|
|
386
|
-
)
|
|
387
|
-
|
|
388
|
-
watch(
|
|
389
|
-
() => props.params,
|
|
390
|
-
(newParams, oldParams) => {
|
|
391
|
-
if (!isEqual(newParams, oldParams)) {
|
|
392
|
-
currentPage.value = 1
|
|
393
|
-
fetchData()
|
|
394
|
-
}
|
|
395
|
-
},
|
|
396
|
-
{
|
|
397
|
-
deep: true,
|
|
398
|
-
immediate: false,
|
|
399
|
-
},
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* 防抖搜索
|
|
404
|
-
*/
|
|
405
|
-
const debouncedSearch = debounce((keyword: string) => {
|
|
406
|
-
searchKeyword.value = keyword
|
|
407
|
-
currentPage.value = 1
|
|
408
|
-
isSearching.value = true
|
|
409
|
-
fetchData().finally(() => {
|
|
410
|
-
isSearching.value = false
|
|
411
|
-
})
|
|
412
|
-
}, props.searchDelay)
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* 搜索事件
|
|
416
|
-
*/
|
|
417
|
-
const handleSearch = (value: string): void => {
|
|
418
|
-
emit('search', value)
|
|
419
|
-
debouncedSearch(value)
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* 清空选择与搜索
|
|
424
|
-
*/
|
|
425
|
-
const handleClear = (): void => {
|
|
426
|
-
if (props.mode === 'multiple' || props.mode === 'tags') {
|
|
427
|
-
selectedValue.value = []
|
|
428
|
-
emit('update:value', [])
|
|
429
|
-
} else {
|
|
430
|
-
selectedValue.value = undefined
|
|
431
|
-
emit('update:value', undefined)
|
|
432
|
-
}
|
|
433
|
-
searchKeyword.value = ''
|
|
434
|
-
currentPage.value = 1
|
|
435
|
-
fetchData()
|
|
436
|
-
emit('clear')
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* 下拉显隐
|
|
441
|
-
*/
|
|
442
|
-
const handleDropdownVisibleChange = (open: boolean): void => {
|
|
443
|
-
if (open && options.value.length === 0 && props.autoLoad) {
|
|
444
|
-
fetchData()
|
|
445
|
-
}
|
|
446
|
-
if (!open && !isSearching.value) {
|
|
447
|
-
searchKeyword.value = ''
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* 聚焦事件
|
|
453
|
-
*/
|
|
454
|
-
const handleFocus = (): void => {
|
|
455
|
-
if (searchKeyword.value && selectRef.value) {
|
|
456
|
-
nextTick(() => {
|
|
457
|
-
if (selectRef.value && typeof selectRef.value.focus === 'function') {
|
|
458
|
-
selectRef.value.focus()
|
|
459
|
-
}
|
|
460
|
-
})
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* 值变更并组装回传对象
|
|
466
|
-
*/
|
|
467
|
-
const handleChange = (value: SelectValue): void => {
|
|
468
|
-
selectedValue.value = value
|
|
469
|
-
|
|
470
|
-
let safeOption: unknown = null
|
|
471
|
-
let allOptions: unknown = null
|
|
472
|
-
if (Array.isArray(value)) {
|
|
473
|
-
safeOption = value
|
|
474
|
-
.map((val) => {
|
|
475
|
-
const opt = options.value.find((item) => getOptionValue(item) === val)
|
|
476
|
-
return opt
|
|
477
|
-
? {
|
|
478
|
-
[mergedDataMapping.value.value]: getOptionValue(opt),
|
|
479
|
-
[mergedDataMapping.value.label]: getOptionLabel(opt),
|
|
480
|
-
__raw: opt,
|
|
481
|
-
}
|
|
482
|
-
: null
|
|
483
|
-
})
|
|
484
|
-
.filter(Boolean)
|
|
485
|
-
allOptions = value
|
|
486
|
-
} else {
|
|
487
|
-
const opt = options.value.find((item) => getOptionValue(item) === value)
|
|
488
|
-
safeOption = opt
|
|
489
|
-
? {
|
|
490
|
-
[mergedDataMapping.value.value]: getOptionValue(opt),
|
|
491
|
-
[mergedDataMapping.value.label]: getOptionLabel(opt),
|
|
492
|
-
__raw: opt,
|
|
493
|
-
}
|
|
494
|
-
: null
|
|
495
|
-
allOptions = opt
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
emit('update:value', value)
|
|
499
|
-
nextTick(() => {
|
|
500
|
-
emit('change', value, safeOption, allOptions)
|
|
501
|
-
})
|
|
502
|
-
searchKeyword.value = ''
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
interface SelectEvents {
|
|
506
|
-
change: (value: SelectValue) => void
|
|
507
|
-
dropdownVisibleChange: (open: boolean) => void
|
|
508
|
-
clear: () => void
|
|
509
|
-
focus: () => void
|
|
510
|
-
search?: (value: string) => void
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* 选择组件事件映射
|
|
515
|
-
*/
|
|
516
|
-
const selectEvents = computed<SelectEvents>(() => {
|
|
517
|
-
const ev: SelectEvents = {
|
|
518
|
-
change: handleChange,
|
|
519
|
-
dropdownVisibleChange: handleDropdownVisibleChange,
|
|
520
|
-
clear: handleClear,
|
|
521
|
-
focus: handleFocus,
|
|
522
|
-
}
|
|
523
|
-
if (props.showSearch) ev.search = handleSearch
|
|
524
|
-
return ev
|
|
525
|
-
})
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* 重新加载并重置分页与搜索
|
|
529
|
-
*/
|
|
530
|
-
const reload = (): void => {
|
|
531
|
-
currentPage.value = 1
|
|
532
|
-
searchKeyword.value = ''
|
|
533
|
-
fetchData()
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// 用于渲染 dropdownRender 的原始 VNode
|
|
537
|
-
const VNodes = defineComponent({
|
|
538
|
-
name: 'VNodes',
|
|
539
|
-
props: {
|
|
540
|
-
vnodes: {
|
|
541
|
-
type: [Object, Array],
|
|
542
|
-
default: null,
|
|
543
|
-
},
|
|
544
|
-
},
|
|
545
|
-
setup(_props) {
|
|
546
|
-
return () => _props.vnodes as any
|
|
547
|
-
},
|
|
548
|
-
})
|
|
549
|
-
|
|
550
|
-
defineExpose<ExposedMethods>({
|
|
551
|
-
reload,
|
|
552
|
-
fetchData,
|
|
553
|
-
})
|
|
554
|
-
</script>
|
|
555
|
-
|
|
556
|
-
<template>
|
|
557
|
-
<Select
|
|
558
|
-
ref="selectRef"
|
|
559
|
-
v-bind="$attrs"
|
|
560
|
-
v-model:value="selectedValue"
|
|
561
|
-
:mode="mode"
|
|
562
|
-
:placeholder="placeholder"
|
|
563
|
-
:loading="loading"
|
|
564
|
-
:disabled="disabled"
|
|
565
|
-
:allow-clear="allowClear"
|
|
566
|
-
:show-search="showSearch"
|
|
567
|
-
:filter-option="false"
|
|
568
|
-
:max-tag-count="maxTagCount"
|
|
569
|
-
v-on="selectEvents"
|
|
570
|
-
>
|
|
571
|
-
<SelectOption
|
|
572
|
-
v-for="item in options"
|
|
573
|
-
:key="getOptionValue(item)"
|
|
574
|
-
:value="getOptionValue(item)"
|
|
575
|
-
:disabled="item.disabled"
|
|
576
|
-
>
|
|
577
|
-
{{ getOptionLabel(item) }}
|
|
578
|
-
</SelectOption>
|
|
579
|
-
<template #dropdownRender="{ menuNode }">
|
|
580
|
-
<div>
|
|
581
|
-
<VNodes :vnodes="menuNode" />
|
|
582
|
-
<template v-if="props.pagination && dynamicPagination && options.length > 0">
|
|
583
|
-
<Divider style="margin: 4px 0" />
|
|
584
|
-
<div style="padding: 8px; text-align: center">
|
|
585
|
-
<Pagination
|
|
586
|
-
:current="currentPage"
|
|
587
|
-
:total="total"
|
|
588
|
-
:page-size="pageSize"
|
|
589
|
-
size="small"
|
|
590
|
-
:show-size-changer="false"
|
|
591
|
-
:show-less-items="true"
|
|
592
|
-
@change="handlePageChange"
|
|
593
|
-
/>
|
|
594
|
-
</div>
|
|
595
|
-
</template>
|
|
596
|
-
</div>
|
|
597
|
-
</template>
|
|
598
|
-
</Select>
|
|
599
|
-
</template>
|
|
600
|
-
|
|
601
|
-
<style lang="scss" scoped>
|
|
602
|
-
:deep(.ant-select-dropdown) {
|
|
603
|
-
.ant-pagination {
|
|
604
|
-
margin: 0;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
</style>
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { Divider, Pagination, Select, SelectOption } from '@antsoo-lib/components'
|
|
3
|
+
import type { SelectProps } from '@antsoo-lib/components'
|
|
4
|
+
import type { AnyObject } from '@antsoo-lib/shared'
|
|
5
|
+
import { debounce, isEqual } from 'lodash-es'
|
|
6
|
+
|
|
7
|
+
import { computed, defineComponent, nextTick, reactive, ref, watch } from 'vue'
|
|
8
|
+
|
|
9
|
+
// 请求基础配置
|
|
10
|
+
interface RequestConfig {
|
|
11
|
+
method?: 'GET' | 'POST'
|
|
12
|
+
url: string
|
|
13
|
+
headers?: Record<string, string>
|
|
14
|
+
params?: AnyObject
|
|
15
|
+
data?: AnyObject
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 外部注入请求函数类型
|
|
19
|
+
type Requester = (
|
|
20
|
+
config: Required<Pick<RequestConfig, 'method' | 'url'>> & RequestConfig,
|
|
21
|
+
) => Promise<any>
|
|
22
|
+
|
|
23
|
+
// 组件 API 配置
|
|
24
|
+
interface ApiConfig extends RequestConfig {
|
|
25
|
+
request?: Requester
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 数据映射配置
|
|
29
|
+
interface DataMapping {
|
|
30
|
+
list?: string
|
|
31
|
+
total?: string
|
|
32
|
+
value?: string
|
|
33
|
+
label?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 分页配置
|
|
37
|
+
interface PaginationConfig {
|
|
38
|
+
pageField?: string
|
|
39
|
+
pageSizeField?: string
|
|
40
|
+
searchField?: string
|
|
41
|
+
searchContains?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 组件 Props
|
|
45
|
+
interface Props extends /* @vue-ignore */ Omit<SelectProps, 'options' | 'loading'> {
|
|
46
|
+
placeholder?: string
|
|
47
|
+
allowClear?: boolean
|
|
48
|
+
showSearch?: boolean
|
|
49
|
+
disabled?: boolean
|
|
50
|
+
maxTagCount?: 'responsive' | number
|
|
51
|
+
size?: 'large' | 'middle' | 'small'
|
|
52
|
+
mode?: 'multiple' | 'tags'
|
|
53
|
+
value?: any
|
|
54
|
+
api: ApiConfig
|
|
55
|
+
dataMapping?: DataMapping
|
|
56
|
+
pagination?: boolean
|
|
57
|
+
paginationConfig?: PaginationConfig
|
|
58
|
+
pageSize?: number
|
|
59
|
+
searchDelay?: number
|
|
60
|
+
extraParams?: AnyObject
|
|
61
|
+
params?: AnyObject
|
|
62
|
+
transformData?: (data: any) => any
|
|
63
|
+
autoLoad?: boolean
|
|
64
|
+
attr?: Record<string, string>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type OptionItem = Record<string, unknown> & { disabled?: boolean }
|
|
68
|
+
type SelectValue = SelectProps['value']
|
|
69
|
+
type RequestPayload = Required<Pick<RequestConfig, 'method' | 'url'>> & RequestConfig
|
|
70
|
+
interface ResponseProcessResult {
|
|
71
|
+
listData: OptionItem[]
|
|
72
|
+
totalCount: number
|
|
73
|
+
shouldDisablePagination: boolean
|
|
74
|
+
}
|
|
75
|
+
interface ExposedMethods {
|
|
76
|
+
reload: () => void
|
|
77
|
+
fetchData: () => Promise<void> | void
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
81
|
+
pagination: true,
|
|
82
|
+
pageSize: 10,
|
|
83
|
+
searchDelay: 300,
|
|
84
|
+
extraParams: () => ({}),
|
|
85
|
+
params: () => ({}),
|
|
86
|
+
autoLoad: true,
|
|
87
|
+
allowClear: true,
|
|
88
|
+
showSearch: true,
|
|
89
|
+
placeholder: '请选择',
|
|
90
|
+
maxTagCount: 1,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const emit = defineEmits<{
|
|
94
|
+
change: [value: any, option: any, allOptions: any]
|
|
95
|
+
search: [value: string]
|
|
96
|
+
clear: []
|
|
97
|
+
'update:value': [value: any]
|
|
98
|
+
}>()
|
|
99
|
+
|
|
100
|
+
const selectedValue = ref<SelectValue>()
|
|
101
|
+
const loading = ref(false)
|
|
102
|
+
const options = ref<OptionItem[]>([])
|
|
103
|
+
const currentPage = ref(1)
|
|
104
|
+
const total = ref(0)
|
|
105
|
+
const searchKeyword = ref('')
|
|
106
|
+
const selectRef = ref<{ focus?: () => void } | null>(null)
|
|
107
|
+
const isSearching = ref(false)
|
|
108
|
+
const dynamicPagination = ref(true)
|
|
109
|
+
let lastRequestId = 0
|
|
110
|
+
|
|
111
|
+
// 临时查询参数(用于回显时按 value 拉取选项)
|
|
112
|
+
const seekParams = reactive<AnyObject>({})
|
|
113
|
+
|
|
114
|
+
const mergedDataMapping = computed<Required<DataMapping>>(() => {
|
|
115
|
+
const defaultMapping: Required<DataMapping> = {
|
|
116
|
+
list: 'list',
|
|
117
|
+
total: 'total',
|
|
118
|
+
value: 'id',
|
|
119
|
+
label: 'name',
|
|
120
|
+
}
|
|
121
|
+
if (!props.dataMapping) return defaultMapping
|
|
122
|
+
return {
|
|
123
|
+
list: props.dataMapping.list ?? defaultMapping.list,
|
|
124
|
+
total: props.dataMapping.total ?? defaultMapping.total,
|
|
125
|
+
value: props.dataMapping.value ?? defaultMapping.value,
|
|
126
|
+
label: props.dataMapping.label ?? defaultMapping.label,
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const mergedPaginationConfig = computed<Required<PaginationConfig>>(() => {
|
|
131
|
+
const defaultPaginationConfig: Required<PaginationConfig> = {
|
|
132
|
+
pageField: 'pageNum',
|
|
133
|
+
pageSizeField: 'pageSize',
|
|
134
|
+
searchField: 'keyword',
|
|
135
|
+
searchContains: '',
|
|
136
|
+
}
|
|
137
|
+
if (!props.paginationConfig) return defaultPaginationConfig
|
|
138
|
+
return {
|
|
139
|
+
pageField: props.paginationConfig.pageField ?? defaultPaginationConfig.pageField,
|
|
140
|
+
pageSizeField: props.paginationConfig.pageSizeField ?? defaultPaginationConfig.pageSizeField,
|
|
141
|
+
searchField: props.paginationConfig.searchField ?? defaultPaginationConfig.searchField,
|
|
142
|
+
searchContains: props.paginationConfig.searchContains ?? defaultPaginationConfig.searchContains,
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const requestParams = computed(() => {
|
|
147
|
+
const params: AnyObject = {}
|
|
148
|
+
if (props.pagination && dynamicPagination.value) {
|
|
149
|
+
params[mergedPaginationConfig.value.pageField] = currentPage.value
|
|
150
|
+
params[mergedPaginationConfig.value.pageSizeField] = props.pageSize
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const allParams = {
|
|
154
|
+
...props.extraParams,
|
|
155
|
+
...props.params,
|
|
156
|
+
...seekParams,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (searchKeyword.value && mergedPaginationConfig.value.searchField) {
|
|
160
|
+
allParams[mergedPaginationConfig.value.searchField] = searchKeyword.value
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
...params,
|
|
165
|
+
...allParams,
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 获取嵌套路径值
|
|
171
|
+
* @param obj 源对象
|
|
172
|
+
* @param path 使用点号分隔的路径
|
|
173
|
+
*/
|
|
174
|
+
const getNestedValue = (obj: unknown, path: string): any => {
|
|
175
|
+
const source = (obj ?? {}) as AnyObject
|
|
176
|
+
return path.split('.').reduce((current: AnyObject, key: string) => current?.[key], source)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const getOptionValue = (item: any) => {
|
|
180
|
+
return getNestedValue(item, mergedDataMapping.value.value)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const getOptionLabel = (item: any) => {
|
|
184
|
+
return getNestedValue(item, mergedDataMapping.value.label)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 处理后端响应结构,统一抽取列表与总数,并判断是否需要关闭分页
|
|
189
|
+
* @param data 响应数据
|
|
190
|
+
*/
|
|
191
|
+
const processResponseData = (data: unknown): ResponseProcessResult => {
|
|
192
|
+
if (!data || typeof data !== 'object') {
|
|
193
|
+
return { listData: [], totalCount: 0, shouldDisablePagination: true }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if ('code' in data && 'data' in data) {
|
|
197
|
+
const actualData = data.data
|
|
198
|
+
if (actualData && typeof actualData === 'object' && 'list' in actualData) {
|
|
199
|
+
const listData = actualData.list
|
|
200
|
+
if (Array.isArray(listData) && listData.length > 0) {
|
|
201
|
+
const totalCount = mergedDataMapping.value.total
|
|
202
|
+
? getNestedValue(actualData, mergedDataMapping.value.total)
|
|
203
|
+
: listData.length
|
|
204
|
+
return { listData, totalCount, shouldDisablePagination: false }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (Array.isArray(actualData)) {
|
|
209
|
+
return { listData: actualData, totalCount: actualData.length, shouldDisablePagination: true }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (actualData && typeof actualData === 'object') {
|
|
213
|
+
const listData = mergedDataMapping.value.list
|
|
214
|
+
? getNestedValue(actualData, mergedDataMapping.value.list)
|
|
215
|
+
: actualData
|
|
216
|
+
if (Array.isArray(listData)) {
|
|
217
|
+
const totalCount = mergedDataMapping.value.total
|
|
218
|
+
? getNestedValue(actualData, mergedDataMapping.value.total)
|
|
219
|
+
: listData.length
|
|
220
|
+
const shouldDisablePagination =
|
|
221
|
+
!mergedDataMapping.value.list || mergedDataMapping.value.list === 'list'
|
|
222
|
+
return { listData, totalCount, shouldDisablePagination }
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { listData: [], totalCount: 0, shouldDisablePagination: true }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if ('list' in data) {
|
|
230
|
+
const listData = data.list
|
|
231
|
+
if (Array.isArray(listData) && listData.length > 0) {
|
|
232
|
+
const totalCount = mergedDataMapping.value.total
|
|
233
|
+
? getNestedValue(data, mergedDataMapping.value.total)
|
|
234
|
+
: listData.length
|
|
235
|
+
return { listData, totalCount, shouldDisablePagination: false }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (Array.isArray(data)) {
|
|
240
|
+
return { listData: data, totalCount: data.length, shouldDisablePagination: true }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const listData = mergedDataMapping.value.list
|
|
244
|
+
? getNestedValue(data, mergedDataMapping.value.list)
|
|
245
|
+
: data
|
|
246
|
+
if (Array.isArray(listData)) {
|
|
247
|
+
const totalCount = mergedDataMapping.value.total
|
|
248
|
+
? getNestedValue(data, mergedDataMapping.value.total)
|
|
249
|
+
: listData.length
|
|
250
|
+
const shouldDisablePagination =
|
|
251
|
+
!mergedDataMapping.value.list || mergedDataMapping.value.list === 'list'
|
|
252
|
+
return { listData, totalCount, shouldDisablePagination }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { listData: [], totalCount: 0, shouldDisablePagination: true }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 统一兼容 axios 风格与直返数据风格
|
|
260
|
+
* @param response 请求返回值或 axios 响应对象
|
|
261
|
+
*/
|
|
262
|
+
const normalizeRequestData = (response: unknown): unknown => {
|
|
263
|
+
if (response && typeof response === 'object' && 'data' in response) {
|
|
264
|
+
return (response as { data: unknown }).data
|
|
265
|
+
}
|
|
266
|
+
return response
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 发起请求并刷新选项
|
|
271
|
+
*/
|
|
272
|
+
const fetchData = async (): Promise<void> => {
|
|
273
|
+
if (!props.api.url) return
|
|
274
|
+
if (!props.api.request || typeof props.api.request !== 'function') {
|
|
275
|
+
options.value = []
|
|
276
|
+
total.value = 0
|
|
277
|
+
dynamicPagination.value = true
|
|
278
|
+
loading.value = false
|
|
279
|
+
console.error('SSelectPage: api.request 未提供,无法发起请求')
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const requestId = ++lastRequestId
|
|
284
|
+
loading.value = true
|
|
285
|
+
try {
|
|
286
|
+
const method = props.api.method || 'GET'
|
|
287
|
+
const request = props.api.request
|
|
288
|
+
const payload: RequestPayload = {
|
|
289
|
+
method,
|
|
290
|
+
url: props.api.url,
|
|
291
|
+
headers: props.api.headers || {},
|
|
292
|
+
params: { ...requestParams.value, ...props.api.params },
|
|
293
|
+
data: { ...requestParams.value, ...props.api.data },
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const response = await request(payload)
|
|
297
|
+
|
|
298
|
+
let data = normalizeRequestData(response)
|
|
299
|
+
if (props.transformData) {
|
|
300
|
+
data = props.transformData(data)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const { listData, totalCount, shouldDisablePagination } = processResponseData(data)
|
|
304
|
+
if (requestId !== lastRequestId) return
|
|
305
|
+
|
|
306
|
+
options.value = listData
|
|
307
|
+
total.value = totalCount
|
|
308
|
+
dynamicPagination.value = !shouldDisablePagination
|
|
309
|
+
if (shouldDisablePagination) {
|
|
310
|
+
currentPage.value = 1
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
if (requestId !== lastRequestId) return
|
|
314
|
+
options.value = []
|
|
315
|
+
total.value = 0
|
|
316
|
+
dynamicPagination.value = true
|
|
317
|
+
console.error('SSelectPage 数据加载失败:', error)
|
|
318
|
+
} finally {
|
|
319
|
+
if (requestId === lastRequestId) {
|
|
320
|
+
loading.value = false
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
watch(
|
|
326
|
+
() => props.value,
|
|
327
|
+
(newValue) => {
|
|
328
|
+
let hasRequestedByValue = false
|
|
329
|
+
const canBackfillBySingleValue =
|
|
330
|
+
newValue !== undefined &&
|
|
331
|
+
newValue !== null &&
|
|
332
|
+
!Array.isArray(newValue) &&
|
|
333
|
+
typeof newValue !== 'object'
|
|
334
|
+
|
|
335
|
+
if (canBackfillBySingleValue && newValue !== selectedValue.value) {
|
|
336
|
+
const exists = Array.isArray(options.value)
|
|
337
|
+
? options.value.some((item) => getOptionValue(item) === newValue)
|
|
338
|
+
: false
|
|
339
|
+
if (!exists) {
|
|
340
|
+
hasRequestedByValue = true
|
|
341
|
+
seekParams[`${mergedDataMapping.value.value}`] = newValue
|
|
342
|
+
fetchData().finally(() => {
|
|
343
|
+
Object.keys(seekParams).forEach((key) => {
|
|
344
|
+
delete seekParams[key]
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!hasRequestedByValue && props.autoLoad && (!options.value || options.value.length === 0)) {
|
|
351
|
+
fetchData()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (
|
|
355
|
+
(props.mode === 'multiple' || props.mode === 'tags') &&
|
|
356
|
+
(newValue === null || newValue === undefined)
|
|
357
|
+
) {
|
|
358
|
+
selectedValue.value = []
|
|
359
|
+
} else {
|
|
360
|
+
selectedValue.value = newValue || undefined
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
{ immediate: true },
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* 分页变更
|
|
368
|
+
*/
|
|
369
|
+
const handlePageChange = (page: number): void => {
|
|
370
|
+
currentPage.value = page
|
|
371
|
+
fetchData()
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
watch(
|
|
375
|
+
() => props.extraParams,
|
|
376
|
+
(newParams, oldParams) => {
|
|
377
|
+
if (!isEqual(newParams, oldParams)) {
|
|
378
|
+
currentPage.value = 1
|
|
379
|
+
fetchData()
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
deep: true,
|
|
384
|
+
immediate: false,
|
|
385
|
+
},
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
watch(
|
|
389
|
+
() => props.params,
|
|
390
|
+
(newParams, oldParams) => {
|
|
391
|
+
if (!isEqual(newParams, oldParams)) {
|
|
392
|
+
currentPage.value = 1
|
|
393
|
+
fetchData()
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
deep: true,
|
|
398
|
+
immediate: false,
|
|
399
|
+
},
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* 防抖搜索
|
|
404
|
+
*/
|
|
405
|
+
const debouncedSearch = debounce((keyword: string) => {
|
|
406
|
+
searchKeyword.value = keyword
|
|
407
|
+
currentPage.value = 1
|
|
408
|
+
isSearching.value = true
|
|
409
|
+
fetchData().finally(() => {
|
|
410
|
+
isSearching.value = false
|
|
411
|
+
})
|
|
412
|
+
}, props.searchDelay)
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* 搜索事件
|
|
416
|
+
*/
|
|
417
|
+
const handleSearch = (value: string): void => {
|
|
418
|
+
emit('search', value)
|
|
419
|
+
debouncedSearch(value)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* 清空选择与搜索
|
|
424
|
+
*/
|
|
425
|
+
const handleClear = (): void => {
|
|
426
|
+
if (props.mode === 'multiple' || props.mode === 'tags') {
|
|
427
|
+
selectedValue.value = []
|
|
428
|
+
emit('update:value', [])
|
|
429
|
+
} else {
|
|
430
|
+
selectedValue.value = undefined
|
|
431
|
+
emit('update:value', undefined)
|
|
432
|
+
}
|
|
433
|
+
searchKeyword.value = ''
|
|
434
|
+
currentPage.value = 1
|
|
435
|
+
fetchData()
|
|
436
|
+
emit('clear')
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* 下拉显隐
|
|
441
|
+
*/
|
|
442
|
+
const handleDropdownVisibleChange = (open: boolean): void => {
|
|
443
|
+
if (open && options.value.length === 0 && props.autoLoad) {
|
|
444
|
+
fetchData()
|
|
445
|
+
}
|
|
446
|
+
if (!open && !isSearching.value) {
|
|
447
|
+
searchKeyword.value = ''
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* 聚焦事件
|
|
453
|
+
*/
|
|
454
|
+
const handleFocus = (): void => {
|
|
455
|
+
if (searchKeyword.value && selectRef.value) {
|
|
456
|
+
nextTick(() => {
|
|
457
|
+
if (selectRef.value && typeof selectRef.value.focus === 'function') {
|
|
458
|
+
selectRef.value.focus()
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* 值变更并组装回传对象
|
|
466
|
+
*/
|
|
467
|
+
const handleChange = (value: SelectValue): void => {
|
|
468
|
+
selectedValue.value = value
|
|
469
|
+
|
|
470
|
+
let safeOption: unknown = null
|
|
471
|
+
let allOptions: unknown = null
|
|
472
|
+
if (Array.isArray(value)) {
|
|
473
|
+
safeOption = value
|
|
474
|
+
.map((val) => {
|
|
475
|
+
const opt = options.value.find((item) => getOptionValue(item) === val)
|
|
476
|
+
return opt
|
|
477
|
+
? {
|
|
478
|
+
[mergedDataMapping.value.value]: getOptionValue(opt),
|
|
479
|
+
[mergedDataMapping.value.label]: getOptionLabel(opt),
|
|
480
|
+
__raw: opt,
|
|
481
|
+
}
|
|
482
|
+
: null
|
|
483
|
+
})
|
|
484
|
+
.filter(Boolean)
|
|
485
|
+
allOptions = value
|
|
486
|
+
} else {
|
|
487
|
+
const opt = options.value.find((item) => getOptionValue(item) === value)
|
|
488
|
+
safeOption = opt
|
|
489
|
+
? {
|
|
490
|
+
[mergedDataMapping.value.value]: getOptionValue(opt),
|
|
491
|
+
[mergedDataMapping.value.label]: getOptionLabel(opt),
|
|
492
|
+
__raw: opt,
|
|
493
|
+
}
|
|
494
|
+
: null
|
|
495
|
+
allOptions = opt
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
emit('update:value', value)
|
|
499
|
+
nextTick(() => {
|
|
500
|
+
emit('change', value, safeOption, allOptions)
|
|
501
|
+
})
|
|
502
|
+
searchKeyword.value = ''
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
interface SelectEvents {
|
|
506
|
+
change: (value: SelectValue) => void
|
|
507
|
+
dropdownVisibleChange: (open: boolean) => void
|
|
508
|
+
clear: () => void
|
|
509
|
+
focus: () => void
|
|
510
|
+
search?: (value: string) => void
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* 选择组件事件映射
|
|
515
|
+
*/
|
|
516
|
+
const selectEvents = computed<SelectEvents>(() => {
|
|
517
|
+
const ev: SelectEvents = {
|
|
518
|
+
change: handleChange,
|
|
519
|
+
dropdownVisibleChange: handleDropdownVisibleChange,
|
|
520
|
+
clear: handleClear,
|
|
521
|
+
focus: handleFocus,
|
|
522
|
+
}
|
|
523
|
+
if (props.showSearch) ev.search = handleSearch
|
|
524
|
+
return ev
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* 重新加载并重置分页与搜索
|
|
529
|
+
*/
|
|
530
|
+
const reload = (): void => {
|
|
531
|
+
currentPage.value = 1
|
|
532
|
+
searchKeyword.value = ''
|
|
533
|
+
fetchData()
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// 用于渲染 dropdownRender 的原始 VNode
|
|
537
|
+
const VNodes = defineComponent({
|
|
538
|
+
name: 'VNodes',
|
|
539
|
+
props: {
|
|
540
|
+
vnodes: {
|
|
541
|
+
type: [Object, Array],
|
|
542
|
+
default: null,
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
setup(_props) {
|
|
546
|
+
return () => _props.vnodes as any
|
|
547
|
+
},
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
defineExpose<ExposedMethods>({
|
|
551
|
+
reload,
|
|
552
|
+
fetchData,
|
|
553
|
+
})
|
|
554
|
+
</script>
|
|
555
|
+
|
|
556
|
+
<template>
|
|
557
|
+
<Select
|
|
558
|
+
ref="selectRef"
|
|
559
|
+
v-bind="$attrs"
|
|
560
|
+
v-model:value="selectedValue"
|
|
561
|
+
:mode="mode"
|
|
562
|
+
:placeholder="placeholder"
|
|
563
|
+
:loading="loading"
|
|
564
|
+
:disabled="disabled"
|
|
565
|
+
:allow-clear="allowClear"
|
|
566
|
+
:show-search="showSearch"
|
|
567
|
+
:filter-option="false"
|
|
568
|
+
:max-tag-count="maxTagCount"
|
|
569
|
+
v-on="selectEvents"
|
|
570
|
+
>
|
|
571
|
+
<SelectOption
|
|
572
|
+
v-for="item in options"
|
|
573
|
+
:key="getOptionValue(item)"
|
|
574
|
+
:value="getOptionValue(item)"
|
|
575
|
+
:disabled="item.disabled"
|
|
576
|
+
>
|
|
577
|
+
{{ getOptionLabel(item) }}
|
|
578
|
+
</SelectOption>
|
|
579
|
+
<template #dropdownRender="{ menuNode }">
|
|
580
|
+
<div>
|
|
581
|
+
<VNodes :vnodes="menuNode" />
|
|
582
|
+
<template v-if="props.pagination && dynamicPagination && options.length > 0">
|
|
583
|
+
<Divider style="margin: 4px 0" />
|
|
584
|
+
<div style="padding: 8px; text-align: center">
|
|
585
|
+
<Pagination
|
|
586
|
+
:current="currentPage"
|
|
587
|
+
:total="total"
|
|
588
|
+
:page-size="pageSize"
|
|
589
|
+
size="small"
|
|
590
|
+
:show-size-changer="false"
|
|
591
|
+
:show-less-items="true"
|
|
592
|
+
@change="handlePageChange"
|
|
593
|
+
/>
|
|
594
|
+
</div>
|
|
595
|
+
</template>
|
|
596
|
+
</div>
|
|
597
|
+
</template>
|
|
598
|
+
</Select>
|
|
599
|
+
</template>
|
|
600
|
+
|
|
601
|
+
<style lang="scss" scoped>
|
|
602
|
+
:deep(.ant-select-dropdown) {
|
|
603
|
+
.ant-pagination {
|
|
604
|
+
margin: 0;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
</style>
|