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保存登录信息
相关推荐
轻语呢喃几秒前
Babel :现代前端开发的语法转换核心
javascript·react.js
CodeTransfer1 分钟前
今天给大家搬运的是四角线框hover效果
前端·vue.js
归于尽3 分钟前
别让类名打架!CSS 模块化教你给样式上 "保险"
前端·css·react.js
凤凰AI30 分钟前
Python知识点4-嵌套循环&break和continue使用&死循环
开发语言·前端·python
Lazy_zheng43 分钟前
虚拟 DOM 到底是啥?为什么 React 要用它?
前端·javascript·react.js
多啦C梦a1 小时前
前端按钮大撞衫,CSS 模块化闪亮登场!
前端·javascript·面试
拾光拾趣录1 小时前
WebRTC深度解析:从原理到实战
前端·webrtc
TreeNewBeeMVP1 小时前
Vue 3 核心原理剖析:响应式、编译与运行时优化
前端
哒哒哒5285201 小时前
vue3基础知识
前端
FogLetter1 小时前
受控组件 vs 非受控组件:React表单的双面哲学
前端·react.js