说明:
本组件是一个基于若依的Vue 3版本和 Element Plus的表格组件,旨在提供灵活且可配置的表格展示方式。它支持加载状态、选择框、多种列类型(如开关、操作按钮和时间格式化),并可以通过 props 进行配置,以满足不同的业务需求(主要解决表格繁琐、代码太多、维护起来不方便的问题,进行代码优化、封装处理)。
代码gitee:git clone https://gitee.com/liu_yu_ting09/ruo_yi.git
备注:实在不想写,gitee上面代码更清晰
一、表格操作说明
- 加载状态:通过
loading
属性控制加载指示器的显示。- 多选功能:通过
selectionShow
属性控制是否显示选择框,便于用户进行批量操作。- 动态列配置:根据
columns
属性动态渲染表格列,可以轻松调整列的显示和隐藏。- 自定义操作按钮:支持在每行添加操作按钮,便于用户进行编辑、删除等操作。
- 时间格式化:支持将时间戳格式化为本地字符串显示,使数据更加友好易读。
- 响应式设计:组件能够适应不同屏幕尺寸,确保良好的用户体验。
二、组件属性说明
属性 类型 默认值 说明 loading Boolean false
控制加载状态 dataSource Array []
表格数据源 columns Array []
列配置 selectionShow Boolean false
是否显示选择框
三、事件说明
事件名 参数 说明 selectionChange selection: Array
当选择变化时触发 statusChange row: Object
当状态变化时触发 action action: Object, row: Object
当操作按钮被点击时触发
四、支持的列类型
- 普通文本列:直接显示文本内容。
- 开关列:使用
<el-switch>
组件,支持状态切换。- 操作列:自定义操作按钮,支持多种操作。
- 时间列:显示格式化后的时间,自动转换为用户本地时间。
JeecgListMixin.js说明
- additionalParams: (可选) 额外的参数,用于定制化混合函数的行为,例如 API 地址、表单配置等。
返回值
该函数返回一个对象,包含以下属性和方法:
状态变量
- loading:
ref(false)
- 表示当前是否正在加载数据的状态。- dataSource:
ref([])
- 存储从后端获取的数据源,通常用于展示在表格中。- total:
ref(0)
- 表示数据的总条数,用于分页等功能。- selectionRows:
ref([])
- 用于存储当前选中的行数据,支持多选功能。- selectedRowKeys:
ref([])
- 存储当前选中行的唯一标识(如 ID),便于后续操作(如删除)。方法
- loadData(index): 加载数据的函数,根据传入的
index
参数决定成功提示的消息类型。- handleAdd(): 新增按钮的操作,调用模态框中的新增方法。
- handleUpdate(row): 修改按钮的操作,接收一行数据并调用编辑方法。
- handleQuery(index): 搜索按钮操作,重置当前页码为1,并根据
index
调用loadData
加载数据。- handleDeleteOne(id): 删除单项操作,确认后调用删除方法删除指定的 ID 项,并在成功后重新加载数据。
- handleDeleteBatch(index): 批量删除操作,确认后删除选中的多项数据,将选中的行 ID 转换为字符串进行批量删除。
- handleSelectionChange(selection): 选中事件触发时调用,更新选中行的数据和对应的键值数组。
- searchReset(): 搜索重置操作,重置搜索表单并重新加载数据。
- handleExport(url, queryParams, name): 导出操作,接受请求地址、查询参数和文件名称,调用下载方法进行数据导出。
代码模块:若依的用户管理代码
JeecgListMixin.js(文件代码)
import { ref } from "vue";
export const useCommonMixin = (additionalParams) => {///公共参数
const { list, queryParams, registerModal,deleteOne,proxy,id,queryRef } = additionalParams; //传递过来的公共参数
const loading = ref(false);//是否加载中
const selectionRows = ref([]); //选中的数组,用于多选
const selectedRowKeys = ref([]);//选中的key数组
const dataSource = ref([]);//数据源
const total = ref(0);//总条数
function loadData(index){//加载数据
loading.value = true;
list(queryParams.value).then((res) => {
loading.value = false;
dataSource.value = res.rows;
total.value = res.total;
if(index ==1)proxy.$modal.msgSuccess("查询成功!");
if(index ==2)proxy.$modal.msgSuccess("重置成功!");
});
}
/** 新增按钮操作 */
function handleAdd() {
registerModal.value.add();
}
/** 修改按钮操作 */
function handleUpdate(row) {
registerModal.value.edit(row);
}
/** 搜索按钮操作 */
function handleQuery(index) {
queryParams.value.pageNum = 1;
if(index ==1) return loadData(2);
loadData(1);
}
// /** 一项删除按钮操作 */
function handleDeleteOne(id) {
proxy.$modal
.confirm('是否确认删除这一项吗?')
.then(function () {
return deleteOne(id);
})
.then(() => {
loadData();
proxy.$modal.msgSuccess("删除成功");
})
.catch(() => {});
}
// 选中事件触发
function handleSelectionChange(selection) {
selectionRows.value = selection;
selectedRowKeys.value = selection.map((item) => item[id]);;
}
function handleDeleteBatch(index) {
if (selectedRowKeys.value.length == 0) return proxy.$modal.msgWarning("请选择需要删除的数据");;
let ids = selectedRowKeys
if(index ==1) ids = selectedRowKeys.value.join(','); //批量删除转化字符串转化
proxy.$modal
.confirm(`是否确认批量删除 ${selectedRowKeys.value.length} 条数据?`)
.then(function () {
return deleteOne(ids.value);
})
.then(() => {
loadData();
proxy.$modal.msgSuccess("删除成功");
})
.catch(() => {});
}
function searchReset(){ //搜索重置
proxy.resetForm(queryRef);
handleQuery(1);
}
function handleExport(url,queryParams,name) { //导出 --参数 url 请求地址 queryParams 参数名 name 文件名称
proxy.download(
url,queryParams,name
);
}
return {
loading,
dataSource,
total,
loadData,
handleAdd,
handleUpdate,
selectionRows,
selectedRowKeys,
handleQuery,
handleDeleteOne,
handleSelectionChange,
handleDeleteBatch,
searchReset,
handleExport
};
};
Table(封装文件)UniversalTable.vue文件和ActionButtons.vue(文件)
<template>
<div>
<!-- 表格组件,显示加载状态 -->
<el-table
v-loading="loading"
:data="dataSource"
@selection-change="handleSelectionChange"
>
<!-- 是否显示多选 selectionShow等于 true 显示,否则不显示-->
<el-table-column
v-if="selectionShow"
type="selection"
width="50"
align="center"
/>
<!-- 遍历可见列配置 -->
<el-table-column
v-for="item in visibleColumns"
:key="item.key ? item.key : item.prop"
:label="item.label"
:align="item.align ? item.align : 'center'"
:prop="item.prop"
:width="item.width"
>
<!-- 开关操作 -->
<template v-if="item.type === 'switch'" v-slot="scope">
<el-switch
v-model="scope.row[item.prop]"
active-value="0"
inactive-value="1"
@click="() => handleStatusChange(scope.row)"
/>
</template>
<!-- 操作栏 -->
<template v-if="item.type === 'action'" v-slot="scope">
<!-- 条件渲染动作按钮组件 -->
<!--1、 :row="scope.row" 当前行数据
2、:actions="item.actions"动作按钮的配置
handleAction操作栏按钮的事件 -->
<ActionButtons
:row="scope.row"
:actions="item.actions"
:onAction="handleAction"
/>
</template>
<!-- 时间格式化类型的列 -->
<template v-if="item.type === 'time'" v-slot="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { ref, computed } from "vue"; // 导入 ref 和 computed API
import ActionButtons from "./ActionButtons.vue"; // 引入操作按钮组件
const props = defineProps({
loading: {
type: Boolean,
default: false, // 默认不显示加载状态
},
dataSource: {
type: Array,
default: () => [], // 默认数据源为空数组
},
columns: {
type: Array,
default: () => [], // 默认列配置为空数组
},
selectionShow: {
type: Boolean,
default: false, // 默认不显示选择框
},
});
// 计算可见列,只返回 visible 属性为 true 的列
const visibleColumns = computed(() =>
props.columns.filter((column) => column.visible)
);
const emit = defineEmits(); // 定义事件发射函数
// 处理选择变化事件
const handleSelectionChange = (selection) => {
emit("selectionChange", selection); // 触发 selectionChange 事件
};
// 处理状态变更事件
const handleStatusChange = (row) => {
emit("statusChange", row); // 触发 statusChange 事件
};
// 处理操作按钮事件
const handleAction = (action, row) => {
emit("action", action, row); // 触发 action 事件
};
// 时间解析函数,格式化时间为本地字符串
const parseTime = (time) => {
return new Date(time).toLocaleString(); // 将时间转换为本地格式
};
</script>
<template>
<div>
<el-button
v-for="item in actions"
:key="item.label"
type="text"
:icon="item.icon?item.icon:''"
@click="() => handleAction(item)"
>
{{ item.label }}
</el-button>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue"; // 导入 Vue 的 API
// 定义组件的 props
const props = defineProps({
row: {
type: Object,
default: () => ({}), // 默认值为空对象
},
actions: {
type: Array,
default: () => [], // 默认值为空数组
},
onAction: {
type: Function,
default: () => (action, row) => {}, // 默认事件处理函数
}, // 接收事件处理函数
});
// 定义事件发射器
const emit = defineEmits([]); // 此组件不需要发射任何事件
// 定义处理动作的函数
const handleAction = (action) => {
if (props.onAction) { // 检查是否传入了 onAction 函数
props.onAction(action, props.row); // 调用传入的事件处理函数,并传递动作和行数据
}
};
</script>
index.vue(主页面代码)
<template>
<div class="app-container">
<el-row :gutter="24">
<el-col :span="24" :xs="24">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="用户名称" prop="userName">
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable style="width: 240px"
@keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable style="width: 240px"
@keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd"
v-hasPermi="['system:user:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="selectedRowKeys.length == 0"
@click="handleDeleteBatch()" v-hasPermi="['system:user:remove']">批量删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="
handleExport(
'system/user/export',
queryParams,
`user_${new Date().getTime()}.xlsx`
)
" v-hasPermi="['system:user:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="loadData" :columns="columns"></right-toolbar>
</el-row>
<UniversalTable :loading="loading" :dataSource="dataSource" :selectionShow="true" :columns="columns"
@action="handleTableAction" @selectionChange="handleSelectionChange" />
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize" @pagination="loadData" />
</el-col>
</el-row>
<userModal ref="registerModal" @success="loadData" />
</div>
</template>
<script setup name="customerIntion">
import { listUser, delUser } from "./api/userApi";
const router = useRouter();
const { proxy } = getCurrentInstance();
import { useCommonMixin } from "@/mixins/JeecgListMixin.js"; //公共方法
import userModal from "./components/userModal.vue"; //编辑新增
import UniversalTable from "@/mixins/Table/UniversalTable.vue"; //通用表格
const registerModal = ref();
const showSearch = ref(true);
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
userName: undefined,
phonenumber: undefined,
status: undefined,
deptId: undefined,
},
});
const { queryParams } = toRefs(data);
// 根据需要传入额外参数
const additionalParams = {
list: listUser, //查询用户列表
queryParams: queryParams, //查询参数
registerModal: registerModal, //用户注册编辑对话框
deleteOne: delUser, //删除用户接口
proxy: proxy, //公共方法
id: "userId", //主键id
queryRef: "queryRef", //查询框ref
};
const {
loading, // 是否正在加载数据的状态
dataSource, // 存储从后端获取的数据源
total, // 数据的总条数,用于分页显示
loadData, // 加载数据的函数
handleAdd, // 新增操作的处理函数
handleUpdate, // 修改操作的处理函数
handleQuery, // 查询操作的处理函数
handleDeleteOne, // 单项删除操作的处理函数
handleDeleteBatch, // 批量删除操作的处理函数
handleSelectionChange, // 选中行变化时的处理函数
searchReset, // 搜索重置的处理函数
handleExport, // 数据导出的处理函数
selectionRows, // 当前选中的行数据数组
selectedRowKeys, // 当前选中行的唯一标识(如 ID)数组
} = useCommonMixin(additionalParams);
const columns = ref([
{ label: "用户编号", prop: "userId", visible: true },
{ label: "用户名称", prop: "userName", visible: true },
{ label: "用户昵称", prop: "nickName", visible: true },
{
label: "部门",
prop: "dept.deptName",
key: "deptName",
visible: true,
},
{ label: "手机号", prop: "phonenumber", visible: true, width: "150" },
{
label: "创建时间",
prop: "createTime",
type: "time",
visible: true,
},
{
label: "操作",
prop: "action",
visible: true,
type: "action",
actions: [
{ label: "修改", icon: "Edit", event: "edit" },
{ label: "删除", icon: "Delete", event: "delete" },
{ label: "详情", icon: "ZoomIn", event: "view" },
],
},
]);
/** 重置按钮操作 */
function resetQuery() {
queryParams.value.deptId = undefined;
proxy.$refs.deptTreeRef.setCurrentKey(null);
searchReset();
}
// 表格事件处理
const handleTableAction = (action, row) => {
if(action.event =='edit') return handleUpdate(row); //编辑
if(action.event =='delete') return handleDeleteOne(row.userId);//删除
if(action.event =='view') return router.push({ path: "/customerIntionView", query: { pageNum: 10 } }); //详情
};
loadData();
</script>
编辑、新增页面(userModal.vue、userForm.vue)页面代码
<!-- @userModal -->
<template>
<el-dialog v-model="visible" :title="title" :width="width">
<div v-if="visible">
<userForm ref="registerForm" @ok="submitCallback" @onLose="onLose" />
</div>
<template #footer> <!-- 按钮 -->
<div class="dialog-footer">
<el-button type="primary" @click="handleOk" :disabled="disabled">确 定</el-button>
<el-button @click="handleCancel">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup >
import { ref, nextTick } from "vue";
import userForm from "./userForm.vue";
const title = ref("");
const width = ref('800');
const visible = ref(false);
const disabled = ref(false);
const registerForm = ref();
const emit = defineEmits(["register", "success"]);
/**
* 新增
*/
function add() {
title.value = "新增";
visible.value = true;
nextTick(() => {
registerForm.value.add();
});
}
/**
* 编辑
*
*/
function edit(record) {
title.value = "编辑";
visible.value = true;
nextTick(() => {
registerForm.value.edit(record);
});
}
/**
* 确定按钮点击事件
*/
function handleOk() {
disabled.value = true;
registerForm.value.submitForm();
}
function onLose() {
disabled.value = false;
}
/**
* form保存回调事件
*/
function submitCallback() {
handleCancel();
onLose();
emit("success");
}
/**
* 取消按钮回调事件
*/
function handleCancel() {
visible.value = false;
}
defineExpose({
add,
edit,
});
</script>
<!-- @userForm.vue -->
<template>
<el-form :model="formData" :rules="rules" :disabled="disabled" ref="formRef" label-width="80px">
<el-row>
<el-col :span="12">
<el-form-item label="用户昵称" prop="nickName">
<el-input
v-model="formData.nickName"
placeholder="请输入用户昵称"
maxlength="30"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="归属部门" prop="deptId">
<el-tree-select
v-model="formData.deptId"
:data="deptOptions"
:props="{ value: 'id', label: 'label', children: 'children' }"
value-key="id"
placeholder="请选择归属部门"
check-strictly
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="手机号码" prop="phonenumber">
<el-input
v-model="formData.phonenumber"
placeholder="请输入手机号码"
maxlength="11"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input
v-model="formData.email"
placeholder="请输入邮箱"
maxlength="50"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item
v-if="formData.userId == undefined"
label="用户名称"
prop="userName"
>
<el-input
v-model="formData.userName"
placeholder="请输入用户名称"
maxlength="30"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
v-if="formData.userId == undefined"
label="用户密码"
prop="password"
>
<el-input
v-model="formData.password"
placeholder="请输入用户密码"
type="password"
maxlength="20"
show-password
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="用户性别">
<el-select v-model="formData.sex" placeholder="请选择">
<el-option
v-for="dict in sys_user_sex"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in sys_normal_disable"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio
>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="岗位">
<el-select v-model="formData.postIds" multiple placeholder="请选择">
<el-option
v-for="item in postOptions"
:key="item.postId"
:label="item.postName"
:value="item.postId"
:disabled="item.status == 1"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="角色">
<el-select v-model="formData.roleIds" multiple placeholder="请选择">
<el-option
v-for="item in roleOptions"
:key="item.roleId"
:label="item.roleName"
:value="item.roleId"
:disabled="item.status == 1"
></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="备注">
<el-input
v-model="formData.remark"
type="textarea"
placeholder="请输入内容"
></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup>
import { ref, reactive, nextTick } from "vue";
const { proxy } = getCurrentInstance();
import {
saveOrUpdate,
deptTreeSelect,
getUser
} from "../api/userApi";
const { sys_normal_disable, sys_user_sex } = proxy.useDict(
"sys_normal_disable",
"sys_user_sex"
);
const emit = defineEmits(["register", "ok"]);
const formRef = ref();
const deptOptions = ref(undefined);
const postOptions = ref([]);
const disabled = ref(false);
const roleOptions = ref([]);
const formData = reactive({
userId: undefined,
deptId: undefined,
userName: undefined,
nickName: undefined,
password: undefined,
phonenumber: undefined,
email: undefined,
sex: undefined,
status: "0",
remark: undefined,
postIds: [],
roleIds: [],
});
//表单验证
const rules = {
userName: [
{ required: true, message: "用户名称不能为空", trigger: "blur" },
{
min: 2,
max: 20,
message: "用户名称长度必须介于 2 和 20 之间",
trigger: "blur",
},
],
nickName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
password: [
{ required: true, message: "用户密码不能为空", trigger: "blur" },
{
min: 5,
max: 20,
message: "用户密码长度必须介于 5 和 20 之间",
trigger: "blur",
},
{
pattern: /^[^<>"'|\\]+$/,
message: "不能包含非法字符:< > \" ' \\\ |",
trigger: "blur",
},
],
email: [
{
type: "email",
message: "请输入正确的邮箱地址",
trigger: ["blur", "change"],
},
],
phonenumber: [
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur",
},
],
};
/**
* 新增
*/
function add() {
edit({});
}
/**
* 编辑
*/
function edit(record) {
nextTick(() => {
//赋值表单数据
Object.assign(formData, record);
});
console.log(formData);
proxy.resetForm("formRef");
}
/**
* 提交数据
*/
async function submitForm() {
disabled.value = true;
proxy.$refs["formRef"].validate((valid) => {
if (valid) {
let model = { ...formData };
const isUpdate = formData.createTime;
saveOrUpdate(model,isUpdate).then((res) => {
disabled.value = false;
if (res.code ==200) {
proxy.$modal.msgSuccess(res.msg);
emit("ok");
} else {
emit("onLose");
proxy.$modal.msgError(res.msg);
}
});
}else{
disabled.value = false;
emit("onLose");
}
});
}
/** 查询部门下拉树结构 */
function getDeptTree() {
deptTreeSelect().then((response) => { //部门下拉树
deptOptions.value = response.data;
});
getUser().then(response => { //岗位角色
postOptions.value = response.posts;
roleOptions.value = response.roles;
});
}
getDeptTree();
defineExpose({
add,
edit,
submitForm,
});
</script>
接口文件(userApi.js)代码
import request from '@/utils/request'
import { parseStrEmpty } from "@/utils/ruoyi";
// 查询用户列表
export function listUser(query) {
return request({
url: '/system/user/list',
method: 'get',
params: query
})
}
// 查询用户详细
export function getUser(userId) {
return request({
url: '/system/user/' + parseStrEmpty(userId),
method: 'get'
})
}
// 新增用户
export function addUser(data) {
return request({
url: '/system/user',
method: 'post',
data: data
})
}
// 修改用户
export function updateUser(data) {
return request({
url: '/system/user',
method: 'put',
data: data
})
}
export function saveOrUpdate(data,isUpdate) {
const method = isUpdate ? 'put' : 'post';
return request({
url: '/system/user',
method: method,
data: data
})
}
// 删除用户
export function delUser(userId) {
return request({
url: '/system/user/' + userId,
method: 'delete'
})
}
// 用户密码重置
export function resetUserPwd(userId, password) {
const data = {
userId,
password
}
return request({
url: '/system/user/resetPwd',
method: 'put',
data: data
})
}
// 用户状态修改
export function changeUserStatus(userId, status) {
const data = {
userId,
status
}
return request({
url: '/system/user/changeStatus',
method: 'put',
data: data
})
}
// 查询用户个人信息
export function getUserProfile() {
return request({
url: '/system/user/profile',
method: 'get'
})
}
// 修改用户个人信息
export function updateUserProfile(data) {
return request({
url: '/system/user/profile',
method: 'put',
data: data
})
}
// 用户密码重置
export function updateUserPwd(oldPassword, newPassword) {
const data = {
oldPassword,
newPassword
}
return request({
url: '/system/user/profile/updatePwd',
method: 'put',
params: data
})
}
// 用户头像上传
export function uploadAvatar(data) {
return request({
url: '/system/user/profile/avatar',
method: 'post',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: data
})
}
// 查询授权角色
export function getAuthRole(userId) {
return request({
url: '/system/user/authRole/' + userId,
method: 'get'
})
}
// 保存授权角色
export function updateAuthRole(data) {
return request({
url: '/system/user/authRole',
method: 'put',
params: data
})
}
// 查询部门下拉树结构
export function deptTreeSelect() {
return request({
url: '/system/user/deptTree',
method: 'get'
})
}