122
社区成员
发帖
与我相关
我的任务
分享| 这个作业属于哪个课程 | <2302软件工程社区> |
|---|---|
| 这个作业要求在哪里 | <结对第二次作业--编程实现> |
| 结对学号 | <222100128黄煦陶,222100129梅明胜> |
| 这个作业的目标 | <完成编码撰写;完成博客撰写> |
| 其他参考文献 | vue 官网教程, Element-Plus 教程, Git 官网 |
| PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 60 | 60 |
| • Estimate | • 估计这个任务需要多少时间 | 60 | 60 |
| Development | 开发 | 1860 | 2190 |
| • Analysis | • 需求分析(包括学习新技术) | 210 | 240 |
| • Design Spec | • 生成设计文档 | 60 | 60 |
| • Design Review | • 设计复审 | 30 | 30 |
| • Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
| • Design | • 具体设计 | 90 | 120 |
| • Coding | • 具体编码 | 1200 | 1500 |
| • Code Review | • 代码复审 | 120 | 90 |
| • Test | • 测试(自我测试,修改代码,提交修改) | 120 | 120 |
| Reporting | 报告 | 150 | 210 |
| • Test Repor | • 测试报告 | 60 | 120 |
| • Size Measurement | • 计算工作量 | 30 | 30 |
| • Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 60 | 60 |
| 合计 | 2070 | 2460 |










本次作业我们基本上是采用线下方式(同寝室)进行探讨和交流,线上辅助讨论一些问题和 bug。









在我们的项目开发过程中,我们遇到了多个挑战和相应的解决方案,这些体验为我们的技术成长提供了宝贵的实践机会。
- CSS布局挑战:项目初期构建网页结构的时候,我们发现自己调整的CSS样式无法如预期生效,预计组件会布局在页面的上方,但实际上布局在页面的正中央,令我们百思不得其解。
解决方式:后来意识到是由于未删除Vue 3项目的默认CSS文件所致。这个问题一度让我们在页面布局上浪费了大量时间,但也教会了我们在新建项目时要注意检查和清理默认文件。

- UI组件选择:我们对待每一份作业都一丝不苟,原型设计也是,所以原先的原型设计没想到给我们埋下了这么大的坑
(保证不是夸自己原型设计多么好)。对我们自己能力和时间来说,完成这么多精美的组件是不可能的事情。解决方式:面对自身CSS技能的不足,我们决定采用==Element Plus==,这个决策大幅提高了开发效率,让我们能够快速实现漂亮且功能丰富的组件,减少了调整样式的时间 ,只需要按照接口文档说明,填写相应数据,简单布局排版即可。
- 数据处理: 由于我们项目是复刻一个赛事官网,特点就是需要呈现的比赛数据特别多,对于我们这种新手来说,把数据手工抄到代码上无疑是一种选择。但可想而知这种做法的工作量巨大。
解决方式:权衡之下我们愿意把时间用在学习高级一点的技术代替我们搬砖的活。项目中需要处理大量数据,我们选择了结合==TypeScript==和==Pinia==来解析JSON文件,实现数据的批量处理。这种方法不仅提高了效率,还使得数据在多个Vue页面中得到复用 ,所以我们的选择是正确的。
- 页面跳转Bug1:在实现从“赛程” (Schedule)页面到“比赛结果” (Result)页面的跳转时, 发现路由网址变化了,但导航栏的激活状态还没有变化,导航栏还显示当前页面是“赛程” (Schedule)页面。
解决方式:把路由信息与导航栏的激活状态绑定,让这两个状态同步更改。即可轻易解决。

- 页面跳转Bug2:在实现从“赛程” (Schedule)页面到“比赛结果” (Result)页面的跳转时, 确实可以精确打开指定比赛的指定阶段,如初赛或者决赛。但跳转之后再点击其他的比赛结果进行查看,可能会发生数据无法加载的问题。例如我们跳转查看Women 1m Springboard的preliminaries,然后我们查看Men 10m Synchronised,就无法查看任何数据。
解决方式:原因就是在跳转的时候,路由中包含props参数,指定我们要看preliminaries,使得可以精确跳转,但点击其他比赛结果的时候,props参数仍然奏效,导致数据加载异常。上面例子中的问题就是因为Men 10m Synchronised没有preliminaries,导致查看不了任何数据。我们只需要设置当用户点击其他组件的时候,令props中相关参数失效即可。
- 部署问题:部署项目时,我们遇到了
npm run build失败的问题,导致dist文件夹中只有一个ico图标文件。解决方式:原因是
TypeScript的强类型机制,我们项目可以顺利运行,但typescript代码会有警告,就无法npm run build成功。通过选择不检查语法的方式强行构建,我们解决了这个问题(尝试解决类型转换问题,但费了很多功夫还是没能解决,不过不影响项目运行)。此外,我们还调整了生产环境下的资源路径,以适应生产环境的需求,得我们带来了很多项目构建部署上的经验。
<el-header class="header">: 代表页面的头部区域。在这个标签内,有一个自定义组件,这可能是一个包含网站logo、导航菜单或其他信息的组件。class="header"用于给头部添加特定的样式。
<el-main class="main">: 表示页面的主体内容区域。内部使用了组件,这是Vue Router的一部分,用于根据当前路由动态显示不同的页面内容。这意味着,用户在导航网站时,主体内容会根据用户的操作显示不同的组件,而头部和尾部保持不变。class="main"可能用于设置主体区域的样式,比如间距、宽度或背景色。
<el-footer class="footer">: 代表页面的底部区域。里面有一个自定义组件,可能包含版权信息、联系方式或其他一些网站的基本信息。class="footer"用于为页脚区域添加样式。
这段代码通过==Element Plus==的布局组件实现了一个响应式的页面布局,分为头部、主体和尾部三个区域。头部和尾部是静态的,对于网站的所有页面来说是共通的,而主体部分则是动态的,可以根据用户的不同操作展示不同的内容。这种布局方式很适合像我们一样需要有统一头部和尾部,但中间内容频繁变动的网站或应用。
<template>
<div class="content">
<div class="common-layout">
<el-container class="container">
<el-header class="header">
<Head></Head>
</el-header>
<el-main class="main">
<RouterView />
</el-main>
<el-footer class="footer">
<Foot/>
</el-footer>
</el-container>
</div>
</div>
</template>
history: createWebHistory()指定了Vue Router使用HTML5的历史模式。这种模式利用history.pushState API来改变URL而不重新加载页面。这使得用户在浏览单页面应用时可以享受到像浏览传统页面一样的体验,包括前进和后退导航。routes数组定义了所有路由的数组。每个路由都是一个对象,包含path、name和component等属性:path: URL路径,当用户访问这个路径时,路由会被激活。name: 路由的名称,可以用于编程式导航。component: 当路由被激活时,应该渲染的组件。/home时,应该渲染HomeView组件。其余页面同理,代码中就不重复给出了。/home。这意味着当用户访问应用的根路径时,会自动被导航到/home。/:pathMatch(.*)*来匹配,并将这些访问重定向到/home。这通常用于处理404页面,确保所有未识别的路径都会重定向到首页。const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/home',
name: 'home',
component: HomeView
},
{
path: '/',
redirect: '/home'
},
{
path: '/:pathMatch(.*)*',
redirect: '/home'
},
......
]
})
flatMap与map进行数据处理,代码既保证了类型安全,又提供了一个清晰、易于管理的方法来处理和访问运动员数据。这种模式特别适合于需要预加载和全局访问大量静态数据的应用场景。这里处理的数据,具体的使用将在下一点中演示Athlete:这个接口用于定义单个运动员对象应该具有的属性。这有助于在处理运动员数据时提供类型安全,确保数据的一致性和可预测性。AthletesData.flatMap(...)用于将原始的JSON数据转换为Athlete接口定义的形式。flatMap方法首先对AthletesData数组的每个元素(每个国家)执行一个映射操作,然后将结果扁平化为一个数组。这意味着无论原始数据中有多少个国家,最终都会得到一个单层级的运动员数组。country.Participations.map(...)遍历其Participations数组(参与的运动员),并为每个参与者创建一个新的对象,这个对象符合Athlete接口的结构。getters:getters是用于从存储中派生状态的函数。它简单地返回状态中的athletes数组。通过这种方式,组件可以直接访问经过处理的运动员数据。 import { defineStore } from 'pinia';
import AthletesData from '@/assets/data/athletes.json';
interface Athlete {
CountryName: string;
CountryCode: string;
Gender: number;
PreferredLastName: string;
PreferredFirstName: string;
ScoreboardPhotoId: string;
DOB: string;
}
export const useAthletesData = defineStore('athletesData', {
state: () => ({
// 在状态初始化时立即处理数据
athletes: AthletesData.flatMap(country =>
country.Participations.map(participation => ({
CountryName: country.CountryName,
CountryCode: country.CountryCode,
Gender: participation.Gender,
PreferredLastName: participation.PreferredLastName,
PreferredFirstName: participation.PreferredFirstName,
ScoreboardPhotoId: participation.ScoreboardPhotoId,
DOB: participation.DOB,
}))
),
}),
getters: {
allAthletes: (state) => state.athletes,
},
});
Pinia存储中获取的数据以一种用户友好的方式展示出来。通过el-table和el-table-column,我们可以自定义每列的展示逻辑,包括通过插槽自定义复杂的数据显示(如姓名的全名显示、性别的图标显示以及日期的格式化)。这样的数据展示方法既灵活又强大,能够应对各种数据展示需求。el-table: 使用==Element Plus==的<el-table>组件来展示数据。这个组件接受一个数组作为数据源,并为每个数据项渲染一行。el-table-column: 每个<el-table-column>定义了表格中的一列。prop属性用于指定该列要显示数据对象中的哪个属性。label属性定义了列头的标题。如果列需要特殊格式化或包含复杂内容,则不使用prop属性,而是通过具名插槽(template)来自定义列的内容。import { useAthletesData } from '@/store/athletes'导入之前定义的Pinia存储。useAthletesData()来获取存储实例,然后通过store.allAthletes访问存储中的全部运动员数据。这里的athletes变量将被用作<el-table>的数据源。formatDate函数用于将日期字符串格式化为更易读的格式(例如:"YYYY-MM-DD")。这在显示日期时非常有用,因为原始数据可能不是以最终希望展示给用户的格式存储的<template>
<el-table>
<el-table-column prop="CountryName" label="Country" sortable> </el-table-column>
<el-table-column label=" "> </el-table-column>
<el-table-column label="Athlete Name">
<template #default="{ row }">
<span class="AthleteName">{{ row.PreferredFirstName + ' ' + row.PreferredLastName }}</span>
</template>
</el-table-column>
<el-table-column prop="Gender" label="Gender">
<template #default="{ row }">
{{ row.Gender === 0 ? 'Male' : 'Female' }}
<el-icon v-if="row.Gender === 0">
<male />
</el-icon>
<el-icon v-else>
<female />
</el-icon>
</template>
</el-table-column>
<el-table-column label="DOB" sortable>
<template #default="{ row }">
<span class="DOB">{{ formatDate(row.DOB) }}</span>
</template>
</el-table-column>
<el-table-column>
<img src="@/assets/image/div.png" alt="div" />
</el-table-column>
</el-table>
</template>
<script setup>
import { useAthletesData } from '@/store/athletes'
// 使用 Pinia store
const store = useAthletesData()
// 从 Pinia store 获取运动员数据
const athletes = store.allAthletes
// 定义一个简单的日期格式化函数
const formatDate = (dateString) => {
const date = new Date(dateString)
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`
}
</script>
fetchMatchDetails(matchType: string)*matchType的数据是否存在以及数据中的Heats属性是否为一个数组。如果这些验证失败,会在控制台输出错误信息,并返回一个空数组。heat返回一个包含EndUtcDateTime(结束时间)和PhaseName(阶段名称)的对象。这个操作的目的是为了简化数据,使其更适合显示比赛的基本信息。fetchMatchScore(matchType: string, PhaseName: string)*fetchMatchDetails,首先验证是否能根据matchType找到对应的数据。如果找不到,输出错误信息并返回空数组。find方法在Heats数组中查找名称匹配PhaseName的比赛。如果找不到对应的热身赛或该热身赛没有结果数据(Results),同样输出错误信息并返回空数组。targetHeat.Results数组进行映射,为每个结果提取出Rank(排名)、NAT(国家代码)、FullName(全名)、AthleteResultAge(运动员参赛年龄)、TotalPoints(总分)以及PointsBehind(落后分数)。对于PointsBehind,如果不存在,则默认为null。actions: {
fetchMatchDetails(matchType: string) {
const data = this.matchDataMap[matchType];
if (!data || !Array.isArray(data.Heats)) {
console.error("Heats is not an array or undefined in", matchType);
return [];
}
return data.Heats.map(heat => ({
EndUtcDateTime: heat.EndUtcDateTime,
PhaseName: heat.PhaseName,
}));
},
fetchMatchScore(matchType: string, PhaseName: string) {
const data = this.matchDataMap[matchType];
if (!data) {
console.error("Match data not found for:", matchType);
return [];
}
const targetHeat = data.Heats.find(heat => heat.PhaseName === PhaseName);
if (!targetHeat || !targetHeat.Results) {
console.error("No matching heat or results found for phase:", PhaseName);
return [];
}
return targetHeat.Results.map(result => ({
Rank: result.Rank,
NAT: result.NAT,
FullName: result.FullName,
AthleteResultAge: result.AthleteResultAge,
TotalPoints: result.TotalPoints,
PointsBehind: result.PointsBehind || null,
}));
}
}
(props)向子组件传递数据。在这个例子中,matchType和phaseName是从父组件传递到resultsTab和resultsDetails子组件的数据。这种数据传递机制是单向的,从父组件流向子组件,这有助于保持组件间数据流的清晰和可预测。通过这种方式,可以在不同组件之间共享数据,同时保持它们的独立性和可重用性。这部分传递的数据将在下一点演示子组件如何接受来自父组件传递的数据。resultsTab子组件传递matchType:在<el-collapse-item>的标题插槽中,matchType通过属性绑定(:matchType="matchType")传递给resultsTab组件。这允许resultsTab组件根据传入的matchType显示相应的标题信息。resultsDetails子组件传递matchType和phaseName:在<el-collapse-item>内部,matchType和phaseName通过属性绑定(:matchType="matchType"和:phaseName="phaseName")传递给resultsDetails组件。这使得resultsDetails组件能根据这些属性显示详细的比赛结果。<template>
<div class="content">
<el-collapse accordion class="collapse" v-model="activeName">
<el-card shadow="hover">
<el-collapse-item v-for="(matchType, index) in matchTypes" :key="index" :name="matchType" >
<template #title>
<!-- 传递 matchType 给 resultsTab 组件 -->
<resultsTab :matchType="matchType"></resultsTab>
</template>
<!-- 传递 matchType 和 phaseName 给 resultsDetails 组件 -->
<resultsDetails :matchType="matchType" :phaseName="phaseName"></resultsDetails>
</el-collapse-item>
</el-card>
</el-collapse>
</div>
</template>
props,如何利用Vue的组合式API进行数据的异步加载和响应式管理。通过将业务逻辑和数据加载操作定义在组件内部,resultsDetails组件能够根据传入的matchType和phaseName从全局状态管理(如Pinia store)中检索特定比赛的详细信息和分数,进而在组件内部以期望的方式展示这些数据。这种模式大大增强了组件的可重用性和灵活性,同时也保持了代码的清晰和易于维护。Props:通过defineProps方法定义接收的props。这里定义了matchType和phaseName两个属性,其中matchType是必需的,而phaseName则不是。onMounted生命周期钩子中,使用await关键字异步调用store的方法(fetchMatchDetails和fetchMatchScore),以根据props.matchType和props.phaseName获取匹配的详细信息和分数。matchDetails.value和scores.value,使得数据变更能够触发组件的重新渲染。watch函数来监视props.phaseName的变化。一旦phaseName发生变化,便会更新activeTabName.value。通过设置{ immediate: true },watch会在组件初次渲染时立即执行,确保即使在phaseName初始值存在时也能正确设置activeTabName。<script setup name="resultsDetails">
import { onMounted, ref ,watch } from 'vue';
import useResultsData from '@/store/results';
const store = useResultsData();
const props = defineProps({
matchType: {
type: String,
required: true,
},
phaseName: {
type: String,
required: false,
},
});
const matchDetails = ref([]);
const scores = ref([]);
const activeTabName = ref('');
watch(() => props.phaseName, (newPhaseName) => {
if (newPhaseName) {
activeTabName.value = newPhaseName;
}
}, { immediate: true });
onMounted(async () => {
const details = await store.fetchMatchDetails(props.matchType);
// 输出details
//console.log(details)
matchDetails.value = details;
// Initialize activeTabName with props.phaseName or fallback to the first detail's PhaseName
if (!props.phaseName && details.length > 0) {
activeTabName.value = details[0].PhaseName;
}
for (const detail of details) {
const score = await store.fetchMatchScore(props.matchType, detail.PhaseName);
scores.value.push(score);
}
});
</script>
ECharts来创建动态的数据可视化组件。通过从Pinia store中获取数据,并根据这些数据动态生成图表,可以提供丰富的用户交互和数据展示。此外,ECharts的灵活性和强大的配置选项使得定制化高质量的图表变得简单快捷。setPieChartOptions函数:定义了如何设置饼图的配置项。这个函数接收四个参数:图表实例、图表标题、数据数组、以及要展示的奖牌类型(金、银、铜)。title对象配置了图表的标题和其在图表中的位置。tooltip配置了鼠标悬浮时显示的信息。legend配置了图例的布局和位置。series数组定义了图表的类型为pie(饼图),包括饼图的半径、数据项(每个国家的奖牌数和国家名),以及饼图扇区被高亮时的样式。import { computed, onMounted, ref } from 'vue';
import * as echarts from 'echarts';
import useMedalCount from '@/store/statistic';
const medalCountStore = useMedalCount();
const goldChartRef = ref<echarts.ECharts | null>(null);
const silverChartRef = ref<echarts.ECharts | null>(null);
const bronzeChartRef = ref<echarts.ECharts | null>(null);
onMounted(async () => {
await medalCountStore.calculateMedalTally();
const goldChartDom = document.getElementById('gold-chart');
const silverChartDom = document.getElementById('silver-chart');
const bronzeChartDom = document.getElementById('bronze-chart');
if (goldChartDom) goldChartRef.value = echarts.init(goldChartDom);
if (silverChartDom) silverChartRef.value = echarts.init(silverChartDom);
if (bronzeChartDom) bronzeChartRef.value = echarts.init(bronzeChartDom);
const sortedData = Object.entries(medalCountStore.medalTally).map(([country, medals]) => ({
country,
gold: medals.Gold,
silver: medals.Silver,
bronze: medals.Bronze,
}));
if (goldChartRef.value) {
setPieChartOptions(goldChartRef.value, 'Gold Medals Distribution', sortedData, 'gold');
}
if (silverChartRef.value) {
setPieChartOptions(silverChartRef.value, 'Silver Medals Distribution', sortedData, 'silver');
}
if (bronzeChartRef.value) {
setPieChartOptions(bronzeChartRef.value, 'Bronze Medals Distribution', sortedData, 'bronze');
}
});
function setPieChartOptions(chart: echarts.ECharts, title: string, data: Array<{ country: string; gold: number; silver: number; bronze: number; }>, type: 'gold' | 'silver' | 'bronze') {
const option = {
title: {
text: title,
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: type,
type: 'pie',
radius: '50%',
data: data.map(item => ({
value: item[type],
name: item.country
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
if (chart) {
chart.setOption(option);
}
}
2221000128 的心路历程和收获:本次的作业时间对我们来说相对有些紧迫,在作业发布前我们就已经开始学习新知识了(vue),但是在编程的过程中还是遇到很多问题,归根结底的原因还是技术掌握的不熟练。网页的布局也是真的很磨人,大量的时间都在搞 css,一开始怎么调整css样式都无法让页面按照自己代码所写的来呈现,最后发现是npm create vue@latest创建项目的时候有一个默认的全局css文件,影响了自己写的css样式,所以才css代码才不如自己所意,消耗了大量时间,人麻了(o(╥﹏╥)o)。在后续的学习中还需要积极动手实践,只有动手才能对一项技术更加熟练,我在此次作业是第一次接触了 vue,能够用 vue 写出一个简单的网页,学习与之相关的知识,对我后续做团队项目有较大的帮助,收获满满。
2221000129 的心路历程和收获:开始coding的时候,我没有马上就写第一个页面,而是像教程里面,先设计路由index.ts文件,创建简单的views。然后再开始写每一views的内容。期间由于我的经验不足,没有把vue3新项目的默认css文件删除,导致队友调整页面布局浪费了很多时间。
最省心的是,我没有马上写代码,而是多花一点时间学习element PLUS,导致我能够很快地利用已有的组件,轻轻松松地在页面上呈现各种好看的效果,省去我们自己调整样式的时间。
我也愿意尝试使用pinia进行状态管理,使得一份json数据文件可以只处理一次就被多个vue页面使用。与其花时间手工把数据写到代码中,还不如想想如何利用typescript解析json文件中的数据,一步到位的感觉太爽啦。
在实现从赛程Schedule页面跳转到比赛结果Result页面的时候,遇到两个bug。首先是跳转以后,导航栏的激活状态还停留在Schedule页面。这个bug比较简单,只需要把路由信息和导航栏激活选项绑定就好了,二者就能够同步。第二个bug是从Schedule跳转到Result页面的时候,路由中包含props参数,才允许精确跳转决赛还是初赛。但跳转以后查看其他比赛结果,可能数据就加载不出来。原因其实很简单,当要查看其他比赛的结果的时候,跳转过来的参数任然奏效,多余参数导致数据筛选失败。解决方法也很简单,适时让参数失效就好了。
最后部署的时候,也遇到不少问题。由于typescript强类型机制,项目能正常运行但代码会报错,且npm run build失败。查询资料,使用不检查语法强行build即可。后面也发现部署后,生产环境的资源路径和开发环境下不一样,也一一修改了,积累了不少经验。
222100128 的评价:我的队友还是一如既往的给力,前端组件设计和部署云服务器都是他一手操持,执行力和效率都很高,能够带领我这个小菜鸟在编写作业的过程中有条不紊地进行每一步,还能在帮我一起解决我这边解决不了的难题,是个大佬!!希望下次团队合作也能愉快进行!
222100129 的评价:我的队友自学能力很强,共同学习同一个vue和git视频教程的时候,他总能细心的学,所以一些技术上的细节了解得比较深,比如git相关操作,他能帮我解决很多问题,还有初始项目的默认css文件,也是他及时发现,免得我们浪费很多时间。我和他沟通json文件如何处理,需要什么样的数据进行展示和通信,他可以把复杂的问题用通俗易懂的方法解释给我,让我们沟通交流很顺畅,项目进展也较为顺利。