@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.
- package/.turbo/turbo-build.log +0 -0
- package/CHANGELOG.md +16 -0
- package/dist/core.css +1 -1
- package/dist/index.cjs +4 -8
- package/dist/index.js +2367 -49071
- package/dist/types/BaseSearch/index.d.ts +59 -0
- package/dist/types/{core/src/BaseTable → BaseTable}/index.d.ts +20 -6
- package/dist/types/Form/CoreForm.d.ts +82 -0
- package/dist/types/Form/types.d.ts +57 -0
- package/dist/types/SSelectPage/index.d.ts +102 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/{core/src/BaseTable/renderers → render}/AreaCascader.d.ts +3 -3
- package/dist/types/{core/src/BaseTable/renderers → render}/AutoComplete.d.ts +2 -2
- package/dist/types/{core/src/BaseTable/renderers → render}/Button.d.ts +2 -2
- package/dist/types/{core/src/BaseTable/renderers → render}/Cascader.d.ts +3 -3
- package/dist/types/{core/src/BaseTable/renderers → render}/Checkbox.d.ts +2 -2
- package/dist/types/{core/src/BaseTable/renderers → render}/CheckboxGroup.d.ts +2 -2
- package/dist/types/render/Custom.d.ts +8 -0
- package/dist/types/{core/src/BaseTable/renderers → render}/DatePicker.d.ts +2 -2
- package/dist/types/{core/src/BaseTable/renderers → render}/Input.d.ts +2 -2
- package/dist/types/{core/src/BaseTable/renderers → render}/InputGroup.d.ts +3 -3
- package/dist/types/{core/src/BaseTable/renderers → render}/InputNumber.d.ts +2 -2
- package/dist/types/{core/src/BaseTable/renderers → render}/InputPassword.d.ts +2 -2
- package/dist/types/render/InputRange.d.ts +9 -0
- package/dist/types/{core/src/BaseTable/renderers → render}/RadioGroup.d.ts +2 -2
- package/dist/types/{core/src/BaseTable/renderers → render}/Select.d.ts +2 -2
- package/dist/types/{core/src/BaseTable/renderers → render}/SselectPage.d.ts +2 -2
- package/dist/types/{core/src/BaseTable/renderers → render}/Switch.d.ts +2 -2
- package/dist/types/render/Tree.d.ts +9 -0
- package/dist/types/{core/src/BaseTable/renderers → render}/TreeSelect.d.ts +2 -2
- package/dist/types/{core/src/BaseTable/renderers → render}/Upload.d.ts +2 -2
- package/dist/types/{core/src/BaseTable/helpers.d.ts → render/helper.d.ts} +2 -1
- package/dist/types/{core/src/BaseTable/renderers → render}/index.d.ts +24 -5
- package/dist/types/{core/src/BaseTable → render}/types.d.ts +48 -4
- package/dist/types/utils/attrMapping.d.ts +26 -0
- package/package.json +12 -15
- package/src/BaseSearch/index.vue +371 -0
- package/src/BaseTable/index.vue +48 -22
- package/src/Form/CoreForm.vue +782 -0
- package/src/Form/types.ts +86 -0
- package/src/SSelectPage/index.vue +607 -0
- package/src/index.ts +15 -1
- package/src/{BaseTable/renderers → render}/AreaCascader.tsx +3 -3
- package/src/{BaseTable/renderers → render}/AutoComplete.tsx +3 -3
- package/src/{BaseTable/renderers → render}/Button.tsx +2 -2
- package/src/{BaseTable/renderers → render}/Cascader.tsx +3 -3
- package/src/{BaseTable/renderers → render}/Checkbox.tsx +2 -2
- package/src/{BaseTable/renderers → render}/CheckboxGroup.tsx +2 -2
- package/src/render/Custom.tsx +19 -0
- package/src/{BaseTable/renderers → render}/DatePicker.tsx +2 -2
- package/src/{BaseTable/renderers → render}/Input.tsx +3 -3
- package/src/{BaseTable/renderers → render}/InputGroup.tsx +3 -3
- package/src/{BaseTable/renderers → render}/InputNumber.tsx +3 -3
- package/src/{BaseTable/renderers → render}/InputPassword.tsx +3 -3
- package/src/render/InputRange.tsx +154 -0
- package/src/{BaseTable/renderers → render}/RadioGroup.tsx +2 -2
- package/src/{BaseTable/renderers → render}/Select.tsx +2 -2
- package/src/{BaseTable/renderers → render}/SselectPage.tsx +3 -3
- package/src/{BaseTable/renderers → render}/Switch.tsx +2 -2
- package/src/render/Tree.tsx +136 -0
- package/src/{BaseTable/renderers → render}/TreeSelect.tsx +2 -2
- package/src/{BaseTable/renderers → render}/Upload.tsx +4 -5
- package/src/{BaseTable/utils.tsx → render/helper.tsx} +86 -9
- package/src/{BaseTable/renderers → render}/index.ts +45 -4
- package/src/{BaseTable → render}/types.ts +62 -2
- package/src/utils/attrMapping.ts +106 -0
- package/vite.config.ts +15 -2
- package/dist/types/core/index.d.ts +0 -6
- package/dist/types/core/src/BaseTable/utils.d.ts +0 -8
- package/dist/types/core/src/index.d.ts +0 -2
- package/index.css +0 -2
- package/index.ts +0 -21
- package/src/BaseTable/helpers.tsx +0 -91
- /package/dist/types/{core/src/BaseTable → render}/registry.d.ts +0 -0
- /package/dist/types/{core/src/BaseTable → render}/state.d.ts +0 -0
- /package/src/{BaseTable → render}/registry.ts +0 -0
- /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'
|