122
社区成员
发帖
与我相关
我的任务
分享| 这个作业属于哪个课程 | 2302软件工程社区 |
|---|---|
| 这个作业要求在哪里 | 结对第二次作业--编程实现 |
| 结对学号 | 222100308 222100318 |
| 这个作业的目标 | 编程实现、部署、上传git、编写博客 |
| 其他参考文献 | 一文搞定:前端如何选择Angular、React和Vue三大主流框架 、前端Vue项目打包部署实战教程 |
gitcode仓库地址:gitcode仓库
| Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|
| 任务理解 | 20 | 15 |
| 分工安排 | 10 | 10 |
| 调研资料 | 60 | 80 |
| 配置环境 | 60 | 100 |
| 代码编写 | 800 | 750 |
| 前后端接口调试 | 60 | 80 |
| 部署服务器 | 30 | 40 |
| 编写文档 | 60 | 50 |
| 总时长 | 1200 | 1125 |
云服务器访问链接:网站

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

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

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

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


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

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


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


- 通过地图的方式直观地呈现出该次赛事各个参赛国家的参赛情况。
- 根据奖牌数目的不同,将各个国家的区块标志以不同的颜色,当用户将鼠标悬停与国家区块上时,能够看到改国家的金银牌奖牌数目以及国家的赛事排名。
在做第一次原型设计的时候就要想到后面的作业是否是实现其功能,因此在本次作业发布前就简单构想了要使用什么技术栈,由于我们两个人之前都没有前端的基础(只会web课上学会的html,css等),经过简单的调研我们决定使用vue作为前端框架,后端我有使用springboot框架的经验,因此选择其作为后端框架
原本我们计划使用的数据都通过调用官方api来获取,但是考虑到时间问题,不想花费大量时间在后端接口的搭建,与助教沟通后决定使用第一次实战作业中助教提供的比赛数据进行封装
前端框架调研
由于我们都没有前端框架的使用经验,因此我们先进行前端框架选择的调查,我们看了这篇文章一文搞定:前端如何选择Angular、React和Vue三大主流框架,简单了解了三个主流框架的特点,总结如下:
- react:渲染性能好,组件灵活,但是学习曲线较为陡峭,需要第三方库才能提供完整的解决方案
- Angular:提供开箱即用的完整解决方案, 但是学习曲线陡峭,对于小型项目可能会过于庞大
- vue:轻量级且易于学习、提供了构建Web应用程序的简单且直观的API
考虑到这次作业是小项目且我们都为新手,故选择vue作为框架
附加功能实现调研
- 附加功能上由于基础功能提供了奖牌榜,我们就想写一个奖牌地图,更加直观的展示各个国家的获奖情况,我们查找资料得到可以使用echarts作为功能组件,查找资料后,我们在echarts官方文档简单学习了如何使用,并且通过这篇博客具体学习:教你从零开始画echarts地图
- 概要页面中我们想实现一个轮播图,查找资料找到了相关教程:vue2.0 + element-ui 实战项目-实现一个简单的轮播图(六),得知可以通过设置
setInterval来实现自动播放
服务器部署
服务器部署方面,我们选择了腾讯云服务器,他有提供免费的公网ip,且学生优惠下价格也比较适合,部署我们主要查找了后端接口部署以及前端接口部署
- 后端接口:后端部署上首先与第一次实战作业类似,通过maven导出jar包,然后我们使用
xftp将其移动到服务器上,一开始我们直接使用命令java -jar xxxx.jar来运行springboot项目,但是这样做只要我们关闭了shell,接口就停止使用了,我们查阅资料得到可以使用nohup指令实现后台运行,nohup菜鸟指南,因此完成了后端接口部署- 前端服务器:前端部署我们主要参考了这篇文章:前端Vue项目打包部署实战教程,通过部署nginx实现前端页面在服务器中运行
我们整体的分工是:我负责后端接口以及前端网页的基础功能,我的队友负责网页的附加功能以及网页的美化、服务器部署。


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

由于后端代码不是本次作业的重点,因此做简要介绍,大部分后端代码都是基于第一次个人实战作业而成,只介绍springboot特性的代码
使用自定义的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);
};
}
}
使用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;
}
}
}
}
el-table展示表格,同时使用el-pageination提供分页展示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>
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>
tooltip和visualMap实现鼠标浮动到指定国家显示奖牌情况 <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>
<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"
>❮</a
>
<a
class="next"
@click="nextSlide"
:disabled="currentIndex === slides.length - 1"
>❯</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>
向至尚的心得:
这次作业之前,我没有任何前端框架的使用经验,我借助chatgpt以及各路大神的博客半天速成了vue,并且最后完成了大体的网站功能,这次作业锻炼了我的前端代码编写能力,其实我个人是很反感前端开发,因为要调试各种各样的位置和边距,我觉得极其费神费眼,可以说这一次作业强迫我学习了一点前端知识,同时在与队友合作过程加深了对git的使用,总的来说这一次作业在蛮极限的条件下完成了前后端的开发和部署,内心还是蛮有成就感。
张璟楠的心得
这次作业我主要完成的部分是前端的部分,由于我的队友已经把整体的框架搭建完毕,我完成的部分只要编写html,js和css的内容并添加到对应的文件中,并不需要对vue有一个非常全面的学习,所以在整个项目的完成过程中,没有在学习vue框架中花费太多的时间,当然这次作业加强了我在前端方面的编程能力,这对我将来有很大的帮助。
对张璟楠的评价
我的队友相比我来说项目开发经验较少,因此本次作业他的主要任务还是学习为主,我们分工安排时给他安排了不怎么需要前期经验的部署运维环节,并且让他来做前端页面的美化工作,他能够很好地完成任务和工作,也从中学会了很多开发知识
对向至尚的评价
我的队友开发经验丰富,由于我们都是第一次学习用 vue 框架搭建网站,我的队友能够快速的掌握新的知识,并且对于整体框架有很清晰的认知,快速搭建了一个框架,后端的任务也是快速完成。