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

222000321熊中伟 学生 2023-03-24 21:16:25
这个作业属于哪个课程2023软工W班
这个作业要求在哪里结对第二次作业--编程实现
结对学号222000321, 222000320
这个作业的目标基本要求,编码实现,结果汇报,结对合作
其他参考文献

目录

  • 1 GitCode仓库地址
  • 2 PSP表格
  • 3 上线访问链接
  • 4 代码规范链接
  • 5 成品展示
  • 5.1 导航栏
  • 5.2 首页部分
  • 5.3 排行部分
  • 5.4 赛程部分
  • 5.5 赛况部分
  • 5.6 晋级图
  • 6 功能分析
  • 7 功能实现
  • 7.1 系统架构
  • 7.2 项目结构
  • 7.3 总体流程
  • 7.4 视图实现
  • 7.4.1 总体布局
  • 7.4.2 带显式滚动的界面(晋级图)
  • 7.4.3 带隐式滚动的界面(Days选择器)
  • 7.4.4 晋级图
  • 7.5 交互实现
  • 7.5.1 hover以及active状态
  • 7.5.2 Set选择器
  • 7.5.3 路由切换
  • 7.6 数据控制
  • 7.6.1 JSON文件的保存与读取
  • 7.6.2 从JSON到线性数据结构
  • 7.6.3 从JSON到二叉树(在晋级图中)
  • 7.7 部署
  • 7.7.1 域名和解析
  • 7.7.2 Nginx部署
  • 7.8 创新点或亮点
  • 8 代码展示
  • 9 结对过程
  • 9.1 分工
  • 9.2 过程描述
  • 9.3 讨论截图
  • 10 心路历程和感受
  • 11 评价

1 GitCode仓库地址

https://gitcode.net/MarkPoloChina/pair_project

2 PSP表格

下面的时间是两个人的总和时间。

PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划3030
• Estimate• 估计这个任务需要多少时间3030
Development开发23603340
• Analysis• 需求分析 (包括学习新技术)120180
• Design Spec• 生成设计文档10080
• Design Review• 设计复审3030
• Coding Standard• 代码规范 (为目前的开发制定合适的规范)1010
• Design• 具体设计300500
• Coding• 具体编码12002000
• Code Review• 代码复审200220
• Test• 测试(自我测试,修改代码,提交修改)400320
Reporting报告160270
• Project Repor• 项目报告120230
• Size Measurement• 计算工作量1010
• Postmortem & Process Improvement Plan• 事后总结, 并提出过程改进计划3030
SUM总计25503640

3 上线访问链接

http://ao.markpolo.cn/

4 代码规范链接

https://gitcode.net/MarkPoloChina/pair_project/-/blob/dev/222000321_222000320/README.md

5 成品展示

5.1 导航栏

img

导航栏实现了hover效果和active效果,含淡入和淡出。

赛况这个按钮不能主动点击。

5.2 首页部分

img

主页是进入网站的第一个界面。切换到其他页面后,点击AO的logo(位于导航栏左侧)即可返回主页

这部分内容大部分是静态的,因为在数据源方面有一些问题。

首页由主头栏,新闻模块和选手模块组成。在头栏,简要介绍了澳网。在新闻模块,枚举了新闻,按新闻按钮可以跳转到某个新闻链接(此处没有实现具体跳转);中间的视频静态引用自官网,可以点击播放;右侧是新闻频道,点击可以跳转到某个频道(此处没有实现具体跳转)。

img

5.3 排行部分

img

点击排行按钮可以跳转到排行页面。

排行实现了Men’s Singles Aces Leaders以及Women’s Singles Aces Leaders,分别给出了前20位选手的数据,数据来自官方API。

5.4 赛程部分

img

点击赛程按钮可以跳转到赛程页面。

枚举出了day1-day14全部的赛程。点击上面的day1-day14的按钮组可以切换day。切换day后下面的内容自动刷新。

img

支持hover效果,以及点击跳转(仅Day1前6个,以及Day2的第一个。)

img

img

支持在屏幕宽度较小时,点击左右按钮来滑动上面的滚动条

img

5.5 赛况部分

img

通过点击赛程中的比赛块能跳转到详细赛况。

赛况给出了比赛双方的信息,以及每个Set的比分和赢家,以及Set每个Game的比分序列和得分原因。

img

能够点击下拉选框来选择当前Set

5.6 晋级图

给出了第4轮到决赛的晋级图。支持横向滚动和纵向滚动

img

img

6 功能分析

项目基于原型所列出的功能需求,包含以下内容:

  • 主页
    • 头部介绍模块
      • 新闻速览
      • 视频回放:支持点击播放
      • 频道
    • 热点选手
  • 排行榜
    • 男单积分排行
    • 女单积分排行
  • 赛程信息
    • 头部Days选择器
      • 支持hover淡出淡入颜色加深
      • 支持当前的day对应的按钮颜色加深
      • 支持在浏览器宽度比较小的时候能够通过点击左右的滚动按钮来滚动选择器
    • 所选day的所有赛程以及每场赛事的类别,胜者和比分模块
      • 支持hover到对应的比赛的信息块时,出现蓝色的淡出淡入边框
      • 支持点击信息块后跳转到赛程详情
  • 赛程详情
    • 头部单场比赛的基本信息模块(复用“赛程信息”中的)以及对局双方全名
    • 局(set)选择器
      • 点击下拉菜单可以选择不同的Set
      • 选择Set之后,下方的赛程详情自动刷新
    • 当前局的胜者及当前局比分模块
    • 当前局每节(Game)比分序列模块
  • 晋级图
    • 树状晋级图,从第4轮到决赛,从左到右
    • 支持晋级图的递归二叉形态,包含线条提示
    • 支持上下和左右滚动,避免晋级图过大超出边界以及变形
  • 导航栏
    • 能够实现在hover时淡出淡入显示文字的颜色加深以及下划线
    • 能够实现在当前模块路由下的对应的文字颜色加深
    • 点击非当前模块的文字跳转到对应模块路由

7 功能实现

注:详细赛况数据仅支持到赛程Day1的前6个和Day2的第一个。点击其他的会弹出信息提示并跳转到首页。

根据上述设计,生成功能结构图如下:

img

7.1 系统架构

  • 本项目是纯前端项目,没有调用网络通讯的地方(但是部分素材使用了官网的开放静态素材),大部分数据使用位于项目包的静态json文件作为数据来源。
  • 前端使用Vue3.2,基于纯CompositeAPI,setup语法糖。
  • UI部分使用了ElementPlus组件库,并且引用了其中的少量图标。
  • 使用yarn进行包管理。
  • VM层使用Typescript
  • 使用Vite进行项目管理。
  • 使用scss作为样式表语言。

7.2 项目结构

以下是前端项目结构:

大体使用了Vite推荐的项目结构,在此基础上进行了少量优化。

  • public - 根静态资源目录
  • src - 源码目录
    • assets - 静态资源
      • json - JSON文件目录
    • components - 可复用组件
    • router - 路由配置
    • ts - 自组脚本库
    • types - 类型定义目录,对类型的声明放在这里
    • view - 视图目录,直接挂在路由下的vue在这里
    • App.vue - 主vue
    • main.ts - 程序主入口
    • vite-env.d.ts - vite类型定义
  • index.html - 挂载html
  • package.json - 包管理文件
  • tsconfig.json - ts编译配置
  • vite.config.ts - vite项目配置文件
  • yarn.lock - yarn依赖版本约束

7.3 总体流程

  1. 在main.ts中注册所需的依赖,本项目使用了router以及element-plus两个依赖。
  2. 在App.vue中设计总体页面。包括一个头部导航栏和底部的页脚。中间部分填充路由。
  3. 在路由配置中注册若干路由,指向对应的view。
  4. 在view中设计对应页面的样式,必要时引用其他组件。
  5. 在view的VM层,在初始化时调用声明周期钩子,异步地读取来自json的文件。
  6. 经过一层由ts脚本的适配器函数,json转化成符合页面渲染需求的数据结构,比如线性列表或二叉树。设计这层脚本,并定义类型。
  7. 设计若干响应对象,能够接受这些动态的数据,然后把他们渲染到页面上。

7.4 视图实现

7.4.1 总体布局

以flex布局为核心,尤其是在用到嵌套的晋级图中。避免使用浮动,尤其不能在父元素高度不确定或者不含非浮动的兄弟元素时。

容器宽度高度避免使用静态数值。尽量避免使用vh和vw。

7.4.2 带显式滚动的界面(晋级图)

晋级图需要纵向以及横向的滚动。将整张晋级图的父元素的overflow-x设成scroll,这样在横向移出屏幕时,会产生一个滚动条,可以拖动。

.container {
  overflow-x: scroll;
}

在其他常规界面,只要不限制最外层元素的高度和宽度即可做到自适应。

7.4.3 带隐式滚动的界面(Days选择器)

在days选择器中,当宽度不足以容纳全部的按钮时,多余的按钮会移出,然后有两种情况:

  • 溢出后与右侧内容重叠,最后超出边框范围。
  • 产生滚动条

这两种都不是本项目想要的。

因此我们直接将其设为hide,也就是隐藏,这样溢出的内容就不可见。然后通过点击左右两侧的按钮,回调获取整个容器元素,直接控制它的滚动位置,然后步进向左或者向右。此外还能将behavior设成smooth,这样滚动能平滑一些。

const scrollLeft = () => {
  const ele = document.getElementById("btns");
  ele?.scrollTo({ left: ele?.scrollLeft - 40, behavior: "smooth" });
};
const scrollRight = () => {
  const ele = document.getElementById("btns");
  ele?.scrollTo({ left: ele?.scrollLeft + 40, behavior: "smooth" });
};

7.4.4 晋级图

在晋级图中,需要实现:

  • 元素沿垂直方向分散居中,且不同轮的比赛沿同一条中轴线对齐
  • 画出晋级线条
  1. 使用Flex布局的justify-content: space-between;即可。由于晋级图每个模块是递归二叉的,最后就会沿一条中轴线排列。
  2. 晋级线条使用了伪元素。直接在对应块内为一个空的div赋伪元素css。
   div {
      position: absolute;
      background-color: $border_color;
      height: 50%;
      width: 100%;
      top: 0;
      bottom: 0;
      margin: auto;
    }
    div::after {
      content: "";
      position: absolute;
      border-top: $border_width solid $border_color;
      border-bottom: $border_width solid $border_color;
      height: 100%;
      width: calc(20px + $border_width);
      top: 0;
      bottom: 0;
      left: -20px;
      margin: auto;
    }
    div::before {
      content: "";
      position: absolute;
      background-color: $border_color;
      height: $border_width;
      width: 20px;
      top: 50%;
      right: -20px;
    }

7.5 交互实现

7.5.1 hover以及active状态

hover使用了伪类。直接在css中即可实现。通过先在非hover中设置透明边框,然后在hover中将透明取消,避免宽度突变。

其次也使用了变换transition来添加淡出淡入效果。

.link {
          z-index: 1000;
          position: relative;
          text-decoration: none;
          font-weight: bolder;
          color: rgba(255, 255, 255, 0.5);
          transition: 0.2s all linear;
        }
        .link:hover {
          color: rgb(255, 255, 255);
          transition: 0.2s all linear;
        }

active状态使用了vue的一个响应式变量。当用户点击一个按钮时,回调储存按到的数字,然后变换class类名,再通过不同类名控制active和非active状态的按钮样式。

7.5.2 Set选择器

Set选择器是使用了ElementPlus中的组件。直接使用选择组件el-select即可。

7.5.3 路由切换

路由切换用到了vue-router在VM中的实例router。

路由跳转等价与将该对象调用push()

7.6 数据控制

数据仅用于学习!非商用。

7.6.1 JSON文件的保存与读取

  1. JSON文件统一保存在assets的json文件夹中。
  2. 当页面被载入,也就是onMounted生命周期时,异步地从json中import进来,然后传递经过转换函数传递给响应式对象。

7.6.2 从JSON到线性数据结构

排名,赛程,以及赛况需要从json转为线性数据结构,这个过程与之前的java输出相似,也是对json的matches不断的嵌套查询,对于一个非显式的id,遍历对应的数组,找到满足id == uuid的对象,填充到目标对象中,最后遍历完之后就是一个能直接渲染的线性结构。

7.6.3 从JSON到二叉树(在晋级图中)

json到二叉树是为了构建晋级图。在这个部分,需要从决赛轮开始进行遍历,对于决赛轮的两支队伍,选择在半决赛轮中队伍含有其中一支队伍的比赛作为当前比赛的子节点。然后对于每个子节点,递归地调用上述方法,就能完成对全部层的迭代,生成二叉树。

const processRoundName = [
    "Final",
    "Semifinals",
    "Quarterfinals",
    "4th Round",
  ];
const ArraysObj: { [propsName: string]: MatchArray } = {};
  const RoundNameMap: { [propName: string]: any } = {};
  jsonData.rounds.forEach((round) => {
    RoundNameMap[round.uuid] = round.name;
  });
  jsonData.matches.forEach((match) => {
    if (processRoundName.includes(RoundNameMap[match.round_id])) {
      if (!ArraysObj[RoundNameMap[match.round_id]])
        ArraysObj[RoundNameMap[match.round_id]] = [];
      ArraysObj[RoundNameMap[match.round_id]].push(match);
    }
  });

  const findNext = (objectToExpand: Tree, nextIndex: number) => {
    if (nextIndex == 4) return;
    const nextRoundName = processRoundName[nextIndex];
    ArraysObj[nextRoundName].forEach((nextItem) => {
      if (
        nextItem.teams.find(
          (team) =>
            objectToExpand.match.teams.findIndex(
              (_team) => _team.team_id == team.team_id
            ) != -1
        )
      ) {
        objectToExpand.child.push({
          name: nextRoundName,
          match: nextItem,
          child: [],
        });
        findNext(
          objectToExpand.child[objectToExpand.child.length - 1],
          nextIndex + 1
        );
      }
    });
  };
  treeData.value.push({
    name: "Final",
    match: ArraysObj["Final"][0],
    child: [],
  });
  findNext(treeData.value[0], 1);

7.7 部署

7.7.1 域名和解析

域名注册以后要在DNS上填写解析,使得子域名能被解析到ip。

7.7.2 Nginx部署

yarn
yarn build

在项目目录执行上述命令来编译打包vue工程。

将生成的dist内的全部文件移至nginx下的html文件夹,之后就能用80端口访问。

7.8 创新点或亮点

  • 本项目是纯前端项目,避免了网络通讯代价
  • 使用ElementPlus组件,降低代码编写代价
  • 界面简单高效,一目了然
  • 添加主页,丰富信息
  • 添加了淡入淡出等效果使得页面动态平滑
  • vue的动态视图使得性能提升

8 代码展示

一些具体的功能性的代码已经展示在了上面的功能实现中。下面仅展示主Vue文件的代码。

<script setup lang="ts">
import { ref } from "vue";
import { useRoute, useRouter } from "vue-router";

const currentNav = ref("home");
const route = useRoute();
const router = useRouter();
const getClassName = (path: string) => {
  if (route.path == path) return "active";
  else return "";
};
</script>

<template>
  <header>
    <img
      src="/src/assets/ao_blue_1.png"
      height="30"
      @click="router.push('/')"
      style="z-index: 1200; cursor: pointer"
    />
    <nav>
      <ul>
        <li :class="getClassName('/arrange')">
          <router-link to="/arrange" class="link">排名</router-link>
        </li>
        <li :class="getClassName('/match')">
          <router-link to="/match" class="link">赛程</router-link>
        </li>
        <li :class="getClassName('/detail')">
          <span class="link">赛况</span>
        </li>
        <li :class="getClassName('/draw')">
          <router-link to="/draw" class="link">晋级</router-link>
        </li>
      </ul>
    </nav>
  </header>
  <div class="main">
    <router-view></router-view>
  </div>
  <footer>
    <p>Only Used For SE2023, CSC, FZU.</p>
    <p>[NON-COMMERCIAL PRODUCT]</p>
    <p>© Copyright 2020-2023 MarkPolo, all rights reserved.</p>
  </footer>
</template>

<style scoped lang="scss">
header {
  position: relative;
  height: 50px;
  width: 100%;
  background-color: #041423;
  img {
    position: absolute;
    left: 20px;
    top: 0;
    bottom: 0;
    margin: auto;
  }
  nav {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    margin: auto;
    text-align: center;
    ul {
      list-style: none;
      margin-top: 7px;
      margin-bottom: 0;
      li {
        display: inline;
        font-size: 25px;
        position: relative;
        padding-bottom: 5px;
        .link {
          z-index: 1000;
          position: relative;
          text-decoration: none;
          font-weight: bolder;
          color: rgba(255, 255, 255, 0.5);
          transition: 0.2s all linear;
        }
        .link:hover {
          color: rgb(255, 255, 255);
          transition: 0.2s all linear;
        }
        &.active {
          .link {
            color: rgb(255, 255, 255);
          }
        }
      }
      li::after {
        content: "";
        position: absolute;
        top: 0;
        right: 100%;
        width: 0;
        height: 100%;
        border-bottom: 2px solid rgb(255, 255, 255);
        transition: 0.2s all linear;
      }
      li:hover::after {
        width: 100%;
        right: 0;
        transition: 0.2s all linear;
      }
      li.active::after {
        width: 100%;
        right: 0;
      }
      li + li {
        margin-left: 60px;
      }
    }
  }
}
.main {
  background-color: #e1e1e1;
  min-height: calc(100vh - 50px);
}
footer {
  padding: 20px 50px;
  text-align: center;
  color: #c6c6c6;
  background-color: #041423;
}
</style>
<style>
body {
  margin: 0;
}
</style>

上面的代码是App.vue,定义了整个页面的基本公共布局,也就是上面导航栏,中间路由,下方页脚。

<script setup lang="ts">
import { ref, computed, reactive } from "vue";
import { DrawParser } from "../ts/parser";
import { Select } from "@element-plus/icons-vue";
defineProps({
  expandedObject: Object,
});
</script>

<template>
  <div class="single-block">
    <div class="parent-block">
      <div class="block">
        {{ expandedObject?.name }}
        <div class="player-block" v-for="team in expandedObject?.match.teams">
          <div :class="team.status ? 'winner' : ''">
            <img
              :src="`https://ausopen.com${DrawParser.getTeamFlag(
                team.team_id
              ).replace('2022-04/rus-empty', '2021-11/ru')}`"
            />{{ DrawParser.getTeamName(team.team_id) }}
          </div>
          <div class="scores">
            <el-icon
              color="#2892ce"
              v-if="team.status"
              style="vertical-align: middle"
              ><Select
            /></el-icon>
            <span v-for="s in team.score" :class="s.winner ? 'winner' : ''">
              {{ s.game }}
            </span>
          </div>
        </div>
      </div>
    </div>
    <div
      :class="
        expandedObject?.child && expandedObject?.child.length != 0 ? 'line' : ''
      "
    >
      <div></div>
    </div>
    <div
      class="child-blocks"
      v-if="expandedObject?.child && expandedObject?.child.length != 0"
    >
      <div>
        <BinaryTree :expanded-object="expandedObject?.child[0]"></BinaryTree>
      </div>
      <div>
        <BinaryTree :expanded-object="expandedObject?.child[1]"></BinaryTree>
      </div>
    </div>
  </div>
</template>
<style scoped lang="scss">
$border_color: rgba(0, 0, 0, 0.4);
$border_width: 2px;
.single-block {
  display: flex;
  flex-direction: row-reverse;
  align-items: center;
  > div {
    flex: none;
  }
  .line {
    width: $border_width;
    align-self: stretch;
    position: relative;
    div {
      position: absolute;
      background-color: $border_color;
      height: 50%;
      width: 100%;
      top: 0;
      bottom: 0;
      margin: auto;
    }
    div::after {
      content: "";
      position: absolute;
      border-top: $border_width solid $border_color;
      border-bottom: $border_width solid $border_color;
      height: 100%;
      width: calc(20px + $border_width);
      top: 0;
      bottom: 0;
      left: -20px;
      margin: auto;
    }
    div::before {
      content: "";
      position: absolute;
      background-color: $border_color;
      height: $border_width;
      width: 20px;
      top: 50%;
      right: -20px;
    }
  }

  .block {
    width: 350px;
    margin: 20px;
    background-color: #f5f5f5;
    border-radius: 8px;
    padding: 15px;
    border: 2px solid #0089c800;
    transition: 0.2s all linear;

    .player-block {
      padding: 10px 20px;
      display: flex;
      flex-direction: row;
      justify-content: space-between;
      font-weight: light;
      color: #777777;
      .winner {
        font-weight: bolder;
        color: #000;
      }
      img {
        vertical-align: top;
        margin-right: 10px;
        height: 20px;
        width: 30px;
        object-fit: cover;
      }
      .scores {
        span {
          width: 20px;
          display: inline-block;
          text-align: right;
        }
      }
    }
    .player-block + .player-block {
      border-top: 1px solid rgb(199, 199, 199);
    }
    &:hover {
      border: 2px solid #008ac8;
      transition: 0.2s all linear;
    }
  }
}
</style>

上面是BinaryTree.vue文件的内容。这是一个可复用模块,主要封装了晋级图的二叉树,并且含递归调用。

在line10-56,以flex布局的方式定义了最简二叉树的父亲节点和儿子节点的内容。在儿子节点中,再次调用自身,传入对应的对象,完成递归调用。

9 结对过程

9.1 分工

学号分工
222000321总体设计,样式表编写,依赖确定,晋级图,每日赛程,导航栏
222000320数据填充,部署,赛况详情部分,排行榜

9.2 过程描述

拿到需求后,首先我们先讨论技术选型,我们讨论了多种前端框架的可能性,最后选择vue3作为我们的前端框架。

在是否需要后端的问题上,我们讨论了很久。因为避免再从json向数据库填充并建立实体类,我们选择了纯前端,直接读入json文件并进行处理。

在样式设计上,我们先统一了布局规范,然后按页面分开实现。

在函数设计上,我们采用了类似接口的形式,先建一个工具类库来放全部的接口,这样两个人就可以同时分别完成vue和具体函数。

在沟通上,我们使用qq进行意见交换,使用git进行代码协作。

9.3 讨论截图

img

img

10 心路历程和感受

这是结对的第二次作业,主要是完成对原型的具体编码实现,时间很短,工作量和密度还是比较大的。

为了在规定时间内完成任务,我们简化了很多步骤,为了高效地填充数据,省去了后端和数据通讯,这个使得我们在这次项目中没有用到后端的内容,但是不影响我们最后的成品。感觉还是损失不大,可接受的。

我们第一次使用了TS版本的vue工程来构建项目,一个直觉是,vue在搭建工程上的方便性还是很高的,但是必须熟练运用其中的API,不然就会云里雾里地出现很多undefined问题,这应该是我们遇到的最多的bug,好在vue有丰富的调试机制,尤其是vue devtool,避免我们打断点。

在团队协作上,我们又一次高效完成了协作,这主要依赖于我们在刚开始的时候体验了很多不同的技术,最后确定下来技术依赖,约定了一些规范,这样就使得后续的工作少了很多返工。

11 评价

  • 222000321 对 222000320的评价:高效地完成了分配的分工,在遇到问题的时候能够自己处理,当不能处理的时候也能准确跟我分享出错信息,与我一起调试。
  • 222000322 对 222000321的评价:高质量的队友!和我一起完成了项目,感觉很好。

本博客的编写未使用ChatGPT或newBing等工具。

Only Used For SE2023, CSC, FZU.

© Copyright 2020-2023 MarkPolo, all rights reserved.

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

1.整体完整性很好!赞!2.进入4个具体功能之后,无法返回首页?3.如果从晋级图中能够进入详细赛况就更好了。

222000321熊中伟 学生 2023-03-29
  • 举报
回复
@2023年福大-软件工程实践-W班 谢谢老师指导。A2:可以点击AO的Logo按钮返回首页,可能未考虑到用户不知道logo可以点击;A3:未全面考虑到用户需求,发现确实这是一个潜在的需求,后续补充。
SoftwareTeacher 2023-03-29
  • 举报
回复
@222000321熊中伟 可以弹出一个气泡,告诉用户这样的链接, 但是只告诉一次。

688

社区成员

发帖
与我相关
我的任务
社区描述
2023年福州大学软件工程实践课程W班的教学社区
软件工程团队开发软件构建 高校 福建省·福州市
社区管理员
  • FZU_SE_teacherW
  • 张书旖
  • 郭渊伟
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

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