分享一套锋哥原创的SpringBoot4+Vue3通讯录管理系统
大家好,我是Java1234_小锋老师,分享一套锋哥原创的SpringBoot4+Vue3通讯录管理系统
项目介绍
随着移动互联网与信息化办公的快速发展,个人与组织对联系人信息的高效管理需求日益增长。传统的纸质通讯录或分散的电子表格方式存在检索困难、数据易丢失、难以多人协同和扩展性差等问题。因此,设计并实现一套基于B/S架构的通讯录管理系统具有较强的实际应用价值。本文以Spring Boot作为后端核心框架,结合MyBatis-Plus完成持久层访问,前端采用Vue3与Element Plus构建交互界面,数据库选用MySQL,认证机制采用JWT,构建了一套功能完整的通讯录管理系统。系统实现了用户注册与登录、联系人增删改查、分组管理、收藏联系人、Excel导入导出、个人中心(头像上传、资料修改、密码修改)、管理员用户管理、首页数据统计图表以及操作日志查询等功能。在数据库设计方面,围绕用户、联系人、联系人分组和操作日志四类核心实体完成概念结构设计与逻辑结构设计,并给出详细的数据表字段说明。在系统实现方面,重点阐述了Spring Boot分层架构、MyBatis-Plus条件构造与分页、Vue3组合式API及前后端分离交互流程。测试结果表明,系统功能运行稳定,界面交互清晰,能够满足日常通讯录管理的基本业务需求。本文的研究与实现过程可为同类中小型信息管理系统的开发提供参考。
源码下载
链接: https://pan.baidu.com/s/1ylAImSIMkYUc47iTmF2nbA?pwd=1234
提取码: 1234
系统展示
![]()
![]()
![]()
![]()
![]()
![]()
![]()
核心代码
package com.java1234.addressbook.controller; import com.java1234.addressbook.common.Result; import com.java1234.addressbook.common.UserContext; import com.java1234.addressbook.dto.PasswordDTO; import com.java1234.addressbook.entity.User; import com.java1234.addressbook.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; /** * 个人中心控制器 * * @author addressbook */ @RestController @RequestMapping("/api/profile") @RequiredArgsConstructor public class ProfileController { private final UserService userService; /** * 获取当前用户信息 */ @GetMapping public Result<User> getProfile() { User user = userService.getById(UserContext.getUserId()); if (user != null) { user.setPassword(null); } return Result.success(user); } /** * 更新个人资料 */ @PutMapping public Result<Void> updateProfile(@RequestBody User user) { user.setId(UserContext.getUserId()); userService.updateProfile(user); return Result.success("资料更新成功", null); } /** * 修改密码 */ @PutMapping("/password") public Result<Void> changePassword(@RequestBody PasswordDTO dto) { userService.changePassword(UserContext.getUserId(), dto.getOldPassword(), dto.getNewPassword()); return Result.success("密码修改成功", null); } }<template> <div class="page-container"> <!-- 统计卡片 --> <el-row :gutter="16" class="stat-row"> <el-col :span="6"> <div class="stat-card blue"> <div class="stat-label">联系人总数</div> <div class="stat-value">{{ stats.contactCount || 0 }}</div> <el-icon :size="40" style="opacity:0.3;position:absolute;right:20px;top:20px"><User /></el-icon> </div> </el-col> <el-col :span="6"> <div class="stat-card green"> <div class="stat-label">分组数量</div> <div class="stat-value">{{ stats.groupCount || 0 }}</div> <el-icon :size="40" style="opacity:0.3;position:absolute;right:20px;top:20px"><FolderOpened /></el-icon> </div> </el-col> <el-col :span="6"> <div class="stat-card orange"> <div class="stat-label">收藏联系人</div> <div class="stat-value">{{ stats.starredCount || 0 }}</div> <el-icon :size="40" style="opacity:0.3;position:absolute;right:20px;top:20px"><Star /></el-icon> </div> </el-col> <el-col :span="6"> <div class="stat-card purple"> <div class="stat-label">{{ userStore.isAdmin ? '用户总数' : '我的联系人' }}</div> <div class="stat-value">{{ userStore.isAdmin ? (stats.userCount || 0) : (stats.contactCount || 0) }}</div> <el-icon :size="40" style="opacity:0.3;position:absolute;right:20px;top:20px"><Avatar /></el-icon> </div> </el-col> </el-row> <!-- 图表区域 --> <el-row :gutter="16" style="margin-top:16px"> <el-col :span="12"> <div class="chart-card"> <div class="chart-title">联系人分组分布</div> <div ref="pieChartRef" style="height:320px"></div> </div> </el-col> <el-col :span="12"> <div class="chart-card"> <div class="chart-title">近6个月新增趋势</div> <div ref="barChartRef" style="height:320px"></div> </div> </el-col> </el-row> <!-- 生日提醒 --> <div class="chart-card" style="margin-top:16px"> <div class="chart-title">近期生日提醒 (30天内)</div> <el-table :data="stats.birthdays || []" stripe style="width:100%"> <el-table-column prop="name" label="姓名" min-width="120" /> <el-table-column prop="mobile" label="手机号" min-width="140" /> <el-table-column prop="groupName" label="分组" min-width="100" /> <el-table-column label="生日" min-width="120"> <template #default="{ row }"> {{ formatDate(row.birthday) }} </template> </el-table-column> <el-table-column prop="company" label="公司" min-width="160" show-overflow-tooltip /> </el-table> <el-empty v-if="!stats.birthdays?.length" description="近期暂无生日提醒" /> </div> </div> </template> <script setup> import { ref, onMounted, onUnmounted } from 'vue' import * as echarts from 'echarts' import dayjs from 'dayjs' import { getDashboard } from '@/api' import { useUserStore } from '@/store/user' const userStore = useUserStore() const stats = ref({}) const pieChartRef = ref() const barChartRef = ref() let pieChart = null let barChart = null /** 格式化日期 */ function formatDate(date) { return date ? dayjs(date).format('YYYY-MM-DD') : '' } /** 加载统计数据 */ async function loadData() { const res = await getDashboard() stats.value = res.data renderPieChart(res.data.groupDistribution || []) renderBarChart(res.data.monthlyTrend || []) } /** 渲染饼图 */ function renderPieChart(data) { if (!pieChartRef.value) return pieChart = echarts.init(pieChartRef.value) pieChart.setOption({ tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }, legend: { bottom: 0, type: 'scroll' }, color: ['#667eea', '#11998e', '#f5576c', '#4facfe', '#f093fb', '#38ef7d'], series: [{ type: 'pie', radius: ['40%', '70%'], center: ['50%', '45%'], itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 }, label: { show: true, formatter: '{b}\n{c}人' }, data: data.length ? data : [{ name: '暂无数据', value: 0 }] }] }) } /** 渲染柱状图 */ function renderBarChart(data) { if (!barChartRef.value) return barChart = echarts.init(barChartRef.value) barChart.setOption({ tooltip: { trigger: 'axis' }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', data: data.map(d => d.month), axisLabel: { rotate: 30 } }, yAxis: { type: 'value', minInterval: 1 }, series: [{ type: 'bar', data: data.map(d => d.count), barWidth: '40%', itemStyle: { borderRadius: [6, 6, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#667eea' }, { offset: 1, color: '#764ba2' } ]) } }] }) } /** 窗口大小变化时重绘图表 */ function handleResize() { pieChart?.resize() barChart?.resize() } onMounted(() => { loadData() window.addEventListener('resize', handleResize) }) onUnmounted(() => { window.removeEventListener('resize', handleResize) pieChart?.dispose() barChart?.dispose() }) </script> <style scoped> .stat-row .stat-card { position: relative; overflow: hidden; } </style>