项目包含用户权限管理、表单提交、数据上传下载、仪表盘可视化等功能,非常适合新手从零开始学习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保存登录信息