vue3-smart-table 1.0.2 → 1.0.3
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 +1209 -169
- package/dist/index.css +1 -0
- package/dist/vue3-smart-table.cjs.js +2 -21
- package/dist/vue3-smart-table.cjs.js.map +1 -0
- package/dist/vue3-smart-table.es.js +560 -397
- package/dist/vue3-smart-table.es.js.map +1 -0
- package/dist/vue3-smart-table.umd.js +3 -0
- package/dist/vue3-smart-table.umd.js.map +1 -0
- package/package.json +19 -6
- package/src/assets/vue.svg +1 -0
- package/src/components/SmartTable/column/index.vue +170 -0
- package/src/components/SmartTable/config.ts +124 -0
- package/src/components/SmartTable/hooks/useOperationColumn.ts +136 -0
- package/src/components/SmartTable/hooks/useTableColumns.ts +143 -0
- package/src/components/SmartTable/index.vue +99 -0
- package/src/components/SmartTable/renderer.ts +173 -0
- package/src/components/SmartTable/renderers/index.ts +307 -0
- package/src/components/SmartTable/renderers/input.vue +31 -0
- package/src/components/SmartTable/renderers/inputNumber.vue +33 -0
- package/src/components/SmartTable/renderers/select.vue +41 -0
- package/src/components/SmartTable/types.ts +229 -0
- package/src/components/SmartTable/utils/path.ts +29 -0
- package/src/index.ts +38 -0
- package/src/types/enhanced.ts +51 -0
- package/dist/vue3-smart-table.css +0 -1
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内置渲染器集合
|
|
3
|
+
* 可以按需引入或批量注册
|
|
4
|
+
*/
|
|
5
|
+
import { h } from 'vue'
|
|
6
|
+
import { ElButton, ElTag, ElImage, ElMessage } from 'element-plus'
|
|
7
|
+
import { DocumentCopy, CopyDocument } from '@element-plus/icons-vue'
|
|
8
|
+
import type { ColumnConfig } from '../types'
|
|
9
|
+
import { getValueByPath } from '../utils/path'
|
|
10
|
+
import { wrapSFCComponent, createFunctionalRenderer } from '../renderer'
|
|
11
|
+
import EditableInput from './input.vue'
|
|
12
|
+
import EditableNumber from './inputNumber.vue'
|
|
13
|
+
import EditableSelect from './select.vue'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 包装 SFC 组件
|
|
17
|
+
*/
|
|
18
|
+
const input = wrapSFCComponent(EditableInput)
|
|
19
|
+
const inputNumber = wrapSFCComponent(EditableNumber)
|
|
20
|
+
const select = wrapSFCComponent(EditableSelect)
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* button 渲染器
|
|
24
|
+
*/
|
|
25
|
+
const button = createFunctionalRenderer((props) => {
|
|
26
|
+
const rp = props.col.renderProps || {}
|
|
27
|
+
const val = getValueByPath(props.row, props.col.key)
|
|
28
|
+
return h(ElButton as any, {
|
|
29
|
+
type: rp.type || 'primary',
|
|
30
|
+
...rp,
|
|
31
|
+
onClick: () => props.onClick?.(props.row, props.col)
|
|
32
|
+
}, () => rp.label || val)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* link 渲染器
|
|
37
|
+
*/
|
|
38
|
+
const link = createFunctionalRenderer((props) => {
|
|
39
|
+
const rp = props.col.renderProps || {}
|
|
40
|
+
const val = getValueByPath(props.row, props.col.key)
|
|
41
|
+
return h('a', {
|
|
42
|
+
href: rp.href || '#',
|
|
43
|
+
target: rp.blank ? '_blank' : '_self',
|
|
44
|
+
style: rp.style || 'color:#409EFF;cursor:pointer;',
|
|
45
|
+
}, rp.label || val)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* html 渲染器
|
|
50
|
+
*/
|
|
51
|
+
const html = createFunctionalRenderer((props) => {
|
|
52
|
+
const val = getValueByPath(props.row, props.col.key)
|
|
53
|
+
return h('div', {
|
|
54
|
+
class: 'line-clamp-2',
|
|
55
|
+
innerHTML: val ?? '',
|
|
56
|
+
...(props.col?.renderProps || {})
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* copy 渲染器
|
|
62
|
+
*/
|
|
63
|
+
const copy = createFunctionalRenderer((props) => {
|
|
64
|
+
const val = getValueByPath(props.row, props.col.key) ?? ''
|
|
65
|
+
const rp = props.col.renderProps ?? {}
|
|
66
|
+
const butStyle = {
|
|
67
|
+
'position': 'absolute',
|
|
68
|
+
'right': '-5px',
|
|
69
|
+
'top': '50%',
|
|
70
|
+
'transform': 'translateY(-50%)',
|
|
71
|
+
'cursor': 'pointer',
|
|
72
|
+
'display': 'none',
|
|
73
|
+
'font-size': '12px',
|
|
74
|
+
'color': rp.iconColor || '#409EFF',
|
|
75
|
+
'user-select': 'none'
|
|
76
|
+
}
|
|
77
|
+
return h('div', {
|
|
78
|
+
class: 'st_copy_wrapper',
|
|
79
|
+
style: 'width: 100%; position: relative; display: inline-block;'
|
|
80
|
+
},
|
|
81
|
+
[
|
|
82
|
+
h('span', {
|
|
83
|
+
class: 'st_copy_text line-clamp-1',
|
|
84
|
+
style: 'padding-right: 10px; display: block;',
|
|
85
|
+
}, val),
|
|
86
|
+
val && h('span', {
|
|
87
|
+
class: 'st_copy_btn',
|
|
88
|
+
style: butStyle,
|
|
89
|
+
title: rp.copyTitle || '复制',
|
|
90
|
+
onClick: () => {
|
|
91
|
+
if (!val) return
|
|
92
|
+
try {
|
|
93
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
94
|
+
navigator.clipboard.writeText(val).then(() => {
|
|
95
|
+
ElMessage.success(rp.successText ?? '复制成功')
|
|
96
|
+
}).catch(() => {
|
|
97
|
+
ElMessage.error(rp.errorText ?? '复制失败')
|
|
98
|
+
})
|
|
99
|
+
} else {
|
|
100
|
+
const textarea = document.createElement('textarea')
|
|
101
|
+
textarea.value = val
|
|
102
|
+
textarea.style.position = 'fixed'
|
|
103
|
+
textarea.style.opacity = '0'
|
|
104
|
+
document.body.appendChild(textarea)
|
|
105
|
+
textarea.select()
|
|
106
|
+
const successful = document.execCommand('copy')
|
|
107
|
+
document.body.removeChild(textarea)
|
|
108
|
+
|
|
109
|
+
if (successful) {
|
|
110
|
+
ElMessage.success(rp.successText ?? '复制成功')
|
|
111
|
+
} else {
|
|
112
|
+
ElMessage.error(rp.errorText ?? '复制失败')
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
ElMessage.error(rp.errorText ?? '复制失败')
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}, [h(DocumentCopy, {
|
|
120
|
+
style: 'width: 1em; height: 1em;'
|
|
121
|
+
})])
|
|
122
|
+
].filter(Boolean)
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* img 渲染器
|
|
128
|
+
*/
|
|
129
|
+
const img = createFunctionalRenderer((props) => {
|
|
130
|
+
const val = getValueByPath(props.row, props.col.key) ?? ''
|
|
131
|
+
const rp = props.col?.renderProps || {}
|
|
132
|
+
|
|
133
|
+
const getImageList = () => {
|
|
134
|
+
if (!val) return []
|
|
135
|
+
if (Array.isArray(val)) {
|
|
136
|
+
return val.filter(item => item && typeof item === 'string')
|
|
137
|
+
}
|
|
138
|
+
return [val]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const imageList = getImageList()
|
|
142
|
+
|
|
143
|
+
if (imageList.length === 0) {
|
|
144
|
+
return rp.placeholder || ''
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const defaultStyle = {
|
|
148
|
+
width: rp.width || '80px',
|
|
149
|
+
height: rp.height || '80px',
|
|
150
|
+
marginRight: imageList.length > 1 ? '4px' : '0',
|
|
151
|
+
...(rp.style || {})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (imageList.length === 1) {
|
|
155
|
+
return h(ElImage, {
|
|
156
|
+
src: imageList[0],
|
|
157
|
+
previewSrcList: rp.previewSrcList || imageList,
|
|
158
|
+
fit: rp.fit || 'contain',
|
|
159
|
+
style: defaultStyle,
|
|
160
|
+
...rp
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return h('div',
|
|
165
|
+
{
|
|
166
|
+
class: 'st_img_wrapper',
|
|
167
|
+
style: 'display: flex; align-items: center; position: relative'
|
|
168
|
+
},
|
|
169
|
+
[
|
|
170
|
+
h(ElImage, {
|
|
171
|
+
src: imageList[0],
|
|
172
|
+
previewSrcList: rp.previewSrcList || imageList,
|
|
173
|
+
fit: rp.fit || 'contain',
|
|
174
|
+
style: defaultStyle,
|
|
175
|
+
...rp
|
|
176
|
+
}),
|
|
177
|
+
imageList.length > 1 && h('span', {
|
|
178
|
+
class: 'st_img_total',
|
|
179
|
+
style: `position: absolute; top: 0; right: 0; `,
|
|
180
|
+
title: `${imageList.length}`
|
|
181
|
+
}, [h(CopyDocument, { style: `width: 1em; height: 1em; ` })])
|
|
182
|
+
]
|
|
183
|
+
)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* dict 渲染器
|
|
188
|
+
*/
|
|
189
|
+
const dict = createFunctionalRenderer((props) => {
|
|
190
|
+
const val = getValueByPath(props.row, props.col.key) ?? ''
|
|
191
|
+
const rp = props.col.renderProps || {}
|
|
192
|
+
const options = rp.options ?? []
|
|
193
|
+
const showValue = rp.showValue ?? false
|
|
194
|
+
|
|
195
|
+
if (val === null || val === undefined || val === '') return ''
|
|
196
|
+
|
|
197
|
+
const values = Array.isArray(val) ? val.map(String) : [String(val)]
|
|
198
|
+
const matchedOptions = options.filter((opt: any) => values.includes(String(opt.value)))
|
|
199
|
+
const unmatched = values.filter(v => !options.some((opt: any) => String(opt.value) === v))
|
|
200
|
+
|
|
201
|
+
const children = matchedOptions.map((item: any, _index: number) => {
|
|
202
|
+
return h(
|
|
203
|
+
ElTag,
|
|
204
|
+
{ key: item.value, type: item.listClass, class: item.cssClass, disableTransitions: true },
|
|
205
|
+
{ default: () => item.label + ' ' }
|
|
206
|
+
)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
if (showValue && unmatched.length > 0) {
|
|
210
|
+
children.push(h('span', {}, unmatched.join(' ')))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return h('div', {}, children)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* map 渲染器
|
|
218
|
+
*/
|
|
219
|
+
const map = createFunctionalRenderer((props) => {
|
|
220
|
+
const val = getValueByPath(props.row, props.col.key) ?? ''
|
|
221
|
+
const options = (props.col.renderProps?.options ?? {}) as Record<string, any>
|
|
222
|
+
return val != null ? options[val] ?? '' : ''
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* formatter 渲染器
|
|
227
|
+
*/
|
|
228
|
+
export function isDataColumn(
|
|
229
|
+
col: ColumnConfig
|
|
230
|
+
): col is any {
|
|
231
|
+
return typeof (col as any).formatter === 'function'
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const formatter = createFunctionalRenderer((props) => {
|
|
235
|
+
const { col, row } = props
|
|
236
|
+
const val = getValueByPath(props.row, props.col.key) ?? ''
|
|
237
|
+
if (isDataColumn(col)) {
|
|
238
|
+
return col.formatter?.(val, row)
|
|
239
|
+
}
|
|
240
|
+
return val ?? ''
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* icon 渲染器
|
|
245
|
+
*/
|
|
246
|
+
const icon = createFunctionalRenderer((props) => {
|
|
247
|
+
const val = getValueByPath(props.row, props.col.key) ?? ''
|
|
248
|
+
const rp = props.col.renderProps || {}
|
|
249
|
+
if (!val) return ''
|
|
250
|
+
// 判断网络图片
|
|
251
|
+
if (/^https?:\/\//.test(val)) {
|
|
252
|
+
return h(ElImage, {
|
|
253
|
+
src: val,
|
|
254
|
+
previewSrcList: [val],
|
|
255
|
+
fit: 'contain',
|
|
256
|
+
style: 'width:40px;height:40px',
|
|
257
|
+
...rp
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
// 判断 svg 源码
|
|
261
|
+
if (/^\s*<svg[\s\S]*<\/svg>\s*$/.test(val)) {
|
|
262
|
+
return h('div', {
|
|
263
|
+
innerHTML: val,
|
|
264
|
+
style: `width:40px;height:40px;display:inline-block;${rp.style || ''}`,
|
|
265
|
+
...rp
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
// 默认当作 iconfont
|
|
269
|
+
return h('i', {
|
|
270
|
+
class: val,
|
|
271
|
+
style: `font-size:20px;${rp.style || ''}`,
|
|
272
|
+
...rp
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 所有内置渲染器
|
|
278
|
+
*/
|
|
279
|
+
export const builtInRenderers = {
|
|
280
|
+
input,
|
|
281
|
+
'input-number': inputNumber,
|
|
282
|
+
select,
|
|
283
|
+
button,
|
|
284
|
+
link,
|
|
285
|
+
html,
|
|
286
|
+
copy,
|
|
287
|
+
img,
|
|
288
|
+
dict,
|
|
289
|
+
map,
|
|
290
|
+
formatter,
|
|
291
|
+
icon,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 安装所有内置渲染器
|
|
296
|
+
*/
|
|
297
|
+
export function registerBuiltInRenderers(registry: { registerMultiple: (renderers: Record<string, any>) => void }) {
|
|
298
|
+
registry.registerMultiple(builtInRenderers)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 创建默认渲染器集合(兼容旧 API)
|
|
303
|
+
* @deprecated 建议使用插件化架构
|
|
304
|
+
*/
|
|
305
|
+
export function createRenderer() {
|
|
306
|
+
return builtInRenderers
|
|
307
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-input
|
|
3
|
+
v-model="value"
|
|
4
|
+
v-bind="{ placeholder: '', size: 'small', clearable: true, ...col.renderProps }"
|
|
5
|
+
@blur="onBlur"
|
|
6
|
+
@keyup.enter="onEnter"
|
|
7
|
+
/>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import { ref, watch } from 'vue'
|
|
12
|
+
import type { ColumnConfig } from '../types'
|
|
13
|
+
import { getValueByPath, setValueByPath } from '../utils/path'
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
readonly row: any
|
|
17
|
+
readonly col: ColumnConfig
|
|
18
|
+
onCellBlur?: (row: any, col: ColumnConfig) => void
|
|
19
|
+
onCellEnter?: (row: any, col: ColumnConfig) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const props = defineProps<Props>()
|
|
23
|
+
const value = ref(getValueByPath(props.row, props.col.key))
|
|
24
|
+
|
|
25
|
+
watch(value, (v) => {
|
|
26
|
+
setValueByPath(props.row, props.col.key, v)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const onBlur = () => props.onCellBlur?.(props.row, props.col)
|
|
30
|
+
const onEnter = () => props.onCellEnter?.(props.row, props.col)
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-input-number
|
|
3
|
+
v-model="value"
|
|
4
|
+
v-bind="{ min: 0, max: 99999, controls: false, size: 'small', ...col.renderProps }"
|
|
5
|
+
@blur="onBlur"
|
|
6
|
+
@keyup.enter="onEnter"
|
|
7
|
+
/>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import { ref, watch } from 'vue'
|
|
12
|
+
import type { ColumnConfig } from '../types'
|
|
13
|
+
import { getValueByPath, setValueByPath } from '../utils/path'
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
readonly row: any
|
|
17
|
+
readonly col: ColumnConfig
|
|
18
|
+
onCellChange?: (row: any, col: ColumnConfig) => void
|
|
19
|
+
onCellBlur?: (row: any, col: ColumnConfig) => void
|
|
20
|
+
onCellEnter?: (row: any, col: ColumnConfig) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const props = defineProps<Props>()
|
|
24
|
+
const value = ref(getValueByPath(props.row, props.col.key))
|
|
25
|
+
|
|
26
|
+
watch(value, (v) => {
|
|
27
|
+
setValueByPath(props.row, props.col.key, v)
|
|
28
|
+
props.onCellChange?.(props.row, props.col)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const onBlur = () => props.onCellBlur?.(props.row, props.col)
|
|
32
|
+
const onEnter = () => props.onCellEnter?.(props.row, props.col)
|
|
33
|
+
</script>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-select
|
|
3
|
+
v-model="value"
|
|
4
|
+
v-bind="{ placeholder: '请选择', size: 'small', clearable: true, ...col.renderProps }"
|
|
5
|
+
@change="onChange"
|
|
6
|
+
@blur="onBlur"
|
|
7
|
+
@keyup.enter="onEnter"
|
|
8
|
+
>
|
|
9
|
+
<el-option
|
|
10
|
+
v-for="opt in col.renderProps?.options || []"
|
|
11
|
+
:key="opt.value"
|
|
12
|
+
:label="opt.label"
|
|
13
|
+
:value="opt.value"
|
|
14
|
+
/>
|
|
15
|
+
</el-select>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
import { ref, watch } from 'vue'
|
|
20
|
+
import type { ColumnConfig } from '../types'
|
|
21
|
+
import { getValueByPath, setValueByPath } from '../utils/path'
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
readonly row: any
|
|
25
|
+
readonly col: ColumnConfig
|
|
26
|
+
onCellChange?: (row: any, col: ColumnConfig) => void
|
|
27
|
+
onCellBlur?: (row: any, col: ColumnConfig) => void
|
|
28
|
+
onCellEnter?: (row: any, col: ColumnConfig) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const props = defineProps<Props>()
|
|
32
|
+
const value = ref(getValueByPath(props.row, props.col.key))
|
|
33
|
+
|
|
34
|
+
watch(value, (v) => {
|
|
35
|
+
setValueByPath(props.row, props.col.key, v)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const onChange = () => props.onCellChange?.(props.row, props.col)
|
|
39
|
+
const onBlur = () => props.onCellBlur?.(props.row, props.col)
|
|
40
|
+
const onEnter = () => props.onCellEnter?.(props.row, props.col)
|
|
41
|
+
</script>
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { ButtonProps, TableColumnCtx } from 'element-plus'
|
|
2
|
+
import { DefaultRow } from 'element-plus/es/components/table/src/table/defaults'
|
|
3
|
+
import type { Component } from 'vue'
|
|
4
|
+
|
|
5
|
+
// 导出验证函数类型
|
|
6
|
+
export type { validateRendererProps } from './renderer'
|
|
7
|
+
|
|
8
|
+
/* ======================= 基础工具类型 ======================= */
|
|
9
|
+
|
|
10
|
+
/** 支持额外参数(Element Plus 透传 props) */
|
|
11
|
+
export type WithRestProps<T> = T & {
|
|
12
|
+
[key: string]: any
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* ======================= 渲染器系统 ======================= */
|
|
16
|
+
|
|
17
|
+
/** Renderer 组件类型 */
|
|
18
|
+
export type Renderer = Component
|
|
19
|
+
|
|
20
|
+
/** 渲染器注册表接口 */
|
|
21
|
+
export interface RendererRegistry {
|
|
22
|
+
register(name: string, renderer: Renderer): void
|
|
23
|
+
registerMultiple(renderers: Record<string, Renderer>): void
|
|
24
|
+
get(name: string): Renderer | undefined
|
|
25
|
+
has(name: string): boolean
|
|
26
|
+
unregister(name: string): boolean
|
|
27
|
+
clear(): void
|
|
28
|
+
names(): string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 全局配置接口 */
|
|
32
|
+
export interface SmartTableConfig {
|
|
33
|
+
/** 自定义渲染器 */
|
|
34
|
+
renderers?: Record<string, Renderer>
|
|
35
|
+
/** 默认分页配置 */
|
|
36
|
+
defaultPagination?: {
|
|
37
|
+
page?: number
|
|
38
|
+
size?: number
|
|
39
|
+
total?: number
|
|
40
|
+
}
|
|
41
|
+
/** 默认表格属性 */
|
|
42
|
+
defaultTableProps?: Record<string, any>
|
|
43
|
+
/** 默认列属性 */
|
|
44
|
+
defaultColumnProps?: Record<string, any>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ======================= 操作列按钮 ======================= */
|
|
48
|
+
|
|
49
|
+
export interface ButtonConfig<R = any> {
|
|
50
|
+
permission?: string | string[]
|
|
51
|
+
label: string
|
|
52
|
+
type?: 'primary' | 'success' | 'warning' | 'danger' | 'info'
|
|
53
|
+
action: (row: R) => void
|
|
54
|
+
visible?: (row: R) => boolean
|
|
55
|
+
width?: number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* ======================= Renderer ======================= */
|
|
59
|
+
|
|
60
|
+
export type RendererName =
|
|
61
|
+
| 'html'
|
|
62
|
+
| 'copy'
|
|
63
|
+
| 'img'
|
|
64
|
+
| 'dict'
|
|
65
|
+
| 'map'
|
|
66
|
+
| 'formatter'
|
|
67
|
+
| 'icon'
|
|
68
|
+
| 'input'
|
|
69
|
+
| 'input-number'
|
|
70
|
+
| 'select'
|
|
71
|
+
| 'button'
|
|
72
|
+
| 'link'
|
|
73
|
+
| 'slot'
|
|
74
|
+
|
|
75
|
+
/** renderer 对应的 renderProps */
|
|
76
|
+
export interface RendererPropsMap {
|
|
77
|
+
html: WithRestProps<{
|
|
78
|
+
style?: string
|
|
79
|
+
class?: string
|
|
80
|
+
}>
|
|
81
|
+
|
|
82
|
+
copy: WithRestProps<{
|
|
83
|
+
/** 复制按钮图标颜色 */
|
|
84
|
+
iconColor?: string
|
|
85
|
+
/** 复制按钮提示文本 */
|
|
86
|
+
copyTitle?: string
|
|
87
|
+
/** 复制成功提示 */
|
|
88
|
+
successText?: string
|
|
89
|
+
/** 复制失败提示 */
|
|
90
|
+
errorText?: string
|
|
91
|
+
}>
|
|
92
|
+
|
|
93
|
+
img: WithRestProps<{
|
|
94
|
+
/** 图片宽度 */
|
|
95
|
+
width?: string | number
|
|
96
|
+
/** 图片高度 */
|
|
97
|
+
height?: string | number
|
|
98
|
+
/** 图片适应方式 */
|
|
99
|
+
fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'
|
|
100
|
+
/** 预览图片列表 */
|
|
101
|
+
previewSrcList?: string[]
|
|
102
|
+
/** 无图片时的占位文本 */
|
|
103
|
+
placeholder?: string
|
|
104
|
+
/** 自定义样式 */
|
|
105
|
+
style?: string
|
|
106
|
+
}>
|
|
107
|
+
|
|
108
|
+
dict: WithRestProps<{
|
|
109
|
+
/** 字典配置 */
|
|
110
|
+
options: Array<{
|
|
111
|
+
label: string
|
|
112
|
+
value: string | number
|
|
113
|
+
listClass?: string
|
|
114
|
+
cssClass?: string
|
|
115
|
+
}>
|
|
116
|
+
/** 是否显示未匹配的值 */
|
|
117
|
+
showValue?: boolean
|
|
118
|
+
}>
|
|
119
|
+
|
|
120
|
+
map: WithRestProps<{
|
|
121
|
+
/** key-value 映射 */
|
|
122
|
+
options: Record<string | number, any>
|
|
123
|
+
}>
|
|
124
|
+
|
|
125
|
+
formatter: never // formatter 使用 ColumnConfig.formatter 函数
|
|
126
|
+
|
|
127
|
+
icon: WithRestProps<{
|
|
128
|
+
/** 自定义样式 */
|
|
129
|
+
style?: string
|
|
130
|
+
/** 图标大小(像素) */
|
|
131
|
+
size?: number
|
|
132
|
+
/** 自定义类名 */
|
|
133
|
+
class?: string
|
|
134
|
+
}>
|
|
135
|
+
|
|
136
|
+
input: WithRestProps<{
|
|
137
|
+
/** 占位文本 */
|
|
138
|
+
placeholder?: string
|
|
139
|
+
/** 输入框尺寸 */
|
|
140
|
+
size?: 'small' | 'default' | 'large'
|
|
141
|
+
/** 是否可清空 */
|
|
142
|
+
clearable?: boolean
|
|
143
|
+
}>
|
|
144
|
+
|
|
145
|
+
'input-number': WithRestProps<{
|
|
146
|
+
/** 最小值 */
|
|
147
|
+
min?: number
|
|
148
|
+
/** 最大值 */
|
|
149
|
+
max?: number
|
|
150
|
+
/** 步长 */
|
|
151
|
+
step?: number
|
|
152
|
+
/** 精度 */
|
|
153
|
+
precision?: number
|
|
154
|
+
/** 输入框尺寸 */
|
|
155
|
+
size?: 'small' | 'default' | 'large'
|
|
156
|
+
/** 是否显示增减按钮 */
|
|
157
|
+
controls?: boolean
|
|
158
|
+
}>
|
|
159
|
+
|
|
160
|
+
select: WithRestProps<{
|
|
161
|
+
/** 选项配置 */
|
|
162
|
+
options: Array<{
|
|
163
|
+
label: string
|
|
164
|
+
value: string | number
|
|
165
|
+
}>
|
|
166
|
+
/** 占位文本 */
|
|
167
|
+
placeholder?: string
|
|
168
|
+
/** 选择器尺寸 */
|
|
169
|
+
size?: 'small' | 'default' | 'large'
|
|
170
|
+
/** 是否可清空 */
|
|
171
|
+
clearable?: boolean
|
|
172
|
+
}>
|
|
173
|
+
|
|
174
|
+
button: WithRestProps<ButtonProps & {
|
|
175
|
+
/** 按钮文本 */
|
|
176
|
+
label?: string
|
|
177
|
+
/** 自定义样式 */
|
|
178
|
+
style?: string
|
|
179
|
+
/** 自定义类名 */
|
|
180
|
+
class?: string
|
|
181
|
+
}>
|
|
182
|
+
|
|
183
|
+
link: WithRestProps<{
|
|
184
|
+
/** 链接文本 */
|
|
185
|
+
label?: string
|
|
186
|
+
/** 链接地址 */
|
|
187
|
+
href: string
|
|
188
|
+
/** 是否新窗口打开 */
|
|
189
|
+
blank?: boolean
|
|
190
|
+
/** 自定义样式 */
|
|
191
|
+
style?: string
|
|
192
|
+
/** 自定义类名 */
|
|
193
|
+
class?: string
|
|
194
|
+
}>
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* ======================= 列类型 ======================= */
|
|
198
|
+
|
|
199
|
+
export type ColumnType =
|
|
200
|
+
| 'default'
|
|
201
|
+
| 'selection'
|
|
202
|
+
| 'index'
|
|
203
|
+
| 'operation'
|
|
204
|
+
|
|
205
|
+
/* ======================= ColumnConfig ======================= */
|
|
206
|
+
export interface BaseColumn<R extends DefaultRow> {
|
|
207
|
+
key: keyof R & string
|
|
208
|
+
label?: string
|
|
209
|
+
visible?: boolean
|
|
210
|
+
inControl?: boolean
|
|
211
|
+
columnProps?: Partial<TableColumnCtx<R>>
|
|
212
|
+
render?: RendererName
|
|
213
|
+
slot?: string // 插槽名称,默认用 key
|
|
214
|
+
renderProps?: Partial<RendererPropsMap[keyof RendererPropsMap]>
|
|
215
|
+
buttons?: ButtonConfig<R>[] // operation 列专用
|
|
216
|
+
maxbtn?: number // operation 列专用
|
|
217
|
+
__rows?: R[] // operation 列内部使用
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface SelectionColumn<R extends DefaultRow> extends BaseColumn<R> { type: 'selection' }
|
|
221
|
+
export interface IndexColumn<R extends DefaultRow> extends BaseColumn<R> { type: 'index' }
|
|
222
|
+
export interface OperationColumn<R extends DefaultRow> extends BaseColumn<R> { type: 'operation'; buttons: ButtonConfig<R>[] }
|
|
223
|
+
export interface DataColumn<R extends DefaultRow> extends BaseColumn<R> { type?: 'default'; formatter?: (value: any, row: R) => any }
|
|
224
|
+
|
|
225
|
+
export type ColumnConfig<R extends DefaultRow = any> =
|
|
226
|
+
| SelectionColumn<R>
|
|
227
|
+
| IndexColumn<R>
|
|
228
|
+
| OperationColumn<R>
|
|
229
|
+
| DataColumn<R>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 安全获取对象深层属性
|
|
3
|
+
* 支持 a.b.c / a.0.b
|
|
4
|
+
*/
|
|
5
|
+
export function getValueByPath(obj: any, path?: string) {
|
|
6
|
+
if (!obj || !path) return undefined
|
|
7
|
+
return path.split('.').reduce((acc, key) => acc?.[key], obj)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 安全设置对象深层属性(用于可编辑表格)
|
|
12
|
+
*/
|
|
13
|
+
export function setValueByPath(
|
|
14
|
+
obj: any,
|
|
15
|
+
path: string,
|
|
16
|
+
value: any
|
|
17
|
+
) {
|
|
18
|
+
if (!obj || !path) return
|
|
19
|
+
const keys = path.split('.')
|
|
20
|
+
const lastKey = keys.pop()!
|
|
21
|
+
|
|
22
|
+
const target = keys.reduce((acc, key) => {
|
|
23
|
+
if (!acc[key]) acc[key] = {}
|
|
24
|
+
return acc[key]
|
|
25
|
+
}, obj)
|
|
26
|
+
|
|
27
|
+
target[lastKey] = value
|
|
28
|
+
}
|
|
29
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue3 Smart Table - 主入口文件
|
|
3
|
+
*
|
|
4
|
+
* 基于 Vue 3 + Element Plus 的高可复用表格组件库
|
|
5
|
+
*/
|
|
6
|
+
import SmartTable from './components/SmartTable/index.vue'
|
|
7
|
+
|
|
8
|
+
// 导出类型
|
|
9
|
+
export * from './components/SmartTable/types'
|
|
10
|
+
|
|
11
|
+
// 导出核心功能(从 SmartTable 内部导出)
|
|
12
|
+
export {
|
|
13
|
+
getRendererManager,
|
|
14
|
+
wrapSFCComponent,
|
|
15
|
+
createFunctionalRenderer,
|
|
16
|
+
validateRendererProps
|
|
17
|
+
} from './components/SmartTable/renderer'
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
setSmartTableConfig,
|
|
21
|
+
getSmartTableConfig
|
|
22
|
+
} from './components/SmartTable/config'
|
|
23
|
+
|
|
24
|
+
// 导出内置渲染器
|
|
25
|
+
export {
|
|
26
|
+
builtInRenderers,
|
|
27
|
+
registerBuiltInRenderers,
|
|
28
|
+
createRenderer
|
|
29
|
+
} from './components/SmartTable/renderers'
|
|
30
|
+
|
|
31
|
+
// 导出类型工具
|
|
32
|
+
export { defineColumn } from './types/enhanced'
|
|
33
|
+
|
|
34
|
+
// 导出主组件
|
|
35
|
+
export { SmartTable }
|
|
36
|
+
|
|
37
|
+
// 默认导出
|
|
38
|
+
export default SmartTable
|