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

222100128黄煦陶 2024-03-26 22:42:39
这个作业属于哪个课程<2302软件工程社区>
这个作业要求在哪里<结对第二次作业--编程实现>
结对学号<222100128黄煦陶,222100129梅明胜>
这个作业的目标<完成编码撰写;完成博客撰写>
其他参考文献vue 官网教程, Element-Plus 教程, Git 官网

目录

  • 1.项目链接
  • 1.1 Gitcode 仓库网址
  • 1.2 云服务器项目网址
  • 1.3 代码规范地址
  • 2.PSP 表格和效能分析
  • 2.1 PSP 表格
  • 2.2 效能分析
  • 3.成品展示
  • 3.1 首页
  • 3.2 选手信息
  • 3.3 每日赛况
  • 3.4 详细赛况
  • 3.5 奖牌榜
  • 3.6 服务器部署
  • 4.结对讨论过程描述
  • 5.设计实现过程
  • 5.1 功能结构图
  • 5.2 设计概述
  • 5.3 遇到的问题和解方式
  • 6.代码说明
  • 7.心路历程和收获
  • 8.评价队友

1.项目链接

1.1 Gitcode 仓库网址

2.PSP 表格和效能分析

2.1 PSP 表格

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

2.2 效能分析

  • 需求分析部分:在本次学习中,我们对于前端的技术不太了解,实际花了很长时间在学习新技术上,包括作业布置前我们学习 vue 技术花了一些时间,但是在编写的过程中总是有一些小状况出现,这说明我们对技术的掌握还是不够熟练。在后续的课程作业中,应当先掌握技术,在编写过程中就能减少去学习新知识的时间。
  • 具体编码部分:编码超出预期时间是在我们的意料之内,一开始我们对vue整体的编写并不熟悉,后期在为了让组件呈现更好的效果上花了较多时间。

    3.成品展示

  • 本次编写的网站偏向于信息展示类网站,网站的元素应简洁明了,给予用户直观明确的信息;契合游泳比赛的主题,我们本次采用了蓝+白的主题配色;页面采用导航栏、内容、页脚的结构,方便用户在各个模块之间的切换。

    3.1 首页

  • 首页设计:上方设计了一个轮播图,用来展示关于比赛的主题图片

    在这里插入图片描述

  • 我们把了解更多模块直接放在首页的轮播图下方,采用图文交加的形式,介绍游泳世锦赛的举办背景等等。

    请添加图片描述

3.2 选手信息

  • 选手排名设计:采用表格的方式展示选手的信息。相同国籍的选手放在一起陈列。

    在这里插入图片描述

  • 排序查看信息:点击标题,可以根据此标题对数据进行排序

    在这里插入图片描述

3.3 每日赛况

  • 每日赛况设计:以日期组织展示每天的比赛。显示有每场比赛的详细举办时间与比赛性质(初/决赛)

    在这里插入图片描述

  • 每场比赛右侧详细信息按钮,点击可跳转至详细赛况页面对应的比赛栏目,展示该比赛的结果。

    在这里插入图片描述

3.4 详细赛况

  • 详细赛况设计:展示有每场比赛初赛与决赛的比赛结果。决赛金银铜牌有特殊标志标识

    请添加图片描述

3.5 奖牌榜

  • 奖牌统计:统计各国家队奖牌数,表格展示准确信息,饼状图直观展示各国获奖情况。

    在这里插入图片描述

  • 折线图:直观展示单场比赛各国家队获奖情况

    在这里插入图片描述

    3.6 服务器部署

  • 使用GitHub Pages进行服务器部署,免费且方便,实时开发实时查看

    在这里插入图片描述

4.结对讨论过程描述

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

在这里插入图片描述


在这里插入图片描述


请添加图片描述


请添加图片描述


请添加图片描述


请添加图片描述


请添加图片描述

5.设计实现过程

5.1 功能结构图

请添加图片描述

5.2 设计概述

请添加图片描述

5.3 遇到的问题和解方式

在我们的项目开发过程中,我们遇到了多个挑战和相应的解决方案,这些体验为我们的技术成长提供了宝贵的实践机会。

  1. CSS布局挑战:项目初期,我们发现自己调整的CSS样式无法如预期生效,后来意识到是由于未删除Vue 3项目的默认CSS文件所致。这个问题一度让我们在页面布局上浪费了大量时间,但也教会了我们在新建项目时要注意检查和清理默认文件。

  2. UI组件选择:面对自身CSS技能的不足,我们决定采用==Element Plus==,这个决策大幅提高了开发效率,让我们能够快速实现漂亮且功能丰富的组件,减少了调整样式的时间。

  3. 数据处理:项目中需要处理大量数据,我们选择了结合==TypeScript==和==Pinia==来解析JSON文件,实现数据的批量处理。这种方法不仅提高了效率,还使得数据在多个Vue页面中得到复用。

  4. 页面跳转Bug:在实现从“赛程”页面到“比赛结果”页面的跳转时,我们遇到了两个问题:一是导航栏激活状态不同步,二是携带props参数导致的数据加载问题。通过绑定路由信息与导航状态及适时使参数失效,我们顺利解决了这两个问题。

  5. 部署问题:部署项目时,我们遇到了npm run build失败的问题,原因是TypeScript的强类型机制。通过选择不检查语法的方式强行构建,我们解决了这个问题。此外,我们还调整了生产环境下的资源路径,以适应生产环境的需求。

    6.代码说明

  6. App.vue:

  • <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>
    

  1. router/index.ts:
  • 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'
    },
    ......
  ]
})

  1. store\athletes.ts:
  • 这段代码的主要思路是在应用启动时读取和处理JSON文件中的数据,然后使用==Pinia==存储处理后的数据。通过定义类型和使用flatMapmap进行数据处理,代码既保证了类型安全,又提供了一个清晰、易于管理的方法来处理和访问运动员数据。这种模式特别适合于需要预加载和全局访问大量静态数据的应用场景。这里处理的数据,具体的使用将在下一点中演示

  • 定义接口Athlete:这个接口用于定义单个运动员对象应该具有的属性。这有助于在处理运动员数据时提供类型安全,确保数据的一致性和可预测性。

  • 处理JSON数据:AthletesData.flatMap(...)用于将原始的JSON数据转换为Athlete接口定义的形式。flatMap方法首先对AthletesData数组的每个元素(每个国家)执行一个映射操作,然后将结果扁平化为一个数组。这意味着无论原始数据中有多少个国家,最终都会得到一个单层级的运动员数组。

  • 数据映射:对于每个国家对象,通过country.Participations.map(...)遍历其Participations数组(参与的运动员),并为每个参与者创建一个新的对象,这个对象符合Athlete接口的结构。

  • gettersgetters是用于从存储中派生状态的函数。它简单地返回状态中的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,
      },
    });
    

  1. AthletesView.vue:
  • 数据使用和展示逻辑:这段代码的关键在于如何将从Pinia存储中获取的数据以一种用户友好的方式展示出来。通过el-tableel-table-column,我们可以自定义每列的展示逻辑,包括通过插槽自定义复杂的数据显示(如姓名的全名显示、性别的图标显示以及日期的格式化)。这样的数据展示方法既灵活又强大,能够应对各种数据展示需求。
  • el-table: 使用==Element Plus==的<el-table>组件来展示数据。这个组件接受一个数组作为数据源,并为每个数据项渲染一行。
  • el-table-column: 每个<el-table-column>定义了表格中的一列。prop属性用于指定该列要显示数据对象中的哪个属性。label属性定义了列头的标题。如果列需要特殊格式化或包含复杂内容,则不使用prop属性,而是通过具名插槽(template)来自定义列的内容。
  • 导入Pinia存储: 通过import { useAthletesData } from '@/store/athletes'导入之前定义的Pinia存储。
  • 使用存储: 通过调用useAthletesData()来获取存储实例,然后通过store.allAthletes访问存储中的全部运动员数据。这里的athletes变量将被用作<el-table>的数据源。
  • 数据处理函数: formatDate函数用于将日期字符串格式化为更易读的格式(例如:"YYYY-MM-DD")。这在显示日期时非常有用,因为原始数据可能不是以最终希望展示给用户的格式存储的
    ```html{{ row.PreferredFirstName + ' ' + row.PreferredLastName }} {{ row.Gender === 0 ? 'Male' : 'Female' }} {{ formatDate(row.DOB) }} div
---
5. store\results.ts:
* 这两个操作展示了如何根据输入参数从复杂的数据结构中提取和处理信息,使其更适合在用户界面上显示。通过这种方式,可以灵活地根据页面需要显示相应的比赛细节和分数信息,同时也保持了代码的清晰性和可维护性。这种模式在处理具有复杂数据结构的应用中非常有用,特别是在需要根据用户输入动态提取数据时。
**`fetchMatchDetails(matchType: string)`**
* 数据验证:首先检查获取的数据是否有效。这包括验证对应`matchType`的数据是否存在以及数据中的Heats属性是否为一个数组。如果这些验证失败,会在控制台输出错误信息,并返回一个空数组。
* 数据映射:如果数据有效,接下来对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`。
```ts
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,
      }));
    }
  }

  1. ResultsView.vue:
  • 这个代码段展示了Vue中父子组件之间数据传递的基本模式:父组件通过属性(props)向子组件传递数据。在这个例子中,matchTypephaseName是从父组件传递到resultsTabresultsDetails子组件的数据。这种数据传递机制是单向的,从父组件流向子组件,这有助于保持组件间数据流的清晰和可预测。通过这种方式,可以在不同组件之间共享数据,同时保持它们的独立性和可重用性。这部分传递的数据将在下一点演示子组件如何接受来自父组件传递的数据。
  • resultsTab子组件传递matchType:在<el-collapse-item>的标题插槽中,matchType通过属性绑定(:matchType="matchType")传递给resultsTab组件。这允许resultsTab组件根据传入的matchType显示相应的标题信息。
  • resultsDetails子组件传递matchTypephaseName:在<el-collapse-item>内部,matchTypephaseName通过属性绑定(: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>
    

  1. components\results\resultsDetails.vue:
  • 这段代码演示了在Vue 3组件中接收和使用来自父组件的props,如何利用Vue的组合式API进行数据的异步加载和响应式管理。通过将业务逻辑和数据加载操作定义在组件内部,resultsDetails组件能够根据传入的matchTypephaseName从全局状态管理(如Pinia store)中检索特定比赛的详细信息和分数,进而在组件内部以期望的方式展示这些数据。这种模式大大增强了组件的可重用性和灵活性,同时也保持了代码的清晰和易于维护。
  • 接收来自父组件的Props:通过defineProps方法定义接收的props。这里定义了matchTypephaseName两个属性,其中matchType是必需的,而phaseName则不是。
  • onMounted生命周期钩子中,使用await关键字异步调用store的方法(fetchMatchDetailsfetchMatchScore),以根据props.matchTypeprops.phaseName获取匹配的详细信息和分数。
  • 将获取到的数据赋值给响应式变量,如matchDetails.valuescores.value,使得数据变更能够触发组件的重新渲染。
  • 使用watch函数来监视props.phaseName的变化。一旦phaseName发生变化,便会更新activeTabName.value。通过设置{ immediate: true }watch会在组件初次渲染时立即执行,确保即使在phaseName初始值存在时也能正确设置activeTabName
    ```ts

    ---
    8. StatisticsView.vue:
    * 这段代码展现了如何利用**Vue 3**和`ECharts`来创建动态的数据可视化组件。通过从`Pinia store`中获取数据,并根据这些数据动态生成图表,可以提供丰富的用户交互和数据展示。此外,`ECharts`的灵活性和强大的配置选项使得定制化高质量的图表变得简单快捷。
    * `setPieChartOptions`函数:定义了如何设置饼图的配置项。这个函数接收四个参数:图表实例、图表标题、数据数组、以及要展示的奖牌类型(金、银、铜)。
    图表标题和布局:`title`对象配置了图表的标题和其在图表中的位置。
    提示框组件:`tooltip`配置了鼠标悬浮时显示的信息。
    图例组件:`legend`配置了图例的布局和位置。
    系列列表:`series`数组定义了图表的类型为`pie`(饼图),包括饼图的半径、数据项(每个国家的奖牌数和国家名),以及饼图扇区被高亮时的样式。
    
    ```ts
    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);
        }
    }
    

    7.心路历程和收获

    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即可。后面也发现部署后,生产环境的资源路径和开发环境下不一样,也一一修改了,积累了不少经验。

    8.评价队友

    222100128 的评价:我的队友还是一如既往的给力,前端组件设计和部署云服务器都是他一手操持,执行力和效率都很高,能够带领我这个小菜鸟在编写作业的过程中有条不紊地进行每一步,还能在帮我一起解决我这边解决不了的难题,是个大佬!!希望下次团队合作也能愉快进行!

    222100129 的评价:我的队友自学能力很强,共同学习同一个vue和git视频教程的时候,他总能细心的学,所以一些技术上的细节了解得比较深,比如git相关操作,他能帮我解决很多问题,还有初始项目的默认css文件,也是他及时发现,免得我们浪费很多时间。我和他沟通json文件如何处理,需要什么样的数据进行展示和通信,他可以把复杂的问题用通俗易懂的方法解释给我,让我们沟通交流很顺畅,项目进展也较为顺利。

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

122

社区成员

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

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