@antsoo-lib/core 1.0.17 → 2.0.2

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.
Files changed (77) hide show
  1. package/.turbo/turbo-build.log +0 -0
  2. package/CHANGELOG.md +16 -0
  3. package/dist/core.css +1 -1
  4. package/dist/index.cjs +4 -8
  5. package/dist/index.js +2367 -49071
  6. package/dist/types/BaseSearch/index.d.ts +59 -0
  7. package/dist/types/{core/src/BaseTable → BaseTable}/index.d.ts +20 -6
  8. package/dist/types/Form/CoreForm.d.ts +82 -0
  9. package/dist/types/Form/types.d.ts +57 -0
  10. package/dist/types/SSelectPage/index.d.ts +102 -0
  11. package/dist/types/index.d.ts +13 -0
  12. package/dist/types/{core/src/BaseTable/renderers → render}/AreaCascader.d.ts +3 -3
  13. package/dist/types/{core/src/BaseTable/renderers → render}/AutoComplete.d.ts +2 -2
  14. package/dist/types/{core/src/BaseTable/renderers → render}/Button.d.ts +2 -2
  15. package/dist/types/{core/src/BaseTable/renderers → render}/Cascader.d.ts +3 -3
  16. package/dist/types/{core/src/BaseTable/renderers → render}/Checkbox.d.ts +2 -2
  17. package/dist/types/{core/src/BaseTable/renderers → render}/CheckboxGroup.d.ts +2 -2
  18. package/dist/types/render/Custom.d.ts +8 -0
  19. package/dist/types/{core/src/BaseTable/renderers → render}/DatePicker.d.ts +2 -2
  20. package/dist/types/{core/src/BaseTable/renderers → render}/Input.d.ts +2 -2
  21. package/dist/types/{core/src/BaseTable/renderers → render}/InputGroup.d.ts +3 -3
  22. package/dist/types/{core/src/BaseTable/renderers → render}/InputNumber.d.ts +2 -2
  23. package/dist/types/{core/src/BaseTable/renderers → render}/InputPassword.d.ts +2 -2
  24. package/dist/types/render/InputRange.d.ts +9 -0
  25. package/dist/types/{core/src/BaseTable/renderers → render}/RadioGroup.d.ts +2 -2
  26. package/dist/types/{core/src/BaseTable/renderers → render}/Select.d.ts +2 -2
  27. package/dist/types/{core/src/BaseTable/renderers → render}/SselectPage.d.ts +2 -2
  28. package/dist/types/{core/src/BaseTable/renderers → render}/Switch.d.ts +2 -2
  29. package/dist/types/render/Tree.d.ts +9 -0
  30. package/dist/types/{core/src/BaseTable/renderers → render}/TreeSelect.d.ts +2 -2
  31. package/dist/types/{core/src/BaseTable/renderers → render}/Upload.d.ts +2 -2
  32. package/dist/types/{core/src/BaseTable/helpers.d.ts → render/helper.d.ts} +2 -1
  33. package/dist/types/{core/src/BaseTable/renderers → render}/index.d.ts +24 -5
  34. package/dist/types/{core/src/BaseTable → render}/types.d.ts +48 -4
  35. package/dist/types/utils/attrMapping.d.ts +26 -0
  36. package/package.json +12 -15
  37. package/src/BaseSearch/index.vue +371 -0
  38. package/src/BaseTable/index.vue +48 -22
  39. package/src/Form/CoreForm.vue +782 -0
  40. package/src/Form/types.ts +86 -0
  41. package/src/SSelectPage/index.vue +607 -0
  42. package/src/index.ts +15 -1
  43. package/src/{BaseTable/renderers → render}/AreaCascader.tsx +3 -3
  44. package/src/{BaseTable/renderers → render}/AutoComplete.tsx +3 -3
  45. package/src/{BaseTable/renderers → render}/Button.tsx +2 -2
  46. package/src/{BaseTable/renderers → render}/Cascader.tsx +3 -3
  47. package/src/{BaseTable/renderers → render}/Checkbox.tsx +2 -2
  48. package/src/{BaseTable/renderers → render}/CheckboxGroup.tsx +2 -2
  49. package/src/render/Custom.tsx +19 -0
  50. package/src/{BaseTable/renderers → render}/DatePicker.tsx +2 -2
  51. package/src/{BaseTable/renderers → render}/Input.tsx +3 -3
  52. package/src/{BaseTable/renderers → render}/InputGroup.tsx +3 -3
  53. package/src/{BaseTable/renderers → render}/InputNumber.tsx +3 -3
  54. package/src/{BaseTable/renderers → render}/InputPassword.tsx +3 -3
  55. package/src/render/InputRange.tsx +154 -0
  56. package/src/{BaseTable/renderers → render}/RadioGroup.tsx +2 -2
  57. package/src/{BaseTable/renderers → render}/Select.tsx +2 -2
  58. package/src/{BaseTable/renderers → render}/SselectPage.tsx +3 -3
  59. package/src/{BaseTable/renderers → render}/Switch.tsx +2 -2
  60. package/src/render/Tree.tsx +136 -0
  61. package/src/{BaseTable/renderers → render}/TreeSelect.tsx +2 -2
  62. package/src/{BaseTable/renderers → render}/Upload.tsx +4 -5
  63. package/src/{BaseTable/utils.tsx → render/helper.tsx} +86 -9
  64. package/src/{BaseTable/renderers → render}/index.ts +45 -4
  65. package/src/{BaseTable → render}/types.ts +62 -2
  66. package/src/utils/attrMapping.ts +106 -0
  67. package/vite.config.ts +15 -2
  68. package/dist/types/core/index.d.ts +0 -6
  69. package/dist/types/core/src/BaseTable/utils.d.ts +0 -8
  70. package/dist/types/core/src/index.d.ts +0 -2
  71. package/index.css +0 -2
  72. package/index.ts +0 -21
  73. package/src/BaseTable/helpers.tsx +0 -91
  74. /package/dist/types/{core/src/BaseTable → render}/registry.d.ts +0 -0
  75. /package/dist/types/{core/src/BaseTable → render}/state.d.ts +0 -0
  76. /package/src/{BaseTable → render}/registry.ts +0 -0
  77. /package/src/{BaseTable → render}/state.ts +0 -0
@@ -0,0 +1,86 @@
1
+ import type { AnyObject } from '@antsoo-lib/shared'
2
+
3
+ import type { InjectionKey } from 'vue'
4
+
5
+ // 通用代码服务接口
6
+ export interface CommonCodeItem {
7
+ name: string
8
+ code: string | number
9
+ [key: string]: any
10
+ }
11
+
12
+ export interface CommonCodeService {
13
+ getCodesByType: (type: string) => CommonCodeItem[]
14
+ }
15
+
16
+ export const COMMON_CODE_SERVICE_KEY: InjectionKey<CommonCodeService> = Symbol('CommonCodeService')
17
+
18
+ // 校验规则类型定义
19
+ export interface ValidationRule {
20
+ required?: boolean
21
+ message?: string
22
+ pattern?: RegExp
23
+ min?: number
24
+ max?: number
25
+ len?: number
26
+ type?:
27
+ | 'string'
28
+ | 'number'
29
+ | 'boolean'
30
+ | 'method'
31
+ | 'regexp'
32
+ | 'integer'
33
+ | 'float'
34
+ | 'array'
35
+ | 'object'
36
+ | 'enum'
37
+ | 'date'
38
+ | 'url'
39
+ | 'hex'
40
+ | 'email'
41
+ validator?: (rule: any, value: any) => Promise<void> | void
42
+ trigger?: 'change' | 'blur' | ['change', 'blur']
43
+ }
44
+
45
+ // 字段接口定义
46
+ export interface FormField {
47
+ key?: string // 字段唯一标识(可选)
48
+ // 拆分/映射字段:
49
+ // 1) 通用:当组件返回值为数组时,提供 string[],按下标对应需要写入的字段
50
+ // 2) 地区选择器(areaCascader):支持 { ids: string[]; names: string[] } 形式,分别映射地区 id 与名称
51
+ attr?:
52
+ | string[]
53
+ | {
54
+ ids?: string[]
55
+ names?: string[]
56
+ [key: string]: any
57
+ }
58
+ type?: string // 组件类型
59
+ name?: string // 显示标签
60
+ span?: number // 栅格占位
61
+ hidden?: boolean // 是否隐藏
62
+ disabled?: boolean | ((formValues: AnyObject, toolbarState: any, field: FormField) => boolean) // 是否禁用
63
+ labelWidth?: number // 标签宽度
64
+ labelAlign?: 'left' | 'right' // 标签对齐方式
65
+ props?: AnyObject // 组件属性
66
+ events?: Record<string, (...args: any[]) => any> // 事件处理
67
+ beforeCreate?: boolean | ((field: FormField) => boolean)
68
+ required?: boolean // 是否必填
69
+ rules?: ValidationRule[] // 校验规则
70
+ textarea?: boolean // 是否为文本域
71
+ commonCode?: string // 数据字典类型
72
+ helpText?: string | false // 帮助提示
73
+ }
74
+
75
+ export interface BaseFormProps {
76
+ value: AnyObject
77
+ fields?: FormField[]
78
+ disabled?: boolean
79
+ labelWidth?: number
80
+ labelPosition?: 'horizontal' | 'vertical'
81
+ labelAlign?: 'left' | 'right'
82
+ inlineActions?: boolean
83
+ actionsSpan?: number
84
+ colon?: boolean
85
+ gutter?: number | [number, number]
86
+ }
@@ -0,0 +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>
package/src/index.ts CHANGED
@@ -1,3 +1,17 @@
1
+ import BaseSearch from './BaseSearch/index.vue'
1
2
  import BaseTable from './BaseTable/index.vue'
3
+ import CoreForm from './Form/CoreForm.vue'
4
+ import SSelectPage from './SSelectPage/index.vue'
5
+ import { registerRenderer } from './render'
6
+ import type { Renderer } from './render'
2
7
 
3
- export { BaseTable }
8
+ export { BaseSearch, BaseTable, CoreForm, registerRenderer, SSelectPage }
9
+ export type { Renderer }
10
+
11
+ export const version = '0.0.0'
12
+
13
+ export * from './render/registry'
14
+ export * from './render/types'
15
+ export * from './render/state'
16
+ export * from './render'
17
+ export * from './Form/types'