这个作业属于哪个课程 | 2022年福大-软件工程、实践-W班 |
---|---|
这个作业要求在哪里 | 软件工程实践结对作业二 |
结对学号 | 221900420 221900428 |
这个作业的目标 | 实现上一次结对作业中的原型设计的部分功能:包括但不限于奖牌总榜、每日赛程、奖牌地图 学会使用git进行项目协同开发 学习项目部署流程 |
其他参考文献 | ElementUI Vue2 Echarts |
文章目录
1. GitCode仓库地址
2. PSP表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
• Estimate | • 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | ||
• Analysis | • 需求分析 (包括学习新技术) | 60 | 180 |
• Design Spec | • 生成设计文档 | 240 | 240 |
• Design Review | • 设计复审 | 60 | 30 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 45 | 30 |
• Design | • 具体设计 | 120 | 60 |
• Coding | • 具体编码 | 1080 | 1440 |
• Code Review | • 代码复审 | 120 | 120 |
• Test | • 测试(自我测试,修改代码,提交修改) | 240 | 240 |
Reporting | 报告 | ||
• Test Repor | • 测试报告 | 120 | 120 |
• Size Measurement | • 计算工作量 | 30 | 20 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 45 | 45 |
合计 | 2190 | 2555 |
3. 项目部署地址
4. 设计实现过程
- 相关技术:Vue2、ElementUI、Echarts、Axios
- 相关工具:WebStrom、IDEA
- 代码托管平台:Gitcode
由于有现成的接口可供调用,所以采用纯前端的形式。
一开始讨论决定使用Vue3进行开发,但是一方面是对Vue3不够熟悉,一方面是考虑Vue3中地图支持可能会有些问题
所以综合考虑后,选择采用Vue2进行开发
(1)功能结构图
(2)项目结构
(3)数据来源
【该爬取行为仅用于课程教学】
数据来源主要是直接对某CCTV冬奥栏目的接口进行访问。
(4)项目部署
我们租用了阿里云的服务器,用nginx部署在服务器的80端口上。刚开始部署时出现了跨域的问题,最后是在nginx的配置文件配置了冬奥api接口的映射,同时在vue.config文件中也进行了api的映射。
5. 成品展示
主页
主页整体展示,默认入口。
每日赛程
每日赛程页面。通过列表左侧的时间选项,可以按时间进行筛选,显示对应时间的赛程,默认显示2月20日的赛程信息。通过右上角的选择项目、选择场馆下拉框,可以按照项目、场馆进行任意组合筛选当日赛程。
奖牌总榜
奖牌总榜页面,展示在此次冬奥会中获取过奖牌的国家的奖牌获取情况进行展示,支持通过升序降序显示。
奖牌地图
在主页中展示此次冬奥会的最终奖牌获取情况,鼠标悬停在不同区域时,会展示悬浮框显示对应国家的奖牌数据。
利用颜色深浅代表各国获取奖牌数量的多少,通过左下角的交互可以控制对奖牌获取数量在对应区间内的国家的显示。
导航栏
用于单页面应用中多页面的路由切换,当鼠标悬停在运动项目一栏时,下拉展示此次冬奥的所有运动项目。
在这里插入图片描述
轮播图
在主页提供冬奥会相关图片的轮播展示。
冬奥项目介绍
在主页提供项目介绍展示。当点击右侧对应运动项目图标时,左侧展示对应项目的介绍内容。默认展示花滑。
6. 部分关键代码
相关接口封装
主要对某cctv的接口访问进行封装
const service = axios.create({
baseURL: process.env.BASE_API, // api的base_url
timeout: 15000, // 请求超时时间
withCredentials: true // 设置请求携带cookie 保证session有效性
})
//加个拦截器,(单纯只是为了后续能少写一次.data,还有看起来厉害一点(?))
service.interceptors.response.use(success => {
if (success.status && success.status === 200) {
return success.data
}
}
)
export default service
let base = '/dongaoApi'
//获取奖牌数据
export function fetchMedals() {
return request({
url:`${base}/olympic/getBjOlyMedals?serviceId=2022dongao&itemcode=GEN-------------------------------`,
method:'get'
})
}
......
......
//获取冬奥项目数据
export function getOlyItems(){
//localStorage已经存在OlyItemList,则直接返回
if (window.localStorage.getItem('OlyItemList')){
return JSON.parse(window.localStorage.getItem('OlyItemList'))
}
//localStorage中不存在,发起请求
request({
url:`${base}/Olympic/getBjOlyItemList?serviceId=2022dongao`,
method:'get'
}).then((res) => {
if (res){
console.log('dongaoAPI',res.data.ogItemList)
window.localStorage.setItem('OlyItemList',JSON.stringify(res.data.ogItemList))
return res.data.ogItemList
}
})
}
每日赛程部分代码
- 用 el-table 双向绑定数据 filteredMatchData 。
- 当页面挂载时,调用
mounted()
方法,在 mounted() 方法中调用 initMatchList() 方法,初始化当日(默认2月20日)赛程的数据列表。- 当对“选择项目”和“选择场馆”下拉框进行交互后,触发 el-dropdown 标签的 @command 事件,在调用的方法中将选择的项目或场馆参数赋值给 selectedOlyItem 或 selectedVenue,由于给 selectedOlyItem 和 selectedVenue 添加了变量监听,所以当这两个值发生变化时,自动调用过滤方法 filterMatchList() 。在 filterMatchList() 方法中对数据进行过滤筛选后,重新赋值给 filteredMatchData 。由于 filteredMatchData 与 el-table 进行了双向数据绑定,所以刷新列表,展示满足条件的数据。
- 当对左侧的日期选项进行选择时,selectedDate 属性发生变化,自动调用 selectedDate 属性的监听回调,回调函数中重置 selectedOlyItem 和 selectedVenue ,并调用 initMatchList() 方法获取所选日期的赛程数据。
- 在 initMatchList() 方法中判断localStorage中是否存在所需数据,若存在,则直接获取数据,否则调用接口获取数据并存入localStorage中。
<!-- 列表部分 -->
<el-tabs v-model="selectedDate"
class="tabsContainer"
tab-position="left"
type="border-card">
<el-tab-pane
v-for="(date,index) in dates" :key="index"
:label="chosenDate(date)"
:name="date">
<el-table class="matchesTable"
:data="filteredMatchData"
height="700">
<el-table-column
prop="startdatecn"
label="时间"
align="center">
</el-table-column>
<el-table-column
prop="itemcodename"
label="大项"
align="center">
</el-table-column>
......
......
</el-table>
</el-tab-pane>
</el-tabs>
export default {
name: "ScheduleMatches",
data(){
return {
selectedDate: '0220', //用于筛选日期
selectedOlyItem: '选择项目', //用于筛选项目
selectedVenue:'选择场馆', //用于筛选场馆
matchData:[], //用于存放未过滤的当日赛程数据
filteredMatchData:[], //用于存放进行项目和场馆筛选后的当日赛程数据
dates:[
'0202','0203','0204','0205','0206','0207','0208','0209','0210','0211',
'0212','0213','0214','0215','0216','0217','0218','0219','0220',
],
}
},
watch:{
//selectedDate属性发生改变时,重新初始化列表
selectedDate(){
this.selectedOlyItem = '选择项目'
this.selectedVenue = '选择场馆'
this.initMatchList()
},
//selectedOlyItem属性发生变化时,调用过滤方法
selectedOlyItem(){
this.filterMatchList()
},
//selectedVenue属性发生变化时,调用过滤方法
selectedVenue(){
this.filterMatchList()
}
},
//页面挂载时,初始化当日赛程列表
mounted() {
//初始化赛程列表
this.initMatchList()
},
methods:{
//过滤方法,通过项目、场馆条件筛选数据
filterMatchList(){
//主要为了保证后续箭头函数中能拿到筛选条件
let selectedOlyItem = this.selectedOlyItem;
let selectedVenue = this.selectedVenue;
//条件
let itemCondition = false;
let venueCondition = false;
this.filteredMatchData = this.matchData.filter((item)=>{
//若选择项为“选择项目”,则不对项目进行筛选,场馆部分同理
itemCondition = (selectedOlyItem == '选择项目')? true:item.itemcodename == selectedOlyItem
venueCondition = (selectedVenue == '选择场馆')? true:item.venuename == selectedVenue
return (itemCondition && venueCondition)
})
},
..... //省略一部分代码,
,
//初始化所选日期的赛程数据
initMatchList(){
//判断localStorage中是否存在所需数据,若存在,则直接获取数据,否则调用接口获取数据并存入localStorage中
if (!window.localStorage.getItem('matchList'+this.selectedDate)){
this.fetchMatches(this.selectedDate).then((res) => {
if (res){
console.log(res)
this.matchData = res.data.matchList
this.filteredMatchData = this.matchData
//存入localStorage
window.localStorage
.setItem('matchList'+this.selectedDate,JSON.stringify(res.data.matchList))
this.$message.success('赛程数据获取成功!')
}
})
}
else{
this.matchData = JSON.parse(window.localStorage.getItem('matchList'+this.selectedDate))
}
this.filteredMatchData = this.matchData
}
},
}
奖牌地图部分关键代码
echarts中地图的参数的相关使用则参考[echarts文档]([外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1HE8QCae-1648209687354)(file:///C:\Users\administer\AppData\Roaming\Tencent\QQTempSys[5UQ[BL(6~BS2JV6W}N6[%S.png)]https://echarts.apache.org/zh/option.html)。
包括悬浮框的设计,标题栏的自定义,数据和地图的交互,左下角的分类视图还有颜色等。但是文档中没有给明悬浮栏如何自定义,在经过测试后发现tooltip中的formatter回调函数是支持原生的html代码的,所以想到可以通过写css样式来自定义悬浮栏,但是内嵌的html代码不支持class来设置css,因此将纯html代码重构成支持内嵌style的形式,同时内嵌的图片src不支持vue的url自动映射,所以在外层还需要require资源后传入src中才能正常显示图片。
tooltip: {
trigger: 'item',
showDelay: 20,
transitionDuration: 0.5,
backgroundColor: 'rgba(0,0,0,0)',
hideDelay: 0,
margin: 0,
borderWidth: 0,
padding: 0,
formatter: function (params) {
let medalPic = require('../assets/images/medal1.png')
let goldPic = require('../assets/images/gold1.png')
let silverPic = require('../assets/images/silver1.png')
let bronzePic = require('../assets/images/bronze1.png')
let countryName = params.data.name;
let countryRank = params.data.rank;
let goldCounts = params.data.gold;
let silverCounts = params.data.silver;
let bronzeCounts = params.data.bronze;
// let totalCounts = params.data.count;
//在此处自定义悬浮栏的样式,因为此处的内嵌代码不支持class,必须把style写在里面
//可以先写一份纯html代码然后修改成适配echarts内嵌的格式
return `
<div style="width: 200px;
height: 130px;
background-color: red;
background-image: linear-gradient(#e66465, #9198e5);
border-radius: 25px;
box-shadow: 10px 10px 5px #888888;
border: 1px #9198e5 solid;">
<span style=" width: 20%;
height: 100%;
margin-left: 13px;
margin-top: 10px;
float: left;">
<div>
<img src="${medalPic}" width="40px" height="40px" alt="奖牌"/>
</div>
<div style="margin-top: 10px;font-weight: lighter;color: #ffffff">${countryName}</div>
<div style="margin-left:10px;color: #eecfcf;font-size: 11px">第 ${countryRank}</div>
</span>
<span style=" width: 30%;
height: 100%;
margin-left: 15px;
margin-right: 5px;
margin-top: 10px;
font-size: 14px;
float: left;
font-weight: bold;">
<div>
奖牌情况
</div>
<div><img src="${goldPic}" width="30px"height="30px" style="margin: auto 15px"></div>
<div><img src="${silverPic}" width="30px"height="30px" style="margin: auto 15px"></div>
<div><img src="${bronzePic}" width="30px"height="30px" style="margin: auto 15px"></div>
</span>
<span style=" width: 25%;
height: 100%;
margin-top: 15px;
font-size: 14px;
float: left;">
<div style="text-align: right;margin-right: 10px">数量</div>
<div style="height: 20%;width:100%;font-weight: bold;text-align: center;margin-top: 5px">${goldCounts}</div>
<div style="height: 20%;width:100%;font-weight: bold;text-align: center;margin-top: 5px">${silverCounts}</div>
<div style="height: 20%;width:100%;font-weight: bold;text-align: center;margin-top: 5px">${bronzeCounts}</div>
</span>
</div>
`
}
},
// 视觉映射组件,即左下角的标签
visualMap: {
type: 'piecewise',
show: true,
min: 1,
max: 41,
splitNumber: 5,
textStyle: {
fontSize: 14,
color: '#1a1a1a'
},
realtime: false,
calculable: true,
inRange: {
color: ['#fff6d5', '#f9e36c', '#f8c552', '#f7933b', '#f87326',]
}
结对过程
结对讨论
协同开发
心路历程与收获
**221900428:**之前学习了Vue2,只是跟着视频写了点简单的后台界面。这次写前台界面,确实遇到了不少麻烦,虽然也有其他组件库,但还是选择了比较熟悉的ElementUI。加上我们两个都是以后端为主,所以写样式的时候有点痛苦。因为起先没有思考太多,直接无脑套了ElementUI的布局容器,导致后续有响应式页面需求的时候,不得不对已经写好的多个页面的结构进行重构,浪费了不少时间。
**221900420:**这个项目让我系统地复习了一遍vue2的知识,还学到了echarts和elementui如何使用,通过查看这两个前端框架的文档才发现,原来不同框架之间文档的差距可以这么大。有对比才有伤害,我也深深理解到了非侵入式的框架对开发人员的帮助有多大。而且架构的设计要在一开始就定好,不然后期需要重构会浪费很多不必要的时间。就比如最后的项目部署环节,因为接口层单独从视图层中分离出来了,所以后期在发现部署到服务器上的跨域问题时,非常快地就解决了,这也体现了架构解耦的重要性。
队友评价
221900428 TO 221900420:
第一次使用SVN进行协同开发,很多不懂的地方都能问他,学到了不少,也省去了不少麻烦。编码水平很高,起先在与他进行关于项目的一些编码规范讨论时,也了解了很多之前没有用过结构和方式。项目部署基本也都是由他负责的。
真要挑点毛病,就是这人实在太忙了。我的评价是:大腿,我的大腿,斯溜。
221900420 TO 221900428:
不得不说我的队友学习能力真的很强,我的表达能力并不强,但是他一下就能理解我说的意思,然后偷偷的惊艳我。跟他讲的项目结构即使很抽象,他也能马上领悟,开发效率非常高,所以跟他合作很舒服,还不用考虑很多简单的问题。