SpringBoot校园失物招领信息平台
文章目录
1、技术栈
本项目采用前后端分离的架构,前端和后端分别使用了不同的技术栈,以实现高效的开发和良好的用户体验。
前端:
- Vue: 作为核心的渐进式 JavaScript 框架,Vue 提供了强大的组件化开发能力,使得前端界面的构建更加灵活和高效。通过 Vue 的响应式数据绑定和组件系统,可以轻松构建复杂的用户界面,并实现数据的实时更新。
- axios: 一个基于 Promise 的 HTTP 客户端,用于前端与后端进行数据交互。axios 提供了简洁易用的 API,支持请求拦截、响应拦截、请求取消等功能,方便地处理前后端通信。
- Element UI: 一套基于 Vue 2.0 的桌面端组件库。Element UI 提供了丰富的 UI 组件,如表格、表单、按钮、弹窗等,可以快速构建美观且功能完善的用户界面,提升开发效率。
- Apache ECharts: 一个强大、灵活且易用的数据可视化库。在本项目中,ECharts 用于生成各种图表,例如平台物品数量统计、失物招领物品数量占比等,以直观地展示平台数据,帮助管理员更好地了解平台运营情况。
后端:
- Springboot: 基于 Spring 框架的快速开发框架。Springboot 简化了 Spring 应用的搭建和开发过程,提供了自动配置、内嵌式服务器等功能,使得后端开发更加便捷高效。
- MySQL: 一款流行的关系型数据库管理系统。MySQL 用于存储平台的所有数据,包括用户信息、失物信息、招领信息、公告信息等。其稳定性和可靠性为平台的正常运行提供了保障。
- Mybatis: 一款优秀的持久层框架。Mybatis 通过 XML 或注解的方式配置 SQL 语句,将 Java 对象与数据库记录进行映射,简化了数据库操作,提高了开发效率。
- 阿里云 OSS: 阿里云对象存储服务。OSS 用于存储用户上传的失物和招领物品的图片,提供了高可用、高可靠、安全的数据存储服务,确保图片的稳定存储和访问。
2、项目说明
失物招领管理系统采用前后端分离的设计思想,清晰地划分了管理员和用户两种角色,为不同用户提供定制化的功能和服务。
- 角色划分:
- 管理员: 拥有平台的最高权限,负责维护平台信息,包括发布和管理公告、查看和管理失物信息、查看和管理招领信息、管理用户等。管理员通过后台管理界面对平台进行全面的管理和监控。
- 用户: 普通用户可以在平台上发布失物信息和招领信息,查找自己丢失的物品或捡到的物品。用户可以浏览公告、查看失物广场和招领广场的信息,并与物品发布者进行联系。
2.1、登录注册
系统提供了完善的登录注册功能,确保用户身份的合法性。
- 登录: 用户通过输入用户名、密码和选择角色(管理员或用户)进行登录。登录成功后,系统会根据用户的角色跳转到相应的界面。登录界面设计简洁明了,背景图采用了充满科技感的火箭发射场景,寓意着平台的快速发展和便捷服务。
- 注册: 新用户可以通过注册功能创建自己的账号。注册时需要填写用户名、密码等信息。注册成功后,用户即可使用新创建的账号登录平台。注册功能为用户提供了便捷的平台入口。
登录

注册

2.2、管理员端截图
管理员端提供了丰富的功能模块,方便管理员对平台进行全面的管理。
- 系统首页: 系统首页提供了平台数据的概览,通过 ECharts 图表直观展示了平台所有物品数量的统计、平台招领物品数量的占比、失物广告物品数量的占比等数据,帮助管理员快速了解平台的运营状况。
- 信息管理:
- 公告信息: 管理员可以在此模块发布、编辑和删除平台公告。公告信息会显示在用户端的主页,方便用户及时了解平台的重要通知和活动。
- 失物信息: 管理员可以查看所有用户发布的失物信息,包括物品名称、描述、图片、状态(丢失中、已找回)、发布时间等。管理员可以对失物信息进行编辑或删除操作。
- 招领信息: 管理员可以查看所有用户发布的招领信息,包括物品名称、描述、图片、状态(未找到失主、已找到失主)、发布时间等。管理员可以对招领信息进行编辑或删除操作。
- 平台建议: 用户可以通过反馈建议模块向平台提交建议,管理员可以在此查看和处理用户的建议。
- 用户管理: 管理员可以查看所有注册用户的信息,包括用户名、密码、头像、身份(普通用户)、注册时间等。管理员可以对用户信息进行编辑或删除操作。
系统首页

公告信息

失物信息

用户管理

2.3、用户端截图
用户端提供了便捷的功能,方便用户发布和查找失物招领信息。
- 主页: 用户登录后的主页展示了最新的平台公告和最新发布的失物信息,方便用户快速获取重要信息和关注最新动态。主页设计简洁友好,提供了失物广场、招领广场和反馈建议的入口。
- 失物广场: 失物广场展示了所有用户发布的失物信息,用户可以通过搜索功能查找特定的失物。每条失物信息都包含物品名称、图片、描述、发布时间等信息。用户可以点击"查看详情"查看更详细的信息,或点击"联系失主"与物品发布者取得联系。
- 招领广场: 招领广场展示了所有用户发布的招领信息,用户可以通过搜索功能查找特定的招领物品。每条招领信息都包含物品名称、图片、描述、发布时间等信息。用户可以点击"查看详情"查看更详细的信息,或点击"联系ta"与物品发布者取得联系。
- 反馈建议: 用户可以在此模块向平台提交反馈和建议,帮助平台改进服务。
- 个人中心: 用户可以在个人中心查看和修改自己的信息,包括用户名、密码、头像等。
主页

失物广场



招领广场

反馈建议

个人中心

3、核心代码实现
3.1、前端首页
创建父组件
vue
<template>
<div class="common-layout">
<el-container>
<el-header class="header">
<div class="header-left">
<img src="@/assets/lostLogo.png" alt="logo" class="logo" width="40px" height="40px">
<span class="logo-text">失物招领平台</span>
</div>
<div class="nav-links">
<router-link to="/home">
<el-button type="button" class="nav-button">首页</el-button>
</router-link>
<router-link to="/LostPropertySquare">
<el-button class="nav-button">失物广场</el-button>
</router-link>
<router-link to="/FoundPropertySquare">
<el-button class="nav-button" >招领广场</el-button>
</router-link>
<router-link to="/feedback">
<el-button class="nav-button" >反馈建议</el-button>
</router-link>
</div>
<div>
<AuthButtons v-if="token === null" />
<ExitsLogin v-else />
</div>
</el-header>
<el-main>
<div class="centered-content" style="width: 80%; height: 100%; margin: 0 auto;">
<router-view></router-view>
</div>
</el-main>
</el-container>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import AuthButtons from '@/views/AuthButtons.vue'
import ExitsLogin from '@/views/ExitsLogin.vue'
const router = useRouter()
const token = ref<string | null>(null)
onMounted(() => {
token.value = localStorage.getItem('token')
router.push('/home')
console.log('token:',token.value)
})
</script>
<style scoped>
.header {
background-color: #f8cf47;
color: white;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
}
.header-left {
display: flex;
align-items: center;
}
.logo {
margin-right: 10px;
}
.nav-links {
display: flex;
}
.nav-button {
font-size: 18px;
margin: 0 10px;
background-color: transparent;
color: #ffffff;
border: none;
right: 250px;
position: relative;
transition: color 0.3s;
}
.nav-button:hover {
background-color: transparent;
color: white;
font-weight: bold;
}
.nav-button::after {
content: '';
display: block;
width: 100%;
height: 2px;
background-color: white;
position: absolute;
bottom: -5px;
left: 0;
transform: scaleX(0);
transition: transform 0.3s;
}
.nav-button:hover::after {
transform: scaleX(1);
}
.header-right {
display: flex;
align-items: center;
}
.header-right el-button {
background-color: transparent;
color: #f0f0f0;
border: none;
}
.header-right el-button:hover {
background-color: #f0f0f0;
}
.logo-text {
color: #fdfdfd;
font-size: 20px;
font-weight: bold;
margin-left: 5px;
/* 垂直居中 */
display: flex;
justify-content: center;
}
.centered-content {
text-align: center;
background-color: transparent;
border-radius: 8px;
}
.info-container {
display: flex;
justify-content: space-between;
width: 80%;
margin: 20px auto;
gap: 10px;
min-height: 300px;
}
.announcement-board {
width: 48%;
margin: 0;
}
.announcement-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
height: 100%;
}
.announcement-header {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: bold;
color: #333;
padding: 10px 15px;
}
.announcement-icon {
font-size: 20px;
color: #f8cf47;
}
.announcement-content {
padding: 15px 0;
min-height: 250px;
}
.announcement-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 15px;
border-bottom: 1px solid #eee;
font-size: 15px;
text-align: left;
}
.announcement-item:last-child {
border-bottom: none;
}
.announcement-date {
margin-left: auto;
color: #999;
font-size: 12px;
}
.latest-lost {
width: 48%;
}
.latest-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
height: 100%;
}
.latest-header {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: bold;
color: #333;
padding: 10px 15px;
}
.latest-icon {
font-size: 20px;
color: #f8cf47;
}
.latest-content {
padding: 15px 0;
min-height: 250px;
}
.latest-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 15px;
border-bottom: 1px solid #eee;
}
.latest-item:last-child {
border-bottom: none;
}
.item-image {
width: 50px;
height: 50px;
border-radius: 4px;
}
.item-info {
flex: 1;
}
.item-title {
font-size: 14px;
color: #333;
margin-bottom: 4px;
}
.item-time {
font-size: 12px;
color: #999;
}
</style>
创建子组件
vue
<template>
<div>
<div class="block text-center">
<el-carousel height="350px" width="100%">
<el-carousel-item v-for="(image, index) in images" :key="index">
<img :src="image" alt="carousel image" class="carousel-image" />
</el-carousel-item>
</el-carousel>
</div>
<div class="info-container">
<div class="announcement-board">
<el-card class="announcement-card">
<template #header>
<div class="announcement-header">
<el-icon class="announcement-icon"><Bell /></el-icon>
<span>公告栏</span>
</div>
</template>
<div class="announcement-content">
<el-scrollbar height="200px">
<div v-for="(item, index) in announcements" :key="index" class="announcement-item">
<el-icon><InfoFilled /></el-icon>
<span class="clickable" @click="showAnnouncementDetail(item)">{{ item.title }}</span>
<span class="announcement-date">{{ formatDateTime(item.createTime) }}</span>
</div>
</el-scrollbar>
</div>
</el-card>
</div>
<div class="latest-lost">
<el-card class="latest-card">
<template #header>
<div class="latest-header">
<el-icon class="latest-icon"><Timer /></el-icon>
<span>最新发布</span>
</div>
</template>
<div class="latest-content">
<el-scrollbar height="200px">
<div v-for="(item, index) in latestItems" :key="index" class="latest-item">
<el-image :src="item.image" fit="cover" class="item-image"></el-image>
<div class="item-info">
<div class="item-title">{{ item.name }}</div>
<div class="item-time">{{ formatDateTime(item.date) }}</div>
<div class="item-description">{{ item.description }}</div>
</div>
</div>
</el-scrollbar>
</div>
</el-card>
</div>
</div>
<!-- Add dialog component -->
<el-dialog
v-model="dialogVisible"
:title="selectedAnnouncement.title"
width="30%"
>
<span>{{ selectedAnnouncement.content }}</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { getBanner, getAnnouncement,getNewLost } from '@/api/api';
import { Bell, InfoFilled, Timer } from '@element-plus/icons-vue'
const images = ref<string[]>([]);
// Add announcement and latest items data
const announcements = ref([
{id: 1,title: '欢迎使用失物招领平台!', content: '欢迎使用失物招领平台!', createTime: '2024-03-20',userId: 1 },
{id: 2,title: '请文明发布信息,共建和谐社区。', content: '请文明发布信息,共建和谐社区。', createTime: '2024-03-19',userId: 2 },
{id: 3,title: '新功能上线:现在可以更方便地搜索失物了!', content: '新功能上线:现在可以更方便地搜索失物了!', createTime: '2024-03-18',userId: 3 },
]);
const latestItems = ref([
{
name: '蓝色钱包',
date: [1,2,20, 15, 30],
image: 'path/to/image1.jpg',
description: '蓝色钱包,内有身份证、银行卡等重要物品。'
},
{
name: '学生证',
date: [1,2,20, 15, 30],
image: 'path/to/image2.jpg',
description: '学生证,内有学生姓名、学号、照片等信息。'
},
]);
const formatDateTime = (timeArray: number[]) => {
if (!Array.isArray(timeArray)) return '';
const [year, month, day, hour, minute, second] = timeArray;
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')} ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
}
// Add new refs for dialog
const dialogVisible = ref(false);
const selectedAnnouncement = ref({
title: '',
content: ''
});
// Add click handler
const showAnnouncementDetail = (item: any) => {
selectedAnnouncement.value = item;
dialogVisible.value = true;
};
onMounted(async () => {
let res = await getBanner();
let res2 = await getAnnouncement();
let res3 = await getNewLost();
images.value = res.data.data;
announcements.value = res2.data.data;
latestItems.value = res3.data.data;
});
</script>
<style scoped>
.demonstration {
color: var(--el-text-color-secondary);
}
.el-carousel__item h3 {
color: #475669;
opacity: 0.75;
line-height: 150px;
margin: 0;
text-align: center;
}
.el-carousel__item:nth-child(2n) {
background-color: #ffffff;
}
.el-carousel__item:nth-child(2n + 1) {
background-color: #d3dce6;
}
.carousel-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
margin: 0 auto;
}
.info-container {
display: flex;
justify-content: space-between;
width: 100%;
margin: 20px auto;
gap: 20px;
}
.announcement-board {
flex: 1;
min-height: 300px;
margin-right: auto;
max-width: 49%;
}
.latest-lost {
flex: 1;
min-height: 300px;
margin-left: auto;
max-width: 49%;
}
.announcement-header, .latest-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: bold;
padding: 20px;
}
.announcement-icon, .latest-icon {
font-size: 24px;
}
.announcement-item {
display: flex;
align-items: center;
gap: 8px;
padding: 20px 0;
border-bottom: 1px solid #eee;
font-size: 16px;
}
.announcement-item:last-child {
border-bottom: none;
}
.announcement-date {
margin-left: auto;
color: #999;
font-size: 14px;
}
.latest-item {
display: flex;
align-items: center;
gap: 12px;
padding: 20px 0;
border-bottom: 1px solid #eee;
text-align: left;
font-size: 16px;
}
.latest-item:last-child {
border-bottom: none;
}
.item-image {
width: 80px;
height: 80px;
border-radius: 4px;
}
.item-info {
flex: 1;
text-align: left;
}
.item-title {
font-size: 16px;
margin-bottom: 4px;
}
.item-time {
font-size: 14px;
color: #999;
}
.announcement-content {
padding: 10px 20px;
text-align: left;
}
.latest-content {
padding: 10px 20px;
text-align: left;
}
.latest-header {
justify-content: flex-start;
}
/* 添加失败状态样式 */
.failed {
color: #F56C6C;
font-size: 12px;
}
/* 增加滚动区域高度 */
:deep(.el-scrollbar) {
height: 250px !important;
}
/* 增加卡片内容区域的内边距 */
.announcement-content, .latest-content {
padding: 10px 20px;
}
/* 增加标题区域的内边距 */
.announcement-header, .latest-header {
padding: 20px;
font-size: 20px;
}
/* 调整内容项的间距 */
.announcement-item, .latest-item {
padding: 20px 0;
font-size: 16px;
}
/* 调整图片大小 */
.item-image {
width: 80px;
height: 80px;
}
/* 调整文字大小 */
.item-title {
font-size: 16px;
}
.item-time, .announcement-date {
font-size: 14px;
}
.clickable {
cursor: pointer;
&:hover {
color: var(--el-color-primary);
}
}
</style>
3.2、前端招领广场
vue
<template>
<div style="text-align: left; margin-bottom: 10px;">
<el-button type="success" plain @click="handleAdd">新增失物</el-button>
</div>
<el-table :data="paginatedData" :default-sort="{ prop: 'date', order: 'descending' }" style="width: 100%">
<el-table-column label="序号" width="80">
<template #default="scope">
{{ (currentPage - 1) * pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="name" label="物件名称" />
<el-table-column prop="description" label="物件描述" />
<el-table-column prop="image" label="图片">
<template #default="scope">
<img :src="scope.row.image" alt="Image" style="width: 100px; height: auto;" />
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="scope">
{{ scope.row.status === 1 ? '招领中' : '已归还' }}
</template>
</el-table-column>
<el-table-column prop="date" label="发布时间" sortable>
<template #default="scope">
{{ scope.row.date }}
</template>
</el-table-column>
<!-- 操作 -->
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button type="primary" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination @current-change="handleCurrentChange" :current-page="currentPage" :page-size="pageSize"
:total="tableData.length" layout="total, prev, pager, next, jumper" />
<el-dialog v-model="isUpdateDialogVisible" title="编辑招领物件" width="35%">
<el-form :model="updateForm" label-width="auto">
<el-form-item label="物件名称">
<el-input v-model="updateForm.name" />
</el-form-item>
<el-form-item label="物件描述">
<el-input v-model="updateForm.description" />
</el-form-item>
<el-form-item label="物件图片">
<el-upload class="avatar-uploader" :http-request="uploadImage"
:show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</el-form-item>
<el-form-item label="物件状态">
<el-select v-model="updateForm.status" placeholder="请选择状态">
<el-option label="招领中" value="1" />
<el-option label="已归还" value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleUpdate">确认修改</el-button>
<el-button @click="isUpdateDialogVisible = false">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
<el-dialog v-model="isAddDialogVisible" title="新增招领物件" width="35%">
<el-form :model="addForm" label-width="auto">
<el-form-item label="物件名称">
<el-input v-model="addForm.name" />
</el-form-item>
<el-form-item label="物件描述">
<el-input v-model="addForm.description" />
</el-form-item>
<el-form-item label="物件图片">
<el-upload class="avatar-uploader" :http-request="uploadImage"
:show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</el-form-item>
<el-form-item label="物件状态">
<el-select v-model="addForm.status" placeholder="请选择状态">
<el-option label="招领中" value="1" />
<el-option label="已归还" value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleAddSubmit">确认新增</el-button>
<el-button @click="isAddDialogVisible = false">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { getFoundAll, deleteFound, updateFound, addFound } from '@/api/foundApi'
import { ElMessage } from 'element-plus'
import axios from 'axios'
import { Plus } from '@element-plus/icons-vue'
import type { UploadProps } from 'element-plus'
interface Found {
id: number
name: string
description: string
image: string
status: number
date: number[]
}
// Define tableData as a ref
const tableData = ref<Found[]>([])
onMounted(async () => {
let res = await getFoundAll()
console.log(res.data.data)
tableData.value = res.data.data // Use .value to assign data
})
const handleDelete = async (id: number) => {
try {
let res = await deleteFound(id)
if (res.data.code === 1) {
ElMessage.success('删除成功')
}
} catch (error) {
ElMessage.error('删除失败')
} finally {
// Refresh data regardless of success or failure
let res = await getFoundAll()
tableData.value = res.data.data
}
}
let isUpdateDialogVisible = ref(false)
const updateForm = ref<{
id: number;
name: string;
description: string;
status: number | null; // Adjusted to allow null
image: string | null; // Adjusted to allow null
}>({
id: 0,
name: '',
description: '',
status: null,
image: null,
})
const handleEdit = (row: Found) => {
updateForm.value.id = row.id
updateForm.value.name = row.name
updateForm.value.description = row.description
updateForm.value.image = row.image
updateForm.value.status = row.status
isUpdateDialogVisible.value = true // Open the dialog after setting the form values
}
const handleUpdate = async () => {
let res = await updateFound(updateForm.value.id, updateForm.value as Found)
if (res.data.code === 1) {
ElMessage.success('修改成功')
let res = await getFoundAll()
tableData.value = res.data.data
isUpdateDialogVisible.value = false
}
}
const currentPage = ref(1)
const pageSize = ref(5)
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return tableData.value.slice(start, start + pageSize.value).map(item => ({
...item,
date: formatDate(item.date) // 格式化日期
})) // Use .value here as well
})
const handleCurrentChange = (page: number) => {
currentPage.value = page
}
const uploadImage = async (options: any) => {
const { file, onSuccess, onError } = options
const formData = new FormData()
formData.append('file', file)
try {
const response = await axios.post('http://localhost:8080/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
if (response.data.code === 1) {
onSuccess(response.data.data)
updateForm.value.image = response.data.data
} else {
onError(new Error(response.data.msg || '图片上传失败'))
}
} catch (error) {
onError(error)
}
}
const imageUrl = ref('')
const handleAvatarSuccess: UploadProps['onSuccess'] = (
response,
uploadFile
) => {
imageUrl.value = URL.createObjectURL(uploadFile.raw!)
}
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
ElMessage.error('文件格式错误,请上传 JPG or PNG 格式文件!')
return false
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('文件大小不能超过 2MB!')
return false
}
return true
}
// 新增一个格式化日期的函数
const formatDate = (dateArray: number[]) => {
const [year, month, day] = dateArray
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
}
let isAddDialogVisible = ref(false)
const handleAdd = () => {
isAddDialogVisible.value = true
}
const addForm = ref<{
name: string | null;
description: string | null;
image: string | null;
status: number | null;
}>({
name: null,
description: null,
image: null,
status: null,
})
const handleAddSubmit = async () => {
addForm.value.image = imageUrl.value
let res = await addFound(addForm.value as Found)
if (res.data.code === 1) {
ElMessage.success('新增成功')
let res = await getFoundAll()
tableData.value = res.data.data
imageUrl.value = res.data.data.image
isAddDialogVisible.value = false
}
}
</script>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>
3.3、后端业务处理
java
@RequiredArgsConstructor
@RestController
@RequestMapping("/home")
@Slf4j
public class HomeController {
private final LostService lostService;
private final HomeService homeService;
@GetMapping("getLostAll")
public Result getLostAll(){
log.info("获取全部");
List<LostVO> list = lostService.getLostAll();
return Result.success(list);
}
@GetMapping("getFoundAll")
public Result getFoundAll(){
log.info("获取全部");
List<FoundVO> list = lostService.getFoundAll();
return Result.success(list);
}
@GetMapping("/getAnnouncement")
public Result getAnnouncement(){
log.info("获取公告");
List<BulletinBoard> list = homeService.getAnnouncement();
return Result.success(list);
}
@GetMapping("/getNewLost")
public Result getNewLost(){
log.info("获取最新失物");
List<LostVO> newLost = homeService.getNewLost();
return Result.success(newLost);
}
@GetMapping("/getFriendshipLinks")
public Result<List<FriendshipLinks>> getFriendshipLinks(){
log.info("获取友情链接");
List<FriendshipLinks> list = homeService.getFriendshipLinks();
return Result.success(list);
}
@PutMapping("/savoOrUpdateFriendshipLinks")
public Result<Void> savoOrUpdateFriendshipLinks(@RequestBody FriendshipLinks friendshipLinks){
log.info("更新或新增:{}",friendshipLinks);
homeService.savoOrUpdateFriendshipLinks(friendshipLinks);
return Result.success();
}
@DeleteMapping("/deleteFriendshipLinks/{id}")
public Result<Void> deleteFriendshipLinks(@PathVariable Long id){
log.info("删除:{}",id);
homeService.deleteFriendshipLinks(id);
return Result.success();
}
}
java
@RequiredArgsConstructor
@RestController
@Slf4j
@RequestMapping("/lost")
public class LostController {
private final LostService lostService;
/**
* 查询所有发布
* @return
*/
@GetMapping("getLostAll")
public Result<List<Lost>> getAllLost() {
log.info("查询所有丢失物件");
List<Lost> lostList =lostService.getAllLost();
return Result.success(lostList);
}
@DeleteMapping("/deleteLost/{id}")
public Result deleteLostById(@PathVariable Long id) {
lostService.deleteLostById(id);
return Result.success();
}
@PutMapping("/updateLost/{id}")
public Result updateLostById(@PathVariable("id") Integer id,@RequestBody LostDTO dto){
log.info("id,dto:{},{}",id,dto);
lostService.updateLostById(id,dto);
return Result.success();
}
@PostMapping("/addLost")
public Result addLost(@RequestBody LostDTO dto){
log.info("新增dto:{}",dto);
lostService.addLost(dto);
return Result.success();
}
@PostMapping("/feedbackLostSave")
public Result feedbackLostSave(@RequestBody ContactInfoDTO dto){
lostService.feedbackLostSave(dto);
return Result.success();
}
}
java
@Service
@RequiredArgsConstructor
public class HomeServiceImpl implements HomeService {
private final BulletinBoardMapper bulletinBoardMapper;
private final LostService lostService;
private final FriendshipLinksMapper friendshipLinksMapper;
/**
* 获取公告
*
* @return
*/
@Override
public List<BulletinBoard> getAnnouncement() {
return bulletinBoardMapper.getList();
}
/**
* 获取最新失物
* @return
*/
@Override
public List<LostVO> getNewLost() {
// 获取所有的 LostVO 数据
List<LostVO> lostAll = lostService.getLostAll();
// 按照 date 字段降序排序,并取前 10 条
return lostAll.stream()
.sorted((o1, o2) -> o2.getDate().compareTo(o1.getDate())) // 降序排序
.limit(10).collect(Collectors.toList());// 取前 10 // 收集为 List
}
/**
* 获取友情链接
*
* @return
*/
@Override
public List<FriendshipLinks> getFriendshipLinks() {
//获取友情链接
return friendshipLinksMapper.getFriendshipLinks();
}
/**
* 更新或新增
*
* @param friendshipLinks
*/
@Override
public void savoOrUpdateFriendshipLinks(FriendshipLinks friendshipLinks) {
//如果id等于null是新增
if (friendshipLinks.getId() == null) {
friendshipLinksMapper.insert(friendshipLinks);
} else {
friendshipLinksMapper.update(friendshipLinks);
}
}
/**
* 删除
*
* @param id
*/
@Override
public void deleteFriendshipLinks(Long id) {
friendshipLinksMapper.deleteFriendshipLinks(id);
}
}