@aspire-ui/element-component-pro 1.0.25 → 1.0.26
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/ProTableForm/CellEditor.vue.d.ts +29 -0
- package/dist/ProTableForm/ProTableForm.vue.d.ts +122 -102
- package/dist/ProTableForm/index.d.ts +4 -2
- package/dist/ProTableForm/types.d.ts +1 -69
- package/dist/ProTableForm/useProTableForm.d.ts +55 -0
- package/dist/element-component-pro.es.js +1430 -1254
- 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 +445 -282
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +222 -0
- package/package.json +1 -1
- package/src/ProTable/ProTable.vue +0 -1
- package/src/ProTableForm/CellEditor.vue +188 -0
- package/src/ProTableForm/ProTableForm.vue +348 -452
- package/src/ProTableForm/TableFormCell.vue +46 -0
- package/src/ProTableForm/index.ts +6 -7
- package/src/ProTableForm/types.ts +12 -72
- package/src/ProTableForm/useProTableForm.ts +442 -0
- package/src/index.ts +6 -4
- package/src/types/index.ts +235 -0
|
@@ -1,170 +1,225 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="ecp-pro-table-form">
|
|
2
|
+
<div class="ecp-pro-table-form" v-bind="$attrs">
|
|
3
3
|
<el-form
|
|
4
4
|
ref="formRef"
|
|
5
|
-
:model="
|
|
5
|
+
:model="formModelRef"
|
|
6
6
|
:rules="mergedRules"
|
|
7
|
+
:validate-on-rule-change="false"
|
|
7
8
|
:label-width="labelWidth"
|
|
9
|
+
:size="formSize"
|
|
10
|
+
:label-position="labelPosition"
|
|
11
|
+
:disabled="disabled"
|
|
12
|
+
v-bind="formProps"
|
|
8
13
|
class="ecp-pro-table-form__form"
|
|
9
14
|
>
|
|
10
15
|
<el-table
|
|
16
|
+
ref="tableRef"
|
|
11
17
|
:data="tableRows"
|
|
12
18
|
:border="bordered"
|
|
19
|
+
:stripe="stripe"
|
|
20
|
+
:size="size"
|
|
21
|
+
:max-height="maxHeight"
|
|
22
|
+
:height="height"
|
|
13
23
|
:row-key="rowKeyFn"
|
|
24
|
+
:row-class-name="rowClassName"
|
|
25
|
+
:default-expand-all="defaultExpandAll"
|
|
26
|
+
:span-method="spanMethodAdapter"
|
|
14
27
|
header-cell-class-name="ecp-pro-table-form__header-cell"
|
|
28
|
+
v-bind="tableProps"
|
|
15
29
|
class="ecp-pro-table-form__table"
|
|
30
|
+
@row-click="handleTableRowClick"
|
|
31
|
+
@row-dblclick="handleTableRowDblclick"
|
|
32
|
+
@sort-change="handleTableSortChange"
|
|
33
|
+
@expand-change="handleTableExpandChange"
|
|
16
34
|
>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
<template v-for="col in flatColumns">
|
|
36
|
+
<!-- 分组列:有 children -->
|
|
37
|
+
<el-table-column
|
|
38
|
+
v-if="col.children && col.children.length > 0"
|
|
39
|
+
:key="'col-' + col._key"
|
|
40
|
+
:label="col.title"
|
|
41
|
+
:min-width="col.minWidth || col.width || 120"
|
|
42
|
+
:width="col.width"
|
|
43
|
+
:align="col.align || 'center'"
|
|
44
|
+
:header-align="col.headerAlign"
|
|
45
|
+
:fixed="col.fixed"
|
|
46
|
+
:cell-style="col.cellStyle"
|
|
47
|
+
:header-cell-style="col.headerCellStyle"
|
|
48
|
+
:cell-class-name="col.cellClassName"
|
|
49
|
+
:header-cell-class-name="col.headerCellClassName"
|
|
50
|
+
:sortable="col.sortable"
|
|
51
|
+
:resizable="col.resizable !== false"
|
|
52
|
+
>
|
|
53
|
+
<template #header>
|
|
54
|
+
<slot :name="`header-${col.key}`" :column="col">
|
|
55
|
+
<span class="ecp-pro-table-form__th-text">
|
|
56
|
+
<span v-if="col.required" class="ecp-pro-table-form__req">*</span>{{ col.title }}
|
|
57
|
+
</span>
|
|
58
|
+
</slot>
|
|
59
|
+
</template>
|
|
60
|
+
<template v-for="child in col.children">
|
|
61
|
+
<el-table-column
|
|
62
|
+
v-if="!child.hideInTable"
|
|
63
|
+
:key="'col-' + col.key + '-' + child.key"
|
|
64
|
+
:label="child.title"
|
|
65
|
+
:min-width="child.minWidth || child.width || 120"
|
|
66
|
+
:width="child.width"
|
|
67
|
+
:align="child.align || 'center'"
|
|
68
|
+
:header-align="child.headerAlign"
|
|
69
|
+
:fixed="child.fixed"
|
|
70
|
+
:cell-style="child.cellStyle"
|
|
71
|
+
:header-cell-style="child.headerCellStyle"
|
|
72
|
+
:cell-class-name="child.cellClassName"
|
|
73
|
+
:header-cell-class-name="child.headerCellClassName"
|
|
74
|
+
:sortable="child.sortable"
|
|
75
|
+
:resizable="child.resizable !== false"
|
|
76
|
+
>
|
|
77
|
+
<template #header>
|
|
78
|
+
<slot :name="`header-${child.key}`" :column="child">
|
|
79
|
+
<span class="ecp-pro-table-form__th-text">
|
|
80
|
+
<span v-if="child.required" class="ecp-pro-table-form__req">*</span>{{ child.title }}
|
|
81
|
+
</span>
|
|
82
|
+
</slot>
|
|
83
|
+
</template>
|
|
84
|
+
<template #default="slotProps">
|
|
85
|
+
<!-- 渲染函数 -->
|
|
86
|
+
<template v-if="child.render">
|
|
87
|
+
<el-form-item
|
|
88
|
+
:prop="getCellProp(slotProps.row, col, child.key)"
|
|
89
|
+
:rules="mergedRules[getCellProp(slotProps.row, col, child.key)]"
|
|
90
|
+
class="ecp-pro-table-form__cell-item"
|
|
91
|
+
>
|
|
92
|
+
<template v-if="getRender(child, slotProps.row, col, child.key).isText">
|
|
93
|
+
<span>{{ getRender(child, slotProps.row, col, child.key).value }}</span>
|
|
94
|
+
</template>
|
|
95
|
+
<template v-else>
|
|
96
|
+
<RenderCell :render-fn="child.render" :render-params="{ row: slotProps.row, col, childKey: child.key }" />
|
|
97
|
+
</template>
|
|
98
|
+
</el-form-item>
|
|
99
|
+
</template>
|
|
100
|
+
<!-- 插槽列 -->
|
|
101
|
+
<template v-else-if="child.component === 'slot' && child.slotName">
|
|
102
|
+
<el-form-item
|
|
103
|
+
:prop="getCellProp(slotProps.row, col, child.key)"
|
|
104
|
+
:rules="mergedRules[getCellProp(slotProps.row, col, child.key)]"
|
|
105
|
+
class="ecp-pro-table-form__cell-item"
|
|
106
|
+
>
|
|
107
|
+
<slot
|
|
108
|
+
:name="`cell-${child.slotName}`"
|
|
109
|
+
:column="child"
|
|
110
|
+
:row="slotProps.row"
|
|
111
|
+
:index="slotProps.$index"
|
|
112
|
+
:value="getCellValue(slotProps.row, col, child.key)"
|
|
113
|
+
:update-value="slotUpdateHandler(slotProps, col, child.key)"
|
|
114
|
+
/>
|
|
115
|
+
</el-form-item>
|
|
116
|
+
</template>
|
|
117
|
+
<!-- 内置组件 -->
|
|
118
|
+
<el-form-item
|
|
119
|
+
v-else
|
|
120
|
+
:prop="getCellProp(slotProps.row, col, child.key)"
|
|
121
|
+
:rules="mergedRules[getCellProp(slotProps.row, col, child.key)]"
|
|
122
|
+
class="ecp-pro-table-form__cell-item"
|
|
123
|
+
>
|
|
124
|
+
<CellEditor
|
|
125
|
+
:col="child"
|
|
126
|
+
:value="getCellValue(slotProps.row, col, child.key)"
|
|
127
|
+
:row="slotProps.row"
|
|
128
|
+
:size="size"
|
|
129
|
+
:placeholder="col.placeholder || metricPlaceholder"
|
|
130
|
+
@update="setCellValue(slotProps.row, col, child.key, $event)"
|
|
131
|
+
/>
|
|
132
|
+
</el-form-item>
|
|
133
|
+
</template>
|
|
134
|
+
</el-table-column>
|
|
135
|
+
</template>
|
|
136
|
+
</el-table-column>
|
|
137
|
+
|
|
138
|
+
<!-- 叶子列:无 children -->
|
|
139
|
+
<el-table-column
|
|
140
|
+
v-else-if="!col.hideInTable"
|
|
141
|
+
:key="'leaf-' + col._key"
|
|
142
|
+
:label="col.title"
|
|
143
|
+
:min-width="col.minWidth || col.width || 120"
|
|
144
|
+
:width="col.width"
|
|
145
|
+
:align="col.align || 'center'"
|
|
146
|
+
:header-align="col.headerAlign"
|
|
147
|
+
:fixed="col.fixed"
|
|
148
|
+
:cell-style="col.cellStyle"
|
|
149
|
+
:header-cell-style="col.headerCellStyle"
|
|
150
|
+
:cell-class-name="col.cellClassName"
|
|
151
|
+
:header-cell-class-name="col.headerCellClassName"
|
|
152
|
+
:sortable="col.sortable"
|
|
153
|
+
:resizable="col.resizable !== false"
|
|
154
|
+
>
|
|
155
|
+
<template #header>
|
|
156
|
+
<slot :name="`header-${col.key}`" :column="col">
|
|
157
|
+
<span class="ecp-pro-table-form__th-text">
|
|
158
|
+
<span v-if="col.required" class="ecp-pro-table-form__req">*</span>{{ col.title }}
|
|
159
|
+
</span>
|
|
160
|
+
</slot>
|
|
161
|
+
</template>
|
|
162
|
+
<template #default="slotProps">
|
|
163
|
+
<!-- 渲染函数 -->
|
|
164
|
+
<template v-if="col.render">
|
|
165
|
+
<el-form-item
|
|
166
|
+
:prop="getCellProp(slotProps.row, col)"
|
|
167
|
+
:rules="mergedRules[getCellProp(slotProps.row, col)]"
|
|
168
|
+
class="ecp-pro-table-form__cell-item"
|
|
169
|
+
>
|
|
170
|
+
<template v-if="getRender(col, slotProps.row, col).isText">
|
|
171
|
+
<span>{{ getRender(col, slotProps.row, col).value }}</span>
|
|
172
|
+
</template>
|
|
173
|
+
<template v-else>
|
|
174
|
+
<RenderCell :render-fn="col.render" :render-params="{ row: slotProps.row, col }" />
|
|
175
|
+
</template>
|
|
176
|
+
</el-form-item>
|
|
34
177
|
</template>
|
|
178
|
+
<!-- 插槽列 -->
|
|
179
|
+
<template v-else-if="col.component === 'slot' && col.slotName">
|
|
180
|
+
<el-form-item
|
|
181
|
+
:prop="getCellProp(slotProps.row, col)"
|
|
182
|
+
:rules="mergedRules[getCellProp(slotProps.row, col)]"
|
|
183
|
+
class="ecp-pro-table-form__cell-item"
|
|
184
|
+
>
|
|
185
|
+
<slot
|
|
186
|
+
:name="`cell-${col.slotName}`"
|
|
187
|
+
:column="col"
|
|
188
|
+
:row="slotProps.row"
|
|
189
|
+
:index="slotProps.$index"
|
|
190
|
+
:value="getCellValue(slotProps.row, col)"
|
|
191
|
+
:update-value="slotUpdateHandler(slotProps, col)"
|
|
192
|
+
/>
|
|
193
|
+
</el-form-item>
|
|
194
|
+
</template>
|
|
195
|
+
<!-- 内置组件 -->
|
|
35
196
|
<el-form-item
|
|
36
197
|
v-else
|
|
37
|
-
:prop="
|
|
38
|
-
|
|
39
|
-
>
|
|
40
|
-
<el-input
|
|
41
|
-
:value="getCompetitorName(slotProps.row._index)"
|
|
42
|
-
:placeholder="competitorNamePlaceholder"
|
|
43
|
-
@input="setCompetitorName(slotProps.row._index, $event)"
|
|
44
|
-
/>
|
|
45
|
-
</el-form-item>
|
|
46
|
-
</slot>
|
|
47
|
-
</template>
|
|
48
|
-
</el-table-column>
|
|
49
|
-
|
|
50
|
-
<!-- 数据列:内置 input / formatted-number 或插槽 cell-{slotName} -->
|
|
51
|
-
<el-table-column
|
|
52
|
-
v-for="col in columns"
|
|
53
|
-
:key="col.key"
|
|
54
|
-
:min-width="col.minWidth || col.width || 120"
|
|
55
|
-
:width="col.width"
|
|
56
|
-
>
|
|
57
|
-
<template #header>
|
|
58
|
-
<slot :name="'header-' + col.key" :column="col">
|
|
59
|
-
<span class="ecp-pro-table-form__th-text">
|
|
60
|
-
<span v-if="col.required" class="ecp-pro-table-form__req">*</span>{{ col.title }}
|
|
61
|
-
</span>
|
|
62
|
-
</slot>
|
|
63
|
-
</template>
|
|
64
|
-
<template #default="slotProps">
|
|
65
|
-
<!-- 完全自定义列 -->
|
|
66
|
-
<template v-if="col.component === 'slot' && col.slotName">
|
|
67
|
-
<el-form-item
|
|
68
|
-
v-if="slotProps.row._type === 'fixed'"
|
|
69
|
-
:prop="fixedMetricProp(slotProps.row.rowKey, col.key)"
|
|
198
|
+
:prop="getCellProp(slotProps.row, col)"
|
|
199
|
+
:rules="mergedRules[getCellProp(slotProps.row, col)]"
|
|
70
200
|
class="ecp-pro-table-form__cell-item"
|
|
71
201
|
>
|
|
72
|
-
<
|
|
73
|
-
:
|
|
74
|
-
:column="col"
|
|
75
|
-
:row="slotProps.row"
|
|
202
|
+
<CellEditor
|
|
203
|
+
:col="col"
|
|
76
204
|
:value="getCellValue(slotProps.row, col)"
|
|
77
|
-
:update-value="slotUpdateHandler(slotProps, col)"
|
|
78
|
-
/>
|
|
79
|
-
</el-form-item>
|
|
80
|
-
<el-form-item
|
|
81
|
-
v-else
|
|
82
|
-
:prop="competitorMetricProp(slotProps.row._index, col.key)"
|
|
83
|
-
class="ecp-pro-table-form__cell-item"
|
|
84
|
-
>
|
|
85
|
-
<slot
|
|
86
|
-
:name="'cell-' + col.slotName"
|
|
87
|
-
:column="col"
|
|
88
205
|
:row="slotProps.row"
|
|
89
|
-
:
|
|
90
|
-
:update-value="slotUpdateHandler(slotProps, col)"
|
|
91
|
-
/>
|
|
92
|
-
</el-form-item>
|
|
93
|
-
</template>
|
|
94
|
-
<!-- 内置组件 -->
|
|
95
|
-
<template v-else>
|
|
96
|
-
<el-form-item
|
|
97
|
-
v-if="slotProps.row._type === 'fixed'"
|
|
98
|
-
:prop="fixedMetricProp(slotProps.row.rowKey, col.key)"
|
|
99
|
-
class="ecp-pro-table-form__cell-item"
|
|
100
|
-
>
|
|
101
|
-
<component
|
|
102
|
-
:is="cellComponent(col)"
|
|
103
|
-
:value="getFixedMetric(slotProps.row.rowKey, col.key)"
|
|
104
|
-
v-bind="cellBind(col)"
|
|
206
|
+
:size="size"
|
|
105
207
|
:placeholder="col.placeholder || metricPlaceholder"
|
|
106
|
-
@
|
|
107
|
-
/>
|
|
108
|
-
</el-form-item>
|
|
109
|
-
<el-form-item
|
|
110
|
-
v-else
|
|
111
|
-
:prop="competitorMetricProp(slotProps.row._index, col.key)"
|
|
112
|
-
class="ecp-pro-table-form__cell-item"
|
|
113
|
-
>
|
|
114
|
-
<component
|
|
115
|
-
:is="cellComponent(col)"
|
|
116
|
-
:value="getCompetitorMetric(slotProps.row._index, col.key)"
|
|
117
|
-
v-bind="cellBind(col)"
|
|
118
|
-
:placeholder="col.placeholder || metricPlaceholder"
|
|
119
|
-
@input="setCompetitorMetric(slotProps.row._index, col.key, $event)"
|
|
208
|
+
@update="setCellValue(slotProps.row, col, undefined, $event)"
|
|
120
209
|
/>
|
|
121
210
|
</el-form-item>
|
|
122
211
|
</template>
|
|
123
|
-
</
|
|
124
|
-
</
|
|
212
|
+
</el-table-column>
|
|
213
|
+
</template>
|
|
125
214
|
|
|
126
|
-
<!--
|
|
127
|
-
<
|
|
128
|
-
<template #header>
|
|
129
|
-
<slot name="actionHeader">
|
|
130
|
-
<span v-if="actionColumn?.title" class="ecp-pro-table-form__action-title">{{ actionColumn.title }}</span>
|
|
131
|
-
<el-button type="text" class="ecp-pro-table-form__add-btn" @click="addCompetitor">
|
|
132
|
-
{{ addCompetitorText }}
|
|
133
|
-
</el-button>
|
|
134
|
-
</slot>
|
|
135
|
-
</template>
|
|
136
|
-
<template #default="slotProps">
|
|
137
|
-
<slot
|
|
138
|
-
name="action"
|
|
139
|
-
:row="slotProps.row"
|
|
140
|
-
:can-delete="canDeleteCompetitor"
|
|
141
|
-
:add-competitor="addCompetitor"
|
|
142
|
-
:remove-competitor="removeCompetitor"
|
|
143
|
-
>
|
|
144
|
-
<el-button
|
|
145
|
-
v-if="slotProps.row._type === 'competitor'"
|
|
146
|
-
type="text"
|
|
147
|
-
class="ecp-pro-table-form__del-btn"
|
|
148
|
-
:disabled="!canDeleteCompetitor"
|
|
149
|
-
@click="removeCompetitor(slotProps.row._index)"
|
|
150
|
-
>
|
|
151
|
-
删除
|
|
152
|
-
</el-button>
|
|
153
|
-
<el-button v-else type="text" class="ecp-pro-table-form__del-btn" disabled>
|
|
154
|
-
删除
|
|
155
|
-
</el-button>
|
|
156
|
-
</slot>
|
|
157
|
-
</template>
|
|
158
|
-
</el-table-column>
|
|
215
|
+
<!-- 操作列 -->
|
|
216
|
+
<slot name="action" :add-row="handleAddRow" :remove-row="handleRemoveRow" />
|
|
159
217
|
</el-table>
|
|
160
218
|
</el-form>
|
|
161
219
|
</div>
|
|
162
220
|
</template>
|
|
163
221
|
|
|
164
222
|
<script lang="ts">
|
|
165
|
-
/**
|
|
166
|
-
* Vue 2 默认 v-model 绑定 value + input;本组件使用 modelValue(与 Vue 3 一致),需显式声明 model。
|
|
167
|
-
*/
|
|
168
223
|
export default {
|
|
169
224
|
name: 'ProTableForm',
|
|
170
225
|
model: {
|
|
@@ -176,335 +231,190 @@ export default {
|
|
|
176
231
|
|
|
177
232
|
<script setup lang="ts">
|
|
178
233
|
import { computed, ref } from 'vue'
|
|
179
|
-
import
|
|
180
|
-
import
|
|
234
|
+
import { useProTableForm } from './useProTableForm'
|
|
235
|
+
import { defineComponent } from 'vue'
|
|
236
|
+
import type { VNode } from 'vue'
|
|
237
|
+
import type {
|
|
238
|
+
ProTableFormColumn,
|
|
239
|
+
ProTableFormColumnChild,
|
|
240
|
+
ProTableFormColumnRender,
|
|
241
|
+
} from '../types'
|
|
242
|
+
import CellEditor from './CellEditor.vue'
|
|
243
|
+
|
|
244
|
+
/** 与 useProTableForm 内部 tableRows 元素类型对齐 */
|
|
245
|
+
type TableRow = { _index: number }
|
|
246
|
+
|
|
247
|
+
/** RenderCell:接收 render 函数,在 td 内渲染 VNode,完全脱离 el-table 插槽作用域 */
|
|
248
|
+
const RenderCell = defineComponent({
|
|
249
|
+
name: 'RenderCell',
|
|
250
|
+
props: {
|
|
251
|
+
renderFn: { type: Function as unknown as () => ProTableFormColumnRender, required: true },
|
|
252
|
+
renderParams: { type: Object as () => Record<string, unknown>, required: true },
|
|
253
|
+
},
|
|
254
|
+
setup(props) {
|
|
255
|
+
return () => {
|
|
256
|
+
const vnode = props.renderFn(props.renderParams as Parameters<ProTableFormColumnRender>[0])
|
|
257
|
+
return Array.isArray(vnode) ? vnode : (vnode as VNode)
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
/** 单次执行 render,返回 { isText, value } */
|
|
263
|
+
function execRenderOnce(
|
|
264
|
+
child: ProTableFormColumn | ProTableFormColumnChild,
|
|
265
|
+
row: Record<string, unknown>,
|
|
266
|
+
col: ProTableFormColumn,
|
|
267
|
+
colKey?: string
|
|
268
|
+
): { isText: boolean; value: ReturnType<NonNullable<typeof child.render>> } {
|
|
269
|
+
const value = colKey !== undefined
|
|
270
|
+
? getCellValue(row as TableRow, col, colKey)
|
|
271
|
+
: getCellValue(row as TableRow, col)
|
|
272
|
+
const result = child.render!({ row, value, column: child })
|
|
273
|
+
const isText = isPrimitive(result)
|
|
274
|
+
return { isText, value: result }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** 行→列key→渲染结果的缓存,避免同一 render 在模板中被多次调用 */
|
|
278
|
+
const _renderCache = new WeakMap<object, Map<string, { isText: boolean; value: unknown }>>()
|
|
279
|
+
|
|
280
|
+
function getRender(
|
|
281
|
+
child: ProTableFormColumn | ProTableFormColumnChild,
|
|
282
|
+
row: Record<string, unknown>,
|
|
283
|
+
col: ProTableFormColumn,
|
|
284
|
+
colKey?: string
|
|
285
|
+
): { isText: boolean; value: unknown } {
|
|
286
|
+
if (!_renderCache.has(row as object)) _renderCache.set(row as object, new Map())
|
|
287
|
+
const cache = _renderCache.get(row as object)!
|
|
288
|
+
const cacheKey = col.key + (colKey ? '.' + colKey : '')
|
|
289
|
+
if (!cache.has(cacheKey)) cache.set(cacheKey, execRenderOnce(child, row, col, colKey))
|
|
290
|
+
return cache.get(cacheKey)!
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** 是否为基础类型(string / number / boolean / null / undefined) */
|
|
294
|
+
function isPrimitive(val: unknown): val is string | number | boolean | null | undefined {
|
|
295
|
+
return val === null || typeof val in { string: 1, number: 1, boolean: 1, bigint: 1, symbol: 1 }
|
|
296
|
+
}
|
|
181
297
|
|
|
182
298
|
const props = withDefaults(
|
|
183
299
|
defineProps<{
|
|
184
|
-
modelValue?: Record<string, unknown>
|
|
300
|
+
modelValue?: Record<string, unknown>[]
|
|
185
301
|
columns: ProTableFormColumn[]
|
|
186
|
-
fixedRows: ProTableFormFixedRow[]
|
|
187
|
-
competitorsKey?: string
|
|
188
|
-
competitorNameKey?: string
|
|
189
|
-
firstColumnTitle?: string
|
|
190
|
-
competitorNamePlaceholder?: string
|
|
191
302
|
metricPlaceholder?: string
|
|
192
|
-
|
|
193
|
-
minCompetitors?: number
|
|
303
|
+
minRows?: number
|
|
194
304
|
rules?: Record<string, unknown>
|
|
195
305
|
labelWidth?: string
|
|
196
306
|
bordered?: boolean
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
307
|
+
spanMethod?: (params: {
|
|
308
|
+
row: Record<string, unknown>
|
|
309
|
+
column: { property: string; label: string }
|
|
310
|
+
rowIndex: number
|
|
311
|
+
columnIndex: number
|
|
312
|
+
}) => [number, number] | { rowspan: number; colspan: number } | void
|
|
313
|
+
// ─── el-form 配置 ────────────────────────────────────────────────
|
|
314
|
+
formSize?: 'medium' | 'small' | 'large'
|
|
315
|
+
labelPosition?: 'left' | 'right' | 'top'
|
|
316
|
+
disabled?: boolean
|
|
317
|
+
// ─── el-table 配置 ────────────────────────────────────────────────
|
|
318
|
+
stripe?: boolean
|
|
319
|
+
size?: 'medium' | 'small' | 'large'
|
|
320
|
+
maxHeight?: number | string
|
|
321
|
+
height?: number | string
|
|
322
|
+
rowClassName?: string | ((params: { row: Record<string, unknown>; rowIndex: number }) => string)
|
|
323
|
+
expandRowKeys?: (string | number)[]
|
|
324
|
+
defaultExpandAll?: boolean
|
|
325
|
+
onRowClick?: (row: Record<string, unknown>, event: Event) => void
|
|
326
|
+
onRowDblclick?: (row: Record<string, unknown>, event: Event) => void
|
|
327
|
+
onSortChange?: (sortInfo: { prop: string; order: string }) => void
|
|
328
|
+
onExpandChange?: (row: Record<string, unknown>, expanded: boolean) => void
|
|
329
|
+
tableProps?: Record<string, unknown>
|
|
330
|
+
formProps?: Record<string, unknown>
|
|
202
331
|
}>(),
|
|
203
332
|
{
|
|
204
|
-
modelValue: () =>
|
|
205
|
-
competitorsKey: 'competitors',
|
|
206
|
-
competitorNameKey: 'name',
|
|
207
|
-
firstColumnTitle: '维度/友商',
|
|
208
|
-
competitorNamePlaceholder: '请输入友商名称',
|
|
333
|
+
modelValue: () => [],
|
|
209
334
|
metricPlaceholder: '请输入',
|
|
210
|
-
|
|
211
|
-
minCompetitors: 0,
|
|
335
|
+
minRows: 0,
|
|
212
336
|
labelWidth: '0',
|
|
213
337
|
bordered: true,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
showActionColumn: true,
|
|
338
|
+
stripe: false,
|
|
339
|
+
size: 'medium',
|
|
340
|
+
defaultExpandAll: false,
|
|
218
341
|
}
|
|
219
342
|
)
|
|
220
343
|
|
|
221
344
|
const emit = defineEmits<{
|
|
222
|
-
(e: 'update:modelValue', v: Record<string, unknown>): void
|
|
345
|
+
(e: 'update:modelValue', v: Record<string, unknown>[]): void
|
|
346
|
+
(e: 'add-row'): void
|
|
347
|
+
(e: 'remove-row', index: number): void
|
|
348
|
+
(e: 'register', action: Record<string, unknown>): void
|
|
223
349
|
}>()
|
|
224
350
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
function rowKeyFn(row: TableRow) {
|
|
249
|
-
return row._type === 'fixed' ? `f-${row.rowKey}` : `c-${row._index}`
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
type TableRow =
|
|
253
|
-
| { _type: 'fixed'; rowKey: string; rowLabel: string }
|
|
254
|
-
| { _type: 'competitor'; _index: number }
|
|
255
|
-
|
|
256
|
-
const tableRows = computed<TableRow[]>(() => {
|
|
257
|
-
const rows: TableRow[] = []
|
|
258
|
-
props.fixedRows.forEach((fr) => {
|
|
259
|
-
rows.push({
|
|
260
|
-
_type: 'fixed',
|
|
261
|
-
rowKey: fr.rowKey,
|
|
262
|
-
rowLabel: fr.label,
|
|
263
|
-
})
|
|
264
|
-
})
|
|
265
|
-
const mv = props.modelValue
|
|
266
|
-
const list = (mv && typeof mv === 'object' ? (mv[ck()] as Record<string, unknown>[] | undefined) : undefined) ?? []
|
|
267
|
-
list.forEach((_, i) => {
|
|
268
|
-
rows.push({ _type: 'competitor', _index: i })
|
|
269
|
-
})
|
|
270
|
-
return rows
|
|
351
|
+
const {
|
|
352
|
+
formModelRef,
|
|
353
|
+
currentModelValue,
|
|
354
|
+
mergedRules,
|
|
355
|
+
tableRows,
|
|
356
|
+
rowKeyFn,
|
|
357
|
+
spanMethodAdapter,
|
|
358
|
+
formRef,
|
|
359
|
+
slotUpdateHandler,
|
|
360
|
+
getCellProp,
|
|
361
|
+
getCellValue,
|
|
362
|
+
setCellValue,
|
|
363
|
+
handleAddRow,
|
|
364
|
+
handleRemoveRow,
|
|
365
|
+
validate,
|
|
366
|
+
clearValidate,
|
|
367
|
+
} = useProTableForm({
|
|
368
|
+
props,
|
|
369
|
+
emit,
|
|
370
|
+
emitAddRow: () => emit('add-row'),
|
|
371
|
+
emitRemoveRow: (index) => emit('remove-row', index),
|
|
271
372
|
})
|
|
272
373
|
|
|
273
|
-
|
|
274
|
-
const mv = props.modelValue
|
|
275
|
-
const n = ((mv && typeof mv === 'object' ? (mv[ck()] as unknown[]) : undefined) ?? []).length
|
|
276
|
-
return n > props.minCompetitors
|
|
277
|
-
})
|
|
374
|
+
void formRef
|
|
278
375
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return {}
|
|
283
|
-
}
|
|
284
|
-
return JSON.parse(JSON.stringify(m)) as Record<string, unknown>
|
|
376
|
+
/** 表格行点击 */
|
|
377
|
+
function handleTableRowClick(row: TableRow, _column: unknown, event: Event) {
|
|
378
|
+
props.onRowClick?.(row as Record<string, unknown>, event)
|
|
285
379
|
}
|
|
286
|
-
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
if (!mv || typeof mv !== 'object') {
|
|
290
|
-
const o: Record<string, unknown> = {}
|
|
291
|
-
for (const c of props.columns) {
|
|
292
|
-
o[c.key] = ''
|
|
293
|
-
}
|
|
294
|
-
return o
|
|
295
|
-
}
|
|
296
|
-
const m = mv[rowKey]
|
|
297
|
-
if (m && typeof m === 'object' && !Array.isArray(m)) return m as Record<string, unknown>
|
|
298
|
-
const o: Record<string, unknown> = {}
|
|
299
|
-
for (const c of props.columns) {
|
|
300
|
-
o[c.key] = ''
|
|
301
|
-
}
|
|
302
|
-
return o
|
|
380
|
+
/** 表格行双击 */
|
|
381
|
+
function handleTableRowDblclick(row: TableRow, _column: unknown, event: Event) {
|
|
382
|
+
props.onRowDblclick?.(row as Record<string, unknown>, event)
|
|
303
383
|
}
|
|
304
|
-
|
|
305
|
-
function
|
|
306
|
-
|
|
384
|
+
/** 表格排序变化 */
|
|
385
|
+
function handleTableSortChange(sortInfo: { prop: string; order: string }) {
|
|
386
|
+
props.onSortChange?.(sortInfo)
|
|
307
387
|
}
|
|
308
|
-
|
|
309
|
-
function
|
|
310
|
-
|
|
311
|
-
return block[key] ?? ''
|
|
388
|
+
/** 表格展开变化 */
|
|
389
|
+
function handleTableExpandChange(row: TableRow, expanded: boolean) {
|
|
390
|
+
props.onExpandChange?.(row as Record<string, unknown>, expanded)
|
|
312
391
|
}
|
|
313
392
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function competitorList(): Record<string, unknown>[] {
|
|
323
|
-
const mv = props.modelValue
|
|
324
|
-
if (!mv || typeof mv !== 'object') {
|
|
325
|
-
return []
|
|
326
|
-
}
|
|
327
|
-
const list = mv[ck()]
|
|
328
|
-
if (!Array.isArray(list)) return []
|
|
329
|
-
return list as Record<string, unknown>[]
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function getCompetitorName(index: number): string {
|
|
333
|
-
const row = competitorList()[index]
|
|
334
|
-
const key = nk()
|
|
335
|
-
return row ? String(row[key] ?? '') : ''
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function setCompetitorName(index: number, val: string) {
|
|
339
|
-
const next = cloneModel()
|
|
340
|
-
const list = [...competitorList()]
|
|
341
|
-
const row = { ...(list[index] || {}) }
|
|
342
|
-
row[nk()] = val
|
|
343
|
-
list[index] = row
|
|
344
|
-
next[ck()] = list
|
|
345
|
-
emitNext(next)
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function getCompetitorMetric(index: number, key: string): unknown {
|
|
349
|
-
const row = competitorList()[index]
|
|
350
|
-
return row ? row[key] ?? '' : ''
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function setCompetitorMetric(index: number, key: string, val: unknown) {
|
|
354
|
-
const next = cloneModel()
|
|
355
|
-
const list = [...competitorList()]
|
|
356
|
-
const row = { ...(list[index] || {}) }
|
|
357
|
-
row[key] = val
|
|
358
|
-
list[index] = row
|
|
359
|
-
next[ck()] = list
|
|
360
|
-
emitNext(next)
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function getCellValue(tableRow: TableRow, col: ProTableFormColumn): unknown {
|
|
364
|
-
if (tableRow._type === 'fixed') {
|
|
365
|
-
return getFixedMetric(tableRow.rowKey, col.key)
|
|
366
|
-
}
|
|
367
|
-
return getCompetitorMetric(tableRow._index, col.key)
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function setCellValue(tableRow: TableRow, col: ProTableFormColumn, val: unknown) {
|
|
371
|
-
if (tableRow._type === 'fixed') {
|
|
372
|
-
setFixedMetric(tableRow.rowKey, col.key, val)
|
|
373
|
-
} else {
|
|
374
|
-
setCompetitorMetric(tableRow._index, col.key, val)
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/** 供插槽列 update-value 绑定,避免模板内箭头参数隐式 any */
|
|
379
|
-
function slotUpdateHandler(slotProps: { row: TableRow }, col: ProTableFormColumn) {
|
|
380
|
-
return (v: unknown) => setCellValue(slotProps.row, col, v)
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function emptyCompetitorRow(): Record<string, unknown> {
|
|
384
|
-
const o: Record<string, unknown> = { [nk()]: '' }
|
|
385
|
-
for (const c of props.columns) {
|
|
386
|
-
o[c.key] = ''
|
|
387
|
-
}
|
|
388
|
-
return o
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function addCompetitor() {
|
|
392
|
-
const next = cloneModel()
|
|
393
|
-
const list = [...competitorList()]
|
|
394
|
-
list.push(emptyCompetitorRow())
|
|
395
|
-
next[ck()] = list
|
|
396
|
-
emitNext(next)
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function removeCompetitor(index: number) {
|
|
400
|
-
if (!canDeleteCompetitor.value) return
|
|
401
|
-
const next = cloneModel()
|
|
402
|
-
const list = [...competitorList()]
|
|
403
|
-
list.splice(index, 1)
|
|
404
|
-
next[ck()] = list
|
|
405
|
-
emitNext(next)
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function fixedMetricProp(rowKey: string, key: string) {
|
|
409
|
-
return `${rowKey}.${key}`
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function competitorNameProp(index: number) {
|
|
413
|
-
return `${ck()}.${index}.${nk()}`
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function competitorMetricProp(index: number, key: string) {
|
|
417
|
-
return `${ck()}.${index}.${key}`
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function cellComponent(col: ProTableFormColumn) {
|
|
421
|
-
return col.component === 'formatted-number' ? FormattedNumberInput : 'el-input'
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function cellBind(col: ProTableFormColumn): Record<string, unknown> {
|
|
425
|
-
const cp = col.componentProps || {}
|
|
426
|
-
if (col.component === 'formatted-number') {
|
|
427
|
-
return {
|
|
428
|
-
integerDigits: 5,
|
|
429
|
-
decimalPlaces: 6,
|
|
430
|
-
rounding: 'round',
|
|
431
|
-
inputLimit: true,
|
|
432
|
-
...cp,
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
return { ...cp }
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function firstColumnScope(slotProps: { row: TableRow }) {
|
|
439
|
-
const r = slotProps.row
|
|
440
|
-
if (r._type === 'fixed') {
|
|
441
|
-
return {
|
|
442
|
-
row: r,
|
|
443
|
-
rowType: 'fixed' as const,
|
|
444
|
-
rowKey: r.rowKey,
|
|
445
|
-
rowLabel: r.rowLabel,
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
const idx = r._index
|
|
449
|
-
return {
|
|
450
|
-
row: r,
|
|
451
|
-
rowType: 'competitor' as const,
|
|
452
|
-
rowIndex: idx,
|
|
453
|
-
value: getCompetitorName(idx),
|
|
454
|
-
updateValue: (v: string) => setCompetitorName(idx, v),
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const mergedRules = computed(() => {
|
|
459
|
-
const r: Record<string, unknown[]> = {}
|
|
460
|
-
const req = (title: string) => [{ required: true, message: `请输入${title}`, trigger: 'blur' }]
|
|
461
|
-
|
|
462
|
-
for (const fr of props.fixedRows) {
|
|
463
|
-
for (const col of props.columns) {
|
|
464
|
-
if (col.rules) {
|
|
465
|
-
r[`${fr.rowKey}.${col.key}`] = col.rules as unknown[]
|
|
466
|
-
} else if (col.required) {
|
|
467
|
-
r[`${fr.rowKey}.${col.key}`] = req(col.title)
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
const list = competitorList()
|
|
472
|
-
list.forEach((_, i) => {
|
|
473
|
-
r[`${ck()}.${i}.${nk()}`] = req('友商名称')
|
|
474
|
-
for (const col of props.columns) {
|
|
475
|
-
if (col.rules) {
|
|
476
|
-
r[`${ck()}.${i}.${col.key}`] = col.rules as unknown[]
|
|
477
|
-
} else if (col.required) {
|
|
478
|
-
r[`${ck()}.${i}.${col.key}`] = req(col.title)
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
})
|
|
482
|
-
return { ...r, ...(props.rules || {}) }
|
|
393
|
+
/** 将 columns 扁平化,保留 _key 用于 template v-for key */
|
|
394
|
+
const flatColumns = computed<(ProTableFormColumn & { _key: string })[]>(() => {
|
|
395
|
+
return props.columns.map((col, i) => ({
|
|
396
|
+
...col,
|
|
397
|
+
_key: col.key || `__col-${i}`,
|
|
398
|
+
}))
|
|
483
399
|
})
|
|
484
400
|
|
|
485
|
-
|
|
486
|
-
return new Promise((resolve) => {
|
|
487
|
-
const f = formRef.value
|
|
488
|
-
if (!f || typeof f.validate !== 'function') {
|
|
489
|
-
resolve(true)
|
|
490
|
-
return
|
|
491
|
-
}
|
|
492
|
-
f.validate((valid: boolean) => {
|
|
493
|
-
resolve(valid)
|
|
494
|
-
})
|
|
495
|
-
})
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function clearValidate(propsArg?: string | string[]) {
|
|
499
|
-
formRef.value?.clearValidate?.(propsArg)
|
|
500
|
-
}
|
|
401
|
+
const tableRef = ref<{ clearSelection: () => void } | null>(null)
|
|
501
402
|
|
|
502
|
-
|
|
403
|
+
const action = {
|
|
503
404
|
validate,
|
|
504
405
|
clearValidate,
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
406
|
+
addRow: handleAddRow,
|
|
407
|
+
removeRow: handleRemoveRow,
|
|
408
|
+
getRows: () => [...currentModelValue.value],
|
|
409
|
+
getRowCount: () => currentModelValue.value.length,
|
|
410
|
+
getTable: () => tableRef.value,
|
|
411
|
+
getModelValue: () => [...currentModelValue.value],
|
|
412
|
+
setModelValue: (val: Record<string, unknown>[]) => emit('update:modelValue', val),
|
|
413
|
+
getFormRef: () => formRef.value,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
defineExpose(action)
|
|
417
|
+
emit('register', action)
|
|
508
418
|
</script>
|
|
509
419
|
|
|
510
420
|
<style scoped>
|
|
@@ -518,10 +428,9 @@ defineExpose({
|
|
|
518
428
|
margin-left: 0 !important;
|
|
519
429
|
line-height: normal;
|
|
520
430
|
}
|
|
521
|
-
.ecp-pro-table-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
431
|
+
/* .ecp-pro-table-form__cell-item :deep(.el-form-item__error) {
|
|
432
|
+
display: none;
|
|
433
|
+
} */
|
|
525
434
|
.ecp-pro-table-form__req {
|
|
526
435
|
color: #f56c6c;
|
|
527
436
|
margin-right: 2px;
|
|
@@ -530,26 +439,13 @@ defineExpose({
|
|
|
530
439
|
font-weight: 500;
|
|
531
440
|
color: #606266;
|
|
532
441
|
}
|
|
533
|
-
.ecp-pro-table-form__action-title {
|
|
534
|
-
margin-right: 8px;
|
|
535
|
-
font-size: 13px;
|
|
536
|
-
color: #606266;
|
|
537
|
-
}
|
|
538
|
-
.ecp-pro-table-form__add-btn {
|
|
539
|
-
padding: 0;
|
|
540
|
-
font-size: 14px;
|
|
541
|
-
}
|
|
542
|
-
.ecp-pro-table-form__del-btn {
|
|
543
|
-
padding: 0;
|
|
544
|
-
color: #909399;
|
|
545
|
-
}
|
|
546
|
-
.ecp-pro-table-form__del-btn:not(:disabled) {
|
|
547
|
-
color: #409eff;
|
|
548
|
-
}
|
|
549
442
|
</style>
|
|
550
443
|
|
|
551
444
|
<style>
|
|
552
445
|
.ecp-pro-table-form .ecp-pro-table-form__header-cell {
|
|
553
446
|
background: #f5f7fa !important;
|
|
554
447
|
}
|
|
448
|
+
.ecp-pro-table-form__cell-item .el-form-item__error {
|
|
449
|
+
position: relative;
|
|
450
|
+
}
|
|
555
451
|
</style>
|