结对第二次作业——编程实现

222100308向至尚 2024-03-25 21:11:33
这个作业属于哪个课程2302软件工程社区
这个作业要求在哪里结对第二次作业--编程实现
结对学号222100308 222100318
这个作业的目标编程实现、部署、上传git、编写博客
其他参考文献一文搞定:前端如何选择Angular、React和Vue三大主流框架前端Vue项目打包部署实战教程

目录

  • 一、PSP表格
  • PSP表格
  • 二、成品展示
  • (一)基础功能
  • 1、选手信息
  • 2、每日赛况
  • 3、详细赛况
  • 4、奖牌榜
  • (二)附加功能
  • 1、概述页面
  • 2、奖牌地图
  • 三、讨论过程描述
  • (一)题目分析
  • (二)查找资料过程
  • (三)分工安排以及截图
  • (四)git提交情况
  • 四、实现过程
  • 1. 前端框架结构
  • 2.后端框架结构
  • 3. 功能结构图
  • 五、代码说明
  • (一)后端代码
  • 1. 后端接口封装
  • 2. 实体类介绍
  • (二)前端代码
  • 1. 表格展示(运动员列表、详细赛况)
  • 2. 项目概述卡片
  • 3. 奖牌地图
  • 4. 概要页面(轮播图实现)
  • 六、心路历程和队友评价
  • (一)心路历程
  • (二)队友评价

gitcode仓库地址:gitcode仓库

一、PSP表格

PSP表格

Personal Software Process Stages预估耗时(分钟)实际耗时(分钟)
任务理解2015
分工安排1010
调研资料6080
配置环境60100
代码编写800750
前后端接口调试6080
部署服务器3040
编写文档6050
总时长12001125

云服务器访问链接:网站

二、成品展示

(一)基础功能

1、选手信息

选手信息

该页面展示了选手的国际、姓名、出生年月日以及选手所参加的项目。用户可以通过网页下方的分页条查看剩余选手的信息,并且支持搜索页面数实现跳转。

2、每日赛况

日历展示

通过日历的形式展现比赛的情况,若当天日期有比赛安排则日历上会以红色的字体展示,若当日无比赛则是蓝色的字体。同时在鼠标移动到对应日期时,当天日期会有放大突出的效果,让用户能够明显观察到自己是否选择查看该日比赛。通过点击对应日期可以查看当日的比赛赛况。

每日赛况

每日赛况中可以查看当天的所有比赛信息,包括比赛时间,比赛,比赛类型等,同时若该场比赛为决赛,则会以红色的字体(final round)凸显。用户可以通过点击每场比赛右侧的Details Results跳转到详细赛况页面。若用户想看其他日期的比赛赛况,可通过上方的返回按钮回到日历页面进行日期选择。

项目赛况页面

增加了项目赛况页面,在改页面中展示了该赛事的所有比赛项目,用户可以通过点击每个项目上的比赛选项来查看该项目的各个赛段时间.

在这里插入图片描述

3、详细赛况

详细赛况

通过点击每日赛况中的比赛信息可以进入详细赛况页面,在改页面中,展示了该项比赛的所有参赛选手、选手排名以及选手各自的国籍、年龄、积分和落后积分。

4、奖牌榜

奖牌榜页面展示了各个国家获奖状况 ,包括排名、金银铜牌数目和几康牌总数。同时,通过奖牌榜上方截止时间的展示能够反馈给用户奖牌信息的更新时间。

(二)附加功能

1、概述页面

概述页面1


概述页面2

  • 概述页面向用户展示了本次赛事的赛事说明,并通过图片展示了赛事的精彩瞬间。整体的背景风格采用了毛玻璃特效的样式,在网页底部的页脚部分,用户可以通过邮箱联系赛事官方,也可以在右侧找到官方的联系地址。
  • 中央图片的设计采用了轮播图的形式,每隔两秒的时间会展示下一张图片,用户也可以通过控制图片左右两边的按钮跳过当前图片图片

轮播图展示

2、奖牌地图

奖牌地图

  • 通过地图的方式直观地呈现出该次赛事各个参赛国家的参赛情况。
  • 根据奖牌数目的不同,将各个国家的区块标志以不同的颜色,当用户将鼠标悬停与国家区块上时,能够看到改国家的金银牌奖牌数目以及国家的赛事排名。

三、讨论过程描述

(一)题目分析

  1. 技术栈选择

    在做第一次原型设计的时候就要想到后面的作业是否是实现其功能,因此在本次作业发布前就简单构想了要使用什么技术栈,由于我们两个人之前都没有前端的基础(只会web课上学会的html,css等),经过简单的调研我们决定使用vue作为前端框架,后端我有使用springboot框架的经验,因此选择其作为后端框架

  2. 爬虫?

    原本我们计划使用的数据都通过调用官方api来获取,但是考虑到时间问题,不想花费大量时间在后端接口的搭建,与助教沟通后决定使用第一次实战作业中助教提供的比赛数据进行封装

(二)查找资料过程

  1. 前端框架调研

    由于我们都没有前端框架的使用经验,因此我们先进行前端框架选择的调查,我们看了这篇文章一文搞定:前端如何选择Angular、React和Vue三大主流框架,简单了解了三个主流框架的特点,总结如下:

    • react:渲染性能好,组件灵活,但是学习曲线较为陡峭,需要第三方库才能提供完整的解决方案
    • Angular:提供开箱即用的完整解决方案, 但是学习曲线陡峭,对于小型项目可能会过于庞大
    • vue:轻量级且易于学习、提供了构建Web应用程序的简单且直观的API
      考虑到这次作业是小项目且我们都为新手,故选择vue作为框架
  2. 附加功能实现调研

    • 附加功能上由于基础功能提供了奖牌榜,我们就想写一个奖牌地图,更加直观的展示各个国家的获奖情况,我们查找资料得到可以使用echarts作为功能组件,查找资料后,我们在echarts官方文档简单学习了如何使用,并且通过这篇博客具体学习:教你从零开始画echarts地图
    • 概要页面中我们想实现一个轮播图,查找资料找到了相关教程:vue2.0 + element-ui 实战项目-实现一个简单的轮播图(六),得知可以通过设置setInterval来实现自动播放
  3. 服务器部署

    服务器部署方面,我们选择了腾讯云服务器,他有提供免费的公网ip,且学生优惠下价格也比较适合,部署我们主要查找了后端接口部署以及前端接口部署

    • 后端接口:后端部署上首先与第一次实战作业类似,通过maven导出jar包,然后我们使用xftp将其移动到服务器上,一开始我们直接使用命令java -jar xxxx.jar来运行springboot项目,但是这样做只要我们关闭了shell,接口就停止使用了,我们查阅资料得到可以使用nohup指令实现后台运行,nohup菜鸟指南,因此完成了后端接口部署
    • 前端服务器:前端部署我们主要参考了这篇文章:前端Vue项目打包部署实战教程,通过部署nginx实现前端页面在服务器中运行

    (三)分工安排以及截图

    我们整体的分工是:我负责后端接口以及前端网页的基础功能,我的队友负责网页的附加功能以及网页的美化、服务器部署。

    结对讨论

    (四)git提交情况

    在这里插入图片描述

四、实现过程

我们本次作业整体采取前后端分离的架构设计,前端我们选择了vue作为开发框架,后端则选择使用springboot框架搭建,前后端分离的设计方式也是当下较为流行的开发方法,其简单介绍如下:

  • 前端通常采用单页面应用(SPA)的形式,使用JavaScript、HTML和CSS等技术构建动态的用户界面。
  • 后端则负责处理业务逻辑和数据存取,通常会提供RESTful API供前端调用
  • 将前端界面与后端逻辑完全分开,各自独立开发和部署。这种架构模式使得前端和后端可以分别专注于用户体验和业务逻辑的开发,提高了开发效率,同时也便于维护和扩展。

    1. 前端框架结构

    ├─assets
    ├─components
    │ └─widgets
    └─router
  • assets放一些前端的静态资源
  • components为主要页面组件,其中widgets放了一些可复用的模块
  • router管理前端的全局路由

    2.后端框架结构

    ├─common
    ├─config
    ├─controller
    ├─dao
    ├─dto
    ├─pojo
    └─utils
  • common放了一些常量定义
  • config是一些设置类,比如统一的返回消息格式封装和返回码
  • controller对应mvc中的控制层,作为Spring Boot应用程序的入口点,主要负责接收和处理客户端发来的请求。它通过映射特定的URL到相应的处理方法来响应用户的操作
  • pojo:Pojo即Plain Old Java Object,指的是普通的Java对象,在Spring Boot中,Pojo层通常指代实体类(Entity)或数据传输对象(DTO),这些类定义了应用程序的数据结构,对应mvc中的model层
  • utils:一些常用的工具类

3. 功能结构图

在这里插入图片描述

五、代码说明

(一)后端代码

由于后端代码不是本次作业的重点,因此做简要介绍,大部分后端代码都是基于第一次个人实战作业而成,只介绍springboot特性的代码

1. 后端接口封装

使用自定义的ResponseResult作为结果的封装,通过@RequestParam获取前端请求的参数进行具体处理

@RestController
@RequestMapping("/event")
public class EventController {

    @GetMapping("/msg")
    public ResponseResult<Object> getEventMsg(){
        return SearchUtils.getEventMsg();
    }

    @GetMapping("/calender")
    public ResponseResult<Object> getResultByName(@RequestParam("date") String date){
        return SearchUtils.getEventByDate(date);
    }

    @GetMapping("/single")
    public ResponseResult<Object> getEventByName(@RequestParam("name") String name,@RequestParam("type") String type){
        return switch (type) {
            case "final" -> SearchUtils.getEventByName(name, EventStage.FINAL);
            case "semifinal" -> SearchUtils.getEventByName(name, EventStage.SEMIFINAL);
            case "preliminary" -> SearchUtils.getEventByName(name, EventStage.PRELIMINARY);
            default -> ResponseResult.errorResult(AppHttpCodeEnum.MISS_USER);
        };
    }
}

2. 实体类介绍

使用Lombok提供的@Data,@AllArgsConstructor,@NoArgsConstructor注解自动添加getter、setter等方法,简化开发

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Athlete {
    private String name;
    private String birthday;
    private String nationality;
    private String gender;
    private List<String> disciplines;


    public Athlete(String name, String birthday, String nationality, String gender) {
        this.name = name;
        this.birthday = birthday;
        this.nationality = nationality;
        this.gender = gender;
        this.disciplines = new ArrayList<>();
    }

    public void addDiscipline(String discipline) {
        for(String pattern : validPatterns) {
            if(discipline.toLowerCase().equals(pattern)) {
                this.disciplines.add(pattern);
                break;
            }
        }
    }
}

(二)前端代码

1. 表格展示(运动员列表、详细赛况)

  • 使用element-ui提供的el-table展示表格,同时使用el-pageination提供分页展示
  • 分页展示功能通过维护currentPageData,currentPage,pageSize,handleCurrentChange用来计算指定页面的数据,当使用分页器时调用handleSizeChange来实现表格数据的改变
<el-main class="app">
      <div class="athlete-list">
        <el-table :data="currentPageData" border stripe style="font-size: 16px; color: #0f0101;">
          <el-table-column prop="country" label="Country" width="120">
            <template slot-scope="scope">
              <div class="country-flag">
                <img :src="getCountryFlag(scope.row.country)" alt="Country Flag">
                <span>{{ scope.row.country }}</span>
              </div>
            </template>
          </el-table-column>
          <el-table-column prop="name" label="Athlete" width="150">
          </el-table-column>
          <el-table-column prop="gender" label="Gender" width="100"></el-table-column>
          <el-table-column prop="dob" label="Date of Birth" width="150"></el-table-column>
          <el-table-column label="Discipline">
            <template slot-scope="scope">
              <div class="discipline">
                <img :src="require(`@/assets/jump.jpg`)" alt="Discipline Image" class="discipline-image">
                <span>Diving</span>
              </div>
            </template>
          </el-table-column>
        </el-table>

        <div class="block" style="margin-top:15px;">
          <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
            :current-page="currentPage" :page-sizes="[10, 20, 30]" :page-size="pageSize"
            layout="total, prev, pager, next, jumper" :total="total">
          </el-pagination>
        </div>
      </div>
    </el-main>
    <script>
import axios from 'axios';

export default {
  name: "Atheletes",
  data() {
    return {
      athleteData: [], // 总的运动员数据
      currentPageData: [], // 当前页显示的运动员数据
      currentPage: 1, // 当前页码
      pageSize: 10, // 每页显示的数量
      total: 0 // 总的数据量
    };
  },
  created() {
    this.fetchAthleteData();
  },
  methods: {
    async fetchAthleteData() {
      try {
        const response = await axios.get('http://49.233.255.219:3309/api/athlete/msg');
        this.athleteData = response.data.data;
        this.total = this.athleteData.length;
        this.handleCurrentChange(1); // 获取数据后默认加载第一页数据
      } catch (error) {
        console.error('Error fetching athlete data:', error);
      }
    },
    getCountryFlag(country) {
      console.log(`../assets/${country.toLowerCase()}.png`);
      return require(`@/assets/${country.toLowerCase()}.png`);
    },
    handleSizeChange(size) {
      this.pageSize = size;
      this.handleCurrentChange(1); // 改变每页显示数量时,加载第一页数据
    },
    handleCurrentChange(page) {
      const startIndex = (page - 1) * this.pageSize;
      const endIndex = startIndex + this.pageSize;
      this.currentPageData = this.athleteData.slice(startIndex, endIndex);
      this.currentPage = page;
    },
  }
}

</script>

2. 项目概述卡片

  • 不同按钮点击展示初赛半决赛以及决赛的具体时间信息,通过维护stageButtons数组来确定按钮是否显示(如果没有半决赛则不显示半决赛按钮),通过handleStageClick 改变当前显示的时间
 <el-card class="match-card" shadow="hover">
    <div class="match-info">
      <div class="vertical-info">
        <div class="match-date">{{ displayDate }}</div>
        <div class="match-time">{{ displayTime }}</div>
      </div>
      <div class="vertical-bar"></div>
      <img class="match-image" :src="require(`@/assets/jump.jpg`)" :alt="imageAlt">
      <div class="match-details">
        <p class="match-title">{{ matchTitle }}</p>
        <div class="match-buttons">
          <el-button v-if="stageButtons[0]" type="primary" class="match-stage-button" size="mini"
            @click="handleStageClick(0)">初赛</el-button>
          <el-button v-if="stageButtons[1]" type="warning" class="match-stage-button" size="mini"
            @click="handleStageClick(1)">半决赛</el-button>
          <el-button v-if="stageButtons[2]" type="danger" class="match-stage-button" size="mini"
            @click="handleStageClick(2)">决赛</el-button>
        </div>
      </div>
      <i class="el-icon-arrow-right arrow-icon"></i>
      <div class="details-text" @click="ref(matchTitle.toLowerCase())">{{ detailsText }}</div>
    </div>
  </el-card>
  <script>
export default {
  name: 'MatchCard',
  props: {
    finalDate: {
      type: String,
      required: true
    },
    semifinalDate: {
      type: String,
      required: true
    },
    preliminaryDate: {
      type: String,
      required: true
    },
    finalTime: {
      type: String,
      required: true
    },
    semifinalTime: {
      type: String,
      required: true
    },
    preliminaryTime: {
      type: String,
      required: true
    },
    imageUrl: {
      type: String,
      required: true
    },
    imageAlt: {
      type: String,
      default: 'Match Image'
    },
    matchTitle: {
      type: String,
      required: true
    },
    matchSubtitle: {
      type: String,
      default: 'Preliminary Round'
    },
    detailsText: {
      type: String,
      default: 'Detailed Results'
    }
  },
  data() {
    return {
      stageButtons: [true, true, true], // 初赛、半决赛、决赛按钮是否显示的数组
      displayTime: this.finalTime, // 初始显示的时间
      displayDate: this.finalDate
    };
  },
  created() {
    this.ininialButtons();
  },
  methods: {
    ref(val) {
      this.$router.push({ path: '/Schedule/DetailSchedule', query: { event: val } });
    },
    handleStageClick(index) {
      if (index == 0) {
        this.displayTime = this.preliminaryTime;
        this.displayDate = this.preliminaryDate;
      } else if (index == 1) {
        this.displayTime = this.semifinalTime;
        this.displayDate = this.semifinalDate;
      } else {
        this.displayTime = this.finalTime, // 初始显示的时间
          this.displayDate = this.finalDate
      }

    },
    ininialButtons() {
      this.stageButtons[0] = (this.preliminaryDate=="") ? false : true;
      this.stageButtons[1] = (this.semifinalDate=="") ? false : true;
      this.stageButtons[2] = (this.finalDate=="") ? false : true;
    }
  },
}
</script>

3. 奖牌地图

  • 奖牌地图使用开源的echarts组件,通过配置tooltipvisualMap实现鼠标浮动到指定国家显示奖牌情况
      <el-container>
          <el-main class="appdate">
              <div class="listTitle">
                  <h3>奖牌地图</h3>
              </div>
              <div id="map"></div>
          </el-main>
      </el-container>
     <script>
     created() {
          //   循环注册地图
          for (let index in this.jsonMap) {
              echarts.registerMap(index, this.jsonMap[index])
          };
      },
      mounted() {
          //    初始化地图
          this.chinaConfigure(this.value);
      },
      //   更新数据
      watch: {
          value(newVal) {
              this.myChart.dispose();
              this.chinaConfigure(this.value);
          }
      },
      beforeDestroy() {
          if (!this.myChart) {
              return;
          }
          this.myChart.dispose();
          this.myChart = null;
      },
      methods: {
          chinaConfigure(area) {
              this.myChart = echarts.init(document.getElementById("map")); //这里是为了获得容器所在位置        
              window.onresize = this.myChart.resize;
              let option = { // 进行相关配置
                  backgroundColor: "#E4F3FA",
                  tooltip: {
                      triggerOn: 'mousemove',
                      formatter: function (params) {
                          console.log(params);
                          if (params.data.name != null) {
                              return params.data.name + "\n" + "金牌数:" + params.data.gold +
                                  "\n" + "银牌数:" + params.data.silver + "\n" + "铜牌数:" + params.data.copper + "\n" + "奖牌总数:" + params.data.value + "金牌榜名次:" + params.data.rank
                          }
                      }
                  }, // 鼠标移到图里面的浮动提示框
                  visualMap: {
                      max: 10,
                      calculable: true,
                      inRange: {
                          color: ['#ffffbf', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026']
                      }
                  },
                  series: [{
                      type: 'map',
                      map: area,
                      data: area == 'world' ? this.worldData : [],
                      nameMap: area == 'world' ? this.nameMap : {},
                  }]
              }
              this.myChart.setOption(option, true);
              // this.myChart.on('click', params => { // 点击函数
              // this.value=params.name
              // this.myChart.setOption(option,true);
              // }) 
          },
      }
    };
    </script>
    

    4. 概要页面(轮播图实现)

  • 使用v-for指令遍历slides数组,为数组中的每个元素创建一个幻灯片。:key属性用于Vue的虚拟DOM算法,以便有效地更新和重用元素。:style属性用于动态设置每个幻灯片的样式,使其根据currentIndex向左偏移,实现轮播效果。
  • “上一张”按钮,点击时会触发prevSlide方法,如果currentIndex为0(即已经在第一张幻灯片),则禁用该按钮。
  • “下一张”按钮,点击时会触发nextSlide方法,如果currentIndex等于slides.length - 1(即已经在最后一张幻灯片),则禁用该按钮
    <div class="carousel">
              <div class="slides">
                <div
                  v-for="(slide, index) in slides"
                  :key="index"
                  :style="{ transform: `translateX(-${currentIndex * 100}%)` }"
                >
                  <img :src="slide.imgUrl" alt="Slide" />
                </div>
              </div>
              <a class="prev" @click="prevSlide" :disabled="currentIndex === 0"
                >&#10094;</a
              >
              <a
                class="next"
                @click="nextSlide"
                :disabled="currentIndex === slides.length - 1"
                >&#10095;</a
              >
            </div>
            <script>
    export default {
    data() {
      return {
        currentIndex: 0,
        slides: [
          { imgUrl: require("@/assets/jump1.jpg") },
          { imgUrl: require("@/assets/jump2.jpg") },
          { imgUrl: require("@/assets/jump3.jpg") },
          // 添加更多的轮播图项
        ],
      };
    },
    created() {
      // 使用定时器实现自动播放
      setInterval(() => {
        this.nextSlide();
      }, 3000); // 设置轮播间隔时间,单位为毫秒,这里设置为3秒
    },
    methods: {
      nextSlide() {
        this.currentIndex = (this.currentIndex + 1) % this.slides.length;
      },
      prevSlide() {
        this.currentIndex =
          (this.currentIndex - 1 + this.slides.length) % this.slides.length;
      },
    },
    };
    </script>
    

六、心路历程和队友评价

(一)心路历程

  1. 向至尚的心得:

    这次作业之前,我没有任何前端框架的使用经验,我借助chatgpt以及各路大神的博客半天速成了vue,并且最后完成了大体的网站功能,这次作业锻炼了我的前端代码编写能力,其实我个人是很反感前端开发,因为要调试各种各样的位置和边距,我觉得极其费神费眼,可以说这一次作业强迫我学习了一点前端知识,同时在与队友合作过程加深了对git的使用,总的来说这一次作业在蛮极限的条件下完成了前后端的开发和部署,内心还是蛮有成就感。

  2. 张璟楠的心得

    这次作业我主要完成的部分是前端的部分,由于我的队友已经把整体的框架搭建完毕,我完成的部分只要编写html,js和css的内容并添加到对应的文件中,并不需要对vue有一个非常全面的学习,所以在整个项目的完成过程中,没有在学习vue框架中花费太多的时间,当然这次作业加强了我在前端方面的编程能力,这对我将来有很大的帮助。

(二)队友评价

  1. 对张璟楠的评价

    我的队友相比我来说项目开发经验较少,因此本次作业他的主要任务还是学习为主,我们分工安排时给他安排了不怎么需要前期经验的部署运维环节,并且让他来做前端页面的美化工作,他能够很好地完成任务和工作,也从中学会了很多开发知识

  2. 对向至尚的评价

    我的队友开发经验丰富,由于我们都是第一次学习用 vue 框架搭建网站,我的队友能够快速的掌握新的知识,并且对于整体框架有很清晰的认知,快速搭建了一个框架,后端的任务也是快速完成。

...全文
181 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

122

社区成员

发帖
与我相关
我的任务
社区描述
FZU-SE
软件工程 高校
社区管理员
  • LinQF39
  • 助教-吴可仪
  • 一杯时间
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧