verce-vue-test 0.0.1 → 0.0.5
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/package.json +4 -4
- package/src/App.vue +3 -0
- package/src/components/ElFormDialog.vue +285 -0
- package/src/components/ElTablePro.vue +13 -3
- package/src/router/index.ts +5 -0
- package/src/views/EmployeeView.vue +204 -0
- package/src/views/TableProForView.vue +3 -1
- package/.vscode/settings.json +0 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "verce-vue-test",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
"preview": "vite preview",
|
|
10
10
|
"build-only": "vite build",
|
|
11
11
|
"type-check": "vue-tsc --build",
|
|
12
|
-
|
|
12
|
+
"发布": "npm publish",
|
|
13
13
|
"修订 ": "npm version patch",
|
|
14
14
|
"次版本": "npm version minor",
|
|
15
15
|
"主版本": "npm version major",
|
|
16
16
|
"登录": "npm login ",
|
|
17
17
|
"一键发布": "npm version patch && npm publish"
|
|
18
18
|
},
|
|
19
|
-
|
|
19
|
+
"keywords": [
|
|
20
20
|
"express",
|
|
21
21
|
"web",
|
|
22
22
|
"application"
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"engines": {
|
|
44
44
|
"node": "^20.19.0 || >=22.12.0"
|
|
45
45
|
},
|
|
46
|
-
|
|
46
|
+
"publishConfig": {
|
|
47
47
|
"access": "public",
|
|
48
48
|
"registry": "https://registry.npmjs.org"
|
|
49
49
|
}
|
package/src/App.vue
CHANGED
|
@@ -30,6 +30,9 @@ import { RouterView } from 'vue-router'
|
|
|
30
30
|
<el-menu-item index="/table-pro-for">
|
|
31
31
|
<span>循环表格</span>
|
|
32
32
|
</el-menu-item>
|
|
33
|
+
<el-menu-item index="/employee">
|
|
34
|
+
<span>员工管理</span>
|
|
35
|
+
</el-menu-item>
|
|
33
36
|
<el-menu-item index="/about">
|
|
34
37
|
<span>关于</span>
|
|
35
38
|
</el-menu-item>
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, reactive, watch, nextTick, computed } from 'vue'
|
|
3
|
+
import type { FormInstance, FormRules } from 'element-plus'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 基于 Element Plus 的表单弹窗组件
|
|
7
|
+
* 支持新增/编辑模式,通过 fields 配置动态渲染表单项
|
|
8
|
+
* 支持 input、select、number、date、textarea、radio、switch 等字段类型
|
|
9
|
+
*/
|
|
10
|
+
defineOptions({
|
|
11
|
+
name: 'ElFormDialog',
|
|
12
|
+
inheritAttrs: false, // 不自动将 $attrs 绑定到根元素,避免属性透传到 el-dialog
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
/** 表单字段类型 */
|
|
16
|
+
type FieldType = 'input' | 'select' | 'number' | 'date' | 'textarea' | 'radio' | 'switch'
|
|
17
|
+
|
|
18
|
+
/** 表单字段配置项 */
|
|
19
|
+
type FormField = {
|
|
20
|
+
/** 字段属性名,对应表单数据中的 key */
|
|
21
|
+
prop: string
|
|
22
|
+
/** 字段标签文本 */
|
|
23
|
+
label: string
|
|
24
|
+
/** 字段类型 */
|
|
25
|
+
type: FieldType
|
|
26
|
+
/** 占位提示文本,不传则根据类型自动生成 */
|
|
27
|
+
placeholder?: string
|
|
28
|
+
/** 是否必填,为 true 时自动生成必填校验规则 */
|
|
29
|
+
required?: boolean
|
|
30
|
+
/** 自定义校验规则,优先级低于 props.rules 中同名字段的规则 */
|
|
31
|
+
rules?: FormRules[string]
|
|
32
|
+
/** 下拉选项列表,适用于 select 和 radio 类型 */
|
|
33
|
+
options?: Array<{ label: string; value: any }>
|
|
34
|
+
/** 数字输入框最小值,适用于 number 类型 */
|
|
35
|
+
min?: number
|
|
36
|
+
/** 数字输入框最大值,适用于 number 类型 */
|
|
37
|
+
max?: number
|
|
38
|
+
/** 文本域行数,适用于 textarea 类型,默认 3 */
|
|
39
|
+
rows?: number
|
|
40
|
+
/** switch 打开时的文字描述 */
|
|
41
|
+
activeText?: string
|
|
42
|
+
/** switch 关闭时的文字描述 */
|
|
43
|
+
inactiveText?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const props = withDefaults(
|
|
47
|
+
defineProps<{
|
|
48
|
+
/** 控制弹窗显示/隐藏,支持 v-model 双向绑定 */
|
|
49
|
+
modelValue: boolean
|
|
50
|
+
/** 表单字段配置列表 */
|
|
51
|
+
fields: FormField[]
|
|
52
|
+
/** 表单数据对象,编辑时需传入包含 id 的对象,新增时传入空对象 */
|
|
53
|
+
formData: Record<string, any>
|
|
54
|
+
/** 弹窗标题,不传则根据是否编辑自动显示"新增"或"编辑" */
|
|
55
|
+
title?: string
|
|
56
|
+
/** 弹窗宽度 */
|
|
57
|
+
width?: string
|
|
58
|
+
/** 自定义校验规则,与 fields 中的 required/rules 合并,同名字段优先使用此处的规则 */
|
|
59
|
+
rules?: FormRules
|
|
60
|
+
}>(),
|
|
61
|
+
{
|
|
62
|
+
width: '600px',
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const emit = defineEmits<{
|
|
67
|
+
/** 弹窗显示状态变化时触发,用于 v-model 双向绑定 */
|
|
68
|
+
'update:modelValue': [value: boolean]
|
|
69
|
+
/** 点击确认按钮且表单校验通过后触发,参数为表单数据(编辑时包含 id) */
|
|
70
|
+
confirm: [data: Record<string, any>]
|
|
71
|
+
}>()
|
|
72
|
+
|
|
73
|
+
/** 表单实例引用,用于调用 validate、clearValidate 等方法 */
|
|
74
|
+
const formRef = ref<FormInstance>()
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 弹窗显示状态的双向绑定计算属性
|
|
78
|
+
* 读取时返回 props.modelValue,设置时触发 update:modelValue 事件
|
|
79
|
+
*/
|
|
80
|
+
const dialogVisible = computed({
|
|
81
|
+
get: () => props.modelValue,
|
|
82
|
+
set: value => emit('update:modelValue', value),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
/** 是否为编辑模式(根据 formData 中是否包含 id 判断) */
|
|
86
|
+
const isEdit = computed(() => !!props.formData?.id)
|
|
87
|
+
|
|
88
|
+
/** 弹窗标题,优先使用 props.title,否则编辑模式显示"编辑"、新增模式显示"新增" */
|
|
89
|
+
const dialogTitle = computed(() => {
|
|
90
|
+
if (props.title) return props.title
|
|
91
|
+
return isEdit.value ? '编辑' : '新增'
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
/** 响应式表单数据对象,根据 fields 配置动态初始化 */
|
|
95
|
+
const form = reactive<Record<string, any>>({})
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 合并后的表单校验规则
|
|
99
|
+
* 合并逻辑:
|
|
100
|
+
* 1. 以 props.rules 为基础
|
|
101
|
+
* 2. 若字段配置了 required 且 props.rules 中无对应规则,则自动生成必填规则
|
|
102
|
+
* 3. 若字段配置了自定义 rules 且 props.rules 中无对应规则,则使用字段的自定义规则
|
|
103
|
+
* 注:props.rules 中的规则优先级最高,不会被字段级配置覆盖
|
|
104
|
+
*/
|
|
105
|
+
const mergedRules = computed(() => {
|
|
106
|
+
const rules: FormRules = { ...props.rules }
|
|
107
|
+
props.fields.forEach(field => {
|
|
108
|
+
if (field.required && !rules[field.prop]) {
|
|
109
|
+
rules[field.prop] = [
|
|
110
|
+
{
|
|
111
|
+
required: true,
|
|
112
|
+
// 根据字段类型自动生成提示语:select/date 用"选择",其他用"输入"
|
|
113
|
+
message: `请${field.type === 'select' || field.type === 'date' ? '选择' : '输入'}${field.label}`,
|
|
114
|
+
// 根据字段类型自动选择触发方式:switch/select/date 用 change,其他用 blur
|
|
115
|
+
trigger: field.type === 'switch' ? 'change' : field.type === 'select' || field.type === 'date' ? 'change' : 'blur',
|
|
116
|
+
},
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
if (field.rules && !rules[field.prop]) {
|
|
120
|
+
rules[field.prop] = field.rules
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
return rules
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 初始化表单数据
|
|
128
|
+
* 1. 清空原有表单数据
|
|
129
|
+
* 2. 根据 fields 配置逐字段初始化,使用 formData 中对应值或默认值
|
|
130
|
+
* - switch 类型默认 false
|
|
131
|
+
* - number 类型默认 undefined
|
|
132
|
+
* - 其他类型默认空字符串
|
|
133
|
+
*/
|
|
134
|
+
const initForm = () => {
|
|
135
|
+
Object.keys(form).forEach(key => delete form[key])
|
|
136
|
+
props.fields.forEach(field => {
|
|
137
|
+
if (field.type === 'switch') {
|
|
138
|
+
form[field.prop] = props.formData?.[field.prop] ?? false
|
|
139
|
+
} else if (field.type === 'number') {
|
|
140
|
+
form[field.prop] = props.formData?.[field.prop] ?? undefined
|
|
141
|
+
} else {
|
|
142
|
+
form[field.prop] = props.formData?.[field.prop] ?? ''
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 监听弹窗打开事件
|
|
149
|
+
* 弹窗打开时:初始化表单数据,并在下一帧清除校验状态(避免残留的校验提示)
|
|
150
|
+
*/
|
|
151
|
+
watch(
|
|
152
|
+
() => props.modelValue,
|
|
153
|
+
val => {
|
|
154
|
+
if (val) {
|
|
155
|
+
initForm()
|
|
156
|
+
nextTick(() => formRef.value?.clearValidate())
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 确认按钮点击处理
|
|
163
|
+
* 1. 校验表单
|
|
164
|
+
* 2. 校验通过后触发 confirm 事件,传递表单数据(编辑时附带 id)
|
|
165
|
+
* 3. 关闭弹窗
|
|
166
|
+
*/
|
|
167
|
+
const handleConfirm = async () => {
|
|
168
|
+
if (!formRef.value) return
|
|
169
|
+
await formRef.value.validate(valid => {
|
|
170
|
+
if (!valid) return
|
|
171
|
+
emit('confirm', { ...form, id: props.formData?.id })
|
|
172
|
+
dialogVisible.value = false
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 取消按钮点击处理,直接关闭弹窗 */
|
|
177
|
+
const handleCancel = () => {
|
|
178
|
+
dialogVisible.value = false
|
|
179
|
+
}
|
|
180
|
+
</script>
|
|
181
|
+
|
|
182
|
+
<template>
|
|
183
|
+
<!-- 弹窗容器:禁止点击遮罩关闭、关闭时销毁内容 -->
|
|
184
|
+
<el-dialog
|
|
185
|
+
v-model="dialogVisible"
|
|
186
|
+
:title="dialogTitle"
|
|
187
|
+
:width="width"
|
|
188
|
+
:close-on-click-modal="false"
|
|
189
|
+
destroy-on-close
|
|
190
|
+
>
|
|
191
|
+
<!-- 表单容器 -->
|
|
192
|
+
<el-form
|
|
193
|
+
ref="formRef"
|
|
194
|
+
:model="form"
|
|
195
|
+
:rules="mergedRules"
|
|
196
|
+
label-width="100px"
|
|
197
|
+
>
|
|
198
|
+
<!-- 动态渲染表单项,根据 field.type 选择对应的输入组件 -->
|
|
199
|
+
<el-form-item
|
|
200
|
+
v-for="field in fields"
|
|
201
|
+
:key="field.prop"
|
|
202
|
+
:label="field.label"
|
|
203
|
+
:prop="field.prop"
|
|
204
|
+
>
|
|
205
|
+
<!-- 文本输入框 -->
|
|
206
|
+
<el-input
|
|
207
|
+
v-if="field.type === 'input'"
|
|
208
|
+
v-model="form[field.prop]"
|
|
209
|
+
:placeholder="field.placeholder || `请输入${field.label}`"
|
|
210
|
+
/>
|
|
211
|
+
|
|
212
|
+
<!-- 数字输入框 -->
|
|
213
|
+
<el-input-number
|
|
214
|
+
v-else-if="field.type === 'number'"
|
|
215
|
+
v-model="form[field.prop]"
|
|
216
|
+
:min="field.min"
|
|
217
|
+
:max="field.max"
|
|
218
|
+
:placeholder="field.placeholder || `请输入${field.label}`"
|
|
219
|
+
style="width: 100%"
|
|
220
|
+
/>
|
|
221
|
+
|
|
222
|
+
<!-- 下拉选择框 -->
|
|
223
|
+
<el-select
|
|
224
|
+
v-else-if="field.type === 'select'"
|
|
225
|
+
v-model="form[field.prop]"
|
|
226
|
+
:placeholder="field.placeholder || `请选择${field.label}`"
|
|
227
|
+
style="width: 100%"
|
|
228
|
+
>
|
|
229
|
+
<el-option
|
|
230
|
+
v-for="opt in field.options"
|
|
231
|
+
:key="opt.value"
|
|
232
|
+
:label="opt.label"
|
|
233
|
+
:value="opt.value"
|
|
234
|
+
/>
|
|
235
|
+
</el-select>
|
|
236
|
+
|
|
237
|
+
<!-- 日期选择器 -->
|
|
238
|
+
<el-date-picker
|
|
239
|
+
v-else-if="field.type === 'date'"
|
|
240
|
+
v-model="form[field.prop]"
|
|
241
|
+
type="date"
|
|
242
|
+
:placeholder="field.placeholder || `请选择${field.label}`"
|
|
243
|
+
style="width: 100%"
|
|
244
|
+
/>
|
|
245
|
+
|
|
246
|
+
<!-- 文本域 -->
|
|
247
|
+
<el-input
|
|
248
|
+
v-else-if="field.type === 'textarea'"
|
|
249
|
+
v-model="form[field.prop]"
|
|
250
|
+
type="textarea"
|
|
251
|
+
:rows="field.rows || 3"
|
|
252
|
+
:placeholder="field.placeholder || `请输入${field.label}`"
|
|
253
|
+
/>
|
|
254
|
+
|
|
255
|
+
<!-- 开关 -->
|
|
256
|
+
<el-switch
|
|
257
|
+
v-else-if="field.type === 'switch'"
|
|
258
|
+
v-model="form[field.prop]"
|
|
259
|
+
:active-text="field.activeText"
|
|
260
|
+
:inactive-text="field.inactiveText"
|
|
261
|
+
/>
|
|
262
|
+
|
|
263
|
+
<!-- 单选框组 -->
|
|
264
|
+
<el-radio-group
|
|
265
|
+
v-else-if="field.type === 'radio'"
|
|
266
|
+
v-model="form[field.prop]"
|
|
267
|
+
>
|
|
268
|
+
<el-radio
|
|
269
|
+
v-for="opt in field.options"
|
|
270
|
+
:key="opt.value"
|
|
271
|
+
:value="opt.value"
|
|
272
|
+
>
|
|
273
|
+
{{ opt.label }}
|
|
274
|
+
</el-radio>
|
|
275
|
+
</el-radio-group>
|
|
276
|
+
</el-form-item>
|
|
277
|
+
</el-form>
|
|
278
|
+
|
|
279
|
+
<!-- 底部按钮区域 -->
|
|
280
|
+
<template #footer>
|
|
281
|
+
<el-button @click="handleCancel">取消</el-button>
|
|
282
|
+
<el-button type="primary" @click="handleConfirm">确认</el-button>
|
|
283
|
+
</template>
|
|
284
|
+
</el-dialog>
|
|
285
|
+
</template>
|
|
@@ -91,6 +91,7 @@ type ColumnOption = {
|
|
|
91
91
|
key: string // 列唯一标识
|
|
92
92
|
label: string // 列显示名称
|
|
93
93
|
visible: boolean // 是否可见
|
|
94
|
+
disabled: boolean // 是否禁用选择
|
|
94
95
|
vnode: VNode // 列的 VNode 节点
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -144,10 +145,15 @@ const currentPageSize = computed({
|
|
|
144
145
|
|
|
145
146
|
// 是否全选(弹窗中的临时状态)
|
|
146
147
|
const isAllSelected = computed({
|
|
147
|
-
get: () =>
|
|
148
|
+
get: () => {
|
|
149
|
+
const selectable = pendingColumnOptions.value.filter(col => !col.disabled)
|
|
150
|
+
return selectable.length > 0 && selectable.every(col => col.visible)
|
|
151
|
+
},
|
|
148
152
|
set: value => {
|
|
149
153
|
pendingColumnOptions.value.forEach(col => {
|
|
150
|
-
col.
|
|
154
|
+
if (!col.disabled) {
|
|
155
|
+
col.visible = value
|
|
156
|
+
}
|
|
151
157
|
})
|
|
152
158
|
},
|
|
153
159
|
})
|
|
@@ -211,6 +217,7 @@ const restoreColumnConfig = () => {
|
|
|
211
217
|
|
|
212
218
|
// 遍历列选项,更新可见性状态
|
|
213
219
|
columnOptions.value.forEach(column => {
|
|
220
|
+
if (column.disabled) return
|
|
214
221
|
const match = cacheColumns.find(item => item.key === column.key)
|
|
215
222
|
if (match) {
|
|
216
223
|
column.visible = match.visible
|
|
@@ -289,10 +296,12 @@ const initColumns = () => {
|
|
|
289
296
|
const children = flattenVNodes(rawChildren)
|
|
290
297
|
|
|
291
298
|
columnOptions.value = children.map((vnode, index) => {
|
|
299
|
+
const columnProps = vnode.props as Record<string, any> | null
|
|
292
300
|
return {
|
|
293
301
|
key: getColumnKey(vnode, index),
|
|
294
302
|
label: getColumnLabel(vnode, index),
|
|
295
|
-
visible:
|
|
303
|
+
visible: columnProps?.visible !== false,
|
|
304
|
+
disabled: columnProps?.disabled ?? false,
|
|
296
305
|
vnode,
|
|
297
306
|
}
|
|
298
307
|
})
|
|
@@ -399,6 +408,7 @@ initColumns()
|
|
|
399
408
|
>
|
|
400
409
|
<el-checkbox
|
|
401
410
|
v-model="column.visible"
|
|
411
|
+
:disabled="column.disabled"
|
|
402
412
|
>
|
|
403
413
|
{{ column.label }}
|
|
404
414
|
</el-checkbox>
|
package/src/router/index.ts
CHANGED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ElTablePro
|
|
3
|
+
:data="tableData"
|
|
4
|
+
:total="total"
|
|
5
|
+
v-model:page="query.page"
|
|
6
|
+
v-model:page-size="query.pageSize"
|
|
7
|
+
table-key="employee"
|
|
8
|
+
row-key="id"
|
|
9
|
+
border
|
|
10
|
+
stripe
|
|
11
|
+
v-loading="loading"
|
|
12
|
+
@pagination-change="handlePaginationChange"
|
|
13
|
+
>
|
|
14
|
+
<template #toolbar>
|
|
15
|
+
<el-button type="primary" @click="handleAdd">新增</el-button>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<el-table-column prop="id" label="ID" width="80" disabled />
|
|
19
|
+
<el-table-column prop="name" label="姓名" width="120" />
|
|
20
|
+
<el-table-column prop="department" label="部门" width="120">
|
|
21
|
+
<template #default="{ row }">
|
|
22
|
+
<el-tag :type="deptTagType(row.department)">
|
|
23
|
+
{{ row.department }}
|
|
24
|
+
</el-tag>
|
|
25
|
+
</template>
|
|
26
|
+
</el-table-column>
|
|
27
|
+
<el-table-column prop="position" label="职位" />
|
|
28
|
+
<el-table-column prop="salary" label="薪资" width="120">
|
|
29
|
+
<template #default="{ row }">
|
|
30
|
+
¥{{ row.salary.toLocaleString() }}
|
|
31
|
+
</template>
|
|
32
|
+
</el-table-column>
|
|
33
|
+
<el-table-column prop="status" label="状态" width="100">
|
|
34
|
+
<template #default="{ row }">
|
|
35
|
+
<el-tag :type="row.status ? 'success' : 'danger'">
|
|
36
|
+
{{ row.status ? '在职' : '离职' }}
|
|
37
|
+
</el-tag>
|
|
38
|
+
</template>
|
|
39
|
+
</el-table-column>
|
|
40
|
+
<el-table-column label="操作" width="180">
|
|
41
|
+
<template #default="{ row }">
|
|
42
|
+
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
|
43
|
+
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
|
44
|
+
</template>
|
|
45
|
+
</el-table-column>
|
|
46
|
+
</ElTablePro>
|
|
47
|
+
|
|
48
|
+
<ElFormDialog
|
|
49
|
+
v-model="dialogVisible"
|
|
50
|
+
:fields="formFields"
|
|
51
|
+
:form-data="currentRow"
|
|
52
|
+
title="员工信息"
|
|
53
|
+
@confirm="handleFormConfirm"
|
|
54
|
+
/>
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
<script setup lang="ts">
|
|
58
|
+
import { ref, reactive, onMounted } from 'vue'
|
|
59
|
+
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
60
|
+
import ElTablePro from '@/components/ElTablePro.vue'
|
|
61
|
+
import ElFormDialog from '@/components/ElFormDialog.vue'
|
|
62
|
+
import type { FormRules } from 'element-plus'
|
|
63
|
+
|
|
64
|
+
type Employee = {
|
|
65
|
+
id: number
|
|
66
|
+
name: string
|
|
67
|
+
department: string
|
|
68
|
+
position: string
|
|
69
|
+
salary: number
|
|
70
|
+
status: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const formFields = [
|
|
74
|
+
{ prop: 'name', label: '姓名', type: 'input' as const, required: true },
|
|
75
|
+
{
|
|
76
|
+
prop: 'department',
|
|
77
|
+
label: '部门',
|
|
78
|
+
type: 'select' as const,
|
|
79
|
+
required: true,
|
|
80
|
+
options: [
|
|
81
|
+
{ label: '技术部', value: '技术部' },
|
|
82
|
+
{ label: '产品部', value: '产品部' },
|
|
83
|
+
{ label: '设计部', value: '设计部' },
|
|
84
|
+
{ label: '市场部', value: '市场部' },
|
|
85
|
+
{ label: '人事部', value: '人事部' },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{ prop: 'position', label: '职位', type: 'input' as const, required: true },
|
|
89
|
+
{ prop: 'salary', label: '薪资', type: 'number' as const, required: true, min: 0 },
|
|
90
|
+
{ prop: 'status', label: '状态', type: 'switch' as const, activeText: '在职', inactiveText: '离职' },
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
const rules: FormRules = {
|
|
94
|
+
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
|
95
|
+
department: [{ required: true, message: '请选择部门', trigger: 'change' }],
|
|
96
|
+
position: [{ required: true, message: '请输入职位', trigger: 'blur' }],
|
|
97
|
+
salary: [{ required: true, message: '请输入薪资', trigger: 'blur' }],
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const loading = ref(false)
|
|
101
|
+
const total = ref(0)
|
|
102
|
+
const dialogVisible = ref(false)
|
|
103
|
+
const currentRow = ref<Record<string, any>>({})
|
|
104
|
+
|
|
105
|
+
const query = reactive({
|
|
106
|
+
page: 1,
|
|
107
|
+
pageSize: 10,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const allData = ref<Employee[]>([
|
|
111
|
+
{ id: 1, name: '陈伟', department: '技术部', position: '前端工程师', salary: 18000, status: true },
|
|
112
|
+
{ id: 2, name: '刘芳', department: '产品部', position: '产品经理', salary: 22000, status: true },
|
|
113
|
+
{ id: 3, name: '张磊', department: '技术部', position: '后端工程师', salary: 20000, status: true },
|
|
114
|
+
{ id: 4, name: '王丽', department: '设计部', position: 'UI设计师', salary: 16000, status: true },
|
|
115
|
+
{ id: 5, name: '李强', department: '市场部', position: '市场总监', salary: 25000, status: true },
|
|
116
|
+
{ id: 6, name: '赵敏', department: '人事部', position: 'HR经理', salary: 17000, status: false },
|
|
117
|
+
{ id: 7, name: '孙涛', department: '技术部', position: '架构师', salary: 35000, status: true },
|
|
118
|
+
{ id: 8, name: '周婷', department: '产品部', position: '产品助理', salary: 10000, status: true },
|
|
119
|
+
{ id: 9, name: '吴凯', department: '技术部', position: '测试工程师', salary: 14000, status: false },
|
|
120
|
+
{ id: 10, name: '郑华', department: '设计部', position: '视觉设计师', salary: 15000, status: true },
|
|
121
|
+
{ id: 11, name: '冯雪', department: '市场部', position: '运营专员', salary: 12000, status: true },
|
|
122
|
+
{ id: 12, name: '杨帆', department: '技术部', position: 'DevOps工程师', salary: 23000, status: true },
|
|
123
|
+
])
|
|
124
|
+
|
|
125
|
+
const tableData = ref<Employee[]>([])
|
|
126
|
+
|
|
127
|
+
let nextId = 13
|
|
128
|
+
|
|
129
|
+
const deptTagType = (dept: string) => {
|
|
130
|
+
const map: Record<string, string> = {
|
|
131
|
+
'技术部': 'primary',
|
|
132
|
+
'产品部': 'warning',
|
|
133
|
+
'设计部': 'success',
|
|
134
|
+
'市场部': 'danger',
|
|
135
|
+
'人事部': 'info',
|
|
136
|
+
}
|
|
137
|
+
return map[dept] || 'info'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const getList = () => {
|
|
141
|
+
loading.value = true
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
const start = (query.page - 1) * query.pageSize
|
|
144
|
+
const end = start + query.pageSize
|
|
145
|
+
tableData.value = allData.value.slice(start, end)
|
|
146
|
+
total.value = allData.value.length
|
|
147
|
+
loading.value = false
|
|
148
|
+
}, 300)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const handlePaginationChange = () => {
|
|
152
|
+
getList()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const handleAdd = () => {
|
|
156
|
+
currentRow.value = {}
|
|
157
|
+
dialogVisible.value = true
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const handleEdit = (row: Employee) => {
|
|
161
|
+
currentRow.value = { ...row }
|
|
162
|
+
dialogVisible.value = true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const handleFormConfirm = (data: Record<string, any>) => {
|
|
166
|
+
if (data.id) {
|
|
167
|
+
const index = allData.value.findIndex(item => item.id === data.id)
|
|
168
|
+
if (index !== -1) {
|
|
169
|
+
allData.value[index] = { ...allData.value[index], ...data }
|
|
170
|
+
}
|
|
171
|
+
ElMessage.success('编辑成功')
|
|
172
|
+
} else {
|
|
173
|
+
allData.value.unshift({
|
|
174
|
+
id: nextId++,
|
|
175
|
+
name: data.name,
|
|
176
|
+
department: data.department,
|
|
177
|
+
position: data.position,
|
|
178
|
+
salary: data.salary,
|
|
179
|
+
status: data.status ?? true,
|
|
180
|
+
})
|
|
181
|
+
ElMessage.success('新增成功')
|
|
182
|
+
}
|
|
183
|
+
getList()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const handleDelete = (row: Employee) => {
|
|
187
|
+
ElMessageBox.confirm(`确认删除员工 ${row.name}?`, '提示', {
|
|
188
|
+
confirmButtonText: '确认',
|
|
189
|
+
cancelButtonText: '取消',
|
|
190
|
+
type: 'warning',
|
|
191
|
+
}).then(() => {
|
|
192
|
+
const index = allData.value.findIndex(item => item.id === row.id)
|
|
193
|
+
if (index !== -1) {
|
|
194
|
+
allData.value.splice(index, 1)
|
|
195
|
+
}
|
|
196
|
+
getList()
|
|
197
|
+
ElMessage.success('删除成功')
|
|
198
|
+
}).catch(() => {})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
onMounted(() => {
|
|
202
|
+
getList()
|
|
203
|
+
})
|
|
204
|
+
</script>
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
:label="col.label"
|
|
23
23
|
:width="col.width"
|
|
24
24
|
:min-width="col.minWidth"
|
|
25
|
+
:visible="col.visible"
|
|
25
26
|
>
|
|
26
27
|
<template v-if="col.slot" #default="{ row }">
|
|
27
28
|
<el-tag
|
|
@@ -56,11 +57,12 @@ interface ColumnConfig {
|
|
|
56
57
|
width?: number | string
|
|
57
58
|
minWidth?: number | string
|
|
58
59
|
slot?: boolean
|
|
60
|
+
visible?: boolean
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
const columns: ColumnConfig[] = [
|
|
62
64
|
{ prop: 'id', label: 'ID', width: 80 },
|
|
63
|
-
{ prop: 'name', label: '商品名称', minWidth: 150 },
|
|
65
|
+
{ prop: 'name', label: '商品名称', minWidth: 150, visible: false },
|
|
64
66
|
{ prop: 'category', label: '分类', width: 120, slot: true },
|
|
65
67
|
{ prop: 'price', label: '价格', width: 100 },
|
|
66
68
|
{ prop: 'stock', label: '库存', width: 80 },
|
package/.vscode/settings.json
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"explorer.fileNesting.enabled": true,
|
|
3
|
-
"explorer.fileNesting.patterns": {
|
|
4
|
-
"tsconfig.json": "tsconfig.*.json, env.d.ts, typed-router.d.ts",
|
|
5
|
-
"vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*",
|
|
6
|
-
"package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .oxlint*, oxlint*, .oxfmt*, .prettier*, prettier*, .editorconfig"
|
|
7
|
-
}
|
|
8
|
-
}
|