VUE前端项目开发1.0.0版本

项目包含用户权限管理、表单提交、数据上传下载、仪表盘可视化等功能,非常适合新手从零开始学习Vue3前端开发,下述包含所有开发源代码,敬请食用

废话少说先上图:

登陆界面:

仪表盘管理系统:

用户管理:

对于普通用户只有仪表盘和个人信息界面:

一、创建Vue3前端项目

1.1 创建项目

Vue 复制代码
# 安装Vue CLI(如果没有的话)
npm install -g @vue/cli
# 创建项目
vue create vue-user-management

选择配置

java 复制代码
- Vue 3
- Router
- Vuex
- CSS Pre-processors (Sass/SCSS)
- Linter / Formatter

1.2 安装额外依赖

java 复制代码
cd vue-user-management
# 安装UI框架和其他依赖
npm install element-plus axios @element-plus/icons-vue
npm install --save-dev sass sass-loader

1.3 项目结构

java 复制代码
vue-user-management/
├── public/
├── src/
│   ├── api/           # API接口
│   ├── assets/        # 静态资源
│   ├── components/    # 组件
│   ├── router/        # 路由
│   ├── store/         # Vuex状态管理
│   ├── utils/         # 工具类
│   ├── views/         # 页面
│   ├── App.vue
│   └── main.js
├── package.json
└── vue.config.js

1.4 主要文件代码

4.1 // src/main.js

java 复制代码
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
import zhCn from "element-plus/dist/locale/zh-cn.mjs";

const app = createApp(App);

// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component);
}

app.use(store);
app.use(router);
app.use(ElementPlus, {
  locale: zhCn,
});

app.mount("#app");

4.2 // src/utils/request.js

java 复制代码
import axios from "axios";
import { ElMessage } from "element-plus";
import store from "@/store";
import router from "@/router";

// 创建axios实例
const service = axios.create({
  baseURL: "http://localhost:8080/api", // 后端地址
  timeout: 5000,
});

// 请求拦截器
service.interceptors.request.use(
  (config) => {
    // 如果有token,添加到请求头
    if (store.getters.token) {
      config.headers["Authorization"] = "Bearer " + store.getters.token;
    }
    return config;
  },
  (error) => {
    console.log(error);
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  (response) => {
    const res = response.data;

    // 如果code不是200,说明出错了
    if (res.code !== 200) {
      ElMessage({
        message: res.message || "请求失败",
        type: "error",
        duration: 5 * 1000,
      });

      // 401: 未登录
      if (res.code === 401) {
        store.dispatch("user/logout");
        router.push("/login");
      }

      return Promise.reject(new Error(res.message || "请求失败"));
    } else {
      return res;
    }
  },
  (error) => {
    console.log("err" + error);
    ElMessage({
      message: error.message,
      type: "error",
      duration: 5 * 1000,
    });
    return Promise.reject(error);
  }
);

export default service;

4.3 // src/api/user.js

java 复制代码
import request from "@/utils/request";

// 登录
export function login(data) {
  return request({
    url: "/auth/login",
    method: "post",
    data,
  });
}

// 登出
export function logout() {
  return request({
    url: "/auth/logout",
    method: "post",
  });
}

// 获取用户列表
export function getUserList(params) {
  return request({
    url: "/users/page",
    method: "get",
    params,
  });
}

// 获取所有用户
export function getAllUsers() {
  return request({
    url: "/users",
    method: "get",
  });
}

// 获取单个用户
export function getUser(id) {
  return request({
    url: `/users/${id}`,
    method: "get",
  });
}

// 创建用户
export function createUser(data) {
  return request({
    url: "/users",
    method: "post",
    data,
  });
}

// 更新用户
export function updateUser(id, data) {
  return request({
    url: `/users/${id}`,
    method: "put",
    data,
  });
}

// 删除用户
export function deleteUser(id) {
  return request({
    url: `/users/${id}`,
    method: "delete",
  });
}

// 导出CSV
export function exportUsersCsv() {
  return request({
    url: "/csv/export",
    method: "get",
    responseType: "blob",
  });
}

// 导入CSV
export function importUsersCsv(formData) {
  return request({
    url: "/csv/import",
    method: "post",
    data: formData,
    headers: {
      "Content-Type": "multipart/form-data",
    },
  });
}

4.4 // src/store/index.js

java 复制代码
// src/store/index.js
import { createStore } from "vuex";
import { login, logout } from "@/api/user";

const store = createStore({
  state: {
    token: localStorage.getItem("token") || "",
    user: JSON.parse(localStorage.getItem("user") || "{}"),
    role: localStorage.getItem("role") || "",
  },

  getters: {
    token: (state) => state.token,
    user: (state) => state.user,
    role: (state) => state.role,
    isAdmin: (state) => state.role === "admin",
  },

  mutations: {
    SET_TOKEN(state, token) {
      state.token = token;
      localStorage.setItem("token", token);
    },
    SET_USER(state, user) {
      state.user = user;
      localStorage.setItem("user", JSON.stringify(user));
    },
    SET_ROLE(state, role) {
      state.role = role;
      localStorage.setItem("role", role);
    },
    CLEAR_USER(state) {
      state.token = "";
      state.user = {};
      state.role = "";
      localStorage.removeItem("token");
      localStorage.removeItem("user");
      localStorage.removeItem("role");
    },
  },

  actions: {
    // 登录
    async login({ commit }, loginForm) {
      try {
        const response = await login(loginForm);
        const { token, user, role } = response.data;

        commit("SET_TOKEN", token);
        commit("SET_USER", user);
        commit("SET_ROLE", role);

        return response;
      } catch (error) {
        console.error('发生错误:', error);
        // 或者其他处理逻辑
        throw error;
      }
    }, // <-- 这里添加了逗号

    // 登出
    async logout({ commit }) {
      try {
        await logout();
      } catch (error) {
        console.error("Logout error:", error);
      } finally {
        commit("CLEAR_USER");
      }
    },
  },
});

export default store;

4.5 // src/router/index.js

java 复制代码
// src/router/index.js
import { createRouter, createWebHistory } from "vue-router";
import store from "@/store";
import { ElMessage } from "element-plus";

const routes = [
  {
    path: "/login",
    name: "Login",
    component: () => import("@/views/Login.vue"),
    meta: { requiresAuth: false },
  },
  {
    path: "/",
    name: "Layout",
    component: () => import("@/views/Layout.vue"),
    redirect: "/dashboard",
    meta: { requiresAuth: true },
    children: [
      {
        path: "dashboard",
        name: "Dashboard",
        component: () => import("@/views/Dashboard.vue"),
        meta: { title: "仪表盘", icon: "Odometer" },
      },
      {
        path: "users",
        name: "Users",
        component: () => import("@/views/UserManagement.vue"),
        meta: { title: "用户管理", icon: "User", requiresAdmin: true },
      },
      {
        path: "profile",
        name: "Profile",
        component: () => import("@/views/Profile.vue"),
        meta: { title: "个人信息", icon: "UserFilled" },
      },
    ],
  },
  {
    path: "/:pathMatch(.*)*",
    redirect: "/login",
  },
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

// 路由守卫
router.beforeEach((to, from, next) => {
  const token = store.getters.token;

  if (to.meta.requiresAuth && !token) {
    // 需要登录但未登录
    ElMessage.warning("请先登录");
    next("/login");
  } else if (to.meta.requiresAdmin && !store.getters.isAdmin) {
    // 需要管理员权限但不是管理员
    ElMessage.error("权限不足");
    next("/dashboard");
  } else if (to.path === "/login" && token) {
    // 已登录但访问登录页
    next("/dashboard");
  } else {
    next();
  }
});

export default router;

4.6//src/views/Login.vue

java 复制代码
<!-- src/views/Login.vue -->
<template>
  <div class="login-container">
    <el-card class="login-card">
      <h2 class="login-title">用户管理系统</h2>
      <el-form
        ref="loginFormRef"
        :model="loginForm"
        :rules="rules"
        label-width="0px"
      >
        <el-form-item prop="username">
          <el-input
            v-model="loginForm.username"
            prefix-icon="User"
            placeholder="请输入用户名"
            size="large"
          />
        </el-form-item>
        <el-form-item prop="password">
          <el-input
            v-model="loginForm.password"
            type="password"
            prefix-icon="Lock"
            placeholder="请输入密码"
            size="large"
            show-password
            @keyup.enter="handleLogin"
          />
        </el-form-item>
        <el-form-item>
          <el-button
            type="primary"
            size="large"
            style="width: 100%"
            :loading="loading"
            @click="handleLogin"
          >
            登 录
          </el-button>
        </el-form-item>
      </el-form>
      <div class="login-tips">
        <p>管理员账号:admin / 123456</p>
        <p>普通用户:zhangsan / 123456</p>
      </div>
    </el-card>
  </div>
</template>

<script setup>
import { ref, reactive } from "vue";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
import { ElMessage } from "element-plus";


const router = useRouter();
const store = useStore();

const loginFormRef = ref();
const loading = ref(false);

const loginForm = reactive({
  username: "",
  password: "",
});

const rules = {
  username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
  password: [{ required: true, message: "请输入密码", trigger: "blur" }],
};

const handleLogin = async () => {
  const valid = await loginFormRef.value.validate();
  if (!valid) return;

  loading.value = true;
  try {
    await store.dispatch("login", loginForm);
    ElMessage.success("登录成功");
    router.push("/");
  } catch (error) {
    ElMessage.error(error.message || "登录失败");
  } finally {
    loading.value = false;
  }
};
</script>

<style scoped lang="scss">
.login-container {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.login-card {
  width: 400px;
  padding: 20px;
}

.login-title {
  text-align: center;
  margin-bottom: 30px;
  color: #333;
}

.login-tips {
  margin-top: 20px;
  text-align: center;
  color: #999;
  font-size: 14px;

  p {
    margin: 5px 0;
  }
}
</style>

4.7//src/views/Layout.vue

java 复制代码
<!-- src/views/Layout.vue -->
<template>
  <el-container class="layout-container">
    <!-- 侧边栏 -->
    <el-aside width="200px" class="sidebar">
      <div class="logo">
        <h3>用户管理系统</h3>
      </div>
      <el-menu
        :default-active="activeMenu"
        background-color="#304156"
        text-color="#bfcbd9"
        active-text-color="#409eff"
        router
      >
        <el-menu-item index="/dashboard">
          <el-icon><Odometer /></el-icon>
          <span>仪表盘</span>
        </el-menu-item>
        <el-menu-item index="/users" v-if="isAdmin">
          <el-icon><User /></el-icon>
          <span>用户管理</span>
        </el-menu-item>
        <el-menu-item index="/profile">
          <el-icon><UserFilled /></el-icon>
          <span>个人信息</span>
        </el-menu-item>
      </el-menu>
    </el-aside>

    <!-- 主体区域 -->
    <el-container>
      <!-- 顶部栏 -->
      <el-header class="header">
        <div class="header-left">
          <h4>{{ pageTitle }}</h4>
        </div>
        <div class="header-right">
          <el-dropdown @command="handleCommand">
            <span class="user-info">
              <el-icon><UserFilled /></el-icon>
              {{ user.username }}
              <el-icon><ArrowDown /></el-icon>
            </span>
            <template #dropdown>
              <el-dropdown-menu>
                <el-dropdown-item command="profile">个人信息</el-dropdown-item>
                <el-dropdown-item command="logout" divided
                  >退出登录</el-dropdown-item
                >
              </el-dropdown-menu>
            </template>
          </el-dropdown>
        </div>
      </el-header>

      <!-- 内容区域 -->
      <el-main class="main">
        <router-view />
      </el-main>
    </el-container>
  </el-container>
</template>

<script setup>
import { computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useStore } from "vuex";
import { ElMessageBox } from "element-plus";


const route = useRoute();
const router = useRouter();
const store = useStore();

const user = computed(() => store.getters.user);
const isAdmin = computed(() => store.getters.isAdmin);
const activeMenu = computed(() => route.path);
const pageTitle = computed(() => route.meta.title || "页面");

const handleCommand = async (command) => {
  if (command === "profile") {
    router.push("/profile");
  } else if (command === "logout") {
    try {
      await ElMessageBox.confirm("确定要退出登录吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      });

      await store.dispatch("logout");
      router.push("/login");
    } catch (error) {
      // 用户取消
    }
  }
};
</script>

<style scoped lang="scss">
.layout-container {
  height: 100vh;
}

.sidebar {
  background-color: #304156;

  .logo {
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: #2b3548;

    h3 {
      color: #fff;
      margin: 0;
    }
  }

  .el-menu {
    border-right: none;
  }
}

.header {
  background-color: #fff;
  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 20px;

  .user-info {
    display: flex;
    align-items: center;
    cursor: pointer;

    .el-icon {
      margin: 0 5px;
    }
  }
}

.main {
  background-color: #f0f2f5;
}
</style>

4.8//src/views/UserManagement.vue

java 复制代码
<!-- src/views/UserManagement.vue -->
<template>
  <div class="user-management">
    <!-- 搜索和操作栏 -->
    <el-card class="search-card">
      <el-row :gutter="20">
        <el-col :span="6">
          <el-input
            v-model="searchForm.username"
            placeholder="请输入用户名"
            clearable
            @clear="handleSearch"
            @keyup.enter="handleSearch"
          >
            <template #prefix>
              <el-icon><Search /></el-icon>
            </template>
          </el-input>
        </el-col>
        <el-col :span="6">
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-col>
        <el-col :span="12" style="text-align: right">
          <el-button type="primary" @click="showAddDialog">
            <el-icon><Plus /></el-icon>
            新增用户
          </el-button>
          <el-button type="success" @click="handleExport">
            <el-icon><Download /></el-icon>
            导出CSV
          </el-button>
          <el-upload
            :show-file-list="false"
            :before-upload="handleImport"
            accept=".csv"
            style="display: inline-block; margin-left: 10px"
          >
            <el-button type="warning">
              <el-icon><Upload /></el-icon>
              导入CSV
            </el-button>
          </el-upload>
        </el-col>
      </el-row>
    </el-card>

    <!-- 数据表格 -->
    <el-card class="table-card">
      <el-table
        :data="tableData"
        v-loading="loading"
        stripe
        style="width: 100%"
      >
        <el-table-column prop="id" label="ID" width="80" />
        <el-table-column prop="username" label="用户名" />
        <el-table-column prop="email" label="邮箱" />
        <el-table-column prop="phone" label="手机号" />
        <el-table-column prop="score" label="分数" width="80">
          <template #default="{ row }">
            <el-tag :type="getScoreType(row.score)">{{ row.score }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="status" label="状态" width="100">
          <template #default="{ row }">
            <el-switch
              v-model="row.status"
              :active-value="1"
              :inactive-value="0"
              @change="handleStatusChange(row)"
            />
          </template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" width="180">
          <template #default="{ row }">
            {{ formatDate(row.createTime) }}
          </template>
        </el-table-column>
        <el-table-column label="操作" width="200" fixed="right">
          <template #default="{ row }">
            <el-button type="primary" size="small" @click="showEditDialog(row)">
              编辑
            </el-button>
            <el-button type="danger" size="small" @click="handleDelete(row)">
              删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>

      <!-- 分页 -->
      <el-pagination
        v-model:current-page="pagination.pageNum"
        v-model:page-size="pagination.pageSize"
        :page-sizes="[10, 20, 50, 100]"
        :total="pagination.total"
        layout="total, sizes, prev, pager, next, jumper"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        style="margin-top: 20px"
      />
    </el-card>

    <!-- 新增/编辑对话框 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="500px"
      @close="resetForm"
    >
      <el-form
        ref="userFormRef"
        :model="userForm"
        :rules="rules"
        label-width="80px"
      >
        <el-form-item label="用户名" prop="username">
          <el-input
            v-model="userForm.username"
            placeholder="请输入用户名"
            :disabled="isEdit"
          />
        </el-form-item>
        <el-form-item label="密码" prop="password" v-if="!isEdit">
          <el-input
            v-model="userForm.password"
            type="password"
            placeholder="请输入密码"
            show-password
          />
        </el-form-item>
        <el-form-item label="邮箱" prop="email">
          <el-input v-model="userForm.email" placeholder="请输入邮箱" />
        </el-form-item>
        <el-form-item label="手机号" prop="phone">
          <el-input v-model="userForm.phone" placeholder="请输入手机号" />
        </el-form-item>
        <el-form-item label="分数" prop="score">
          <el-input-number
            v-model="userForm.score"
            :min="0"
            :max="100"
            placeholder="请输入分数"
          />
        </el-form-item>
        <el-form-item label="状态" prop="status">
          <el-radio-group v-model="userForm.status">
            <el-radio :label="1">启用</el-radio>
            <el-radio :label="0">禁用</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="submitForm">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
  getUserList,
  createUser,
  updateUser,
  deleteUser,
  exportUsersCsv,
  importUsersCsv,
} from "@/api/user";


// 数据定义
const loading = ref(false);
const tableData = ref([]);
const dialogVisible = ref(false);
const isEdit = ref(false);
const userFormRef = ref();

// 搜索表单
const searchForm = reactive({
  username: "",
});

// 分页
const pagination = reactive({
  pageNum: 1,
  pageSize: 10,
  total: 0,
});

// 用户表单
const userForm = reactive({
  id: null,
  username: "",
  password: "",
  email: "",
  phone: "",
  score: 0,
  status: 1,
});

// 表单验证规则
const rules = {
  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" },
  ],
  email: [
    { required: true, message: "请输入邮箱", trigger: "blur" },
    { type: "email", message: "请输入正确的邮箱地址", trigger: "blur" },
  ],
  phone: [
    {
      pattern: /^1[3-9]\d{9}$/,
      message: "请输入正确的手机号",
      trigger: "blur",
    },
  ],
};

// 计算属性
const dialogTitle = ref("新增用户");

// 方法
const getScoreType = (score) => {
  if (score >= 90) return "success";
  if (score >= 60) return "warning";
  return "danger";
};

const formatDate = (dateStr) => {
  if (!dateStr) return "";
  const date = new Date(dateStr);
  return date.toLocaleString("zh-CN");
};

// 获取用户列表
const getList = async () => {
  loading.value = true;
  try {
    const params = {
      pageNum: pagination.pageNum,
      pageSize: pagination.pageSize,
    };
    
    // 添加搜索参数
    if (searchForm.username) {
      params.username = searchForm.username;
    }
    
    const res = await getUserList(params);

    tableData.value = res.data.list;
    pagination.total = res.data.total;
  } catch (error) {
    ElMessage.error("获取用户列表失败");
  } finally {
    loading.value = false;
  }
};

// 搜索
const handleSearch = () => {
  pagination.pageNum = 1;
  getList();
};

// 重置搜索
const resetSearch = () => {
  searchForm.username = "";
  handleSearch();
};

// 分页
const handleSizeChange = () => {
  pagination.pageNum = 1;
  getList();
};

const handleCurrentChange = () => {
  getList();
};

// 新增用户
const showAddDialog = () => {
  isEdit.value = false;
  dialogTitle.value = "新增用户";
  dialogVisible.value = true;
};

// 编辑用户
const showEditDialog = (row) => {
  isEdit.value = true;
  dialogTitle.value = "编辑用户";
  Object.assign(userForm, row);
  dialogVisible.value = true;
};

// 提交表单
const submitForm = async () => {
  const valid = await userFormRef.value.validate();
  if (!valid) return;

  try {
    if (isEdit.value) {
      await updateUser(userForm.id, userForm);
      ElMessage.success("更新成功");
    } else {
      await createUser(userForm);
      ElMessage.success("创建成功");
    }
    dialogVisible.value = false;
    getList();
  } catch (error) {
    ElMessage.error(error.message || "操作失败");
  }
};

// 重置表单
const resetForm = () => {
  userFormRef.value?.resetFields();
  Object.assign(userForm, {
    id: null,
    username: "",
    password: "",
    email: "",
    phone: "",
    score: 0,
    status: 1,
  });
};

// 状态切换
const handleStatusChange = async (row) => {
  try {
    await updateUser(row.id, { status: row.status });
    ElMessage.success("状态更新成功");
  } catch (error) {
    row.status = row.status === 1 ? 0 : 1;
    ElMessage.error("状态更新失败");
  }
};

// 删除用户
const handleDelete = async (row) => {
  try {
    await ElMessageBox.confirm(
      `确定要删除用户 ${row.username} 吗?`,
      "删除确认",
      {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      }
    );

    await deleteUser(row.id);
    ElMessage.success("删除成功");
    getList();
  } catch (error) {
    if (error !== "cancel") {
      ElMessage.error("删除失败");
    }
  }
};

// 导出CSV
const handleExport = async () => {
  try {
    const res = await exportUsersCsv();
    const blob = new Blob([res], { type: "text/csv;charset=utf-8;" });
    const link = document.createElement("a");
    link.href = URL.createObjectURL(blob);
    link.download = `用户数据_${new Date().getTime()}.csv`;
    link.click();
    URL.revokeObjectURL(link.href);
    ElMessage.success("导出成功");
  } catch (error) {
    ElMessage.error("导出失败");
  }
};

// 导入CSV
const handleImport = async (file) => {
  const formData = new FormData();
  formData.append("file", file);

  try {
    const res = await importUsersCsv(formData);
    const { successCount, errorCount, errors } = res.data;

    if (errorCount > 0) {
      ElMessage.warning(
        `导入完成:成功 ${successCount} 条,失败 ${errorCount} 条`
      );
      console.error("导入错误:", errors);
    } else {
      ElMessage.success(`导入成功:共 ${successCount} 条数据`);
    }

    getList();
  } catch (error) {
    ElMessage.error("导入失败");
  }

  return false; // 阻止默认上传行为
};

// 生命周期
onMounted(() => {
  getList();
});
</script>

<style scoped lang="scss">
.user-management {
  .search-card {
    margin-bottom: 20px;
  }

  .table-card {
    .el-table {
      margin-bottom: 20px;
    }
  }
}
</style>

4.9 //src/views/Dashboard.vue

java 复制代码
<!-- src/views/Dashboard.vue -->
<template>
  <div class="dashboard">
    <el-row :gutter="20">
      <!-- 统计卡片 -->
      <el-col :span="6" v-for="item in statsCards" :key="item.title">
        <el-card class="stats-card">
          <div class="stats-content">
            <div class="stats-icon" :style="{ backgroundColor: item.color }">
              <el-icon :size="30">
                <component :is="item.icon" />
              </el-icon>
            </div>
            <div class="stats-info">
              <div class="stats-value">{{ item.value }}</div>
              <div class="stats-title">{{ item.title }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>

    <el-row :gutter="20" style="margin-top: 20px">
      <!-- 用户分数分布 -->
      <el-col :span="12">
        <el-card>
          <template #header>
            <div class="card-header">
              <span>用户分数分布</span>
            </div>
          </template>
          <div class="chart-container">
            <el-progress
              v-for="item in scoreDistribution"
              :key="item.label"
              :text-inside="true"
              :stroke-width="26"
              :percentage="item.percentage"
              :color="item.color"
              style="margin-bottom: 20px"
            >
              <template #default="{ percentage }">
                <span
                  >{{ item.label }}:{{ item.count }}人 ({{
                    percentage
                  }}%)</span
                >
              </template>
            </el-progress>
          </div>
        </el-card>
      </el-col>

      <!-- 最近用户 -->
      <el-col :span="12">
        <el-card>
          <template #header>
            <div class="card-header">
              <span>最近注册用户</span>
            </div>
          </template>
          <el-table :data="recentUsers" height="300">
            <el-table-column prop="username" label="用户名" />
            <el-table-column prop="email" label="邮箱" />
            <el-table-column prop="createTime" label="注册时间">
              <template #default="{ row }">
                {{ formatDate(row.createTime) }}
              </template>
            </el-table-column>
          </el-table>
        </el-card>
      </el-col>
    </el-row>

    <!-- 欢迎信息 -->
    <el-card style="margin-top: 20px">
      <div class="welcome-section">
        <h2>欢迎回来,{{ user.username }}!</h2>
        <p>您的角色是:{{ role === "admin" ? "管理员" : "普通用户" }}</p>
        <p>上次登录时间:{{ new Date().toLocaleString() }}</p>
      </div>
    </el-card>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from "vue";
import { useStore } from "vuex";
import { getAllUsers } from "@/api/user";


const store = useStore();
const user = computed(() => store.getters.user);
const role = computed(() => store.getters.role);

// 统计卡片数据
const statsCards = ref([
  {
    title: "总用户数",
    value: 0,
    icon: "User",
    color: "#409eff",
  },
  {
    title: "活跃用户",
    value: 0,
    icon: "UserFilled",
    color: "#67c23a",
  },
  {
    title: "禁用用户",
    value: 0,
    icon: "Warning",
    color: "#e6a23c",
  },
  {
    title: "平均分数",
    value: 0,
    icon: "TrendCharts",
    color: "#f56c6c",
  },
]);

// 分数分布
const scoreDistribution = ref([
  { label: "优秀(90-100)", count: 0, percentage: 0, color: "#67c23a" },
  { label: "良好(70-89)", count: 0, percentage: 0, color: "#409eff" },
  { label: "及格(60-69)", count: 0, percentage: 0, color: "#e6a23c" },
  { label: "不及格(0-59)", count: 0, percentage: 0, color: "#f56c6c" },
]);

// 最近用户
const recentUsers = ref([]);

const formatDate = (dateStr) => {
  if (!dateStr) return "";
  return new Date(dateStr).toLocaleDateString();
};

// 加载数据
const loadDashboardData = async () => {
  try {
    const res = await getAllUsers();
    const users = res.data;

    // 计算统计数据
    statsCards.value[0].value = users.length;
    statsCards.value[1].value = users.filter((u) => u.status === 1).length;
    statsCards.value[2].value = users.filter((u) => u.status === 0).length;

    const avgScore =
      users.reduce((sum, u) => sum + (u.score || 0), 0) / users.length;
    statsCards.value[3].value = Math.round(avgScore);

    // 计算分数分布
    const distribution = [
      { range: [90, 100], index: 0 },
      { range: [70, 89], index: 1 },
      { range: [60, 69], index: 2 },
      { range: [0, 59], index: 3 },
    ];

    users.forEach((user) => {
      const score = user.score || 0;
      const item = distribution.find(
        (d) => score >= d.range[0] && score <= d.range[1]
      );
      if (item) {
        scoreDistribution.value[item.index].count++;
      }
    });

    // 计算百分比
    scoreDistribution.value.forEach((item) => {
      item.percentage = Math.round((item.count / users.length) * 100);
    });

    // 获取最近5个用户
    recentUsers.value = users
      .sort((a, b) => new Date(b.createTime) - new Date(a.createTime))
      .slice(0, 5);
  } catch (error) {
    console.error("加载仪表盘数据失败", error);
  }
};

onMounted(() => {
  loadDashboardData();
});
</script>

<style scoped lang="scss">
.dashboard {
  .stats-card {
    .stats-content {
      display: flex;
      align-items: center;

      .stats-icon {
        width: 60px;
        height: 60px;
        border-radius: 10px;
        display: flex;
        align-items: center;
        justify-content: center;
        color: white;
        margin-right: 20px;
      }

      .stats-info {
        flex: 1;

        .stats-value {
          font-size: 24px;
          font-weight: bold;
          color: #333;
        }

        .stats-title {
          color: #999;
          margin-top: 5px;
        }
      }
    }
  }

  .card-header {
    font-weight: bold;
  }

  .welcome-section {
    text-align: center;
    padding: 20px;

    h2 {
      color: #333;
      margin-bottom: 10px;
    }

    p {
      color: #666;
      margin: 5px 0;
    }
  }
}
</style>

4.10 //src/views/Profile.vue

java 复制代码
<!-- src/views/Profile.vue -->
<template>
  <div class="profile">
    <el-card>
      <template #header>
        <div class="card-header">
          <span>个人信息</span>
          <el-button type="primary" size="small" @click="isEdit = !isEdit">
            {{ isEdit ? "取消编辑" : "编辑信息" }}
          </el-button>
        </div>
      </template>

      <el-form
        ref="profileFormRef"
        :model="profileForm"
        :rules="rules"
        :disabled="!isEdit"
        label-width="100px"
        style="max-width: 600px"
      >
        <el-form-item label="用户ID">
          <el-input v-model="profileForm.id" disabled />
        </el-form-item>

        <el-form-item label="用户名">
          <el-input v-model="profileForm.username" disabled />
        </el-form-item>

        <el-form-item label="邮箱" prop="email">
          <el-input v-model="profileForm.email" />
        </el-form-item>

        <el-form-item label="手机号" prop="phone">
          <el-input v-model="profileForm.phone" />
        </el-form-item>

        <el-form-item label="分数">
          <el-tag :type="getScoreType(profileForm.score)" size="large">
            {{ profileForm.score }} 分
          </el-tag>
        </el-form-item>

        <el-form-item label="账号状态">
          <el-tag :type="profileForm.status === 1 ? 'success' : 'danger'">
            {{ profileForm.status === 1 ? "正常" : "已禁用" }}
          </el-tag>
        </el-form-item>

        <el-form-item label="注册时间">
          <el-input :value="formatDate(profileForm.createTime)" disabled />
        </el-form-item>

        <el-form-item label="更新时间">
          <el-input :value="formatDate(profileForm.updateTime)" disabled />
        </el-form-item>

        <el-form-item v-if="isEdit">
          <el-button type="primary" @click="saveProfile">保存修改</el-button>
          <el-button @click="cancelEdit">取消</el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <!-- 修改密码 -->
    <el-card style="margin-top: 20px">
      <template #header>
        <span>修改密码</span>
      </template>

      <el-form
        ref="passwordFormRef"
        :model="passwordForm"
        :rules="passwordRules"
        label-width="100px"
        style="max-width: 600px"
      >
        <el-form-item label="原密码" prop="oldPassword">
          <el-input
            v-model="passwordForm.oldPassword"
            type="password"
            show-password
          />
        </el-form-item>

        <el-form-item label="新密码" prop="newPassword">
          <el-input
            v-model="passwordForm.newPassword"
            type="password"
            show-password
          />
        </el-form-item>

        <el-form-item label="确认密码" prop="confirmPassword">
          <el-input
            v-model="passwordForm.confirmPassword"
            type="password"
            show-password
          />
        </el-form-item>

        <el-form-item>
          <el-button type="primary" @click="changePassword">修改密码</el-button>
          <el-button @click="resetPasswordForm">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from "vue";
import { useStore } from "vuex";
import { ElMessage } from "element-plus";
import { getUser, updateUser } from "@/api/user";


const store = useStore();
const currentUser = store.getters.user;

const isEdit = ref(false);
const profileFormRef = ref();
const passwordFormRef = ref();

// 个人信息表单
const profileForm = reactive({
  id: "",
  username: "",
  email: "",
  phone: "",
  score: 0,
  status: 1,
  createTime: "",
  updateTime: "",
});

// 修改密码表单
const passwordForm = reactive({
  oldPassword: "",
  newPassword: "",
  confirmPassword: "",
});

// 验证规则
const rules = {
  email: [
    { required: true, message: "请输入邮箱", trigger: "blur" },
    { type: "email", message: "请输入正确的邮箱地址", trigger: "blur" },
  ],
  phone: [
    {
      pattern: /^1[3-9]\d{9}$/,
      message: "请输入正确的手机号",
      trigger: "blur",
    },
  ],
};

const passwordRules = {
  oldPassword: [{ required: true, message: "请输入原密码", trigger: "blur" }],
  newPassword: [
    { required: true, message: "请输入新密码", trigger: "blur" },
    { min: 6, max: 20, message: "密码长度在 6 到 20 个字符", trigger: "blur" },
  ],
  confirmPassword: [
    { required: true, message: "请再次输入新密码", trigger: "blur" },
    {
      validator: (rule, value, callback) => {
        if (value !== passwordForm.newPassword) {
          callback(new Error("两次输入密码不一致"));
        } else {
          callback();
        }
      },
      trigger: "blur",
    },
  ],
};

// 获取分数类型
const getScoreType = (score) => {
  if (score >= 90) return "success";
  if (score >= 60) return "warning";
  return "danger";
};

// 格式化日期
const formatDate = (dateStr) => {
  if (!dateStr) return "";
  return new Date(dateStr).toLocaleString("zh-CN");
};

// 加载用户信息
const loadUserInfo = async () => {
  try {
    const res = await getUser(currentUser.id);
    Object.assign(profileForm, res.data);
  } catch (error) {
    ElMessage.error("获取用户信息失败");
  }
};

// 保存个人信息
const saveProfile = async () => {
  const valid = await profileFormRef.value.validate();
  if (!valid) return;

  try {
    await updateUser(profileForm.id, {
      email: profileForm.email,
      phone: profileForm.phone,
    });

    // 更新store中的用户信息
    store.commit("SET_USER", { ...currentUser, ...profileForm });

    ElMessage.success("保存成功");
    isEdit.value = false;
  } catch (error) {
    ElMessage.error("保存失败");
  }
};

// 取消编辑
const cancelEdit = () => {
  isEdit.value = false;
  loadUserInfo();
};

// 修改密码
const changePassword = async () => {
  const valid = await passwordFormRef.value.validate();
  if (!valid) return;

  try {
    // 实际项目中应该调用修改密码的API
    ElMessage.success("密码修改成功,请重新登录");
    // 可以在这里退出登录
  } catch (error) {
    ElMessage.error("密码修改失败");
  }
};

// 重置密码表单
const resetPasswordForm = () => {
  passwordFormRef.value?.resetFields();
};

onMounted(() => {
  loadUserInfo();
});
</script>

<style scoped lang="scss">
.profile {
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
}
</style>

4.11 //src/App.vue

java 复制代码
<!-- src/App.vue -->
<template>
  <router-view />
</template>

<script setup>
// Vue 3 组合式API,这里不需要额外的逻辑
</script>

<style>
/* 全局样式 */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html,
body,
#app {
  height: 100%;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
    "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Element Plus 样式覆盖 */
.el-button {
  font-weight: 400;
}

.el-message {
  min-width: 300px;
}

/* 滚动条样式 */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

::-webkit-scrollbar-track {
  background: #f1f1f1;
}

::-webkit-scrollbar-thumb {
  background: #888;
  border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
  background: #555;
}
</style>

4.12 //vue.config.js

java 复制代码
// vue.config.js
const { defineConfig } = require("@vue/cli-service");

module.exports = defineConfig({
  transpileDependencies: true,

  // 开发服务器配置
  devServer: {
    port: 8081, // 前端端口
    open: true, // 自动打开浏览器
    proxy: {
      // 代理配置,解决开发环境跨域问题
      "/api": {
        target: "http://localhost:8080", // 后端地址
        changeOrigin: true,
        pathRewrite: {
          "^/api": "/api",
        },
      },
    },
  },

  // 生产环境配置
  productionSourceMap: false, // 生产环境不生成source map

  // CSS相关配置
  css: {
    loaderOptions: {
      scss: {
        // 全局引入scss变量文件(如果有的话)
        // additionalData: `@import "@/styles/variables.scss";`
      },
    },
  },
});

4.13 //.eslintrc.js

java 复制代码
module.exports = {
  root: true,
  env: {
    node: true
  },
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended'
  ],
  parserOptions: {
    parser: '@babel/eslint-parser'
  },
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'vue/multi-word-component-names': 'off'
  }
}

4.14 //babel.config.js

java 复制代码
module.exports = {
  presets: ["@vue/cli-plugin-babel/preset"],
};

4.15 jsconfig.json

java 复制代码
{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "baseUrl": "./",
    "moduleResolution": "node",
    "paths": {
      "@/*": [
        "src/*"
      ]
    },
    "lib": [
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  }
}

4.16 package.json

java 复制代码
{
  "name": "vue-user-management",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "@element-plus/icons-vue": "^2.3.1",
    "axios": "^1.10.0",
    "core-js": "^3.8.3",
    "element-plus": "^2.10.4",
    "vue": "^3.2.13",
    "vue-router": "^4.0.3",
    "vuex": "^4.0.0"
  },
  "devDependencies": {
    "@babel/core": "^7.12.16",
    "@babel/eslint-parser": "^7.12.16",
    "@vue/cli-plugin-babel": "~5.0.0",
    "@vue/cli-plugin-eslint": "~5.0.0",
    "@vue/cli-plugin-router": "~5.0.0",
    "@vue/cli-plugin-vuex": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "eslint": "^7.32.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "eslint-plugin-vue": "^8.0.3",
    "prettier": "^2.4.1",
    "sass": "^1.89.2",
    "sass-loader": "^16.0.5"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/vue3-essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "@babel/eslint-parser"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead",
    "not ie 11"
  ]
}

二、前端运行

2.1 项目运行说明

复制代码
# 进入项目目录
cd vue-user-management

# 安装依赖
npm install

# 运行开发服务器
npm run serve

访问 http://localhost:8081

2.2 功能说明

2.2.1 登录功能

  • 管理员账号:admin / 123456
  • 普通用户:zhangsan / 123456
  • 登录后根据角色显示不同菜单

2.2.2 权限管理

  • 管理员可以访问用户管理页面
  • 普通用户只能查看个人信息和仪表盘
  • 路由守卫自动验证权限

2.2.3 用户管理功能

  • 查询:支持用户名搜索和分页
  • 新增:填写表单创建新用户
  • 编辑:修改用户信息(用户名不可修改)
  • 删除:删除确认后删除用户
  • 状态切换:启用/禁用用户

2.2.4 CSV导入导出

  • 导出:点击"导出CSV"按钮下载当前所有用户数据
  • 导入:上传CSV文件批量导入/更新用户数据
  • CSV格式:ID,用户名,密码,邮箱,手机号,状态,分数,创建时间,更新时间

2.2.5 仪表盘

  • 显示用户统计信息
  • 分数分布图表
  • 最近注册用户列表

2.2.6 个人信息

  • 查看和编辑个人资料
  • 修改密码功能(示例)

2.2.7 . 项目特点

  • 响应式设计:适配不同屏幕尺寸
  • 统一错误处理:axios拦截器统一处理错误
  • 状态管理:Vuex管理用户登录状态
  • 路由守卫:自动验证登录和权限
  • UI美化:使用Element Plus组件库
  • 数据持久化:localStorage保存登录信息
相关推荐
崔庆才丨静觅11 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606112 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了12 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅12 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅12 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅13 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment13 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅13 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊13 小时前
jwt介绍
前端
爱敲代码的小鱼13 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax