明树Git Lab

Commit 37c7b72f authored by chenron's avatar chenron

用户管理页面开发

parent 9d98e887
# ConfigForm 可配置表单组件
一个基于 Vue 3 和 Element Plus 的功能强大的可配置表单组件,支持多种表单控件类型和灵活的配置选项。
## 功能特性
- 🎯 **支持多种表单控件**:输入框、选择器、单选框、复选框、日期选择器、时间选择器、数字输入框、开关、滑块、评分、颜色选择器、上传等
- 🎨 **灵活的布局配置**:支持响应式布局、自定义列宽、间距等
-**完整的表单验证**:支持自定义验证规则、异步验证等
- 🔧 **高度可配置**:每个表单项都支持丰富的配置选项
- 🎪 **支持自定义插槽**:可以插入自定义内容
- 📱 **响应式设计**:支持不同屏幕尺寸的自适应布局
## 基础用法
```vue
<template>
<config-form
v-model="formData"
:config="formConfig"
:items="formItems"
:rules="formRules"
@submit="handleSubmit"
/>
</template>
<script setup>
import { ref } from 'vue'
import ConfigForm from '@/components/ConfigForm.vue'
const formData = ref({
name: '',
age: null,
gender: ''
})
const formConfig = {
labelWidth: '120px',
showButtons: true,
submitText: '提交',
resetText: '重置'
}
const formItems = [
{
type: 'input',
prop: 'name',
label: '姓名',
placeholder: '请输入姓名',
required: true
},
{
type: 'number',
prop: 'age',
label: '年龄',
min: 1,
max: 120
},
{
type: 'radio',
prop: 'gender',
label: '性别',
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' }
]
}
]
const formRules = {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' }
]
}
const handleSubmit = (data) => {
console.log('表单提交:', data)
}
</script>
```
## Props
### config (表单配置)
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| labelWidth | string | '120px' | 表单标签宽度 |
| labelPosition | string | 'right' | 表单标签位置:left/right/top |
| inline | boolean | false | 是否行内表单 |
| disabled | boolean | false | 是否禁用整个表单 |
| size | string | 'default' | 表单尺寸:large/default/small |
| gutter | number | 20 | 栅格间隔 |
| showButtons | boolean | true | 是否显示操作按钮 |
| showSubmit | boolean | true | 是否显示提交按钮 |
| showReset | boolean | true | 是否显示重置按钮 |
| submitText | string | '提交' | 提交按钮文本 |
| resetText | string | '重置' | 重置按钮文本 |
| submitLoading | boolean | false | 提交按钮加载状态 |
| customButtons | array | [] | 自定义按钮配置 |
### items (表单项配置)
每个表单项都支持以下基础配置:
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| type | string | - | 表单项类型(必填) |
| prop | string | - | 表单字段名(必填) |
| label | string | - | 表单标签 |
| placeholder | string | - | 占位符文本 |
| required | boolean | false | 是否必填 |
| disabled | boolean | false | 是否禁用 |
| readonly | boolean | false | 是否只读 |
| span | number | 24 | 栅格占位格数 |
| offset | number | 0 | 栅格左侧间隔格数 |
| xs/sm/md/lg/xl | number | - | 响应式栅格配置 |
| rules | array | - | 验证规则 |
#### 不同类型的特殊配置
##### input (输入框)
| 属性 | 类型 | 说明 |
|------|------|------|
| clearable | boolean | 是否可清空 |
| maxlength | number | 最大输入长度 |
| showWordLimit | boolean | 是否显示字数统计 |
| inputType | string | 输入框类型:text/password等 |
| rows | number | 文本域行数 |
| autosize | boolean/object | 文本域自适应高度 |
| prepend | string | 前置内容 |
| append | string | 后置内容 |
| prefix | string | 前缀图标 |
| suffix | string | 后缀图标 |
##### select (选择器)
| 属性 | 类型 | 说明 |
|------|------|------|
| clearable | boolean | 是否可清空 |
| multiple | boolean | 是否多选 |
| filterable | boolean | 是否可搜索 |
| remote | boolean | 是否远程搜索 |
| remoteMethod | function | 远程搜索方法 |
| loading | boolean | 是否加载中 |
| options | array | 选项数据 |
##### radio/checkbox (单选/复选框)
| 属性 | 类型 | 说明 |
|------|------|------|
| border | boolean | 是否显示边框 |
| options | array | 选项数据 |
| min/max | number | 复选框最少/最多选择数量 |
##### date (日期选择器)
| 属性 | 类型 | 说明 |
|------|------|------|
| dateType | string | 日期类型:date/daterange等 |
| format | string | 显示格式 |
| valueFormat | string | 绑定值格式 |
| startPlaceholder | string | 范围选择时开始日期占位符 |
| endPlaceholder | string | 范围选择时结束日期占位符 |
| disabledDate | function | 禁用日期函数 |
##### upload (上传)
| 属性 | 类型 | 说明 |
|------|------|------|
| action | string | 上传地址 |
| headers | object | 请求头 |
| multiple | boolean | 是否多选 |
| accept | string | 接受文件类型 |
| drag | boolean | 是否拖拽上传 |
| limit | number | 上传文件数量限制 |
| tip | string | 提示文本 |
## Events
| 事件名 | 参数 | 说明 |
|--------|------|------|
| update:modelValue | formData | 表单数据更新 |
| submit | formData | 表单提交 |
| reset | - | 表单重置 |
| change | prop, value | 表单项值变化 |
| input | prop, value | 输入事件 |
| focus | prop, event | 获得焦点 |
| blur | prop, event | 失去焦点 |
| validate | isValid, error | 表单验证 |
## Methods
通过 ref 可以调用以下方法:
| 方法名 | 参数 | 说明 |
|--------|------|------|
| validate | - | 验证整个表单 |
| validateField | prop | 验证指定字段 |
| clearValidate | props | 清除验证信息 |
| resetFields | - | 重置表单 |
## 完整示例
```javascript
const formItems = [
// 基础输入框
{
type: 'input',
prop: 'username',
label: '用户名',
placeholder: '请输入用户名',
clearable: true,
prefix: 'User',
rules: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
]
},
// 密码输入框
{
type: 'input',
prop: 'password',
label: '密码',
inputType: 'password',
placeholder: '请输入密码',
showPassword: true,
rules: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
]
},
// 数字输入框
{
type: 'number',
prop: 'age',
label: '年龄',
min: 18,
max: 100,
step: 1
},
// 选择器
{
type: 'select',
prop: 'city',
label: '城市',
placeholder: '请选择城市',
clearable: true,
filterable: true,
options: [
{ label: '北京', value: 'beijing' },
{ label: '上海', value: 'shanghai' },
{ label: '广州', value: 'guangzhou' },
{ label: '深圳', value: 'shenzhen' }
]
},
// 级联选择器
{
type: 'cascader',
prop: 'region',
label: '地区',
options: regionOptions,
props: {
value: 'code',
label: 'name',
children: 'children'
}
},
// 单选框
{
type: 'radio',
prop: 'gender',
label: '性别',
border: true,
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' }
]
},
// 复选框
{
type: 'checkbox',
prop: 'hobbies',
label: '爱好',
options: [
{ label: '读书', value: 'reading' },
{ label: '运动', value: 'sports' },
{ label: '音乐', value: 'music' },
{ label: '旅行', value: 'travel' }
]
},
// 开关
{
type: 'switch',
prop: 'status',
label: '状态',
activeText: '启用',
inactiveText: '禁用',
activeValue: 1,
inactiveValue: 0
},
// 滑块
{
type: 'slider',
prop: 'score',
label: '分数',
min: 0,
max: 100,
step: 5,
showStops: true
},
// 评分
{
type: 'rate',
prop: 'rating',
label: '评分',
max: 5,
showText: true,
texts: ['很差', '较差', '一般', '推荐', '非常推荐']
},
// 日期选择器
{
type: 'date',
prop: 'birthday',
label: '生日',
valueFormat: 'YYYY-MM-DD'
},
// 日期范围选择器
{
type: 'date',
prop: 'dateRange',
label: '日期范围',
dateType: 'daterange',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期'
},
// 时间选择器
{
type: 'time',
prop: 'workTime',
label: '工作时间',
valueFormat: 'HH:mm:ss'
},
// 颜色选择器
{
type: 'color',
prop: 'themeColor',
label: '主题色',
showAlpha: true,
predefine: ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C']
},
// 文本域
{
type: 'textarea',
prop: 'description',
label: '描述',
placeholder: '请输入描述',
rows: 4,
maxlength: 500,
showWordLimit: true
},
// 上传
{
type: 'upload',
prop: 'avatar',
label: '头像',
action: '/api/upload',
accept: 'image/*',
limit: 1,
tip: '只能上传jpg/png文件,且不超过2MB'
},
// 自定义插槽
{
type: 'slot',
prop: 'customField',
label: '自定义字段',
slotName: 'customSlot'
}
]
```
## 注意事项
1. **表单数据绑定**:使用 `v-model` 双向绑定表单数据
2. **验证规则**:支持在 `rules` prop 中定义全局规则,也可以在每个表单项中单独定义
3. **响应式布局**:使用 Element Plus 的栅格系统,支持 `xs/sm/md/lg/xl` 响应式配置
4. **自定义插槽**:通过 `type: 'slot'` 可以插入自定义内容
5. **文件上传**:需要配合后端接口实现文件上传功能
这个组件提供了表单开发中所需的大部分功能,通过配置化的方式可以快速构建各种类型的表单。
\ No newline at end of file
<template>
<div class="config-form">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
:label-width="config.labelWidth || '120px'"
:label-position="config.labelPosition || 'right'"
:inline="config.inline || false"
:disabled="config.disabled || false"
:size="config.size || 'default'"
>
<el-row :gutter="config.gutter || 20">
<el-col
v-for="(item, index) in formItems"
:key="item.prop || index"
:span="item.span || 24"
:offset="item.offset || 0"
:xs="item.xs"
:sm="item.sm"
:md="item.md"
:lg="item.lg"
:xl="item.xl"
>
<el-form-item
:label="item.label"
:prop="item.prop"
:required="item.required"
:error="item.error"
:show-message="item.showMessage !== false"
>
<!-- 输入框 -->
<el-input
v-if="item.type === 'input'"
v-model="formData[item.prop]"
:placeholder="item.placeholder || `请输入${item.label}`"
:clearable="item.clearable !== false"
:disabled="item.disabled"
:readonly="item.readonly"
:maxlength="item.maxlength"
:show-word-limit="item.showWordLimit"
:type="item.inputType || 'text'"
:rows="item.rows"
:autosize="item.autosize"
@blur="handleBlur(item, $event)"
@focus="handleFocus(item, $event)"
@input="handleInput(item, $event)"
@change="handleChange(item, $event)"
>
<template v-if="item.prepend" #prepend>{{
item.prepend
}}</template>
<template v-if="item.append" #append>{{ item.append }}</template>
<template v-if="item.prefix" #prefix>
<el-icon><component :is="item.prefix" /></el-icon>
</template>
<template v-if="item.suffix" #suffix>
<el-icon><component :is="item.suffix" /></el-icon>
</template>
</el-input>
<!-- 文本域 -->
<el-input
v-else-if="item.type === 'textarea'"
v-model="formData[item.prop]"
type="textarea"
:placeholder="item.placeholder || `请输入${item.label}`"
:clearable="item.clearable !== false"
:disabled="item.disabled"
:readonly="item.readonly"
:maxlength="item.maxlength"
:show-word-limit="item.showWordLimit"
:rows="item.rows || 3"
:autosize="item.autosize"
@blur="handleBlur(item, $event)"
@focus="handleFocus(item, $event)"
@input="handleInput(item, $event)"
@change="handleChange(item, $event)"
/>
<!-- 选择器 -->
<el-select
v-else-if="item.type === 'select'"
v-model="formData[item.prop]"
:placeholder="item.placeholder || `请选择${item.label}`"
:clearable="item.clearable !== false"
:disabled="item.disabled"
:multiple="item.multiple"
:filterable="item.filterable"
:remote="item.remote"
:remote-method="item.remoteMethod"
:loading="item.loading"
:no-match-text="item.noMatchText"
:no-data-text="item.noDataText"
@change="handleChange(item, $event)"
@visible-change="handleVisibleChange(item, $event)"
@remove-tag="handleRemoveTag(item, $event)"
@clear="handleClear(item)"
>
<el-option
v-for="option in item.options"
:key="option.value"
:label="option.label"
:value="option.value"
:disabled="option.disabled"
>
<span v-if="option.icon" style="margin-right: 8px">
<el-icon><component :is="option.icon" /></el-icon>
</span>
{{ option.label }}
</el-option>
</el-select>
<!-- 单选框组 -->
<el-radio-group
v-else-if="item.type === 'radio'"
v-model="formData[item.prop]"
:disabled="item.disabled"
:size="item.size"
@change="handleChange(item, $event)"
>
<el-radio
v-for="option in item.options"
:key="option.value"
:label="option.value"
:disabled="option.disabled"
:border="item.border"
>
{{ option.label }}
</el-radio>
</el-radio-group>
<!-- 复选框组 -->
<el-checkbox-group
v-else-if="item.type === 'checkbox'"
v-model="formData[item.prop]"
:disabled="item.disabled"
:size="item.size"
:min="item.min"
:max="item.max"
@change="handleChange(item, $event)"
>
<el-checkbox
v-for="option in item.options"
:key="option.value"
:label="option.value"
:disabled="option.disabled"
:border="item.border"
>
{{ option.label }}
</el-checkbox>
</el-checkbox-group>
<!-- 日期选择器 -->
<el-date-picker
v-else-if="item.type === 'date'"
v-model="formData[item.prop]"
:type="item.dateType || 'date'"
:placeholder="item.placeholder || `请选择${item.label}`"
:format="item.format"
:value-format="item.valueFormat"
:clearable="item.clearable !== false"
:disabled="item.disabled"
:readonly="item.readonly"
:editable="item.editable"
:start-placeholder="item.startPlaceholder"
:end-placeholder="item.endPlaceholder"
:range-separator="item.rangeSeparator"
:disabled-date="item.disabledDate"
@change="handleChange(item, $event)"
@blur="handleBlur(item, $event)"
@focus="handleFocus(item, $event)"
/>
<!-- 时间选择器 -->
<el-time-picker
v-else-if="item.type === 'time'"
v-model="formData[item.prop]"
:placeholder="item.placeholder || `请选择${item.label}`"
:format="item.format"
:value-format="item.valueFormat"
:clearable="item.clearable !== false"
:disabled="item.disabled"
:readonly="item.readonly"
:editable="item.editable"
@change="handleChange(item, $event)"
@blur="handleBlur(item, $event)"
@focus="handleFocus(item, $event)"
/>
<!-- 数字输入框 -->
<el-input-number
v-else-if="item.type === 'number'"
v-model="formData[item.prop]"
:min="item.min"
:max="item.max"
:step="item.step || 1"
:step-strictly="item.stepStrictly"
:precision="item.precision"
:size="item.size"
:disabled="item.disabled"
:controls="item.controls !== false"
:controls-position="item.controlsPosition"
:name="item.name"
@change="handleChange(item, $event)"
@focus="handleFocus(item, $event)"
@blur="handleBlur(item, $event)"
/>
<!-- 开关 -->
<el-switch
v-else-if="item.type === 'switch'"
v-model="formData[item.prop]"
:disabled="item.disabled"
:loading="item.loading"
:size="item.size"
:active-text="item.activeText"
:inactive-text="item.inactiveText"
:active-value="item.activeValue"
:inactive-value="item.inactiveValue"
@change="handleChange(item, $event)"
/>
<!-- 滑块 -->
<el-slider
v-else-if="item.type === 'slider'"
v-model="formData[item.prop]"
:min="item.min || 0"
:max="item.max || 100"
:step="item.step || 1"
:show-stops="item.showStops"
:show-tooltip="item.showTooltip !== false"
:disabled="item.disabled"
:range="item.range"
:vertical="item.vertical"
:height="item.height"
@change="handleChange(item, $event)"
/>
<!-- 评分 -->
<el-rate
v-else-if="item.type === 'rate'"
v-model="formData[item.prop]"
:max="item.max || 5"
:disabled="item.disabled"
:allow-half="item.allowHalf"
:low-threshold="item.lowThreshold"
:high-threshold="item.highThreshold"
:colors="item.colors"
:void-color="item.voidColor"
:disabled-void-color="item.disabledVoidColor"
:icon-classes="item.iconClasses"
:void-icon-class="item.voidIconClass"
:disabled-void-icon-class="item.disabledVoidIconClass"
:show-text="item.showText"
:show-score="item.showScore"
:texts="item.texts"
@change="handleChange(item, $event)"
/>
<!-- 颜色选择器 -->
<el-color-picker
v-else-if="item.type === 'color'"
v-model="formData[item.prop]"
:disabled="item.disabled"
:size="item.size"
:show-alpha="item.showAlpha"
:color-format="item.colorFormat"
:predefine="item.predefine"
@change="handleChange(item, $event)"
@active-change="handleActiveChange(item, $event)"
/>
<!-- 树形选择器 -->
<el-tree-select
v-else-if="item.type === 'tree'"
v-model="formData[item.prop]"
:data="item.data"
:props="
item.props || {
label: 'label',
value: 'value',
children: 'children',
}
"
:placeholder="item.placeholder || `请选择${item.label}`"
:clearable="item.clearable !== false"
:disabled="item.disabled"
:filterable="item.filterable"
:check-strictly="item.checkStrictly"
:render-after-expand="item.renderAfterExpand !== false"
:show-checkbox="item.showCheckbox"
:multiple="item.multiple"
:collapse-tags="item.collapseTags"
:max-collapse-tags="item.maxCollapseTags"
:node-key="item.nodeKey"
:default-expanded-keys="item.defaultExpandedKeys"
:default-checked-keys="item.defaultCheckedKeys"
:current-node-key="item.currentNodeKey"
:expand-on-click-node="item.expandOnClickNode !== false"
:check-on-click-node="item.checkOnClickNode"
:auto-expand-parent="item.autoExpandParent !== false"
:default-expand-all="item.defaultExpandAll"
:indent="item.indent"
:icon="item.icon"
:lazy="item.lazy"
:load="item.load"
:highlight-current="item.highlightCurrent"
@change="handleChange(item, $event)"
@node-click="handleNodeClick(item, $event)"
@check="handleCheck(item, $event)"
@node-expand="handleNodeExpand(item, $event)"
@node-collapse="handleNodeCollapse(item, $event)"
/>
<!-- 上传 -->
<el-upload
v-else-if="item.type === 'upload'"
:action="item.action"
:headers="item.headers"
:method="item.method"
:multiple="item.multiple"
:data="item.data"
:name="item.name"
:with-credentials="item.withCredentials"
:show-file-list="item.showFileList !== false"
:drag="item.drag"
:accept="item.accept"
:auto-upload="item.autoUpload !== false"
:limit="item.limit"
:on-exceed="item.onExceed"
:on-success="
(response, file, fileList) =>
handleUploadSuccess(item, response, file, fileList)
"
:on-error="
(error, file, fileList) =>
handleUploadError(item, error, file, fileList)
"
:on-progress="
(event, file, fileList) =>
handleUploadProgress(item, event, file, fileList)
"
:on-change="
(file, fileList) => handleUploadChange(item, file, fileList)
"
:on-remove="
(file, fileList) => handleUploadRemove(item, file, fileList)
"
:on-preview="item.onPreview"
:before-upload="item.beforeUpload"
:before-remove="item.beforeRemove"
:http-request="item.httpRequest"
>
<el-button v-if="!item.drag" type="primary">{{
item.buttonText || "点击上传"
}}</el-button>
<div v-else class="upload-dragger">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
</div>
<template v-if="item.tip" #tip>
<div class="el-upload__tip">{{ item.tip }}</div>
</template>
</el-upload>
<!-- 自定义插槽 -->
<slot
v-else-if="item.type === 'slot'"
:name="item.slotName || item.prop"
:prop="item.prop"
:item="item"
:value="formData[item.prop]"
:onChange="(value) => handleSlotChange(item, value)"
/>
<!-- 纯文本显示 -->
<span v-else-if="item.type === 'text'">{{
formData[item.prop]
}}</span>
<!-- 自定义组件 -->
<component
v-else-if="item.type === 'component' && item.component"
:is="item.component"
v-model="formData[item.prop]"
v-bind="item.componentProps"
@change="handleChange(item, $event)"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 表单操作按钮 -->
<el-form-item v-if="config.showButtons !== false">
<el-button
v-if="config.showSubmit !== false"
type="primary"
:loading="config.submitLoading"
@click="handleSubmit"
>
{{ config.submitText || "提交" }}
</el-button>
<el-button v-if="config.showReset !== false" @click="handleReset">
{{ config.resetText || "重置" }}
</el-button>
<el-button
v-for="btn in config.customButtons"
:key="btn.key"
:type="btn.type"
:loading="btn.loading"
@click="btn.handler"
>
{{ btn.text }}
</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted } from "vue";
import { UploadFilled } from "@element-plus/icons-vue";
const props = defineProps({
// 表单配置
config: {
type: Object,
default: () => ({}),
},
// 表单项配置
items: {
type: Array,
default: () => [],
},
// 表单数据
modelValue: {
type: Object,
default: () => ({}),
},
// 表单规则
rules: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits([
"update:modelValue",
"submit",
"reset",
"change",
"blur",
"focus",
"input",
"validate",
]);
const formRef = ref();
const formData = reactive({});
// 表单项配置
const formItems = computed(() => props.items);
// 表单验证规则
const formRules = computed(() => {
const rules = { ...props.rules };
formItems.value.forEach((item) => {
if (item.rules) {
rules[item.prop] = item.rules;
}
});
return rules;
});
// 初始化表单数据
const initFormData = () => {
Object.keys(formData).forEach((key) => {
delete formData[key];
});
formItems.value.forEach((item) => {
if (item.prop) {
if (props.modelValue[item.prop] !== undefined) {
formData[item.prop] = props.modelValue[item.prop];
} else {
formData[item.prop] =
item.defaultValue !== undefined
? item.defaultValue
: getDefaultValue(item.type);
}
}
});
};
// 获取不同类型的默认值
const getDefaultValue = (type) => {
switch (type) {
case "checkbox":
return [];
case "switch":
return false;
case "number":
return 0;
case "rate":
return 0;
default:
return "";
}
};
// 监听表单数据变化
watch(
formData,
(newVal) => {
emit("update:modelValue", { ...newVal });
emit("change", newVal);
},
{ deep: true }
);
// 监听外部数据变化
watch(
() => props.modelValue,
(newVal) => {
Object.keys(newVal).forEach((key) => {
if (formData.hasOwnProperty(key)) {
formData[key] = newVal[key];
}
});
},
{ deep: true }
);
// 事件处理
const handleInput = (item, value) => {
emit("input", item.prop, value);
};
const handleChange = (item, value) => {
emit("change", item.prop, value);
};
const handleBlur = (item, event) => {
emit("blur", item.prop, event);
};
const handleFocus = (item, event) => {
emit("focus", item.prop, event);
};
const handleVisibleChange = (item, visible) => {
emit("visible-change", item.prop, visible);
};
const handleRemoveTag = (item, tag) => {
emit("remove-tag", item.prop, tag);
};
const handleClear = (item) => {
emit("clear", item.prop);
};
const handleActiveChange = (item, color) => {
emit("active-change", item.prop, color);
};
const handleSlotChange = (item, value) => {
formData[item.prop] = value;
emit("change", item.prop, value);
};
// 树形选择器事件处理
const handleNodeClick = (item, data) => {
emit("node-click", item.prop, data);
};
const handleCheck = (item, data) => {
emit("check", item.prop, data);
};
const handleNodeExpand = (item, data) => {
emit("node-expand", item.prop, data);
};
const handleNodeCollapse = (item, data) => {
emit("node-collapse", item.prop, data);
};
// 上传事件处理
const handleUploadSuccess = (item, response, file, fileList) => {
if (item.onSuccess) {
item.onSuccess(response, file, fileList);
}
emit("upload-success", item.prop, response, file, fileList);
};
const handleUploadError = (item, error, file, fileList) => {
if (item.onError) {
item.onError(error, file, fileList);
}
emit("upload-error", item.prop, error, file, fileList);
};
const handleUploadProgress = (item, event, file, fileList) => {
if (item.onProgress) {
item.onProgress(event, file, fileList);
}
emit("upload-progress", item.prop, event, file, fileList);
};
const handleUploadChange = (item, file, fileList) => {
if (item.onChange) {
item.onChange(file, fileList);
}
emit("upload-change", item.prop, file, fileList);
};
const handleUploadRemove = (item, file, fileList) => {
if (item.onRemove) {
item.onRemove(file, fileList);
}
emit("upload-remove", item.prop, file, fileList);
};
// 表单提交
const handleSubmit = async () => {
try {
const valid = await formRef.value.validate();
if (valid) {
emit("submit", { ...formData });
}
} catch (error) {
emit("validate", false, error);
}
};
// 表单重置
const handleReset = () => {
formRef.value.resetFields();
emit("reset");
};
// 表单验证
const validate = async () => {
try {
const valid = await formRef.value.validate();
emit("validate", true);
return valid;
} catch (error) {
emit("validate", false, error);
return false;
}
};
// 验证指定字段
const validateField = async (prop) => {
try {
await formRef.value.validateField(prop);
return true;
} catch (error) {
return false;
}
};
// 清除验证
const clearValidate = (props) => {
formRef.value.clearValidate(props);
};
// 暴露方法
defineExpose({
validate,
validateField,
clearValidate,
resetFields: handleReset,
formRef,
});
onMounted(() => {
initFormData();
});
</script>
<style lang="less" scoped>
.config-form {
.upload-dragger {
text-align: center;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
:deep(.el-form-item) {
margin-bottom: 18px;
}
:deep(.el-input-number) {
width: 100%;
}
:deep(.el-select) {
width: 100%;
}
:deep(.el-date-editor) {
width: 100%;
}
:deep(.el-time-picker) {
width: 100%;
}
:deep(.el-form-item) {
&:last-child {
.el-form-item__content {
justify-content: flex-end;
}
}
}
:deep(.el-form--inline .el-form-item) {
display: flex;
}
}
</style>
...@@ -11,11 +11,12 @@ ...@@ -11,11 +11,12 @@
<div class="table-container"> <div class="table-container">
<el-table <el-table
style="width: 100%"
:data="tableData" :data="tableData"
:height="height" :height="tableHeight"
:max-height="maxHeight" :max-height="maxHeight"
:stripe="stripe" :stripe="stripe"
:border="border" border
:size="size" :size="size"
:fit="fit" :fit="fit"
:show-header="showTableHeader" :show-header="showTableHeader"
...@@ -165,7 +166,10 @@ const props = defineProps({ ...@@ -165,7 +166,10 @@ const props = defineProps({
default: true, default: true,
}, },
// 表格高度 // 表格高度
height: [String, Number], tableHeight: {
type: [String, Number],
default: "auto",
},
// 表格最大高度 // 表格最大高度
maxHeight: [String, Number], maxHeight: [String, Number],
// 是否为斑马纹表格 // 是否为斑马纹表格
...@@ -516,32 +520,7 @@ const handleNextClick = (val) => { ...@@ -516,32 +520,7 @@ const handleNextClick = (val) => {
} }
} }
// 响应式设计
@media (max-width: 768px) {
.common-table {
padding: 12px;
.table-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.table-footer {
justify-content: center;
.el-pagination {
justify-content: center;
}
}
}
}
:deep(.el-table) { :deep(.el-table) {
th.el-table__cell {
// background-color: var(--el-table-header-bg-color);
// background-color: #e8ebf0;
background-color: #f2f6fd;
}
thead { thead {
color: var(--el-table-header-text-color); color: var(--el-table-header-text-color);
} }
...@@ -556,14 +535,10 @@ const handleNextClick = (val) => { ...@@ -556,14 +535,10 @@ const handleNextClick = (val) => {
.el-pagination.is-background { .el-pagination.is-background {
.el-pager li { .el-pager li {
border: 1px solid #e7e9ee; border: 1px solid #e7e9ee;
// background-color: #fff;
font-weight: 400; font-weight: 400;
// color: #333;
} }
.el-pager li:not(.disabled).is-active { .el-pager li:not(.disabled).is-active {
// background: rgba(91, 183, 59, 0.1);
border: none; border: none;
// color: var(--el-color-primary);
font-weight: 400; font-weight: 400;
} }
.btn-prev, .btn-prev,
...@@ -571,48 +546,23 @@ const handleNextClick = (val) => { ...@@ -571,48 +546,23 @@ const handleNextClick = (val) => {
background-color: var(--el-disabled-bg-color); background-color: var(--el-disabled-bg-color);
} }
} }
.el-table__fixed-right-patch { }
top: 0; :deep(.el-table) {
border: 1px solid #fff;
}
th.el-table__cell { th.el-table__cell {
border-right: 1px solid #ebeef5; background: #f5f7fa;
height: 50px;
text-align: center;
} }
.el-table__fixed-right { .el-table__body {
top: -1px; td.el-table__cell {
height: 45px;
line-height: 45px;
} }
.el-table--border,
.el-table--group {
border: 0px;
} }
.el-table__cell { .el-table__cell {
border-right: 0; .cell {
} text-align: center;
&::before {
width: 0px;
} }
&::after {
width: 0px;
} }
.el-table__border-left-patch {
width: 0;
}
}
:deep(.el-table__header .cell) {
display: flex;
align-items: center;
}
:deep(.el-table) {
background-color: rgba(255, 255, 255, 0.5);
}
.common-table {
background: rgb(157 188 218 / 40%);
}
:deep(.el-table th.el-table__cell) {
background: rgba(4, 66, 126, 0.4);
color: #fff;
}
:deep(.el-table tr) {
background: rgb(157 188 218 / 40%);
} }
</style> </style>
...@@ -33,23 +33,32 @@ ...@@ -33,23 +33,32 @@
router router
> >
<template v-for="route in menuRoutes" :key="route.path"> <template v-for="route in menuRoutes" :key="route.path">
<el-menu-item v-if="!route.meta?.parent" :index="route.path"> <!-- 无子菜单的项目 -->
<el-menu-item
v-if="!route.children || route.children.length === 0"
:index="route.path"
>
<el-icon><component :is="route.meta?.icon || 'menu'" /></el-icon> <el-icon><component :is="route.meta?.icon || 'menu'" /></el-icon>
<span>{{ route.meta?.menuName || route.name }}</span> <span>{{ route.meta?.menuName || route.name }}</span>
</el-menu-item> </el-menu-item>
<el-sub-menu v-else :index="route.meta.parent">
<!-- 有子菜单的项目 -->
<el-sub-menu v-else :index="route.path">
<template #title> <template #title>
<el-icon <el-icon
><component :is="route.meta?.icon || 'menu'" ><component :is="route.meta?.icon || 'menu'"
/></el-icon> /></el-icon>
<span>{{ route.meta.parent }}</span> <span>{{ route.meta?.menuName || route.name }}</span>
</template> </template>
<template v-for="child in route.children" :key="child.path">
<el-menu-item <el-menu-item
:index="route.path" :index="child.path"
style="padding-left: 70px !important" style="padding-left: 20px !important"
> >
{{ route.meta.menuName }} <el-icon><component :is="child.meta?.icon || ''" /></el-icon>
<span>{{ child.meta?.menuName || child.name }}</span>
</el-menu-item> </el-menu-item>
</template>
</el-sub-menu> </el-sub-menu>
</template> </template>
</el-menu> </el-menu>
...@@ -64,7 +73,7 @@ ...@@ -64,7 +73,7 @@
</template> </template>
<script setup> <script setup>
import { computed } from "vue"; import { computed, h, resolveComponent } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
...@@ -72,7 +81,11 @@ const router = useRouter(); ...@@ -72,7 +81,11 @@ const router = useRouter();
// 计算菜单路由 // 计算菜单路由
const menuRoutes = computed(() => { const menuRoutes = computed(() => {
const mainRoute = router.options.routes.find((route) => route.path === "/"); const mainRoute = router.options.routes.find((route) => route.path === "/");
return mainRoute?.children && mainRoute?.children.filter(route => !route.meta || route.meta.showInMenu !== false ) || []; if (!mainRoute?.children) return [];
return mainRoute.children.filter(
(route) => !route.meta || route.meta.showInMenu !== false
);
}); });
// 处理退出登录 // 处理退出登录
...@@ -92,7 +105,7 @@ const handleLogout = () => { ...@@ -92,7 +105,7 @@ const handleLogout = () => {
} }
/* 选中菜单项样式 */ /* 选中菜单项样式 */
.el-menu-item{ .el-menu-item {
color: #666; color: #666;
background-color: #fff; background-color: #fff;
} }
...@@ -102,6 +115,12 @@ const handleLogout = () => { ...@@ -102,6 +115,12 @@ const handleLogout = () => {
border-right: 4px solid #3d84ee; border-right: 4px solid #3d84ee;
} }
/* 完全去掉系统管理子菜单的背景色 */
:deep(.el-sub-menu__title) {
color: #666;
background-color: #fff !important;
}
.city-header { .city-header {
width: 100%; width: 100%;
height: 64px; height: 64px;
......
...@@ -20,7 +20,7 @@ const routes = [ ...@@ -20,7 +20,7 @@ const routes = [
name: '数据大屏', name: '数据大屏',
title: 'dataSummary', title: 'dataSummary',
component: () => import('@/views/homePage/index.vue'), component: () => import('@/views/homePage/index.vue'),
meta: { menuName: '数据大屏', icon: 'home' } meta: { menuName: '数据大屏', icon: 'platform' }
}, },
{ {
path: '/projectManage', path: '/projectManage',
...@@ -38,8 +38,24 @@ const routes = [ ...@@ -38,8 +38,24 @@ const routes = [
menuName: '新增项目', menuName: '新增项目',
showInMenu: false // 不在菜单中显示 showInMenu: false // 不在菜单中显示
} }
},
{
path: '/systemManage',
name: '系统管理',
title: 'systemManage',
// component: () => import('@/views/systemManage/index.vue'),
meta: { menuName: '系统管理', icon: 'tools' },
children: [
{
path: '/systemManage/userManage',
name: '用户管理',
title: 'userManage',
component: () => import('@/views/systemManage/userManage.vue'),
meta: { menuName: '用户管理',}
} }
] ]
},
]
} }
] ]
......
全局样式重置和滚动条控制 /* 全局样式重置和滚动条控制 */
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
...@@ -18,6 +18,7 @@ html, body { ...@@ -18,6 +18,7 @@ html, body {
#app { #app {
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
background: #f5f5f5;
overflow: hidden; /* 确保应用容器不产生滚动条 */ overflow: hidden; /* 确保应用容器不产生滚动条 */
} }
......
<template>
<div class="form-demo">
<h2>可配置表单组件示例</h2>
<!-- 基础表单示例 -->
<el-card class="demo-card" header="基础表单">
<config-form
v-model="basicForm"
:config="basicConfig"
:items="basicItems"
:rules="basicRules"
@submit="handleBasicSubmit"
@reset="handleBasicReset"
/>
</el-card>
<!-- 复杂表单示例 -->
<el-card class="demo-card" header="复杂表单">
<config-form
v-model="complexForm"
:config="complexConfig"
:items="complexItems"
:rules="complexRules"
@submit="handleComplexSubmit"
@reset="handleComplexReset"
@change="handleComplexChange"
/>
</el-card>
<!-- 树形选择器示例 -->
<el-card class="demo-card" header="树形选择器">
<config-form
v-model="treeForm"
:config="treeConfig"
:items="treeItems"
@submit="handleTreeSubmit"
@node-click="handleNodeClick"
@check="handleTreeCheck"
/>
</el-card>
<!-- 自定义表单示例 -->
<el-card class="demo-card" header="自定义表单">
<config-form
v-model="customForm"
:config="customConfig"
:items="customItems"
@submit="handleCustomSubmit"
>
<template #customSlot="{ item, value, onChange }">
<el-input
:model-value="value"
placeholder="这是一个自定义插槽"
@input="onChange"
>
<template #append>
<el-button @click="handleCustomAction(item)">自定义操作</el-button>
</template>
</el-input>
</template>
</config-form>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import ConfigForm from '../ConfigForm.vue'
// 基础表单数据
const basicForm = ref({
name: '',
age: null,
gender: '',
email: '',
description: ''
})
// 基础表单配置
const basicConfig = {
labelWidth: '100px',
showButtons: true,
submitText: '保存',
resetText: '清空'
}
// 基础表单项配置
const basicItems = [
{
type: 'input',
prop: 'name',
label: '姓名',
placeholder: '请输入姓名',
clearable: true,
rules: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
]
},
{
type: 'number',
prop: 'age',
label: '年龄',
min: 1,
max: 120,
rules: [
{ required: true, message: '请输入年龄', trigger: 'blur' },
{ type: 'number', message: '年龄必须为数字', trigger: 'blur' }
]
},
{
type: 'radio',
prop: 'gender',
label: '性别',
required: true,
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' }
]
},
{
type: 'input',
prop: 'email',
label: '邮箱',
placeholder: '请输入邮箱地址',
rules: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
]
},
{
type: 'textarea',
prop: 'description',
label: '个人简介',
placeholder: '请输入个人简介',
rows: 3,
maxlength: 200,
showWordLimit: true
}
]
// 基础表单验证规则
const basicRules = {}
// 复杂表单数据
const complexForm = ref({
userInfo: {
name: '',
department: ''
},
skills: [],
level: 3,
isActive: true,
birthDate: '',
workTime: '',
avatar: '',
color: '#409EFF',
range: [20, 80]
})
// 复杂表单配置
const complexConfig = {
labelWidth: '120px',
gutter: 20,
showButtons: true,
submitText: '提交',
resetText: '重置',
customButtons: [
{
key: 'preview',
text: '预览',
type: 'info',
handler: handlePreview
}
]
}
// 复杂表单项配置
const complexItems = [
{
type: 'input',
prop: 'userInfo.name',
label: '姓名',
placeholder: '请输入姓名',
span: 12,
prefix: 'User'
},
{
type: 'select',
prop: 'userInfo.department',
label: '部门',
placeholder: '请选择部门',
span: 12,
clearable: true,
options: [
{ label: '技术部', value: 'tech' },
{ label: '产品部', value: 'product' },
{ label: '设计部', value: 'design' },
{ label: '运营部', value: 'operation' }
]
},
{
type: 'checkbox',
prop: 'skills',
label: '技能',
span: 12,
options: [
{ label: 'JavaScript', value: 'js' },
{ label: 'Vue.js', value: 'vue' },
{ label: 'React', value: 'react' },
{ label: 'Node.js', value: 'node' },
{ label: 'Python', value: 'python' }
]
},
{
type: 'rate',
prop: 'level',
label: '熟练度',
span: 12,
showText: true,
texts: ['新手', '初级', '中级', '高级', '专家']
},
{
type: 'switch',
prop: 'isActive',
label: '状态',
span: 8,
activeText: '激活',
inactiveText: '禁用'
},
{
type: 'date',
prop: 'birthDate',
label: '出生日期',
span: 8,
valueFormat: 'YYYY-MM-DD'
},
{
type: 'time',
prop: 'workTime',
label: '工作时间',
span: 8,
valueFormat: 'HH:mm:ss'
},
{
type: 'upload',
prop: 'avatar',
label: '头像',
span: 12,
action: '/api/upload',
accept: 'image/*',
tip: '只能上传jpg/png文件,且不超过500kb'
},
{
type: 'color',
prop: 'color',
label: '主题色',
span: 12,
showAlpha: true
},
{
type: 'slider',
prop: 'range',
label: '范围',
span: 24,
range: true,
min: 0,
max: 100,
showStops: true
}
]
// 复杂表单验证规则
const complexRules = {
'userInfo.name': [
{ required: true, message: '请输入姓名', trigger: 'blur' }
],
'userInfo.department': [
{ required: true, message: '请选择部门', trigger: 'change' }
],
skills: [
{ type: 'array', required: true, message: '请至少选择一项技能', trigger: 'change' }
]
}
// 树形选择器数据
const treeForm = ref({
department: '',
departments: [],
category: ''
})
// 部门树形数据
const departmentData = [
{
value: '1',
label: '总公司',
children: [
{
value: '1-1',
label: '技术中心',
children: [
{ value: '1-1-1', label: '前端开发部' },
{ value: '1-1-2', label: '后端开发部' },
{ value: '1-1-3', label: '测试部' }
]
},
{
value: '1-2',
label: '产品中心',
children: [
{ value: '1-2-1', label: '产品设计部' },
{ value: '1-2-2', label: '用户研究部' }
]
},
{
value: '1-3',
label: '运营中心',
children: [
{ value: '1-3-1', label: '市场推广部' },
{ value: '1-3-2', label: '客户服务部' }
]
}
]
},
{
value: '2',
label: '分公司A',
children: [
{ value: '2-1', label: '技术部' },
{ value: '2-2', label: '销售部' }
]
}
]
// 分类数据
const categoryData = [
{
value: 'electronics',
label: '电子产品',
children: [
{
value: 'phones',
label: '手机',
children: [
{ value: 'iphone', label: 'iPhone' },
{ value: 'android', label: 'Android手机' },
{ value: 'huawei', label: '华为手机' }
]
},
{
value: 'computers',
label: '电脑',
children: [
{ value: 'laptop', label: '笔记本电脑' },
{ value: 'desktop', label: '台式电脑' },
{ value: 'tablet', label: '平板电脑' }
]
}
]
},
{
value: 'clothing',
label: '服装',
children: [
{ value: 'mens', label: '男装' },
{ value: 'womens', label: '女装' },
{ value: 'kids', label: '童装' }
]
}
]
// 自定义表单数据
const customForm = ref({
customField: '',
status: 'pending'
})
// 树形选择器配置
const treeConfig = {
labelWidth: '120px',
showButtons: true,
submitText: '保存',
resetText: '重置'
}
// 树形选择器表单项配置
const treeItems = [
{
type: 'tree',
prop: 'department',
label: '所属部门',
placeholder: '请选择部门',
data: departmentData,
clearable: true,
filterable: true,
checkStrictly: true,
renderAfterExpand: false,
span: 12
},
{
type: 'tree',
prop: 'departments',
label: '多选部门',
placeholder: '请选择多个部门',
data: departmentData,
clearable: true,
filterable: true,
showCheckbox: true,
multiple: true,
collapseTags: true,
maxCollapseTags: 3,
span: 12
},
{
type: 'tree',
prop: 'category',
label: '商品分类',
placeholder: '请选择分类',
data: categoryData,
clearable: true,
filterable: true,
checkStrictly: true,
defaultExpandAll: true,
span: 24
}
]
// 自定义表单配置
const customConfig = {
labelWidth: '100px',
inline: true
}
// 自定义表单项配置
const customItems = [
{
type: 'slot',
prop: 'customField',
label: '自定义字段',
slotName: 'customSlot'
},
{
type: 'select',
prop: 'status',
label: '状态',
options: [
{ label: '待处理', value: 'pending' },
{ label: '处理中', value: 'processing' },
{ label: '已完成', value: 'completed' }
]
}
]
// 事件处理函数
const handleBasicSubmit = (formData) => {
console.log('基础表单提交:', formData)
ElMessage.success('基础表单提交成功')
}
const handleBasicReset = () => {
ElMessage.info('基础表单已重置')
}
const handleComplexSubmit = (formData) => {
console.log('复杂表单提交:', formData)
ElMessage.success('复杂表单提交成功')
}
const handleComplexReset = () => {
ElMessage.info('复杂表单已重置')
}
const handleComplexChange = (formData) => {
console.log('复杂表单数据变化:', formData)
}
const handleCustomSubmit = (formData) => {
console.log('自定义表单提交:', formData)
ElMessage.success('自定义表单提交成功')
}
const handleCustomAction = (item) => {
console.log('自定义操作:', item)
ElMessage.info('执行自定义操作')
}
const handleTreeSubmit = (formData) => {
console.log('树形表单提交:', formData)
ElMessage.success('树形表单提交成功')
}
const handleNodeClick = (prop, data) => {
console.log('节点点击:', prop, data)
ElMessage.info(`点击了节点: ${data.label}`)
}
const handleTreeCheck = (prop, data) => {
console.log('节点选择:', prop, data)
ElMessage.info(`选择了节点: ${data.checkedNodes?.map(n => n.label).join(', ')}`)
}
const handlePreview = () => {
console.log('预览数据:', complexForm.value)
ElMessage.info('预览功能')
}
</script>
<style lang="less" scoped>
.form-demo {
padding: 20px;
h2 {
margin-bottom: 20px;
color: #333;
}
.demo-card {
margin-bottom: 20px;
:deep(.el-card__header) {
background-color: #f5f7fa;
font-weight: 600;
}
}
}
</style>
\ No newline at end of file
...@@ -147,7 +147,7 @@ const recycleList = reactive([ ...@@ -147,7 +147,7 @@ const recycleList = reactive([
.vw(left,65); .vw(left,65);
height: 30%; height: 30%;
max-width: 83%; max-width: 83%;
background-image: url(/src/assets/images/baseRateRel.png); background-image: url(/src/assets/images/progress.png);
background-size: 100% 100%; background-size: 100% 100%;
background-repeat: no-repeat; background-repeat: no-repeat;
} }
......
...@@ -71,9 +71,9 @@ ...@@ -71,9 +71,9 @@
</template> </template>
<script setup> <script setup>
import Construct from "../components/Construct.vue"; import Construct from "./components/Construct.vue";
import ProjectApproval from "../components/ProjectApproval.vue"; import ProjectApproval from "./components/ProjectApproval.vue";
import Operation from "../components/Operation.vue"; import Operation from "./components/Operation.vue";
import { reactive, ref } from "vue"; import { reactive, ref } from "vue";
const selectedLeftBtn = ref("equity"); const selectedLeftBtn = ref("equity");
......
<template>
<div>系统管理 setting</div>
</template>
<script setup></script>
<style scoped lang="less"></style>
<template>
<div class="user-manage" v-loading="loading">
<!-- 查询表单 -->
<div class="search-form">
<commonForm
v-model="searchForm"
:config="searchConfig"
:items="searchItems"
@submit="handleSearch"
@reset="handleReset"
/>
</div>
<!-- 用户列表表格 -->
<div class="table-container">
<common-table
:tableHeight="tableHeight"
:data="tableData"
:columns="tableColumns"
:total="total"
:current-page="currentPage"
:page-size="pageSize"
title="用户列表"
:border="true"
@size-change="handleSizeChange"
@current-page-change="handleCurrentPageChange"
>
<template #header-actions>
<el-button type="primary" @click="handleAdd">
<!-- <el-icon><Plus /></el-icon> -->
新增
</el-button>
</template>
<template #enable="{ row }">
<el-switch
:model-value="row.enable === 0 ? true : false"
@change="handleStatusChange($event, row)"
active-color="#13ce66"
inactive-color="#ff4949"
></el-switch>
</template>
<template #operations="{ row, index }">
<el-button type="text" size="small" @click="handleEdit(row, index)">
编辑
</el-button>
<el-button type="text" size="small" @click="handleDelete(row, index)">
删除
</el-button>
</template>
</common-table>
</div>
<!-- 用户表单对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="600px"
@close="handleDialogClose"
>
<commonForm
v-model="userForm"
:config="formConfig"
:items="formItems"
:rules="formRules"
@submit="handleFormSubmit"
@reset="handleFormReset"
/>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Edit, Delete } from "@element-plus/icons-vue";
import commonForm from "@/components/common/commonForm.vue";
import CommonTable from "@/components/common/commonTable.vue";
import { da } from "element-plus/es/locales.mjs";
const { proxy } = getCurrentInstance();
const loading = ref(false);
// 计算表格高度
const tableHeight = computed(() => {
const headerHeight = 50;
const paginationHeight = 50;
const rowHeight = 40;
const baseHeight = headerHeight + paginationHeight;
// 1.如果数据超过10条,固定显示10行的高度 + 滚动条;2.如果数据不超过10条,按实际行数计算高度
const maxRows = Math.min(tableData.value.length, 10);
const contentHeight = maxRows * rowHeight;
return `${baseHeight + contentHeight}px`;
});
// 数据转换函数
const convertToTreeData = (apiData) => {
return apiData.map((item) => ({
value: item.id.toString(),
label: item.name,
children: item.children ? convertToTreeData(item.children) : [],
}));
};
// 查询表单数据
const searchForm = ref({
name: "",
mobile: "",
});
// 查询表单配置
const searchConfig = {
inline: true,
labelWidth: "80px",
showButtons: true,
submitText: "查询",
resetText: "重置",
};
// 查询表单项配置
const searchItems = [
{
type: "input",
prop: "name",
label: "用户姓名",
placeholder: "请输入用户姓名",
clearable: true,
span: 8,
},
{
type: "input",
prop: "mobile",
label: "手机号码",
placeholder: "请输入手机号码",
clearable: true,
span: 8,
},
];
// 表格数据
const tableData = ref([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(10);
// 表格列配置
const tableColumns = [
{
prop: "name",
label: "用户姓名",
minWidth: 100,
showOverflowTooltip: true,
},
{
prop: "departs",
label: "所属部门",
minWidth: 120,
showOverflowTooltip: true,
},
{
prop: "positions",
label: "岗位",
minWidth: 100,
showOverflowTooltip: true,
},
{
prop: "roles",
label: "角色",
minWidth: 100,
showOverflowTooltip: true,
},
{
prop: "mobile",
label: "手机号码",
minWidth: 100,
},
{
prop: "createdAt",
label: "创建时间",
minWidth: 160,
},
{
prop: "enable",
label: "状态",
width: 100,
slot: "enable",
align: "center",
},
{
prop: "operations",
label: "操作",
width: 160,
slot: "operations",
fixed: "right",
align: "center",
},
];
// 对话框相关
const dialogVisible = ref(false);
const dialogTitle = ref("新增用户");
const isEdit = ref(false);
const editIndex = ref(-1);
// 用户表单数据
const userForm = ref({
name: "",
departs: [],
positions: [],
roles: [],
enable: "0",
});
// 用户表单配置
const formConfig = {
labelWidth: "100px",
showButtons: true,
submitText: "保存",
resetText: "取消",
};
const departmentData = ref([]);
const positionsData = ref([]);
const rolesData = ref([]);
const loadDepartmentData = () => {
proxy.$post({
url: "/api/user/depart/treeDepart",
data: {},
callback: (data) => {
departmentData.value = convertToTreeData(data);
},
error: (err) => {},
});
};
// 岗位下拉数据
const loadPositionsData = () => {
proxy.$post({
url: "/api/user/position/listPosition",
data: {},
callback: (data) => {
positionsData.value = convertToTreeData(data.rows);
},
error: (err) => {},
});
};
// 角色下拉数据
const loadRolesData = () => {
proxy.$post({
url: "/api/user/role/listRole",
data: {
page: 1,
pageSize: 10,
},
callback: (data) => {
rolesData.value = convertToTreeData(data.rows);
},
error: (err) => {},
});
};
// 用户表单项配置
const formItems = computed(() => [
{
type: "input",
prop: "name",
label: "用户姓名",
placeholder: "请输入用户姓名",
// required: true,
span: 12,
// rules: [{ required: true, message: "请输入用户姓名", trigger: "blur" }],
},
{
type: "tree",
prop: "departs",
label: "所属部门",
placeholder: "请选择部门",
data: departmentData.value,
clearable: true,
filterable: true,
checkStrictly: true,
renderAfterExpand: false,
showCheckbox: true,
multiple: true,
collapseTags: true,
maxCollapseTags: 2,
span: 12,
},
{
type: "tree",
prop: "positions",
label: "岗位",
placeholder: "请选择岗位",
data: positionsData.value,
clearable: true,
filterable: true,
checkStrictly: true,
renderAfterExpand: false,
showCheckbox: true,
multiple: true,
collapseTags: true,
maxCollapseTags: 2,
span: 12,
},
{
type: "tree",
prop: "roles",
label: "角色",
placeholder: "请选择角色",
data: rolesData.value,
clearable: true,
filterable: true,
checkStrictly: true,
renderAfterExpand: false,
showCheckbox: true,
multiple: true,
collapseTags: true,
maxCollapseTags: 2,
span: 12,
},
{
type: "input",
prop: "mobile",
label: "手机号码",
placeholder: "请输入手机号码",
span: 12,
},
{
type: "radio",
prop: "enable",
label: "状态",
span: 12,
options: [
{ label: "启用", value: "0" },
{ label: "停用", value: "1" },
],
},
]);
// 表单验证规则
const formRules = {};
// 事件处理函数
const handleSearch = (formData) => {
currentPage.value = 1;
loadTableData();
};
const handleReset = () => {
searchForm.value = {
name: "",
mobile: "",
};
currentPage.value = 1;
loadTableData();
};
const handleSizeChange = (size) => {
pageSize.value = size;
currentPage.value = 1;
loadTableData();
};
const handleCurrentPageChange = (page) => {
currentPage.value = page;
loadTableData();
};
// 新增用户
const handleAdd = () => {
isEdit.value = false;
dialogTitle.value = "新增用户";
userForm.value = {
name: "",
departs: [],
positions: [],
roles: [],
enable: "0",
};
loadDepartmentData();
loadPositionsData();
loadRolesData();
dialogVisible.value = true;
};
let currentID = ref();
// 编辑
const handleEdit = (row, index) => {
isEdit.value = true;
dialogTitle.value = "编辑用户";
editIndex.value = index;
proxy.$post({
url: "/api/user/manage/getUserInfo",
data: { id: row.id },
callback: (data) => {
userForm.value = { ...data };
currentID.value = data.id;
},
error: (err) => {
ElMessage.error("编辑失败:", err);
},
});
dialogVisible.value = true;
};
// 删除
const handleDelete = async (row, index) => {
try {
await ElMessageBox.confirm(`确定要删除用户"${row.name}"吗?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
proxy.$post({
url: "/api/user/manage/deleteUser",
data: { id: row.id },
callback: (data) => {
dialogVisible.value = false;
loadTableData();
ElMessage.success("删除成功");
},
error: (err) => {
ElMessage.error("删除失败:", err);
},
});
loadTableData();
} catch {}
};
const handleFormSubmit = (formData) => {
if (isEdit.value) {
// 编辑用户
const updateUser = {
...formData,
departs: Array.isArray(formData.departs) ? formData.departs : [],
positions: Array.isArray(formData.positions) ? formData.positions : [],
roles: Array.isArray(formData.roles) ? formData.roles : [],
id: currentID.value,
};
proxy.$post({
url: "/api/user/manage/updateUser",
data: updateUser,
callback: (data) => {
dialogVisible.value = false;
loadTableData();
ElMessage.success("用户信息更新成功");
},
error: (err) => {
ElMessage.error("用户信息更新失败:", err);
},
});
} else {
// 新增用户
const newUser = {
...formData,
departs: Array.isArray(formData.departs) ? formData.departs : [],
positions: Array.isArray(formData.positions) ? formData.positions : [],
roles: Array.isArray(formData.roles) ? formData.roles : [],
};
proxy.$post({
url: "/api/user/manage/createUser",
data: newUser,
callback: (data) => {
dialogVisible.value = false;
loadTableData();
ElMessage.success("用户添加成功");
},
error: (err) => {
ElMessage.error("用户添加失败:", err);
},
});
}
};
const handleFormReset = () => {
dialogVisible.value = false;
};
const handleDialogClose = () => {
dialogVisible.value = false;
};
// 处理状态切换
const handleStatusChange = (newValue, row) => {
const newEnableValue = newValue ? "0" : "1";
proxy.$post({
url: "/api/user/manage/updateUser",
data: {
id: row.id,
enable: newEnableValue,
},
callback: (data) => {
row.enable = newEnableValue;
loadTableData();
ElMessage.success(
`用户状态已${newEnableValue === "0" ? "启用" : "停用"}`
);
},
error: (err) => {
ElMessage.error("状态更新失败");
},
});
};
// 表格数据
const loadTableData = () => {
loading.value = true;
proxy.$post({
url: "/api/user/manage/listUser",
data: {
...searchForm.value,
page: currentPage.value,
pageSize: pageSize.value,
},
callback: (data) => {
tableData.value = data.rows.map((item) => {
item.departs = item.departs.map((item) => item.name).join(",");
item.positions = item.positions.map((item) => item.name).join(",");
item.roles = item.roles.map((item) => item.name).join(",");
return item;
});
total.value = data.count;
loading.value = false;
},
error: (err) => {
loading.value = false;
ElMessage.error("加载数据失败");
},
});
};
onMounted(() => {
loadTableData();
loadDepartmentData();
loadPositionsData();
loadRolesData();
});
</script>
<style scoped lang="less">
.user-manage {
padding: 20px;
background: rgba(157, 188, 218, 0.1);
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
.search-form {
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
flex-shrink: 0;
}
.table-container {
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
// overflow: hidden;
// display: flex;
// flex-direction: column;
}
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment