源本科技 | 码上会

Vue3 联调后端 - 用户管理

2026/05/08
68
0

创建请求接口

  • src/api/system/SysUserApi.ts

import { http } from "@/utils/http";

export type UserResult = {
  success: boolean;
  data: any;
};

export type UserPageResult = {
  success: boolean;
  data: {
    rows: any[];
    totalRows: number;
  };
};

/** 获取用户分页列表 */
export const getUserPage = (params?: object) => {
  return http.request<UserPageResult>("get", "/sys/user/page", { params });
};

/** 获取用户详情 */
export const getUserDetail = (data?: object) => {
  return http.request<UserResult>("post", "/sys/user/detail", { data });
};

/** 新增用户 */
export const addUser = (data?: object) => {
  return http.request<UserResult>("post", "/sys/user/add", { data });
};

/** 编辑用户 */
export const editUser = (data?: object) => {
  return http.request<UserResult>("post", "/sys/user/edit", { data });
};

/** 删除用户 */
export const deleteUser = (data?: object) => {
  return http.request<UserResult>("post", "/sys/user/delete", { data });
};

创建管理界面

  • src/views/system/user/index.vue

注意:前端 Vue 界面请根据数据库 sys_menu 表中的 uri 路径创建,因为我们使用了前端模板的动态路由

<template>
  <div class="main">
    <el-card shadow="never">
      <!-- 搜索区域 -->
      <el-form :inline="true" :model="searchForm" class="search-form">
        <el-form-item label="搜索:">
          <el-input
            v-model="searchForm.searchText"
            placeholder="请输入用户名或昵称"
            clearable
            style="width: 200px"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" :icon="Search" @click="handleSearch">
            搜索
          </el-button>
          <el-button :icon="Refresh" @click="handleReset">重置</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">
            新增
          </el-button>
        </el-col>
      </el-row>

      <!-- 数据表格 -->
      <el-table
        v-loading="loading"
        :data="tableData"
        border
        stripe
        style="width: 100%"
      >
        <el-table-column prop="userId" label="用户ID" width="100" />
        <el-table-column prop="username" label="用户名" min-width="120" />
        <el-table-column prop="nickName" label="昵称" min-width="120" />
        <el-table-column prop="email" label="邮箱" min-width="150" />
        <el-table-column prop="phone" label="手机号" min-width="120" />
        <el-table-column label="状态" width="100">
          <template #default="{ row }">
            <el-tag :type="row.statusFlag === '1' ? 'success' : 'danger'">
              {{ row.statusFlag === "1" ? "启用" : "禁用" }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="180" fixed="right">
          <template #default="{ row }">
            <el-button
              link
              type="primary"
              :icon="Edit"
              @click="handleEdit(row)"
            >
              编辑
            </el-button>
            <el-button
              link
              type="danger"
              :icon="Delete"
              @click="handleDelete(row)"
            >
              删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>

      <!-- 分页组件 -->
      <el-pagination
        v-model:current-page="searchForm.pageNum"
        v-model:page-size="searchForm.pageSize"
        :page-sizes="[10, 20, 50, 100]"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        class="mt-4"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </el-card>

    <!-- 新增/编辑对话框 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="600px"
      @close="handleClose"
    >
      <el-form
        ref="formRef"
        :model="formData"
        :rules="rules"
        label-width="100px"
      >
        <el-form-item label="用户名" prop="username">
          <el-input
            v-model="formData.username"
            placeholder="请输入用户名"
            :disabled="isEdit"
          />
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input
            v-model="formData.password"
            type="password"
            :placeholder="isEdit ? '请输入密码(不填则不修改)' : '请输入密码'"
            show-password
          />
        </el-form-item>
        <el-form-item label="昵称" prop="nickName">
          <el-input v-model="formData.nickName" placeholder="请输入昵称" />
        </el-form-item>
        <el-form-item label="邮箱" prop="email">
          <el-input v-model="formData.email" placeholder="请输入邮箱" />
        </el-form-item>
        <el-form-item label="手机号" prop="phone">
          <el-input v-model="formData.phone" placeholder="请输入手机号" />
        </el-form-item>
        <el-form-item label="状态" prop="statusFlag">
          <el-radio-group v-model="formData.statusFlag">
            <el-radio value="1">启用</el-radio>
            <el-radio value="0">禁用</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="handleClose">取消</el-button>
        <el-button type="primary" @click="handleSubmit">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Delete, Edit, Search, Refresh } from "@element-plus/icons-vue";
import {
  getUserPage,
  addUser,
  editUser,
  deleteUser,
  getUserDetail
} from "@/api/system/SysUserApi";

// 定义组件选项
defineOptions({
  // 设置组件名称
  name: "UserManage"
});

/** 加载状态 */
const loading = ref(false);
/** 表格数据 */
const tableData = ref([]);
/** 总记录数 */
const total = ref(0);
/** 对话框显示状态 */
const dialogVisible = ref(false);
/** 对话框标题 */
const dialogTitle = ref("");
/** 表单引用 */
const formRef = ref();

/** 搜索表单数据 */
const searchForm = reactive({
  searchText: "",
  pageNum: 1,
  pageSize: 10
});

/** 编辑/新增表单数据 */
const formData = reactive({
  userId: undefined,
  username: "",
  password: "",
  nickName: "",
  email: "",
  phone: "",
  statusFlag: "1"
});

/** 表单验证规则 */
const rules = reactive({
  username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
  password: [{ required: true, message: "请输入密码", trigger: "blur" }],
  nickName: [{ required: true, message: "请输入昵称", trigger: "blur" }]
});

/** 是否为编辑模式 */
const isEdit = ref(false);

/**
 * 加载用户列表数据
 */
const loadData = async () => {
  loading.value = true;
  try {
    const res = await getUserPage(searchForm);
    if (res.success) {
      tableData.value = res.data.rows || [];
      total.value = res.data.totalRows || 0;
    }
  } catch (error) {
    console.error("加载数据失败", error);
    ElMessage.error("加载数据失败");
  } finally {
    loading.value = false;
  }
};

/**
 * 搜索用户
 */
const handleSearch = () => {
  searchForm.pageNum = 1;
  loadData();
};

/**
 * 重置搜索条件
 */
const handleReset = () => {
  searchForm.searchText = "";
  searchForm.pageNum = 1;
  loadData();
};

/**
 * 打开新增用户对话框
 */
const handleAdd = () => {
  isEdit.value = false;
  dialogTitle.value = "新增用户";
  resetForm();
  updateRules();
  dialogVisible.value = true;
};

/**
 * 打开编辑用户对话框
 * @param row 当前行数据
 */
const handleEdit = async (row: any) => {
  isEdit.value = true;
  dialogTitle.value = "编辑用户";
  try {
    const res = await getUserDetail({ userId: row.userId });
    if (res.success) {
      Object.assign(formData, res.data);
      // 编辑时不显示密码,保持为空
      formData.password = "";
      updateRules();
      dialogVisible.value = true;
    }
  } catch (error) {
    console.error("获取用户详情失败", error);
    ElMessage.error("获取用户详情失败");
  }
};

/**
 * 删除用户
 * @param row 当前行数据
 */
const handleDelete = (row: any) => {
  ElMessageBox.confirm("确定要删除该用户吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning"
  })
    .then(async () => {
      try {
        const res = await deleteUser({ userId: row.userId });
        if (res.success) {
          ElMessage.success("删除成功");
          loadData();
        }
      } catch (error) {
        console.error("删除失败", error);
        ElMessage.error("删除失败");
      }
    })
    .catch(() => {
      // 用户取消删除
    });
};

/**
 * 提交表单(新增或编辑)
 */
const handleSubmit = async () => {
  if (!formRef.value) return;

  await formRef.value.validate(async (valid: boolean) => {
    if (valid) {
      try {
        // 创建提交数据的副本
        const submitData = { ...formData };

        // 编辑模式下,如果密码为空则不传递该字段
        if (isEdit.value && !submitData.password) {
          delete submitData.password;
        }

        // 转换状态标识为字符类型,匹配后端 Character 类型
        if (submitData.statusFlag) {
          submitData.statusFlag = String(submitData.statusFlag).charAt(0);
        }

        // 根据模式调用对应的 API
        const api = isEdit.value ? editUser : addUser;
        const res = await api(submitData);

        if (res.success) {
          ElMessage.success(isEdit.value ? "修改成功" : "新增成功");
          dialogVisible.value = false;
          loadData();
        }
      } catch (error) {
        console.error(isEdit.value ? "修改失败" : "新增失败", error);
        ElMessage.error(isEdit.value ? "修改失败" : "新增失败");
      }
    }
  });
};

/**
 * 关闭对话框并重置表单
 */
const handleClose = () => {
  dialogVisible.value = false;
  resetForm();
};

/**
 * 重置表单数据
 */
const resetForm = () => {
  Object.assign(formData, {
    userId: undefined,
    username: "",
    password: "",
    nickName: "",
    email: "",
    phone: "",
    statusFlag: "1"
  });
  formRef.value?.clearValidate();
};

/**
 * 根据模式更新表单验证规则
 * 新增模式:密码必填
 * 编辑模式:密码非必填
 */
const updateRules = () => {
  if (isEdit.value) {
    // 编辑模式:密码可选
    rules.password = [];
  } else {
    // 新增模式:密码必填
    rules.password = [
      { required: true, message: "请输入密码", trigger: "blur" }
    ];
  }
};

/**
 * 分页:每页条数改变
 * @param val 新的每页条数
 */
const handleSizeChange = (val: number) => {
  searchForm.pageSize = val;
  loadData();
};

/**
 * 分页:当前页改变
 * @param val 新的页码
 */
const handleCurrentChange = (val: number) => {
  searchForm.pageNum = val;
  loadData();
};

// 组件挂载时加载数据
onMounted(() => {
  loadData();
});
</script>

<style scoped lang="scss">
.main {
  padding: 20px;
}

.search-form {
  margin-bottom: 20px;
}

.mb8 {
  margin-bottom: 8px;
}

.mt-4 {
  margin-top: 16px;
  display: flex;
  justify-content: flex-end;
}
</style>
  • 管理界面示例图

  • 新增 / 编辑示例图

解决前端 JS 精度丢失

由于 JavaScript 的 Number 类型无法表示大整数,在实体类中添加 Jackson 注解,将 Long 类型序列化为字符串

  • 修改 com.lusifer.crmeb.entity.SysUser 在数据类型为 Long 的字段上追加 Jackson 注解

public class SysUser extends BaseBusinessEntity {
  /** 用户ID */
  @TableId(value = "user_id", type = IdType.ASSIGN_ID)
  @JsonSerialize(using = ToStringSerializer.class)
  private Long userId;

    /** 所属部门 */
  @JsonSerialize(using = ToStringSerializer.class)
  private Long deptId;
}

测试运行

上述操作完成后,在用户管理界面测试 CRUD 功能