@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/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-1b2d6c42],.ecp-table-action__item[data-v-1b2d6c42]{display:inline-flex;align-items:center;gap:4px}.ecp-table-action__icon[data-v-1b2d6c42]{margin-right:4px}.ecp-table-action__more[data-v-1b2d6c42]{display:inline-flex;align-items:center}.ecp-table-action__dropdown-item[data-v-1b2d6c42]{display:inline-flex;align-items:center;gap:4px}.ecp-pro-form-item__help-icon[data-v-cfc968c5]{margin-left:4px;color:#909399;cursor:help;font-size:14px}.ecp-pro-form-item__help-icon[data-v-cfc968c5]:hover{color:#409eff}.ecp-pro-form-item__help-item[data-v-cfc968c5]{margin-bottom:4px}.ecp-pro-form-item__help-item[data-v-cfc968c5]: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-140fc052]{padding:16px;position:relative}.ecp-pro-form__advance[data-v-140fc052]{margin-bottom:16px}.ecp-pro-form_col[data-v-140fc052]{position:relative;float:right}.el-icon-d-arrow-left.up[data-v-140fc052]{transform:rotate(90deg)}.el-icon-d-arrow-left.down[data-v-140fc052]{transform:rotate(-90deg)}.ecp-form-actions__advance[data-v-140fc052]{position:absolute;bottom:0;left:50%;transform:translate(-50%,-50%)}
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%)}
@@ -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
- component?: 'input' | 'select' | 'date-picker' | 'date-range' | 'input-number' | 'switch' | 'cascader' | 'checkbox' | 'radio';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspire-ui/element-component-pro",
3
- "version": "1.0.02",
3
+ "version": "1.0.04",
4
4
  "description": "Element UI 二次封装组件库,基于 Vue 2.7 + TypeScript + setup 语法糖,实现 VbenAdmin 风格的 Pro 组件",
5
5
  "type": "module",
6
6
  "main": "./dist/element-component-pro.umd.js",
@@ -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>
@@ -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="(e) => handlePopConfirm(action, 'confirm', e)"
16
- @cancel="(e) => handlePopConfirm(action, 'cancel', e)"
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
- @click="(e) => handleClick(action, e)"
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="(e) => handleClick(action, e)"
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
-
@@ -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
- component?: 'input' | 'select' | 'date-picker' | 'date-range' | 'input-number' | 'switch' | 'cascader' | 'checkbox' | 'radio'
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
  /** 占位符 */