项目包含用户权限管理、表单提交、数据上传下载、仪表盘可视化等功能,非常适合新手从零开始学习Vue3前端开发,下述包含所有开发源代码,敬请食用
废话少说先上图:
登陆界面:
仪表盘管理系统:
用户管理:
对于普通用户只有仪表盘和个人信息界面:
一、创建Vue3前端项目
1.1 创建项目
# 安装Vue CLI(如果没有的话)
npm install -g @vue/cli
# 创建项目
vue create vue-user-management
选择配置
- Vue 3
- Router
- Vuex
- CSS Pre-processors (Sass/SCSS)
- Linter / Formatter
1.2 安装额外依赖
cd vue-user-management
# 安装UI框架和其他依赖
npm install element-plus axios @element-plus/icons-vue
npm install --save-dev sass sass-loader
1.3 项目结构
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
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
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) => {// 如果是blob类型的响应(文件下载),直接返回if (response.config.responseType === 'blob') {return response.data;}// 如果响应的Content-Type是text/csv,也直接返回const contentType = response.headers['content-type'];if (contentType && contentType.includes('text/csv')) {return response.data;}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
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
// 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
// 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
<!-- src/views/Login.vue -->
<template><div class="login-container"><el-card class="login-card"><h2 class="login-title">用户管理系统</h2><el-formref="loginFormRef":model="loginForm":rules="rules"label-width="0px"><el-form-item prop="username"><el-inputv-model="loginForm.username"prefix-icon="User"placeholder="请输入用户名"size="large"/></el-form-item><el-form-item prop="password"><el-inputv-model="loginForm.password"type="password"prefix-icon="Lock"placeholder="请输入密码"size="large"show-password@keyup.enter="handleLogin"/></el-form-item><el-form-item><el-buttontype="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
<!-- 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
<!-- src/views/UserManagement.vue -->
<template><div class="user-management"><!-- 搜索和操作栏 --><el-card class="search-card"><el-row :gutter="20"><el-col :span="6"><el-inputv-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"stripestyle="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-switchv-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-paginationv-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-dialogv-model="dialogVisible":title="dialogTitle"width="500px"@close="resetForm"><el-formref="userFormRef":model="userForm":rules="rules"label-width="80px"><el-form-item label="用户名" prop="username"><el-inputv-model="userForm.username"placeholder="请输入用户名":disabled="isEdit"/></el-form-item><el-form-item label="密码" prop="password" v-if="!isEdit"><el-inputv-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-numberv-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 :value="1">启用</el-radio><el-radio :value="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 url = window.URL.createObjectURL(new Blob([res], { type: "text/csv;charset=utf-8;" }));const link = document.createElement("a");link.href = url;link.download = `用户数据_${new Date().getTime()}.csv`;document.body.appendChild(link);link.click();// 清理setTimeout(() => {document.body.removeChild(link);window.URL.revokeObjectURL(url);}, 100);ElMessage.success("导出成功");} catch (error) {console.error("导出错误:", error);ElMessage.error("导出失败: " + (error.message || "未知错误"));}
};// 导入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
<!-- 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-progressv-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
<!-- 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-formref="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-formref="passwordFormRef":model="passwordForm":rules="passwordRules"label-width="100px"style="max-width: 600px"><el-form-item label="原密码" prop="oldPassword"><el-inputv-model="passwordForm.oldPassword"type="password"show-password/></el-form-item><el-form-item label="新密码" prop="newPassword"><el-inputv-model="passwordForm.newPassword"type="password"show-password/></el-form-item><el-form-item label="确认密码" prop="confirmPassword"><el-inputv-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 {// 实际项目中应该调用修改密码的APIElMessage.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
<!-- 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
// 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
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
module.exports = {presets: ["@vue/cli-plugin-babel/preset"],
};
4.15 jsconfig.json
{"compilerOptions": {"target": "es5","module": "esnext","baseUrl": "./","moduleResolution": "node","paths": {"@/*": ["src/*"]},"lib": ["esnext","dom","dom.iterable","scripthost"]}
}
4.16 package.json
{"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保存登录信息