SpringBoot校园失物招领信息平台

发布于:2025-05-13 ⋅ 阅读:(11) ⋅ 点赞:(0)

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、登录注册

系统提供了完善的登录注册功能,确保用户身份的合法性。

  • 登录: 用户通过输入用户名、密码和选择角色(管理员或用户)进行登录。登录成功后,系统会根据用户的角色跳转到相应的界面。登录界面设计简洁明了,背景图采用了充满科技感的火箭发射场景,寓意着平台的快速发展和便捷服务。
  • 注册: 新用户可以通过注册功能创建自己的账号。注册时需要填写用户名、密码等信息。注册成功后,用户即可使用新创建的账号登录平台。注册功能为用户提供了便捷的平台入口。

登录

image-20250512203007454

注册

image-20250512203021766

2.2、管理员端截图

管理员端提供了丰富的功能模块,方便管理员对平台进行全面的管理。

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

系统首页

image-20250512203117818

公告信息

image-20250512203136592

失物信息

image-20250512203150377

用户管理

image-20250512203212492

2.3、用户端截图

用户端提供了便捷的功能,方便用户发布和查找失物招领信息。

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

主页

image-20250512202715149

失物广场

image-20250512202735896

image-20250512202746409

image-20250512202755940

招领广场

image-20250512202810253

反馈建议

image-20250512202830960

个人中心

image-20250512202915864

3、核心代码实现
3.1、前端首页

创建父组件

<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>

创建子组件

<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、前端招领广场
<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、后端业务处理
@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();
    }

}
@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();
    }


}
@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);
    }
}

网站公告

今日签到

点亮在社区的每一天
去签到