AI 摘要

本文基于 Solon + EasyQuery + ElementPlus 实现后台管理系统的部门、角色、用户三大模块。后端提供分页、模糊查询、关联校验、密码加密、强制下线等完整 API;前端采用 ElementPlus 构建搜索、表格、模态框,支持数据回显、表单校验、下拉选项动态加载,并处理默认数据与当前用户禁用逻辑。通过配置 EasyQuery 更新策略及替换 Snack3 序列化,解决空值更新与敏感字段泄露问题,实现安全高效的后台运营能力。

部门操作

后端 API

核心注意事项:

  • 部门管理的后端 API,提供基础增删改查方法。
  • 获取部门列表:可以根据部门名称条件进行模糊查询,返回分页结果。
  • 删除部门:部门下有关联的用户,禁止删除。

编写部门控制器(cn.duozai.sadmin.controller.DeptController),提供基础方法:

/**
 * 部门控制器
 * @visduo
 */
@Valid
@Mapping("/dept")
@Controller
public class DeptController {

    @Db
    EasyEntityQuery easyEntityQuery;

    /**
     * 获取部门列表
     * @visduo
     *
     * @param name 查询条件-部门名称
     * @param pageIndex 查询条件-页码
     * @param pageSize 查询条件-页大小
     * @return 部门列表分页结果
     */
    @Get
    @Mapping("/list")
    public ResponseResult list(@Param(required = false) String name,
                               @Param(required = false, defaultValue = "1") Integer pageIndex,
                               @Param(required = false, defaultValue = "10") Integer pageSize) {
        EasyPageResult<DeptEntity> deptPageResult = easyEntityQuery.queryable(DeptEntity.class)
                .where(d -> {
                    // 部门名称不为空时对部门名称进行模糊查询
                    d.name().like(StrUtil.isNotBlank(name), name);
                })
                // 查询分页数据
                .toPageResult(pageIndex, pageSize);

        return ResponseResult.success("查询成功", deptPageResult);
    }

    /**
     * 添加部门
     * @visduo
     *
     * @param name 部门名称
     * @return 添加结果
     */
    @Post
    @Mapping("/add")
    public ResponseResult add(@NotBlank(message = "部门名称不能为空") String name) {
        DeptEntity deptEntity = new DeptEntity();
        deptEntity.setName(name);

        // 插入部门数据
        easyEntityQuery.insertable(deptEntity).executeRows();

        return ResponseResult.success("添加成功", null);
    }

    /**
     * 修改部门
     * @visduo
     *
     * @param id 部门id
     * @param name 部门名称
     * @return 修改结果
     */
    @Put
    @Mapping("/update/{id}")
    public ResponseResult update(@Path int id,
                                 @NotBlank(message = "部门名称不能为空") String name) {
        DeptEntity deptEntity = new DeptEntity();
        deptEntity.setId(id);
        deptEntity.setName(name);

        // 修改部门数据
        easyEntityQuery.updatable(deptEntity).executeRows();

        return ResponseResult.success("修改成功", null);
    }

    /**
     * 删除部门
     * @visduo
     *
     * @param id 部门id
     * @return 删除结果
     */
    @Delete
    @Mapping("/delete/{id}")
    public ResponseResult delete(@Path int id) {
        // 该部门下有关联用户,禁止删除
        long count = easyEntityQuery.queryable(UsersEntity.class)
                .where(u -> {
                    u.deptId().eq(id);
                }).count();

        if (count > 0) {
            return ResponseResult.failure("该部门下有关联用户,禁止删除", null);
        }

        // 删除部门数据
        easyEntityQuery.deletable(DeptEntity.class)
                .where(d -> {
                    d.id().eq(id);
                }).executeRows();

        return ResponseResult.success("删除成功", null);
    }

    /**
     * 根据部门ID获取部门信息
     * @visduo
     *
     * @param id 部门ID
     * @return 部门信息
     */
    @Get
    @Mapping("/get/{id}")
    public ResponseResult get(@Path int id) {
        DeptEntity deptEntity = easyEntityQuery.queryable(DeptEntity.class)
                .where(d -> {
                    d.id().eq(id);
                }).firstOrNull();

        return ResponseResult.success("查询成功", deptEntity);
    }

}

前端组件

核心注意事项:

  • 部门管理的前端组件,提供基础增删改查表单。
  • 增改表单,使用 ElementPlus 模态框进行展示。
  • 修改部门表单需要实现表单数据回显,修改模态框展示时,需要根据修改的部门 ID 发送请求到后端获取最新的部门数据进行回显。
  • 删除部门时,提供确认框,确认后发送请求进行删除。

AI 生成组件提示词:

基于ElementPlus,生成部门列表页面,要求如下:
1、提供搜索表单,搜索条件为部门名称name。
2、请求后端接口/dept/list获取部门分页列表数据并展示。
3、部门数据包含部门id、部门名称name,表格需要展示出来。
4、后端返回分页结果data、数据总数total,你需要自行计算其他分页所需参数,并显示分页组件。
5、点击添加按钮,弹出添加部门的模态框,并提供添加部门表单,表单项包括部门名称name。
6、新增部门表单提交前,需要进行表单数据校验。表单数据校验通过后,发送post请求给后端接口/dept/add。
7、点击修改按钮,弹出修改部门的模态框,并提供修改部门表单,表单要做数据回显,可修改项包括部门名称name。
8、修改部门的模态框出现后,需要发送get请求给后端接口/dept/get/xxx,获取到部门信息后进行回显。
9、修改部门表单提交前,需要进行表单数据校验。表单数据校验通过后,发送put请求给后端接口/dept/update/xxx。
10、新增部门和修改部门的模态框消失后,需要清除表单数据。
11、点击删除按钮,弹出删除确认提示,确定删除,发送delete请求给后端接口/dept/delete/xxx。

编写部门组件(views/Dept.vue),提供部门管理视图:

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import qs from "qs";

// 分页数据
const tableData = ref([])
const total = ref(0)

// 搜索表单数据
const searchForm = reactive({
    name: ''
})

// 分页参数
const pagination = reactive({
    currentPage: 1,
    pageSize: 10
})

// 添加部门对话框相关
const addDialogVisible = ref(false)
const addFormRef = ref()
const addFormData = reactive({
    name: ''
})
const addFormRules = {
    name: [
        { required: true, message: '请输入部门名称', trigger: 'blur' },
        { min: 1, max: 50, message: '长度应在1到50个字符之间', trigger: 'blur' }
    ]
}

// 编辑部门对话框相关
const editDialogVisible = ref(false)
const editFormRef = ref()
const editFormData = reactive({
    id: '',
    name: ''
})
const editFormRules = {
    name: [
        { required: true, message: '请输入部门名称', trigger: 'blur' },
        { min: 1, max: 50, message: '长度应在1到50个字符之间', trigger: 'blur' }
    ]
}

// 获取部门列表
const fetchDeptList = () => {
    const params = {
        page: pagination.currentPage,
        size: pagination.pageSize,
        name: searchForm.name || undefined
    }

    axios.get('/dept/list', { params }).then((res) => {
        tableData.value = res.data.data
        total.value = res.data.total
    })
}

// 搜索
const submitSearchForm = () => {
    pagination.currentPage = 1
    fetchDeptList()
}

// 重置搜索
const resetSearchForm = () => {
    searchForm.name = ''
    pagination.currentPage = 1
    fetchDeptList()
}

// 分页变化
const handleSizeChange = (val) => {
    pagination.pageSize = val
    pagination.currentPage = 1
    fetchDeptList()
}

const handleCurrentChange = (val) => {
    pagination.currentPage = val
    fetchDeptList()
}

// 显示添加部门对话框
const showAddDialog = () => {
    addDialogVisible.value = true
}

// 提交添加表单
const submitAddForm = () => {
    addFormRef.value.validate((valid) => {
        if (valid) {
            axios.post('/dept/add', qs.stringify(addFormData)).then((res) => {
                ElMessage.success('添加成功')
                addDialogVisible.value = false
                resetAddForm()
                fetchDeptList()
            })
        } else {
            ElMessage.error('请正确填写表单信息')
            return false
        }
    })
}

// 重置添加表单
const resetAddForm = () => {
    addFormRef.value.resetFields()
}

// 显示编辑部门对话框
const showEditDialog = (row) => {
    // 先设置ID,用于请求详细信息
    editFormData.id = row.id
    editDialogVisible.value = true

    // 获取部门详细信息
    axios.get(`/dept/get/${row.id}`).then((res) => {
        editFormData.name = res.data.name
    })
}

// 提交编辑表单
const submitEditForm = () => {
    editFormRef.value.validate((valid) => {
        if (valid) {
            axios.put(`/dept/update/${editFormData.id}`, qs.stringify(editFormData)).then((res) => {
                ElMessage.success('修改成功')
                editDialogVisible.value = false
                resetEditForm()
                fetchDeptList()
            })
        } else {
            ElMessage.error('请正确填写表单信息')
            return false
        }
    })
}

// 重置编辑表单
const resetEditForm = () => {
    editFormRef.value.resetFields()
}

// 删除部门
const handleDelete = (id) => {
    ElMessageBox.confirm('确定要删除该部门吗?', '删除确认', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
    }).then(() => {
        axios.delete(`/dept/delete/${id}`)
            .then((res) => {
                ElMessage.success('删除成功')
                fetchDeptList()
            })
    }).catch(() => {
        // 用户取消删除
    })
}

// 初始化加载数据
onMounted(() => {
    fetchDeptList()
})
</script>

<template>
    <el-main class="layout-main">
        <!-- 搜索表单 -->
        <el-card shadow="never" style="margin-bottom: 1rem;" :body-style="{paddingBottom: '2px'}">
            <el-form :model="searchForm" inline>
                <el-form-item label="部门名称">
                    <el-input v-model="searchForm.name" />
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" @click="submitSearchForm">搜索</el-button>
                    <el-button @click="resetSearchForm">重置</el-button>
                    <el-button type="warning" @click="showAddDialog">添加部门</el-button>
                </el-form-item>
            </el-form>
        </el-card>

        <!-- 数据表格 -->
        <el-card shadow="never">
            <template #header>
                <div class="card-header">
                    <span>部门列表</span>
                </div>
            </template>
            <el-table :data="tableData" style="width: 100%" stripe>
                <el-table-column prop="id" label="部门ID" width="80" />
                <el-table-column prop="name" label="部门名称" />
                <el-table-column label="操作">
                    <template #default="scope">
                        <el-button type="primary" size="small" @click="showEditDialog(scope.row)">编辑</el-button>
                        <el-button type="danger" size="small" @click="handleDelete(scope.row.id)">删除</el-button>
                    </template>
                </el-table-column>
            </el-table>

            <!-- 分页组件 -->
            <el-pagination v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize"
                :page-sizes="[10, 20, 50, 100]" :small="false" :disabled="false" :background="true"
                layout="total, sizes, prev, pager, next, jumper"
                :total="total" @size-change="handleSizeChange" @current-change="handleCurrentChange"
                style="margin-top: 20px; justify-content: flex-end; display: flex;"
            />
        </el-card>

        <!-- 添加部门对话框 -->
        <el-dialog v-model="addDialogVisible" title="添加部门" width="500px" @close="resetAddForm">
            <el-form ref="addFormRef" :model="addFormData" :rules="addFormRules" label-width="80px" label-position="top">
                <el-form-item label="部门名称" prop="name">
                    <el-input v-model="addFormData.name" placeholder="请输入部门名称"/>
                </el-form-item>
            </el-form>

            <template #footer>
                <div style="width: 100%; text-align: center">
                    <el-button @click="addDialogVisible = false">取消</el-button>
                    <el-button type="primary" @click="submitAddForm">确定</el-button>
                </div>
            </template>
        </el-dialog>

        <!-- 编辑部门对话框 -->
        <el-dialog v-model="editDialogVisible" title="编辑部门" width="500px" @close="resetEditForm">
            <el-form ref="editFormRef" :model="editFormData" :rules="editFormRules" label-width="80px" label-position="top">
                <el-form-item label="部门名称" prop="name">
                    <el-input v-model="editFormData.name" placeholder="请输入部门名称"/>
                </el-form-item>
            </el-form>

            <template #footer>
                <div style="width: 100%; text-align: center">
                    <el-button @click="editDialogVisible = false">取消</el-button>
                    <el-button type="primary" @click="submitEditForm">确定</el-button>
                </div>
            </template>
        </el-dialog>
    </el-main>
</template>

编写路由配置文件(router/index.js),提供部门管理路由信息:

{
    // 部门管理
    path: 'dept/list',
    component: () => import('@/views/Dept.vue'),
},

编写布局组件(views/Layouts.vue),提供部门管理菜单:

{
    id: 13,
    title: '部门管理',
    path: '/dept/list'
},

在浏览器中测试:

角色操作

后端 API

核心注意事项:

  • 角色管理的后端 API,提供基础增删改查方法。
  • 获取角色列表:可以根据角色名称条件、角色备注条件进行模糊查询,返回分页结果。
  • 删除部门:角色下有关联的用户,禁止删除,角色 ID 为 1 即最高角色,禁止删除。

AI 生成控制器提示词:

基于Solon+EasyQuery,生成角色控制器,要求如下:
1、获取角色列表:可以根据角色名称name、角色备注remarks模糊查询,返回分页结果。
2、添加角色:前端提交角色名称name、角色备注remarks,使用注解进行数据校验,校验后插入数据到数据库。
3、修改角色:流程同添加角色。
4、删除角色:删除时校验角色下是否有关联用户,如有禁止删除,校验角色ID是否为1,如是禁止删除。
5、根据角色ID获取角色信息:根据角色ID查询角色信息后返回结果。
6、编码风格,严格参考部门控制器DeptController。

编写角色控制器(cn.duozai.sadmin.controller.RoleController),提供基础方法:

/**
 * 角色控制器
 * @visduo
 */
@Valid
@Mapping("/role")
@Controller
public class RoleController {

    @Db
    EasyEntityQuery easyEntityQuery;

    /**
     * 获取角色列表
     * @visduo
     *
     * @param name 查询条件-角色名称
     * @param remarks 查询条件-角色备注
     * @param pageIndex 查询条件-页码
     * @param pageSize 查询条件-页大小
     * @return 角色列表分页结果
     */
    @Get
    @Mapping("/list")
    public ResponseResult list(@Param(required = false) String name,
                               @Param(required = false) String remarks,
                               @Param(required = false, defaultValue = "1") Integer pageIndex,
                               @Param(required = false, defaultValue = "10") Integer pageSize) {
        EasyPageResult<RoleEntity> rolePageResult = easyEntityQuery.queryable(RoleEntity.class)
                .where(r -> {
                    // 角色名称不为空时对角色名称进行模糊查询
                    r.name().like(StrUtil.isNotBlank(name), name);
                    // 角色备注不为空时对角色备注进行模糊查询
                    r.remarks().like(StrUtil.isNotBlank(remarks), remarks);
                })
                // 查询分页数据
                .toPageResult(pageIndex, pageSize);

        return ResponseResult.success("查询成功", rolePageResult);
    }

    /**
     * 添加角色
     * @visduo
     *
     * @param name 角色名称
     * @param remarks 角色备注
     * @return 添加结果
     */
    @Post
    @Mapping("/add")
    public ResponseResult add(@NotBlank(message = "角色名称不能为空") String name,
                              @NotBlank(message = "角色备注不能为空") String remarks) {
        RoleEntity roleEntity = new RoleEntity();
        roleEntity.setName(name);
        roleEntity.setRemarks(remarks);

        // 插入角色数据
        easyEntityQuery.insertable(roleEntity).executeRows();

        return ResponseResult.success("添加成功", null);
    }

    /**
     * 修改角色
     * @visduo
     *
     * @param id 角色id
     * @param name 角色名称
     * @param remarks 角色备注
     * @return 修改结果
     */
    @Put
    @Mapping("/update/{id}")
    public ResponseResult update(@Path int id,
                                 @NotBlank(message = "角色名称不能为空") String name,
                                 @NotBlank(message = "角色备注不能为空") String remarks) {
        RoleEntity roleEntity = new RoleEntity();
        roleEntity.setId(id);
        roleEntity.setName(name);
        roleEntity.setRemarks(remarks);

        // 修改角色数据
        easyEntityQuery.updatable(roleEntity).executeRows();

        return ResponseResult.success("修改成功", null);
    }

    /**
     * 删除角色
     * @visduo
     *
     * @param id 角色id
     * @return 删除结果
     */
    @Delete
    @Mapping("/delete/{id}")
    public ResponseResult delete(@Path int id) {
        // 禁止删除ID为1的角色
        if (id == 1) {
            return ResponseResult.failure("默认角色禁止删除", null);
        }

        // 该角色下有关联用户,禁止删除
        long count = easyEntityQuery.queryable(UsersEntity.class)
                .where(u -> {
                    u.roleId().eq(id);
                }).count();

        if (count > 0) {
            return ResponseResult.failure("该角色下有关联用户,禁止删除", null);
        }

        // 删除角色数据
        easyEntityQuery.deletable(RoleEntity.class)
                .where(r -> {
                    r.id().eq(id);
                }).executeRows();

        return ResponseResult.success("删除成功", null);
    }

    /**
     * 根据角色ID获取角色信息
     * @visduo
     *
     * @param id 角色ID
     * @return 角色信息
     */
    @Get
    @Mapping("/get/{id}")
    public ResponseResult get(@Path int id) {
        RoleEntity roleEntity = easyEntityQuery.queryable(RoleEntity.class)
                .where(r -> {
                    r.id().eq(id);
                }).firstOrNull();

        return ResponseResult.success("查询成功", roleEntity);
    }

}

前端组件

核心注意事项:

  • 角色管理的前端组件,提供基础增删改查表单,表单相关注意事项和要求同前。

AI 生成组件提示词:

基于ElementPlus,生成角色列表页面,要求如下:
1、提供搜索表单,搜索条件为角色名称name、角色备注remarks。
2、请求后端接口/role/list获取角色分页列表数据并展示。
3、角色数据包含角色id、角色名称name、角色备注remarks,表格需要展示出来。
4、后端返回分页结果data、数据总数total,你需要自行计算其他分页所需参数,并显示分页组件。
5、点击添加按钮,弹出添加角色的模态框,并提供添加角色表单,表单项包括角色名称name、角色备注remarks。
6、新增角色表单提交前,需要进行表单数据校验。表单数据校验通过后,发送post请求给后端接口/role/add。
7、点击修改按钮,弹出修改角色的模态框,并提供修改角色表单,表单要做数据回显,可修改项包括角色名称name、角色备注remarks。
8、修改角色的模态框出现后,需要发送get请求给后端接口/role/get/xxx,获取到角色信息后进行回显。
9、修改角色表单提交前,需要进行表单数据校验。表单数据校验通过后,发送put请求给后端接口/role/update/xxx。
10、新增角色和修改角色的模态框消失后,需要清除表单数据。
11、点击删除按钮,弹出删除确认提示,确定删除,发送delete请求给后端接口/role/delete/xxx。
12、编码风格,严格参考部门组件Dept.vue。
13、注意:ID为1的角色是默认角色,需要禁用删除按钮。

编写角色组件(views/Role.vue),提供角色管理视图:

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import qs from "qs";

// 分页数据
const tableData = ref([])
const total = ref(0)

// 搜索表单数据
const searchForm = reactive({
    name: '',
    remarks: ''
})

// 分页参数
const pagination = reactive({
    currentPage: 1,
    pageSize: 10
})

// 添加角色对话框相关
const addDialogVisible = ref(false)
const addFormRef = ref()
const addFormData = reactive({
    name: '',
    remarks: ''
})
const addFormRules = {
    name: [
        { required: true, message: '请输入角色名称', trigger: 'blur' },
        { min: 1, max: 50, message: '长度应在1到50个字符之间', trigger: 'blur' }
    ],
    remarks: [
        { required: true, message: '请输入角色备注', trigger: 'blur' },
        { min: 1, max: 200, message: '长度应在1到200个字符之间', trigger: 'blur' }
    ]
}

// 编辑角色对话框相关
const editDialogVisible = ref(false)
const editFormRef = ref()
const editFormData = reactive({
    id: '',
    name: '',
    remarks: ''
})
const editFormRules = {
    name: [
        { required: true, message: '请输入角色名称', trigger: 'blur' },
        { min: 1, max: 50, message: '长度应在1到50个字符之间', trigger: 'blur' }
    ],
    remarks: [
        { required: true, message: '请输入角色备注', trigger: 'blur' },
        { min: 1, max: 200, message: '长度应在1到200个字符之间', trigger: 'blur' }
    ]
}

// 获取角色列表
const fetchRoleList = () => {
    const params = {
        page: pagination.currentPage,
        size: pagination.pageSize,
        name: searchForm.name || undefined,
        remarks: searchForm.remarks || undefined
    }

    axios.get('/role/list', { params }).then((res) => {
        tableData.value = res.data.data
        total.value = res.data.total
    })
}

// 搜索
const submitSearchForm = () => {
    pagination.currentPage = 1
    fetchRoleList()
}

// 重置搜索
const resetSearchForm = () => {
    searchForm.name = ''
    searchForm.remarks = ''
    pagination.currentPage = 1
    fetchRoleList()
}

// 分页变化
const handleSizeChange = (val) => {
    pagination.pageSize = val
    pagination.currentPage = 1
    fetchRoleList()
}

const handleCurrentChange = (val) => {
    pagination.currentPage = val
    fetchRoleList()
}

// 显示添加角色对话框
const showAddDialog = () => {
    addDialogVisible.value = true
}

// 提交添加表单
const submitAddForm = () => {
    addFormRef.value.validate((valid) => {
        if (valid) {
            axios.post('/role/add', qs.stringify(addFormData)).then((res) => {
                ElMessage.success('添加成功')
                addDialogVisible.value = false
                resetAddForm()
                fetchRoleList()
            })
        } else {
            ElMessage.error('请正确填写表单信息')
            return false
        }
    })
}

// 重置添加表单
const resetAddForm = () => {
    addFormRef.value.resetFields()
}

// 显示编辑角色对话框
const showEditDialog = (row) => {
    // 先设置ID,用于请求详细信息
    editFormData.id = row.id
    editDialogVisible.value = true

    // 获取角色详细信息
    axios.get(`/role/get/${row.id}`).then((res) => {
        editFormData.name = res.data.name
        editFormData.remarks = res.data.remarks
    })
}

// 提交编辑表单
const submitEditForm = () => {
    editFormRef.value.validate((valid) => {
        if (valid) {
            axios.put(`/role/update/${editFormData.id}`, qs.stringify(editFormData)).then((res) => {
                ElMessage.success('修改成功')
                editDialogVisible.value = false
                resetEditForm()
                fetchRoleList()
            })
        } else {
            ElMessage.error('请正确填写表单信息')
            return false
        }
    })
}

// 重置编辑表单
const resetEditForm = () => {
    editFormRef.value.resetFields()
}

// 删除角色
const handleDelete = (id) => {
    ElMessageBox.confirm('确定要删除该角色吗?', '删除确认', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
    }).then(() => {
        axios.delete(`/role/delete/${id}`)
            .then((res) => {
                ElMessage.success('删除成功')
                fetchRoleList()
            })
    }).catch(() => {
        // 用户取消删除
    })
}

// 初始化加载数据
onMounted(() => {
    fetchRoleList()
})
</script>

<template>
    <el-main class="layout-main">
        <!-- 搜索表单 -->
        <el-card shadow="never" style="margin-bottom: 1rem;" :body-style="{paddingBottom: '2px'}">
            <el-form :model="searchForm" inline>
                <el-form-item label="角色名称">
                    <el-input v-model="searchForm.name" />
                </el-form-item>
                <el-form-item label="角色备注">
                    <el-input v-model="searchForm.remarks" />
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" @click="submitSearchForm">搜索</el-button>
                    <el-button @click="resetSearchForm">重置</el-button>
                    <el-button type="warning" @click="showAddDialog">添加角色</el-button>
                </el-form-item>
            </el-form>
        </el-card>

        <!-- 数据表格 -->
        <el-card shadow="never">
            <template #header>
                <div class="card-header">
                    <span>角色列表</span>
                </div>
            </template>
            <el-table :data="tableData" style="width: 100%" stripe>
                <el-table-column prop="id" label="角色ID" width="80" />
                <el-table-column prop="name" label="角色名称" />
                <el-table-column prop="remarks" label="角色备注" />
                <el-table-column label="操作">
                    <template #default="scope">
                        <el-button type="primary" size="small" @click="showEditDialog(scope.row)">编辑</el-button>
                        <el-button
                            type="danger"
                            size="small"
                            @click="handleDelete(scope.row.id)"
                            :disabled="scope.row.id === 1"
                        >
                            删除
                        </el-button>
                    </template>
                </el-table-column>
            </el-table>

            <!-- 分页组件 -->
            <el-pagination v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize"
                :page-sizes="[10, 20, 50, 100]" :small="false" :disabled="false" :background="true"
                layout="total, sizes, prev, pager, next, jumper"
                :total="total" @size-change="handleSizeChange" @current-change="handleCurrentChange"
                style="margin-top: 20px; justify-content: flex-end; display: flex;"
            />
        </el-card>

        <!-- 添加角色对话框 -->
        <el-dialog v-model="addDialogVisible" title="添加角色" width="500px" @close="resetAddForm">
            <el-form ref="addFormRef" :model="addFormData" :rules="addFormRules" label-width="80px" label-position="top">
                <el-form-item label="角色名称" prop="name">
                    <el-input v-model="addFormData.name" placeholder="请输入角色名称"/>
                </el-form-item>
                <el-form-item label="角色备注" prop="remarks">
                    <el-input v-model="addFormData.remarks" placeholder="请输入角色备注" type="textarea"/>
                </el-form-item>
            </el-form>

            <template #footer>
                <div style="width: 100%; text-align: center">
                    <el-button @click="addDialogVisible = false">取消</el-button>
                    <el-button type="primary" @click="submitAddForm">确定</el-button>
                </div>
            </template>
        </el-dialog>

        <!-- 编辑角色对话框 -->
        <el-dialog v-model="editDialogVisible" title="编辑角色" width="500px" @close="resetEditForm">
            <el-form ref="editFormRef" :model="editFormData" :rules="editFormRules" label-width="80px" label-position="top">
                <el-form-item label="角色名称" prop="name">
                    <el-input v-model="editFormData.name" placeholder="请输入角色名称"/>
                </el-form-item>
                <el-form-item label="角色备注" prop="remarks">
                    <el-input v-model="editFormData.remarks" placeholder="请输入角色备注" type="textarea"/>
                </el-form-item>
            </el-form>

            <template #footer>
                <div style="width: 100%; text-align: center">
                    <el-button @click="editDialogVisible = false">取消</el-button>
                    <el-button type="primary" @click="submitEditForm">确定</el-button>
                </div>
            </template>
        </el-dialog>
    </el-main>
</template>

编写路由配置文件(router/index.js),提供角色管理路由信息:

{
    // 角色管理
    path: 'role/list',
    component: () => import('@/views/Role.vue'),
},

编写布局组件(views/Layouts.vue),提供角色管理菜单:

{
    id: 12,
    title: '角色管理',
    path: '/role/list'
},

在浏览器中测试:

用户操作

部门与角色后端 API 变动

在用户的基础增改查功能中,会涉及到:

  • 根据关联部门和关联角色查询用户列表。
  • 增改用户时选择用户关联的部门和关联的角色。

此时关联部门和关联角色以下拉框的形式存在,下拉框中的选项,应该从数据库中取出,在前端表单中渲染,由于部门、角色下拉框需要加载全量数据(无分页、无筛选)以支撑下拉选项的完整展示,所以需要新增两个查询接口。

编写部门控制器(cn.duozai.sadmin.controller.DeptController),提供获取部门列表(全量)方法:

/**
 * 部门控制器
 * @visduo
 */
@Valid
@Mapping("/dept")
@Controller
public class DeptController {

    // ...

    /**
     * 获取部门列表
     * @visduo
     *
     * @return 部门列表
     */
    @Get
    @Mapping("/optionList")
    public ResponseResult optionList() {
        List<DeptEntity> deptList = easyEntityQuery.queryable(DeptEntity.class)
                .toList();

        return ResponseResult.success("查询成功", deptList);
    }

}

编写角色控制器(cn.duozai.sadmin.controller.RoleController),提供获取角色列表(全量)方法:

/**
 * 角色控制器
 * @visduo
 */
@Valid
@Mapping("/role")
@Controller
public class RoleController {

    // ...

    /**
     * 获取角色列表
     * @visduo
     *
     * @return 角色列表
     */
    @Get
    @Mapping("/optionList")
    public ResponseResult optionList() {
        List<RoleEntity> roleList = easyEntityQuery.queryable(RoleEntity.class)
                .toList();
        
        return ResponseResult.success("查询成功", roleList);
    }

}

后端 API

核心注意事项:

  • 用户管理的后端 API,提供基础增删改查方法。
  • 获取用户列表:可以根据关联部门、关联角色、账户状态进行精确查询,根据账户账号、真实姓名、用户备注进行查询,返回分页结果。
  • 修改用户:账户密码非必填项,如果修改了登录密码,需要重新生成盐值并加密得到密文密码,且让对应用户掉线,重新登录。禁止修改当前登录用户。禁止修改账户账号。
  • 删除用户:用户 ID 为 1 即最高角色,禁止删除,删除前让对应用户掉线。

AI 生成控制器提示词:

基于Solon+EasyQuery,生成用户控制器,要求如下:
1、获取用户列表:可以根据关联部门ID-deptId、关联角色ID-roleId、账户状态status精确查询、根据账户账号username、真实姓名realname、用户备注remarks模糊查询,返回分页结果。
2、添加用户:前端提交关联部门ID-deptId、关联角色ID-roleId、账户账号username、账户密码password、真实姓名realname、用户备注remarks、账户状态status,使用注解进行数据校验,校验后插入数据到数据库。 
3、添加用户时,账户账号username不能重复,前端传递的账户密码password是明文密码,你需要调用MD5盐值加密工具类生成盐值salts并加密得到密文密码。
4、修改用户:流程同添加用户,但是修改用户时不能修改用户账号username。
5、修改用户时,如果前端传递了密码,你需要重新生成盐值并进行加密得到新的密文密码,密码修改后,使用SaToken工具类将该用户踢下线。
6、删除角色:删除时校验用户ID是否为当前登录的用户ID,如是禁止删除,校验用户ID是否为1,如是禁止删除。
7、根据用户ID获取用户信息:根据用户ID查询用户信息后返回结果。
8、编码风格,严格参考部门控制器DeptController。

编写用户控制器(cn.duozai.sadmin.controller.UserController),提供基础方法:

/**
 * 用户控制器
 * @visduo
 */
@Valid
@Mapping("/users")
@Controller
public class UsersController {

    @Db
    EasyEntityQuery easyEntityQuery;

    /**
     * 获取用户列表
     * @visduo
     *
     * @param deptId 查询条件-关联部门ID
     * @param roleId 查询条件-关联角色ID
     * @param status 查询条件-账户状态
     * @param username 查询条件-账户账号
     * @param realname 查询条件-真实姓名
     * @param remarks 查询条件-用户备注
     * @param pageIndex 查询条件-页码
     * @param pageSize 查询条件-页大小
     * @return 用户列表分页结果
     */
    @Get
    @Mapping("/list")
    public ResponseResult list(@Param(required = false) Integer deptId,
                               @Param(required = false) Integer roleId,
                               @Param(required = false) Integer status,
                               @Param(required = false) String username,
                               @Param(required = false) String realname,
                               @Param(required = false) String remarks,
                               @Param(required = false, defaultValue = "1") Integer pageIndex,
                               @Param(required = false, defaultValue = "10") Integer pageSize) {
        EasyPageResult<UsersEntity> userPageResult = easyEntityQuery.queryable(UsersEntity.class)
                .where(u -> {
                    // 关联部门ID不为空时进行精确查询
                    u.deptId().eq(deptId != null, deptId);
                    // 关联角色ID不为空时进行精确查询
                    u.roleId().eq(roleId != null, roleId);
                    // 账户状态不为空时进行精确查询
                    u.status().eq(status != null, status);
                    // 账户账号不为空时进行模糊查询
                    u.username().like(StrUtil.isNotBlank(username), username);
                    // 真实姓名不为空时进行模糊查询
                    u.realname().like(StrUtil.isNotBlank(realname), realname);
                    // 用户备注不为空时进行模糊查询
                    u.remarks().like(StrUtil.isNotBlank(remarks), remarks);
                })
                // 关联查询部门、角色实体
                .include(UsersEntityProxy::dept)
                .include(UsersEntityProxy::role)
                // 查询分页数据
                .toPageResult(pageIndex, pageSize);

        return ResponseResult.success("查询成功", userPageResult);
    }

    /**
     * 添加用户
     * @visduo
     *
     * @param deptId 关联部门ID
     * @param roleId 关联角色ID
     * @param username 账户账号
     * @param password 账户密码
     * @param realname 真实姓名
     * @param remarks 用户备注
     * @param status 账户状态
     * @return 添加结果
     */
    @Post
    @Mapping("/add")
    public ResponseResult add(@NotNull(message = "关联部门ID不能为空") Integer deptId,
                              @NotNull(message = "关联角色ID不能为空") Integer roleId,
                              @NotBlank(message = "账户账号不能为空") String username,
                              @NotBlank(message = "账户密码不能为空") String password,
                              @NotBlank(message = "真实姓名不能为空") String realname,
                              @NotBlank(message = "用户备注不能为空") String remarks,
                              @NotNull(message = "账户状态不能为空") Integer status) {
        // 检查账户账号是否已存在
        long count = easyEntityQuery.queryable(UsersEntity.class)
                .where(u -> {
                    u.username().eq(username);
                }).count();

        if (count > 0) {
            return ResponseResult.failure("账户账号已存在", null);
        }

        UsersEntity usersEntity = new UsersEntity();
        usersEntity.setDeptId(deptId);
        usersEntity.setRoleId(roleId);
        usersEntity.setUsername(username);
        usersEntity.setRealname(realname);
        usersEntity.setRemarks(remarks);
        usersEntity.setStatus(status);

        // 生成盐值并加密密码
        String salts = MD5SaltsUtil.salts();
        String md5Password = MD5SaltsUtil.md5(password, salts);
        usersEntity.setPassword(md5Password);
        usersEntity.setSalts(salts);

        // 插入用户数据
        easyEntityQuery.insertable(usersEntity).executeRows();

        return ResponseResult.success("添加成功", null);
    }

    /**
     * 修改用户
     * @visduo
     *
     * @param id 用户id
     * @param deptId 关联部门ID
     * @param roleId 关联角色ID
     * @param password 账户密码
     * @param realname 真实姓名
     * @param remarks 用户备注
     * @param status 账户状态
     * @return 修改结果
     */
    @Put
    @Mapping("/update/{id}")
    public ResponseResult update(@Path int id,
                                 @NotNull(message = "关联部门ID不能为空") Integer deptId,
                                 @NotNull(message = "关联角色ID不能为空") Integer roleId,
                                 @Param(required = false) String password,
                                 @NotBlank(message = "真实姓名不能为空") String realname,
                                 @NotBlank(message = "用户备注不能为空") String remarks,
                                 @NotNull(message = "账户状态不能为空") Integer status) {
        // 禁止修改当前登录的用户
        if (StpUtil.getLoginIdAsInt() == id) {
            return ResponseResult.failure("不能修改当前登录用户", null);
        }

        UsersEntity usersEntity = new UsersEntity();
        usersEntity.setId(id);
        usersEntity.setDeptId(deptId);
        usersEntity.setRoleId(roleId);
        usersEntity.setRealname(realname);
        usersEntity.setRemarks(remarks);
        usersEntity.setStatus(status);

        // 如果传递了密码,则重新生成盐值并加密
        if (StrUtil.isNotBlank(password)) {
            String salts = MD5SaltsUtil.salts();
            String md5Password = MD5SaltsUtil.md5(password, salts);
            usersEntity.setPassword(md5Password);
            usersEntity.setSalts(salts);

            // 密码修改后,将该用户踢下线
            StpUtil.kickout(id);
        }

        // 修改用户数据
        easyEntityQuery.updatable(usersEntity).executeRows();

        return ResponseResult.success("修改成功", null);
    }

    /**
     * 删除用户
     * @visduo
     *
     * @param id 用户id
     * @return 删除结果
     */
    @Delete
    @Mapping("/delete/{id}")
    public ResponseResult delete(@Path int id) {
        // 禁止删除ID为1的用户
        if (id == 1) {
            return ResponseResult.failure("默认用户禁止删除", null);
        }

        // 禁止删除当前登录的用户
        if (StpUtil.getLoginIdAsInt() == id) {
            return ResponseResult.failure("不能删除当前登录用户", null);
        }

        // 删除前,将该用户踢下线
        StpUtil.kickout(id);

        // 删除用户数据
        easyEntityQuery.deletable(UsersEntity.class)
                .where(u -> {
                    u.id().eq(id);
                }).executeRows();

        return ResponseResult.success("删除成功", null);
    }

    /**
     * 根据用户ID获取用户信息
     * @visduo
     *
     * @param id 用户ID
     * @return 用户信息
     */
    @Get
    @Mapping("/get/{id}")
    public ResponseResult get(@Path int id) {
        UsersEntity usersEntity = easyEntityQuery.queryable(UsersEntity.class)
                .where(u -> {
                    u.id().eq(id);
                })
                .include(UsersEntityProxy::dept)
                .include(UsersEntityProxy::role)
                .firstOrNull();

        return ResponseResult.success("查询成功", usersEntity);
    }

}

前端组件

核心注意事项:

  • 用户管理的前端组件,提供基础增删改查表单,表单相关注意事项和要求同前。
  • 增改查表单中涉及关联部门、关联角色,需要从后端 API 中获取对应数据并展示为下拉框。

AI 生成组件提示词:

基于ElementPlus,生成用户列表页面,要求如下:
1、编码风格,严格参考部门组件Dept.vue。
2、提供搜索表单,搜索条件为关联部门ID-deptId、关联角色ID-roleId、账户状态status、根据账户账号username、真实姓名realname、用户备注remarks。
3、搜索/新增/修改表单中,关联部门ID、关联角色ID显示下拉框,数据需要发送请求到后端接口/dept/optionList和/role/optionList获取。
4、获取用户分页列表:响应用户数据包含用户id、关联部门名称dept.name、关联角色role.name、账户账号username、真实姓名realname、用户备注remarks、账户状态status。
5、新增用户:表单项包括关联部门ID-deptId、关联角色ID-roleId、账户账号username、账户密码password、真实姓名realname、用户备注remarks、账户状态status。
6、修改用户:表单可修改项包括关联部门ID-deptId、关联角色ID-roleId、账户密码password(非必填)、真实姓名realname、用户备注remarks、账户状态status。
7、注意:ID为1的用户是默认用户,需要禁用删除按钮。ID为当前登录的用户ID,禁止修改和删除,需要禁用修改和删除按钮。

编写用户组件(views/User.vue),提供用户管理视图:

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import qs from "qs";
import { useCurrentUserStore } from '@/stores/currentUser.js'

// 分页数据
const tableData = ref([])
const total = ref(0)

// 下拉选项数据
const deptOptions = ref([])
const roleOptions = ref([])

// 状态选项
const statusOptions = ref([
    { value: 0, label: '禁用' },
    { value: 1, label: '正常' }
])

// 搜索表单数据
const searchForm = reactive({
    deptId: '',
    roleId: '',
    status: '',
    username: '',
    realname: '',
    remarks: ''
})

// 分页参数
const pagination = reactive({
    currentPage: 1,
    pageSize: 10
})

// 添加用户对话框相关
const addDialogVisible = ref(false)
const addFormRef = ref()
const addFormData = reactive({
    deptId: '',
    roleId: '',
    username: '',
    password: '',
    realname: '',
    remarks: '',
    status: 1
})
const addFormRules = {
    deptId: [
        { required: true, message: '请选择关联部门', trigger: 'change' }
    ],
    roleId: [
        { required: true, message: '请选择关联角色', trigger: 'change' }
    ],
    username: [
        { required: true, message: '请输入账户账号', trigger: 'blur' },
        { min: 3, max: 20, message: '长度应在3到20个字符之间', trigger: 'blur' }
    ],
    password: [
        { required: true, message: '请输入账户密码', trigger: 'blur' },
        { min: 6, max: 20, message: '长度应在6到20个字符之间', trigger: 'blur' }
    ],
    realname: [
        { required: true, message: '请输入真实姓名', trigger: 'blur' },
        { min: 1, max: 50, message: '长度应在1到50个字符之间', trigger: 'blur' }
    ],
    remarks: [
        { required: true, message: '请输入用户备注', trigger: 'blur' },
        { min: 1, max: 200, message: '长度应在1到200个字符之间', trigger: 'blur' }
    ],
    status: [
        { required: true, message: '请选择账户状态', trigger: 'change' }
    ]
}

// 编辑用户对话框相关
const editDialogVisible = ref(false)
const editFormRef = ref()
const editFormData = reactive({
    id: '',
    deptId: '',
    roleId: '',
    password: '',
    realname: '',
    remarks: '',
    status: 1
})
const editFormRules = {
    deptId: [
        { required: true, message: '请选择关联部门', trigger: 'change' }
    ],
    roleId: [
        { required: true, message: '请选择关联角色', trigger: 'change' }
    ],
    password: [
        { min: 6, max: 20, message: '长度应在6到20个字符之间', trigger: 'blur' }
    ],
    realname: [
        { required: true, message: '请输入真实姓名', trigger: 'blur' },
        { min: 1, max: 50, message: '长度应在1到50个字符之间', trigger: 'blur' }
    ],
    remarks: [
        { required: true, message: '请输入用户备注', trigger: 'blur' },
        { min: 1, max: 200, message: '长度应在1到200个字符之间', trigger: 'blur' }
    ],
    status: [
        { required: true, message: '请选择账户状态', trigger: 'change' }
    ]
}

// 获取部门下拉选项
const fetchDeptOptions = () => {
    axios.get('/dept/optionList').then((res) => {
        deptOptions.value = res.data
    })
}

// 获取角色下拉选项
const fetchRoleOptions = () => {
    axios.get('/role/optionList').then((res) => {
        roleOptions.value = res.data
    })
}

// 获取用户列表
const fetchUserList = () => {
    const params = {
        page: pagination.currentPage,
        size: pagination.pageSize,
        deptId: searchForm.deptId || undefined,
        roleId: searchForm.roleId || undefined,
        status: searchForm.status !== '' ? searchForm.status : undefined,
        username: searchForm.username || undefined,
        realname: searchForm.realname || undefined,
        remarks: searchForm.remarks || undefined
    }

    axios.get('/users/list', { params }).then((res) => {
        tableData.value = res.data.data
        total.value = res.data.total
    })
}

// 搜索
const submitSearchForm = () => {
    pagination.currentPage = 1
    fetchUserList()
}

// 重置搜索
const resetSearchForm = () => {
    searchForm.deptId = ''
    searchForm.roleId = ''
    searchForm.status = ''
    searchForm.username = ''
    searchForm.realname = ''
    searchForm.remarks = ''
    pagination.currentPage = 1
    fetchUserList()
}

// 分页变化
const handleSizeChange = (val) => {
    pagination.pageSize = val
    pagination.currentPage = 1
    fetchUserList()
}

const handleCurrentChange = (val) => {
    pagination.currentPage = val
    fetchUserList()
}

// 显示添加用户对话框
const showAddDialog = () => {
    addDialogVisible.value = true
}

// 提交添加表单
const submitAddForm = () => {
    addFormRef.value.validate((valid) => {
        if (valid) {
            axios.post('/users/add', qs.stringify(addFormData)).then((res) => {
                ElMessage.success('添加成功')
                addDialogVisible.value = false
                resetAddForm()
                fetchUserList()
            })
        } else {
            ElMessage.error('请正确填写表单信息')
            return false
        }
    })
}

// 重置添加表单
const resetAddForm = () => {
    addFormRef.value.resetFields()
}

// 显示编辑用户对话框
const showEditDialog = (row) => {
    // 先设置ID,用于请求详细信息
    editFormData.id = row.id
    editDialogVisible.value = true

    // 获取用户详细信息
    axios.get(`/users/get/${row.id}`).then((res) => {
        editFormData.deptId = res.data.deptId
        editFormData.roleId = res.data.roleId
        editFormData.realname = res.data.realname
        editFormData.remarks = res.data.remarks
        editFormData.status = res.data.status
    })
}

// 提交编辑表单
const submitEditForm = () => {
    editFormRef.value.validate((valid) => {
        if (valid) {
            axios.put(`/users/update/${editFormData.id}`, qs.stringify(editFormData)).then((res) => {
                ElMessage.success('修改成功')
                editDialogVisible.value = false
                resetEditForm()
                fetchUserList()
            })
        } else {
            ElMessage.error('请正确填写表单信息')
            return false
        }
    })
}

// 重置编辑表单
const resetEditForm = () => {
    editFormRef.value.resetFields()
}

// 删除用户
const handleDelete = (id) => {
    ElMessageBox.confirm('确定要删除该用户吗?', '删除确认', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
    }).then(() => {
        axios.delete(`/users/delete/${id}`)
            .then((res) => {
                ElMessage.success('删除成功')
                fetchUserList()
            })
    }).catch(() => {
        // 用户取消删除
    })
}

// 初始化加载数据
onMounted(() => {
    fetchUserList()
    fetchDeptOptions()
    fetchRoleOptions()
})
</script>

<template>
    <el-main class="layout-main">
        <!-- 搜索表单 -->
        <el-card shadow="never" style="margin-bottom: 1rem;" :body-style="{paddingBottom: '2px'}">
            <el-form :model="searchForm" inline>
                <el-form-item label="关联部门">
                    <el-select style="width: 150px" v-model="searchForm.deptId" placeholder="请选择关联部门" clearable>
                        <el-option v-for="item in deptOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
                    </el-select>
                </el-form-item>

                <el-form-item label="关联角色">
                    <el-select style="width: 150px" v-model="searchForm.roleId" placeholder="请选择关联角色" clearable>
                        <el-option v-for="item in roleOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
                    </el-select>
                </el-form-item>

                <el-form-item label="账户状态">
                    <el-select style="width: 150px" v-model="searchForm.status" placeholder="请选择账户状态" clearable>
                        <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
                    </el-select>
                </el-form-item>

                <el-form-item label="账户账号">
                    <el-input v-model="searchForm.username" />
                </el-form-item>

                <el-form-item label="真实姓名">
                    <el-input v-model="searchForm.realname" />
                </el-form-item>

                <el-form-item label="用户备注">
                    <el-input v-model="searchForm.remarks" />
                </el-form-item>

                <el-form-item>
                    <el-button type="primary" @click="submitSearchForm">搜索</el-button>
                    <el-button @click="resetSearchForm">重置</el-button>
                    <el-button type="warning" @click="showAddDialog">添加用户</el-button>
                </el-form-item>
            </el-form>
        </el-card>

        <!-- 数据表格 -->
        <el-card shadow="never">
            <template #header>
                <div class="card-header">
                    <span>用户列表</span>
                </div>
            </template>
            <el-table :data="tableData" style="width: 100%" stripe>
                <el-table-column prop="id" label="用户ID" width="80" />
                <el-table-column prop="dept.name" label="关联部门" />
                <el-table-column prop="role.name" label="关联角色" />
                <el-table-column prop="username" label="账户账号" />
                <el-table-column prop="realname" label="真实姓名" />
                <el-table-column prop="remarks" label="用户备注" />
                <el-table-column prop="status" label="账户状态">
                    <template #default="scope">
                        <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
                            {{ scope.row.status === 1 ? '正常' : '禁用' }}
                        </el-tag>
                    </template>
                </el-table-column>
                <el-table-column label="操作">
                    <template #default="scope">
                        <el-button type="primary" size="small" @click="showEditDialog(scope.row)" :disabled="scope.row.id === 1 || scope.row.id === useCurrentUserStore().currentUser.id">
                            编辑
                        </el-button>
                        <el-button type="danger" size="small" @click="handleDelete(scope.row.id)" :disabled="scope.row.id === 1">
                            删除
                        </el-button>
                    </template>
                </el-table-column>
            </el-table>

            <!-- 分页组件 -->
            <el-pagination v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize"
                           :page-sizes="[10, 20, 50, 100]" :small="false" :disabled="false" :background="true"
                           layout="total, sizes, prev, pager, next, jumper"
                           :total="total" @size-change="handleSizeChange" @current-change="handleCurrentChange"
                           style="margin-top: 20px; justify-content: flex-end; display: flex;"
            />
        </el-card>

        <!-- 添加用户对话框 -->
        <el-dialog v-model="addDialogVisible" title="添加用户" width="500px" @close="resetAddForm">
            <el-form ref="addFormRef" :model="addFormData" :rules="addFormRules" label-width="80px" label-position="top">
                <el-form-item label="关联部门" prop="deptId">
                    <el-select v-model="addFormData.deptId" placeholder="请选择关联部门" style="width: 100%">
                        <el-option v-for="item in deptOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
                    </el-select>
                </el-form-item>

                <el-form-item label="关联角色" prop="roleId">
                    <el-select v-model="addFormData.roleId" placeholder="请选择关联角色" style="width: 100%">
                        <el-option v-for="item in roleOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
                    </el-select>
                </el-form-item>

                <el-form-item label="账户账号" prop="username">
                    <el-input v-model="addFormData.username" placeholder="请输入账户账号"/>
                </el-form-item>

                <el-form-item label="账户密码" prop="password">
                    <el-input v-model="addFormData.password" placeholder="请输入账户密码" type="password"/>
                </el-form-item>

                <el-form-item label="真实姓名" prop="realname">
                    <el-input v-model="addFormData.realname" placeholder="请输入真实姓名"/>
                </el-form-item>

                <el-form-item label="用户备注" prop="remarks">
                    <el-input v-model="addFormData.remarks" placeholder="请输入用户备注" type="textarea"/>
                </el-form-item>

                <el-form-item label="账户状态" prop="status">
                    <el-select v-model="addFormData.status" placeholder="请选择账户状态" style="width: 100%">
                        <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
                    </el-select>
                </el-form-item>
            </el-form>

            <template #footer>
                <div style="width: 100%; text-align: center">
                    <el-button @click="addDialogVisible = false">取消</el-button>
                    <el-button type="primary" @click="submitAddForm">确定</el-button>
                </div>
            </template>
        </el-dialog>

        <!-- 编辑用户对话框 -->
        <el-dialog v-model="editDialogVisible" title="编辑用户" width="500px" @close="resetEditForm">
            <el-form ref="editFormRef" :model="editFormData" :rules="editFormRules" label-width="80px" label-position="top">
                <el-form-item label="关联部门" prop="deptId">
                    <el-select v-model="editFormData.deptId" placeholder="请选择关联部门" style="width: 100%">
                        <el-option v-for="item in deptOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
                    </el-select>
                </el-form-item>

                <el-form-item label="关联角色" prop="roleId">
                    <el-select v-model="editFormData.roleId" placeholder="请选择关联角色" style="width: 100%">
                        <el-option v-for="item in roleOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
                    </el-select>
                </el-form-item>

                <el-form-item label="账户密码" prop="password">
                    <el-input v-model="editFormData.password" placeholder="请输入账户密码(留空则不修改)" type="password"/>
                </el-form-item>

                <el-form-item label="真实姓名" prop="realname">
                    <el-input v-model="editFormData.realname" placeholder="请输入真实姓名"/>
                </el-form-item>

                <el-form-item label="用户备注" prop="remarks">
                    <el-input v-model="editFormData.remarks" placeholder="请输入用户备注" type="textarea"/>
                </el-form-item>

                <el-form-item label="账户状态" prop="status">
                    <el-select v-model="editFormData.status" placeholder="请选择账户状态" style="width: 100%">
                        <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
                    </el-select>
                </el-form-item>
            </el-form>

            <template #footer>
                <div style="width: 100%; text-align: center">
                    <el-button @click="editDialogVisible = false">取消</el-button>
                    <el-button type="primary" @click="submitEditForm">确定</el-button>
                </div>
            </template>
        </el-dialog>
    </el-main>
</template>

编写路由配置文件(router/index.js),提供用户管理路由信息:

{
    // 用户管理
    path: 'users/list',
    component: () => import('@/views/Users.vue'),
},

编写布局组件(views/Layouts.vue),提供用户管理菜单:

{
    id: 11,
    title: '用户管理',
    path: '/users/list'
},

在浏览器中测试:

修改报错问题

前置后端 API、前端组件开发完成后,测试修改用户功能时,会报错:

报错的原因,是因为 EasyQuery 中默认 update(entity) 操作是更新对象全部列到数据库,不忽略 null 值,在修改用户时,禁止修改账户账号,因此没有传递账户账号,此时账户账号为 null,在数据库中账户账号字段却是 NOT NULL,因此会产生报错。

参考文档:http://www.easy-query.com/easy-query-doc/ability/update.html

解决该问题,可以通过修改修改策略 updateStrategy,忽略 null 值。

参考文档:http://www.easy-query.com/easy-query-doc/framework/config-option.html

编写 Solon 项目配置文件(classpath:/app.yml),配置 EasyQuery:

easy-query:
  ds1:  # 数据源-ds1
    database: mysql # 数据库类型设置为mysql
    print-sql: true # 打印SQL日志
    update-strategy: only_not_null_columns # 更新策略,只更新NOT NULL的字段

在浏览器中测试:

序列化问题

Solon 3.7.2 默认的序列化规则是 Snack4,Solon-SaToken 使用的序列化规则是 Snack3,在使用 @ONodeAttr 时容易导错包,导致序列化规则不生效。

在获取用户列表时,账户密码、账户盐值会被序列化返回给前端:

解决该问题,可以统一使用 Snack3 序列化方式,而不使用 Snack4 序列化方式。

编写 Maven 项目配置文件(pom.xml),加入 Snack3 依赖:

<dependencies>
    <!-- Solon Web -->
    <dependency>
        <groupId>org.noear</groupId>
        <artifactId>solon-web</artifactId>
        <exclusions>
            <!-- 排除Snack4依赖 -->
            <exclusion>
                <groupId>org.noear</groupId>
                <artifactId>solon-serialization-snack4</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <!-- 重新引入Snack3序列化 -->
    <dependency>
        <groupId>org.noear</groupId>
        <artifactId>solon-serialization-snack3</artifactId>
    </dependency>
    
    <!-- ... -->
</dependencies>

在浏览器中测试: