@aspire-ui/element-component-pro 1.0.2 → 1.0.4
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/README.md +1 -0
- package/dist/ProForm/ApiSelect.vue.d.ts +67 -0
- package/dist/ProForm/ProForm.vue.d.ts +6 -1
- package/dist/ProForm/ProFormItem.vue.d.ts +4 -0
- package/dist/ProForm/TreeSelect.vue.d.ts +64 -0
- package/dist/ProTable/ProTable.vue.d.ts +1 -1
- package/dist/ProTable/index.d.ts +1 -1
- package/dist/ProTable/useProTable.d.ts +4 -2
- package/dist/element-component-pro.es.js +753 -527
- package/dist/element-component-pro.es.js.map +1 -1
- package/dist/element-component-pro.umd.js +2 -2
- package/dist/element-component-pro.umd.js.map +1 -1
- package/dist/index.d.ts +187 -167
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +15 -2
- package/package.json +1 -1
- package/src/ProForm/ApiSelect.vue +85 -0
- package/src/ProForm/ProForm.vue +10 -2
- package/src/ProForm/ProFormItem.vue +54 -1
- package/src/ProForm/TreeSelect.vue +272 -0
- package/src/ProTable/TableAction.vue +16 -5
- package/src/types/index.ts +28 -2
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.ecp-pro-table[data-v-c5638c20]{padding:16px;background:#fff;width:100%;box-sizing:border-box}.ecp-pro-table[data-v-c5638c20] .el-table{width:100%!important}.ecp-pro-table__header[data-v-c5638c20]{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.ecp-pro-table__title-wrapper[data-v-c5638c20]{display:flex;align-items:center;gap:4px}.ecp-pro-table__title[data-v-c5638c20]{font-size:16px;font-weight:600}.ecp-pro-table__help[data-v-c5638c20]{color:#909399;cursor:help}.ecp-pro-table__toolbar[data-v-c5638c20]{display:flex;align-items:center;gap:8px}.ecp-pro-table__body[data-v-c5638c20]{width:100%}.ecp-pro-table__pagination[data-v-c5638c20]{margin-top:16px;display:flex;justify-content:flex-end}.ecp-pro-table__col-help[data-v-c5638c20]{margin-left:4px;color:#909399;cursor:help}.ecp-table-action[data-v-
|
|
1
|
+
.ecp-pro-table[data-v-c5638c20]{padding:16px;background:#fff;width:100%;box-sizing:border-box}.ecp-pro-table[data-v-c5638c20] .el-table{width:100%!important}.ecp-pro-table__header[data-v-c5638c20]{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.ecp-pro-table__title-wrapper[data-v-c5638c20]{display:flex;align-items:center;gap:4px}.ecp-pro-table__title[data-v-c5638c20]{font-size:16px;font-weight:600}.ecp-pro-table__help[data-v-c5638c20]{color:#909399;cursor:help}.ecp-pro-table__toolbar[data-v-c5638c20]{display:flex;align-items:center;gap:8px}.ecp-pro-table__body[data-v-c5638c20]{width:100%}.ecp-pro-table__pagination[data-v-c5638c20]{margin-top:16px;display:flex;justify-content:flex-end}.ecp-pro-table__col-help[data-v-c5638c20]{margin-left:4px;color:#909399;cursor:help}.ecp-table-action[data-v-f319e73a],.ecp-table-action__item[data-v-f319e73a]{display:inline-flex;align-items:center;gap:4px}.ecp-table-action__icon[data-v-f319e73a]{margin-right:4px}.ecp-table-action__more[data-v-f319e73a]{display:inline-flex;align-items:center}.ecp-table-action__dropdown-item[data-v-f319e73a]{display:inline-flex;align-items:center;gap:4px}.ecp-tree-select[data-v-f30bba11]{position:relative;width:100%}.ecp-tree-select__filter-inner[data-v-f30bba11]{margin-bottom:8px}.ecp-tree-select__dropdown[data-v-f30bba11]{position:absolute;top:100%;left:0;right:0;max-height:280px;overflow:auto;background:#fff;border:1px solid #dcdfe6;border-radius:4px;margin-top:4px;z-index:1000;padding:8px}.ecp-tree-select__loading[data-v-f30bba11]{padding:24px;text-align:center;color:#909399;font-size:14px}.ecp-pro-form-item__help-icon[data-v-2227d67d]{margin-left:4px;color:#909399;cursor:help;font-size:14px}.ecp-pro-form-item__help-icon[data-v-2227d67d]:hover{color:#409eff}.ecp-pro-form-item__help-item[data-v-2227d67d]{margin-bottom:4px}.ecp-pro-form-item__help-item[data-v-2227d67d]:last-child{margin-bottom:0}.ecp-form-actions[data-v-489c88d2]{text-align:right}.ecp-form-actions__advance[data-v-489c88d2]{margin-right:8px}.el-icon-d-arrow-left.up[data-v-489c88d2]{transform:rotate(90deg)}.el-icon-d-arrow-left.down[data-v-489c88d2]{transform:rotate(-90deg)}.ecp-pro-form[data-v-4ee1cb87]{padding:16px;position:relative}.ecp-pro-form__advance[data-v-4ee1cb87]{margin-bottom:16px}.ecp-pro-form_col[data-v-4ee1cb87]{position:relative;float:right}.el-icon-d-arrow-left.up[data-v-4ee1cb87]{transform:rotate(90deg)}.el-icon-d-arrow-left.down[data-v-4ee1cb87]{transform:rotate(-90deg)}.ecp-form-actions__advance[data-v-4ee1cb87]{position:absolute;bottom:0;left:50%;transform:translate(-50%,-50%)}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -72,15 +72,28 @@ export interface ProFormProps {
|
|
|
72
72
|
fieldMapToTime?: FieldMapToTime[];
|
|
73
73
|
/** 透传给 el-form 的事件监听器 */
|
|
74
74
|
formListeners?: FormListeners;
|
|
75
|
+
/** 自定义组件映射(组件名 -> 组件定义),供 schema.component 使用 */
|
|
76
|
+
components?: Record<string, unknown>;
|
|
75
77
|
}
|
|
78
|
+
/** ProForm 内置表单项组件类型 */
|
|
79
|
+
export type ProFormBuiltInComponent = 'input' | 'select' | 'api-select' | 'tree-select' | 'date-picker' | 'date-range' | 'input-number' | 'switch' | 'cascader' | 'checkbox' | 'radio';
|
|
80
|
+
/** 自定义组件:组件名(string)或 Vue 组件选项/构造函数(object | Function) */
|
|
81
|
+
export type ProFormCustomComponent = string | object | ((...args: unknown[]) => unknown);
|
|
76
82
|
/** ProForm 表单项配置 */
|
|
77
83
|
export interface ProFormSchema {
|
|
78
84
|
/** 字段名 */
|
|
79
85
|
field: string;
|
|
80
86
|
/** 标签 */
|
|
81
87
|
label: string;
|
|
82
|
-
/**
|
|
83
|
-
|
|
88
|
+
/** 单个表单项标签宽度,优先级高于 Form 的 labelWidth */
|
|
89
|
+
labelWidth?: string;
|
|
90
|
+
/**
|
|
91
|
+
* 组件类型:
|
|
92
|
+
* - 内置:'input' | 'select' | 'date-picker' | 'date-range' | 'input-number' | 'switch' | 'cascader' | 'checkbox' | 'radio'
|
|
93
|
+
* - 自定义组件名:任意字符串,对应全局或 ProForm 传入的 components 中注册的组件
|
|
94
|
+
* - 内联组件:直接传入 Vue 组件选项对象或构造函数
|
|
95
|
+
*/
|
|
96
|
+
component?: ProFormBuiltInComponent | ProFormCustomComponent;
|
|
84
97
|
/** 组件属性,支持函数 */
|
|
85
98
|
componentProps?: Record<string, unknown> | ((params: RenderCallbackParams & {
|
|
86
99
|
formActionType?: FormActionType;
|
package/package.json
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-select :value="value" :placeholder="placeholder" :disabled="disabled" :loading="loading" :clearable="clearable"
|
|
3
|
+
:filterable="filterable" :multiple="multiple" v-bind="$attrs" @input="$emit('input', $event)"
|
|
4
|
+
@visible-change="onVisibleChange">
|
|
5
|
+
<el-option v-for="opt in options" :key="String(opt.value)" :label="opt.label" :value="opt.value" />
|
|
6
|
+
</el-select>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import { ref, watch, onMounted } from 'vue'
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(
|
|
13
|
+
defineProps<{
|
|
14
|
+
value?: unknown
|
|
15
|
+
/** 拉取选项的接口,返回 Promise<Array<{ label, value }>> 或 { list: [] };可接收当前 params 作为参数 */
|
|
16
|
+
api?: (params?: Record<string, unknown>) => Promise<unknown>
|
|
17
|
+
/** 请求参数,变化时会重新拉取 options */
|
|
18
|
+
params?: Record<string, unknown>
|
|
19
|
+
/** 是否懒加载:为 true 时在展开下拉时再请求数据,不在挂载时请求 */
|
|
20
|
+
lazy?: boolean
|
|
21
|
+
/** 接口返回列表中「标签」字段名,默认 'label' */
|
|
22
|
+
labelField?: string
|
|
23
|
+
/** 接口返回列表中「值」字段名,默认 'value' */
|
|
24
|
+
valueField?: string
|
|
25
|
+
placeholder?: string
|
|
26
|
+
disabled?: boolean
|
|
27
|
+
clearable?: boolean
|
|
28
|
+
filterable?: boolean
|
|
29
|
+
multiple?: boolean
|
|
30
|
+
}>(),
|
|
31
|
+
{ labelField: 'label', valueField: 'value', lazy: false }
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
defineEmits<{ (e: 'input', value: unknown): void }>()
|
|
35
|
+
|
|
36
|
+
const loading = ref(false)
|
|
37
|
+
const options = ref<Array<{ label: string; value: unknown }>>([])
|
|
38
|
+
|
|
39
|
+
const onVisibleChange = (visible: boolean) => {
|
|
40
|
+
if (props.lazy && visible) fetchOptions()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fetchOptions = async () => {
|
|
44
|
+
if (!props.api) return
|
|
45
|
+
loading.value = true
|
|
46
|
+
try {
|
|
47
|
+
const res = await props.api(props.params)
|
|
48
|
+
const raw = Array.isArray(res)
|
|
49
|
+
? res
|
|
50
|
+
: ((res as Record<string, unknown>)?.list as unknown[]) ??
|
|
51
|
+
((res as Record<string, unknown>)?.data as unknown[]) ??
|
|
52
|
+
[]
|
|
53
|
+
const labelKey = props.labelField ?? 'label'
|
|
54
|
+
const valueKey = props.valueField ?? 'value'
|
|
55
|
+
options.value = raw.map((item: unknown) => {
|
|
56
|
+
const o = item as Record<string, unknown>
|
|
57
|
+
return {
|
|
58
|
+
label: String(o[labelKey] ?? o.label ?? ''),
|
|
59
|
+
value: o[valueKey] ?? o.value,
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
} finally {
|
|
63
|
+
loading.value = false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onMounted(() => {
|
|
68
|
+
if (!props.lazy) fetchOptions()
|
|
69
|
+
})
|
|
70
|
+
watch(() => props.api, () => {
|
|
71
|
+
if (!props.lazy) {
|
|
72
|
+
fetchOptions()
|
|
73
|
+
} else {
|
|
74
|
+
options.value = []
|
|
75
|
+
}
|
|
76
|
+
}, { deep: true }
|
|
77
|
+
)
|
|
78
|
+
watch(() => props.params, () => {
|
|
79
|
+
if (props.lazy) {
|
|
80
|
+
options.value = []
|
|
81
|
+
} else {
|
|
82
|
+
fetchOptions()
|
|
83
|
+
}
|
|
84
|
+
}, { deep: true })
|
|
85
|
+
</script>
|
package/src/ProForm/ProForm.vue
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
<el-col v-if="shouldShow(schema)" :key="schema.field" v-bind="getColProps(schema)"
|
|
10
10
|
:offset="schema.colProps?.offset ?? effectiveProps.baseColProps?.offset ?? 0" :data-field="schema.field">
|
|
11
11
|
<ProFormItem :schema="schema" :form-model="formModel" :form-disabled="effectiveProps.disabled"
|
|
12
|
-
:auto-placeholder="effectiveProps.autoSetPlaceholder" :form-action-type="formActionRef"
|
|
12
|
+
:auto-placeholder="effectiveProps.autoSetPlaceholder" :form-action-type="formActionRef"
|
|
13
|
+
:custom-components="formCustomComponents">
|
|
13
14
|
<template v-if="slots[getSlotName(schema)]">
|
|
14
15
|
<slot :name="getSlotName(schema)" :model="formModel" :schema="schema" :field="schema.field"
|
|
15
16
|
:values="formModel" />
|
|
@@ -88,6 +89,8 @@ const props = withDefaults(
|
|
|
88
89
|
resetFunc?: () => Promise<void>
|
|
89
90
|
submitOnReset?: boolean
|
|
90
91
|
formListeners?: FormListeners
|
|
92
|
+
/** 自定义组件映射(组件名 -> 组件),供 schema.component 使用 */
|
|
93
|
+
components?: Record<string, unknown>
|
|
91
94
|
}>(),
|
|
92
95
|
{
|
|
93
96
|
labelWidth: '120px',
|
|
@@ -145,6 +148,12 @@ const getEffectiveSpan = (colProps?: ColEx | null, baseColProps?: ColEx | null,
|
|
|
145
148
|
|
|
146
149
|
const { getSetting: getComponentSetting } = useComponentSetting()
|
|
147
150
|
const effectiveProps = computed(() => ({ ...getComponentSetting('ProForm'), ...props, ...innerProps.value }))
|
|
151
|
+
/** 传给 ProFormItem 的自定义组件映射:显式合并 setSetting 与 props,避免响应式代理导致组件引用丢失 */
|
|
152
|
+
const formCustomComponents = computed(() => ({
|
|
153
|
+
...(getComponentSetting('ProForm').components as Record<string, unknown> | undefined) ?? {},
|
|
154
|
+
...(props.components ?? {}),
|
|
155
|
+
...(innerProps.value.components ?? {}),
|
|
156
|
+
}))
|
|
148
157
|
const effectiveActionColOptions = computed(() => effectiveProps.value.actionColOptions ?? { span: 24 })
|
|
149
158
|
|
|
150
159
|
/** 当前视口宽度,用于响应式计算 span */
|
|
@@ -349,7 +358,6 @@ const setProps = async (formProps: Partial<ProFormProps>) => {
|
|
|
349
358
|
innerProps.value = { ...innerProps.value, ...formProps }
|
|
350
359
|
if (formProps.schemas) {
|
|
351
360
|
innerSchemas.value = [...formProps.schemas]
|
|
352
|
-
debugger
|
|
353
361
|
initForm()
|
|
354
362
|
}
|
|
355
363
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
:prop="schema.field"
|
|
6
6
|
:required="schema.required"
|
|
7
7
|
:rules="effectiveRules"
|
|
8
|
+
:label-width="schema.labelWidth"
|
|
8
9
|
>
|
|
9
10
|
<template slot="label">
|
|
10
11
|
<span>{{ schema.label }}</span>
|
|
@@ -33,8 +34,19 @@
|
|
|
33
34
|
<slot v-else-if="hasSlot" :model="formModel" :schema="schema" :field="schema.field" :values="formModel" />
|
|
34
35
|
<!-- 默认组件渲染 -->
|
|
35
36
|
<template v-else>
|
|
37
|
+
<!-- 自定义 component:组件名或内联组件 -->
|
|
38
|
+
<component
|
|
39
|
+
v-if="resolvedCustomComponent"
|
|
40
|
+
:is="resolvedCustomComponent"
|
|
41
|
+
:value="formModel[schema.field]"
|
|
42
|
+
@input="setFieldValue"
|
|
43
|
+
:placeholder="schema.placeholder || (autoPlaceholder ? `请输入${schema.label}` : undefined)"
|
|
44
|
+
:disabled="effectiveDisabled"
|
|
45
|
+
v-bind="effectiveComponentProps"
|
|
46
|
+
v-on="effectiveComponentListeners"
|
|
47
|
+
/>
|
|
36
48
|
<el-input
|
|
37
|
-
v-if="schema.component === 'input' || !schema.component"
|
|
49
|
+
v-else-if="schema.component === 'input' || !schema.component"
|
|
38
50
|
v-model="formModel[schema.field]"
|
|
39
51
|
:placeholder="schema.placeholder || (autoPlaceholder ? `请输入${schema.label}` : undefined)"
|
|
40
52
|
:disabled="effectiveDisabled"
|
|
@@ -65,6 +77,24 @@
|
|
|
65
77
|
:value="opt.value"
|
|
66
78
|
/>
|
|
67
79
|
</el-select>
|
|
80
|
+
<ApiSelect
|
|
81
|
+
v-else-if="schema.component === 'api-select'"
|
|
82
|
+
:value="formModel[schema.field]"
|
|
83
|
+
:placeholder="schema.placeholder || (autoPlaceholder ? `请选择${schema.label}` : undefined)"
|
|
84
|
+
:disabled="effectiveDisabled"
|
|
85
|
+
v-bind="effectiveComponentProps"
|
|
86
|
+
v-on="effectiveComponentListeners"
|
|
87
|
+
@input="setFieldValue"
|
|
88
|
+
/>
|
|
89
|
+
<TreeSelect
|
|
90
|
+
v-else-if="schema.component === 'tree-select'"
|
|
91
|
+
:value="formModel[schema.field]"
|
|
92
|
+
:placeholder="schema.placeholder || (autoPlaceholder ? `请选择${schema.label}` : undefined)"
|
|
93
|
+
:disabled="effectiveDisabled"
|
|
94
|
+
v-bind="effectiveComponentProps"
|
|
95
|
+
v-on="effectiveComponentListeners"
|
|
96
|
+
@input="setFieldValue"
|
|
97
|
+
/>
|
|
68
98
|
<el-date-picker
|
|
69
99
|
v-else-if="schema.component === 'date-picker'"
|
|
70
100
|
v-model="formModel[schema.field]"
|
|
@@ -136,14 +166,23 @@
|
|
|
136
166
|
|
|
137
167
|
<script setup lang="ts">
|
|
138
168
|
import { computed, useSlots, h } from 'vue'
|
|
169
|
+
import ApiSelect from './ApiSelect.vue'
|
|
170
|
+
import TreeSelect from './TreeSelect.vue'
|
|
139
171
|
import type { ProFormSchema, RenderCallbackParams } from '../types'
|
|
140
172
|
|
|
173
|
+
const BUILT_IN_COMPONENTS: Set<string> = new Set([
|
|
174
|
+
'input', 'select', 'api-select', 'tree-select', 'date-picker', 'date-range', 'input-number',
|
|
175
|
+
'switch', 'cascader', 'checkbox', 'radio',
|
|
176
|
+
])
|
|
177
|
+
|
|
141
178
|
const props = defineProps<{
|
|
142
179
|
schema: ProFormSchema
|
|
143
180
|
formModel: Record<string, unknown>
|
|
144
181
|
formDisabled?: boolean
|
|
145
182
|
autoPlaceholder?: boolean
|
|
146
183
|
formActionType?: import('../types').FormActionType
|
|
184
|
+
/** 自定义组件映射(由 ProForm 传入) */
|
|
185
|
+
customComponents?: Record<string, unknown>
|
|
147
186
|
}>()
|
|
148
187
|
|
|
149
188
|
const slots = useSlots()
|
|
@@ -216,6 +255,20 @@ const getOptions = (props: Record<string, unknown>): Array<{ label: string; valu
|
|
|
216
255
|
return Array.isArray(opts) ? opts : undefined
|
|
217
256
|
}
|
|
218
257
|
|
|
258
|
+
const resolvedCustomComponent = computed(() => {
|
|
259
|
+
const c = props.schema.component
|
|
260
|
+
if (c == null) return null
|
|
261
|
+
if (typeof c === 'string') {
|
|
262
|
+
if (BUILT_IN_COMPONENTS.has(c)) return null
|
|
263
|
+
return (props.customComponents && props.customComponents[c]) || c
|
|
264
|
+
}
|
|
265
|
+
return c
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const setFieldValue = (v: unknown) => {
|
|
269
|
+
props.formModel[props.schema.field] = v
|
|
270
|
+
}
|
|
271
|
+
|
|
219
272
|
const renderComponent = computed(() => {
|
|
220
273
|
const renderFn = props.schema.render
|
|
221
274
|
if (!renderFn) return null
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="ecp-tree-select" ref="rootRef">
|
|
3
|
+
<el-input
|
|
4
|
+
:value="displayText"
|
|
5
|
+
:placeholder="placeholder"
|
|
6
|
+
:disabled="disabled"
|
|
7
|
+
:clearable="clearable"
|
|
8
|
+
readonly
|
|
9
|
+
suffix-icon="el-icon-arrow-down"
|
|
10
|
+
class="ecp-tree-select__input"
|
|
11
|
+
@focus="openDropdown"
|
|
12
|
+
@clear="clearValue"
|
|
13
|
+
/>
|
|
14
|
+
<transition name="el-zoom-in-top">
|
|
15
|
+
<div v-show="dropdownVisible" class="ecp-tree-select__dropdown">
|
|
16
|
+
<div v-if="filterable" class="ecp-tree-select__filter-inner">
|
|
17
|
+
<el-input
|
|
18
|
+
v-model="filterText"
|
|
19
|
+
size="small"
|
|
20
|
+
placeholder="搜索节点"
|
|
21
|
+
prefix-icon="el-icon-search"
|
|
22
|
+
clearable
|
|
23
|
+
@click.native.stop
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
<el-tree
|
|
27
|
+
v-show="!loading"
|
|
28
|
+
ref="treeRef"
|
|
29
|
+
:data="treeData"
|
|
30
|
+
:props="treeProps"
|
|
31
|
+
:node-key="valueField"
|
|
32
|
+
:filter-node-method="filterable ? filterNodeMethod : undefined"
|
|
33
|
+
:highlight-current="true"
|
|
34
|
+
default-expand-all
|
|
35
|
+
@node-click="onNodeClick"
|
|
36
|
+
/>
|
|
37
|
+
<div v-if="loading" class="ecp-tree-select__loading">
|
|
38
|
+
<i class="el-icon-loading" /> 加载中...
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</transition>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script setup lang="ts">
|
|
46
|
+
import { ref, watch, onMounted, computed, nextTick } from 'vue'
|
|
47
|
+
|
|
48
|
+
interface TreeNode {
|
|
49
|
+
label?: string
|
|
50
|
+
value?: unknown
|
|
51
|
+
children?: TreeNode[]
|
|
52
|
+
[key: string]: unknown
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const props = withDefaults(
|
|
56
|
+
defineProps<{
|
|
57
|
+
value?: unknown
|
|
58
|
+
/** 树数据,直接传入时优先使用,不请求 api */
|
|
59
|
+
treeData?: unknown[]
|
|
60
|
+
api?: (params?: Record<string, unknown>) => Promise<unknown>
|
|
61
|
+
params?: Record<string, unknown>
|
|
62
|
+
lazy?: boolean
|
|
63
|
+
labelField?: string
|
|
64
|
+
valueField?: string
|
|
65
|
+
childrenField?: string
|
|
66
|
+
filterable?: boolean
|
|
67
|
+
placeholder?: string
|
|
68
|
+
disabled?: boolean
|
|
69
|
+
clearable?: boolean
|
|
70
|
+
}>(),
|
|
71
|
+
{
|
|
72
|
+
labelField: 'label',
|
|
73
|
+
valueField: 'value',
|
|
74
|
+
childrenField: 'children',
|
|
75
|
+
lazy: false,
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const emit = defineEmits<{ (e: 'input', value: unknown): void }>()
|
|
80
|
+
|
|
81
|
+
const rootRef = ref<HTMLElement>()
|
|
82
|
+
const treeRef = ref()
|
|
83
|
+
const dropdownVisible = ref(false)
|
|
84
|
+
const loading = ref(false)
|
|
85
|
+
const filterText = ref('')
|
|
86
|
+
const treeData = ref<TreeNode[]>([])
|
|
87
|
+
const flatLabelMap = ref<Record<string, string>>({})
|
|
88
|
+
|
|
89
|
+
const treeProps = computed(() => ({
|
|
90
|
+
label: props.labelField,
|
|
91
|
+
children: props.childrenField,
|
|
92
|
+
}))
|
|
93
|
+
|
|
94
|
+
function normalizeNode(node: Record<string, unknown>): TreeNode {
|
|
95
|
+
const labelKey = props.labelField ?? 'label'
|
|
96
|
+
const valueKey = props.valueField ?? 'value'
|
|
97
|
+
const childrenKey = props.childrenField ?? 'children'
|
|
98
|
+
const children = node[childrenKey] as unknown[]
|
|
99
|
+
const out: TreeNode = {}
|
|
100
|
+
out[props.labelField ?? 'label'] = node[labelKey] ?? node.label
|
|
101
|
+
out[props.valueField ?? 'value'] = node[valueKey] ?? node.value
|
|
102
|
+
if (Array.isArray(children) && children.length) {
|
|
103
|
+
out[props.childrenField ?? 'children'] = children.map((c) => normalizeNode(c as Record<string, unknown>))
|
|
104
|
+
}
|
|
105
|
+
return out
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildFlatLabelMap(nodes: TreeNode[], prefix = ''): Record<string, string> {
|
|
109
|
+
const map: Record<string, string> = {}
|
|
110
|
+
const labelKey = props.labelField ?? 'label'
|
|
111
|
+
const valueKey = props.valueField ?? 'value'
|
|
112
|
+
const childrenKey = props.childrenField ?? 'children'
|
|
113
|
+
for (const node of nodes) {
|
|
114
|
+
const label = String(node[labelKey] ?? node.label ?? '')
|
|
115
|
+
const value = node[valueKey] ?? node.value
|
|
116
|
+
if (value !== undefined && value !== null) {
|
|
117
|
+
map[String(value)] = prefix ? prefix + ' / ' + label : label
|
|
118
|
+
}
|
|
119
|
+
const children = node[childrenKey] ?? node.children
|
|
120
|
+
if (Array.isArray(children) && children.length) {
|
|
121
|
+
Object.assign(map, buildFlatLabelMap(children as TreeNode[], label))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return map
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function hasTreeDataProp() {
|
|
128
|
+
const td = props.treeData
|
|
129
|
+
return Array.isArray(td) && td.length > 0
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function applyTreeData(nodes: TreeNode[]) {
|
|
133
|
+
treeData.value = nodes
|
|
134
|
+
flatLabelMap.value = buildFlatLabelMap(nodes)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function syncFromTreeDataProp() {
|
|
138
|
+
const td = props.treeData
|
|
139
|
+
if (!Array.isArray(td) || td.length === 0) return
|
|
140
|
+
const normalized = td.map((item) => normalizeNode(item as Record<string, unknown>))
|
|
141
|
+
applyTreeData(normalized)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const displayText = computed(() => {
|
|
145
|
+
if (props.value == null || props.value === '') return ''
|
|
146
|
+
return flatLabelMap.value[String(props.value)] ?? String(props.value)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const filterNodeMethod = (value: string, data: TreeNode) => {
|
|
150
|
+
if (!value) return true
|
|
151
|
+
const labelKey = props.labelField ?? 'label'
|
|
152
|
+
const label = String(data[labelKey] ?? data.label ?? '')
|
|
153
|
+
return label.toLowerCase().includes(value.toLowerCase())
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
watch(filterText, (val) => {
|
|
157
|
+
treeRef.value?.filter(val)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
let clickOutsideHandler: ((e: MouseEvent) => void) | null = null
|
|
161
|
+
|
|
162
|
+
function openDropdown() {
|
|
163
|
+
if (props.disabled) return
|
|
164
|
+
dropdownVisible.value = true
|
|
165
|
+
if (props.lazy && !hasTreeDataProp()) fetchData()
|
|
166
|
+
nextTick(() => {
|
|
167
|
+
clickOutsideHandler = (e: MouseEvent) => {
|
|
168
|
+
if (rootRef.value && !rootRef.value.contains(e.target as Node)) {
|
|
169
|
+
closeDropdown()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
document.addEventListener('click', clickOutsideHandler)
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function closeDropdown() {
|
|
177
|
+
dropdownVisible.value = false
|
|
178
|
+
filterText.value = ''
|
|
179
|
+
if (clickOutsideHandler) {
|
|
180
|
+
document.removeEventListener('click', clickOutsideHandler)
|
|
181
|
+
clickOutsideHandler = null
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function clearValue() {
|
|
186
|
+
emit('input', undefined)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function onNodeClick(data: TreeNode) {
|
|
190
|
+
const valueKey = props.valueField ?? 'value'
|
|
191
|
+
const val = data[valueKey] ?? data.value
|
|
192
|
+
emit('input', val)
|
|
193
|
+
closeDropdown()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function fetchData() {
|
|
197
|
+
if (!props.api || hasTreeDataProp()) return
|
|
198
|
+
loading.value = true
|
|
199
|
+
try {
|
|
200
|
+
const res = await props.api(props.params)
|
|
201
|
+
const raw = Array.isArray(res)
|
|
202
|
+
? res
|
|
203
|
+
: ((res as Record<string, unknown>)?.list as unknown[]) ??
|
|
204
|
+
((res as Record<string, unknown>)?.data as unknown[]) ??
|
|
205
|
+
[]
|
|
206
|
+
treeData.value = raw.map((item) => normalizeNode(item as Record<string, unknown>))
|
|
207
|
+
flatLabelMap.value = buildFlatLabelMap(treeData.value)
|
|
208
|
+
} finally {
|
|
209
|
+
loading.value = false
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
onMounted(() => {
|
|
214
|
+
if (hasTreeDataProp()) {
|
|
215
|
+
syncFromTreeDataProp()
|
|
216
|
+
} else if (!props.lazy) {
|
|
217
|
+
fetchData()
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
watch(() => props.treeData, () => {
|
|
222
|
+
if (hasTreeDataProp()) {
|
|
223
|
+
syncFromTreeDataProp()
|
|
224
|
+
} else {
|
|
225
|
+
treeData.value = []
|
|
226
|
+
flatLabelMap.value = {}
|
|
227
|
+
if (!props.lazy && props.api) fetchData()
|
|
228
|
+
}
|
|
229
|
+
}, { deep: true })
|
|
230
|
+
|
|
231
|
+
watch(() => props.api, () => {
|
|
232
|
+
if (hasTreeDataProp()) return
|
|
233
|
+
if (!props.lazy) fetchData()
|
|
234
|
+
else { treeData.value = []; flatLabelMap.value = {} }
|
|
235
|
+
}, { deep: true })
|
|
236
|
+
|
|
237
|
+
watch(() => props.params, () => {
|
|
238
|
+
if (hasTreeDataProp()) return
|
|
239
|
+
if (props.lazy) { treeData.value = []; flatLabelMap.value = {} }
|
|
240
|
+
else fetchData()
|
|
241
|
+
}, { deep: true })
|
|
242
|
+
</script>
|
|
243
|
+
|
|
244
|
+
<style scoped>
|
|
245
|
+
.ecp-tree-select {
|
|
246
|
+
position: relative;
|
|
247
|
+
width: 100%;
|
|
248
|
+
}
|
|
249
|
+
.ecp-tree-select__filter-inner {
|
|
250
|
+
margin-bottom: 8px;
|
|
251
|
+
}
|
|
252
|
+
.ecp-tree-select__dropdown {
|
|
253
|
+
position: absolute;
|
|
254
|
+
top: 100%;
|
|
255
|
+
left: 0;
|
|
256
|
+
right: 0;
|
|
257
|
+
max-height: 280px;
|
|
258
|
+
overflow: auto;
|
|
259
|
+
background: #fff;
|
|
260
|
+
border: 1px solid #dcdfe6;
|
|
261
|
+
border-radius: 4px;
|
|
262
|
+
margin-top: 4px;
|
|
263
|
+
z-index: 1000;
|
|
264
|
+
padding: 8px;
|
|
265
|
+
}
|
|
266
|
+
.ecp-tree-select__loading {
|
|
267
|
+
padding: 24px;
|
|
268
|
+
text-align: center;
|
|
269
|
+
color: #909399;
|
|
270
|
+
font-size: 14px;
|
|
271
|
+
}
|
|
272
|
+
</style>
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
:title="action.popConfirm.title"
|
|
13
13
|
:confirm-button-text="action.popConfirm.okText || '确定'"
|
|
14
14
|
:cancel-button-text="action.popConfirm.cancelText || '取消'"
|
|
15
|
-
@confirm="(
|
|
16
|
-
@cancel="(
|
|
15
|
+
@confirm="handlePopConfirmConfirm(action, $event)"
|
|
16
|
+
@cancel="handlePopConfirmCancel(action, $event)"
|
|
17
17
|
>
|
|
18
18
|
<span slot="reference">
|
|
19
19
|
<component
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
size="small"
|
|
26
26
|
:disabled="action.disabled"
|
|
27
27
|
v-bind="action.props"
|
|
28
|
-
|
|
28
|
+
@click="handleActionClick(action, $event)"
|
|
29
29
|
>
|
|
30
30
|
<i v-if="action.icon" :class="['ecp-table-action__icon', action.icon]" />
|
|
31
31
|
<span>{{ action.label }}</span>
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
size="small"
|
|
46
46
|
:disabled="action.disabled"
|
|
47
47
|
v-bind="action.props"
|
|
48
|
-
@click="(
|
|
48
|
+
@click="handleActionClick(action, $event)"
|
|
49
49
|
>
|
|
50
50
|
<i v-if="action.icon" :class="['ecp-table-action__icon', action.icon]" />
|
|
51
51
|
<span>{{ action.label }}</span>
|
|
@@ -133,6 +133,10 @@ const handleClick = (action: TableActionItem, e: MouseEvent) => {
|
|
|
133
133
|
action.onClick?.(e)
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
const handleActionClick = (action: TableActionItem, e: MouseEvent) => {
|
|
137
|
+
handleClick(action, e)
|
|
138
|
+
}
|
|
139
|
+
|
|
136
140
|
const handlePopConfirm = (action: TableActionItem, type: 'confirm' | 'cancel', e: MouseEvent) => {
|
|
137
141
|
if (props.stopButtonPropagation) {
|
|
138
142
|
e.stopPropagation()
|
|
@@ -145,6 +149,14 @@ const handlePopConfirm = (action: TableActionItem, type: 'confirm' | 'cancel', e
|
|
|
145
149
|
}
|
|
146
150
|
}
|
|
147
151
|
|
|
152
|
+
const handlePopConfirmConfirm = (action: TableActionItem, e: MouseEvent) => {
|
|
153
|
+
handlePopConfirm(action, 'confirm', e)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const handlePopConfirmCancel = (action: TableActionItem, e: MouseEvent) => {
|
|
157
|
+
handlePopConfirm(action, 'cancel', e)
|
|
158
|
+
}
|
|
159
|
+
|
|
148
160
|
const handleDropdownCommand = (index: number | string) => {
|
|
149
161
|
const idx = Number(index)
|
|
150
162
|
const action = visibleDropDownActions.value[idx]
|
|
@@ -190,4 +202,3 @@ const handleDropdownCommand = (index: number | string) => {
|
|
|
190
202
|
gap: 4px;
|
|
191
203
|
}
|
|
192
204
|
</style>
|
|
193
|
-
|
package/src/types/index.ts
CHANGED
|
@@ -78,16 +78,42 @@ export interface ProFormProps {
|
|
|
78
78
|
fieldMapToTime?: FieldMapToTime[]
|
|
79
79
|
/** 透传给 el-form 的事件监听器 */
|
|
80
80
|
formListeners?: FormListeners
|
|
81
|
+
/** 自定义组件映射(组件名 -> 组件定义),供 schema.component 使用 */
|
|
82
|
+
components?: Record<string, unknown>
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
/** ProForm 内置表单项组件类型 */
|
|
86
|
+
export type ProFormBuiltInComponent =
|
|
87
|
+
| 'input'
|
|
88
|
+
| 'select'
|
|
89
|
+
| 'api-select'
|
|
90
|
+
| 'tree-select'
|
|
91
|
+
| 'date-picker'
|
|
92
|
+
| 'date-range'
|
|
93
|
+
| 'input-number'
|
|
94
|
+
| 'switch'
|
|
95
|
+
| 'cascader'
|
|
96
|
+
| 'checkbox'
|
|
97
|
+
| 'radio'
|
|
98
|
+
|
|
99
|
+
/** 自定义组件:组件名(string)或 Vue 组件选项/构造函数(object | Function) */
|
|
100
|
+
export type ProFormCustomComponent = string | object | ((...args: unknown[]) => unknown)
|
|
101
|
+
|
|
83
102
|
/** ProForm 表单项配置 */
|
|
84
103
|
export interface ProFormSchema {
|
|
85
104
|
/** 字段名 */
|
|
86
105
|
field: string
|
|
87
106
|
/** 标签 */
|
|
88
107
|
label: string
|
|
89
|
-
/**
|
|
90
|
-
|
|
108
|
+
/** 单个表单项标签宽度,优先级高于 Form 的 labelWidth */
|
|
109
|
+
labelWidth?: string
|
|
110
|
+
/**
|
|
111
|
+
* 组件类型:
|
|
112
|
+
* - 内置:'input' | 'select' | 'date-picker' | 'date-range' | 'input-number' | 'switch' | 'cascader' | 'checkbox' | 'radio'
|
|
113
|
+
* - 自定义组件名:任意字符串,对应全局或 ProForm 传入的 components 中注册的组件
|
|
114
|
+
* - 内联组件:直接传入 Vue 组件选项对象或构造函数
|
|
115
|
+
*/
|
|
116
|
+
component?: ProFormBuiltInComponent | ProFormCustomComponent
|
|
91
117
|
/** 组件属性,支持函数 */
|
|
92
118
|
componentProps?: Record<string, unknown> | ((params: RenderCallbackParams & { formActionType?: FormActionType }) => Record<string, unknown>)
|
|
93
119
|
/** 占位符 */
|