@aspire-ui/element-component-pro 1.0.12 → 1.0.14
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/ProTable/ProTable.vue.d.ts +7 -1
- package/dist/ProTable/types/index.d.ts +1 -0
- package/dist/ProTableForm/ProTableForm.vue.d.ts +164 -0
- package/dist/ProTableForm/index.d.ts +5 -0
- package/dist/ProTableForm/types.d.ts +69 -0
- package/dist/element-component-pro.es.js +1202 -912
- 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 +782 -243
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/useComponentSetting.d.ts +9 -4
- package/package.json +1 -1
- package/src/ProDescriptions/ProDescriptions.vue +2 -2
- package/src/ProForm/FormattedNumberInput.vue +1 -1
- package/src/ProTable/ProTable.vue +36 -5
- package/src/ProTable/types/index.ts +1 -0
- package/src/ProTableForm/ProTableForm.vue +555 -0
- package/src/ProTableForm/index.ts +9 -0
- package/src/ProTableForm/types.ts +72 -0
- package/src/index.ts +10 -0
- package/src/types/index.ts +1 -0
- package/src/useComponentSetting.ts +63 -8
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="ecp-pro-table-form">
|
|
3
|
+
<el-form
|
|
4
|
+
ref="formRef"
|
|
5
|
+
:model="modelValue"
|
|
6
|
+
:rules="mergedRules"
|
|
7
|
+
:label-width="labelWidth"
|
|
8
|
+
class="ecp-pro-table-form__form"
|
|
9
|
+
>
|
|
10
|
+
<el-table
|
|
11
|
+
:data="tableRows"
|
|
12
|
+
:border="bordered"
|
|
13
|
+
:row-key="rowKeyFn"
|
|
14
|
+
header-cell-class-name="ecp-pro-table-form__header-cell"
|
|
15
|
+
class="ecp-pro-table-form__table"
|
|
16
|
+
>
|
|
17
|
+
<!-- 首列:维度 / 友商名称(可插槽自定义) -->
|
|
18
|
+
<el-table-column
|
|
19
|
+
v-if="showFirstColumnComputed"
|
|
20
|
+
:min-width="firstColMinWidth"
|
|
21
|
+
:fixed="firstColumnFixed"
|
|
22
|
+
>
|
|
23
|
+
<template #header>
|
|
24
|
+
<slot name="firstColumnHeader">
|
|
25
|
+
<span class="ecp-pro-table-form__th-text">
|
|
26
|
+
<span class="ecp-pro-table-form__req">*</span>{{ firstColumnTitle }}
|
|
27
|
+
</span>
|
|
28
|
+
</slot>
|
|
29
|
+
</template>
|
|
30
|
+
<template #default="slotProps">
|
|
31
|
+
<slot name="firstColumn" v-bind="firstColumnScope(slotProps)">
|
|
32
|
+
<template v-if="slotProps.row._type === 'fixed'">
|
|
33
|
+
<span class="ecp-pro-table-form__fixed-label">{{ slotProps.row.rowLabel }}</span>
|
|
34
|
+
</template>
|
|
35
|
+
<el-form-item
|
|
36
|
+
v-else
|
|
37
|
+
:prop="competitorNameProp(slotProps.row._index)"
|
|
38
|
+
class="ecp-pro-table-form__cell-item"
|
|
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)"
|
|
70
|
+
class="ecp-pro-table-form__cell-item"
|
|
71
|
+
>
|
|
72
|
+
<slot
|
|
73
|
+
:name="'cell-' + col.slotName"
|
|
74
|
+
:column="col"
|
|
75
|
+
:row="slotProps.row"
|
|
76
|
+
: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
|
+
:row="slotProps.row"
|
|
89
|
+
:value="getCellValue(slotProps.row, col)"
|
|
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)"
|
|
105
|
+
:placeholder="col.placeholder || metricPlaceholder"
|
|
106
|
+
@input="setFixedMetric(slotProps.row.rowKey, col.key, $event)"
|
|
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)"
|
|
120
|
+
/>
|
|
121
|
+
</el-form-item>
|
|
122
|
+
</template>
|
|
123
|
+
</template>
|
|
124
|
+
</el-table-column>
|
|
125
|
+
|
|
126
|
+
<!-- 操作列:表头 / 行内均可插槽 -->
|
|
127
|
+
<el-table-column v-if="showActionColumn" v-bind="actionColumnBind">
|
|
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>
|
|
159
|
+
</el-table>
|
|
160
|
+
</el-form>
|
|
161
|
+
</div>
|
|
162
|
+
</template>
|
|
163
|
+
|
|
164
|
+
<script lang="ts">
|
|
165
|
+
/**
|
|
166
|
+
* Vue 2 默认 v-model 绑定 value + input;本组件使用 modelValue(与 Vue 3 一致),需显式声明 model。
|
|
167
|
+
*/
|
|
168
|
+
export default {
|
|
169
|
+
name: 'ProTableForm',
|
|
170
|
+
model: {
|
|
171
|
+
prop: 'modelValue',
|
|
172
|
+
event: 'update:modelValue',
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
</script>
|
|
176
|
+
|
|
177
|
+
<script setup lang="ts">
|
|
178
|
+
import { computed, ref } from 'vue'
|
|
179
|
+
import FormattedNumberInput from '../ProForm/FormattedNumberInput.vue'
|
|
180
|
+
import type { ProTableFormActionColumn, ProTableFormColumn, ProTableFormFixedRow } from './types'
|
|
181
|
+
|
|
182
|
+
const props = withDefaults(
|
|
183
|
+
defineProps<{
|
|
184
|
+
modelValue?: Record<string, unknown>
|
|
185
|
+
columns: ProTableFormColumn[]
|
|
186
|
+
fixedRows: ProTableFormFixedRow[]
|
|
187
|
+
competitorsKey?: string
|
|
188
|
+
competitorNameKey?: string
|
|
189
|
+
firstColumnTitle?: string
|
|
190
|
+
competitorNamePlaceholder?: string
|
|
191
|
+
metricPlaceholder?: string
|
|
192
|
+
addCompetitorText?: string
|
|
193
|
+
minCompetitors?: number
|
|
194
|
+
rules?: Record<string, unknown>
|
|
195
|
+
labelWidth?: string
|
|
196
|
+
bordered?: boolean
|
|
197
|
+
firstColMinWidth?: number
|
|
198
|
+
actionWidth?: number
|
|
199
|
+
showFirstColumn?: boolean
|
|
200
|
+
showActionColumn?: boolean
|
|
201
|
+
actionColumn?: ProTableFormActionColumn
|
|
202
|
+
}>(),
|
|
203
|
+
{
|
|
204
|
+
modelValue: () => ({}),
|
|
205
|
+
competitorsKey: 'competitors',
|
|
206
|
+
competitorNameKey: 'name',
|
|
207
|
+
firstColumnTitle: '维度/友商',
|
|
208
|
+
competitorNamePlaceholder: '请输入友商名称',
|
|
209
|
+
metricPlaceholder: '请输入',
|
|
210
|
+
addCompetitorText: '+新增友商',
|
|
211
|
+
minCompetitors: 0,
|
|
212
|
+
labelWidth: '0',
|
|
213
|
+
bordered: true,
|
|
214
|
+
firstColMinWidth: 160,
|
|
215
|
+
actionWidth: 120,
|
|
216
|
+
showFirstColumn: true,
|
|
217
|
+
showActionColumn: true,
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
const emit = defineEmits<{
|
|
222
|
+
(e: 'update:modelValue', v: Record<string, unknown>): void
|
|
223
|
+
}>()
|
|
224
|
+
|
|
225
|
+
const formRef = ref<{ validate: (cb?: (valid: boolean) => void) => Promise<unknown> | void; clearValidate: (p?: string | string[]) => void } | null>(null)
|
|
226
|
+
|
|
227
|
+
const ck = () => props.competitorsKey ?? 'competitors'
|
|
228
|
+
const nk = () => props.competitorNameKey ?? 'name'
|
|
229
|
+
|
|
230
|
+
/** 有固定行时必须保留首列 */
|
|
231
|
+
const showFirstColumnComputed = computed(() => {
|
|
232
|
+
if (props.fixedRows.length > 0) return true
|
|
233
|
+
return props.showFirstColumn !== false
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
const firstColumnFixed = computed(() => 'left' as const)
|
|
237
|
+
|
|
238
|
+
const actionColumnBind = computed(() => {
|
|
239
|
+
const ac = props.actionColumn
|
|
240
|
+
return {
|
|
241
|
+
width: ac?.width ?? props.actionWidth,
|
|
242
|
+
minWidth: ac?.minWidth,
|
|
243
|
+
align: ac?.align ?? 'center',
|
|
244
|
+
fixed: ac?.fixed === undefined ? 'right' : ac.fixed,
|
|
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
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const canDeleteCompetitor = computed(() => {
|
|
274
|
+
const mv = props.modelValue
|
|
275
|
+
const n = ((mv && typeof mv === 'object' ? (mv[ck()] as unknown[]) : undefined) ?? []).length
|
|
276
|
+
return n > props.minCompetitors
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
function cloneModel(): Record<string, unknown> {
|
|
280
|
+
const m = props.modelValue
|
|
281
|
+
if (!m || typeof m !== 'object') {
|
|
282
|
+
return {}
|
|
283
|
+
}
|
|
284
|
+
return JSON.parse(JSON.stringify(m)) as Record<string, unknown>
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function ensureFixedBlock(rowKey: string): Record<string, unknown> {
|
|
288
|
+
const mv = props.modelValue
|
|
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
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function emitNext(next: Record<string, unknown>) {
|
|
306
|
+
emit('update:modelValue', next)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function getFixedMetric(rowKey: string, key: string): unknown {
|
|
310
|
+
const block = ensureFixedBlock(rowKey)
|
|
311
|
+
return block[key] ?? ''
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function setFixedMetric(rowKey: string, key: string, val: unknown) {
|
|
315
|
+
const next = cloneModel()
|
|
316
|
+
const b = { ...((next[rowKey] as Record<string, unknown>) || {}) }
|
|
317
|
+
b[key] = val
|
|
318
|
+
next[rowKey] = b
|
|
319
|
+
emitNext(next)
|
|
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 || {}) }
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
function validate(): Promise<boolean> {
|
|
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
|
+
}
|
|
501
|
+
|
|
502
|
+
defineExpose({
|
|
503
|
+
validate,
|
|
504
|
+
clearValidate,
|
|
505
|
+
addCompetitor,
|
|
506
|
+
removeCompetitor,
|
|
507
|
+
})
|
|
508
|
+
</script>
|
|
509
|
+
|
|
510
|
+
<style scoped>
|
|
511
|
+
.ecp-pro-table-form__form :deep(.el-form-item) {
|
|
512
|
+
margin-bottom: 0;
|
|
513
|
+
}
|
|
514
|
+
.ecp-pro-table-form__cell-item {
|
|
515
|
+
width: 100%;
|
|
516
|
+
}
|
|
517
|
+
.ecp-pro-table-form__cell-item :deep(.el-form-item__content) {
|
|
518
|
+
margin-left: 0 !important;
|
|
519
|
+
line-height: normal;
|
|
520
|
+
}
|
|
521
|
+
.ecp-pro-table-form__fixed-label {
|
|
522
|
+
color: #303133;
|
|
523
|
+
font-size: 14px;
|
|
524
|
+
}
|
|
525
|
+
.ecp-pro-table-form__req {
|
|
526
|
+
color: #f56c6c;
|
|
527
|
+
margin-right: 2px;
|
|
528
|
+
}
|
|
529
|
+
.ecp-pro-table-form__th-text {
|
|
530
|
+
font-weight: 500;
|
|
531
|
+
color: #606266;
|
|
532
|
+
}
|
|
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
|
+
</style>
|
|
550
|
+
|
|
551
|
+
<style>
|
|
552
|
+
.ecp-pro-table-form .ecp-pro-table-form__header-cell {
|
|
553
|
+
background: #f5f7fa !important;
|
|
554
|
+
}
|
|
555
|
+
</style>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** 指标列(或含友商名称等任意 key) */
|
|
2
|
+
export interface ProTableFormColumn {
|
|
3
|
+
/** 对应 model 中对象字段名 */
|
|
4
|
+
key: string
|
|
5
|
+
title: string
|
|
6
|
+
required?: boolean
|
|
7
|
+
/**
|
|
8
|
+
* 单元格渲染方式:
|
|
9
|
+
* - input / formatted-number:内置
|
|
10
|
+
* - slot:使用具名插槽 `cell-{slotName}` 完全自定义
|
|
11
|
+
*/
|
|
12
|
+
component?: 'input' | 'formatted-number' | 'slot'
|
|
13
|
+
/**
|
|
14
|
+
* component 为 slot 时必填,对应插槽名为 `cell-{slotName}`(如 slotName: 'score' → #cell-score)
|
|
15
|
+
*/
|
|
16
|
+
slotName?: string
|
|
17
|
+
placeholder?: string
|
|
18
|
+
width?: number
|
|
19
|
+
minWidth?: number
|
|
20
|
+
/**
|
|
21
|
+
* 透传给单元格组件。`component === 'formatted-number'` 时在此传入
|
|
22
|
+
* `integerDigits`、`decimalPlaces`、`rounding`、`inputLimit` 等(与 FormattedNumberInput / ProForm 一致)。
|
|
23
|
+
*/
|
|
24
|
+
componentProps?: Record<string, unknown>
|
|
25
|
+
/** 覆盖该列默认必填规则(与 Element 表单 rules 一致) */
|
|
26
|
+
rules?: unknown[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 固定行(首列为文案,不可删) */
|
|
30
|
+
export interface ProTableFormFixedRow {
|
|
31
|
+
rowKey: string
|
|
32
|
+
label: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 操作列配置(showActionColumn 为 true 时生效) */
|
|
36
|
+
export interface ProTableFormActionColumn {
|
|
37
|
+
width?: number
|
|
38
|
+
minWidth?: number
|
|
39
|
+
align?: 'left' | 'center' | 'right'
|
|
40
|
+
fixed?: boolean | 'left' | 'right'
|
|
41
|
+
/** 表头文案,使用默认「新增」按钮时显示在按钮左侧,可留空 */
|
|
42
|
+
title?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ProTableFormProps {
|
|
46
|
+
modelValue?: Record<string, unknown>
|
|
47
|
+
columns: ProTableFormColumn[]
|
|
48
|
+
fixedRows: ProTableFormFixedRow[]
|
|
49
|
+
competitorsKey?: string
|
|
50
|
+
/** 友商行「名称」字段,默认 name */
|
|
51
|
+
competitorNameKey?: string
|
|
52
|
+
firstColumnTitle?: string
|
|
53
|
+
competitorNamePlaceholder?: string
|
|
54
|
+
metricPlaceholder?: string
|
|
55
|
+
addCompetitorText?: string
|
|
56
|
+
minCompetitors?: number
|
|
57
|
+
rules?: Record<string, unknown>
|
|
58
|
+
labelWidth?: string
|
|
59
|
+
bordered?: boolean
|
|
60
|
+
firstColMinWidth?: number
|
|
61
|
+
/**
|
|
62
|
+
* 是否展示首列(维度/友商)。存在 fixedRows 时强制为 true。
|
|
63
|
+
* 无固定行且为 false 时隐藏首列,需在 columns 中自行配置名称等字段。
|
|
64
|
+
*/
|
|
65
|
+
showFirstColumn?: boolean
|
|
66
|
+
/** 是否展示操作列,默认 true */
|
|
67
|
+
showActionColumn?: boolean
|
|
68
|
+
/** 操作列宽度(showActionColumn 时) */
|
|
69
|
+
actionWidth?: number
|
|
70
|
+
/** 操作列详细配置 */
|
|
71
|
+
actionColumn?: ProTableFormActionColumn
|
|
72
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { VueConstructor } from 'vue'
|
|
|
2
2
|
import ProTable, { TableAction } from './ProTable'
|
|
3
3
|
import ProForm, { ProFormItem, FormActions, FormattedNumberInput } from './ProForm'
|
|
4
4
|
import ProDescriptions from './ProDescriptions'
|
|
5
|
+
import { ProTableForm } from './ProTableForm'
|
|
5
6
|
import { useForm } from './ProForm/useForm'
|
|
6
7
|
import { useDescription } from './ProDescriptions/useDescription'
|
|
7
8
|
import { useProTable } from './ProTable/useProTable'
|
|
@@ -10,6 +11,13 @@ import { useComponentSetting } from './useComponentSetting'
|
|
|
10
11
|
export { ProForm, ProFormItem, FormActions, FormattedNumberInput, useForm }
|
|
11
12
|
export { ProTable, useProTable, TableAction }
|
|
12
13
|
export { ProDescriptions, useDescription }
|
|
14
|
+
export { ProTableForm }
|
|
15
|
+
export type {
|
|
16
|
+
ProTableFormColumn,
|
|
17
|
+
ProTableFormFixedRow,
|
|
18
|
+
ProTableFormProps,
|
|
19
|
+
ProTableFormActionColumn,
|
|
20
|
+
} from './ProTableForm/types'
|
|
13
21
|
export { useComponentSetting }
|
|
14
22
|
export type { UseComponentSettingReturn } from './useComponentSetting'
|
|
15
23
|
export type { UseProTableReturn, UseProTablePropsReactive } from './ProTable/useProTable'
|
|
@@ -26,6 +34,7 @@ const components = [
|
|
|
26
34
|
{ name: 'FormActions', component: FormActions },
|
|
27
35
|
{ name: 'FormattedNumberInput', component: FormattedNumberInput },
|
|
28
36
|
{ name: 'ProDescriptions', component: ProDescriptions },
|
|
37
|
+
{ name: 'ProTableForm', component: ProTableForm },
|
|
29
38
|
]
|
|
30
39
|
|
|
31
40
|
export function install(Vue: VueConstructor) {
|
|
@@ -41,4 +50,5 @@ export default {
|
|
|
41
50
|
ProDescriptions,
|
|
42
51
|
TableAction,
|
|
43
52
|
FormattedNumberInput,
|
|
53
|
+
ProTableForm,
|
|
44
54
|
}
|
package/src/types/index.ts
CHANGED